Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into webrtc_settings

and i18nize webrtc stufffs

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

# Conflicts:
#	src/components/structures/UserSettings.js
This commit is contained in:
Michael Telatynski 2017-06-01 22:56:31 +01:00
commit 04b86e5d1d
135 changed files with 8102 additions and 1115 deletions

2
.gitignore vendored
View file

@ -10,5 +10,5 @@ npm-debug.log
# test reports created by karma # test reports created by karma
/karma-reports /karma-reports
# ignore auto-generated component index /.idea
/src/component-index.js /src/component-index.js

View file

@ -5,5 +5,6 @@ install:
- npm install - npm install
- (cd node_modules/matrix-js-sdk && npm install) - (cd node_modules/matrix-js-sdk && npm install)
script: script:
- npm run test # don't run the riot tests unless the react-sdk tests pass, otherwise
- ./.travis-test-riot.sh # the output is confusing.
- npm run test && ./.travis-test-riot.sh

View file

@ -24,6 +24,10 @@ In the interim, `vector-im/riot-web` and `matrix-org/matrix-react-sdk` should
be considered as a single project (for instance, matrix-react-sdk bugs be considered as a single project (for instance, matrix-react-sdk bugs
are currently filed against vector-im/riot-web rather than this project). are currently filed against vector-im/riot-web rather than this project).
Translation Status
==================
[![translationsstatus](https://translate.nordgedanken.de/widgets/riot-web/-/multi-auto.svg)](https://translate.nordgedanken.de/engage/riot-web/?utm_source=widget)
Developer Guide Developer Guide
=============== ===============
@ -190,4 +194,3 @@ Alternative instructions:
* Create an index.html file pulling in your compiled javascript and the * Create an index.html file pulling in your compiled javascript and the
CSS bundle from the skin you use. For now, you'll also need to manually CSS bundle from the skin you use. For now, you'll also need to manually
import CSS from any skins that your skin inherts from. import CSS from any skins that your skin inherts from.

View file

@ -55,11 +55,18 @@ module.exports = function (config) {
// some images to reduce noise from the tests // some images to reduce noise from the tests
{pattern: 'test/img/*', watched: false, included: false, {pattern: 'test/img/*', watched: false, included: false,
served: true, nocache: false}, served: true, nocache: false},
// translation files
{pattern: 'src/i18n/strings/*', watcheed: false, included: false, served: true},
{pattern: 'test/i18n/*', watched: false, included: false, served: true},
], ],
// redirect img links to the karma server
proxies: { proxies: {
// redirect img links to the karma server
"/img/": "/base/test/img/", "/img/": "/base/test/img/",
// special languages.json file for the tests
"/i18n/languages.json": "/base/test/i18n/languages.json",
// and redirect i18n requests
"/i18n/": "/base/src/i18n/strings/",
}, },
// list of files to exclude // list of files to exclude
@ -166,11 +173,15 @@ module.exports = function (config) {
'sinon': 'sinon/pkg/sinon.js', 'sinon': 'sinon/pkg/sinon.js',
}, },
root: [ root: [
path.resolve('./src'),
path.resolve('./test'), path.resolve('./test'),
], ],
}, },
devtool: 'inline-source-map', devtool: 'inline-source-map',
externals: {
// Don't try to bundle electron: leave it as a commonjs dependency
// (the 'commonjs' here means it will output a 'require')
"electron": "commonjs electron",
},
}, },
webpackMiddleware: { webpackMiddleware: {

View file

@ -50,6 +50,7 @@
"browser-request": "^0.3.3", "browser-request": "^0.3.3",
"classnames": "^2.1.2", "classnames": "^2.1.2",
"commonmark": "^0.27.0", "commonmark": "^0.27.0",
"counterpart": "^0.18.0",
"draft-js": "^0.8.1", "draft-js": "^0.8.1",
"draft-js-export-html": "^0.5.0", "draft-js-export-html": "^0.5.0",
"draft-js-export-markdown": "^0.2.0", "draft-js-export-markdown": "^0.2.0",
@ -63,7 +64,7 @@
"isomorphic-fetch": "^2.2.1", "isomorphic-fetch": "^2.2.1",
"linkifyjs": "^2.1.3", "linkifyjs": "^2.1.3",
"lodash": "^4.13.1", "lodash": "^4.13.1",
"matrix-js-sdk": "0.7.8", "matrix-js-sdk": "0.7.9",
"optimist": "^0.6.1", "optimist": "^0.6.1",
"q": "^1.4.1", "q": "^1.4.1",
"react": "^15.4.0", "react": "^15.4.0",

192
scripts/check-i18n.pl Executable file
View file

@ -0,0 +1,192 @@
#!/usr/bin/perl
use strict;
use warnings;
use Cwd 'abs_path';
# script which checks how out of sync the i18ns are drifting
# example i18n format:
# "%(oneUser)sleft": "%(oneUser)sleft",
$|=1;
$0 =~ /^(.*\/)/;
my $i18ndir = abs_path($1."/../src/i18n/strings");
my $srcdir = abs_path($1."/../src");
my $en = read_i18n($i18ndir."/en_EN.json");
my $src_strings = read_src_strings($srcdir);
my $src = {};
print "Checking strings in src\n";
foreach my $tuple (@$src_strings) {
my ($s, $file) = (@$tuple);
$src->{$s} = $file;
if (!$en->{$s}) {
if ($en->{$s . '.'}) {
printf ("%50s %24s\t%s\n", $file, "en_EN has fullstop!", $s);
}
else {
$s =~ /^(.*)\.?$/;
if ($en->{$1}) {
printf ("%50s %24s\t%s\n", $file, "en_EN lacks fullstop!", $s);
}
else {
printf ("%50s %24s\t%s\n", $file, "Translation missing!", $s);
}
}
}
}
print "\nChecking en_EN\n";
my $count = 0;
my $remaining_src = {};
foreach (keys %$src) { $remaining_src->{$_}++ };
foreach my $k (sort keys %$en) {
# crappy heuristic to ignore country codes for now...
next if ($k =~ /^(..|..-..)$/);
if ($en->{$k} ne $k) {
printf ("%50s %24s\t%s\n", "en_EN", "en_EN is not symmetrical", $k);
}
if (!$src->{$k}) {
if ($src->{$k. '.'}) {
printf ("%50s %24s\t%s\n", $src->{$k. '.'}, "src has fullstop!", $k);
}
else {
$k =~ /^(.*)\.?$/;
if ($src->{$1}) {
printf ("%50s %24s\t%s\n", $src->{$1}, "src lacks fullstop!", $k);
}
else {
printf ("%50s %24s\t%s\n", '???', "Not present in src?", $k);
}
}
}
else {
$count++;
delete $remaining_src->{$k};
}
}
printf ("$count/" . (scalar keys %$src) . " strings found in src are present in en_EN\n");
foreach (keys %$remaining_src) {
print "missing: $_\n";
}
opendir(DIR, $i18ndir) || die $!;
my @files = readdir(DIR);
closedir(DIR);
foreach my $lang (grep { -f "$i18ndir/$_" && !/(basefile|en_EN)\.json/ } @files) {
print "\nChecking $lang\n";
my $map = read_i18n($i18ndir."/".$lang);
my $count = 0;
my $remaining_en = {};
foreach (keys %$en) { $remaining_en->{$_}++ };
foreach my $k (sort keys %$map) {
{
no warnings 'uninitialized';
my $vars = {};
while ($k =~ /%\((.*?)\)s/g) {
$vars->{$1}++;
}
while ($map->{$k} =~ /%\((.*?)\)s/g) {
$vars->{$1}--;
}
foreach my $var (keys %$vars) {
if ($vars->{$var} != 0) {
printf ("%10s %24s\t%s\n", $lang, "Broken var ($var)s", $k);
}
}
}
if ($en->{$k}) {
if ($map->{$k} eq $k) {
printf ("%10s %24s\t%s\n", $lang, "Untranslated string?", $k);
}
$count++;
delete $remaining_en->{$k};
}
else {
if ($en->{$k . "."}) {
printf ("%10s %24s\t%s\n", $lang, "en_EN has fullstop!", $k);
next;
}
$k =~ /^(.*)\.?$/;
if ($en->{$1}) {
printf ("%10s %24s\t%s\n", $lang, "en_EN lacks fullstop!", $k);
next;
}
printf ("%10s %24s\t%s\n", $lang, "Not present in en_EN", $k);
}
}
if (scalar keys %$remaining_en < 100) {
foreach (keys %$remaining_en) {
printf ("%10s %24s\t%s\n", $lang, "Not yet translated", $_);
}
}
printf ("$count/" . (scalar keys %$en) . " strings translated\n");
}
sub read_i18n {
my $path = shift;
my $map = {};
$path =~ /.*\/(.*)$/;
my $lang = $1;
open(FILE, "<", $path) || die $!;
while(<FILE>) {
if ($_ =~ m/^(\s+)"(.*?)"(: *)"(.*?)"(,?)$/) {
my ($indent, $src, $colon, $dst, $comma) = ($1, $2, $3, $4, $5);
$src =~ s/\\"/"/g;
$dst =~ s/\\"/"/g;
if ($map->{$src}) {
printf ("%10s %24s\t%s\n", $lang, "Duplicate translation!", $src);
}
$map->{$src} = $dst;
}
}
close(FILE);
return $map;
}
sub read_src_strings {
my $path = shift;
use File::Find;
use File::Slurp;
my $strings = [];
my @files;
find( sub { push @files, $File::Find::name if (-f $_ && /\.jsx?$/) }, $path );
foreach my $file (@files) {
my $src = read_file($file);
$src =~ s/'\s*\+\s*'//g;
$src =~ s/"\s*\+\s*"//g;
$file =~ s/^.*\/src/src/;
while ($src =~ /_t(?:Jsx)?\(\s*'(.*?[^\\])'/sg) {
my $s = $1;
$s =~ s/\\'/'/g;
push @$strings, [$s, $file];
}
while ($src =~ /_t(?:Jsx)?\(\s*"(.*?[^\\])"/sg) {
push @$strings, [$1, $file];
}
}
return $strings;
}

104
scripts/fix-i18n.pl Executable file
View file

@ -0,0 +1,104 @@
#!/usr/bin/perl -ni
use strict;
use warnings;
# script which synchronises i18n strings to include punctuation.
# i've cherry-picked ones which seem to have diverged between the different translations
# from TextForEvent, causing missing events all over the place
BEGIN {
$::fixups = [split(/\n/, <<EOT
%(targetName)s accepted the invitation for %(displayName)s.
%(targetName)s accepted an invitation.
%(senderName)s requested a VoIP conference.
%(senderName)s invited %(targetName)s.
%(senderName)s banned %(targetName)s.
%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.
%(senderName)s set their display name to %(displayName)s.
%(senderName)s removed their display name (%(oldDisplayName)s).
%(senderName)s removed their profile picture.
%(senderName)s changed their profile picture.
%(senderName)s set a profile picture.
VoIP conference started.
%(targetName)s joined the room.
VoIP conference finished.
%(targetName)s rejected the invitation.
%(targetName)s left the room.
%(senderName)s unbanned %(targetName)s.
%(senderName)s kicked %(targetName)s.
%(senderName)s withdrew %(targetName)s's inivitation.
%(targetName)s left the room.
%(senderDisplayName)s changed the topic to "%(topic)s".
%(senderDisplayName)s changed the room name to %(roomName)s.
%(senderDisplayName)s sent an image.
%(senderName)s answered the call.
%(senderName)s ended the call.
%(senderName)s placed a %(callType)s call.
%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.
%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).
%(senderName)s changed the power level of %(powerLevelDiffText)s.
For security, this session has been signed out. Please sign in again.
You need to log back in to generate end-to-end encryption keys for this device and submit the public key to your homeserver. This is a once off; sorry for the inconvenience.
A new password must be entered.
Guests can't set avatars. Please register.
Failed to set avatar.
Unable to verify email address.
Guests can't use labs features. Please register.
A new password must be entered.
Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.
Guests cannot join this room even if explicitly invited.
Guest users can't invite users. Please register to invite.
This room is inaccessible to guests. You may be able to join if you register.
delete the alias.
remove %(name)s from the directory.
Conference call failed.
Conference calling is in development and may not be reliable.
Guest users can't create new rooms. Please register to create room and start a chat.
Server may be unavailable, overloaded, or you hit a bug.
Server unavailable, overloaded, or something else went wrong.
You are already in a call.
You cannot place VoIP calls in this browser.
You cannot place a call with yourself.
Your email address does not appear to be associated with a Matrix ID on this Homeserver.
EOT
)];
}
# example i18n format:
# "%(oneUser)sleft": "%(oneUser)sleft",
# script called with the line of the file to be checked
my $sub = 0;
if ($_ =~ m/^(\s+)"(.*?)"(: *)"(.*?)"(,?)$/) {
my ($indent, $src, $colon, $dst, $comma) = ($1, $2, $3, $4, $5);
$src =~ s/\\"/"/g;
$dst =~ s/\\"/"/g;
foreach my $fixup (@{$::fixups}) {
my $dotless_fixup = substr($fixup, 0, -1);
if ($src eq $dotless_fixup) {
print STDERR "fixing up src: $src\n";
$src .= '.';
$sub = 1;
}
if ($src eq $fixup && $dst !~ /\.$/) {
print STDERR "fixing up dst: $dst\n";
$dst .= '.';
$sub = 1;
}
if ($sub) {
$src =~ s/"/\\"/g;
$dst =~ s/"/\\"/g;
print qq($indent"$src"$colon"$dst"$comma\n);
last;
}
}
}
if (!$sub) {
print $_;
}

View file

@ -16,6 +16,7 @@ limitations under the License.
*/ */
var MatrixClientPeg = require("./MatrixClientPeg"); var MatrixClientPeg = require("./MatrixClientPeg");
import { _t } from './languageHandler';
/** /**
* Allows a user to add a third party identifier to their Home Server and, * Allows a user to add a third party identifier to their Home Server and,
@ -44,7 +45,7 @@ class AddThreepid {
return res; return res;
}, function(err) { }, function(err) {
if (err.errcode == 'M_THREEPID_IN_USE') { if (err.errcode == 'M_THREEPID_IN_USE') {
err.message = "This email address is already in use"; err.message = _t('This email address is already in use');
} else if (err.httpStatus) { } else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`; err.message = err.message + ` (Status ${err.httpStatus})`;
} }
@ -69,7 +70,7 @@ class AddThreepid {
return res; return res;
}, function(err) { }, function(err) {
if (err.errcode == 'M_THREEPID_IN_USE') { if (err.errcode == 'M_THREEPID_IN_USE') {
err.message = "This phone number is already in use"; err.message = _t('This phone number is already in use');
} else if (err.httpStatus) { } else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`; err.message = err.message + ` (Status ${err.httpStatus})`;
} }
@ -91,7 +92,7 @@ class AddThreepid {
id_server: identityServerDomain id_server: identityServerDomain
}, this.bind).catch(function(err) { }, this.bind).catch(function(err) {
if (err.httpStatus === 401) { if (err.httpStatus === 401) {
err.message = "Failed to verify email address: make sure you clicked the link in the email"; err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
} }
else if (err.httpStatus) { else if (err.httpStatus) {
err.message += ` (Status ${err.httpStatus})`; err.message += ` (Status ${err.httpStatus})`;

145
src/Analytics.js Normal file
View file

@ -0,0 +1,145 @@
/*
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { getCurrentLanguage } from './languageHandler';
import MatrixClientPeg from './MatrixClientPeg';
import PlatformPeg from './PlatformPeg';
import SdkConfig from './SdkConfig';
function redact(str) {
return str.replace(/#\/(room|user)\/(.+)/, "#/$1/<redacted>");
}
const customVariables = {
'App Platform': 1,
'App Version': 2,
'User Type': 3,
'Chosen Language': 4,
};
class Analytics {
constructor() {
this._paq = null;
this.disabled = true;
this.firstPage = true;
}
/**
* Enable Analytics if initialized but disabled
* otherwise try and initalize, no-op if piwik config missing
*/
enable() {
if (this._paq || this._init()) {
this.disabled = false;
}
}
/**
* Disable Analytics calls, will not fully unload Piwik until a refresh,
* but this is second best, Piwik should not pull anything implicitly.
*/
disable() {
this.disabled = true;
}
_init() {
const config = SdkConfig.get();
if (!config || !config.piwik || !config.piwik.url || !config.piwik.siteId) return;
const url = config.piwik.url;
const siteId = config.piwik.siteId;
const self = this;
window._paq = this._paq = window._paq || [];
this._paq.push(['setTrackerUrl', url+'piwik.php']);
this._paq.push(['setSiteId', siteId]);
this._paq.push(['trackAllContentImpressions']);
this._paq.push(['discardHashTag', false]);
this._paq.push(['enableHeartBeatTimer']);
this._paq.push(['enableLinkTracking', true]);
const platform = PlatformPeg.get();
this._setVisitVariable('App Platform', platform.getHumanReadableName());
platform.getAppVersion().then((version) => {
this._setVisitVariable('App Version', version);
}).catch(() => {
this._setVisitVariable('App Version', 'unknown');
});
this._setVisitVariable('Chosen Language', getCurrentLanguage());
(function() {
const g = document.createElement('script');
const s = document.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=url+'piwik.js';
g.onload = function() {
console.log('Initialised anonymous analytics');
self._paq = window._paq;
};
s.parentNode.insertBefore(g, s);
})();
return true;
}
trackPageChange() {
if (this.disabled) return;
if (this.firstPage) {
// De-duplicate first page
// router seems to hit the fn twice
this.firstPage = false;
return;
}
this._paq.push(['setCustomUrl', redact(window.location.href)]);
this._paq.push(['trackPageView']);
}
trackEvent(category, action, name) {
if (this.disabled) return;
this._paq.push(['trackEvent', category, action, name]);
}
logout() {
if (this.disabled) return;
this._paq.push(['deleteCookies']);
}
login() { // not used currently
const cli = MatrixClientPeg.get();
if (this.disabled || !cli) return;
this._paq.push(['setUserId', `@${cli.getUserIdLocalpart()}:${cli.getDomain()}`]);
}
_setVisitVariable(key, value) {
this._paq.push(['setCustomVariable', customVariables[key], key, value, 'visit']);
}
setGuest(guest) {
if (this.disabled) return;
this._setVisitVariable('User Type', guest ? 'Guest' : 'Logged In');
}
}
if (!global.mxAnalytics) {
global.mxAnalytics = new Analytics();
}
module.exports = global.mxAnalytics;

View file

@ -29,6 +29,11 @@ export default class BasePlatform {
this.errorDidOccur = false; this.errorDidOccur = false;
} }
// Used primarily for Analytics
getHumanReadableName(): string {
return 'Base Platform';
}
setNotificationCount(count: number) { setNotificationCount(count: number) {
this.notificationCount = count; this.notificationCount = count;
} }
@ -66,11 +71,14 @@ export default class BasePlatform {
displayNotification(title: string, msg: string, avatarUrl: string, room: Object) { displayNotification(title: string, msg: string, avatarUrl: string, room: Object) {
} }
loudNotification(ev: Event, room: Object) {
}
/** /**
* Returns a promise that resolves to a string representing * Returns a promise that resolves to a string representing
* the current version of the application. * the current version of the application.
*/ */
getAppVersion() { getAppVersion(): Promise<string> {
throw new Error("getAppVersion not implemented!"); throw new Error("getAppVersion not implemented!");
} }
@ -79,10 +87,12 @@ export default class BasePlatform {
* with getUserMedia, return a string explaining why not. * with getUserMedia, return a string explaining why not.
* Otherwise, return null. * Otherwise, return null.
*/ */
screenCaptureErrorString() { screenCaptureErrorString(): string {
return "Not implemented"; return "Not implemented";
} }
isElectron(): boolean { return false; }
/** /**
* Restarts the application, without neccessarily reloading * Restarts the application, without neccessarily reloading
* any application code * any application code

View file

@ -55,6 +55,7 @@ var MatrixClientPeg = require('./MatrixClientPeg');
var PlatformPeg = require("./PlatformPeg"); var PlatformPeg = require("./PlatformPeg");
var Modal = require('./Modal'); var Modal = require('./Modal');
var sdk = require('./index'); var sdk = require('./index');
import { _t } from './languageHandler';
var Matrix = require("matrix-js-sdk"); var Matrix = require("matrix-js-sdk");
var dis = require("./dispatcher"); var dis = require("./dispatcher");
@ -142,8 +143,8 @@ function _setCallListeners(call) {
play("busyAudio"); play("busyAudio");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Call Timeout", title: _t('Call Timeout'),
description: "The remote side failed to pick up." description: _t('The remote side failed to pick up') + '.',
}); });
} }
else if (oldState === "invite_sent") { else if (oldState === "invite_sent") {
@ -179,7 +180,8 @@ function _setCallState(call, roomId, status) {
} }
dis.dispatch({ dis.dispatch({
action: 'call_state', action: 'call_state',
room_id: roomId room_id: roomId,
state: status,
}); });
} }
@ -203,8 +205,8 @@ function _onAction(payload) {
console.log("Can't capture screen: " + screenCapErrorString); console.log("Can't capture screen: " + screenCapErrorString);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Unable to capture screen", title: _t('Unable to capture screen'),
description: screenCapErrorString description: screenCapErrorString,
}); });
return; return;
} }
@ -223,8 +225,8 @@ function _onAction(payload) {
if (module.exports.getAnyActiveCall()) { if (module.exports.getAnyActiveCall()) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Existing Call", title: _t('Existing Call'),
description: "You are already in a call." description: _t('You are already in a call.'),
}); });
return; // don't allow >1 call to be placed. return; // don't allow >1 call to be placed.
} }
@ -233,8 +235,8 @@ function _onAction(payload) {
if (!MatrixClientPeg.get().supportsVoip()) { if (!MatrixClientPeg.get().supportsVoip()) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "VoIP is unsupported", title: _t('VoIP is unsupported'),
description: "You cannot place VoIP calls in this browser." description: _t('You cannot place VoIP calls in this browser.'),
}); });
return; return;
} }
@ -249,7 +251,7 @@ function _onAction(payload) {
if (members.length <= 1) { if (members.length <= 1) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
description: "You cannot place a call with yourself." description: _t('You cannot place a call with yourself.'),
}); });
return; return;
} }
@ -275,14 +277,14 @@ function _onAction(payload) {
if (!ConferenceHandler) { if (!ConferenceHandler) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
description: "Conference calls are not supported in this client" description: _t('Conference calls are not supported in this client'),
}); });
} }
else if (!MatrixClientPeg.get().supportsVoip()) { else if (!MatrixClientPeg.get().supportsVoip()) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "VoIP is unsupported", title: _t('VoIP is unsupported'),
description: "You cannot place VoIP calls in this browser." description: _t('You cannot place VoIP calls in this browser.'),
}); });
} }
else if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) { else if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) {
@ -294,14 +296,14 @@ function _onAction(payload) {
// Therefore we disable conference calling in E2E rooms. // Therefore we disable conference calling in E2E rooms.
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
description: "Conference calls are not supported in encrypted rooms", description: _t('Conference calls are not supported in encrypted rooms'),
}); });
} }
else { else {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Warning!", title: _t('Warning!'),
description: "Conference calling is in development and may not be reliable.", description: _t('Conference calling is in development and may not be reliable.'),
onFinished: confirm=>{ onFinished: confirm=>{
if (confirm) { if (confirm) {
ConferenceHandler.createNewMatrixCall( ConferenceHandler.createNewMatrixCall(
@ -312,8 +314,8 @@ function _onAction(payload) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Conference call failed: " + err); console.error("Conference call failed: " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to set up conference call", title: _t('Failed to set up conference call'),
description: "Conference call failed. " + ((err && err.message) ? err.message : ""), description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''),
}); });
}); });
} }

View file

@ -21,6 +21,7 @@ var extend = require('./extend');
var dis = require('./dispatcher'); var dis = require('./dispatcher');
var MatrixClientPeg = require('./MatrixClientPeg'); var MatrixClientPeg = require('./MatrixClientPeg');
var sdk = require('./index'); var sdk = require('./index');
import { _t } from './languageHandler';
var Modal = require('./Modal'); var Modal = require('./Modal');
var encrypt = require("browser-encrypt-attachment"); var encrypt = require("browser-encrypt-attachment");
@ -347,14 +348,14 @@ class ContentMessages {
}, function(err) { }, function(err) {
error = err; error = err;
if (!upload.canceled) { if (!upload.canceled) {
var desc = "The file '"+upload.fileName+"' failed to upload."; var desc = _t('The file \'%(fileName)s\' failed to upload', {fileName: upload.fileName}) + '.';
if (err.http_status == 413) { if (err.http_status == 413) {
desc = "The file '"+upload.fileName+"' exceeds this home server's size limit for uploads"; desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName});
} }
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Upload Failed", title: _t('Upload Failed'),
description: desc description: desc,
}); });
} }
}).finally(() => { }).finally(() => {

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,40 +16,89 @@ limitations under the License.
*/ */
'use strict'; 'use strict';
import { _t } from './languageHandler';
var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; function getDaysArray() {
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; return [
_t('Sun'),
_t('Mon'),
_t('Tue'),
_t('Wed'),
_t('Thu'),
_t('Fri'),
_t('Sat'),
];
}
function getMonthsArray() {
return [
_t('Jan'),
_t('Feb'),
_t('Mar'),
_t('Apr'),
_t('May'),
_t('Jun'),
_t('Jul'),
_t('Aug'),
_t('Sep'),
_t('Oct'),
_t('Nov'),
_t('Dec'),
];
}
function pad(n) { function pad(n) {
return (n < 10 ? '0' : '') + n; return (n < 10 ? '0' : '') + n;
} }
function twelveHourTime(date) {
let hours = date.getHours() % 12;
const minutes = pad(date.getMinutes());
const ampm = date.getHours() >= 12 ? 'PM' : 'AM';
hours = pad(hours ? hours : 12);
return `${hours}:${minutes}${ampm}`;
}
module.exports = { module.exports = {
formatDate: function(date) { formatDate: function(date) {
// date.toLocaleTimeString is completely system dependent.
// just go 24h for now
var now = new Date(); var now = new Date();
const days = getDaysArray();
const months = getMonthsArray();
if (date.toDateString() === now.toDateString()) { if (date.toDateString() === now.toDateString()) {
return pad(date.getHours()) + ':' + pad(date.getMinutes()); return this.formatTime(date);
} }
else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
return days[date.getDay()] + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); // TODO: use standard date localize function provided in counterpart
return _t('%(weekDayName)s %(time)s', {weekDayName: days[date.getDay()], time: this.formatTime(date)});
} }
else if (now.getFullYear() === date.getFullYear()) { else if (now.getFullYear() === date.getFullYear()) {
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); // TODO: use standard date localize function provided in counterpart
} return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', {
else { weekDayName: days[date.getDay()],
return this.formatFullDate(date); monthName: months[date.getMonth()],
day: date.getDate(),
time: this.formatTime(date),
});
} }
return this.formatFullDate(date);
}, },
formatFullDate: function(date) { formatFullDate: function(date, showTwelveHour=false) {
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); const days = getDaysArray();
const months = getMonthsArray();
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', {
weekDayName: days[date.getDay()],
monthName: months[date.getMonth()],
day: date.getDate(),
fullYear: date.getFullYear(),
time: showTwelveHour ? twelveHourTime(date) : this.formatTime(date),
});
}, },
formatTime: function(date) { formatTime: function(date, showTwelveHour=false) {
if (showTwelveHour) {
return twelveHourTime(date);
}
return pad(date.getHours()) + ':' + pad(date.getMinutes()); return pad(date.getHours()) + ':' + pad(date.getMinutes());
} },
}; };

View file

