From 3afbaf61e7e52052ed9c6f0fd6a75e0025e4e13d Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Mon, 13 Nov 2017 20:19:33 +0100 Subject: [PATCH 01/30] Refactor i18n stuff a bit --- src/components/structures/FilePanel.js | 7 +- src/components/structures/GroupView.js | 10 +- src/components/structures/MyGroups.js | 10 +- src/components/structures/RoomStatusBar.js | 34 ++-- src/components/structures/login/Login.js | 25 +-- .../dialogs/SessionRestoreErrorDialog.js | 7 +- src/components/views/dialogs/SetMxIdDialog.js | 23 ++- src/components/views/login/CaptchaForm.js | 8 +- .../login/InteractiveAuthEntryComponents.js | 12 +- .../views/messages/RoomAvatarEvent.js | 27 ++- .../views/messages/SenderProfile.js | 19 +-- .../views/room_settings/UrlPreviewSettings.js | 13 +- src/components/views/rooms/AuxPanel.js | 16 +- src/components/views/rooms/RoomList.js | 25 ++- src/components/views/rooms/RoomPreviewBar.js | 20 +-- src/components/views/rooms/RoomSettings.js | 14 +- src/languageHandler.js | 154 +++++++++++------- 17 files changed, 225 insertions(+), 199 deletions(-) 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..cba030c1cc 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/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 cad55351d1..03859f522e 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -15,13 +15,12 @@ limitations under the License. */ import React from 'react'; -import { _t, _tJsx } from '../../languageHandler'; +import { _t } from '../../languageHandler'; import sdk from '../../index'; import WhoIsTyping from '../../WhoIsTyping'; import MatrixClientPeg from '../../MatrixClientPeg'; import MemberAvatar from '../views/avatars/MemberAvatar'; -const HIDE_DEBOUNCE_MS = 10000; const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED_LARGE = 2; @@ -272,12 +271,16 @@ module.exports = React.createClass({ { this.props.unsentMessageError }
- { _tJsx("Resend all or cancel all now. You can also select individual messages to resend or cancel.", - [/(.*?)<\/a>/, /(.*?)<\/a>/], - [ - (sub) => { sub }, - (sub) => { sub }, - ], + { + _t("Resend all or cancel all now. " + + "You can also select individual messages to resend or cancel.", + {}, + { + 'resendText': (sub) => + { sub }, + 'cancelText': (sub) => + { sub }, + }, ) }
@@ -322,12 +325,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/login/Login.js b/src/components/structures/login/Login.js index 8ee6eafad4..3b68234abd 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'; @@ -256,17 +256,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 }; } }, ) } ; } @@ -277,7 +279,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; } @@ -307,6 +311,7 @@ module.exports = React.createClass({ { _t('Sorry, this homeserver is using a login which is not recognised ') }({ step }) ); + } } }, 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..53fdee20ff 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/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..de8746230c 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/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.js index 995d5f8531..3a572d0cbe 100644 --- a/src/components/views/messages/RoomAvatarEvent.js +++ b/src/components/views/messages/RoomAvatarEvent.js @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { ContentRepo } from 'matrix-js-sdk'; -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import Modal from '../../../Modal'; import AccessibleButton from '../elements/AccessibleButton'; @@ -67,24 +67,17 @@ module.exports = React.createClass({ 'crop', ); - // it sucks that _tJsx doesn't support normal _t substitutions :(( 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..d5f78fe252 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,19 @@ export default function SenderProfile(props) { : null, ]; - let content = ''; - + 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, - ]); + 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 }); } 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 56ae24e2f8..b9bf997009 100644 --- a/src/components/views/room_settings/UrlPreviewSettings.js +++ b/src/components/views/room_settings/UrlPreviewSettings.js @@ -14,13 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; const React = require('react'); const MatrixClientPeg = require('../../../MatrixClientPeg'); -const sdk = require("../../../index"); -const Modal = require("../../../Modal"); const UserSettingsStore = require('../../../UserSettingsStore'); -import { _t, _tJsx } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; module.exports = React.createClass({ @@ -31,9 +28,6 @@ module.exports = React.createClass({ }, getInitialState: function() { - const cli = MatrixClientPeg.get(); - const roomState = this.props.room.currentState; - const roomPreviewUrls = this.props.room.currentState.getStateEvents('org.matrix.room.preview_urls', ''); const userPreviewUrls = this.props.room.getAccountData("org.matrix.room.preview_urls"); @@ -109,7 +103,6 @@ module.exports = React.createClass({ }, render: function() { - const self = this; const roomState = this.props.room.currentState; const cli = MatrixClientPeg.get(); @@ -133,11 +126,11 @@ module.exports = React.createClass({ let urlPreviewText = null; if (UserSettingsStore.getUrlPreviewsDisabled()) { urlPreviewText = ( - _tJsx("You have disabled URL previews by default.", /(.*?)<\/a>/, (sub)=>{ sub }) + _t("You have disabled URL previews by default.", {}, { 'a': (sub)=>{ sub } }) ); } else { urlPreviewText = ( - _tJsx("You have enabled URL previews by default.", /(.*?)<\/a>/, (sub)=>{ sub }) + _t("You have enabled URL previews by default.", {}, { 'a': (sub)=>{ sub } }) ); } diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index 271b0e48db..b8f31ef896 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -21,9 +21,7 @@ import sdk from '../../../index'; import dis from "../../../dispatcher"; import ObjectUtils from '../../../ObjectUtils'; import AppsDrawer from './AppsDrawer'; -import { _t, _tJsx} from '../../../languageHandler'; -import UserSettingsStore from '../../../UserSettingsStore'; - +import { _t } from '../../../languageHandler'; module.exports = React.createClass({ displayName: 'AuxPanel', @@ -100,13 +98,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/RoomList.js b/src/components/views/rooms/RoomList.js index 1a9fa5d4e9..c1e1ce2cb2 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..fe7948aeb3 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 } @@ -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 c7e839ab40..be5fb0fe2f 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'; @@ -637,9 +637,7 @@ module.exports = React.createClass({ const ColorSettings = sdk.getComponent("room_settings.ColorSettings"); const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings"); const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings"); - const EditableText = sdk.getComponent('elements.EditableText'); const PowerSelector = sdk.getComponent('elements.PowerSelector'); - const Loader = sdk.getComponent("elements.Spinner"); const cli = MatrixClientPeg.get(); const roomState = this.props.room.currentState; @@ -760,7 +758,7 @@ module.exports = React.createClass({ var tagsSection = null; if (canSetTag || self.state.tags) { - var tagsSection = + tagsSection =
{ _t("Tagged as: ") }{ canSetTag ? (tags.map(function(tag, i) { @@ -790,10 +788,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 } }, ) }
; } @@ -940,7 +938,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/languageHandler.js b/src/languageHandler.js index da62bfee56..33ae229185 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -35,12 +35,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 safe_counterpart_translate(...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 @@ -51,11 +48,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("safe_counterpart_translate 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("safe_counterpart_translate called with null interpolation name: " + k); args[1][k] = 'null'; } }); @@ -64,75 +61,112 @@ 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(...) + * 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. * - * @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. - * - * @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 hanle it ourselves so we can replace with React components + 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 = safe_counterpart_translate(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]; + + let wrap = false; // Remember if the output needs to be wrapped later + for (const regexpString in mapping) { + 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]); - if (!match) { - throw new Error(`_tJsx: translator error. expected translation to match regexp: ${patterns[i]}`); + const match = inputText.match(regexp); + if(!match) { + output.push(inputText); // Push back input + continue; // Missing matches is entirely possible, because translation might change things } - 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)); + + let toPush; + // If substitution is a function, call it + if(mapping[regexpString] instanceof Function) { + toPush = mapping[regexpString].apply(null, capturedGroups); + } else { + toPush = mapping[regexpString]; + } + + output.push(toPush); + + // Check if we need to wrap the output into a span at the end + if(typeof toPush === 'object') { + wrap = true; + } + output.push(inputText.substr(match.index + match[0].length)); } - // 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(wrap) { + // 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); + } else { + return output.join(''); + } } // Allow overriding the text displayed when no translation exists From 9cf7e1b4808f144eb3f3c544812702d686c0144d Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Mon, 13 Nov 2017 20:20:14 +0100 Subject: [PATCH 02/30] Validate tag replacements in gen-i18n --- scripts/gen-i18n.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) 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}`); From 672d5080adae87f5269a4fcf2407f513a43852a9 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Mon, 13 Nov 2017 20:20:41 +0100 Subject: [PATCH 03/30] Add unit tests for translation --- test/i18n-test/languageHandler-test.js | 60 ++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 test/i18n-test/languageHandler-test.js diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js new file mode 100644 index 0000000000..2a94768092 --- /dev/null +++ b/test/i18n-test/languageHandler-test.js @@ -0,0 +1,60 @@ +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() { + var 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() { + var 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() { + var 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() { + var 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() { + // Need an extra space at the end because the result of _t() has an extra empty node at the end + var text = 'You are now ignoring %(userId)s '; + expect(JSON.stringify(languageHandler._t(text, { userId: () => foo }))).toBe(JSON.stringify((You are now ignoring foo ))); + }); + + it('tag substitution with React component', function() { + var text = 'Press to start a chat with someone'; + expect(JSON.stringify(languageHandler._t(text, {}, { 'StartChatButton': () => foo }))).toBe(JSON.stringify(Press foo to start a chat with someone)); + + }); +}); From 2acd42e7c55df9941c578558dded05b641fb4916 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Mon, 13 Nov 2017 21:10:08 +0100 Subject: [PATCH 04/30] Make eslint happy --- .../views/messages/SenderProfile.js | 8 ++++-- test/i18n-test/languageHandler-test.js | 25 +++++++++++-------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index d5f78fe252..d64c5fe651 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -52,9 +52,13 @@ export default function SenderProfile(props) { return (
- { content.props.children[0] ? { content.props.children[0] } : '' } + { content.props.children[0] ? + { content.props.children[0] } : '' + } { content.props.children[1] } - { content.props.children[2] ? { content.props.children[2] } : '' } + { content.props.children[2] ? + { content.props.children[2] } : '' + }
); } diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index 2a94768092..f3c2e135d5 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -26,35 +26,38 @@ describe('languageHandler', function() { }); it('handles plurals', function() { - var text = 'and %(count)s others...'; + 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() { - var text = 'You are now ignoring %(userId)s'; + 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() { - var text = 'Press to start a chat with someone'; - expect(languageHandler._t(text, {}, { 'StartChatButton': () => 'foo' })).toBe('Press foo to start a chat with someone'); + 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() { - var text = 'Click here to join the discussion!'; - expect(languageHandler._t(text, {}, { 'a': (sub) => `x${sub}x` })).toBe('xClick herex to join the discussion!'); + 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() { // Need an extra space at the end because the result of _t() has an extra empty node at the end - var text = 'You are now ignoring %(userId)s '; - expect(JSON.stringify(languageHandler._t(text, { userId: () => foo }))).toBe(JSON.stringify((You are now ignoring foo ))); + const text = 'You are now ignoring %(userId)s '; + expect(JSON.stringify(languageHandler._t(text, { userId: () => foo }))) + .toBe(JSON.stringify((You are now ignoring foo ))); }); it('tag substitution with React component', function() { - var text = 'Press to start a chat with someone'; - expect(JSON.stringify(languageHandler._t(text, {}, { 'StartChatButton': () => foo }))).toBe(JSON.stringify(Press foo to start a chat with someone)); - + const text = 'Press to start a chat with someone'; + expect(JSON.stringify(languageHandler._t(text, {}, { 'StartChatButton': () => foo }))) + .toBe(JSON.stringify(Press foo to start a chat with someone)); }); }); From 772550a24a51e7a23492c4fbed81438d14d00ec8 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 14 Nov 2017 19:33:12 +0100 Subject: [PATCH 05/30] Dont't add empty nodes --- src/languageHandler.js | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index 33ae229185..272b0a4848 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -139,24 +139,34 @@ export function replaceByRegexes(text, mapping) { // 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)); - let toPush; - // If substitution is a function, call it - if(mapping[regexpString] instanceof Function) { - toPush = mapping[regexpString].apply(null, capturedGroups); - } else { - toPush = mapping[regexpString]; + const head = inputText.substr(0, match.index); + if (head !== '') { // Don't push empty nodes, they are of no use + output.push(head); } - output.push(toPush); + let replaced; + // If substitution is a function, call it + if(mapping[regexpString] instanceof Function) { + replaced = mapping[regexpString].apply(null, capturedGroups); + } else { + replaced = mapping[regexpString]; + } - // Check if we need to wrap the output into a span at the end - if(typeof toPush === 'object') { + // 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') { wrap = true; } - output.push(inputText.substr(match.index + match[0].length)); + const tail = inputText.substr(match.index + match[0].length); + if (tail !== '') { + output.push(tail); + } } if(wrap) { From cdd03dd1c59e24423c2bd02a2419841ef73ba205 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 14 Nov 2017 19:34:47 +0100 Subject: [PATCH 06/30] Use toEqual instead of toBe --- test/i18n-test/languageHandler-test.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index f3c2e135d5..bdaa431e0a 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -49,15 +49,14 @@ describe('languageHandler', function() { }); it('variable substitution with React component', function() { - // Need an extra space at the end because the result of _t() has an extra empty node at the end - const text = 'You are now ignoring %(userId)s '; - expect(JSON.stringify(languageHandler._t(text, { userId: () => foo }))) - .toBe(JSON.stringify((You are now ignoring foo ))); + 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(JSON.stringify(languageHandler._t(text, {}, { 'StartChatButton': () => foo }))) - .toBe(JSON.stringify(Press foo to start a chat with someone)); + expect(languageHandler._t(text, {}, { 'StartChatButton': () => foo })) + .toEqual(Press foo to start a chat with someone); }); }); From 788be67c75e67f5f69c46e2e645d06b39744a5f5 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 14 Nov 2017 19:55:47 +0100 Subject: [PATCH 07/30] Clarifications --- .../views/messages/SenderProfile.js | 4 +++ src/languageHandler.js | 30 +++++++++++-------- test/i18n-test/languageHandler-test.js | 6 ++++ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index d64c5fe651..2c557bebe2 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -52,6 +52,10 @@ export default function SenderProfile(props) { return (
+ // 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 :-( { content.props.children[0] ? { content.props.children[0] } : '' } diff --git a/src/languageHandler.js b/src/languageHandler.js index 272b0a4848..d6660be283 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -36,7 +36,7 @@ export function _td(s) { } // Wrapper for counterpart's translation function so that it handles nulls and undefineds properly -//Takes the same arguments as counterpart.translate() +// Takes the same arguments as counterpart.translate() function safe_counterpart_translate(...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 @@ -66,14 +66,20 @@ function safe_counterpart_translate(...args) { * @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. + * 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. + * + * 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. * * @return a React component if any non-strings were used in substitutions, otherwise a string */ export function _t(text, variables, tags) { - // Don't do subsitutions in counterpart. We hanle it ourselves so we can replace with React components + // 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) @@ -123,15 +129,18 @@ export function substitute(text, variables, tags) { export function replaceByRegexes(text, mapping) { const output = [text]; - let wrap = false; // Remember if the output needs to be wrapped later + // 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); // 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(regexp); - if(!match) { + if (!match) { output.push(inputText); // Push back input continue; // Missing matches is entirely possible, because translation might change things } @@ -160,7 +169,7 @@ export function replaceByRegexes(text, mapping) { } if(typeof replaced === 'object') { - wrap = true; + shouldWrapInSpan = true; } const tail = inputText.substr(match.index + match[0].length); @@ -169,10 +178,7 @@ export function replaceByRegexes(text, mapping) { } } - if(wrap) { - // 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 . + if(shouldWrapInSpan) { return React.createElement('span', null, ...output); } else { return output.join(''); diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index bdaa431e0a..9c08916235 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -54,6 +54,12 @@ describe('languageHandler', function() { .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 })) From df6d5cc2b4ae11adae1a079ca0ecac996834a802 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 14 Nov 2017 20:09:52 +0100 Subject: [PATCH 08/30] Pass plain components, rather than functions returning them --- src/components/structures/GroupView.js | 2 +- src/components/views/dialogs/SetMxIdDialog.js | 2 +- .../views/login/InteractiveAuthEntryComponents.js | 2 +- src/components/views/rooms/RoomList.js | 6 +++--- src/components/views/rooms/RoomSettings.js | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index cba030c1cc..b137893bde 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -936,7 +936,7 @@ export default React.createClass({ '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!', {}, - { 'br': () =>
}, + { 'br':
}, ) }
; } diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 53fdee20ff..6fc1d77682 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -272,7 +272,7 @@ export default React.createClass({ 'homeserver, or you can pick a different server.', {}, { - 'span': () => { this.props.homeserverUrl }, + 'span': { this.props.homeserverUrl }, 'a': (sub) => { sub }, }, ) } diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index de8746230c..d0b6c8decb 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -374,7 +374,7 @@ export const MsisdnAuthEntry = React.createClass({ return (

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

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

diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index c1e1ce2cb2..ebe0bdb03f 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -490,7 +490,7 @@ module.exports = React.createClass({ { _t( "Press to start a chat with someone", {}, - { 'StartChatButton': () => }, + { 'StartChatButton': }, ) }
; case 'im.vector.fake.recent': @@ -500,8 +500,8 @@ module.exports = React.createClass({ " to browse the directory", {}, { - 'CreateRoomButton': () => , - 'RoomDirectoryButton': () => , + 'CreateRoomButton': , + 'RoomDirectoryButton': , }, ) }
; diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index be5fb0fe2f..d11a45732e 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -938,7 +938,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 = _t("To send events of type , you must be a", {}, { 'eventType': () => { event_type } }); + else label = _t("To send events of type , you must be a", {}, { 'eventType': { event_type } }); return (
{ label } From f8660de52d75bcd9471eb87edf86a8c652d9d1a0 Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 14 Nov 2017 20:13:00 +0100 Subject: [PATCH 09/30] Add note about alternative to opacity --- src/components/views/messages/SenderProfile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index 2c557bebe2..04e7832493 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -56,6 +56,7 @@ export default function SenderProfile(props) { // 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 { content.props.children[0] ? { content.props.children[0] } : '' } From ae252f7e59cdbce5851d43675b7842cf90a40bbd Mon Sep 17 00:00:00 2001 From: Stefan Parviainen Date: Tue, 14 Nov 2017 21:34:20 +0100 Subject: [PATCH 10/30] Log if no match is found --- src/languageHandler.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index d6660be283..7f23f6e4b6 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -37,7 +37,7 @@ export function _td(s) { // Wrapper for counterpart's translation function so that it handles nulls and undefineds properly // Takes the same arguments as counterpart.translate() -function safe_counterpart_translate(...args) { +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 @@ -48,11 +48,11 @@ function safe_counterpart_translate(...args) { if (args[1] && typeof args[1] === 'object') { Object.keys(args[1]).forEach((k) => { if (args[1][k] === undefined) { - console.warn("safe_counterpart_translate 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("safe_counterpart_translate called with null interpolation name: " + k); + console.warn("safeCounterpartTranslate called with null interpolation name: " + k); args[1][k] = 'null'; } }); @@ -83,7 +83,7 @@ export function _t(text, variables, tags) { 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 translated = safe_counterpart_translate(text, args); + const translated = safeCounterpartTranslate(text, args); return substitute(translated, variables, tags); } @@ -142,7 +142,15 @@ export function replaceByRegexes(text, mapping) { const match = inputText.match(regexp); if (!match) { output.push(inputText); // Push back input - continue; // Missing matches is entirely possible, because translation might change things + + // 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(2); From 06b319937f6e3c7878ba05ed5de73d4b66686d5a Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 15 Nov 2017 10:14:16 +0000 Subject: [PATCH 11/30] Fix linting errors. --- src/Tinter.js | 74 ++++++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/src/Tinter.js b/src/Tinter.js index f2a02b6e6d..d0d71bf1c9 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -31,16 +31,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 @@ -152,9 +153,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 +197,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(); @@ -229,18 +234,15 @@ class Tinter { // 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(); @@ -261,9 +263,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] = []; @@ -319,21 +323,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. @@ -355,7 +362,7 @@ 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) { @@ -366,7 +373,7 @@ class Tinter { if (e.stack) { msg += ' | stack: ' + e.stack; } - console.error(e); + console.error(msg); } if (!svgDoc) continue; const tags = svgDoc.getElementsByTagName("*"); @@ -376,8 +383,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, From 56a70f5530bf969222f87cdd8c8cfec7af661efe Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 15 Nov 2017 10:40:07 +0000 Subject: [PATCH 12/30] Add tinting for lowlights. --- src/Tinter.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Tinter.js b/src/Tinter.js index f2a02b6e6d..cbecccf9ab 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -72,6 +72,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 +82,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 +92,7 @@ class Tinter { undefined, undefined, undefined, + undefined, ]; this.cssFixups = [ @@ -223,7 +226,23 @@ 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) { this.theme = theme; // update keyRgb from the current theme CSS itself, if it defines it @@ -252,8 +271,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'); } } From 6c2e9096cd43624c1eac452080a3a0dda202c7e3 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 15 Nov 2017 13:08:24 +0000 Subject: [PATCH 13/30] Tintable SVGs that behave like normal image buttons / links. --- .../views/elements/TintableSvgButton.js | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/components/views/elements/TintableSvgButton.js diff --git a/src/components/views/elements/TintableSvgButton.js b/src/components/views/elements/TintableSvgButton.js new file mode 100644 index 0000000000..df7b7cd844 --- /dev/null +++ b/src/components/views/elements/TintableSvgButton.js @@ -0,0 +1,59 @@ +/* +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, + height: PropTypes.string, + onClick: PropTypes.func, +}; + +TintableSvgButton.defaultProps = { + onClick: function() {}, +}; From 59d405d4fa20bc9063449b1f0fd649709e9a20a5 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 15 Nov 2017 13:24:38 +0000 Subject: [PATCH 14/30] Use TintableSvgButtons for widget menubar icons. --- src/components/views/elements/AppTile.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index aa781c2d62..8b9f1f2cd9 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'; @@ -371,8 +372,8 @@ 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'; + 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,19 @@ export default React.createClass({ { this.formatAppTileName() } { /* Edit widget */ } - { showEditButton && {_t('Edit')} } { /* Delete widget */ } - {_t(deleteWidgetLabel)}
From f4ecc7fa5d2e552fb82fae3839616d3533819aad Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 15 Nov 2017 14:47:20 +0000 Subject: [PATCH 15/30] speculatively fix @lampholder's NPE --- src/Tinter.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Tinter.js b/src/Tinter.js index 4ab48dda38..916c8b18d8 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) { From 750e64deee0fe1e8d681f2160776ce359dcd63ee Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 15 Nov 2017 15:17:21 +0000 Subject: [PATCH 16/30] Pass required props to TintableSvg. --- src/components/views/elements/AppTile.js | 4 ++++ src/components/views/elements/TintableSvgButton.js | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 8b9f1f2cd9..e31b50be37 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -390,6 +390,8 @@ export default React.createClass({ className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding" title={_t('Edit')} onClick={this._onEditClick} + width="10" + height="10" /> } { /* Delete widget */ } @@ -398,6 +400,8 @@ export default React.createClass({ className={deleteClasses} title={_t(deleteWidgetLabel)} onClick={this._onDeleteClick} + width="10" + height="10" />
diff --git a/src/components/views/elements/TintableSvgButton.js b/src/components/views/elements/TintableSvgButton.js index df7b7cd844..9ca2cdcbb4 100644 --- a/src/components/views/elements/TintableSvgButton.js +++ b/src/components/views/elements/TintableSvgButton.js @@ -36,6 +36,8 @@ export default class TintableSvgButton extends React.Component { className={classes}> Date: Wed, 15 Nov 2017 15:56:54 +0000 Subject: [PATCH 17/30] differentiate between state events and message events so that people can't fake state event types and have them rendered. Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/EventTile.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 499d0ec09a..d7d40d1e6e 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; From 022e40a12751728347daef46829990c8f3573c56 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 15 Nov 2017 19:04:49 -0700 Subject: [PATCH 18/30] Use SettingsStore for default theme Signed-off-by: Travis Ralston --- src/components/structures/MatrixChat.js | 2 +- src/settings/SettingsStore.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 37005b0d69..05ed9c95ed 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -891,7 +891,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/settings/SettingsStore.js b/src/settings/SettingsStore.js index b6343c4a96..dde1d3ca15 100644 --- a/src/settings/SettingsStore.js +++ b/src/settings/SettingsStore.js @@ -181,8 +181,8 @@ export default class SettingsStore { /** * 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 From cf8ff6aed3ea690731f0f05f45357948524a9db0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 15 Nov 2017 19:22:30 -0700 Subject: [PATCH 19/30] Validate that URL previews are explicitly enabled/disabled Otherwise `!null` ends up being "true", therefore forcing URL previews on for everyone. Fixes https://github.com/vector-im/riot-web/issues/5607 Signed-off-by: Travis Ralston --- src/settings/handlers/AccountSettingsHandler.js | 3 +++ src/settings/handlers/RoomAccountSettingsHandler.js | 3 +++ src/settings/handlers/RoomSettingsHandler.js | 3 +++ 3 files changed, 9 insertions(+) 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/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']; } From f141ee1944893b1a8bbc23d6d53fe583fa7dc60a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 15 Nov 2017 19:24:32 -0700 Subject: [PATCH 20/30] Use the correct level order when getting arbitrary settings This shouldn't currently be causing problems, but will in teh future. The bug can be exposed by having a setting where the level order is completely reversed, therefore causing LEVEL_ORDER[0] to actually be the most generic, not the most specific. Instead, we'll pull in the setting's level order and fallback to LEVEL_ORDER, therefore requesting the most specific value. Signed-off-by: Travis Ralston --- src/settings/SettingsStore.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js index dde1d3ca15..d93a48005d 100644 --- a/src/settings/SettingsStore.js +++ b/src/settings/SettingsStore.js @@ -176,7 +176,15 @@ 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); } /** From 10a1d9cb29e45bb90b5454ad67a472777ca53030 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 15 Nov 2017 21:16:12 -0700 Subject: [PATCH 21/30] Language is a local setting Fixes https://github.com/vector-im/riot-web/issues/5611 Signed-off-by: Travis Ralston --- src/components/structures/UserSettings.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 692dd4e01d..794c0d5d4d 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -613,8 +613,7 @@ 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); + SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); this.setState({ language: newLang, }); From 5976fb2eed668f556ed98ee1a94a141933e9ea45 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 15 Nov 2017 22:08:13 -0700 Subject: [PATCH 22/30] Treat null/undefined notification settings as "not set" Otherwise we end up lying and saying notifications are disabled, despite the push rules saying otherwise. Part 1 of the fix for: * https://github.com/vector-im/riot-web/issues/5603 * https://github.com/vector-im/riot-web/issues/5606 Signed-off-by: Travis Ralston --- src/settings/handlers/DeviceSettingsHandler.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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]; From fb1f20b7d4d2c856b39e6370a6dad748b6c6e2e3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 15 Nov 2017 22:09:40 -0700 Subject: [PATCH 23/30] Treat the master push rule as authoritative Previously the push rule was ignored, leading to all kinds of interesting issues regarding notifications. This fixes those issues by giving the master push rule the authority it deserves for reasonable defaults. Part 2 of the fix for: * https://github.com/vector-im/riot-web/issues/5603 * https://github.com/vector-im/riot-web/issues/5606 Signed-off-by: Travis Ralston --- .../controllers/NotificationControllers.js | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/settings/controllers/NotificationControllers.js b/src/settings/controllers/NotificationControllers.js index 261e194d74..3831cd6891 100644 --- a/src/settings/controllers/NotificationControllers.js +++ b/src/settings/controllers/NotificationControllers.js @@ -15,12 +15,36 @@ 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) { + console.log(isMasterRuleEnabled()); + return isMasterRuleEnabled(); + } + + return calculatedValue; } onChange(level, roomId, newValue) { @@ -35,15 +59,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; } } From d0a0a9ce7fe7264f08586d7280c100bfc250ceed Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 15 Nov 2017 22:31:16 -0700 Subject: [PATCH 24/30] Fix URL preview string not being translated Signed-off-by: Travis Ralston --- src/components/views/room_settings/UrlPreviewSettings.js | 6 +++--- src/i18n/strings/en_EN.json | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/room_settings/UrlPreviewSettings.js b/src/components/views/room_settings/UrlPreviewSettings.js index 6fb04f3378..7fe4472017 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, _tJsx} from '../../../languageHandler'; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; @@ -63,9 +63,9 @@ 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 = (); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 51f23c0968..ee299eee58 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -210,6 +210,8 @@ "Enable Notifications": "Enable Notifications", "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", "Cannot add any more widgets": "Cannot add any more widgets", "The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.", From 342378f48a1c304dd562f92c4c7b73e644502f98 Mon Sep 17 00:00:00 2001 From: pafcu Date: Thu, 16 Nov 2017 12:19:56 +0100 Subject: [PATCH 25/30] Add space after if --- src/languageHandler.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index 9495cc758e..b9b5371022 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -102,13 +102,13 @@ export function _t(text, variables, tags) { export function substitute(text, variables, tags) { const regexpMapping = {}; - if(variables !== undefined) { + if (variables !== undefined) { for (const variable in variables) { regexpMapping[`%\\(${variable}\\)s`] = variables[variable]; } } - if(tags !== undefined) { + if (tags !== undefined) { for (const tag in tags) { regexpMapping[`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`] = tags[tag]; } @@ -163,7 +163,7 @@ export function replaceByRegexes(text, mapping) { let replaced; // If substitution is a function, call it - if(mapping[regexpString] instanceof Function) { + if (mapping[regexpString] instanceof Function) { replaced = mapping[regexpString].apply(null, capturedGroups); } else { replaced = mapping[regexpString]; @@ -175,7 +175,7 @@ export function replaceByRegexes(text, mapping) { output.push(replaced); } - if(typeof replaced === 'object') { + if (typeof replaced === 'object') { shouldWrapInSpan = true; } @@ -185,7 +185,7 @@ export function replaceByRegexes(text, mapping) { } } - if(shouldWrapInSpan) { + if (shouldWrapInSpan) { return React.createElement('span', null, ...output); } else { return output.join(''); From a80935e181da404e64643d4619d150872da3db75 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Nov 2017 11:45:40 +0000 Subject: [PATCH 26/30] JSX does not do comments in a way one might expect --- src/components/views/messages/SenderProfile.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index 04e7832493..a8116b1e8a 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -50,13 +50,13 @@ export default function SenderProfile(props) { 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 (
- // 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 { content.props.children[0] ? { content.props.children[0] } : '' } From 3e13a919edcd85ef0a7656bcdf1a78eb310a5639 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 16 Nov 2017 13:09:53 +0000 Subject: [PATCH 27/30] remove rogue debug --- src/settings/controllers/NotificationControllers.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/settings/controllers/NotificationControllers.js b/src/settings/controllers/NotificationControllers.js index 3831cd6891..9dcf78e26b 100644 --- a/src/settings/controllers/NotificationControllers.js +++ b/src/settings/controllers/NotificationControllers.js @@ -40,7 +40,6 @@ export class NotificationsEnabledController extends SettingController { if (!Notifier.isPossible()) return false; if (calculatedValue === null) { - console.log(isMasterRuleEnabled()); return isMasterRuleEnabled(); } From c429b13b05688eb0eba54ad6187d9039e5268b48 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Nov 2017 13:18:58 +0000 Subject: [PATCH 28/30] Add eslint rule keyword-spacing Because we follow it almost all of the time. --- .eslintrc.js | 4 ++++ 1 file changed, 4 insertions(+) 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 From dad797d4a201ad1f7d0877c9589e23c588225307 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Nov 2017 13:19:36 +0000 Subject: [PATCH 29/30] Run linting --fix --- src/ComposerHistoryManager.js | 2 +- src/MatrixClientPeg.js | 4 ++-- src/RichText.js | 2 +- src/TextForEvent.js | 4 ++-- src/Tinter.js | 2 +- src/autocomplete/UserProvider.js | 2 +- src/components/structures/MatrixChat.js | 2 +- src/components/structures/RoomView.js | 14 +++++++------- src/components/structures/ScrollPanel.js | 2 +- src/components/structures/TimelinePanel.js | 6 +++--- src/components/structures/UserSettings.js | 2 +- src/components/structures/login/ForgotPassword.js | 2 +- src/components/structures/login/Login.js | 13 ++++++------- src/components/structures/login/Registration.js | 3 +-- src/components/views/dialogs/KeyShareDialog.js | 2 +- src/components/views/elements/AppPermission.js | 6 +++--- src/components/views/elements/AppTile.js | 4 ++-- src/components/views/elements/LanguageDropdown.js | 6 +++--- .../views/elements/MemberEventListSummary.js | 4 ++-- src/components/views/login/PasswordLogin.js | 6 +++--- src/components/views/login/RegistrationForm.js | 2 +- src/components/views/messages/SenderProfile.js | 2 +- src/components/views/rooms/AppsDrawer.js | 4 ++-- src/components/views/rooms/EventTile.js | 2 +- src/components/views/rooms/MemberInfo.js | 2 +- src/components/views/rooms/MessageComposer.js | 4 ++-- src/components/views/rooms/MessageComposerInput.js | 4 ++-- src/components/views/rooms/RoomHeader.js | 2 +- src/components/views/rooms/RoomPreviewBar.js | 4 ++-- src/components/views/rooms/RoomSettings.js | 8 ++++---- src/components/views/rooms/RoomTile.js | 2 +- src/components/views/voip/VideoFeed.js | 2 +- src/utils/MegolmExportEncryption.js | 2 +- src/utils/createMatrixClient.js | 2 +- 34 files changed, 64 insertions(+), 66 deletions(-) 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 5c9b436d45..c7402c15be 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -393,7 +393,7 @@ class Tinter { 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; 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/MatrixChat.js b/src/components/structures/MatrixChat.js index 37005b0d69..32762a081c 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -317,7 +317,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 diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 38a3392e43..1381b4fce0 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -303,7 +303,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") { @@ -713,7 +713,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}); }, @@ -1110,7 +1110,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 @@ -1421,13 +1421,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); } }, @@ -1446,7 +1446,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..d88eb5c7f8 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -612,7 +612,7 @@ module.exports = React.createClass({ }, 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.ACCOUNT, newLang); this.setState({ 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 53f1b0e380..baa2064277 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -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({ @@ -332,7 +332,7 @@ module.exports = React.createClass({ }, _onLanguageChange: function(newLang) { - if(languageHandler.getCurrentLanguage() !== newLang) { + if (languageHandler.getCurrentLanguage() !== newLang) { SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); PlatformPeg.get().reload(); } @@ -393,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/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 e31b50be37..a005406133 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -284,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; @@ -374,7 +374,7 @@ export default React.createClass({ const deleteWidgetLabel = this._deleteWidgetLabel(); let deleteIcon = 'img/cancel_green.svg'; let deleteClasses = 'mx_AppTileMenuBarWidget'; - if(this._canUserModify()) { + if (this._canUserModify()) { deleteIcon = 'img/icon-delete-pink.svg'; deleteClasses += ' mx_AppTileMenuBarWidgetDelete'; } 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/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 nameElem }); } else { // There is nothing to translate here, so call substitute() instead diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 9a3ba5f329..423f345b1d 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -133,7 +133,7 @@ module.exports = React.createClass({ '$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '', }; - if(app.data) { + if (app.data) { Object.keys(app.data).forEach((key) => { 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/EventTile.js b/src/components/views/rooms/EventTile.js index 812d72a26a..51d12c4f76 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -44,7 +44,7 @@ const eventTileTypes = { 'm.room.history_visibility': 'messages.TextualEvent', 'm.room.encryption': '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', }; 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 @@ -696,7 +696,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/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index fe7948aeb3..175a3ea552 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -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}); diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 22550a1b65..4ac2da2030 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -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 = ( 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/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 From 6f8427a5af9a930cb97ea0d4ad64dd7dc0bfe1e1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Nov 2017 15:11:47 +0000 Subject: [PATCH 30/30] Revert rest of https://github.com/matrix-org/matrix-react-sdk/pull/1584 Because apparently the revert did not revert this part --- src/components/views/dialogs/UnknownDeviceDialog.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index cd6e4a3403..34cf544bb7 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -113,11 +113,6 @@ export default React.createClass({ }, render: function() { - if (this.state.devices === null) { - const Spinner = sdk.getComponent("elements.Spinner"); - return ; - } - let warning; if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) { warning = (