Merge branch 'develop' into travis/redact-to-remove

This commit is contained in:
Travis Ralston 2017-05-28 17:45:32 -04:00 committed by GitHub
commit 1b6685c5da
120 changed files with 7005 additions and 2054 deletions

View file

@ -64,7 +64,7 @@ module.exports = {
// to JSX. // to JSX.
ignorePattern: '^\\s*<', ignorePattern: '^\\s*<',
ignoreComments: true, ignoreComments: true,
code: 90, code: 120,
}], }],
"valid-jsdoc": ["warn"], "valid-jsdoc": ["warn"],
"new-cap": ["warn"], "new-cap": ["warn"],

3
.gitignore vendored
View file

@ -9,3 +9,6 @@ npm-debug.log
# test reports created by karma # test reports created by karma
/karma-reports /karma-reports
/.idea
/src/component-index.js

View file

@ -9,11 +9,16 @@ set -ev
RIOT_WEB_DIR=riot-web RIOT_WEB_DIR=riot-web
REACT_SDK_DIR=`pwd` REACT_SDK_DIR=`pwd`
git clone --depth=1 --branch develop https://github.com/vector-im/riot-web.git \ curbranch="${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH}"
echo "Determined branch to be $curbranch"
git clone https://github.com/vector-im/riot-web.git \
"$RIOT_WEB_DIR" "$RIOT_WEB_DIR"
cd "$RIOT_WEB_DIR" cd "$RIOT_WEB_DIR"
git checkout "$curbranch" || git checkout develop
mkdir node_modules mkdir node_modules
npm install npm install

View file

