diff --git a/.eslintrc.js b/.eslintrc.js index fd4d1da631..c6aeb0d1be 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,6 +29,10 @@ module.exports = { // so we replace it with a version that is class property aware "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 **/ // This just uses the react plugin to help eslint known when // variables have been used in JSX diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js index dd990b5210..fa9ccc8ed7 100755 --- a/scripts/gen-i18n.js +++ b/scripts/gen-i18n.js @@ -32,7 +32,7 @@ const walk = require('walk'); const flowParser = require('flow-parser'); 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 OUTPUT_FILE = 'src/i18n/strings/en_EN.json'; @@ -126,7 +126,7 @@ function getTranslationsJs(file) { if (tKey === null) return; // 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') { try { const placeholders = getFormatStrings(tKey); @@ -139,6 +139,22 @@ function getTranslationsJs(file) { 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) { console.log(); console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`); diff --git a/src/ComposerHistoryManager.js b/src/ComposerHistoryManager.js index 2fff3882b4..2757c5bd3d 100644 --- a/src/ComposerHistoryManager.js +++ b/src/ComposerHistoryManager.js @@ -61,7 +61,7 @@ export default class ComposerHistoryManager { // TODO: Performance issues? 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( Object.assign(new HistoryItem(), JSON.parse(item)), ); diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 7a4f0b99b0..86b38d4150 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -84,7 +84,7 @@ class MatrixClientPeg { if (this.matrixClient.initCrypto) { await this.matrixClient.initCrypto(); } - } catch(e) { + } catch (e) { // this can happen for a number of reasons, the most likely being // that the olm library was missing. It's not fatal. console.warn("Unable to initialise e2e: " + e); @@ -99,7 +99,7 @@ class MatrixClientPeg { const promise = this.matrixClient.store.startup(); console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`); await promise; - } catch(err) { + } catch (err) { // log any errors when starting up the database (if one exists) console.error(`Error starting matrixclient store: ${err}`); } diff --git a/src/RichText.js b/src/RichText.js index b61ba0b9a4..12274ee9f3 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -68,7 +68,7 @@ function unicodeToEmojiUri(str) { return unicodeChar; } else { // 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]; } diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 51e3eb8dc9..1bdf5ad90c 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -151,9 +151,9 @@ function textForCallHangupEvent(event) { const senderName = event.sender ? event.sender.name : _t('Someone'); const eventContent = event.getContent(); let reason = ""; - if(!MatrixClientPeg.get().supportsVoip()) { + if (!MatrixClientPeg.get().supportsVoip()) { reason = _t('(not supported by this browser)'); - } else if(eventContent.reason) { + } else if (eventContent.reason) { if (eventContent.reason === "ice_failed") { reason = _t('(could not connect media)'); } else if (eventContent.reason === "invite_timeout") { diff --git a/src/Tinter.js b/src/Tinter.js index b4a624e0ad..c7402c15be 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -19,6 +19,10 @@ const DEBUG = 0; // utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue] function colorToRgb(color) { + if (!color) { + return [0, 0, 0]; + } + if (color[0] === '#') { color = color.slice(1); if (color.length === 3) { @@ -31,16 +35,17 @@ function colorToRgb(color) { const g = (val >> 8) & 255; const b = val & 255; return [r, g, b]; - } - else { - let match = color.match(/rgb\((.*?),(.*?),(.*?)\)/); + } else { + const match = color.match(/rgb\((.*?),(.*?),(.*?)\)/); if (match) { - return [ parseInt(match[1]), - parseInt(match[2]), - parseInt(match[3]) ]; + return [ + parseInt(match[1]), + parseInt(match[2]), + parseInt(match[3]), + ]; } } - return [0,0,0]; + return [0, 0, 0]; } // utility to turn [red,green,blue] into #rrggbb @@ -72,6 +77,7 @@ class Tinter { "#EAF5F0", // Vector Light Green "#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green) "#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 @@ -81,6 +87,7 @@ class Tinter { this.keyHex[1], this.keyHex[2], this.keyHex[3], + this.keyHex[4], ]; // track the most current tint request inputs (which may differ from the @@ -90,6 +97,7 @@ class Tinter { undefined, undefined, undefined, + undefined, ]; this.cssFixups = [ @@ -152,9 +160,11 @@ class Tinter { this.calcCssFixups(); - if (DEBUG) console.log("Tinter.tint(" + primaryColor + ", " + - secondaryColor + ", " + - tertiaryColor + ")"); + if (DEBUG) { + console.log("Tinter.tint(" + primaryColor + ", " + + secondaryColor + ", " + + tertiaryColor + ")"); + } if (!primaryColor) { primaryColor = this.keyRgb[0]; @@ -194,9 +204,11 @@ class Tinter { this.colors[1] = secondaryColor; this.colors[2] = tertiaryColor; - if (DEBUG) console.log("Tinter.tint final: (" + primaryColor + ", " + - secondaryColor + ", " + - tertiaryColor + ")"); + if (DEBUG) { + console.log("Tinter.tint final: (" + primaryColor + ", " + + secondaryColor + ", " + + tertiaryColor + ")"); + } // go through manually fixing up the stylesheets. 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); this.theme = theme; // update keyRgb from the current theme CSS itself, if it defines it if (document.getElementById('mx_theme_accentColor')) { this.keyRgb[0] = window.getComputedStyle( - document.getElementById('mx_theme_accentColor') - ).color; + document.getElementById('mx_theme_accentColor')).color; } if (document.getElementById('mx_theme_secondaryAccentColor')) { this.keyRgb[1] = window.getComputedStyle( - document.getElementById('mx_theme_secondaryAccentColor') - ).color; + document.getElementById('mx_theme_secondaryAccentColor')).color; } if (document.getElementById('mx_theme_tertiaryAccentColor')) { this.keyRgb[2] = window.getComputedStyle( - document.getElementById('mx_theme_tertiaryAccentColor') - ).color; + document.getElementById('mx_theme_tertiaryAccentColor')).color; } this.calcCssFixups(); @@ -253,8 +278,10 @@ class Tinter { // abuse the tinter to change all the SVG's #fff to #2d2d2d // XXX: obviously this shouldn't be hardcoded here. this.tintSvgWhite('#2d2d2d'); + this.tintSvgBlack('#dddddd'); } else { this.tintSvgWhite('#ffffff'); + this.tintSvgBlack('#000000'); } } @@ -262,9 +289,11 @@ class Tinter { // cache our fixups if (this.cssFixups[this.theme]) return; - if (DEBUG) console.debug("calcCssFixups start for " + this.theme + " (checking " + - document.styleSheets.length + - " stylesheets)"); + if (DEBUG) { + console.debug("calcCssFixups start for " + this.theme + " (checking " + + document.styleSheets.length + + " stylesheets)"); + } this.cssFixups[this.theme] = []; @@ -322,21 +351,24 @@ class Tinter { } } } - if (DEBUG) console.log("calcCssFixups end (" + - this.cssFixups[this.theme].length + - " fixups)"); + if (DEBUG) { + console.log("calcCssFixups end (" + + this.cssFixups[this.theme].length + + " fixups)"); + } } applyCssFixups() { - if (DEBUG) console.log("applyCssFixups start (" + - this.cssFixups[this.theme].length + - " fixups)"); + if (DEBUG) { + console.log("applyCssFixups start (" + + this.cssFixups[this.theme].length + + " fixups)"); + } for (let i = 0; i < this.cssFixups[this.theme].length; i++) { const cssFixup = this.cssFixups[this.theme][i]; try { cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index]; - } - catch (e) { + } catch (e) { // Firefox Quantum explodes if you manually edit the CSS in the // inspector and then try to do a tint, as apparently all the // fixups are then stale. @@ -358,10 +390,10 @@ class Tinter { if (DEBUG) console.log("calcSvgFixups start for " + svgs); const fixups = []; for (let i = 0; i < svgs.length; i++) { - var svgDoc; + let svgDoc; try { svgDoc = svgs[i].contentDocument; - } catch(e) { + } catch (e) { let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString(); if (e.message) { msg += e.message; @@ -369,7 +401,7 @@ class Tinter { if (e.stack) { msg += ' | stack: ' + e.stack; } - console.error(e); + console.error(msg); } if (!svgDoc) continue; const tags = svgDoc.getElementsByTagName("*"); @@ -379,8 +411,7 @@ class Tinter { const attr = this.svgAttrs[k]; for (let l = 0; l < this.keyHex.length; l++) { if (tag.getAttribute(attr) && - tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) - { + tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) { fixups.push({ node: tag, attr: attr, diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 8b43964b1a..9d587c2eb4 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -126,7 +126,7 @@ export default class UserProvider extends AutocompleteProvider { const events = this.room.getLiveTimeline().getEvents(); const lastSpoken = {}; - for(const event of events) { + for (const event of events) { lastSpoken[event.getSender()] = event.getTs(); } diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 23feb4cf30..ffa5e45249 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -19,7 +19,7 @@ import React from 'react'; import Matrix from 'matrix-js-sdk'; import sdk from '../../index'; import MatrixClientPeg from '../../MatrixClientPeg'; -import { _t, _tJsx } from '../../languageHandler'; +import { _t } from '../../languageHandler'; /* * Component which shows the filtered file using a TimelinePanel @@ -92,7 +92,10 @@ const FilePanel = React.createClass({ if (MatrixClientPeg.get().isGuest()) { return
- { _tJsx("You must register to use this functionality", /(.*?)<\/a>/, (sub) => { sub }) } + { _t("You must register to use this functionality", + {}, + { 'a': (sub) => { sub } }) + }
; } else if (this.noRoom) { diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 1b5ebb6b36..b137893bde 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -22,7 +22,7 @@ import MatrixClientPeg from '../../MatrixClientPeg'; import sdk from '../../index'; import dis from '../../dispatcher'; import { sanitizedHtmlNode } from '../../HtmlUtils'; -import { _t, _td, _tJsx } from '../../languageHandler'; +import { _t, _td } from '../../languageHandler'; import AccessibleButton from '../views/elements/AccessibleButton'; import Modal from '../../Modal'; import classnames from 'classnames'; @@ -932,12 +932,12 @@ export default React.createClass({ className="mx_GroupView_groupDesc_placeholder" onClick={this._onEditClick} > - { _tJsx( + { _t( 'Your community hasn\'t got a Long Description, a HTML page to show to community members.
' + 'Click here to open settings and give it one!', - [/
/], - [(sub) =>
]) - } + {}, + { 'br':
}, + ) } ; } const groupDescEditingClasses = classnames({ diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 085dc7f22e..ccdeadcfee 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -315,7 +315,7 @@ module.exports = React.createClass({ // the first thing to do is to try the token params in the query-string Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => { - if(loggedIn) { + if (loggedIn) { this.props.onTokenLoginCompleted(); // don't do anything else until the page reloads - just stay in @@ -888,7 +888,7 @@ module.exports = React.createClass({ */ _onSetTheme: function(theme) { if (!theme) { - theme = this.props.config.default_theme || 'light'; + theme = SettingsStore.getValueAt(SettingLevel.DEFAULT, "theme"); } // look for the stylesheet elements. diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index cc4783fdac..c669d7dd73 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import GeminiScrollbar from 'react-gemini-scrollbar'; import {MatrixClient} from 'matrix-js-sdk'; import sdk from '../../index'; -import { _t, _tJsx } from '../../languageHandler'; +import { _t } from '../../languageHandler'; import withMatrixClient from '../../wrappers/withMatrixClient'; import AccessibleButton from '../views/elements/AccessibleButton'; import dis from '../../dispatcher'; @@ -165,13 +165,13 @@ export default withMatrixClient(React.createClass({
{ _t('Join an existing community') }
- { _tJsx( + { _t( 'To join an existing community you\'ll have to '+ 'know its community identifier; this will look '+ 'something like +example:matrix.org.', - /(.*)<\/i>/, - (sub) => { sub }, - ) } + {}, + { 'i': (sub) => { sub } }) + } diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index eabeef7a04..f9f30e1715 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -16,8 +16,8 @@ limitations under the License. */ import React from 'react'; -import { _t, _tJsx } from '../../languageHandler'; import Matrix from 'matrix-js-sdk'; +import { _t } from '../../languageHandler'; import sdk from '../../index'; import WhoIsTyping from '../../WhoIsTyping'; import MatrixClientPeg from '../../MatrixClientPeg'; @@ -25,7 +25,6 @@ import MemberAvatar from '../views/avatars/MemberAvatar'; import Resend from '../../Resend'; import { showUnknownDeviceDialogForMessages } from '../../cryptodevices'; -const HIDE_DEBOUNCE_MS = 10000; const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED_LARGE = 2; @@ -286,13 +285,13 @@ module.exports = React.createClass({ if (hasUDE) { title = _t("Message not sent due to unknown devices being present"); - content = _tJsx( - "Show devices or cancel all.", - [/(.*?)<\/a>/, /(.*?)<\/a>/], - [ - (sub) => { sub }, - (sub) => { sub }, - ], + content = _t( + "Show devices or cancel all.", + {}, + { + 'showDevicesText': (sub) => { sub }, + 'cancelText': (sub) => { sub }, + }, ); } else { if ( @@ -305,14 +304,15 @@ module.exports = React.createClass({ } else { title = _t("Some of your messages have not been sent."); } - content = _tJsx( - "Resend all or cancel all now. "+ - "You can also select individual messages to resend or cancel.", - [/(.*?)<\/a>/, /(.*?)<\/a>/], - [ - (sub) => { sub }, - (sub) => { sub }, - ], + content = _t("Resend all or cancel all now. " + + "You can also select individual messages to resend or cancel.", + {}, + { + 'resendText': (sub) => + { sub }, + 'cancelText': (sub) => + { sub }, + }, ); } @@ -391,12 +391,15 @@ module.exports = React.createClass({ if (this.props.sentMessageAndIsAlone) { return (
- { _tJsx("There's no one else here! Would you like to invite others or stop warning about the empty room?", - [/(.*?)<\/a>/, /(.*?)<\/a>/], - [ - (sub) => { sub }, - (sub) => { sub }, - ], + { _t("There's no one else here! Would you like to invite others " + + "or stop warning about the empty room?", + {}, + { + 'inviteText': (sub) => + { sub }, + 'nowarnText': (sub) => + { sub }, + }, ) }
); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 0cc4c6410c..123bd54797 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -299,7 +299,7 @@ module.exports = React.createClass({ // Check if user has previously chosen to hide the app drawer for this // room. If so, do not show apps - let hideWidgetDrawer = localStorage.getItem( + const hideWidgetDrawer = localStorage.getItem( room.roomId + "_hide_widget_drawer"); if (hideWidgetDrawer === "true") { @@ -704,7 +704,7 @@ module.exports = React.createClass({ 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}); }, @@ -1060,7 +1060,7 @@ module.exports = React.createClass({ } if (this.state.searchScope === 'All') { - if(roomId != lastRoomId) { + if (roomId != lastRoomId) { const room = cli.getRoom(roomId); // XXX: if we've left the room, we might not know about @@ -1371,13 +1371,13 @@ module.exports = React.createClass({ */ handleScrollKey: function(ev) { let panel; - if(this.refs.searchResultsPanel) { + if (this.refs.searchResultsPanel) { panel = this.refs.searchResultsPanel; - } else if(this.refs.messagePanel) { + } else if (this.refs.messagePanel) { panel = this.refs.messagePanel; } - if(panel) { + if (panel) { panel.handleScrollKey(ev); } }, @@ -1396,7 +1396,7 @@ module.exports = React.createClass({ // otherwise react calls it with null on each update. _gatherTimelinePanelRef: function(r) { this.refs.messagePanel = r; - if(r) { + if (r) { console.log("updateTint from RoomView._gatherTimelinePanelRef"); this.updateTint(); } diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index cda60c606f..dfc6b0f7a1 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -573,7 +573,7 @@ module.exports = React.createClass({ debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" + pixelOffset + " (delta: "+scrollDelta+")"); - if(scrollDelta != 0) { + if (scrollDelta != 0) { this._setScrollTop(scrollNode.scrollTop + scrollDelta); } }, diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 56661b0d26..aeb6cce6c3 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -310,7 +310,7 @@ var TimelinePanel = React.createClass({ return Promise.resolve(false); } - if(!this._timelineWindow.canPaginate(dir)) { + if (!this._timelineWindow.canPaginate(dir)) { debuglog("TimelinePanel: can't", dir, "paginate any further"); this.setState({[canPaginateKey]: false}); return Promise.resolve(false); @@ -440,7 +440,7 @@ var TimelinePanel = React.createClass({ var callback = null; if (sender != myUserId && !UserActivity.userCurrentlyActive()) { 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 // 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 // down, they don't see it. - if(this.state.readMarkerVisible) { + if (this.state.readMarkerVisible) { this.setState({ readMarkerVisible: false, }); diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 692dd4e01d..933f90523a 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -612,9 +612,8 @@ module.exports = React.createClass({ }, onLanguageChange: function(newLang) { - if(this.state.language !== newLang) { - // We intentionally promote this to the account level at this point - SettingsStore.setValue("language", null, SettingLevel.ACCOUNT, newLang); + if (this.state.language !== newLang) { + SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); this.setState({ language: newLang, }); diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 8a2714d96a..43753bfd38 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -154,7 +154,7 @@ module.exports = React.createClass({ }, render: function() { - const LoginPage = sdk.getComponent("login.LoginPage"); + const LoginPage = sdk.getComponent("login.LoginPage"); const LoginHeader = sdk.getComponent("login.LoginHeader"); const LoginFooter = sdk.getComponent("login.LoginFooter"); const ServerConfig = sdk.getComponent("login.ServerConfig"); diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index bd0afcb335..baa2064277 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -18,7 +18,7 @@ limitations under the License. 'use strict'; import React from 'react'; -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import * as languageHandler from '../../../languageHandler'; import sdk from '../../../index'; import Login from '../../../Login'; @@ -96,7 +96,7 @@ module.exports = React.createClass({ ).then((data) => { this.props.onLoggedIn(data); }, (error) => { - if(this._unmounted) { + if (this._unmounted) { return; } let errorText; @@ -113,14 +113,14 @@ module.exports = React.createClass({
{ _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?:\/\//, ''), }) }
); } else { - errorText = _t('Incorrect username and/or password.'); + errorText = _t('Incorrect username and/or password.'); } } else { // other errors, not specific to doing a password login @@ -136,7 +136,7 @@ module.exports = React.createClass({ loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403, }); }).finally(() => { - if(this._unmounted) { + if (this._unmounted) { return; } this.setState({ @@ -272,17 +272,19 @@ module.exports = React.createClass({ !this.state.enteredHomeserverUrl.startsWith("http")) ) { errorText = - { _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 enable unsafe scripts.", - /(.*?)<\/a>/, - (sub) => { return { sub }; }, + {}, + { 'a': (sub) => { return { sub }; } }, ) } ; } else { errorText = - { _tJsx("Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.", - /(.*?)<\/a>/, - (sub) => { return { sub }; }, + { + _t("Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.", + {}, + { 'a': (sub) => { return { sub }; } }, ) } ; } @@ -293,7 +295,7 @@ module.exports = React.createClass({ componentForStep: function(step) { switch (step) { - case 'm.login.password': + case 'm.login.password': { const PasswordLogin = sdk.getComponent('login.PasswordLogin'); return ( ); - case 'm.login.cas': + } + case 'm.login.cas': { const CasLogin = sdk.getComponent('login.CasLogin'); return ( ); - default: + } + default: { if (!step) { return; } @@ -323,11 +327,12 @@ module.exports = React.createClass({ { _t('Sorry, this homeserver is using a login which is not recognised ') }({ step }) ); + } } }, _onLanguageChange: function(newLang) { - if(languageHandler.getCurrentLanguage() !== newLang) { + if (languageHandler.getCurrentLanguage() !== newLang) { SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); PlatformPeg.get().reload(); } @@ -388,8 +393,7 @@ module.exports = React.createClass({ const theme = SettingsStore.getValue("theme"); if (theme !== "status") { header =

{ _t('Sign in') }

; - } - else { + } else { if (!this.state.errorText) { header =

{ _t('Sign in to get started') }

; } diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 709bb0b7f6..e57b7fd0c2 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -399,8 +399,7 @@ module.exports = React.createClass({ // FIXME: remove hardcoded Status team tweaks at some point if (theme === 'status' && this.state.errorText) { header =
{ this.state.errorText }
; - } - else { + } else { header =

{ _t('Create an account') }

; if (this.state.errorText) { errorText =
{ this.state.errorText }
; diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index b0dc0a304e..9c8be27c89 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -54,7 +54,7 @@ export default React.createClass({ const deviceInfo = r[userId][deviceId]; - if(!deviceInfo) { + if (!deviceInfo) { console.warn(`No details found for device ${userId}:${deviceId}`); this.props.onFinished(false); diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js index f404bdd975..75ae0eda17 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.js +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -18,7 +18,7 @@ import React from 'react'; import sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; export default React.createClass({ @@ -45,9 +45,10 @@ export default React.createClass({ if (SdkConfig.get().bug_report_endpoint_url) { bugreport = (

- { _tJsx( + { _t( "Otherwise, click here to send a bug report.", - /(.*?)<\/a>/, (sub) => { sub }, + {}, + { 'a': (sub) => { sub } }, ) }

); diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 057609b344..6fc1d77682 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -21,7 +21,7 @@ import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import classnames from 'classnames'; 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 // sending a request to the server @@ -267,24 +267,21 @@ export default React.createClass({ { usernameIndicator }

- { _tJsx( + { _t( 'This will be your account name on the ' + 'homeserver, or you can pick a different server.', - [ - /<\/span>/, - /(.*?)<\/a>/, - ], - [ - (sub) => { this.props.homeserverUrl }, - (sub) => { sub }, - ], + {}, + { + 'span': { this.props.homeserverUrl }, + 'a': (sub) => { sub }, + }, ) }

- { _tJsx( + { _t( 'If you already have a Matrix account you can log in instead.', - /(.*?)<\/a>/, - [(sub) => { sub }], + {}, + { 'a': (sub) => { sub } }, ) }

{ auth } diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js index f1117fd5aa..ef08c8355b 100644 --- a/src/components/views/elements/AppPermission.js +++ b/src/components/views/elements/AppPermission.js @@ -19,9 +19,9 @@ export default class AppPermission extends React.Component { 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')); - if(curl) { + if (curl) { curl.search = curl.query = ""; curlString = curl.format(); } @@ -34,7 +34,7 @@ export default class AppPermission extends React.Component { } isScalarWurl(wurl) { - if(wurl && wurl.hostname && ( + if (wurl && wurl.hostname && ( wurl.hostname === 'scalar.vector.im' || wurl.hostname === 'scalar-staging.riot.im' || wurl.hostname === 'scalar-develop.riot.im' || diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index aa781c2d62..a005406133 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -22,6 +22,7 @@ import React from 'react'; import MatrixClientPeg from '../../../MatrixClientPeg'; import PlatformPeg from '../../../PlatformPeg'; import ScalarAuthClient from '../../../ScalarAuthClient'; +import TintableSvgButton from './TintableSvgButton'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; import { _t, _td } from '../../../languageHandler'; @@ -283,7 +284,7 @@ export default React.createClass({ formatAppTileName() { 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(); } return appTileName; @@ -371,9 +372,9 @@ export default React.createClass({ // editing is done in scalar const showEditButton = Boolean(this._scalarClient && this._canUserModify()); const deleteWidgetLabel = this._deleteWidgetLabel(); - let deleteIcon = 'img/cancel.svg'; - let deleteClasses = 'mx_filterFlipColor mx_AppTileMenuBarWidget'; - if(this._canUserModify()) { + let deleteIcon = 'img/cancel_green.svg'; + let deleteClasses = 'mx_AppTileMenuBarWidget'; + if (this._canUserModify()) { deleteIcon = 'img/icon-delete-pink.svg'; deleteClasses += ' mx_AppTileMenuBarWidgetDelete'; } @@ -384,22 +385,23 @@ export default React.createClass({ { this.formatAppTileName() } { /* Edit widget */ } - { showEditButton && {_t('Edit')} } { /* Delete widget */ } - {_t(deleteWidgetLabel)} diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index 8f2ba006cf..6c86296a38 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -41,8 +41,8 @@ export default class LanguageDropdown extends React.Component { componentWillMount() { languageHandler.getAllLanguagesFromJson().then((langs) => { 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; }); this.setState({langs}); @@ -57,7 +57,7 @@ export default class LanguageDropdown extends React.Component { const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true); if (language) { this.props.onOptionChange(language); - }else { + } else { const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser()); this.props.onOptionChange(language); } diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index de6f801a21..b25b816a34 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -216,7 +216,7 @@ module.exports = React.createClass({ // are there only to show translators to non-English languages // that the verb is conjugated to plural or singular Subject. let res = null; - switch(t) { + switch (t) { case "joined": res = (userCount > 1) ? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count: repeats }) @@ -304,7 +304,7 @@ module.exports = React.createClass({ return items[0]; } else if (remaining > 0) { 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 { const lastItem = items.pop(); return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem }); diff --git a/src/components/views/elements/TintableSvgButton.js b/src/components/views/elements/TintableSvgButton.js new file mode 100644 index 0000000000..9ca2cdcbb4 --- /dev/null +++ b/src/components/views/elements/TintableSvgButton.js @@ -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 ( + + + + + ); + } +} + +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() {}, +}; diff --git a/src/components/views/login/CaptchaForm.js b/src/components/views/login/CaptchaForm.js index cf814b0a6e..21e5094b28 100644 --- a/src/components/views/login/CaptchaForm.js +++ b/src/components/views/login/CaptchaForm.js @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; 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) // * embedding the captcha in an iframe (if that works) // * using a better captcha lib - ReactDOM.render(_tJsx( + ReactDOM.render(_t( "Robot check is currently unavailable on desktop - please use a web browser", - /(.*?)<\/a>/, - (sub) => { return { sub }; }), warning); + {}, + { 'a': (sub) => { return { sub }; }}), warning); this.refs.recaptchaContainer.appendChild(warning); } else { const scriptTag = document.createElement('script'); diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index 5f5a74ccd1..d0b6c8decb 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -20,7 +20,7 @@ import url from 'url'; import classnames from 'classnames'; 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 * InteractiveAuth to prompt the user to enter the information needed @@ -256,7 +256,10 @@ export const EmailIdentityAuthEntry = React.createClass({ } else { return (
-

{ _tJsx("An email has been sent to %(emailAddress)s", /%\(emailAddress\)s/, (sub) => {this.props.inputs.emailAddress}) }

+

{ _t("An email has been sent to %(emailAddress)s", + { emailAddress: (sub) => { this.props.inputs.emailAddress } }, + ) } +

{ _t("Please check your email to continue registration.") }

); @@ -370,7 +373,10 @@ export const MsisdnAuthEntry = React.createClass({ }); return (
-

{ _tJsx("A text message has been sent to %(msisdn)s", /%\(msisdn\)s/, (sub) => {this._msisdn}) }

+

{ _t("A text message has been sent to %(msisdn)s", + { msisdn: this._msisdn }, + ) } +

{ _t("Please enter the code it contains:") }

diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 77b695ef12..83bb41c1a3 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -122,7 +122,7 @@ class PasswordLogin extends React.Component { mx_Login_field_disabled: disabled, }; - switch(loginType) { + switch (loginType) { case PasswordLogin.LOGIN_FIELD_EMAIL: classes.mx_Login_email = true; return - { _tJsx('%(senderDisplayName)s changed the room avatar to ', - [ - /%\(senderDisplayName\)s/, - //, - ], - [ - (sub) => senderDisplayName, - (sub) => - - - , - ], - ) + { _t('%(senderDisplayName)s changed the room avatar to ', + { senderDisplayName: senderDisplayName }, + { + 'img': () => + + + , + }) }
); diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index afdb97272f..10e3082938 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -19,7 +19,7 @@ import React from 'react'; import sdk from '../../../index'; import Flair from '../elements/Flair.js'; -import { _tJsx } from '../../../languageHandler'; +import { _t, substitute } from '../../../languageHandler'; export default function SenderProfile(props) { const EmojiText = sdk.getComponent('elements.EmojiText'); @@ -42,22 +42,28 @@ export default function SenderProfile(props) { : null, ]; - let content = ''; - - if(props.text) { - // Replace senderName, and wrap surrounding text in spans with the right class - content = _tJsx(props.text, /^(.*)\%\(senderName\)s(.*)$/m, (p1, p2) => [ - p1 ? { p1 } : null, - nameElem, - p2 ? { p2 } : null, - ]); + let content; + if (props.text) { + content = _t(props.text, { senderName: () => nameElem }); } 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 (
- { content } + { content.props.children[0] ? + { content.props.children[0] } : '' + } + { content.props.children[1] } + { content.props.children[2] ? + { content.props.children[2] } : '' + }
); } diff --git a/src/components/views/room_settings/UrlPreviewSettings.js b/src/components/views/room_settings/UrlPreviewSettings.js index 6fb04f3378..6491bb0e09 100644 --- a/src/components/views/room_settings/UrlPreviewSettings.js +++ b/src/components/views/room_settings/UrlPreviewSettings.js @@ -17,7 +17,7 @@ limitations under the License. const React = require('react'); const sdk = require("../../../index"); -import { _t, _tJsx } from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; @@ -42,11 +42,11 @@ module.exports = React.createClass({ let previewsForAccount = null; if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) { previewsForAccount = ( - _tJsx("You have enabled URL previews by default.", /(.*?)<\/a>/, (sub)=>{ sub }) + _t("You have enabled URL previews by default.", {}, { 'a': (sub)=>{ sub } }) ); } else { previewsForAccount = ( - _tJsx("You have disabled URL previews by default.", /(.*?)<\/a>/, (sub)=>{ sub }) + _t("You have disabled URL previews by default.", {}, { 'a': (sub)=>{ sub } }) ); } @@ -63,14 +63,14 @@ module.exports = React.createClass({ ); } 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")) { - 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 = (); } - let previewsForRoomAccount = ( + const previewsForRoomAccount = ( { params['$' + key] = app.data[key]; }); @@ -177,7 +177,7 @@ module.exports = React.createClass({ _canUserModify: function() { try { return WidgetUtils.canUserModifyWidgets(this.props.room.roomId); - } catch(err) { + } catch (err) { console.error(err); return false; } diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index b8bdad3af8..c0857d9691 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -21,7 +21,7 @@ import sdk from '../../../index'; import dis from "../../../dispatcher"; import ObjectUtils from '../../../ObjectUtils'; import AppsDrawer from './AppsDrawer'; -import { _t, _tJsx} from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; module.exports = React.createClass({ @@ -99,13 +99,13 @@ module.exports = React.createClass({ supportedText = _t(" (unsupported)"); } else { joinNode = ( - { _tJsx( + { _t( "Join as voice or video.", - [/(.*?)<\/voiceText>/, /(.*?)<\/videoText>/], - [ - (sub) => { this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }, - (sub) => { this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }, - ], + {}, + { + 'voiceText': (sub) => { this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }, + 'videoText': (sub) => { this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }, + }, ) } ); } diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 812d72a26a..3407ea159d 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -33,22 +33,30 @@ const ObjectUtils = require('../../../ObjectUtils'); const eventTileTypes = { 'm.room.message': 'messages.MessageEvent', - 'm.room.member': 'messages.TextualEvent', 'm.call.invite': 'messages.TextualEvent', 'm.call.answer': 'messages.TextualEvent', 'm.call.hangup': 'messages.TextualEvent', +}; + +const stateEventTileTypes = { + 'm.room.member': 'messages.TextualEvent', 'm.room.name': 'messages.TextualEvent', 'm.room.avatar': 'messages.RoomAvatarEvent', - 'm.room.topic': 'messages.TextualEvent', 'm.room.third_party_invite': 'messages.TextualEvent', 'm.room.history_visibility': 'messages.TextualEvent', 'm.room.encryption': 'messages.TextualEvent', + 'm.room.topic': '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', }; +function getHandlerTile(ev) { + const type = ev.getType(); + return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type]; +} + const MAX_READ_AVATARS = 5; // 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 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 // before trying to instantiate us if (!EventTileType) { @@ -600,8 +608,10 @@ module.exports = withMatrixClient(React.createClass({ module.exports.haveTileForEvent = function(e) { // Only messages have a tile (black-rectangle) if redacted 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) !== ''; } else { return true; diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 4d875ea24a..cb6cb6c0f3 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -562,7 +562,7 @@ module.exports = withMatrixClient(React.createClass({ onMemberAvatarClick: function() { const member = this.props.member; 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 ImageView = sdk.getComponent("elements.ImageView"); diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 2ac7075189..2841f30423 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -111,10 +111,10 @@ export default class MessageComposer extends React.Component {
), onFinished: (shouldUpload) => { - if(shouldUpload) { + if (shouldUpload) { // MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file if (files) { - for(let i=0; i hex unicode const emojiUc = asciiList[emojiMatch[1]]; // hex unicode -> shortname -> actual unicode @@ -689,7 +689,7 @@ export default class MessageComposerInput extends React.Component { } const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState); - if( + if ( ['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item'] .includes(currentBlockType) ) { diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index a44673c879..aee229c5da 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -389,7 +389,7 @@ module.exports = React.createClass({ let rightRow; 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 = ; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 1a9fa5d4e9..ebe0bdb03f 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -18,12 +18,10 @@ limitations under the License. 'use strict'; const React = require("react"); const ReactDOM = require("react-dom"); -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; const GeminiScrollbar = require('react-gemini-scrollbar'); const MatrixClientPeg = require("../../../MatrixClientPeg"); const CallHandler = require('../../../CallHandler'); -const RoomListSorter = require("../../../RoomListSorter"); -const Unread = require('../../../Unread'); const dis = require("../../../dispatcher"); const sdk = require('../../../index'); const rate_limited_func = require('../../../ratelimitedfunc'); @@ -486,28 +484,25 @@ module.exports = React.createClass({ const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton'); const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton'); - const TintableSvg = sdk.getComponent('elements.TintableSvg'); switch (section) { case 'im.vector.fake.direct': return
- { _tJsx( + { _t( "Press to start a chat with someone", - [//], - [ - (sub) => , - ], + {}, + { 'StartChatButton': }, ) }
; case 'im.vector.fake.recent': return
- { _tJsx( + { _t( "You're not in any rooms yet! Press to make a room or"+ " to browse the directory", - [//, //], - [ - (sub) => , - (sub) => , - ], + {}, + { + 'CreateRoomButton': , + 'RoomDirectoryButton': , + }, ) }
; } diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 0c0601a504..175a3ea552 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -21,7 +21,7 @@ const React = require('react'); const sdk = require('../../../index'); const MatrixClientPeg = require('../../../MatrixClientPeg'); -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; module.exports = React.createClass({ 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}) }
- { _tJsx( + { _t( 'Would you like to accept or decline this invitation?', - [/(.*?)<\/acceptText>/, /(.*?)<\/declineText>/], - [ - (sub) => { sub }, - (sub) => { sub }, - ], + {}, + { + 'acceptText': (sub) => { sub }, + 'declineText': (sub) => { sub }, + }, ) }
{ emailMatchBlock } @@ -165,13 +165,13 @@ module.exports = React.createClass({ let actionText; if (kicked) { - if(roomName) { + if (roomName) { actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName}); } else { actionText = _t("You have been kicked from this room by %(userName)s.", {userName: kickerName}); } } else if (banned) { - if(roomName) { + if (roomName) { actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName}); } else { actionText = _t("You have been banned from this room by %(userName)s.", {userName: kickerName}); @@ -211,9 +211,9 @@ module.exports = React.createClass({
{ name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') }
- { _tJsx("Click here to join the discussion!", - /(.*?)<\/a>/, - (sub) => { sub }, + { _t("Click here to join the discussion!", + {}, + { 'a': (sub) => { sub } }, ) }
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 8ba18ee96e..4ac2da2030 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -17,7 +17,7 @@ limitations under the License. import Promise from 'bluebird'; import React from 'react'; -import { _t, _tJsx, _td } from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; import Modal from '../../../Modal'; @@ -309,9 +309,9 @@ module.exports = React.createClass({ } // url preview settings - let ps = this.saveUrlPreviewSettings(); + const ps = this.saveUrlPreviewSettings(); if (ps.length > 0) { - ps.map(p => promises.push(p)); + ps.map((p) => promises.push(p)); } // related groups @@ -584,7 +584,7 @@ module.exports = React.createClass({ const roomState = this.props.room.currentState; const isEncrypted = cli.isRoomEncrypted(this.props.room.roomId); - let settings = ( + const settings = ( { _t("Tagged as: ") }{ canSetTag ? (tags.map(function(tag, i) { @@ -781,10 +779,10 @@ module.exports = React.createClass({ if (this.state.join_rule === "public" && aliasCount == 0) { addressWarning =
- { _tJsx( + { _t( 'To link to a room it must have an address.', - /(.*?)<\/a>/, - (sub) => { sub }, + {}, + { 'a': (sub) => { sub } }, ) }
; } @@ -931,7 +929,7 @@ module.exports = React.createClass({ { Object.keys(events_levels).map(function(event_type, i) { let label = plEventsToLabels[event_type]; if (label) label = _t(label); - else label = _tJsx("To send events of type , you must be a", //, () => { event_type }); + else label = _t("To send events of type , you must be a", {}, { 'eventType': { event_type } }); return (
{ label } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 8034dd0fa6..743ec93da9 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -54,7 +54,7 @@ module.exports = React.createClass({ }, getInitialState: function() { - return({ + return ({ hover: false, badgeHover: false, menuDisplayed: false, diff --git a/src/components/views/voip/VideoFeed.js b/src/components/views/voip/VideoFeed.js index 953dbc866f..f955df62d9 100644 --- a/src/components/views/voip/VideoFeed.js +++ b/src/components/views/voip/VideoFeed.js @@ -39,7 +39,7 @@ module.exports = React.createClass({ }, onResize: function(e) { - if(this.props.onResize) { + if (this.props.onResize) { this.props.onResize(e); } }, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 05143e96b1..72fd4503b5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -460,6 +460,8 @@ "New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)", "You have enabled URL previews by default.": "You have enabled URL previews by default.", "You have disabled URL previews by default.": "You have disabled 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", "Error decrypting audio": "Error decrypting audio", "Error decrypting attachment": "Error decrypting attachment", @@ -752,16 +754,16 @@ "You have no visible notifications": "You have no visible notifications", "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", - "Show devices or cancel all.": "Show devices or cancel all.", + "Show devices or cancel all.": "Show devices or cancel all.", "Some of your messages have not been sent.": "Some of your messages have not been sent.", - "Resend all or cancel all now. You can also select individual messages to resend or cancel.": "Resend all or cancel all now. You can also select individual messages to resend or cancel.", + "Resend all or cancel all now. You can also select individual messages to resend or cancel.": "Resend all or cancel all now. You can also select individual messages to resend or cancel.", "Warning": "Warning", "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.", "%(count)s new messages|other": "%(count)s new messages", "%(count)s new messages|one": "%(count)s new message", "Active call": "Active call", - "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", + "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", "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?", "Failed to upload file": "Failed to upload file", diff --git a/src/languageHandler.js b/src/languageHandler.js index 34b28e848f..b9b5371022 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -34,12 +34,9 @@ export function _td(s) { return s; } -// The translation function. This is just a simple wrapper to counterpart, -// but exists mostly because we must use the same counterpart instance -// between modules (ie. here (react-sdk) and the app (riot-web), and if we -// just import counterpart and use it directly, we end up using a different -// instance. -export function _t(...args) { +// Wrapper for counterpart's translation function so that it handles nulls and undefineds properly +// Takes the same arguments as counterpart.translate() +function safeCounterpartTranslate(...args) { // Horrible hack to avoid https://github.com/vector-im/riot-web/issues/4191 // 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 @@ -50,11 +47,11 @@ export function _t(...args) { if (args[1] && typeof args[1] === 'object') { Object.keys(args[1]).forEach((k) => { 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'; } 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'; } }); @@ -63,75 +60,136 @@ export function _t(...args) { } /* - * Translates stringified JSX into translated JSX. E.g - * _tJsx( - * "click here now", - * /(.*?)<\/a>/, - * (sub) => { return { sub }; } - * ); + * Translates text and optionally also replaces XML-ish elements in the text with e.g. React components + * @param {string} text The untranslated text, e.g "click here now to %(foo)s". + * @param {object} variables Variable substitutions, e.g { foo: 'bar' } + * @param {object} tags Tag substitutions e.g. { 'a': (sub) => {sub} } * - * @param {string} jsxText The untranslated stringified JSX e.g "click here now". - * This will be translated by passing the string through to _t(...) + * In both variables and tags, the values to substitute with can be either simple strings, React components, + * 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. - * The captured groups from the regexp will be fed to 'sub'. - * Only the captured groups will be included in the output, the match itself is discarded. - * 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. + * Use tag substitutions if you need to translate text between tags (e.g. "Click here!"), otherwise + * you will end up with literal "" in your output, rather than HTML. Note that you can also use variable + * substitution to insert React components, but you can't use it to translate text between tags. * - * @param {Function|Function[]} subs A function which will be called - * with multiple args, each arg representing a captured group of the matching regexp. - * This function must return a JSX node. - * - * @return a React component containing the generated text + * @return a React component if any non-strings were used in substitutions, otherwise a string */ -export function _tJsx(jsxText, patterns, subs) { - // convert everything to arrays - if (patterns instanceof RegExp) { - patterns = [patterns]; - } - 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}`); - } - } +export function _t(text, variables, tags) { + // Don't do subsitutions in counterpart. We handle it ourselves so we can replace with React components + // However, still pass the variables to counterpart so that it can choose the correct plural if count is given + // 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); // The translation returns text so there's no XSS vector here (no unsafe HTML, no code execution) - const tJsxText = _t(jsxText, {interpolate: false}); - const output = [tJsxText]; + const translated = safeCounterpartTranslate(text, args); + + return substitute(translated, variables, tags); +} + +/* + * Similar to _t(), except only does substitutions, and no translation + * @param {string} text The text, e.g "click here now to %(foo)s". + * @param {object} variables Variable substitutions, e.g { foo: 'bar' } + * @param {object} tags Tag substitutions e.g. { 'a': (sub) => {sub} } + * + * 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 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 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). // Rinse and repeat for other patterns (using post-text). const inputText = output.pop(); - const match = inputText.match(patterns[i]); + const match = inputText.match(regexp); 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 // by the raw translation after the *match* (not captured group). - output.push(inputText.substr(0, match.index)); - output.push(subs[i].apply(null, capturedGroups)); - output.push(inputText.substr(match.index + match[0].length)); + + const head = inputText.substr(0, match.index); + 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 - // should have a unique "key" prop' error: we explicitly pass the generated - // nodes into React.createElement as children of a . - return React.createElement('span', null, ...output); + if (shouldWrapInSpan) { + return React.createElement('span', null, ...output); + } else { + return output.join(''); + } } // Allow overriding the text displayed when no translation exists diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js index b6343c4a96..d93a48005d 100644 --- a/src/settings/SettingsStore.js +++ b/src/settings/SettingsStore.js @@ -176,13 +176,21 @@ export default class SettingsStore { * @return {*} The value, or null if not found */ 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. - * @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level to - * look at. + * @param {"device"|"room-device"|"room-account"|"account"|"room"|"config"|"default"} level The + * level to look at. * @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 {boolean} explicit If true, this method will not consider other levels, just the one diff --git a/src/settings/controllers/NotificationControllers.js b/src/settings/controllers/NotificationControllers.js index 261e194d74..9dcf78e26b 100644 --- a/src/settings/controllers/NotificationControllers.js +++ b/src/settings/controllers/NotificationControllers.js @@ -15,12 +15,35 @@ limitations under the License. */ 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 { getValueOverride(level, roomId, calculatedValue) { 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) { @@ -35,15 +58,22 @@ export class NotificationsEnabledController extends SettingController { export class NotificationBodyEnabledController extends SettingController { getValueOverride(level, roomId, calculatedValue) { 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 { getValueOverride(level, roomId, calculatedValue) { 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; } } diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index fe0ad2a500..e50358a728 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -26,6 +26,9 @@ export default class AccountSettingHandler extends SettingsHandler { // Special case URL previews if (settingName === "urlPreviewsEnabled") { 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']; } diff --git a/src/settings/handlers/DeviceSettingsHandler.js b/src/settings/handlers/DeviceSettingsHandler.js index 7b5ec6a5dd..22f6140a80 100644 --- a/src/settings/handlers/DeviceSettingsHandler.js +++ b/src/settings/handlers/DeviceSettingsHandler.js @@ -40,11 +40,17 @@ export default class DeviceSettingsHandler extends SettingsHandler { // Special case notifications 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") { - 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") { - 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]; diff --git a/src/settings/handlers/RoomAccountSettingsHandler.js b/src/settings/handlers/RoomAccountSettingsHandler.js index 503d5de6c4..e946581807 100644 --- a/src/settings/handlers/RoomAccountSettingsHandler.js +++ b/src/settings/handlers/RoomAccountSettingsHandler.js @@ -25,6 +25,9 @@ export default class RoomAccountSettingsHandler extends SettingsHandler { // Special case URL previews if (settingName === "urlPreviewsEnabled") { 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']; } diff --git a/src/settings/handlers/RoomSettingsHandler.js b/src/settings/handlers/RoomSettingsHandler.js index 3aee0dd6eb..cb3e836c7f 100644 --- a/src/settings/handlers/RoomSettingsHandler.js +++ b/src/settings/handlers/RoomSettingsHandler.js @@ -25,6 +25,9 @@ export default class RoomSettingsHandler extends SettingsHandler { // Special case URL previews if (settingName === "urlPreviewsEnabled") { 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']; } diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index 11f9d86816..01c521da0c 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -116,7 +116,7 @@ export async function decryptMegolmKeyFile(data, password) { aesKey, ciphertext, ); - } catch(e) { + } catch (e) { throw friendlyError('subtleCrypto.decrypt failed: ' + e, cryptoFailMsg()); } diff --git a/src/utils/createMatrixClient.js b/src/utils/createMatrixClient.js index 2d294e262b..77b8cdb120 100644 --- a/src/utils/createMatrixClient.js +++ b/src/utils/createMatrixClient.js @@ -23,7 +23,7 @@ const localStorage = window.localStorage; let indexedDB; try { indexedDB = window.indexedDB; -} catch(e) {} +} catch (e) {} /** * Create a new matrix client, with the persistent stores set up appropriately diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js new file mode 100644 index 0000000000..9c08916235 --- /dev/null +++ b/test/i18n-test/languageHandler-test.js @@ -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 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 = 'Click here 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: () => foo })) + .toEqual((You are now ignoring foo)); + }); + + it('variable substitution with plain React component', function() { + const text = 'You are now ignoring %(userId)s'; + expect(languageHandler._t(text, { userId: foo })) + .toEqual((You are now ignoring foo)); + }); + + it('tag substitution with React component', function() { + const text = 'Press to start a chat with someone'; + expect(languageHandler._t(text, {}, { 'StartChatButton': () => foo })) + .toEqual(Press foo to start a chat with someone); + }); +});