@ -360,7 +360,7 @@ export function bodyToHtml(content, highlights, opts) {
'mx_EventTile_bigEmoji': emojiBody, 'mx_EventTile_bigEmoji': emojiBody,
'markdown-body': isHtml, 'markdown-body': isHtml,
}); });
return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} />; return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" />;
} }
export function emojifyText(text) { export function emojifyText(text) {

View file

@ -19,6 +19,7 @@ import q from 'q';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import Analytics from './Analytics';
import Notifier from './Notifier'; import Notifier from './Notifier';
import UserActivity from './UserActivity'; import UserActivity from './UserActivity';
import Presence from './Presence'; import Presence from './Presence';
@ -27,6 +28,7 @@ import DMRoomMap from './utils/DMRoomMap';
import RtsClient from './RtsClient'; import RtsClient from './RtsClient';
import Modal from './Modal'; import Modal from './Modal';
import sdk from './index'; import sdk from './index';
import { _t } from './languageHandler';
/** /**
* Called at startup, to attempt to build a logged-in Matrix session. It tries * Called at startup, to attempt to build a logged-in Matrix session. It tries
@ -229,14 +231,16 @@ function _handleRestoreFailure(e) {
let msg = e.message; let msg = e.message;
if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") { if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") {
msg = "You need to log back in to generate end-to-end encryption keys " msg = _t(
+ "for this device and submit the public key to your homeserver. " 'You need to log back in to generate end-to-end encryption keys'
+ "This is a once off; sorry for the inconvenience."; + ' for this device and submit the public key to your homeserver.'
+ ' This is a once off; sorry for the inconvenience.',
);
_clearLocalStorage(); _clearStorage();
return q.reject(new Error( return q.reject(new Error(
"Unable to restore previous session: " + msg, _t('Unable to restore previous session') + ': ' + msg,
)); ));
} }
@ -254,7 +258,7 @@ function _handleRestoreFailure(e) {
return def.promise.then((success) => { return def.promise.then((success) => {
if (success) { if (success) {
// user clicked continue. // user clicked continue.
_clearLocalStorage(); _clearStorage();
return false; return false;
} }
@ -275,6 +279,8 @@ export function initRtsClient(url) {
export function setLoggedIn(credentials) { export function setLoggedIn(credentials) {
credentials.guest = Boolean(credentials.guest); credentials.guest = Boolean(credentials.guest);
Analytics.setGuest(credentials.guest);
console.log( console.log(
"setLoggedIn: mxid:", credentials.userId, "setLoggedIn: mxid:", credentials.userId,
"deviceId:", credentials.deviceId, "deviceId:", credentials.deviceId,
@ -326,6 +332,10 @@ export function setLoggedIn(credentials) {
} }
// stop any running clients before we create a new one with these new credentials // stop any running clients before we create a new one with these new credentials
//
// XXX: why do we have any running clients here? Maybe on sign-in after
// initial use as a guest? but what about our persistent storage? we need to
// be careful not to leak e2e data created as one user into another session.
stopMatrixClient(); stopMatrixClient();
MatrixClientPeg.replaceUsingCreds(credentials); MatrixClientPeg.replaceUsingCreds(credentials);
@ -396,12 +406,19 @@ export function startMatrixClient() {
* a session has been logged out / ended. * a session has been logged out / ended.
*/ */
export function onLoggedOut() { export function onLoggedOut() {
_clearLocalStorage(); stopMatrixClient(true);
stopMatrixClient();
dis.dispatch({action: 'on_logged_out'}); dis.dispatch({action: 'on_logged_out'});
} }
function _clearLocalStorage() { function _clearStorage() {
Analytics.logout();
const cli = MatrixClientPeg.get();
if (cli) {
// TODO: *really* ought to wait for the promise to complete
cli.clearStores().done();
}
if (!window.localStorage) { if (!window.localStorage) {
return; return;
} }
@ -418,9 +435,13 @@ function _clearLocalStorage() {
} }
/** /**
* Stop all the background processes related to the current client * Stop all the background processes related to the current client.
*
* Optionally clears persistent stores.
*
* @param {boolean} clearStores true to clear the persistent stores.
*/ */
export function stopMatrixClient() { export function stopMatrixClient(clearStores) {
Notifier.stop(); Notifier.stop();
UserActivity.stop(); UserActivity.stop();
Presence.stop(); Presence.stop();
@ -429,7 +450,13 @@ export function stopMatrixClient() {
if (cli) { if (cli) {
cli.stopClient(); cli.stopClient();
cli.removeAllListeners(); cli.removeAllListeners();
cli.store.deleteAllData();
MatrixClientPeg.unset();
} }
if (clearStores) {
// note that we have to do this *after* stopping the client, but
// *before* clearing the MatrixClientPeg.
_clearStorage();
}
MatrixClientPeg.unset();
} }

View file

@ -16,6 +16,7 @@ limitations under the License.
*/ */
import Matrix from "matrix-js-sdk"; import Matrix from "matrix-js-sdk";
import { _t } from "./languageHandler";
import q from 'q'; import q from 'q';
import url from 'url'; import url from 'url';
@ -97,9 +98,9 @@ export default class Login {
}; };
}, (error) => { }, (error) => {
if (error.httpStatus === 403) { if (error.httpStatus === 403) {
error.friendlyText = "Guest access is disabled on this Home Server."; error.friendlyText = _t("Guest access is disabled on this Home Server.");
} else { } else {
error.friendlyText = "Failed to register as guest: " + error.data; error.friendlyText = _t("Failed to register as guest:") + ' ' + error.data;
} }
throw error; throw error;
}); });
@ -158,12 +159,12 @@ export default class Login {
}, function(error) { }, function(error) {
if (error.httpStatus == 400 && loginParams.medium) { if (error.httpStatus == 400 && loginParams.medium) {
error.friendlyText = ( error.friendlyText = (
'This Home Server does not support login using email address.' _t('This Home Server does not support login using email address.')
); );
} }
else if (error.httpStatus === 403) { else if (error.httpStatus === 403) {
error.friendlyText = ( error.friendlyText = (
'Incorrect username and/or password.' _t('Incorrect username and/or password.')
); );
if (self._fallbackHsUrl) { if (self._fallbackHsUrl) {
var fbClient = Matrix.createClient({ var fbClient = Matrix.createClient({
@ -187,7 +188,7 @@ export default class Login {
} }
else { else {
error.friendlyText = ( error.friendlyText = (
'There was a problem logging in. (HTTP ' + error.httpStatus + ")" _t("There was a problem logging in.") + ' (HTTP ' + error.httpStatus + ")"
); );
} }
throw error; throw error;

View file

@ -19,6 +19,7 @@ limitations under the License.
var React = require('react'); var React = require('react');
var ReactDOM = require('react-dom'); var ReactDOM = require('react-dom');
import Analytics from './Analytics';
import sdk from './index'; import sdk from './index';
const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
@ -104,6 +105,9 @@ class ModalManager {
} }
createDialog(Element, props, className) { createDialog(Element, props, className) {
if (props && props.title) {
Analytics.trackEvent('Modal', props.title, 'createDialog');
}
return this.createDialogAsync((cb) => {cb(Element);}, props, className); return this.createDialogAsync((cb) => {cb(Element);}, props, className);
} }

View file

@ -18,9 +18,11 @@ limitations under the License.
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import PlatformPeg from './PlatformPeg'; import PlatformPeg from './PlatformPeg';
import TextForEvent from './TextForEvent'; import TextForEvent from './TextForEvent';
import Analytics from './Analytics';
import Avatar from './Avatar'; import Avatar from './Avatar';
import dis from './dispatcher'; import dis from './dispatcher';
import sdk from './index'; import sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
/* /*
@ -120,6 +122,9 @@ const Notifier = {
setEnabled: function(enable, callback) { setEnabled: function(enable, callback) {
const plaf = PlatformPeg.get(); const plaf = PlatformPeg.get();
if (!plaf) return; if (!plaf) return;
Analytics.trackEvent('Notifier', 'Set Enabled', enable);
// make sure that we persist the current setting audio_enabled setting // make sure that we persist the current setting audio_enabled setting
// before changing anything // before changing anything
if (global.localStorage) { if (global.localStorage) {
@ -134,13 +139,11 @@ const Notifier = {
if (result !== 'granted') { if (result !== 'granted') {
// The permission request was dismissed or denied // The permission request was dismissed or denied
const description = result === 'denied' const description = result === 'denied'
? 'Riot does not have permission to send you notifications' ? _t('Riot does not have permission to send you notifications - please check your browser settings')
+ ' - please check your browser settings' : _t('Riot was not given permission to send notifications - please try again');
: 'Riot was not given permission to send notifications'
+ ' - please try again';
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: 'Unable to enable Notifications', title: _t('Unable to enable Notifications'),
description, description,
}); });
return; return;
@ -200,6 +203,8 @@ const Notifier = {
setToolbarHidden: function(hidden, persistent = true) { setToolbarHidden: function(hidden, persistent = true) {
this.toolbarHidden = hidden; this.toolbarHidden = hidden;
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden);
// XXX: why are we dispatching this here? // XXX: why are we dispatching this here?
// this is nothing to do with notifier_enabled // this is nothing to do with notifier_enabled
dis.dispatch({ dis.dispatch({
@ -245,6 +250,7 @@ const Notifier = {
this._displayPopupNotification(ev, room); this._displayPopupNotification(ev, room);
} }
if (actions.tweaks.sound && this.isAudioEnabled()) { if (actions.tweaks.sound && this.isAudioEnabled()) {
PlatformPeg.get().loudNotification(ev, room);
this._playAudioNotification(ev, room); this._playAudioNotification(ev, room);
} }
} }

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
var Matrix = require("matrix-js-sdk"); var Matrix = require("matrix-js-sdk");
import { _t } from './languageHandler';
/** /**
* Allows a user to reset their password on a homeserver. * Allows a user to reset their password on a homeserver.
@ -53,7 +54,7 @@ class PasswordReset {
return res; return res;
}, function(err) { }, function(err) {
if (err.errcode == 'M_THREEPID_NOT_FOUND') { if (err.errcode == 'M_THREEPID_NOT_FOUND') {
err.message = "This email address was not found"; err.message = _t('This email address was not found');
} else if (err.httpStatus) { } else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`; err.message = err.message + ` (Status ${err.httpStatus})`;
} }
@ -78,10 +79,10 @@ class PasswordReset {
} }
}, this.password).catch(function(err) { }, this.password).catch(function(err) {
if (err.httpStatus === 401) { if (err.httpStatus === 401) {
err.message = "Failed to verify email address: make sure you clicked the link in the email"; err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
} }
else if (err.httpStatus === 404) { else if (err.httpStatus === 404) {
err.message = "Your email address does not appear to be associated with a Matrix ID on this Homeserver."; err.message = _t('Your email address does not appear to be associated with a Matrix ID on this Homeserver.');
} }
else if (err.httpStatus) { else if (err.httpStatus) {
err.message += ` (Status ${err.httpStatus})`; err.message += ` (Status ${err.httpStatus})`;

View file

@ -13,14 +13,19 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export const LEVEL_ROLE_MAP = { import { _t } from './languageHandler';
undefined: 'Default',
0: 'User', export function levelRoleMap() {
50: 'Moderator', return {
100: 'Admin', undefined: _t('Default'),
}; 0: _t('User'),
50: _t('Moderator'),
100: _t('Admin'),
};
}
export function textualPowerLevel(level, userDefault) { export function textualPowerLevel(level, userDefault) {
const LEVEL_ROLE_MAP = this.levelRoleMap();
if (LEVEL_ROLE_MAP[level]) { if (LEVEL_ROLE_MAP[level]) {
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`); return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`);
} else { } else {

View file

@ -125,6 +125,7 @@ const SdkConfig = require('./SdkConfig');
const MatrixClientPeg = require("./MatrixClientPeg"); const MatrixClientPeg = require("./MatrixClientPeg");
const MatrixEvent = require("matrix-js-sdk").MatrixEvent; const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
const dis = require("./dispatcher"); const dis = require("./dispatcher");
import { _t } from './languageHandler';
function sendResponse(event, res) { function sendResponse(event, res) {
const data = JSON.parse(JSON.stringify(event.data)); const data = JSON.parse(JSON.stringify(event.data));
@ -150,7 +151,7 @@ function inviteUser(event, roomId, userId) {
console.log(`Received request to invite ${userId} into room ${roomId}`); console.log(`Received request to invite ${userId} into room ${roomId}`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, "You need to be logged in."); sendError(event, _t('You need to be logged in.'));
return; return;
} }
const room = client.getRoom(roomId); const room = client.getRoom(roomId);
@ -170,7 +171,7 @@ function inviteUser(event, roomId, userId) {
success: true, success: true,
}); });
}, function(err) { }, function(err) {
sendError(event, "You need to be able to invite users to do that.", err); sendError(event, _t('You need to be able to invite users to do that.'), err);
}); });
} }
@ -181,7 +182,7 @@ function setPlumbingState(event, roomId, status) {
console.log(`Received request to set plumbing state to status "${status}" in room ${roomId}`); console.log(`Received request to set plumbing state to status "${status}" in room ${roomId}`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, "You need to be logged in."); sendError(event, _t('You need to be logged in.'));
return; return;
} }
client.sendStateEvent(roomId, "m.room.plumbing", { status : status }).done(() => { client.sendStateEvent(roomId, "m.room.plumbing", { status : status }).done(() => {
@ -189,7 +190,7 @@ function setPlumbingState(event, roomId, status) {
success: true, success: true,
}); });
}, (err) => { }, (err) => {
sendError(event, err.message ? err.message : "Failed to send request.", err); sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
}); });
} }
@ -197,7 +198,7 @@ function setBotOptions(event, roomId, userId) {
console.log(`Received request to set options for bot ${userId} in room ${roomId}`); console.log(`Received request to set options for bot ${userId} in room ${roomId}`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, "You need to be logged in."); sendError(event, _t('You need to be logged in.'));
return; return;
} }
client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => { client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => {
@ -205,20 +206,20 @@ function setBotOptions(event, roomId, userId) {
success: true, success: true,
}); });
}, (err) => { }, (err) => {
sendError(event, err.message ? err.message : "Failed to send request.", err); sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
}); });
} }
function setBotPower(event, roomId, userId, level) { function setBotPower(event, roomId, userId, level) {
if (!(Number.isInteger(level) && level >= 0)) { if (!(Number.isInteger(level) && level >= 0)) {
sendError(event, "Power level must be positive integer."); sendError(event, _t('Power level must be positive integer.'));
return; return;
} }
console.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`); console.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, "You need to be logged in."); sendError(event, _t('You need to be logged in.'));
return; return;
} }
@ -235,7 +236,7 @@ function setBotPower(event, roomId, userId, level) {
success: true, success: true,
}); });
}, (err) => { }, (err) => {
sendError(event, err.message ? err.message : "Failed to send request.", err); sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
}); });
}); });
} }
@ -258,12 +259,12 @@ function botOptions(event, roomId, userId) {
function returnStateEvent(event, roomId, eventType, stateKey) { function returnStateEvent(event, roomId, eventType, stateKey) {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, "You need to be logged in."); sendError(event, _t('You need to be logged in.'));
return; return;
} }
const room = client.getRoom(roomId); const room = client.getRoom(roomId);
if (!room) { if (!room) {
sendError(event, "This room is not recognised."); sendError(event, _t('This room is not recognised.'));
return; return;
} }
const stateEvent = room.currentState.getStateEvents(eventType, stateKey); const stateEvent = room.currentState.getStateEvents(eventType, stateKey);
@ -313,13 +314,13 @@ const onMessage = function(event) {
const roomId = event.data.room_id; const roomId = event.data.room_id;
const userId = event.data.user_id; const userId = event.data.user_id;
if (!roomId) { if (!roomId) {
sendError(event, "Missing room_id in request"); sendError(event, _t('Missing room_id in request'));
return; return;
} }
let promise = Promise.resolve(currentRoomId); let promise = Promise.resolve(currentRoomId);
if (!currentRoomId) { if (!currentRoomId) {
if (!currentRoomAlias) { if (!currentRoomAlias) {
sendError(event, "Must be viewing a room"); sendError(event, _t('Must be viewing a room'));
return; return;
} }
// no room ID but there is an alias, look it up. // no room ID but there is an alias, look it up.
@ -331,7 +332,7 @@ const onMessage = function(event) {
promise.then((viewingRoomId) => { promise.then((viewingRoomId) => {
if (roomId !== viewingRoomId) { if (roomId !== viewingRoomId) {
sendError(event, "Room " + roomId + " not visible"); sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId}));
return; return;
} }
@ -345,7 +346,7 @@ const onMessage = function(event) {
} }
if (!userId) { if (!userId) {
sendError(event, "Missing user_id in request"); sendError(event, _t('Missing user_id in request'));
return; return;
} }
switch (event.data.action) { switch (event.data.action) {
@ -370,7 +371,7 @@ const onMessage = function(event) {
} }
}, (err) => { }, (err) => {
console.error(err); console.error(err);
sendError(event, "Failed to lookup current room."); sendError(event, _t('Failed to lookup current room') + '.');
}); });
}; };

View file

@ -23,22 +23,28 @@ class Skinner {
if (this.components === null) { if (this.components === null) {
throw new Error( throw new Error(
"Attempted to get a component before a skin has been loaded."+ "Attempted to get a component before a skin has been loaded."+
"This is probably because either:"+ " This is probably because either:"+
" a) Your app has not called sdk.loadSkin(), or"+ " a) Your app has not called sdk.loadSkin(), or"+
" b) A component has called getComponent at the root level" " b) A component has called getComponent at the root level",
); );
} }
var comp = this.components[name]; let comp = this.components[name];
if (comp) {
return comp;
}
// XXX: Temporarily also try 'views.' as we're currently // XXX: Temporarily also try 'views.' as we're currently
// leaving the 'views.' off views. // leaving the 'views.' off views.
var comp = this.components['views.'+name]; if (!comp) {
if (comp) { comp = this.components['views.'+name];
return comp;
} }
throw new Error("No such component: "+name);
if (!comp) {
throw new Error("No such component: "+name);
}
// components have to be functions.
const validType = typeof comp === 'function';
if (!validType) {
throw new Error(`Not a valid component: ${name}.`);
}
return comp;
} }
load(skinObject) { load(skinObject) {

View file

@ -18,6 +18,7 @@ import MatrixClientPeg from "./MatrixClientPeg";
import dis from "./dispatcher"; import dis from "./dispatcher";
import Tinter from "./Tinter"; import Tinter from "./Tinter";
import sdk from './index'; import sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
@ -41,7 +42,7 @@ class Command {
} }
getUsage() { getUsage() {
return "Usage: " + this.getCommandWithArgs(); return _t('Usage') + ': ' + this.getCommandWithArgs();
} }
} }
@ -68,8 +69,8 @@ const commands = {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
// TODO Don't explain this away, actually show a search UI here. // TODO Don't explain this away, actually show a search UI here.
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "/ddg is not a command", title: _t('/ddg is not a command'),
description: "To use it, just wait for autocomplete results to load and tab through them.", description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
}); });
return success(); return success();
}), }),
@ -143,7 +144,7 @@ const commands = {
dis.dispatch({ dis.dispatch({
action: 'view_room', action: 'view_room',
roomAlias: roomAlias, room_alias: roomAlias,
auto_join: true, auto_join: true,
}); });
@ -185,7 +186,7 @@ const commands = {
if (targetRoomId) { break; } if (targetRoomId) { break; }
} }
if (!targetRoomId) { if (!targetRoomId) {
return reject("Unrecognised room alias: " + roomAlias); return reject(_t("Unrecognised room alias:") + ' ' + roomAlias);
} }
} }
} }
@ -302,14 +303,14 @@ const commands = {
const device = MatrixClientPeg.get().getStoredDevice(userId, deviceId); const device = MatrixClientPeg.get().getStoredDevice(userId, deviceId);
if (!device) { if (!device) {
return reject(`Unknown (user, device) pair: (${userId}, ${deviceId})`); return reject(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`);
} }
if (device.isVerified()) { if (device.isVerified()) {
if (device.getFingerprint() === fingerprint) { if (device.getFingerprint() === fingerprint) {
return reject(`Device already verified!`); return reject(_t(`Device already verified!`));
} else { } else {
return reject(`WARNING: Device already verified, but keys do NOT MATCH!`); return reject(_t(`WARNING: Device already verified, but keys do NOT MATCH!`));
} }
} }
@ -321,12 +322,15 @@ const commands = {
// Tell the user we verified everything! // Tell the user we verified everything!
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Verified key", title: _t("Verified key"),
description: ( description: (
<div> <div>
<p> <p>
The signing key you provided matches the signing key you received {
from { userId }'s device { deviceId }. Device marked as verified. _t("The signing key you provided matches the signing key you received " +
"from %(userId)s's device %(deviceId)s. Device marked as verified.",
{userId: userId, deviceId: deviceId})
}
</p> </p>
</div> </div>
), ),
@ -335,9 +339,13 @@ const commands = {
return success(); return success();
} else { } else {
return reject(`WARNING: KEY VERIFICATION FAILED! The signing key for ${userId} and device const fprint = device.getFingerprint();
${deviceId} is "${device.getFingerprint()}" which does not match the provided key return reject(
"${fingerprint}". This could mean your communications are being intercepted!`); _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
' %(deviceId)s is "%(fprint)s" which does not match the provided key' +
' "%(fingerprint)s". This could mean your communications are being intercepted!',
{deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint})
);
} }
} }
} }
@ -382,7 +390,7 @@ module.exports = {
if (commands[cmd]) { if (commands[cmd]) {
return commands[cmd].run(roomId, args); return commands[cmd].run(roomId, args);
} else { } else {
return reject("Unrecognised command: " + input); return reject(_t("Unrecognised command:") + ' ' + input);
} }
} }
return null; // not a command return null; // not a command

View file

@ -16,7 +16,7 @@ limitations under the License.
var MatrixClientPeg = require("./MatrixClientPeg"); var MatrixClientPeg = require("./MatrixClientPeg");
var CallHandler = require("./CallHandler"); var CallHandler = require("./CallHandler");
import { _t } from './languageHandler';
import * as Roles from './Roles'; import * as Roles from './Roles';
function textForMemberEvent(ev) { function textForMemberEvent(ev) {
@ -25,45 +25,45 @@ function textForMemberEvent(ev) {
var targetName = ev.target ? ev.target.name : ev.getStateKey(); var targetName = ev.target ? ev.target.name : ev.getStateKey();
var ConferenceHandler = CallHandler.getConferenceHandler(); var ConferenceHandler = CallHandler.getConferenceHandler();
var reason = ev.getContent().reason ? ( var reason = ev.getContent().reason ? (
" Reason: " + ev.getContent().reason _t('Reason') + ': ' + ev.getContent().reason
) : ""; ) : "";
switch (ev.getContent().membership) { switch (ev.getContent().membership) {
case 'invite': case 'invite':
var threePidContent = ev.getContent().third_party_invite; var threePidContent = ev.getContent().third_party_invite;
if (threePidContent) { if (threePidContent) {
if (threePidContent.display_name) { if (threePidContent.display_name) {
return targetName + " accepted the invitation for " + return _t('%(targetName)s accepted the invitation for %(displayName)s.', {targetName: targetName, displayName: threePidContent.display_name});
threePidContent.display_name + ".";
} else { } else {
return targetName + " accepted an invitation."; return _t('%(targetName)s accepted an invitation.', {targetName: targetName});
} }
} }
else { else {
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
return senderName + " requested a VoIP conference"; return _t('%(senderName)s requested a VoIP conference.', {senderName: senderName});
} }
else { else {
return senderName + " invited " + targetName + "."; return _t('%(senderName)s invited %(targetName)s.', {senderName: senderName, targetName: targetName});
} }
} }
case 'ban': case 'ban':
return senderName + " banned " + targetName + "." + reason; return _t(
'%(senderName)s banned %(targetName)s.',
{senderName: senderName, targetName: targetName}
) + ' ' + reason;
case 'join': case 'join':
if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') { if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') {
if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) { if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) {
return ev.getSender() + " changed their display name from " + return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname, displayName: ev.getContent().displayname});
ev.getPrevContent().displayname + " to " +
ev.getContent().displayname;
} else if (!ev.getPrevContent().displayname && ev.getContent().displayname) { } else if (!ev.getPrevContent().displayname && ev.getContent().displayname) {
return ev.getSender() + " set their display name to " + ev.getContent().displayname; return _t('%(senderName)s set their display name to %(displayName)s.', {senderName: ev.getSender(), displayName: ev.getContent().displayname});
} else if (ev.getPrevContent().displayname && !ev.getContent().displayname) { } else if (ev.getPrevContent().displayname && !ev.getContent().displayname) {
return ev.getSender() + " removed their display name (" + ev.getPrevContent().displayname + ")"; return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname});
} else if (ev.getPrevContent().avatar_url && !ev.getContent().avatar_url) { } else if (ev.getPrevContent().avatar_url && !ev.getContent().avatar_url) {
return senderName + " removed their profile picture"; return _t('%(senderName)s removed their profile picture.', {senderName: senderName});
} else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) { } else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) {
return senderName + " changed their profile picture"; return _t('%(senderName)s changed their profile picture.', {senderName: senderName});
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) { } else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
return senderName + " set a profile picture"; return _t('%(senderName)s set a profile picture.', {senderName: senderName});
} else { } else {
// suppress null rejoins // suppress null rejoins
return ''; return '';
@ -71,49 +71,57 @@ function textForMemberEvent(ev) {
} else { } else {
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key); if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
return "VoIP conference started"; return _t('VoIP conference started.');
} }
else { else {
return targetName + " joined the room."; return _t('%(targetName)s joined the room.', {targetName: targetName});
} }
} }
case 'leave': case 'leave':
if (ev.getSender() === ev.getStateKey()) { if (ev.getSender() === ev.getStateKey()) {
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
return "VoIP conference finished"; return _t('VoIP conference finished.');
} }
else if (ev.getPrevContent().membership === "invite") { else if (ev.getPrevContent().membership === "invite") {
return targetName + " rejected the invitation."; return _t('%(targetName)s rejected the invitation.', {targetName: targetName});
} }
else { else {
return targetName + " left the room."; return _t('%(targetName)s left the room.', {targetName: targetName});
} }
} }
else if (ev.getPrevContent().membership === "ban") { else if (ev.getPrevContent().membership === "ban") {
return senderName + " unbanned " + targetName + "."; return _t('%(senderName)s unbanned %(targetName)s.', {senderName: senderName, targetName: targetName});
} }
else if (ev.getPrevContent().membership === "join") { else if (ev.getPrevContent().membership === "join") {
return senderName + " kicked " + targetName + "." + reason; return _t(
'%(senderName)s kicked %(targetName)s.',
{senderName: senderName, targetName: targetName}
) + ' ' + reason;
} }
else if (ev.getPrevContent().membership === "invite") { else if (ev.getPrevContent().membership === "invite") {
return senderName + " withdrew " + targetName + "'s invitation." + reason; return _t(
'%(senderName)s withdrew %(targetName)s\'s invitation.',
{senderName: senderName, targetName: targetName}
) + ' ' + reason;
} }
else { else {
return targetName + " left the room."; return _t('%(targetName)s left the room.', {targetName: targetName});
} }
} }
} }
function textForTopicEvent(ev) { function textForTopicEvent(ev) {
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {senderDisplayName: senderDisplayName, topic: ev.getContent().topic});
return senderDisplayName + ' changed the topic to "' + ev.getContent().topic + '"';
} }
function textForRoomNameEvent(ev) { function textForRoomNameEvent(ev) {
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return senderDisplayName + ' changed the room name to "' + ev.getContent().name + '"'; if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName: senderDisplayName});
}
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {senderDisplayName: senderDisplayName, roomName: ev.getContent().name});
} }
function textForMessageEvent(ev) { function textForMessageEvent(ev) {
@ -122,66 +130,66 @@ function textForMessageEvent(ev) {
if (ev.getContent().msgtype === "m.emote") { if (ev.getContent().msgtype === "m.emote") {
message = "* " + senderDisplayName + " " + message; message = "* " + senderDisplayName + " " + message;
} else if (ev.getContent().msgtype === "m.image") { } else if (ev.getContent().msgtype === "m.image") {
message = senderDisplayName + " sent an image."; message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName: senderDisplayName});
} }
return message; return message;
} }
function textForCallAnswerEvent(event) { function textForCallAnswerEvent(event) {
var senderName = event.sender ? event.sender.name : "Someone"; var senderName = event.sender ? event.sender.name : _t('Someone');
var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)"; var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
return senderName + " answered the call." + supported; return _t('%(senderName)s answered the call.', {senderName: senderName}) + ' ' + supported;
} }
function textForCallHangupEvent(event) { function textForCallHangupEvent(event) {
var senderName = event.sender ? event.sender.name : "Someone"; var senderName = event.sender ? event.sender.name : _t('Someone');
var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)"; var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
return senderName + " ended the call." + supported; return _t('%(senderName)s ended the call.', {senderName: senderName}) + ' ' + supported;
} }
function textForCallInviteEvent(event) { function textForCallInviteEvent(event) {
var senderName = event.sender ? event.sender.name : "Someone"; var senderName = event.sender ? event.sender.name : _t('Someone');
// FIXME: Find a better way to determine this from the event? // FIXME: Find a better way to determine this from the event?
var type = "voice"; var type = "voice";
if (event.getContent().offer && event.getContent().offer.sdp && if (event.getContent().offer && event.getContent().offer.sdp &&
event.getContent().offer.sdp.indexOf('m=video') !== -1) { event.getContent().offer.sdp.indexOf('m=video') !== -1) {
type = "video"; type = "video";
} }
var supported = MatrixClientPeg.get().supportsVoip() ? "" : " (not supported by this browser)"; var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
return senderName + " placed a " + type + " call." + supported; return _t('%(senderName)s placed a %(callType)s call.', {senderName: senderName, callType: type}) + ' ' + supported;
} }
function textForThreePidInviteEvent(event) { function textForThreePidInviteEvent(event) {
var senderName = event.sender ? event.sender.name : event.getSender(); var senderName = event.sender ? event.sender.name : event.getSender();
return senderName + " sent an invitation to " + event.getContent().display_name + return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {senderName: senderName, targetDisplayName: event.getContent().display_name});
" to join the room.";
} }
function textForHistoryVisibilityEvent(event) { function textForHistoryVisibilityEvent(event) {
var senderName = event.sender ? event.sender.name : event.getSender(); var senderName = event.sender ? event.sender.name : event.getSender();
var vis = event.getContent().history_visibility; var vis = event.getContent().history_visibility;
var text = senderName + " made future room history visible to "; // XXX: This i18n just isn't going to work for languages with different sentence structure.
var text = _t('%(senderName)s made future room history visible to', {senderName: senderName}) + ' ';
if (vis === "invited") { if (vis === "invited") {
text += "all room members, from the point they are invited."; text += _t('all room members, from the point they are invited') + '.';
} }
else if (vis === "joined") { else if (vis === "joined") {
text += "all room members, from the point they joined."; text += _t('all room members, from the point they joined') + '.';
} }
else if (vis === "shared") { else if (vis === "shared") {
text += "all room members."; text += _t('all room members') + '.';
} }
else if (vis === "world_readable") { else if (vis === "world_readable") {
text += "anyone."; text += _t('anyone') + '.';
} }
else { else {
text += " unknown (" + vis + ")"; text += ' ' + _t('unknown') + ' (' + vis + ').';
} }
return text; return text;
} }
function textForEncryptionEvent(event) { function textForEncryptionEvent(event) {
var senderName = event.sender ? event.sender.name : event.getSender(); var senderName = event.sender ? event.sender.name : event.getSender();
return senderName + " turned on end-to-end encryption (algorithm " + event.getContent().algorithm + ")"; return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {senderName: senderName, algorithm: event.getContent().algorithm});
} }
// Currently will only display a change if a user's power level is changed // Currently will only display a change if a user's power level is changed
@ -204,6 +212,7 @@ function textForPowerEvent(event) {
} }
); );
let diff = []; let diff = [];
// XXX: This is also surely broken for i18n
users.forEach((userId) => { users.forEach((userId) => {
// Previous power level // Previous power level
const from = event.getPrevContent().users[userId]; const from = event.getPrevContent().users[userId];
@ -211,16 +220,21 @@ function textForPowerEvent(event) {
const to = event.getContent().users[userId]; const to = event.getContent().users[userId];
if (to !== from) { if (to !== from) {
diff.push( diff.push(
userId + _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
' from ' + Roles.textualPowerLevel(from, userDefault) + userId: userId,
' to ' + Roles.textualPowerLevel(to, userDefault) fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
toPowerLevel: Roles.textualPowerLevel(to, userDefault)
})
); );
} }
}); });
if (!diff.length) { if (!diff.length) {
return ''; return '';
} }
return senderName + ' changed the power level of ' + diff.join(', '); return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
senderName: senderName,
powerLevelDiffText: diff.join(", ")
});
} }
var handlers = { var handlers = {

View file

@ -22,7 +22,7 @@ let isDialogOpen = false;
const onAction = function(payload) { const onAction = function(payload) {
if (payload.action === 'unknown_device_error' && !isDialogOpen) { if (payload.action === 'unknown_device_error' && !isDialogOpen) {
var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
isDialogOpen = true; isDialogOpen = true;
Modal.createDialog(UnknownDeviceDialog, { Modal.createDialog(UnknownDeviceDialog, {
devices: payload.err.devices, devices: payload.err.devices,
@ -33,17 +33,17 @@ const onAction = function(payload) {
// https://github.com/vector-im/riot-web/issues/3148 // https://github.com/vector-im/riot-web/issues/3148
console.log('UnknownDeviceDialog closed with '+r); console.log('UnknownDeviceDialog closed with '+r);
}, },
}, "mx_Dialog_unknownDevice"); }, 'mx_Dialog_unknownDevice');
} }
} };
let ref = null; let ref = null;
export function startListening () { export function startListening() {
ref = dis.register(onAction); ref = dis.register(onAction);
} }
export function stopListening () { export function stopListening() {
if (ref) { if (ref) {
dis.unregister(ref); dis.unregister(ref);
ref = null; ref = null;

View file

@ -14,24 +14,29 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict';
import q from 'q'; import q from 'q';
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import Notifier from './Notifier'; import Notifier from './Notifier';
import { _t } from './languageHandler';
/* /*
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage. * TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
*/ */
module.exports = { export default {
LABS_FEATURES: [ LABS_FEATURES: [
{ {
name: 'New Composer & Autocomplete', name: "-",
id: 'rich_text_editor', id: 'rich_text_editor',
default: false, default: false,
}, },
], ],
// horrible but it works. The locality makes this somewhat more palatable.
doTranslations: function() {
this.LABS_FEATURES[0].name = _t("New Composer & Autocomplete");
},
loadProfileInfo: function() { loadProfileInfo: function() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
return cli.getProfileInfo(cli.credentials.userId); return cli.getProfileInfo(cli.credentials.userId);

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
var MatrixClientPeg = require("./MatrixClientPeg"); var MatrixClientPeg = require("./MatrixClientPeg");
import { _t } from './languageHandler';
module.exports = { module.exports = {
usersTypingApartFromMe: function(room) { usersTypingApartFromMe: function(room) {
@ -56,18 +57,18 @@ module.exports = {
if (whoIsTyping.length == 0) { if (whoIsTyping.length == 0) {
return ''; return '';
} else if (whoIsTyping.length == 1) { } else if (whoIsTyping.length == 1) {
return whoIsTyping[0].name + ' is typing'; return _t('%(displayName)s is typing', {displayName: whoIsTyping[0].name});
} }
const names = whoIsTyping.map(function(m) { const names = whoIsTyping.map(function(m) {
return m.name; return m.name;
}); });
if (othersCount) { if (othersCount==1) {
const other = ' other' + (othersCount > 1 ? 's' : ''); return _t('%(names)s and one other are typing', {names: names.slice(0, limit - 1).join(', ')});
return names.slice(0, limit - 1).join(', ') + ' and ' + } else if (othersCount>1) {
othersCount + other + ' are typing'; return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount});
} else { } else {
const lastPerson = names.pop(); const lastPerson = names.pop();
return names.join(', ') + ' and ' + lastPerson + ' are typing'; return _t('%(names)s and %(lastPerson)s are typing', {names: names.join(', '), lastPerson: lastPerson});
} }
} }
}; };

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
var React = require("react"); var React = require("react");
import { _t } from '../../../languageHandler';
var sdk = require('../../../index'); var sdk = require('../../../index');
var MatrixClientPeg = require("../../../MatrixClientPeg"); var MatrixClientPeg = require("../../../MatrixClientPeg");
@ -78,33 +79,33 @@ module.exports = React.createClass({
_renderDeviceInfo: function() { _renderDeviceInfo: function() {
var device = this.state.device; var device = this.state.device;
if (!device) { if (!device) {
return (<i>unknown device</i>); return (<i>{ _t('unknown device') }</i>);
} }
var verificationStatus = (<b>NOT verified</b>); var verificationStatus = (<b>{ _t('NOT verified') }</b>);
if (device.isBlocked()) { if (device.isBlocked()) {
verificationStatus = (<b>Blacklisted</b>); verificationStatus = (<b>{ _t('Blacklisted') }</b>);
} else if (device.isVerified()) { } else if (device.isVerified()) {
verificationStatus = "verified"; verificationStatus = _t('verified');
} }
return ( return (
<table> <table>
<tbody> <tbody>
<tr> <tr>
<td>Name</td> <td>{ _t('Name') }</td>
<td>{ device.getDisplayName() }</td> <td>{ device.getDisplayName() }</td>
</tr> </tr>
<tr> <tr>
<td>Device ID</td> <td>{ _t('Device ID') }</td>
<td><code>{ device.deviceId }</code></td> <td><code>{ device.deviceId }</code></td>
</tr> </tr>
<tr> <tr>
<td>Verification</td> <td>{ _t('Verification') }</td>
<td>{ verificationStatus }</td> <td>{ verificationStatus }</td>
</tr> </tr>
<tr> <tr>
<td>Ed25519 fingerprint</td> <td>{ _t('Ed25519 fingerprint') }</td>
<td><code>{device.getFingerprint()}</code></td> <td><code>{device.getFingerprint()}</code></td>
</tr> </tr>
</tbody> </tbody>
@ -119,32 +120,32 @@ module.exports = React.createClass({
<table> <table>
<tbody> <tbody>
<tr> <tr>
<td>User ID</td> <td>{ _t('User ID') }</td>
<td>{ event.getSender() }</td> <td>{ event.getSender() }</td>
</tr> </tr>
<tr> <tr>
<td>Curve25519 identity key</td> <td>{ _t('Curve25519 identity key') }</td>
<td><code>{ event.getSenderKey() || <i>none</i> }</code></td> <td><code>{ event.getSenderKey() || <i>{ _t('none') }</i> }</code></td>
</tr> </tr>
<tr> <tr>
<td>Claimed Ed25519 fingerprint key</td> <td>{ _t('Claimed Ed25519 fingerprint key') }</td>
<td><code>{ event.getKeysClaimed().ed25519 || <i>none</i> }</code></td> <td><code>{ event.getKeysClaimed().ed25519 || <i>{ _t('none') }</i> }</code></td>
</tr> </tr>
<tr> <tr>
<td>Algorithm</td> <td>{ _t('Algorithm') }</td>
<td>{ event.getWireContent().algorithm || <i>unencrypted</i> }</td> <td>{ event.getWireContent().algorithm || <i>{ _t('unencrypted') }</i> }</td>
</tr> </tr>
{ {
event.getContent().msgtype === 'm.bad.encrypted' ? ( event.getContent().msgtype === 'm.bad.encrypted' ? (
<tr> <tr>
<td>Decryption error</td> <td>{ _t('Decryption error') }</td>
<td>{ event.getContent().body }</td> <td>{ event.getContent().body }</td>
</tr> </tr>
) : null ) : null
} }
<tr> <tr>
<td>Session ID</td> <td>{ _t('Session ID') }</td>
<td><code>{ event.getWireContent().session_id || <i>none</i> }</code></td> <td><code>{ event.getWireContent().session_id || <i>{ _t('none') }</i> }</code></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -166,18 +167,18 @@ module.exports = React.createClass({
return ( return (
<div className="mx_EncryptedEventDialog" onKeyDown={ this.onKeyDown }> <div className="mx_EncryptedEventDialog" onKeyDown={ this.onKeyDown }>
<div className="mx_Dialog_title"> <div className="mx_Dialog_title">
End-to-end encryption information { _t('End-to-end encryption information') }
</div> </div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<h4>Event information</h4> <h4>{ _t('Event information') }</h4>
{this._renderEventInfo()} {this._renderEventInfo()}
<h4>Sender device information</h4> <h4>{ _t('Sender device information') }</h4>
{this._renderDeviceInfo()} {this._renderDeviceInfo()}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={ this.props.onFinished } autoFocus={ true }> <button className="mx_Dialog_primary" onClick={ this.props.onFinished } autoFocus={ true }>
OK { _t('OK') }
</button> </button>
{buttons} {buttons}
</div> </div>

View file

@ -16,6 +16,7 @@ limitations under the License.
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import React from 'react'; import React from 'react';
import { _t } from '../../../languageHandler';
import * as Matrix from 'matrix-js-sdk'; import * as Matrix from 'matrix-js-sdk';
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
@ -52,11 +53,11 @@ export default React.createClass({
const passphrase = this.refs.passphrase1.value; const passphrase = this.refs.passphrase1.value;
if (passphrase !== this.refs.passphrase2.value) { if (passphrase !== this.refs.passphrase2.value) {
this.setState({errStr: 'Passphrases must match'}); this.setState({errStr: _t('Passphrases must match')});
return false; return false;
} }
if (!passphrase) { if (!passphrase) {
this.setState({errStr: 'Passphrase must not be empty'}); this.setState({errStr: _t('Passphrase must not be empty')});
return false; return false;
} }
@ -109,24 +110,28 @@ export default React.createClass({
return ( return (
<BaseDialog className='mx_exportE2eKeysDialog' <BaseDialog className='mx_exportE2eKeysDialog'
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title="Export room keys" title={_t("Export room keys")}
> >
<form onSubmit={this._onPassphraseFormSubmit}> <form onSubmit={this._onPassphraseFormSubmit}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<p> <p>
This process allows you to export the keys for messages { _t(
you have received in encrypted rooms to a local file. You 'This process allows you to export the keys for messages ' +
will then be able to import the file into another Matrix 'you have received in encrypted rooms to a local file. You ' +
client in the future, so that client will also be able to 'will then be able to import the file into another Matrix ' +
decrypt these messages. 'client in the future, so that client will also be able to ' +
'decrypt these messages.'
) }
</p> </p>
<p> <p>
The exported file will allow anyone who can read it to decrypt { _t(
any encrypted messages that you can see, so you should be 'The exported file will allow anyone who can read it to decrypt ' +
careful to keep it secure. To help with this, you should enter 'any encrypted messages that you can see, so you should be ' +
a passphrase below, which will be used to encrypt the exported 'careful to keep it secure. To help with this, you should enter ' +
data. It will only be possible to import the data by using the 'a passphrase below, which will be used to encrypt the exported ' +
same passphrase. 'data. It will only be possible to import the data by using the ' +
'same passphrase.'
) }
</p> </p>
<div className='error'> <div className='error'>
{this.state.errStr} {this.state.errStr}
@ -135,7 +140,7 @@ export default React.createClass({
<div className='mx_E2eKeysDialog_inputRow'> <div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'> <div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase1'> <label htmlFor='passphrase1'>
Enter passphrase {_t("Enter passphrase")}
</label> </label>
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
@ -148,7 +153,7 @@ export default React.createClass({
<div className='mx_E2eKeysDialog_inputRow'> <div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'> <div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase2'> <label htmlFor='passphrase2'>
Confirm passphrase {_t("Confirm passphrase")}
</label> </label>
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
@ -161,11 +166,11 @@ export default React.createClass({
</div> </div>
</div> </div>
<div className='mx_Dialog_buttons'> <div className='mx_Dialog_buttons'>
<input className='mx_Dialog_primary' type='submit' value='Export' <input className='mx_Dialog_primary' type='submit' value={_t('Export')}
disabled={disableForm} disabled={disableForm}
/> />
<button onClick={this._onCancelClick} disabled={disableForm}> <button onClick={this._onCancelClick} disabled={disableForm}>
Cancel {_t("Cancel")}
</button> </button>
</div> </div>
</form> </form>

View file

@ -19,6 +19,7 @@ import React from 'react';
import * as Matrix from 'matrix-js-sdk'; import * as Matrix from 'matrix-js-sdk';
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
function readFileAsArrayBuffer(file) { function readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -112,20 +113,23 @@ export default React.createClass({
return ( return (
<BaseDialog className='mx_importE2eKeysDialog' <BaseDialog className='mx_importE2eKeysDialog'
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title="Import room keys" title={_t("Import room keys")}
> >
<form onSubmit={this._onFormSubmit}> <form onSubmit={this._onFormSubmit}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<p> <p>
This process allows you to import encryption keys { _t(
that you had previously exported from another Matrix 'This process allows you to import encryption keys ' +
client. You will then be able to decrypt any 'that you had previously exported from another Matrix ' +
messages that the other client could decrypt. 'client. You will then be able to decrypt any ' +
'messages that the other client could decrypt.'
) }
</p> </p>
<p> <p>
The export file will be protected with a passphrase. { _t(
You should enter the passphrase here, to decrypt the 'The export file will be protected with a passphrase. ' +
file. 'You should enter the passphrase here, to decrypt the file.'
) }
</p> </p>
<div className='error'> <div className='error'>
{this.state.errStr} {this.state.errStr}
@ -134,7 +138,7 @@ export default React.createClass({
<div className='mx_E2eKeysDialog_inputRow'> <div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'> <div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='importFile'> <label htmlFor='importFile'>
File to import {_t("File to import")}
</label> </label>
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
@ -147,7 +151,7 @@ export default React.createClass({
<div className='mx_E2eKeysDialog_inputRow'> <div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'> <div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase'> <label htmlFor='passphrase'>
Enter passphrase {_t("Enter passphrase")}
</label> </label>
</div> </div>
<div className='mx_E2eKeysDialog_inputCell'> <div className='mx_E2eKeysDialog_inputCell'>
@ -160,11 +164,11 @@ export default React.createClass({
</div> </div>
</div> </div>
<div className='mx_Dialog_buttons'> <div className='mx_Dialog_buttons'>
<input className='mx_Dialog_primary' type='submit' value='Import' <input className='mx_Dialog_primary' type='submit' value={_t('Import')}
disabled={!this.state.enableSubmit || disableForm} disabled={!this.state.enableSubmit || disableForm}
/> />
<button onClick={this._onCancelClick} disabled={disableForm}> <button onClick={this._onCancelClick} disabled={disableForm}>
Cancel {_t("Cancel")}
</button> </button>
</div> </div>
</form> </form>

View file

@ -1,3 +1,20 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react'; import React from 'react';
import type {Completion, SelectionRange} from './Autocompleter'; import type {Completion, SelectionRange} from './Autocompleter';

View file

@ -1,3 +1,19 @@
/*
Copyright 2016 Aviral Dasgupta
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.
*/
// @flow // @flow
import type {Component} from 'react'; import type {Component} from 'react';

View file

@ -1,8 +1,27 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react'; import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import {TextualCompletion} from './Components'; import {TextualCompletion} from './Components';
// Warning: Since the description string will be translated in _t(result.description), all these strings below must be in i18n/strings/en_EN.json file
const COMMANDS = [ const COMMANDS = [
{ {
command: '/me', command: '/me',
@ -43,10 +62,10 @@ const COMMANDS = [
command: '/ddg', command: '/ddg',
args: '<query>', args: '<query>',
description: 'Searches DuckDuckGo for results', description: 'Searches DuckDuckGo for results',
} },
]; ];
let COMMAND_RE = /(^\/\w*)/g; const COMMAND_RE = /(^\/\w*)/g;
let instance = null; let instance = null;
@ -60,15 +79,15 @@ export default class CommandProvider extends AutocompleteProvider {
async getCompletions(query: string, selection: {start: number, end: number}) { async getCompletions(query: string, selection: {start: number, end: number}) {
let completions = []; let completions = [];
let {command, range} = this.getCurrentCommand(query, selection); const {command, range} = this.getCurrentCommand(query, selection);
if (command) { if (command) {
completions = this.fuse.search(command[0]).map(result => { completions = this.fuse.search(command[0]).map((result) => {
return { return {
completion: result.command + ' ', completion: result.command + ' ',
component: (<TextualCompletion component: (<TextualCompletion
title={result.command} title={result.command}
subtitle={result.args} subtitle={result.args}
description={result.description} description={ _t(result.description) }
/>), />),
range, range,
}; };
@ -78,12 +97,11 @@ export default class CommandProvider extends AutocompleteProvider {
} }
getName() { getName() {
return '*️⃣ Commands'; return '*️⃣ ' + _t('Commands');
} }
static getInstance(): CommandProvider { static getInstance(): CommandProvider {
if (instance == null) if (instance === null) instance = new CommandProvider();
{instance = new CommandProvider();}
return instance; return instance;
} }

View file

@ -1,3 +1,19 @@
/*
Copyright 2016 Aviral Dasgupta
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 React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import classNames from 'classnames'; import classNames from 'classnames';

View file

@ -1,4 +1,22 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react'; import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import 'whatwg-fetch'; import 'whatwg-fetch';
@ -75,7 +93,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
} }
getName() { getName() {
return '🔍 Results from DuckDuckGo'; return '🔍 ' + _t('Results from DuckDuckGo');
} }
static getInstance(): DuckDuckGoProvider { static getInstance(): DuckDuckGoProvider {

View file

@ -1,4 +1,22 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react'; import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione'; import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
@ -39,7 +57,7 @@ export default class EmojiProvider extends AutocompleteProvider {
} }
getName() { getName() {
return '😃 Emoji'; return '😃 ' + _t('Emoji');
} }
static getInstance() { static getInstance() {

View file

@ -1,4 +1,22 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react'; import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import MatrixClientPeg from '../MatrixClientPeg'; import MatrixClientPeg from '../MatrixClientPeg';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
@ -50,7 +68,7 @@ export default class RoomProvider extends AutocompleteProvider {
} }
getName() { getName() {
return '💬 Rooms'; return '💬 ' + _t('Rooms');
} }
static getInstance() { static getInstance() {

View file

@ -1,4 +1,22 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react'; import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import Q from 'q'; import Q from 'q';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
@ -51,7 +69,7 @@ export default class UserProvider extends AutocompleteProvider {
} }
getName() { getName() {
return '👥 Users'; return '👥 ' + _t('Users');
} }
setUserList(users) { setUserList(users) {

View file

@ -16,15 +16,16 @@ limitations under the License.
'use strict'; 'use strict';
var React = require("react"); import React from 'react';
var MatrixClientPeg = require("../../MatrixClientPeg"); import q from 'q';
var PresetValues = { import { _t } from '../../languageHandler';
import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
const PresetValues = {
PrivateChat: "private_chat", PrivateChat: "private_chat",
PublicChat: "public_chat", PublicChat: "public_chat",
Custom: "custom", Custom: "custom",
}; };
var q = require('q');
var sdk = require('../../index');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'CreateRoom', displayName: 'CreateRoom',
@ -231,7 +232,7 @@ module.exports = React.createClass({
if (curr_phase == this.phases.ERROR) { if (curr_phase == this.phases.ERROR) {
error_box = ( error_box = (
<div className="mx_Error"> <div className="mx_Error">
An error occured: {this.state.error_string} {_t('An error occured: %(error_string)s', {error_string: this.state.error_string})}
</div> </div>
); );
} }
@ -248,27 +249,27 @@ module.exports = React.createClass({
<div className="mx_CreateRoom"> <div className="mx_CreateRoom">
<SimpleRoomHeader title="CreateRoom" collapsedRhs={ this.props.collapsedRhs }/> <SimpleRoomHeader title="CreateRoom" collapsedRhs={ this.props.collapsedRhs }/>
<div className="mx_CreateRoom_body"> <div className="mx_CreateRoom_body">
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder="Name"/> <br /> <input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder={_t('Name')}/> <br />
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder="Topic"/> <br /> <textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder={_t('Topic')}/> <br />
<RoomAlias ref="alias" alias={this.state.alias} homeserver={ domain } onChange={this.onAliasChanged}/> <br /> <RoomAlias ref="alias" alias={this.state.alias} homeserver={ domain } onChange={this.onAliasChanged}/> <br />
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged}/> <br /> <UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged}/> <br />
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br /> <Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br />
<div> <div>
<label> <label>
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged}/> <input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged}/>
Make this room private {_t('Make this room private')}
</label> </label>
</div> </div>
<div> <div>
<label> <label>
<input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged}/> <input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged}/>
Share message history with new users {_t('Share message history with new users')}
</label> </label>
</div> </div>
<div className="mx_CreateRoom_encrypt"> <div className="mx_CreateRoom_encrypt">
<label> <label>
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged}/> <input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged}/>
Encrypt room {_t('Encrypt room')}
</label> </label>
</div> </div>
<div> <div>

View file

@ -14,13 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require('react'); import React from 'react';
var ReactDOM = require("react-dom");
var Matrix = require("matrix-js-sdk"); import Matrix from 'matrix-js-sdk';
var sdk = require('../../index'); import sdk from '../../index';
var MatrixClientPeg = require("../../MatrixClientPeg"); import MatrixClientPeg from '../../MatrixClientPeg';
var dis = require("../../dispatcher"); import { _t } from '../../languageHandler';
/* /*
* Component which shows the filtered file using a TimelinePanel * Component which shows the filtered file using a TimelinePanel
@ -96,7 +95,7 @@ var FilePanel = React.createClass({
</div>; </div>;
} else if (this.noRoom) { } else if (this.noRoom) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper"> return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
<div className="mx_RoomView_empty">You must join the room to see its files</div> <div className="mx_RoomView_empty">{_t("You must join the room to see its files")}</div>
</div>; </div>;
} }
@ -116,7 +115,7 @@ var FilePanel = React.createClass({
showUrlPreview = { false } showUrlPreview = { false }
tileShape="file_grid" tileShape="file_grid"
opacity={ this.props.opacity } opacity={ this.props.opacity }
empty="There are no visible files in this room" empty={_t('There are no visible files in this room')}
/> />
); );
} }

View file

@ -18,6 +18,7 @@ limitations under the License.
import * as Matrix from 'matrix-js-sdk'; import * as Matrix from 'matrix-js-sdk';
import React from 'react'; import React from 'react';
import UserSettingsStore from '../../UserSettingsStore';
import KeyCode from '../../KeyCode'; import KeyCode from '../../KeyCode';
import Notifier from '../../Notifier'; import Notifier from '../../Notifier';
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
@ -64,6 +65,13 @@ export default React.createClass({
}; };
}, },
getInitialState: function() {
return {
// use compact timeline view
useCompactLayout: UserSettingsStore.getSyncedSetting('useCompactLayout'),
};
},
componentWillMount: function() { componentWillMount: function() {
// stash the MatrixClient in case we log out before we are unmounted // stash the MatrixClient in case we log out before we are unmounted
this._matrixClient = this.props.matrixClient; this._matrixClient = this.props.matrixClient;
@ -75,10 +83,12 @@ export default React.createClass({
CallMediaHandler.loadDevices(); CallMediaHandler.loadDevices();
document.addEventListener('keydown', this._onKeyDown); document.addEventListener('keydown', this._onKeyDown);
this._matrixClient.on("accountData", this.onAccountData);
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
document.removeEventListener('keydown', this._onKeyDown); document.removeEventListener('keydown', this._onKeyDown);
this._matrixClient.removeListener("accountData", this.onAccountData);
}, },
getScrollStateForRoom: function(roomId) { getScrollStateForRoom: function(roomId) {
@ -92,6 +102,14 @@ export default React.createClass({
return this.refs.roomView.canResetTimeline(); return this.refs.roomView.canResetTimeline();
}, },
onAccountData: function(event) {
if (event.getType() === "im.vector.web.settings") {
this.setState({
useCompactLayout: event.getContent().useCompactLayout
});
}
},
_onKeyDown: function(ev) { _onKeyDown: function(ev) {
/* /*
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers // Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
@ -186,7 +204,7 @@ export default React.createClass({
ConferenceHandler={this.props.ConferenceHandler} ConferenceHandler={this.props.ConferenceHandler}
scrollStateMap={this._scrollStateMap} scrollStateMap={this._scrollStateMap}
/>; />;
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.sideOpacity} />; if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />;
break; break;
case PageTypes.UserSettings: case PageTypes.UserSettings:
@ -198,7 +216,7 @@ export default React.createClass({
referralBaseUrl={this.props.config.referralBaseUrl} referralBaseUrl={this.props.config.referralBaseUrl}
teamToken={this.props.teamToken} teamToken={this.props.teamToken}
/>; />;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>; if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
break; break;
case PageTypes.CreateRoom: case PageTypes.CreateRoom:
@ -206,7 +224,7 @@ export default React.createClass({
onRoomCreated={this.props.onRoomCreated} onRoomCreated={this.props.onRoomCreated}
collapsedRhs={this.props.collapse_rhs} collapsedRhs={this.props.collapse_rhs}
/>; />;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>; if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
break; break;
case PageTypes.RoomDirectory: case PageTypes.RoomDirectory:
@ -222,12 +240,12 @@ export default React.createClass({
teamServerUrl={this.props.config.teamServerConfig.teamServerURL} teamServerUrl={this.props.config.teamServerConfig.teamServerURL}
teamToken={this.props.teamToken} teamToken={this.props.teamToken}
/> />
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/> if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>
break; break;
case PageTypes.UserView: case PageTypes.UserView:
page_element = null; // deliberately null for now page_element = null; // deliberately null for now
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.sideOpacity} />; right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.rightOpacity} />;
break; break;
} }
@ -248,6 +266,9 @@ export default React.createClass({
if (topBar) { if (topBar) {
bodyClasses += ' mx_MatrixChat_toolbarShowing'; bodyClasses += ' mx_MatrixChat_toolbarShowing';
} }
if (this.state.useCompactLayout) {
bodyClasses += ' mx_MatrixChat_useCompactLayout';
}
return ( return (
<div className='mx_MatrixChat_wrapper'> <div className='mx_MatrixChat_wrapper'>
@ -256,7 +277,7 @@ export default React.createClass({
<LeftPanel <LeftPanel
selectedRoom={this.props.currentRoomId} selectedRoom={this.props.currentRoomId}
collapsed={this.props.collapse_lhs || false} collapsed={this.props.collapse_lhs || false}
opacity={this.props.sideOpacity} opacity={this.props.leftOpacity}
teamToken={this.props.teamToken} teamToken={this.props.teamToken}
/> />
<main className='mx_MatrixChat_middlePanel'> <main className='mx_MatrixChat_middlePanel'>

View file

@ -20,6 +20,8 @@ import q from 'q';
import React from 'react'; import React from 'react';
import Matrix from "matrix-js-sdk"; import Matrix from "matrix-js-sdk";
import Analytics from "../../Analytics";
import UserSettingsStore from '../../UserSettingsStore';
import MatrixClientPeg from "../../MatrixClientPeg"; import MatrixClientPeg from "../../MatrixClientPeg";
import PlatformPeg from "../../PlatformPeg"; import PlatformPeg from "../../PlatformPeg";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
@ -36,6 +38,7 @@ import PageTypes from '../../PageTypes';
import createRoom from "../../createRoom"; import createRoom from "../../createRoom";
import * as UDEHandler from '../../UnknownDeviceErrorHandler'; import * as UDEHandler from '../../UnknownDeviceErrorHandler';
import { _t } from '../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MatrixChat', displayName: 'MatrixChat',
@ -116,8 +119,9 @@ module.exports = React.createClass({
collapse_rhs: false, collapse_rhs: false,
ready: false, ready: false,
width: 10000, width: 10000,
sideOpacity: 1.0, leftOpacity: 1.0,
middleOpacity: 1.0, middleOpacity: 1.0,
rightOpacity: 1.0,
version: null, version: null,
newVersion: null, newVersion: null,
@ -187,6 +191,8 @@ module.exports = React.createClass({
componentWillMount: function() { componentWillMount: function() {
SdkConfig.put(this.props.config); SdkConfig.put(this.props.config);
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable();
// Used by _viewRoom before getting state from sync // Used by _viewRoom before getting state from sync
this.firstSyncComplete = false; this.firstSyncComplete = false;
this.firstSyncPromise = q.defer(); this.firstSyncPromise = q.defer();
@ -246,7 +252,6 @@ module.exports = React.createClass({
UDEHandler.startListening(); UDEHandler.startListening();
this.focusComposer = false; this.focusComposer = false;
window.addEventListener("focus", this.onFocus);
// this can technically be done anywhere but doing this here keeps all // this can technically be done anywhere but doing this here keeps all
// the routing url path logic together. // the routing url path logic together.
@ -287,7 +292,7 @@ module.exports = React.createClass({
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
Lifecycle.stopMatrixClient(); Lifecycle.stopMatrixClient(false);
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
UDEHandler.stopListening(); UDEHandler.stopListening();
window.removeEventListener("focus", this.onFocus); window.removeEventListener("focus", this.onFocus);
@ -359,7 +364,7 @@ module.exports = React.createClass({
// is completed in another browser, we'll be 401ed for using // is completed in another browser, we'll be 401ed for using
// a guest access token for a non-guest account. // a guest access token for a non-guest account.
// It will be restarted in onReturnToGuestClick // It will be restarted in onReturnToGuestClick
Lifecycle.stopMatrixClient(); Lifecycle.stopMatrixClient(false);
this.notifyNewScreen('register'); this.notifyNewScreen('register');
break; break;
@ -375,8 +380,8 @@ module.exports = React.createClass({
break; break;
case 'reject_invite': case 'reject_invite':
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Reject invitation", title: _t('Reject invitation'),
description: "Are you sure you want to reject the invitation?", description: _t('Are you sure you want to reject the invitation?'),
onFinished: (confirm) => { onFinished: (confirm) => {
if (confirm) { if (confirm) {
// FIXME: controller shouldn't be loading a view :( // FIXME: controller shouldn't be loading a view :(
@ -391,7 +396,7 @@ module.exports = React.createClass({
}, (err) => { }, (err) => {
modal.close(); modal.close();
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to reject invitation", title: _t('Failed to reject invitation'),
description: err.toString(), description: err.toString(),
}); });
}); });
@ -437,9 +442,9 @@ module.exports = React.createClass({
//this._setPage(PageTypes.CreateRoom); //this._setPage(PageTypes.CreateRoom);
//this.notifyNewScreen('new'); //this.notifyNewScreen('new');
Modal.createDialog(TextInputDialog, { Modal.createDialog(TextInputDialog, {
title: "Create Room", title: _t('Create Room'),
description: "Room name (optional)", description: _t('Room name (optional)'),
button: "Create Room", button: _t('Create Room'),
onFinished: (shouldCreate, name) => { onFinished: (shouldCreate, name) => {
if (shouldCreate) { if (shouldCreate) {
const createOpts = {}; const createOpts = {};
@ -490,12 +495,14 @@ module.exports = React.createClass({
collapse_rhs: false, collapse_rhs: false,
}); });
break; break;
case 'ui_opacity': case 'ui_opacity': {
const sideDefault = payload.sideOpacity >= 0.0 ? payload.sideOpacity : 1.0;
this.setState({ this.setState({
sideOpacity: payload.sideOpacity, leftOpacity: payload.leftOpacity >= 0.0 ? payload.leftOpacity : sideDefault,
middleOpacity: payload.middleOpacity, middleOpacity: payload.middleOpacity || 1.0,
rightOpacity: payload.rightOpacity >= 0.0 ? payload.rightOpacity : sideDefault,
}); });
break; break; }
case 'set_theme': case 'set_theme':
this._onSetTheme(payload.value); this._onSetTheme(payload.value);
break; break;
@ -572,44 +579,44 @@ module.exports = React.createClass({
// switch view to the given room // switch view to the given room
// //
// @param {Object} room_info Object containing data about the room to be joined // @param {Object} roomInfo Object containing data about the room to be joined
// @param {string=} room_info.room_id ID of the room to join. One of room_id or room_alias must be given. // @param {string=} roomInfo.room_id ID of the room to join. One of room_id or room_alias must be given.
// @param {string=} room_info.room_alias Alias of the room to join. One of room_id or room_alias must be given. // @param {string=} roomInfo.room_alias Alias of the room to join. One of room_id or room_alias must be given.
// @param {boolean=} room_info.auto_join If true, automatically attempt to join the room if not already a member. // @param {boolean=} roomInfo.auto_join If true, automatically attempt to join the room if not already a member.
// @param {boolean=} room_info.show_settings Makes RoomView show the room settings dialog. // @param {boolean=} roomInfo.show_settings Makes RoomView show the room settings dialog.
// @param {string=} room_info.event_id ID of the event in this room to show: this will cause a switch to the // @param {string=} roomInfo.event_id ID of the event in this room to show: this will cause a switch to the
// context of that particular event. // context of that particular event.
// @param {Object=} room_info.third_party_invite Object containing data about the third party // @param {Object=} roomInfo.third_party_invite Object containing data about the third party
// we received to join the room, if any. // we received to join the room, if any.
// @param {string=} room_info.third_party_invite.inviteSignUrl 3pid invite sign URL // @param {string=} roomInfo.third_party_invite.inviteSignUrl 3pid invite sign URL
// @param {string=} room_info.third_party_invite.invitedEmail The email address the invite was sent to // @param {string=} roomInfo.third_party_invite.invitedEmail The email address the invite was sent to
// @param {Object=} room_info.oob_data Object of additional data about the room // @param {Object=} roomInfo.oob_data Object of additional data about the room
// that has been passed out-of-band (eg. // that has been passed out-of-band (eg.
// room name and avatar from an invite email) // room name and avatar from an invite email)
_viewRoom: function(room_info) { _viewRoom: function(roomInfo) {
this.focusComposer = true; this.focusComposer = true;
const newState = { const newState = {
initialEventId: room_info.event_id, initialEventId: roomInfo.event_id,
highlightedEventId: room_info.event_id, highlightedEventId: roomInfo.event_id,
initialEventPixelOffset: undefined, initialEventPixelOffset: undefined,
page_type: PageTypes.RoomView, page_type: PageTypes.RoomView,
thirdPartyInvite: room_info.third_party_invite, thirdPartyInvite: roomInfo.third_party_invite,
roomOobData: room_info.oob_data, roomOobData: roomInfo.oob_data,
currentRoomAlias: room_info.room_alias, currentRoomAlias: roomInfo.room_alias,
autoJoin: room_info.auto_join, autoJoin: roomInfo.auto_join,
}; };
if (!room_info.room_alias) { if (!roomInfo.room_alias) {
newState.currentRoomId = room_info.room_id; newState.currentRoomId = roomInfo.room_id;
} }
// if we aren't given an explicit event id, look for one in the // if we aren't given an explicit event id, look for one in the
// scrollStateMap. // scrollStateMap.
// //
// TODO: do this in RoomView rather than here // TODO: do this in RoomView rather than here
if (!room_info.event_id && this.refs.loggedInView) { if (!roomInfo.event_id && this.refs.loggedInView) {
const scrollState = this.refs.loggedInView.getScrollStateForRoom(room_info.room_id); const scrollState = this.refs.loggedInView.getScrollStateForRoom(roomInfo.room_id);
if (scrollState) { if (scrollState) {
newState.initialEventId = scrollState.focussedEvent; newState.initialEventId = scrollState.focussedEvent;
newState.initialEventPixelOffset = scrollState.pixelOffset; newState.initialEventPixelOffset = scrollState.pixelOffset;
@ -621,15 +628,15 @@ module.exports = React.createClass({
let waitFor = q(null); let waitFor = q(null);
if (!this.firstSyncComplete) { if (!this.firstSyncComplete) {
if (!this.firstSyncPromise) { if (!this.firstSyncPromise) {
console.warn('Cannot view a room before first sync. room_id:', room_info.room_id); console.warn('Cannot view a room before first sync. room_id:', roomInfo.room_id);
return; return;
} }
waitFor = this.firstSyncPromise.promise; waitFor = this.firstSyncPromise.promise;
} }
waitFor.done(() => { waitFor.done(() => {
let presentedId = room_info.room_alias || room_info.room_id; let presentedId = roomInfo.room_alias || roomInfo.room_id;
const room = MatrixClientPeg.get().getRoom(room_info.room_id); const room = MatrixClientPeg.get().getRoom(roomInfo.room_id);
if (room) { if (room) {
const theAlias = Rooms.getDisplayAliasForRoom(room); const theAlias = Rooms.getDisplayAliasForRoom(room);
if (theAlias) presentedId = theAlias; if (theAlias) presentedId = theAlias;
@ -641,8 +648,8 @@ module.exports = React.createClass({
} }
} }
if (room_info.event_id) { if (roomInfo.event_id) {
presentedId += "/" + room_info.event_id; presentedId += "/" + roomInfo.event_id;
} }
this.notifyNewScreen('room/' + presentedId); this.notifyNewScreen('room/' + presentedId);
newState.ready = true; newState.ready = true;
@ -653,16 +660,20 @@ module.exports = React.createClass({
_createChat: function() { _createChat: function() {
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
Modal.createDialog(ChatInviteDialog, { Modal.createDialog(ChatInviteDialog, {
title: "Start a new chat", title: _t('Start a chat'),
description: _t("Who would you like to communicate with?"),
placeholder: _t("Email, name or matrix ID"),
button: _t("Start Chat"),
}); });
}, },
_invite: function(roomId) { _invite: function(roomId) {
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
Modal.createDialog(ChatInviteDialog, { Modal.createDialog(ChatInviteDialog, {
title: "Invite new room members", title: _t('Invite new room members'),
button: "Send Invites", description: _t('Who would you like to add to this room?'),
description: "Who would you like to add to this room?", button: _t('Send Invites'),
placeholder: _t("Email, name or matrix ID"),
roomId: roomId, roomId: roomId,
}); });
}, },
@ -692,9 +703,9 @@ module.exports = React.createClass({
modal.close(); modal.close();
console.error("Failed to leave room " + roomId + " " + err); console.error("Failed to leave room " + roomId + " " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to leave room", title: _t("Failed to leave room"),
description: (err && err.message ? err.message : description: (err && err.message ? err.message :
"Server may be unavailable, overloaded, or you hit a bug."), _t("Server may be unavailable, overloaded, or you hit a bug.")),
}); });
}); });
} }
@ -886,8 +897,8 @@ module.exports = React.createClass({
cli.on('Session.logged_out', function(call) { cli.on('Session.logged_out', function(call) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Signed Out", title: _t('Signed Out'),
description: "For security, this session has been signed out. Please sign in again.", description: _t('For security, this session has been signed out. Please sign in again.'),
}); });
dis.dispatch({ dis.dispatch({
action: 'logout', action: 'logout',
@ -905,10 +916,6 @@ module.exports = React.createClass({
}); });
}, },
onFocus: function(ev) {
dis.dispatch({action: 'focus_composer'});
},
showScreen: function(screen, params) { showScreen: function(screen, params) {
if (screen == 'register') { if (screen == 'register') {
dis.dispatch({ dis.dispatch({
@ -999,6 +1006,7 @@ module.exports = React.createClass({
if (this.props.onNewScreen) { if (this.props.onNewScreen) {
this.props.onNewScreen(screen); this.props.onNewScreen(screen);
} }
Analytics.trackPageChange();
}, },
onAliasClick: function(event, alias) { onAliasClick: function(event, alias) {
@ -1199,7 +1207,7 @@ module.exports = React.createClass({
<div className="mx_MatrixChat_splash"> <div className="mx_MatrixChat_splash">
<Spinner /> <Spinner />
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }> <a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
Logout { _t('Logout') }
</a> </a>
</div> </div>
); );

View file

@ -84,6 +84,12 @@ module.exports = React.createClass({
// shape parameter to be passed to EventTiles // shape parameter to be passed to EventTiles
tileShape: React.PropTypes.string, tileShape: React.PropTypes.string,
// show twelve hour timestamps
isTwelveHour: React.PropTypes.bool,
// show timestamps always
alwaysShowTimestamps: React.PropTypes.bool,
}, },
componentWillMount: function() { componentWillMount: function() {
@ -230,8 +236,8 @@ module.exports = React.createClass({
}, },
_getEventTiles: function() { _getEventTiles: function() {
var EventTile = sdk.getComponent('rooms.EventTile'); const EventTile = sdk.getComponent('rooms.EventTile');
var DateSeparator = sdk.getComponent('messages.DateSeparator'); const DateSeparator = sdk.getComponent('messages.DateSeparator');
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
this.eventNodes = {}; this.eventNodes = {};
@ -310,7 +316,7 @@ module.exports = React.createClass({
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial"); const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) { if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
let dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1}/></li>; let dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
ret.push(dateSeparator); ret.push(dateSeparator);
} }
@ -413,8 +419,8 @@ module.exports = React.createClass({
}, },
_getTilesForEvent: function(prevEvent, mxEv, last) { _getTilesForEvent: function(prevEvent, mxEv, last) {
var EventTile = sdk.getComponent('rooms.EventTile'); const EventTile = sdk.getComponent('rooms.EventTile');
var DateSeparator = sdk.getComponent('messages.DateSeparator'); const DateSeparator = sdk.getComponent('messages.DateSeparator');
var ret = []; var ret = [];
// is this a continuation of the previous message? // is this a continuation of the previous message?
@ -452,7 +458,7 @@ module.exports = React.createClass({
// do we need a date separator since the last event? // do we need a date separator since the last event?
if (this._wantsDateSeparator(prevEvent, eventDate)) { if (this._wantsDateSeparator(prevEvent, eventDate)) {
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1}/></li>; var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
ret.push(dateSeparator); ret.push(dateSeparator);
continuation = false; continuation = false;
} }
@ -468,7 +474,6 @@ module.exports = React.createClass({
if (this.props.manageReadReceipts) { if (this.props.manageReadReceipts) {
readReceipts = this._getReadReceiptsForEvent(mxEv); readReceipts = this._getReadReceiptsForEvent(mxEv);
} }
ret.push( ret.push(
<li key={eventId} <li key={eventId}
ref={this._collectEventNode.bind(this, eventId)} ref={this._collectEventNode.bind(this, eventId)}
@ -482,6 +487,7 @@ module.exports = React.createClass({
checkUnmounting={this._isUnmounting} checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.status} eventSendStatus={mxEv.status}
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
last={last} isSelectedEvent={highlight}/> last={last} isSelectedEvent={highlight}/>
</li> </li>
); );
@ -615,8 +621,13 @@ module.exports = React.createClass({
var style = this.props.hidden ? { display: 'none' } : {}; var style = this.props.hidden ? { display: 'none' } : {};
style.opacity = this.props.opacity; style.opacity = this.props.opacity;
var className = this.props.className + " mx_fadable";
if (this.props.alwaysShowTimestamps) {
className += " mx_MessagePanel_alwaysShowTimestamps";
}
return ( return (
<ScrollPanel ref="scrollPanel" className={ this.props.className + " mx_fadable" } <ScrollPanel ref="scrollPanel" className={ className }
onScroll={ this.props.onScroll } onScroll={ this.props.onScroll }
onResize={ this.onResize } onResize={ this.onResize }
onFillRequest={ this.props.onFillRequest } onFillRequest={ this.props.onFillRequest }

View file

@ -16,7 +16,7 @@ limitations under the License.
var React = require('react'); var React = require('react');
var ReactDOM = require("react-dom"); var ReactDOM = require("react-dom");
import { _t } from '../../languageHandler';
var Matrix = require("matrix-js-sdk"); var Matrix = require("matrix-js-sdk");
var sdk = require('../../index'); var sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg"); var MatrixClientPeg = require("../../MatrixClientPeg");
@ -37,7 +37,6 @@ var NotificationPanel = React.createClass({
var Loader = sdk.getComponent("elements.Spinner"); var Loader = sdk.getComponent("elements.Spinner");
var timelineSet = MatrixClientPeg.get().getNotifTimelineSet(); var timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) { if (timelineSet) {
return ( return (
<TimelinePanel key={"NotificationPanel_" + this.props.roomId} <TimelinePanel key={"NotificationPanel_" + this.props.roomId}
@ -48,7 +47,7 @@ var NotificationPanel = React.createClass({
showUrlPreview = { false } showUrlPreview = { false }
opacity={ this.props.opacity } opacity={ this.props.opacity }
tileShape="notif" tileShape="notif"
empty="You have no visible notifications" empty={ _t('You have no visible notifications') }
/> />
); );
} }

View file

@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require('react'); import React from 'react';
var sdk = require('../../index'); import { _t } from '../../languageHandler';
var dis = require("../../dispatcher"); import sdk from '../../index';
var WhoIsTyping = require("../../WhoIsTyping"); import dis from '../../dispatcher';
var MatrixClientPeg = require("../../MatrixClientPeg"); import WhoIsTyping from '../../WhoIsTyping';
const MemberAvatar = require("../views/avatars/MemberAvatar"); import MatrixClientPeg from '../../MatrixClientPeg';
import MemberAvatar from '../views/avatars/MemberAvatar';
const HIDE_DEBOUNCE_MS = 10000; const HIDE_DEBOUNCE_MS = 10000;
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;
@ -175,8 +176,8 @@ module.exports = React.createClass({
<div className="mx_RoomStatusBar_scrollDownIndicator" <div className="mx_RoomStatusBar_scrollDownIndicator"
onClick={ this.props.onScrollToBottomClick }> onClick={ this.props.onScrollToBottomClick }>
<img src="img/scrolldown.svg" width="24" height="24" <img src="img/scrolldown.svg" width="24" height="24"
alt="Scroll to bottom of page" alt={ _t("Scroll to bottom of page") }
title="Scroll to bottom of page"/> title={ _t("Scroll to bottom of page") }/>
</div> </div>
); );
} }
@ -250,10 +251,10 @@ module.exports = React.createClass({
<div className="mx_RoomStatusBar_connectionLostBar"> <div className="mx_RoomStatusBar_connectionLostBar">
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/> <img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
<div className="mx_RoomStatusBar_connectionLostBar_title"> <div className="mx_RoomStatusBar_connectionLostBar_title">
Connectivity to the server has been lost. {_t('Connectivity to the server has been lost.')}
</div> </div>
<div className="mx_RoomStatusBar_connectionLostBar_desc"> <div className="mx_RoomStatusBar_connectionLostBar_desc">
Sent messages will be stored until your connection has returned. {_t('Sent messages will be stored until your connection has returned.')}
</div> </div>
</div> </div>
); );
@ -266,7 +267,7 @@ module.exports = React.createClass({
<TabCompleteBar tabComplete={this.props.tabComplete} /> <TabCompleteBar tabComplete={this.props.tabComplete} />
<div className="mx_RoomStatusBar_tabCompleteEol" title="->|"> <div className="mx_RoomStatusBar_tabCompleteEol" title="->|">
<TintableSvg src="img/eol.svg" width="22" height="16"/> <TintableSvg src="img/eol.svg" width="22" height="16"/>
Auto-complete {_t('Auto-complete')}
</div> </div>
</div> </div>
</div> </div>
@ -283,13 +284,12 @@ module.exports = React.createClass({
<div className="mx_RoomStatusBar_connectionLostBar_desc"> <div className="mx_RoomStatusBar_connectionLostBar_desc">
<a className="mx_RoomStatusBar_resend_link" <a className="mx_RoomStatusBar_resend_link"
onClick={ this.props.onResendAllClick }> onClick={ this.props.onResendAllClick }>
Resend all {_t('Resend all')}
</a> or <a </a> {_t('or')} <a
className="mx_RoomStatusBar_resend_link" className="mx_RoomStatusBar_resend_link"
onClick={ this.props.onCancelAllClick }> onClick={ this.props.onCancelAllClick }>
cancel all {_t('cancel all')}
</a> now. You can also select individual messages to </a> {_t('now. You can also select individual messages to resend or cancel.')}
resend or cancel.
</div> </div>
</div> </div>
); );
@ -324,7 +324,7 @@ module.exports = React.createClass({
if (this.props.hasActiveCall) { if (this.props.hasActiveCall) {
return ( return (
<div className="mx_RoomStatusBar_callBar"> <div className="mx_RoomStatusBar_callBar">
<b>Active call</b> <b>{_t('Active call')}</b>
</div> </div>
); );
} }

View file

@ -25,6 +25,7 @@ var ReactDOM = require("react-dom");
var q = require("q"); var q = require("q");
var classNames = require("classnames"); var classNames = require("classnames");
var Matrix = require("matrix-js-sdk"); var Matrix = require("matrix-js-sdk");
import { _t } from '../../languageHandler';
var UserSettingsStore = require('../../UserSettingsStore'); var UserSettingsStore = require('../../UserSettingsStore');
var MatrixClientPeg = require("../../MatrixClientPeg"); var MatrixClientPeg = require("../../MatrixClientPeg");
@ -124,6 +125,8 @@ module.exports = React.createClass({
room: null, room: null,
roomId: null, roomId: null,
roomLoading: true, roomLoading: true,
forwardingEvent: null,
editingRoomSettings: false, editingRoomSettings: false,
uploadingRoomSettings: false, uploadingRoomSettings: false,
numUnreadMessages: 0, numUnreadMessages: 0,
@ -296,7 +299,7 @@ module.exports = React.createClass({
componentWillReceiveProps: function(newProps) { componentWillReceiveProps: function(newProps) {
if (newProps.roomAddress != this.props.roomAddress) { if (newProps.roomAddress != this.props.roomAddress) {
throw new Error("changing room on a RoomView is not supported"); throw new Error(_t("changing room on a RoomView is not supported"));
} }
if (newProps.eventId != this.props.eventId) { if (newProps.eventId != this.props.eventId) {
@ -370,10 +373,10 @@ module.exports = React.createClass({
onPageUnload(event) { onPageUnload(event) {
if (ContentMessages.getCurrentUploads().length > 0) { if (ContentMessages.getCurrentUploads().length > 0) {
return event.returnValue = return event.returnValue =
'You seem to be uploading files, are you sure you want to quit?'; _t("You seem to be uploading files, are you sure you want to quit?");
} else if (this._getCallForRoom() && this.state.callState !== 'ended') { } else if (this._getCallForRoom() && this.state.callState !== 'ended') {
return event.returnValue = return event.returnValue =
'You seem to be in a call, are you sure you want to quit?'; _t("You seem to be in a call, are you sure you want to quit?");
} }
}, },
@ -451,6 +454,11 @@ module.exports = React.createClass({
callState: callState callState: callState
}); });
break;
case 'forward_event':
this.setState({
forwardingEvent: payload.content,
});
break; break;
} }
}, },
@ -530,14 +538,14 @@ module.exports = React.createClass({
if (!userHasUsedEncryption) { if (!userHasUsedEncryption) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Warning!", title: _t("Warning!"),
hasCancelButton: false, hasCancelButton: false,
description: ( description: (
<div> <div>
<p>End-to-end encryption is in beta and may not be reliable.</p> <p>{ _t("End-to-end encryption is in beta and may not be reliable") }.</p>
<p>You should <b>not</b> yet trust it to secure data.</p> <p>{ _t("You should not yet trust it to secure data") }.</p>
<p>Devices will <b>not</b> yet be able to decrypt history from before they joined the room.</p> <p>{ _t("Devices will not yet be able to decrypt history from before they joined the room") }.</p>
<p>Encrypted messages will not be visible on clients that do not yet implement encryption.</p> <p>{ _t("Encrypted messages will not be visible on clients that do not yet implement encryption") }.</p>
</div> </div>
), ),
}); });
@ -708,10 +716,10 @@ module.exports = React.createClass({
if (!unsentMessages.length) return ""; if (!unsentMessages.length) return "";
for (const event of unsentMessages) { for (const event of unsentMessages) {
if (!event.error || event.error.name !== "UnknownDeviceError") { if (!event.error || event.error.name !== "UnknownDeviceError") {
return "Some of your messages have not been sent."; return _t("Some of your messages have not been sent") + ".";
} }
} }
return "Message not sent due to unknown devices being present"; return _t("Message not sent due to unknown devices being present");
}, },
_getUnsentMessages: function(room) { _getUnsentMessages: function(room) {
@ -871,15 +879,15 @@ module.exports = React.createClass({
) { ) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, { Modal.createDialog(NeedToRegisterDialog, {
title: "Failed to join the room", title: _t("Failed to join the room"),
description: "This room is private or inaccessible to guests. You may be able to join if you register." description: _t("This room is private or inaccessible to guests. You may be able to join if you register") + "."
}); });
} else { } else {
var msg = error.message ? error.message : JSON.stringify(error); var msg = error.message ? error.message : JSON.stringify(error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to join room", title: _t("Failed to join room"),
description: msg description: msg,
}); });
} }
}).done(); }).done();
@ -939,8 +947,8 @@ module.exports = React.createClass({
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, { Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register", title: _t("Please Register"),
description: "Guest users can't upload files. Please register to upload." description: _t("Guest users can't upload files. Please register to upload") + "."
}); });
return; return;
} }
@ -959,8 +967,8 @@ module.exports = React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to upload file " + file + " " + error); console.error("Failed to upload file " + file + " " + error);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to upload file", title: _t('Failed to upload file'),
description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or the file too big"), description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
}); });
}); });
}, },
@ -1046,8 +1054,8 @@ module.exports = React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Search failed: " + error); console.error("Search failed: " + error);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Search failed", title: _t("Search failed"),
description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or search timed out :("), description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or search timed out :(")),
}); });
}).finally(function() { }).finally(function() {
self.setState({ self.setState({
@ -1082,12 +1090,12 @@ module.exports = React.createClass({
if (!this.state.searchResults.next_batch) { if (!this.state.searchResults.next_batch) {
if (this.state.searchResults.results.length == 0) { if (this.state.searchResults.results.length == 0) {
ret.push(<li key="search-top-marker"> ret.push(<li key="search-top-marker">
<h2 className="mx_RoomView_topMarker">No results</h2> <h2 className="mx_RoomView_topMarker">{ _t("No results") }</h2>
</li> </li>
); );
} else { } else {
ret.push(<li key="search-top-marker"> ret.push(<li key="search-top-marker">
<h2 className="mx_RoomView_topMarker">No more results</h2> <h2 className="mx_RoomView_topMarker">{ _t("No more results") }</h2>
</li> </li>
); );
} }
@ -1124,10 +1132,10 @@ module.exports = React.createClass({
// it. We should tell the js sdk to go and find out about // it. We should tell the js sdk to go and find out about
// it. But that's not an issue currently, as synapse only // it. But that's not an issue currently, as synapse only
// returns results for rooms we're joined to. // returns results for rooms we're joined to.
var roomName = room ? room.name : "Unknown room "+roomId; var roomName = room ? room.name : _t("Unknown room %(roomId)s", { roomId: roomId });
ret.push(<li key={mxEv.getId() + "-room"}> ret.push(<li key={mxEv.getId() + "-room"}>
<h1>Room: { roomName }</h1> <h1>{ _t("Room") }: { roomName }</h1>
</li>); </li>);
lastRoomId = roomId; lastRoomId = roomId;
} }
@ -1173,7 +1181,7 @@ module.exports = React.createClass({
}); });
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to save settings", title: _t("Failed to save settings"),
description: fails.map(function(result) { return result.reason; }).join("\n"), description: fails.map(function(result) { return result.reason; }).join("\n"),
}); });
// still editing room settings // still editing room settings
@ -1194,7 +1202,10 @@ module.exports = React.createClass({
onCancelClick: function() { onCancelClick: function() {
console.log("updateTint from onCancelClick"); console.log("updateTint from onCancelClick");
this.updateTint(); this.updateTint();
this.setState({editingRoomSettings: false}); this.setState({
editingRoomSettings: false,
forwardingEvent: null,
});
dis.dispatch({action: 'focus_composer'}); dis.dispatch({action: 'focus_composer'});
}, },
@ -1209,11 +1220,11 @@ module.exports = React.createClass({
MatrixClientPeg.get().forget(this.state.room.roomId).done(function() { MatrixClientPeg.get().forget(this.state.room.roomId).done(function() {
dis.dispatch({ action: 'view_next_room' }); dis.dispatch({ action: 'view_next_room' });
}, function(err) { }, function(err) {
var errCode = err.errcode || "unknown error code"; var errCode = err.errcode || _t("unknown error code");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: _t("Error"),
description: `Failed to forget room (${errCode})` description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
}); });
}); });
}, },
@ -1234,8 +1245,8 @@ module.exports = React.createClass({
var msg = error.message ? error.message : JSON.stringify(error); var msg = error.message ? error.message : JSON.stringify(error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to reject invite", title: _t("Failed to reject invite"),
description: msg description: msg,
}); });
self.setState({ self.setState({
@ -1472,61 +1483,61 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var RoomHeader = sdk.getComponent('rooms.RoomHeader'); const RoomHeader = sdk.getComponent('rooms.RoomHeader');
var MessageComposer = sdk.getComponent('rooms.MessageComposer'); const MessageComposer = sdk.getComponent('rooms.MessageComposer');
var RoomSettings = sdk.getComponent("rooms.RoomSettings"); const ForwardMessage = sdk.getComponent("rooms.ForwardMessage");
var AuxPanel = sdk.getComponent("rooms.AuxPanel"); const RoomSettings = sdk.getComponent("rooms.RoomSettings");
var SearchBar = sdk.getComponent("rooms.SearchBar"); const AuxPanel = sdk.getComponent("rooms.AuxPanel");
var ScrollPanel = sdk.getComponent("structures.ScrollPanel"); const SearchBar = sdk.getComponent("rooms.SearchBar");
var TintableSvg = sdk.getComponent("elements.TintableSvg"); const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
var RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
var Loader = sdk.getComponent("elements.Spinner"); const RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar");
var TimelinePanel = sdk.getComponent("structures.TimelinePanel"); const Loader = sdk.getComponent("elements.Spinner");
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
if (!this.state.room) { if (!this.state.room) {
if (this.state.roomLoading) { if (this.state.roomLoading) {
return ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<Loader /> <Loader />
</div> </div>
); );
} else {
var inviterName = undefined;
if (this.props.oobData) {
inviterName = this.props.oobData.inviterName;
}
var invitedEmail = undefined;
if (this.props.thirdPartyInvite) {
invitedEmail = this.props.thirdPartyInvite.invitedEmail;
} }
else {
var inviterName = undefined;
if (this.props.oobData) {
inviterName = this.props.oobData.inviterName;
}
var invitedEmail = undefined;
if (this.props.thirdPartyInvite) {
invitedEmail = this.props.thirdPartyInvite.invitedEmail;
}
// We have no room object for this room, only the ID. // We have no room object for this room, only the ID.
// We've got to this room by following a link, possibly a third party invite. // We've got to this room by following a link, possibly a third party invite.
var room_alias = this.props.roomAddress[0] == '#' ? this.props.roomAddress : null; var room_alias = this.props.roomAddress[0] == '#' ? this.props.roomAddress : null;
return ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<RoomHeader ref="header" <RoomHeader ref="header"
room={this.state.room} room={this.state.room}
oobData={this.props.oobData} oobData={this.props.oobData}
collapsedRhs={ this.props.collapsedRhs } collapsedRhs={ this.props.collapsedRhs }
/>
<div className="mx_RoomView_auxPanel">
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
onForgetClick={ this.onForgetClick }
onRejectClick={ this.onRejectThreepidInviteButtonClicked }
canPreview={ false } error={ this.state.roomLoadError }
roomAlias={room_alias}
spinner={this.state.joining}
inviterName={inviterName}
invitedEmail={invitedEmail}
room={this.state.room}
/> />
<div className="mx_RoomView_auxPanel">
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
onForgetClick={ this.onForgetClick }
onRejectClick={ this.onRejectThreepidInviteButtonClicked }
canPreview={ false } error={ this.state.roomLoadError }
roomAlias={room_alias}
spinner={this.state.joining}
inviterName={inviterName}
invitedEmail={invitedEmail}
room={this.state.room}
/>
</div>
<div className="mx_RoomView_messagePanel"></div>
</div> </div>
); <div className="mx_RoomView_messagePanel"></div>
} </div>
);
}
} }
var myUserId = MatrixClientPeg.get().credentials.userId; var myUserId = MatrixClientPeg.get().credentials.userId;
@ -1609,17 +1620,18 @@ module.exports = React.createClass({
/>; />;
} }
var aux = null; let aux = null;
let hideCancel = false;
if (this.state.editingRoomSettings) { if (this.state.editingRoomSettings) {
aux = <RoomSettings ref="room_settings" onSaveClick={this.onSettingsSaveClick} onCancelClick={this.onCancelClick} room={this.state.room} />; aux = <RoomSettings ref="room_settings" onSaveClick={this.onSettingsSaveClick} onCancelClick={this.onCancelClick} room={this.state.room} />;
} } else if (this.state.uploadingRoomSettings) {
else if (this.state.uploadingRoomSettings) {
aux = <Loader/>; aux = <Loader/>;
} } else if (this.state.forwardingEvent !== null) {
else if (this.state.searching) { aux = <ForwardMessage onCancelClick={this.onCancelClick} currentRoomId={this.state.room.roomId} mxEvent={this.state.forwardingEvent} />;
} else if (this.state.searching) {
hideCancel = true; // has own cancel
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress } onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch}/>; aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress } onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch}/>;
} } else if (!myMember || myMember.membership !== "join") {
else if (!myMember || myMember.membership !== "join") {
// We do have a room object for this room, but we're not currently in it. // We do have a room object for this room, but we're not currently in it.
// We may have a 3rd party invite to it. // We may have a 3rd party invite to it.
var inviterName = undefined; var inviterName = undefined;
@ -1630,6 +1642,7 @@ module.exports = React.createClass({
if (this.props.thirdPartyInvite) { if (this.props.thirdPartyInvite) {
invitedEmail = this.props.thirdPartyInvite.invitedEmail; invitedEmail = this.props.thirdPartyInvite.invitedEmail;
} }
hideCancel = true;
aux = ( aux = (
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} <RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={ this.onForgetClick } onForgetClick={ this.onForgetClick }
@ -1681,7 +1694,7 @@ module.exports = React.createClass({
if (call.type === "video") { if (call.type === "video") {
zoomButton = ( zoomButton = (
<div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title="Fill screen"> <div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title={ _t("Fill screen") }>
<TintableSvg src="img/fullscreen.svg" width="29" height="22" style={{ marginTop: 1, marginRight: 4 }}/> <TintableSvg src="img/fullscreen.svg" width="29" height="22" style={{ marginTop: 1, marginRight: 4 }}/>
</div> </div>
); );
@ -1689,14 +1702,14 @@ module.exports = React.createClass({
videoMuteButton = videoMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}> <div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}>
<TintableSvg src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"} <TintableSvg src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"}
alt={call.isLocalVideoMuted() ? "Click to unmute video" : "Click to mute video"} alt={call.isLocalVideoMuted() ? _t("Click to unmute video") : _t("Click to mute video")}
width="31" height="27"/> width="31" height="27"/>
</div>; </div>;
} }
voiceMuteButton = voiceMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}> <div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
<TintableSvg src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"} <TintableSvg src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"}
alt={call.isMicrophoneMuted() ? "Click to unmute audio" : "Click to mute audio"} alt={call.isMicrophoneMuted() ? _t("Click to unmute audio") : _t("Click to mute audio")}
width="21" height="26"/> width="21" height="26"/>
</div>; </div>;
@ -1732,14 +1745,13 @@ module.exports = React.createClass({
} }
// console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); // console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
var messagePanel = ( var messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef} <TimelinePanel ref={this._gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()} timelineSet={this.state.room.getUnfilteredTimelineSet()}
manageReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)} manageReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
manageReadMarkers={true} manageReadMarkers={true}
hidden={hideMessagePanel} hidden={hideMessagePanel}
highlightedEventId={this.props.highlightedEventId} highlightedEventId={this.state.forwardingEvent ? this.state.forwardingEvent.getId() : this.props.highlightedEventId}
eventId={this.props.eventId} eventId={this.props.eventId}
eventPixelOffset={this.props.eventPixelOffset} eventPixelOffset={this.props.eventPixelOffset}
onScroll={ this.onMessageListScroll } onScroll={ this.onMessageListScroll }
@ -1777,13 +1789,10 @@ module.exports = React.createClass({
onSearchClick={this.onSearchClick} onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick} onSettingsClick={this.onSettingsClick}
onSaveClick={this.onSettingsSaveClick} onSaveClick={this.onSettingsSaveClick}
onCancelClick={this.onCancelClick} onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={ onForgetClick={(myMember && myMember.membership === "leave") ? this.onForgetClick : null}
(myMember && myMember.membership === "leave") ? this.onForgetClick : null onLeaveClick={(myMember && myMember.membership === "join") ? this.onLeaveClick : null}
} />
onLeaveClick={
(myMember && myMember.membership === "join") ? this.onLeaveClick : null
} />
{ auxPanel } { auxPanel }
{ topUnreadMessagesBar } { topUnreadMessagesBar }
{ messagePanel } { messagePanel }

View file

@ -23,12 +23,14 @@ var Matrix = require("matrix-js-sdk");
var EventTimeline = Matrix.EventTimeline; var EventTimeline = Matrix.EventTimeline;
var sdk = require('../../index'); var sdk = require('../../index');
import { _t } from '../../languageHandler';
var MatrixClientPeg = require("../../MatrixClientPeg"); var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher"); var dis = require("../../dispatcher");
var ObjectUtils = require('../../ObjectUtils'); var ObjectUtils = require('../../ObjectUtils');
var Modal = require("../../Modal"); var Modal = require("../../Modal");
var UserActivity = require("../../UserActivity"); var UserActivity = require("../../UserActivity");
var KeyCode = require('../../KeyCode'); var KeyCode = require('../../KeyCode');
import UserSettingsStore from '../../UserSettingsStore';
var PAGINATE_SIZE = 20; var PAGINATE_SIZE = 20;
var INITIAL_SIZE = 20; var INITIAL_SIZE = 20;
@ -122,7 +124,7 @@ var TimelinePanel = React.createClass({
let initialReadMarker = null; let initialReadMarker = null;
if (this.props.manageReadMarkers) { if (this.props.manageReadMarkers) {
const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read'); const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read');
if (readmarker){ if (readmarker) {
initialReadMarker = readmarker.getContent().event_id; initialReadMarker = readmarker.getContent().event_id;
} else { } else {
initialReadMarker = this._getCurrentReadReceipt(); initialReadMarker = this._getCurrentReadReceipt();
@ -171,6 +173,12 @@ var TimelinePanel = React.createClass({
// cache of matrixClient.getSyncState() (but from the 'sync' event) // cache of matrixClient.getSyncState() (but from the 'sync' event)
clientSyncState: MatrixClientPeg.get().getSyncState(), clientSyncState: MatrixClientPeg.get().getSyncState(),
// should the event tiles have twelve hour times
isTwelveHour: UserSettingsStore.getSyncedSetting('showTwelveHourTimestamps'),
// always show timestamps on event tiles?
alwaysShowTimestamps: UserSettingsStore.getSyncedSetting('alwaysShowTimestamps'),
}; };
}, },
@ -907,14 +915,11 @@ var TimelinePanel = React.createClass({
}); });
}; };
} }
var message = "Tried to load a specific point in this room's timeline, but "; var message = (error.errcode == 'M_FORBIDDEN')
if (error.errcode == 'M_FORBIDDEN') { ? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question") + "."
message += "you do not have permission to view the message in question."; : _t("Tried to load a specific point in this room's timeline, but was unable to find it") + ".";
} else {
message += "was unable to find it.";
}
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to load timeline position", title: _t("Failed to load timeline position"),
description: message, description: message,
onFinished: onFinished, onFinished: onFinished,
}); });
@ -1106,7 +1111,6 @@ var TimelinePanel = React.createClass({
const forwardPaginating = ( const forwardPaginating = (
this.state.forwardPaginating || this.state.clientSyncState == 'PREPARED' this.state.forwardPaginating || this.state.clientSyncState == 'PREPARED'
); );
return ( return (
<MessagePanel ref="messagePanel" <MessagePanel ref="messagePanel"
hidden={ this.props.hidden } hidden={ this.props.hidden }
@ -1125,6 +1129,8 @@ var TimelinePanel = React.createClass({
onFillRequest={ this.onMessageListFillRequest } onFillRequest={ this.onMessageListFillRequest }
onUnfillRequest={ this.onMessageListUnfillRequest } onUnfillRequest={ this.onMessageListUnfillRequest }
opacity={ this.props.opacity } opacity={ this.props.opacity }
isTwelveHour={ this.state.isTwelveHour }
alwaysShowTimestamps={ this.state.alwaysShowTimestamps }
className={ this.props.className } className={ this.props.className }
tileShape={ this.props.tileShape } tileShape={ this.props.tileShape }
/> />

View file

@ -29,7 +29,10 @@ const GeminiScrollbar = require('react-gemini-scrollbar');
const Email = require('../../email'); const Email = require('../../email');
const AddThreepid = require('../../AddThreepid'); const AddThreepid = require('../../AddThreepid');
const SdkConfig = require('../../SdkConfig'); const SdkConfig = require('../../SdkConfig');
import Analytics from '../../Analytics';
import AccessibleButton from '../views/elements/AccessibleButton'; import AccessibleButton from '../views/elements/AccessibleButton';
import { _t } from '../../languageHandler';
import * as languageHandler from '../../languageHandler';
import * as FormattingUtils from '../../utils/FormattingUtils'; import * as FormattingUtils from '../../utils/FormattingUtils';
// if this looks like a release, use the 'version' from package.json; else use // if this looks like a release, use the 'version' from package.json; else use
@ -54,6 +57,8 @@ const gHVersionLabel = function(repo, token='') {
// Enumerate some simple 'flip a bit' UI settings (if any). // Enumerate some simple 'flip a bit' UI settings (if any).
// 'id' gives the key name in the im.vector.web.settings account data event // 'id' gives the key name in the im.vector.web.settings account data event
// 'label' is how we describe it in the UI. // 'label' is how we describe it in the UI.
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json,
// since they will be translated when rendered.
const SETTINGS_LABELS = [ const SETTINGS_LABELS = [
{ {
id: 'autoplayGifsAndVideos', id: 'autoplayGifsAndVideos',
@ -67,7 +72,6 @@ const SETTINGS_LABELS = [
id: 'dontSendTypingNotifications', id: 'dontSendTypingNotifications',
label: "Don't send typing notifications", label: "Don't send typing notifications",
}, },
/*
{ {
id: 'alwaysShowTimestamps', id: 'alwaysShowTimestamps',
label: 'Always show message timestamps', label: 'Always show message timestamps',
@ -80,6 +84,7 @@ const SETTINGS_LABELS = [
id: 'useCompactLayout', id: 'useCompactLayout',
label: 'Use compact timeline layout', label: 'Use compact timeline layout',
}, },
/*
{ {
id: 'useFixedWidthFont', id: 'useFixedWidthFont',
label: 'Use fixed width font', label: 'Use fixed width font',
@ -87,10 +92,25 @@ const SETTINGS_LABELS = [
*/ */
]; ];
const ANALYTICS_SETTINGS_LABELS = [
{
id: 'analyticsOptOut',
label: 'Opt out of analytics',
fn: function(checked) {
Analytics[checked ? 'disable' : 'enable']();
},
},
];
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json,
// since they will be translated when rendered.
const CRYPTO_SETTINGS_LABELS = [ const CRYPTO_SETTINGS_LABELS = [
{ {
id: 'blacklistUnverifiedDevices', id: 'blacklistUnverifiedDevices',
label: 'Never send encrypted messages to unverified devices from this device', label: 'Never send encrypted messages to unverified devices from this device',
fn: function(checked) {
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
},
}, },
// XXX: this is here for documentation; the actual setting is managed via RoomSettings // XXX: this is here for documentation; the actual setting is managed via RoomSettings
// { // {
@ -200,6 +220,17 @@ module.exports = React.createClass({
this._syncedSettings = syncedSettings; this._syncedSettings = syncedSettings;
this._localSettings = UserSettingsStore.getLocalSettings(); this._localSettings = UserSettingsStore.getLocalSettings();
if (PlatformPeg.get().isElectron()) {
const {ipcRenderer} = require('electron');
ipcRenderer.on('settings', this._electronSettings);
ipcRenderer.send('settings_get');
}
this.setState({
language: languageHandler.getCurrentLanguage(),
});
}, },
componentDidMount: function() { componentDidMount: function() {
@ -219,6 +250,15 @@ module.exports = React.createClass({
if (cli) { if (cli) {
cli.removeListener("RoomMember.membership", this._onInviteStateChange); cli.removeListener("RoomMember.membership", this._onInviteStateChange);
} }
if (PlatformPeg.get().isElectron()) {
const {ipcRenderer} = require('electron');
ipcRenderer.removeListener('settings', this._electronSettings);
}
},
_electronSettings: function(ev, settings) {
this.setState({ electron_settings: settings });
}, },
_refreshMediaDevices: function() { _refreshMediaDevices: function() {
@ -249,8 +289,8 @@ module.exports = React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to load user settings: " + error); console.error("Failed to load user settings: " + error);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Can't load user settings", title: _t("Can't load user settings"),
description: ((error && error.message) ? error.message : "Server may be unavailable or overloaded"), description: ((error && error.message) ? error.message : _t("Server may be unavailable or overloaded")),
}); });
}); });
}, },
@ -265,8 +305,8 @@ module.exports = React.createClass({
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, { Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register", title: _t("Please Register"),
description: "Guests can't set avatars. Please register.", description: _t("Guests can't set avatars. Please register."),
}); });
return; return;
} }
@ -291,8 +331,8 @@ module.exports = React.createClass({
console.error("Failed to set avatar: " + err); console.error("Failed to set avatar: " + err);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to set avatar", title: _t("Failed to set avatar."),
description: ((err && err.message) ? err.message : "Operation failed"), description: ((err && err.message) ? err.message : _t("Operation failed")),
}); });
}); });
}, },
@ -300,19 +340,19 @@ module.exports = React.createClass({
onLogoutClicked: function(ev) { onLogoutClicked: function(ev) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Sign out?", title: _t("Sign out"),
description: description:
<div> <div>
For security, logging out will delete any end-to-end encryption keys from this browser. { _t("For security, logging out will delete any end-to-end " +
"encryption keys from this browser. If you want to be able " +
If you want to be able to decrypt your conversation history from future Riot sessions, "to decrypt your conversation history from future Riot sessions, " +
please export your room keys for safe-keeping. "please export your room keys for safe-keeping.") }.
</div>, </div>,
button: "Sign out", button: _t("Sign out"),
extraButtons: [ extraButtons: [
<button key="export" className="mx_Dialog_primary" <button key="export" className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}> onClick={this._onExportE2eKeysClicked}>
Export E2E room keys { _t("Export E2E room keys") }
</button>, </button>,
], ],
onFinished: (confirmed) => { onFinished: (confirmed) => {
@ -329,14 +369,14 @@ module.exports = React.createClass({
onPasswordChangeError: function(err) { onPasswordChangeError: function(err) {
let errMsg = err.error || ""; let errMsg = err.error || "";
if (err.httpStatus === 403) { if (err.httpStatus === 403) {
errMsg = "Failed to change password. Is your password correct?"; errMsg = _t("Failed to change password. Is your password correct?");
} else if (err.httpStatus) { } else if (err.httpStatus) {
errMsg += ` (HTTP status ${err.httpStatus})`; errMsg += ` (HTTP status ${err.httpStatus})`;
} }
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change password: " + errMsg); console.error("Failed to change password: " + errMsg);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: _t("Error"),
description: errMsg, description: errMsg,
}); });
}, },
@ -344,10 +384,8 @@ module.exports = React.createClass({
onPasswordChanged: function() { onPasswordChanged: function() {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Success", title: _t("Success"),
description: `Your password was successfully changed. You will not description: _t("Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them") + ".",
receive push notifications on other devices until you
log back in to them.`,
}); });
}, },
@ -373,8 +411,8 @@ module.exports = React.createClass({
const emailAddress = this.refs.add_email_input.value; const emailAddress = this.refs.add_email_input.value;
if (!Email.looksValid(emailAddress)) { if (!Email.looksValid(emailAddress)) {
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Invalid Email Address", title: _t("Invalid Email Address"),
description: "This doesn't appear to be a valid email address", description: _t("This doesn't appear to be a valid email address"),
}); });
return; return;
} }
@ -383,17 +421,17 @@ module.exports = React.createClass({
// same here. // same here.
this._addThreepid.addEmailAddress(emailAddress, true).done(() => { this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Verification Pending", title: _t("Verification Pending"),
description: "Please check your email and click on the link it contains. Once this is done, click continue.", description: _t("Please check your email and click on the link it contains. Once this is done, click continue."),
button: 'Continue', button: _t('Continue'),
onFinished: this.onEmailDialogFinished, onFinished: this.onEmailDialogFinished,
}); });
}, (err) => { }, (err) => {
this.setState({email_add_pending: false}); this.setState({email_add_pending: false});
console.error("Unable to add email address " + emailAddress + " " + err); console.error("Unable to add email address " + emailAddress + " " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Unable to add email address", title: _t("Unable to add email address"),
description: ((err && err.message) ? err.message : "Operation failed"), description: ((err && err.message) ? err.message : _t("Operation failed")),
}); });
}); });
ReactDOM.findDOMNode(this.refs.add_email_input).blur(); ReactDOM.findDOMNode(this.refs.add_email_input).blur();
@ -403,9 +441,9 @@ module.exports = React.createClass({
onRemoveThreepidClicked: function(threepid) { onRemoveThreepidClicked: function(threepid) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Remove Contact Information?", title: _t("Remove Contact Information?"),
description: "Remove " + threepid.address + "?", description: _t("Remove %(threePid)s?", { threePid : threepid.address }),
button: 'Remove', button: _t('Remove'),
onFinished: (submit) => { onFinished: (submit) => {
if (submit) { if (submit) {
this.setState({ this.setState({
@ -417,8 +455,8 @@ module.exports = React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to remove contact information: " + err); console.error("Unable to remove contact information: " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Unable to remove contact information", title: _t("Unable to remove contact information"),
description: ((err && err.message) ? err.message : "Operation failed"), description: ((err && err.message) ? err.message : _t("Operation failed")),
}); });
}).done(); }).done();
} }
@ -444,22 +482,22 @@ module.exports = React.createClass({
this.setState({email_add_pending: false}); this.setState({email_add_pending: false});
}, (err) => { }, (err) => {
this.setState({email_add_pending: false}); this.setState({email_add_pending: false});
if (err.errcode === 'M_THREEPID_AUTH_FAILED') { if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let message = "Unable to verify email address. "; let message = _t("Unable to verify email address.") + " " +
message += "Please check your email and click on the link it contains. Once this is done, click continue."; _t("Please check your email and click on the link it contains. Once this is done, click continue.");
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Verification Pending", title: _t("Verification Pending"),
description: message, description: message,
button: 'Continue', button: _t('Continue'),
onFinished: this.onEmailDialogFinished, onFinished: this.onEmailDialogFinished,
}); });
} else { } else {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify email address: " + err); console.error("Unable to verify email address: " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Unable to verify email address", title: _t("Unable to verify email address."),
description: ((err && err.message) ? err.message : "Operation failed"), description: ((err && err.message) ? err.message : _t("Operation failed")),
}); });
} }
}); });
@ -549,20 +587,43 @@ module.exports = React.createClass({
<div> <div>
<h3>Referral</h3> <h3>Referral</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
Refer a friend to Riot: <a href={href}>{href}</a> {_t("Refer a friend to Riot:")} <a href={href}>{href}</a>
</div> </div>
</div> </div>
); );
}, },
onLanguageChange: function(newLang) {
if(this.state.language !== newLang) {
UserSettingsStore.setLocalSetting('language', newLang);
this.setState({
language: newLang,
});
PlatformPeg.get().reload();
}
},
_renderLanguageSetting: function () {
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
return <div>
<label htmlFor="languageSelector">{_t('Interface Language')}</label>
<LanguageDropdown ref="language" onOptionChange={this.onLanguageChange}
className="mx_UserSettings_language"
value={this.state.language}
/>
</div>;
},
_renderUserInterfaceSettings: function() { _renderUserInterfaceSettings: function() {
return ( return (
<div> <div>
<h3>User Interface</h3> <h3>{ _t("User Interface") }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
{ this._renderUrlPreviewSelector() } { this._renderUrlPreviewSelector() }
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) } { SETTINGS_LABELS.map( this._renderSyncedSetting ) }
{ THEMES.map( this._renderThemeSelector ) } { THEMES.map( this._renderThemeSelector ) }
{ this._renderLanguageSetting() }
</div> </div>
</div> </div>
); );
@ -576,7 +637,7 @@ module.exports = React.createClass({
onChange={ (e) => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) } onChange={ (e) => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
/> />
<label htmlFor="urlPreviewsDisabled"> <label htmlFor="urlPreviewsDisabled">
Disable inline URL previews by default { _t("Disable inline URL previews by default") }
</label> </label>
</div>; </div>;
}, },
@ -586,10 +647,15 @@ module.exports = React.createClass({
<input id={ setting.id } <input id={ setting.id }
type="checkbox" type="checkbox"
defaultChecked={ this._syncedSettings[setting.id] } defaultChecked={ this._syncedSettings[setting.id] }
onChange={ (e) => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) } onChange={
(e) => {
UserSettingsStore.setSyncedSetting(setting.id, e.target.checked);
if (setting.fn) setting.fn(e.target.checked);
}
}
/> />
<label htmlFor={ setting.id }> <label htmlFor={ setting.id }>
{ setting.label } { _t(setting.label) }
</label> </label>
</div>; </div>;
}, },
@ -623,7 +689,7 @@ module.exports = React.createClass({
const deviceId = client.deviceId; const deviceId = client.deviceId;
let identityKey = client.getDeviceEd25519Key(); let identityKey = client.getDeviceEd25519Key();
if (!identityKey) { if (!identityKey) {
identityKey = "<not supported>"; identityKey = _t("<not supported>");
} else { } else {
identityKey = FormattingUtils.formatCryptoKey(identityKey); identityKey = FormattingUtils.formatCryptoKey(identityKey);
} }
@ -635,18 +701,18 @@ module.exports = React.createClass({
<div className="mx_UserSettings_importExportButtons"> <div className="mx_UserSettings_importExportButtons">
<AccessibleButton className="mx_UserSettings_button" <AccessibleButton className="mx_UserSettings_button"
onClick={this._onExportE2eKeysClicked}> onClick={this._onExportE2eKeysClicked}>
Export E2E room keys { _t("Export E2E room keys") }
</AccessibleButton> </AccessibleButton>
<AccessibleButton className="mx_UserSettings_button" <AccessibleButton className="mx_UserSettings_button"
onClick={this._onImportE2eKeysClicked}> onClick={this._onImportE2eKeysClicked}>
Import E2E room keys { _t("Import E2E room keys") }
</AccessibleButton> </AccessibleButton>
</div> </div>
); );
} }
return ( return (
<div> <div>
<h3>Cryptography</h3> <h3>{ _t("Cryptography") }</h3>
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection"> <div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
<ul> <ul>
<li><label>Device ID:</label> <span><code>{deviceId}</code></span></li> <li><label>Device ID:</label> <span><code>{deviceId}</code></span></li>
@ -662,7 +728,6 @@ module.exports = React.createClass({
}, },
_renderLocalSetting: function(setting) { _renderLocalSetting: function(setting) {
const client = MatrixClientPeg.get();
return <div className="mx_UserSettings_toggle" key={ setting.id }> return <div className="mx_UserSettings_toggle" key={ setting.id }>
<input id={ setting.id } <input id={ setting.id }
type="checkbox" type="checkbox"
@ -670,14 +735,12 @@ module.exports = React.createClass({
onChange={ onChange={
(e) => { (e) => {
UserSettingsStore.setLocalSetting(setting.id, e.target.checked); UserSettingsStore.setLocalSetting(setting.id, e.target.checked);
if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly if (setting.fn) setting.fn(e.target.checked);
client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
}
} }
} }
/> />
<label htmlFor={ setting.id }> <label htmlFor={ setting.id }>
{ setting.label } { _t(setting.label) }
</label> </label>
</div>; </div>;
}, },
@ -698,20 +761,31 @@ module.exports = React.createClass({
} }
return ( return (
<div> <div>
<h3>Bug Report</h3> <h3>{ _t("Bug Report") }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
<p>Found a bug?</p> <p>{ _t("Found a bug?") }</p>
<button className="mx_UserSettings_button danger" <button className="mx_UserSettings_button danger"
onClick={this._onBugReportClicked}>Report it onClick={this._onBugReportClicked}>{_t('Report it')}
</button> </button>
</div> </div>
</div> </div>
); );
}, },
_renderAnalyticsControl: function() {
return <div>
<h3>{ _t('Analytics') }</h3>
<div className="mx_UserSettings_section">
{_t('Riot collects anonymous analytics to allow us to improve the application.')}
{ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting )}
</div>
</div>;
},
_renderLabs: function() { _renderLabs: function() {
// default to enabled if undefined // default to enabled if undefined
if (this.props.enableLabs === false) return null; if (this.props.enableLabs === false) return null;
UserSettingsStore.doTranslations();
const features = UserSettingsStore.LABS_FEATURES.map((feature) => ( const features = UserSettingsStore.LABS_FEATURES.map((feature) => (
<div key={feature.id} className="mx_UserSettings_toggle"> <div key={feature.id} className="mx_UserSettings_toggle">
@ -725,8 +799,8 @@ module.exports = React.createClass({
e.target.checked = false; e.target.checked = false;
const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, { Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register", title: _t("Please Register"),
description: "Guests can't use labs features. Please register.", description: _t("Guests can't use labs features. Please register."),
}); });
return; return;
} }
@ -739,9 +813,9 @@ module.exports = React.createClass({
)); ));
return ( return (
<div> <div>
<h3>Labs</h3> <h3>{ _t("Labs") }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
<p>These are experimental features that may break in unexpected ways. Use with caution.</p> <p>{ _t("These are experimental features that may break in unexpected ways") }. { _t("Use with caution") }.</p>
{features} {features}
</div> </div>
</div> </div>
@ -753,10 +827,10 @@ module.exports = React.createClass({
if (MatrixClientPeg.get().isGuest()) return null; if (MatrixClientPeg.get().isGuest()) return null;
return <div> return <div>
<h3>Deactivate Account</h3> <h3>{ _t("Deactivate Account") }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
<AccessibleButton className="mx_UserSettings_button danger" <AccessibleButton className="mx_UserSettings_button danger"
onClick={this._onDeactivateAccountClicked}>Deactivate my account onClick={this._onDeactivateAccountClicked}> { _t("Deactivate my account") }
</AccessibleButton> </AccessibleButton>
</div> </div>
</div>; </div>;
@ -764,11 +838,11 @@ module.exports = React.createClass({
_renderClearCache: function() { _renderClearCache: function() {
return <div> return <div>
<h3>Clear Cache</h3> <h3>{ _t("Clear Cache") }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
<AccessibleButton className="mx_UserSettings_button danger" <AccessibleButton className="mx_UserSettings_button danger"
onClick={this._onClearCacheClicked}> onClick={this._onClearCacheClicked}>
Clear Cache and Reload { _t("Clear Cache and Reload") }
</AccessibleButton> </AccessibleButton>
</div> </div>
</div>; </div>;
@ -791,19 +865,42 @@ module.exports = React.createClass({
reject = ( reject = (
<AccessibleButton className="mx_UserSettings_button danger" <AccessibleButton className="mx_UserSettings_button danger"
onClick={this._onRejectAllInvitesClicked.bind(this, invitedRooms)}> onClick={this._onRejectAllInvitesClicked.bind(this, invitedRooms)}>
Reject all {invitedRooms.length} invites {_t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length})}
</AccessibleButton> </AccessibleButton>
); );
} }
return <div> return <div>
<h3>Bulk Options</h3> <h3>{ _t("Bulk Options") }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
{reject} {reject}
</div> </div>
</div>; </div>;
}, },
_renderElectronSettings: function() {
const settings = this.state.electron_settings;
if (!settings) return;
const {ipcRenderer} = require('electron');
return <div>
<h3>{ _t('Desktop specific') }</h3>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_toggle">
<input type="checkbox"
name="auto-launch"
defaultChecked={settings['auto-launch']}
onChange={(e) => {
ipcRenderer.send('settings_set', 'auto-launch', e.target.checked);
}}
/>
<label htmlFor="auto-launch">{_t('Start automatically after system login')}</label>
</div>
</div>
</div>;
},
_mapWebRtcDevicesToSpans: function(devices) { _mapWebRtcDevicesToSpans: function(devices) {
return Object.keys(devices).map( return Object.keys(devices).map(
(deviceId) => <span key={deviceId}>{devices[deviceId]}</span> (deviceId) => <span key={deviceId}>{devices[deviceId]}</span>
@ -904,7 +1001,8 @@ module.exports = React.createClass({
}, },
nameForMedium: function(medium) { nameForMedium: function(medium) {
if (medium === 'msisdn') return 'Phone'; if (medium === 'msisdn') return _t('Phone');
if (medium === 'email') return _t('Email');
return medium[0].toUpperCase() + medium.slice(1); return medium[0].toUpperCase() + medium.slice(1);
}, },
@ -953,7 +1051,7 @@ module.exports = React.createClass({
/> />
</div> </div>
<div className="mx_UserSettings_threepidButton mx_filterFlipColor"> <div className="mx_UserSettings_threepidButton mx_filterFlipColor">
<img src="img/cancel-small.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} /> <img src="img/cancel-small.svg" width="14" height="14" alt={ _t("Remove") } onClick={this.onRemoveThreepidClicked.bind(this, val)} />
</div> </div>
</div> </div>
); );
@ -965,14 +1063,14 @@ module.exports = React.createClass({
addEmailSection = ( addEmailSection = (
<div className="mx_UserSettings_profileTableRow" key="_newEmail"> <div className="mx_UserSettings_profileTableRow" key="_newEmail">
<div className="mx_UserSettings_profileLabelCell"> <div className="mx_UserSettings_profileLabelCell">
<label>Email</label> <label>{_t('Email')}</label>
</div> </div>
<div className="mx_UserSettings_profileInputCell"> <div className="mx_UserSettings_profileInputCell">
<EditableText <EditableText
ref="add_email_input" ref="add_email_input"
className="mx_UserSettings_editable" className="mx_UserSettings_editable"
placeholderClassName="mx_UserSettings_threepidPlaceholder" placeholderClassName="mx_UserSettings_threepidPlaceholder"
placeholder={ "Add email address" } placeholder={ _t("Add email address") }
blurToCancel={ false } blurToCancel={ false }
onValueChanged={ this._onAddEmailEditFinished } /> onValueChanged={ this._onAddEmailEditFinished } />
</div> </div>
@ -994,7 +1092,7 @@ module.exports = React.createClass({
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
accountJsx = ( accountJsx = (
<div className="mx_UserSettings_button" onClick={this.onUpgradeClicked}> <div className="mx_UserSettings_button" onClick={this.onUpgradeClicked}>
Create an account { _t("Create an account") }
</div> </div>
); );
} else { } else {
@ -1012,7 +1110,7 @@ module.exports = React.createClass({
let notificationArea; let notificationArea;
if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) { if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) {
notificationArea = (<div> notificationArea = (<div>
<h3>Notifications</h3> <h3>{ _t("Notifications") }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
<Notifications threepids={this.state.threepids} brand={this.props.brand} /> <Notifications threepids={this.state.threepids} brand={this.props.brand} />
@ -1031,7 +1129,7 @@ module.exports = React.createClass({
return ( return (
<div className="mx_UserSettings"> <div className="mx_UserSettings">
<SimpleRoomHeader <SimpleRoomHeader
title="Settings" title={ _t("Settings") }
collapsedRhs={ this.props.collapsedRhs } collapsedRhs={ this.props.collapsedRhs }
onCancelClick={ this.props.onClose } onCancelClick={ this.props.onClose }
/> />
@ -1039,13 +1137,13 @@ module.exports = React.createClass({
<GeminiScrollbar className="mx_UserSettings_body" <GeminiScrollbar className="mx_UserSettings_body"
autoshow={true}> autoshow={true}>
<h3>Profile</h3> <h3>{ _t("Profile") }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
<div className="mx_UserSettings_profileTable"> <div className="mx_UserSettings_profileTable">
<div className="mx_UserSettings_profileTableRow"> <div className="mx_UserSettings_profileTableRow">
<div className="mx_UserSettings_profileLabelCell"> <div className="mx_UserSettings_profileLabelCell">
<label htmlFor="displayName">Display name</label> <label htmlFor="displayName">{ _t('Display name') }</label>
</div> </div>
<div className="mx_UserSettings_profileInputCell"> <div className="mx_UserSettings_profileInputCell">
<ChangeDisplayName /> <ChangeDisplayName />
@ -1062,7 +1160,7 @@ module.exports = React.createClass({
<div className="mx_UserSettings_avatarPicker_edit"> <div className="mx_UserSettings_avatarPicker_edit">
<label htmlFor="avatarInput" ref="file_label"> <label htmlFor="avatarInput" ref="file_label">
<img src="img/camera.svg" className="mx_filterFlipColor" <img src="img/camera.svg" className="mx_filterFlipColor"
alt="Upload avatar" title="Upload avatar" alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
width="17" height="15" /> width="17" height="15" />
</label> </label>
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/> <input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
@ -1070,12 +1168,12 @@ module.exports = React.createClass({
</div> </div>
</div> </div>
<h3>Account</h3> <h3>{ _t("Account") }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section cadcampoHide">
<AccessibleButton className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}> <AccessibleButton className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
Sign out { _t("Sign out") }
</AccessibleButton> </AccessibleButton>
{accountJsx} {accountJsx}
@ -1093,34 +1191,35 @@ module.exports = React.createClass({
{this._renderBulkOptions()} {this._renderBulkOptions()}
{this._renderBugReport()} {this._renderBugReport()}
<h3>Advanced</h3> {PlatformPeg.get().isElectron() && this._renderElectronSettings()}
{this._renderAnalyticsControl()}
<h3>{ _t("Advanced") }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
<div className="mx_UserSettings_advanced"> <div className="mx_UserSettings_advanced">
Logged in as {this._me} { _t("Logged in as:") } {this._me}
</div> </div>
<div className="mx_UserSettings_advanced"> <div className="mx_UserSettings_advanced">
Access Token: <span className="mx_UserSettings_advanced_spoiler" {_t('Access Token:')} <span className="mx_UserSettings_advanced_spoiler" onClick={this._showSpoiler} data-spoiler={ MatrixClientPeg.get().getAccessToken() }>&lt;{ _t("click to reveal") }&gt;</span>
onClick={this._showSpoiler}
data-spoiler={ MatrixClientPeg.get().getAccessToken() }
>&lt;click to reveal&gt;</span>
</div> </div>
<div className="mx_UserSettings_advanced"> <div className="mx_UserSettings_advanced">
Homeserver is { MatrixClientPeg.get().getHomeserverUrl() } { _t("Homeserver is") } { MatrixClientPeg.get().getHomeserverUrl() }
</div> </div>
<div className="mx_UserSettings_advanced"> <div className="mx_UserSettings_advanced">
Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() } { _t("Identity Server is") } { MatrixClientPeg.get().getIdentityServerUrl() }
</div> </div>
<div className="mx_UserSettings_advanced"> <div className="mx_UserSettings_advanced">
matrix-react-sdk version: {(REACT_SDK_VERSION !== '<local>') {_t('matrix-react-sdk version:')} {(REACT_SDK_VERSION !== '<local>')
? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION) ? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
: REACT_SDK_VERSION : REACT_SDK_VERSION
}<br/> }<br/>
riot-web version: {(this.state.vectorVersion !== undefined) {_t('riot-web version:')} {(this.state.vectorVersion !== undefined)
? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion) ? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
: 'unknown' : 'unknown'
}<br/> }<br/>
olm version: {olmVersionString}<br/> { _t("olm version:") } {olmVersionString}<br/>
</div> </div>
</div> </div>

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); var React = require('react');
import { _t } from '../../../languageHandler';
var sdk = require('../../../index'); var sdk = require('../../../index');
var Modal = require("../../../Modal"); var Modal = require("../../../Modal");
var MatrixClientPeg = require('../../../MatrixClientPeg'); var MatrixClientPeg = require('../../../MatrixClientPeg');
@ -54,7 +55,7 @@ module.exports = React.createClass({
progress: "sent_email" progress: "sent_email"
}); });
}, (err) => { }, (err) => {
this.showErrorDialog("Failed to send email: " + err.message); this.showErrorDialog(_t('Failed to send email') + ": " + err.message);
this.setState({ this.setState({
progress: null progress: null
}); });
@ -78,30 +79,33 @@ module.exports = React.createClass({
ev.preventDefault(); ev.preventDefault();
if (!this.state.email) { if (!this.state.email) {
this.showErrorDialog("The email address linked to your account must be entered."); this.showErrorDialog(_t('The email address linked to your account must be entered.'));
} }
else if (!this.state.password || !this.state.password2) { else if (!this.state.password || !this.state.password2) {
this.showErrorDialog("A new password must be entered."); this.showErrorDialog(_t('A new password must be entered.'));
} }
else if (this.state.password !== this.state.password2) { else if (this.state.password !== this.state.password2) {
this.showErrorDialog("New passwords must match each other."); this.showErrorDialog(_t('New passwords must match each other.'));
} }
else { else {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Warning", title: _t('Warning!'),
description: description:
<div> <div>
Resetting password will currently reset any end-to-end encryption keys on all devices, { _t(
making encrypted chat history unreadable, unless you first export your room keys 'Resetting password will currently reset any ' +
and re-import them afterwards. 'end-to-end encryption keys on all devices, ' +
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">will be improved</a>. 'making encrypted chat history unreadable, ' +
'unless you first export your room keys and re-import ' +
'them afterwards. In future this will be improved.'
) }
</div>, </div>,
button: "Continue", button: _t('Continue'),
extraButtons: [ extraButtons: [
<button className="mx_Dialog_primary" <button className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}> onClick={this._onExportE2eKeysClicked}>
Export E2E room keys { _t('Export E2E room keys') }
</button> </button>
], ],
onFinished: (confirmed) => { onFinished: (confirmed) => {
@ -150,7 +154,7 @@ module.exports = React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: title, title: title,
description: body description: body,
}); });
}, },
@ -168,22 +172,20 @@ module.exports = React.createClass({
else if (this.state.progress === "sent_email") { else if (this.state.progress === "sent_email") {
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div>
An email has been sent to {this.state.email}. Once you&#39;ve followed { _t('An email has been sent to') } {this.state.email}. { _t('Once you&#39;ve followed the link it contains, click below') }.
the link it contains, click below.
<br /> <br />
<input className="mx_Login_submit" type="button" onClick={this.onVerify} <input className="mx_Login_submit" type="button" onClick={this.onVerify}
value="I have verified my email address" /> value={ _t('I have verified my email address') } />
</div> </div>
); );
} }
else if (this.state.progress === "complete") { else if (this.state.progress === "complete") {
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div>
<p>Your password has been reset.</p> <p>{ _t('Your password has been reset') }.</p>
<p>You have been logged out of all devices and will no longer receive push notifications. <p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p>
To re-enable notifications, sign in again on each device.</p>
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete} <input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
value="Return to login screen" /> value={ _t('Return to login screen') } />
</div> </div>
); );
} }
@ -191,7 +193,7 @@ module.exports = React.createClass({
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div>
<div className="mx_Login_prompt"> <div className="mx_Login_prompt">
To reset your password, enter the email address linked to your account: { _t('To reset your password, enter the email address linked to your account') }:
</div> </div>
<div> <div>
<form onSubmit={this.onSubmitForm}> <form onSubmit={this.onSubmitForm}>
@ -199,21 +201,21 @@ module.exports = React.createClass({
name="reset_email" // define a name so browser's password autofill gets less confused name="reset_email" // define a name so browser's password autofill gets less confused
value={this.state.email} value={this.state.email}
onChange={this.onInputChanged.bind(this, "email")} onChange={this.onInputChanged.bind(this, "email")}
placeholder="Email address" autoFocus /> placeholder={ _t('Email address') } autoFocus />
<br /> <br />
<input className="mx_Login_field" ref="pass" type="password" <input className="mx_Login_field" ref="pass" type="password"
name="reset_password" name="reset_password"
value={this.state.password} value={this.state.password}
onChange={this.onInputChanged.bind(this, "password")} onChange={this.onInputChanged.bind(this, "password")}
placeholder="New password" /> placeholder={ _t('New password') } />
<br /> <br />
<input className="mx_Login_field" ref="pass" type="password" <input className="mx_Login_field" ref="pass" type="password"
name="reset_password_confirm" name="reset_password_confirm"
value={this.state.password2} value={this.state.password2}
onChange={this.onInputChanged.bind(this, "password2")} onChange={this.onInputChanged.bind(this, "password2")}
placeholder="Confirm your new password" /> placeholder={ _t('Confirm your new password') } />
<br /> <br />
<input className="mx_Login_submit" type="submit" value="Send Reset Email" /> <input className="mx_Login_submit" type="submit" value={ _t('Send Reset Email') } />
</form> </form>
<ServerConfig ref="serverConfig" <ServerConfig ref="serverConfig"
withToggleButton={true} withToggleButton={true}
@ -230,7 +232,7 @@ module.exports = React.createClass({
Return to login Return to login
</a> </a>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#"> <a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
Create a new account { _t('Create an account') }
</a> </a>
<LoginFooter /> <LoginFooter />
</div> </div>

View file

@ -18,8 +18,8 @@ limitations under the License.
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import { _t, _tJsx } from '../../../languageHandler';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import url from 'url';
import sdk from '../../../index'; import sdk from '../../../index';
import Login from '../../../Login'; import Login from '../../../Login';
@ -223,14 +223,19 @@ module.exports = React.createClass({
!this.state.enteredHomeserverUrl.startsWith("http"))) !this.state.enteredHomeserverUrl.startsWith("http")))
{ {
errorText = <span> errorText = <span>
Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. { _tJsx("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
Either use HTTPS or <a href='https://www.google.com/search?&q=enable%20unsafe%20scripts'>enable unsafe scripts</a> "Either use HTTPS or <a>enable unsafe scripts</a>.",
/<a>(.*?)<\/a>/,
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; }
)}
</span>; </span>;
} }
else { else {
errorText = <span> errorText = <span>
Can't connect to homeserver - please check your connectivity and ensure { _tJsx("Can't connect to homeserver - please check your connectivity and ensure your <a>homeserver's SSL certificate</a> is trusted.",
your <a href={ this.state.enteredHomeserverUrl }>homeserver's SSL certificate</a> is trusted. /<a>(.*?)<\/a>/,
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; }
)}
</span>; </span>;
} }
} }
@ -242,12 +247,6 @@ module.exports = React.createClass({
switch (step) { switch (step) {
case 'm.login.password': case 'm.login.password':
const PasswordLogin = sdk.getComponent('login.PasswordLogin'); const PasswordLogin = sdk.getComponent('login.PasswordLogin');
// HSs that are not matrix.org may not be configured to have their
// domain name === domain part.
let hsDomain = url.parse(this.state.enteredHomeserverUrl).hostname;
if (hsDomain !== 'matrix.org') {
hsDomain = null;
}
return ( return (
<PasswordLogin <PasswordLogin
onSubmit={this.onPasswordLogin} onSubmit={this.onPasswordLogin}
@ -259,7 +258,6 @@ module.exports = React.createClass({
onPhoneNumberChanged={this.onPhoneNumberChanged} onPhoneNumberChanged={this.onPhoneNumberChanged}
onForgotPasswordClick={this.props.onForgotPasswordClick} onForgotPasswordClick={this.props.onForgotPasswordClick}
loginIncorrect={this.state.loginIncorrect} loginIncorrect={this.state.loginIncorrect}
hsDomain={hsDomain}
/> />
); );
case 'm.login.cas': case 'm.login.cas':
@ -273,8 +271,7 @@ module.exports = React.createClass({
} }
return ( return (
<div> <div>
Sorry, this homeserver is using a login which is not { _t('Sorry, this homeserver is using a login which is not recognised ')}({step})
recognised ({step})
</div> </div>
); );
} }
@ -291,7 +288,7 @@ module.exports = React.createClass({
if (this.props.enableGuest) { if (this.props.enableGuest) {
loginAsGuestJsx = loginAsGuestJsx =
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#"> <a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
Login as guest { _t('Login as guest')}
</a>; </a>;
} }
@ -299,7 +296,7 @@ module.exports = React.createClass({
if (this.props.onCancelClick) { if (this.props.onCancelClick) {
returnToAppJsx = returnToAppJsx =
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#"> <a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
Return to app { _t('Return to app')}
</a>; </a>;
} }
@ -308,7 +305,7 @@ module.exports = React.createClass({
<div className="mx_Login_box"> <div className="mx_Login_box">
<LoginHeader /> <LoginHeader />
<div> <div>
<h2>Sign in <h2>{ _t('Sign in')}
{ loader } { loader }
</h2> </h2>
{ this.componentForStep(this.state.currentFlow) } { this.componentForStep(this.state.currentFlow) }
@ -324,7 +321,7 @@ module.exports = React.createClass({
{ this.state.errorText } { this.state.errorText }
</div> </div>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#"> <a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
Create a new account { _t('Create an account')}
</a> </a>
{ loginAsGuestJsx } { loginAsGuestJsx }
{ returnToAppJsx } { returnToAppJsx }

View file

@ -16,9 +16,10 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); import React from 'react';
var sdk = require('../../../index'); import sdk from '../../../index';
var MatrixClientPeg = require('../../../MatrixClientPeg'); import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'PostRegistration', displayName: 'PostRegistration',
@ -64,12 +65,12 @@ module.exports = React.createClass({
<div className="mx_Login_box"> <div className="mx_Login_box">
<LoginHeader /> <LoginHeader />
<div className="mx_Login_profile"> <div className="mx_Login_profile">
Set a display name: { _t('Set a display name:') }
<ChangeDisplayName /> <ChangeDisplayName />
Upload an avatar: { _t('Upload an avatar:') }
<ChangeAvatar <ChangeAvatar
initialAvatarUrl={this.state.avatarUrl} /> initialAvatarUrl={this.state.avatarUrl} />
<button onClick={this.props.onComplete}>Continue</button> <button onClick={this.props.onComplete}>{ _t('Continue') }</button>
{this.state.errorString} {this.state.errorString}
</div> </div>
</div> </div>

View file

@ -27,6 +27,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import RegistrationForm from '../../views/login/RegistrationForm'; import RegistrationForm from '../../views/login/RegistrationForm';
import CaptchaForm from '../../views/login/CaptchaForm'; import CaptchaForm from '../../views/login/CaptchaForm';
import RtsClient from '../../../RtsClient'; import RtsClient from '../../../RtsClient';
import { _t } from '../../../languageHandler';
const MIN_PASSWORD_LENGTH = 6; const MIN_PASSWORD_LENGTH = 6;
@ -162,7 +163,7 @@ module.exports = React.createClass({
msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1; msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1;
} }
if (!msisdn_available) { if (!msisdn_available) {
msg = "This server does not support authentication with a phone number"; msg = _t('This server does not support authentication with a phone number.');
} }
} }
this.setState({ this.setState({
@ -260,29 +261,29 @@ module.exports = React.createClass({
var errMsg; var errMsg;
switch (errCode) { switch (errCode) {
case "RegistrationForm.ERR_PASSWORD_MISSING": case "RegistrationForm.ERR_PASSWORD_MISSING":
errMsg = "Missing password."; errMsg = _t('Missing password.');
break; break;
case "RegistrationForm.ERR_PASSWORD_MISMATCH": case "RegistrationForm.ERR_PASSWORD_MISMATCH":
errMsg = "Passwords don't match."; errMsg = _t('Passwords don\'t match.');
break; break;
case "RegistrationForm.ERR_PASSWORD_LENGTH": case "RegistrationForm.ERR_PASSWORD_LENGTH":
errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`; errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH: MIN_PASSWORD_LENGTH});
break; break;
case "RegistrationForm.ERR_EMAIL_INVALID": case "RegistrationForm.ERR_EMAIL_INVALID":
errMsg = "This doesn't look like a valid email address"; errMsg = _t('This doesn\'t look like a valid email address.');
break; break;
case "RegistrationForm.ERR_PHONE_NUMBER_INVALID": case "RegistrationForm.ERR_PHONE_NUMBER_INVALID":
errMsg = "This doesn't look like a valid phone number"; errMsg = _t('This doesn\'t look like a valid phone number.');
break; break;
case "RegistrationForm.ERR_USERNAME_INVALID": case "RegistrationForm.ERR_USERNAME_INVALID":
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores."; errMsg = _t('User names may only contain letters, numbers, dots, hyphens and underscores.');
break; break;
case "RegistrationForm.ERR_USERNAME_BLANK": case "RegistrationForm.ERR_USERNAME_BLANK":
errMsg = "You need to enter a user name"; errMsg = _t('You need to enter a user name.');
break; break;
default: default:
console.error("Unknown error code: %s", errCode); console.error("Unknown error code: %s", errCode);
errMsg = "An unknown error occurred."; errMsg = _t('An unknown error occurred.');
break; break;
} }
this.setState({ this.setState({
@ -400,7 +401,7 @@ module.exports = React.createClass({
if (this.props.onCancelClick) { if (this.props.onCancelClick) {
returnToAppJsx = ( returnToAppJsx = (
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#"> <a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
Return to app {_t('Return to app')}
</a> </a>
); );
} }
@ -413,10 +414,10 @@ module.exports = React.createClass({
this.state.teamSelected.domain + "/icon.png" : this.state.teamSelected.domain + "/icon.png" :
null} null}
/> />
<h2>Create an account</h2> <h2>{_t('Create an account')}</h2>
{registerBody} {registerBody}
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#"> <a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
I already have an account {_t('I already have an account')}
</a> </a>
{returnToAppJsx} {returnToAppJsx}
<LoginFooter /> <LoginFooter />

View file

@ -16,8 +16,8 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); import React from 'react';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'CreateRoomButton', displayName: 'CreateRoomButton',
propTypes: { propTypes: {
@ -36,7 +36,7 @@ module.exports = React.createClass({
render: function() { render: function() {
return ( return (
<button className="mx_CreateRoomButton" onClick={this.onClick}>Create Room</button> <button className="mx_CreateRoomButton" onClick={this.onClick}>{_t("Create Room")}</button>
); );
} }
}); });

View file

@ -67,7 +67,7 @@ export default React.createClass({
render: function() { render: function() {
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
return ( return (
<div onKeyDown={this._onKeyDown} className={this.props.className}> <div onKeyDown={this._onKeyDown} className={this.props.className}>
<AccessibleButton onClick={this._onCancelClick} <AccessibleButton onClick={this._onCancelClick}

View file

@ -86,7 +86,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
<div className="mx_RoomTile_avatar"> <div className="mx_RoomTile_avatar">
<img src="img/create-big.svg" width="26" height="26" /> <img src="img/create-big.svg" width="26" height="26" />
</div> </div>
<div className={labelClasses}><i>Start new chat</i></div> <div className={labelClasses}><i>{_("Start new chat")}</i></div>
</AccessibleButton>; </AccessibleButton>;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import { getAddressType, inviteMultipleToRoom } from '../../../Invite'; import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
import createRoom from '../../../createRoom'; import createRoom from '../../../createRoom';
@ -26,14 +27,13 @@ import dis from '../../../dispatcher';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import q from 'q'; import q from 'q';
import Fuse from 'fuse.js';
const TRUNCATE_QUERY_LIST = 40; const TRUNCATE_QUERY_LIST = 40;
module.exports = React.createClass({ module.exports = React.createClass({
displayName: "ChatInviteDialog", displayName: "ChatInviteDialog",
propTypes: { propTypes: {
title: React.PropTypes.string, title: React.PropTypes.string.isRequired,
description: React.PropTypes.oneOfType([ description: React.PropTypes.oneOfType([
React.PropTypes.element, React.PropTypes.element,
React.PropTypes.string, React.PropTypes.string,
@ -48,11 +48,7 @@ module.exports = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
title: "Start a chat",
description: "Who would you like to communicate with?",
value: "", value: "",
placeholder: "Email, name or matrix ID",
button: "Start Chat",
focus: true focus: true
}; };
}, },
@ -77,19 +73,6 @@ module.exports = React.createClass({
// Set the cursor at the end of the text input // Set the cursor at the end of the text input
this.refs.textinput.value = this.props.value; this.refs.textinput.value = this.props.value;
} }
// Create a Fuse instance for fuzzy searching this._userList
this._fuse = new Fuse(
// Use an empty list at first that will later be populated
// (see this._updateUserList)
[],
{
shouldSort: true,
location: 0, // The index of the query in the test string
distance: 5, // The distance away from location the query can be
// 0.0 = exact match, 1.0 = match anything
threshold: 0.3,
}
);
this._updateUserList(); this._updateUserList();
}, },
@ -178,7 +161,7 @@ module.exports = React.createClass({
}, },
onQueryChanged: function(ev) { onQueryChanged: function(ev) {
const query = ev.target.value; const query = ev.target.value.toLowerCase();
let queryList = []; let queryList = [];
if (query.length < 2) { if (query.length < 2) {
@ -191,24 +174,27 @@ module.exports = React.createClass({
this.queryChangedDebouncer = setTimeout(() => { this.queryChangedDebouncer = setTimeout(() => {
// Only do search if there is something to search // Only do search if there is something to search
if (query.length > 0 && query != '@') { if (query.length > 0 && query != '@') {
// Weighted keys prefer to match userIds when first char is @ this._userList.forEach((user) => {
this._fuse.options.keys = [{ if (user.userId.toLowerCase().indexOf(query) === -1 &&
name: 'displayName', user.displayName.toLowerCase().indexOf(query) === -1
weight: query[0] === '@' ? 0.1 : 0.9, ) {
},{ return;
name: 'userId', }
weight: query[0] === '@' ? 0.9 : 0.1,
}];
queryList = this._fuse.search(query).map((user) => {
// Return objects, structure of which is defined // Return objects, structure of which is defined
// by InviteAddressType // by InviteAddressType
return { queryList.push({
addressType: 'mx', addressType: 'mx',
address: user.userId, address: user.userId,
displayName: user.displayName, displayName: user.displayName,
avatarMxc: user.avatarUrl, avatarMxc: user.avatarUrl,
isKnown: true, isKnown: true,
} order: user.getLastActiveTs(),
});
});
queryList = queryList.sort((a,b) => {
return a.order < b.order;
}); });
// If the query is a valid address, add an entry for that // If the query is a valid address, add an entry for that
@ -286,8 +272,8 @@ module.exports = React.createClass({
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, { Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register", title: _t("Please Register"),
description: "Guest users can't invite users. Please register." description: _t("Guest users can't invite users. Please register."),
}); });
return; return;
} }
@ -308,8 +294,8 @@ module.exports = React.createClass({
console.error(err.stack); console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to invite", title: _t("Failed to invite"),
description: ((err && err.message) ? err.message : "Operation failed"), description: ((err && err.message) ? err.message : _t("Operation failed")),
}); });
return null; return null;
}) })
@ -321,8 +307,8 @@ module.exports = React.createClass({
console.error(err.stack); console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to invite user", title: _t("Failed to invite user"),
description: ((err && err.message) ? err.message : "Operation failed"), description: ((err && err.message) ? err.message : _t("Operation failed")),
}); });
return null; return null;
}) })
@ -342,8 +328,8 @@ module.exports = React.createClass({
console.error(err.stack); console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to invite", title: _t("Failed to invite"),
description: ((err && err.message) ? err.message : "Operation failed"), description: ((err && err.message) ? err.message : _t("Operation failed")),
}); });
return null; return null;
}) })
@ -354,7 +340,7 @@ module.exports = React.createClass({
this.props.onFinished(true, addrTexts); this.props.onFinished(true, addrTexts);
}, },
_updateUserList: new rate_limited_func(function() { _updateUserList: function() {
// Get all the users // Get all the users
this._userList = MatrixClientPeg.get().getUsers(); this._userList = MatrixClientPeg.get().getUsers();
// Remove current user // Remove current user
@ -362,9 +348,7 @@ module.exports = React.createClass({
return u.userId === MatrixClientPeg.get().credentials.userId; return u.userId === MatrixClientPeg.get().credentials.userId;
}); });
this._userList.splice(meIx, 1); this._userList.splice(meIx, 1);
},
this._fuse.set(this._userList);
}, 500),
_isOnInviteList: function(uid) { _isOnInviteList: function(uid) {
for (let i = 0; i < this.state.inviteList.length; i++) { for (let i = 0; i < this.state.inviteList.length; i++) {
@ -401,7 +385,7 @@ module.exports = React.createClass({
if (errorList.length > 0) { if (errorList.length > 0) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to invite the following users to the " + room.name + " room:", title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
description: errorList.join(", "), description: errorList.join(", "),
}); });
} }

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import classnames from 'classnames'; import classnames from 'classnames';
import { _t } from '../../../languageHandler';
/* /*
* A dialog for confirming a redaction. * A dialog for confirming a redaction.
@ -42,7 +43,7 @@ export default React.createClass({
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const title = "Confirm Redaction"; const title = _t("Confirm Removal");
const confirmButtonClass = classnames({ const confirmButtonClass = classnames({
'mx_Dialog_primary': true, 'mx_Dialog_primary': true,
@ -55,16 +56,16 @@ export default React.createClass({
title={title} title={title}
> >
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
Are you sure you wish to redact (delete) this event? {_t("Are you sure you wish to remove (delete) this event? " +
Note that if you redact a room name or topic change, it could undo the change. "Note that if you delete a room name or topic change, it could undo the change.")}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button className={confirmButtonClass} onClick={this.onOk}> <button className={confirmButtonClass} onClick={this.onOk}>
Redact {_t("Remove")}
</button> </button>
<button onClick={this.onCancel}> <button onClick={this.onCancel}>
Cancel {_t("Cancel")}
</button> </button>
</div> </div>
</BaseDialog> </BaseDialog>

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classnames from 'classnames'; import classnames from 'classnames';
/* /*
@ -69,7 +70,7 @@ export default React.createClass({
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
const title = this.props.action + " this person?"; const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action});
const confirmButtonClass = classnames({ const confirmButtonClass = classnames({
'mx_Dialog_primary': true, 'mx_Dialog_primary': true,
'danger': this.props.danger, 'danger': this.props.danger,
@ -82,7 +83,7 @@ export default React.createClass({
<form onSubmit={this.onOk}> <form onSubmit={this.onOk}>
<input className="mx_ConfirmUserActionDialog_reasonField" <input className="mx_ConfirmUserActionDialog_reasonField"
ref={this._collectReasonField} ref={this._collectReasonField}
placeholder="Reason" placeholder={ _t("Reason") }
autoFocus={true} autoFocus={true}
/> />
</form> </form>
@ -111,7 +112,7 @@ export default React.createClass({
</button> </button>
<button onClick={this.onCancel}> <button onClick={this.onCancel}>
Cancel { _t("Cancel") }
</button> </button>
</div> </div>
</BaseDialog> </BaseDialog>

View file

@ -20,6 +20,7 @@ import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import * as Lifecycle from '../../../Lifecycle'; import * as Lifecycle from '../../../Lifecycle';
import Velocity from 'velocity-vector'; import Velocity from 'velocity-vector';
import { _t } from '../../../languageHandler';
export default class DeactivateAccountDialog extends React.Component { export default class DeactivateAccountDialog extends React.Component {
constructor(props, context) { constructor(props, context) {
@ -56,10 +57,10 @@ export default class DeactivateAccountDialog extends React.Component {
Lifecycle.onLoggedOut(); Lifecycle.onLoggedOut();
this.props.onFinished(false); this.props.onFinished(false);
}, (err) => { }, (err) => {
let errStr = 'Unknown error'; let errStr = _t('Unknown error');
// https://matrix.org/jira/browse/SYN-744 // https://matrix.org/jira/browse/SYN-744
if (err.httpStatus == 401 || err.httpStatus == 403) { if (err.httpStatus == 401 || err.httpStatus == 403) {
errStr = 'Incorrect password'; errStr = _t('Incorrect password');
Velocity(this._passwordField, "callout.shake", 300); Velocity(this._passwordField, "callout.shake", 300);
} }
this.setState({ this.setState({
@ -91,23 +92,23 @@ export default class DeactivateAccountDialog extends React.Component {
let cancelButton = null; let cancelButton = null;
if (!this.state.busy) { if (!this.state.busy) {
cancelButton = <button onClick={this._onCancel} autoFocus={true}> cancelButton = <button onClick={this._onCancel} autoFocus={true}>
Cancel {_t("Cancel")}
</button>; </button>;
} }
return ( return (
<div className="mx_DeactivateAccountDialog"> <div className="mx_DeactivateAccountDialog">
<div className="mx_Dialog_title danger"> <div className="mx_Dialog_title danger">
Deactivate Account {_t("Deactivate Account")}
</div> </div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<p>This will make your account permanently unusable. You will not be able to re-register the same user ID.</p> <p>{_t("This will make your account permanently unusable. You will not be able to re-register the same user ID.")}</p>
<p>This action is irreversible.</p> <p>{_t("This action is irreversible.")}</p>
<p>To continue, please enter your password.</p> <p>{_t("To continue, please enter your password.")}</p>
<p>Password:</p> <p>{_t("Password")}:</p>
<input <input
type="password" type="password"
onChange={this._onPasswordFieldChange} onChange={this._onPasswordFieldChange}

View file

@ -19,6 +19,7 @@ import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index'; import sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils'; import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
export default function DeviceVerifyDialog(props) { export default function DeviceVerifyDialog(props) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
@ -27,25 +28,25 @@ export default function DeviceVerifyDialog(props) {
const body = ( const body = (
<div> <div>
<p> <p>
To verify that this device can be trusted, please contact its {_t("To verify that this device can be trusted, please contact its " +
owner using some other means (e.g. in person or a phone call) "owner using some other means (e.g. in person or a phone call) " +
and ask them whether the key they see in their User Settings "and ask them whether the key they see in their User Settings " +
for this device matches the key below: "for this device matches the key below:")}
</p> </p>
<div className="mx_UserSettings_cryptoSection"> <div className="mx_UserSettings_cryptoSection">
<ul> <ul>
<li><label>Device name:</label> <span>{ props.device.getDisplayName() }</span></li> <li><label>{_t("Device name")}:</label> <span>{ props.device.getDisplayName() }</span></li>
<li><label>Device ID:</label> <span><code>{ props.device.deviceId}</code></span></li> <li><label>{_t("Device ID")}:</label> <span><code>{ props.device.deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{ key }</b></code></span></li> <li><label>{_t("Device key")}:</label> <span><code><b>{ key }</b></code></span></li>
</ul> </ul>
</div> </div>
<p> <p>
If it matches, press the verify button below. {_t("If it matches, press the verify button below. " +
If it doesnt, then someone else is intercepting this device "If it doesn't, then someone else is intercepting this device " +
and you probably want to press the blacklist button instead. "and you probably want to press the blacklist button instead.")}
</p> </p>
<p> <p>
In future this verification process will be more sophisticated. {_t("In future this verification process will be more sophisticated.")}
</p> </p>
</div> </div>
); );
@ -61,9 +62,9 @@ export default function DeviceVerifyDialog(props) {
return ( return (
<QuestionDialog <QuestionDialog
title="Verify device" title={_t("Verify device")}
description={body} description={body}
button="I verify that the keys match" button={_t("I verify that the keys match")}
onFinished={onFinished} onFinished={onFinished}
/> />
); );

View file

@ -27,6 +27,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({ export default React.createClass({
displayName: 'ErrorDialog', displayName: 'ErrorDialog',
@ -43,10 +44,10 @@ export default React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
title: "Error",
description: "An error has occurred.",
button: "OK",
focus: true, focus: true,
title: null,
description: null,
button: null,
}; };
}, },
@ -60,13 +61,13 @@ export default React.createClass({
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return ( return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished} <BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={this.props.title}> title={this.props.title || _t('Error')}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
{this.props.description} {this.props.description || _t('An error has occurred.')}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}> <button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
{this.props.button} {this.props.button || _t('OK')}
</button> </button>
</div> </div>
</BaseDialog> </BaseDialog>

View file

@ -20,6 +20,7 @@ import Matrix from 'matrix-js-sdk';
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
@ -46,12 +47,6 @@ export default React.createClass({
title: React.PropTypes.string, title: React.PropTypes.string,
}, },
getDefaultProps: function() {
return {
title: "Authentication",
};
},
getInitialState: function() { getInitialState: function() {
return { return {
authError: null, authError: null,
@ -105,7 +100,7 @@ export default React.createClass({
return ( return (
<BaseDialog className="mx_InteractiveAuthDialog" <BaseDialog className="mx_InteractiveAuthDialog"
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={this.state.authError ? 'Error' : this.props.title} title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
> >
{content} {content}
</BaseDialog> </BaseDialog>

View file

@ -26,6 +26,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'NeedToRegisterDialog', displayName: 'NeedToRegisterDialog',
@ -38,13 +39,6 @@ module.exports = React.createClass({
onFinished: React.PropTypes.func.isRequired, onFinished: React.PropTypes.func.isRequired,
}, },
getDefaultProps: function() {
return {
title: "Registration required",
description: "A registered account is required for this action",
};
},
onRegisterClicked: function() { onRegisterClicked: function() {
dis.dispatch({ dis.dispatch({
action: "start_upgrade_registration", action: "start_upgrade_registration",
@ -59,10 +53,10 @@ module.exports = React.createClass({
return ( return (
<BaseDialog className="mx_NeedToRegisterDialog" <BaseDialog className="mx_NeedToRegisterDialog"
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={this.props.title} title={this.props.title || _t('Registration required')}
> >
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
{this.props.description} {this.props.description || _t('A registered account is required for this action')}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={true}> <button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={true}>

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({ export default React.createClass({
displayName: 'QuestionDialog', displayName: 'QuestionDialog',
@ -33,7 +34,6 @@ export default React.createClass({
title: "", title: "",
description: "", description: "",
extraButtons: null, extraButtons: null,
button: "OK",
focus: true, focus: true,
hasCancelButton: true, hasCancelButton: true,
}; };
@ -64,7 +64,7 @@ export default React.createClass({
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}> <button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
{this.props.button} {this.props.button || _t('OK')}
</button> </button>
{this.props.extraButtons} {this.props.extraButtons}
{cancelButton} {cancelButton}

View file

@ -18,6 +18,7 @@ import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
export default React.createClass({ export default React.createClass({
@ -51,21 +52,21 @@ export default React.createClass({
return ( return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished} <BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title='Unable to restore session'> title={_t('Unable to restore session')}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<p>We encountered an error trying to restore your previous session. If <p>{_t("We encountered an error trying to restore your previous session. If " +
you continue, you will need to log in again, and encrypted chat "you continue, you will need to log in again, and encrypted chat " +
history will be unreadable.</p> "history will be unreadable.")}</p>
<p>If you have previously used a more recent version of Riot, your session <p>{_t("If you have previously used a more recent version of Riot, your session " +
may be incompatible with this version. Close this window and return "may be incompatible with this version. Close this window and return " +
to the more recent version.</p> "to the more recent version.")}</p>
{bugreport} {bugreport}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this._continueClicked}> <button className="mx_Dialog_primary" onClick={this._continueClicked}>
Continue anyway {_t("Continue anyway")}
</button> </button>
</div> </div>
</BaseDialog> </BaseDialog>

View file

@ -18,6 +18,8 @@ import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
/** /**
* Prompt the user to set a display name. * Prompt the user to set a display name.
* *
@ -64,11 +66,11 @@ export default React.createClass({
return ( return (
<BaseDialog className="mx_SetDisplayNameDialog" <BaseDialog className="mx_SetDisplayNameDialog"
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title="Set a Display Name" title={_t("Set a Display Name")}
> >
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
Your display name is how you'll appear to others when you speak in rooms.<br/> {_t("Your display name is how you'll appear to others when you speak in rooms. " +
What would you like it to be? "What would you like it to be?")}
</div> </div>
<form onSubmit={this.onFormSubmit}> <form onSubmit={this.onFormSubmit}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({ export default React.createClass({
displayName: 'TextInputDialog', displayName: 'TextInputDialog',
@ -36,7 +37,6 @@ export default React.createClass({
title: "", title: "",
value: "", value: "",
description: "", description: "",
button: "OK",
focus: true, focus: true,
}; };
}, },
@ -73,7 +73,7 @@ export default React.createClass({
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this.onCancel}> <button onClick={this.onCancel}>
Cancel { _t("Cancel") }
</button> </button>
<button className="mx_Dialog_primary" onClick={this.onOk}> <button className="mx_Dialog_primary" onClick={this.onOk}>
{this.props.button} {this.props.button}

View file

@ -20,6 +20,7 @@ import dis from '../../../dispatcher';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
import Resend from '../../../Resend'; import Resend from '../../../Resend';
import { _t } from '../../../languageHandler';
function DeviceListEntry(props) { function DeviceListEntry(props) {
const {userId, device} = props; const {userId, device} = props;
@ -120,17 +121,17 @@ export default React.createClass({
if (blacklistUnverified) { if (blacklistUnverified) {
warning = ( warning = (
<h4> <h4>
You are currently blacklisting unverified devices; to send {_t("You are currently blacklisting unverified devices; to send " +
messages to these devices you must verify them. "messages to these devices you must verify them.")}
</h4> </h4>
); );
} else { } else {
warning = ( warning = (
<div> <div>
<p> <p>
We recommend you go through the verification process {_t("We recommend you go through the verification process " +
for each device to confirm they belong to their legitimate owner, "for each device to confirm they belong to their legitimate owner, " +
but you can resend the message without verifying if you prefer. "but you can resend the message without verifying if you prefer.")}
</p> </p>
</div> </div>
); );
@ -149,10 +150,10 @@ export default React.createClass({
> >
<GeminiScrollbar autoshow={false} className="mx_Dialog_content"> <GeminiScrollbar autoshow={false} className="mx_Dialog_content">
<h4> <h4>
"{this.props.room.name}" contains devices that you haven't seen before. {_t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name})}
</h4> </h4>
{ warning } { warning }
Unknown devices: {_t("Unknown devices")}:
<UnknownDeviceList devices={this.props.devices} /> <UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbar> </GeminiScrollbar>

View file

@ -16,12 +16,13 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); import React from 'react';
var classNames = require('classnames'); import classNames from 'classnames';
var sdk = require("../../../index"); import sdk from "../../../index";
var Invite = require("../../../Invite"); import Invite from "../../../Invite";
var MatrixClientPeg = require("../../../MatrixClientPeg"); import MatrixClientPeg from "../../../MatrixClientPeg";
var Avatar = require('../../../Avatar'); import Avatar from '../../../Avatar';
import { _t } from '../../../languageHandler';
// React PropType definition for an object describing // React PropType definition for an object describing
// an address that can be invited to a room (which // an address that can be invited to a room (which
@ -142,7 +143,7 @@ export default React.createClass({
}); });
info = ( info = (
<div className={unknownClasses}>Unknown Address</div> <div className={unknownClasses}>{_t("Unknown Address")}</div>
); );
} }

View file

@ -18,6 +18,7 @@ import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index'; import sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
export default React.createClass({ export default React.createClass({
displayName: 'DeviceVerifyButtons', displayName: 'DeviceVerifyButtons',
@ -82,14 +83,14 @@ export default React.createClass({
blacklistButton = ( blacklistButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
onClick={this.onUnblacklistClick}> onClick={this.onUnblacklistClick}>
Unblacklist {_t("Unblacklist")}
</button> </button>
); );
} else { } else {
blacklistButton = ( blacklistButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist"
onClick={this.onBlacklistClick}> onClick={this.onBlacklistClick}>
Blacklist {_t("Blacklist")}
</button> </button>
); );
} }
@ -98,14 +99,14 @@ export default React.createClass({
verifyButton = ( verifyButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
onClick={this.onUnverifyClick}> onClick={this.onUnverifyClick}>
Unverify {_t("Unverify")}
</button> </button>
); );
} else { } else {
verifyButton = ( verifyButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
onClick={this.onVerifyClick}> onClick={this.onVerifyClick}>
Verify... {_t("Verify...")}
</button> </button>
); );
} }

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import AccessibleButton from './AccessibleButton'; import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler';
class MenuOption extends React.Component { class MenuOption extends React.Component {
constructor(props) { constructor(props) {
@ -255,7 +256,7 @@ export default class Dropdown extends React.Component {
}); });
if (options.length === 0) { if (options.length === 0) {
return [<div key="0" className="mx_Dropdown_option"> return [<div key="0" className="mx_Dropdown_option">
No results {_t("No results")}
</div>]; </div>];
} }
return options; return options;

View file

@ -0,0 +1,121 @@
/*
Copyright 2017 Marcel Radzio (MTRNord)
Copyright 2017 Vector Creations Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler';
import * as languageHandler from '../../../languageHandler';
function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
if (language.value.toUpperCase() == query.toUpperCase()) return true;
return false;
}
export default class LanguageDropdown extends React.Component {
constructor(props) {
super(props);
this._onSearchChange = this._onSearchChange.bind(this);
this.state = {
searchQuery: '',
langs: null,
}
}
componentWillMount() {
languageHandler.getAllLanguagesFromJson().then((langs) => {
langs.sort(function(a, b){
if(a.label < b.label) return -1;
if(a.label > b.label) return 1;
return 0;
});
this.setState({langs});
}).catch(() => {
this.setState({langs: ['en']});
}).done();
if (!this.props.value) {
// If no value is given, we start with the first
// country selected, but our parent component
// doesn't know this, therefore we do this.
const _localSettings = UserSettingsStore.getLocalSettings();
if (_localSettings.hasOwnProperty('language')) {
this.props.onOptionChange(_localSettings.language);
}else {
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
this.props.onOptionChange(language);
}
}
}
_onSearchChange(search) {
this.setState({
searchQuery: search,
});
}
render() {
if (this.state.langs === null) {
const Spinner = sdk.getComponent('elements.Spinner');
return <Spinner />;
}
const Dropdown = sdk.getComponent('elements.Dropdown');
let displayedLanguages;
if (this.state.searchQuery) {
displayedLanguages = this.state.langs.filter((lang) => {
return languageMatchesSearchQuery(this.state.searchQuery, lang);
});
} else {
displayedLanguages = this.state.langs;
}
const options = displayedLanguages.map((language) => {
return <div key={language.value}>
{language.label}
</div>;
});
// default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propgating
let value = null;
const _localSettings = UserSettingsStore.getLocalSettings();
if (_localSettings.hasOwnProperty('language')) {
value = this.props.value || _localSettings.language;
} else {
const language = navigator.language || navigator.userLanguage;
value = this.props.value || language;
}
return <Dropdown className={this.props.className}
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
searchEnabled={true} value={value}
>
{options}
</Dropdown>
}
}
LanguageDropdown.propTypes = {
className: React.PropTypes.string,
onOptionChange: React.PropTypes.func.isRequired,
value: React.PropTypes.string,
};

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
const MemberAvatar = require('../avatars/MemberAvatar.js'); const MemberAvatar = require('../avatars/MemberAvatar.js');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MemberEventListSummary', displayName: 'MemberEventListSummary',
@ -203,30 +204,146 @@ module.exports = React.createClass({
* @param {boolean} plural whether there were multiple users undergoing the same * @param {boolean} plural whether there were multiple users undergoing the same
* transition. * transition.
* @param {number} repeats the number of times the transition was repeated in a row. * @param {number} repeats the number of times the transition was repeated in a row.
* @returns {string} the written English equivalent of the transition. * @returns {string} the written Human Readable equivalent of the transition.
*/ */
_getDescriptionForTransition(t, plural, repeats) { _getDescriptionForTransition(t, plural, repeats) {
const beConjugated = plural ? "were" : "was"; // The empty interpolations 'severalUsers' and 'oneUser'
const invitation = "their invitation" + (plural || (repeats > 1) ? "s" : ""); // are there only to show translators to non-English languages
// that the verb is conjugated to plural or singular Subject.
let res = null; let res = null;
const map = { switch(t) {
"joined": "joined", case "joined":
"left": "left", if (repeats > 1) {
"joined_and_left": "joined and left", res = (plural)
"left_and_joined": "left and rejoined", ? _t("%(severalUsers)sjoined %(repeats)s times", { severalUsers: "", repeats: repeats })
"invite_reject": "rejected " + invitation, : _t("%(oneUser)sjoined %(repeats)s times", { oneUser: "", repeats: repeats });
"invite_withdrawal": "had " + invitation + " withdrawn", } else {
"invited": beConjugated + " invited", res = (plural)
"banned": beConjugated + " banned", ? _t("%(severalUsers)sjoined", { severalUsers: "" })
"unbanned": beConjugated + " unbanned", : _t("%(oneUser)sjoined", { oneUser: "" });
"kicked": beConjugated + " kicked", }
"changed_name": "changed name",
"changed_avatar": "changed avatar",
};
if (Object.keys(map).includes(t)) { break;
res = map[t] + (repeats > 1 ? " " + repeats + " times" : "" ); case "left":
if (repeats > 1) {
res = (plural)
? _t("%(severalUsers)sleft %(repeats)s times", { severalUsers: "", repeats: repeats })
: _t("%(oneUser)sleft %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)sleft", { severalUsers: "" })
: _t("%(oneUser)sleft", { oneUser: "" });
} break;
case "joined_and_left":
if (repeats > 1) {
res = (plural)
? _t("%(severalUsers)sjoined and left %(repeats)s times", { severalUsers: "", repeats: repeats })
: _t("%(oneUser)sjoined and left %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)sjoined and left", { severalUsers: "" })
: _t("%(oneUser)sjoined and left", { oneUser: "" });
}
break;
case "left_and_joined":
if (repeats > 1) {
res = (plural)
? _t("%(severalUsers)sleft and rejoined %(repeats)s times", { severalUsers: "", repeats: repeats })
: _t("%(oneUser)sleft and rejoined %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)sleft and rejoined", { severalUsers: "" })
: _t("%(oneUser)sleft and rejoined", { oneUser: "" });
} break;
break;
case "invite_reject":
if (repeats > 1) {
res = (plural)
? _t("%(severalUsers)srejected their invitations %(repeats)s times", { severalUsers: "", repeats: repeats })
: _t("%(oneUser)srejected their invitation %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)srejected their invitations", { severalUsers: "" })
: _t("%(oneUser)srejected their invitation", { oneUser: "" });
}
break;
case "invite_withdrawal":
if (repeats > 1) {
res = (plural)
? _t("%(severalUsers)shad their invitations withdrawn %(repeats)s times", { severalUsers: "", repeats: repeats })
: _t("%(oneUser)shad their invitation withdrawn %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)shad their invitations withdrawn", { severalUsers: "" })
: _t("%(oneUser)shad their invitation withdrawn", { oneUser: "" });
}
break;
case "invited":
if (repeats > 1) {
res = (plural)
? _t("were invited %(repeats)s times", { repeats: repeats })
: _t("was invited %(repeats)s times", { repeats: repeats });
} else {
res = (plural)
? _t("were invited")
: _t("was invited");
}
break;
case "banned":
if (repeats > 1) {
res = (plural)
? _t("were banned %(repeats)s times", { repeats: repeats })
: _t("was banned %(repeats)s times", { repeats: repeats });
} else {
res = (plural)
? _t("were banned")
: _t("was banned");
}
break;
case "unbanned":
if (repeats > 1) {
res = (plural)
? _t("were unbanned %(repeats)s times", { repeats: repeats })
: _t("was unbanned %(repeats)s times", { repeats: repeats });
} else {
res = (plural)
? _t("were unbanned")
: _t("was unbanned");
}
break;
case "kicked":
if (repeats > 1) {
res = (plural)
? _t("were kicked %(repeats)s times", { repeats: repeats })
: _t("was kicked %(repeats)s times", { repeats: repeats });
} else {
res = (plural)
? _t("were kicked")
: _t("was kicked");
}
break;
case "changed_name":
if (repeats > 1) {
res = (plural)
? _t("%(severalUsers)schanged their name %(repeats)s times", { severalUsers: "", repeats: repeats })
: _t("%(oneUser)schanged their name %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)schanged their name", { severalUsers: "" })
: _t("%(oneUser)schanged their name", { oneUser: "" });
}
break;
case "changed_avatar":
if (repeats > 1) {
res = (plural)
? _t("%(severalUsers)schanged their avatar %(repeats)s times", { severalUsers: "", repeats: repeats })
: _t("%(oneUser)schanged their avatar %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)schanged their avatar", { severalUsers: "" })
: _t("%(oneUser)schanged their avatar", { oneUser: "" });
}
break;
} }
return res; return res;
@ -254,11 +371,12 @@ module.exports = React.createClass({
return items[0]; return items[0];
} else if (remaining) { } else if (remaining) {
items = items.slice(0, itemLimit); items = items.slice(0, itemLimit);
const other = " other" + (remaining > 1 ? "s" : ""); return (remaining > 1)
return items.join(', ') + ' and ' + remaining + other; ? _t("%(items)s and %(remaining)s others", { items: items.join(', '), remaining: remaining } )
: _t("%(items)s and one other", { items: items.join(', ') });
} else { } else {
const lastItem = items.pop(); const lastItem = items.pop();
return items.join(', ') + ' and ' + lastItem; return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
} }
}, },

View file

@ -19,10 +19,8 @@ limitations under the License.
import React from 'react'; import React from 'react';
import * as Roles from '../../../Roles'; import * as Roles from '../../../Roles';
var LEVEL_ROLE_MAP = {};
var reverseRoles = {}; var reverseRoles = {};
Object.keys(Roles.LEVEL_ROLE_MAP).forEach(function(key) {
reverseRoles[Roles.LEVEL_ROLE_MAP[key]] = key;
});
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'PowerSelector', displayName: 'PowerSelector',
@ -44,9 +42,16 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
custom: (Roles.LEVEL_ROLE_MAP[this.props.value] === undefined), custom: (LEVEL_ROLE_MAP[this.props.value] === undefined),
}; };
}, },
componentWillMount: function() {
LEVEL_ROLE_MAP = Roles.levelRoleMap();
Object.keys(LEVEL_ROLE_MAP).forEach(function(key) {
reverseRoles[LEVEL_ROLE_MAP[key]] = key;
});
},
onSelectChange: function(event) { onSelectChange: function(event) {
this.setState({ custom: event.target.value === "Custom" }); this.setState({ custom: event.target.value === "Custom" });
@ -94,7 +99,7 @@ module.exports = React.createClass({
selectValue = "Custom"; selectValue = "Custom";
} }
else { else {
selectValue = Roles.LEVEL_ROLE_MAP[this.props.value] || "Custom"; selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom";
} }
var select; var select;
if (this.props.disabled) { if (this.props.disabled) {
@ -105,7 +110,7 @@ module.exports = React.createClass({
const levels = [0, 50, 100]; const levels = [0, 50, 100];
let options = levels.map((level) => { let options = levels.map((level) => {
return { return {
value: Roles.LEVEL_ROLE_MAP[level], value: LEVEL_ROLE_MAP[level],
// Give a userDefault (users_default in the power event) of 0 but // Give a userDefault (users_default in the power event) of 0 but
// because level !== undefined, this should never be used. // because level !== undefined, this should never be used.
text: Roles.textualPowerLevel(level, 0), text: Roles.textualPowerLevel(level, 0),
@ -113,7 +118,7 @@ module.exports = React.createClass({
}); });
options.push({ value: "Custom", text: "Custom level" }); options.push({ value: "Custom", text: "Custom level" });
options = options.map((op) => { options = options.map((op) => {
return <option value={op.value}>{op.text}</option>; return <option value={op.value} key={op.value}>{op.text}</option>;
}); });
select = select =

View file

@ -16,7 +16,8 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); import React from 'react';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'UserSelector', displayName: 'UserSelector',
@ -59,9 +60,9 @@ module.exports = React.createClass({
return <li key={user_id}>{user_id} - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>; return <li key={user_id}>{user_id} - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>;
})} })}
</ul> </ul>
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder="ex. @bob:example.com"/> <input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder={_t("ex. @bob:example.com")}/>
<button onClick={this.onAddUserId} className="mx_UserSelector_AddUserId"> <button onClick={this.onAddUserId} className="mx_UserSelector_AddUserId">
Add User {_t("Add User")}
</button> </button>
</div> </div>
); );

View file

@ -16,7 +16,9 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); import React from 'react';
import { _t } from '../../../languageHandler';
var DIV_ID = 'mx_recaptcha'; var DIV_ID = 'mx_recaptcha';
/** /**
@ -117,7 +119,7 @@ module.exports = React.createClass({
return ( return (
<div ref="recaptchaContainer"> <div ref="recaptchaContainer">
This Home Server would like to make sure you are not a robot {_t("This Home Server would like to make sure you are not a robot")}
<br/> <br/>
<div id={DIV_ID}></div> <div id={DIV_ID}></div>
{error} {error}

View file

@ -16,7 +16,8 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); import React from 'react';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'CasLogin', displayName: 'CasLogin',
@ -28,7 +29,7 @@ module.exports = React.createClass({
render: function() { render: function() {
return ( return (
<div> <div>
<button onClick={this.props.onSubmit}>Sign in with CAS</button> <button onClick={this.props.onSubmit}>{_t("Sign in with CAS")}</button>
</div> </div>
); );
} }

View file

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require("react"); import React from 'react';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'CustomServerDialog', displayName: 'CustomServerDialog',
@ -23,24 +24,24 @@ module.exports = React.createClass({
return ( return (
<div className="mx_ErrorDialog"> <div className="mx_ErrorDialog">
<div className="mx_Dialog_title"> <div className="mx_Dialog_title">
Custom Server Options {_t("Custom Server Options")}
</div> </div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<span> <span>
You can use the custom server options to sign into other Matrix {_t("You can use the custom server options to sign into other Matrix " +
servers by specifying a different Home server URL. "servers by specifying a different Home server URL.")}
<br/> <br/>
This allows you to use this app with an existing Matrix account on {_t("This allows you to use this app with an existing Matrix account on " +
a different home server. "a different home server.")}
<br/> <br/>
<br/> <br/>
You can also set a custom identity server but this will typically prevent {_t("You can also set a custom identity server but this will typically prevent " +
interaction with users based on email address. "interaction with users based on email address.")}
</span> </span>
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this.props.onFinished} autoFocus={true}> <button onClick={this.props.onFinished} autoFocus={true}>
Dismiss {_t("Dismiss")}
</button> </button>
</div> </div>
</div> </div>

View file

@ -20,6 +20,7 @@ import url from 'url';
import classnames from 'classnames'; import classnames from 'classnames';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
/* This file contains a collection of components which are used by the /* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed * InteractiveAuth to prompt the user to enter the information needed
@ -255,8 +256,8 @@ export const EmailIdentityAuthEntry = React.createClass({
} else { } else {
return ( return (
<div> <div>
<p>An email has been sent to <i>{this.props.inputs.emailAddress}</i></p> <p>{_t("An email has been sent to")} <i>{this.props.inputs.emailAddress}</i></p>
<p>Please check your email to continue registration.</p> <p>{_t("Please check your email to continue registration.")}</p>
</div> </div>
); );
} }
@ -348,7 +349,7 @@ export const MsisdnAuthEntry = React.createClass({
}); });
} else { } else {
this.setState({ this.setState({
errorText: "Token incorrect", errorText: _t("Token incorrect"),
}); });
} }
}).catch((e) => { }).catch((e) => {
@ -369,8 +370,8 @@ export const MsisdnAuthEntry = React.createClass({
}); });
return ( return (
<div> <div>
<p>A text message has been sent to +<i>{this._msisdn}</i></p> <p>{_t("A text message has been sent to")} +<i>{this._msisdn}</i></p>
<p>Please enter the code it contains:</p> <p>{_t("Please enter the code it contains:")}</p>
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper"> <div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
<form onSubmit={this._onFormSubmit}> <form onSubmit={this._onFormSubmit}>
<input type="text" <input type="text"

View file

@ -16,7 +16,7 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); import React from 'react';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'LoginFooter', displayName: 'LoginFooter',
@ -24,7 +24,7 @@ module.exports = React.createClass({
render: function() { render: function() {
return ( return (
<div className="mx_Login_links"> <div className="mx_Login_links">
<a href="https://matrix.org">powered by Matrix</a> <a href="https://matrix.org">{_t("powered by Matrix")}</a>
</div> </div>
); );
} }

View file

@ -19,6 +19,7 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {field_input_incorrect} from '../../../UiEffects'; import {field_input_incorrect} from '../../../UiEffects';
@ -121,32 +122,16 @@ class PasswordLogin extends React.Component {
autoFocus autoFocus
/>; />;
case PasswordLogin.LOGIN_FIELD_MXID: case PasswordLogin.LOGIN_FIELD_MXID:
const mxidInputClasses = classNames({ return <input
"mx_Login_field": true, className="mx_Login_field mx_Login_username"
"mx_Login_username": true, key="username_input"
"mx_Login_field_has_prefix": true, type="text"
"mx_Login_field_has_suffix": Boolean(this.props.hsDomain), name="username" // make it a little easier for browser's remember-password
}); onChange={this.onUsernameChanged}
let suffix = null; placeholder={_t('User name')}
if (this.props.hsDomain) { value={this.state.username}
suffix = <div className="mx_Login_field_suffix"> autoFocus
:{this.props.hsDomain} />;
</div>;
}
return <div className="mx_Login_field_group">
<div className="mx_Login_field_prefix">@</div>
<input
className={mxidInputClasses}
key="username_input"
type="text"
name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged}
placeholder="username"
value={this.state.username}
autoFocus
/>
{suffix}
</div>;
case PasswordLogin.LOGIN_FIELD_PHONE: case PasswordLogin.LOGIN_FIELD_PHONE:
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
return <div className="mx_Login_phoneSection"> return <div className="mx_Login_phoneSection">
@ -179,7 +164,7 @@ class PasswordLogin extends React.Component {
if (this.props.onForgotPasswordClick) { if (this.props.onForgotPasswordClick) {
forgotPasswordJsx = ( forgotPasswordJsx = (
<a className="mx_Login_forgot" onClick={this.props.onForgotPasswordClick} href="#"> <a className="mx_Login_forgot" onClick={this.props.onForgotPasswordClick} href="#">
Forgot your password? { _t('Forgot your password?') }
</a> </a>
); );
} }
@ -197,24 +182,24 @@ class PasswordLogin extends React.Component {
<div> <div>
<form onSubmit={this.onSubmitForm}> <form onSubmit={this.onSubmitForm}>
<div className="mx_Login_type_container"> <div className="mx_Login_type_container">
<label className="mx_Login_type_label">I want to sign in with my</label> <label className="mx_Login_type_label">{ _t('I want to sign in with') }</label>
<Dropdown <Dropdown
className="mx_Login_type_dropdown" className="mx_Login_type_dropdown"
value={this.state.loginType} value={this.state.loginType}
onOptionChange={this.onLoginTypeChange}> onOptionChange={this.onLoginTypeChange}>
<span key={PasswordLogin.LOGIN_FIELD_MXID}>Matrix ID</span> <span key={PasswordLogin.LOGIN_FIELD_MXID}>{ _t('my Matrix ID') }</span>
<span key={PasswordLogin.LOGIN_FIELD_EMAIL}>Email Address</span> <span key={PasswordLogin.LOGIN_FIELD_EMAIL}>{ _t('Email address') }</span>
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>Phone</span> <span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
</Dropdown> </Dropdown>
</div> </div>
{loginField} {loginField}
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password" <input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
name="password" name="password"
value={this.state.password} onChange={this.onPasswordChanged} value={this.state.password} onChange={this.onPasswordChanged}
placeholder="Password" /> placeholder={ _t('Password') } />
<br /> <br />
{forgotPasswordJsx} {forgotPasswordJsx}
<input className="mx_Login_submit" type="submit" value="Sign in" /> <input className="mx_Login_submit" type="submit" value={ _t('Sign in') } />
</form> </form>
</div> </div>
); );
@ -237,7 +222,6 @@ PasswordLogin.propTypes = {
onPhoneNumberChanged: React.PropTypes.func, onPhoneNumberChanged: React.PropTypes.func,
onPasswordChanged: React.PropTypes.func, onPasswordChanged: React.PropTypes.func,
loginIncorrect: React.PropTypes.bool, loginIncorrect: React.PropTypes.bool,
hsDomain: React.PropTypes.string,
}; };
module.exports = PasswordLogin; module.exports = PasswordLogin;

View file

@ -21,6 +21,7 @@ import sdk from '../../../index';
import Email from '../../../email'; import Email from '../../../email';
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
const FIELD_EMAIL = 'field_email'; const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_COUNTRY = 'field_phone_country'; const FIELD_PHONE_COUNTRY = 'field_phone_country';
@ -100,13 +101,13 @@ module.exports = React.createClass({
if (this.refs.email.value == '') { if (this.refs.email.value == '') {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Warning", title: "Warning!",
description: description:
<div> <div>
If you don't specify an email address, you won't be able to reset your password.<br/> {_t("If you don't specify an email address, you won't be able to reset your password. " +
Are you sure? "Are you sure?")}
</div>, </div>,
button: "Continue", button: _t("Continue"),
onFinished: function(confirmed) { onFinished: function(confirmed) {
if (confirmed) { if (confirmed) {
self._doSubmit(); self._doSubmit();
@ -304,7 +305,7 @@ module.exports = React.createClass({
} else if (this.state.selectedTeam) { } else if (this.state.selectedTeam) {
belowEmailSection = ( belowEmailSection = (
<p className="mx_Login_support"> <p className="mx_Login_support">
You are registering with {this.state.selectedTeam.name} {_t("You are registering with %(SelectedTeamName)s", {SelectedTeamName: this.state.selectedTeam.name})}
</p> </p>
); );
} }

View file

@ -19,6 +19,7 @@ limitations under the License.
var React = require('react'); var React = require('react');
var Modal = require('../../../Modal'); var Modal = require('../../../Modal');
var sdk = require('../../../index'); var sdk = require('../../../index');
import { _t } from '../../../languageHandler';
/** /**
* A pure UI component which displays the HS and IS to use. * A pure UI component which displays the HS and IS to use.
@ -136,14 +137,14 @@ module.exports = React.createClass({
checked={!this.state.configVisible} checked={!this.state.configVisible}
onChange={this.onServerConfigVisibleChange.bind(this, false)} /> onChange={this.onServerConfigVisibleChange.bind(this, false)} />
<label className="mx_Login_label" htmlFor="basic"> <label className="mx_Login_label" htmlFor="basic">
Default server {_t("Default server")}
</label> </label>
&nbsp;&nbsp; &nbsp;&nbsp;
<input className="mx_Login_radio" id="advanced" name="configVisible" type="radio" <input className="mx_Login_radio" id="advanced" name="configVisible" type="radio"
checked={this.state.configVisible} checked={this.state.configVisible}
onChange={this.onServerConfigVisibleChange.bind(this, true)} /> onChange={this.onServerConfigVisibleChange.bind(this, true)} />
<label className="mx_Login_label" htmlFor="advanced"> <label className="mx_Login_label" htmlFor="advanced">
Custom server {_t("Custom server")}
</label> </label>
</div> </div>
); );
@ -155,7 +156,7 @@ module.exports = React.createClass({
<div style={serverConfigStyle}> <div style={serverConfigStyle}>
<div className="mx_ServerConfig"> <div className="mx_ServerConfig">
<label className="mx_Login_label mx_ServerConfig_hslabel" htmlFor="hsurl"> <label className="mx_Login_label mx_ServerConfig_hslabel" htmlFor="hsurl">
Home server URL {_t("Home server URL")}
</label> </label>
<input className="mx_Login_field" id="hsurl" type="text" <input className="mx_Login_field" id="hsurl" type="text"
placeholder={this.props.defaultHsUrl} placeholder={this.props.defaultHsUrl}
@ -163,7 +164,7 @@ module.exports = React.createClass({
value={this.state.hs_url} value={this.state.hs_url}
onChange={this.onHomeserverChanged} /> onChange={this.onHomeserverChanged} />
<label className="mx_Login_label mx_ServerConfig_islabel" htmlFor="isurl"> <label className="mx_Login_label mx_ServerConfig_islabel" htmlFor="isurl">
Identity server URL {_t("Identity server URL")}
</label> </label>
<input className="mx_Login_field" id="isurl" type="text" <input className="mx_Login_field" id="isurl" type="text"
placeholder={this.props.defaultIsUrl} placeholder={this.props.defaultIsUrl}
@ -171,7 +172,7 @@ module.exports = React.createClass({
value={this.state.is_url} value={this.state.is_url}
onChange={this.onIdentityServerChanged} /> onChange={this.onIdentityServerChanged} />
<a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}> <a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
What does this mean? {_t("What does this mean?")}
</a> </a>
</div> </div>
</div> </div>

View file

@ -22,6 +22,7 @@ import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index'; import sdk from '../../../index';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile'; import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import { _t } from '../../../languageHandler';
export default class MAudioBody extends React.Component { export default class MAudioBody extends React.Component {
constructor(props) { constructor(props) {
@ -77,7 +78,7 @@ export default class MAudioBody extends React.Component {
return ( return (
<span className="mx_MAudioBody" ref="body"> <span className="mx_MAudioBody" ref="body">
<img src="img/warning.svg" width="16" height="16"/> <img src="img/warning.svg" width="16" height="16"/>
Error decrypting audio {_t("Error decrypting audio")}
</span> </span>
); );
} }

View file

@ -20,6 +20,7 @@ import React from 'react';
import filesize from 'filesize'; import filesize from 'filesize';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {decryptFile} from '../../../utils/DecryptFile'; import {decryptFile} from '../../../utils/DecryptFile';
import Tinter from '../../../Tinter'; import Tinter from '../../../Tinter';
import request from 'browser-request'; import request from 'browser-request';
@ -202,7 +203,7 @@ module.exports = React.createClass({
* @return {string} the human readable link text for the attachment. * @return {string} the human readable link text for the attachment.
*/ */
presentableTextForFile: function(content) { presentableTextForFile: function(content) {
var linkText = 'Attachment'; var linkText = _t("Attachment");
if (content.body && content.body.length > 0) { if (content.body && content.body.length > 0) {
// The content body should be the name of the file including a // The content body should be the name of the file including a
// file extension. // file extension.
@ -261,7 +262,7 @@ module.exports = React.createClass({
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent();
const text = this.presentableTextForFile(content); const text = this.presentableTextForFile(content);
const isEncrypted = content.file !== undefined; const isEncrypted = content.file !== undefined;
const fileName = content.body && content.body.length > 0 ? content.body : "Attachment"; const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment");
const contentUrl = this._getContentUrl(); const contentUrl = this._getContentUrl();
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -283,7 +284,8 @@ module.exports = React.createClass({
}).catch((err) => { }).catch((err) => {
console.warn("Unable to decrypt attachment: ", err); console.warn("Unable to decrypt attachment: ", err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
description: "Error decrypting attachment" title: _t("Error"),
description: _t("Error decrypting attachment"),
}); });
}).finally(() => { }).finally(() => {
decrypting = false; decrypting = false;
@ -295,7 +297,7 @@ module.exports = React.createClass({
<span className="mx_MFileBody" ref="body"> <span className="mx_MFileBody" ref="body">
<div className="mx_MImageBody_download"> <div className="mx_MImageBody_download">
<a href="javascript:void(0)" onClick={decrypt}> <a href="javascript:void(0)" onClick={decrypt}>
Decrypt {text} { _t("Decrypt %(text)s", { text: text }) }
</a> </a>
</div> </div>
</span> </span>
@ -314,7 +316,7 @@ module.exports = React.createClass({
// We can't provide a Content-Disposition header like we would for HTTP. // We can't provide a Content-Disposition header like we would for HTTP.
download: fileName, download: fileName,
target: "_blank", target: "_blank",
textContent: "Download " + text, textContent: _t("Download %(text)s", { text: text }),
}, "*"); }, "*");
}; };
@ -362,7 +364,7 @@ module.exports = React.createClass({
<div className="mx_MImageBody_download"> <div className="mx_MImageBody_download">
<a href={contentUrl} download={fileName} target="_blank" rel="noopener"> <a href={contentUrl} download={fileName} target="_blank" rel="noopener">
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage"/> <img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage"/>
Download {text} { _t("Download %(text)s", { text: text }) }
</a> </a>
</div> </div>
</span> </span>
@ -371,7 +373,7 @@ module.exports = React.createClass({
} else { } else {
var extra = text ? (': ' + text) : ''; var extra = text ? (': ' + text) : '';
return <span className="mx_MFileBody"> return <span className="mx_MFileBody">
Invalid file{extra} { _t("Invalid file%(extra)s", { extra: extra }) }
</span>; </span>;
} }
}, },

View file

@ -26,6 +26,7 @@ import dis from '../../../dispatcher';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile'; import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import q from 'q'; import q from 'q';
import UserSettingsStore from '../../../UserSettingsStore'; import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MImageBody', displayName: 'MImageBody',
@ -56,7 +57,7 @@ module.exports = React.createClass({
const ImageView = sdk.getComponent("elements.ImageView"); const ImageView = sdk.getComponent("elements.ImageView");
const params = { const params = {
src: httpUrl, src: httpUrl,
name: content.body && content.body.length > 0 ? content.body : 'Attachment', name: content.body && content.body.length > 0 ? content.body : _t('Attachment'),
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
}; };
@ -191,7 +192,7 @@ module.exports = React.createClass({
return ( return (
<span className="mx_MImageBody" ref="body"> <span className="mx_MImageBody" ref="body">
<img src="img/warning.svg" width="16" height="16"/> <img src="img/warning.svg" width="16" height="16"/>
Error decrypting image {_t("Error decrypting image")}
</span> </span>
); );
} }
@ -238,13 +239,13 @@ module.exports = React.createClass({
} else if (content.body) { } else if (content.body) {
return ( return (
<span className="mx_MImageBody"> <span className="mx_MImageBody">
Image '{content.body}' cannot be displayed. {_t("Image '%(Body)s' cannot be displayed.", {Body: content.body})}
</span> </span>
); );
} else { } else {
return ( return (
<span className="mx_MImageBody"> <span className="mx_MImageBody">
This image cannot be displayed. {_t("This image cannot be displayed.")}
</span> </span>
); );
} }

View file

@ -24,6 +24,7 @@ import sdk from '../../../index';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile'; import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import q from 'q'; import q from 'q';
import UserSettingsStore from '../../../UserSettingsStore'; import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MVideoBody', displayName: 'MVideoBody',
@ -128,7 +129,7 @@ module.exports = React.createClass({
return ( return (
<span className="mx_MVideoBody" ref="body"> <span className="mx_MVideoBody" ref="body">
<img src="img/warning.svg" width="16" height="16"/> <img src="img/warning.svg" width="16" height="16"/>
Error decrypting video {_t("Error decrypting video")}
</span> </span>
); );
} }

View file

@ -0,0 +1,92 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { ContentRepo } from 'matrix-js-sdk';
import { _t, _tJsx } from '../../../languageHandler';
import sdk from '../../../index';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
module.exports = React.createClass({
displayName: 'RoomAvatarEvent',
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
},
onAvatarClick: function(name) {
var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(this.props.mxEvent.getContent().url);
var ImageView = sdk.getComponent("elements.ImageView");
var params = {
src: httpUrl,
name: name,
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
},
render: function() {
var ev = this.props.mxEvent;
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
var name = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
senderDisplayName: senderDisplayName,
roomName: room ? room.name : '',
});
if (!ev.getContent().url || ev.getContent().url.trim().length === 0) {
return (
<div className="mx_TextualEvent">
{ _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName: senderDisplayName}) }
</div>
);
}
var url = ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
ev.getContent().url,
14 * window.devicePixelRatio,
14 * window.devicePixelRatio,
'crop'
);
// it sucks that _tJsx doesn't support normal _t substitutions :((
return (
<div className="mx_RoomAvatarEvent">
{ _tJsx('$senderDisplayName changed the room avatar to <img/>',
[
/\$senderDisplayName/,
/<img\/>/,
],
[
(sub) => senderDisplayName,
(sub) =>
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
onClick={ this.onAvatarClick.bind(this, name) }>
<BaseAvatar width={14} height={14} url={ url }
name={ name } />
</AccessibleButton>,
]
)
}
</div>
);
},
});

View file

@ -28,6 +28,7 @@ import ScalarAuthClient from '../../../ScalarAuthClient';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
linkifyMatrix(linkify); linkifyMatrix(linkify);
@ -230,14 +231,14 @@ module.exports = React.createClass({
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let integrationsUrl = SdkConfig.get().integrations_ui_url; let integrationsUrl = SdkConfig.get().integrations_ui_url;
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Add an Integration", title: _t("Add an Integration"),
description: description:
<div> <div>
You are about to be taken to a third-party site so you can {_t("You are about to be taken to a third-party site so you can " +
authenticate your account for use with {integrationsUrl}.<br/> "authenticate your account for use with %(integrationsUrl)s. " +
Do you wish to continue? "Do you wish to continue?", { integrationsUrl: integrationsUrl })}
</div>, </div>,
button: "Continue", button: _t("Continue"),
onFinished: function(confirmed) { onFinished: function(confirmed) {
if (!confirmed) { if (!confirmed) {
return; return;

View file

@ -24,6 +24,11 @@ import sdk from '../../../index';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'TextualEvent', displayName: 'TextualEvent',
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
},
render: function() { render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText'); const EmojiText = sdk.getComponent('elements.EmojiText');
var text = TextForEvent.textForEvent(this.props.mxEvent); var text = TextForEvent.textForEvent(this.props.mxEvent);

View file

@ -16,7 +16,8 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); import React from 'react';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'UnknownBody', displayName: 'UnknownBody',
@ -24,7 +25,7 @@ module.exports = React.createClass({
render: function() { render: function() {
const text = this.props.mxEvent.getContent().body; const text = this.props.mxEvent.getContent().body;
return ( return (
<span className="mx_UnknownBody" title="Redacted or unknown message type"> <span className="mx_UnknownBody" title={_t("Removed or unknown message type")}>
{text} {text}
</span> </span>
); );

View file

@ -19,6 +19,7 @@ var React = require('react');
var ObjectUtils = require("../../../ObjectUtils"); var ObjectUtils = require("../../../ObjectUtils");
var MatrixClientPeg = require('../../../MatrixClientPeg'); var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require("../../../index"); var sdk = require("../../../index");
import { _t } from '../../../languageHandler';
var Modal = require("../../../Modal"); var Modal = require("../../../Modal");
module.exports = React.createClass({ module.exports = React.createClass({
@ -154,8 +155,8 @@ module.exports = React.createClass({
else { else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Invalid alias format", title: _t('Invalid alias format'),
description: "'" + alias + "' is not a valid format for an alias", description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
}); });
} }
}, },
@ -170,8 +171,8 @@ module.exports = React.createClass({
else { else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Invalid address format", title: _t('Invalid address format'),
description: "'" + alias + "' is not a valid format for an address", description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }),
}); });
} }
}, },
@ -203,7 +204,7 @@ module.exports = React.createClass({
if (this.props.canSetCanonicalAlias) { if (this.props.canSetCanonicalAlias) {
canonical_alias_section = ( canonical_alias_section = (
<select onChange={this.onCanonicalAliasChange} defaultValue={ this.state.canonicalAlias }> <select onChange={this.onCanonicalAliasChange} defaultValue={ this.state.canonicalAlias }>
<option value="" key="unset">not specified</option> <option value="" key="unset">{ _t('not specified') }</option>
{ {
Object.keys(self.state.domainToAliases).map(function(domain, i) { Object.keys(self.state.domainToAliases).map(function(domain, i) {
return self.state.domainToAliases[domain].map(function(alias, j) { return self.state.domainToAliases[domain].map(function(alias, j) {
@ -220,7 +221,7 @@ module.exports = React.createClass({
} }
else { else {
canonical_alias_section = ( canonical_alias_section = (
<b>{ this.state.canonicalAlias || "not set" }</b> <b>{ this.state.canonicalAlias || _t('not set') }</b>
); );
} }
@ -254,13 +255,13 @@ module.exports = React.createClass({
<div> <div>
<h3>Addresses</h3> <h3>Addresses</h3>
<div className="mx_RoomSettings_aliasLabel"> <div className="mx_RoomSettings_aliasLabel">
The main address for this room is: { canonical_alias_section } { _t('The main address for this room is') }: { canonical_alias_section }
</div> </div>
<div className="mx_RoomSettings_aliasLabel"> <div className="mx_RoomSettings_aliasLabel">
{ (this.state.domainToAliases[localDomain] && { (this.state.domainToAliases[localDomain] &&
this.state.domainToAliases[localDomain].length > 0) this.state.domainToAliases[localDomain].length > 0)
? "Local addresses for this room:" ? _t('Local addresses for this room:')
: "This room has no local addresses" } : _t('This room has no local addresses') }
</div> </div>
<div className="mx_RoomSettings_aliasesTable"> <div className="mx_RoomSettings_aliasesTable">
{ (this.state.domainToAliases[localDomain] || []).map((alias, i) => { { (this.state.domainToAliases[localDomain] || []).map((alias, i) => {
@ -268,7 +269,7 @@ module.exports = React.createClass({
if (this.props.canSetAliases) { if (this.props.canSetAliases) {
deleteButton = ( deleteButton = (
<img src="img/cancel-small.svg" width="14" height="14" <img src="img/cancel-small.svg" width="14" height="14"
alt="Delete" onClick={ self.onAliasDeleted.bind(self, localDomain, i) } /> alt={ _t('Delete') } onClick={ self.onAliasDeleted.bind(self, localDomain, i) } />
); );
} }
return ( return (
@ -276,7 +277,7 @@ module.exports = React.createClass({
<EditableText <EditableText
className="mx_RoomSettings_alias mx_RoomSettings_editable" className="mx_RoomSettings_alias mx_RoomSettings_editable"
placeholderClassName="mx_RoomSettings_aliasPlaceholder" placeholderClassName="mx_RoomSettings_aliasPlaceholder"
placeholder={ "New address (e.g. #foo:" + localDomain + ")" } placeholder={ _t('New address (e.g. #foo:%(localDomain)s)', { localDomain: localDomain}) }
blurToCancel={ false } blurToCancel={ false }
onValueChanged={ self.onAliasChanged.bind(self, localDomain, i) } onValueChanged={ self.onAliasChanged.bind(self, localDomain, i) }
editable={ self.props.canSetAliases } editable={ self.props.canSetAliases }
@ -294,7 +295,7 @@ module.exports = React.createClass({
ref="add_alias" ref="add_alias"
className="mx_RoomSettings_alias mx_RoomSettings_editable" className="mx_RoomSettings_alias mx_RoomSettings_editable"
placeholderClassName="mx_RoomSettings_aliasPlaceholder" placeholderClassName="mx_RoomSettings_aliasPlaceholder"
placeholder={ "New address (e.g. #foo:" + localDomain + ")" } placeholder={ _t('New address (e.g. #foo:%(localDomain)s)', { localDomain: localDomain}) }
blurToCancel={ false } blurToCancel={ false }
onValueChanged={ self.onAliasAdded } /> onValueChanged={ self.onAliasAdded } />
<div className="mx_RoomSettings_addAlias mx_filterFlipColor"> <div className="mx_RoomSettings_addAlias mx_filterFlipColor">

View file

@ -20,6 +20,7 @@ var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require("../../../index"); var sdk = require("../../../index");
var Modal = require("../../../Modal"); var Modal = require("../../../Modal");
var UserSettingsStore = require('../../../UserSettingsStore'); var UserSettingsStore = require('../../../UserSettingsStore');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
@ -120,19 +121,19 @@ module.exports = React.createClass({
<input type="checkbox" ref="globalDisableUrlPreview" <input type="checkbox" ref="globalDisableUrlPreview"
onChange={ this.onGlobalDisableUrlPreviewChange } onChange={ this.onGlobalDisableUrlPreviewChange }
checked={ this.state.globalDisableUrlPreview } /> checked={ this.state.globalDisableUrlPreview } />
Disable URL previews by default for participants in this room {_t("Disable URL previews by default for participants in this room")}
</label>; </label>;
} }
else { else {
disableRoomPreviewUrls = disableRoomPreviewUrls =
<label> <label>
URL previews are { this.state.globalDisableUrlPreview ? "disabled" : "enabled" } by default for participants in this room. {_t("URL previews are %(globalDisableUrlPreview)s by default for participants in this room.", {globalDisableUrlPreview: this.state.globalDisableUrlPreview ? _t("disabled") : _t("enabled")})}
</label>; </label>;
} }
return ( return (
<div className="mx_RoomSettings_toggles"> <div className="mx_RoomSettings_toggles">
<h3>URL Previews</h3> <h3>{_t("URL Previews")}</h3>
<label> <label>
You have <a href="#/settings">{ UserSettingsStore.getUrlPreviewsDisabled() ? 'disabled' : 'enabled' }</a> URL previews by default. You have <a href="#/settings">{ UserSettingsStore.getUrlPreviewsDisabled() ? 'disabled' : 'enabled' }</a> URL previews by default.
@ -142,13 +143,13 @@ module.exports = React.createClass({
<input type="checkbox" ref="userEnableUrlPreview" <input type="checkbox" ref="userEnableUrlPreview"
onChange={ this.onUserEnableUrlPreviewChange } onChange={ this.onUserEnableUrlPreviewChange }
checked={ this.state.userEnableUrlPreview } /> checked={ this.state.userEnableUrlPreview } />
Enable URL previews for this room (affects only you) {_t("Enable URL previews for this room (affects only you)")}
</label> </label>
<label> <label>
<input type="checkbox" ref="userDisableUrlPreview" <input type="checkbox" ref="userDisableUrlPreview"
onChange={ this.onUserDisableUrlPreviewChange } onChange={ this.onUserDisableUrlPreviewChange }
checked={ this.state.userDisableUrlPreview } /> checked={ this.state.userDisableUrlPreview } />
Disable URL previews for this room (affects only you) {_t("Disable URL previews for this room (affects only you)")}
</label> </label>
</div> </div>
); );

View file

@ -14,11 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require('react'); import React from 'react';
var MatrixClientPeg = require("../../../MatrixClientPeg"); import MatrixClientPeg from "../../../MatrixClientPeg";
var sdk = require('../../../index'); import sdk from '../../../index';
var dis = require("../../../dispatcher"); import dis from "../../../dispatcher";
var ObjectUtils = require('../../../ObjectUtils'); import ObjectUtils from '../../../ObjectUtils';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'AuxPanel', displayName: 'AuxPanel',
@ -79,7 +81,7 @@ module.exports = React.createClass({
title="Drop File Here"> title="Drop File Here">
<TintableSvg src="img/upload-big.svg" width="45" height="59"/> <TintableSvg src="img/upload-big.svg" width="45" height="59"/>
<br/> <br/>
Drop file here to upload {_t("Drop file here to upload")}
</div> </div>
</div> </div>
); );
@ -89,7 +91,7 @@ module.exports = React.createClass({
if (this.props.displayConfCallNotification) { if (this.props.displayConfCallNotification) {
var supportedText, joinText; var supportedText, joinText;
if (!MatrixClientPeg.get().supportsVoip()) { if (!MatrixClientPeg.get().supportsVoip()) {
supportedText = " (unsupported)"; supportedText = _t(" (unsupported)");
} }
else { else {
joinText = (<span> joinText = (<span>
@ -101,7 +103,7 @@ module.exports = React.createClass({
} }
conferenceCallNotification = ( conferenceCallNotification = (
<div className="mx_RoomView_ongoingConfCallNotification"> <div className="mx_RoomView_ongoingConfCallNotification">
Ongoing conference call{ supportedText }. { joinText } {_t("Ongoing conference call%(supportedText)s. %(joinText)s", {supportedText: supportedText, joinText: joinText})}
</div> </div>
); );
} }

View file

@ -16,8 +16,10 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); var React = require('react');
var classNames = require("classnames"); var classNames = require("classnames");
import { _t } from '../../../languageHandler';
var Modal = require('../../../Modal'); var Modal = require('../../../Modal');
var sdk = require('../../../index'); var sdk = require('../../../index');
@ -36,6 +38,7 @@ var eventTileTypes = {
'm.call.answer' : 'messages.TextualEvent', 'm.call.answer' : 'messages.TextualEvent',
'm.call.hangup' : 'messages.TextualEvent', 'm.call.hangup' : 'messages.TextualEvent',
'm.room.name' : 'messages.TextualEvent', 'm.room.name' : 'messages.TextualEvent',
'm.room.avatar' : 'messages.RoomAvatarEvent',
'm.room.topic' : 'messages.TextualEvent', 'm.room.topic' : 'messages.TextualEvent',
'm.room.third_party_invite' : 'messages.TextualEvent', 'm.room.third_party_invite' : 'messages.TextualEvent',
'm.room.history_visibility' : 'messages.TextualEvent', 'm.room.history_visibility' : 'messages.TextualEvent',
@ -129,6 +132,9 @@ module.exports = WithMatrixClient(React.createClass({
* for now. * for now.
*/ */
tileShape: React.PropTypes.string, tileShape: React.PropTypes.string,
// show twelve hour timestamps
isTwelveHour: React.PropTypes.bool,
}, },
getInitialState: function() { getInitialState: function() {
@ -404,9 +410,10 @@ module.exports = WithMatrixClient(React.createClass({
var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1); var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted; const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
var classes = classNames({ const classes = classNames({
mx_EventTile: true, mx_EventTile: true,
mx_EventTile_info: isInfoMessage, mx_EventTile_info: isInfoMessage,
mx_EventTile_12hr: this.props.isTwelveHour,
mx_EventTile_encrypting: this.props.eventSendStatus == 'encrypting', mx_EventTile_encrypting: this.props.eventSendStatus == 'encrypting',
mx_EventTile_sending: isSending, mx_EventTile_sending: isSending,
mx_EventTile_notSent: this.props.eventSendStatus == 'not_sent', mx_EventTile_notSent: this.props.eventSendStatus == 'not_sent',
@ -464,9 +471,9 @@ module.exports = WithMatrixClient(React.createClass({
if (needsSenderProfile) { if (needsSenderProfile) {
let aux = null; let aux = null;
if (!this.props.tileShape) { if (!this.props.tileShape) {
if (msgtype === 'm.image') aux = "sent an image"; if (msgtype === 'm.image') aux = _t('sent an image');
else if (msgtype === 'm.video') aux = "sent a video"; else if (msgtype === 'm.video') aux = _t('sent a video');
else if (msgtype === 'm.file') aux = "uploaded a file"; else if (msgtype === 'm.file') aux = _t('uploaded a file');
sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />; sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />;
} }
else { else {
@ -474,11 +481,10 @@ module.exports = WithMatrixClient(React.createClass({
} }
} }
var editButton = ( const editButton = (
<span className="mx_EventTile_editButton" title="Options" onClick={this.onEditClicked} /> <span className="mx_EventTile_editButton" title={ _t("Options") } onClick={this.onEditClicked} />
); );
let e2e;
var e2e;
// cosmetic padlocks: // cosmetic padlocks:
if ((e2eEnabled && this.props.eventSendStatus) || this.props.mxEvent.getType() === 'm.room.encryption') { if ((e2eEnabled && this.props.eventSendStatus) || this.props.mxEvent.getType() === 'm.room.encryption') {
e2e = <img style={{ cursor: 'initial', marginLeft: '-1px' }} className="mx_EventTile_e2eIcon" alt="Encrypted by verified device" src="img/e2e-verified.svg" width="10" height="12" />; e2e = <img style={{ cursor: 'initial', marginLeft: '-1px' }} className="mx_EventTile_e2eIcon" alt="Encrypted by verified device" src="img/e2e-verified.svg" width="10" height="12" />;
@ -489,7 +495,7 @@ module.exports = WithMatrixClient(React.createClass({
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Undecryptable" src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />; e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Undecryptable" src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />;
} }
else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) { else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Encrypted by verified device" src="img/e2e-verified.svg" width="10" height="12"/>; e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Encrypted by verified device" src="img/e2e-verified.svg" width="10" height="12"/>;
} }
else { else {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Encrypted by unverified device" src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }}/>; e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Encrypted by unverified device" src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }}/>;
@ -499,11 +505,10 @@ module.exports = WithMatrixClient(React.createClass({
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Unencrypted message" src="img/e2e-unencrypted.svg" width="12" height="12"/>; e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Unencrypted message" src="img/e2e-unencrypted.svg" width="12" height="12"/>;
} }
const timestamp = this.props.mxEvent.getTs() ? const timestamp = this.props.mxEvent.getTs() ?
<MessageTimestamp ts={this.props.mxEvent.getTs()} /> : null; <MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
if (this.props.tileShape === "notif") { if (this.props.tileShape === "notif") {
var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId()); const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
return ( return (
<div className={classes}> <div className={classes}>
<div className="mx_EventTile_roomName"> <div className="mx_EventTile_roomName">

View file

@ -0,0 +1,96 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2017 Michael Telatynski
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 { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
import KeyCode from '../../../KeyCode';
module.exports = React.createClass({
displayName: 'ForwardMessage',
propTypes: {
currentRoomId: React.PropTypes.string.isRequired,
/* the MatrixEvent to be forwarded */
mxEvent: React.PropTypes.object.isRequired,
onCancelClick: React.PropTypes.func.isRequired,
},
componentWillMount: function() {
dis.dispatch({
action: 'ui_opacity',
leftOpacity: 1.0,
rightOpacity: 0.3,
middleOpacity: 0.5,
});
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
document.addEventListener('keydown', this._onKeyDown);
},
componentWillUnmount: function() {
dis.dispatch({
action: 'ui_opacity',
sideOpacity: 1.0,
middleOpacity: 1.0,
});
dis.unregister(this.dispatcherRef);
document.removeEventListener('keydown', this._onKeyDown);
},
onAction: function(payload) {
if (payload.action === 'view_room') {
const event = this.props.mxEvent;
const Client = MatrixClientPeg.get();
Client.sendEvent(payload.room_id, event.getType(), event.getContent()).done(() => {
dis.dispatch({action: 'message_sent'});
}, (err) => {
if (err.name === "UnknownDeviceError") {
dis.dispatch({
action: 'unknown_device_error',
err: err,
room: Client.getRoom(payload.room_id),
});
}
dis.dispatch({action: 'message_send_failed'});
});
if (this.props.currentRoomId === payload.room_id) this.props.onCancelClick();
}
},
_onKeyDown: function(ev) {
switch (ev.keyCode) {
case KeyCode.ESCAPE:
this.props.onCancelClick();
break;
}
},
render: function() {
return (
<div className="mx_ForwardMessage">
<h1>{_t('Please select the destination room for this message')}</h1>
</div>
);
},
});

View file

@ -31,6 +31,7 @@ import classNames from 'classnames';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import createRoom from '../../../createRoom'; import createRoom from '../../../createRoom';
import DMRoomMap from '../../../utils/DMRoomMap'; import DMRoomMap from '../../../utils/DMRoomMap';
import Unread from '../../../Unread'; import Unread from '../../../Unread';
@ -219,7 +220,7 @@ module.exports = WithMatrixClient(React.createClass({
onKick: function() { onKick: function() {
const membership = this.props.member.membership; const membership = this.props.member.membership;
const kickLabel = membership === "invite" ? "Disinvite" : "Kick"; const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick");
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createDialog(ConfirmUserActionDialog, { Modal.createDialog(ConfirmUserActionDialog, {
member: this.props.member, member: this.props.member,
@ -241,7 +242,7 @@ module.exports = WithMatrixClient(React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Kick error: " + err); console.error("Kick error: " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to kick", title: _t("Failed to kick"),
description: ((err && err.message) ? err.message : "Operation failed"), description: ((err && err.message) ? err.message : "Operation failed"),
}); });
} }
@ -256,7 +257,7 @@ module.exports = WithMatrixClient(React.createClass({
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createDialog(ConfirmUserActionDialog, { Modal.createDialog(ConfirmUserActionDialog, {
member: this.props.member, member: this.props.member,
action: this.props.member.membership == 'ban' ? 'Unban' : 'Ban', action: this.props.member.membership == 'ban' ? _t("Unban") : _t("Ban"),
askReason: this.props.member.membership != 'ban', askReason: this.props.member.membership != 'ban',
danger: this.props.member.membership != 'ban', danger: this.props.member.membership != 'ban',
onFinished: (proceed, reason) => { onFinished: (proceed, reason) => {
@ -283,8 +284,8 @@ module.exports = WithMatrixClient(React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Ban error: " + err); console.error("Ban error: " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: _t("Error"),
description: "Failed to ban user", description: _t("Failed to ban user"),
}); });
} }
).finally(()=>{ ).finally(()=>{
@ -333,8 +334,8 @@ module.exports = WithMatrixClient(React.createClass({
}, function(err) { }, function(err) {
console.error("Mute error: " + err); console.error("Mute error: " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: _t("Error"),
description: "Failed to mute user", description: _t("Failed to mute user"),
}); });
} }
).finally(()=>{ ).finally(()=>{
@ -376,14 +377,14 @@ module.exports = WithMatrixClient(React.createClass({
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') { if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, { Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register", title: _t("Please Register"),
description: "This action cannot be performed by a guest user. Please register to be able to do this." description: _t("This action cannot be performed by a guest user. Please register to be able to do this") + ".",
}); });
} else { } else {
console.error("Toggle moderator error:" + err); console.error("Toggle moderator error:" + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: _t("Error"),
description: "Failed to toggle moderator status", description: _t("Failed to toggle moderator status"),
}); });
} }
} }
@ -403,8 +404,8 @@ module.exports = WithMatrixClient(React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change power level " + err); console.error("Failed to change power level " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: _t("Error"),
description: "Failed to change power level", description: _t("Failed to change power level"),
}); });
} }
).finally(()=>{ ).finally(()=>{
@ -432,13 +433,13 @@ module.exports = WithMatrixClient(React.createClass({
if (parseInt(myPower) === parseInt(powerLevel)) { if (parseInt(myPower) === parseInt(powerLevel)) {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Warning", title: _t("Warning!"),
description: description:
<div> <div>
You will not be able to undo this change as you are promoting the user to have the same power level as yourself.<br/> { _t("You will not be able to undo this change as you are promoting the user to have the same power level as yourself") }.<br/>
Are you sure? { _t("Are you sure?") }
</div>, </div>,
button: "Continue", button: _t("Continue"),
onFinished: function(confirmed) { onFinished: function(confirmed) {
if (confirmed) { if (confirmed) {
self._applyPowerChange(roomId, target, powerLevel, powerLevelEvent); self._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
@ -581,9 +582,9 @@ module.exports = WithMatrixClient(React.createClass({
// still loading // still loading
devComponents = <Spinner />; devComponents = <Spinner />;
} else if (devices === null) { } else if (devices === null) {
devComponents = "Unable to load device list"; devComponents = _t("Unable to load device list");
} else if (devices.length === 0) { } else if (devices.length === 0) {
devComponents = "No devices with registered encryption keys"; devComponents = _t("No devices with registered encryption keys");
} else { } else {
devComponents = []; devComponents = [];
for (var i = 0; i < devices.length; i++) { for (var i = 0; i < devices.length; i++) {
@ -595,7 +596,7 @@ module.exports = WithMatrixClient(React.createClass({
return ( return (
<div> <div>
<h3>Devices</h3> <h3>{ _t("Devices") }</h3>
<div className="mx_MemberInfo_devices"> <div className="mx_MemberInfo_devices">
{devComponents} {devComponents}
</div> </div>
@ -644,11 +645,11 @@ module.exports = WithMatrixClient(React.createClass({
<div className="mx_RoomTile_avatar"> <div className="mx_RoomTile_avatar">
<img src="img/create-big.svg" width="26" height="26" /> <img src="img/create-big.svg" width="26" height="26" />
</div> </div>
<div className={labelClasses}><i>Start new chat</i></div> <div className={labelClasses}><i>{ _t("Start a chat") }</i></div>
</AccessibleButton>; </AccessibleButton>;
startChat = <div> startChat = <div>
<h3>Direct chats</h3> <h3>{ _t("Direct chats") }</h3>
{tiles} {tiles}
{startNewChat} {startNewChat}
</div>; </div>;
@ -661,7 +662,7 @@ module.exports = WithMatrixClient(React.createClass({
if (this.state.can.kick) { if (this.state.can.kick) {
const membership = this.props.member.membership; const membership = this.props.member.membership;
const kickLabel = membership === "invite" ? "Disinvite" : "Kick"; const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick");
kickButton = ( kickButton = (
<AccessibleButton className="mx_MemberInfo_field" <AccessibleButton className="mx_MemberInfo_field"
onClick={this.onKick}> onClick={this.onKick}>
@ -670,9 +671,9 @@ module.exports = WithMatrixClient(React.createClass({
); );
} }
if (this.state.can.ban) { if (this.state.can.ban) {
let label = 'Ban'; let label = _t("Ban");
if (this.props.member.membership == 'ban') { if (this.props.member.membership == 'ban') {
label = 'Unban'; label = _t("Unban");
} }
banButton = ( banButton = (
<AccessibleButton className="mx_MemberInfo_field" <AccessibleButton className="mx_MemberInfo_field"
@ -682,7 +683,7 @@ module.exports = WithMatrixClient(React.createClass({
); );
} }
if (this.state.can.mute) { if (this.state.can.mute) {
const muteLabel = this.state.muted ? "Unmute" : "Mute"; const muteLabel = this.state.muted ? _t("Unmute") : _t("Mute");
muteButton = ( muteButton = (
<AccessibleButton className="mx_MemberInfo_field" <AccessibleButton className="mx_MemberInfo_field"
onClick={this.onMuteToggle}> onClick={this.onMuteToggle}>
@ -691,7 +692,7 @@ module.exports = WithMatrixClient(React.createClass({
); );
} }
if (this.state.can.toggleMod) { if (this.state.can.toggleMod) {
var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator"; var giveOpLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator");
giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}> giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}>
{giveOpLabel} {giveOpLabel}
</AccessibleButton>; </AccessibleButton>;
@ -742,7 +743,7 @@ module.exports = WithMatrixClient(React.createClass({
{ this.props.member.userId } { this.props.member.userId }
</div> </div>
<div className="mx_MemberInfo_profileField"> <div className="mx_MemberInfo_profileField">
Level: <b><PowerSelector controlled={true} value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b> { _t("Level") }: <b><PowerSelector controlled={true} value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b>
</div> </div>
<div className="mx_MemberInfo_profileField"> <div className="mx_MemberInfo_profileField">
<PresenceLabel activeAgo={ presenceLastActiveAgo } <PresenceLabel activeAgo={ presenceLastActiveAgo }

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require('react'); var React = require('react');
import { _t } from '../../../languageHandler';
var classNames = require('classnames'); var classNames = require('classnames');
var Matrix = require("matrix-js-sdk"); var Matrix = require("matrix-js-sdk");
var q = require('q'); var q = require('q');
@ -27,12 +28,6 @@ var CallHandler = require("../../../CallHandler");
var Invite = require("../../../Invite"); var Invite = require("../../../Invite");
var INITIAL_LOAD_NUM_MEMBERS = 30; var INITIAL_LOAD_NUM_MEMBERS = 30;
var SHARE_HISTORY_WARNING =
<span>
Newly invited users will see the history of this room. <br/>
If you'd prefer invited users not to see messages that were sent before they joined, <br/>
turn off, 'Share message history with new users' in the settings for this room.
</span>;
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MemberList', displayName: 'MemberList',
@ -207,7 +202,9 @@ module.exports = React.createClass({
// For now we'll pretend this is any entity. It should probably be a separate tile. // For now we'll pretend this is any entity. It should probably be a separate tile.
var EntityTile = sdk.getComponent("rooms.EntityTile"); var EntityTile = sdk.getComponent("rooms.EntityTile");
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "..."; var text = (overflowCount > 1)
? _t("and %(overflowCount)s others...", { overflowCount: overflowCount })
: _t("and one other...");
return ( return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={ <EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} /> <BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
@ -352,7 +349,7 @@ module.exports = React.createClass({
if (invitedMemberTiles.length > 0) { if (invitedMemberTiles.length > 0) {
invitedSection = ( invitedSection = (
<div className="mx_MemberList_invited"> <div className="mx_MemberList_invited">
<h2>Invited</h2> <h2>{ _t("Invited") }</h2>
<div className="mx_MemberList_wrapper"> <div className="mx_MemberList_wrapper">
{invitedMemberTiles} {invitedMemberTiles}
</div> </div>
@ -363,8 +360,8 @@ module.exports = React.createClass({
var inputBox = ( var inputBox = (
<form autoComplete="off"> <form autoComplete="off">
<input className="mx_MemberList_query" id="mx_MemberList_query" type="text" <input className="mx_MemberList_query" id="mx_MemberList_query" type="text"
onChange={this.onSearchQueryChanged} value={this.state.searchQuery} onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
placeholder="Filter room members" /> placeholder={ _t('Filter room members') } />
</form> </form>
); );

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require('react'); var React = require('react');
import { _t } from '../../../languageHandler';
var CallHandler = require('../../../CallHandler'); var CallHandler = require('../../../CallHandler');
var MatrixClientPeg = require('../../../MatrixClientPeg'); var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require('../../../Modal'); var Modal = require('../../../Modal');
@ -93,8 +93,8 @@ export default class MessageComposer extends React.Component {
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, { Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register", title: _t('Please Register'),
description: "Guest users can't upload files. Please register to upload.", description: _t('Guest users can\'t upload files. Please register to upload') + '.',
}); });
return; return;
} }
@ -118,10 +118,10 @@ export default class MessageComposer extends React.Component {
} }
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Upload Files", title: _t('Upload Files'),
description: ( description: (
<div> <div>
<p>Are you sure you want upload the following files?</p> <p>{ _t('Are you sure you want to upload the following files?') }</p>
<ul style={{listStyle: 'none', textAlign: 'left'}}> <ul style={{listStyle: 'none', textAlign: 'left'}}>
{fileList} {fileList}
</ul> </ul>
@ -240,11 +240,11 @@ export default class MessageComposer extends React.Component {
if (roomIsEncrypted) { if (roomIsEncrypted) {
// FIXME: show a /!\ if there are untrusted devices in the room... // FIXME: show a /!\ if there are untrusted devices in the room...
e2eImg = 'img/e2e-verified.svg'; e2eImg = 'img/e2e-verified.svg';
e2eTitle = 'Encrypted room'; e2eTitle = _t('Encrypted room');
e2eClass = 'mx_MessageComposer_e2eIcon'; e2eClass = 'mx_MessageComposer_e2eIcon';
} else { } else {
e2eImg = 'img/e2e-unencrypted.svg'; e2eImg = 'img/e2e-unencrypted.svg';
e2eTitle = 'Unencrypted room'; e2eTitle = _t('Unencrypted room');
e2eClass = 'mx_MessageComposer_e2eIcon mx_filterFlipColor'; e2eClass = 'mx_MessageComposer_e2eIcon mx_filterFlipColor';
} }
@ -257,16 +257,16 @@ export default class MessageComposer extends React.Component {
if (this.props.callState && this.props.callState !== 'ended') { if (this.props.callState && this.props.callState !== 'ended') {
hangupButton = hangupButton =
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}> <div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
<img src="img/hangup.svg" alt="Hangup" title="Hangup" width="25" height="26"/> <img src="img/hangup.svg" alt={ _t('Hangup') } title={ _t('Hangup') } width="25" height="26"/>
</div>; </div>;
} }
else { else {
callButton = callButton =
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title="Voice call"> <div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={ _t('Voice call') }>
<TintableSvg src="img/icon-call.svg" width="35" height="35"/> <TintableSvg src="img/icon-call.svg" width="35" height="35"/>
</div>; </div>;
videoCallButton = videoCallButton =
<div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title="Video call"> <div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title={ _t('Video call') }>
<TintableSvg src="img/icons-video.svg" width="35" height="35"/> <TintableSvg src="img/icons-video.svg" width="35" height="35"/>
</div>; </div>;
} }
@ -280,7 +280,7 @@ export default class MessageComposer extends React.Component {
// complex because of conference calls. // complex because of conference calls.
var uploadButton = ( var uploadButton = (
<div key="controls_upload" className="mx_MessageComposer_upload" <div key="controls_upload" className="mx_MessageComposer_upload"
onClick={this.onUploadClick} title="Upload file"> onClick={this.onUploadClick} title={ _t('Upload file') }>
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/> <TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
<input ref="uploadInput" type="file" <input ref="uploadInput" type="file"
style={uploadInputStyle} style={uploadInputStyle}
@ -300,7 +300,7 @@ export default class MessageComposer extends React.Component {
); );
const placeholderText = roomIsEncrypted ? const placeholderText = roomIsEncrypted ?
"Send an encrypted message…" : "Send a message (unencrypted)…"; _t('Send an encrypted message') + '…' : _t('Send a message (unencrypted)') + '…';
controls.push( controls.push(
<MessageComposerInput <MessageComposerInput
@ -325,7 +325,7 @@ export default class MessageComposer extends React.Component {
} else { } else {
controls.push( controls.push(
<div key="controls_error" className="mx_MessageComposer_noperm_error"> <div key="controls_error" className="mx_MessageComposer_noperm_error">
You do not have permission to post to this room { _t('You do not have permission to post to this room') }
</div> </div>
); );
} }
@ -354,7 +354,7 @@ export default class MessageComposer extends React.Component {
mx_filterFlipColor: true, mx_filterFlipColor: true,
}); });
return <img className={className} return <img className={className}
title={name} title={ _t(name) }
onMouseDown={disabled ? null : onFormatButtonClicked} onMouseDown={disabled ? null : onFormatButtonClicked}
key={name} key={name}
src={`img/button-text-${name}${suffix}.svg`} src={`img/button-text-${name}${suffix}.svg`}
@ -374,11 +374,11 @@ export default class MessageComposer extends React.Component {
<div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}> <div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}>
{formatButtons} {formatButtons}
<div style={{flex: 1}}></div> <div style={{flex: 1}}></div>
<img title={`Turn Markdown ${this.state.inputState.isRichtextEnabled ? 'on' : 'off'}`} <img title={ this.state.inputState.isRichtextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off") }
onMouseDown={this.onToggleMarkdownClicked} onMouseDown={this.onToggleMarkdownClicked}
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor" className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} /> src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
<img title="Hide Text Formatting Toolbar" <img title={ _t("Hide Text Formatting Toolbar") }
onClick={this.onToggleFormattingClicked} onClick={this.onToggleFormattingClicked}
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor" className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
src="img/icon-text-cancel.svg" /> src="img/icon-text-cancel.svg" />

View file

@ -30,6 +30,7 @@ import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
import SlashCommands from '../../../SlashCommands'; import SlashCommands from '../../../SlashCommands';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import KeyCode from '../../../KeyCode'; import KeyCode from '../../../KeyCode';
@ -504,8 +505,8 @@ export default class MessageComposerInput extends React.Component {
console.error("Command failure: %s", err); console.error("Command failure: %s", err);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Server error", title: _t("Server error"),
description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."), description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong.")),
}); });
}); });
} }
@ -513,8 +514,8 @@ export default class MessageComposerInput extends React.Component {
console.error(cmd.error); console.error(cmd.error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Command error", title: _t("Command error"),
description: cmd.error description: cmd.error,
}); });
} }
return true; return true;
@ -719,7 +720,7 @@ export default class MessageComposerInput extends React.Component {
<div className={className}> <div className={className}>
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor" <img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"
onMouseDown={this.onMarkdownToggleClicked} onMouseDown={this.onMarkdownToggleClicked}
title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`} title={ this.state.isRichtextEnabled ? _t("Markdown is disabled") : _t("Markdown is enabled")}
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} /> src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
<Editor ref="editor" <Editor ref="editor"
placeholder={this.props.placeholder} placeholder={this.props.placeholder}

Some files were not shown because too many files have changed in this diff Show more