@ -1,3 +1,175 @@
Changes in [0.8.9](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.9) (2017-05-22)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.9-rc.1...v0.8.9)
* No changes
Changes in [0.8.9-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.9-rc.1) (2017-05-19)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8...v0.8.9-rc.1)
* Prevent an exception getting scroll node
[\#902](https://github.com/matrix-org/matrix-react-sdk/pull/902)
* Fix a few remaining snags with country dd
[\#901](https://github.com/matrix-org/matrix-react-sdk/pull/901)
* Add left_aligned class to CountryDropdown
[\#900](https://github.com/matrix-org/matrix-react-sdk/pull/900)
* Swap to new flag files (which are stored as GB.png)
[\#899](https://github.com/matrix-org/matrix-react-sdk/pull/899)
* Improve phone number country dropdown for registration and login (Act. 2,
Return of the Prefix)
[\#897](https://github.com/matrix-org/matrix-react-sdk/pull/897)
* Support for pasting files into normal composer
[\#892](https://github.com/matrix-org/matrix-react-sdk/pull/892)
* tell guests they can't use filepanel until they register
[\#887](https://github.com/matrix-org/matrix-react-sdk/pull/887)
* Prevent reskindex -w from running when file names have not changed
[\#888](https://github.com/matrix-org/matrix-react-sdk/pull/888)
* I broke UserSettings for webpack-dev-server
[\#884](https://github.com/matrix-org/matrix-react-sdk/pull/884)
* various fixes to RoomHeader
[\#880](https://github.com/matrix-org/matrix-react-sdk/pull/880)
* remove /me whether or not it has a space after it
[\#885](https://github.com/matrix-org/matrix-react-sdk/pull/885)
* show error if we can't set a filter because no room
[\#883](https://github.com/matrix-org/matrix-react-sdk/pull/883)
* Fix RM not updating if RR event unpaginated
[\#874](https://github.com/matrix-org/matrix-react-sdk/pull/874)
* change roomsettings wording
[\#878](https://github.com/matrix-org/matrix-react-sdk/pull/878)
* make reskindex windows friendly
[\#875](https://github.com/matrix-org/matrix-react-sdk/pull/875)
* Fixes 2 issues with Dialog closing
[\#867](https://github.com/matrix-org/matrix-react-sdk/pull/867)
* Automatic Reskindex
[\#871](https://github.com/matrix-org/matrix-react-sdk/pull/871)
* Put room name in 'leave room' confirmation dialog
[\#873](https://github.com/matrix-org/matrix-react-sdk/pull/873)
* Fix this/self fail in LeftPanel
[\#872](https://github.com/matrix-org/matrix-react-sdk/pull/872)
* Don't show null URL previews
[\#870](https://github.com/matrix-org/matrix-react-sdk/pull/870)
* Fix keys for AddressSelector
[\#869](https://github.com/matrix-org/matrix-react-sdk/pull/869)
* Make left panel better for new users (mk II)
[\#859](https://github.com/matrix-org/matrix-react-sdk/pull/859)
* Explicitly save composer content onUnload
[\#866](https://github.com/matrix-org/matrix-react-sdk/pull/866)
* Warn on unload
[\#851](https://github.com/matrix-org/matrix-react-sdk/pull/851)
* Log deviceid at login
[\#862](https://github.com/matrix-org/matrix-react-sdk/pull/862)
* Guests can't send RR so no point trying
[\#860](https://github.com/matrix-org/matrix-react-sdk/pull/860)
* Remove babelcheck
[\#861](https://github.com/matrix-org/matrix-react-sdk/pull/861)
* T3chguy/settings versions improvements
[\#857](https://github.com/matrix-org/matrix-react-sdk/pull/857)
* Change max-len 90->120
[\#852](https://github.com/matrix-org/matrix-react-sdk/pull/852)
* Remove DM-guessing code
[\#829](https://github.com/matrix-org/matrix-react-sdk/pull/829)
* Fix jumping to an unread event when in MELS
[\#855](https://github.com/matrix-org/matrix-react-sdk/pull/855)
* Validate phone number on login
[\#856](https://github.com/matrix-org/matrix-react-sdk/pull/856)
* Failed to enable HTML5 Notifications Error Dialogs
[\#827](https://github.com/matrix-org/matrix-react-sdk/pull/827)
* Pin filesize ver to fix break upstream
[\#854](https://github.com/matrix-org/matrix-react-sdk/pull/854)
* Improve RoomDirectory Look & Feel
[\#848](https://github.com/matrix-org/matrix-react-sdk/pull/848)
* Only show jumpToReadMarker bar when RM !== RR
[\#845](https://github.com/matrix-org/matrix-react-sdk/pull/845)
* Allow MELS to have its own RM
[\#846](https://github.com/matrix-org/matrix-react-sdk/pull/846)
* Use document.onkeydown instead of onkeypress
[\#844](https://github.com/matrix-org/matrix-react-sdk/pull/844)
* (Room)?Avatar: Request 96x96 avatars on high DPI screens
[\#808](https://github.com/matrix-org/matrix-react-sdk/pull/808)
* Add mx_EventTile_emote class
[\#842](https://github.com/matrix-org/matrix-react-sdk/pull/842)
* Fix dialog reappearing after hitting Enter
[\#841](https://github.com/matrix-org/matrix-react-sdk/pull/841)
* Fix spinner that shows until the first sync
[\#840](https://github.com/matrix-org/matrix-react-sdk/pull/840)
* Show spinner until first sync has completed
[\#839](https://github.com/matrix-org/matrix-react-sdk/pull/839)
* Style fixes for LoggedInView
[\#838](https://github.com/matrix-org/matrix-react-sdk/pull/838)
* Fix specifying custom server for registration
[\#834](https://github.com/matrix-org/matrix-react-sdk/pull/834)
* Improve country dropdown UX and expose +prefix
[\#833](https://github.com/matrix-org/matrix-react-sdk/pull/833)
* Fix user settings store
[\#836](https://github.com/matrix-org/matrix-react-sdk/pull/836)
* show the room name in the UDE Dialog
[\#832](https://github.com/matrix-org/matrix-react-sdk/pull/832)
* summarise profile changes in MELS
[\#826](https://github.com/matrix-org/matrix-react-sdk/pull/826)
* Transform h1 and h2 tags to h3 tags
[\#820](https://github.com/matrix-org/matrix-react-sdk/pull/820)
* limit our keyboard shortcut modifiers correctly
[\#825](https://github.com/matrix-org/matrix-react-sdk/pull/825)
* Specify cross platform regexes and add olm to noParse
[\#823](https://github.com/matrix-org/matrix-react-sdk/pull/823)
* Remember element that was in focus before rendering dialog
[\#822](https://github.com/matrix-org/matrix-react-sdk/pull/822)
* move user settings outward and use built in read receipts disabling
[\#824](https://github.com/matrix-org/matrix-react-sdk/pull/824)
* File Download Consistency
[\#802](https://github.com/matrix-org/matrix-react-sdk/pull/802)
* Show Access Token under Advanced in Settings
[\#806](https://github.com/matrix-org/matrix-react-sdk/pull/806)
* Link tags/commit hashes in the UserSettings version section
[\#810](https://github.com/matrix-org/matrix-react-sdk/pull/810)
* On return to RoomView from auxPanel, send focus back to Composer
[\#813](https://github.com/matrix-org/matrix-react-sdk/pull/813)
* Change presence status labels to 'for' instead of 'ago'
[\#817](https://github.com/matrix-org/matrix-react-sdk/pull/817)
* Disable Scalar Integrations if urls passed to it are falsey
[\#816](https://github.com/matrix-org/matrix-react-sdk/pull/816)
* Add option to hide other people's read receipts.
[\#818](https://github.com/matrix-org/matrix-react-sdk/pull/818)
* Add option to not send typing notifications
[\#819](https://github.com/matrix-org/matrix-react-sdk/pull/819)
* Sync RM across instances of Riot
[\#805](https://github.com/matrix-org/matrix-react-sdk/pull/805)
* First iteration on improving login UI
[\#811](https://github.com/matrix-org/matrix-react-sdk/pull/811)
* focus on composer after jumping to bottom
[\#809](https://github.com/matrix-org/matrix-react-sdk/pull/809)
* Improve RoomList performance via side-stepping React
[\#807](https://github.com/matrix-org/matrix-react-sdk/pull/807)
* Don't show link preview when link is inside of a quote
[\#762](https://github.com/matrix-org/matrix-react-sdk/pull/762)
* Escape closes UserSettings
[\#765](https://github.com/matrix-org/matrix-react-sdk/pull/765)
* Implement user power-level changes in timeline
[\#794](https://github.com/matrix-org/matrix-react-sdk/pull/794)
Changes in [0.8.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8) (2017-04-25)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.2...v0.8.8)
* No changes
Changes in [0.8.8-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.2) (2017-04-24)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.1...v0.8.8-rc.2)
* Fix bug where links to Riot would fail to open.
Changes in [0.8.8-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.1) (2017-04-21)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7...v0.8.8-rc.1)
* Update js-sdk to fix registration without a captcha (https://github.com/vector-im/riot-web/issues/3621)
Changes in [0.8.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7) (2017-04-12) Changes in [0.8.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7) (2017-04-12)
=================================================================================================== ===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.4...v0.8.7) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.4...v0.8.7)

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

@ -69,25 +69,41 @@ General Style
console.log("I am a fish"); // Bad console.log("I am a fish"); // Bad
} }
``` ```
- No new line before else, catch, finally, etc:
```javascript
if (x) {
console.log("I am a fish");
} else {
console.log("I am a chimp"); // Good
}
if (x) {
console.log("I am a fish");
}
else {
console.log("I am a chimp"); // Bad
}
```
- Declare one variable per var statement (consistent with Node). Unless they - Declare one variable per var statement (consistent with Node). Unless they
are simple and closely related. If you put the next declaration on a new line, are simple and closely related. If you put the next declaration on a new line,
treat yourself to another `var`: treat yourself to another `var`:
```javascript ```javascript
var key = "foo", const key = "foo",
comparator = function(x, y) { comparator = function(x, y) {
return x - y; return x - y;
}; // Bad }; // Bad
var key = "foo"; const key = "foo";
var comparator = function(x, y) { const comparator = function(x, y) {
return x - y; return x - y;
}; // Good }; // Good
var x = 0, y = 0; // Fine let x = 0, y = 0; // Fine
var x = 0; let x = 0;
var y = 0; // Also fine let y = 0; // Also fine
``` ```
- A single line `if` is fine, all others have braces. This prevents errors when adding to the code.: - A single line `if` is fine, all others have braces. This prevents errors when adding to the code.:

1
header
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.

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,7 +173,6 @@ 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'),
], ],
}, },

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "0.8.7", "version": "0.8.9",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -31,9 +31,11 @@
"reskindex": "scripts/reskindex.js" "reskindex": "scripts/reskindex.js"
}, },
"scripts": { "scripts": {
"reskindex": "scripts/reskindex.js -h header", "reskindex": "node scripts/reskindex.js -h header",
"build": "node scripts/babelcheck.js && babel src -d lib --source-maps", "reskindex:watch": "node scripts/reskindex.js -h header -w",
"start": "node scripts/babelcheck.js && babel src -w -d lib --source-maps", "build": "npm run reskindex && babel src -d lib --source-maps",
"build:watch": "babel src -w -d lib --source-maps",
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
"lint": "eslint src/", "lint": "eslint src/",
"lintall": "eslint src/ test/", "lintall": "eslint src/ test/",
"clean": "rimraf lib", "clean": "rimraf lib",
@ -48,12 +50,13 @@
"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",
"emojione": "2.2.3", "emojione": "2.2.3",
"file-saver": "^1.3.3", "file-saver": "^1.3.3",
"filesize": "^3.1.2", "filesize": "3.5.6",
"flux": "^2.0.3", "flux": "^2.0.3",
"fuse.js": "^2.2.0", "fuse.js": "^2.2.0",
"glob": "^5.0.14", "glob": "^5.0.14",
@ -67,7 +70,7 @@
"react": "^15.4.0", "react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2", "react-addons-css-transition-group": "15.3.2",
"react-dom": "^15.4.0", "react-dom": "^15.4.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#39d858c", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"sanitize-html": "^1.11.1", "sanitize-html": "^1.11.1",
"text-encoding-utf-8": "^1.0.1", "text-encoding-utf-8": "^1.0.1",
"velocity-vector": "vector-im/velocity#059e3b2", "velocity-vector": "vector-im/velocity#059e3b2",
@ -88,6 +91,7 @@
"babel-preset-es2016": "^6.11.3", "babel-preset-es2016": "^6.11.3",
"babel-preset-es2017": "^6.14.0", "babel-preset-es2017": "^6.14.0",
"babel-preset-react": "^6.11.1", "babel-preset-react": "^6.11.1",
"chokidar": "^1.6.1",
"eslint": "^3.13.1", "eslint": "^3.13.1",
"eslint-config-google": "^0.7.1", "eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^4.0.1", "eslint-plugin-babel": "^4.0.1",
@ -104,6 +108,7 @@
"karma-sourcemap-loader": "^0.3.7", "karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^1.7.0", "karma-webpack": "^1.7.0",
"mocha": "^2.4.5", "mocha": "^2.4.5",
"parallelshell": "^1.2.0",
"phantomjs-prebuilt": "^2.1.7", "phantomjs-prebuilt": "^2.1.7",
"react-addons-test-utils": "^15.4.0", "react-addons-test-utils": "^15.4.0",
"require-json": "0.0.1", "require-json": "0.0.1",

View file

@ -1,22 +0,0 @@
#!/usr/bin/env node
var exec = require('child_process').exec;
// Makes sure the babel executable in the path is babel 6 (or greater), not
// babel 5, which it is if you upgrade from an older version of react-sdk and
// run 'npm install' since the package has changed to babel-cli, so 'babel'
// remains installed and the executable in node_modules/.bin remains as babel
// 5.
exec("babel -V", function (error, stdout, stderr) {
if ((error && error.code) || parseInt(stdout.substr(0,1), 10) < 6) {
console.log("\033[31m\033[1m"+
'*****************************************\n'+
'* matrix-react-sdk has moved to babel 6 *\n'+
'* Please "rm -rf node_modules && npm i" *\n'+
'* then restore links as appropriate *\n'+
'*****************************************\n'+
"\033[91m");
process.exit(1);
}
});

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\(\s*'(.*?[^\\])'/sg) {
my $s = $1;
$s =~ s/\\'/'/g;
push @$strings, [$s, $file];
}
while ($src =~ /_t\(\s*"(.*?[^\\])"/sg) {
push @$strings, [$1, $file];
}
}
return $strings;
}

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

@ -0,0 +1,91 @@
#!/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.
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

@ -1,53 +1,99 @@
#!/usr/bin/env node #!/usr/bin/env node
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var glob = require('glob'); var glob = require('glob');
var args = require('optimist').argv; var args = require('optimist').argv;
var chokidar = require('chokidar');
var header = args.h || args.header;
var componentsDir = path.join('src', 'components');
var componentIndex = path.join('src', 'component-index.js'); var componentIndex = path.join('src', 'component-index.js');
var componentIndexTmp = componentIndex+".tmp";
var componentsDir = path.join('src', 'components');
var componentGlob = '**/*.js';
var prevFiles = [];
var packageJson = JSON.parse(fs.readFileSync('./package.json')); function reskindex() {
var files = glob.sync(componentGlob, {cwd: componentsDir}).sort();
if (!filesHaveChanged(files, prevFiles)) {
return;
}
prevFiles = files;
var strm = fs.createWriteStream(componentIndex); var header = args.h || args.header;
var packageJson = JSON.parse(fs.readFileSync('./package.json'));
if (header) { var strm = fs.createWriteStream(componentIndexTmp);
strm.write(fs.readFileSync(header));
strm.write('\n'); if (header) {
strm.write(fs.readFileSync(header));
strm.write('\n');
}
strm.write("/*\n");
strm.write(" * THIS FILE IS AUTO-GENERATED\n");
strm.write(" * You can edit it you like, but your changes will be overwritten,\n");
strm.write(" * so you'd just be trying to swim upstream like a salmon.\n");
strm.write(" * You are not a salmon.\n");
strm.write(" */\n\n");
if (packageJson['matrix-react-parent']) {
const parentIndex = packageJson['matrix-react-parent'] +
'/lib/component-index';
strm.write(
`let components = require('${parentIndex}').components;
if (!components) {
throw new Error("'${parentIndex}' didn't export components");
}
`);
} else {
strm.write("let components = {};\n");
}
for (var i = 0; i < files.length; ++i) {
var file = files[i].replace('.js', '');
var moduleName = (file.replace(/\//g, '.'));
var importName = moduleName.replace(/\./g, "$");
strm.write("import " + importName + " from './components/" + file + "';\n");
strm.write(importName + " && (components['"+moduleName+"'] = " + importName + ");");
strm.write('\n');
strm.uncork();
}
strm.write("export {components};\n");
strm.end();
fs.rename(componentIndexTmp, componentIndex, function(err) {
if(err) {
console.error("Error moving new index into place: " + err);
} else {
console.log('Reskindex: completed');
}
});
} }
strm.write("/*\n"); // Expects both arrays of file names to be sorted
strm.write(" * THIS FILE IS AUTO-GENERATED\n"); function filesHaveChanged(files, prevFiles) {
strm.write(" * You can edit it you like, but your changes will be overwritten,\n"); if (files.length !== prevFiles.length) {
strm.write(" * so you'd just be trying to swim upstream like a salmon.\n"); return true;
strm.write(" * You are not a salmon.\n"); }
strm.write(" *\n"); // Check for name changes
strm.write(" * To update it, run:\n"); for (var i = 0; i < files.length; i++) {
strm.write(" * ./reskindex.js -h header\n"); if (prevFiles[i] !== files[i]) {
strm.write(" */\n\n"); return true;
}
if (packageJson['matrix-react-parent']) { }
strm.write("module.exports.components = require('"+packageJson['matrix-react-parent']+"/lib/component-index').components;\n\n"); return false;
} else {
strm.write("module.exports.components = {};\n");
} }
var files = glob.sync('**/*.js', {cwd: componentsDir}).sort(); // -w indicates watch mode where any FS events will trigger reskindex
for (var i = 0; i < files.length; ++i) { if (!args.w) {
var file = files[i].replace('.js', ''); reskindex();
return;
var moduleName = (file.replace(/\//g, '.'));
var importName = moduleName.replace(/\./g, "$");
strm.write("import " + importName + " from './components/" + file + "';\n");
strm.write(importName + " && (module.exports.components['"+moduleName+"'] = " + importName + ");");
strm.write('\n');
strm.uncork();
} }
strm.end(); var watchDebouncer = null;
chokidar.watch(path.join(componentsDir, componentGlob)).on('all', (event, path) => {
if (path === componentIndex) return;
if (watchDebouncer) clearTimeout(watchDebouncer);
watchDebouncer = setTimeout(reskindex, 1000);
});

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})`;

View file

@ -22,8 +22,8 @@ module.exports = {
avatarUrlForMember: function(member, width, height, resizeMethod) { avatarUrlForMember: function(member, width, height, resizeMethod) {
var url = member.getAvatarUrl( var url = member.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(), MatrixClientPeg.get().getHomeserverUrl(),
width, Math.floor(width * window.devicePixelRatio),
height, Math.floor(height * window.devicePixelRatio),
resizeMethod, resizeMethod,
false, false,
false false
@ -40,7 +40,9 @@ module.exports = {
avatarUrlForUser: function(user, width, height, resizeMethod) { avatarUrlForUser: function(user, width, height, resizeMethod) {
var url = ContentRepo.getHttpUriForMxc( var url = ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
width, height, resizeMethod Math.floor(width * window.devicePixelRatio),
Math.floor(height * window.devicePixelRatio),
resizeMethod
); );
if (!url || url.length === 0) { if (!url || url.length === 0) {
return null; return null;
@ -57,4 +59,3 @@ module.exports = {
return 'img/' + images[total % images.length] + '.png'; return 'img/' + images[total % images.length] + '.png';
} }
}; };

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

@ -1,62 +0,0 @@
/*
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.
*/
// singleton which dispatches invocations of a given type & argument
// rather than just a type (as per EventEmitter and Flux's dispatcher etc)
//
// This means you can have a single point which listens for an EventEmitter event
// and then dispatches out to one of thousands of RoomTiles (for instance) rather than
// having each RoomTile register for the EventEmitter event and having to
// iterate over all of them.
class ConstantTimeDispatcher {
constructor() {
// type -> arg -> [ listener(arg, params) ]
this.listeners = {};
}
register(type, arg, listener) {
if (!this.listeners[type]) this.listeners[type] = {};
if (!this.listeners[type][arg]) this.listeners[type][arg] = [];
this.listeners[type][arg].push(listener);
}
unregister(type, arg, listener) {
if (this.listeners[type] && this.listeners[type][arg]) {
var i = this.listeners[type][arg].indexOf(listener);
if (i > -1) {
this.listeners[type][arg].splice(i, 1);
}
}
else {
console.warn("Unregistering unrecognised listener (type=" + type + ", arg=" + arg + ")");
}
}
dispatch(type, arg, params) {
if (!this.listeners[type] || !this.listeners[type][arg]) {
console.warn("No registered listeners for dispatch (type=" + type + ", arg=" + arg + ")");
return;
}
this.listeners[type][arg].forEach(listener=>{
listener.call(arg, params);
});
}
}
if (!global.constantTimeDispatcher) {
global.constantTimeDispatcher = new ConstantTimeDispatcher();
}
module.exports = global.constantTimeDispatcher;

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,38 +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) {
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
function pad(n) {
return (n < 10 ? '0' : '') + n;
}
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', {
weekDayName: days[date.getDay()],
monthName: months[date.getMonth()],
day: date.getDate(),
time: this.formatTime(date),
});
} }
/* return this.formatFullDate(date);
else {
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
}
*/
}, },
formatTime: function(date) { formatFullDate: function(date) {
//return pad(date.getHours()) + ':' + pad(date.getMinutes()); const days = getDaysArray();
return ('00' + date.getHours()).slice(-2) + ':' + ('00' + date.getMinutes()).slice(-2); 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: this.formatTime(date),
});
},
formatTime: function(date, showTwelveHour=false) {
if (showTwelveHour) {
return twelveHourTime(date);
}
return pad(date.getHours()) + ':' + pad(date.getMinutes());
},
};

View file

@ -111,8 +111,7 @@ var sanitizeHtmlParams = {
allowedTags: [ allowedTags: [
'font', // custom to matrix for IRC-style font coloring 'font', // custom to matrix for IRC-style font coloring
'del', // for markdown 'del', // for markdown
// deliberately no h1/h2 to stop people shouting. 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', 'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span',
], ],
@ -149,17 +148,18 @@ var sanitizeHtmlParams = {
attribs.href = m[1]; attribs.href = m[1];
delete attribs.target; delete attribs.target;
} }
else {
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN); m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
if (m) { if (m) {
var entity = m[1]; var entity = m[1];
if (entity[0] === '@') { if (entity[0] === '@') {
attribs.href = '#/user/' + entity; attribs.href = '#/user/' + entity;
}
else if (entity[0] === '#' || entity[0] === '!') {
attribs.href = '#/room/' + entity;
}
delete attribs.target;
} }
else if (entity[0] === '#' || entity[0] === '!') {
attribs.href = '#/room/' + entity;
}
delete attribs.target;
} }
} }
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/

View file

@ -32,5 +32,4 @@ module.exports = {
DELETE: 46, DELETE: 46,
KEY_D: 68, KEY_D: 68,
KEY_E: 69, KEY_E: 69,
KEY_K: 75,
}; };

View file

@ -27,6 +27,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
@ -49,7 +50,7 @@ import sdk from './index';
* If any of steps 1-4 are successful, it will call {setLoggedIn}, which in * If any of steps 1-4 are successful, it will call {setLoggedIn}, which in
* turn will raise on_logged_in and will_start_client events. * turn will raise on_logged_in and will_start_client events.
* *
* It returns a promise which resolves when the above process completes. * @param {object} opts
* *
* @param {object} opts.realQueryParams: string->string map of the * @param {object} opts.realQueryParams: string->string map of the
* query-parameters extracted from the real query-string of the starting * query-parameters extracted from the real query-string of the starting
@ -67,6 +68,7 @@ import sdk from './index';
* @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is * @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is
* true; defines the IS to use. * true; defines the IS to use.
* *
* @returns {Promise} a promise which resolves when the above process completes.
*/ */
export function loadSession(opts) { export function loadSession(opts) {
const realQueryParams = opts.realQueryParams || {}; const realQueryParams = opts.realQueryParams || {};
@ -127,7 +129,7 @@ export function loadSession(opts) {
function _loginWithToken(queryParams, defaultDeviceDisplayName) { function _loginWithToken(queryParams, defaultDeviceDisplayName) {
// create a temporary MatrixClient to do the login // create a temporary MatrixClient to do the login
var client = Matrix.createClient({ const client = Matrix.createClient({
baseUrl: queryParams.homeserver, baseUrl: queryParams.homeserver,
}); });
@ -159,7 +161,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
// Not really sure where the right home for it is. // Not really sure where the right home for it is.
// create a temporary MatrixClient to do the login // create a temporary MatrixClient to do the login
var client = Matrix.createClient({ const client = Matrix.createClient({
baseUrl: hsUrl, baseUrl: hsUrl,
}); });
@ -188,30 +190,30 @@ function _restoreFromLocalStorage() {
if (!localStorage) { if (!localStorage) {
return q(false); return q(false);
} }
const hs_url = localStorage.getItem("mx_hs_url"); const hsUrl = localStorage.getItem("mx_hs_url");
const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
const access_token = localStorage.getItem("mx_access_token"); const accessToken = localStorage.getItem("mx_access_token");
const user_id = localStorage.getItem("mx_user_id"); const userId = localStorage.getItem("mx_user_id");
const device_id = localStorage.getItem("mx_device_id"); const deviceId = localStorage.getItem("mx_device_id");
let is_guest; let isGuest;
if (localStorage.getItem("mx_is_guest") !== null) { if (localStorage.getItem("mx_is_guest") !== null) {
is_guest = localStorage.getItem("mx_is_guest") === "true"; isGuest = localStorage.getItem("mx_is_guest") === "true";
} else { } else {
// legacy key name // legacy key name
is_guest = localStorage.getItem("matrix-is-guest") === "true"; isGuest = localStorage.getItem("matrix-is-guest") === "true";
} }
if (access_token && user_id && hs_url) { if (accessToken && userId && hsUrl) {
console.log("Restoring session for %s", user_id); console.log("Restoring session for %s", userId);
try { try {
setLoggedIn({ setLoggedIn({
userId: user_id, userId: userId,
deviceId: device_id, deviceId: deviceId,
accessToken: access_token, accessToken: accessToken,
homeserverUrl: hs_url, homeserverUrl: hsUrl,
identityServerUrl: is_url, identityServerUrl: isUrl,
guest: is_guest, guest: isGuest,
}); });
return q(true); return q(true);
} catch (e) { } catch (e) {
@ -228,14 +230,14 @@ 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 for this device and submit the public key to your homeserver. This is a once off; sorry for the inconvenience.'
+ "This is a once off; sorry for the inconvenience."; );
_clearLocalStorage(); _clearLocalStorage();
return q.reject(new Error( return q.reject(new Error(
"Unable to restore previous session: " + msg, _t('Unable to restore previous session') + ': ' + msg,
)); ));
} }
@ -273,9 +275,13 @@ export function initRtsClient(url) {
*/ */
export function setLoggedIn(credentials) { export function setLoggedIn(credentials) {
credentials.guest = Boolean(credentials.guest); credentials.guest = Boolean(credentials.guest);
console.log("setLoggedIn => %s (guest=%s) hs=%s",
credentials.userId, credentials.guest, console.log(
credentials.homeserverUrl); "setLoggedIn: mxid:", credentials.userId,
"deviceId:", credentials.deviceId,
"guest:", credentials.guest,
"hs:", credentials.homeserverUrl,
);
// This is dispatched to indicate that the user is still in the process of logging in // This is dispatched to indicate that the user is still in the process of logging in
// because `teamPromise` may take some time to resolve, breaking the assumption that // because `teamPromise` may take some time to resolve, breaking the assumption that
// `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms // `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms
@ -352,7 +358,7 @@ export function logout() {
return; return;
} }
return MatrixClientPeg.get().logout().then(onLoggedOut, MatrixClientPeg.get().logout().then(onLoggedOut,
(err) => { (err) => {
// Just throwing an error here is going to be very unhelpful // Just throwing an error here is going to be very unhelpful
// if you're trying to log out because your server's down and // if you're trying to log out because your server's down and
@ -363,8 +369,8 @@ export function logout() {
// change your password). // change your password).
console.log("Failed to call logout API: token will not be invalidated"); console.log("Failed to call logout API: token will not be invalidated");
onLoggedOut(); onLoggedOut();
} },
); ).done();
} }
/** /**
@ -420,7 +426,7 @@ export function stopMatrixClient() {
UserActivity.stop(); UserActivity.stop();
Presence.stop(); Presence.stop();
if (DMRoomMap.shared()) DMRoomMap.shared().stop(); if (DMRoomMap.shared()) DMRoomMap.shared().stop();
var cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli) { if (cli) {
cli.stopClient(); cli.stopClient();
cli.removeAllListeners(); cli.removeAllListeners();

View file

@ -15,11 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var MatrixClientPeg = require("./MatrixClientPeg"); import MatrixClientPeg from './MatrixClientPeg';
var PlatformPeg = require("./PlatformPeg"); import PlatformPeg from './PlatformPeg';
var TextForEvent = require('./TextForEvent'); import TextForEvent from './TextForEvent';
var Avatar = require('./Avatar'); import Avatar from './Avatar';
var dis = require("./dispatcher"); import dis from './dispatcher';
import sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal';
/* /*
* Dispatches: * Dispatches:
@ -29,7 +32,7 @@ var dis = require("./dispatcher");
* } * }
*/ */
var Notifier = { const Notifier = {
notifsByRoom: {}, notifsByRoom: {},
notificationMessageForEvent: function(ev) { notificationMessageForEvent: function(ev) {
@ -48,16 +51,16 @@ var Notifier = {
return; return;
} }
var msg = this.notificationMessageForEvent(ev); let msg = this.notificationMessageForEvent(ev);
if (!msg) return; if (!msg) return;
var title; let title;
if (!ev.sender || room.name == ev.sender.name) { if (!ev.sender || room.name === ev.sender.name) {
title = room.name; title = room.name;
// notificationMessageForEvent includes sender, // notificationMessageForEvent includes sender,
// but we already have the sender here // but we already have the sender here
if (ev.getContent().body) msg = ev.getContent().body; if (ev.getContent().body) msg = ev.getContent().body;
} else if (ev.getType() == 'm.room.member') { } else if (ev.getType() === 'm.room.member') {
// context is all in the message here, we don't need // context is all in the message here, we don't need
// to display sender info // to display sender info
title = room.name; title = room.name;
@ -68,7 +71,7 @@ var Notifier = {
if (ev.getContent().body) msg = ev.getContent().body; if (ev.getContent().body) msg = ev.getContent().body;
} }
var avatarUrl = ev.sender ? Avatar.avatarUrlForMember( const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
ev.sender, 40, 40, 'crop' ev.sender, 40, 40, 'crop'
) : null; ) : null;
@ -83,7 +86,7 @@ var Notifier = {
}, },
_playAudioNotification: function(ev, room) { _playAudioNotification: function(ev, room) {
var e = document.getElementById("messageAudio"); const e = document.getElementById("messageAudio");
if (e) { if (e) {
e.load(); e.load();
e.play(); e.play();
@ -95,7 +98,7 @@ var Notifier = {
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this); this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this); this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline); MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt); MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt);
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange); MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
this.toolbarHidden = false; this.toolbarHidden = false;
this.isSyncing = false; this.isSyncing = false;
@ -104,7 +107,7 @@ var Notifier = {
stop: function() { stop: function() {
if (MatrixClientPeg.get() && this.boundOnRoomTimeline) { if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline); MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt); MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt);
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange); MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
} }
this.isSyncing = false; this.isSyncing = false;
@ -121,7 +124,7 @@ var Notifier = {
// 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) {
if(global.localStorage.getItem('audio_notifications_enabled') == null) { if (global.localStorage.getItem('audio_notifications_enabled') === null) {
this.setAudioEnabled(this.isEnabled()); this.setAudioEnabled(this.isEnabled());
} }
} }
@ -131,6 +134,14 @@ var Notifier = {
plaf.requestNotificationPermission().done((result) => { plaf.requestNotificationPermission().done((result) => {
if (result !== 'granted') { if (result !== 'granted') {
// The permission request was dismissed or denied // The permission request was dismissed or denied
const description = result === 'denied'
? _t('Riot does not have permission to send you notifications - please check your browser settings')
: _t('Riot was not given permission to send notifications - please try again');
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createDialog(ErrorDialog, {
title: _t('Unable to enable Notifications'),
description,
});
return; return;
} }
@ -141,7 +152,7 @@ var Notifier = {
if (callback) callback(); if (callback) callback();
dis.dispatch({ dis.dispatch({
action: "notifier_enabled", action: "notifier_enabled",
value: true value: true,
}); });
}); });
// clear the notifications_hidden flag, so that if notifications are // clear the notifications_hidden flag, so that if notifications are
@ -152,7 +163,7 @@ var Notifier = {
global.localStorage.setItem('notifications_enabled', 'false'); global.localStorage.setItem('notifications_enabled', 'false');
dis.dispatch({ dis.dispatch({
action: "notifier_enabled", action: "notifier_enabled",
value: false value: false,
}); });
} }
}, },
@ -165,7 +176,7 @@ var Notifier = {
if (!global.localStorage) return true; if (!global.localStorage) return true;
var enabled = global.localStorage.getItem('notifications_enabled'); const enabled = global.localStorage.getItem('notifications_enabled');
if (enabled === null) return true; if (enabled === null) return true;
return enabled === 'true'; return enabled === 'true';
}, },
@ -173,12 +184,12 @@ var Notifier = {
setAudioEnabled: function(enable) { setAudioEnabled: function(enable) {
if (!global.localStorage) return; if (!global.localStorage) return;
global.localStorage.setItem('audio_notifications_enabled', global.localStorage.setItem('audio_notifications_enabled',
enable ? 'true' : 'false'); enable ? 'true' : 'false');
}, },
isAudioEnabled: function(enable) { isAudioEnabled: function(enable) {
if (!global.localStorage) return true; if (!global.localStorage) return true;
var enabled = global.localStorage.getItem( const enabled = global.localStorage.getItem(
'audio_notifications_enabled'); 'audio_notifications_enabled');
// default to true if the popups are enabled // default to true if the popups are enabled
if (enabled === null) return this.isEnabled(); if (enabled === null) return this.isEnabled();
@ -192,7 +203,7 @@ var Notifier = {
// this is nothing to do with notifier_enabled // this is nothing to do with notifier_enabled
dis.dispatch({ dis.dispatch({
action: "notifier_enabled", action: "notifier_enabled",
value: this.isEnabled() value: this.isEnabled(),
}); });
// update the info to localStorage for persistent settings // update the info to localStorage for persistent settings
@ -215,8 +226,7 @@ var Notifier = {
onSyncStateChange: function(state) { onSyncStateChange: function(state) {
if (state === "SYNCING") { if (state === "SYNCING") {
this.isSyncing = true; this.isSyncing = true;
} } else if (state === "STOPPED" || state === "ERROR") {
else if (state === "STOPPED" || state === "ERROR") {
this.isSyncing = false; this.isSyncing = false;
} }
}, },
@ -225,10 +235,10 @@ var Notifier = {
if (toStartOfTimeline) return; if (toStartOfTimeline) return;
if (!room) return; if (!room) return;
if (!this.isSyncing) return; // don't alert for any messages initially if (!this.isSyncing) return; // don't alert for any messages initially
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return; if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return;
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev); const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.notify) { if (actions && actions.notify) {
if (this.isEnabled()) { if (this.isEnabled()) {
this._displayPopupNotification(ev, room); this._displayPopupNotification(ev, room);
@ -240,7 +250,7 @@ var Notifier = {
}, },
onRoomReceipt: function(ev, room) { onRoomReceipt: function(ev, room) {
if (room.getUnreadNotificationCount() == 0) { if (room.getUnreadNotificationCount() === 0) {
// ideally we would clear each notification when it was read, // ideally we would clear each notification when it was read,
// but we have no way, given a read receipt, to know whether // but we have no way, given a read receipt, to know whether
// the receipt comes before or after an event, so we can't // the receipt comes before or after an event, so we can't
@ -255,7 +265,7 @@ var Notifier = {
} }
delete this.notifsByRoom[room.roomId]; delete this.notifsByRoom[room.roomId];
} }
} },
}; };
if (!global.mxNotifier) { if (!global.mxNotifier) {

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

@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var MatrixClientPeg = require("./MatrixClientPeg"); import MatrixClientPeg from "./MatrixClientPeg";
var dis = require("./dispatcher"); import dis from "./dispatcher";
var Tinter = require("./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,58 +42,64 @@ class Command {
} }
getUsage() { getUsage() {
return "Usage: " + this.getCommandWithArgs(); return _t('Usage') + ': ' + this.getCommandWithArgs();
} }
} }
var reject = function(msg) { function reject(msg) {
return { return {
error: msg error: msg,
}; };
}; }
var success = function(promise) { function success(promise) {
return { return {
promise: promise promise: promise,
}; };
}; }
var commands = { /* Disable the "unexpected this" error for these commands - all of the run
* functions are called with `this` bound to the Command instance.
*/
/* eslint-disable babel/no-invalid-this */
const commands = {
ddg: new Command("ddg", "<query>", function(roomId, args) { ddg: new Command("ddg", "<query>", function(roomId, args) {
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();
}), }),
// Change your nickname // Change your nickname
nick: new Command("nick", "<display_name>", function(room_id, args) { nick: new Command("nick", "<display_name>", function(roomId, args) {
if (args) { if (args) {
return success( return success(
MatrixClientPeg.get().setDisplayName(args) MatrixClientPeg.get().setDisplayName(args),
); );
} }
return reject(this.getUsage()); return reject(this.getUsage());
}), }),
// Changes the colorscheme of your current room // Changes the colorscheme of your current room
tint: new Command("tint", "<color1> [<color2>]", function(room_id, args) { tint: new Command("tint", "<color1> [<color2>]", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); const matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/);
if (matches) { if (matches) {
Tinter.tint(matches[1], matches[4]); Tinter.tint(matches[1], matches[4]);
var colorScheme = {}; const colorScheme = {};
colorScheme.primary_color = matches[1]; colorScheme.primary_color = matches[1];
if (matches[4]) { if (matches[4]) {
colorScheme.secondary_color = matches[4]; colorScheme.secondary_color = matches[4];
} }
return success( return success(
MatrixClientPeg.get().setRoomAccountData( MatrixClientPeg.get().setRoomAccountData(
room_id, "org.matrix.room.color_scheme", colorScheme roomId, "org.matrix.room.color_scheme", colorScheme,
) ),
); );
} }
} }
@ -100,22 +107,22 @@ var commands = {
}), }),
// Change the room topic // Change the room topic
topic: new Command("topic", "<topic>", function(room_id, args) { topic: new Command("topic", "<topic>", function(roomId, args) {
if (args) { if (args) {
return success( return success(
MatrixClientPeg.get().setRoomTopic(room_id, args) MatrixClientPeg.get().setRoomTopic(roomId, args),
); );
} }
return reject(this.getUsage()); return reject(this.getUsage());
}), }),
// Invite a user // Invite a user
invite: new Command("invite", "<userId>", function(room_id, args) { invite: new Command("invite", "<userId>", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(\S+)$/); const matches = args.match(/^(\S+)$/);
if (matches) { if (matches) {
return success( return success(
MatrixClientPeg.get().invite(room_id, matches[1]) MatrixClientPeg.get().invite(roomId, matches[1]),
); );
} }
} }
@ -123,21 +130,21 @@ var commands = {
}), }),
// Join a room // Join a room
join: new Command("join", "#alias:domain", function(room_id, args) { join: new Command("join", "#alias:domain", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(\S+)$/); const matches = args.match(/^(\S+)$/);
if (matches) { if (matches) {
var room_alias = matches[1]; let roomAlias = matches[1];
if (room_alias[0] !== '#') { if (roomAlias[0] !== '#') {
return reject(this.getUsage()); return reject(this.getUsage());
} }
if (!room_alias.match(/:/)) { if (!roomAlias.match(/:/)) {
room_alias += ':' + MatrixClientPeg.get().getDomain(); roomAlias += ':' + MatrixClientPeg.get().getDomain();
} }
dis.dispatch({ dis.dispatch({
action: 'view_room', action: 'view_room',
room_alias: room_alias, room_alias: roomAlias,
auto_join: true, auto_join: true,
}); });
@ -147,29 +154,29 @@ var commands = {
return reject(this.getUsage()); return reject(this.getUsage());
}), }),
part: new Command("part", "[#alias:domain]", function(room_id, args) { part: new Command("part", "[#alias:domain]", function(roomId, args) {
var targetRoomId; let targetRoomId;
if (args) { if (args) {
var matches = args.match(/^(\S+)$/); const matches = args.match(/^(\S+)$/);
if (matches) { if (matches) {
var room_alias = matches[1]; let roomAlias = matches[1];
if (room_alias[0] !== '#') { if (roomAlias[0] !== '#') {
return reject(this.getUsage()); return reject(this.getUsage());
} }
if (!room_alias.match(/:/)) { if (!roomAlias.match(/:/)) {
room_alias += ':' + MatrixClientPeg.get().getDomain(); roomAlias += ':' + MatrixClientPeg.get().getDomain();
} }
// Try to find a room with this alias // Try to find a room with this alias
var rooms = MatrixClientPeg.get().getRooms(); const rooms = MatrixClientPeg.get().getRooms();
for (var i = 0; i < rooms.length; i++) { for (let i = 0; i < rooms.length; i++) {
var aliasEvents = rooms[i].currentState.getStateEvents( const aliasEvents = rooms[i].currentState.getStateEvents(
"m.room.aliases" "m.room.aliases",
); );
for (var j = 0; j < aliasEvents.length; j++) { for (let j = 0; j < aliasEvents.length; j++) {
var aliases = aliasEvents[j].getContent().aliases || []; const aliases = aliasEvents[j].getContent().aliases || [];
for (var k = 0; k < aliases.length; k++) { for (let k = 0; k < aliases.length; k++) {
if (aliases[k] === room_alias) { if (aliases[k] === roomAlias) {
targetRoomId = rooms[i].roomId; targetRoomId = rooms[i].roomId;
break; break;
} }
@ -178,27 +185,28 @@ var commands = {
} }
if (targetRoomId) { break; } if (targetRoomId) { break; }
} }
} if (!targetRoomId) {
if (!targetRoomId) { return reject("Unrecognised room alias: " + roomAlias);
return reject("Unrecognised room alias: " + room_alias); }
} }
} }
if (!targetRoomId) targetRoomId = room_id; if (!targetRoomId) targetRoomId = roomId;
return success( return success(
MatrixClientPeg.get().leave(targetRoomId).then( MatrixClientPeg.get().leave(targetRoomId).then(
function() { function() {
dis.dispatch({action: 'view_next_room'}); dis.dispatch({action: 'view_next_room'});
}) },
),
); );
}), }),
// Kick a user from the room with an optional reason // Kick a user from the room with an optional reason
kick: new Command("kick", "<userId> [<reason>]", function(room_id, args) { kick: new Command("kick", "<userId> [<reason>]", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(\S+?)( +(.*))?$/); const matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) { if (matches) {
return success( return success(
MatrixClientPeg.get().kick(room_id, matches[1], matches[3]) MatrixClientPeg.get().kick(roomId, matches[1], matches[3]),
); );
} }
} }
@ -206,12 +214,12 @@ var commands = {
}), }),
// Ban a user from the room with an optional reason // Ban a user from the room with an optional reason
ban: new Command("ban", "<userId> [<reason>]", function(room_id, args) { ban: new Command("ban", "<userId> [<reason>]", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(\S+?)( +(.*))?$/); const matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) { if (matches) {
return success( return success(
MatrixClientPeg.get().ban(room_id, matches[1], matches[3]) MatrixClientPeg.get().ban(roomId, matches[1], matches[3]),
); );
} }
} }
@ -219,13 +227,13 @@ var commands = {
}), }),
// Unban a user from the room // Unban a user from the room
unban: new Command("unban", "<userId>", function(room_id, args) { unban: new Command("unban", "<userId>", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(\S+)$/); const matches = args.match(/^(\S+)$/);
if (matches) { if (matches) {
// Reset the user membership to "leave" to unban him // Reset the user membership to "leave" to unban him
return success( return success(
MatrixClientPeg.get().unban(room_id, matches[1]) MatrixClientPeg.get().unban(roomId, matches[1]),
); );
} }
} }
@ -233,27 +241,27 @@ var commands = {
}), }),
// Define the power level of a user // Define the power level of a user
op: new Command("op", "<userId> [<power level>]", function(room_id, args) { op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(\S+?)( +(\d+))?$/); const matches = args.match(/^(\S+?)( +(\d+))?$/);
var powerLevel = 50; // default power level for op let powerLevel = 50; // default power level for op
if (matches) { if (matches) {
var user_id = matches[1]; const userId = matches[1];
if (matches.length === 4 && undefined !== matches[3]) { if (matches.length === 4 && undefined !== matches[3]) {
powerLevel = parseInt(matches[3]); powerLevel = parseInt(matches[3]);
} }
if (powerLevel !== NaN) { if (!isNaN(powerLevel)) {
var room = MatrixClientPeg.get().getRoom(room_id); const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) { if (!room) {
return reject("Bad room ID: " + room_id); return reject("Bad room ID: " + roomId);
} }
var powerLevelEvent = room.currentState.getStateEvents( const powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "" "m.room.power_levels", "",
); );
return success( return success(
MatrixClientPeg.get().setPowerLevel( MatrixClientPeg.get().setPowerLevel(
room_id, user_id, powerLevel, powerLevelEvent roomId, userId, powerLevel, powerLevelEvent,
) ),
); );
} }
} }
@ -262,32 +270,87 @@ var commands = {
}), }),
// Reset the power level of a user // Reset the power level of a user
deop: new Command("deop", "<userId>", function(room_id, args) { deop: new Command("deop", "<userId>", function(roomId, args) {
if (args) { if (args) {
var matches = args.match(/^(\S+)$/); const matches = args.match(/^(\S+)$/);
if (matches) { if (matches) {
var room = MatrixClientPeg.get().getRoom(room_id); const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) { if (!room) {
return reject("Bad room ID: " + room_id); return reject("Bad room ID: " + roomId);
} }
var powerLevelEvent = room.currentState.getStateEvents( const powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "" "m.room.power_levels", "",
); );
return success( return success(
MatrixClientPeg.get().setPowerLevel( MatrixClientPeg.get().setPowerLevel(
room_id, args, undefined, powerLevelEvent roomId, args, undefined, powerLevelEvent,
) ),
); );
} }
} }
return reject(this.getUsage()); return reject(this.getUsage());
}) }),
// Verify a user, device, and pubkey tuple
verify: new Command("verify", "<userId> <deviceId> <deviceSigningKey>", function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
if (matches) {
const userId = matches[1];
const deviceId = matches[2];
const fingerprint = matches[3];
const device = MatrixClientPeg.get().getStoredDevice(userId, deviceId);
if (!device) {
return reject(`Unknown (user, device) pair: (${userId}, ${deviceId})`);
}
if (device.isVerified()) {
if (device.getFingerprint() === fingerprint) {
return reject(`Device already verified!`);
} else {
return reject(`WARNING: Device already verified, but keys do NOT MATCH!`);
}
}
if (device.getFingerprint() === fingerprint) {
MatrixClientPeg.get().setDeviceVerified(
userId, deviceId, true,
);
// Tell the user we verified everything!
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Verified key",
description: (
<div>
<p>
The signing key you provided matches the signing key you received
from { userId }'s device { deviceId }. Device marked as verified.
</p>
</div>
),
hasCancelButton: false,
});
return success();
} else {
return reject(`WARNING: KEY VERIFICATION FAILED! The signing key for ${userId} and device
${deviceId} is "${device.getFingerprint()}" which does not match the provided key
"${fingerprint}". This could mean your communications are being intercepted!`);
}
}
}
return reject(this.getUsage());
}),
}; };
/* eslint-enable babel/no-invalid-this */
// helpful aliases // helpful aliases
var aliases = { const aliases = {
j: "join" j: "join",
}; };
module.exports = { module.exports = {
@ -304,13 +367,13 @@ module.exports = {
// IRC-style commands // IRC-style commands
input = input.replace(/\s+$/, ""); input = input.replace(/\s+$/, "");
if (input[0] === "/" && input[1] !== "/") { if (input[0] === "/" && input[1] !== "/") {
var bits = input.match(/^(\S+?)( +((.|\n)*))?$/); const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
var cmd, args; let cmd;
let args;
if (bits) { if (bits) {
cmd = bits[1].substring(1).toLowerCase(); cmd = bits[1].substring(1).toLowerCase();
args = bits[3]; args = bits[3];
} } else {
else {
cmd = input; cmd = input;
} }
if (cmd === "me") return null; if (cmd === "me") return null;
@ -319,8 +382,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("Unrecognised command: " + input);
} }
} }
@ -329,12 +391,12 @@ module.exports = {
getCommandList: function() { getCommandList: function() {
// Return all the commands plus /me and /markdown which aren't handled like normal commands // Return all the commands plus /me and /markdown which aren't handled like normal commands
var cmds = Object.keys(commands).sort().map(function(cmdKey) { const cmds = Object.keys(commands).sort().map(function(cmdKey) {
return commands[cmdKey]; return commands[cmdKey];
}); });
cmds.push(new Command("me", "<action>", function() {})); cmds.push(new Command("me", "<action>", function() {}));
cmds.push(new Command("markdown", "<on|off>", function() {})); cmds.push(new Command("markdown", "<on|off>", function() {}));
return cmds; return cmds;
} },
}; };

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,95 +25,100 @@ 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 {
// hacky hack for https://github.com/vector-im/vector-web/issues/2020 // suppress null rejoins
return senderName + " rejoined the room."; return '';
} }
} 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 + '"'; 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 +127,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 +209,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 +217,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

@ -25,7 +25,9 @@ module.exports = {
eventTriggersUnreadCount: function(ev) { eventTriggersUnreadCount: function(ev) {
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
return false; return false;
} else if (ev.getType() == "m.room.member") { } else if (ev.getType() == 'm.room.member') {
return false;
} else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') {
return false; return false;
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') { } else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
return false; return false;

View file

@ -32,7 +32,7 @@ class UserActivity {
start() { start() {
document.onmousedown = this._onUserActivity.bind(this); document.onmousedown = this._onUserActivity.bind(this);
document.onmousemove = this._onUserActivity.bind(this); document.onmousemove = this._onUserActivity.bind(this);
document.onkeypress = this._onUserActivity.bind(this); document.onkeydown = this._onUserActivity.bind(this);
// can't use document.scroll here because that's only the document // can't use document.scroll here because that's only the document
// itself being scrolled. Need to use addEventListener's useCapture. // itself being scrolled. Need to use addEventListener's useCapture.
// also this needs to be the wheel event, not scroll, as scroll is // also this needs to be the wheel event, not scroll, as scroll is
@ -50,7 +50,7 @@ class UserActivity {
stop() { stop() {
document.onmousedown = undefined; document.onmousedown = undefined;
document.onmousemove = undefined; document.onmousemove = undefined;
document.onkeypress = undefined; document.onkeydown = undefined;
window.removeEventListener('wheel', this._onUserActivity.bind(this), window.removeEventListener('wheel', this._onUserActivity.bind(this),
{ passive: true, capture: true }); { passive: true, capture: true });
} }

View file

@ -14,26 +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';
var q = require("q"); import MatrixClientPeg from './MatrixClientPeg';
var MatrixClientPeg = require("./MatrixClientPeg"); import Notifier from './Notifier';
var Notifier = require("./Notifier");
/* /*
* 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 = { /*
* TODO: Find a way to translate the names of LABS_FEATURES. In other words, guarantee that languages were already loaded before building this array.
*/
export default {
LABS_FEATURES: [ LABS_FEATURES: [
{ {
name: 'New Composer & Autocomplete', name: "New Composer & Autocomplete",
id: 'rich_text_editor', id: 'rich_text_editor',
default: false, default: false,
}, },
], ],
loadProfileInfo: function() { loadProfileInfo: function() {
var cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
return cli.getProfileInfo(cli.credentials.userId); return cli.getProfileInfo(cli.credentials.userId);
}, },
@ -44,7 +47,7 @@ module.exports = {
loadThreePids: function() { loadThreePids: function() {
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
return q({ return q({
threepids: [] threepids: [],
}); // guests can't poke 3pid endpoint }); // guests can't poke 3pid endpoint
} }
return MatrixClientPeg.get().getThreePids(); return MatrixClientPeg.get().getThreePids();
@ -73,19 +76,19 @@ module.exports = {
Notifier.setAudioEnabled(enable); Notifier.setAudioEnabled(enable);
}, },
changePassword: function(old_password, new_password) { changePassword: function(oldPassword, newPassword) {
var cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
var authDict = { const authDict = {
type: 'm.login.password', type: 'm.login.password',
user: cli.credentials.userId, user: cli.credentials.userId,
password: old_password password: oldPassword,
}; };
return cli.setPassword(authDict, new_password); return cli.setPassword(authDict, newPassword);
}, },
/** /*
* Returns the email pusher (pusher of type 'email') for a given * Returns the email pusher (pusher of type 'email') for a given
* email address. Email pushers all have the same app ID, so since * email address. Email pushers all have the same app ID, so since
* pushers are unique over (app ID, pushkey), there will be at most * pushers are unique over (app ID, pushkey), there will be at most
@ -95,8 +98,8 @@ module.exports = {
if (pushers === undefined) { if (pushers === undefined) {
return undefined; return undefined;
} }
for (var i = 0; i < pushers.length; ++i) { for (let i = 0; i < pushers.length; ++i) {
if (pushers[i].kind == 'email' && pushers[i].pushkey == address) { if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
return pushers[i]; return pushers[i];
} }
} }
@ -110,7 +113,7 @@ module.exports = {
addEmailPusher: function(address, data) { addEmailPusher: function(address, data) {
return MatrixClientPeg.get().setPusher({ return MatrixClientPeg.get().setPusher({
kind: 'email', kind: 'email',
app_id: "m.email", app_id: 'm.email',
pushkey: address, pushkey: address,
app_display_name: 'Email Notifications', app_display_name: 'Email Notifications',
device_display_name: address, device_display_name: address,
@ -121,46 +124,46 @@ module.exports = {
}, },
getUrlPreviewsDisabled: function() { getUrlPreviewsDisabled: function() {
var event = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls"); const event = MatrixClientPeg.get().getAccountData('org.matrix.preview_urls');
return (event && event.getContent().disable); return (event && event.getContent().disable);
}, },
setUrlPreviewsDisabled: function(disabled) { setUrlPreviewsDisabled: function(disabled) {
// FIXME: handle errors // FIXME: handle errors
return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", { return MatrixClientPeg.get().setAccountData('org.matrix.preview_urls', {
disable: disabled disable: disabled,
}); });
}, },
getSyncedSettings: function() { getSyncedSettings: function() {
var event = MatrixClientPeg.get().getAccountData("im.vector.web.settings"); const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings');
return event ? event.getContent() : {}; return event ? event.getContent() : {};
}, },
getSyncedSetting: function(type, defaultValue = null) { getSyncedSetting: function(type, defaultValue = null) {
var settings = this.getSyncedSettings(); const settings = this.getSyncedSettings();
return settings.hasOwnProperty(type) ? settings[type] : null; return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
}, },
setSyncedSetting: function(type, value) { setSyncedSetting: function(type, value) {
var settings = this.getSyncedSettings(); const settings = this.getSyncedSettings();
settings[type] = value; settings[type] = value;
// FIXME: handle errors // FIXME: handle errors
return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings); return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings);
}, },
getLocalSettings: function() { getLocalSettings: function() {
var localSettingsString = localStorage.getItem('mx_local_settings') || '{}'; const localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
return JSON.parse(localSettingsString); return JSON.parse(localSettingsString);
}, },
getLocalSetting: function(type, defaultValue = null) { getLocalSetting: function(type, defaultValue = null) {
var settings = this.getLocalSettings(); const settings = this.getLocalSettings();
return settings.hasOwnProperty(type) ? settings[type] : null; return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
}, },
setLocalSetting: function(type, value) { setLocalSetting: function(type, value) {
var settings = this.getLocalSettings(); const settings = this.getLocalSettings();
settings[type] = value; settings[type] = value;
// FIXME: handle errors // FIXME: handle errors
localStorage.setItem('mx_local_settings', JSON.stringify(settings)); localStorage.setItem('mx_local_settings', JSON.stringify(settings));
@ -171,8 +174,8 @@ module.exports = {
if (MatrixClientPeg.get().isGuest()) return false; if (MatrixClientPeg.get().isGuest()) return false;
if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) { if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) {
for (var i = 0; i < this.LABS_FEATURES.length; i++) { for (let i = 0; i < this.LABS_FEATURES.length; i++) {
var f = this.LABS_FEATURES[i]; const f = this.LABS_FEATURES[i];
if (f.id === feature) { if (f.id === feature) {
return f.default; return f.default;
} }
@ -183,5 +186,5 @@ module.exports = {
setFeatureEnabled: function(feature: string, enabled: boolean) { setFeatureEnabled: function(feature: string, enabled: boolean) {
localStorage.setItem(`mx_labs_feature_${feature}`, enabled); localStorage.setItem(`mx_labs_feature_${feature}`, enabled);
} },
}; };

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

@ -1,8 +1,10 @@
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',
@ -68,7 +70,7 @@ export default class CommandProvider extends AutocompleteProvider {
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,7 +80,7 @@ export default class CommandProvider extends AutocompleteProvider {
} }
getName() { getName() {
return '*️⃣ Commands'; return '*️⃣ ' + _t('Commands');
} }
static getInstance(): CommandProvider { static getInstance(): CommandProvider {

View file

@ -1,4 +1,5 @@
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';
@ -14,7 +15,7 @@ let instance = null;
export default class EmojiProvider extends AutocompleteProvider { export default class EmojiProvider extends AutocompleteProvider {
constructor() { constructor() {
super(EMOJI_REGEX); super(EMOJI_REGEX);
this.fuse = new Fuse(EMOJI_SHORTNAMES); this.fuse = new Fuse(EMOJI_SHORTNAMES, {});
} }
async getCompletions(query: string, selection: SelectionRange) { async getCompletions(query: string, selection: SelectionRange) {
@ -39,7 +40,7 @@ export default class EmojiProvider extends AutocompleteProvider {
} }
getName() { getName() {
return '😃 Emoji'; return '😃 ' + _t('Emoji');
} }
static getInstance() { static getInstance() {

View file

@ -1,4 +1,5 @@
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 +51,7 @@ export default class RoomProvider extends AutocompleteProvider {
} }
getName() { getName() {
return '💬 Rooms'; return '💬 ' + _t('Rooms');
} }
static getInstance() { static getInstance() {

View file

@ -1,4 +1,5 @@
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 +52,7 @@ export default class UserProvider extends AutocompleteProvider {
} }
getName() { getName() {
return '👥 Users'; return '👥 ' + _t('Users');
} }
setUserList(users) { setUserList(users) {

View file

@ -1,253 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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.
*/
/*
* THIS FILE IS AUTO-GENERATED
* You can edit it you like, but your changes will be overwritten,
* so you'd just be trying to swim upstream like a salmon.
* You are not a salmon.
*
* To update it, run:
* ./reskindex.js -h header
*/
module.exports.components = {};
import structures$ContextualMenu from './components/structures/ContextualMenu';
structures$ContextualMenu && (module.exports.components['structures.ContextualMenu'] = structures$ContextualMenu);
import structures$CreateRoom from './components/structures/CreateRoom';
structures$CreateRoom && (module.exports.components['structures.CreateRoom'] = structures$CreateRoom);
import structures$FilePanel from './components/structures/FilePanel';
structures$FilePanel && (module.exports.components['structures.FilePanel'] = structures$FilePanel);
import structures$InteractiveAuth from './components/structures/InteractiveAuth';
structures$InteractiveAuth && (module.exports.components['structures.InteractiveAuth'] = structures$InteractiveAuth);
import structures$LoggedInView from './components/structures/LoggedInView';
structures$LoggedInView && (module.exports.components['structures.LoggedInView'] = structures$LoggedInView);
import structures$MatrixChat from './components/structures/MatrixChat';
structures$MatrixChat && (module.exports.components['structures.MatrixChat'] = structures$MatrixChat);
import structures$MessagePanel from './components/structures/MessagePanel';
structures$MessagePanel && (module.exports.components['structures.MessagePanel'] = structures$MessagePanel);
import structures$NotificationPanel from './components/structures/NotificationPanel';
structures$NotificationPanel && (module.exports.components['structures.NotificationPanel'] = structures$NotificationPanel);
import structures$RoomStatusBar from './components/structures/RoomStatusBar';
structures$RoomStatusBar && (module.exports.components['structures.RoomStatusBar'] = structures$RoomStatusBar);
import structures$RoomView from './components/structures/RoomView';
structures$RoomView && (module.exports.components['structures.RoomView'] = structures$RoomView);
import structures$ScrollPanel from './components/structures/ScrollPanel';
structures$ScrollPanel && (module.exports.components['structures.ScrollPanel'] = structures$ScrollPanel);
import structures$TimelinePanel from './components/structures/TimelinePanel';
structures$TimelinePanel && (module.exports.components['structures.TimelinePanel'] = structures$TimelinePanel);
import structures$UploadBar from './components/structures/UploadBar';
structures$UploadBar && (module.exports.components['structures.UploadBar'] = structures$UploadBar);
import structures$UserSettings from './components/structures/UserSettings';
structures$UserSettings && (module.exports.components['structures.UserSettings'] = structures$UserSettings);
import structures$login$ForgotPassword from './components/structures/login/ForgotPassword';
structures$login$ForgotPassword && (module.exports.components['structures.login.ForgotPassword'] = structures$login$ForgotPassword);
import structures$login$Login from './components/structures/login/Login';
structures$login$Login && (module.exports.components['structures.login.Login'] = structures$login$Login);
import structures$login$PostRegistration from './components/structures/login/PostRegistration';
structures$login$PostRegistration && (module.exports.components['structures.login.PostRegistration'] = structures$login$PostRegistration);
import structures$login$Registration from './components/structures/login/Registration';
structures$login$Registration && (module.exports.components['structures.login.Registration'] = structures$login$Registration);
import views$avatars$BaseAvatar from './components/views/avatars/BaseAvatar';
views$avatars$BaseAvatar && (module.exports.components['views.avatars.BaseAvatar'] = views$avatars$BaseAvatar);
import views$avatars$MemberAvatar from './components/views/avatars/MemberAvatar';
views$avatars$MemberAvatar && (module.exports.components['views.avatars.MemberAvatar'] = views$avatars$MemberAvatar);
import views$avatars$RoomAvatar from './components/views/avatars/RoomAvatar';
views$avatars$RoomAvatar && (module.exports.components['views.avatars.RoomAvatar'] = views$avatars$RoomAvatar);
import views$create_room$CreateRoomButton from './components/views/create_room/CreateRoomButton';
views$create_room$CreateRoomButton && (module.exports.components['views.create_room.CreateRoomButton'] = views$create_room$CreateRoomButton);
import views$create_room$Presets from './components/views/create_room/Presets';
views$create_room$Presets && (module.exports.components['views.create_room.Presets'] = views$create_room$Presets);
import views$create_room$RoomAlias from './components/views/create_room/RoomAlias';
views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias);
import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog';
views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog);
import views$dialogs$ChatCreateOrReuseDialog from './components/views/dialogs/ChatCreateOrReuseDialog';
views$dialogs$ChatCreateOrReuseDialog && (module.exports.components['views.dialogs.ChatCreateOrReuseDialog'] = views$dialogs$ChatCreateOrReuseDialog);
import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog';
views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog);
import views$dialogs$ConfirmRedactDialog from './components/views/dialogs/ConfirmRedactDialog';
views$dialogs$ConfirmRedactDialog && (module.exports.components['views.dialogs.ConfirmRedactDialog'] = views$dialogs$ConfirmRedactDialog);
import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog';
views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog);
import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog';
views$dialogs$DeactivateAccountDialog && (module.exports.components['views.dialogs.DeactivateAccountDialog'] = views$dialogs$DeactivateAccountDialog);
import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog';
views$dialogs$ErrorDialog && (module.exports.components['views.dialogs.ErrorDialog'] = views$dialogs$ErrorDialog);
import views$dialogs$InteractiveAuthDialog from './components/views/dialogs/InteractiveAuthDialog';
views$dialogs$InteractiveAuthDialog && (module.exports.components['views.dialogs.InteractiveAuthDialog'] = views$dialogs$InteractiveAuthDialog);
import views$dialogs$NeedToRegisterDialog from './components/views/dialogs/NeedToRegisterDialog';
views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog);
import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDialog';
views$dialogs$QuestionDialog && (module.exports.components['views.dialogs.QuestionDialog'] = views$dialogs$QuestionDialog);
import views$dialogs$SessionRestoreErrorDialog from './components/views/dialogs/SessionRestoreErrorDialog';
views$dialogs$SessionRestoreErrorDialog && (module.exports.components['views.dialogs.SessionRestoreErrorDialog'] = views$dialogs$SessionRestoreErrorDialog);
import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDisplayNameDialog';
views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog);
import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog';
views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog);
import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/UnknownDeviceDialog';
views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog);
import views$elements$AccessibleButton from './components/views/elements/AccessibleButton';
views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton);
import views$elements$AddressSelector from './components/views/elements/AddressSelector';
views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector);
import views$elements$AddressTile from './components/views/elements/AddressTile';
views$elements$AddressTile && (module.exports.components['views.elements.AddressTile'] = views$elements$AddressTile);
import views$elements$DeviceVerifyButtons from './components/views/elements/DeviceVerifyButtons';
views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons);
import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox';
views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox);
import views$elements$Dropdown from './components/views/elements/Dropdown';
views$elements$Dropdown && (module.exports.components['views.elements.Dropdown'] = views$elements$Dropdown);
import views$elements$EditableText from './components/views/elements/EditableText';
views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText);
import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer';
views$elements$EditableTextContainer && (module.exports.components['views.elements.EditableTextContainer'] = views$elements$EditableTextContainer);
import views$elements$EmojiText from './components/views/elements/EmojiText';
views$elements$EmojiText && (module.exports.components['views.elements.EmojiText'] = views$elements$EmojiText);
import views$elements$MemberEventListSummary from './components/views/elements/MemberEventListSummary';
views$elements$MemberEventListSummary && (module.exports.components['views.elements.MemberEventListSummary'] = views$elements$MemberEventListSummary);
import views$elements$PowerSelector from './components/views/elements/PowerSelector';
views$elements$PowerSelector && (module.exports.components['views.elements.PowerSelector'] = views$elements$PowerSelector);
import views$elements$ProgressBar from './components/views/elements/ProgressBar';
views$elements$ProgressBar && (module.exports.components['views.elements.ProgressBar'] = views$elements$ProgressBar);
import views$elements$TintableSvg from './components/views/elements/TintableSvg';
views$elements$TintableSvg && (module.exports.components['views.elements.TintableSvg'] = views$elements$TintableSvg);
import views$elements$TruncatedList from './components/views/elements/TruncatedList';
views$elements$TruncatedList && (module.exports.components['views.elements.TruncatedList'] = views$elements$TruncatedList);
import views$elements$UserSelector from './components/views/elements/UserSelector';
views$elements$UserSelector && (module.exports.components['views.elements.UserSelector'] = views$elements$UserSelector);
import views$login$CaptchaForm from './components/views/login/CaptchaForm';
views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm);
import views$login$CasLogin from './components/views/login/CasLogin';
views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin);
import views$login$CountryDropdown from './components/views/login/CountryDropdown';
views$login$CountryDropdown && (module.exports.components['views.login.CountryDropdown'] = views$login$CountryDropdown);
import views$login$CustomServerDialog from './components/views/login/CustomServerDialog';
views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog);
import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents';
views$login$InteractiveAuthEntryComponents && (module.exports.components['views.login.InteractiveAuthEntryComponents'] = views$login$InteractiveAuthEntryComponents);
import views$login$LoginFooter from './components/views/login/LoginFooter';
views$login$LoginFooter && (module.exports.components['views.login.LoginFooter'] = views$login$LoginFooter);
import views$login$LoginHeader from './components/views/login/LoginHeader';
views$login$LoginHeader && (module.exports.components['views.login.LoginHeader'] = views$login$LoginHeader);
import views$login$PasswordLogin from './components/views/login/PasswordLogin';
views$login$PasswordLogin && (module.exports.components['views.login.PasswordLogin'] = views$login$PasswordLogin);
import views$login$RegistrationForm from './components/views/login/RegistrationForm';
views$login$RegistrationForm && (module.exports.components['views.login.RegistrationForm'] = views$login$RegistrationForm);
import views$login$ServerConfig from './components/views/login/ServerConfig';
views$login$ServerConfig && (module.exports.components['views.login.ServerConfig'] = views$login$ServerConfig);
import views$messages$MAudioBody from './components/views/messages/MAudioBody';
views$messages$MAudioBody && (module.exports.components['views.messages.MAudioBody'] = views$messages$MAudioBody);
import views$messages$MFileBody from './components/views/messages/MFileBody';
views$messages$MFileBody && (module.exports.components['views.messages.MFileBody'] = views$messages$MFileBody);
import views$messages$MImageBody from './components/views/messages/MImageBody';
views$messages$MImageBody && (module.exports.components['views.messages.MImageBody'] = views$messages$MImageBody);
import views$messages$MVideoBody from './components/views/messages/MVideoBody';
views$messages$MVideoBody && (module.exports.components['views.messages.MVideoBody'] = views$messages$MVideoBody);
import views$messages$MessageEvent from './components/views/messages/MessageEvent';
views$messages$MessageEvent && (module.exports.components['views.messages.MessageEvent'] = views$messages$MessageEvent);
import views$messages$SenderProfile from './components/views/messages/SenderProfile';
views$messages$SenderProfile && (module.exports.components['views.messages.SenderProfile'] = views$messages$SenderProfile);
import views$messages$TextualBody from './components/views/messages/TextualBody';
views$messages$TextualBody && (module.exports.components['views.messages.TextualBody'] = views$messages$TextualBody);
import views$messages$TextualEvent from './components/views/messages/TextualEvent';
views$messages$TextualEvent && (module.exports.components['views.messages.TextualEvent'] = views$messages$TextualEvent);
import views$messages$UnknownBody from './components/views/messages/UnknownBody';
views$messages$UnknownBody && (module.exports.components['views.messages.UnknownBody'] = views$messages$UnknownBody);
import views$room_settings$AliasSettings from './components/views/room_settings/AliasSettings';
views$room_settings$AliasSettings && (module.exports.components['views.room_settings.AliasSettings'] = views$room_settings$AliasSettings);
import views$room_settings$ColorSettings from './components/views/room_settings/ColorSettings';
views$room_settings$ColorSettings && (module.exports.components['views.room_settings.ColorSettings'] = views$room_settings$ColorSettings);
import views$room_settings$UrlPreviewSettings from './components/views/room_settings/UrlPreviewSettings';
views$room_settings$UrlPreviewSettings && (module.exports.components['views.room_settings.UrlPreviewSettings'] = views$room_settings$UrlPreviewSettings);
import views$rooms$Autocomplete from './components/views/rooms/Autocomplete';
views$rooms$Autocomplete && (module.exports.components['views.rooms.Autocomplete'] = views$rooms$Autocomplete);
import views$rooms$AuxPanel from './components/views/rooms/AuxPanel';
views$rooms$AuxPanel && (module.exports.components['views.rooms.AuxPanel'] = views$rooms$AuxPanel);
import views$rooms$EntityTile from './components/views/rooms/EntityTile';
views$rooms$EntityTile && (module.exports.components['views.rooms.EntityTile'] = views$rooms$EntityTile);
import views$rooms$EventTile from './components/views/rooms/EventTile';
views$rooms$EventTile && (module.exports.components['views.rooms.EventTile'] = views$rooms$EventTile);
import views$rooms$LinkPreviewWidget from './components/views/rooms/LinkPreviewWidget';
views$rooms$LinkPreviewWidget && (module.exports.components['views.rooms.LinkPreviewWidget'] = views$rooms$LinkPreviewWidget);
import views$rooms$MemberDeviceInfo from './components/views/rooms/MemberDeviceInfo';
views$rooms$MemberDeviceInfo && (module.exports.components['views.rooms.MemberDeviceInfo'] = views$rooms$MemberDeviceInfo);
import views$rooms$MemberInfo from './components/views/rooms/MemberInfo';
views$rooms$MemberInfo && (module.exports.components['views.rooms.MemberInfo'] = views$rooms$MemberInfo);
import views$rooms$MemberList from './components/views/rooms/MemberList';
views$rooms$MemberList && (module.exports.components['views.rooms.MemberList'] = views$rooms$MemberList);
import views$rooms$MemberTile from './components/views/rooms/MemberTile';
views$rooms$MemberTile && (module.exports.components['views.rooms.MemberTile'] = views$rooms$MemberTile);
import views$rooms$MessageComposer from './components/views/rooms/MessageComposer';
views$rooms$MessageComposer && (module.exports.components['views.rooms.MessageComposer'] = views$rooms$MessageComposer);
import views$rooms$MessageComposerInput from './components/views/rooms/MessageComposerInput';
views$rooms$MessageComposerInput && (module.exports.components['views.rooms.MessageComposerInput'] = views$rooms$MessageComposerInput);
import views$rooms$MessageComposerInputOld from './components/views/rooms/MessageComposerInputOld';
views$rooms$MessageComposerInputOld && (module.exports.components['views.rooms.MessageComposerInputOld'] = views$rooms$MessageComposerInputOld);
import views$rooms$PresenceLabel from './components/views/rooms/PresenceLabel';
views$rooms$PresenceLabel && (module.exports.components['views.rooms.PresenceLabel'] = views$rooms$PresenceLabel);
import views$rooms$ReadReceiptMarker from './components/views/rooms/ReadReceiptMarker';
views$rooms$ReadReceiptMarker && (module.exports.components['views.rooms.ReadReceiptMarker'] = views$rooms$ReadReceiptMarker);
import views$rooms$RoomHeader from './components/views/rooms/RoomHeader';
views$rooms$RoomHeader && (module.exports.components['views.rooms.RoomHeader'] = views$rooms$RoomHeader);
import views$rooms$RoomList from './components/views/rooms/RoomList';
views$rooms$RoomList && (module.exports.components['views.rooms.RoomList'] = views$rooms$RoomList);
import views$rooms$RoomNameEditor from './components/views/rooms/RoomNameEditor';
views$rooms$RoomNameEditor && (module.exports.components['views.rooms.RoomNameEditor'] = views$rooms$RoomNameEditor);
import views$rooms$RoomPreviewBar from './components/views/rooms/RoomPreviewBar';
views$rooms$RoomPreviewBar && (module.exports.components['views.rooms.RoomPreviewBar'] = views$rooms$RoomPreviewBar);
import views$rooms$RoomSettings from './components/views/rooms/RoomSettings';
views$rooms$RoomSettings && (module.exports.components['views.rooms.RoomSettings'] = views$rooms$RoomSettings);
import views$rooms$RoomTile from './components/views/rooms/RoomTile';
views$rooms$RoomTile && (module.exports.components['views.rooms.RoomTile'] = views$rooms$RoomTile);
import views$rooms$RoomTopicEditor from './components/views/rooms/RoomTopicEditor';
views$rooms$RoomTopicEditor && (module.exports.components['views.rooms.RoomTopicEditor'] = views$rooms$RoomTopicEditor);
import views$rooms$SearchResultTile from './components/views/rooms/SearchResultTile';
views$rooms$SearchResultTile && (module.exports.components['views.rooms.SearchResultTile'] = views$rooms$SearchResultTile);
import views$rooms$SearchableEntityList from './components/views/rooms/SearchableEntityList';
views$rooms$SearchableEntityList && (module.exports.components['views.rooms.SearchableEntityList'] = views$rooms$SearchableEntityList);
import views$rooms$SimpleRoomHeader from './components/views/rooms/SimpleRoomHeader';
views$rooms$SimpleRoomHeader && (module.exports.components['views.rooms.SimpleRoomHeader'] = views$rooms$SimpleRoomHeader);
import views$rooms$TabCompleteBar from './components/views/rooms/TabCompleteBar';
views$rooms$TabCompleteBar && (module.exports.components['views.rooms.TabCompleteBar'] = views$rooms$TabCompleteBar);
import views$rooms$TopUnreadMessagesBar from './components/views/rooms/TopUnreadMessagesBar';
views$rooms$TopUnreadMessagesBar && (module.exports.components['views.rooms.TopUnreadMessagesBar'] = views$rooms$TopUnreadMessagesBar);
import views$rooms$UserTile from './components/views/rooms/UserTile';
views$rooms$UserTile && (module.exports.components['views.rooms.UserTile'] = views$rooms$UserTile);
import views$settings$AddPhoneNumber from './components/views/settings/AddPhoneNumber';
views$settings$AddPhoneNumber && (module.exports.components['views.settings.AddPhoneNumber'] = views$settings$AddPhoneNumber);
import views$settings$ChangeAvatar from './components/views/settings/ChangeAvatar';
views$settings$ChangeAvatar && (module.exports.components['views.settings.ChangeAvatar'] = views$settings$ChangeAvatar);
import views$settings$ChangeDisplayName from './components/views/settings/ChangeDisplayName';
views$settings$ChangeDisplayName && (module.exports.components['views.settings.ChangeDisplayName'] = views$settings$ChangeDisplayName);
import views$settings$ChangePassword from './components/views/settings/ChangePassword';
views$settings$ChangePassword && (module.exports.components['views.settings.ChangePassword'] = views$settings$ChangePassword);
import views$settings$DevicesPanel from './components/views/settings/DevicesPanel';
views$settings$DevicesPanel && (module.exports.components['views.settings.DevicesPanel'] = views$settings$DevicesPanel);
import views$settings$DevicesPanelEntry from './components/views/settings/DevicesPanelEntry';
views$settings$DevicesPanelEntry && (module.exports.components['views.settings.DevicesPanelEntry'] = views$settings$DevicesPanelEntry);
import views$settings$EnableNotificationsButton from './components/views/settings/EnableNotificationsButton';
views$settings$EnableNotificationsButton && (module.exports.components['views.settings.EnableNotificationsButton'] = views$settings$EnableNotificationsButton);
import views$voip$CallView from './components/views/voip/CallView';
views$voip$CallView && (module.exports.components['views.voip.CallView'] = views$voip$CallView);
import views$voip$IncomingCallBox from './components/views/voip/IncomingCallBox';
views$voip$IncomingCallBox && (module.exports.components['views.voip.IncomingCallBox'] = views$voip$IncomingCallBox);
import views$voip$VideoFeed from './components/views/voip/VideoFeed';
views$voip$VideoFeed && (module.exports.components['views.voip.VideoFeed'] = views$voip$VideoFeed);
import views$voip$VideoView from './components/views/voip/VideoView';
views$voip$VideoView && (module.exports.components['views.voip.VideoView'] = views$voip$VideoView);

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
@ -59,6 +58,8 @@ var FilePanel = React.createClass({
var client = MatrixClientPeg.get(); var client = MatrixClientPeg.get();
var room = client.getRoom(roomId); var room = client.getRoom(roomId);
this.noRoom = !room;
if (room) { if (room) {
var filter = new Matrix.Filter(client.credentials.userId); var filter = new Matrix.Filter(client.credentials.userId);
filter.setDefinition( filter.setDefinition(
@ -82,13 +83,22 @@ var FilePanel = React.createClass({
console.error("Failed to get or create file panel filter", error); console.error("Failed to get or create file panel filter", error);
} }
); );
} } else {
else {
console.error("Failed to add filtered timelineSet for FilePanel as no room!"); console.error("Failed to add filtered timelineSet for FilePanel as no room!");
} }
}, },
render: function() { render: function() {
if (MatrixClientPeg.get().isGuest()) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
<div className="mx_RoomView_empty">You must <a href="#/register">register</a> to use this functionality</div>
</div>;
} else if (this.noRoom) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
<div className="mx_RoomView_empty">You must join the room to see its files</div>
</div>;
}
// wrap a TimelinePanel with the jump-to-event bits turned off. // wrap a TimelinePanel with the jump-to-event bits turned off.
var TimelinePanel = sdk.getComponent("structures.TimelinePanel"); var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
var Loader = sdk.getComponent("elements.Spinner"); var Loader = sdk.getComponent("elements.Spinner");
@ -105,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

@ -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.
@ -106,18 +107,6 @@ export default React.createClass({
var handled = false; var handled = false;
switch (ev.keyCode) { switch (ev.keyCode) {
case KeyCode.ESCAPE:
// Implemented this way so possible handling for other pages is neater
switch (this.props.page_type) {
case PageTypes.UserSettings:
this.props.onUserSettingsClose();
handled = true;
break;
}
break;
case KeyCode.UP: case KeyCode.UP:
case KeyCode.DOWN: case KeyCode.DOWN:
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) { if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
@ -162,19 +151,19 @@ export default React.createClass({
}, },
render: function() { render: function() {
var LeftPanel = sdk.getComponent('structures.LeftPanel'); const LeftPanel = sdk.getComponent('structures.LeftPanel');
var RightPanel = sdk.getComponent('structures.RightPanel'); const RightPanel = sdk.getComponent('structures.RightPanel');
var RoomView = sdk.getComponent('structures.RoomView'); const RoomView = sdk.getComponent('structures.RoomView');
var UserSettings = sdk.getComponent('structures.UserSettings'); const UserSettings = sdk.getComponent('structures.UserSettings');
var CreateRoom = sdk.getComponent('structures.CreateRoom'); const CreateRoom = sdk.getComponent('structures.CreateRoom');
var RoomDirectory = sdk.getComponent('structures.RoomDirectory'); const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
var HomePage = sdk.getComponent('structures.HomePage'); const HomePage = sdk.getComponent('structures.HomePage');
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
var GuestWarningBar = sdk.getComponent('globals.GuestWarningBar'); const GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
var NewVersionBar = sdk.getComponent('globals.NewVersionBar'); const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
var page_element; let page_element;
var right_panel = ''; let right_panel = '';
switch (this.props.page_type) { switch (this.props.page_type) {
case PageTypes.RoomView: case PageTypes.RoomView:
@ -194,7 +183,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:
@ -206,7 +195,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:
@ -214,16 +203,14 @@ 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:
page_element = <RoomDirectory page_element = <RoomDirectory
ref="roomDirectory" ref="roomDirectory"
collapsedRhs={this.props.collapse_rhs}
config={this.props.config.roomDirectory} config={this.props.config.roomDirectory}
/>; />;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
break; break;
case PageTypes.HomePage: case PageTypes.HomePage:
@ -232,12 +219,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;
} }
@ -266,7 +253,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

@ -17,28 +17,26 @@ limitations under the License.
import q from 'q'; import q from 'q';
var React = require('react'); import React from 'react';
var Matrix = require("matrix-js-sdk"); import Matrix from "matrix-js-sdk";
var MatrixClientPeg = require("../../MatrixClientPeg"); import MatrixClientPeg from "../../MatrixClientPeg";
var PlatformPeg = require("../../PlatformPeg"); import PlatformPeg from "../../PlatformPeg";
var SdkConfig = require("../../SdkConfig"); import SdkConfig from "../../SdkConfig";
var ContextualMenu = require("./ContextualMenu"); import * as RoomListSorter from "../../RoomListSorter";
var RoomListSorter = require("../../RoomListSorter"); import dis from "../../dispatcher";
var UserActivity = require("../../UserActivity");
var Presence = require("../../Presence");
var dis = require("../../dispatcher");
var Modal = require("../../Modal"); import Modal from "../../Modal";
var Tinter = require("../../Tinter"); import Tinter from "../../Tinter";
var sdk = require('../../index'); import sdk from '../../index';
var Rooms = require('../../Rooms'); import * as Rooms from '../../Rooms';
var linkifyMatrix = require("../../linkify-matrix"); import linkifyMatrix from "../../linkify-matrix";
var Lifecycle = require('../../Lifecycle'); import * as Lifecycle from '../../Lifecycle';
var PageTypes = require('../../PageTypes'); import PageTypes from '../../PageTypes';
var createRoom = require("../../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',
@ -89,7 +87,7 @@ module.exports = React.createClass({
}, },
getInitialState: function() { getInitialState: function() {
var s = { const s = {
loading: true, loading: true,
screen: undefined, screen: undefined,
screenAfterLogin: this.props.initialScreenAfterLogin, screenAfterLogin: this.props.initialScreenAfterLogin,
@ -119,8 +117,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,
@ -156,11 +155,9 @@ module.exports = React.createClass({
return this.state.register_hs_url; return this.state.register_hs_url;
} else if (MatrixClientPeg.get()) { } else if (MatrixClientPeg.get()) {
return MatrixClientPeg.get().getHomeserverUrl(); return MatrixClientPeg.get().getHomeserverUrl();
} } else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) {
else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) {
return window.localStorage.getItem("mx_hs_url"); return window.localStorage.getItem("mx_hs_url");
} } else {
else {
return this.getDefaultHsUrl(); return this.getDefaultHsUrl();
} }
}, },
@ -178,11 +175,9 @@ module.exports = React.createClass({
return this.state.register_is_url; return this.state.register_is_url;
} else if (MatrixClientPeg.get()) { } else if (MatrixClientPeg.get()) {
return MatrixClientPeg.get().getIdentityServerUrl(); return MatrixClientPeg.get().getIdentityServerUrl();
} } else if (window.localStorage && window.localStorage.getItem("mx_is_url")) {
else if (window.localStorage && window.localStorage.getItem("mx_is_url")) {
return window.localStorage.getItem("mx_is_url"); return window.localStorage.getItem("mx_is_url");
} } else {
else {
return this.getDefaultIsUrl(); return this.getDefaultIsUrl();
} }
}, },
@ -253,7 +248,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.
@ -324,28 +318,14 @@ module.exports = React.createClass({
onAction: function(payload) { onAction: function(payload) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var roomIndexDelta = 1; const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
var self = this;
switch (payload.action) { switch (payload.action) {
case 'logout': case 'logout':
Lifecycle.logout(); Lifecycle.logout();
break; break;
case 'start_registration': case 'start_registration':
const params = payload.params || {}; this._startRegistration(payload.params || {});
this.setStateForNewScreen({
screen: 'register',
// these params may be undefined, but if they are,
// unset them from our state: we don't want to
// resume a previous registration session if the
// user just clicked 'register'
register_client_secret: params.client_secret,
register_session_id: params.session_id,
register_hs_url: params.hs_url,
register_is_url: params.is_url,
register_id_sid: params.sid,
});
this.notifyNewScreen('register');
break; break;
case 'start_login': case 'start_login':
if (MatrixClientPeg.get() && if (MatrixClientPeg.get() &&
@ -362,7 +342,7 @@ module.exports = React.createClass({
break; break;
case 'start_post_registration': case 'start_post_registration':
this.setState({ // don't clobber loggedIn status this.setState({ // don't clobber loggedIn status
screen: 'post_registration' screen: 'post_registration',
}); });
break; break;
case 'start_upgrade_registration': case 'start_upgrade_registration':
@ -392,38 +372,12 @@ module.exports = React.createClass({
this.notifyNewScreen('forgot_password'); this.notifyNewScreen('forgot_password');
break; break;
case 'leave_room': case 'leave_room':
Modal.createDialog(QuestionDialog, { this._leaveRoom(payload.room_id);
title: "Leave room",
description: "Are you sure you want to leave the room?",
onFinished: (should_leave) => {
if (should_leave) {
const d = MatrixClientPeg.get().leave(payload.room_id);
// FIXME: controller shouldn't be loading a view :(
const Loader = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
d.then(() => {
modal.close();
if (this.currentRoomId === payload.room_id) {
dis.dispatch({action: 'view_next_room'});
}
}, (err) => {
modal.close();
console.error("Failed to leave room " + payload.room_id + " " + err);
Modal.createDialog(ErrorDialog, {
title: "Failed to leave room",
description: (err && err.message ? err.message : "Server may be unavailable, overloaded, or you hit a bug."),
});
});
}
}
});
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 :(
@ -438,12 +392,12 @@ 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(),
}); });
}); });
} }
} },
}); });
break; break;
case 'view_user': case 'view_user':
@ -468,30 +422,13 @@ module.exports = React.createClass({
this._viewRoom(payload); this._viewRoom(payload);
break; break;
case 'view_prev_room': case 'view_prev_room':
roomIndexDelta = -1; this._viewNextRoom(-1);
break;
case 'view_next_room': case 'view_next_room':
var allRooms = RoomListSorter.mostRecentActivityFirst( this._viewNextRoom(1);
MatrixClientPeg.get().getRooms()
);
var roomIndex = -1;
for (var i = 0; i < allRooms.length; ++i) {
if (allRooms[i].roomId == this.state.currentRoomId) {
roomIndex = i;
break;
}
}
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
if (roomIndex < 0) roomIndex = allRooms.length - 1;
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
break; break;
case 'view_indexed_room': case 'view_indexed_room':
var allRooms = RoomListSorter.mostRecentActivityFirst( this._viewIndexedRoom(payload.roomIndex);
MatrixClientPeg.get().getRooms()
);
var roomIndex = payload.roomIndex;
if (allRooms[roomIndex]) {
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
}
break; break;
case 'view_user_settings': case 'view_user_settings':
this._setPage(PageTypes.UserSettings); this._setPage(PageTypes.UserSettings);
@ -500,19 +437,17 @@ module.exports = React.createClass({
case 'view_create_room': case 'view_create_room':
//this._setPage(PageTypes.CreateRoom); //this._setPage(PageTypes.CreateRoom);
//this.notifyNewScreen('new'); //this.notifyNewScreen('new');
var TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
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: (should_create, name) => { onFinished: (should_create, name) => {
if (should_create) { if (should_create) {
const createOpts = {}; const createOpts = {};
if (name) createOpts.name = name; if (name) createOpts.name = name;
createRoom({createOpts}).done(); createRoom({createOpts}).done();
} }
} },
}); });
break; break;
case 'view_room_directory': case 'view_room_directory':
@ -556,12 +491,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;
@ -583,7 +520,7 @@ module.exports = React.createClass({
case 'new_version': case 'new_version':
this.onVersion( this.onVersion(
payload.currentVersion, payload.newVersion, payload.currentVersion, payload.newVersion,
payload.releaseNotes payload.releaseNotes,
); );
break; break;
} }
@ -595,6 +532,47 @@ module.exports = React.createClass({
}); });
}, },
_startRegistration: function(params) {
this.setStateForNewScreen({
screen: 'register',
// these params may be undefined, but if they are,
// unset them from our state: we don't want to
// resume a previous registration session if the
// user just clicked 'register'
register_client_secret: params.client_secret,
register_session_id: params.session_id,
register_hs_url: params.hs_url,
register_is_url: params.is_url,
register_id_sid: params.sid,
});
this.notifyNewScreen('register');
},
_viewNextRoom: function(roomIndexDelta) {
const allRooms = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms(),
);
let roomIndex = -1;
for (let i = 0; i < allRooms.length; ++i) {
if (allRooms[i].roomId == this.state.currentRoomId) {
roomIndex = i;
break;
}
}
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
if (roomIndex < 0) roomIndex = allRooms.length - 1;
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
},
_viewIndexedRoom: function(roomIndex) {
const allRooms = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms(),
);
if (allRooms[roomIndex]) {
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
}
},
// 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} room_info Object containing data about the room to be joined
@ -614,7 +592,7 @@ module.exports = React.createClass({
_viewRoom: function(room_info) { _viewRoom: function(room_info) {
this.focusComposer = true; this.focusComposer = true;
var newState = { const newState = {
initialEventId: room_info.event_id, initialEventId: room_info.event_id,
highlightedEventId: room_info.event_id, highlightedEventId: room_info.event_id,
initialEventPixelOffset: undefined, initialEventPixelOffset: undefined,
@ -634,7 +612,7 @@ module.exports = React.createClass({
// //
// 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 (!room_info.event_id && this.refs.loggedInView) {
var scrollState = this.refs.loggedInView.getScrollStateForRoom(room_info.room_id); const scrollState = this.refs.loggedInView.getScrollStateForRoom(room_info.room_id);
if (scrollState) { if (scrollState) {
newState.initialEventId = scrollState.focussedEvent; newState.initialEventId = scrollState.focussedEvent;
newState.initialEventPixelOffset = scrollState.pixelOffset; newState.initialEventPixelOffset = scrollState.pixelOffset;
@ -676,22 +654,61 @@ module.exports = React.createClass({
}, },
_createChat: function() { _createChat: function() {
var 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) {
var 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,
}); });
}, },
_leaveRoom: function(roomId) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
Modal.createDialog(QuestionDialog, {
title: "Leave room",
description: <span>Are you sure you want to leave the room <i>{roomToLeave.name}</i>?</span>,
onFinished: (shouldLeave) => {
if (shouldLeave) {
const d = MatrixClientPeg.get().leave(roomId);
// FIXME: controller shouldn't be loading a view :(
const Loader = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
d.then(() => {
modal.close();
if (this.currentRoomId === roomId) {
dis.dispatch({action: 'view_next_room'});
}
}, (err) => {
modal.close();
console.error("Failed to leave room " + roomId + " " + err);
Modal.createDialog(ErrorDialog, {
title: "Failed to leave room",
description: (err && err.message ? err.message :
"Server may be unavailable, overloaded, or you hit a bug."),
});
});
}
},
});
},
/** /**
* Called when the sessionloader has finished * Called when the sessionloader has finished
*/ */
@ -710,6 +727,8 @@ module.exports = React.createClass({
/** /**
* Called whenever someone changes the theme * Called whenever someone changes the theme
*
* @param {string} theme new theme
*/ */
_onSetTheme: function(theme) { _onSetTheme: function(theme) {
if (!theme) { if (!theme) {
@ -718,12 +737,12 @@ module.exports = React.createClass({
// look for the stylesheet elements. // look for the stylesheet elements.
// styleElements is a map from style name to HTMLLinkElement. // styleElements is a map from style name to HTMLLinkElement.
var styleElements = Object.create(null); const styleElements = Object.create(null);
var i, a; let a;
for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) { for (let i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
var href = a.getAttribute("href"); const href = a.getAttribute("href");
// shouldn't we be using the 'title' tag rather than the href? // shouldn't we be using the 'title' tag rather than the href?
var match = href.match(/^bundles\/.*\/theme-(.*)\.css$/); const match = href.match(/^bundles\/.*\/theme-(.*)\.css$/);
if (match) { if (match) {
styleElements[match[1]] = a; styleElements[match[1]] = a;
} }
@ -746,14 +765,15 @@ module.exports = React.createClass({
// abuse the tinter to change all the SVG's #fff to #2d2d2d // abuse the tinter to change all the SVG's #fff to #2d2d2d
// XXX: obviously this shouldn't be hardcoded here. // XXX: obviously this shouldn't be hardcoded here.
Tinter.tintSvgWhite('#2d2d2d'); Tinter.tintSvgWhite('#2d2d2d');
} } else {
else {
Tinter.tintSvgWhite('#ffffff'); Tinter.tintSvgWhite('#ffffff');
} }
}, },
/** /**
* Called when a new logged in session has started * Called when a new logged in session has started
*
* @param {string} teamToken
*/ */
_onLoggedIn: function(teamToken) { _onLoggedIn: function(teamToken) {
this.setState({ this.setState({
@ -767,8 +787,12 @@ module.exports = React.createClass({
this._teamToken = teamToken; this._teamToken = teamToken;
dis.dispatch({action: 'view_home_page'}); dis.dispatch({action: 'view_home_page'});
} else if (this._is_registered) { } else if (this._is_registered) {
if (this.props.config.welcomeUserId) {
createRoom({dmUserId: this.props.config.welcomeUserId});
return;
}
// The user has just logged in after registering // The user has just logged in after registering
dis.dispatch({action: 'view_user_settings'}); dis.dispatch({action: 'view_room_directory'});
} else { } else {
this._showScreenAfterLogin(); this._showScreenAfterLogin();
} }
@ -780,7 +804,7 @@ module.exports = React.createClass({
if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) { if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) {
this.showScreen( this.showScreen(
this.state.screenAfterLogin.screen, this.state.screenAfterLogin.screen,
this.state.screenAfterLogin.params this.state.screenAfterLogin.params,
); );
this.notifyNewScreen(this.state.screenAfterLogin.screen); this.notifyNewScreen(this.state.screenAfterLogin.screen);
this.setState({screenAfterLogin: null}); this.setState({screenAfterLogin: null});
@ -821,8 +845,8 @@ module.exports = React.createClass({
* (useful for setting listeners) * (useful for setting listeners)
*/ */
_onWillStartClient() { _onWillStartClient() {
var self = this; const self = this;
var cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
// Allow the JS SDK to reap timeline events. This reduces the amount of // Allow the JS SDK to reap timeline events. This reduces the amount of
// memory consumed as the JS SDK stores multiple distinct copies of room // memory consumed as the JS SDK stores multiple distinct copies of room
@ -863,17 +887,17 @@ module.exports = React.createClass({
cli.on('Call.incoming', function(call) { cli.on('Call.incoming', function(call) {
dis.dispatch({ dis.dispatch({
action: 'incoming_call', action: 'incoming_call',
call: call call: call,
}); });
}); });
cli.on('Session.logged_out', function(call) { cli.on('Session.logged_out', function(call) {
var 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',
}); });
}); });
cli.on("accountData", function(ev) { cli.on("accountData", function(ev) {
@ -888,25 +912,21 @@ 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({
action: 'start_registration', action: 'start_registration',
params: params params: params,
}); });
} else if (screen == 'login') { } else if (screen == 'login') {
dis.dispatch({ dis.dispatch({
action: 'start_login', action: 'start_login',
params: params params: params,
}); });
} else if (screen == 'forgot_password') { } else if (screen == 'forgot_password') {
dis.dispatch({ dis.dispatch({
action: 'start_password_recovery', action: 'start_password_recovery',
params: params params: params,
}); });
} else if (screen == 'new') { } else if (screen == 'new') {
dis.dispatch({ dis.dispatch({
@ -929,26 +949,26 @@ module.exports = React.createClass({
action: 'start_post_registration', action: 'start_post_registration',
}); });
} else if (screen.indexOf('room/') == 0) { } else if (screen.indexOf('room/') == 0) {
var segments = screen.substring(5).split('/'); const segments = screen.substring(5).split('/');
var roomString = segments[0]; const roomString = segments[0];
var eventId = segments[1]; // undefined if no event id given const eventId = segments[1]; // undefined if no event id given
// FIXME: sort_out caseConsistency // FIXME: sort_out caseConsistency
var third_party_invite = { const thirdPartyInvite = {
inviteSignUrl: params.signurl, inviteSignUrl: params.signurl,
invitedEmail: params.email, invitedEmail: params.email,
}; };
var oob_data = { const oobData = {
name: params.room_name, name: params.room_name,
avatarUrl: params.room_avatar_url, avatarUrl: params.room_avatar_url,
inviterName: params.inviter_name, inviterName: params.inviter_name,
}; };
var payload = { const payload = {
action: 'view_room', action: 'view_room',
event_id: eventId, event_id: eventId,
third_party_invite: third_party_invite, third_party_invite: thirdPartyInvite,
oob_data: oob_data, oob_data: oobData,
}; };
if (roomString[0] == '#') { if (roomString[0] == '#') {
payload.room_alias = roomString; payload.room_alias = roomString;
@ -962,19 +982,18 @@ module.exports = React.createClass({
dis.dispatch(payload); dis.dispatch(payload);
} }
} else if (screen.indexOf('user/') == 0) { } else if (screen.indexOf('user/') == 0) {
var userId = screen.substring(5); const userId = screen.substring(5);
this.setState({ viewUserId: userId }); this.setState({ viewUserId: userId });
this._setPage(PageTypes.UserView); this._setPage(PageTypes.UserView);
this.notifyNewScreen('user/' + userId); this.notifyNewScreen('user/' + userId);
var member = new Matrix.RoomMember(null, userId); const member = new Matrix.RoomMember(null, userId);
if (member) { if (member) {
dis.dispatch({ dis.dispatch({
action: 'view_user', action: 'view_user',
member: member, member: member,
}); });
} }
} } else {
else {
console.info("Ignoring showScreen for '%s'", screen); console.info("Ignoring showScreen for '%s'", screen);
} }
}, },
@ -993,7 +1012,7 @@ module.exports = React.createClass({
onUserClick: function(event, userId) { onUserClick: function(event, userId) {
event.preventDefault(); event.preventDefault();
var member = new Matrix.RoomMember(null, userId); const member = new Matrix.RoomMember(null, userId);
if (!member) { return; } if (!member) { return; }
dis.dispatch({ dis.dispatch({
action: 'view_user', action: 'view_user',
@ -1003,17 +1022,17 @@ module.exports = React.createClass({
onLogoutClick: function(event) { onLogoutClick: function(event) {
dis.dispatch({ dis.dispatch({
action: 'logout' action: 'logout',
}); });
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
}, },
handleResize: function(e) { handleResize: function(e) {
var hideLhsThreshold = 1000; const hideLhsThreshold = 1000;
var showLhsThreshold = 1000; const showLhsThreshold = 1000;
var hideRhsThreshold = 820; const hideRhsThreshold = 820;
var showRhsThreshold = 820; const showRhsThreshold = 820;
if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) { if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
dis.dispatch({ action: 'hide_left_panel' }); dis.dispatch({ action: 'hide_left_panel' });
@ -1031,10 +1050,10 @@ module.exports = React.createClass({
this.setState({width: window.innerWidth}); this.setState({width: window.innerWidth});
}, },
onRoomCreated: function(room_id) { onRoomCreated: function(roomId) {
dis.dispatch({ dis.dispatch({
action: "view_room", action: "view_room",
room_id: room_id, room_id: roomId,
}); });
}, },
@ -1068,7 +1087,7 @@ module.exports = React.createClass({
onFinishPostRegistration: function() { onFinishPostRegistration: function() {
// Don't confuse this with "PageType" which is the middle window to show // Don't confuse this with "PageType" which is the middle window to show
this.setState({ this.setState({
screen: undefined screen: undefined,
}); });
this.showScreen("settings"); this.showScreen("settings");
}, },
@ -1083,10 +1102,10 @@ module.exports = React.createClass({
}, },
updateStatusIndicator: function(state, prevState) { updateStatusIndicator: function(state, prevState) {
var notifCount = 0; let notifCount = 0;
var rooms = MatrixClientPeg.get().getRooms(); const rooms = MatrixClientPeg.get().getRooms();
for (var i = 0; i < rooms.length; ++i) { for (let i = 0; i < rooms.length; ++i) {
if (rooms[i].hasMembershipState(MatrixClientPeg.get().credentials.userId, 'invite')) { if (rooms[i].hasMembershipState(MatrixClientPeg.get().credentials.userId, 'invite')) {
notifCount++; notifCount++;
} else if (rooms[i].getUnreadNotificationCount()) { } else if (rooms[i].getUnreadNotificationCount()) {
@ -1113,19 +1132,18 @@ module.exports = React.createClass({
action: 'view_room', action: 'view_room',
room_id: this.state.currentRoomId, room_id: this.state.currentRoomId,
}); });
} } else {
else {
dis.dispatch({ dis.dispatch({
action: 'view_room_directory', action: 'view_room_directory',
}); });
} }
}, },
onRoomIdResolved: function(room_id) { onRoomIdResolved: function(roomId) {
// It's the RoomView's resposibility to look up room aliases, but we need the // It's the RoomView's resposibility to look up room aliases, but we need the
// ID to pass into things like the Member List, so the Room View tells us when // ID to pass into things like the Member List, so the Room View tells us when
// its done that resolution so we can display things that take a room ID. // its done that resolution so we can display things that take a room ID.
this.setState({currentRoomId: room_id}); this.setState({currentRoomId: roomId});
}, },
_makeRegistrationUrl: function(params) { _makeRegistrationUrl: function(params) {
@ -1148,14 +1166,20 @@ module.exports = React.createClass({
</div> </div>
); );
} }
// needs to be before normal PageTypes as you are logged in technically // needs to be before normal PageTypes as you are logged in technically
else if (this.state.screen == 'post_registration') { if (this.state.screen == 'post_registration') {
const PostRegistration = sdk.getComponent('structures.login.PostRegistration'); const PostRegistration = sdk.getComponent('structures.login.PostRegistration');
return ( return (
<PostRegistration <PostRegistration
onComplete={this.onFinishPostRegistration} /> onComplete={this.onFinishPostRegistration} />
); );
} else if (this.state.loggedIn && this.state.ready) { }
// `ready` and `loggedIn` may be set before `page_type` (because the
// latter is set via the dispatcher). If we don't yet have a `page_type`,
// keep showing the spinner for now.
if (this.state.loggedIn && this.state.ready && this.state.page_type) {
/* for now, we stuff the entirety of our props and state into the LoggedInView. /* for now, we stuff the entirety of our props and state into the LoggedInView.
* we should go through and figure out what we actually need to pass down, as well * we should go through and figure out what we actually need to pass down, as well
* as using something like redux to avoid having a billion bits of state kicking around. * as using something like redux to avoid having a billion bits of state kicking around.
@ -1178,7 +1202,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>
); );
@ -1237,5 +1261,5 @@ module.exports = React.createClass({
/> />
); );
} }
} },
}); });

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 = {};
@ -279,20 +285,19 @@ module.exports = React.createClass({
this.currentGhostEventId = null; this.currentGhostEventId = null;
} }
var isMembershipChange = (e) => var isMembershipChange = (e) => e.getType() === 'm.room.member';
e.getType() === 'm.room.member'
&& (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
for (i = 0; i < this.props.events.length; i++) { for (i = 0; i < this.props.events.length; i++) {
var mxEv = this.props.events[i]; let mxEv = this.props.events[i];
var wantTile = true; let wantTile = true;
var eventId = mxEv.getId(); let eventId = mxEv.getId();
let readMarkerInMels = false;
if (!EventTile.haveTileForEvent(mxEv)) { if (!EventTile.haveTileForEvent(mxEv)) {
wantTile = false; wantTile = false;
} }
var last = (i == lastShownEventIndex); let last = (i == lastShownEventIndex);
// Wrap consecutive member events in a ListSummary, ignore if redacted // Wrap consecutive member events in a ListSummary, ignore if redacted
if (isMembershipChange(mxEv) && if (isMembershipChange(mxEv) &&
@ -334,6 +339,9 @@ module.exports = React.createClass({
let eventTiles = summarisedEvents.map( let eventTiles = summarisedEvents.map(
(e) => { (e) => {
if (e.getId() === this.props.readMarkerEventId) {
readMarkerInMels = true;
}
// In order to prevent DateSeparators from appearing in the expanded form // In order to prevent DateSeparators from appearing in the expanded form
// of MemberEventListSummary, render each member event as if the previous // of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the // one was itself. This way, the timestamp of the previous event === the
@ -352,12 +360,16 @@ module.exports = React.createClass({
<MemberEventListSummary <MemberEventListSummary
key={key} key={key}
events={summarisedEvents} events={summarisedEvents}
data-scroll-token={eventId}
onToggle={this._onWidgetLoad} // Update scroll state onToggle={this._onWidgetLoad} // Update scroll state
> >
{eventTiles} {eventTiles}
</MemberEventListSummary> </MemberEventListSummary>
); );
if (readMarkerInMels) {
ret.push(this._getReadMarkerTile(visible));
}
continue; continue;
} }
@ -407,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?
@ -462,11 +474,10 @@ 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)}
data-scroll-token={scrollToken}> data-scroll-tokens={scrollToken}>
<EventTile mxEvent={mxEv} continuation={continuation} <EventTile mxEvent={mxEv} continuation={continuation}
isRedacted={mxEv.isRedacted()} isRedacted={mxEv.isRedacted()}
onWidgetLoad={this._onWidgetLoad} onWidgetLoad={this._onWidgetLoad}
@ -476,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>
); );
@ -609,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,
@ -271,6 +274,7 @@ module.exports = React.createClass({
this._updateConfCallNotification(); this._updateConfCallNotification();
window.addEventListener('beforeunload', this.onPageUnload);
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
this.onResize(); this.onResize();
@ -295,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) {
@ -353,6 +357,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("accountData", this.onAccountData); MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
} }
window.removeEventListener('beforeunload', this.onPageUnload);
window.removeEventListener('resize', this.onResize); window.removeEventListener('resize', this.onResize);
document.removeEventListener("keydown", this.onKeyDown); document.removeEventListener("keydown", this.onKeyDown);
@ -365,6 +370,17 @@ module.exports = React.createClass({
// Tinter.tint(); // reset colourscheme // Tinter.tint(); // reset colourscheme
}, },
onPageUnload(event) {
if (ContentMessages.getCurrentUploads().length > 0) {
return event.returnValue =
_t("You seem to be uploading files, are you sure you want to quit?");
} else if (this._getCallForRoom() && this.state.callState !== 'ended') {
return event.returnValue =
_t("You seem to be in a call, are you sure you want to quit?");
}
},
onKeyDown: function(ev) { onKeyDown: function(ev) {
let handled = false; let handled = false;
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
@ -438,6 +454,11 @@ module.exports = React.createClass({
callState: callState callState: callState
}); });
break;
case 'forward_event':
this.setState({
forwardingEvent: payload.content,
});
break; break;
} }
}, },
@ -517,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>
), ),
}); });
@ -695,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) {
@ -858,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();
@ -926,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;
} }
@ -946,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")),
}); });
}); });
}, },
@ -1033,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({
@ -1069,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>
); );
} }
@ -1111,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;
} }
@ -1160,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
@ -1181,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'});
}, },
@ -1196,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 }),
}); });
}); });
}, },
@ -1221,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({
@ -1276,12 +1300,7 @@ module.exports = React.createClass({
return; return;
} }
var pos = this.refs.messagePanel.getReadMarkerPosition(); const showBar = this.refs.messagePanel.canJumpToReadMarker();
// we want to show the bar if the read-marker is off the top of the
// screen.
var showBar = (pos < 0);
if (this.state.showTopUnreadMessagesBar != showBar) { if (this.state.showTopUnreadMessagesBar != showBar) {
this.setState({showTopUnreadMessagesBar: showBar}, this.setState({showTopUnreadMessagesBar: showBar},
this.onChildResize); this.onChildResize);
@ -1464,16 +1483,17 @@ 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) {
@ -1601,17 +1621,16 @@ module.exports = React.createClass({
/>; />;
} }
var aux = null; let aux = null;
if (this.state.editingRoomSettings) { if (this.state.forwardingEvent !== null) {
aux = <ForwardMessage onCancelClick={this.onCancelClick} currentRoomId={this.state.room.roomId} mxEvent={this.state.forwardingEvent} />;
} else 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.searching) {
else if (this.state.searching) {
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;
@ -1673,7 +1692,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>
); );
@ -1681,14 +1700,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>;
@ -1724,14 +1743,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 }
@ -1764,11 +1782,12 @@ module.exports = React.createClass({
oobData={this.props.oobData} oobData={this.props.oobData}
editing={this.state.editingRoomSettings} editing={this.state.editingRoomSettings}
saving={this.state.uploadingRoomSettings} saving={this.state.uploadingRoomSettings}
inRoom={myMember && myMember.membership === 'join'}
collapsedRhs={ this.props.collapsedRhs } collapsedRhs={ this.props.collapsedRhs }
onSearchClick={this.onSearchClick} onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick} onSettingsClick={this.onSettingsClick}
onSaveClick={this.onSettingsSaveClick} onSaveClick={this.onSettingsSaveClick}
onCancelClick={this.onCancelClick} onCancelClick={aux ? this.onCancelClick : null}
onForgetClick={ onForgetClick={
(myMember && myMember.membership === "leave") ? this.onForgetClick : null (myMember && myMember.membership === "leave") ? this.onForgetClick : null
} }

View file

@ -46,9 +46,13 @@ if (DEBUG_SCROLL) {
* It also provides a hook which allows parents to provide more list elements * It also provides a hook which allows parents to provide more list elements
* when we get close to the start or end of the list. * when we get close to the start or end of the list.
* *
* Each child element should have a 'data-scroll-token'. This token is used to * Each child element should have a 'data-scroll-tokens'. This string of
* serialise the scroll state, and returned as the 'trackedScrollToken' * comma-separated tokens may contain a single token or many, where many indicates
* attribute by getScrollState(). * that the element contains elements that have scroll tokens themselves. The first
* token in 'data-scroll-tokens' is used to serialise the scroll state, and returned
* as the 'trackedScrollToken' attribute by getScrollState().
*
* IMPORTANT: INDIVIDUAL TOKENS WITHIN 'data-scroll-tokens' MUST NOT CONTAIN COMMAS.
* *
* Some notes about the implementation: * Some notes about the implementation:
* *
@ -349,8 +353,8 @@ module.exports = React.createClass({
// Subtract height of tile as if it were unpaginated // Subtract height of tile as if it were unpaginated
excessHeight -= tile.clientHeight; excessHeight -= tile.clientHeight;
// The tile may not have a scroll token, so guard it // The tile may not have a scroll token, so guard it
if (tile.dataset.scrollToken) { if (tile.dataset.scrollTokens) {
markerScrollToken = tile.dataset.scrollToken; markerScrollToken = tile.dataset.scrollTokens.split(',')[0];
} }
if (tile.clientHeight > excessHeight) { if (tile.clientHeight > excessHeight) {
break; break;
@ -419,7 +423,8 @@ module.exports = React.createClass({
* scroll. false if we are tracking a particular child. * scroll. false if we are tracking a particular child.
* *
* string trackedScrollToken: undefined if stuckAtBottom is true; if it is * string trackedScrollToken: undefined if stuckAtBottom is true; if it is
* false, the data-scroll-token of the child which we are tracking. * false, the first token in data-scroll-tokens of the child which we are
* tracking.
* *
* number pixelOffset: undefined if stuckAtBottom is true; if it is false, * number pixelOffset: undefined if stuckAtBottom is true; if it is false,
* the number of pixels the bottom of the tracked child is above the * the number of pixels the bottom of the tracked child is above the
@ -551,8 +556,10 @@ module.exports = React.createClass({
var messages = this.refs.itemlist.children; var messages = this.refs.itemlist.children;
for (var i = messages.length-1; i >= 0; --i) { for (var i = messages.length-1; i >= 0; --i) {
var m = messages[i]; var m = messages[i];
if (!m.dataset.scrollToken) continue; // 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
if (m.dataset.scrollToken == scrollToken) { // There might only be one scroll token
if (m.dataset.scrollTokens &&
m.dataset.scrollTokens.split(',').indexOf(scrollToken) !== -1) {
node = m; node = m;
break; break;
} }
@ -568,7 +575,7 @@ module.exports = React.createClass({
var boundingRect = node.getBoundingClientRect(); var boundingRect = node.getBoundingClientRect();
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom; var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" + debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
pixelOffset + " (delta: "+scrollDelta+")"); pixelOffset + " (delta: "+scrollDelta+")");
if(scrollDelta != 0) { if(scrollDelta != 0) {
@ -591,12 +598,12 @@ module.exports = React.createClass({
for (var i = messages.length-1; i >= 0; --i) { for (var i = messages.length-1; i >= 0; --i) {
var node = messages[i]; var node = messages[i];
if (!node.dataset.scrollToken) continue; if (!node.dataset.scrollTokens) continue;
var boundingRect = node.getBoundingClientRect(); var boundingRect = node.getBoundingClientRect();
newScrollState = { newScrollState = {
stuckAtBottom: false, stuckAtBottom: false,
trackedScrollToken: node.dataset.scrollToken, trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
pixelOffset: wrapperRect.bottom - boundingRect.bottom, pixelOffset: wrapperRect.bottom - boundingRect.bottom,
}; };
// If the bottom of the panel intersects the ClientRect of node, use this node // If the bottom of the panel intersects the ClientRect of node, use this node
@ -608,7 +615,7 @@ module.exports = React.createClass({
break; break;
} }
} }
// This is only false if there were no nodes with `node.dataset.scrollToken` set. // This is only false if there were no nodes with `node.dataset.scrollTokens` set.
if (newScrollState) { if (newScrollState) {
this.scrollState = newScrollState; this.scrollState = newScrollState;
debuglog("ScrollPanel: saved scroll state", this.scrollState); debuglog("ScrollPanel: saved scroll state", this.scrollState);

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 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.
@ -22,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;
@ -121,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();
@ -167,14 +170,23 @@ var TimelinePanel = React.createClass({
backPaginating: false, backPaginating: false,
forwardPaginating: false, forwardPaginating: false,
// cache of matrixClient.getSyncState() (but from the 'sync' event)
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'),
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
debuglog("TimelinePanel: mounting"); debuglog("TimelinePanel: mounting");
this.last_rr_sent_event_id = undefined; this.lastRRSentEventId = undefined;
this.last_rm_sent_event_id = undefined; this.lastRMSentEventId = undefined;
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
@ -183,6 +195,7 @@ var TimelinePanel = React.createClass({
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated); MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
MatrixClientPeg.get().on("Room.accountData", this.onAccountData); MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
MatrixClientPeg.get().on("sync", this.onSync);
this._initTimeline(this.props); this._initTimeline(this.props);
}, },
@ -251,6 +264,7 @@ var TimelinePanel = React.createClass({
client.removeListener("Room.receipt", this.onRoomReceipt); client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated); client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
client.removeListener("Room.accountData", this.onAccountData); client.removeListener("Room.accountData", this.onAccountData);
client.removeListener("sync", this.onSync);
} }
}, },
@ -487,17 +501,24 @@ var TimelinePanel = React.createClass({
}, this.props.onReadMarkerUpdated); }, this.props.onReadMarkerUpdated);
}, },
onSync: function(state, prevState, data) {
this.setState({clientSyncState: state});
},
sendReadReceipt: function() { sendReadReceipt: function() {
if (!this.refs.messagePanel) return; if (!this.refs.messagePanel) return;
if (!this.props.manageReadReceipts) return; if (!this.props.manageReadReceipts) return;
// This happens on user_activity_end which is delayed, and it's // This happens on user_activity_end which is delayed, and it's
// very possible have logged out within that timeframe, so check // very possible have logged out within that timeframe, so check
// we still have a client. // we still have a client.
if (!MatrixClientPeg.get()) return; const cli = MatrixClientPeg.get();
// if no client or client is guest don't send RR or RM
if (!cli || cli.isGuest()) return;
var currentReadUpToEventId = this._getCurrentReadReceipt(true); let shouldSendRR = true;
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
const currentRREventId = this._getCurrentReadReceipt(true);
const currentRREventIndex = this._indexForEventId(currentRREventId);
// We want to avoid sending out read receipts when we are looking at // We want to avoid sending out read receipts when we are looking at
// events in the past which are before the latest RR. // events in the past which are before the latest RR.
// //
@ -511,43 +532,60 @@ var TimelinePanel = React.createClass({
// RRs) - but that is a bit of a niche case. It will sort itself out when // RRs) - but that is a bit of a niche case. It will sort itself out when
// the user eventually hits the live timeline. // the user eventually hits the live timeline.
// //
if (currentReadUpToEventId && currentReadUpToEventIndex === null && if (currentRREventId && currentRREventIndex === null &&
this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
return; shouldSendRR = false;
} }
var lastReadEventIndex = this._getLastDisplayedEventIndex({ const lastReadEventIndex = this._getLastDisplayedEventIndex({
ignoreOwn: true ignoreOwn: true,
}); });
if (lastReadEventIndex === null) return; if (lastReadEventIndex === null) {
shouldSendRR = false;
}
let lastReadEvent = this.state.events[lastReadEventIndex];
shouldSendRR = shouldSendRR &&
// Only send a RR if the last read event is ahead in the timeline relative to
// the current RR event.
lastReadEventIndex > currentRREventIndex &&
// Only send a RR if the last RR set != the one we would send
this.lastRRSentEventId != lastReadEvent.getId();
var lastReadEvent = this.state.events[lastReadEventIndex]; // Only send a RM if the last RM sent != the one we would send
const shouldSendRM =
this.lastRMSentEventId != this.state.readMarkerEventId;
// we also remember the last read receipt we sent to avoid spamming the // we also remember the last read receipt we sent to avoid spamming the
// same one at the server repeatedly // same one at the server repeatedly
if ((lastReadEventIndex > currentReadUpToEventIndex && if (shouldSendRR || shouldSendRM) {
this.last_rr_sent_event_id != lastReadEvent.getId()) || if (shouldSendRR) {
this.last_rm_sent_event_id != this.state.readMarkerEventId) { this.lastRRSentEventId = lastReadEvent.getId();
} else {
this.last_rr_sent_event_id = lastReadEvent.getId(); lastReadEvent = null;
this.last_rm_sent_event_id = this.state.readMarkerEventId; }
this.lastRMSentEventId = this.state.readMarkerEventId;
debuglog('TimelinePanel: Sending Read Markers for ',
this.props.timelineSet.room.roomId,
'rm', this.state.readMarkerEventId,
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
);
MatrixClientPeg.get().setRoomReadMarkers( MatrixClientPeg.get().setRoomReadMarkers(
this.props.timelineSet.room.roomId, this.props.timelineSet.room.roomId,
this.state.readMarkerEventId, this.state.readMarkerEventId,
lastReadEvent lastReadEvent, // Could be null, in which case no RR is sent
).catch((e) => { ).catch((e) => {
// /read_markers API is not implemented on this HS, fallback to just RR // /read_markers API is not implemented on this HS, fallback to just RR
if (e.errcode === 'M_UNRECOGNIZED') { if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
return MatrixClientPeg.get().sendReadReceipt( return MatrixClientPeg.get().sendReadReceipt(
lastReadEvent lastReadEvent,
).catch(() => { ).catch(() => {
this.last_rr_sent_event_id = undefined; this.lastRRSentEventId = undefined;
}); });
} }
// it failed, so allow retries next time the user is active // it failed, so allow retries next time the user is active
this.last_rr_sent_event_id = undefined; this.lastRRSentEventId = undefined;
this.last_rm_sent_event_id = undefined; this.lastRMSentEventId = undefined;
}); });
// do a quick-reset of our unreadNotificationCount to avoid having // do a quick-reset of our unreadNotificationCount to avoid having
@ -560,7 +598,6 @@ var TimelinePanel = React.createClass({
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
dis.dispatch({ dis.dispatch({
action: 'on_room_read', action: 'on_room_read',
room: this.props.timelineSet.room,
}); });
} }
} }
@ -756,6 +793,19 @@ var TimelinePanel = React.createClass({
return null; return null;
}, },
canJumpToReadMarker: function() {
// 1. Do not show jump bar if neither the RM nor the RR are set.
// 2. Only show jump bar if RR !== RM. If they are the same, there are only fully
// read messages and unread messages. We already have a badge count and the bottom
// bar to jump to "live" when we have unread messages.
// 3. We want to show the bar if the read-marker is off the top of the screen.
// 4. Also, if pos === null, the event might not be paginated - show the unread bar
const pos = this.getReadMarkerPosition();
return this.state.readMarkerEventId !== null && // 1.
this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2.
(pos < 0 || pos === null); // 3., 4.
},
/** /**
* called by the parent component when PageUp/Down/etc is pressed. * called by the parent component when PageUp/Down/etc is pressed.
* *
@ -865,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,
}); });
@ -1058,11 +1105,17 @@ var TimelinePanel = React.createClass({
// events when viewing historical messages, we get stuck in a loop // events when viewing historical messages, we get stuck in a loop
// of paginating our way through the entire history of the room. // of paginating our way through the entire history of the room.
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS); var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
// If the state is PREPARED, we're still waiting for the js-sdk to sync with
// the HS and fetch the latest events, so we are effectively forward paginating.
const forwardPaginating = (
this.state.forwardPaginating || this.state.clientSyncState == 'PREPARED'
);
return ( return (
<MessagePanel ref="messagePanel" <MessagePanel ref="messagePanel"
hidden={ this.props.hidden } hidden={ this.props.hidden }
backPaginating={ this.state.backPaginating } backPaginating={ this.state.backPaginating }
forwardPaginating={ this.state.forwardPaginating } forwardPaginating={ forwardPaginating }
events={ this.state.events } events={ this.state.events }
highlightedEventId={ this.props.highlightedEventId } highlightedEventId={ this.props.highlightedEventId }
readMarkerEventId={ this.state.readMarkerEventId } readMarkerEventId={ this.state.readMarkerEventId }
@ -1076,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

@ -14,35 +14,49 @@ 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.
*/ */
var React = require('react'); const React = require('react');
var ReactDOM = require('react-dom'); const ReactDOM = require('react-dom');
var sdk = require('../../index'); const sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg"); const MatrixClientPeg = require("../../MatrixClientPeg");
var PlatformPeg = require("../../PlatformPeg"); const PlatformPeg = require("../../PlatformPeg");
var Modal = require('../../Modal'); const Modal = require('../../Modal');
var dis = require("../../dispatcher"); const dis = require("../../dispatcher");
var q = require('q'); const q = require('q');
var package_json = require('../../../package.json'); const packageJson = require('../../../package.json');
var UserSettingsStore = require('../../UserSettingsStore'); const UserSettingsStore = require('../../UserSettingsStore');
var GeminiScrollbar = require('react-gemini-scrollbar'); const GeminiScrollbar = require('react-gemini-scrollbar');
var Email = require('../../email'); const Email = require('../../email');
var AddThreepid = require('../../AddThreepid'); const AddThreepid = require('../../AddThreepid');
var SdkConfig = require('../../SdkConfig'); const SdkConfig = require('../../SdkConfig');
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';
// 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
// the git sha. Prepend version with v, to look like riot-web version // the git sha. Prepend version with v, to look like riot-web version
const REACT_SDK_VERSION = 'dist' in package_json ? `v${package_json.version}` : package_json.gitHead || '<local>'; const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || '<local>';
// Simple method to help prettify GH Release Tags and Commit Hashes. // Simple method to help prettify GH Release Tags and Commit Hashes.
const GHVersionUrl = function(repo, token) { const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
const uriTail = (token.startsWith('v') && token.includes('.')) ? `releases/tag/${token}` : `commit/${token}`; const gHVersionLabel = function(repo, token='') {
return `https://github.com/${repo}/${uriTail}`; const match = token.match(semVerRegex);
} let url;
if (match && match[1]) { // basic semVer string possibly with commit hash
url = (match.length > 1 && match[2])
? `https://github.com/${repo}/commit/${match[2]}`
: `https://github.com/${repo}/releases/tag/v${match[1]}`;
} else {
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
}
return <a target="_blank" rel="noopener" href={url}>{token}</a>;
};
// 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',
@ -50,13 +64,12 @@ const SETTINGS_LABELS = [
}, },
{ {
id: 'hideReadReceipts', id: 'hideReadReceipts',
label: 'Hide read receipts' label: 'Hide read receipts',
}, },
{ {
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',
@ -65,6 +78,7 @@ const SETTINGS_LABELS = [
id: 'showTwelveHourTimestamps', id: 'showTwelveHourTimestamps',
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)', label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
}, },
/*
{ {
id: 'useCompactLayout', id: 'useCompactLayout',
label: 'Use compact timeline layout', label: 'Use compact timeline layout',
@ -76,6 +90,8 @@ const SETTINGS_LABELS = [
*/ */
]; ];
// 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',
@ -106,10 +122,9 @@ const THEMES = [
id: 'theme', id: 'theme',
label: 'Dark theme', label: 'Dark theme',
value: 'dark', value: 'dark',
} },
]; ];
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'UserSettings', displayName: 'UserSettings',
@ -142,10 +157,10 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
avatarUrl: null, avatarUrl: null,
threePids: [], threepids: [],
phase: "UserSettings.LOADING", // LOADING, DISPLAY phase: "UserSettings.LOADING", // LOADING, DISPLAY
email_add_pending: false, email_add_pending: false,
vectorVersion: null, vectorVersion: undefined,
rejectingInvites: false, rejectingInvites: false,
}; };
}, },
@ -180,13 +195,17 @@ module.exports = React.createClass({
}); });
this._refreshFromServer(); this._refreshFromServer();
var syncedSettings = UserSettingsStore.getSyncedSettings(); const syncedSettings = UserSettingsStore.getSyncedSettings();
if (!syncedSettings.theme) { if (!syncedSettings.theme) {
syncedSettings.theme = 'light'; syncedSettings.theme = 'light';
} }
this._syncedSettings = syncedSettings; this._syncedSettings = syncedSettings;
this._localSettings = UserSettingsStore.getLocalSettings(); this._localSettings = UserSettingsStore.getLocalSettings();
this.setState({
language: languageHandler.getCurrentLanguage(),
});
}, },
componentDidMount: function() { componentDidMount: function() {
@ -202,16 +221,16 @@ module.exports = React.createClass({
middleOpacity: 1.0, middleOpacity: 1.0,
}); });
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
let cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli) { if (cli) {
cli.removeListener("RoomMember.membership", this._onInviteStateChange); cli.removeListener("RoomMember.membership", this._onInviteStateChange);
} }
}, },
_refreshFromServer: function() { _refreshFromServer: function() {
var self = this; const self = this;
q.all([ q.all([
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids() UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids(),
]).done(function(resps) { ]).done(function(resps) {
self.setState({ self.setState({
avatarUrl: resps[0].avatar_url, avatarUrl: resps[0].avatar_url,
@ -219,11 +238,11 @@ module.exports = React.createClass({
phase: "UserSettings.DISPLAY", phase: "UserSettings.DISPLAY",
}); });
}, function(error) { }, function(error) {
var 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")),
}); });
}); });
}, },
@ -236,10 +255,10 @@ module.exports = React.createClass({
onAvatarPickerClick: function(ev) { onAvatarPickerClick: function(ev) {
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
var 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;
} }
@ -250,8 +269,8 @@ module.exports = React.createClass({
}, },
onAvatarSelected: function(ev) { onAvatarSelected: function(ev) {
var self = this; const self = this;
var changeAvatar = this.refs.changeAvatar; const changeAvatar = this.refs.changeAvatar;
if (!changeAvatar) { if (!changeAvatar) {
console.error("No ChangeAvatar found to upload image to!"); console.error("No ChangeAvatar found to upload image to!");
return; return;
@ -260,33 +279,33 @@ module.exports = React.createClass({
// dunno if the avatar changed, re-check it. // dunno if the avatar changed, re-check it.
self._refreshFromServer(); self._refreshFromServer();
}, function(err) { }, function(err) {
var errMsg = (typeof err === "string") ? err : (err.error || ""); // const errMsg = (typeof err === "string") ? err : (err.error || "");
console.error("Failed to set avatar: " + err); console.error("Failed to set avatar: " + err);
var 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")),
}); });
}); });
}, },
onLogoutClicked: function(ev) { onLogoutClicked: function(ev) {
var 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) => {
if (confirmed) { if (confirmed) {
@ -300,34 +319,31 @@ module.exports = React.createClass({
}, },
onPasswordChangeError: function(err) { onPasswordChangeError: function(err) {
var 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})`;
} }
var 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,
}); });
}, },
onPasswordChanged: function() { onPasswordChanged: function() {
var 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.`
}); });
}, },
onUpgradeClicked: function() { onUpgradeClicked: function() {
dis.dispatch({ dis.dispatch({
action: "start_upgrade_registration" action: "start_upgrade_registration",
}); });
}, },
@ -341,33 +357,33 @@ module.exports = React.createClass({
}, },
_addEmail: function() { _addEmail: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var email_address = this.refs.add_email_input.value; const emailAddress = this.refs.add_email_input.value;
if (!Email.looksValid(email_address)) { 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;
} }
this._addThreepid = new AddThreepid(); this._addThreepid = new AddThreepid();
// we always bind emails when registering, so let's do the // we always bind emails when registering, so let's do the
// same here. // same here.
this._addThreepid.addEmailAddress(email_address, 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 " + email_address + " " + 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();
@ -377,9 +393,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({
@ -391,8 +407,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();
} }
@ -419,21 +435,21 @@ module.exports = React.createClass({
}, (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') {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var 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 {
var 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")),
}); });
} }
}); });
@ -469,17 +485,17 @@ module.exports = React.createClass({
_onRejectAllInvitesClicked: function(rooms, ev) { _onRejectAllInvitesClicked: function(rooms, ev) {
this.setState({ this.setState({
rejectingInvites: true rejectingInvites: true,
}); });
// reject the invites // reject the invites
let promises = rooms.map((room) => { const promises = rooms.map((room) => {
return MatrixClientPeg.get().leave(room.roomId); return MatrixClientPeg.get().leave(room.roomId);
}); });
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI // purposefully drop errors to the floor: we'll just have a non-zero number on the UI
// after trying to reject all the invites. // after trying to reject all the invites.
q.allSettled(promises).then(() => { q.allSettled(promises).then(() => {
this.setState({ this.setState({
rejectingInvites: false rejectingInvites: false,
}); });
}).done(); }).done();
}, },
@ -492,7 +508,7 @@ module.exports = React.createClass({
}, "e2e-export"); }, "e2e-export");
}, { }, {
matrixClient: MatrixClientPeg.get(), matrixClient: MatrixClientPeg.get(),
} },
); );
}, },
@ -504,7 +520,7 @@ module.exports = React.createClass({
}, "e2e-export"); }, "e2e-export");
}, { }, {
matrixClient: MatrixClientPeg.get(), matrixClient: MatrixClientPeg.get(),
} },
); );
}, },
@ -523,22 +539,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>
); );
}, },
_renderUserInterfaceSettings: function() { onLanguageChange: function(newLang) {
var client = MatrixClientPeg.get(); 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() {
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>
); );
@ -549,10 +586,10 @@ module.exports = React.createClass({
<input id="urlPreviewsDisabled" <input id="urlPreviewsDisabled"
type="checkbox" type="checkbox"
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() } defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
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>;
}, },
@ -562,10 +599,10 @@ 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) }
/> />
<label htmlFor={ setting.id }> <label htmlFor={ setting.id }>
{ setting.label } { _t(setting.label) }
</label> </label>
</div>; </div>;
}, },
@ -577,7 +614,7 @@ module.exports = React.createClass({
name={ setting.id } name={ setting.id }
value={ setting.value } value={ setting.value }
defaultChecked={ this._syncedSettings[setting.id] === setting.value } defaultChecked={ this._syncedSettings[setting.id] === setting.value }
onChange={ e => { onChange={ (e) => {
if (e.target.checked) { if (e.target.checked) {
UserSettingsStore.setSyncedSetting(setting.id, setting.value); UserSettingsStore.setSyncedSetting(setting.id, setting.value);
} }
@ -597,7 +634,12 @@ module.exports = React.createClass({
_renderCryptoInfo: function() { _renderCryptoInfo: function() {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const deviceId = client.deviceId; const deviceId = client.deviceId;
const identityKey = client.getDeviceEd25519Key() || "<not supported>"; let identityKey = client.getDeviceEd25519Key();
if (!identityKey) {
identityKey = _t("<not supported>");
} else {
identityKey = FormattingUtils.formatCryptoKey(identityKey);
}
let importExportButtons = null; let importExportButtons = null;
@ -606,18 +648,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>
@ -639,8 +681,8 @@ module.exports = React.createClass({
type="checkbox" type="checkbox"
defaultChecked={ this._localSettings[setting.id] } defaultChecked={ this._localSettings[setting.id] }
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.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly
client.setGlobalBlacklistUnverifiedDevices(e.target.checked); client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
} }
@ -648,13 +690,13 @@ module.exports = React.createClass({
} }
/> />
<label htmlFor={ setting.id }> <label htmlFor={ setting.id }>
{ setting.label } { _t(setting.label) }
</label> </label>
</div>; </div>;
}, },
_renderDevicesPanel: function() { _renderDevicesPanel: function() {
var DevicesPanel = sdk.getComponent('settings.DevicesPanel'); const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
return ( return (
<div> <div>
<h3>Devices</h3> <h3>Devices</h3>
@ -665,15 +707,15 @@ module.exports = React.createClass({
_renderBugReport: function() { _renderBugReport: function() {
if (!SdkConfig.get().bug_report_endpoint_url) { if (!SdkConfig.get().bug_report_endpoint_url) {
return <div /> return <div />;
} }
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>
@ -684,20 +726,20 @@ module.exports = React.createClass({
// default to enabled if undefined // default to enabled if undefined
if (this.props.enableLabs === false) return null; if (this.props.enableLabs === false) return null;
let 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">
<input <input
type="checkbox" type="checkbox"
id={feature.id} id={feature.id}
name={feature.id} name={feature.id}
defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) } defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) }
onChange={e => { onChange={(e) => {
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
e.target.checked = false; e.target.checked = false;
var 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;
} }
@ -710,9 +752,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>
@ -724,10 +766,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>;
@ -735,25 +777,25 @@ 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>;
}, },
_renderBulkOptions: function() { _renderBulkOptions: function() {
let invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => { const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
return r.hasMembershipState(this._me, "invite"); return r.hasMembershipState(this._me, "invite");
}); });
if (invitedRooms.length === 0) { if (invitedRooms.length === 0) {
return null; return null;
} }
let Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
let reject = <Spinner />; let reject = <Spinner />;
if (!this.state.rejectingInvites) { if (!this.state.rejectingInvites) {
@ -768,7 +810,7 @@ module.exports = React.createClass({
} }
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>
@ -777,9 +819,7 @@ module.exports = React.createClass({
_showSpoiler: function(event) { _showSpoiler: function(event) {
const target = event.target; const target = event.target;
const hidden = target.getAttribute('data-spoiler'); target.innerHTML = target.getAttribute('data-spoiler');
target.innerHTML = hidden;
const range = document.createRange(); const range = document.createRange();
range.selectNodeContents(target); range.selectNodeContents(target);
@ -790,12 +830,13 @@ 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);
}, },
presentableTextForThreepid: function(threepid) { presentableTextForThreepid: function(threepid) {
if (threepid.medium == 'msisdn') { if (threepid.medium === 'msisdn') {
return '+' + threepid.address; return '+' + threepid.address;
} else { } else {
return threepid.address; return threepid.address;
@ -803,7 +844,7 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
switch (this.state.phase) { switch (this.state.phase) {
case "UserSettings.LOADING": case "UserSettings.LOADING":
return ( return (
@ -815,18 +856,18 @@ module.exports = React.createClass({
throw new Error("Unknown state.phase => " + this.state.phase); throw new Error("Unknown state.phase => " + this.state.phase);
} }
// can only get here if phase is UserSettings.DISPLAY // can only get here if phase is UserSettings.DISPLAY
var SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName"); const ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
var ChangePassword = sdk.getComponent("views.settings.ChangePassword"); const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
var Notifications = sdk.getComponent("settings.Notifications"); const Notifications = sdk.getComponent("settings.Notifications");
var EditableText = sdk.getComponent('elements.EditableText'); const EditableText = sdk.getComponent('elements.EditableText');
var avatarUrl = ( const avatarUrl = (
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
); );
var threepidsSection = this.state.threepids.map((val, pidIndex) => { const threepidsSection = this.state.threepids.map((val, pidIndex) => {
const id = "3pid-" + val.address; const id = "3pid-" + val.address;
return ( return (
<div className="mx_UserSettings_profileTableRow" key={pidIndex}> <div className="mx_UserSettings_profileTableRow" key={pidIndex}>
@ -839,7 +880,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>
); );
@ -851,13 +892,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>{_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>
@ -874,16 +916,15 @@ module.exports = React.createClass({
threepidsSection.push(addEmailSection); threepidsSection.push(addEmailSection);
threepidsSection.push(addMsisdnSection); threepidsSection.push(addMsisdnSection);
var accountJsx; let accountJsx;
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 {
accountJsx = ( accountJsx = (
<ChangePassword <ChangePassword
className="mx_UserSettings_accountTable" className="mx_UserSettings_accountTable"
@ -895,10 +936,10 @@ module.exports = React.createClass({
onFinished={this.onPasswordChanged} /> onFinished={this.onPasswordChanged} />
); );
} }
var notification_area; let notificationArea;
if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) { if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) {
notification_area = (<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} />
@ -911,13 +952,13 @@ module.exports = React.createClass({
// we are using a version old version of olm. We assume the former. // we are using a version old version of olm. We assume the former.
let olmVersionString = "<not-enabled>"; let olmVersionString = "<not-enabled>";
if (olmVersion !== undefined) { if (olmVersion !== undefined) {
olmVersionString = `v${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`; olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
} }
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 }
/> />
@ -925,13 +966,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 />
@ -948,7 +989,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}/>
@ -956,12 +997,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}
@ -969,7 +1010,7 @@ module.exports = React.createClass({
{this._renderReferral()} {this._renderReferral()}
{notification_area} {notificationArea}
{this._renderUserInterfaceSettings()} {this._renderUserInterfaceSettings()}
{this._renderLabs()} {this._renderLabs()}
@ -978,31 +1019,31 @@ module.exports = React.createClass({
{this._renderBulkOptions()} {this._renderBulkOptions()}
{this._renderBugReport()} {this._renderBugReport()}
<h3>Advanced</h3> <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" onClick={this._showSpoiler} data-spoiler={ MatrixClientPeg.get().getAccessToken() }>&lt;click to reveal&gt;</span> {_t('Access Token:')} <span className="mx_UserSettings_advanced_spoiler" onClick={this._showSpoiler} data-spoiler={ MatrixClientPeg.get().getAccessToken() }>&lt;{ _t("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>')
? <a href={ GHVersionUrl('matrix-org/matrix-react-sdk', REACT_SDK_VERSION) }>{REACT_SDK_VERSION}</a> ? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
: REACT_SDK_VERSION : REACT_SDK_VERSION
}<br/> }<br/>
riot-web version: {(this.state.vectorVersion !== null) {_t('riot-web version:')} {(this.state.vectorVersion !== undefined)
? <a href={ GHVersionUrl('vector-im/riot-web', this.state.vectorVersion.split('-')[0]) }>{this.state.vectorVersion}</a> ? 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>
@ -1013,5 +1054,5 @@ module.exports = React.createClass({
</GeminiScrollbar> </GeminiScrollbar>
</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,11 +18,14 @@ limitations under the License.
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import { _t } 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';
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
/** /**
* A wire component which glues together login UI components and Login logic * A wire component which glues together login UI components and Login logic
*/ */
@ -125,7 +128,16 @@ module.exports = React.createClass({
}, },
onPhoneNumberChanged: function(phoneNumber) { onPhoneNumberChanged: function(phoneNumber) {
this.setState({ phoneNumber: phoneNumber }); // Validate the phone number entered
if (!PHONE_NUMBER_REGEX.test(phoneNumber)) {
this.setState({ errorText: 'The phone number entered looks invalid' });
return;
}
this.setState({
phoneNumber: phoneNumber,
errorText: null,
});
}, },
onServerConfigChange: function(config) { onServerConfigChange: function(config) {
@ -210,15 +222,17 @@ module.exports = React.createClass({
(this.state.enteredHomeserverUrl.startsWith("http:") || (this.state.enteredHomeserverUrl.startsWith("http:") ||
!this.state.enteredHomeserverUrl.startsWith("http"))) !this.state.enteredHomeserverUrl.startsWith("http")))
{ {
const urlStart = <a href='https://www.google.com/search?&q=enable%20unsafe%20scripts'>;
const urlEnd = </a>;
errorText = <span> errorText = <span>
Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. { _t('Can\'t connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or %(urlStart)s enable unsafe scripts %(urlEnd)s', {urlStart: urlStart, urlEnd: urlEnd})}
Either use HTTPS or <a href='https://www.google.com/search?&q=enable%20unsafe%20scripts'>enable unsafe scripts</a>
</span>; </span>;
} }
else { else {
const urlStart = <a href={this.state.enteredHomeserverUrl}>;
const urlEnd = </a>;
errorText = <span> errorText = <span>
Can't connect to homeserver - please check your connectivity and ensure { _t('Can\'t connect to homeserver - please check your connectivity and ensure your %(urlStart)s homeserver\'s SSL certificate %(urlEnd)s is trusted', {urlStart: urlStart, urlEnd: urlEnd})}
your <a href={ this.state.enteredHomeserverUrl }>homeserver's SSL certificate</a> is trusted.
</span>; </span>;
} }
} }
@ -230,12 +244,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}
@ -247,7 +255,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':
@ -261,8 +268,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>
); );
} }
@ -279,7 +285,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>;
} }
@ -287,7 +293,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>;
} }
@ -296,7 +302,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) }
@ -312,7 +318,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;
@ -123,18 +124,17 @@ module.exports = React.createClass({
} }
}, },
onHsUrlChanged: function(newHsUrl) { onServerConfigChange: function(config) {
this.setState({ let newState = {};
hsUrl: newHsUrl, if (config.hsUrl !== undefined) {
newState.hsUrl = config.hsUrl;
}
if (config.isUrl !== undefined) {
newState.isUrl = config.isUrl;
}
this.setState(newState, function() {
this._replaceClient();
}); });
this._replaceClient();
},
onIsUrlChanged: function(newIsUrl) {
this.setState({
isUrl: newIsUrl,
});
this._replaceClient();
}, },
_replaceClient: function() { _replaceClient: function() {
@ -163,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({
@ -261,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({
@ -390,8 +390,7 @@ module.exports = React.createClass({
customIsUrl={this.props.customIsUrl} customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl} defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl} defaultIsUrl={this.props.defaultIsUrl}
onHsUrlChanged={this.onHsUrlChanged} onServerConfigChange={this.onServerConfigChange}
onIsUrlChanged={this.onIsUrlChanged}
delayTimeMs={1000} delayTimeMs={1000}
/> />
</div> </div>
@ -402,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>
); );
} }
@ -415,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

@ -59,7 +59,9 @@ module.exports = React.createClass({
ContentRepo.getHttpUriForMxc( ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), MatrixClientPeg.get().getHomeserverUrl(),
props.oobData.avatarUrl, props.oobData.avatarUrl,
props.width, props.height, props.resizeMethod Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod
), // highest priority ), // highest priority
this.getRoomAvatarUrl(props), this.getRoomAvatarUrl(props),
this.getOneToOneAvatar(props), this.getOneToOneAvatar(props),
@ -74,7 +76,9 @@ module.exports = React.createClass({
return props.room.getAvatarUrl( return props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(), MatrixClientPeg.get().getHomeserverUrl(),
props.width, props.height, props.resizeMethod, Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false false
); );
}, },
@ -103,14 +107,18 @@ module.exports = React.createClass({
} }
return theOtherGuy.getAvatarUrl( return theOtherGuy.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(), MatrixClientPeg.get().getHomeserverUrl(),
props.width, props.height, props.resizeMethod, Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false false
); );
} else if (userIds.length == 1) { } else if (userIds.length == 1) {
return mlist[userIds[0]].getAvatarUrl( return mlist[userIds[0]].getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(), MatrixClientPeg.get().getHomeserverUrl(),
props.width, props.height, props.resizeMethod, Math.floor(props.width * window.devicePixelRatio),
false Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false
); );
} else { } else {
return null; return null;

View file

@ -47,16 +47,6 @@ export default React.createClass({
children: React.PropTypes.node, children: React.PropTypes.node,
}, },
componentWillMount: function() {
this.priorActiveElement = document.activeElement;
},
componentWillUnmount: function() {
if (this.priorActiveElement !== null) {
this.priorActiveElement.focus();
}
},
_onKeyDown: function(e) { _onKeyDown: function(e) {
if (e.keyCode === KeyCode.ESCAPE) { if (e.keyCode === KeyCode.ESCAPE) {
e.stopPropagation(); e.stopPropagation();
@ -77,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

@ -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';
@ -48,11 +49,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
}; };
}, },

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

@ -0,0 +1,76 @@
/*
Copyright 2016 OpenMarket Ltd
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 sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
export default function DeviceVerifyDialog(props) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const key = FormattingUtils.formatCryptoKey(props.device.getFingerprint());
const body = (
<div>
<p>
To verify that this device can be trusted, please contact its
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
for this device matches the key below:
</p>
<div className="mx_UserSettings_cryptoSection">
<ul>
<li><label>Device name:</label> <span>{ props.device.getDisplayName() }</span></li>
<li><label>Device ID:</label> <span><code>{ props.device.deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{ key }</b></code></span></li>
</ul>
</div>
<p>
If it matches, press the verify button below.
If it doesnt, then someone else is intercepting this device
and you probably want to press the blacklist button instead.
</p>
<p>
In future this verification process will be more sophisticated.
</p>
</div>
);
function onFinished(confirm) {
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
props.userId, props.device.deviceId, true,
);
}
props.onFinished(confirm);
}
return (
<QuestionDialog
title="Verify device"
description={body}
button="I verify that the keys match"
onFinished={onFinished}
/>
);
}
DeviceVerifyDialog.propTypes = {
userId: React.PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired,
onFinished: React.PropTypes.func.isRequired,
};

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,
}; };
@ -47,12 +47,6 @@ export default React.createClass({
this.props.onFinished(false); this.props.onFinished(false);
}, },
componentDidMount: function() {
if (this.props.focus) {
this.refs.button.focus();
}
},
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const cancelButton = this.props.hasCancelButton ? ( const cancelButton = this.props.hasCancelButton ? (
@ -69,8 +63,8 @@ export default React.createClass({
{this.props.description} {this.props.description}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button ref="button" className="mx_Dialog_primary" onClick={this.onOk}> <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

@ -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

@ -149,7 +149,7 @@ export default React.createClass({
> >
<GeminiScrollbar autoshow={false} className="mx_Dialog_content"> <GeminiScrollbar autoshow={false} className="mx_Dialog_content">
<h4> <h4>
This room contains devices that you haven't seen before. "{this.props.room.name}" contains devices that you haven't seen before.
</h4> </h4>
{ warning } { warning }
Unknown devices: Unknown devices:

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.
@ -138,7 +139,7 @@ export default React.createClass({
onClick={this.onClick.bind(this, i)} onClick={this.onClick.bind(this, i)}
onMouseEnter={this.onMouseEnter.bind(this, i)} onMouseEnter={this.onMouseEnter.bind(this, i)}
onMouseLeave={this.onMouseLeave} onMouseLeave={this.onMouseLeave}
key={this.props.addressList[i].userId} key={this.props.addressList[i].addressType + "/" + this.props.addressList[i].address}
ref={(ref) => { this.addressListElement = ref; }} ref={(ref) => { this.addressListElement = ref; }}
> >
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" /> <AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />

View file

@ -50,42 +50,10 @@ export default React.createClass({
}, },
onVerifyClick: function() { onVerifyClick: function() {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
Modal.createDialog(QuestionDialog, { Modal.createDialog(DeviceVerifyDialog, {
title: "Verify device", userId: this.props.userId,
description: ( device: this.state.device,
<div>
<p>
To verify that this device can be trusted, please contact its
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
for this device matches the key below:
</p>
<div className="mx_UserSettings_cryptoSection">
<ul>
<li><label>Device name:</label> <span>{ this.state.device.getDisplayName() }</span></li>
<li><label>Device ID:</label> <span><code>{ this.state.device.deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{ this.state.device.getFingerprint() }</b></code></span></li>
</ul>
</div>
<p>
If it matches, press the verify button below.
If it doesnt, then someone else is intercepting this device
and you probably want to press the blacklist button instead.
</p>
<p>
In future this verification process will be more sophisticated.
</p>
</div>
),
button: "I verify that the keys match",
onFinished: confirm=>{
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.state.device.deviceId, true
);
}
},
}); });
}, },

View file

@ -93,7 +93,7 @@ export default class DirectorySearchBox extends React.Component {
className="mx_DirectorySearchBox_input" className="mx_DirectorySearchBox_input"
ref={this._collectInput} ref={this._collectInput}
onChange={this._onChange} onKeyUp={this._onKeyUp} onChange={this._onChange} onKeyUp={this._onKeyUp}
placeholder={this.props.placeholder} placeholder={this.props.placeholder} autoFocus
/> />
{join_button} {join_button}
<span className="mx_DirectorySearchBox_clear_wrapper"> <span className="mx_DirectorySearchBox_clear_wrapper">

View file

@ -114,8 +114,11 @@ export default class Dropdown extends React.Component {
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (!nextProps.children || nextProps.children.length === 0) {
return;
}
this._reindexChildren(nextProps.children); this._reindexChildren(nextProps.children);
const firstChild = React.Children.toArray(nextProps.children)[0]; const firstChild = nextProps.children[0];
this.setState({ this.setState({
highlightedOption: firstChild ? firstChild.key : null, highlightedOption: firstChild ? firstChild.key : null,
}); });
@ -149,10 +152,12 @@ export default class Dropdown extends React.Component {
} }
_onInputClick(ev) { _onInputClick(ev) {
this.setState({ if (!this.state.expanded) {
expanded: !this.state.expanded, this.setState({
}); expanded: true,
ev.preventDefault(); });
ev.preventDefault();
}
} }
_onMenuOptionClick(dropdownKey) { _onMenuOptionClick(dropdownKey) {
@ -249,7 +254,7 @@ export default class Dropdown extends React.Component {
); );
}); });
if (options.length === 0) { if (options.length === 0) {
return [<div className="mx_Dropdown_option"> return [<div key="0" className="mx_Dropdown_option">
No results No results
</div>]; </div>];
} }

View file

@ -0,0 +1,128 @@
/*
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.getAllLanguageKeysFromJson().then((langKeys) => {
const langs = [];
langKeys.forEach((languageKey) => {
langs.push({
value: languageKey,
label: _t(languageKey)
});
});
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,28 +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", }
};
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;
@ -252,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 });
} }
}, },
@ -267,7 +387,7 @@ module.exports = React.createClass({
); );
}); });
return ( return (
<span className="mx_MemberEventListSummary_avatars"> <span className="mx_MemberEventListSummary_avatars" onClick={ this._toggleSummary }>
{avatars} {avatars}
</span> </span>
); );
@ -289,7 +409,24 @@ module.exports = React.createClass({
switch (e.mxEvent.getContent().membership) { switch (e.mxEvent.getContent().membership) {
case 'invite': return 'invited'; case 'invite': return 'invited';
case 'ban': return 'banned'; case 'ban': return 'banned';
case 'join': return 'joined'; case 'join':
if (e.mxEvent.getPrevContent().membership === 'join') {
if (e.mxEvent.getContent().displayname !==
e.mxEvent.getPrevContent().displayname)
{
return 'changed_name';
}
else if (e.mxEvent.getContent().avatar_url !==
e.mxEvent.getPrevContent().avatar_url)
{
return 'changed_avatar';
}
// console.log("MELS ignoring duplicate membership join event");
return null;
}
else {
return 'joined';
}
case 'leave': case 'leave':
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) { if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
switch (e.mxEvent.getPrevContent().membership) { switch (e.mxEvent.getPrevContent().membership) {
@ -350,6 +487,7 @@ module.exports = React.createClass({
render: function() { render: function() {
const eventsToRender = this.props.events; const eventsToRender = this.props.events;
const eventIds = eventsToRender.map(e => e.getId()).join(',');
const fewEvents = eventsToRender.length < this.props.threshold; const fewEvents = eventsToRender.length < this.props.threshold;
const expanded = this.state.expanded || fewEvents; const expanded = this.state.expanded || fewEvents;
@ -360,7 +498,7 @@ module.exports = React.createClass({
if (fewEvents) { if (fewEvents) {
return ( return (
<div className="mx_MemberEventListSummary"> <div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
{expandedEvents} {expandedEvents}
</div> </div>
); );
@ -418,7 +556,7 @@ module.exports = React.createClass({
); );
return ( return (
<div className="mx_MemberEventListSummary"> <div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
{toggleButton} {toggleButton}
{summaryContainer} {summaryContainer}
{expanded ? <div className="mx_MemberEventListSummary_line">&nbsp;</div> : null} {expanded ? <div className="mx_MemberEventListSummary_line">&nbsp;</div> : null}

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),

View file

@ -19,7 +19,6 @@ import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import { COUNTRIES } from '../../../phonenumber'; import { COUNTRIES } from '../../../phonenumber';
import { charactersToImageNode } from '../../../HtmlUtils';
const COUNTRIES_BY_ISO2 = new Object(null); const COUNTRIES_BY_ISO2 = new Object(null);
for (const c of COUNTRIES) { for (const c of COUNTRIES) {
@ -27,9 +26,14 @@ for (const c of COUNTRIES) {
} }
function countryMatchesSearchQuery(query, country) { function countryMatchesSearchQuery(query, country) {
// Remove '+' if present (when searching for a prefix)
if (query[0] === '+') {
query = query.slice(1);
}
if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true; if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
if (country.iso2 == query.toUpperCase()) return true; if (country.iso2 == query.toUpperCase()) return true;
if (country.prefix == query) return true; if (country.prefix.indexOf(query) !== -1) return true;
return false; return false;
} }
@ -37,10 +41,12 @@ export default class CountryDropdown extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this._onSearchChange = this._onSearchChange.bind(this); this._onSearchChange = this._onSearchChange.bind(this);
this._onOptionChange = this._onOptionChange.bind(this);
this._getShortOption = this._getShortOption.bind(this);
this.state = { this.state = {
searchQuery: '', searchQuery: '',
} };
} }
componentWillMount() { componentWillMount() {
@ -48,7 +54,7 @@ export default class CountryDropdown extends React.Component {
// If no value is given, we start with the first // If no value is given, we start with the first
// country selected, but our parent component // country selected, but our parent component
// doesn't know this, therefore we do this. // doesn't know this, therefore we do this.
this.props.onOptionChange(COUNTRIES[0].iso2); this.props.onOptionChange(COUNTRIES[0]);
} }
} }
@ -58,14 +64,26 @@ export default class CountryDropdown extends React.Component {
}); });
} }
_onOptionChange(iso2) {
this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]);
}
_flagImgForIso2(iso2) { _flagImgForIso2(iso2) {
// Unicode Regional Indicator Symbol letter 'A' return <img src={`flags/${iso2}.png`}/>;
const RIS_A = 0x1F1E6; }
const ASCII_A = 65;
return charactersToImageNode(iso2, true, _getShortOption(iso2) {
RIS_A + (iso2.charCodeAt(0) - ASCII_A), if (!this.props.isSmall) {
RIS_A + (iso2.charCodeAt(1) - ASCII_A), return undefined;
); }
let countryPrefix;
if (this.props.showPrefix) {
countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix;
}
return <span>
{ this._flagImgForIso2(iso2) }
{ countryPrefix }
</span>;
} }
render() { render() {
@ -94,7 +112,7 @@ export default class CountryDropdown extends React.Component {
const options = displayedCountries.map((country) => { const options = displayedCountries.map((country) => {
return <div key={country.iso2}> return <div key={country.iso2}>
{this._flagImgForIso2(country.iso2)} {this._flagImgForIso2(country.iso2)}
{country.name} {country.name} <span>(+{country.prefix})</span>
</div>; </div>;
}); });
@ -102,18 +120,21 @@ export default class CountryDropdown extends React.Component {
// values between mounting and the initial value propgating // values between mounting and the initial value propgating
const value = this.props.value || COUNTRIES[0].iso2; const value = this.props.value || COUNTRIES[0].iso2;
return <Dropdown className={this.props.className} return <Dropdown className={this.props.className + " left_aligned"}
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange} onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
menuWidth={298} getShortOption={this._flagImgForIso2} menuWidth={298} getShortOption={this._getShortOption}
value={value} searchEnabled={true} value={value} searchEnabled={true}
> >
{options} {options}
</Dropdown> </Dropdown>;
} }
} }
CountryDropdown.propTypes = { CountryDropdown.propTypes = {
className: React.PropTypes.string, className: React.PropTypes.string,
isSmall: React.PropTypes.bool,
// if isSmall, show +44 in the selected value
showPrefix: React.PropTypes.bool,
onOptionChange: React.PropTypes.func.isRequired, onOptionChange: React.PropTypes.func.isRequired,
value: React.PropTypes.string, value: React.PropTypes.string,
}; };

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';
@ -90,8 +91,11 @@ class PasswordLogin extends React.Component {
} }
onPhoneCountryChanged(country) { onPhoneCountryChanged(country) {
this.setState({phoneCountry: country}); this.setState({
this.props.onPhoneCountryChanged(country); phoneCountry: country.iso2,
phonePrefix: country.prefix,
});
this.props.onPhoneCountryChanged(country.iso2);
} }
onPhoneNumberChanged(ev) { onPhoneNumberChanged(ev) {
@ -118,42 +122,29 @@ 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_suffix": Boolean(this.props.hsDomain), type="text"
}); name="username" // make it a little easier for browser's remember-password
let suffix = null; onChange={this.onUsernameChanged}
if (this.props.hsDomain) { placeholder={_t('User name')}
suffix = <div className="mx_Login_username_suffix"> value={this.state.username}
:{this.props.hsDomain} autoFocus
</div>; />;
}
return <div className="mx_Login_username_group">
<div className="mx_Login_username_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">
<CountryDropdown <CountryDropdown
className="mx_Login_phoneCountry" className="mx_Login_phoneCountry mx_Login_field_prefix"
ref="phone_country" ref="phone_country"
onOptionChange={this.onPhoneCountryChanged} onOptionChange={this.onPhoneCountryChanged}
value={this.state.phoneCountry} value={this.state.phoneCountry}
isSmall={true}
showPrefix={true}
/> />
<input <input
className="mx_Login_phoneNumberField mx_Login_field" className="mx_Login_phoneNumberField mx_Login_field mx_Login_field_has_prefix"
ref="phoneNumber" ref="phoneNumber"
key="phone_input" key="phone_input"
type="text" type="text"
@ -173,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>
); );
} }
@ -191,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>
); );
@ -231,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

@ -100,7 +100,7 @@ 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/> If you don't specify an email address, you won't be able to reset your password.<br/>
@ -270,7 +270,8 @@ module.exports = React.createClass({
_onPhoneCountryChange(newVal) { _onPhoneCountryChange(newVal) {
this.setState({ this.setState({
phoneCountry: newVal, phoneCountry: newVal.iso2,
phonePrefix: newVal.prefix,
}); });
}, },
@ -313,14 +314,19 @@ module.exports = React.createClass({
const phoneSection = ( const phoneSection = (
<div className="mx_Login_phoneSection"> <div className="mx_Login_phoneSection">
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange} <CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
className="mx_Login_phoneCountry" className="mx_Login_phoneCountry mx_Login_field_prefix"
value={this.state.phoneCountry} value={this.state.phoneCountry}
isSmall={true}
showPrefix={true}
/> />
<input type="text" ref="phoneNumber" <input type="text" ref="phoneNumber"
placeholder="Mobile phone number (optional)" placeholder="Mobile phone number (optional)"
defaultValue={this.props.defaultPhoneNumber} defaultValue={this.props.defaultPhoneNumber}
className={this._classForField( className={this._classForField(
FIELD_PHONE_NUMBER, 'mx_Login_phoneNumberField', 'mx_Login_field' FIELD_PHONE_NUMBER,
'mx_Login_phoneNumberField',
'mx_Login_field',
'mx_Login_field_has_prefix'
)} )}
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}} onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
value={self.state.phoneNumber} value={self.state.phoneNumber}

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

@ -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

@ -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');
@ -129,6 +131,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() {
@ -295,16 +300,6 @@ module.exports = WithMatrixClient(React.createClass({
const receiptOffset = 15; const receiptOffset = 15;
let left = 0; let left = 0;
// It's possible that the receipt was sent several days AFTER the event.
// If it is, we want to display the complete date along with the HH:MM:SS,
// rather than just HH:MM:SS.
let dayAfterEvent = new Date(this.props.mxEvent.getTs());
dayAfterEvent.setDate(dayAfterEvent.getDate() + 1);
dayAfterEvent.setHours(0);
dayAfterEvent.setMinutes(0);
dayAfterEvent.setSeconds(0);
let dayAfterEventTime = dayAfterEvent.getTime();
var receipts = this.props.readReceipts || []; var receipts = this.props.readReceipts || [];
for (var i = 0; i < receipts.length; ++i) { for (var i = 0; i < receipts.length; ++i) {
var receipt = receipts[i]; var receipt = receipts[i];
@ -340,7 +335,6 @@ module.exports = WithMatrixClient(React.createClass({
suppressAnimation={this._suppressReadReceiptAnimation} suppressAnimation={this._suppressReadReceiptAnimation}
onClick={this.toggleAllReadAvatars} onClick={this.toggleAllReadAvatars}
timestamp={receipt.ts} timestamp={receipt.ts}
showFullTimestamp={receipt.ts >= dayAfterEventTime}
/> />
); );
} }
@ -401,8 +395,7 @@ module.exports = WithMatrixClient(React.createClass({
var msgtype = content.msgtype; var msgtype = content.msgtype;
var eventType = this.props.mxEvent.getType(); var eventType = this.props.mxEvent.getType();
// Info messages are basically information about commands processed on a // Info messages are basically information about commands processed on a room
// room, or emote messages
var isInfoMessage = (eventType !== 'm.room.message'); var isInfoMessage = (eventType !== 'm.room.message');
var EventTileType = sdk.getComponent(eventTileTypes[eventType]); var EventTileType = sdk.getComponent(eventTileTypes[eventType]);
@ -416,9 +409,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',
@ -430,7 +424,8 @@ module.exports = WithMatrixClient(React.createClass({
menu: this.state.menu, menu: this.state.menu,
mx_EventTile_verified: this.state.verified == true, mx_EventTile_verified: this.state.verified == true,
mx_EventTile_unverified: this.state.verified == false, mx_EventTile_unverified: this.state.verified == false,
mx_EventTile_bad: this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted', mx_EventTile_bad: msgtype === 'm.bad.encrypted',
mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_redacted: isRedacted, mx_EventTile_redacted: isRedacted,
}); });
@ -475,9 +470,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 {
@ -485,36 +480,34 @@ 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="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" 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" />;
} }
// real padlocks // real padlocks
else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) { else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) {
if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') { if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" 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" 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" 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" }}/>;
} }
} }
else if (e2eEnabled) { else if (e2eEnabled) {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" 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

@ -100,7 +100,9 @@ module.exports = React.createClass({
render: function() { render: function() {
var p = this.state.preview; var p = this.state.preview;
if (!p) return <div/>; if (!p || Object.keys(p).length === 0) {
return <div/>;
}
// FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing? // FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
var image = p["og:image"]; var image = p["og:image"];

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>;
@ -717,8 +718,16 @@ module.exports = WithMatrixClient(React.createClass({
const memberName = this.props.member.name; const memberName = this.props.member.name;
if (this.props.member.user) {
var presenceState = this.props.member.user.presence;
var presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
var presenceLastTs = this.props.member.user.lastPresenceTs;
var presenceCurrentlyActive = this.props.member.user.currentlyActive;
}
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var PowerSelector = sdk.getComponent('elements.PowerSelector'); var PowerSelector = sdk.getComponent('elements.PowerSelector');
var PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
const EmojiText = sdk.getComponent('elements.EmojiText'); const EmojiText = sdk.getComponent('elements.EmojiText');
return ( return (
<div className="mx_MemberInfo"> <div className="mx_MemberInfo">
@ -734,7 +743,12 @@ 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 className="mx_MemberInfo_profileField">
<PresenceLabel activeAgo={ presenceLastActiveAgo }
currentlyActive={ presenceCurrentlyActive }
presenceState={ presenceState } />
</div> </div>
</div> </div>

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');
@ -207,7 +208,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} />
@ -363,8 +366,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');
@ -33,6 +33,7 @@ export default class MessageComposer extends React.Component {
this.onHangupClick = this.onHangupClick.bind(this); this.onHangupClick = this.onHangupClick.bind(this);
this.onUploadClick = this.onUploadClick.bind(this); this.onUploadClick = this.onUploadClick.bind(this);
this.onUploadFileSelected = this.onUploadFileSelected.bind(this); this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
this.uploadFiles = this.uploadFiles.bind(this);
this.onVoiceCallClick = this.onVoiceCallClick.bind(this); this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
this.onInputContentChanged = this.onInputContentChanged.bind(this); this.onInputContentChanged = this.onInputContentChanged.bind(this);
this.onUpArrow = this.onUpArrow.bind(this); this.onUpArrow = this.onUpArrow.bind(this);
@ -43,6 +44,7 @@ export default class MessageComposer extends React.Component {
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this); this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
this.onInputStateChanged = this.onInputStateChanged.bind(this); this.onInputStateChanged = this.onInputStateChanged.bind(this);
this.onEvent = this.onEvent.bind(this); this.onEvent = this.onEvent.bind(this);
this.onPageUnload = this.onPageUnload.bind(this);
this.state = { this.state = {
autocompleteQuery: '', autocompleteQuery: '',
@ -50,7 +52,7 @@ export default class MessageComposer extends React.Component {
inputState: { inputState: {
style: [], style: [],
blockType: null, blockType: null,
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true), isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false),
wordCount: 0, wordCount: 0,
}, },
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false), showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
@ -64,12 +66,21 @@ export default class MessageComposer extends React.Component {
// marked as encrypted. // marked as encrypted.
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something. // XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
MatrixClientPeg.get().on("event", this.onEvent); MatrixClientPeg.get().on("event", this.onEvent);
window.addEventListener('beforeunload', this.onPageUnload);
} }
componentWillUnmount() { componentWillUnmount() {
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("event", this.onEvent); MatrixClientPeg.get().removeListener("event", this.onEvent);
} }
window.removeEventListener('beforeunload', this.onPageUnload);
}
onPageUnload(event) {
if (this.messageComposerInput) {
this.messageComposerInput.sentHistory.saveLastTextEntry();
}
} }
onEvent(event) { onEvent(event) {
@ -82,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;
} }
@ -91,10 +102,11 @@ export default class MessageComposer extends React.Component {
this.refs.uploadInput.click(); this.refs.uploadInput.click();
} }
onUploadFileSelected(files, isPasted) { onUploadFileSelected(files) {
if (!isPasted) this.uploadFiles(files.target.files);
files = files.target.files; }
uploadFiles(files) {
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let TintableSvg = sdk.getComponent("elements.TintableSvg"); let TintableSvg = sdk.getComponent("elements.TintableSvg");
@ -106,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 upload the following files?') }</p>
<ul style={{listStyle: 'none', textAlign: 'left'}}> <ul style={{listStyle: 'none', textAlign: 'left'}}>
{fileList} {fileList}
</ul> </ul>
@ -228,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';
} }
@ -245,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>;
} }
@ -268,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}
@ -288,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
@ -300,7 +312,7 @@ export default class MessageComposer extends React.Component {
tryComplete={this._tryComplete} tryComplete={this._tryComplete}
onUpArrow={this.onUpArrow} onUpArrow={this.onUpArrow}
onDownArrow={this.onDownArrow} onDownArrow={this.onDownArrow}
onUploadFileSelected={this.onUploadFileSelected} onFilesPasted={this.uploadFiles}
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
onContentChanged={this.onInputContentChanged} onContentChanged={this.onInputContentChanged}
onInputStateChanged={this.onInputStateChanged} />, onInputStateChanged={this.onInputStateChanged} />,
@ -313,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>
); );
} }
@ -342,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`}
@ -362,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';
@ -84,7 +85,6 @@ export default class MessageComposerInput extends React.Component {
this.onAction = this.onAction.bind(this); this.onAction = this.onAction.bind(this);
this.handleReturn = this.handleReturn.bind(this); this.handleReturn = this.handleReturn.bind(this);
this.handleKeyCommand = this.handleKeyCommand.bind(this); this.handleKeyCommand = this.handleKeyCommand.bind(this);
this.handlePastedFiles = this.handlePastedFiles.bind(this);
this.onEditorContentChanged = this.onEditorContentChanged.bind(this); this.onEditorContentChanged = this.onEditorContentChanged.bind(this);
this.setEditorState = this.setEditorState.bind(this); this.setEditorState = this.setEditorState.bind(this);
this.onUpArrow = this.onUpArrow.bind(this); this.onUpArrow = this.onUpArrow.bind(this);
@ -94,7 +94,7 @@ export default class MessageComposerInput extends React.Component {
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this); this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this); this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true); const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
this.state = { this.state = {
// whether we're in rich text or markdown mode // whether we're in rich text or markdown mode
@ -477,10 +477,6 @@ export default class MessageComposerInput extends React.Component {
return false; return false;
} }
handlePastedFiles(files) {
this.props.onUploadFileSelected(files, true);
}
handleReturn(ev) { handleReturn(ev) {
if (ev.shiftKey) { if (ev.shiftKey) {
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
@ -509,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") + "."),
}); });
}); });
} }
@ -518,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;
@ -542,9 +538,9 @@ export default class MessageComposerInput extends React.Component {
let sendTextFn = this.client.sendTextMessage; let sendTextFn = this.client.sendTextMessage;
if (contentText.startsWith('/me')) { if (contentText.startsWith('/me')) {
contentText = contentText.replace('/me ', ''); contentText = contentText.substring(4);
// bit of a hack, but the alternative would be quite complicated // bit of a hack, but the alternative would be quite complicated
if (contentHTML) contentHTML = contentHTML.replace('/me ', ''); if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, '');
sendHtmlFn = this.client.sendHtmlEmote; sendHtmlFn = this.client.sendHtmlEmote;
sendTextFn = this.client.sendEmoteMessage; sendTextFn = this.client.sendEmoteMessage;
} }
@ -724,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}
@ -734,7 +730,7 @@ export default class MessageComposerInput extends React.Component {
keyBindingFn={MessageComposerInput.getKeyBinding} keyBindingFn={MessageComposerInput.getKeyBinding}
handleKeyCommand={this.handleKeyCommand} handleKeyCommand={this.handleKeyCommand}
handleReturn={this.handleReturn} handleReturn={this.handleReturn}
handlePastedFiles={this.handlePastedFiles} handlePastedFiles={this.props.onFilesPasted}
stripPastedStyles={!this.state.isRichtextEnabled} stripPastedStyles={!this.state.isRichtextEnabled}
onTab={this.onTab} onTab={this.onTab}
onUpArrow={this.onUpArrow} onUpArrow={this.onUpArrow}
@ -764,7 +760,7 @@ MessageComposerInput.propTypes = {
onDownArrow: React.PropTypes.func, onDownArrow: React.PropTypes.func,
onUploadFileSelected: React.PropTypes.func, onFilesPasted: React.PropTypes.func,
// attempts to confirm currently selected completion, returns whether actually confirmed // attempts to confirm currently selected completion, returns whether actually confirmed
tryComplete: React.PropTypes.func, tryComplete: React.PropTypes.func,

View file

@ -20,6 +20,7 @@ var SlashCommands = require("../../../SlashCommands");
var Modal = require("../../../Modal"); var Modal = require("../../../Modal");
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry; var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
var sdk = require('../../../index'); var sdk = require('../../../index');
import { _t } from '../../../languageHandler';
import UserSettingsStore from "../../../UserSettingsStore"; import UserSettingsStore from "../../../UserSettingsStore";
var dis = require("../../../dispatcher"); var dis = require("../../../dispatcher");
@ -69,6 +70,9 @@ export default React.createClass({
// The text to use a placeholder in the input box // The text to use a placeholder in the input box
placeholder: React.PropTypes.string.isRequired, placeholder: React.PropTypes.string.isRequired,
// callback to handle files pasted into the composer
onFilesPasted: React.PropTypes.func,
}, },
componentWillMount: function() { componentWillMount: function() {
@ -291,8 +295,8 @@ export default React.createClass({
else { else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Unknown command", title: _t("Unknown command"),
description: "Usage: /markdown on|off" description: _t("Usage") + ": /markdown on|off",
}); });
} }
return; return;
@ -311,8 +315,8 @@ export default React.createClass({
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") + "."),
}); });
}); });
} }
@ -320,8 +324,8 @@ export default React.createClass({
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; return;
@ -439,10 +443,27 @@ export default React.createClass({
this.refs.textarea.focus(); this.refs.textarea.focus();
}, },
_onPaste: function(ev) {
const items = ev.clipboardData.items;
const files = [];
for (const item of items) {
if (item.kind === 'file') {
files.push(item.getAsFile());
}
}
if (files.length && this.props.onFilesPasted) {
this.props.onFilesPasted(files);
return true;
}
return false;
},
render: function() { render: function() {
return ( return (
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }> <div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
<textarea autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder={this.props.placeholder} /> <textarea autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder={this.props.placeholder}
onPaste={this._onPaste}
/>
</div> </div>
); );
} }

View file

@ -24,6 +24,8 @@ var sdk = require('../../../index');
var Velociraptor = require('../../../Velociraptor'); var Velociraptor = require('../../../Velociraptor');
require('../../../VelocityBounce'); require('../../../VelocityBounce');
import DateUtils from '../../../DateUtils';
var bounce = false; var bounce = false;
try { try {
if (global.localStorage) { if (global.localStorage) {
@ -63,9 +65,6 @@ module.exports = React.createClass({
// Timestamp when the receipt was read // Timestamp when the receipt was read
timestamp: React.PropTypes.number, timestamp: React.PropTypes.number,
// True to show the full date/time rather than just the time
showFullTimestamp: React.PropTypes.bool,
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -170,16 +169,8 @@ module.exports = React.createClass({
let title; let title;
if (this.props.timestamp) { if (this.props.timestamp) {
const prefix = "Seen by " + this.props.member.userId + " at "; title = "Seen by " + this.props.member.userId + " at " +
let ts = new Date(this.props.timestamp); DateUtils.formatDate(new Date(this.props.timestamp));
if (this.props.showFullTimestamp) {
// "15/12/2016, 7:05:45 PM (@alice:matrix.org)"
title = prefix + ts.toLocaleString();
}
else {
// "7:05:45 PM (@alice:matrix.org)"
title = prefix + ts.toLocaleTimeString();
}
} }
return ( return (

View file

@ -17,7 +17,9 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); var React = require('react');
var classNames = require('classnames');
var sdk = require('../../../index'); var sdk = require('../../../index');
import { _t } from '../../../languageHandler';
var MatrixClientPeg = require('../../../MatrixClientPeg'); var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require("../../../Modal"); var Modal = require("../../../Modal");
var dis = require("../../../dispatcher"); var dis = require("../../../dispatcher");
@ -39,6 +41,7 @@ module.exports = React.createClass({
oobData: React.PropTypes.object, oobData: React.PropTypes.object,
editing: React.PropTypes.bool, editing: React.PropTypes.bool,
saving: React.PropTypes.bool, saving: React.PropTypes.bool,
inRoom: React.PropTypes.bool,
collapsedRhs: React.PropTypes.bool, collapsedRhs: React.PropTypes.bool,
onSettingsClick: React.PropTypes.func, onSettingsClick: React.PropTypes.func,
onSaveClick: React.PropTypes.func, onSaveClick: React.PropTypes.func,
@ -49,7 +52,7 @@ module.exports = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
editing: false, editing: false,
onSettingsClick: function() {}, inRoom: false,
onSaveClick: function() {}, onSaveClick: function() {},
}; };
}, },
@ -117,8 +120,8 @@ module.exports = React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to set avatar: " + errMsg); console.error("Failed to set avatar: " + errMsg);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: _t("Error"),
description: "Failed to set avatar.", description: _t("Failed to set avatar."),
}); });
}).done(); }).done();
}, },
@ -186,6 +189,9 @@ module.exports = React.createClass({
); );
save_button = <AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</AccessibleButton>; save_button = <AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</AccessibleButton>;
}
if (this.props.onCancelClick) {
cancel_button = <CancelButton onClick={this.props.onCancelClick}/>; cancel_button = <CancelButton onClick={this.props.onCancelClick}/>;
} }
@ -203,7 +209,7 @@ module.exports = React.createClass({
// don't display the search count until the search completes and // don't display the search count until the search completes and
// gives us a valid (possibly zero) searchCount. // gives us a valid (possibly zero) searchCount.
if (this.props.searchInfo && this.props.searchInfo.searchCount !== undefined && this.props.searchInfo.searchCount !== null) { if (this.props.searchInfo && this.props.searchInfo.searchCount !== undefined && this.props.searchInfo.searchCount !== null) {
searchStatus = <div className="mx_RoomHeader_searchStatus">&nbsp;(~{ this.props.searchInfo.searchCount } results)</div>; searchStatus = <div className="mx_RoomHeader_searchStatus">&nbsp;{ _t("(~%(searchCount)s results)", { searchCount: this.props.searchInfo.searchCount }) }</div>;
} }
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'... // XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
@ -218,17 +224,17 @@ module.exports = React.createClass({
} }
} }
var roomName = 'Join Room'; var roomName = _t("Join Room");
if (this.props.oobData && this.props.oobData.name) { if (this.props.oobData && this.props.oobData.name) {
roomName = this.props.oobData.name; roomName = this.props.oobData.name;
} else if (this.props.room) { } else if (this.props.room) {
roomName = this.props.room.name; roomName = this.props.room.name;
} }
const emojiTextClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
name = name =
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}> <div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
<EmojiText element="div" className={ "mx_RoomHeader_nametext " + (settingsHint ? "mx_RoomHeader_settingsHint" : "") } title={ roomName }>{roomName}</EmojiText> <EmojiText element="div" className={emojiTextClasses} title={roomName}>{ roomName }</EmojiText>
{ searchStatus } { searchStatus }
</div>; </div>;
} }
@ -259,7 +265,7 @@ module.exports = React.createClass({
<div className="mx_RoomHeader_avatarPicker_edit"> <div className="mx_RoomHeader_avatarPicker_edit">
<label htmlFor="avatarInput" ref="file_label"> <label htmlFor="avatarInput" ref="file_label">
<img src="img/camera.svg" <img src="img/camera.svg"
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 }/>
@ -294,15 +300,23 @@ module.exports = React.createClass({
var forget_button; var forget_button;
if (this.props.onForgetClick) { if (this.props.onForgetClick) {
forget_button = forget_button =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title="Forget room"> <AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={ _t("Forget room") }>
<TintableSvg src="img/leave.svg" width="26" height="20"/> <TintableSvg src="img/leave.svg" width="26" height="20"/>
</AccessibleButton>; </AccessibleButton>;
} }
let search_button;
if (this.props.onSearchClick && this.props.inRoom) {
search_button =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={ _t("Search") }>
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
</AccessibleButton>;
}
var rightPanel_buttons; var rightPanel_buttons;
if (this.props.collapsedRhs) { if (this.props.collapsedRhs) {
rightPanel_buttons = rightPanel_buttons =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="Show panel"> <AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={ _t('Show panel') }>
<TintableSvg src="img/maximise.svg" width="10" height="16"/> <TintableSvg src="img/maximise.svg" width="10" height="16"/>
</AccessibleButton>; </AccessibleButton>;
} }
@ -313,9 +327,7 @@ module.exports = React.createClass({
<div className="mx_RoomHeader_rightRow"> <div className="mx_RoomHeader_rightRow">
{ settings_button } { settings_button }
{ forget_button } { forget_button }
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search"> { search_button }
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
</AccessibleButton>
{ rightPanel_buttons } { rightPanel_buttons }
</div>; </div>;
} }

View file

@ -17,17 +17,18 @@ limitations under the License.
'use strict'; 'use strict';
var React = require("react"); var React = require("react");
var ReactDOM = require("react-dom"); var ReactDOM = require("react-dom");
import { _t } from '../../../languageHandler';
var GeminiScrollbar = require('react-gemini-scrollbar'); var GeminiScrollbar = require('react-gemini-scrollbar');
var MatrixClientPeg = require("../../../MatrixClientPeg"); var MatrixClientPeg = require("../../../MatrixClientPeg");
var CallHandler = require('../../../CallHandler'); var CallHandler = require('../../../CallHandler');
var RoomListSorter = require("../../../RoomListSorter"); var RoomListSorter = require("../../../RoomListSorter");
var Unread = require('../../../Unread');
var dis = require("../../../dispatcher"); var dis = require("../../../dispatcher");
var sdk = require('../../../index'); var sdk = require('../../../index');
var rate_limited_func = require('../../../ratelimitedfunc'); var rate_limited_func = require('../../../ratelimitedfunc');
var Rooms = require('../../../Rooms'); var Rooms = require('../../../Rooms');
import DMRoomMap from '../../../utils/DMRoomMap'; import DMRoomMap from '../../../utils/DMRoomMap';
var Receipt = require('../../../utils/Receipt'); var Receipt = require('../../../utils/Receipt');
var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
var HIDE_CONFERENCE_CHANS = true; var HIDE_CONFERENCE_CHANS = true;
@ -37,19 +38,10 @@ module.exports = React.createClass({
propTypes: { propTypes: {
ConferenceHandler: React.PropTypes.any, ConferenceHandler: React.PropTypes.any,
collapsed: React.PropTypes.bool.isRequired, collapsed: React.PropTypes.bool.isRequired,
selectedRoom: React.PropTypes.string, currentRoom: React.PropTypes.string,
searchFilter: React.PropTypes.string, searchFilter: React.PropTypes.string,
}, },
shouldComponentUpdate: function(nextProps, nextState) {
if (nextProps.collapsed !== this.props.collapsed) return true;
if (nextProps.searchFilter !== this.props.searchFilter) return true;
if (nextState.lists !== this.state.lists ||
nextState.isLoadingLeftRooms !== this.state.isLoadingLeftRooms ||
nextState.incomingCall !== this.state.incomingCall) return true;
return false;
},
getInitialState: function() { getInitialState: function() {
return { return {
isLoadingLeftRooms: false, isLoadingLeftRooms: false,
@ -59,6 +51,8 @@ module.exports = React.createClass({
}, },
componentWillMount: function() { componentWillMount: function() {
this.mounted = false;
var cli = MatrixClientPeg.get(); var cli = MatrixClientPeg.get();
cli.on("Room", this.onRoom); cli.on("Room", this.onRoom);
cli.on("deleteRoom", this.onDeleteRoom); cli.on("deleteRoom", this.onDeleteRoom);
@ -66,46 +60,23 @@ module.exports = React.createClass({
cli.on("Room.name", this.onRoomName); cli.on("Room.name", this.onRoomName);
cli.on("Room.tags", this.onRoomTags); cli.on("Room.tags", this.onRoomTags);
cli.on("Room.receipt", this.onRoomReceipt); cli.on("Room.receipt", this.onRoomReceipt);
cli.on("RoomState.members", this.onRoomStateMember); cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName); cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("accountData", this.onAccountData); cli.on("accountData", this.onAccountData);
// lookup for which lists a given roomId is currently in.
this.listsForRoomId = {};
var s = this.getRoomLists(); var s = this.getRoomLists();
this.setState(s); this.setState(s);
// order of the sublists
//this.listOrder = [];
// loop count to stop a stack overflow if the user keeps waggling the
// mouse for >30s in a row, or if running under mocha
this._delayedRefreshRoomListLoopCount = 0
}, },
componentDidMount: function() { componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
// Initialise the stickyHeaders when the component is created // Initialise the stickyHeaders when the component is created
this._updateStickyHeaders(true); this._updateStickyHeaders(true);
this.mounted = true;
}, },
componentWillReceiveProps: function(nextProps) { componentDidUpdate: function() {
// short-circuit react when the room changes
// to avoid rerendering all the sublists everywhere
if (nextProps.selectedRoom !== this.props.selectedRoom) {
if (this.props.selectedRoom) {
constantTimeDispatcher.dispatch(
"RoomTile.select", this.props.selectedRoom, {}
);
}
constantTimeDispatcher.dispatch(
"RoomTile.select", nextProps.selectedRoom, { selected: true }
);
}
},
componentDidUpdate: function(prevProps, prevState) {
// Reinitialise the stickyHeaders when the component is updated // Reinitialise the stickyHeaders when the component is updated
this._updateStickyHeaders(true); this._updateStickyHeaders(true);
this._repositionIncomingCallBox(undefined, false); this._repositionIncomingCallBox(undefined, false);
@ -131,29 +102,17 @@ module.exports = React.createClass({
} }
break; break;
case 'on_room_read': case 'on_room_read':
// poke the right RoomTile to refresh, using the constantTimeDispatcher // Force an update because the notif count state is too deep to cause
// to avoid each and every RoomTile registering to the 'on_room_read' event // an update. This forces the local echo of reading notifs to be
// XXX: if we like the constantTimeDispatcher we might want to dispatch // reflected by the RoomTiles.
// directly from TimelinePanel rather than needlessly bouncing via here. this.forceUpdate();
constantTimeDispatcher.dispatch(
"RoomTile.refresh", payload.room.roomId, {}
);
// also have to poke the right list(s)
var lists = this.listsForRoomId[payload.room.roomId];
if (lists) {
lists.forEach(list=>{
constantTimeDispatcher.dispatch(
"RoomSubList.refreshHeader", list, { room: payload.room }
);
});
}
break; break;
} }
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
this.mounted = false;
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom); MatrixClientPeg.get().removeListener("Room", this.onRoom);
@ -162,7 +121,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags); MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags);
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData); MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
} }
@ -171,14 +130,10 @@ module.exports = React.createClass({
}, },
onRoom: function(room) { onRoom: function(room) {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList(); this._delayedRefreshRoomList();
}, },
onDeleteRoom: function(roomId) { onDeleteRoom: function(roomId) {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList(); this._delayedRefreshRoomList();
}, },
@ -201,10 +156,6 @@ module.exports = React.createClass({
} }
}, },
_onMouseOver: function(ev) {
this._lastMouseOverTs = Date.now();
},
onSubListHeaderClick: function(isHidden, scrollToPosition) { onSubListHeaderClick: function(isHidden, scrollToPosition) {
// The scroll area has expanded or contracted, so re-calculate sticky headers positions // The scroll area has expanded or contracted, so re-calculate sticky headers positions
this._updateStickyHeaders(true, scrollToPosition); this._updateStickyHeaders(true, scrollToPosition);
@ -214,98 +165,41 @@ module.exports = React.createClass({
if (toStartOfTimeline) return; if (toStartOfTimeline) return;
if (!room) return; if (!room) return;
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
this._delayedRefreshRoomList();
// rather than regenerate our full roomlists, which is very heavy, we poke the
// correct sublists to just re-sort themselves. This isn't enormously reacty,
// but is much faster than the default react reconciler, or having to do voodoo
// with shouldComponentUpdate and a pleaseRefresh property or similar.
var lists = this.listsForRoomId[room.roomId];
if (lists) {
lists.forEach(list=>{
constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room });
});
}
// we have to explicitly hit the roomtile which just changed
constantTimeDispatcher.dispatch(
"RoomTile.refresh", room.roomId, {}
);
}, },
onRoomReceipt: function(receiptEvent, room) { onRoomReceipt: function(receiptEvent, room) {
// because if we read a notification, it will affect notification count // because if we read a notification, it will affect notification count
// only bother updating if there's a receipt from us // only bother updating if there's a receipt from us
if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) { if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) {
var lists = this.listsForRoomId[room.roomId]; this._delayedRefreshRoomList();
if (lists) {
lists.forEach(list=>{
constantTimeDispatcher.dispatch(
"RoomSubList.refreshHeader", list, { room: room }
);
});
}
// we have to explicitly hit the roomtile which just changed
constantTimeDispatcher.dispatch(
"RoomTile.refresh", room.roomId, {}
);
} }
}, },
onRoomName: function(room) { onRoomName: function(room) {
constantTimeDispatcher.dispatch(
"RoomTile.refresh", room.roomId, {}
);
},
onRoomTags: function(event, room) {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList(); this._delayedRefreshRoomList();
}, },
onRoomStateMember: function(ev, state, member) { onRoomTags: function(event, room) {
if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId && this._delayedRefreshRoomList();
ev.getPrevContent() && ev.getPrevContent().membership === "invite") },
{
this._delayedRefreshRoomList(); onRoomStateEvents: function(ev, state) {
} this._delayedRefreshRoomList();
else {
constantTimeDispatcher.dispatch(
"RoomTile.refresh", member.roomId, {}
);
}
}, },
onRoomMemberName: function(ev, member) { onRoomMemberName: function(ev, member) {
constantTimeDispatcher.dispatch( this._delayedRefreshRoomList();
"RoomTile.refresh", member.roomId, {}
);
}, },
onAccountData: function(ev) { onAccountData: function(ev) {
if (ev.getType() == 'm.direct') { if (ev.getType() == 'm.direct') {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList(); this._delayedRefreshRoomList();
} }
else if (ev.getType() == 'm.push_rules') {
this._delayedRefreshRoomList();
}
}, },
_delayedRefreshRoomList: new rate_limited_func(function() { _delayedRefreshRoomList: new rate_limited_func(function() {
// if the mouse has been moving over the RoomList in the last 500ms this.refreshRoomList();
// then delay the refresh further to avoid bouncing around under the
// cursor
if (Date.now() - this._lastMouseOverTs > 500 || this._delayedRefreshRoomListLoopCount > 60) {
this.refreshRoomList();
this._delayedRefreshRoomListLoopCount = 0;
}
else {
this._delayedRefreshRoomListLoopCount++;
this._delayedRefreshRoomList();
}
}, 500), }, 500),
refreshRoomList: function() { refreshRoomList: function() {
@ -313,12 +207,14 @@ module.exports = React.createClass({
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs)) // (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
// ); // );
// TODO: ideally we'd calculate this once at start, and then maintain // TODO: rather than bluntly regenerating and re-sorting everything
// any changes to it incrementally, updating the appropriate sublists // every time we see any kind of room change from the JS SDK
// as needed. // we could do incremental updates on our copy of the state
// Alternatively we'd do something magical with Immutable.js or similar. // based on the room which has actually changed. This would stop
// us re-rendering all the sublists every time anything changes anywhere
// in the state of the client.
this.setState(this.getRoomLists()); this.setState(this.getRoomLists());
// this._lastRefreshRoomListTs = Date.now(); // this._lastRefreshRoomListTs = Date.now();
}, },
@ -333,26 +229,18 @@ module.exports = React.createClass({
s.lists["m.lowpriority"] = []; s.lists["m.lowpriority"] = [];
s.lists["im.vector.fake.archived"] = []; s.lists["im.vector.fake.archived"] = [];
this.listsForRoomId = {};
var otherTagNames = {};
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
MatrixClientPeg.get().getRooms().forEach(function(room) { MatrixClientPeg.get().getRooms().forEach(function(room) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId); const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (!me) return; if (!me) return;
// console.log("room = " + room.name + ", me.membership = " + me.membership + // console.log("room = " + room.name + ", me.membership = " + me.membership +
// ", sender = " + me.events.member.getSender() + // ", sender = " + me.events.member.getSender() +
// ", target = " + me.events.member.getStateKey() + // ", target = " + me.events.member.getStateKey() +
// ", prevMembership = " + me.events.member.getPrevContent().membership); // ", prevMembership = " + me.events.member.getPrevContent().membership);
if (!self.listsForRoomId[room.roomId]) {
self.listsForRoomId[room.roomId] = [];
}
if (me.membership == "invite") { if (me.membership == "invite") {
self.listsForRoomId[room.roomId].push("im.vector.fake.invite");
s.lists["im.vector.fake.invite"].push(room); s.lists["im.vector.fake.invite"].push(room);
} }
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) { else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
@ -363,27 +251,23 @@ module.exports = React.createClass({
{ {
// Used to split rooms via tags // Used to split rooms via tags
var tagNames = Object.keys(room.tags); var tagNames = Object.keys(room.tags);
if (tagNames.length) { if (tagNames.length) {
for (var i = 0; i < tagNames.length; i++) { for (var i = 0; i < tagNames.length; i++) {
var tagName = tagNames[i]; var tagName = tagNames[i];
s.lists[tagName] = s.lists[tagName] || []; s.lists[tagName] = s.lists[tagName] || [];
s.lists[tagName].push(room); s.lists[tagNames[i]].push(room);
self.listsForRoomId[room.roomId].push(tagName);
otherTagNames[tagName] = 1;
} }
} }
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged) // "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
s.lists["im.vector.fake.direct"].push(room); s.lists["im.vector.fake.direct"].push(room);
} }
else { else {
self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
s.lists["im.vector.fake.recent"].push(room); s.lists["im.vector.fake.recent"].push(room);
} }
} }
else if (me.membership === "leave") { else if (me.membership === "leave") {
self.listsForRoomId[room.roomId].push("im.vector.fake.archived");
s.lists["im.vector.fake.archived"].push(room); s.lists["im.vector.fake.archived"].push(room);
} }
else { else {
@ -404,10 +288,8 @@ module.exports = React.createClass({
const me = room.getMember(MatrixClientPeg.get().credentials.userId); const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me && Rooms.looksLikeDirectMessageRoom(room, me)) { if (me && Rooms.looksLikeDirectMessageRoom(room, me)) {
self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
s.lists["im.vector.fake.direct"].push(room); s.lists["im.vector.fake.direct"].push(room);
} else { } else {
self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
s.lists["im.vector.fake.recent"].push(room); s.lists["im.vector.fake.recent"].push(room);
} }
} }
@ -424,8 +306,6 @@ module.exports = React.createClass({
newMDirectEvent[otherPerson.userId] = roomList; newMDirectEvent[otherPerson.userId] = roomList;
} }
console.warn("Resetting room DM state to be " + JSON.stringify(newMDirectEvent));
// if this fails, fine, we'll just do the same thing next time we get the room lists // if this fails, fine, we'll just do the same thing next time we get the room lists
MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done(); MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done();
} }
@ -434,32 +314,19 @@ module.exports = React.createClass({
// we actually apply the sorting to this when receiving the prop in RoomSubLists. // we actually apply the sorting to this when receiving the prop in RoomSubLists.
// we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
/*
this.listOrder = [
"im.vector.fake.invite",
"m.favourite",
"im.vector.fake.recent",
"im.vector.fake.direct",
Object.keys(otherTagNames).filter(tagName=>{
return (!tagName.match(/^m\.(favourite|lowpriority)$/));
}).sort(),
"m.lowpriority",
"im.vector.fake.archived"
];
*/
return s; return s;
}, },
_getScrollNode: function() { _getScrollNode: function() {
if (!this.mounted) return null;
var panel = ReactDOM.findDOMNode(this); var panel = ReactDOM.findDOMNode(this);
if (!panel) return null; if (!panel) return null;
// empirically, if we have gm-prevented for some reason, the scroll node if (panel.classList.contains('gm-prevented')) {
// is still the 3rd child (i.e. the view child). This looks to be due return panel;
// to vdh's improved resize updater logic...? } else {
return panel.children[2]; // XXX: Fragile! return panel.children[2]; // XXX: Fragile!
}
}, },
_whenScrolling: function(e) { _whenScrolling: function(e) {
@ -479,6 +346,7 @@ module.exports = React.createClass({
var incomingCallBox = document.getElementById("incomingCallBox"); var incomingCallBox = document.getElementById("incomingCallBox");
if (incomingCallBox && incomingCallBox.parentElement) { if (incomingCallBox && incomingCallBox.parentElement) {
var scrollArea = this._getScrollNode(); var scrollArea = this._getScrollNode();
if (!scrollArea) return;
// Use the offset of the top of the scroll area from the window // Use the offset of the top of the scroll area from the window
// as this is used to calculate the CSS fixed top position for the stickies // as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset; var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
@ -502,10 +370,11 @@ module.exports = React.createClass({
// properly through React // properly through React
_initAndPositionStickyHeaders: function(initialise, scrollToPosition) { _initAndPositionStickyHeaders: function(initialise, scrollToPosition) {
var scrollArea = this._getScrollNode(); var scrollArea = this._getScrollNode();
if (!scrollArea) return;
// Use the offset of the top of the scroll area from the window // Use the offset of the top of the scroll area from the window
// as this is used to calculate the CSS fixed top position for the stickies // as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset; var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
// Use the offset of the top of the component from the window // Use the offset of the top of the componet from the window
// as this is used to calculate the CSS fixed top position for the stickies // as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height; var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
@ -602,75 +471,72 @@ module.exports = React.createClass({
render: function() { render: function() {
var RoomSubList = sdk.getComponent('structures.RoomSubList'); var RoomSubList = sdk.getComponent('structures.RoomSubList');
var self = this; var self = this;
return ( return (
<GeminiScrollbar className="mx_RoomList_scrollbar" <GeminiScrollbar className="mx_RoomList_scrollbar"
autoshow={true} onScroll={ self._whenScrolling } onResize={ self._whenScrolling } ref="gemscroll"> autoshow={true} onScroll={ self._whenScrolling } ref="gemscroll">
<div className="mx_RoomList" onMouseOver={ this._onMouseOver }> <div className="mx_RoomList">
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] } <RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
label="Invites" label={ _t('Invites') }
tagName="im.vector.fake.invite"
editable={ false } editable={ false }
order="recent" order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter } searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick } onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } /> onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['m.favourite'] } <RoomSubList list={ self.state.lists['m.favourite'] }
label="Favourites" label={ _t('Favourites') }
tagName="m.favourite" tagName="m.favourite"
verb="favourite" verb={ _t('to favourite') }
editable={ true } editable={ true }
order="manual" order="manual"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter } searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick } onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } /> onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['im.vector.fake.direct'] } <RoomSubList list={ self.state.lists['im.vector.fake.direct'] }
label="People" label={ _t('People') }
tagName="im.vector.fake.direct" tagName="im.vector.fake.direct"
verb="tag direct chat" verb={ _t('to tag direct chat') }
editable={ true } editable={ true }
order="recent" order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
alwaysShowHeader={ true } alwaysShowHeader={ true }
searchFilter={ self.props.searchFilter } searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick } onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } /> onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] } <RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
label="Rooms" label={ _t('Rooms') }
tagName="im.vector.fake.recent"
editable={ true } editable={ true }
verb="restore" verb={ _t('to restore') }
order="recent" order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter } searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick } onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } /> onShowMoreRooms={ self.onShowMoreRooms } />
{ Object.keys(self.state.lists).sort().map(function(tagName) { { Object.keys(self.state.lists).map(function(tagName) {
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
return <RoomSubList list={ self.state.lists[tagName] } return <RoomSubList list={ self.state.lists[tagName] }
key={ tagName } key={ tagName }
label={ tagName } label={ tagName }
tagName={ tagName } tagName={ tagName }
verb={ "tag as " + tagName } verb={ _t('to tag as %(tagName)s', {tagName: tagName}) }
editable={ true } editable={ true }
order="manual" order="manual"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter } searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick } onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />; onShowMoreRooms={ self.onShowMoreRooms } />;
@ -679,25 +545,24 @@ module.exports = React.createClass({
}) } }) }
<RoomSubList list={ self.state.lists['m.lowpriority'] } <RoomSubList list={ self.state.lists['m.lowpriority'] }
label="Low priority" label={ _t('Low priority') }
tagName="m.lowpriority" tagName="m.lowpriority"
verb="demote" verb={ _t('to demote') }
editable={ true } editable={ true }
order="recent" order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter } searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick } onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } /> onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] } <RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
label="Historical" label={ _t('Historical') }
tagName="im.vector.fake.archived"
editable={ false } editable={ false }
order="recent" order="recent"
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom } selectedRoom={ self.props.selectedRoom }
collapsed={ self.props.collapsed }
alwaysShowHeader={ true } alwaysShowHeader={ true }
startAsHidden={ true } startAsHidden={ true }
showSpinner={ self.state.isLoadingLeftRooms } showSpinner={ self.state.isLoadingLeftRooms }

View file

@ -21,6 +21,8 @@ var React = require('react');
var sdk = require('../../../index'); var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg'); var MatrixClientPeg = require('../../../MatrixClientPeg');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomPreviewBar', displayName: 'RoomPreviewBar',
@ -47,7 +49,7 @@ module.exports = React.createClass({
// The alias that was used to access this room, if appropriate // The alias that was used to access this room, if appropriate
// If given, this will be how the room is referred to (eg. // If given, this will be how the room is referred to (eg.
// in error messages). // in error messages).
roomAlias: React.PropTypes.object, roomAlias: React.PropTypes.string,
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -84,7 +86,7 @@ module.exports = React.createClass({
_roomNameElement: function(fallback) { _roomNameElement: function(fallback) {
fallback = fallback || 'a room'; fallback = fallback || 'a room';
const name = this.props.room ? this.props.room.name : (this.props.room_alias || ""); const name = this.props.room ? this.props.room.name : (this.props.room_alias || "");
return name ? <b>{ name }</b> : fallback; return name ? name : fallback;
}, },
render: function() { render: function() {
@ -128,13 +130,14 @@ module.exports = React.createClass({
</div>; </div>;
} }
} }
// TODO: find a way to respect HTML in counterpart!
joinBlock = ( joinBlock = (
<div> <div>
<div className="mx_RoomPreviewBar_invite_text"> <div className="mx_RoomPreviewBar_invite_text">
You have been invited to join this room by <b>{ this.props.inviterName }</b> { _t('You have been invited to join this room by %(inviterName)s', {inviterName: this.props.inviterName}) }
</div> </div>
<div className="mx_RoomPreviewBar_join_text"> <div className="mx_RoomPreviewBar_join_text">
Would you like to <a onClick={ this.props.onJoinClick }>accept</a> or <a onClick={ this.props.onRejectClick }>decline</a> this invitation? { _t('Would you like to') } <a onClick={ this.props.onJoinClick }>{ _t('accept') }</a> { _t('or') } <a onClick={ this.props.onRejectClick }>{ _t('decline') }</a> { _t('this invitation?') }
</div> </div>
{emailMatchBlock} {emailMatchBlock}
</div> </div>
@ -186,8 +189,8 @@ module.exports = React.createClass({
joinBlock = ( joinBlock = (
<div> <div>
<div className="mx_RoomPreviewBar_join_text"> <div className="mx_RoomPreviewBar_join_text">
You are trying to access { name }.<br/> { _t('You are trying to access %(roomName)s', {roomName: name}) }.<br/>
<a onClick={ this.props.onJoinClick }><b>Click here</b></a> to join the discussion! <a onClick={ this.props.onJoinClick }><b>{ _t('Click here') }</b></a> { _t('to join the discussion') }!
</div> </div>
</div> </div>
); );
@ -196,7 +199,7 @@ module.exports = React.createClass({
if (this.props.canPreview) { if (this.props.canPreview) {
previewBlock = ( previewBlock = (
<div className="mx_RoomPreviewBar_preview_text"> <div className="mx_RoomPreviewBar_preview_text">
This is a preview of this room. Room interactions have been disabled. { _t('This is a preview of this room. Room interactions have been disabled') }.
</div> </div>
); );
} }

View file

@ -17,6 +17,7 @@ limitations under the License.
import q from 'q'; import q from 'q';
import React from 'react'; import React from 'react';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import sdk from '../../../index'; import sdk from '../../../index';
@ -56,8 +57,8 @@ const BannedUser = React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to unban: " + err); console.error("Failed to unban: " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: _t('Error'),
description: "Failed to unban", description: _t('Failed to unban'),
}); });
}).done(); }).done();
}, },
@ -70,7 +71,7 @@ const BannedUser = React.createClass({
<AccessibleButton className="mx_RoomSettings_unbanButton" <AccessibleButton className="mx_RoomSettings_unbanButton"
onClick={this._onUnbanClick} onClick={this._onUnbanClick}
> >
Unban { _t('Unban') }
</AccessibleButton> </AccessibleButton>
{this.props.member.userId} {this.props.member.userId}
</li> </li>
@ -400,13 +401,13 @@ module.exports = React.createClass({
var value = ev.target.value; var value = ev.target.value;
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Privacy warning", title: _t('Privacy warning'),
description: description:
<div> <div>
Changes to who can read history will only apply to future messages in this room.<br/> { _t('Changes to who can read history will only apply to future messages in this room') }.<br/>
The visibility of existing history will be unchanged. { _t('The visibility of existing history will be unchanged') }.
</div>, </div>,
button: "Continue", button: _t('Continue'),
onFinished: function(confirmed) { onFinished: function(confirmed) {
if (confirmed) { if (confirmed) {
self.setState({ self.setState({
@ -523,11 +524,11 @@ module.exports = React.createClass({
MatrixClientPeg.get().forget(this.props.room.roomId).done(function() { MatrixClientPeg.get().forget(this.props.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 }),
}); });
}); });
}, },
@ -537,14 +538,14 @@ module.exports = React.createClass({
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>
<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>Once encryption is enabled for a room it <b>cannot</b> be turned off again (for now).</p> <p>{ _t('Once encryption is enabled for a room it cannot be turned off again (for now)') }.</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>
), ),
onFinished: confirm=>{ onFinished: confirm=>{
@ -572,7 +573,7 @@ module.exports = React.createClass({
<input type="checkbox" ref="blacklistUnverified" <input type="checkbox" ref="blacklistUnverified"
defaultChecked={ isGlobalBlacklistUnverified || isRoomBlacklistUnverified } defaultChecked={ isGlobalBlacklistUnverified || isRoomBlacklistUnverified }
disabled={ isGlobalBlacklistUnverified || (this.refs.encrypt && !this.refs.encrypt.checked) }/> disabled={ isGlobalBlacklistUnverified || (this.refs.encrypt && !this.refs.encrypt.checked) }/>
Never send encrypted messages to unverified devices in this room from this device. { _t('Never send encrypted messages to unverified devices in this room from this device') }.
</label>; </label>;
if (!isEncrypted && if (!isEncrypted &&
@ -582,7 +583,7 @@ module.exports = React.createClass({
<label> <label>
<input type="checkbox" ref="encrypt" onClick={ this.onEnableEncryptionClick }/> <input type="checkbox" ref="encrypt" onClick={ this.onEnableEncryptionClick }/>
<img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" /> <img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
Enable encryption (warning: cannot be disabled again!) { _t('Enable encryption') } { _t('(warning: cannot be disabled again!)') }
</label> </label>
{ settings } { settings }
</div> </div>
@ -596,7 +597,7 @@ module.exports = React.createClass({
? <img className="mx_RoomSettings_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" /> ? <img className="mx_RoomSettings_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" />
: <img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" /> : <img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
} }
Encryption is { isEncrypted ? "" : "not " } enabled in this room. { isEncrypted ? "Encryption is enabled in this room" : "Encryption is not enabled in this room" }.
</label> </label>
{ settings } { settings }
</div> </div>
@ -647,12 +648,12 @@ module.exports = React.createClass({
if (Object.keys(user_levels).length) { if (Object.keys(user_levels).length) {
userLevelsSection = userLevelsSection =
<div> <div>
<h3>Privileged Users</h3> <h3>{ _t('Privileged Users') }</h3>
<ul className="mx_RoomSettings_userLevels"> <ul className="mx_RoomSettings_userLevels">
{Object.keys(user_levels).map(function(user, i) { {Object.keys(user_levels).map(function(user, i) {
return ( return (
<li className="mx_RoomSettings_userLevel" key={user}> <li className="mx_RoomSettings_userLevel" key={user}>
{ user } is a <PowerSelector value={ user_levels[user] } disabled={true}/> { user } { _t('is a') } <PowerSelector value={ user_levels[user] } disabled={true}/>
</li> </li>
); );
})} })}
@ -660,7 +661,7 @@ module.exports = React.createClass({
</div>; </div>;
} }
else { else {
userLevelsSection = <div>No users have specific privileges in this room.</div>; userLevelsSection = <div>{ _t('No users have specific privileges in this room') }.</div>;
} }
var banned = this.props.room.getMembersWithMembership("ban"); var banned = this.props.room.getMembersWithMembership("ban");
@ -668,7 +669,7 @@ module.exports = React.createClass({
if (banned.length) { if (banned.length) {
bannedUsersSection = bannedUsersSection =
<div> <div>
<h3>Banned users</h3> <h3>{ _t('Banned users') }</h3>
<ul className="mx_RoomSettings_banned"> <ul className="mx_RoomSettings_banned">
{banned.map(function(member) { {banned.map(function(member) {
return ( return (
@ -683,7 +684,7 @@ module.exports = React.createClass({
if (this._yankValueFromEvent("m.room.create", "m.federate") === false) { if (this._yankValueFromEvent("m.room.create", "m.federate") === false) {
unfederatableSection = ( unfederatableSection = (
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
Ths room is not accessible by remote Matrix servers. { _t('This room is not accessible by remote Matrix servers') }.
</div> </div>
); );
} }
@ -694,14 +695,14 @@ module.exports = React.createClass({
if (myMember.membership === "join") { if (myMember.membership === "join") {
leaveButton = ( leaveButton = (
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={ this.onLeaveClick }> <AccessibleButton className="mx_RoomSettings_leaveButton" onClick={ this.onLeaveClick }>
Leave room { _t('Leave room') }
</AccessibleButton> </AccessibleButton>
); );
} }
else if (myMember.membership === "leave") { else if (myMember.membership === "leave") {
leaveButton = ( leaveButton = (
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={ this.onForgetClick }> <AccessibleButton className="mx_RoomSettings_leaveButton" onClick={ this.onForgetClick }>
Forget room { _t('Forget room') }
</AccessibleButton> </AccessibleButton>
); );
} }
@ -711,8 +712,8 @@ module.exports = React.createClass({
// TODO: support editing custom user_levels // TODO: support editing custom user_levels
var tags = [ var tags = [
{ name: "m.favourite", label: "Favourite", ref: "tag_favourite" }, { name: "m.favourite", label: _t('Favourite'), ref: "tag_favourite" },
{ name: "m.lowpriority", label: "Low priority", ref: "tag_lowpriority" }, { name: "m.lowpriority", label: _t('Low priority'), ref: "tag_lowpriority" },
]; ];
Object.keys(this.state.tags).sort().forEach(function(tagName) { Object.keys(this.state.tags).sort().forEach(function(tagName) {
@ -753,7 +754,7 @@ module.exports = React.createClass({
if (this.state.join_rule === "public" && aliasCount == 0) { if (this.state.join_rule === "public" && aliasCount == 0) {
addressWarning = addressWarning =
<div className="mx_RoomSettings_warning"> <div className="mx_RoomSettings_warning">
To link to a room it must have <a href="#addresses">an address</a>. { _t('To link to a room it must have') } <a href="#addresses"> { _t('an address') }</a>.
</div>; </div>;
} }
@ -761,10 +762,10 @@ module.exports = React.createClass({
if (this.state.join_rule !== "public" && this.state.guest_access === "forbidden") { if (this.state.join_rule !== "public" && this.state.guest_access === "forbidden") {
inviteGuestWarning = inviteGuestWarning =
<div className="mx_RoomSettings_warning"> <div className="mx_RoomSettings_warning">
Guests cannot join this room even if explicitly invited. <a href="#" onClick={ (e) => { { _t('Guests cannot join this room even if explicitly invited.') } <a href="#" onClick={ (e) => {
this.setState({ join_rule: "invite", guest_access: "can_join" }); this.setState({ join_rule: "invite", guest_access: "can_join" });
e.preventDefault(); e.preventDefault();
}}>Click here to fix</a>. }}>{ _t('Click here to fix') }</a>.
</div>; </div>;
} }
@ -776,7 +777,7 @@ module.exports = React.createClass({
console.error(this.state.scalar_error); console.error(this.state.scalar_error);
integrationsError = ( integrationsError = (
<span className="mx_RoomSettings_integrationsButton_errorPopup"> <span className="mx_RoomSettings_integrationsButton_errorPopup">
Could not connect to the integration server { _t('Could not connect to the integration server') }
</span> </span>
); );
} }
@ -784,7 +785,7 @@ module.exports = React.createClass({
if (this.scalarClient.hasCredentials()) { if (this.scalarClient.hasCredentials()) {
integrationsButton = ( integrationsButton = (
<div className="mx_RoomSettings_integrationsButton" onClick={ this.onManageIntegrations }> <div className="mx_RoomSettings_integrationsButton" onClick={ this.onManageIntegrations }>
Manage Integrations { _t('Manage Integrations') }
</div> </div>
); );
} else if (this.state.scalar_error) { } else if (this.state.scalar_error) {
@ -797,7 +798,7 @@ module.exports = React.createClass({
} else { } else {
integrationsButton = ( integrationsButton = (
<div className="mx_RoomSettings_integrationsButton" style={{opacity: 0.5}}> <div className="mx_RoomSettings_integrationsButton" style={{opacity: 0.5}}>
Manage Integrations { _t('Manage Integrations') }
</div> </div>
); );
} }
@ -813,28 +814,28 @@ module.exports = React.createClass({
<div className="mx_RoomSettings_toggles"> <div className="mx_RoomSettings_toggles">
<div className="mx_RoomSettings_settings"> <div className="mx_RoomSettings_settings">
<h3>Who can access this room?</h3> <h3>{ _t('Who can access this room?') }</h3>
{ inviteGuestWarning } { inviteGuestWarning }
<label> <label>
<input type="radio" name="roomVis" value="invite_only" <input type="radio" name="roomVis" value="invite_only"
disabled={ !this.mayChangeRoomAccess() } disabled={ !this.mayChangeRoomAccess() }
onChange={this._onRoomAccessRadioToggle} onChange={this._onRoomAccessRadioToggle}
checked={this.state.join_rule !== "public"}/> checked={this.state.join_rule !== "public"}/>
Only people who have been invited { _t('Only people who have been invited') }
</label> </label>
<label> <label>
<input type="radio" name="roomVis" value="public_no_guests" <input type="radio" name="roomVis" value="public_no_guests"
disabled={ !this.mayChangeRoomAccess() } disabled={ !this.mayChangeRoomAccess() }
onChange={this._onRoomAccessRadioToggle} onChange={this._onRoomAccessRadioToggle}
checked={this.state.join_rule === "public" && this.state.guest_access !== "can_join"}/> checked={this.state.join_rule === "public" && this.state.guest_access !== "can_join"}/>
Anyone who knows the room's link, apart from guests { _t('Anyone who knows the room\'s link, apart from guests') }
</label> </label>
<label> <label>
<input type="radio" name="roomVis" value="public_with_guests" <input type="radio" name="roomVis" value="public_with_guests"
disabled={ !this.mayChangeRoomAccess() } disabled={ !this.mayChangeRoomAccess() }
onChange={this._onRoomAccessRadioToggle} onChange={this._onRoomAccessRadioToggle}
checked={this.state.join_rule === "public" && this.state.guest_access === "can_join"}/> checked={this.state.join_rule === "public" && this.state.guest_access === "can_join"}/>
Anyone who knows the room's link, including guests { _t('Anyone who knows the room\'s link, including guests') }
</label> </label>
{ addressWarning } { addressWarning }
<br/> <br/>
@ -847,7 +848,7 @@ module.exports = React.createClass({
</label> </label>
</div> </div>
<div className="mx_RoomSettings_settings"> <div className="mx_RoomSettings_settings">
<h3>Who can read history?</h3> <h3>{ _t('Who can read history?') }</h3>
<label> <label>
<input type="radio" name="historyVis" value="world_readable" <input type="radio" name="historyVis" value="world_readable"
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) } disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
@ -860,28 +861,28 @@ module.exports = React.createClass({
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) } disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
checked={historyVisibility === "shared"} checked={historyVisibility === "shared"}
onChange={this._onHistoryRadioToggle} /> onChange={this._onHistoryRadioToggle} />
Members only (since the point in time of selecting this option) { _t('Members only') } ({ _t('since the point in time of selecting this option') })
</label> </label>
<label> <label>
<input type="radio" name="historyVis" value="invited" <input type="radio" name="historyVis" value="invited"
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) } disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
checked={historyVisibility === "invited"} checked={historyVisibility === "invited"}
onChange={this._onHistoryRadioToggle} /> onChange={this._onHistoryRadioToggle} />
Members only (since they were invited) { _t('Members only') } ({ _t('since they were invited') })
</label> </label>
<label > <label >
<input type="radio" name="historyVis" value="joined" <input type="radio" name="historyVis" value="joined"
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) } disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
checked={historyVisibility === "joined"} checked={historyVisibility === "joined"}
onChange={this._onHistoryRadioToggle} /> onChange={this._onHistoryRadioToggle} />
Members only (since they joined) { _t('Members only') } ({ _t('since they joined') })
</label> </label>
</div> </div>
</div> </div>
<div> <div>
<h3>Room Colour</h3> <h3>{ _t('Room Colour') }</h3>
<ColorSettings ref="color_settings" room={this.props.room} /> <ColorSettings ref="color_settings" room={this.props.room} />
</div> </div>
@ -899,41 +900,41 @@ module.exports = React.createClass({
<UrlPreviewSettings ref="url_preview_settings" room={this.props.room} /> <UrlPreviewSettings ref="url_preview_settings" room={this.props.room} />
<h3>Permissions</h3> <h3>{ _t('Permissions') }</h3>
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings"> <div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">The default role for new room members is </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('The default role for new room members is') } </span>
<PowerSelector ref="users_default" value={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged}/> <PowerSelector ref="users_default" value={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged}/>
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">To send messages, you must be a </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To send messages') }, { _t('you must be a') } </span>
<PowerSelector ref="events_default" value={send_level} controlled={false} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged}/> <PowerSelector ref="events_default" value={send_level} controlled={false} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged}/>
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">To invite users into the room, you must be a </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To invite users into the room') }, { _t('you must be a') } </span>
<PowerSelector ref="invite" value={invite_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged}/> <PowerSelector ref="invite" value={invite_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged}/>
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">To configure the room, you must be a </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To configure the room') }, { _t('you must be a') } </span>
<PowerSelector ref="state_default" value={state_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged}/> <PowerSelector ref="state_default" value={state_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged}/>
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">To kick users, you must be a </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To kick users') }, { _t('you must be a') } </span>
<PowerSelector ref="kick" value={kick_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged}/> <PowerSelector ref="kick" value={kick_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged}/>
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">To ban users, you must be a </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To ban users') }, { _t('you must be a') } </span>
<PowerSelector ref="ban" value={ban_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged}/> <PowerSelector ref="ban" value={ban_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged}/>
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">To remove messages, you must be a </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To remove other users\' messages') }, { _t('you must be a') } </span>
<PowerSelector ref="redact" value={redact_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged}/> <PowerSelector ref="redact" value={redact_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged}/>
</div> </div>
{Object.keys(events_levels).map(function(event_type, i) { {Object.keys(events_levels).map(function(event_type, i) {
return ( return (
<div className="mx_RoomSettings_powerLevel" key={event_type}> <div className="mx_RoomSettings_powerLevel" key={event_type}>
<span className="mx_RoomSettings_powerLevelKey">To send events of type <code>{ event_type }</code>, you must be a </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To send events of type') } <code>{ event_type }</code>, { _t('you must be a') } </span>
<PowerSelector value={ events_levels[event_type] } controlled={false} disabled={true} onChange={self.onPowerLevelsChanged}/> <PowerSelector value={ events_levels[event_type] } controlled={false} disabled={true} onChange={self.onPowerLevelsChanged}/>
</div> </div>
); );
@ -946,9 +947,9 @@ module.exports = React.createClass({
{ bannedUsersSection } { bannedUsersSection }
<h3>Advanced</h3> <h3>{ _t('Advanced') }</h3>
<div className="mx_RoomSettings_settings"> <div className="mx_RoomSettings_settings">
This room's internal ID is <code>{ this.props.room.roomId }</code> { _t('This room\'s internal ID is') } <code>{ this.props.room.roomId }</code>
</div> </div>
</div> </div>
); );

View file

@ -27,8 +27,6 @@ var RoomNotifs = require('../../../RoomNotifs');
var FormattingUtils = require('../../../utils/FormattingUtils'); var FormattingUtils = require('../../../utils/FormattingUtils');
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
var UserSettingsStore = require('../../../UserSettingsStore'); var UserSettingsStore = require('../../../UserSettingsStore');
var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
var Unread = require('../../../Unread');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomTile', displayName: 'RoomTile',
@ -38,10 +36,12 @@ module.exports = React.createClass({
connectDropTarget: React.PropTypes.func, connectDropTarget: React.PropTypes.func,
onClick: React.PropTypes.func, onClick: React.PropTypes.func,
isDragging: React.PropTypes.bool, isDragging: React.PropTypes.bool,
selectedRoom: React.PropTypes.string,
room: React.PropTypes.object.isRequired, room: React.PropTypes.object.isRequired,
collapsed: React.PropTypes.bool.isRequired, collapsed: React.PropTypes.bool.isRequired,
selected: React.PropTypes.bool.isRequired,
unread: React.PropTypes.bool.isRequired,
highlight: React.PropTypes.bool.isRequired,
isInvite: React.PropTypes.bool.isRequired, isInvite: React.PropTypes.bool.isRequired,
incomingCall: React.PropTypes.object, incomingCall: React.PropTypes.object,
}, },
@ -54,11 +54,10 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
return({ return({
hover: false, hover : false,
badgeHover: false, badgeHover : false,
menuDisplayed: false, menuDisplayed: false,
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
selected: this.props.room ? (this.props.selectedRoom === this.props.room.roomId) : false,
}); });
}, },
@ -80,32 +79,23 @@ module.exports = React.createClass({
} }
}, },
onAccountData: function(accountDataEvent) {
if (accountDataEvent.getType() == 'm.push_rules') {
this.setState({
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
});
}
},
componentWillMount: function() { componentWillMount: function() {
constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh); MatrixClientPeg.get().on("accountData", this.onAccountData);
constantTimeDispatcher.register("RoomTile.select", this.props.room.roomId, this.onSelect);
this.onRefresh();
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh); var cli = MatrixClientPeg.get();
constantTimeDispatcher.unregister("RoomTile.select", this.props.room.roomId, this.onSelect); if (cli) {
}, MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
}
componentWillReceiveProps: function(nextProps) {
this.onRefresh();
},
onRefresh: function(params) {
this.setState({
unread: Unread.doesRoomHaveUnreadMessages(this.props.room),
highlight: this.props.room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite,
});
},
onSelect: function(params) {
this.setState({
selected: params.selected,
});
}, },
onClick: function(ev) { onClick: function(ev) {
@ -179,13 +169,13 @@ module.exports = React.createClass({
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight"); // var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge(); const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
const mentionBadges = this.state.highlight && this._shouldShowMentionBadge(); const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
const badges = notifBadges || mentionBadges; const badges = notifBadges || mentionBadges;
var classes = classNames({ var classes = classNames({
'mx_RoomTile': true, 'mx_RoomTile': true,
'mx_RoomTile_selected': this.state.selected, 'mx_RoomTile_selected': this.props.selected,
'mx_RoomTile_unread': this.state.unread, 'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_unreadNotify': notifBadges, 'mx_RoomTile_unreadNotify': notifBadges,
'mx_RoomTile_highlight': mentionBadges, 'mx_RoomTile_highlight': mentionBadges,
'mx_RoomTile_invited': (me && me.membership == 'invite'), 'mx_RoomTile_invited': (me && me.membership == 'invite'),
@ -231,7 +221,7 @@ module.exports = React.createClass({
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed, 'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
}); });
if (this.state.selected) { if (this.props.selected) {
let nameSelected = <EmojiText>{name}</EmojiText>; let nameSelected = <EmojiText>{name}</EmojiText>;
label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>; label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>;
@ -265,8 +255,7 @@ module.exports = React.createClass({
let ret = ( let ret = (
<div> { /* Only native elements can be wrapped in a DnD object. */} <div> { /* Only native elements can be wrapped in a DnD object. */}
<AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} <AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className={avatarClasses}> <div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container"> <div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} /> <RoomAvatar room={this.props.room} width={24} height={24} />

View file

@ -60,7 +60,7 @@ module.exports = React.createClass({
} }
} }
return ( return (
<li data-scroll-token={eventId+"+"+j}> <li data-scroll-tokens={eventId+"+"+j}>
{ret} {ret}
</li>); </li>);
}, },

View file

@ -17,6 +17,7 @@ var React = require('react');
var MatrixClientPeg = require("../../../MatrixClientPeg"); var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal"); var Modal = require("../../../Modal");
var sdk = require("../../../index"); var sdk = require("../../../index");
import { _t } from '../../../languageHandler';
var GeminiScrollbar = require('react-gemini-scrollbar'); var GeminiScrollbar = require('react-gemini-scrollbar');
// A list capable of displaying entities which conform to the SearchableEntity // A list capable of displaying entities which conform to the SearchableEntity
@ -25,7 +26,6 @@ var SearchableEntityList = React.createClass({
displayName: 'SearchableEntityList', displayName: 'SearchableEntityList',
propTypes: { propTypes: {
searchPlaceholderText: React.PropTypes.string,
emptyQueryShowsAll: React.PropTypes.bool, emptyQueryShowsAll: React.PropTypes.bool,
showInputBox: React.PropTypes.bool, showInputBox: React.PropTypes.bool,
onQueryChanged: React.PropTypes.func, // fn(inputText) onQueryChanged: React.PropTypes.func, // fn(inputText)
@ -37,7 +37,6 @@ var SearchableEntityList = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
showInputBox: true, showInputBox: true,
searchPlaceholderText: "Search",
entities: [], entities: [],
emptyQueryShowsAll: false, emptyQueryShowsAll: false,
onSubmit: function() {}, onSubmit: function() {},
@ -118,7 +117,9 @@ var SearchableEntityList = React.createClass({
_createOverflowEntity: function(overflowCount, totalCount) { _createOverflowEntity: function(overflowCount, totalCount) {
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} />
@ -137,7 +138,7 @@ var SearchableEntityList = React.createClass({
onChange={this.onQueryChanged} value={this.state.query} onChange={this.onQueryChanged} value={this.state.query}
onFocus= {() => { this.setState({ focused: true }); }} onFocus= {() => { this.setState({ focused: true }); }}
onBlur= {() => { this.setState({ focused: false }); }} onBlur= {() => { this.setState({ focused: false }); }}
placeholder={this.props.searchPlaceholderText} /> placeholder={ _t("Search") } />
</form> </form>
); );
} }

View file

@ -19,6 +19,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import sdk from '../../../index';
// cancel button which is shared between room header and simple room header // cancel button which is shared between room header and simple room header
export function CancelButton(props) { export function CancelButton(props) {
@ -45,6 +46,9 @@ export default React.createClass({
// is the RightPanel collapsed? // is the RightPanel collapsed?
collapsedRhs: React.PropTypes.bool, collapsedRhs: React.PropTypes.bool,
// `src` to a TintableSvg. Optional.
icon: React.PropTypes.string,
}, },
onShowRhsClick: function(ev) { onShowRhsClick: function(ev) {
@ -53,9 +57,17 @@ export default React.createClass({
render: function() { render: function() {
let cancelButton; let cancelButton;
let icon;
if (this.props.onCancelClick) { if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />; cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
} }
if (this.props.icon) {
const TintableSvg = sdk.getComponent('elements.TintableSvg');
icon = <TintableSvg
className="mx_RoomHeader_icon" src={this.props.icon}
width="25" height="25"
/>;
}
let showRhsButton; let showRhsButton;
/* // don't bother cluttering things up with this for now. /* // don't bother cluttering things up with this for now.
@ -73,6 +85,7 @@ export default React.createClass({
<div className="mx_RoomHeader" > <div className="mx_RoomHeader" >
<div className="mx_RoomHeader_wrapper"> <div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_simpleHeader"> <div className="mx_RoomHeader_simpleHeader">
{ icon }
{ this.props.title } { this.props.title }
{ showRhsButton } { showRhsButton }
{ cancelButton } { cancelButton }

View file

@ -18,6 +18,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');
module.exports = React.createClass({ module.exports = React.createClass({
@ -34,11 +35,11 @@ module.exports = React.createClass({
<div className="mx_TopUnreadMessagesBar_scrollUp" <div className="mx_TopUnreadMessagesBar_scrollUp"
onClick={this.props.onScrollUpClick}> onClick={this.props.onScrollUpClick}>
<img src="img/scrollto.svg" width="24" height="24" <img src="img/scrollto.svg" width="24" height="24"
alt="Scroll to unread messages" alt={ _t('Scroll to unread messages') }
title="Scroll to unread messages"/> title={ _t('Scroll to unread messages') }/>
Jump to first unread message. Jump to first unread message.
</div> </div>
<img className="mx_TopUnreadMessagesBar_close" <img className="mx_TopUnreadMessagesBar_close mx_filterFlipColor"
src="img/cancel.svg" width="18" height="18" src="img/cancel.svg" width="18" height="18"
alt="Close" title="Close" alt="Close" title="Close"
onClick={this.props.onCloseClick} /> onClick={this.props.onCloseClick} />
@ -46,4 +47,3 @@ module.exports = React.createClass({
); );
}, },
}); });

View file

@ -15,13 +15,13 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { _t } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import AddThreepid from '../../../AddThreepid'; import AddThreepid from '../../../AddThreepid';
import WithMatrixClient from '../../../wrappers/WithMatrixClient'; import WithMatrixClient from '../../../wrappers/WithMatrixClient';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
export default WithMatrixClient(React.createClass({ export default WithMatrixClient(React.createClass({
displayName: 'AddPhoneNumber', displayName: 'AddPhoneNumber',
@ -50,7 +50,7 @@ export default WithMatrixClient(React.createClass({
}, },
_onPhoneCountryChange: function(phoneCountry) { _onPhoneCountryChange: function(phoneCountry) {
this.setState({ phoneCountry: phoneCountry }); this.setState({ phoneCountry: phoneCountry.iso2 });
}, },
_onPhoneNumberChange: function(ev) { _onPhoneNumberChange: function(ev) {
@ -83,7 +83,7 @@ export default WithMatrixClient(React.createClass({
console.error("Unable to add phone number: " + err); console.error("Unable to add phone number: " + err);
let msg = err.message; let msg = err.message;
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Error", title: _t("Error"),
description: msg, description: msg,
}); });
}).finally(() => { }).finally(() => {
@ -98,20 +98,19 @@ export default WithMatrixClient(React.createClass({
if (this._unmounted) return; if (this._unmounted) return;
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
let msgElements = [ let msgElements = [
<div key="_static" >A text message has been sent to +{msisdn}. <div key="_static" >{ _t("A text message has been sent to +%(msisdn)s. Please enter the verification code it contains", { msisdn: msisdn} ) }</div>
Please enter the verification code it contains</div>
]; ];
if (err) { if (err) {
let msg = err.error; let msg = err.error;
if (err.errcode == 'M_THREEPID_AUTH_FAILED') { if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
msg = "Incorrect verification code"; msg = _t("Incorrect verification code");
} }
msgElements.push(<div key="_error" className="error">{msg}</div>); msgElements.push(<div key="_error" className="error">{msg}</div>);
} }
Modal.createDialog(TextInputDialog, { Modal.createDialog(TextInputDialog, {
title: "Enter Code", title: _t("Enter Code"),
description: <div>{msgElements}</div>, description: <div>{msgElements}</div>,
button: "Submit", button: _t("Submit"),
onFinished: (should_verify, token) => { onFinished: (should_verify, token) => {
if (!should_verify) { if (!should_verify) {
this._addThreepid = null; this._addThreepid = null;
@ -147,17 +146,19 @@ export default WithMatrixClient(React.createClass({
return ( return (
<form className="mx_UserSettings_profileTableRow" onSubmit={this._onAddMsisdnSubmit}> <form className="mx_UserSettings_profileTableRow" onSubmit={this._onAddMsisdnSubmit}>
<div className="mx_UserSettings_profileLabelCell"> <div className="mx_UserSettings_profileLabelCell">
<label>{_t('Phone')}</label>
</div> </div>
<div className="mx_UserSettings_profileInputCell"> <div className="mx_UserSettings_profileInputCell">
<div className="mx_Login_phoneSection"> <div className="mx_UserSettings_phoneSection">
<CountryDropdown onOptionChange={this._onPhoneCountryChange} <CountryDropdown onOptionChange={this._onPhoneCountryChange}
className="mx_Login_phoneCountry" className="mx_UserSettings_phoneCountry"
value={this.state.phoneCountry} value={this.state.phoneCountry}
isSmall={true}
/> />
<input type="text" <input type="text"
ref={this._collectAddMsisdnInput} ref={this._collectAddMsisdnInput}
className="mx_UserSettings_phoneNumberField" className="mx_UserSettings_phoneNumberField"
placeholder="Add phone number" placeholder={ _t('Add phone number') }
value={this.state.phoneNumber} value={this.state.phoneNumber}
onChange={this._onPhoneNumberChange} onChange={this._onPhoneNumberChange}
/> />

View file

@ -21,6 +21,7 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal"); var Modal = require("../../../Modal");
var sdk = require("../../../index"); var sdk = require("../../../index");
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'ChangePassword', displayName: 'ChangePassword',
@ -47,11 +48,11 @@ module.exports = React.createClass({
onCheckPassword: function(oldPass, newPass, confirmPass) { onCheckPassword: function(oldPass, newPass, confirmPass) {
if (newPass !== confirmPass) { if (newPass !== confirmPass) {
return { return {
error: "New passwords don't match." error: _t("New passwords don't match") + "."
}; };
} else if (!newPass || newPass.length === 0) { } else if (!newPass || newPass.length === 0) {
return { return {
error: "Passwords can't be empty" error: _t("Passwords can't be empty")
}; };
} }
} }
@ -69,19 +70,21 @@ module.exports = React.createClass({
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>
Changing 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 'Changing password will currently reset any end-to-end encryption keys on all devices, ' +
and re-import them afterwards. 'making encrypted chat history unreadable, unless you first export your room keys ' +
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">will be improved</a>. 'and re-import them afterwards. ' +
'In future this will be improved.'
) } (<a href="https://github.com/vector-im/riot-web/issues/2671">https://github.com/vector-im/riot-web/issues/2671</a>)
</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 +153,7 @@ module.exports = React.createClass({
<div className={this.props.className}> <div className={this.props.className}>
<div className={rowClassName}> <div className={rowClassName}>
<div className={rowLabelClassName}> <div className={rowLabelClassName}>
<label htmlFor="passwordold">Current password</label> <label htmlFor="passwordold">{ _t('Current password') }</label>
</div> </div>
<div className={rowInputClassName}> <div className={rowInputClassName}>
<input id="passwordold" type="password" ref="old_input" /> <input id="passwordold" type="password" ref="old_input" />
@ -158,7 +161,7 @@ module.exports = React.createClass({
</div> </div>
<div className={rowClassName}> <div className={rowClassName}>
<div className={rowLabelClassName}> <div className={rowLabelClassName}>
<label htmlFor="password1">New password</label> <label htmlFor="password1">{ _t('New password') }</label>
</div> </div>
<div className={rowInputClassName}> <div className={rowInputClassName}>
<input id="password1" type="password" ref="new_input" /> <input id="password1" type="password" ref="new_input" />
@ -166,7 +169,7 @@ module.exports = React.createClass({
</div> </div>
<div className={rowClassName}> <div className={rowClassName}>
<div className={rowLabelClassName}> <div className={rowLabelClassName}>
<label htmlFor="password2">Confirm password</label> <label htmlFor="password2">{ _t('Confirm password') }</label>
</div> </div>
<div className={rowInputClassName}> <div className={rowInputClassName}>
<input id="password2" type="password" ref="confirm_input" /> <input id="password2" type="password" ref="confirm_input" />
@ -174,7 +177,7 @@ module.exports = React.createClass({
</div> </div>
<AccessibleButton className={buttonClassName} <AccessibleButton className={buttonClassName}
onClick={this.onClickChange}> onClick={this.onClickChange}>
Change Password { _t('Change Password') }
</AccessibleButton> </AccessibleButton>
</div> </div>
); );

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 { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import DateUtils from '../../../DateUtils'; import DateUtils from '../../../DateUtils';
@ -48,7 +49,7 @@ export default class DevicesPanelEntry extends React.Component {
display_name: value, display_name: value,
}).catch((e) => { }).catch((e) => {
console.error("Error setting device display name", e); console.error("Error setting device display name", e);
throw new Error("Failed to set display name"); throw new Error(_t("Failed to set display name"));
}); });
} }
@ -71,6 +72,7 @@ export default class DevicesPanelEntry extends React.Component {
var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
Modal.createDialog(InteractiveAuthDialog, { Modal.createDialog(InteractiveAuthDialog, {
title: _t("Authentication"),
matrixClient: MatrixClientPeg.get(), matrixClient: MatrixClientPeg.get(),
authData: error.data, authData: error.data,
makeRequest: this._makeDeleteRequest, makeRequest: this._makeDeleteRequest,
@ -84,7 +86,7 @@ export default class DevicesPanelEntry extends React.Component {
if (this._unmounted) { return; } if (this._unmounted) { return; }
this.setState({ this.setState({
deleting: false, deleting: false,
deleteError: "Failed to delete device", deleteError: _t("Failed to delete device"),
}); });
}).done(); }).done();
} }
@ -132,7 +134,7 @@ export default class DevicesPanelEntry extends React.Component {
deleteButton = ( deleteButton = (
<div className="mx_textButton" <div className="mx_textButton"
onClick={this._onDeleteClick}> onClick={this._onDeleteClick}>
Delete { _t("Delete") }
</div> </div>
); );
} }

View file

@ -17,6 +17,7 @@ limitations under the License.
var MatrixClientPeg = require('./MatrixClientPeg'); var MatrixClientPeg = require('./MatrixClientPeg');
var Modal = require('./Modal'); var Modal = require('./Modal');
var sdk = require('./index'); var sdk = require('./index');
import { _t } from './languageHandler';
var dis = require("./dispatcher"); var dis = require("./dispatcher");
var Rooms = require("./Rooms"); var Rooms = require("./Rooms");
@ -43,8 +44,8 @@ function createRoom(opts) {
if (client.isGuest()) { if (client.isGuest()) {
setTimeout(()=>{ setTimeout(()=>{
Modal.createDialog(NeedToRegisterDialog, { Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register", title: _t('Please Register'),
description: "Guest users can't create new rooms. Please register to create room and start a chat." description: _t('Guest users can\'t create new rooms. Please register to create room and start a chat') + '.'
}); });
}, 0); }, 0);
return q(null); return q(null);
@ -104,8 +105,8 @@ function createRoom(opts) {
}, function(err) { }, function(err) {
console.error("Failed to create room " + roomId + " " + err); console.error("Failed to create room " + roomId + " " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failure to create room", title: _t("Failure to create room"),
description: "Server may be unavailable, overloaded, or you hit a bug.", description: _t("Server may be unavailable, overloaded, or you hit a bug") + ".",
}); });
return null; return null;
}); });

View file

@ -16,13 +16,13 @@ limitations under the License.
'use strict'; 'use strict';
var flux = require("flux"); const flux = require("flux");
class MatrixDispatcher extends flux.Dispatcher { class MatrixDispatcher extends flux.Dispatcher {
/** /**
* @param {Object} payload Required. The payload to dispatch. * @param {Object} payload Required. The payload to dispatch.
* Must contain at least an 'action' key. * Must contain at least an 'action' key.
* @param {boolean} sync Optional. Pass true to dispatch * @param {boolean=} sync Optional. Pass true to dispatch
* synchronously. This is useful for anything triggering * synchronously. This is useful for anything triggering
* an operation that the browser requires user interaction * an operation that the browser requires user interaction
* for. * for.

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