mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-18 06:35:35 +08:00
Merge remote-tracking branch 'origin/develop' into dbkr/udd_no_auto_show
This commit is contained in:
commit
196eafdc7f
@ -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
|
||||
|
@ -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}`);
|
||||
|
@ -19,6 +19,10 @@ const DEBUG = 0;
|
||||
|
||||
// utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue]
|
||||
function colorToRgb(color) {
|
||||
if (!color) {
|
||||
return [0, 0, 0];
|
||||
}
|
||||
|
||||
if (color[0] === '#') {
|
||||
color = color.slice(1);
|
||||
if (color.length === 3) {
|
||||
@ -31,13 +35,14 @@ 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]),
|
||||
return [
|
||||
parseInt(match[1]),
|
||||
parseInt(match[2]),
|
||||
parseInt(match[3]) ];
|
||||
parseInt(match[3]),
|
||||
];
|
||||
}
|
||||
}
|
||||
return [0, 0, 0];
|
||||
@ -72,6 +77,7 @@ class Tinter {
|
||||
"#EAF5F0", // Vector Light Green
|
||||
"#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
|
||||
"#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
|
||||
"#000000", // black lowlights of the SVGs (for switching to dark theme)
|
||||
];
|
||||
|
||||
// track the replacement colours actually being used
|
||||
@ -81,6 +87,7 @@ class Tinter {
|
||||
this.keyHex[1],
|
||||
this.keyHex[2],
|
||||
this.keyHex[3],
|
||||
this.keyHex[4],
|
||||
];
|
||||
|
||||
// track the most current tint request inputs (which may differ from the
|
||||
@ -90,6 +97,7 @@ class Tinter {
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
];
|
||||
|
||||
this.cssFixups = [
|
||||
@ -152,9 +160,11 @@ class Tinter {
|
||||
|
||||
this.calcCssFixups();
|
||||
|
||||
if (DEBUG) console.log("Tinter.tint(" + primaryColor + ", " +
|
||||
if (DEBUG) {
|
||||
console.log("Tinter.tint(" + primaryColor + ", " +
|
||||
secondaryColor + ", " +
|
||||
tertiaryColor + ")");
|
||||
}
|
||||
|
||||
if (!primaryColor) {
|
||||
primaryColor = this.keyRgb[0];
|
||||
@ -194,9 +204,11 @@ class Tinter {
|
||||
this.colors[1] = secondaryColor;
|
||||
this.colors[2] = tertiaryColor;
|
||||
|
||||
if (DEBUG) console.log("Tinter.tint final: (" + primaryColor + ", " +
|
||||
if (DEBUG) {
|
||||
console.log("Tinter.tint final: (" + primaryColor + ", " +
|
||||
secondaryColor + ", " +
|
||||
tertiaryColor + ")");
|
||||
}
|
||||
|
||||
// go through manually fixing up the stylesheets.
|
||||
this.applyCssFixups();
|
||||
@ -223,6 +235,22 @@ class Tinter {
|
||||
});
|
||||
}
|
||||
|
||||
tintSvgBlack(blackColor) {
|
||||
this.currentTint[4] = blackColor;
|
||||
|
||||
if (!blackColor) {
|
||||
blackColor = this.colors[4];
|
||||
}
|
||||
if (this.colors[4] === blackColor) {
|
||||
return;
|
||||
}
|
||||
this.colors[4] = blackColor;
|
||||
this.tintables.forEach(function(tintable) {
|
||||
tintable();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setTheme(theme) {
|
||||
console.trace("setTheme " + theme);
|
||||
this.theme = theme;
|
||||
@ -230,18 +258,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();
|
||||
@ -253,8 +278,10 @@ class Tinter {
|
||||
// abuse the tinter to change all the SVG's #fff to #2d2d2d
|
||||
// XXX: obviously this shouldn't be hardcoded here.
|
||||
this.tintSvgWhite('#2d2d2d');
|
||||
this.tintSvgBlack('#dddddd');
|
||||
} else {
|
||||
this.tintSvgWhite('#ffffff');
|
||||
this.tintSvgBlack('#000000');
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,9 +289,11 @@ class Tinter {
|
||||
// cache our fixups
|
||||
if (this.cssFixups[this.theme]) return;
|
||||
|
||||
if (DEBUG) console.debug("calcCssFixups start for " + this.theme + " (checking " +
|
||||
if (DEBUG) {
|
||||
console.debug("calcCssFixups start for " + this.theme + " (checking " +
|
||||
document.styleSheets.length +
|
||||
" stylesheets)");
|
||||
}
|
||||
|
||||
this.cssFixups[this.theme] = [];
|
||||
|
||||
@ -322,21 +351,24 @@ class Tinter {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (DEBUG) console.log("calcCssFixups end (" +
|
||||
if (DEBUG) {
|
||||
console.log("calcCssFixups end (" +
|
||||
this.cssFixups[this.theme].length +
|
||||
" fixups)");
|
||||
}
|
||||
}
|
||||
|
||||
applyCssFixups() {
|
||||
if (DEBUG) console.log("applyCssFixups start (" +
|
||||
if (DEBUG) {
|
||||
console.log("applyCssFixups start (" +
|
||||
this.cssFixups[this.theme].length +
|
||||
" fixups)");
|
||||
}
|
||||
for (let i = 0; i < this.cssFixups[this.theme].length; i++) {
|
||||
const cssFixup = this.cssFixups[this.theme][i];
|
||||
try {
|
||||
cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index];
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
// Firefox Quantum explodes if you manually edit the CSS in the
|
||||
// inspector and then try to do a tint, as apparently all the
|
||||
// fixups are then stale.
|
||||
@ -358,7 +390,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) {
|
||||
@ -369,7 +401,7 @@ class Tinter {
|
||||
if (e.stack) {
|
||||
msg += ' | stack: ' + e.stack;
|
||||
}
|
||||
console.error(e);
|
||||
console.error(msg);
|
||||
}
|
||||
if (!svgDoc) continue;
|
||||
const tags = svgDoc.getElementsByTagName("*");
|
||||
@ -379,8 +411,7 @@ class Tinter {
|
||||
const attr = this.svgAttrs[k];
|
||||
for (let l = 0; l < this.keyHex.length; l++) {
|
||||
if (tag.getAttribute(attr) &&
|
||||
tag.getAttribute(attr).toUpperCase() === this.keyHex[l])
|
||||
{
|
||||
tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
|
||||
fixups.push({
|
||||
node: tag,
|
||||
attr: attr,
|
||||
|
@ -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 <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
||||
<div className="mx_RoomView_empty">
|
||||
{ _tJsx("You must <a>register</a> to use this functionality", /<a>(.*?)<\/a>/, (sub) => <a href="#/register" key="sub">{ sub }</a>) }
|
||||
{ _t("You must <a>register</a> to use this functionality",
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#/register" key="sub">{ sub }</a> })
|
||||
}
|
||||
</div>
|
||||
</div>;
|
||||
} else if (this.noRoom) {
|
||||
|
@ -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.<br />' +
|
||||
'Click here to open settings and give it one!',
|
||||
[/<br \/>/],
|
||||
[(sub) => <br />])
|
||||
}
|
||||
{},
|
||||
{ 'br': <br /> },
|
||||
) }
|
||||
</div>;
|
||||
}
|
||||
const groupDescEditingClasses = classnames({
|
||||
|
@ -888,7 +888,7 @@ module.exports = React.createClass({
|
||||
*/
|
||||
_onSetTheme: function(theme) {
|
||||
if (!theme) {
|
||||
theme = this.props.config.default_theme || 'light';
|
||||
theme = SettingsStore.getValueAt(SettingLevel.DEFAULT, "theme");
|
||||
}
|
||||
|
||||
// look for the stylesheet elements.
|
||||
|
@ -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({
|
||||
<div className="mx_MyGroups_headerCard_header">
|
||||
{ _t('Join an existing community') }
|
||||
</div>
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
'To join an existing community you\'ll have to '+
|
||||
'know its community identifier; this will look '+
|
||||
'something like <i>+example:matrix.org</i>.',
|
||||
/<i>(.*)<\/i>/,
|
||||
(sub) => <i>{ sub }</i>,
|
||||
) }
|
||||
{},
|
||||
{ 'i': (sub) => <i>{ sub }</i> })
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,8 +16,8 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t, _tJsx } from '../../languageHandler';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import { _t } from '../../languageHandler';
|
||||
import sdk from '../../index';
|
||||
import WhoIsTyping from '../../WhoIsTyping';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
@ -25,7 +25,6 @@ import MemberAvatar from '../views/avatars/MemberAvatar';
|
||||
import Resend from '../../Resend';
|
||||
import { showUnknownDeviceDialogForMessages } from '../../cryptodevices';
|
||||
|
||||
const HIDE_DEBOUNCE_MS = 10000;
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
const STATUS_BAR_EXPANDED_LARGE = 2;
|
||||
@ -286,13 +285,13 @@ module.exports = React.createClass({
|
||||
|
||||
if (hasUDE) {
|
||||
title = _t("Message not sent due to unknown devices being present");
|
||||
content = _tJsx(
|
||||
"<a>Show devices</a> or <a>cancel all</a>.",
|
||||
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
|
||||
[
|
||||
(sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
|
||||
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
||||
],
|
||||
content = _t(
|
||||
"<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.",
|
||||
{},
|
||||
{
|
||||
'showDevicesText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
|
||||
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
if (
|
||||
@ -305,14 +304,15 @@ module.exports = React.createClass({
|
||||
} else {
|
||||
title = _t("Some of your messages have not been sent.");
|
||||
}
|
||||
content = _tJsx(
|
||||
"<a>Resend all</a> or <a>cancel all</a> now. "+
|
||||
content = _t("<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
|
||||
"You can also select individual messages to resend or cancel.",
|
||||
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
|
||||
[
|
||||
(sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
|
||||
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
||||
],
|
||||
{},
|
||||
{
|
||||
'resendText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
|
||||
'cancelText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -391,12 +391,15 @@ module.exports = React.createClass({
|
||||
if (this.props.sentMessageAndIsAlone) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_isAlone">
|
||||
{ _tJsx("There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?",
|
||||
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
|
||||
[
|
||||
(sub) => <a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>,
|
||||
(sub) => <a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>,
|
||||
],
|
||||
{ _t("There's no one else here! Would you like to <inviteText>invite others</inviteText> " +
|
||||
"or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||
{},
|
||||
{
|
||||
'inviteText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>,
|
||||
'nowarnText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
|
@ -299,7 +299,7 @@ module.exports = React.createClass({
|
||||
|
||||
// Check if user has previously chosen to hide the app drawer for this
|
||||
// room. If so, do not show apps
|
||||
let hideWidgetDrawer = localStorage.getItem(
|
||||
const hideWidgetDrawer = localStorage.getItem(
|
||||
room.roomId + "_hide_widget_drawer");
|
||||
|
||||
if (hideWidgetDrawer === "true") {
|
||||
@ -704,7 +704,7 @@ module.exports = React.createClass({
|
||||
return;
|
||||
}
|
||||
|
||||
const joinedMembers = room.currentState.getMembers().filter(m => m.membership === "join" || m.membership === "invite");
|
||||
const joinedMembers = room.currentState.getMembers().filter((m) => m.membership === "join" || m.membership === "invite");
|
||||
this.setState({isAlone: joinedMembers.length === 1});
|
||||
},
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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';
|
||||
@ -113,7 +113,7 @@ module.exports = React.createClass({
|
||||
<div className="mx_Login_smallError">
|
||||
{ _t('Please note you are logging into the %(hs)s server, not matrix.org.',
|
||||
{
|
||||
hs: this.props.defaultHsUrl.replace(/^https?:\/\//, '')
|
||||
hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''),
|
||||
})
|
||||
}
|
||||
</div>
|
||||
@ -272,17 +272,19 @@ module.exports = React.createClass({
|
||||
!this.state.enteredHomeserverUrl.startsWith("http"))
|
||||
) {
|
||||
errorText = <span>
|
||||
{ _tJsx("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||
{
|
||||
_t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||
"Either use HTTPS or <a>enable unsafe scripts</a>.",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; },
|
||||
{},
|
||||
{ 'a': (sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; } },
|
||||
) }
|
||||
</span>;
|
||||
} else {
|
||||
errorText = <span>
|
||||
{ _tJsx("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; },
|
||||
{
|
||||
_t("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
|
||||
{},
|
||||
{ 'a': (sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; } },
|
||||
) }
|
||||
</span>;
|
||||
}
|
||||
@ -293,7 +295,7 @@ module.exports = React.createClass({
|
||||
|
||||
componentForStep: function(step) {
|
||||
switch (step) {
|
||||
case 'm.login.password':
|
||||
case 'm.login.password': {
|
||||
const PasswordLogin = sdk.getComponent('login.PasswordLogin');
|
||||
return (
|
||||
<PasswordLogin
|
||||
@ -309,12 +311,14 @@ module.exports = React.createClass({
|
||||
hsUrl={this.state.enteredHomeserverUrl}
|
||||
/>
|
||||
);
|
||||
case 'm.login.cas':
|
||||
}
|
||||
case 'm.login.cas': {
|
||||
const CasLogin = sdk.getComponent('login.CasLogin');
|
||||
return (
|
||||
<CasLogin onSubmit={this.onCasLogin} />
|
||||
);
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
if (!step) {
|
||||
return;
|
||||
}
|
||||
@ -324,6 +328,7 @@ module.exports = React.createClass({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onLanguageChange: function(newLang) {
|
||||
@ -388,8 +393,7 @@ module.exports = React.createClass({
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
if (theme !== "status") {
|
||||
header = <h2>{ _t('Sign in') }</h2>;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (!this.state.errorText) {
|
||||
header = <h2>{ _t('Sign in to get started') }</h2>;
|
||||
}
|
||||
|
@ -399,8 +399,7 @@ module.exports = React.createClass({
|
||||
// FIXME: remove hardcoded Status team tweaks at some point
|
||||
if (theme === 'status' && this.state.errorText) {
|
||||
header = <div className="mx_Login_error">{ this.state.errorText }</div>;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
header = <h2>{ _t('Create an account') }</h2>;
|
||||
if (this.state.errorText) {
|
||||
errorText = <div className="mx_Login_error">{ this.state.errorText }</div>;
|
||||
|
@ -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 = (
|
||||
<p>
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
"Otherwise, <a>click here</a> to send a bug report.",
|
||||
/<a>(.*?)<\/a>/, (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a>,
|
||||
{},
|
||||
{ 'a': (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a> },
|
||||
) }
|
||||
</p>
|
||||
);
|
||||
|
@ -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({
|
||||
</div>
|
||||
{ usernameIndicator }
|
||||
<p>
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
'This will be your account name on the <span></span> ' +
|
||||
'homeserver, or you can pick a <a>different server</a>.',
|
||||
[
|
||||
/<span><\/span>/,
|
||||
/<a>(.*?)<\/a>/,
|
||||
],
|
||||
[
|
||||
(sub) => <span>{ this.props.homeserverUrl }</span>,
|
||||
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
|
||||
],
|
||||
{},
|
||||
{
|
||||
'span': <span>{ this.props.homeserverUrl }</span>,
|
||||
'a': (sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
'If you already have a Matrix account you can <a>log in</a> instead.',
|
||||
/<a>(.*?)<\/a>/,
|
||||
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a>],
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a> },
|
||||
) }
|
||||
</p>
|
||||
{ auth }
|
||||
|
@ -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,23 @@ export default React.createClass({
|
||||
<b>{ this.formatAppTileName() }</b>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ /* Edit widget */ }
|
||||
{ showEditButton && <img
|
||||
{ showEditButton && <TintableSvgButton
|
||||
src="img/edit_green.svg"
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
width="8" height="8"
|
||||
alt={_t('Edit')}
|
||||
title={_t('Edit')}
|
||||
onClick={this._onEditClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{ /* Delete widget */ }
|
||||
<img src={deleteIcon}
|
||||
<TintableSvgButton
|
||||
src={deleteIcon}
|
||||
className={deleteClasses}
|
||||
width="8" height="8"
|
||||
alt={_t(deleteWidgetLabel)}
|
||||
title={_t(deleteWidgetLabel)}
|
||||
onClick={this._onDeleteClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -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 });
|
||||
|
61
src/components/views/elements/TintableSvgButton.js
Normal file
61
src/components/views/elements/TintableSvgButton.js
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TintableSvg from './TintableSvg';
|
||||
|
||||
export default class TintableSvgButton extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
let classes = "mx_TintableSvgButton";
|
||||
if (this.props.className) {
|
||||
classes += " " + this.props.className;
|
||||
}
|
||||
return (
|
||||
<span
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
className={classes}>
|
||||
<TintableSvg
|
||||
src={this.props.src}
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
></TintableSvg>
|
||||
<span
|
||||
title={this.props.title}
|
||||
onClick={this.props.onClick} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TintableSvgButton.propTypes = {
|
||||
src: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
width: PropTypes.string.isRequired,
|
||||
height: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
TintableSvgButton.defaultProps = {
|
||||
onClick: function() {},
|
||||
};
|
@ -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 <a>web browser</a>",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }), warning);
|
||||
{},
|
||||
{ 'a': (sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }}), warning);
|
||||
this.refs.recaptchaContainer.appendChild(warning);
|
||||
} else {
|
||||
const scriptTag = document.createElement('script');
|
||||
|
@ -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 (
|
||||
<div>
|
||||
<p>{ _tJsx("An email has been sent to %(emailAddress)s", /%\(emailAddress\)s/, (sub) => <i>{this.props.inputs.emailAddress}</i>) }</p>
|
||||
<p>{ _t("An email has been sent to %(emailAddress)s",
|
||||
{ emailAddress: (sub) => <i>{ this.props.inputs.emailAddress }</i> },
|
||||
) }
|
||||
</p>
|
||||
<p>{ _t("Please check your email to continue registration.") }</p>
|
||||
</div>
|
||||
);
|
||||
@ -370,7 +373,10 @@ export const MsisdnAuthEntry = React.createClass({
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<p>{ _tJsx("A text message has been sent to %(msisdn)s", /%\(msisdn\)s/, (sub) => <i>{this._msisdn}</i>) }</p>
|
||||
<p>{ _t("A text message has been sent to %(msisdn)s",
|
||||
{ msisdn: <i>this._msisdn</i> },
|
||||
) }
|
||||
</p>
|
||||
<p>{ _t("Please enter the code it contains:") }</p>
|
||||
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
|
@ -146,7 +146,7 @@ class PasswordLogin extends React.Component {
|
||||
onChange={this.onUsernameChanged}
|
||||
placeholder={SdkConfig.get().disable_custom_urls ?
|
||||
_t("Username on %(hs)s", {
|
||||
hs: this.props.hsUrl.replace(/^https?:\/\//, '')
|
||||
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
|
||||
}) : _t("User name")}
|
||||
value={this.state.username}
|
||||
autoFocus
|
||||
|
@ -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 (
|
||||
<div className="mx_RoomAvatarEvent">
|
||||
{ _tJsx('%(senderDisplayName)s changed the room avatar to <img/>',
|
||||
[
|
||||
/%\(senderDisplayName\)s/,
|
||||
/<img\/>/,
|
||||
],
|
||||
[
|
||||
(sub) => senderDisplayName,
|
||||
(sub) =>
|
||||
{ _t('%(senderDisplayName)s changed the room avatar to <img/>',
|
||||
{ senderDisplayName: senderDisplayName },
|
||||
{
|
||||
'img': () =>
|
||||
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
|
||||
onClick={this.onAvatarClick.bind(this, name)}>
|
||||
<BaseAvatar width={14} height={14} url={url}
|
||||
name={name} />
|
||||
<BaseAvatar width={14} height={14} url={url} name={name} />
|
||||
</AccessibleButton>,
|
||||
],
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
@ -19,7 +19,7 @@
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import Flair from '../elements/Flair.js';
|
||||
import { _tJsx } from '../../../languageHandler';
|
||||
import { _t, substitute } from '../../../languageHandler';
|
||||
|
||||
export default function SenderProfile(props) {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
@ -42,22 +42,28 @@ export default function SenderProfile(props) {
|
||||
: null,
|
||||
];
|
||||
|
||||
let content = '';
|
||||
|
||||
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 ? <span className='mx_SenderProfile_aux'>{ p1 }</span> : null,
|
||||
nameElem,
|
||||
p2 ? <span className='mx_SenderProfile_aux'>{ p2 }</span> : 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 });
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="mx_SenderProfile" dir="auto" onClick={props.onClick}>
|
||||
{ content }
|
||||
{ content.props.children[0] ?
|
||||
<span className='mx_SenderProfile_aux'>{ content.props.children[0] }</span> : ''
|
||||
}
|
||||
{ content.props.children[1] }
|
||||
{ content.props.children[2] ?
|
||||
<span className='mx_SenderProfile_aux'>{ content.props.children[2] }</span> : ''
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||
|
||||
const React = require('react');
|
||||
const sdk = require("../../../index");
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
|
||||
|
||||
@ -42,11 +42,11 @@ module.exports = React.createClass({
|
||||
let previewsForAccount = null;
|
||||
if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) {
|
||||
previewsForAccount = (
|
||||
_tJsx("You have <a>enabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{ sub }</a>)
|
||||
_t("You have <a>enabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
||||
);
|
||||
} else {
|
||||
previewsForAccount = (
|
||||
_tJsx("You have <a>disabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{ sub }</a>)
|
||||
_t("You have <a>disabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
||||
);
|
||||
}
|
||||
|
||||
@ -63,14 +63,14 @@ module.exports = React.createClass({
|
||||
</label>
|
||||
);
|
||||
} 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 = (<label>{ _t(str) }</label>);
|
||||
}
|
||||
|
||||
let previewsForRoomAccount = (
|
||||
const previewsForRoomAccount = (
|
||||
<SettingsFlag name="urlPreviewsEnabled"
|
||||
level={SettingLevel.ROOM_ACCOUNT}
|
||||
roomId={this.props.room.roomId}
|
||||
|
@ -21,7 +21,7 @@ import sdk from '../../../index';
|
||||
import dis from "../../../dispatcher";
|
||||
import ObjectUtils from '../../../ObjectUtils';
|
||||
import AppsDrawer from './AppsDrawer';
|
||||
import { _t, _tJsx} from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
@ -99,13 +99,13 @@ module.exports = React.createClass({
|
||||
supportedText = _t(" (unsupported)");
|
||||
} else {
|
||||
joinNode = (<span>
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
||||
[/<voiceText>(.*?)<\/voiceText>/, /<videoText>(.*?)<\/videoText>/],
|
||||
[
|
||||
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>,
|
||||
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>,
|
||||
],
|
||||
{},
|
||||
{
|
||||
'voiceText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>,
|
||||
'videoText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</span>);
|
||||
}
|
||||
|
@ -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',
|
||||
|
||||
'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;
|
||||
|
@ -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 <div className="mx_RoomList_emptySubListTip">
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
"Press <StartChatButton> to start a chat with someone",
|
||||
[/<StartChatButton>/],
|
||||
[
|
||||
(sub) => <StartChatButton size="16" callout={true} />,
|
||||
],
|
||||
{},
|
||||
{ 'StartChatButton': <StartChatButton size="16" callout={true} /> },
|
||||
) }
|
||||
</div>;
|
||||
case 'im.vector.fake.recent':
|
||||
return <div className="mx_RoomList_emptySubListTip">
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or"+
|
||||
" <RoomDirectoryButton> to browse the directory",
|
||||
[/<CreateRoomButton>/, /<RoomDirectoryButton>/],
|
||||
[
|
||||
(sub) => <CreateRoomButton size="16" callout={true} />,
|
||||
(sub) => <RoomDirectoryButton size="16" callout={true} />,
|
||||
],
|
||||
{},
|
||||
{
|
||||
'CreateRoomButton': <CreateRoomButton size="16" callout={true} />,
|
||||
'RoomDirectoryButton': <RoomDirectoryButton size="16" callout={true} />,
|
||||
},
|
||||
) }
|
||||
</div>;
|
||||
}
|
||||
|
@ -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}) }
|
||||
</div>
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
'Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?',
|
||||
[/<acceptText>(.*?)<\/acceptText>/, /<declineText>(.*?)<\/declineText>/],
|
||||
[
|
||||
(sub) => <a onClick={this.props.onJoinClick}>{ sub }</a>,
|
||||
(sub) => <a onClick={this.props.onRejectClick}>{ sub }</a>,
|
||||
],
|
||||
{},
|
||||
{
|
||||
'acceptText': (sub) => <a onClick={this.props.onJoinClick}>{ sub }</a>,
|
||||
'declineText': (sub) => <a onClick={this.props.onRejectClick}>{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</div>
|
||||
{ emailMatchBlock }
|
||||
@ -211,9 +211,9 @@ module.exports = React.createClass({
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
{ name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') }
|
||||
<br />
|
||||
{ _tJsx("<a>Click here</a> to join the discussion!",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => <a onClick={this.props.onJoinClick}><b>{ sub }</b></a>,
|
||||
{ _t("<a>Click here</a> to join the discussion!",
|
||||
{},
|
||||
{ 'a': (sub) => <a onClick={this.props.onJoinClick}><b>{ sub }</b></a> },
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
import { _t, _tJsx, _td } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
@ -309,9 +309,9 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
// url preview settings
|
||||
let ps = this.saveUrlPreviewSettings();
|
||||
const ps = this.saveUrlPreviewSettings();
|
||||
if (ps.length > 0) {
|
||||
ps.map(p => promises.push(p));
|
||||
ps.map((p) => promises.push(p));
|
||||
}
|
||||
|
||||
// related groups
|
||||
@ -584,7 +584,7 @@ module.exports = React.createClass({
|
||||
const roomState = this.props.room.currentState;
|
||||
const isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
|
||||
|
||||
let settings = (
|
||||
const settings = (
|
||||
<SettingsFlag name="blacklistUnverifiedDevices"
|
||||
level={SettingLevel.ROOM_DEVICE}
|
||||
roomId={this.props.room.roomId}
|
||||
@ -628,9 +628,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;
|
||||
@ -749,9 +747,9 @@ module.exports = React.createClass({
|
||||
}
|
||||
});
|
||||
|
||||
var tagsSection = null;
|
||||
let tagsSection = null;
|
||||
if (canSetTag || self.state.tags) {
|
||||
var tagsSection =
|
||||
tagsSection =
|
||||
<div className="mx_RoomSettings_tags">
|
||||
{ _t("Tagged as: ") }{ canSetTag ?
|
||||
(tags.map(function(tag, i) {
|
||||
@ -781,10 +779,10 @@ module.exports = React.createClass({
|
||||
if (this.state.join_rule === "public" && aliasCount == 0) {
|
||||
addressWarning =
|
||||
<div className="mx_RoomSettings_warning">
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
'To link to a room it must have <a>an address</a>.',
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => <a href="#addresses">{ sub }</a>,
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#addresses">{ sub }</a> },
|
||||
) }
|
||||
</div>;
|
||||
}
|
||||
@ -931,7 +929,7 @@ module.exports = React.createClass({
|
||||
{ Object.keys(events_levels).map(function(event_type, i) {
|
||||
let label = plEventsToLabels[event_type];
|
||||
if (label) label = _t(label);
|
||||
else label = _tJsx("To send events of type <eventType/>, you must be a", /<eventType\/>/, () => <code>{ event_type }</code>);
|
||||
else label = _t("To send events of type <eventType/>, you must be a", {}, { 'eventType': <code>{ event_type }</code> });
|
||||
return (
|
||||
<div className="mx_RoomSettings_powerLevel" key={event_type}>
|
||||
<span className="mx_RoomSettings_powerLevelKey">{ label } </span>
|
||||
|
@ -460,6 +460,8 @@
|
||||
"New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)",
|
||||
"You have <a>enabled</a> URL previews by default.": "You have <a>enabled</a> URL previews by default.",
|
||||
"You have <a>disabled</a> URL previews by default.": "You have <a>disabled</a> URL previews by default.",
|
||||
"URL previews are enabled by default for participants in this room.": "URL previews are enabled by default for participants in this room.",
|
||||
"URL previews are disabled by default for participants in this room.": "URL previews are disabled by default for participants in this room.",
|
||||
"URL Previews": "URL Previews",
|
||||
"Error decrypting audio": "Error decrypting audio",
|
||||
"Error decrypting attachment": "Error decrypting attachment",
|
||||
@ -752,16 +754,16 @@
|
||||
"You have no visible notifications": "You have no visible notifications",
|
||||
"Scroll to bottom of page": "Scroll to bottom of page",
|
||||
"Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present",
|
||||
"<a>Show devices</a> or <a>cancel all</a>.": "<a>Show devices</a> or <a>cancel all</a>.",
|
||||
"<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.": "<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.",
|
||||
"Some of your messages have not been sent.": "Some of your messages have not been sent.",
|
||||
"<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.": "<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.",
|
||||
"<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.": "<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.",
|
||||
"Warning": "Warning",
|
||||
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
||||
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
||||
"%(count)s new messages|other": "%(count)s new messages",
|
||||
"%(count)s new messages|one": "%(count)s new message",
|
||||
"Active call": "Active call",
|
||||
"There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?": "There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?",
|
||||
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
||||
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
||||
"Failed to upload file": "Failed to upload file",
|
||||
|
@ -34,12 +34,9 @@ export function _td(s) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// The translation function. This is just a simple wrapper to counterpart,
|
||||
// but exists mostly because we must use the same counterpart instance
|
||||
// between modules (ie. here (react-sdk) and the app (riot-web), and if we
|
||||
// just import counterpart and use it directly, we end up using a different
|
||||
// instance.
|
||||
export function _t(...args) {
|
||||
// Wrapper for counterpart's translation function so that it handles nulls and undefineds properly
|
||||
// Takes the same arguments as counterpart.translate()
|
||||
function safeCounterpartTranslate(...args) {
|
||||
// Horrible hack to avoid https://github.com/vector-im/riot-web/issues/4191
|
||||
// The interpolation library that counterpart uses does not support undefined/null
|
||||
// values and instead will throw an error. This is a problem since everywhere else
|
||||
@ -50,11 +47,11 @@ export function _t(...args) {
|
||||
if (args[1] && typeof args[1] === 'object') {
|
||||
Object.keys(args[1]).forEach((k) => {
|
||||
if (args[1][k] === undefined) {
|
||||
console.warn("_t called with undefined interpolation name: " + k);
|
||||
console.warn("safeCounterpartTranslate called with undefined interpolation name: " + k);
|
||||
args[1][k] = 'undefined';
|
||||
}
|
||||
if (args[1][k] === null) {
|
||||
console.warn("_t called with null interpolation name: " + k);
|
||||
console.warn("safeCounterpartTranslate called with null interpolation name: " + k);
|
||||
args[1][k] = 'null';
|
||||
}
|
||||
});
|
||||
@ -63,75 +60,136 @@ export function _t(...args) {
|
||||
}
|
||||
|
||||
/*
|
||||
* Translates stringified JSX into translated JSX. E.g
|
||||
* _tJsx(
|
||||
* "click <a href=''>here</a> now",
|
||||
* /<a href=''>(.*?)<\/a>/,
|
||||
* (sub) => { return <a href=''>{ sub }</a>; }
|
||||
* );
|
||||
* 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 <a>here</a> now to %(foo)s".
|
||||
* @param {object} variables Variable substitutions, e.g { foo: 'bar' }
|
||||
* @param {object} tags Tag substitutions e.g. { 'a': (sub) => <a>{sub}</a> }
|
||||
*
|
||||
* @param {string} jsxText The untranslated stringified JSX e.g "click <a href=''>here</a> now".
|
||||
* This will be translated by passing the string through to _t(...)
|
||||
* In both variables and tags, the values to substitute with can be either simple strings, React components,
|
||||
* or functions that return the value to use in the substitution (e.g. return a React component). In case of
|
||||
* a tag replacement, the function receives as the argument the text inside the element corresponding to the tag.
|
||||
*
|
||||
* @param {RegExp|RegExp[]} patterns A regexp to match against the translated text.
|
||||
* The captured groups from the regexp will be fed to 'sub'.
|
||||
* Only the captured groups will be included in the output, the match itself is discarded.
|
||||
* If multiple RegExps are provided, the function at the same position will be called. The
|
||||
* match will always be done from left to right, so the 2nd RegExp will be matched against the
|
||||
* remaining text from the first RegExp.
|
||||
* Use tag substitutions if you need to translate text between tags (e.g. "<a>Click here!</a>"), otherwise
|
||||
* you will end up with literal "<a>" in your output, rather than HTML. Note that you can also use variable
|
||||
* substitution to insert React components, but you can't use it to translate text between tags.
|
||||
*
|
||||
* @param {Function|Function[]} subs A function which will be called
|
||||
* with multiple args, each arg representing a captured group of the matching regexp.
|
||||
* This function must return a JSX node.
|
||||
*
|
||||
* @return a React <span> component containing the generated text
|
||||
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
|
||||
*/
|
||||
export function _tJsx(jsxText, patterns, subs) {
|
||||
// convert everything to arrays
|
||||
if (patterns instanceof RegExp) {
|
||||
patterns = [patterns];
|
||||
}
|
||||
if (subs instanceof Function) {
|
||||
subs = [subs];
|
||||
}
|
||||
// sanity checks
|
||||
if (subs.length !== patterns.length || subs.length < 1) {
|
||||
throw new Error(`_tJsx: programmer error. expected number of RegExps == number of Functions: ${subs.length} != ${patterns.length}`);
|
||||
}
|
||||
for (let i = 0; i < subs.length; i++) {
|
||||
if (!(patterns[i] instanceof RegExp)) {
|
||||
throw new Error(`_tJsx: programmer error. expected RegExp for text: ${jsxText}`);
|
||||
}
|
||||
if (!(subs[i] instanceof Function)) {
|
||||
throw new Error(`_tJsx: programmer error. expected Function for text: ${jsxText}`);
|
||||
}
|
||||
}
|
||||
export function _t(text, variables, tags) {
|
||||
// Don't do subsitutions in counterpart. We handle it ourselves so we can replace with React components
|
||||
// However, still pass the variables to counterpart so that it can choose the correct plural if count is given
|
||||
// It is enough to pass the count variable, but in the future counterpart might make use of other information too
|
||||
const args = Object.assign({ interpolate: false }, variables);
|
||||
|
||||
// The translation returns text so there's no XSS vector here (no unsafe HTML, no code execution)
|
||||
const tJsxText = _t(jsxText, {interpolate: false});
|
||||
const output = [tJsxText];
|
||||
const translated = safeCounterpartTranslate(text, args);
|
||||
|
||||
return substitute(translated, variables, tags);
|
||||
}
|
||||
|
||||
/*
|
||||
* Similar to _t(), except only does substitutions, and no translation
|
||||
* @param {string} text The text, e.g "click <a>here</a> now to %(foo)s".
|
||||
* @param {object} variables Variable substitutions, e.g { foo: 'bar' }
|
||||
* @param {object} tags Tag substitutions e.g. { 'a': (sub) => <a>{sub}</a> }
|
||||
*
|
||||
* The values to substitute with can be either simple strings, or functions that return the value to use in
|
||||
* the substitution (e.g. return a React component). In case of a tag replacement, the function receives as
|
||||
* the argument the text inside the element corresponding to the tag.
|
||||
*
|
||||
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
|
||||
*/
|
||||
export function substitute(text, variables, tags) {
|
||||
const regexpMapping = {};
|
||||
|
||||
if (variables !== undefined) {
|
||||
for (const variable in variables) {
|
||||
regexpMapping[`%\\(${variable}\\)s`] = variables[variable];
|
||||
}
|
||||
}
|
||||
|
||||
if (tags !== undefined) {
|
||||
for (const tag in tags) {
|
||||
regexpMapping[`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`] = tags[tag];
|
||||
}
|
||||
}
|
||||
return replaceByRegexes(text, regexpMapping);
|
||||
}
|
||||
|
||||
/*
|
||||
* Replace parts of a text using regular expressions
|
||||
* @param {string} text The text on which to perform substitutions
|
||||
* @param {object} mapping A mapping from regular expressions in string form to replacement string or a
|
||||
* function which will receive as the argument the capture groups defined in the regexp. E.g.
|
||||
* { 'Hello (.?) World': (sub) => sub.toUpperCase() }
|
||||
*
|
||||
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
|
||||
*/
|
||||
export function replaceByRegexes(text, mapping) {
|
||||
const output = [text];
|
||||
|
||||
// If we insert any components we need to wrap the output in a span. React doesn't like just an array of components.
|
||||
let shouldWrapInSpan = false;
|
||||
|
||||
for (const regexpString in mapping) {
|
||||
// TODO: Cache regexps
|
||||
const regexp = new RegExp(regexpString);
|
||||
|
||||
for (let i = 0; i < patterns.length; i++) {
|
||||
// convert the last element in 'output' into 3 elements (pre-text, sub function, post-text).
|
||||
// Rinse and repeat for other patterns (using post-text).
|
||||
const inputText = output.pop();
|
||||
const match = inputText.match(patterns[i]);
|
||||
const match = inputText.match(regexp);
|
||||
if (!match) {
|
||||
throw new Error(`_tJsx: translator error. expected translation to match regexp: ${patterns[i]}`);
|
||||
output.push(inputText); // Push back input
|
||||
|
||||
// Missing matches is entirely possible because you might choose to show some variables only in the case
|
||||
// of e.g. plurals. It's still a bit suspicious, and could be due to an error, so log it.
|
||||
// However, not showing count is so common that it's not worth logging. And other commonly unused variables
|
||||
// here, if there are any.
|
||||
if (regexpString !== '%\\(count\\)s') {
|
||||
console.log(`Could not find ${regexp} in ${inputText}`);
|
||||
}
|
||||
const capturedGroups = match.slice(1);
|
||||
continue;
|
||||
}
|
||||
const capturedGroups = match.slice(2);
|
||||
|
||||
// Return the raw translation before the *match* followed by the return value of sub() followed
|
||||
// by the raw translation after the *match* (not captured group).
|
||||
output.push(inputText.substr(0, match.index));
|
||||
output.push(subs[i].apply(null, capturedGroups));
|
||||
output.push(inputText.substr(match.index + match[0].length));
|
||||
|
||||
const head = inputText.substr(0, match.index);
|
||||
if (head !== '') { // Don't push empty nodes, they are of no use
|
||||
output.push(head);
|
||||
}
|
||||
|
||||
// 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 <span>.
|
||||
let replaced;
|
||||
// If substitution is a function, call it
|
||||
if (mapping[regexpString] instanceof Function) {
|
||||
replaced = mapping[regexpString].apply(null, capturedGroups);
|
||||
} else {
|
||||
replaced = mapping[regexpString];
|
||||
}
|
||||
|
||||
// Here we also need to check that it actually is a string before comparing against one
|
||||
// The head and tail are always strings
|
||||
if (typeof replaced !== 'string' || replaced !== '') {
|
||||
output.push(replaced);
|
||||
}
|
||||
|
||||
if (typeof replaced === 'object') {
|
||||
shouldWrapInSpan = true;
|
||||
}
|
||||
|
||||
const tail = inputText.substr(match.index + match[0].length);
|
||||
if (tail !== '') {
|
||||
output.push(tail);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldWrapInSpan) {
|
||||
return React.createElement('span', null, ...output);
|
||||
} else {
|
||||
return output.join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Allow overriding the text displayed when no translation exists
|
||||
|
@ -176,13 +176,21 @@ export default class SettingsStore {
|
||||
* @return {*} The value, or null if not found
|
||||
*/
|
||||
static getValue(settingName, roomId = null, excludeDefault = false) {
|
||||
return SettingsStore.getValueAt(LEVEL_ORDER[0], settingName, roomId, false, excludeDefault);
|
||||
// Verify that the setting is actually a setting
|
||||
if (!SETTINGS[settingName]) {
|
||||
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
|
||||
}
|
||||
|
||||
const setting = SETTINGS[settingName];
|
||||
const levelOrder = (setting.supportedLevelsAreOrdered ? setting.supportedLevels : LEVEL_ORDER);
|
||||
|
||||
return SettingsStore.getValueAt(levelOrder[0], settingName, roomId, false, excludeDefault);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a setting's value at a particular level, ignoring all levels that are more specific.
|
||||
* @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level to
|
||||
* look at.
|
||||
* @param {"device"|"room-device"|"room-account"|"account"|"room"|"config"|"default"} level The
|
||||
* level to look at.
|
||||
* @param {string} settingName The name of the setting to read.
|
||||
* @param {String} roomId The room ID to read the setting value in, may be null.
|
||||
* @param {boolean} explicit If true, this method will not consider other levels, just the one
|
||||
|
@ -15,12 +15,35 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import SettingController from "./SettingController";
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
|
||||
// XXX: This feels wrong.
|
||||
import PushProcessor from "matrix-js-sdk/lib/pushprocessor";
|
||||
|
||||
function isMasterRuleEnabled() {
|
||||
// Return the value of the master push rule as a default
|
||||
const processor = new PushProcessor(MatrixClientPeg.get());
|
||||
const masterRule = processor.getPushRuleById(".m.rule.master");
|
||||
|
||||
if (!masterRule) {
|
||||
console.warn("No master push rule! Notifications are disabled for this user.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Why enabled == false means "enabled" is beyond me.
|
||||
return !masterRule.enabled;
|
||||
}
|
||||
|
||||
export class NotificationsEnabledController extends SettingController {
|
||||
getValueOverride(level, roomId, calculatedValue) {
|
||||
const Notifier = require('../../Notifier'); // avoids cyclical references
|
||||
if (!Notifier.isPossible()) return false;
|
||||
|
||||
return calculatedValue && Notifier.isPossible();
|
||||
if (calculatedValue === null) {
|
||||
return isMasterRuleEnabled();
|
||||
}
|
||||
|
||||
return calculatedValue;
|
||||
}
|
||||
|
||||
onChange(level, roomId, newValue) {
|
||||
@ -35,15 +58,22 @@ export class NotificationsEnabledController extends SettingController {
|
||||
export class NotificationBodyEnabledController extends SettingController {
|
||||
getValueOverride(level, roomId, calculatedValue) {
|
||||
const Notifier = require('../../Notifier'); // avoids cyclical references
|
||||
if (!Notifier.isPossible()) return false;
|
||||
|
||||
return calculatedValue && Notifier.isEnabled();
|
||||
if (calculatedValue === null) {
|
||||
return isMasterRuleEnabled();
|
||||
}
|
||||
|
||||
return calculatedValue;
|
||||
}
|
||||
}
|
||||
|
||||
export class AudioNotificationsEnabledController extends SettingController {
|
||||
getValueOverride(level, roomId, calculatedValue) {
|
||||
const Notifier = require('../../Notifier'); // avoids cyclical references
|
||||
if (!Notifier.isPossible()) return false;
|
||||
|
||||
return calculatedValue && Notifier.isEnabled();
|
||||
// Note: Audio notifications are *not* enabled by default.
|
||||
return calculatedValue;
|
||||
}
|
||||
}
|
||||
|
@ -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'];
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
|
@ -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'];
|
||||
}
|
||||
|
||||
|
@ -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'];
|
||||
}
|
||||
|
||||
|
68
test/i18n-test/languageHandler-test.js
Normal file
68
test/i18n-test/languageHandler-test.js
Normal file
@ -0,0 +1,68 @@
|
||||
const React = require('react');
|
||||
const expect = require('expect');
|
||||
import * as languageHandler from '../../src/languageHandler';
|
||||
|
||||
const testUtils = require('../test-utils');
|
||||
|
||||
describe('languageHandler', function() {
|
||||
let sandbox;
|
||||
|
||||
beforeEach(function(done) {
|
||||
testUtils.beforeEach(this);
|
||||
sandbox = testUtils.stubClient();
|
||||
|
||||
languageHandler.setLanguage('en').done(done);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('translates a string to german', function() {
|
||||
languageHandler.setLanguage('de').then(function() {
|
||||
const translated = languageHandler._t('Rooms');
|
||||
expect(translated).toBe('Räume');
|
||||
});
|
||||
});
|
||||
|
||||
it('handles plurals', function() {
|
||||
const text = 'and %(count)s others...';
|
||||
expect(languageHandler._t(text, { count: 1 })).toBe('and one other...');
|
||||
expect(languageHandler._t(text, { count: 2 })).toBe('and 2 others...');
|
||||
});
|
||||
|
||||
it('handles simple variable subsitutions', function() {
|
||||
const text = 'You are now ignoring %(userId)s';
|
||||
expect(languageHandler._t(text, { userId: 'foo' })).toBe('You are now ignoring foo');
|
||||
});
|
||||
|
||||
it('handles simple tag substitution', function() {
|
||||
const text = 'Press <StartChatButton> to start a chat with someone';
|
||||
expect(languageHandler._t(text, {}, { 'StartChatButton': () => 'foo' }))
|
||||
.toBe('Press foo to start a chat with someone');
|
||||
});
|
||||
|
||||
it('handles text in tags', function() {
|
||||
const text = '<a>Click here</a> to join the discussion!';
|
||||
expect(languageHandler._t(text, {}, { 'a': (sub) => `x${sub}x` }))
|
||||
.toBe('xClick herex to join the discussion!');
|
||||
});
|
||||
|
||||
it('variable substitution with React component', function() {
|
||||
const text = 'You are now ignoring %(userId)s';
|
||||
expect(languageHandler._t(text, { userId: () => <i>foo</i> }))
|
||||
.toEqual((<span>You are now ignoring <i>foo</i></span>));
|
||||
});
|
||||
|
||||
it('variable substitution with plain React component', function() {
|
||||
const text = 'You are now ignoring %(userId)s';
|
||||
expect(languageHandler._t(text, { userId: <i>foo</i> }))
|
||||
.toEqual((<span>You are now ignoring <i>foo</i></span>));
|
||||
});
|
||||
|
||||
it('tag substitution with React component', function() {
|
||||
const text = 'Press <StartChatButton> to start a chat with someone';
|
||||
expect(languageHandler._t(text, {}, { 'StartChatButton': () => <i>foo</i> }))
|
||||
.toEqual(<span>Press <i>foo</i> to start a chat with someone</span>);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user