From e806b0d22ac454be9b4b9bb13a1425bef13b3c49 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 5 Jul 2018 11:55:45 +0100 Subject: [PATCH 0001/2372] initial commit --- package.json | 15 +++++ title.test.js | 162 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 package.json create mode 100644 title.test.js diff --git a/package.json b/package.json new file mode 100644 index 0000000000..fa67de0069 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "e2e-tests", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "jest": "^23.2.0", + "puppeteer": "^1.5.0" + } +} diff --git a/title.test.js b/title.test.js new file mode 100644 index 0000000000..d416e889f0 --- /dev/null +++ b/title.test.js @@ -0,0 +1,162 @@ +const puppeteer = require('puppeteer'); +const riotserver = 'http://localhost:8080'; +const homeserver = 'http://localhost:8008'; +let browser = null; + +jest.setTimeout(10000); + +async function try_get_innertext(page, selector) { + const field = await page.$(selector); + if (field != null) { + const text_handle = await field.getProperty('innerText'); + return await text_handle.jsonValue(); + } + return null; +} + +async function new_page() { + const page = await browser.newPage(); + await page.setViewport({ + width: 1280, + height: 800 + }); + return page; +} + +function log_console(page) { + let buffer = ""; + page.on('console', msg => { + buffer += msg.text() + '\n'; + }); + return { + logs() { + return buffer; + } + } +} + +function log_xhr_requests(page) { + let buffer = ""; + page.on('request', req => { + const type = req.resourceType(); + if (type === 'xhr' || type === 'fetch') { + buffer += `${req.method()} ${req.url()} \n`; + if (req.method() === "POST") { + buffer += " Post data: " + req.postData(); + } + } + }); + return { + logs() { + return buffer; + } + } +} + +function rnd_int(max) { + return Math.ceil(Math.random()*max); +} + +function riot_url(path) { + return riotserver + path; +} + +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function get_outer_html(element_handle) { + const html_handle = await element_handle.getProperty('outerHTML'); + return await html_handle.jsonValue(); +} + +async function print_elements(label, elements) { + console.log(label, await Promise.all(elements.map(get_outer_html))); +} + +async function replace_input_text(input, text) { + // click 3 times to select all text + await input.click({clickCount: 3}); + // then remove it with backspace + await input.press('Backspace'); + // and type the new text + await input.type(text); +} + +beforeAll(async () => { + browser = await puppeteer.launch(); +}); + +afterAll(() => { + return browser.close(); +}) + +test('test page loads', async () => { + const page = await browser.newPage(); + await page.goto(riot_url('/')); + const title = await page.title(); + expect(title).toBe("Riot"); +}); + +test('test signup', async () => { + const page = await new_page(); + const console_logs = log_console(page); + const xhr_logs = log_xhr_requests(page); + await page.goto(riot_url('/#/register')); + //click 'Custom server' radio button + await page.waitForSelector('#advanced', {visible: true, timeout: 500}); + await page.click('#advanced'); + + const username = 'bruno-' + rnd_int(10000); + const password = 'testtest'; + //fill out form + await page.waitForSelector('.mx_ServerConfig', {visible: true, timeout: 500}); + const login_fields = await page.$$('.mx_Login_field'); + expect(login_fields.length).toBe(7); + const username_field = login_fields[2]; + const password_field = login_fields[3]; + const password_repeat_field = login_fields[4]; + const hsurl_field = login_fields[5]; + await replace_input_text(username_field, username); + await replace_input_text(password_field, password); + await replace_input_text(password_repeat_field, password); + await replace_input_text(hsurl_field, homeserver); + //wait over a second because Registration/ServerConfig have a 1000ms + //delay to internally set the homeserver url + //see Registration::render and ServerConfig::props::delayTimeMs + await delay(1200); + /// focus on the button to make sure error validation + /// has happened before checking the form is good to go + const register_button = await page.$('.mx_Login_submit'); + await register_button.focus(); + //check no errors + const error_text = await try_get_innertext(page, '.mx_Login_error'); + expect(error_text).toBeFalsy(); + //submit form + await page.screenshot({path: "beforesubmit.png", fullPage: true}); + await register_button.click(); + + //confirm dialog saying you cant log back in without e-mail + await page.waitForSelector('.mx_QuestionDialog', {visible: true, timeout: 500}); + const continue_button = await page.$('.mx_QuestionDialog button.mx_Dialog_primary'); + print_elements('continue_button', [continue_button]); + await continue_button.click(); + //wait for registration to finish so the hash gets set + //onhashchange better? + await delay(1000); +/* + await page.screenshot({path: "afterlogin.png", fullPage: true}); + console.log('browser console logs:'); + console.log(console_logs.logs()); + console.log('xhr logs:'); + console.log(xhr_logs.logs()); +*/ + + + //print_elements('page', await page.$('#matrixchat')); +// await navigation_promise; + + //await page.waitForSelector('.mx_MatrixChat', {visible: true, timeout: 3000}); + const url = page.url(); + expect(url).toBe(riot_url('/#/home')); +}); From 956beaf1f4a1bf4c4ffcbc7c1a9bdefb587d764d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 5 Jul 2018 11:56:19 +0100 Subject: [PATCH 0002/2372] add initial README --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000000..349e0294b7 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Matrix React Web App End-to-End tests + +This repository contains tests for the matrix-react-sdk web app. The tests fire up a headless chrome and simulate user interaction (end-to-end). Note that end-to-end has little to do with the end-to-end encryption matrix supports, just that we test the full stack, going from user interaction to expected DOM in the browser. + +## Current tests + - test riot loads (check title) + - signup with custom homeserver + +## Roadmap +- get rid of jest, as a test framework won't be helpful to have a continuous flow going from one use case to another (think: do login, create a room, invite a user, ...). a test framework usually assumes the tests are semi-indepedent. +- better error reporting (show console.log, XHR requests, partial DOM, screenshot) on error +- cleanup helper methods +- avoid delay when waiting for location.hash to change +- more tests! +- setup installing & running riot and synapse as part of the tests +- look into CI(Travis) integration + +## How to run + +### Setup + + - install dependencies with `npm install` + - have riot-web running on `localhost:8080` + - have a local synapse running at `localhost:8008` + +### Run tests + - run tests with `./node_modules/jest/bin/jest.js` \ No newline at end of file From a240c5617c416034f4d544d00c157eef684c7605 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 6 Jul 2018 11:40:42 +0100 Subject: [PATCH 0003/2372] Update roadmap --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 349e0294b7..fdbd0caf3b 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This repository contains tests for the matrix-react-sdk web app. The tests fire - get rid of jest, as a test framework won't be helpful to have a continuous flow going from one use case to another (think: do login, create a room, invite a user, ...). a test framework usually assumes the tests are semi-indepedent. - better error reporting (show console.log, XHR requests, partial DOM, screenshot) on error - cleanup helper methods +- add more css id's/classes to riot web to make css selectors in test less brittle. - avoid delay when waiting for location.hash to change - more tests! - setup installing & running riot and synapse as part of the tests @@ -24,4 +25,4 @@ This repository contains tests for the matrix-react-sdk web app. The tests fire - have a local synapse running at `localhost:8008` ### Run tests - - run tests with `./node_modules/jest/bin/jest.js` \ No newline at end of file + - run tests with `./node_modules/jest/bin/jest.js` From f43d53c0201b434a0f61417d3b19ed8cc1832681 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 9 Jul 2018 12:41:24 +0200 Subject: [PATCH 0004/2372] Update roadmap --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fdbd0caf3b..a76301d5a2 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ This repository contains tests for the matrix-react-sdk web app. The tests fire - add more css id's/classes to riot web to make css selectors in test less brittle. - avoid delay when waiting for location.hash to change - more tests! -- setup installing & running riot and synapse as part of the tests +- setup installing & running riot and synapse as part of the tests. + - Run 2 synapse instances to test federation use cases. + - start synapse with clean config/database on every test run - look into CI(Travis) integration ## How to run From 9921573076c26ad4b6f72a04793e2f9f0186fe10 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 9 Jul 2018 16:50:49 +0200 Subject: [PATCH 0005/2372] add license and copyright notice --- title.test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/title.test.js b/title.test.js index d416e889f0..9c10452230 100644 --- a/title.test.js +++ b/title.test.js @@ -1,3 +1,19 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + const puppeteer = require('puppeteer'); const riotserver = 'http://localhost:8080'; const homeserver = 'http://localhost:8008'; From 5429bfde11de378b6a564a1eb45f55a1eef40461 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 9 Jul 2018 17:06:17 +0200 Subject: [PATCH 0006/2372] move helpers to other module --- helpers.js | 110 +++++++++++++++++++++++++++++++++++++++++++++++ title.test.js | 117 +++++++++----------------------------------------- 2 files changed, 130 insertions(+), 97 deletions(-) create mode 100644 helpers.js diff --git a/helpers.js b/helpers.js new file mode 100644 index 0000000000..bd0035f13d --- /dev/null +++ b/helpers.js @@ -0,0 +1,110 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// puppeteer helpers + +async function try_get_innertext(page, selector) { + const field = await page.$(selector); + if (field != null) { + const text_handle = await field.getProperty('innerText'); + return await text_handle.jsonValue(); + } + return null; +} + +async function new_page() { + const page = await browser.newPage(); + await page.setViewport({ + width: 1280, + height: 800 + }); + return page; +} + +function log_console(page) { + let buffer = ""; + page.on('console', msg => { + buffer += msg.text() + '\n'; + }); + return { + logs() { + return buffer; + } + } +} + +function log_xhr_requests(page) { + let buffer = ""; + page.on('request', req => { + const type = req.resourceType(); + if (type === 'xhr' || type === 'fetch') { + buffer += `${req.method()} ${req.url()} \n`; + if (req.method() === "POST") { + buffer += " Post data: " + req.postData(); + } + } + }); + return { + logs() { + return buffer; + } + } +} + +async function get_outer_html(element_handle) { + const html_handle = await element_handle.getProperty('outerHTML'); + return await html_handle.jsonValue(); +} + +async function print_elements(label, elements) { + console.log(label, await Promise.all(elements.map(get_outer_html))); +} + +async function replace_input_text(input, text) { + // click 3 times to select all text + await input.click({clickCount: 3}); + // then remove it with backspace + await input.press('Backspace'); + // and type the new text + await input.type(text); +} + +// other helpers + +function rnd_int(max) { + return Math.ceil(Math.random()*max); +} + +function riot_url(path) { + return riotserver + path; +} + +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +module.exports = { + try_get_innertext, + new_page, + log_console, + log_xhr_requests, + get_outer_html, + print_elements, + replace_input_text, + rnd_int, + riot_url, + delay, +} \ No newline at end of file diff --git a/title.test.js b/title.test.js index 9c10452230..0bbb8d56ac 100644 --- a/title.test.js +++ b/title.test.js @@ -15,92 +15,15 @@ limitations under the License. */ const puppeteer = require('puppeteer'); -const riotserver = 'http://localhost:8080'; -const homeserver = 'http://localhost:8008'; -let browser = null; +const helpers = require('./helpers'); +global.riotserver = 'http://localhost:8080'; +global.homeserver = 'http://localhost:8008'; +global.browser = null; jest.setTimeout(10000); -async function try_get_innertext(page, selector) { - const field = await page.$(selector); - if (field != null) { - const text_handle = await field.getProperty('innerText'); - return await text_handle.jsonValue(); - } - return null; -} - -async function new_page() { - const page = await browser.newPage(); - await page.setViewport({ - width: 1280, - height: 800 - }); - return page; -} - -function log_console(page) { - let buffer = ""; - page.on('console', msg => { - buffer += msg.text() + '\n'; - }); - return { - logs() { - return buffer; - } - } -} - -function log_xhr_requests(page) { - let buffer = ""; - page.on('request', req => { - const type = req.resourceType(); - if (type === 'xhr' || type === 'fetch') { - buffer += `${req.method()} ${req.url()} \n`; - if (req.method() === "POST") { - buffer += " Post data: " + req.postData(); - } - } - }); - return { - logs() { - return buffer; - } - } -} - -function rnd_int(max) { - return Math.ceil(Math.random()*max); -} - -function riot_url(path) { - return riotserver + path; -} - -function delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -async function get_outer_html(element_handle) { - const html_handle = await element_handle.getProperty('outerHTML'); - return await html_handle.jsonValue(); -} - -async function print_elements(label, elements) { - console.log(label, await Promise.all(elements.map(get_outer_html))); -} - -async function replace_input_text(input, text) { - // click 3 times to select all text - await input.click({clickCount: 3}); - // then remove it with backspace - await input.press('Backspace'); - // and type the new text - await input.type(text); -} - beforeAll(async () => { - browser = await puppeteer.launch(); + global.browser = await puppeteer.launch(); }); afterAll(() => { @@ -109,21 +32,21 @@ afterAll(() => { test('test page loads', async () => { const page = await browser.newPage(); - await page.goto(riot_url('/')); + await page.goto(helpers.riot_url('/')); const title = await page.title(); expect(title).toBe("Riot"); }); test('test signup', async () => { - const page = await new_page(); - const console_logs = log_console(page); - const xhr_logs = log_xhr_requests(page); - await page.goto(riot_url('/#/register')); + const page = await helpers.new_page(); + const console_logs = helpers.log_console(page); + const xhr_logs = helpers.log_xhr_requests(page); + await page.goto(helpers.riot_url('/#/register')); //click 'Custom server' radio button await page.waitForSelector('#advanced', {visible: true, timeout: 500}); await page.click('#advanced'); - const username = 'bruno-' + rnd_int(10000); + const username = 'bruno-' + helpers.rnd_int(10000); const password = 'testtest'; //fill out form await page.waitForSelector('.mx_ServerConfig', {visible: true, timeout: 500}); @@ -133,20 +56,20 @@ test('test signup', async () => { const password_field = login_fields[3]; const password_repeat_field = login_fields[4]; const hsurl_field = login_fields[5]; - await replace_input_text(username_field, username); - await replace_input_text(password_field, password); - await replace_input_text(password_repeat_field, password); - await replace_input_text(hsurl_field, homeserver); + await helpers.replace_input_text(username_field, username); + await helpers.replace_input_text(password_field, password); + await helpers.replace_input_text(password_repeat_field, password); + await helpers.replace_input_text(hsurl_field, homeserver); //wait over a second because Registration/ServerConfig have a 1000ms //delay to internally set the homeserver url //see Registration::render and ServerConfig::props::delayTimeMs - await delay(1200); + await helpers.delay(1200); /// focus on the button to make sure error validation /// has happened before checking the form is good to go const register_button = await page.$('.mx_Login_submit'); await register_button.focus(); //check no errors - const error_text = await try_get_innertext(page, '.mx_Login_error'); + const error_text = await helpers.try_get_innertext(page, '.mx_Login_error'); expect(error_text).toBeFalsy(); //submit form await page.screenshot({path: "beforesubmit.png", fullPage: true}); @@ -155,11 +78,11 @@ test('test signup', async () => { //confirm dialog saying you cant log back in without e-mail await page.waitForSelector('.mx_QuestionDialog', {visible: true, timeout: 500}); const continue_button = await page.$('.mx_QuestionDialog button.mx_Dialog_primary'); - print_elements('continue_button', [continue_button]); + await helpers.print_elements('continue_button', [continue_button]); await continue_button.click(); //wait for registration to finish so the hash gets set //onhashchange better? - await delay(1000); + await helpers.delay(1000); /* await page.screenshot({path: "afterlogin.png", fullPage: true}); console.log('browser console logs:'); @@ -174,5 +97,5 @@ test('test signup', async () => { //await page.waitForSelector('.mx_MatrixChat', {visible: true, timeout: 3000}); const url = page.url(); - expect(url).toBe(riot_url('/#/home')); + expect(url).toBe(helpers.riot_url('/#/home')); }); From 473af6ff6212947e918a5e02dd484d1b7e2ed8ef Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 9 Jul 2018 17:07:03 +0200 Subject: [PATCH 0007/2372] add ignore file --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..8a48fb815d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +package-lock.json +*.png \ No newline at end of file From 61ac9898470acb045aaaf44dbfaf5a744d555d13 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 9 Jul 2018 17:08:02 +0200 Subject: [PATCH 0008/2372] add code style --- code_style.md | 184 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 code_style.md diff --git a/code_style.md b/code_style.md new file mode 100644 index 0000000000..2cac303e54 --- /dev/null +++ b/code_style.md @@ -0,0 +1,184 @@ +Matrix JavaScript/ECMAScript Style Guide +======================================== + +The intention of this guide is to make Matrix's JavaScript codebase clean, +consistent with other popular JavaScript styles and consistent with the rest of +the Matrix codebase. For reference, the Matrix Python style guide can be found +at https://github.com/matrix-org/synapse/blob/master/docs/code_style.rst + +This document reflects how we would like Matrix JavaScript code to look, with +acknowledgement that a significant amount of code is written to older +standards. + +Write applications in modern ECMAScript and use a transpiler where necessary to +target older platforms. When writing library code, consider carefully whether +to write in ES5 to allow all JavaScript application to use the code directly or +writing in modern ECMAScript and using a transpile step to generate the file +that applications can then include. There are significant benefits in being +able to use modern ECMAScript, although the tooling for doing so can be awkward +for library code, especially with regard to translating source maps and line +number throgh from the original code to the final application. + +General Style +------------- +- 4 spaces to indent, for consistency with Matrix Python. +- 120 columns per line, but try to keep JavaScript code around the 80 column mark. + Inline JSX in particular can be nicer with more columns per line. +- No trailing whitespace at end of lines. +- Don't indent empty lines. +- One newline at the end of the file. +- Unix newlines, never `\r` +- Indent similar to our python code: break up long lines at logical boundaries, + more than one argument on a line is OK +- Use semicolons, for consistency with node. +- UpperCamelCase for class and type names +- lowerCamelCase for functions and variables. +- Single line ternary operators are fine. +- UPPER_CAMEL_CASE for constants +- Single quotes for strings by default, for consistency with most JavaScript styles: + + ```javascript + "bad" // Bad + 'good' // Good + ``` +- Use parentheses or `` ` `` instead of `\` for line continuation where ever possible +- Open braces on the same line (consistent with Node): + + ```javascript + if (x) { + console.log("I am a fish"); // Good + } + + if (x) + { + console.log("I am a fish"); // Bad + } + ``` +- Spaces after `if`, `for`, `else` etc, no space around the condition: + + ```javascript + if (x) { + console.log("I am a fish"); // Good + } + + if(x) { + console.log("I am a fish"); // Bad + } + + if ( x ) { + 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 + are simple and closely related. If you put the next declaration on a new line, + treat yourself to another `var`: + + ```javascript + const key = "foo", + comparator = function(x, y) { + return x - y; + }; // Bad + + const key = "foo"; + const comparator = function(x, y) { + return x - y; + }; // Good + + let x = 0, y = 0; // Fine + + let x = 0; + let y = 0; // Also fine + ``` +- A single line `if` is fine, all others have braces. This prevents errors when adding to the code.: + + ```javascript + if (x) return true; // Fine + + if (x) { + return true; // Also fine + } + + if (x) + return true; // Not fine + ``` +- Terminate all multi-line lists, object literals, imports and ideally function calls with commas (if using a transpiler). Note that trailing function commas require explicit configuration in babel at time of writing: + + ```javascript + var mascots = [ + "Patrick", + "Shirley", + "Colin", + "Susan", + "Sir Arthur David" // Bad + ]; + + var mascots = [ + "Patrick", + "Shirley", + "Colin", + "Susan", + "Sir Arthur David", // Good + ]; + ``` +- Use `null`, `undefined` etc consistently with node: + Boolean variables and functions should always be either true or false. Don't set it to 0 unless it's supposed to be a number. + When something is intentionally missing or removed, set it to null. + If returning a boolean, type coerce: + + ```javascript + function hasThings() { + return !!length; // bad + return new Boolean(length); // REALLY bad + return Boolean(length); // good + } + ``` + Don't set things to undefined. Reserve that value to mean "not yet set to anything." + Boolean objects are verboten. +- Use JSDoc + +ECMAScript +---------- +- Use `const` unless you need a re-assignable variable. This ensures things you don't want to be re-assigned can't be. +- Be careful migrating files to newer syntax. + - Don't mix `require` and `import` in the same file. Either stick to the old style or change them all. + - Likewise, don't mix things like class properties and `MyClass.prototype.MY_CONSTANT = 42;` + - Be careful mixing arrow functions and regular functions, eg. if one function in a promise chain is an + arrow function, they probably all should be. +- Apart from that, newer ES features should be used whenever the author deems them to be appropriate. +- Flow annotations are welcome and encouraged. + +React +----- +- Use React.createClass rather than ES6 classes for components, as the boilerplate is way too heavy on ES6 currently. ES7 might improve it. +- Pull out functions in props to the class, generally as specific event handlers: + + ```jsx + // Bad + {doStuff();}}> // Equally bad + // Better + // Best, if onFooClick would do anything other than directly calling doStuff + ``` + + Not doing so is acceptable in a single case; in function-refs: + + ```jsx + this.component = self}> + ``` +- Think about whether your component really needs state: are you duplicating + information in component state that could be derived from the model? From b76c3a1842fb0b1eff7debf57a3df36774550e51 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 9 Jul 2018 17:43:21 +0200 Subject: [PATCH 0009/2372] don't use jest and just run test code sequentially since a lot of tests will be interdepent and need to happen in order, it seems easier to not use a test runner enforcing tests to be semi-independent and instead just run the code and have some logging code to see where a problem occurs --- title.test.js => index.js | 57 ++++++++++++----- package.json | 1 - start.js | 126 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 17 deletions(-) rename title.test.js => index.js (77%) create mode 100644 start.js diff --git a/title.test.js b/index.js similarity index 77% rename from title.test.js rename to index.js index 0bbb8d56ac..a47a51252f 100644 --- a/title.test.js +++ b/index.js @@ -16,28 +16,53 @@ limitations under the License. const puppeteer = require('puppeteer'); const helpers = require('./helpers'); +const assert = require('assert'); + global.riotserver = 'http://localhost:8080'; global.homeserver = 'http://localhost:8008'; global.browser = null; -jest.setTimeout(10000); +async function run_tests() { + await start_session(); -beforeAll(async () => { + process.stdout.write(`* testing riot loads ... `); + await test_title(); + process.stdout.write('done\n'); + + const username = 'bruno-' + helpers.rnd_int(10000); + const password = 'testtest'; + process.stdout.write(`* signing up as ${username} ... `); + await do_signup(username, password, homeserver); + process.stdout.write('done\n'); + await end_session(); +} + +async function start_session() { global.browser = await puppeteer.launch(); -}); +} -afterAll(() => { +function end_session() { return browser.close(); -}) +} -test('test page loads', async () => { +function on_success() { + console.log('all tests finished successfully'); +} + +function on_failure(err) { + console.log('failure: ', err); + process.exit(-1); +} + + +async function test_title() { const page = await browser.newPage(); await page.goto(helpers.riot_url('/')); const title = await page.title(); - expect(title).toBe("Riot"); -}); + assert.strictEqual(title, "Riot"); +}; -test('test signup', async () => { +async function do_signup(username, password, homeserver) { const page = await helpers.new_page(); const console_logs = helpers.log_console(page); const xhr_logs = helpers.log_xhr_requests(page); @@ -46,12 +71,10 @@ test('test signup', async () => { await page.waitForSelector('#advanced', {visible: true, timeout: 500}); await page.click('#advanced'); - const username = 'bruno-' + helpers.rnd_int(10000); - const password = 'testtest'; //fill out form await page.waitForSelector('.mx_ServerConfig', {visible: true, timeout: 500}); const login_fields = await page.$$('.mx_Login_field'); - expect(login_fields.length).toBe(7); + assert.strictEqual(login_fields.length, 7); const username_field = login_fields[2]; const password_field = login_fields[3]; const password_repeat_field = login_fields[4]; @@ -70,7 +93,7 @@ test('test signup', async () => { await register_button.focus(); //check no errors const error_text = await helpers.try_get_innertext(page, '.mx_Login_error'); - expect(error_text).toBeFalsy(); + assert.strictEqual(!!error_text, false); //submit form await page.screenshot({path: "beforesubmit.png", fullPage: true}); await register_button.click(); @@ -78,7 +101,7 @@ test('test signup', async () => { //confirm dialog saying you cant log back in without e-mail await page.waitForSelector('.mx_QuestionDialog', {visible: true, timeout: 500}); const continue_button = await page.$('.mx_QuestionDialog button.mx_Dialog_primary'); - await helpers.print_elements('continue_button', [continue_button]); + //await helpers.print_elements('continue_button', [continue_button]); await continue_button.click(); //wait for registration to finish so the hash gets set //onhashchange better? @@ -97,5 +120,7 @@ test('test signup', async () => { //await page.waitForSelector('.mx_MatrixChat', {visible: true, timeout: 3000}); const url = page.url(); - expect(url).toBe(helpers.riot_url('/#/home')); -}); + assert.strictEqual(url, helpers.riot_url('/#/home')); +}; + +run_tests().then(on_success, on_failure); \ No newline at end of file diff --git a/package.json b/package.json index fa67de0069..48644df401 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "author": "", "license": "ISC", "dependencies": { - "jest": "^23.2.0", "puppeteer": "^1.5.0" } } diff --git a/start.js b/start.js new file mode 100644 index 0000000000..a47a51252f --- /dev/null +++ b/start.js @@ -0,0 +1,126 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const puppeteer = require('puppeteer'); +const helpers = require('./helpers'); +const assert = require('assert'); + +global.riotserver = 'http://localhost:8080'; +global.homeserver = 'http://localhost:8008'; +global.browser = null; + +async function run_tests() { + await start_session(); + + process.stdout.write(`* testing riot loads ... `); + await test_title(); + process.stdout.write('done\n'); + + const username = 'bruno-' + helpers.rnd_int(10000); + const password = 'testtest'; + process.stdout.write(`* signing up as ${username} ... `); + await do_signup(username, password, homeserver); + process.stdout.write('done\n'); + await end_session(); +} + +async function start_session() { + global.browser = await puppeteer.launch(); +} + +function end_session() { + return browser.close(); +} + +function on_success() { + console.log('all tests finished successfully'); +} + +function on_failure(err) { + console.log('failure: ', err); + process.exit(-1); +} + + +async function test_title() { + const page = await browser.newPage(); + await page.goto(helpers.riot_url('/')); + const title = await page.title(); + assert.strictEqual(title, "Riot"); +}; + +async function do_signup(username, password, homeserver) { + const page = await helpers.new_page(); + const console_logs = helpers.log_console(page); + const xhr_logs = helpers.log_xhr_requests(page); + await page.goto(helpers.riot_url('/#/register')); + //click 'Custom server' radio button + await page.waitForSelector('#advanced', {visible: true, timeout: 500}); + await page.click('#advanced'); + + //fill out form + await page.waitForSelector('.mx_ServerConfig', {visible: true, timeout: 500}); + const login_fields = await page.$$('.mx_Login_field'); + assert.strictEqual(login_fields.length, 7); + const username_field = login_fields[2]; + const password_field = login_fields[3]; + const password_repeat_field = login_fields[4]; + const hsurl_field = login_fields[5]; + await helpers.replace_input_text(username_field, username); + await helpers.replace_input_text(password_field, password); + await helpers.replace_input_text(password_repeat_field, password); + await helpers.replace_input_text(hsurl_field, homeserver); + //wait over a second because Registration/ServerConfig have a 1000ms + //delay to internally set the homeserver url + //see Registration::render and ServerConfig::props::delayTimeMs + await helpers.delay(1200); + /// focus on the button to make sure error validation + /// has happened before checking the form is good to go + const register_button = await page.$('.mx_Login_submit'); + await register_button.focus(); + //check no errors + const error_text = await helpers.try_get_innertext(page, '.mx_Login_error'); + assert.strictEqual(!!error_text, false); + //submit form + await page.screenshot({path: "beforesubmit.png", fullPage: true}); + await register_button.click(); + + //confirm dialog saying you cant log back in without e-mail + await page.waitForSelector('.mx_QuestionDialog', {visible: true, timeout: 500}); + const continue_button = await page.$('.mx_QuestionDialog button.mx_Dialog_primary'); + //await helpers.print_elements('continue_button', [continue_button]); + await continue_button.click(); + //wait for registration to finish so the hash gets set + //onhashchange better? + await helpers.delay(1000); +/* + await page.screenshot({path: "afterlogin.png", fullPage: true}); + console.log('browser console logs:'); + console.log(console_logs.logs()); + console.log('xhr logs:'); + console.log(xhr_logs.logs()); +*/ + + + //print_elements('page', await page.$('#matrixchat')); +// await navigation_promise; + + //await page.waitForSelector('.mx_MatrixChat', {visible: true, timeout: 3000}); + const url = page.url(); + assert.strictEqual(url, helpers.riot_url('/#/home')); +}; + +run_tests().then(on_success, on_failure); \ No newline at end of file From 5c4f92952f44b1efb83f309d10f0322603fbe926 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 9 Jul 2018 17:51:02 +0200 Subject: [PATCH 0010/2372] move tests to separate file --- README.md | 2 +- start.js | 71 ++----------------------------------- tests/loads.js | 25 +++++++++++++ index.js => tests/signup.js | 53 ++------------------------- 4 files changed, 31 insertions(+), 120 deletions(-) create mode 100644 tests/loads.js rename index.js => tests/signup.js (72%) diff --git a/README.md b/README.md index a76301d5a2..c56a47fb49 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,4 @@ This repository contains tests for the matrix-react-sdk web app. The tests fire - have a local synapse running at `localhost:8008` ### Run tests - - run tests with `./node_modules/jest/bin/jest.js` + - run tests with `node start.js` diff --git a/start.js b/start.js index a47a51252f..9ba9cc0e55 100644 --- a/start.js +++ b/start.js @@ -17,6 +17,8 @@ limitations under the License. const puppeteer = require('puppeteer'); const helpers = require('./helpers'); const assert = require('assert'); +const do_signup = require('./tests/signup'); +const test_title = require('./tests/loads'); global.riotserver = 'http://localhost:8080'; global.homeserver = 'http://localhost:8008'; @@ -54,73 +56,4 @@ function on_failure(err) { process.exit(-1); } - -async function test_title() { - const page = await browser.newPage(); - await page.goto(helpers.riot_url('/')); - const title = await page.title(); - assert.strictEqual(title, "Riot"); -}; - -async function do_signup(username, password, homeserver) { - const page = await helpers.new_page(); - const console_logs = helpers.log_console(page); - const xhr_logs = helpers.log_xhr_requests(page); - await page.goto(helpers.riot_url('/#/register')); - //click 'Custom server' radio button - await page.waitForSelector('#advanced', {visible: true, timeout: 500}); - await page.click('#advanced'); - - //fill out form - await page.waitForSelector('.mx_ServerConfig', {visible: true, timeout: 500}); - const login_fields = await page.$$('.mx_Login_field'); - assert.strictEqual(login_fields.length, 7); - const username_field = login_fields[2]; - const password_field = login_fields[3]; - const password_repeat_field = login_fields[4]; - const hsurl_field = login_fields[5]; - await helpers.replace_input_text(username_field, username); - await helpers.replace_input_text(password_field, password); - await helpers.replace_input_text(password_repeat_field, password); - await helpers.replace_input_text(hsurl_field, homeserver); - //wait over a second because Registration/ServerConfig have a 1000ms - //delay to internally set the homeserver url - //see Registration::render and ServerConfig::props::delayTimeMs - await helpers.delay(1200); - /// focus on the button to make sure error validation - /// has happened before checking the form is good to go - const register_button = await page.$('.mx_Login_submit'); - await register_button.focus(); - //check no errors - const error_text = await helpers.try_get_innertext(page, '.mx_Login_error'); - assert.strictEqual(!!error_text, false); - //submit form - await page.screenshot({path: "beforesubmit.png", fullPage: true}); - await register_button.click(); - - //confirm dialog saying you cant log back in without e-mail - await page.waitForSelector('.mx_QuestionDialog', {visible: true, timeout: 500}); - const continue_button = await page.$('.mx_QuestionDialog button.mx_Dialog_primary'); - //await helpers.print_elements('continue_button', [continue_button]); - await continue_button.click(); - //wait for registration to finish so the hash gets set - //onhashchange better? - await helpers.delay(1000); -/* - await page.screenshot({path: "afterlogin.png", fullPage: true}); - console.log('browser console logs:'); - console.log(console_logs.logs()); - console.log('xhr logs:'); - console.log(xhr_logs.logs()); -*/ - - - //print_elements('page', await page.$('#matrixchat')); -// await navigation_promise; - - //await page.waitForSelector('.mx_MatrixChat', {visible: true, timeout: 3000}); - const url = page.url(); - assert.strictEqual(url, helpers.riot_url('/#/home')); -}; - run_tests().then(on_success, on_failure); \ No newline at end of file diff --git a/tests/loads.js b/tests/loads.js new file mode 100644 index 0000000000..7136b934db --- /dev/null +++ b/tests/loads.js @@ -0,0 +1,25 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const helpers = require('../helpers'); +const assert = require('assert'); + +module.exports = async function test_title() { + const page = await browser.newPage(); + await page.goto(helpers.riot_url('/')); + const title = await page.title(); + assert.strictEqual(title, "Riot"); +}; \ No newline at end of file diff --git a/index.js b/tests/signup.js similarity index 72% rename from index.js rename to tests/signup.js index a47a51252f..b3443bd3ec 100644 --- a/index.js +++ b/tests/signup.js @@ -14,55 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -const puppeteer = require('puppeteer'); -const helpers = require('./helpers'); +const helpers = require('../helpers'); const assert = require('assert'); -global.riotserver = 'http://localhost:8080'; -global.homeserver = 'http://localhost:8008'; -global.browser = null; - -async function run_tests() { - await start_session(); - - process.stdout.write(`* testing riot loads ... `); - await test_title(); - process.stdout.write('done\n'); - - const username = 'bruno-' + helpers.rnd_int(10000); - const password = 'testtest'; - process.stdout.write(`* signing up as ${username} ... `); - await do_signup(username, password, homeserver); - process.stdout.write('done\n'); - await end_session(); -} - -async function start_session() { - global.browser = await puppeteer.launch(); -} - -function end_session() { - return browser.close(); -} - -function on_success() { - console.log('all tests finished successfully'); -} - -function on_failure(err) { - console.log('failure: ', err); - process.exit(-1); -} - - -async function test_title() { - const page = await browser.newPage(); - await page.goto(helpers.riot_url('/')); - const title = await page.title(); - assert.strictEqual(title, "Riot"); -}; - -async function do_signup(username, password, homeserver) { +module.exports = async function do_signup(username, password, homeserver) { const page = await helpers.new_page(); const console_logs = helpers.log_console(page); const xhr_logs = helpers.log_xhr_requests(page); @@ -121,6 +76,4 @@ async function do_signup(username, password, homeserver) { //await page.waitForSelector('.mx_MatrixChat', {visible: true, timeout: 3000}); const url = page.url(); assert.strictEqual(url, helpers.riot_url('/#/home')); -}; - -run_tests().then(on_success, on_failure); \ No newline at end of file +} \ No newline at end of file From 400327a0f16bcfff10aa04db30765b7f23ff2d76 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 9 Jul 2018 18:21:05 +0200 Subject: [PATCH 0011/2372] add test for joining preexisting room --- README.md | 1 + helpers.js | 6 ++++++ start.js | 12 +++++++++++- tests/join_room.js | 35 +++++++++++++++++++++++++++++++++++ tests/signup.js | 3 +-- 5 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 tests/join_room.js diff --git a/README.md b/README.md index c56a47fb49..c473db5555 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ This repository contains tests for the matrix-react-sdk web app. The tests fire ## Current tests - test riot loads (check title) - signup with custom homeserver + - join preexisting room ## Roadmap - get rid of jest, as a test framework won't be helpful to have a continuous flow going from one use case to another (think: do login, create a room, invite a user, ...). a test framework usually assumes the tests are semi-indepedent. diff --git a/helpers.js b/helpers.js index bd0035f13d..bb4f0b20ca 100644 --- a/helpers.js +++ b/helpers.js @@ -82,6 +82,11 @@ async function replace_input_text(input, text) { await input.type(text); } +async function wait_and_query_selector(page, selector, timeout = 500) { + await page.waitForSelector(selector, {visible: true, timeout}); + return await page.$(selector); +} + // other helpers function rnd_int(max) { @@ -104,6 +109,7 @@ module.exports = { get_outer_html, print_elements, replace_input_text, + wait_and_query_selector, rnd_int, riot_url, delay, diff --git a/start.js b/start.js index 9ba9cc0e55..002019438a 100644 --- a/start.js +++ b/start.js @@ -19,6 +19,7 @@ const helpers = require('./helpers'); const assert = require('assert'); const do_signup = require('./tests/signup'); const test_title = require('./tests/loads'); +const join_room = require('./tests/join_room'); global.riotserver = 'http://localhost:8080'; global.homeserver = 'http://localhost:8008'; @@ -31,11 +32,20 @@ async function run_tests() { await test_title(); process.stdout.write('done\n'); + + + const page = await helpers.new_page(); const username = 'bruno-' + helpers.rnd_int(10000); const password = 'testtest'; process.stdout.write(`* signing up as ${username} ... `); - await do_signup(username, password, homeserver); + await do_signup(page, username, password, homeserver); process.stdout.write('done\n'); + + const room = 'test'; + process.stdout.write(`* joining room ${room} ... `); + await join_room(page, room); + process.stdout.write('done\n'); + await end_session(); } diff --git a/tests/join_room.js b/tests/join_room.js new file mode 100644 index 0000000000..7975fad648 --- /dev/null +++ b/tests/join_room.js @@ -0,0 +1,35 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const helpers = require('../helpers'); +const assert = require('assert'); + +module.exports = async function join_room(page, room_name) { + //TODO: brittle selector + const directory_button = await helpers.wait_and_query_selector(page, '.mx_RoleButton[aria-label="Room directory"]'); + await directory_button.click(); + + const room_input = await helpers.wait_and_query_selector(page, '.mx_DirectorySearchBox_input'); + await helpers.replace_input_text(room_input, room_name); + + const first_room_label = await helpers.wait_and_query_selector(page, '.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child'); + await first_room_label.click(); + + const join_link = await helpers.wait_and_query_selector(page, '.mx_RoomPreviewBar_join_text a'); + await join_link.click(); + + await page.waitForSelector('.mx_MessageComposer'); +} \ No newline at end of file diff --git a/tests/signup.js b/tests/signup.js index b3443bd3ec..155c5a1e0a 100644 --- a/tests/signup.js +++ b/tests/signup.js @@ -17,8 +17,7 @@ limitations under the License. const helpers = require('../helpers'); const assert = require('assert'); -module.exports = async function do_signup(username, password, homeserver) { - const page = await helpers.new_page(); +module.exports = async function do_signup(page, username, password, homeserver) { const console_logs = helpers.log_console(page); const xhr_logs = helpers.log_xhr_requests(page); await page.goto(helpers.riot_url('/#/register')); From 838563f0a6d51227be47e1b35e6227d9eb537830 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 9 Jul 2018 18:21:43 +0200 Subject: [PATCH 0012/2372] add note to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c473db5555..163bffbce7 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ This repository contains tests for the matrix-react-sdk web app. The tests fire ### Setup - - install dependencies with `npm install` + - install dependencies with `npm install` (will download copy of chrome) - have riot-web running on `localhost:8080` - have a local synapse running at `localhost:8008` From d4682eb5e68c9ad2d325cf2bc41c16395da4c56e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 9 Jul 2018 18:35:47 +0200 Subject: [PATCH 0013/2372] apply code style --- helpers.js | 42 +++++++++++++-------------- start.js | 37 ++++++++++-------------- tests/{join_room.js => join.js} | 18 ++++++------ tests/loads.js | 25 ----------------- tests/signup.js | 50 ++++++++++++++++----------------- 5 files changed, 70 insertions(+), 102 deletions(-) rename tests/{join_room.js => join.js} (51%) delete mode 100644 tests/loads.js diff --git a/helpers.js b/helpers.js index bb4f0b20ca..3e2467e622 100644 --- a/helpers.js +++ b/helpers.js @@ -16,7 +16,7 @@ limitations under the License. // puppeteer helpers -async function try_get_innertext(page, selector) { +async function tryGetInnertext(page, selector) { const field = await page.$(selector); if (field != null) { const text_handle = await field.getProperty('innerText'); @@ -25,7 +25,7 @@ async function try_get_innertext(page, selector) { return null; } -async function new_page() { +async function newPage() { const page = await browser.newPage(); await page.setViewport({ width: 1280, @@ -34,7 +34,7 @@ async function new_page() { return page; } -function log_console(page) { +function logConsole(page) { let buffer = ""; page.on('console', msg => { buffer += msg.text() + '\n'; @@ -46,7 +46,7 @@ function log_console(page) { } } -function log_xhr_requests(page) { +function logXHRRequests(page) { let buffer = ""; page.on('request', req => { const type = req.resourceType(); @@ -64,16 +64,16 @@ function log_xhr_requests(page) { } } -async function get_outer_html(element_handle) { +async function getOuterHTML(element_handle) { const html_handle = await element_handle.getProperty('outerHTML'); return await html_handle.jsonValue(); } -async function print_elements(label, elements) { - console.log(label, await Promise.all(elements.map(get_outer_html))); +async function printElements(label, elements) { + console.log(label, await Promise.all(elements.map(getOuterHTML))); } -async function replace_input_text(input, text) { +async function replaceInputText(input, text) { // click 3 times to select all text await input.click({clickCount: 3}); // then remove it with backspace @@ -82,18 +82,18 @@ async function replace_input_text(input, text) { await input.type(text); } -async function wait_and_query_selector(page, selector, timeout = 500) { +async function waitAndQuerySelector(page, selector, timeout = 500) { await page.waitForSelector(selector, {visible: true, timeout}); return await page.$(selector); } // other helpers -function rnd_int(max) { +function randomInt(max) { return Math.ceil(Math.random()*max); } -function riot_url(path) { +function riotUrl(path) { return riotserver + path; } @@ -102,15 +102,15 @@ function delay(ms) { } module.exports = { - try_get_innertext, - new_page, - log_console, - log_xhr_requests, - get_outer_html, - print_elements, - replace_input_text, - wait_and_query_selector, - rnd_int, - riot_url, + tryGetInnertext, + newPage, + logConsole, + logXHRRequests, + getOuterHTML, + printElements, + replaceInputText, + waitAndQuerySelector, + randomInt, + riotUrl, delay, } \ No newline at end of file diff --git a/start.js b/start.js index 002019438a..82b567c566 100644 --- a/start.js +++ b/start.js @@ -17,53 +17,46 @@ limitations under the License. const puppeteer = require('puppeteer'); const helpers = require('./helpers'); const assert = require('assert'); -const do_signup = require('./tests/signup'); -const test_title = require('./tests/loads'); -const join_room = require('./tests/join_room'); + +const signup = require('./tests/signup'); +const join = require('./tests/join'); global.riotserver = 'http://localhost:8080'; global.homeserver = 'http://localhost:8008'; global.browser = null; -async function run_tests() { - await start_session(); - - process.stdout.write(`* testing riot loads ... `); - await test_title(); - process.stdout.write('done\n'); - - - - const page = await helpers.new_page(); - const username = 'bruno-' + helpers.rnd_int(10000); +async function runTests() { + await startSession(); + const page = await helpers.newPage(); + const username = 'bruno-' + helpers.randomInt(10000); const password = 'testtest'; process.stdout.write(`* signing up as ${username} ... `); - await do_signup(page, username, password, homeserver); + await signup(page, username, password, homeserver); process.stdout.write('done\n'); const room = 'test'; process.stdout.write(`* joining room ${room} ... `); - await join_room(page, room); + await join(page, room); process.stdout.write('done\n'); - await end_session(); + await endSession(); } -async function start_session() { +async function startSession() { global.browser = await puppeteer.launch(); } -function end_session() { +function endSession() { return browser.close(); } -function on_success() { +function onSuccess() { console.log('all tests finished successfully'); } -function on_failure(err) { +function onFailure(err) { console.log('failure: ', err); process.exit(-1); } -run_tests().then(on_success, on_failure); \ No newline at end of file +runTests().then(onSuccess, onFailure); \ No newline at end of file diff --git a/tests/join_room.js b/tests/join.js similarity index 51% rename from tests/join_room.js rename to tests/join.js index 7975fad648..ea16a93936 100644 --- a/tests/join_room.js +++ b/tests/join.js @@ -17,19 +17,19 @@ limitations under the License. const helpers = require('../helpers'); const assert = require('assert'); -module.exports = async function join_room(page, room_name) { +module.exports = async function join(page, roomName) { //TODO: brittle selector - const directory_button = await helpers.wait_and_query_selector(page, '.mx_RoleButton[aria-label="Room directory"]'); - await directory_button.click(); + const directoryButton = await helpers.waitAndQuerySelector(page, '.mx_RoleButton[aria-label="Room directory"]'); + await directoryButton.click(); - const room_input = await helpers.wait_and_query_selector(page, '.mx_DirectorySearchBox_input'); - await helpers.replace_input_text(room_input, room_name); + const roomInput = await helpers.waitAndQuerySelector(page, '.mx_DirectorySearchBox_input'); + await helpers.replaceInputText(roomInput, roomName); - const first_room_label = await helpers.wait_and_query_selector(page, '.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child'); - await first_room_label.click(); + const firstRoomLabel = await helpers.waitAndQuerySelector(page, '.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child'); + await firstRoomLabel.click(); - const join_link = await helpers.wait_and_query_selector(page, '.mx_RoomPreviewBar_join_text a'); - await join_link.click(); + const joinLink = await helpers.waitAndQuerySelector(page, '.mx_RoomPreviewBar_join_text a'); + await joinLink.click(); await page.waitForSelector('.mx_MessageComposer'); } \ No newline at end of file diff --git a/tests/loads.js b/tests/loads.js deleted file mode 100644 index 7136b934db..0000000000 --- a/tests/loads.js +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -const helpers = require('../helpers'); -const assert = require('assert'); - -module.exports = async function test_title() { - const page = await browser.newPage(); - await page.goto(helpers.riot_url('/')); - const title = await page.title(); - assert.strictEqual(title, "Riot"); -}; \ No newline at end of file diff --git a/tests/signup.js b/tests/signup.js index 155c5a1e0a..43fe6a87d4 100644 --- a/tests/signup.js +++ b/tests/signup.js @@ -17,62 +17,62 @@ limitations under the License. const helpers = require('../helpers'); const assert = require('assert'); -module.exports = async function do_signup(page, username, password, homeserver) { - const console_logs = helpers.log_console(page); - const xhr_logs = helpers.log_xhr_requests(page); - await page.goto(helpers.riot_url('/#/register')); +module.exports = async function signup(page, username, password, homeserver) { + const consoleLogs = helpers.logConsole(page); + const xhrLogs = helpers.logXHRRequests(page); + await page.goto(helpers.riotUrl('/#/register')); //click 'Custom server' radio button await page.waitForSelector('#advanced', {visible: true, timeout: 500}); await page.click('#advanced'); //fill out form await page.waitForSelector('.mx_ServerConfig', {visible: true, timeout: 500}); - const login_fields = await page.$$('.mx_Login_field'); - assert.strictEqual(login_fields.length, 7); - const username_field = login_fields[2]; - const password_field = login_fields[3]; - const password_repeat_field = login_fields[4]; - const hsurl_field = login_fields[5]; - await helpers.replace_input_text(username_field, username); - await helpers.replace_input_text(password_field, password); - await helpers.replace_input_text(password_repeat_field, password); - await helpers.replace_input_text(hsurl_field, homeserver); + const loginFields = await page.$$('.mx_Login_field'); + assert.strictEqual(loginFields.length, 7); + const usernameField = loginFields[2]; + const passwordField = loginFields[3]; + const passwordRepeatField = loginFields[4]; + const hsurlField = loginFields[5]; + await helpers.replaceInputText(usernameField, username); + await helpers.replaceInputText(passwordField, password); + await helpers.replaceInputText(passwordRepeatField, password); + await helpers.replaceInputText(hsurlField, homeserver); //wait over a second because Registration/ServerConfig have a 1000ms //delay to internally set the homeserver url //see Registration::render and ServerConfig::props::delayTimeMs await helpers.delay(1200); /// focus on the button to make sure error validation /// has happened before checking the form is good to go - const register_button = await page.$('.mx_Login_submit'); - await register_button.focus(); + const registerButton = await page.$('.mx_Login_submit'); + await registerButton.focus(); //check no errors - const error_text = await helpers.try_get_innertext(page, '.mx_Login_error'); + const error_text = await helpers.tryGetInnertext(page, '.mx_Login_error'); assert.strictEqual(!!error_text, false); //submit form await page.screenshot({path: "beforesubmit.png", fullPage: true}); - await register_button.click(); + await registerButton.click(); //confirm dialog saying you cant log back in without e-mail await page.waitForSelector('.mx_QuestionDialog', {visible: true, timeout: 500}); - const continue_button = await page.$('.mx_QuestionDialog button.mx_Dialog_primary'); - //await helpers.print_elements('continue_button', [continue_button]); - await continue_button.click(); + const continueButton = await page.$('.mx_QuestionDialog button.mx_Dialog_primary'); + //await helpers.printElements('continueButton', [continueButton]); + await continueButton.click(); //wait for registration to finish so the hash gets set //onhashchange better? await helpers.delay(1000); /* await page.screenshot({path: "afterlogin.png", fullPage: true}); console.log('browser console logs:'); - console.log(console_logs.logs()); + console.log(consoleLogs.logs()); console.log('xhr logs:'); - console.log(xhr_logs.logs()); + console.log(xhrLogs.logs()); */ - //print_elements('page', await page.$('#matrixchat')); + //printElements('page', await page.$('#matrixchat')); // await navigation_promise; //await page.waitForSelector('.mx_MatrixChat', {visible: true, timeout: 3000}); const url = page.url(); - assert.strictEqual(url, helpers.riot_url('/#/home')); + assert.strictEqual(url, helpers.riotUrl('/#/home')); } \ No newline at end of file From 9c5e43a693778c33e1e673bac2cfe39d06f28357 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 9 Jul 2018 18:40:25 +0200 Subject: [PATCH 0014/2372] cleanup --- start.js | 13 +++---------- tests/signup.js | 8 +++----- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/start.js b/start.js index 82b567c566..0cc80f2fde 100644 --- a/start.js +++ b/start.js @@ -26,8 +26,9 @@ global.homeserver = 'http://localhost:8008'; global.browser = null; async function runTests() { - await startSession(); + global.browser = await puppeteer.launch(); const page = await helpers.newPage(); + const username = 'bruno-' + helpers.randomInt(10000); const password = 'testtest'; process.stdout.write(`* signing up as ${username} ... `); @@ -39,15 +40,7 @@ async function runTests() { await join(page, room); process.stdout.write('done\n'); - await endSession(); -} - -async function startSession() { - global.browser = await puppeteer.launch(); -} - -function endSession() { - return browser.close(); + await browser.close(); } function onSuccess() { diff --git a/tests/signup.js b/tests/signup.js index 43fe6a87d4..2a0d6dc9b4 100644 --- a/tests/signup.js +++ b/tests/signup.js @@ -22,8 +22,8 @@ module.exports = async function signup(page, username, password, homeserver) { const xhrLogs = helpers.logXHRRequests(page); await page.goto(helpers.riotUrl('/#/register')); //click 'Custom server' radio button - await page.waitForSelector('#advanced', {visible: true, timeout: 500}); - await page.click('#advanced'); + const advancedRadioButton = await helpers.waitAndQuerySelector(page, '#advanced'); + await advancedRadioButton.click(); //fill out form await page.waitForSelector('.mx_ServerConfig', {visible: true, timeout: 500}); @@ -53,9 +53,7 @@ module.exports = async function signup(page, username, password, homeserver) { await registerButton.click(); //confirm dialog saying you cant log back in without e-mail - await page.waitForSelector('.mx_QuestionDialog', {visible: true, timeout: 500}); - const continueButton = await page.$('.mx_QuestionDialog button.mx_Dialog_primary'); - //await helpers.printElements('continueButton', [continueButton]); + const continueButton = await helpers.waitAndQuerySelector(page, '.mx_QuestionDialog button.mx_Dialog_primary'); await continueButton.click(); //wait for registration to finish so the hash gets set //onhashchange better? From 9a2d32e64208af6379b3e2334598b649c9274d6e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Jul 2018 19:26:47 +0200 Subject: [PATCH 0015/2372] accept terms when joining --- helpers.js | 18 ++++++++++++++++++ start.js | 2 +- tests/join.js | 18 +++++++++++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/helpers.js b/helpers.js index 3e2467e622..d57595f377 100644 --- a/helpers.js +++ b/helpers.js @@ -87,6 +87,23 @@ async function waitAndQuerySelector(page, selector, timeout = 500) { return await page.$(selector); } +function waitForNewPage(timeout = 500) { + return new Promise((resolve, reject) => { + const timeoutHandle = setTimeout(() => { + browser.removeEventListener('targetcreated', callback); + reject(new Error(`timeout of ${timeout}ms for waitForNewPage elapsed`)); + }, timeout); + + const callback = async (target) => { + clearTimeout(timeoutHandle); + const page = await target.page(); + resolve(page); + }; + + browser.once('targetcreated', callback); + }); +} + // other helpers function randomInt(max) { @@ -110,6 +127,7 @@ module.exports = { printElements, replaceInputText, waitAndQuerySelector, + waitForNewPage, randomInt, riotUrl, delay, diff --git a/start.js b/start.js index 0cc80f2fde..8a3ceb354b 100644 --- a/start.js +++ b/start.js @@ -37,7 +37,7 @@ async function runTests() { const room = 'test'; process.stdout.write(`* joining room ${room} ... `); - await join(page, room); + await join(page, room, true); process.stdout.write('done\n'); await browser.close(); diff --git a/tests/join.js b/tests/join.js index ea16a93936..79990af3a2 100644 --- a/tests/join.js +++ b/tests/join.js @@ -17,7 +17,7 @@ limitations under the License. const helpers = require('../helpers'); const assert = require('assert'); -module.exports = async function join(page, roomName) { +module.exports = async function join(page, roomName, acceptTerms = false) { //TODO: brittle selector const directoryButton = await helpers.waitAndQuerySelector(page, '.mx_RoleButton[aria-label="Room directory"]'); await directoryButton.click(); @@ -31,5 +31,21 @@ module.exports = async function join(page, roomName) { const joinLink = await helpers.waitAndQuerySelector(page, '.mx_RoomPreviewBar_join_text a'); await joinLink.click(); + if (acceptTerms) { + const reviewTermsButton = await helpers.waitAndQuerySelector(page, '.mx_QuestionDialog button.mx_Dialog_primary'); + const termsPagePromise = helpers.waitForNewPage(); + await reviewTermsButton.click(); + const termsPage = await termsPagePromise; + const acceptButton = await termsPage.$('input[type=submit]'); + await acceptButton.click(); + await helpers.delay(500); //TODO yuck, timers + //try to join again after accepting the terms + + //TODO need to do this because joinLink is detached after switching target + const joinLink2 = await helpers.waitAndQuerySelector(page, '.mx_RoomPreviewBar_join_text a'); + await joinLink2.click(); + } + + await page.waitForSelector('.mx_MessageComposer'); } \ No newline at end of file From 83eebfdecc3e8e5b86eb59f765256b7b8b06d2ce Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 16 Jul 2018 18:10:33 +0200 Subject: [PATCH 0016/2372] script to install local synapse --- synapse/.gitignore | 2 + .../config-templates/consent/homeserver.yaml | 697 ++++++++++++++++++ .../consent/res/templates/privacy/en/1.0.html | 23 + .../res/templates/privacy/en/success.html | 9 + synapse/install.sh | 33 + 5 files changed, 764 insertions(+) create mode 100644 synapse/.gitignore create mode 100644 synapse/config-templates/consent/homeserver.yaml create mode 100644 synapse/config-templates/consent/res/templates/privacy/en/1.0.html create mode 100644 synapse/config-templates/consent/res/templates/privacy/en/success.html create mode 100644 synapse/install.sh diff --git a/synapse/.gitignore b/synapse/.gitignore new file mode 100644 index 0000000000..aed68e9f30 --- /dev/null +++ b/synapse/.gitignore @@ -0,0 +1,2 @@ +installations +synapse.zip \ No newline at end of file diff --git a/synapse/config-templates/consent/homeserver.yaml b/synapse/config-templates/consent/homeserver.yaml new file mode 100644 index 0000000000..6ba15c9af0 --- /dev/null +++ b/synapse/config-templates/consent/homeserver.yaml @@ -0,0 +1,697 @@ +# vim:ft=yaml +# PEM encoded X509 certificate for TLS. +# You can replace the self-signed certificate that synapse +# autogenerates on launch with your own SSL certificate + key pair +# if you like. Any required intermediary certificates can be +# appended after the primary certificate in hierarchical order. +tls_certificate_path: "{{SYNAPSE_ROOT}}localhost.tls.crt" + +# PEM encoded private key for TLS +tls_private_key_path: "{{SYNAPSE_ROOT}}localhost.tls.key" + +# PEM dh parameters for ephemeral keys +tls_dh_params_path: "{{SYNAPSE_ROOT}}localhost.tls.dh" + +# Don't bind to the https port +no_tls: True + +# List of allowed TLS fingerprints for this server to publish along +# with the signing keys for this server. Other matrix servers that +# make HTTPS requests to this server will check that the TLS +# certificates returned by this server match one of the fingerprints. +# +# Synapse automatically adds the fingerprint of its own certificate +# to the list. So if federation traffic is handled directly by synapse +# then no modification to the list is required. +# +# If synapse is run behind a load balancer that handles the TLS then it +# will be necessary to add the fingerprints of the certificates used by +# the loadbalancers to this list if they are different to the one +# synapse is using. +# +# Homeservers are permitted to cache the list of TLS fingerprints +# returned in the key responses up to the "valid_until_ts" returned in +# key. It may be necessary to publish the fingerprints of a new +# certificate and wait until the "valid_until_ts" of the previous key +# responses have passed before deploying it. +# +# You can calculate a fingerprint from a given TLS listener via: +# openssl s_client -connect $host:$port < /dev/null 2> /dev/null | +# openssl x509 -outform DER | openssl sha256 -binary | base64 | tr -d '=' +# or by checking matrix.org/federationtester/api/report?server_name=$host +# +tls_fingerprints: [] +# tls_fingerprints: [{"sha256": ""}] + + +## Server ## + +# The domain name of the server, with optional explicit port. +# This is used by remote servers to connect to this server, +# e.g. matrix.org, localhost:8080, etc. +# This is also the last part of your UserID. +server_name: "localhost" + +# When running as a daemon, the file to store the pid in +pid_file: {{SYNAPSE_ROOT}}homeserver.pid + +# CPU affinity mask. Setting this restricts the CPUs on which the +# process will be scheduled. It is represented as a bitmask, with the +# lowest order bit corresponding to the first logical CPU and the +# highest order bit corresponding to the last logical CPU. Not all CPUs +# may exist on a given system but a mask may specify more CPUs than are +# present. +# +# For example: +# 0x00000001 is processor #0, +# 0x00000003 is processors #0 and #1, +# 0xFFFFFFFF is all processors (#0 through #31). +# +# Pinning a Python process to a single CPU is desirable, because Python +# is inherently single-threaded due to the GIL, and can suffer a +# 30-40% slowdown due to cache blow-out and thread context switching +# if the scheduler happens to schedule the underlying threads across +# different cores. See +# https://www.mirantis.com/blog/improve-performance-python-programs-restricting-single-cpu/. +# +# cpu_affinity: 0xFFFFFFFF + +# Whether to serve a web client from the HTTP/HTTPS root resource. +web_client: True + +# The root directory to server for the above web client. +# If left undefined, synapse will serve the matrix-angular-sdk web client. +# Make sure matrix-angular-sdk is installed with pip if web_client is True +# and web_client_location is undefined +# web_client_location: "/path/to/web/root" + +# The public-facing base URL for the client API (not including _matrix/...) +public_baseurl: http://localhost:8008/ + +# Set the soft limit on the number of file descriptors synapse can use +# Zero is used to indicate synapse should set the soft limit to the +# hard limit. +soft_file_limit: 0 + +# The GC threshold parameters to pass to `gc.set_threshold`, if defined +# gc_thresholds: [700, 10, 10] + +# Set the limit on the returned events in the timeline in the get +# and sync operations. The default value is -1, means no upper limit. +# filter_timeline_limit: 5000 + +# Whether room invites to users on this server should be blocked +# (except those sent by local server admins). The default is False. +# block_non_admin_invites: True + +# Restrict federation to the following whitelist of domains. +# N.B. we recommend also firewalling your federation listener to limit +# inbound federation traffic as early as possible, rather than relying +# purely on this application-layer restriction. If not specified, the +# default is to whitelist everything. +# +# federation_domain_whitelist: +# - lon.example.com +# - nyc.example.com +# - syd.example.com + +# List of ports that Synapse should listen on, their purpose and their +# configuration. +listeners: + # Main HTTPS listener + # For when matrix traffic is sent directly to synapse. + - + # The port to listen for HTTPS requests on. + port: 8448 + + # Local addresses to listen on. + # On Linux and Mac OS, `::` will listen on all IPv4 and IPv6 + # addresses by default. For most other OSes, this will only listen + # on IPv6. + bind_addresses: + - '::' + - '0.0.0.0' + + # This is a 'http' listener, allows us to specify 'resources'. + type: http + + tls: true + + # Use the X-Forwarded-For (XFF) header as the client IP and not the + # actual client IP. + x_forwarded: false + + # List of HTTP resources to serve on this listener. + resources: + - + # List of resources to host on this listener. + names: + - client # The client-server APIs, both v1 and v2 + - webclient # The bundled webclient. + + # Should synapse compress HTTP responses to clients that support it? + # This should be disabled if running synapse behind a load balancer + # that can do automatic compression. + compress: true + + - names: [federation] # Federation APIs + compress: false + + # optional list of additional endpoints which can be loaded via + # dynamic modules + # additional_resources: + # "/_matrix/my/custom/endpoint": + # module: my_module.CustomRequestHandler + # config: {} + + # Unsecure HTTP listener, + # For when matrix traffic passes through loadbalancer that unwraps TLS. + - port: 8008 + tls: false + bind_addresses: ['::', '0.0.0.0'] + type: http + + x_forwarded: false + + resources: + - names: [client, webclient, consent] + compress: true + - names: [federation] + compress: false + + # Turn on the twisted ssh manhole service on localhost on the given + # port. + # - port: 9000 + # bind_addresses: ['::1', '127.0.0.1'] + # type: manhole + + +# Database configuration +database: + # The database engine name + name: "sqlite3" + # Arguments to pass to the engine + args: + # Path to the database + database: "{{SYNAPSE_ROOT}}homeserver.db" + +# Number of events to cache in memory. +event_cache_size: "10K" + + + +# A yaml python logging config file +log_config: "{{SYNAPSE_ROOT}}localhost.log.config" + + +## Ratelimiting ## + +# Number of messages a client can send per second +rc_messages_per_second: 0.2 + +# Number of message a client can send before being throttled +rc_message_burst_count: 10.0 + +# The federation window size in milliseconds +federation_rc_window_size: 1000 + +# The number of federation requests from a single server in a window +# before the server will delay processing the request. +federation_rc_sleep_limit: 10 + +# The duration in milliseconds to delay processing events from +# remote servers by if they go over the sleep limit. +federation_rc_sleep_delay: 500 + +# The maximum number of concurrent federation requests allowed +# from a single server +federation_rc_reject_limit: 50 + +# The number of federation requests to concurrently process from a +# single server +federation_rc_concurrent: 3 + + + +# Directory where uploaded images and attachments are stored. +media_store_path: "{{SYNAPSE_ROOT}}media_store" + +# Media storage providers allow media to be stored in different +# locations. +# media_storage_providers: +# - module: file_system +# # Whether to write new local files. +# store_local: false +# # Whether to write new remote media +# store_remote: false +# # Whether to block upload requests waiting for write to this +# # provider to complete +# store_synchronous: false +# config: +# directory: /mnt/some/other/directory + +# Directory where in-progress uploads are stored. +uploads_path: "{{SYNAPSE_ROOT}}uploads" + +# The largest allowed upload size in bytes +max_upload_size: "10M" + +# Maximum number of pixels that will be thumbnailed +max_image_pixels: "32M" + +# Whether to generate new thumbnails on the fly to precisely match +# the resolution requested by the client. If true then whenever +# a new resolution is requested by the client the server will +# generate a new thumbnail. If false the server will pick a thumbnail +# from a precalculated list. +dynamic_thumbnails: false + +# List of thumbnail to precalculate when an image is uploaded. +thumbnail_sizes: +- width: 32 + height: 32 + method: crop +- width: 96 + height: 96 + method: crop +- width: 320 + height: 240 + method: scale +- width: 640 + height: 480 + method: scale +- width: 800 + height: 600 + method: scale + +# Is the preview URL API enabled? If enabled, you *must* specify +# an explicit url_preview_ip_range_blacklist of IPs that the spider is +# denied from accessing. +url_preview_enabled: False + +# List of IP address CIDR ranges that the URL preview spider is denied +# from accessing. There are no defaults: you must explicitly +# specify a list for URL previewing to work. You should specify any +# internal services in your network that you do not want synapse to try +# to connect to, otherwise anyone in any Matrix room could cause your +# synapse to issue arbitrary GET requests to your internal services, +# causing serious security issues. +# +# url_preview_ip_range_blacklist: +# - '127.0.0.0/8' +# - '10.0.0.0/8' +# - '172.16.0.0/12' +# - '192.168.0.0/16' +# - '100.64.0.0/10' +# - '169.254.0.0/16' +# - '::1/128' +# - 'fe80::/64' +# - 'fc00::/7' +# +# List of IP address CIDR ranges that the URL preview spider is allowed +# to access even if they are specified in url_preview_ip_range_blacklist. +# This is useful for specifying exceptions to wide-ranging blacklisted +# target IP ranges - e.g. for enabling URL previews for a specific private +# website only visible in your network. +# +# url_preview_ip_range_whitelist: +# - '192.168.1.1' + +# Optional list of URL matches that the URL preview spider is +# denied from accessing. You should use url_preview_ip_range_blacklist +# in preference to this, otherwise someone could define a public DNS +# entry that points to a private IP address and circumvent the blacklist. +# This is more useful if you know there is an entire shape of URL that +# you know that will never want synapse to try to spider. +# +# Each list entry is a dictionary of url component attributes as returned +# by urlparse.urlsplit as applied to the absolute form of the URL. See +# https://docs.python.org/2/library/urlparse.html#urlparse.urlsplit +# The values of the dictionary are treated as an filename match pattern +# applied to that component of URLs, unless they start with a ^ in which +# case they are treated as a regular expression match. If all the +# specified component matches for a given list item succeed, the URL is +# blacklisted. +# +# url_preview_url_blacklist: +# # blacklist any URL with a username in its URI +# - username: '*' +# +# # blacklist all *.google.com URLs +# - netloc: 'google.com' +# - netloc: '*.google.com' +# +# # blacklist all plain HTTP URLs +# - scheme: 'http' +# +# # blacklist http(s)://www.acme.com/foo +# - netloc: 'www.acme.com' +# path: '/foo' +# +# # blacklist any URL with a literal IPv4 address +# - netloc: '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' + +# The largest allowed URL preview spidering size in bytes +max_spider_size: "10M" + + + + +## Captcha ## +# See docs/CAPTCHA_SETUP for full details of configuring this. + +# This Home Server's ReCAPTCHA public key. +recaptcha_public_key: "YOUR_PUBLIC_KEY" + +# This Home Server's ReCAPTCHA private key. +recaptcha_private_key: "YOUR_PRIVATE_KEY" + +# Enables ReCaptcha checks when registering, preventing signup +# unless a captcha is answered. Requires a valid ReCaptcha +# public/private key. +enable_registration_captcha: False + +# A secret key used to bypass the captcha test entirely. +#captcha_bypass_secret: "YOUR_SECRET_HERE" + +# The API endpoint to use for verifying m.login.recaptcha responses. +recaptcha_siteverify_api: "https://www.google.com/recaptcha/api/siteverify" + + +## Turn ## + +# The public URIs of the TURN server to give to clients +turn_uris: [] + +# The shared secret used to compute passwords for the TURN server +turn_shared_secret: "YOUR_SHARED_SECRET" + +# The Username and password if the TURN server needs them and +# does not use a token +#turn_username: "TURNSERVER_USERNAME" +#turn_password: "TURNSERVER_PASSWORD" + +# How long generated TURN credentials last +turn_user_lifetime: "1h" + +# Whether guests should be allowed to use the TURN server. +# This defaults to True, otherwise VoIP will be unreliable for guests. +# However, it does introduce a slight security risk as it allows users to +# connect to arbitrary endpoints without having first signed up for a +# valid account (e.g. by passing a CAPTCHA). +turn_allow_guests: True + + +## Registration ## + +# Enable registration for new users. +enable_registration: True + +# The user must provide all of the below types of 3PID when registering. +# +# registrations_require_3pid: +# - email +# - msisdn + +# Mandate that users are only allowed to associate certain formats of +# 3PIDs with accounts on this server. +# +# allowed_local_3pids: +# - medium: email +# pattern: ".*@matrix\.org" +# - medium: email +# pattern: ".*@vector\.im" +# - medium: msisdn +# pattern: "\+44" + +# If set, allows registration by anyone who also has the shared +# secret, even if registration is otherwise disabled. +registration_shared_secret: "{{REGISTRATION_SHARED_SECRET}}" + +# Set the number of bcrypt rounds used to generate password hash. +# Larger numbers increase the work factor needed to generate the hash. +# The default number is 12 (which equates to 2^12 rounds). +# N.B. that increasing this will exponentially increase the time required +# to register or login - e.g. 24 => 2^24 rounds which will take >20 mins. +bcrypt_rounds: 12 + +# Allows users to register as guests without a password/email/etc, and +# participate in rooms hosted on this server which have been made +# accessible to anonymous users. +allow_guest_access: False + +# The list of identity servers trusted to verify third party +# identifiers by this server. +trusted_third_party_id_servers: + - matrix.org + - vector.im + - riot.im + +# Users who register on this homeserver will automatically be joined +# to these roomsS +#auto_join_rooms: +# - "#example:example.com" + + +## Metrics ### + +# Enable collection and rendering of performance metrics +enable_metrics: False +report_stats: False + + +## API Configuration ## + +# A list of event types that will be included in the room_invite_state +room_invite_state_types: + - "m.room.join_rules" + - "m.room.canonical_alias" + - "m.room.avatar" + - "m.room.name" + + +# A list of application service config file to use +app_service_config_files: [] + + +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" + +# Used to enable access token expiration. +expire_access_token: False + +# a secret which is used to calculate HMACs for form values, to stop +# falsification of values +form_secret: "{{FORM_SECRET}}" + +## Signing Keys ## + +# Path to the signing key to sign messages with +signing_key_path: "{{SYNAPSE_ROOT}}localhost.signing.key" + +# The keys that the server used to sign messages with but won't use +# to sign new messages. E.g. it has lost its private key +old_signing_keys: {} +# "ed25519:auto": +# # Base64 encoded public key +# key: "The public part of your old signing key." +# # Millisecond POSIX timestamp when the key expired. +# expired_ts: 123456789123 + +# How long key response published by this server is valid for. +# Used to set the valid_until_ts in /key/v2 APIs. +# Determines how quickly servers will query to check which keys +# are still valid. +key_refresh_interval: "1d" # 1 Day.block_non_admin_invites + +# The trusted servers to download signing keys from. +perspectives: + servers: + "matrix.org": + verify_keys: + "ed25519:auto": + key: "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw" + + + +# Enable SAML2 for registration and login. Uses pysaml2 +# config_path: Path to the sp_conf.py configuration file +# idp_redirect_url: Identity provider URL which will redirect +# the user back to /login/saml2 with proper info. +# See pysaml2 docs for format of config. +#saml2_config: +# enabled: true +# config_path: "{{SYNAPSE_ROOT}}sp_conf.py" +# idp_redirect_url: "http://localhost/idp" + + + +# Enable CAS for registration and login. +#cas_config: +# enabled: true +# server_url: "https://cas-server.com" +# service_url: "https://homeserver.domain.com:8448" +# #required_attributes: +# # name: value + + +# The JWT needs to contain a globally unique "sub" (subject) claim. +# +# jwt_config: +# enabled: true +# secret: "a secret" +# algorithm: "HS256" + + + +# Enable password for login. +password_config: + enabled: true + # Uncomment and change to a secret random string for extra security. + # DO NOT CHANGE THIS AFTER INITIAL SETUP! + #pepper: "" + + + +# Enable sending emails for notification events +# Defining a custom URL for Riot is only needed if email notifications +# should contain links to a self-hosted installation of Riot; when set +# the "app_name" setting is ignored. +# +# If your SMTP server requires authentication, the optional smtp_user & +# smtp_pass variables should be used +# +#email: +# enable_notifs: false +# smtp_host: "localhost" +# smtp_port: 25 +# smtp_user: "exampleusername" +# smtp_pass: "examplepassword" +# require_transport_security: False +# notif_from: "Your Friendly %(app)s Home Server " +# app_name: Matrix +# template_dir: res/templates +# notif_template_html: notif_mail.html +# notif_template_text: notif_mail.txt +# notif_for_new_users: True +# riot_base_url: "http://localhost/riot" + + +# password_providers: +# - module: "ldap_auth_provider.LdapAuthProvider" +# config: +# enabled: true +# uri: "ldap://ldap.example.com:389" +# start_tls: true +# base: "ou=users,dc=example,dc=com" +# attributes: +# uid: "cn" +# mail: "email" +# name: "givenName" +# #bind_dn: +# #bind_password: +# #filter: "(objectClass=posixAccount)" + + + +# Clients requesting push notifications can either have the body of +# the message sent in the notification poke along with other details +# like the sender, or just the event ID and room ID (`event_id_only`). +# If clients choose the former, this option controls whether the +# notification request includes the content of the event (other details +# like the sender are still included). For `event_id_only` push, it +# has no effect. + +# For modern android devices the notification content will still appear +# because it is loaded by the app. iPhone, however will send a +# notification saying only that a message arrived and who it came from. +# +#push: +# include_content: true + + +# spam_checker: +# module: "my_custom_project.SuperSpamChecker" +# config: +# example_option: 'things' + + +# Whether to allow non server admins to create groups on this server +enable_group_creation: false + +# If enabled, non server admins can only create groups with local parts +# starting with this prefix +# group_creation_prefix: "unofficial/" + + + +# User Directory configuration +# +# 'search_all_users' defines whether to search all users visible to your HS +# when searching the user directory, rather than limiting to users visible +# in public rooms. Defaults to false. If you set it True, you'll have to run +# UPDATE user_directory_stream_pos SET stream_id = NULL; +# on your database to tell it to rebuild the user_directory search indexes. +# +#user_directory: +# search_all_users: false + + +# User Consent configuration +# +# for detailed instructions, see +# https://github.com/matrix-org/synapse/blob/master/docs/consent_tracking.md +# +# Parts of this section are required if enabling the 'consent' resource under +# 'listeners', in particular 'template_dir' and 'version'. +# +# 'template_dir' gives the location of the templates for the HTML forms. +# This directory should contain one subdirectory per language (eg, 'en', 'fr'), +# and each language directory should contain the policy document (named as +# '.html') and a success page (success.html). +# +# 'version' specifies the 'current' version of the policy document. It defines +# the version to be served by the consent resource if there is no 'v' +# parameter. +# +# 'server_notice_content', if enabled, will send a user a "Server Notice" +# asking them to consent to the privacy policy. The 'server_notices' section +# must also be configured for this to work. Notices will *not* be sent to +# guest users unless 'send_server_notice_to_guests' is set to true. +# +# 'block_events_error', if set, will block any attempts to send events +# until the user consents to the privacy policy. The value of the setting is +# used as the text of the error. +# +user_consent: + template_dir: res/templates/privacy + version: 1.0 + server_notice_content: + msgtype: m.text + body: >- + To continue using this homeserver you must review and agree to the + terms and conditions at %(consent_uri)s + send_server_notice_to_guests: True + block_events_error: >- + To continue using this homeserver you must review and agree to the + terms and conditions at %(consent_uri)s + + + +# Server Notices room configuration +# +# Uncomment this section to enable a room which can be used to send notices +# from the server to users. It is a special room which cannot be left; notices +# come from a special "notices" user id. +# +# If you uncomment this section, you *must* define the system_mxid_localpart +# setting, which defines the id of the user which will be used to send the +# notices. +# +# It's also possible to override the room name, the display name of the +# "notices" user, and the avatar for the user. +# +server_notices: + system_mxid_localpart: notices + system_mxid_display_name: "Server Notices" + system_mxid_avatar_url: "mxc://localhost:8008/oumMVlgDnLYFaPVkExemNVVZ" + room_name: "Server Notices" diff --git a/synapse/config-templates/consent/res/templates/privacy/en/1.0.html b/synapse/config-templates/consent/res/templates/privacy/en/1.0.html new file mode 100644 index 0000000000..d4959b4bcb --- /dev/null +++ b/synapse/config-templates/consent/res/templates/privacy/en/1.0.html @@ -0,0 +1,23 @@ + + + + Test Privacy policy + + + {% if has_consented %} +

+ Thank you, you've already accepted the license. +

+ {% else %} +

+ Please accept the license! +

+
+ + + + +
+ {% endif %} + + \ No newline at end of file diff --git a/synapse/config-templates/consent/res/templates/privacy/en/success.html b/synapse/config-templates/consent/res/templates/privacy/en/success.html new file mode 100644 index 0000000000..abe27d87ca --- /dev/null +++ b/synapse/config-templates/consent/res/templates/privacy/en/success.html @@ -0,0 +1,9 @@ + + + + Test Privacy policy + + +

Danke schon

+ + \ No newline at end of file diff --git a/synapse/install.sh b/synapse/install.sh new file mode 100644 index 0000000000..47f1f746fc --- /dev/null +++ b/synapse/install.sh @@ -0,0 +1,33 @@ +# config +SYNAPSE_BRANCH=master +INSTALLATION_NAME=consent +SERVER_DIR=installations/$INSTALLATION_NAME +CONFIG_TEMPLATE=consent +PORT=8008 +# set current directory to script directory +BASE_DIR=$(realpath $(dirname $0)) +pushd $BASE_DIR +mkdir -p installations/ +curl https://codeload.github.com/matrix-org/synapse/zip/$SYNAPSE_BRANCH --output synapse.zip +unzip synapse.zip +mv synapse-$SYNAPSE_BRANCH $SERVER_DIR +pushd $SERVER_DIR +virtualenv -p python2.7 env +source env/bin/activate +pip install --upgrade pip +pip install --upgrade setuptools +pip install . +python -m synapse.app.homeserver \ + --server-name localhost \ + --config-path homeserver.yaml \ + --generate-config \ + --report-stats=no +# apply configuration +cp -r $BASE_DIR/config-templates/$CONFIG_TEMPLATE/. ./ +sed -i "s#{{SYNAPSE_ROOT}}#$(pwd)/#g" homeserver.yaml +sed -i "s#{{SYNAPSE_PORT}}#${PORT}/#g" homeserver.yaml +sed -i "s#{{FORM_SECRET}}#$(uuidgen)#g" homeserver.yaml +sed -i "s#{{REGISTRATION_SHARED_SECRET}}#$(uuidgen)#g" homeserver.yaml +sed -i "s#{{MACAROON_SECRET_KEY}}#$(uuidgen)#g" homeserver.yaml +popd #back to synapse root dir +popd #back to wherever we were \ No newline at end of file From fc4c425a22c11737d12b39ca83fc11efd1046d1e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 17 Jul 2018 12:38:20 +0200 Subject: [PATCH 0017/2372] do accepting terms as part of signup since we try to create a room with riot-bot after login, which fails with consent warning --- tests/join.js | 18 +----------------- tests/signup.js | 13 ++++++++++++- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/tests/join.js b/tests/join.js index 79990af3a2..ea16a93936 100644 --- a/tests/join.js +++ b/tests/join.js @@ -17,7 +17,7 @@ limitations under the License. const helpers = require('../helpers'); const assert = require('assert'); -module.exports = async function join(page, roomName, acceptTerms = false) { +module.exports = async function join(page, roomName) { //TODO: brittle selector const directoryButton = await helpers.waitAndQuerySelector(page, '.mx_RoleButton[aria-label="Room directory"]'); await directoryButton.click(); @@ -31,21 +31,5 @@ module.exports = async function join(page, roomName, acceptTerms = false) { const joinLink = await helpers.waitAndQuerySelector(page, '.mx_RoomPreviewBar_join_text a'); await joinLink.click(); - if (acceptTerms) { - const reviewTermsButton = await helpers.waitAndQuerySelector(page, '.mx_QuestionDialog button.mx_Dialog_primary'); - const termsPagePromise = helpers.waitForNewPage(); - await reviewTermsButton.click(); - const termsPage = await termsPagePromise; - const acceptButton = await termsPage.$('input[type=submit]'); - await acceptButton.click(); - await helpers.delay(500); //TODO yuck, timers - //try to join again after accepting the terms - - //TODO need to do this because joinLink is detached after switching target - const joinLink2 = await helpers.waitAndQuerySelector(page, '.mx_RoomPreviewBar_join_text a'); - await joinLink2.click(); - } - - await page.waitForSelector('.mx_MessageComposer'); } \ No newline at end of file diff --git a/tests/signup.js b/tests/signup.js index 2a0d6dc9b4..5560fc56cf 100644 --- a/tests/signup.js +++ b/tests/signup.js @@ -57,7 +57,6 @@ module.exports = async function signup(page, username, password, homeserver) { await continueButton.click(); //wait for registration to finish so the hash gets set //onhashchange better? - await helpers.delay(1000); /* await page.screenshot({path: "afterlogin.png", fullPage: true}); console.log('browser console logs:'); @@ -66,11 +65,23 @@ module.exports = async function signup(page, username, password, homeserver) { console.log(xhrLogs.logs()); */ + await acceptTerms(page); + await helpers.delay(10000); //printElements('page', await page.$('#matrixchat')); // await navigation_promise; //await page.waitForSelector('.mx_MatrixChat', {visible: true, timeout: 3000}); const url = page.url(); assert.strictEqual(url, helpers.riotUrl('/#/home')); +} + +async function acceptTerms(page) { + const reviewTermsButton = await helpers.waitAndQuerySelector(page, '.mx_QuestionDialog button.mx_Dialog_primary'); + const termsPagePromise = helpers.waitForNewPage(); + await reviewTermsButton.click(); + const termsPage = await termsPagePromise; + const acceptButton = await termsPage.$('input[type=submit]'); + await acceptButton.click(); + await helpers.delay(500); //TODO yuck, timers } \ No newline at end of file From dcf4be79b7336716b3a2379e7fd47c0b3564edb3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 18 Jul 2018 17:52:29 +0200 Subject: [PATCH 0018/2372] add start and stop scripts for synapse --- synapse/start.sh | 4 ++++ synapse/stop.sh | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 synapse/start.sh create mode 100644 synapse/stop.sh diff --git a/synapse/start.sh b/synapse/start.sh new file mode 100644 index 0000000000..dec9a7d035 --- /dev/null +++ b/synapse/start.sh @@ -0,0 +1,4 @@ +pushd installations/consent +source env/bin/activate +./synctl start +popd \ No newline at end of file diff --git a/synapse/stop.sh b/synapse/stop.sh new file mode 100644 index 0000000000..916e774a99 --- /dev/null +++ b/synapse/stop.sh @@ -0,0 +1,4 @@ +pushd installations/consent +source env/bin/activate +./synctl stop +popd \ No newline at end of file From 2cb83334ed9c37aa7acc0fc01610af7aa554bace Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 18 Jul 2018 17:52:51 +0200 Subject: [PATCH 0019/2372] add script to install, start and stop riot --- riot/.gitignore | 1 + riot/install.sh | 8 ++++++++ riot/start.sh | 5 +++++ riot/stop.sh | 3 +++ 4 files changed, 17 insertions(+) create mode 100644 riot/.gitignore create mode 100644 riot/install.sh create mode 100644 riot/start.sh create mode 100644 riot/stop.sh diff --git a/riot/.gitignore b/riot/.gitignore new file mode 100644 index 0000000000..ce36d3b374 --- /dev/null +++ b/riot/.gitignore @@ -0,0 +1 @@ +riot-web \ No newline at end of file diff --git a/riot/install.sh b/riot/install.sh new file mode 100644 index 0000000000..3729c3c03a --- /dev/null +++ b/riot/install.sh @@ -0,0 +1,8 @@ +RIOT_BRANCH=master +curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --output riot.zip +unzip riot.zip +rm riot.zip +mv riot-web-${RIOT_BRANCH} riot-web +pushd riot-web +npm install +npm run build diff --git a/riot/start.sh b/riot/start.sh new file mode 100644 index 0000000000..6e9ad218e1 --- /dev/null +++ b/riot/start.sh @@ -0,0 +1,5 @@ +pushd riot-web/webapp/ +python -m SimpleHTTPServer 8080 & +PID=$! +popd +echo $PID > riot.pid \ No newline at end of file diff --git a/riot/stop.sh b/riot/stop.sh new file mode 100644 index 0000000000..d0a0d69502 --- /dev/null +++ b/riot/stop.sh @@ -0,0 +1,3 @@ +PIDFILE=riot.pid +kill $(cat $PIDFILE) +rm $PIDFILE \ No newline at end of file From 01612f71bf3ec8cc11e51dfd7473b37bfdd19891 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 18 Jul 2018 18:04:31 +0200 Subject: [PATCH 0020/2372] dont assume current directory in scripts --- riot/install.sh | 4 ++++ riot/start.sh | 5 ++++- riot/stop.sh | 5 ++++- synapse/start.sh | 3 +++ synapse/stop.sh | 3 +++ 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/riot/install.sh b/riot/install.sh index 3729c3c03a..2eedf0fefa 100644 --- a/riot/install.sh +++ b/riot/install.sh @@ -1,4 +1,7 @@ RIOT_BRANCH=master + +BASE_DIR=$(realpath $(dirname $0)) +pushd $BASE_DIR curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --output riot.zip unzip riot.zip rm riot.zip @@ -6,3 +9,4 @@ mv riot-web-${RIOT_BRANCH} riot-web pushd riot-web npm install npm run build +popd \ No newline at end of file diff --git a/riot/start.sh b/riot/start.sh index 6e9ad218e1..2eb3221511 100644 --- a/riot/start.sh +++ b/riot/start.sh @@ -1,5 +1,8 @@ +BASE_DIR=$(realpath $(dirname $0)) +pushd $BASE_DIR pushd riot-web/webapp/ python -m SimpleHTTPServer 8080 & PID=$! popd -echo $PID > riot.pid \ No newline at end of file +echo $PID > riot.pid +popd \ No newline at end of file diff --git a/riot/stop.sh b/riot/stop.sh index d0a0d69502..ca1da23476 100644 --- a/riot/stop.sh +++ b/riot/stop.sh @@ -1,3 +1,6 @@ +BASE_DIR=$(realpath $(dirname $0)) +pushd $BASE_DIR PIDFILE=riot.pid kill $(cat $PIDFILE) -rm $PIDFILE \ No newline at end of file +rm $PIDFILE +popd \ No newline at end of file diff --git a/synapse/start.sh b/synapse/start.sh index dec9a7d035..6d758630a4 100644 --- a/synapse/start.sh +++ b/synapse/start.sh @@ -1,4 +1,7 @@ +BASE_DIR=$(realpath $(dirname $0)) +pushd $BASE_DIR pushd installations/consent source env/bin/activate ./synctl start +popd popd \ No newline at end of file diff --git a/synapse/stop.sh b/synapse/stop.sh index 916e774a99..ab376d8ac5 100644 --- a/synapse/stop.sh +++ b/synapse/stop.sh @@ -1,4 +1,7 @@ +BASE_DIR=$(realpath $(dirname $0)) +pushd $BASE_DIR pushd installations/consent source env/bin/activate ./synctl stop +popd popd \ No newline at end of file From 7ecd7d387325fe809f1b0bc2c19e2450333b1e05 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 18:50:05 +0200 Subject: [PATCH 0021/2372] add template config file for riot installation --- riot/.gitignore | 3 ++- riot/config-template/config.json | 34 ++++++++++++++++++++++++++++++++ riot/install.sh | 1 + 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 riot/config-template/config.json diff --git a/riot/.gitignore b/riot/.gitignore index ce36d3b374..0f07d8e498 100644 --- a/riot/.gitignore +++ b/riot/.gitignore @@ -1 +1,2 @@ -riot-web \ No newline at end of file +riot-web +riot.pid \ No newline at end of file diff --git a/riot/config-template/config.json b/riot/config-template/config.json new file mode 100644 index 0000000000..6d5eeb9640 --- /dev/null +++ b/riot/config-template/config.json @@ -0,0 +1,34 @@ +{ + "default_hs_url": "http://localhost:8008", + "default_is_url": "https://vector.im", + "disable_custom_urls": false, + "disable_guests": false, + "disable_login_language_selector": false, + "disable_3pid_login": false, + "brand": "Riot", + "integrations_ui_url": "https://scalar.vector.im/", + "integrations_rest_url": "https://scalar.vector.im/api", + "bug_report_endpoint_url": "https://riot.im/bugreports/submit", + "features": { + "feature_groups": "labs", + "feature_pinning": "labs" + }, + "default_federate": true, + "welcomePageUrl": "home.html", + "default_theme": "light", + "roomDirectory": { + "servers": [ + "localhost:8008" + ] + }, + "welcomeUserId": "@someuser:localhost", + "piwik": { + "url": "https://piwik.riot.im/", + "whitelistedHSUrls": ["http://localhost:8008"], + "whitelistedISUrls": ["https://vector.im", "https://matrix.org"], + "siteId": 1 + }, + "enable_presence_by_hs_url": { + "https://matrix.org": false + } +} diff --git a/riot/install.sh b/riot/install.sh index 2eedf0fefa..22bb87f03e 100644 --- a/riot/install.sh +++ b/riot/install.sh @@ -6,6 +6,7 @@ curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --outpu unzip riot.zip rm riot.zip mv riot-web-${RIOT_BRANCH} riot-web +cp config-template/config.json riot-web/ pushd riot-web npm install npm run build From 1468be0db41c49248bba8244bac54842640b4fd1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 18:50:29 +0200 Subject: [PATCH 0022/2372] add script to clear synapse db --- synapse/clear.sh | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 synapse/clear.sh diff --git a/synapse/clear.sh b/synapse/clear.sh new file mode 100644 index 0000000000..b2bee9ac47 --- /dev/null +++ b/synapse/clear.sh @@ -0,0 +1,6 @@ +BASE_DIR=$(realpath $(dirname $0)) +pushd $BASE_DIR +pushd installations/consent +rm homeserver.db +popd +popd \ No newline at end of file From bc1da0565e98b5d390e5f1f57954c707291de347 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 18:50:52 +0200 Subject: [PATCH 0023/2372] WIP: script to run tests on CI --- run.sh | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 run.sh diff --git a/run.sh b/run.sh new file mode 100644 index 0000000000..065d41c569 --- /dev/null +++ b/run.sh @@ -0,0 +1,6 @@ +sh synapse/clear.sh +sh synapse/start.sh +sh riot/start.sh +node start.js +sh riot/stop.sh +sh synapse/stop.sh \ No newline at end of file From a74a753a05514a4cc8ffb4c278c506c8e368a1cf Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 18:51:25 +0200 Subject: [PATCH 0024/2372] working consent test by accepting server notices invite and clicking on link, also create room --- helpers.js | 14 +++++++++++ start.js | 16 +++++++++--- tests/consent.js | 28 +++++++++++++++++++++ tests/create-room.js | 32 ++++++++++++++++++++++++ tests/server-notices-consent.js | 44 +++++++++++++++++++++++++++++++++ tests/signup.js | 14 ++--------- 6 files changed, 132 insertions(+), 16 deletions(-) create mode 100644 tests/consent.js create mode 100644 tests/create-room.js create mode 100644 tests/server-notices-consent.js diff --git a/helpers.js b/helpers.js index d57595f377..fae5c8e859 100644 --- a/helpers.js +++ b/helpers.js @@ -16,6 +16,7 @@ limitations under the License. // puppeteer helpers +// TODO: rename to queryAndInnertext? async function tryGetInnertext(page, selector) { const field = await page.$(selector); if (field != null) { @@ -25,6 +26,11 @@ async function tryGetInnertext(page, selector) { return null; } +async function innerText(page, field) { + const text_handle = await field.getProperty('innerText'); + return await text_handle.jsonValue(); +} + async function newPage() { const page = await browser.newPage(); await page.setViewport({ @@ -82,11 +88,17 @@ async function replaceInputText(input, text) { await input.type(text); } +// TODO: rename to waitAndQuery(Single)? async function waitAndQuerySelector(page, selector, timeout = 500) { await page.waitForSelector(selector, {visible: true, timeout}); return await page.$(selector); } +async function waitAndQueryAll(page, selector, timeout = 500) { + await page.waitForSelector(selector, {visible: true, timeout}); + return await page.$$(selector); +} + function waitForNewPage(timeout = 500) { return new Promise((resolve, reject) => { const timeoutHandle = setTimeout(() => { @@ -120,6 +132,7 @@ function delay(ms) { module.exports = { tryGetInnertext, + innerText, newPage, logConsole, logXHRRequests, @@ -127,6 +140,7 @@ module.exports = { printElements, replaceInputText, waitAndQuerySelector, + waitAndQueryAll, waitForNewPage, randomInt, riotUrl, diff --git a/start.js b/start.js index 8a3ceb354b..0c06bd9731 100644 --- a/start.js +++ b/start.js @@ -20,13 +20,16 @@ const assert = require('assert'); const signup = require('./tests/signup'); const join = require('./tests/join'); +const createRoom = require('./tests/create-room'); +const acceptServerNoticesInviteAndConsent = require('./tests/server-notices-consent'); + +const homeserver = 'http://localhost:8008'; global.riotserver = 'http://localhost:8080'; -global.homeserver = 'http://localhost:8008'; global.browser = null; async function runTests() { - global.browser = await puppeteer.launch(); + global.browser = await puppeteer.launch({headless: false}); const page = await helpers.newPage(); const username = 'bruno-' + helpers.randomInt(10000); @@ -35,9 +38,14 @@ async function runTests() { await signup(page, username, password, homeserver); process.stdout.write('done\n'); + const noticesName = "Server Notices"; + process.stdout.write(`* accepting "${noticesName}" and accepting terms & conditions ...`); + await acceptServerNoticesInviteAndConsent(page, noticesName); + process.stdout.write('done\n'); + const room = 'test'; - process.stdout.write(`* joining room ${room} ... `); - await join(page, room, true); + process.stdout.write(`* creating room ${room} ... `); + await createRoom(page, room); process.stdout.write('done\n'); await browser.close(); diff --git a/tests/consent.js b/tests/consent.js new file mode 100644 index 0000000000..3c8ada9a5e --- /dev/null +++ b/tests/consent.js @@ -0,0 +1,28 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const helpers = require('../helpers'); +const assert = require('assert'); + +module.exports = async function acceptTerms(page) { + const reviewTermsButton = await helpers.waitAndQuerySelector(page, '.mx_QuestionDialog button.mx_Dialog_primary', 5000); + const termsPagePromise = helpers.waitForNewPage(); + await reviewTermsButton.click(); + const termsPage = await termsPagePromise; + const acceptButton = await termsPage.$('input[type=submit]'); + await acceptButton.click(); + await helpers.delay(500); //TODO yuck, timers +} \ No newline at end of file diff --git a/tests/create-room.js b/tests/create-room.js new file mode 100644 index 0000000000..4c9004bcaf --- /dev/null +++ b/tests/create-room.js @@ -0,0 +1,32 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const helpers = require('../helpers'); +const assert = require('assert'); + +module.exports = async function createRoom(page, roomName) { + //TODO: brittle selector + const createRoomButton = await helpers.waitAndQuerySelector(page, '.mx_RoleButton[aria-label="Create new room"]'); + await createRoomButton.click(); + + const roomNameInput = await helpers.waitAndQuerySelector(page, '.mx_CreateRoomDialog_input'); + await helpers.replaceInputText(roomNameInput, roomName); + + const createButton = await helpers.waitAndQuerySelector(page, '.mx_Dialog_primary'); + await createButton.click(); + + await page.waitForSelector('.mx_MessageComposer'); +} \ No newline at end of file diff --git a/tests/server-notices-consent.js b/tests/server-notices-consent.js new file mode 100644 index 0000000000..2689036a96 --- /dev/null +++ b/tests/server-notices-consent.js @@ -0,0 +1,44 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const helpers = require('../helpers'); +const assert = require('assert'); + +module.exports = async function acceptServerNoticesInviteAndConsent(page, name) { + //TODO: brittle selector + const invitesHandles = await helpers.waitAndQueryAll(page, '.mx_RoomTile_name.mx_RoomTile_invite'); + const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => { + const text = await helpers.innerText(page, inviteHandle); + return {inviteHandle, text}; + })); + const inviteHandle = invitesWithText.find(({inviteHandle, text}) => { + return text.trim() === name; + }).inviteHandle; + + await inviteHandle.click(); + + const acceptInvitationLink = await helpers.waitAndQuerySelector(page, ".mx_RoomPreviewBar_join_text a:first-child"); + await acceptInvitationLink.click(); + + const consentLink = await helpers.waitAndQuerySelector(page, ".mx_EventTile_body a", 1000); + + const termsPagePromise = helpers.waitForNewPage(); + await consentLink.click(); + const termsPage = await termsPagePromise; + const acceptButton = await termsPage.$('input[type=submit]'); + await acceptButton.click(); + await helpers.delay(500); //TODO yuck, timers +} \ No newline at end of file diff --git a/tests/signup.js b/tests/signup.js index 5560fc56cf..e482c7dfea 100644 --- a/tests/signup.js +++ b/tests/signup.js @@ -65,9 +65,9 @@ module.exports = async function signup(page, username, password, homeserver) { console.log(xhrLogs.logs()); */ - await acceptTerms(page); + //await acceptTerms(page); - await helpers.delay(10000); + await helpers.delay(2000); //printElements('page', await page.$('#matrixchat')); // await navigation_promise; @@ -75,13 +75,3 @@ module.exports = async function signup(page, username, password, homeserver) { const url = page.url(); assert.strictEqual(url, helpers.riotUrl('/#/home')); } - -async function acceptTerms(page) { - const reviewTermsButton = await helpers.waitAndQuerySelector(page, '.mx_QuestionDialog button.mx_Dialog_primary'); - const termsPagePromise = helpers.waitForNewPage(); - await reviewTermsButton.click(); - const termsPage = await termsPagePromise; - const acceptButton = await termsPage.$('input[type=submit]'); - await acceptButton.click(); - await helpers.delay(500); //TODO yuck, timers -} \ No newline at end of file From 515e34cfde84c9854c9b10f518e72dd8492986ab Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 18:59:45 +0200 Subject: [PATCH 0025/2372] turn headless back on --- start.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.js b/start.js index 0c06bd9731..3eefdc68c0 100644 --- a/start.js +++ b/start.js @@ -29,7 +29,7 @@ global.riotserver = 'http://localhost:8080'; global.browser = null; async function runTests() { - global.browser = await puppeteer.launch({headless: false}); + global.browser = await puppeteer.launch(); const page = await helpers.newPage(); const username = 'bruno-' + helpers.randomInt(10000); From 410b32ff859bcb63427d6365d78f896cbeda19d6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 19:00:27 +0200 Subject: [PATCH 0026/2372] make script runnable in one terminal, without server output garbling up test results. This won't work well on CI server but makes it clear to run locally --- run.sh | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/run.sh b/run.sh index 065d41c569..bc39208384 100644 --- a/run.sh +++ b/run.sh @@ -1,6 +1,4 @@ -sh synapse/clear.sh -sh synapse/start.sh -sh riot/start.sh -node start.js -sh riot/stop.sh -sh synapse/stop.sh \ No newline at end of file +tmux \ + new-session "sh riot/stop.sh; sh synapse/stop.sh; sh synapse/clear.sh; sh synapse/start.sh; sh riot/start.sh; read"\; \ + split-window "sleep 5; node start.js; sh riot/stop.sh; sh synapse/stop.sh; read"\; \ + select-layout even-vertical From 5f2fcefb4ef87dd40d2b34befede72243dcc5ef9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 19:00:38 +0200 Subject: [PATCH 0027/2372] update instructions --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 163bffbce7..5ebcf58a87 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,28 @@ This repository contains tests for the matrix-react-sdk web app. The tests fire - start synapse with clean config/database on every test run - look into CI(Travis) integration +## It's broken! How do I see what's happening in the browser? + +Look for this line: +``` +puppeteer.launch(); +``` +Now change it to: +``` +puppeteer.launch({headless: false}); +``` + ## How to run ### Setup - + - install synapse with `sh synapse/install.sh`, this fetches the master branch at the moment. + - install riot with `sh riot/install.sh`, this fetches the master branch at the moment. - install dependencies with `npm install` (will download copy of chrome) - have riot-web running on `localhost:8080` - have a local synapse running at `localhost:8008` ### Run tests - - run tests with `node start.js` + +Run tests with `sh run.sh`. + +You should see the terminal split with on top the server output (both riot static server, and synapse), and on the bottom the tests running. From 40c09673640594cf2edef7d718c16728fce3b3c5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 19:08:23 +0200 Subject: [PATCH 0028/2372] more readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ebcf58a87..b6249fb6e4 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ puppeteer.launch({headless: false}); ## How to run ### Setup - - install synapse with `sh synapse/install.sh`, this fetches the master branch at the moment. + - install synapse with `sh synapse/install.sh`, this fetches the master branch at the moment. If anything fails here, please refer to the synapse README to see if you're missing one of the prerequisites. - install riot with `sh riot/install.sh`, this fetches the master branch at the moment. - install dependencies with `npm install` (will download copy of chrome) - have riot-web running on `localhost:8080` From bc06d370d0e5aeaa09149dfef7ac67c773a47701 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Jul 2018 09:41:25 +0200 Subject: [PATCH 0029/2372] prevent stop scripts from polluting output --- riot/stop.sh | 4 ++-- synapse/stop.sh | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/riot/stop.sh b/riot/stop.sh index ca1da23476..59fef1dfd0 100644 --- a/riot/stop.sh +++ b/riot/stop.sh @@ -1,6 +1,6 @@ BASE_DIR=$(realpath $(dirname $0)) -pushd $BASE_DIR +pushd $BASE_DIR > /dev/null PIDFILE=riot.pid kill $(cat $PIDFILE) rm $PIDFILE -popd \ No newline at end of file +popd > /dev/null \ No newline at end of file diff --git a/synapse/stop.sh b/synapse/stop.sh index ab376d8ac5..f55d8b50db 100644 --- a/synapse/stop.sh +++ b/synapse/stop.sh @@ -1,7 +1,7 @@ BASE_DIR=$(realpath $(dirname $0)) -pushd $BASE_DIR -pushd installations/consent +pushd $BASE_DIR > /dev/null +pushd installations/consent > /dev/null source env/bin/activate ./synctl stop -popd -popd \ No newline at end of file +popd > /dev/null +popd > /dev/null \ No newline at end of file From eb10296c74609a9d2c8333a7a6c9d5b1887e7695 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Jul 2018 10:09:30 +0200 Subject: [PATCH 0030/2372] disable welcomeUserId for now in riot config, flow seems broken --- riot/config-template/config.json | 1 - 1 file changed, 1 deletion(-) diff --git a/riot/config-template/config.json b/riot/config-template/config.json index 6d5eeb9640..39bbd6490a 100644 --- a/riot/config-template/config.json +++ b/riot/config-template/config.json @@ -21,7 +21,6 @@ "localhost:8008" ] }, - "welcomeUserId": "@someuser:localhost", "piwik": { "url": "https://piwik.riot.im/", "whitelistedHSUrls": ["http://localhost:8008"], From 978081b3c0354d0a30b548eda273fe0907012ef3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Jul 2018 10:09:52 +0200 Subject: [PATCH 0031/2372] remove obsolete code --- tests/signup.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/tests/signup.js b/tests/signup.js index e482c7dfea..4d922829d9 100644 --- a/tests/signup.js +++ b/tests/signup.js @@ -15,9 +15,10 @@ limitations under the License. */ const helpers = require('../helpers'); +const acceptTerms = require('./consent'); const assert = require('assert'); -module.exports = async function signup(page, username, password, homeserver) { +module.exports = async function signup(page, username, password, homeserver, options) { const consoleLogs = helpers.logConsole(page); const xhrLogs = helpers.logXHRRequests(page); await page.goto(helpers.riotUrl('/#/register')); @@ -57,21 +58,8 @@ module.exports = async function signup(page, username, password, homeserver) { await continueButton.click(); //wait for registration to finish so the hash gets set //onhashchange better? -/* - await page.screenshot({path: "afterlogin.png", fullPage: true}); - console.log('browser console logs:'); - console.log(consoleLogs.logs()); - console.log('xhr logs:'); - console.log(xhrLogs.logs()); -*/ - - //await acceptTerms(page); - await helpers.delay(2000); - //printElements('page', await page.$('#matrixchat')); -// await navigation_promise; - //await page.waitForSelector('.mx_MatrixChat', {visible: true, timeout: 3000}); const url = page.url(); assert.strictEqual(url, helpers.riotUrl('/#/home')); } From b42a0411f30aa7192597c635e4599e0dcc16f9e4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Jul 2018 10:10:36 +0200 Subject: [PATCH 0032/2372] add IDEA for better debugging to readme (unrelated to PR really) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b6249fb6e4..e0e3306b74 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ This repository contains tests for the matrix-react-sdk web app. The tests fire - Run 2 synapse instances to test federation use cases. - start synapse with clean config/database on every test run - look into CI(Travis) integration +- create interactive mode, where window is opened, and browser is kept open until Ctrl^C, for easy test debugging. ## It's broken! How do I see what's happening in the browser? From 048a3670811ea30cf8863b45ef65a440ffd74c1b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Jul 2018 10:21:38 +0200 Subject: [PATCH 0033/2372] use in-memory database, faster and no need to clear before every run --- run.sh | 2 +- synapse/clear.sh | 6 ------ synapse/config-templates/consent/homeserver.yaml | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 synapse/clear.sh diff --git a/run.sh b/run.sh index bc39208384..b64fa1dbdd 100644 --- a/run.sh +++ b/run.sh @@ -1,4 +1,4 @@ tmux \ - new-session "sh riot/stop.sh; sh synapse/stop.sh; sh synapse/clear.sh; sh synapse/start.sh; sh riot/start.sh; read"\; \ + new-session "sh riot/stop.sh; sh synapse/stop.sh; sh synapse/start.sh; sh riot/start.sh; read"\; \ split-window "sleep 5; node start.js; sh riot/stop.sh; sh synapse/stop.sh; read"\; \ select-layout even-vertical diff --git a/synapse/clear.sh b/synapse/clear.sh deleted file mode 100644 index b2bee9ac47..0000000000 --- a/synapse/clear.sh +++ /dev/null @@ -1,6 +0,0 @@ -BASE_DIR=$(realpath $(dirname $0)) -pushd $BASE_DIR -pushd installations/consent -rm homeserver.db -popd -popd \ No newline at end of file diff --git a/synapse/config-templates/consent/homeserver.yaml b/synapse/config-templates/consent/homeserver.yaml index 6ba15c9af0..a27fbf6f10 100644 --- a/synapse/config-templates/consent/homeserver.yaml +++ b/synapse/config-templates/consent/homeserver.yaml @@ -193,7 +193,7 @@ database: # Arguments to pass to the engine args: # Path to the database - database: "{{SYNAPSE_ROOT}}homeserver.db" + database: ":memory:" # Number of events to cache in memory. event_cache_size: "10K" From 5934bebafbe9efe549b0504c57bbbbf75d0a3889 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Jul 2018 10:36:03 +0200 Subject: [PATCH 0034/2372] change test user name --- start.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.js b/start.js index 3eefdc68c0..1073f3c9d2 100644 --- a/start.js +++ b/start.js @@ -32,7 +32,7 @@ async function runTests() { global.browser = await puppeteer.launch(); const page = await helpers.newPage(); - const username = 'bruno-' + helpers.randomInt(10000); + const username = 'user-' + helpers.randomInt(10000); const password = 'testtest'; process.stdout.write(`* signing up as ${username} ... `); await signup(page, username, password, homeserver); From c693d861f45cdf0d8c1e3e3f984a83c358b8323b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Jul 2018 10:36:21 +0200 Subject: [PATCH 0035/2372] link to code style document, instead of having local copy --- README.md | 9 +++ code_style.md | 184 -------------------------------------------------- 2 files changed, 9 insertions(+), 184 deletions(-) delete mode 100644 code_style.md diff --git a/README.md b/README.md index e0e3306b74..90a3e869d3 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,12 @@ puppeteer.launch({headless: false}); Run tests with `sh run.sh`. You should see the terminal split with on top the server output (both riot static server, and synapse), and on the bottom the tests running. + +Developer Guide +=============== + +Please follow the standard Matrix contributor's guide: +https://github.com/matrix-org/synapse/tree/master/CONTRIBUTING.rst + +Please follow the Matrix JS/React code style as per: +https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md diff --git a/code_style.md b/code_style.md deleted file mode 100644 index 2cac303e54..0000000000 --- a/code_style.md +++ /dev/null @@ -1,184 +0,0 @@ -Matrix JavaScript/ECMAScript Style Guide -======================================== - -The intention of this guide is to make Matrix's JavaScript codebase clean, -consistent with other popular JavaScript styles and consistent with the rest of -the Matrix codebase. For reference, the Matrix Python style guide can be found -at https://github.com/matrix-org/synapse/blob/master/docs/code_style.rst - -This document reflects how we would like Matrix JavaScript code to look, with -acknowledgement that a significant amount of code is written to older -standards. - -Write applications in modern ECMAScript and use a transpiler where necessary to -target older platforms. When writing library code, consider carefully whether -to write in ES5 to allow all JavaScript application to use the code directly or -writing in modern ECMAScript and using a transpile step to generate the file -that applications can then include. There are significant benefits in being -able to use modern ECMAScript, although the tooling for doing so can be awkward -for library code, especially with regard to translating source maps and line -number throgh from the original code to the final application. - -General Style -------------- -- 4 spaces to indent, for consistency with Matrix Python. -- 120 columns per line, but try to keep JavaScript code around the 80 column mark. - Inline JSX in particular can be nicer with more columns per line. -- No trailing whitespace at end of lines. -- Don't indent empty lines. -- One newline at the end of the file. -- Unix newlines, never `\r` -- Indent similar to our python code: break up long lines at logical boundaries, - more than one argument on a line is OK -- Use semicolons, for consistency with node. -- UpperCamelCase for class and type names -- lowerCamelCase for functions and variables. -- Single line ternary operators are fine. -- UPPER_CAMEL_CASE for constants -- Single quotes for strings by default, for consistency with most JavaScript styles: - - ```javascript - "bad" // Bad - 'good' // Good - ``` -- Use parentheses or `` ` `` instead of `\` for line continuation where ever possible -- Open braces on the same line (consistent with Node): - - ```javascript - if (x) { - console.log("I am a fish"); // Good - } - - if (x) - { - console.log("I am a fish"); // Bad - } - ``` -- Spaces after `if`, `for`, `else` etc, no space around the condition: - - ```javascript - if (x) { - console.log("I am a fish"); // Good - } - - if(x) { - console.log("I am a fish"); // Bad - } - - if ( x ) { - 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 - are simple and closely related. If you put the next declaration on a new line, - treat yourself to another `var`: - - ```javascript - const key = "foo", - comparator = function(x, y) { - return x - y; - }; // Bad - - const key = "foo"; - const comparator = function(x, y) { - return x - y; - }; // Good - - let x = 0, y = 0; // Fine - - let x = 0; - let y = 0; // Also fine - ``` -- A single line `if` is fine, all others have braces. This prevents errors when adding to the code.: - - ```javascript - if (x) return true; // Fine - - if (x) { - return true; // Also fine - } - - if (x) - return true; // Not fine - ``` -- Terminate all multi-line lists, object literals, imports and ideally function calls with commas (if using a transpiler). Note that trailing function commas require explicit configuration in babel at time of writing: - - ```javascript - var mascots = [ - "Patrick", - "Shirley", - "Colin", - "Susan", - "Sir Arthur David" // Bad - ]; - - var mascots = [ - "Patrick", - "Shirley", - "Colin", - "Susan", - "Sir Arthur David", // Good - ]; - ``` -- Use `null`, `undefined` etc consistently with node: - Boolean variables and functions should always be either true or false. Don't set it to 0 unless it's supposed to be a number. - When something is intentionally missing or removed, set it to null. - If returning a boolean, type coerce: - - ```javascript - function hasThings() { - return !!length; // bad - return new Boolean(length); // REALLY bad - return Boolean(length); // good - } - ``` - Don't set things to undefined. Reserve that value to mean "not yet set to anything." - Boolean objects are verboten. -- Use JSDoc - -ECMAScript ----------- -- Use `const` unless you need a re-assignable variable. This ensures things you don't want to be re-assigned can't be. -- Be careful migrating files to newer syntax. - - Don't mix `require` and `import` in the same file. Either stick to the old style or change them all. - - Likewise, don't mix things like class properties and `MyClass.prototype.MY_CONSTANT = 42;` - - Be careful mixing arrow functions and regular functions, eg. if one function in a promise chain is an - arrow function, they probably all should be. -- Apart from that, newer ES features should be used whenever the author deems them to be appropriate. -- Flow annotations are welcome and encouraged. - -React ------ -- Use React.createClass rather than ES6 classes for components, as the boilerplate is way too heavy on ES6 currently. ES7 might improve it. -- Pull out functions in props to the class, generally as specific event handlers: - - ```jsx - // Bad - {doStuff();}}> // Equally bad - // Better - // Best, if onFooClick would do anything other than directly calling doStuff - ``` - - Not doing so is acceptable in a single case; in function-refs: - - ```jsx - this.component = self}> - ``` -- Think about whether your component really needs state: are you duplicating - information in component state that could be derived from the model? From 1643b9552e76414853c50a97be4a797896341d79 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Jul 2018 11:20:07 +0200 Subject: [PATCH 0036/2372] test default server setup for signup --- start.js | 2 +- tests/signup.js | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/start.js b/start.js index 1073f3c9d2..d4b8dab076 100644 --- a/start.js +++ b/start.js @@ -35,7 +35,7 @@ async function runTests() { const username = 'user-' + helpers.randomInt(10000); const password = 'testtest'; process.stdout.write(`* signing up as ${username} ... `); - await signup(page, username, password, homeserver); + await signup(page, username, password); process.stdout.write('done\n'); const noticesName = "Server Notices"; diff --git a/tests/signup.js b/tests/signup.js index 4d922829d9..79f105ee70 100644 --- a/tests/signup.js +++ b/tests/signup.js @@ -18,16 +18,18 @@ const helpers = require('../helpers'); const acceptTerms = require('./consent'); const assert = require('assert'); -module.exports = async function signup(page, username, password, homeserver, options) { +module.exports = async function signup(page, username, password, homeserver) { const consoleLogs = helpers.logConsole(page); const xhrLogs = helpers.logXHRRequests(page); await page.goto(helpers.riotUrl('/#/register')); //click 'Custom server' radio button - const advancedRadioButton = await helpers.waitAndQuerySelector(page, '#advanced'); - await advancedRadioButton.click(); - + if (homeserver) { + const advancedRadioButton = await helpers.waitAndQuerySelector(page, '#advanced'); + await advancedRadioButton.click(); + } + // wait until register button is visible + await page.waitForSelector('.mx_Login_submit[value=Register]'); //fill out form - await page.waitForSelector('.mx_ServerConfig', {visible: true, timeout: 500}); const loginFields = await page.$$('.mx_Login_field'); assert.strictEqual(loginFields.length, 7); const usernameField = loginFields[2]; @@ -37,7 +39,10 @@ module.exports = async function signup(page, username, password, homeserver, opt await helpers.replaceInputText(usernameField, username); await helpers.replaceInputText(passwordField, password); await helpers.replaceInputText(passwordRepeatField, password); - await helpers.replaceInputText(hsurlField, homeserver); + if (homeserver) { + await page.waitForSelector('.mx_ServerConfig', {visible: true, timeout: 500}); + await helpers.replaceInputText(hsurlField, homeserver); + } //wait over a second because Registration/ServerConfig have a 1000ms //delay to internally set the homeserver url //see Registration::render and ServerConfig::props::delayTimeMs From ba1ee86c6732d60d811b56f0b3f2b19ead6db3b9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Jul 2018 11:21:34 +0200 Subject: [PATCH 0037/2372] wait to be visible --- tests/signup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/signup.js b/tests/signup.js index 79f105ee70..65044f900c 100644 --- a/tests/signup.js +++ b/tests/signup.js @@ -28,7 +28,7 @@ module.exports = async function signup(page, username, password, homeserver) { await advancedRadioButton.click(); } // wait until register button is visible - await page.waitForSelector('.mx_Login_submit[value=Register]'); + await page.waitForSelector('.mx_Login_submit[value=Register]', {visible: true, timeout: 500}); //fill out form const loginFields = await page.$$('.mx_Login_field'); assert.strictEqual(loginFields.length, 7); From c9461dd2967f869af469575853566a3ec9d9d105 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 13:29:59 +0200 Subject: [PATCH 0038/2372] hide riot static server output --- riot/start.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/riot/start.sh b/riot/start.sh index 2eb3221511..3e5077717e 100644 --- a/riot/start.sh +++ b/riot/start.sh @@ -1,8 +1,8 @@ BASE_DIR=$(realpath $(dirname $0)) -pushd $BASE_DIR -pushd riot-web/webapp/ -python -m SimpleHTTPServer 8080 & +pushd $BASE_DIR > /dev/null +pushd riot-web/webapp/ > /dev/null +python -m SimpleHTTPServer 8080 > /dev/null 2>&1 & PID=$! -popd +popd > /dev/null echo $PID > riot.pid -popd \ No newline at end of file +popd > /dev/null \ No newline at end of file From 0be2e023812f06013a8f0c086ecea7301a44eb3d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 13:42:36 +0200 Subject: [PATCH 0039/2372] hide synapse schema update logs by redirecting stderr --- synapse/start.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/synapse/start.sh b/synapse/start.sh index 6d758630a4..f59a3641cc 100644 --- a/synapse/start.sh +++ b/synapse/start.sh @@ -1,7 +1,7 @@ BASE_DIR=$(realpath $(dirname $0)) -pushd $BASE_DIR -pushd installations/consent +pushd $BASE_DIR > /dev/null +pushd installations/consent > /dev/null source env/bin/activate -./synctl start -popd -popd \ No newline at end of file +./synctl start 2> /dev/null +popd > /dev/null +popd > /dev/null \ No newline at end of file From a6304ce83ebe21e0f5ae0d9c84a41bac159abf78 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 13:43:12 +0200 Subject: [PATCH 0040/2372] now the output isn't overwhelming anymore, output what's happening at every step --- riot/start.sh | 4 +++- start.js | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/riot/start.sh b/riot/start.sh index 3e5077717e..5feb40eb2b 100644 --- a/riot/start.sh +++ b/riot/start.sh @@ -1,7 +1,9 @@ +PORT=8080 +echo "running riot on http://localhost:$PORT..." BASE_DIR=$(realpath $(dirname $0)) pushd $BASE_DIR > /dev/null pushd riot-web/webapp/ > /dev/null -python -m SimpleHTTPServer 8080 > /dev/null 2>&1 & +python -m SimpleHTTPServer $PORT > /dev/null 2>&1 & PID=$! popd > /dev/null echo $PID > riot.pid diff --git a/start.js b/start.js index d4b8dab076..a37aa4538a 100644 --- a/start.js +++ b/start.js @@ -29,6 +29,7 @@ global.riotserver = 'http://localhost:8080'; global.browser = null; async function runTests() { + console.log("running tests ..."); global.browser = await puppeteer.launch(); const page = await helpers.newPage(); From b3473a7220236f9af0ac0fcf4fc74b281ed89b5c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 13:43:38 +0200 Subject: [PATCH 0041/2372] with no logs polluting the output, we dont need tmux anymore to split the terminal --- run.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/run.sh b/run.sh index b64fa1dbdd..d4d4261430 100644 --- a/run.sh +++ b/run.sh @@ -1,4 +1,5 @@ -tmux \ - new-session "sh riot/stop.sh; sh synapse/stop.sh; sh synapse/start.sh; sh riot/start.sh; read"\; \ - split-window "sleep 5; node start.js; sh riot/stop.sh; sh synapse/stop.sh; read"\; \ - select-layout even-vertical +sh synapse/start.sh +sh riot/start.sh +node start.js +sh riot/stop.sh +sh synapse/stop.sh From a4e7b14728c834f0d992e2b12866ce17c163f7d4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 13:50:58 +0200 Subject: [PATCH 0042/2372] update README --- README.md | 12 +++++------- install.sh | 3 +++ 2 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 install.sh diff --git a/README.md b/README.md index 90a3e869d3..5bd4c1dadc 100644 --- a/README.md +++ b/README.md @@ -34,18 +34,16 @@ puppeteer.launch({headless: false}); ## How to run ### Setup - - install synapse with `sh synapse/install.sh`, this fetches the master branch at the moment. If anything fails here, please refer to the synapse README to see if you're missing one of the prerequisites. - - install riot with `sh riot/install.sh`, this fetches the master branch at the moment. - - install dependencies with `npm install` (will download copy of chrome) - - have riot-web running on `localhost:8080` - - have a local synapse running at `localhost:8008` + +Run `sh install.sh`. This will: + - install synapse, fetches the master branch at the moment. If anything fails here, please refer to the synapse README to see if you're missing one of the prerequisites. + - install riot, this fetches the master branch at the moment. + - install dependencies (will download copy of chrome) ### Run tests Run tests with `sh run.sh`. -You should see the terminal split with on top the server output (both riot static server, and synapse), and on the bottom the tests running. - Developer Guide =============== diff --git a/install.sh b/install.sh new file mode 100644 index 0000000000..c4e6d6253e --- /dev/null +++ b/install.sh @@ -0,0 +1,3 @@ +sh synapse/install.sh +sh riot/install.sh +npm install From 96374f4e5400d1132703927b32db22981fa27886 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 14:00:01 +0200 Subject: [PATCH 0043/2372] only install synapse and riot if directory is not already there --- riot/install.sh | 13 ++++++++++--- synapse/install.sh | 9 ++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/riot/install.sh b/riot/install.sh index 22bb87f03e..43b39611d7 100644 --- a/riot/install.sh +++ b/riot/install.sh @@ -1,13 +1,20 @@ RIOT_BRANCH=master BASE_DIR=$(realpath $(dirname $0)) -pushd $BASE_DIR +if [[ -d $BASE_DIR/riot-web ]]; then + echo "riot is already installed" + exit +fi + + +pushd $BASE_DIR > /dev/null curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --output riot.zip unzip riot.zip rm riot.zip mv riot-web-${RIOT_BRANCH} riot-web cp config-template/config.json riot-web/ -pushd riot-web +pushd riot-web > /dev/null npm install npm run build -popd \ No newline at end of file +popd > /dev/null +popd > /dev/null diff --git a/synapse/install.sh b/synapse/install.sh index 47f1f746fc..959e529e6b 100644 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -6,7 +6,14 @@ CONFIG_TEMPLATE=consent PORT=8008 # set current directory to script directory BASE_DIR=$(realpath $(dirname $0)) -pushd $BASE_DIR + +if [[ -d $BASE_DIR/$SERVER_DIR ]]; then + echo "synapse is already installed" + exit +fi + +pushd $BASE_DIR > /dev/null + mkdir -p installations/ curl https://codeload.github.com/matrix-org/synapse/zip/$SYNAPSE_BRANCH --output synapse.zip unzip synapse.zip From 5e1517eb4d93351a8497eb7401b648d348dedc07 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 14:10:19 +0200 Subject: [PATCH 0044/2372] no need for push/popd in sub-shell --- riot/install.sh | 7 ++----- riot/start.sh | 3 +-- riot/stop.sh | 3 +-- synapse/install.sh | 6 ++---- synapse/start.sh | 6 ++---- synapse/stop.sh | 6 ++---- 6 files changed, 10 insertions(+), 21 deletions(-) diff --git a/riot/install.sh b/riot/install.sh index 43b39611d7..9e7e5bb2da 100644 --- a/riot/install.sh +++ b/riot/install.sh @@ -6,15 +6,12 @@ if [[ -d $BASE_DIR/riot-web ]]; then exit fi - -pushd $BASE_DIR > /dev/null +cd $BASE_DIR curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --output riot.zip unzip riot.zip rm riot.zip mv riot-web-${RIOT_BRANCH} riot-web cp config-template/config.json riot-web/ -pushd riot-web > /dev/null +cd riot-web npm install npm run build -popd > /dev/null -popd > /dev/null diff --git a/riot/start.sh b/riot/start.sh index 5feb40eb2b..141d3176e7 100644 --- a/riot/start.sh +++ b/riot/start.sh @@ -1,10 +1,9 @@ PORT=8080 echo "running riot on http://localhost:$PORT..." BASE_DIR=$(realpath $(dirname $0)) -pushd $BASE_DIR > /dev/null +cd $BASE_DIR/ pushd riot-web/webapp/ > /dev/null python -m SimpleHTTPServer $PORT > /dev/null 2>&1 & PID=$! popd > /dev/null echo $PID > riot.pid -popd > /dev/null \ No newline at end of file diff --git a/riot/stop.sh b/riot/stop.sh index 59fef1dfd0..148116cfe6 100644 --- a/riot/stop.sh +++ b/riot/stop.sh @@ -1,6 +1,5 @@ BASE_DIR=$(realpath $(dirname $0)) -pushd $BASE_DIR > /dev/null +cd $BASE_DIR PIDFILE=riot.pid kill $(cat $PIDFILE) rm $PIDFILE -popd > /dev/null \ No newline at end of file diff --git a/synapse/install.sh b/synapse/install.sh index 959e529e6b..7170ce6d30 100644 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -12,13 +12,13 @@ if [[ -d $BASE_DIR/$SERVER_DIR ]]; then exit fi -pushd $BASE_DIR > /dev/null +cd $BASE_DIR mkdir -p installations/ curl https://codeload.github.com/matrix-org/synapse/zip/$SYNAPSE_BRANCH --output synapse.zip unzip synapse.zip mv synapse-$SYNAPSE_BRANCH $SERVER_DIR -pushd $SERVER_DIR +cd $SERVER_DIR virtualenv -p python2.7 env source env/bin/activate pip install --upgrade pip @@ -36,5 +36,3 @@ sed -i "s#{{SYNAPSE_PORT}}#${PORT}/#g" homeserver.yaml sed -i "s#{{FORM_SECRET}}#$(uuidgen)#g" homeserver.yaml sed -i "s#{{REGISTRATION_SHARED_SECRET}}#$(uuidgen)#g" homeserver.yaml sed -i "s#{{MACAROON_SECRET_KEY}}#$(uuidgen)#g" homeserver.yaml -popd #back to synapse root dir -popd #back to wherever we were \ No newline at end of file diff --git a/synapse/start.sh b/synapse/start.sh index f59a3641cc..e809fb906c 100644 --- a/synapse/start.sh +++ b/synapse/start.sh @@ -1,7 +1,5 @@ BASE_DIR=$(realpath $(dirname $0)) -pushd $BASE_DIR > /dev/null -pushd installations/consent > /dev/null +cd $BASE_DIR +cd installations/consent source env/bin/activate ./synctl start 2> /dev/null -popd > /dev/null -popd > /dev/null \ No newline at end of file diff --git a/synapse/stop.sh b/synapse/stop.sh index f55d8b50db..1c5ab2fed7 100644 --- a/synapse/stop.sh +++ b/synapse/stop.sh @@ -1,7 +1,5 @@ BASE_DIR=$(realpath $(dirname $0)) -pushd $BASE_DIR > /dev/null -pushd installations/consent > /dev/null +cd $BASE_DIR +cd installations/consent source env/bin/activate ./synctl stop -popd > /dev/null -popd > /dev/null \ No newline at end of file From 5389a42bc18d6150770abbe1350ad73e2938d7f1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 15:04:04 +0200 Subject: [PATCH 0045/2372] use readlink instead of realpath as it seems to be more portable --- riot/install.sh | 2 +- riot/start.sh | 2 +- riot/stop.sh | 2 +- synapse/install.sh | 2 +- synapse/start.sh | 2 +- synapse/stop.sh | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/riot/install.sh b/riot/install.sh index 9e7e5bb2da..8a8761b705 100644 --- a/riot/install.sh +++ b/riot/install.sh @@ -1,6 +1,6 @@ RIOT_BRANCH=master -BASE_DIR=$(realpath $(dirname $0)) +BASE_DIR=$(readlink -f $(dirname $0)) if [[ -d $BASE_DIR/riot-web ]]; then echo "riot is already installed" exit diff --git a/riot/start.sh b/riot/start.sh index 141d3176e7..ec3e5b32bc 100644 --- a/riot/start.sh +++ b/riot/start.sh @@ -1,6 +1,6 @@ PORT=8080 echo "running riot on http://localhost:$PORT..." -BASE_DIR=$(realpath $(dirname $0)) +BASE_DIR=$(readlink -f $(dirname $0)) cd $BASE_DIR/ pushd riot-web/webapp/ > /dev/null python -m SimpleHTTPServer $PORT > /dev/null 2>&1 & diff --git a/riot/stop.sh b/riot/stop.sh index 148116cfe6..40695c749c 100644 --- a/riot/stop.sh +++ b/riot/stop.sh @@ -1,4 +1,4 @@ -BASE_DIR=$(realpath $(dirname $0)) +BASE_DIR=$(readlink -f $(dirname $0)) cd $BASE_DIR PIDFILE=riot.pid kill $(cat $PIDFILE) diff --git a/synapse/install.sh b/synapse/install.sh index 7170ce6d30..661c98ecd5 100644 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -5,7 +5,7 @@ SERVER_DIR=installations/$INSTALLATION_NAME CONFIG_TEMPLATE=consent PORT=8008 # set current directory to script directory -BASE_DIR=$(realpath $(dirname $0)) +BASE_DIR=$(readlink -f $(dirname $0)) if [[ -d $BASE_DIR/$SERVER_DIR ]]; then echo "synapse is already installed" diff --git a/synapse/start.sh b/synapse/start.sh index e809fb906c..f7af6ac0f7 100644 --- a/synapse/start.sh +++ b/synapse/start.sh @@ -1,4 +1,4 @@ -BASE_DIR=$(realpath $(dirname $0)) +BASE_DIR=$(readlink -f $(dirname $0)) cd $BASE_DIR cd installations/consent source env/bin/activate diff --git a/synapse/stop.sh b/synapse/stop.sh index 1c5ab2fed7..ff9b004533 100644 --- a/synapse/stop.sh +++ b/synapse/stop.sh @@ -1,4 +1,4 @@ -BASE_DIR=$(realpath $(dirname $0)) +BASE_DIR=$(readlink -f $(dirname $0)) cd $BASE_DIR cd installations/consent source env/bin/activate From ebc9859cce39efd965ea5f9b91b9bf9a725ebbf3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 15:07:43 +0200 Subject: [PATCH 0046/2372] add instruction to install without chrome download --- install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/install.sh b/install.sh index c4e6d6253e..9004176e9a 100644 --- a/install.sh +++ b/install.sh @@ -1,3 +1,4 @@ +# run with PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true sh install.sh if chrome is already installed sh synapse/install.sh sh riot/install.sh npm install From 20becf87354725a7aff9cb402fcae3e76a25cc49 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 15:08:14 +0200 Subject: [PATCH 0047/2372] force running scripts in bash, as it's not the default shell on Ubuntu (which is what Travis runs) --- install.sh | 1 + riot/install.sh | 3 ++- riot/start.sh | 1 + riot/stop.sh | 1 + run.sh | 1 + synapse/install.sh | 3 ++- synapse/start.sh | 1 + synapse/stop.sh | 1 + 8 files changed, 10 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 9004176e9a..ca8a51bd69 100644 --- a/install.sh +++ b/install.sh @@ -1,3 +1,4 @@ +#!/bin/bash # run with PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true sh install.sh if chrome is already installed sh synapse/install.sh sh riot/install.sh diff --git a/riot/install.sh b/riot/install.sh index 8a8761b705..d9cc7292f0 100644 --- a/riot/install.sh +++ b/riot/install.sh @@ -1,7 +1,8 @@ +#!/bin/bash RIOT_BRANCH=master BASE_DIR=$(readlink -f $(dirname $0)) -if [[ -d $BASE_DIR/riot-web ]]; then +if [ -d $BASE_DIR/riot-web ]; then echo "riot is already installed" exit fi diff --git a/riot/start.sh b/riot/start.sh index ec3e5b32bc..3892a80a48 100644 --- a/riot/start.sh +++ b/riot/start.sh @@ -1,3 +1,4 @@ +#!/bin/bash PORT=8080 echo "running riot on http://localhost:$PORT..." BASE_DIR=$(readlink -f $(dirname $0)) diff --git a/riot/stop.sh b/riot/stop.sh index 40695c749c..a3dc722e58 100644 --- a/riot/stop.sh +++ b/riot/stop.sh @@ -1,3 +1,4 @@ +#!/bin/bash BASE_DIR=$(readlink -f $(dirname $0)) cd $BASE_DIR PIDFILE=riot.pid diff --git a/run.sh b/run.sh index d4d4261430..ae073d2ba9 100644 --- a/run.sh +++ b/run.sh @@ -1,3 +1,4 @@ +#!/bin/bash sh synapse/start.sh sh riot/start.sh node start.js diff --git a/synapse/install.sh b/synapse/install.sh index 661c98ecd5..8260f208c2 100644 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -1,3 +1,4 @@ +#!/bin/bash # config SYNAPSE_BRANCH=master INSTALLATION_NAME=consent @@ -7,7 +8,7 @@ PORT=8008 # set current directory to script directory BASE_DIR=$(readlink -f $(dirname $0)) -if [[ -d $BASE_DIR/$SERVER_DIR ]]; then +if [ -d $BASE_DIR/$SERVER_DIR ]; then echo "synapse is already installed" exit fi diff --git a/synapse/start.sh b/synapse/start.sh index f7af6ac0f7..785909a0b1 100644 --- a/synapse/start.sh +++ b/synapse/start.sh @@ -1,3 +1,4 @@ +#!/bin/bash BASE_DIR=$(readlink -f $(dirname $0)) cd $BASE_DIR cd installations/consent diff --git a/synapse/stop.sh b/synapse/stop.sh index ff9b004533..d83ddd0e4a 100644 --- a/synapse/stop.sh +++ b/synapse/stop.sh @@ -1,3 +1,4 @@ +#!/bin/bash BASE_DIR=$(readlink -f $(dirname $0)) cd $BASE_DIR cd installations/consent From edf37e3592a85a7d92026a71dbbfc64ccffefa59 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 15:15:10 +0200 Subject: [PATCH 0048/2372] add support for passing chrome path as env var --- start.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/start.js b/start.js index a37aa4538a..427fa319d0 100644 --- a/start.js +++ b/start.js @@ -30,7 +30,11 @@ global.browser = null; async function runTests() { console.log("running tests ..."); - global.browser = await puppeteer.launch(); + const options = {}; + if (process.env.CHROME_PATH) { + options.executablePath = process.env.CHROME_PATH; + } + global.browser = await puppeteer.launch(options); const page = await helpers.newPage(); const username = 'user-' + helpers.randomInt(10000); From c3b7e6c7cb6d7a7d983f2d7e6e393fff577439c6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 16:01:54 +0200 Subject: [PATCH 0049/2372] make scripts executable, running them with sh does something weird on travis --- README.md | 4 ++-- install.sh | 4 ++-- riot/install.sh | 0 riot/start.sh | 0 riot/stop.sh | 0 run.sh | 8 ++++---- synapse/install.sh | 0 synapse/start.sh | 0 synapse/stop.sh | 0 9 files changed, 8 insertions(+), 8 deletions(-) mode change 100644 => 100755 install.sh mode change 100644 => 100755 riot/install.sh mode change 100644 => 100755 riot/start.sh mode change 100644 => 100755 riot/stop.sh mode change 100644 => 100755 run.sh mode change 100644 => 100755 synapse/install.sh mode change 100644 => 100755 synapse/start.sh mode change 100644 => 100755 synapse/stop.sh diff --git a/README.md b/README.md index 5bd4c1dadc..b1a4e40aac 100644 --- a/README.md +++ b/README.md @@ -35,14 +35,14 @@ puppeteer.launch({headless: false}); ### Setup -Run `sh install.sh`. This will: +Run `./install.sh`. This will: - install synapse, fetches the master branch at the moment. If anything fails here, please refer to the synapse README to see if you're missing one of the prerequisites. - install riot, this fetches the master branch at the moment. - install dependencies (will download copy of chrome) ### Run tests -Run tests with `sh run.sh`. +Run tests with `./run.sh`. Developer Guide =============== diff --git a/install.sh b/install.sh old mode 100644 new mode 100755 index ca8a51bd69..98ce104ba0 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/bin/bash # run with PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true sh install.sh if chrome is already installed -sh synapse/install.sh -sh riot/install.sh +./synapse/install.sh +./riot/install.sh npm install diff --git a/riot/install.sh b/riot/install.sh old mode 100644 new mode 100755 diff --git a/riot/start.sh b/riot/start.sh old mode 100644 new mode 100755 diff --git a/riot/stop.sh b/riot/stop.sh old mode 100644 new mode 100755 diff --git a/run.sh b/run.sh old mode 100644 new mode 100755 index ae073d2ba9..90e1528d48 --- a/run.sh +++ b/run.sh @@ -1,6 +1,6 @@ #!/bin/bash -sh synapse/start.sh -sh riot/start.sh +./synapse/start.sh +./riot/start.sh node start.js -sh riot/stop.sh -sh synapse/stop.sh +./riot/stop.sh +./synapse/stop.sh diff --git a/synapse/install.sh b/synapse/install.sh old mode 100644 new mode 100755 diff --git a/synapse/start.sh b/synapse/start.sh old mode 100644 new mode 100755 diff --git a/synapse/stop.sh b/synapse/stop.sh old mode 100644 new mode 100755 From e8f626ba183831becbdf65e6c864e411b79a93a0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 16:04:41 +0200 Subject: [PATCH 0050/2372] exit on error --- install.sh | 1 + run.sh | 1 + start.js | 1 + 3 files changed, 3 insertions(+) diff --git a/install.sh b/install.sh index 98ce104ba0..4008099a3d 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,6 @@ #!/bin/bash # run with PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true sh install.sh if chrome is already installed +set -e ./synapse/install.sh ./riot/install.sh npm install diff --git a/run.sh b/run.sh index 90e1528d48..c59627e3a6 100755 --- a/run.sh +++ b/run.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e ./synapse/start.sh ./riot/start.sh node start.js diff --git a/start.js b/start.js index 427fa319d0..5e14539bfc 100644 --- a/start.js +++ b/start.js @@ -29,6 +29,7 @@ global.riotserver = 'http://localhost:8080'; global.browser = null; async function runTests() { + process.exit(-1); console.log("running tests ..."); const options = {}; if (process.env.CHROME_PATH) { From 976f041bbadbd43b434c336055f8a670a04c358a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 16:22:17 +0200 Subject: [PATCH 0051/2372] remove test exit, and use port we are semi-sure is free --- riot/start.sh | 2 +- start.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/riot/start.sh b/riot/start.sh index 3892a80a48..8e7e742f98 100755 --- a/riot/start.sh +++ b/riot/start.sh @@ -1,5 +1,5 @@ #!/bin/bash -PORT=8080 +PORT=5000 echo "running riot on http://localhost:$PORT..." BASE_DIR=$(readlink -f $(dirname $0)) cd $BASE_DIR/ diff --git a/start.js b/start.js index 5e14539bfc..05f4df9d0c 100644 --- a/start.js +++ b/start.js @@ -25,11 +25,10 @@ const acceptServerNoticesInviteAndConsent = require('./tests/server-notices-cons const homeserver = 'http://localhost:8008'; -global.riotserver = 'http://localhost:8080'; +global.riotserver = 'http://localhost:5000'; global.browser = null; async function runTests() { - process.exit(-1); console.log("running tests ..."); const options = {}; if (process.env.CHROME_PATH) { From 5cd52e2ebd17fed24f33c91e7b2bcad151788272 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 18:43:11 +0200 Subject: [PATCH 0052/2372] show browser logs on error --- helpers.js | 6 +++--- start.js | 14 +++++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/helpers.js b/helpers.js index fae5c8e859..a1f7434a29 100644 --- a/helpers.js +++ b/helpers.js @@ -58,9 +58,9 @@ function logXHRRequests(page) { const type = req.resourceType(); if (type === 'xhr' || type === 'fetch') { buffer += `${req.method()} ${req.url()} \n`; - if (req.method() === "POST") { - buffer += " Post data: " + req.postData(); - } + // if (req.method() === "POST") { + // buffer += " Post data: " + req.postData(); + // } } }); return { diff --git a/start.js b/start.js index 05f4df9d0c..aa459537c2 100644 --- a/start.js +++ b/start.js @@ -28,6 +28,9 @@ const homeserver = 'http://localhost:8008'; global.riotserver = 'http://localhost:5000'; global.browser = null; +let consoleLogs = null; +let xhrLogs = null; + async function runTests() { console.log("running tests ..."); const options = {}; @@ -36,6 +39,9 @@ async function runTests() { } global.browser = await puppeteer.launch(options); const page = await helpers.newPage(); + + consoleLogs = helpers.logConsole(page); + xhrLogs = helpers.logXHRRequests(page); const username = 'user-' + helpers.randomInt(10000); const password = 'testtest'; @@ -44,10 +50,12 @@ async function runTests() { process.stdout.write('done\n'); const noticesName = "Server Notices"; - process.stdout.write(`* accepting "${noticesName}" and accepting terms & conditions ...`); + process.stdout.write(`* accepting "${noticesName}" and accepting terms & conditions ... `); await acceptServerNoticesInviteAndConsent(page, noticesName); process.stdout.write('done\n'); + throw new Error('blubby'); + const room = 'test'; process.stdout.write(`* creating room ${room} ... `); await createRoom(page, room); @@ -62,6 +70,10 @@ function onSuccess() { function onFailure(err) { console.log('failure: ', err); + console.log('console.log output:'); + console.log(consoleLogs.logs()); + console.log('XHR requests:'); + console.log(xhrLogs.logs()); process.exit(-1); } From 758da7865941d4243ceacaa44f055cb7678c28bc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 18:43:40 +0200 Subject: [PATCH 0053/2372] dont fail when trying to stop riot and its not running --- riot/stop.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/riot/stop.sh b/riot/stop.sh index a3dc722e58..7ed18887f9 100755 --- a/riot/stop.sh +++ b/riot/stop.sh @@ -2,5 +2,8 @@ BASE_DIR=$(readlink -f $(dirname $0)) cd $BASE_DIR PIDFILE=riot.pid -kill $(cat $PIDFILE) -rm $PIDFILE +if [ -f $PIDFILE ]; then + echo "stopping riot server ..." + kill $(cat $PIDFILE) + rm $PIDFILE +fi From 29d688543d34554c5b56c8ce78eb7491f8f51429 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 18:44:01 +0200 Subject: [PATCH 0054/2372] stop servers on error in run script --- riot/start.sh | 2 +- run.sh | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/riot/start.sh b/riot/start.sh index 8e7e742f98..143e5d9f8f 100755 --- a/riot/start.sh +++ b/riot/start.sh @@ -1,6 +1,6 @@ #!/bin/bash PORT=5000 -echo "running riot on http://localhost:$PORT..." +echo "running riot on http://localhost:$PORT ..." BASE_DIR=$(readlink -f $(dirname $0)) cd $BASE_DIR/ pushd riot-web/webapp/ > /dev/null diff --git a/run.sh b/run.sh index c59627e3a6..02b2e4cbdf 100755 --- a/run.sh +++ b/run.sh @@ -1,7 +1,19 @@ #!/bin/bash -set -e + +stop_servers() { + ./riot/stop.sh + ./synapse/stop.sh +} + +handle_error() { + EXIT_CODE=$? + stop_servers + exit $EXIT_CODE +} + +trap 'handle_error' ERR + ./synapse/start.sh ./riot/start.sh node start.js -./riot/stop.sh -./synapse/stop.sh +stop_servers From 5129bb57b608cb68a404d35f1d26f47bd4809a12 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 18:58:37 +0200 Subject: [PATCH 0055/2372] log all requests with their response code --- helpers.js | 9 +++++---- start.js | 2 -- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/helpers.js b/helpers.js index a1f7434a29..e830824e7c 100644 --- a/helpers.js +++ b/helpers.js @@ -54,14 +54,15 @@ function logConsole(page) { function logXHRRequests(page) { let buffer = ""; - page.on('request', req => { + page.on('requestfinished', async (req) => { const type = req.resourceType(); - if (type === 'xhr' || type === 'fetch') { - buffer += `${req.method()} ${req.url()} \n`; + const response = await req.response(); + //if (type === 'xhr' || type === 'fetch') { + buffer += `${type} ${response.status()} ${req.method()} ${req.url()} \n`; // if (req.method() === "POST") { // buffer += " Post data: " + req.postData(); // } - } + //} }); return { logs() { diff --git a/start.js b/start.js index aa459537c2..c4eba26b58 100644 --- a/start.js +++ b/start.js @@ -54,8 +54,6 @@ async function runTests() { await acceptServerNoticesInviteAndConsent(page, noticesName); process.stdout.write('done\n'); - throw new Error('blubby'); - const room = 'test'; process.stdout.write(`* creating room ${room} ... `); await createRoom(page, room); From 31fcf08fece786f7fc7c3fd96770037c0926dc2e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Jul 2018 11:41:01 +0200 Subject: [PATCH 0056/2372] only allow one riot server instance simultaneously --- riot/start.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/riot/start.sh b/riot/start.sh index 143e5d9f8f..2c76747e46 100755 --- a/riot/start.sh +++ b/riot/start.sh @@ -1,10 +1,18 @@ #!/bin/bash PORT=5000 -echo "running riot on http://localhost:$PORT ..." BASE_DIR=$(readlink -f $(dirname $0)) +PIDFILE=riot.pid +CONFIG_BACKUP=config.e2etests_backup.json + cd $BASE_DIR/ + +if [ -f $PIDFILE ]; then + exit +fi + +echo "running riot on http://localhost:$PORT ..." pushd riot-web/webapp/ > /dev/null python -m SimpleHTTPServer $PORT > /dev/null 2>&1 & PID=$! popd > /dev/null -echo $PID > riot.pid +echo $PID > $PIDFILE From e50420dd1baa991f9229f4dec10e82442684b7ac Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 19:27:53 +0200 Subject: [PATCH 0057/2372] apply config file when starting riot, not installing, so we can support riots that were built by another process --- riot/install.sh | 1 - riot/start.sh | 7 +++++++ riot/stop.sh | 12 +++++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/riot/install.sh b/riot/install.sh index d9cc7292f0..a215b46cea 100755 --- a/riot/install.sh +++ b/riot/install.sh @@ -12,7 +12,6 @@ curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --outpu unzip riot.zip rm riot.zip mv riot-web-${RIOT_BRANCH} riot-web -cp config-template/config.json riot-web/ cd riot-web npm install npm run build diff --git a/riot/start.sh b/riot/start.sh index 2c76747e46..35f66f68e3 100755 --- a/riot/start.sh +++ b/riot/start.sh @@ -12,6 +12,13 @@ fi echo "running riot on http://localhost:$PORT ..." pushd riot-web/webapp/ > /dev/null + +# backup config file before we copy template +if [ -f config.json ]; then + mv config.json $CONFIG_BACKUP +fi +cp $BASE_DIR/config-template/config.json . + python -m SimpleHTTPServer $PORT > /dev/null 2>&1 & PID=$! popd > /dev/null diff --git a/riot/stop.sh b/riot/stop.sh index 7ed18887f9..0773174fd1 100755 --- a/riot/stop.sh +++ b/riot/stop.sh @@ -1,9 +1,19 @@ #!/bin/bash BASE_DIR=$(readlink -f $(dirname $0)) -cd $BASE_DIR PIDFILE=riot.pid +CONFIG_BACKUP=config.e2etests_backup.json + +cd $BASE_DIR + if [ -f $PIDFILE ]; then echo "stopping riot server ..." kill $(cat $PIDFILE) rm $PIDFILE + + # revert config file + cd riot-web/webapp + rm config.json + if [ -f $CONFIG_BACKUP ]; then + mv $CONFIG_BACKUP config.json + fi fi From a5c891144567442f5a3fee2a142af4842de7f6e2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 20:01:13 +0200 Subject: [PATCH 0058/2372] output document html on error and dont make a screenshot on submit --- start.js | 13 ++++++++++++- tests/signup.js | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/start.js b/start.js index c4eba26b58..c109430fb8 100644 --- a/start.js +++ b/start.js @@ -30,6 +30,7 @@ global.browser = null; let consoleLogs = null; let xhrLogs = null; +let globalPage = null; async function runTests() { console.log("running tests ..."); @@ -39,6 +40,7 @@ async function runTests() { } global.browser = await puppeteer.launch(options); const page = await helpers.newPage(); + globalPage = page; consoleLogs = helpers.logConsole(page); xhrLogs = helpers.logXHRRequests(page); @@ -66,12 +68,21 @@ function onSuccess() { console.log('all tests finished successfully'); } -function onFailure(err) { +async function onFailure(err) { + + let documentHtml = "no page"; + if (globalPage) { + documentHtml = await globalPage.content(); + } + console.log('failure: ', err); console.log('console.log output:'); console.log(consoleLogs.logs()); console.log('XHR requests:'); console.log(xhrLogs.logs()); + console.log('document html:'); + console.log(documentHtml); + process.exit(-1); } diff --git a/tests/signup.js b/tests/signup.js index 65044f900c..06035b61e3 100644 --- a/tests/signup.js +++ b/tests/signup.js @@ -55,7 +55,7 @@ module.exports = async function signup(page, username, password, homeserver) { const error_text = await helpers.tryGetInnertext(page, '.mx_Login_error'); assert.strictEqual(!!error_text, false); //submit form - await page.screenshot({path: "beforesubmit.png", fullPage: true}); + //await page.screenshot({path: "beforesubmit.png", fullPage: true}); await registerButton.click(); //confirm dialog saying you cant log back in without e-mail From d738b404ca207337ea8295280781c55e3c11439f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 30 Jul 2018 11:11:06 +0200 Subject: [PATCH 0059/2372] try upgrading puppeteer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 48644df401..1cbdf5bd26 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,6 @@ "author": "", "license": "ISC", "dependencies": { - "puppeteer": "^1.5.0" + "puppeteer": "^1.6.0" } } From c357a0158dac92af4c30fc22258b56e2f4c7cc5f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 30 Jul 2018 13:40:23 +0200 Subject: [PATCH 0060/2372] no need to log contents of zip files --- riot/install.sh | 2 +- synapse/install.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/riot/install.sh b/riot/install.sh index a215b46cea..7f37fa9457 100755 --- a/riot/install.sh +++ b/riot/install.sh @@ -9,7 +9,7 @@ fi cd $BASE_DIR curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --output riot.zip -unzip riot.zip +unzip -q riot.zip rm riot.zip mv riot-web-${RIOT_BRANCH} riot-web cd riot-web diff --git a/synapse/install.sh b/synapse/install.sh index 8260f208c2..dc4a08cb41 100755 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -17,7 +17,7 @@ cd $BASE_DIR mkdir -p installations/ curl https://codeload.github.com/matrix-org/synapse/zip/$SYNAPSE_BRANCH --output synapse.zip -unzip synapse.zip +unzip -q synapse.zip mv synapse-$SYNAPSE_BRANCH $SERVER_DIR cd $SERVER_DIR virtualenv -p python2.7 env From 9a2f3094866d56281a6de930ee2c170eeb12325b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Jul 2018 10:08:01 +0200 Subject: [PATCH 0061/2372] xhr and console logs are done for all tests now, no need to do it in signup anymore --- tests/signup.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/signup.js b/tests/signup.js index 06035b61e3..70c478aed1 100644 --- a/tests/signup.js +++ b/tests/signup.js @@ -19,8 +19,6 @@ const acceptTerms = require('./consent'); const assert = require('assert'); module.exports = async function signup(page, username, password, homeserver) { - const consoleLogs = helpers.logConsole(page); - const xhrLogs = helpers.logXHRRequests(page); await page.goto(helpers.riotUrl('/#/register')); //click 'Custom server' radio button if (homeserver) { From 387657721898c3d1618ab2ae29bbc2a759548288 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Jul 2018 10:25:26 +0200 Subject: [PATCH 0062/2372] log when using external chrome! --- start.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/start.js b/start.js index c109430fb8..4912f901c1 100644 --- a/start.js +++ b/start.js @@ -36,7 +36,9 @@ async function runTests() { console.log("running tests ..."); const options = {}; if (process.env.CHROME_PATH) { - options.executablePath = process.env.CHROME_PATH; + const path = process.env.CHROME_PATH; + console.log(`(using external chrome/chromium at ${path}, make sure it's compatible with puppeteer)`); + options.executablePath = path; } global.browser = await puppeteer.launch(options); const page = await helpers.newPage(); From f57628e3d0a0b6b53484df6d95249da4cde6935c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Jul 2018 12:54:39 +0200 Subject: [PATCH 0063/2372] dont swallow riot server errors --- riot/start.sh | 42 ++++++++++++++++++++++++++++++++++-------- riot/stop.sh | 3 ++- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/riot/start.sh b/riot/start.sh index 35f66f68e3..1127535d2b 100755 --- a/riot/start.sh +++ b/riot/start.sh @@ -1,16 +1,15 @@ #!/bin/bash PORT=5000 BASE_DIR=$(readlink -f $(dirname $0)) -PIDFILE=riot.pid +PIDFILE=$BASE_DIR/riot.pid CONFIG_BACKUP=config.e2etests_backup.json -cd $BASE_DIR/ - if [ -f $PIDFILE ]; then exit fi -echo "running riot on http://localhost:$PORT ..." +cd $BASE_DIR/ +echo -n "starting riot on http://localhost:$PORT ... " pushd riot-web/webapp/ > /dev/null # backup config file before we copy template @@ -19,7 +18,34 @@ if [ -f config.json ]; then fi cp $BASE_DIR/config-template/config.json . -python -m SimpleHTTPServer $PORT > /dev/null 2>&1 & -PID=$! -popd > /dev/null -echo $PID > $PIDFILE +LOGFILE=$(mktemp) +# run web server in the background, showing output on error +( + python -m SimpleHTTPServer $PORT > $LOGFILE 2>&1 & + PID=$! + echo $PID > $PIDFILE + # wait so subshell does not exit + # otherwise sleep below would not work + wait $PID; RESULT=$? + + # NOT expected SIGTERM (128 + 15) + # from stop.sh? + if [ $RESULT -ne 143 ]; then + echo "failed" + cat $LOGFILE + rm $PIDFILE 2> /dev/null + fi + rm $LOGFILE + exit $RESULT +)& +# to be able to return the exit code for immediate errors (like address already in use) +# we wait for a short amount of time in the background and exit when the first +# child process exists +sleep 0.5 & +# wait the first child process to exit (python or sleep) +wait -n; RESULT=$? +# return exit code of first child to exit +if [ $RESULT -eq 0 ]; then + echo "running" +fi +exit $RESULT diff --git a/riot/stop.sh b/riot/stop.sh index 0773174fd1..8d3c925c80 100755 --- a/riot/stop.sh +++ b/riot/stop.sh @@ -7,8 +7,9 @@ cd $BASE_DIR if [ -f $PIDFILE ]; then echo "stopping riot server ..." - kill $(cat $PIDFILE) + PID=$(cat $PIDFILE) rm $PIDFILE + kill $PID # revert config file cd riot-web/webapp From 97fa7e03d13bd73726bcbd0e6c556a8e336163a4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Jul 2018 14:45:14 +0200 Subject: [PATCH 0064/2372] dont swallow synapse startup errors --- synapse/start.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/synapse/start.sh b/synapse/start.sh index 785909a0b1..12b89b31ed 100755 --- a/synapse/start.sh +++ b/synapse/start.sh @@ -3,4 +3,10 @@ BASE_DIR=$(readlink -f $(dirname $0)) cd $BASE_DIR cd installations/consent source env/bin/activate -./synctl start 2> /dev/null +LOGFILE=$(mktemp) +./synctl start 2> $LOGFILE +EXIT_CODE=$? +if [ $EXIT_CODE -ne 0 ]; then + cat $LOGFILE +fi +exit $EXIT_CODE \ No newline at end of file From 7c91ecab7eaa7993e4d64faf1a35d5ca183987d4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 7 Aug 2018 16:45:34 +0200 Subject: [PATCH 0065/2372] create session object to scope a user, move helper methods there --- helpers.js | 149 ------------------ src/session.js | 147 +++++++++++++++++ {tests => src/tests}/consent.js | 9 +- {tests => src/tests}/create-room.js | 13 +- {tests => src/tests}/join.js | 15 +- .../tests}/server-notices-consent.js | 15 +- {tests => src/tests}/signup.js | 35 ++-- start.js | 36 ++--- 8 files changed, 203 insertions(+), 216 deletions(-) delete mode 100644 helpers.js create mode 100644 src/session.js rename {tests => src/tests}/consent.js (70%) rename {tests => src/tests}/create-room.js (57%) rename {tests => src/tests}/join.js (53%) rename {tests => src/tests}/server-notices-consent.js (68%) rename {tests => src/tests}/signup.js (60%) diff --git a/helpers.js b/helpers.js deleted file mode 100644 index e830824e7c..0000000000 --- a/helpers.js +++ /dev/null @@ -1,149 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// puppeteer helpers - -// TODO: rename to queryAndInnertext? -async function tryGetInnertext(page, selector) { - const field = await page.$(selector); - if (field != null) { - const text_handle = await field.getProperty('innerText'); - return await text_handle.jsonValue(); - } - return null; -} - -async function innerText(page, field) { - const text_handle = await field.getProperty('innerText'); - return await text_handle.jsonValue(); -} - -async function newPage() { - const page = await browser.newPage(); - await page.setViewport({ - width: 1280, - height: 800 - }); - return page; -} - -function logConsole(page) { - let buffer = ""; - page.on('console', msg => { - buffer += msg.text() + '\n'; - }); - return { - logs() { - return buffer; - } - } -} - -function logXHRRequests(page) { - let buffer = ""; - page.on('requestfinished', async (req) => { - const type = req.resourceType(); - const response = await req.response(); - //if (type === 'xhr' || type === 'fetch') { - buffer += `${type} ${response.status()} ${req.method()} ${req.url()} \n`; - // if (req.method() === "POST") { - // buffer += " Post data: " + req.postData(); - // } - //} - }); - return { - logs() { - return buffer; - } - } -} - -async function getOuterHTML(element_handle) { - const html_handle = await element_handle.getProperty('outerHTML'); - return await html_handle.jsonValue(); -} - -async function printElements(label, elements) { - console.log(label, await Promise.all(elements.map(getOuterHTML))); -} - -async function replaceInputText(input, text) { - // click 3 times to select all text - await input.click({clickCount: 3}); - // then remove it with backspace - await input.press('Backspace'); - // and type the new text - await input.type(text); -} - -// TODO: rename to waitAndQuery(Single)? -async function waitAndQuerySelector(page, selector, timeout = 500) { - await page.waitForSelector(selector, {visible: true, timeout}); - return await page.$(selector); -} - -async function waitAndQueryAll(page, selector, timeout = 500) { - await page.waitForSelector(selector, {visible: true, timeout}); - return await page.$$(selector); -} - -function waitForNewPage(timeout = 500) { - return new Promise((resolve, reject) => { - const timeoutHandle = setTimeout(() => { - browser.removeEventListener('targetcreated', callback); - reject(new Error(`timeout of ${timeout}ms for waitForNewPage elapsed`)); - }, timeout); - - const callback = async (target) => { - clearTimeout(timeoutHandle); - const page = await target.page(); - resolve(page); - }; - - browser.once('targetcreated', callback); - }); -} - -// other helpers - -function randomInt(max) { - return Math.ceil(Math.random()*max); -} - -function riotUrl(path) { - return riotserver + path; -} - -function delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -module.exports = { - tryGetInnertext, - innerText, - newPage, - logConsole, - logXHRRequests, - getOuterHTML, - printElements, - replaceInputText, - waitAndQuerySelector, - waitAndQueryAll, - waitForNewPage, - randomInt, - riotUrl, - delay, -} \ No newline at end of file diff --git a/src/session.js b/src/session.js new file mode 100644 index 0000000000..51bad4a3c9 --- /dev/null +++ b/src/session.js @@ -0,0 +1,147 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const puppeteer = require('puppeteer'); + +module.exports = class RiotSession { + constructor(browser, page, username, riotserver) { + this.browser = browser; + this.page = page; + this.riotserver = riotserver; + this.username = username; + } + + static async create(username, puppeteerOptions, riotserver) { + const browser = await puppeteer.launch(puppeteerOptions); + const page = await browser.newPage(); + await page.setViewport({ + width: 1280, + height: 800 + }); + return new RiotSession(browser, page, username, riotserver); + } + + async tryGetInnertext(selector) { + const field = await this.page.$(selector); + if (field != null) { + const text_handle = await field.getProperty('innerText'); + return await text_handle.jsonValue(); + } + return null; + } + + async innerText(field) { + const text_handle = await field.getProperty('innerText'); + return await text_handle.jsonValue(); + } + + logConsole() { + let buffer = ""; + this.page.on('console', msg => { + buffer += msg.text() + '\n'; + }); + return { + logs() { + return buffer; + } + } + } + + logXHRRequests() { + let buffer = ""; + this.page.on('requestfinished', async (req) => { + const type = req.resourceType(); + const response = await req.response(); + //if (type === 'xhr' || type === 'fetch') { + buffer += `${type} ${response.status()} ${req.method()} ${req.url()} \n`; + // if (req.method() === "POST") { + // buffer += " Post data: " + req.postData(); + // } + //} + }); + return { + logs() { + return buffer; + } + } + } + + async getOuterHTML(element_handle) { + const html_handle = await element_handle.getProperty('outerHTML'); + return await html_handle.jsonValue(); + } + + async printElements(label, elements) { + console.log(label, await Promise.all(elements.map(getOuterHTML))); + } + + async replaceInputText(input, text) { + // click 3 times to select all text + await input.click({clickCount: 3}); + // then remove it with backspace + await input.press('Backspace'); + // and type the new text + await input.type(text); + } + + // TODO: rename to waitAndQuery(Single)? + async waitAndQuerySelector(selector, timeout = 500) { + await this.page.waitForSelector(selector, {visible: true, timeout}); + return await this.page.$(selector); + } + + async waitAndQueryAll(selector, timeout = 500) { + await this.page.waitForSelector(selector, {visible: true, timeout}); + return await this.page.$$(selector); + } + + waitForNewPage(timeout = 500) { + return new Promise((resolve, reject) => { + const timeoutHandle = setTimeout(() => { + this.browser.removeEventListener('targetcreated', callback); + reject(new Error(`timeout of ${timeout}ms for waitForNewPage elapsed`)); + }, timeout); + + const callback = async (target) => { + clearTimeout(timeoutHandle); + const page = await target.page(); + resolve(page); + }; + + this.browser.once('targetcreated', callback); + }); + } + + waitForSelector(selector) { + return this.page.waitForSelector(selector); + } + + goto(url) { + return this.page.goto(url); + } + + riotUrl(path) { + return this.riotserver + path; + } + + delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + close() { + return this.browser.close(); + } +} diff --git a/tests/consent.js b/src/tests/consent.js similarity index 70% rename from tests/consent.js rename to src/tests/consent.js index 3c8ada9a5e..09026a3082 100644 --- a/tests/consent.js +++ b/src/tests/consent.js @@ -14,15 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -const helpers = require('../helpers'); const assert = require('assert'); -module.exports = async function acceptTerms(page) { - const reviewTermsButton = await helpers.waitAndQuerySelector(page, '.mx_QuestionDialog button.mx_Dialog_primary', 5000); - const termsPagePromise = helpers.waitForNewPage(); +module.exports = async function acceptTerms(session) { + const reviewTermsButton = await session.waitAndQuerySelector('.mx_QuestionDialog button.mx_Dialog_primary', 5000); + const termsPagePromise = session.waitForNewPage(); await reviewTermsButton.click(); const termsPage = await termsPagePromise; const acceptButton = await termsPage.$('input[type=submit]'); await acceptButton.click(); - await helpers.delay(500); //TODO yuck, timers + await session.delay(500); //TODO yuck, timers } \ No newline at end of file diff --git a/tests/create-room.js b/src/tests/create-room.js similarity index 57% rename from tests/create-room.js rename to src/tests/create-room.js index 4c9004bcaf..948e0b115f 100644 --- a/tests/create-room.js +++ b/src/tests/create-room.js @@ -14,19 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -const helpers = require('../helpers'); const assert = require('assert'); -module.exports = async function createRoom(page, roomName) { +module.exports = async function createRoom(session, roomName) { //TODO: brittle selector - const createRoomButton = await helpers.waitAndQuerySelector(page, '.mx_RoleButton[aria-label="Create new room"]'); + const createRoomButton = await session.waitAndQuerySelector('.mx_RoleButton[aria-label="Create new room"]'); await createRoomButton.click(); - const roomNameInput = await helpers.waitAndQuerySelector(page, '.mx_CreateRoomDialog_input'); - await helpers.replaceInputText(roomNameInput, roomName); + const roomNameInput = await session.waitAndQuerySelector('.mx_CreateRoomDialog_input'); + await session.replaceInputText(roomNameInput, roomName); - const createButton = await helpers.waitAndQuerySelector(page, '.mx_Dialog_primary'); + const createButton = await session.waitAndQuerySelector('.mx_Dialog_primary'); await createButton.click(); - await page.waitForSelector('.mx_MessageComposer'); + await session.waitForSelector('.mx_MessageComposer'); } \ No newline at end of file diff --git a/tests/join.js b/src/tests/join.js similarity index 53% rename from tests/join.js rename to src/tests/join.js index ea16a93936..a359d6ef64 100644 --- a/tests/join.js +++ b/src/tests/join.js @@ -14,22 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -const helpers = require('../helpers'); const assert = require('assert'); -module.exports = async function join(page, roomName) { +module.exports = async function join(session, roomName) { //TODO: brittle selector - const directoryButton = await helpers.waitAndQuerySelector(page, '.mx_RoleButton[aria-label="Room directory"]'); + const directoryButton = await session.waitAndQuerySelector('.mx_RoleButton[aria-label="Room directory"]'); await directoryButton.click(); - const roomInput = await helpers.waitAndQuerySelector(page, '.mx_DirectorySearchBox_input'); - await helpers.replaceInputText(roomInput, roomName); + const roomInput = await session.waitAndQuerySelector('.mx_DirectorySearchBox_input'); + await session.replaceInputText(roomInput, roomName); - const firstRoomLabel = await helpers.waitAndQuerySelector(page, '.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child'); + const firstRoomLabel = await session.waitAndQuerySelector('.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child'); await firstRoomLabel.click(); - const joinLink = await helpers.waitAndQuerySelector(page, '.mx_RoomPreviewBar_join_text a'); + const joinLink = await session.waitAndQuerySelector('.mx_RoomPreviewBar_join_text a'); await joinLink.click(); - await page.waitForSelector('.mx_MessageComposer'); + await session.waitForSelector('.mx_MessageComposer'); } \ No newline at end of file diff --git a/tests/server-notices-consent.js b/src/tests/server-notices-consent.js similarity index 68% rename from tests/server-notices-consent.js rename to src/tests/server-notices-consent.js index 2689036a96..0eb4cd8722 100644 --- a/tests/server-notices-consent.js +++ b/src/tests/server-notices-consent.js @@ -14,14 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -const helpers = require('../helpers'); const assert = require('assert'); -module.exports = async function acceptServerNoticesInviteAndConsent(page, name) { +module.exports = async function acceptServerNoticesInviteAndConsent(session, name) { //TODO: brittle selector - const invitesHandles = await helpers.waitAndQueryAll(page, '.mx_RoomTile_name.mx_RoomTile_invite'); + const invitesHandles = await session.waitAndQueryAll('.mx_RoomTile_name.mx_RoomTile_invite'); const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => { - const text = await helpers.innerText(page, inviteHandle); + const text = await session.innerText(inviteHandle); return {inviteHandle, text}; })); const inviteHandle = invitesWithText.find(({inviteHandle, text}) => { @@ -30,15 +29,15 @@ module.exports = async function acceptServerNoticesInviteAndConsent(page, name) await inviteHandle.click(); - const acceptInvitationLink = await helpers.waitAndQuerySelector(page, ".mx_RoomPreviewBar_join_text a:first-child"); + const acceptInvitationLink = await session.waitAndQuerySelector(".mx_RoomPreviewBar_join_text a:first-child"); await acceptInvitationLink.click(); - const consentLink = await helpers.waitAndQuerySelector(page, ".mx_EventTile_body a", 1000); + const consentLink = await session.waitAndQuerySelector(".mx_EventTile_body a", 1000); - const termsPagePromise = helpers.waitForNewPage(); + const termsPagePromise = session.waitForNewPage(); await consentLink.click(); const termsPage = await termsPagePromise; const acceptButton = await termsPage.$('input[type=submit]'); await acceptButton.click(); - await helpers.delay(500); //TODO yuck, timers + await session.delay(500); //TODO yuck, timers } \ No newline at end of file diff --git a/tests/signup.js b/src/tests/signup.js similarity index 60% rename from tests/signup.js rename to src/tests/signup.js index 70c478aed1..db6ad6208a 100644 --- a/tests/signup.js +++ b/src/tests/signup.js @@ -14,55 +14,54 @@ See the License for the specific language governing permissions and limitations under the License. */ -const helpers = require('../helpers'); const acceptTerms = require('./consent'); const assert = require('assert'); -module.exports = async function signup(page, username, password, homeserver) { - await page.goto(helpers.riotUrl('/#/register')); +module.exports = async function signup(session, username, password, homeserver) { + await session.goto(session.riotUrl('/#/register')); //click 'Custom server' radio button if (homeserver) { - const advancedRadioButton = await helpers.waitAndQuerySelector(page, '#advanced'); + const advancedRadioButton = await session.waitAndQuerySelector('#advanced'); await advancedRadioButton.click(); } // wait until register button is visible - await page.waitForSelector('.mx_Login_submit[value=Register]', {visible: true, timeout: 500}); + await session.waitForSelector('.mx_Login_submit[value=Register]', {visible: true, timeout: 500}); //fill out form - const loginFields = await page.$$('.mx_Login_field'); + const loginFields = await session.page.$$('.mx_Login_field'); assert.strictEqual(loginFields.length, 7); const usernameField = loginFields[2]; const passwordField = loginFields[3]; const passwordRepeatField = loginFields[4]; const hsurlField = loginFields[5]; - await helpers.replaceInputText(usernameField, username); - await helpers.replaceInputText(passwordField, password); - await helpers.replaceInputText(passwordRepeatField, password); + await session.replaceInputText(usernameField, username); + await session.replaceInputText(passwordField, password); + await session.replaceInputText(passwordRepeatField, password); if (homeserver) { - await page.waitForSelector('.mx_ServerConfig', {visible: true, timeout: 500}); - await helpers.replaceInputText(hsurlField, homeserver); + await session.waitForSelector('.mx_ServerConfig', {visible: true, timeout: 500}); + await session.replaceInputText(hsurlField, homeserver); } //wait over a second because Registration/ServerConfig have a 1000ms //delay to internally set the homeserver url //see Registration::render and ServerConfig::props::delayTimeMs - await helpers.delay(1200); + await session.delay(1200); /// focus on the button to make sure error validation /// has happened before checking the form is good to go - const registerButton = await page.$('.mx_Login_submit'); + const registerButton = await session.page.$('.mx_Login_submit'); await registerButton.focus(); //check no errors - const error_text = await helpers.tryGetInnertext(page, '.mx_Login_error'); + const error_text = await session.tryGetInnertext('.mx_Login_error'); assert.strictEqual(!!error_text, false); //submit form //await page.screenshot({path: "beforesubmit.png", fullPage: true}); await registerButton.click(); //confirm dialog saying you cant log back in without e-mail - const continueButton = await helpers.waitAndQuerySelector(page, '.mx_QuestionDialog button.mx_Dialog_primary'); + const continueButton = await session.waitAndQuerySelector('.mx_QuestionDialog button.mx_Dialog_primary'); await continueButton.click(); //wait for registration to finish so the hash gets set //onhashchange better? - await helpers.delay(2000); + await session.delay(2000); - const url = page.url(); - assert.strictEqual(url, helpers.riotUrl('/#/home')); + const url = session.page.url(); + assert.strictEqual(url, session.riotUrl('/#/home')); } diff --git a/start.js b/start.js index 4912f901c1..c1aecb65aa 100644 --- a/start.js +++ b/start.js @@ -14,19 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -const puppeteer = require('puppeteer'); -const helpers = require('./helpers'); const assert = require('assert'); +const RiotSession = require('./src/session'); -const signup = require('./tests/signup'); -const join = require('./tests/join'); -const createRoom = require('./tests/create-room'); -const acceptServerNoticesInviteAndConsent = require('./tests/server-notices-consent'); +const signup = require('./src/tests/signup'); +const join = require('./src/tests/join'); +const createRoom = require('./src/tests/create-room'); +const acceptServerNoticesInviteAndConsent = require('./src/tests/server-notices-consent'); const homeserver = 'http://localhost:8008'; - -global.riotserver = 'http://localhost:5000'; -global.browser = null; +const riotserver = 'http://localhost:5000'; let consoleLogs = null; let xhrLogs = null; @@ -40,30 +37,27 @@ async function runTests() { console.log(`(using external chrome/chromium at ${path}, make sure it's compatible with puppeteer)`); options.executablePath = path; } - global.browser = await puppeteer.launch(options); - const page = await helpers.newPage(); - globalPage = page; - consoleLogs = helpers.logConsole(page); - xhrLogs = helpers.logXHRRequests(page); + const alice = await RiotSession.create("alice", options, riotserver); + + consoleLogs = alice.logConsole(); + xhrLogs = alice.logXHRRequests(); - const username = 'user-' + helpers.randomInt(10000); - const password = 'testtest'; - process.stdout.write(`* signing up as ${username} ... `); - await signup(page, username, password); + process.stdout.write(`* signing up as ${alice.username} ... `); + await signup(alice, alice.username, 'testtest'); process.stdout.write('done\n'); const noticesName = "Server Notices"; process.stdout.write(`* accepting "${noticesName}" and accepting terms & conditions ... `); - await acceptServerNoticesInviteAndConsent(page, noticesName); + await acceptServerNoticesInviteAndConsent(alice, noticesName); process.stdout.write('done\n'); const room = 'test'; process.stdout.write(`* creating room ${room} ... `); - await createRoom(page, room); + await createRoom(alice, room); process.stdout.write('done\n'); - await browser.close(); + await alice.close(); } function onSuccess() { From 6b843eacfcf76be987273e63100e11b446a7102b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 7 Aug 2018 17:09:43 +0200 Subject: [PATCH 0066/2372] move log buffers into session, start logging implicitely --- src/session.js | 37 +++++++++++++++++++++++++++---------- start.js | 32 ++++++++++++++------------------ 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/session.js b/src/session.js index 51bad4a3c9..f8f41b20b8 100644 --- a/src/session.js +++ b/src/session.js @@ -16,12 +16,33 @@ limitations under the License. const puppeteer = require('puppeteer'); +class LogBuffer { + constructor(page, eventName, eventMapper, reduceAsync=false, initialValue = "") { + this.buffer = initialValue; + page.on(eventName, (arg) => { + const result = eventMapper(arg); + if (reduceAsync) { + result.then((r) => this.buffer += r); + } + else { + this.buffer += result; + } + }); + } +} + module.exports = class RiotSession { constructor(browser, page, username, riotserver) { this.browser = browser; this.page = page; this.riotserver = riotserver; this.username = username; + this.consoleLog = new LogBuffer(page, "console", (msg) => `${msg.text()}\n`); + this.networkLog = new LogBuffer(page, "requestfinished", async (req) => { + const type = req.resourceType(); + const response = await req.response(); + return `${type} ${response.status()} ${req.method()} ${req.url()} \n`; + }, true); } static async create(username, puppeteerOptions, riotserver) { @@ -48,16 +69,12 @@ module.exports = class RiotSession { return await text_handle.jsonValue(); } - logConsole() { - let buffer = ""; - this.page.on('console', msg => { - buffer += msg.text() + '\n'; - }); - return { - logs() { - return buffer; - } - } + consoleLogs() { + return this.consoleLog.buffer; + } + + networkLogs() { + return this.networkLog.buffer; } logXHRRequests() { diff --git a/start.js b/start.js index c1aecb65aa..0aa2cb9364 100644 --- a/start.js +++ b/start.js @@ -25,9 +25,7 @@ const acceptServerNoticesInviteAndConsent = require('./src/tests/server-notices- const homeserver = 'http://localhost:8008'; const riotserver = 'http://localhost:5000'; -let consoleLogs = null; -let xhrLogs = null; -let globalPage = null; +let sessions = []; async function runTests() { console.log("running tests ..."); @@ -39,9 +37,7 @@ async function runTests() { } const alice = await RiotSession.create("alice", options, riotserver); - - consoleLogs = alice.logConsole(); - xhrLogs = alice.logXHRRequests(); + sessions.push(alice); process.stdout.write(`* signing up as ${alice.username} ... `); await signup(alice, alice.username, 'testtest'); @@ -65,19 +61,19 @@ function onSuccess() { } async function onFailure(err) { - - let documentHtml = "no page"; - if (globalPage) { - documentHtml = await globalPage.content(); - } - console.log('failure: ', err); - console.log('console.log output:'); - console.log(consoleLogs.logs()); - console.log('XHR requests:'); - console.log(xhrLogs.logs()); - console.log('document html:'); - console.log(documentHtml); + for(var i = 0; i < sessions.length; ++i) { + const session = sessions[i]; + documentHtml = await session.page.content(); + console.log(`---------------- START OF ${session.username} LOGS ----------------`); + console.log('---------------- console.log output:'); + console.log(session.consoleLogs()); + console.log('---------------- network requests:'); + console.log(session.networkLogs()); + console.log('---------------- document html:'); + console.log(documentHtml); + console.log(`---------------- END OF ${session.username} LOGS ----------------`); + } process.exit(-1); } From 4c0ab117bfab1ce4c4caebe83c5f67c1e7488feb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 7 Aug 2018 17:16:27 +0200 Subject: [PATCH 0067/2372] move outputting steps to session to scope it to username --- src/session.js | 15 +++++++++++++++ start.js | 14 +++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/session.js b/src/session.js index f8f41b20b8..5b0f78ccd3 100644 --- a/src/session.js +++ b/src/session.js @@ -31,6 +31,20 @@ class LogBuffer { } } +class Logger { + constructor(username) { + this.username = username; + } + + step(description) { + process.stdout.write(` * ${this.username} ${description} ... `); + } + + done() { + process.stdout.write("done\n"); + } +} + module.exports = class RiotSession { constructor(browser, page, username, riotserver) { this.browser = browser; @@ -43,6 +57,7 @@ module.exports = class RiotSession { const response = await req.response(); return `${type} ${response.status()} ${req.method()} ${req.url()} \n`; }, true); + this.log = new Logger(this.username); } static async create(username, puppeteerOptions, riotserver) { diff --git a/start.js b/start.js index 0aa2cb9364..9b0ed716e0 100644 --- a/start.js +++ b/start.js @@ -39,19 +39,19 @@ async function runTests() { const alice = await RiotSession.create("alice", options, riotserver); sessions.push(alice); - process.stdout.write(`* signing up as ${alice.username} ... `); + alice.log.step("signs up"); await signup(alice, alice.username, 'testtest'); - process.stdout.write('done\n'); - + alice.log.done(); + const noticesName = "Server Notices"; - process.stdout.write(`* accepting "${noticesName}" and accepting terms & conditions ... `); + alice.log.step(`accepts "${noticesName}" invite and accepting terms & conditions`); await acceptServerNoticesInviteAndConsent(alice, noticesName); - process.stdout.write('done\n'); + alice.log.done(); const room = 'test'; - process.stdout.write(`* creating room ${room} ... `); + alice.log.step(`creates room ${room}`); await createRoom(alice, room); - process.stdout.write('done\n'); + alice.log.done(); await alice.close(); } From 5fe386119087db97500eed5c379b80cb39b2a086 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 7 Aug 2018 17:23:01 +0200 Subject: [PATCH 0068/2372] create second user and join room first user creates --- start.js | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/start.js b/start.js index 9b0ed716e0..28eba781e5 100644 --- a/start.js +++ b/start.js @@ -27,6 +27,21 @@ const riotserver = 'http://localhost:5000'; let sessions = []; +async function createUser(username, options, riotserver) { + const session = await RiotSession.create(username, options, riotserver); + sessions.push(session); + + session.log.step("signs up"); + await signup(session, session.username, 'testtest'); + session.log.done(); + + const noticesName = "Server Notices"; + session.log.step(`accepts "${noticesName}" invite and accepting terms & conditions`); + await acceptServerNoticesInviteAndConsent(session, noticesName); + session.log.done(); + return session; +} + async function runTests() { console.log("running tests ..."); const options = {}; @@ -36,24 +51,21 @@ async function runTests() { options.executablePath = path; } - const alice = await RiotSession.create("alice", options, riotserver); - sessions.push(alice); - - alice.log.step("signs up"); - await signup(alice, alice.username, 'testtest'); - alice.log.done(); - - const noticesName = "Server Notices"; - alice.log.step(`accepts "${noticesName}" invite and accepting terms & conditions`); - await acceptServerNoticesInviteAndConsent(alice, noticesName); - alice.log.done(); + const alice = await createUser("alice", options, riotserver); + const bob = await createUser("bob", options, riotserver); const room = 'test'; alice.log.step(`creates room ${room}`); await createRoom(alice, room); alice.log.done(); + bob.log.step(`joins room ${room}`); + await createRoom(bob, room); + bob.log.done(); + + await alice.close(); + await bob.close(); } function onSuccess() { From 4e7df2126bcd42c6fd9a186d41fb68cc2ec767ff Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 7 Aug 2018 17:58:58 +0200 Subject: [PATCH 0069/2372] move step logging to tests, DRY; put test scenario in separate file, less globals --- src/scenario.js | 37 ++++++++++ src/tests/create-room.js | 2 + src/tests/join.js | 2 + src/tests/server-notices-consent.js | 8 ++- src/tests/signup.js | 2 + start.js | 102 ++++++++++++---------------- 6 files changed, 90 insertions(+), 63 deletions(-) create mode 100644 src/scenario.js diff --git a/src/scenario.js b/src/scenario.js new file mode 100644 index 0000000000..ee049a14f9 --- /dev/null +++ b/src/scenario.js @@ -0,0 +1,37 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + + +const signup = require('./tests/signup'); +const join = require('./tests/join'); +const createRoom = require('./tests/create-room'); +const acceptServerNoticesInviteAndConsent = require('./tests/server-notices-consent'); + +module.exports = async function scenario(createSession) { + async function createUser(username) { + const session = await createSession(username); + await signup(session, session.username, 'testtest'); + const noticesName = "Server Notices"; + await acceptServerNoticesInviteAndConsent(session, noticesName); + return session; + } + + const alice = await createUser("alice"); + const bob = await createUser("bob"); + const room = 'test'; + await createRoom(alice, room); + // await join(bob, room); +} diff --git a/src/tests/create-room.js b/src/tests/create-room.js index 948e0b115f..eff92baf83 100644 --- a/src/tests/create-room.js +++ b/src/tests/create-room.js @@ -17,6 +17,7 @@ limitations under the License. const assert = require('assert'); module.exports = async function createRoom(session, roomName) { + session.log.step(`creates room ${roomName}`); //TODO: brittle selector const createRoomButton = await session.waitAndQuerySelector('.mx_RoleButton[aria-label="Create new room"]'); await createRoomButton.click(); @@ -28,4 +29,5 @@ module.exports = async function createRoom(session, roomName) { await createButton.click(); await session.waitForSelector('.mx_MessageComposer'); + session.log.done(); } \ No newline at end of file diff --git a/src/tests/join.js b/src/tests/join.js index a359d6ef64..72d4fe10cf 100644 --- a/src/tests/join.js +++ b/src/tests/join.js @@ -17,6 +17,7 @@ limitations under the License. const assert = require('assert'); module.exports = async function join(session, roomName) { + session.log.step(`joins room ${roomName}`); //TODO: brittle selector const directoryButton = await session.waitAndQuerySelector('.mx_RoleButton[aria-label="Room directory"]'); await directoryButton.click(); @@ -31,4 +32,5 @@ module.exports = async function join(session, roomName) { await joinLink.click(); await session.waitForSelector('.mx_MessageComposer'); + session.log.done(); } \ No newline at end of file diff --git a/src/tests/server-notices-consent.js b/src/tests/server-notices-consent.js index 0eb4cd8722..53a318a169 100644 --- a/src/tests/server-notices-consent.js +++ b/src/tests/server-notices-consent.js @@ -16,7 +16,8 @@ limitations under the License. const assert = require('assert'); -module.exports = async function acceptServerNoticesInviteAndConsent(session, name) { +module.exports = async function acceptServerNoticesInviteAndConsent(session, noticesName) { + session.log.step(`accepts "${noticesName}" invite and accepting terms & conditions`); //TODO: brittle selector const invitesHandles = await session.waitAndQueryAll('.mx_RoomTile_name.mx_RoomTile_invite'); const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => { @@ -24,7 +25,7 @@ module.exports = async function acceptServerNoticesInviteAndConsent(session, nam return {inviteHandle, text}; })); const inviteHandle = invitesWithText.find(({inviteHandle, text}) => { - return text.trim() === name; + return text.trim() === noticesName; }).inviteHandle; await inviteHandle.click(); @@ -40,4 +41,5 @@ module.exports = async function acceptServerNoticesInviteAndConsent(session, nam const acceptButton = await termsPage.$('input[type=submit]'); await acceptButton.click(); await session.delay(500); //TODO yuck, timers -} \ No newline at end of file + session.log.done(); +} \ No newline at end of file diff --git a/src/tests/signup.js b/src/tests/signup.js index db6ad6208a..6b3f06c12c 100644 --- a/src/tests/signup.js +++ b/src/tests/signup.js @@ -18,6 +18,7 @@ const acceptTerms = require('./consent'); const assert = require('assert'); module.exports = async function signup(session, username, password, homeserver) { + session.log.step("signs up"); await session.goto(session.riotUrl('/#/register')); //click 'Custom server' radio button if (homeserver) { @@ -64,4 +65,5 @@ module.exports = async function signup(session, username, password, homeserver) const url = session.page.url(); assert.strictEqual(url, session.riotUrl('/#/home')); + session.log.done(); } diff --git a/start.js b/start.js index 28eba781e5..5e235dd1ef 100644 --- a/start.js +++ b/start.js @@ -16,33 +16,13 @@ limitations under the License. const assert = require('assert'); const RiotSession = require('./src/session'); +const scenario = require('./src/scenario'); -const signup = require('./src/tests/signup'); -const join = require('./src/tests/join'); -const createRoom = require('./src/tests/create-room'); -const acceptServerNoticesInviteAndConsent = require('./src/tests/server-notices-consent'); - -const homeserver = 'http://localhost:8008'; const riotserver = 'http://localhost:5000'; -let sessions = []; - -async function createUser(username, options, riotserver) { - const session = await RiotSession.create(username, options, riotserver); - sessions.push(session); - - session.log.step("signs up"); - await signup(session, session.username, 'testtest'); - session.log.done(); - - const noticesName = "Server Notices"; - session.log.step(`accepts "${noticesName}" invite and accepting terms & conditions`); - await acceptServerNoticesInviteAndConsent(session, noticesName); - session.log.done(); - return session; -} - async function runTests() { + let sessions = []; + console.log("running tests ..."); const options = {}; if (process.env.CHROME_PATH) { @@ -51,43 +31,45 @@ async function runTests() { options.executablePath = path; } - const alice = await createUser("alice", options, riotserver); - const bob = await createUser("bob", options, riotserver); - - const room = 'test'; - alice.log.step(`creates room ${room}`); - await createRoom(alice, room); - alice.log.done(); - - bob.log.step(`joins room ${room}`); - await createRoom(bob, room); - bob.log.done(); - - - await alice.close(); - await bob.close(); -} - -function onSuccess() { - console.log('all tests finished successfully'); -} - -async function onFailure(err) { - console.log('failure: ', err); - for(var i = 0; i < sessions.length; ++i) { - const session = sessions[i]; - documentHtml = await session.page.content(); - console.log(`---------------- START OF ${session.username} LOGS ----------------`); - console.log('---------------- console.log output:'); - console.log(session.consoleLogs()); - console.log('---------------- network requests:'); - console.log(session.networkLogs()); - console.log('---------------- document html:'); - console.log(documentHtml); - console.log(`---------------- END OF ${session.username} LOGS ----------------`); + async function createSession(username) { + const session = await RiotSession.create(username, options, riotserver); + sessions.push(session); + return session; + } + + let failure = false; + try { + await scenario(createSession); + } catch(err) { + console.log('failure: ', err); + for(let i = 0; i < sessions.length; ++i) { + const session = sessions[i]; + documentHtml = await session.page.content(); + console.log(`---------------- START OF ${session.username} LOGS ----------------`); + console.log('---------------- console.log output:'); + console.log(session.consoleLogs()); + console.log('---------------- network requests:'); + console.log(session.networkLogs()); + console.log('---------------- document html:'); + console.log(documentHtml); + console.log(`---------------- END OF ${session.username} LOGS ----------------`); + } + failure = true; + } + + for(let i = 0; i < sessions.length; ++i) { + const session = sessions[i]; + await session.close(); + } + + if (failure) { + process.exit(-1); + } else { + console.log('all tests finished successfully'); } - - process.exit(-1); } -runTests().then(onSuccess, onFailure); \ No newline at end of file +runTests().catch(function(err) { + console.log(err); + process.exit(-1); +}); \ No newline at end of file From aaa5ee1a25b7d698214a55df594c1aad50ce6294 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 7 Aug 2018 18:21:53 +0200 Subject: [PATCH 0070/2372] more consistent naming on session methods --- src/session.js | 21 ++++++++++++--------- src/tests/consent.js | 2 +- src/tests/create-room.js | 8 ++++---- src/tests/join.js | 8 ++++---- src/tests/room-settings.js | 21 +++++++++++++++++++++ src/tests/server-notices-consent.js | 4 ++-- src/tests/signup.js | 16 ++++++++-------- 7 files changed, 52 insertions(+), 28 deletions(-) create mode 100644 src/tests/room-settings.js diff --git a/src/session.js b/src/session.js index 5b0f78ccd3..d82c90d4d9 100644 --- a/src/session.js +++ b/src/session.js @@ -129,15 +129,22 @@ module.exports = class RiotSession { await input.type(text); } - // TODO: rename to waitAndQuery(Single)? - async waitAndQuerySelector(selector, timeout = 500) { + query(selector) { + return this.page.$(selector); + } + + async waitAndQuery(selector, timeout = 500) { await this.page.waitForSelector(selector, {visible: true, timeout}); - return await this.page.$(selector); + return await this.query(selector); + } + + queryAll(selector) { + return this.page.$$(selector); } async waitAndQueryAll(selector, timeout = 500) { await this.page.waitForSelector(selector, {visible: true, timeout}); - return await this.page.$$(selector); + return await this.queryAll(selector); } waitForNewPage(timeout = 500) { @@ -157,15 +164,11 @@ module.exports = class RiotSession { }); } - waitForSelector(selector) { - return this.page.waitForSelector(selector); - } - goto(url) { return this.page.goto(url); } - riotUrl(path) { + url(path) { return this.riotserver + path; } diff --git a/src/tests/consent.js b/src/tests/consent.js index 09026a3082..cd3d51c1b6 100644 --- a/src/tests/consent.js +++ b/src/tests/consent.js @@ -17,7 +17,7 @@ limitations under the License. const assert = require('assert'); module.exports = async function acceptTerms(session) { - const reviewTermsButton = await session.waitAndQuerySelector('.mx_QuestionDialog button.mx_Dialog_primary', 5000); + const reviewTermsButton = await session.waitAndQuery('.mx_QuestionDialog button.mx_Dialog_primary', 5000); const termsPagePromise = session.waitForNewPage(); await reviewTermsButton.click(); const termsPage = await termsPagePromise; diff --git a/src/tests/create-room.js b/src/tests/create-room.js index eff92baf83..8f5b5c9e85 100644 --- a/src/tests/create-room.js +++ b/src/tests/create-room.js @@ -19,15 +19,15 @@ const assert = require('assert'); module.exports = async function createRoom(session, roomName) { session.log.step(`creates room ${roomName}`); //TODO: brittle selector - const createRoomButton = await session.waitAndQuerySelector('.mx_RoleButton[aria-label="Create new room"]'); + const createRoomButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Create new room"]'); await createRoomButton.click(); - const roomNameInput = await session.waitAndQuerySelector('.mx_CreateRoomDialog_input'); + const roomNameInput = await session.waitAndQuery('.mx_CreateRoomDialog_input'); await session.replaceInputText(roomNameInput, roomName); - const createButton = await session.waitAndQuerySelector('.mx_Dialog_primary'); + const createButton = await session.waitAndQuery('.mx_Dialog_primary'); await createButton.click(); - await session.waitForSelector('.mx_MessageComposer'); + await session.waitAndQuery('.mx_MessageComposer'); session.log.done(); } \ No newline at end of file diff --git a/src/tests/join.js b/src/tests/join.js index 72d4fe10cf..0577b7a8b6 100644 --- a/src/tests/join.js +++ b/src/tests/join.js @@ -19,16 +19,16 @@ const assert = require('assert'); module.exports = async function join(session, roomName) { session.log.step(`joins room ${roomName}`); //TODO: brittle selector - const directoryButton = await session.waitAndQuerySelector('.mx_RoleButton[aria-label="Room directory"]'); + const directoryButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Room directory"]'); await directoryButton.click(); - const roomInput = await session.waitAndQuerySelector('.mx_DirectorySearchBox_input'); + const roomInput = await session.waitAndQuery('.mx_DirectorySearchBox_input'); await session.replaceInputText(roomInput, roomName); - const firstRoomLabel = await session.waitAndQuerySelector('.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child'); + const firstRoomLabel = await session.waitAndQuery('.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child'); await firstRoomLabel.click(); - const joinLink = await session.waitAndQuerySelector('.mx_RoomPreviewBar_join_text a'); + const joinLink = await session.waitAndQuery('.mx_RoomPreviewBar_join_text a'); await joinLink.click(); await session.waitForSelector('.mx_MessageComposer'); diff --git a/src/tests/room-settings.js b/src/tests/room-settings.js new file mode 100644 index 0000000000..70d84de10f --- /dev/null +++ b/src/tests/room-settings.js @@ -0,0 +1,21 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); + +module.exports = async function changeRoomSettings(session, settings) { + session.waitFor +} \ No newline at end of file diff --git a/src/tests/server-notices-consent.js b/src/tests/server-notices-consent.js index 53a318a169..d52588f962 100644 --- a/src/tests/server-notices-consent.js +++ b/src/tests/server-notices-consent.js @@ -30,10 +30,10 @@ module.exports = async function acceptServerNoticesInviteAndConsent(session, not await inviteHandle.click(); - const acceptInvitationLink = await session.waitAndQuerySelector(".mx_RoomPreviewBar_join_text a:first-child"); + const acceptInvitationLink = await session.waitAndQuery(".mx_RoomPreviewBar_join_text a:first-child"); await acceptInvitationLink.click(); - const consentLink = await session.waitAndQuerySelector(".mx_EventTile_body a", 1000); + const consentLink = await session.waitAndQuery(".mx_EventTile_body a", 1000); const termsPagePromise = session.waitForNewPage(); await consentLink.click(); diff --git a/src/tests/signup.js b/src/tests/signup.js index 6b3f06c12c..434083cbb6 100644 --- a/src/tests/signup.js +++ b/src/tests/signup.js @@ -19,16 +19,16 @@ const assert = require('assert'); module.exports = async function signup(session, username, password, homeserver) { session.log.step("signs up"); - await session.goto(session.riotUrl('/#/register')); + await session.goto(session.url('/#/register')); //click 'Custom server' radio button if (homeserver) { - const advancedRadioButton = await session.waitAndQuerySelector('#advanced'); + const advancedRadioButton = await session.waitAndQuery('#advanced'); await advancedRadioButton.click(); } // wait until register button is visible - await session.waitForSelector('.mx_Login_submit[value=Register]', {visible: true, timeout: 500}); + await session.waitAndQuery('.mx_Login_submit[value=Register]'); //fill out form - const loginFields = await session.page.$$('.mx_Login_field'); + const loginFields = await session.queryAll('.mx_Login_field'); assert.strictEqual(loginFields.length, 7); const usernameField = loginFields[2]; const passwordField = loginFields[3]; @@ -38,7 +38,7 @@ module.exports = async function signup(session, username, password, homeserver) await session.replaceInputText(passwordField, password); await session.replaceInputText(passwordRepeatField, password); if (homeserver) { - await session.waitForSelector('.mx_ServerConfig', {visible: true, timeout: 500}); + await session.waitAndQuery('.mx_ServerConfig'); await session.replaceInputText(hsurlField, homeserver); } //wait over a second because Registration/ServerConfig have a 1000ms @@ -47,7 +47,7 @@ module.exports = async function signup(session, username, password, homeserver) await session.delay(1200); /// focus on the button to make sure error validation /// has happened before checking the form is good to go - const registerButton = await session.page.$('.mx_Login_submit'); + const registerButton = await session.query('.mx_Login_submit'); await registerButton.focus(); //check no errors const error_text = await session.tryGetInnertext('.mx_Login_error'); @@ -57,13 +57,13 @@ module.exports = async function signup(session, username, password, homeserver) await registerButton.click(); //confirm dialog saying you cant log back in without e-mail - const continueButton = await session.waitAndQuerySelector('.mx_QuestionDialog button.mx_Dialog_primary'); + const continueButton = await session.waitAndQuery('.mx_QuestionDialog button.mx_Dialog_primary'); await continueButton.click(); //wait for registration to finish so the hash gets set //onhashchange better? await session.delay(2000); const url = session.page.url(); - assert.strictEqual(url, session.riotUrl('/#/home')); + assert.strictEqual(url, session.url('/#/home')); session.log.done(); } From 2a7438e9fbdb3ab4afbc2f722b1536dd5ab946c3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 7 Aug 2018 18:23:02 +0200 Subject: [PATCH 0071/2372] no need to double select here, might speed things up slightly --- src/session.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/session.js b/src/session.js index d82c90d4d9..e5e16400b8 100644 --- a/src/session.js +++ b/src/session.js @@ -133,9 +133,8 @@ module.exports = class RiotSession { return this.page.$(selector); } - async waitAndQuery(selector, timeout = 500) { - await this.page.waitForSelector(selector, {visible: true, timeout}); - return await this.query(selector); + waitAndQuery(selector, timeout = 500) { + return this.page.waitForSelector(selector, {visible: true, timeout}); } queryAll(selector) { @@ -143,7 +142,7 @@ module.exports = class RiotSession { } async waitAndQueryAll(selector, timeout = 500) { - await this.page.waitForSelector(selector, {visible: true, timeout}); + await this.waitAndQuery(selector, timeout); return await this.queryAll(selector); } From 643af2d344870e107944cb3f82a48a09d6026dd9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 7 Aug 2018 18:28:18 +0200 Subject: [PATCH 0072/2372] run synapse on custom port so it doesn't interfere with other synapses on dev machines --- riot/config-template/config.json | 6 +++--- synapse/config-templates/consent/homeserver.yaml | 6 +++--- synapse/install.sh | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/riot/config-template/config.json b/riot/config-template/config.json index 39bbd6490a..6277e567fc 100644 --- a/riot/config-template/config.json +++ b/riot/config-template/config.json @@ -1,5 +1,5 @@ { - "default_hs_url": "http://localhost:8008", + "default_hs_url": "http://localhost:5005", "default_is_url": "https://vector.im", "disable_custom_urls": false, "disable_guests": false, @@ -18,12 +18,12 @@ "default_theme": "light", "roomDirectory": { "servers": [ - "localhost:8008" + "localhost:5005" ] }, "piwik": { "url": "https://piwik.riot.im/", - "whitelistedHSUrls": ["http://localhost:8008"], + "whitelistedHSUrls": ["http://localhost:5005"], "whitelistedISUrls": ["https://vector.im", "https://matrix.org"], "siteId": 1 }, diff --git a/synapse/config-templates/consent/homeserver.yaml b/synapse/config-templates/consent/homeserver.yaml index a27fbf6f10..38aa4747b5 100644 --- a/synapse/config-templates/consent/homeserver.yaml +++ b/synapse/config-templates/consent/homeserver.yaml @@ -86,7 +86,7 @@ web_client: True # web_client_location: "/path/to/web/root" # The public-facing base URL for the client API (not including _matrix/...) -public_baseurl: http://localhost:8008/ +public_baseurl: http://localhost:{{SYNAPSE_PORT}}/ # Set the soft limit on the number of file descriptors synapse can use # Zero is used to indicate synapse should set the soft limit to the @@ -166,7 +166,7 @@ listeners: # Unsecure HTTP listener, # For when matrix traffic passes through loadbalancer that unwraps TLS. - - port: 8008 + - port: {{SYNAPSE_PORT}} tls: false bind_addresses: ['::', '0.0.0.0'] type: http @@ -693,5 +693,5 @@ user_consent: server_notices: system_mxid_localpart: notices system_mxid_display_name: "Server Notices" - system_mxid_avatar_url: "mxc://localhost:8008/oumMVlgDnLYFaPVkExemNVVZ" + system_mxid_avatar_url: "mxc://localhost:{{SYNAPSE_PORT}}/oumMVlgDnLYFaPVkExemNVVZ" room_name: "Server Notices" diff --git a/synapse/install.sh b/synapse/install.sh index dc4a08cb41..2e9b668b5e 100755 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -4,7 +4,7 @@ SYNAPSE_BRANCH=master INSTALLATION_NAME=consent SERVER_DIR=installations/$INSTALLATION_NAME CONFIG_TEMPLATE=consent -PORT=8008 +PORT=5005 # set current directory to script directory BASE_DIR=$(readlink -f $(dirname $0)) @@ -33,7 +33,7 @@ python -m synapse.app.homeserver \ # apply configuration cp -r $BASE_DIR/config-templates/$CONFIG_TEMPLATE/. ./ sed -i "s#{{SYNAPSE_ROOT}}#$(pwd)/#g" homeserver.yaml -sed -i "s#{{SYNAPSE_PORT}}#${PORT}/#g" homeserver.yaml +sed -i "s#{{SYNAPSE_PORT}}#${PORT}#g" homeserver.yaml sed -i "s#{{FORM_SECRET}}#$(uuidgen)#g" homeserver.yaml sed -i "s#{{REGISTRATION_SHARED_SECRET}}#$(uuidgen)#g" homeserver.yaml sed -i "s#{{MACAROON_SECRET_KEY}}#$(uuidgen)#g" homeserver.yaml From a78c095cf657978e442113cc6e56cf80e60bbfb7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 8 Aug 2018 11:39:17 +0200 Subject: [PATCH 0073/2372] add support for changing the room settings --- src/scenario.js | 4 ++- src/session.js | 39 +++++++++++++++-------- src/tests/join.js | 4 +-- src/tests/room-settings.js | 48 ++++++++++++++++++++++++++++- src/tests/server-notices-consent.js | 1 + start.js | 8 ++--- 6 files changed, 83 insertions(+), 21 deletions(-) diff --git a/src/scenario.js b/src/scenario.js index ee049a14f9..9aa0ed2ec0 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -18,6 +18,7 @@ limitations under the License. const signup = require('./tests/signup'); const join = require('./tests/join'); const createRoom = require('./tests/create-room'); +const changeRoomSettings = require('./tests/room-settings'); const acceptServerNoticesInviteAndConsent = require('./tests/server-notices-consent'); module.exports = async function scenario(createSession) { @@ -33,5 +34,6 @@ module.exports = async function scenario(createSession) { const bob = await createUser("bob"); const room = 'test'; await createRoom(alice, room); - // await join(bob, room); + await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests"}); + await join(bob, room); } diff --git a/src/session.js b/src/session.js index e5e16400b8..4f6e04584f 100644 --- a/src/session.js +++ b/src/session.js @@ -33,15 +33,27 @@ class LogBuffer { class Logger { constructor(username) { + this.indent = 0; this.username = username; } - step(description) { - process.stdout.write(` * ${this.username} ${description} ... `); + startGroup(description) { + const indent = " ".repeat(this.indent * 2); + console.log(`${indent} * ${this.username} ${description}:`); + this.indent += 1; } - done() { - process.stdout.write("done\n"); + endGroup() { + this.indent -= 1; + } + + step(description) { + const indent = " ".repeat(this.indent * 2); + process.stdout.write(`${indent} * ${this.username} ${description} ... `); + } + + done(status = "done") { + process.stdout.write(status + "\n"); } } @@ -79,9 +91,17 @@ module.exports = class RiotSession { return null; } - async innerText(field) { - const text_handle = await field.getProperty('innerText'); - return await text_handle.jsonValue(); + async getElementProperty(handle, property) { + const propHandle = await handle.getProperty(property); + return await propHandle.jsonValue(); + } + + innerText(field) { + return this.getElementProperty(field, 'innerText'); + } + + getOuterHTML(element_handle) { + return this.getElementProperty(field, 'outerHTML'); } consoleLogs() { @@ -111,11 +131,6 @@ module.exports = class RiotSession { } } - async getOuterHTML(element_handle) { - const html_handle = await element_handle.getProperty('outerHTML'); - return await html_handle.jsonValue(); - } - async printElements(label, elements) { console.log(label, await Promise.all(elements.map(getOuterHTML))); } diff --git a/src/tests/join.js b/src/tests/join.js index 0577b7a8b6..3c76ad2c67 100644 --- a/src/tests/join.js +++ b/src/tests/join.js @@ -25,12 +25,12 @@ module.exports = async function join(session, roomName) { const roomInput = await session.waitAndQuery('.mx_DirectorySearchBox_input'); await session.replaceInputText(roomInput, roomName); - const firstRoomLabel = await session.waitAndQuery('.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child'); + const firstRoomLabel = await session.waitAndQuery('.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child', 1000); await firstRoomLabel.click(); const joinLink = await session.waitAndQuery('.mx_RoomPreviewBar_join_text a'); await joinLink.click(); - await session.waitForSelector('.mx_MessageComposer'); + await session.waitAndQuery('.mx_MessageComposer'); session.log.done(); } \ No newline at end of file diff --git a/src/tests/room-settings.js b/src/tests/room-settings.js index 70d84de10f..d7bbad3451 100644 --- a/src/tests/room-settings.js +++ b/src/tests/room-settings.js @@ -17,5 +17,51 @@ limitations under the License. const assert = require('assert'); module.exports = async function changeRoomSettings(session, settings) { - session.waitFor + session.log.startGroup(`changes the room settings`); + /// XXX delay is needed here, possible because the header is being rerendered + /// click doesn't do anything otherwise + await session.delay(500); + const settingsButton = await session.query(".mx_RoomHeader .mx_AccessibleButton[title=Settings]"); + await settingsButton.click(); + const checks = await session.waitAndQueryAll(".mx_RoomSettings_settings input[type=checkbox]"); + assert.equal(checks.length, 3); + const e2eEncryptionCheck = checks[0]; + const sendToUnverifiedDevices = checks[1]; + const isDirectory = checks[2]; + + if (typeof settings.directory === "boolean") { + session.log.step(`sets directory listing to ${settings.directory}`); + const checked = await session.getElementProperty(isDirectory, "checked"); + assert(typeof checked, "boolean"); + if (checked !== settings.directory) { + await isDirectory.click(); + session.log.done(); + } else { + session.log.done("already set"); + } + } + + if (settings.visibility) { + session.log.step(`sets visibility to ${settings.visibility}`); + const radios = await session.waitAndQueryAll(".mx_RoomSettings_settings input[type=radio]"); + assert.equal(radios.length, 7); + const inviteOnly = radios[0]; + const publicNoGuests = radios[1]; + const publicWithGuests = radios[2]; + + if (settings.visibility === "invite_only") { + await inviteOnly.click(); + } else if (settings.visibility === "public_no_guests") { + await publicNoGuests.click(); + } else if (settings.visibility === "public_with_guests") { + await publicWithGuests.click(); + } else { + throw new Error(`unrecognized room visibility setting: ${settings.visibility}`); + } + session.log.done(); + } + + const saveButton = await session.query(".mx_RoomHeader_wrapper .mx_RoomHeader_textButton"); + await saveButton.click(); + session.log.endGroup(); } \ No newline at end of file diff --git a/src/tests/server-notices-consent.js b/src/tests/server-notices-consent.js index d52588f962..def21d04c3 100644 --- a/src/tests/server-notices-consent.js +++ b/src/tests/server-notices-consent.js @@ -41,5 +41,6 @@ module.exports = async function acceptServerNoticesInviteAndConsent(session, not const acceptButton = await termsPage.$('input[type=submit]'); await acceptButton.click(); await session.delay(500); //TODO yuck, timers + await termsPage.close(); session.log.done(); } \ No newline at end of file diff --git a/start.js b/start.js index 5e235dd1ef..11dbe8d2fa 100644 --- a/start.js +++ b/start.js @@ -25,6 +25,7 @@ async function runTests() { console.log("running tests ..."); const options = {}; + // options.headless = false; if (process.env.CHROME_PATH) { const path = process.env.CHROME_PATH; console.log(`(using external chrome/chromium at ${path}, make sure it's compatible with puppeteer)`); @@ -41,6 +42,7 @@ async function runTests() { try { await scenario(createSession); } catch(err) { + failure = true; console.log('failure: ', err); for(let i = 0; i < sessions.length; ++i) { const session = sessions[i]; @@ -54,13 +56,9 @@ async function runTests() { console.log(documentHtml); console.log(`---------------- END OF ${session.username} LOGS ----------------`); } - failure = true; } - for(let i = 0; i < sessions.length; ++i) { - const session = sessions[i]; - await session.close(); - } + await Promise.all(sessions.map((session) => session.close())); if (failure) { process.exit(-1); From 1fd379b3d25208c8d53a5b6722129401090dc0a0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 8 Aug 2018 12:17:36 +0200 Subject: [PATCH 0074/2372] wait to receive message from other user --- src/scenario.js | 4 ++++ src/tests/receive-message.js | 42 ++++++++++++++++++++++++++++++++++++ src/tests/room-settings.js | 2 +- src/tests/send-message.js | 25 +++++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 src/tests/receive-message.js create mode 100644 src/tests/send-message.js diff --git a/src/scenario.js b/src/scenario.js index 9aa0ed2ec0..14c901ba99 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -17,6 +17,8 @@ limitations under the License. const signup = require('./tests/signup'); const join = require('./tests/join'); +const sendMessage = require('./tests/send-message'); +const receiveMessage = require('./tests/receive-message'); const createRoom = require('./tests/create-room'); const changeRoomSettings = require('./tests/room-settings'); const acceptServerNoticesInviteAndConsent = require('./tests/server-notices-consent'); @@ -36,4 +38,6 @@ module.exports = async function scenario(createSession) { await createRoom(alice, room); await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests"}); await join(bob, room); + await sendMessage(bob, "hi Alice!"); + await receiveMessage(alice, {sender: "bob", body: "hi Alice!"}); } diff --git a/src/tests/receive-message.js b/src/tests/receive-message.js new file mode 100644 index 0000000000..607bc1625e --- /dev/null +++ b/src/tests/receive-message.js @@ -0,0 +1,42 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); + + +async function getMessageFromTile(eventTile) { + const senderElement = await eventTile.$(".mx_SenderProfile_name"); + const bodyElement = await eventTile.$(".mx_EventTile_body"); + const sender = await(await senderElement.getProperty("innerText")).jsonValue(); + const body = await(await bodyElement.getProperty("innerText")).jsonValue(); + return {sender, body}; +} + +module.exports = async function receiveMessage(session, message) { + session.log.step(`waits to receive message from ${message.sender} in room`); + // wait for a response to come in that contains the message + // crude, but effective + await session.page.waitForResponse(async (response) => { + const body = await response.text(); + return body.indexOf(message.body) !== -1; + }); + + let lastTile = await session.waitAndQuery(".mx_EventTile_last"); + let lastMessage = await getMessageFromTile(lastTile); + assert.equal(lastMessage.body, message.body); + assert.equal(lastMessage.sender, message.sender); + session.log.done(); +} \ No newline at end of file diff --git a/src/tests/room-settings.js b/src/tests/room-settings.js index d7bbad3451..6001d14d34 100644 --- a/src/tests/room-settings.js +++ b/src/tests/room-settings.js @@ -32,7 +32,7 @@ module.exports = async function changeRoomSettings(session, settings) { if (typeof settings.directory === "boolean") { session.log.step(`sets directory listing to ${settings.directory}`); const checked = await session.getElementProperty(isDirectory, "checked"); - assert(typeof checked, "boolean"); + assert.equal(typeof checked, "boolean"); if (checked !== settings.directory) { await isDirectory.click(); session.log.done(); diff --git a/src/tests/send-message.js b/src/tests/send-message.js new file mode 100644 index 0000000000..8a61a15e94 --- /dev/null +++ b/src/tests/send-message.js @@ -0,0 +1,25 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); + +module.exports = async function sendMessage(session, message) { + session.log.step(`writes "${message}" in room`); + const composer = await session.waitAndQuery('.mx_MessageComposer'); + await composer.type(message); + await composer.press("Enter"); + session.log.done(); +} \ No newline at end of file From c5f064e389e970eaa7bbfe04acbf2a313f00655b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 8 Aug 2018 12:35:36 +0200 Subject: [PATCH 0075/2372] make receiving a bit more robust --- src/tests/receive-message.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/tests/receive-message.js b/src/tests/receive-message.js index 607bc1625e..c84aefcbfd 100644 --- a/src/tests/receive-message.js +++ b/src/tests/receive-message.js @@ -18,25 +18,25 @@ const assert = require('assert'); async function getMessageFromTile(eventTile) { - const senderElement = await eventTile.$(".mx_SenderProfile_name"); - const bodyElement = await eventTile.$(".mx_EventTile_body"); - const sender = await(await senderElement.getProperty("innerText")).jsonValue(); - const body = await(await bodyElement.getProperty("innerText")).jsonValue(); - return {sender, body}; } module.exports = async function receiveMessage(session, message) { - session.log.step(`waits to receive message from ${message.sender} in room`); + session.log.step(`receives message "${message.body}" from ${message.sender} in room`); // wait for a response to come in that contains the message // crude, but effective await session.page.waitForResponse(async (response) => { const body = await response.text(); return body.indexOf(message.body) !== -1; }); - - let lastTile = await session.waitAndQuery(".mx_EventTile_last"); - let lastMessage = await getMessageFromTile(lastTile); - assert.equal(lastMessage.body, message.body); - assert.equal(lastMessage.sender, message.sender); + // wait a bit for the incoming event to be rendered + await session.delay(100); + let lastTile = await session.query(".mx_EventTile_last"); + const senderElement = await lastTile.$(".mx_SenderProfile_name"); + const bodyElement = await lastTile.$(".mx_EventTile_body"); + const sender = await(await senderElement.getProperty("innerText")).jsonValue(); + const body = await(await bodyElement.getProperty("innerText")).jsonValue(); + + assert.equal(body, message.body); + assert.equal(sender, message.sender); session.log.done(); } \ No newline at end of file From 73c88fe603219c0c6a3f76d3eaef267e849f9c53 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 8 Aug 2018 12:35:50 +0200 Subject: [PATCH 0076/2372] prepare for more tests --- src/scenario.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/scenario.js b/src/scenario.js index 14c901ba99..07f9518029 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -34,10 +34,17 @@ module.exports = async function scenario(createSession) { const alice = await createUser("alice"); const bob = await createUser("bob"); + + await createDirectoryRoomAndTalk(alice, bob); +} + +async function createDirectoryRoomAndTalk(alice, bob) { + console.log(" creating a public room and join through directory:"); const room = 'test'; await createRoom(alice, room); await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests"}); await join(bob, room); await sendMessage(bob, "hi Alice!"); await receiveMessage(alice, {sender: "bob", body: "hi Alice!"}); -} +} + From dc87e2bfe0a0268871f04c1b1946f01b851d4199 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 8 Aug 2018 12:42:34 +0200 Subject: [PATCH 0077/2372] avoid typos --- src/scenario.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/scenario.js b/src/scenario.js index 07f9518029..f035e94c35 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -41,10 +41,17 @@ module.exports = async function scenario(createSession) { async function createDirectoryRoomAndTalk(alice, bob) { console.log(" creating a public room and join through directory:"); const room = 'test'; + const message = "hi Alice!"; await createRoom(alice, room); await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests"}); await join(bob, room); - await sendMessage(bob, "hi Alice!"); - await receiveMessage(alice, {sender: "bob", body: "hi Alice!"}); + await sendMessage(bob, message); + await receiveMessage(alice, {sender: "bob", body: message}); } +async function createE2ERoomAndTalk(alice, bob) { + await createRoom(bob, "secrets"); + await changeRoomSettings(bob, {encryption: true}); + await invite(bob, "@alice:localhost"); + await acceptInvite(alice, "secrets"); +} \ No newline at end of file From af0c0c0afe95eef2072ee936f7b06090ffa37be6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 8 Aug 2018 15:22:54 +0200 Subject: [PATCH 0078/2372] add test scenario for e2e encryption --- src/scenario.js | 22 +++++++++++--- src/tests/accept-invite.js | 43 ++++++++++++++++++++++++++ src/tests/create-room.js | 2 +- src/tests/dialog.js | 47 +++++++++++++++++++++++++++++ src/tests/e2e-device.js | 31 +++++++++++++++++++ src/tests/invite.js | 30 ++++++++++++++++++ src/tests/join.js | 2 +- src/tests/room-settings.js | 30 +++++++++++++----- src/tests/server-notices-consent.js | 21 ++----------- src/tests/verify-device.js | 44 +++++++++++++++++++++++++++ start.js | 34 +++++++++++++-------- 11 files changed, 263 insertions(+), 43 deletions(-) create mode 100644 src/tests/accept-invite.js create mode 100644 src/tests/dialog.js create mode 100644 src/tests/e2e-device.js create mode 100644 src/tests/invite.js create mode 100644 src/tests/verify-device.js diff --git a/src/scenario.js b/src/scenario.js index f035e94c35..d7dd4f79e0 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -18,10 +18,14 @@ limitations under the License. const signup = require('./tests/signup'); const join = require('./tests/join'); const sendMessage = require('./tests/send-message'); +const acceptInvite = require('./tests/accept-invite'); +const invite = require('./tests/invite'); const receiveMessage = require('./tests/receive-message'); const createRoom = require('./tests/create-room'); const changeRoomSettings = require('./tests/room-settings'); const acceptServerNoticesInviteAndConsent = require('./tests/server-notices-consent'); +const getE2EDeviceFromSettings = require('./tests/e2e-device'); +const verifyDeviceForUser = require("./tests/verify-device"); module.exports = async function scenario(createSession) { async function createUser(username) { @@ -36,6 +40,7 @@ module.exports = async function scenario(createSession) { const bob = await createUser("bob"); await createDirectoryRoomAndTalk(alice, bob); + await createE2ERoomAndTalk(alice, bob); } async function createDirectoryRoomAndTalk(alice, bob) { @@ -47,11 +52,20 @@ async function createDirectoryRoomAndTalk(alice, bob) { await join(bob, room); await sendMessage(bob, message); await receiveMessage(alice, {sender: "bob", body: message}); -} +} async function createE2ERoomAndTalk(alice, bob) { - await createRoom(bob, "secrets"); + console.log(" creating an e2e encrypted room and join through invite:"); + const room = "secrets"; + const message = "Guess what I just heard?!" + await createRoom(bob, room); await changeRoomSettings(bob, {encryption: true}); await invite(bob, "@alice:localhost"); - await acceptInvite(alice, "secrets"); -} \ No newline at end of file + await acceptInvite(alice, room); + const bobDevice = await getE2EDeviceFromSettings(bob); + const aliceDevice = await getE2EDeviceFromSettings(alice); + await verifyDeviceForUser(bob, "alice", aliceDevice); + await verifyDeviceForUser(alice, "bob", bobDevice); + await sendMessage(alice, message); + await receiveMessage(bob, {sender: "alice", body: message, encrypted: true}); +} diff --git a/src/tests/accept-invite.js b/src/tests/accept-invite.js new file mode 100644 index 0000000000..25ce1bdf49 --- /dev/null +++ b/src/tests/accept-invite.js @@ -0,0 +1,43 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); +const {acceptDialogMaybe} = require('./dialog'); + +module.exports = async function acceptInvite(session, name) { + session.log.step(`accepts "${name}" invite`); + //TODO: brittle selector + const invitesHandles = await session.waitAndQueryAll('.mx_RoomTile_name.mx_RoomTile_invite', 1000); + const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => { + const text = await session.innerText(inviteHandle); + return {inviteHandle, text}; + })); + const inviteHandle = invitesWithText.find(({inviteHandle, text}) => { + return text.trim() === name; + }).inviteHandle; + + await inviteHandle.click(); + + const acceptInvitationLink = await session.waitAndQuery(".mx_RoomPreviewBar_join_text a:first-child"); + await acceptInvitationLink.click(); + + // accept e2e warning dialog + try { + acceptDialogMaybe(session, "encryption"); + } catch(err) {} + + session.log.done(); +} \ No newline at end of file diff --git a/src/tests/create-room.js b/src/tests/create-room.js index 8f5b5c9e85..0f7f33ddff 100644 --- a/src/tests/create-room.js +++ b/src/tests/create-room.js @@ -17,7 +17,7 @@ limitations under the License. const assert = require('assert'); module.exports = async function createRoom(session, roomName) { - session.log.step(`creates room ${roomName}`); + session.log.step(`creates room "${roomName}"`); //TODO: brittle selector const createRoomButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Create new room"]'); await createRoomButton.click(); diff --git a/src/tests/dialog.js b/src/tests/dialog.js new file mode 100644 index 0000000000..420438b3f9 --- /dev/null +++ b/src/tests/dialog.js @@ -0,0 +1,47 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); + + +async function acceptDialog(session, expectedContent) { + const foundDialog = await acceptDialogMaybe(session, expectedContent); + if (!foundDialog) { + throw new Error("could not find a dialog"); + } +} + +async function acceptDialogMaybe(session, expectedContent) { + let dialog = null; + try { + dialog = await session.waitAndQuery(".mx_QuestionDialog", 100); + } catch(err) { + return false; + } + if (expectedContent) { + const contentElement = await dialog.$(".mx_Dialog_content"); + const content = await (await contentElement.getProperty("innerText")).jsonValue(); + assert.ok(content.indexOf(expectedContent) !== -1); + } + const primaryButton = await dialog.$(".mx_Dialog_primary"); + await primaryButton.click(); + return true; +} + +module.exports = { + acceptDialog, + acceptDialogMaybe, +}; \ No newline at end of file diff --git a/src/tests/e2e-device.js b/src/tests/e2e-device.js new file mode 100644 index 0000000000..fd81ac43eb --- /dev/null +++ b/src/tests/e2e-device.js @@ -0,0 +1,31 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); + +module.exports = async function getE2EDeviceFromSettings(session) { + session.log.step(`gets e2e device/key from settings`); + const settingsButton = await session.query('.mx_BottomLeftMenu_settings'); + await settingsButton.click(); + const deviceAndKey = await session.waitAndQueryAll(".mx_UserSettings_section.mx_UserSettings_cryptoSection code"); + assert.equal(deviceAndKey.length, 2); + const id = await (await deviceAndKey[0].getProperty("innerText")).jsonValue(); + const key = await (await deviceAndKey[1].getProperty("innerText")).jsonValue(); + const closeButton = await session.query(".mx_RoomHeader_cancelButton"); + await closeButton.click(); + session.log.done(); + return {id, key}; +} \ No newline at end of file diff --git a/src/tests/invite.js b/src/tests/invite.js new file mode 100644 index 0000000000..37549aa2ca --- /dev/null +++ b/src/tests/invite.js @@ -0,0 +1,30 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); + +module.exports = async function invite(session, userId) { + session.log.step(`invites "${userId}" to room`); + await session.delay(200); + const inviteButton = await session.waitAndQuery(".mx_RightPanel_invite"); + await inviteButton.click(); + const inviteTextArea = await session.waitAndQuery(".mx_ChatInviteDialog textarea"); + await inviteTextArea.type(userId); + await inviteTextArea.press("Enter"); + const confirmButton = await session.query(".mx_Dialog_primary"); + await confirmButton.click(); + session.log.done(); +} \ No newline at end of file diff --git a/src/tests/join.js b/src/tests/join.js index 3c76ad2c67..8ab5e80f2d 100644 --- a/src/tests/join.js +++ b/src/tests/join.js @@ -17,7 +17,7 @@ limitations under the License. const assert = require('assert'); module.exports = async function join(session, roomName) { - session.log.step(`joins room ${roomName}`); + session.log.step(`joins room "${roomName}"`); //TODO: brittle selector const directoryButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Room directory"]'); await directoryButton.click(); diff --git a/src/tests/room-settings.js b/src/tests/room-settings.js index 6001d14d34..127afa26dd 100644 --- a/src/tests/room-settings.js +++ b/src/tests/room-settings.js @@ -15,6 +15,19 @@ limitations under the License. */ const assert = require('assert'); +const {acceptDialog} = require('./dialog'); + +async function setCheckboxSetting(session, checkbox, enabled) { + const checked = await session.getElementProperty(checkbox, "checked"); + assert.equal(typeof checked, "boolean"); + if (checked !== enabled) { + await checkbox.click(); + session.log.done(); + return true; + } else { + session.log.done("already set"); + } +} module.exports = async function changeRoomSettings(session, settings) { session.log.startGroup(`changes the room settings`); @@ -31,13 +44,15 @@ module.exports = async function changeRoomSettings(session, settings) { if (typeof settings.directory === "boolean") { session.log.step(`sets directory listing to ${settings.directory}`); - const checked = await session.getElementProperty(isDirectory, "checked"); - assert.equal(typeof checked, "boolean"); - if (checked !== settings.directory) { - await isDirectory.click(); - session.log.done(); - } else { - session.log.done("already set"); + await setCheckboxSetting(session, isDirectory, settings.directory); + } + + if (typeof settings.encryption === "boolean") { + session.log.step(`sets room e2e encryption to ${settings.encryption}`); + const clicked = await setCheckboxSetting(session, e2eEncryptionCheck, settings.encryption); + // if enabling, accept beta warning dialog + if (clicked && settings.encryption) { + await acceptDialog(session, "encryption"); } } @@ -63,5 +78,6 @@ module.exports = async function changeRoomSettings(session, settings) { const saveButton = await session.query(".mx_RoomHeader_wrapper .mx_RoomHeader_textButton"); await saveButton.click(); + session.log.endGroup(); } \ No newline at end of file diff --git a/src/tests/server-notices-consent.js b/src/tests/server-notices-consent.js index def21d04c3..a217daa43b 100644 --- a/src/tests/server-notices-consent.js +++ b/src/tests/server-notices-consent.js @@ -15,26 +15,11 @@ limitations under the License. */ const assert = require('assert'); - +const acceptInvite = require("./accept-invite") module.exports = async function acceptServerNoticesInviteAndConsent(session, noticesName) { - session.log.step(`accepts "${noticesName}" invite and accepting terms & conditions`); - //TODO: brittle selector - const invitesHandles = await session.waitAndQueryAll('.mx_RoomTile_name.mx_RoomTile_invite'); - const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => { - const text = await session.innerText(inviteHandle); - return {inviteHandle, text}; - })); - const inviteHandle = invitesWithText.find(({inviteHandle, text}) => { - return text.trim() === noticesName; - }).inviteHandle; - - await inviteHandle.click(); - - const acceptInvitationLink = await session.waitAndQuery(".mx_RoomPreviewBar_join_text a:first-child"); - await acceptInvitationLink.click(); - + await acceptInvite(session, noticesName); + session.log.step(`accepts terms & conditions`); const consentLink = await session.waitAndQuery(".mx_EventTile_body a", 1000); - const termsPagePromise = session.waitForNewPage(); await consentLink.click(); const termsPage = await termsPagePromise; diff --git a/src/tests/verify-device.js b/src/tests/verify-device.js new file mode 100644 index 0000000000..507de1b91e --- /dev/null +++ b/src/tests/verify-device.js @@ -0,0 +1,44 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); + +module.exports = async function verifyDeviceForUser(session, name, expectedDevice) { + session.log.step(`verifies e2e device for ${name}`); + const memberNameElements = await session.queryAll(".mx_MemberList .mx_EntityTile_name"); + const membersAndNames = await Promise.all(memberNameElements.map(async (el) => { + const innerTextHandle = await memberNameElements.getProperty("innerText"); + const innerText = await innerTextHandle.jsonValue(); + return [el, innerText]; + })); + const matchingMember = membersAndNames.filter(([el, text]) => { + return text === name; + }).map(([el, name]) => el); + await matchingMember.click(); + const firstVerifyButton = await session.waitAndQuery(".mx_MemberDeviceInfo_verify"); + await firstVerifyButton.click(); + const dialogCodeFields = await session.waitAndQueryAll(".mx_QuestionDialog code"); + assert.equal(dialogCodeFields.length, 2); + const deviceId = dialogCodeFields[0]; + const deviceKey = dialogCodeFields[1]; + assert.equal(expectedDevice.id, deviceId); + assert.equal(expectedDevice.key, deviceKey); + const confirmButton = await session.query(".mx_Dialog_primary"); + await confirmButton.click(); + const closeMemberInfo = await session.query(".mx_MemberInfo_cancel"); + await closeMemberInfo.click(); + session.log.done(); +} \ No newline at end of file diff --git a/start.js b/start.js index 11dbe8d2fa..f4005cbe85 100644 --- a/start.js +++ b/start.js @@ -20,12 +20,14 @@ const scenario = require('./src/scenario'); const riotserver = 'http://localhost:5000'; +const noLogs = process.argv.indexOf("--no-logs") !== -1; + async function runTests() { let sessions = []; console.log("running tests ..."); const options = {}; - // options.headless = false; + options.headless = false; if (process.env.CHROME_PATH) { const path = process.env.CHROME_PATH; console.log(`(using external chrome/chromium at ${path}, make sure it's compatible with puppeteer)`); @@ -44,20 +46,28 @@ async function runTests() { } catch(err) { failure = true; console.log('failure: ', err); - for(let i = 0; i < sessions.length; ++i) { - const session = sessions[i]; - documentHtml = await session.page.content(); - console.log(`---------------- START OF ${session.username} LOGS ----------------`); - console.log('---------------- console.log output:'); - console.log(session.consoleLogs()); - console.log('---------------- network requests:'); - console.log(session.networkLogs()); - console.log('---------------- document html:'); - console.log(documentHtml); - console.log(`---------------- END OF ${session.username} LOGS ----------------`); + if (!noLogs) { + for(let i = 0; i < sessions.length; ++i) { + const session = sessions[i]; + documentHtml = await session.page.content(); + console.log(`---------------- START OF ${session.username} LOGS ----------------`); + console.log('---------------- console.log output:'); + console.log(session.consoleLogs()); + console.log('---------------- network requests:'); + console.log(session.networkLogs()); + console.log('---------------- document html:'); + console.log(documentHtml); + console.log(`---------------- END OF ${session.username} LOGS ----------------`); + } } } + // wait 5 minutes on failure if not running headless + // to inspect what went wrong + if (failure && options.headless === false) { + await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000)); + } + await Promise.all(sessions.map((session) => session.close())); if (failure) { From 2c983f8cee9a2b8a6989d032f04a20e723543e07 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 9 Aug 2018 14:23:09 +0200 Subject: [PATCH 0079/2372] fix composer issue and more --- src/scenario.js | 30 +++++++++++++++++++++-------- src/tests/accept-invite.js | 4 +--- src/tests/receive-message.js | 23 ++++++++++++++-------- src/tests/send-message.js | 6 +++++- src/tests/server-notices-consent.js | 4 ++-- src/tests/verify-device.js | 10 ++++------ start.js | 6 +++++- 7 files changed, 54 insertions(+), 29 deletions(-) diff --git a/src/scenario.js b/src/scenario.js index d7dd4f79e0..330db0fef2 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -15,6 +15,7 @@ limitations under the License. */ +const {acceptDialog} = require('./tests/dialog'); const signup = require('./tests/signup'); const join = require('./tests/join'); const sendMessage = require('./tests/send-message'); @@ -31,8 +32,7 @@ module.exports = async function scenario(createSession) { async function createUser(username) { const session = await createSession(username); await signup(session, session.username, 'testtest'); - const noticesName = "Server Notices"; - await acceptServerNoticesInviteAndConsent(session, noticesName); + await acceptServerNoticesInviteAndConsent(session); return session; } @@ -46,26 +46,40 @@ module.exports = async function scenario(createSession) { async function createDirectoryRoomAndTalk(alice, bob) { console.log(" creating a public room and join through directory:"); const room = 'test'; - const message = "hi Alice!"; await createRoom(alice, room); await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests"}); await join(bob, room); - await sendMessage(bob, message); - await receiveMessage(alice, {sender: "bob", body: message}); + const bobMessage = "hi Alice!"; + await sendMessage(bob, bobMessage); + await receiveMessage(alice, {sender: "bob", body: bobMessage}); + const aliceMessage = "hi Bob, welcome!" + await sendMessage(alice, aliceMessage); + await receiveMessage(bob, {sender: "alice", body: aliceMessage}); } async function createE2ERoomAndTalk(alice, bob) { console.log(" creating an e2e encrypted room and join through invite:"); const room = "secrets"; - const message = "Guess what I just heard?!" await createRoom(bob, room); await changeRoomSettings(bob, {encryption: true}); await invite(bob, "@alice:localhost"); await acceptInvite(alice, room); const bobDevice = await getE2EDeviceFromSettings(bob); + // wait some time for the encryption warning dialog + // to appear after closing the settings + await bob.delay(500); + await acceptDialog(bob, "encryption"); const aliceDevice = await getE2EDeviceFromSettings(alice); + // wait some time for the encryption warning dialog + // to appear after closing the settings + await alice.delay(500); + await acceptDialog(alice, "encryption"); await verifyDeviceForUser(bob, "alice", aliceDevice); await verifyDeviceForUser(alice, "bob", bobDevice); - await sendMessage(alice, message); - await receiveMessage(bob, {sender: "alice", body: message, encrypted: true}); + const aliceMessage = "Guess what I just heard?!" + await sendMessage(alice, aliceMessage); + await receiveMessage(bob, {sender: "alice", body: aliceMessage, encrypted: true}); + const bobMessage = "You've got to tell me!"; + await sendMessage(bob, bobMessage); + await receiveMessage(alice, {sender: "bob", body: bobMessage, encrypted: true}); } diff --git a/src/tests/accept-invite.js b/src/tests/accept-invite.js index 25ce1bdf49..5cdeeb3d84 100644 --- a/src/tests/accept-invite.js +++ b/src/tests/accept-invite.js @@ -35,9 +35,7 @@ module.exports = async function acceptInvite(session, name) { await acceptInvitationLink.click(); // accept e2e warning dialog - try { - acceptDialogMaybe(session, "encryption"); - } catch(err) {} + acceptDialogMaybe(session, "encryption"); session.log.done(); } \ No newline at end of file diff --git a/src/tests/receive-message.js b/src/tests/receive-message.js index c84aefcbfd..9c963c45f4 100644 --- a/src/tests/receive-message.js +++ b/src/tests/receive-message.js @@ -16,26 +16,33 @@ limitations under the License. const assert = require('assert'); - -async function getMessageFromTile(eventTile) { -} - module.exports = async function receiveMessage(session, message) { - session.log.step(`receives message "${message.body}" from ${message.sender} in room`); + session.log.step(`receives message "${message.body}" from ${message.sender}`); // wait for a response to come in that contains the message // crude, but effective await session.page.waitForResponse(async (response) => { + if (response.request().url().indexOf("/sync") === -1) { + return false; + } const body = await response.text(); - return body.indexOf(message.body) !== -1; + if (message.encrypted) { + return body.indexOf(message.sender) !== -1 && + body.indexOf("m.room.encrypted") !== -1; + } else { + return body.indexOf(message.body) !== -1; + } }); // wait a bit for the incoming event to be rendered - await session.delay(100); + await session.delay(300); let lastTile = await session.query(".mx_EventTile_last"); const senderElement = await lastTile.$(".mx_SenderProfile_name"); const bodyElement = await lastTile.$(".mx_EventTile_body"); const sender = await(await senderElement.getProperty("innerText")).jsonValue(); const body = await(await bodyElement.getProperty("innerText")).jsonValue(); - + if (message.encrypted) { + const e2eIcon = await lastTile.$(".mx_EventTile_e2eIcon"); + assert.ok(e2eIcon); + } assert.equal(body, message.body); assert.equal(sender, message.sender); session.log.done(); diff --git a/src/tests/send-message.js b/src/tests/send-message.js index 8a61a15e94..e98d0dad72 100644 --- a/src/tests/send-message.js +++ b/src/tests/send-message.js @@ -18,8 +18,12 @@ const assert = require('assert'); module.exports = async function sendMessage(session, message) { session.log.step(`writes "${message}" in room`); - const composer = await session.waitAndQuery('.mx_MessageComposer'); + // this selector needs to be the element that has contenteditable=true, + // not any if its parents, otherwise it behaves flaky at best. + const composer = await session.waitAndQuery('.mx_MessageComposer_editor'); await composer.type(message); + const text = await session.innerText(composer); + assert.equal(text.trim(), message.trim()); await composer.press("Enter"); session.log.done(); } \ No newline at end of file diff --git a/src/tests/server-notices-consent.js b/src/tests/server-notices-consent.js index a217daa43b..3e07248daa 100644 --- a/src/tests/server-notices-consent.js +++ b/src/tests/server-notices-consent.js @@ -16,8 +16,8 @@ limitations under the License. const assert = require('assert'); const acceptInvite = require("./accept-invite") -module.exports = async function acceptServerNoticesInviteAndConsent(session, noticesName) { - await acceptInvite(session, noticesName); +module.exports = async function acceptServerNoticesInviteAndConsent(session) { + await acceptInvite(session, "Server Notices"); session.log.step(`accepts terms & conditions`); const consentLink = await session.waitAndQuery(".mx_EventTile_body a", 1000); const termsPagePromise = session.waitForNewPage(); diff --git a/src/tests/verify-device.js b/src/tests/verify-device.js index 507de1b91e..0622654876 100644 --- a/src/tests/verify-device.js +++ b/src/tests/verify-device.js @@ -20,20 +20,18 @@ module.exports = async function verifyDeviceForUser(session, name, expectedDevic session.log.step(`verifies e2e device for ${name}`); const memberNameElements = await session.queryAll(".mx_MemberList .mx_EntityTile_name"); const membersAndNames = await Promise.all(memberNameElements.map(async (el) => { - const innerTextHandle = await memberNameElements.getProperty("innerText"); - const innerText = await innerTextHandle.jsonValue(); - return [el, innerText]; + return [el, await session.innerText(el)]; })); const matchingMember = membersAndNames.filter(([el, text]) => { return text === name; - }).map(([el, name]) => el); + }).map(([el]) => el)[0]; await matchingMember.click(); const firstVerifyButton = await session.waitAndQuery(".mx_MemberDeviceInfo_verify"); await firstVerifyButton.click(); const dialogCodeFields = await session.waitAndQueryAll(".mx_QuestionDialog code"); assert.equal(dialogCodeFields.length, 2); - const deviceId = dialogCodeFields[0]; - const deviceKey = dialogCodeFields[1]; + const deviceId = await session.innerText(dialogCodeFields[0]); + const deviceKey = await session.innerText(dialogCodeFields[1]); assert.equal(expectedDevice.id, deviceId); assert.equal(expectedDevice.key, deviceKey); const confirmButton = await session.query(".mx_Dialog_primary"); diff --git a/start.js b/start.js index f4005cbe85..6c68050c97 100644 --- a/start.js +++ b/start.js @@ -21,13 +21,17 @@ const scenario = require('./src/scenario'); const riotserver = 'http://localhost:5000'; const noLogs = process.argv.indexOf("--no-logs") !== -1; +const debug = process.argv.indexOf("--debug") !== -1; async function runTests() { let sessions = []; console.log("running tests ..."); const options = {}; - options.headless = false; + if (debug) { + // options.slowMo = 10; + options.headless = false; + } if (process.env.CHROME_PATH) { const path = process.env.CHROME_PATH; console.log(`(using external chrome/chromium at ${path}, make sure it's compatible with puppeteer)`); From 377a20fffa45a00d1f37e7e040e53da2c0a73c14 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 14 Aug 2018 12:53:16 +0200 Subject: [PATCH 0080/2372] bring indentation in line with other front-end projects --- .editorconfig | 23 +++ package.json | 24 +-- riot/install.sh | 4 +- riot/start.sh | 36 ++-- riot/stop.sh | 20 +- src/scenario.js | 92 ++++----- src/session.js | 304 ++++++++++++++-------------- src/tests/accept-invite.js | 34 ++-- src/tests/consent.js | 14 +- src/tests/create-room.js | 22 +- src/tests/dialog.js | 40 ++-- src/tests/e2e-device.js | 24 +-- src/tests/invite.js | 22 +- src/tests/join.js | 26 +-- src/tests/receive-message.js | 56 ++--- src/tests/room-settings.js | 108 +++++----- src/tests/send-message.js | 20 +- src/tests/server-notices-consent.js | 24 +-- src/tests/signup.js | 94 ++++----- src/tests/verify-device.js | 46 ++--- start.js | 106 +++++----- synapse/install.sh | 4 +- 22 files changed, 583 insertions(+), 560 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..880331a09e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# Copyright 2017 Aviral Dasgupta +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +root = true + +[*] +charset=utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true diff --git a/package.json b/package.json index 1cbdf5bd26..b5892a154a 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { - "name": "e2e-tests", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "dependencies": { - "puppeteer": "^1.6.0" - } + "name": "e2e-tests", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "puppeteer": "^1.6.0" + } } diff --git a/riot/install.sh b/riot/install.sh index 7f37fa9457..209926d4c5 100755 --- a/riot/install.sh +++ b/riot/install.sh @@ -3,8 +3,8 @@ RIOT_BRANCH=master BASE_DIR=$(readlink -f $(dirname $0)) if [ -d $BASE_DIR/riot-web ]; then - echo "riot is already installed" - exit + echo "riot is already installed" + exit fi cd $BASE_DIR diff --git a/riot/start.sh b/riot/start.sh index 1127535d2b..0af9f4faef 100755 --- a/riot/start.sh +++ b/riot/start.sh @@ -5,7 +5,7 @@ PIDFILE=$BASE_DIR/riot.pid CONFIG_BACKUP=config.e2etests_backup.json if [ -f $PIDFILE ]; then - exit + exit fi cd $BASE_DIR/ @@ -14,29 +14,29 @@ pushd riot-web/webapp/ > /dev/null # backup config file before we copy template if [ -f config.json ]; then - mv config.json $CONFIG_BACKUP + mv config.json $CONFIG_BACKUP fi cp $BASE_DIR/config-template/config.json . LOGFILE=$(mktemp) # run web server in the background, showing output on error ( - python -m SimpleHTTPServer $PORT > $LOGFILE 2>&1 & - PID=$! - echo $PID > $PIDFILE - # wait so subshell does not exit - # otherwise sleep below would not work - wait $PID; RESULT=$? + python -m SimpleHTTPServer $PORT > $LOGFILE 2>&1 & + PID=$! + echo $PID > $PIDFILE + # wait so subshell does not exit + # otherwise sleep below would not work + wait $PID; RESULT=$? - # NOT expected SIGTERM (128 + 15) - # from stop.sh? - if [ $RESULT -ne 143 ]; then - echo "failed" - cat $LOGFILE - rm $PIDFILE 2> /dev/null - fi - rm $LOGFILE - exit $RESULT + # NOT expected SIGTERM (128 + 15) + # from stop.sh? + if [ $RESULT -ne 143 ]; then + echo "failed" + cat $LOGFILE + rm $PIDFILE 2> /dev/null + fi + rm $LOGFILE + exit $RESULT )& # to be able to return the exit code for immediate errors (like address already in use) # we wait for a short amount of time in the background and exit when the first @@ -46,6 +46,6 @@ sleep 0.5 & wait -n; RESULT=$? # return exit code of first child to exit if [ $RESULT -eq 0 ]; then - echo "running" + echo "running" fi exit $RESULT diff --git a/riot/stop.sh b/riot/stop.sh index 8d3c925c80..a3e07f574f 100755 --- a/riot/stop.sh +++ b/riot/stop.sh @@ -6,15 +6,15 @@ CONFIG_BACKUP=config.e2etests_backup.json cd $BASE_DIR if [ -f $PIDFILE ]; then - echo "stopping riot server ..." - PID=$(cat $PIDFILE) - rm $PIDFILE - kill $PID + echo "stopping riot server ..." + PID=$(cat $PIDFILE) + rm $PIDFILE + kill $PID - # revert config file - cd riot-web/webapp - rm config.json - if [ -f $CONFIG_BACKUP ]; then - mv $CONFIG_BACKUP config.json - fi + # revert config file + cd riot-web/webapp + rm config.json + if [ -f $CONFIG_BACKUP ]; then + mv $CONFIG_BACKUP config.json + fi fi diff --git a/src/scenario.js b/src/scenario.js index 330db0fef2..f7229057a8 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -5,7 +5,7 @@ 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 + 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, @@ -29,57 +29,57 @@ const getE2EDeviceFromSettings = require('./tests/e2e-device'); const verifyDeviceForUser = require("./tests/verify-device"); module.exports = async function scenario(createSession) { - async function createUser(username) { - const session = await createSession(username); - await signup(session, session.username, 'testtest'); - await acceptServerNoticesInviteAndConsent(session); - return session; - } + async function createUser(username) { + const session = await createSession(username); + await signup(session, session.username, 'testtest'); + await acceptServerNoticesInviteAndConsent(session); + return session; + } - const alice = await createUser("alice"); - const bob = await createUser("bob"); + const alice = await createUser("alice"); + const bob = await createUser("bob"); - await createDirectoryRoomAndTalk(alice, bob); - await createE2ERoomAndTalk(alice, bob); + await createDirectoryRoomAndTalk(alice, bob); + await createE2ERoomAndTalk(alice, bob); } async function createDirectoryRoomAndTalk(alice, bob) { - console.log(" creating a public room and join through directory:"); - const room = 'test'; - await createRoom(alice, room); - await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests"}); - await join(bob, room); - const bobMessage = "hi Alice!"; - await sendMessage(bob, bobMessage); - await receiveMessage(alice, {sender: "bob", body: bobMessage}); - const aliceMessage = "hi Bob, welcome!" - await sendMessage(alice, aliceMessage); - await receiveMessage(bob, {sender: "alice", body: aliceMessage}); + console.log(" creating a public room and join through directory:"); + const room = 'test'; + await createRoom(alice, room); + await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests"}); + await join(bob, room); + const bobMessage = "hi Alice!"; + await sendMessage(bob, bobMessage); + await receiveMessage(alice, {sender: "bob", body: bobMessage}); + const aliceMessage = "hi Bob, welcome!" + await sendMessage(alice, aliceMessage); + await receiveMessage(bob, {sender: "alice", body: aliceMessage}); } async function createE2ERoomAndTalk(alice, bob) { - console.log(" creating an e2e encrypted room and join through invite:"); - const room = "secrets"; - await createRoom(bob, room); - await changeRoomSettings(bob, {encryption: true}); - await invite(bob, "@alice:localhost"); - await acceptInvite(alice, room); - const bobDevice = await getE2EDeviceFromSettings(bob); - // wait some time for the encryption warning dialog - // to appear after closing the settings - await bob.delay(500); - await acceptDialog(bob, "encryption"); - const aliceDevice = await getE2EDeviceFromSettings(alice); - // wait some time for the encryption warning dialog - // to appear after closing the settings - await alice.delay(500); - await acceptDialog(alice, "encryption"); - await verifyDeviceForUser(bob, "alice", aliceDevice); - await verifyDeviceForUser(alice, "bob", bobDevice); - const aliceMessage = "Guess what I just heard?!" - await sendMessage(alice, aliceMessage); - await receiveMessage(bob, {sender: "alice", body: aliceMessage, encrypted: true}); - const bobMessage = "You've got to tell me!"; - await sendMessage(bob, bobMessage); - await receiveMessage(alice, {sender: "bob", body: bobMessage, encrypted: true}); + console.log(" creating an e2e encrypted room and join through invite:"); + const room = "secrets"; + await createRoom(bob, room); + await changeRoomSettings(bob, {encryption: true}); + await invite(bob, "@alice:localhost"); + await acceptInvite(alice, room); + const bobDevice = await getE2EDeviceFromSettings(bob); + // wait some time for the encryption warning dialog + // to appear after closing the settings + await bob.delay(500); + await acceptDialog(bob, "encryption"); + const aliceDevice = await getE2EDeviceFromSettings(alice); + // wait some time for the encryption warning dialog + // to appear after closing the settings + await alice.delay(500); + await acceptDialog(alice, "encryption"); + await verifyDeviceForUser(bob, "alice", aliceDevice); + await verifyDeviceForUser(alice, "bob", bobDevice); + const aliceMessage = "Guess what I just heard?!" + await sendMessage(alice, aliceMessage); + await receiveMessage(bob, {sender: "alice", body: aliceMessage, encrypted: true}); + const bobMessage = "You've got to tell me!"; + await sendMessage(bob, bobMessage); + await receiveMessage(alice, {sender: "bob", body: bobMessage, encrypted: true}); } diff --git a/src/session.js b/src/session.js index 4f6e04584f..570bb4b558 100644 --- a/src/session.js +++ b/src/session.js @@ -5,7 +5,7 @@ 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 + 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, @@ -17,180 +17,180 @@ limitations under the License. const puppeteer = require('puppeteer'); class LogBuffer { - constructor(page, eventName, eventMapper, reduceAsync=false, initialValue = "") { - this.buffer = initialValue; - page.on(eventName, (arg) => { - const result = eventMapper(arg); - if (reduceAsync) { - result.then((r) => this.buffer += r); - } - else { - this.buffer += result; - } - }); - } + constructor(page, eventName, eventMapper, reduceAsync=false, initialValue = "") { + this.buffer = initialValue; + page.on(eventName, (arg) => { + const result = eventMapper(arg); + if (reduceAsync) { + result.then((r) => this.buffer += r); + } + else { + this.buffer += result; + } + }); + } } class Logger { - constructor(username) { - this.indent = 0; - this.username = username; - } + constructor(username) { + this.indent = 0; + this.username = username; + } - startGroup(description) { - const indent = " ".repeat(this.indent * 2); - console.log(`${indent} * ${this.username} ${description}:`); - this.indent += 1; - } + startGroup(description) { + const indent = " ".repeat(this.indent * 2); + console.log(`${indent} * ${this.username} ${description}:`); + this.indent += 1; + } - endGroup() { - this.indent -= 1; - } + endGroup() { + this.indent -= 1; + } - step(description) { - const indent = " ".repeat(this.indent * 2); - process.stdout.write(`${indent} * ${this.username} ${description} ... `); - } + step(description) { + const indent = " ".repeat(this.indent * 2); + process.stdout.write(`${indent} * ${this.username} ${description} ... `); + } - done(status = "done") { - process.stdout.write(status + "\n"); - } + done(status = "done") { + process.stdout.write(status + "\n"); + } } module.exports = class RiotSession { - constructor(browser, page, username, riotserver) { - this.browser = browser; - this.page = page; - this.riotserver = riotserver; - this.username = username; - this.consoleLog = new LogBuffer(page, "console", (msg) => `${msg.text()}\n`); - this.networkLog = new LogBuffer(page, "requestfinished", async (req) => { - const type = req.resourceType(); - const response = await req.response(); - return `${type} ${response.status()} ${req.method()} ${req.url()} \n`; - }, true); - this.log = new Logger(this.username); - } - - static async create(username, puppeteerOptions, riotserver) { - const browser = await puppeteer.launch(puppeteerOptions); - const page = await browser.newPage(); - await page.setViewport({ - width: 1280, - height: 800 - }); - return new RiotSession(browser, page, username, riotserver); - } - - async tryGetInnertext(selector) { - const field = await this.page.$(selector); - if (field != null) { - const text_handle = await field.getProperty('innerText'); - return await text_handle.jsonValue(); + constructor(browser, page, username, riotserver) { + this.browser = browser; + this.page = page; + this.riotserver = riotserver; + this.username = username; + this.consoleLog = new LogBuffer(page, "console", (msg) => `${msg.text()}\n`); + this.networkLog = new LogBuffer(page, "requestfinished", async (req) => { + const type = req.resourceType(); + const response = await req.response(); + return `${type} ${response.status()} ${req.method()} ${req.url()} \n`; + }, true); + this.log = new Logger(this.username); } - return null; - } - async getElementProperty(handle, property) { - const propHandle = await handle.getProperty(property); - return await propHandle.jsonValue(); - } - - innerText(field) { - return this.getElementProperty(field, 'innerText'); - } - - getOuterHTML(element_handle) { - return this.getElementProperty(field, 'outerHTML'); - } - - consoleLogs() { - return this.consoleLog.buffer; - } - - networkLogs() { - return this.networkLog.buffer; - } - - logXHRRequests() { - let buffer = ""; - this.page.on('requestfinished', async (req) => { - const type = req.resourceType(); - const response = await req.response(); - //if (type === 'xhr' || type === 'fetch') { - buffer += `${type} ${response.status()} ${req.method()} ${req.url()} \n`; - // if (req.method() === "POST") { - // buffer += " Post data: " + req.postData(); - // } - //} - }); - return { - logs() { - return buffer; - } + static async create(username, puppeteerOptions, riotserver) { + const browser = await puppeteer.launch(puppeteerOptions); + const page = await browser.newPage(); + await page.setViewport({ + width: 1280, + height: 800 + }); + return new RiotSession(browser, page, username, riotserver); } - } - async printElements(label, elements) { - console.log(label, await Promise.all(elements.map(getOuterHTML))); - } + async tryGetInnertext(selector) { + const field = await this.page.$(selector); + if (field != null) { + const text_handle = await field.getProperty('innerText'); + return await text_handle.jsonValue(); + } + return null; + } - async replaceInputText(input, text) { - // click 3 times to select all text - await input.click({clickCount: 3}); - // then remove it with backspace - await input.press('Backspace'); - // and type the new text - await input.type(text); - } + async getElementProperty(handle, property) { + const propHandle = await handle.getProperty(property); + return await propHandle.jsonValue(); + } - query(selector) { - return this.page.$(selector); - } - - waitAndQuery(selector, timeout = 500) { - return this.page.waitForSelector(selector, {visible: true, timeout}); - } + innerText(field) { + return this.getElementProperty(field, 'innerText'); + } - queryAll(selector) { - return this.page.$$(selector); - } + getOuterHTML(element_handle) { + return this.getElementProperty(field, 'outerHTML'); + } - async waitAndQueryAll(selector, timeout = 500) { - await this.waitAndQuery(selector, timeout); - return await this.queryAll(selector); - } + consoleLogs() { + return this.consoleLog.buffer; + } - waitForNewPage(timeout = 500) { - return new Promise((resolve, reject) => { - const timeoutHandle = setTimeout(() => { - this.browser.removeEventListener('targetcreated', callback); - reject(new Error(`timeout of ${timeout}ms for waitForNewPage elapsed`)); - }, timeout); + networkLogs() { + return this.networkLog.buffer; + } - const callback = async (target) => { - clearTimeout(timeoutHandle); - const page = await target.page(); - resolve(page); - }; + logXHRRequests() { + let buffer = ""; + this.page.on('requestfinished', async (req) => { + const type = req.resourceType(); + const response = await req.response(); + //if (type === 'xhr' || type === 'fetch') { + buffer += `${type} ${response.status()} ${req.method()} ${req.url()} \n`; + // if (req.method() === "POST") { + // buffer += " Post data: " + req.postData(); + // } + //} + }); + return { + logs() { + return buffer; + } + } + } - this.browser.once('targetcreated', callback); - }); - } + async printElements(label, elements) { + console.log(label, await Promise.all(elements.map(getOuterHTML))); + } - goto(url) { - return this.page.goto(url); - } + async replaceInputText(input, text) { + // click 3 times to select all text + await input.click({clickCount: 3}); + // then remove it with backspace + await input.press('Backspace'); + // and type the new text + await input.type(text); + } - url(path) { - return this.riotserver + path; - } + query(selector) { + return this.page.$(selector); + } - delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } + waitAndQuery(selector, timeout = 500) { + return this.page.waitForSelector(selector, {visible: true, timeout}); + } - close() { - return this.browser.close(); - } + queryAll(selector) { + return this.page.$$(selector); + } + + async waitAndQueryAll(selector, timeout = 500) { + await this.waitAndQuery(selector, timeout); + return await this.queryAll(selector); + } + + waitForNewPage(timeout = 500) { + return new Promise((resolve, reject) => { + const timeoutHandle = setTimeout(() => { + this.browser.removeEventListener('targetcreated', callback); + reject(new Error(`timeout of ${timeout}ms for waitForNewPage elapsed`)); + }, timeout); + + const callback = async (target) => { + clearTimeout(timeoutHandle); + const page = await target.page(); + resolve(page); + }; + + this.browser.once('targetcreated', callback); + }); + } + + goto(url) { + return this.page.goto(url); + } + + url(path) { + return this.riotserver + path; + } + + delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + close() { + return this.browser.close(); + } } diff --git a/src/tests/accept-invite.js b/src/tests/accept-invite.js index 5cdeeb3d84..c83e0c02cc 100644 --- a/src/tests/accept-invite.js +++ b/src/tests/accept-invite.js @@ -5,7 +5,7 @@ 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 + 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, @@ -18,24 +18,24 @@ const assert = require('assert'); const {acceptDialogMaybe} = require('./dialog'); module.exports = async function acceptInvite(session, name) { - session.log.step(`accepts "${name}" invite`); - //TODO: brittle selector - const invitesHandles = await session.waitAndQueryAll('.mx_RoomTile_name.mx_RoomTile_invite', 1000); - const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => { - const text = await session.innerText(inviteHandle); - return {inviteHandle, text}; - })); - const inviteHandle = invitesWithText.find(({inviteHandle, text}) => { - return text.trim() === name; - }).inviteHandle; + session.log.step(`accepts "${name}" invite`); + //TODO: brittle selector + const invitesHandles = await session.waitAndQueryAll('.mx_RoomTile_name.mx_RoomTile_invite', 1000); + const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => { + const text = await session.innerText(inviteHandle); + return {inviteHandle, text}; + })); + const inviteHandle = invitesWithText.find(({inviteHandle, text}) => { + return text.trim() === name; + }).inviteHandle; - await inviteHandle.click(); + await inviteHandle.click(); - const acceptInvitationLink = await session.waitAndQuery(".mx_RoomPreviewBar_join_text a:first-child"); - await acceptInvitationLink.click(); + const acceptInvitationLink = await session.waitAndQuery(".mx_RoomPreviewBar_join_text a:first-child"); + await acceptInvitationLink.click(); - // accept e2e warning dialog - acceptDialogMaybe(session, "encryption"); + // accept e2e warning dialog + acceptDialogMaybe(session, "encryption"); - session.log.done(); + session.log.done(); } \ No newline at end of file diff --git a/src/tests/consent.js b/src/tests/consent.js index cd3d51c1b6..595caf4d99 100644 --- a/src/tests/consent.js +++ b/src/tests/consent.js @@ -17,11 +17,11 @@ limitations under the License. const assert = require('assert'); module.exports = async function acceptTerms(session) { - const reviewTermsButton = await session.waitAndQuery('.mx_QuestionDialog button.mx_Dialog_primary', 5000); - const termsPagePromise = session.waitForNewPage(); - await reviewTermsButton.click(); - const termsPage = await termsPagePromise; - const acceptButton = await termsPage.$('input[type=submit]'); - await acceptButton.click(); - await session.delay(500); //TODO yuck, timers + const reviewTermsButton = await session.waitAndQuery('.mx_QuestionDialog button.mx_Dialog_primary', 5000); + const termsPagePromise = session.waitForNewPage(); + await reviewTermsButton.click(); + const termsPage = await termsPagePromise; + const acceptButton = await termsPage.$('input[type=submit]'); + await acceptButton.click(); + await session.delay(500); //TODO yuck, timers } \ No newline at end of file diff --git a/src/tests/create-room.js b/src/tests/create-room.js index 0f7f33ddff..7d3488bfbe 100644 --- a/src/tests/create-room.js +++ b/src/tests/create-room.js @@ -5,7 +5,7 @@ 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 + 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, @@ -17,17 +17,17 @@ limitations under the License. const assert = require('assert'); module.exports = async function createRoom(session, roomName) { - session.log.step(`creates room "${roomName}"`); - //TODO: brittle selector - const createRoomButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Create new room"]'); - await createRoomButton.click(); + session.log.step(`creates room "${roomName}"`); + //TODO: brittle selector + const createRoomButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Create new room"]'); + await createRoomButton.click(); - const roomNameInput = await session.waitAndQuery('.mx_CreateRoomDialog_input'); - await session.replaceInputText(roomNameInput, roomName); + const roomNameInput = await session.waitAndQuery('.mx_CreateRoomDialog_input'); + await session.replaceInputText(roomNameInput, roomName); - const createButton = await session.waitAndQuery('.mx_Dialog_primary'); - await createButton.click(); + const createButton = await session.waitAndQuery('.mx_Dialog_primary'); + await createButton.click(); - await session.waitAndQuery('.mx_MessageComposer'); - session.log.done(); + await session.waitAndQuery('.mx_MessageComposer'); + session.log.done(); } \ No newline at end of file diff --git a/src/tests/dialog.js b/src/tests/dialog.js index 420438b3f9..8d9c798c45 100644 --- a/src/tests/dialog.js +++ b/src/tests/dialog.js @@ -18,30 +18,30 @@ const assert = require('assert'); async function acceptDialog(session, expectedContent) { - const foundDialog = await acceptDialogMaybe(session, expectedContent); - if (!foundDialog) { - throw new Error("could not find a dialog"); - } + const foundDialog = await acceptDialogMaybe(session, expectedContent); + if (!foundDialog) { + throw new Error("could not find a dialog"); + } } async function acceptDialogMaybe(session, expectedContent) { - let dialog = null; - try { - dialog = await session.waitAndQuery(".mx_QuestionDialog", 100); - } catch(err) { - return false; - } - if (expectedContent) { - const contentElement = await dialog.$(".mx_Dialog_content"); - const content = await (await contentElement.getProperty("innerText")).jsonValue(); - assert.ok(content.indexOf(expectedContent) !== -1); - } - const primaryButton = await dialog.$(".mx_Dialog_primary"); - await primaryButton.click(); - return true; + let dialog = null; + try { + dialog = await session.waitAndQuery(".mx_QuestionDialog", 100); + } catch(err) { + return false; + } + if (expectedContent) { + const contentElement = await dialog.$(".mx_Dialog_content"); + const content = await (await contentElement.getProperty("innerText")).jsonValue(); + assert.ok(content.indexOf(expectedContent) !== -1); + } + const primaryButton = await dialog.$(".mx_Dialog_primary"); + await primaryButton.click(); + return true; } module.exports = { - acceptDialog, - acceptDialogMaybe, + acceptDialog, + acceptDialogMaybe, }; \ No newline at end of file diff --git a/src/tests/e2e-device.js b/src/tests/e2e-device.js index fd81ac43eb..03b8c8ce2b 100644 --- a/src/tests/e2e-device.js +++ b/src/tests/e2e-device.js @@ -5,7 +5,7 @@ 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 + 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, @@ -17,15 +17,15 @@ limitations under the License. const assert = require('assert'); module.exports = async function getE2EDeviceFromSettings(session) { - session.log.step(`gets e2e device/key from settings`); - const settingsButton = await session.query('.mx_BottomLeftMenu_settings'); - await settingsButton.click(); - const deviceAndKey = await session.waitAndQueryAll(".mx_UserSettings_section.mx_UserSettings_cryptoSection code"); - assert.equal(deviceAndKey.length, 2); - const id = await (await deviceAndKey[0].getProperty("innerText")).jsonValue(); - const key = await (await deviceAndKey[1].getProperty("innerText")).jsonValue(); - const closeButton = await session.query(".mx_RoomHeader_cancelButton"); - await closeButton.click(); - session.log.done(); - return {id, key}; + session.log.step(`gets e2e device/key from settings`); + const settingsButton = await session.query('.mx_BottomLeftMenu_settings'); + await settingsButton.click(); + const deviceAndKey = await session.waitAndQueryAll(".mx_UserSettings_section.mx_UserSettings_cryptoSection code"); + assert.equal(deviceAndKey.length, 2); + const id = await (await deviceAndKey[0].getProperty("innerText")).jsonValue(); + const key = await (await deviceAndKey[1].getProperty("innerText")).jsonValue(); + const closeButton = await session.query(".mx_RoomHeader_cancelButton"); + await closeButton.click(); + session.log.done(); + return {id, key}; } \ No newline at end of file diff --git a/src/tests/invite.js b/src/tests/invite.js index 37549aa2ca..5a5c66b7c2 100644 --- a/src/tests/invite.js +++ b/src/tests/invite.js @@ -5,7 +5,7 @@ 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 + 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, @@ -17,14 +17,14 @@ limitations under the License. const assert = require('assert'); module.exports = async function invite(session, userId) { - session.log.step(`invites "${userId}" to room`); - await session.delay(200); - const inviteButton = await session.waitAndQuery(".mx_RightPanel_invite"); - await inviteButton.click(); - const inviteTextArea = await session.waitAndQuery(".mx_ChatInviteDialog textarea"); - await inviteTextArea.type(userId); - await inviteTextArea.press("Enter"); - const confirmButton = await session.query(".mx_Dialog_primary"); - await confirmButton.click(); - session.log.done(); + session.log.step(`invites "${userId}" to room`); + await session.delay(200); + const inviteButton = await session.waitAndQuery(".mx_RightPanel_invite"); + await inviteButton.click(); + const inviteTextArea = await session.waitAndQuery(".mx_ChatInviteDialog textarea"); + await inviteTextArea.type(userId); + await inviteTextArea.press("Enter"); + const confirmButton = await session.query(".mx_Dialog_primary"); + await confirmButton.click(); + session.log.done(); } \ No newline at end of file diff --git a/src/tests/join.js b/src/tests/join.js index 8ab5e80f2d..76b98ca397 100644 --- a/src/tests/join.js +++ b/src/tests/join.js @@ -5,7 +5,7 @@ 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 + 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, @@ -17,20 +17,20 @@ limitations under the License. const assert = require('assert'); module.exports = async function join(session, roomName) { - session.log.step(`joins room "${roomName}"`); - //TODO: brittle selector - const directoryButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Room directory"]'); - await directoryButton.click(); + session.log.step(`joins room "${roomName}"`); + //TODO: brittle selector + const directoryButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Room directory"]'); + await directoryButton.click(); - const roomInput = await session.waitAndQuery('.mx_DirectorySearchBox_input'); - await session.replaceInputText(roomInput, roomName); + const roomInput = await session.waitAndQuery('.mx_DirectorySearchBox_input'); + await session.replaceInputText(roomInput, roomName); - const firstRoomLabel = await session.waitAndQuery('.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child', 1000); - await firstRoomLabel.click(); + const firstRoomLabel = await session.waitAndQuery('.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child', 1000); + await firstRoomLabel.click(); - const joinLink = await session.waitAndQuery('.mx_RoomPreviewBar_join_text a'); - await joinLink.click(); + const joinLink = await session.waitAndQuery('.mx_RoomPreviewBar_join_text a'); + await joinLink.click(); - await session.waitAndQuery('.mx_MessageComposer'); - session.log.done(); + await session.waitAndQuery('.mx_MessageComposer'); + session.log.done(); } \ No newline at end of file diff --git a/src/tests/receive-message.js b/src/tests/receive-message.js index 9c963c45f4..73d0cac1a0 100644 --- a/src/tests/receive-message.js +++ b/src/tests/receive-message.js @@ -5,7 +5,7 @@ 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 + 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, @@ -17,33 +17,33 @@ limitations under the License. const assert = require('assert'); module.exports = async function receiveMessage(session, message) { - session.log.step(`receives message "${message.body}" from ${message.sender}`); - // wait for a response to come in that contains the message - // crude, but effective - await session.page.waitForResponse(async (response) => { - if (response.request().url().indexOf("/sync") === -1) { - return false; - } - const body = await response.text(); + session.log.step(`receives message "${message.body}" from ${message.sender}`); + // wait for a response to come in that contains the message + // crude, but effective + await session.page.waitForResponse(async (response) => { + if (response.request().url().indexOf("/sync") === -1) { + return false; + } + const body = await response.text(); + if (message.encrypted) { + return body.indexOf(message.sender) !== -1 && + body.indexOf("m.room.encrypted") !== -1; + } else { + return body.indexOf(message.body) !== -1; + } + }); + // wait a bit for the incoming event to be rendered + await session.delay(300); + let lastTile = await session.query(".mx_EventTile_last"); + const senderElement = await lastTile.$(".mx_SenderProfile_name"); + const bodyElement = await lastTile.$(".mx_EventTile_body"); + const sender = await(await senderElement.getProperty("innerText")).jsonValue(); + const body = await(await bodyElement.getProperty("innerText")).jsonValue(); if (message.encrypted) { - return body.indexOf(message.sender) !== -1 && - body.indexOf("m.room.encrypted") !== -1; - } else { - return body.indexOf(message.body) !== -1; + const e2eIcon = await lastTile.$(".mx_EventTile_e2eIcon"); + assert.ok(e2eIcon); } - }); - // wait a bit for the incoming event to be rendered - await session.delay(300); - let lastTile = await session.query(".mx_EventTile_last"); - const senderElement = await lastTile.$(".mx_SenderProfile_name"); - const bodyElement = await lastTile.$(".mx_EventTile_body"); - const sender = await(await senderElement.getProperty("innerText")).jsonValue(); - const body = await(await bodyElement.getProperty("innerText")).jsonValue(); - if (message.encrypted) { - const e2eIcon = await lastTile.$(".mx_EventTile_e2eIcon"); - assert.ok(e2eIcon); - } - assert.equal(body, message.body); - assert.equal(sender, message.sender); - session.log.done(); + assert.equal(body, message.body); + assert.equal(sender, message.sender); + session.log.done(); } \ No newline at end of file diff --git a/src/tests/room-settings.js b/src/tests/room-settings.js index 127afa26dd..663f275203 100644 --- a/src/tests/room-settings.js +++ b/src/tests/room-settings.js @@ -18,66 +18,66 @@ const assert = require('assert'); const {acceptDialog} = require('./dialog'); async function setCheckboxSetting(session, checkbox, enabled) { - const checked = await session.getElementProperty(checkbox, "checked"); - assert.equal(typeof checked, "boolean"); - if (checked !== enabled) { - await checkbox.click(); - session.log.done(); - return true; - } else { - session.log.done("already set"); - } + const checked = await session.getElementProperty(checkbox, "checked"); + assert.equal(typeof checked, "boolean"); + if (checked !== enabled) { + await checkbox.click(); + session.log.done(); + return true; + } else { + session.log.done("already set"); + } } module.exports = async function changeRoomSettings(session, settings) { - session.log.startGroup(`changes the room settings`); - /// XXX delay is needed here, possible because the header is being rerendered - /// click doesn't do anything otherwise - await session.delay(500); - const settingsButton = await session.query(".mx_RoomHeader .mx_AccessibleButton[title=Settings]"); - await settingsButton.click(); - const checks = await session.waitAndQueryAll(".mx_RoomSettings_settings input[type=checkbox]"); - assert.equal(checks.length, 3); - const e2eEncryptionCheck = checks[0]; - const sendToUnverifiedDevices = checks[1]; - const isDirectory = checks[2]; + session.log.startGroup(`changes the room settings`); + /// XXX delay is needed here, possible because the header is being rerendered + /// click doesn't do anything otherwise + await session.delay(500); + const settingsButton = await session.query(".mx_RoomHeader .mx_AccessibleButton[title=Settings]"); + await settingsButton.click(); + const checks = await session.waitAndQueryAll(".mx_RoomSettings_settings input[type=checkbox]"); + assert.equal(checks.length, 3); + const e2eEncryptionCheck = checks[0]; + const sendToUnverifiedDevices = checks[1]; + const isDirectory = checks[2]; - if (typeof settings.directory === "boolean") { - session.log.step(`sets directory listing to ${settings.directory}`); - await setCheckboxSetting(session, isDirectory, settings.directory); - } + if (typeof settings.directory === "boolean") { + session.log.step(`sets directory listing to ${settings.directory}`); + await setCheckboxSetting(session, isDirectory, settings.directory); + } - if (typeof settings.encryption === "boolean") { - session.log.step(`sets room e2e encryption to ${settings.encryption}`); - const clicked = await setCheckboxSetting(session, e2eEncryptionCheck, settings.encryption); - // if enabling, accept beta warning dialog - if (clicked && settings.encryption) { - await acceptDialog(session, "encryption"); - } - } + if (typeof settings.encryption === "boolean") { + session.log.step(`sets room e2e encryption to ${settings.encryption}`); + const clicked = await setCheckboxSetting(session, e2eEncryptionCheck, settings.encryption); + // if enabling, accept beta warning dialog + if (clicked && settings.encryption) { + await acceptDialog(session, "encryption"); + } + } - if (settings.visibility) { - session.log.step(`sets visibility to ${settings.visibility}`); - const radios = await session.waitAndQueryAll(".mx_RoomSettings_settings input[type=radio]"); - assert.equal(radios.length, 7); - const inviteOnly = radios[0]; - const publicNoGuests = radios[1]; - const publicWithGuests = radios[2]; - - if (settings.visibility === "invite_only") { - await inviteOnly.click(); - } else if (settings.visibility === "public_no_guests") { - await publicNoGuests.click(); - } else if (settings.visibility === "public_with_guests") { - await publicWithGuests.click(); - } else { - throw new Error(`unrecognized room visibility setting: ${settings.visibility}`); - } - session.log.done(); - } + if (settings.visibility) { + session.log.step(`sets visibility to ${settings.visibility}`); + const radios = await session.waitAndQueryAll(".mx_RoomSettings_settings input[type=radio]"); + assert.equal(radios.length, 7); + const inviteOnly = radios[0]; + const publicNoGuests = radios[1]; + const publicWithGuests = radios[2]; + + if (settings.visibility === "invite_only") { + await inviteOnly.click(); + } else if (settings.visibility === "public_no_guests") { + await publicNoGuests.click(); + } else if (settings.visibility === "public_with_guests") { + await publicWithGuests.click(); + } else { + throw new Error(`unrecognized room visibility setting: ${settings.visibility}`); + } + session.log.done(); + } - const saveButton = await session.query(".mx_RoomHeader_wrapper .mx_RoomHeader_textButton"); - await saveButton.click(); + const saveButton = await session.query(".mx_RoomHeader_wrapper .mx_RoomHeader_textButton"); + await saveButton.click(); - session.log.endGroup(); + session.log.endGroup(); } \ No newline at end of file diff --git a/src/tests/send-message.js b/src/tests/send-message.js index e98d0dad72..eb70f5ce23 100644 --- a/src/tests/send-message.js +++ b/src/tests/send-message.js @@ -5,7 +5,7 @@ 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 + 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, @@ -17,13 +17,13 @@ limitations under the License. const assert = require('assert'); module.exports = async function sendMessage(session, message) { - session.log.step(`writes "${message}" in room`); - // this selector needs to be the element that has contenteditable=true, - // not any if its parents, otherwise it behaves flaky at best. - const composer = await session.waitAndQuery('.mx_MessageComposer_editor'); - await composer.type(message); - const text = await session.innerText(composer); - assert.equal(text.trim(), message.trim()); - await composer.press("Enter"); - session.log.done(); + session.log.step(`writes "${message}" in room`); + // this selector needs to be the element that has contenteditable=true, + // not any if its parents, otherwise it behaves flaky at best. + const composer = await session.waitAndQuery('.mx_MessageComposer_editor'); + await composer.type(message); + const text = await session.innerText(composer); + assert.equal(text.trim(), message.trim()); + await composer.press("Enter"); + session.log.done(); } \ No newline at end of file diff --git a/src/tests/server-notices-consent.js b/src/tests/server-notices-consent.js index 3e07248daa..f1f4b7edae 100644 --- a/src/tests/server-notices-consent.js +++ b/src/tests/server-notices-consent.js @@ -5,7 +5,7 @@ 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 + 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, @@ -17,15 +17,15 @@ limitations under the License. const assert = require('assert'); const acceptInvite = require("./accept-invite") module.exports = async function acceptServerNoticesInviteAndConsent(session) { - await acceptInvite(session, "Server Notices"); - session.log.step(`accepts terms & conditions`); - const consentLink = await session.waitAndQuery(".mx_EventTile_body a", 1000); - const termsPagePromise = session.waitForNewPage(); - await consentLink.click(); - const termsPage = await termsPagePromise; - const acceptButton = await termsPage.$('input[type=submit]'); - await acceptButton.click(); - await session.delay(500); //TODO yuck, timers - await termsPage.close(); - session.log.done(); + await acceptInvite(session, "Server Notices"); + session.log.step(`accepts terms & conditions`); + const consentLink = await session.waitAndQuery(".mx_EventTile_body a", 1000); + const termsPagePromise = session.waitForNewPage(); + await consentLink.click(); + const termsPage = await termsPagePromise; + const acceptButton = await termsPage.$('input[type=submit]'); + await acceptButton.click(); + await session.delay(500); //TODO yuck, timers + await termsPage.close(); + session.log.done(); } \ No newline at end of file diff --git a/src/tests/signup.js b/src/tests/signup.js index 434083cbb6..363600f03d 100644 --- a/src/tests/signup.js +++ b/src/tests/signup.js @@ -5,7 +5,7 @@ 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 + 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, @@ -18,52 +18,52 @@ const acceptTerms = require('./consent'); const assert = require('assert'); module.exports = async function signup(session, username, password, homeserver) { - session.log.step("signs up"); - await session.goto(session.url('/#/register')); - //click 'Custom server' radio button - if (homeserver) { - const advancedRadioButton = await session.waitAndQuery('#advanced'); - await advancedRadioButton.click(); - } - // wait until register button is visible - await session.waitAndQuery('.mx_Login_submit[value=Register]'); - //fill out form - const loginFields = await session.queryAll('.mx_Login_field'); - assert.strictEqual(loginFields.length, 7); - const usernameField = loginFields[2]; - const passwordField = loginFields[3]; - const passwordRepeatField = loginFields[4]; - const hsurlField = loginFields[5]; - await session.replaceInputText(usernameField, username); - await session.replaceInputText(passwordField, password); - await session.replaceInputText(passwordRepeatField, password); - if (homeserver) { - await session.waitAndQuery('.mx_ServerConfig'); - await session.replaceInputText(hsurlField, homeserver); - } - //wait over a second because Registration/ServerConfig have a 1000ms - //delay to internally set the homeserver url - //see Registration::render and ServerConfig::props::delayTimeMs - await session.delay(1200); - /// focus on the button to make sure error validation - /// has happened before checking the form is good to go - const registerButton = await session.query('.mx_Login_submit'); - await registerButton.focus(); - //check no errors - const error_text = await session.tryGetInnertext('.mx_Login_error'); - assert.strictEqual(!!error_text, false); - //submit form - //await page.screenshot({path: "beforesubmit.png", fullPage: true}); - await registerButton.click(); + session.log.step("signs up"); + await session.goto(session.url('/#/register')); + //click 'Custom server' radio button + if (homeserver) { + const advancedRadioButton = await session.waitAndQuery('#advanced'); + await advancedRadioButton.click(); + } + // wait until register button is visible + await session.waitAndQuery('.mx_Login_submit[value=Register]'); + //fill out form + const loginFields = await session.queryAll('.mx_Login_field'); + assert.strictEqual(loginFields.length, 7); + const usernameField = loginFields[2]; + const passwordField = loginFields[3]; + const passwordRepeatField = loginFields[4]; + const hsurlField = loginFields[5]; + await session.replaceInputText(usernameField, username); + await session.replaceInputText(passwordField, password); + await session.replaceInputText(passwordRepeatField, password); + if (homeserver) { + await session.waitAndQuery('.mx_ServerConfig'); + await session.replaceInputText(hsurlField, homeserver); + } + //wait over a second because Registration/ServerConfig have a 1000ms + //delay to internally set the homeserver url + //see Registration::render and ServerConfig::props::delayTimeMs + await session.delay(1200); + /// focus on the button to make sure error validation + /// has happened before checking the form is good to go + const registerButton = await session.query('.mx_Login_submit'); + await registerButton.focus(); + //check no errors + const error_text = await session.tryGetInnertext('.mx_Login_error'); + assert.strictEqual(!!error_text, false); + //submit form + //await page.screenshot({path: "beforesubmit.png", fullPage: true}); + await registerButton.click(); - //confirm dialog saying you cant log back in without e-mail - const continueButton = await session.waitAndQuery('.mx_QuestionDialog button.mx_Dialog_primary'); - await continueButton.click(); - //wait for registration to finish so the hash gets set - //onhashchange better? - await session.delay(2000); + //confirm dialog saying you cant log back in without e-mail + const continueButton = await session.waitAndQuery('.mx_QuestionDialog button.mx_Dialog_primary'); + await continueButton.click(); + //wait for registration to finish so the hash gets set + //onhashchange better? + await session.delay(2000); - const url = session.page.url(); - assert.strictEqual(url, session.url('/#/home')); - session.log.done(); + const url = session.page.url(); + assert.strictEqual(url, session.url('/#/home')); + session.log.done(); } diff --git a/src/tests/verify-device.js b/src/tests/verify-device.js index 0622654876..7b01e7c756 100644 --- a/src/tests/verify-device.js +++ b/src/tests/verify-device.js @@ -5,7 +5,7 @@ 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 + 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, @@ -17,26 +17,26 @@ limitations under the License. const assert = require('assert'); module.exports = async function verifyDeviceForUser(session, name, expectedDevice) { - session.log.step(`verifies e2e device for ${name}`); - const memberNameElements = await session.queryAll(".mx_MemberList .mx_EntityTile_name"); - const membersAndNames = await Promise.all(memberNameElements.map(async (el) => { - return [el, await session.innerText(el)]; - })); - const matchingMember = membersAndNames.filter(([el, text]) => { - return text === name; - }).map(([el]) => el)[0]; - await matchingMember.click(); - const firstVerifyButton = await session.waitAndQuery(".mx_MemberDeviceInfo_verify"); - await firstVerifyButton.click(); - const dialogCodeFields = await session.waitAndQueryAll(".mx_QuestionDialog code"); - assert.equal(dialogCodeFields.length, 2); - const deviceId = await session.innerText(dialogCodeFields[0]); - const deviceKey = await session.innerText(dialogCodeFields[1]); - assert.equal(expectedDevice.id, deviceId); - assert.equal(expectedDevice.key, deviceKey); - const confirmButton = await session.query(".mx_Dialog_primary"); - await confirmButton.click(); - const closeMemberInfo = await session.query(".mx_MemberInfo_cancel"); - await closeMemberInfo.click(); - session.log.done(); + session.log.step(`verifies e2e device for ${name}`); + const memberNameElements = await session.queryAll(".mx_MemberList .mx_EntityTile_name"); + const membersAndNames = await Promise.all(memberNameElements.map(async (el) => { + return [el, await session.innerText(el)]; + })); + const matchingMember = membersAndNames.filter(([el, text]) => { + return text === name; + }).map(([el]) => el)[0]; + await matchingMember.click(); + const firstVerifyButton = await session.waitAndQuery(".mx_MemberDeviceInfo_verify"); + await firstVerifyButton.click(); + const dialogCodeFields = await session.waitAndQueryAll(".mx_QuestionDialog code"); + assert.equal(dialogCodeFields.length, 2); + const deviceId = await session.innerText(dialogCodeFields[0]); + const deviceKey = await session.innerText(dialogCodeFields[1]); + assert.equal(expectedDevice.id, deviceId); + assert.equal(expectedDevice.key, deviceKey); + const confirmButton = await session.query(".mx_Dialog_primary"); + await confirmButton.click(); + const closeMemberInfo = await session.query(".mx_MemberInfo_cancel"); + await closeMemberInfo.click(); + session.log.done(); } \ No newline at end of file diff --git a/start.js b/start.js index 6c68050c97..229a9ab535 100644 --- a/start.js +++ b/start.js @@ -5,7 +5,7 @@ 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 + 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, @@ -24,64 +24,64 @@ const noLogs = process.argv.indexOf("--no-logs") !== -1; const debug = process.argv.indexOf("--debug") !== -1; async function runTests() { - let sessions = []; + let sessions = []; - console.log("running tests ..."); - const options = {}; - if (debug) { - // options.slowMo = 10; - options.headless = false; - } - if (process.env.CHROME_PATH) { - const path = process.env.CHROME_PATH; - console.log(`(using external chrome/chromium at ${path}, make sure it's compatible with puppeteer)`); - options.executablePath = path; - } - - async function createSession(username) { - const session = await RiotSession.create(username, options, riotserver); - sessions.push(session); - return session; - } - - let failure = false; - try { - await scenario(createSession); - } catch(err) { - failure = true; - console.log('failure: ', err); - if (!noLogs) { - for(let i = 0; i < sessions.length; ++i) { - const session = sessions[i]; - documentHtml = await session.page.content(); - console.log(`---------------- START OF ${session.username} LOGS ----------------`); - console.log('---------------- console.log output:'); - console.log(session.consoleLogs()); - console.log('---------------- network requests:'); - console.log(session.networkLogs()); - console.log('---------------- document html:'); - console.log(documentHtml); - console.log(`---------------- END OF ${session.username} LOGS ----------------`); - } + console.log("running tests ..."); + const options = {}; + if (debug) { + options.slowMo = 20; + options.headless = false; + } + if (process.env.CHROME_PATH) { + const path = process.env.CHROME_PATH; + console.log(`(using external chrome/chromium at ${path}, make sure it's compatible with puppeteer)`); + options.executablePath = path; } - } - // wait 5 minutes on failure if not running headless - // to inspect what went wrong - if (failure && options.headless === false) { - await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000)); - } + async function createSession(username) { + const session = await RiotSession.create(username, options, riotserver); + sessions.push(session); + return session; + } - await Promise.all(sessions.map((session) => session.close())); + let failure = false; + try { + await scenario(createSession); + } catch(err) { + failure = true; + console.log('failure: ', err); + if (!noLogs) { + for(let i = 0; i < sessions.length; ++i) { + const session = sessions[i]; + documentHtml = await session.page.content(); + console.log(`---------------- START OF ${session.username} LOGS ----------------`); + console.log('---------------- console.log output:'); + console.log(session.consoleLogs()); + console.log('---------------- network requests:'); + console.log(session.networkLogs()); + console.log('---------------- document html:'); + console.log(documentHtml); + console.log(`---------------- END OF ${session.username} LOGS ----------------`); + } + } + } - if (failure) { - process.exit(-1); - } else { - console.log('all tests finished successfully'); - } + // wait 5 minutes on failure if not running headless + // to inspect what went wrong + if (failure && options.headless === false) { + await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000)); + } + + await Promise.all(sessions.map((session) => session.close())); + + if (failure) { + process.exit(-1); + } else { + console.log('all tests finished successfully'); + } } runTests().catch(function(err) { - console.log(err); - process.exit(-1); + console.log(err); + process.exit(-1); }); \ No newline at end of file diff --git a/synapse/install.sh b/synapse/install.sh index 2e9b668b5e..3d8172a9f6 100755 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -9,8 +9,8 @@ PORT=5005 BASE_DIR=$(readlink -f $(dirname $0)) if [ -d $BASE_DIR/$SERVER_DIR ]; then - echo "synapse is already installed" - exit + echo "synapse is already installed" + exit fi cd $BASE_DIR From 8507cf82582f7810344a1b89594aa40ff2c3175e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 15 Aug 2018 10:49:06 +0200 Subject: [PATCH 0081/2372] add argument for passing riot server, makes local testing easier --- package.json | 1 + start.js | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index b5892a154a..8035fbb508 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "author": "", "license": "ISC", "dependencies": { + "commander": "^2.17.1", "puppeteer": "^1.6.0" } } diff --git a/start.js b/start.js index 229a9ab535..f1f7960555 100644 --- a/start.js +++ b/start.js @@ -18,18 +18,22 @@ const assert = require('assert'); const RiotSession = require('./src/session'); const scenario = require('./src/scenario'); -const riotserver = 'http://localhost:5000'; - -const noLogs = process.argv.indexOf("--no-logs") !== -1; -const debug = process.argv.indexOf("--debug") !== -1; +const program = require('commander'); +program + .option('--no-logs', "don't output logs, document html on error", false) + .option('--debug', "open browser window and slow down interactions", false) + .option('--riot-url [url]', "riot url to test", "http://localhost:5000") + .parse(process.argv); async function runTests() { let sessions = []; + console.log("program.riotUrl", program.riotUrl); console.log("running tests ..."); const options = {}; - if (debug) { + if (program.debug) { options.slowMo = 20; + options.devtools = true; options.headless = false; } if (process.env.CHROME_PATH) { @@ -39,7 +43,7 @@ async function runTests() { } async function createSession(username) { - const session = await RiotSession.create(username, options, riotserver); + const session = await RiotSession.create(username, options, program.riotUrl); sessions.push(session); return session; } @@ -50,7 +54,7 @@ async function runTests() { } catch(err) { failure = true; console.log('failure: ', err); - if (!noLogs) { + if (!program.noLogs) { for(let i = 0; i < sessions.length; ++i) { const session = sessions[i]; documentHtml = await session.page.content(); @@ -84,4 +88,4 @@ async function runTests() { runTests().catch(function(err) { console.log(err); process.exit(-1); -}); \ No newline at end of file +}); From 0e56250bc2cf2939d7e5ff8995888ecc559dfe79 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 15 Aug 2018 12:21:08 +0200 Subject: [PATCH 0082/2372] didnt mean to commit this --- start.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/start.js b/start.js index f1f7960555..ac9a2f8684 100644 --- a/start.js +++ b/start.js @@ -27,8 +27,6 @@ program async function runTests() { let sessions = []; - - console.log("program.riotUrl", program.riotUrl); console.log("running tests ..."); const options = {}; if (program.debug) { From 4f76ad83d515881ce878d5358cdb2485d26a4ea0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 15 Aug 2018 15:04:24 +0200 Subject: [PATCH 0083/2372] increase timeout --- src/tests/e2e-device.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/e2e-device.js b/src/tests/e2e-device.js index 03b8c8ce2b..4be6677396 100644 --- a/src/tests/e2e-device.js +++ b/src/tests/e2e-device.js @@ -20,7 +20,7 @@ module.exports = async function getE2EDeviceFromSettings(session) { session.log.step(`gets e2e device/key from settings`); const settingsButton = await session.query('.mx_BottomLeftMenu_settings'); await settingsButton.click(); - const deviceAndKey = await session.waitAndQueryAll(".mx_UserSettings_section.mx_UserSettings_cryptoSection code"); + const deviceAndKey = await session.waitAndQueryAll(".mx_UserSettings_section.mx_UserSettings_cryptoSection code", 1000); assert.equal(deviceAndKey.length, 2); const id = await (await deviceAndKey[0].getProperty("innerText")).jsonValue(); const key = await (await deviceAndKey[1].getProperty("innerText")).jsonValue(); @@ -28,4 +28,4 @@ module.exports = async function getE2EDeviceFromSettings(session) { await closeButton.click(); session.log.done(); return {id, key}; -} \ No newline at end of file +} From 440b1032d5aa46a23c5ad8bd1dc4d089a5bb5c5f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 15 Aug 2018 15:17:11 +0200 Subject: [PATCH 0084/2372] increase receive message timeout --- src/tests/receive-message.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/receive-message.js b/src/tests/receive-message.js index 73d0cac1a0..9a283f4695 100644 --- a/src/tests/receive-message.js +++ b/src/tests/receive-message.js @@ -33,7 +33,7 @@ module.exports = async function receiveMessage(session, message) { } }); // wait a bit for the incoming event to be rendered - await session.delay(300); + await session.delay(500); let lastTile = await session.query(".mx_EventTile_last"); const senderElement = await lastTile.$(".mx_SenderProfile_name"); const bodyElement = await lastTile.$(".mx_EventTile_body"); @@ -46,4 +46,4 @@ module.exports = async function receiveMessage(session, message) { assert.equal(body, message.body); assert.equal(sender, message.sender); session.log.done(); -} \ No newline at end of file +} From fd67ace078f86c213d348b22625285ff935c5827 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 23 Aug 2018 00:27:30 +0200 Subject: [PATCH 0085/2372] increase timeouts so the tests dont timeout on build server --- src/scenario.js | 4 ++-- src/session.js | 6 +++--- src/tests/consent.js | 4 ++-- src/tests/invite.js | 4 ++-- src/tests/receive-message.js | 2 +- src/tests/room-settings.js | 6 +++--- src/tests/server-notices-consent.js | 4 ++-- src/tests/signup.js | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/scenario.js b/src/scenario.js index f7229057a8..d859b74781 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -67,12 +67,12 @@ async function createE2ERoomAndTalk(alice, bob) { const bobDevice = await getE2EDeviceFromSettings(bob); // wait some time for the encryption warning dialog // to appear after closing the settings - await bob.delay(500); + await bob.delay(1000); await acceptDialog(bob, "encryption"); const aliceDevice = await getE2EDeviceFromSettings(alice); // wait some time for the encryption warning dialog // to appear after closing the settings - await alice.delay(500); + await alice.delay(1000); await acceptDialog(alice, "encryption"); await verifyDeviceForUser(bob, "alice", aliceDevice); await verifyDeviceForUser(alice, "bob", bobDevice); diff --git a/src/session.js b/src/session.js index 570bb4b558..ba0384b91f 100644 --- a/src/session.js +++ b/src/session.js @@ -148,7 +148,7 @@ module.exports = class RiotSession { return this.page.$(selector); } - waitAndQuery(selector, timeout = 500) { + waitAndQuery(selector, timeout = 5000) { return this.page.waitForSelector(selector, {visible: true, timeout}); } @@ -156,12 +156,12 @@ module.exports = class RiotSession { return this.page.$$(selector); } - async waitAndQueryAll(selector, timeout = 500) { + async waitAndQueryAll(selector, timeout = 5000) { await this.waitAndQuery(selector, timeout); return await this.queryAll(selector); } - waitForNewPage(timeout = 500) { + waitForNewPage(timeout = 5000) { return new Promise((resolve, reject) => { const timeoutHandle = setTimeout(() => { this.browser.removeEventListener('targetcreated', callback); diff --git a/src/tests/consent.js b/src/tests/consent.js index 595caf4d99..0b25eb06be 100644 --- a/src/tests/consent.js +++ b/src/tests/consent.js @@ -23,5 +23,5 @@ module.exports = async function acceptTerms(session) { const termsPage = await termsPagePromise; const acceptButton = await termsPage.$('input[type=submit]'); await acceptButton.click(); - await session.delay(500); //TODO yuck, timers -} \ No newline at end of file + await session.delay(1000); //TODO yuck, timers +} diff --git a/src/tests/invite.js b/src/tests/invite.js index 5a5c66b7c2..934beb6819 100644 --- a/src/tests/invite.js +++ b/src/tests/invite.js @@ -18,7 +18,7 @@ const assert = require('assert'); module.exports = async function invite(session, userId) { session.log.step(`invites "${userId}" to room`); - await session.delay(200); + await session.delay(1000); const inviteButton = await session.waitAndQuery(".mx_RightPanel_invite"); await inviteButton.click(); const inviteTextArea = await session.waitAndQuery(".mx_ChatInviteDialog textarea"); @@ -27,4 +27,4 @@ module.exports = async function invite(session, userId) { const confirmButton = await session.query(".mx_Dialog_primary"); await confirmButton.click(); session.log.done(); -} \ No newline at end of file +} diff --git a/src/tests/receive-message.js b/src/tests/receive-message.js index 9a283f4695..afe4247181 100644 --- a/src/tests/receive-message.js +++ b/src/tests/receive-message.js @@ -33,7 +33,7 @@ module.exports = async function receiveMessage(session, message) { } }); // wait a bit for the incoming event to be rendered - await session.delay(500); + await session.delay(1000); let lastTile = await session.query(".mx_EventTile_last"); const senderElement = await lastTile.$(".mx_SenderProfile_name"); const bodyElement = await lastTile.$(".mx_EventTile_body"); diff --git a/src/tests/room-settings.js b/src/tests/room-settings.js index 663f275203..9f802da711 100644 --- a/src/tests/room-settings.js +++ b/src/tests/room-settings.js @@ -33,7 +33,7 @@ module.exports = async function changeRoomSettings(session, settings) { session.log.startGroup(`changes the room settings`); /// XXX delay is needed here, possible because the header is being rerendered /// click doesn't do anything otherwise - await session.delay(500); + await session.delay(1000); const settingsButton = await session.query(".mx_RoomHeader .mx_AccessibleButton[title=Settings]"); await settingsButton.click(); const checks = await session.waitAndQueryAll(".mx_RoomSettings_settings input[type=checkbox]"); @@ -63,7 +63,7 @@ module.exports = async function changeRoomSettings(session, settings) { const inviteOnly = radios[0]; const publicNoGuests = radios[1]; const publicWithGuests = radios[2]; - + if (settings.visibility === "invite_only") { await inviteOnly.click(); } else if (settings.visibility === "public_no_guests") { @@ -80,4 +80,4 @@ module.exports = async function changeRoomSettings(session, settings) { await saveButton.click(); session.log.endGroup(); -} \ No newline at end of file +} diff --git a/src/tests/server-notices-consent.js b/src/tests/server-notices-consent.js index f1f4b7edae..d0c91da7b1 100644 --- a/src/tests/server-notices-consent.js +++ b/src/tests/server-notices-consent.js @@ -25,7 +25,7 @@ module.exports = async function acceptServerNoticesInviteAndConsent(session) { const termsPage = await termsPagePromise; const acceptButton = await termsPage.$('input[type=submit]'); await acceptButton.click(); - await session.delay(500); //TODO yuck, timers + await session.delay(1000); //TODO yuck, timers await termsPage.close(); session.log.done(); -} \ No newline at end of file +} diff --git a/src/tests/signup.js b/src/tests/signup.js index 363600f03d..b715e111a1 100644 --- a/src/tests/signup.js +++ b/src/tests/signup.js @@ -44,7 +44,7 @@ module.exports = async function signup(session, username, password, homeserver) //wait over a second because Registration/ServerConfig have a 1000ms //delay to internally set the homeserver url //see Registration::render and ServerConfig::props::delayTimeMs - await session.delay(1200); + await session.delay(1500); /// focus on the button to make sure error validation /// has happened before checking the form is good to go const registerButton = await session.query('.mx_Login_submit'); From f49b85897d2eef02574ef5719cf0dccb12986446 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 23 Aug 2018 00:29:24 +0200 Subject: [PATCH 0086/2372] remove specific timeout for selectors as these are not hard sleeps, but timeouts, its better to put them a bit larger, as in the best case they'll return quickly anyway and in the worst case where they need a lot of time it's still better if the tests don't fail --- src/tests/accept-invite.js | 4 ++-- src/tests/consent.js | 2 +- src/tests/dialog.js | 4 ++-- src/tests/server-notices-consent.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/tests/accept-invite.js b/src/tests/accept-invite.js index c83e0c02cc..8cc1a0b37d 100644 --- a/src/tests/accept-invite.js +++ b/src/tests/accept-invite.js @@ -20,7 +20,7 @@ const {acceptDialogMaybe} = require('./dialog'); module.exports = async function acceptInvite(session, name) { session.log.step(`accepts "${name}" invite`); //TODO: brittle selector - const invitesHandles = await session.waitAndQueryAll('.mx_RoomTile_name.mx_RoomTile_invite', 1000); + const invitesHandles = await session.waitAndQueryAll('.mx_RoomTile_name.mx_RoomTile_invite'); const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => { const text = await session.innerText(inviteHandle); return {inviteHandle, text}; @@ -38,4 +38,4 @@ module.exports = async function acceptInvite(session, name) { acceptDialogMaybe(session, "encryption"); session.log.done(); -} \ No newline at end of file +} diff --git a/src/tests/consent.js b/src/tests/consent.js index 0b25eb06be..b4a6289fca 100644 --- a/src/tests/consent.js +++ b/src/tests/consent.js @@ -17,7 +17,7 @@ limitations under the License. const assert = require('assert'); module.exports = async function acceptTerms(session) { - const reviewTermsButton = await session.waitAndQuery('.mx_QuestionDialog button.mx_Dialog_primary', 5000); + const reviewTermsButton = await session.waitAndQuery('.mx_QuestionDialog button.mx_Dialog_primary'); const termsPagePromise = session.waitForNewPage(); await reviewTermsButton.click(); const termsPage = await termsPagePromise; diff --git a/src/tests/dialog.js b/src/tests/dialog.js index 8d9c798c45..89c70470d9 100644 --- a/src/tests/dialog.js +++ b/src/tests/dialog.js @@ -27,7 +27,7 @@ async function acceptDialog(session, expectedContent) { async function acceptDialogMaybe(session, expectedContent) { let dialog = null; try { - dialog = await session.waitAndQuery(".mx_QuestionDialog", 100); + dialog = await session.waitAndQuery(".mx_QuestionDialog"); } catch(err) { return false; } @@ -44,4 +44,4 @@ async function acceptDialogMaybe(session, expectedContent) { module.exports = { acceptDialog, acceptDialogMaybe, -}; \ No newline at end of file +}; diff --git a/src/tests/server-notices-consent.js b/src/tests/server-notices-consent.js index d0c91da7b1..25c3bb3bd5 100644 --- a/src/tests/server-notices-consent.js +++ b/src/tests/server-notices-consent.js @@ -19,7 +19,7 @@ const acceptInvite = require("./accept-invite") module.exports = async function acceptServerNoticesInviteAndConsent(session) { await acceptInvite(session, "Server Notices"); session.log.step(`accepts terms & conditions`); - const consentLink = await session.waitAndQuery(".mx_EventTile_body a", 1000); + const consentLink = await session.waitAndQuery(".mx_EventTile_body a"); const termsPagePromise = session.waitForNewPage(); await consentLink.click(); const termsPage = await termsPagePromise; From 6be597505026fdc006ce6e47f9b1795ad4626596 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 23 Aug 2018 10:03:37 +0200 Subject: [PATCH 0087/2372] dont assume new target is a new page --- src/session.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/session.js b/src/session.js index ba0384b91f..bcccc9d6cc 100644 --- a/src/session.js +++ b/src/session.js @@ -164,17 +164,21 @@ module.exports = class RiotSession { waitForNewPage(timeout = 5000) { return new Promise((resolve, reject) => { const timeoutHandle = setTimeout(() => { - this.browser.removeEventListener('targetcreated', callback); + this.browser.removeListener('targetcreated', callback); reject(new Error(`timeout of ${timeout}ms for waitForNewPage elapsed`)); }, timeout); const callback = async (target) => { + if (target.type() !== 'page') { + return; + } + this.browser.removeListener('targetcreated', callback); clearTimeout(timeoutHandle); const page = await target.page(); resolve(page); }; - this.browser.once('targetcreated', callback); + this.browser.on('targetcreated', callback); }); } From a65d6af8c5ae72ba691756e88adb0487cfed2e72 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 23 Aug 2018 10:04:06 +0200 Subject: [PATCH 0088/2372] encryption dialogs dont always appear coming back from settings... weird --- src/scenario.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scenario.js b/src/scenario.js index d859b74781..394e89f7cd 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -15,7 +15,7 @@ limitations under the License. */ -const {acceptDialog} = require('./tests/dialog'); +const {acceptDialogMaybe} = require('./tests/dialog'); const signup = require('./tests/signup'); const join = require('./tests/join'); const sendMessage = require('./tests/send-message'); @@ -68,12 +68,12 @@ async function createE2ERoomAndTalk(alice, bob) { // wait some time for the encryption warning dialog // to appear after closing the settings await bob.delay(1000); - await acceptDialog(bob, "encryption"); + await acceptDialogMaybe(bob, "encryption"); const aliceDevice = await getE2EDeviceFromSettings(alice); // wait some time for the encryption warning dialog // to appear after closing the settings await alice.delay(1000); - await acceptDialog(alice, "encryption"); + await acceptDialogMaybe(alice, "encryption"); await verifyDeviceForUser(bob, "alice", aliceDevice); await verifyDeviceForUser(alice, "bob", bobDevice); const aliceMessage = "Guess what I just heard?!" From 2321e43fd8c9622efbe539efaace4089021474fc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 23 Aug 2018 10:04:37 +0200 Subject: [PATCH 0089/2372] commander inverts the meaning of program args by itself ... nice, I guess --- start.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.js b/start.js index ac9a2f8684..f3eac32f9f 100644 --- a/start.js +++ b/start.js @@ -52,7 +52,7 @@ async function runTests() { } catch(err) { failure = true; console.log('failure: ', err); - if (!program.noLogs) { + if (program.logs) { for(let i = 0; i < sessions.length; ++i) { const session = sessions[i]; documentHtml = await session.page.content(); From 98aafd6abbe63748886e198bca0ed044d8bb86de Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 11 Sep 2018 14:37:36 +0200 Subject: [PATCH 0090/2372] add rest/non-browser session, which we can create a lot more off --- package.json | 6 +++- src/rest/consent.js | 30 ++++++++++++++++ src/rest/factory.js | 77 +++++++++++++++++++++++++++++++++++++++ src/rest/room.js | 42 ++++++++++++++++++++++ src/rest/session.js | 88 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 src/rest/consent.js create mode 100644 src/rest/factory.js create mode 100644 src/rest/room.js create mode 100644 src/rest/session.js diff --git a/package.json b/package.json index 8035fbb508..f3c47ac491 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,11 @@ "author": "", "license": "ISC", "dependencies": { + "cheerio": "^1.0.0-rc.2", "commander": "^2.17.1", - "puppeteer": "^1.6.0" + "puppeteer": "^1.6.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.5", + "uuid": "^3.3.2" } } diff --git a/src/rest/consent.js b/src/rest/consent.js new file mode 100644 index 0000000000..1e36f541a3 --- /dev/null +++ b/src/rest/consent.js @@ -0,0 +1,30 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const request = require('request-promise-native'); +const cheerio = require('cheerio'); +const url = require("url"); + +module.exports.approveConsent = async function(consentUrl) { + const body = await request.get(consentUrl); + const doc = cheerio.load(body); + const v = doc("input[name=v]").val(); + const u = doc("input[name=u]").val(); + const h = doc("input[name=h]").val(); + const formAction = doc("form").attr("action"); + const absAction = url.resolve(consentUrl, formAction); + await request.post(absAction).form({v, u, h}); +}; diff --git a/src/rest/factory.js b/src/rest/factory.js new file mode 100644 index 0000000000..2df6624ef9 --- /dev/null +++ b/src/rest/factory.js @@ -0,0 +1,77 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const util = require('util'); +const exec = util.promisify(require('child_process').exec); +const request = require('request-promise-native'); +const RestSession = require('./session'); + +module.exports = class RestSessionFactory { + constructor(synapseSubdir, hsUrl, cwd) { + this.synapseSubdir = synapseSubdir; + this.hsUrl = hsUrl; + this.cwd = cwd; + } + + async createSession(username, password) { + await this._register(username, password); + const authResult = await this._authenticate(username, password); + return new RestSession(authResult); + } + + _register(username, password) { + const registerArgs = [ + '-c homeserver.yaml', + `-u ${username}`, + `-p ${password}`, + // '--regular-user', + '-a', //until PR gets merged + this.hsUrl + ]; + const registerCmd = `./scripts/register_new_matrix_user ${registerArgs.join(' ')}`; + const allCmds = [ + `cd ${this.synapseSubdir}`, + "source env/bin/activate", + registerCmd + ].join(';'); + + return exec(allCmds, {cwd: this.cwd, encoding: 'utf-8'}).catch((result) => { + const lines = result.stdout.trim().split('\n'); + const failureReason = lines[lines.length - 1]; + throw new Error(`creating user ${username} failed: ${failureReason}`); + }); + } + + async _authenticate(username, password) { + const requestBody = { + "type": "m.login.password", + "identifier": { + "type": "m.id.user", + "user": username + }, + "password": password + }; + const url = `${this.hsUrl}/_matrix/client/r0/login`; + const responseBody = await request.post({url, json: true, body: requestBody}); + return { + accessToken: responseBody.access_token, + homeServer: responseBody.home_server, + userId: responseBody.user_id, + deviceId: responseBody.device_id, + hsUrl: this.hsUrl, + }; + } +} diff --git a/src/rest/room.js b/src/rest/room.js new file mode 100644 index 0000000000..b7da1789ff --- /dev/null +++ b/src/rest/room.js @@ -0,0 +1,42 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const uuidv4 = require('uuid/v4'); + +/* no pun intented */ +module.exports = class RestRoom { + constructor(session, roomId) { + this.session = session; + this.roomId = roomId; + } + + async talk(message) { + const txId = uuidv4(); + await this.session._put(`/rooms/${this.roomId}/send/m.room.message/${txId}`, { + "msgtype": "m.text", + "body": message + }); + return txId; + } + + async leave() { + await this.session._post(`/rooms/${this.roomId}/leave`); + } + + id() { + return this.roomId; + } +} diff --git a/src/rest/session.js b/src/rest/session.js new file mode 100644 index 0000000000..f57d0467f5 --- /dev/null +++ b/src/rest/session.js @@ -0,0 +1,88 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const request = require('request-promise-native'); +const RestRoom = require('./room'); +const {approveConsent} = require('./consent'); + +module.exports = class RestSession { + constructor(credentials) { + this.credentials = credentials; + } + + _post(csApiPath, body) { + return this._request("POST", csApiPath, body); + } + + _put(csApiPath, body) { + return this._request("PUT", csApiPath, body); + } + + async _request(method, csApiPath, body) { + try { + const responseBody = await request({ + url: `${this.credentials.hsUrl}/_matrix/client/r0${csApiPath}`, + method, + headers: { + "Authorization": `Bearer ${this.credentials.accessToken}` + }, + json: true, + body + }); + return responseBody; + + } catch(err) { + const responseBody = err.response.body; + if (responseBody.errcode === 'M_CONSENT_NOT_GIVEN') { + await approveConsent(responseBody.consent_uri); + return this._request(method, csApiPath, body); + } else if(responseBody && responseBody.error) { + throw new Error(`${method} ${csApiPath}: ${responseBody.error}`); + } else { + throw new Error(`${method} ${csApiPath}: ${err.response.statusCode}`); + } + } + } + + async join(roomId) { + const {room_id} = await this._post(`/rooms/${roomId}/join`); + return new RestRoom(this, room_id); + } + + + async createRoom(name, options) { + const body = { + name, + }; + if (options.invite) { + body.invite = options.invite; + } + if (options.public) { + body.visibility = "public"; + } else { + body.visibility = "private"; + } + if (options.dm) { + body.is_direct = true; + } + if (options.topic) { + body.topic = options.topic; + } + + const {room_id} = await this._post(`/createRoom`, body); + return new RestRoom(this, room_id); + } +} From afc678fea066e8d5e75461bb078c0dacb2401eee Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 11 Sep 2018 14:46:25 +0200 Subject: [PATCH 0091/2372] pass rest session creator to scenario --- src/scenario.js | 2 +- start.js | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/scenario.js b/src/scenario.js index f7229057a8..f774a8649d 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -28,7 +28,7 @@ const acceptServerNoticesInviteAndConsent = require('./tests/server-notices-cons const getE2EDeviceFromSettings = require('./tests/e2e-device'); const verifyDeviceForUser = require("./tests/verify-device"); -module.exports = async function scenario(createSession) { +module.exports = async function scenario(createSession, createRestSession) { async function createUser(username) { const session = await createSession(username); await signup(session, session.username, 'testtest'); diff --git a/start.js b/start.js index ac9a2f8684..c21fcd0a2d 100644 --- a/start.js +++ b/start.js @@ -17,6 +17,7 @@ limitations under the License. const assert = require('assert'); const RiotSession = require('./src/session'); const scenario = require('./src/scenario'); +const RestSessionFactory = require('./src/rest/factory'); const program = require('commander'); program @@ -40,6 +41,16 @@ async function runTests() { options.executablePath = path; } + const restFactory = new RestSessionFactory( + 'synapse/installations/consent', + 'http://localhost:5005', + __dirname + ); + + function createRestSession(username, password) { + return restFactory.createSession(username, password); + } + async function createSession(username) { const session = await RiotSession.create(username, options, program.riotUrl); sessions.push(session); @@ -48,7 +59,7 @@ async function runTests() { let failure = false; try { - await scenario(createSession); + await scenario(createSession, createRestSession); } catch(err) { failure = true; console.log('failure: ', err); From 3c5e73d64414e69a46cf1624c4cb91254570a2e5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 11 Sep 2018 14:54:14 +0200 Subject: [PATCH 0092/2372] support setting the display name on rest session --- src/rest/session.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/rest/session.js b/src/rest/session.js index f57d0467f5..caa10a430b 100644 --- a/src/rest/session.js +++ b/src/rest/session.js @@ -57,12 +57,17 @@ module.exports = class RestSession { } } + async setDisplayName(displayName) { + await this._put(`/profile/${this.credentials.userId}/displayname`, { + displayname: displayName + }); + } + async join(roomId) { const {room_id} = await this._post(`/rooms/${roomId}/join`); return new RestRoom(this, room_id); } - async createRoom(name, options) { const body = { name, From 48d95c228a2e4f483c4602f31fb12d55e7e0da2c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 11 Sep 2018 15:02:02 +0200 Subject: [PATCH 0093/2372] creator instead of factory, as it does registration and authentication --- src/rest/{factory.js => creator.js} | 2 +- start.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/rest/{factory.js => creator.js} (98%) diff --git a/src/rest/factory.js b/src/rest/creator.js similarity index 98% rename from src/rest/factory.js rename to src/rest/creator.js index 2df6624ef9..05976552d0 100644 --- a/src/rest/factory.js +++ b/src/rest/creator.js @@ -19,7 +19,7 @@ const exec = util.promisify(require('child_process').exec); const request = require('request-promise-native'); const RestSession = require('./session'); -module.exports = class RestSessionFactory { +module.exports = class RestSessionCreator { constructor(synapseSubdir, hsUrl, cwd) { this.synapseSubdir = synapseSubdir; this.hsUrl = hsUrl; diff --git a/start.js b/start.js index c21fcd0a2d..e39681f71b 100644 --- a/start.js +++ b/start.js @@ -17,7 +17,7 @@ limitations under the License. const assert = require('assert'); const RiotSession = require('./src/session'); const scenario = require('./src/scenario'); -const RestSessionFactory = require('./src/rest/factory'); +const RestSessionCreator = require('./src/rest/creator'); const program = require('commander'); program @@ -41,14 +41,14 @@ async function runTests() { options.executablePath = path; } - const restFactory = new RestSessionFactory( + const restCreator = new RestSessionCreator( 'synapse/installations/consent', 'http://localhost:5005', __dirname ); function createRestSession(username, password) { - return restFactory.createSession(username, password); + return restCreator.createSession(username, password); } async function createSession(username) { From 827e6365bbe4779c89fe9a1ea87856a9f76cb4c3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 11 Sep 2018 16:18:27 +0200 Subject: [PATCH 0094/2372] add wrapper around multiple rest sessions --- src/rest/creator.js | 7 +++++ src/rest/multi.js | 61 ++++++++++++++++++++++++++++++++++++++++ src/rest/room.js | 10 +++---- src/rest/session.js | 68 +++++++++++++++++++++++++-------------------- 4 files changed, 111 insertions(+), 35 deletions(-) create mode 100644 src/rest/multi.js diff --git a/src/rest/creator.js b/src/rest/creator.js index 05976552d0..9090a21e70 100644 --- a/src/rest/creator.js +++ b/src/rest/creator.js @@ -18,6 +18,7 @@ const util = require('util'); const exec = util.promisify(require('child_process').exec); const request = require('request-promise-native'); const RestSession = require('./session'); +const RestMultiSession = require('./multi'); module.exports = class RestSessionCreator { constructor(synapseSubdir, hsUrl, cwd) { @@ -26,6 +27,12 @@ module.exports = class RestSessionCreator { this.cwd = cwd; } + async createSessionRange(usernames, password) { + const sessionPromises = usernames.map((username) => this.createSession(username, password)); + const sessions = await Promise.all(sessionPromises); + return new RestMultiSession(sessions); + } + async createSession(username, password) { await this._register(username, password); const authResult = await this._authenticate(username, password); diff --git a/src/rest/multi.js b/src/rest/multi.js new file mode 100644 index 0000000000..12ebe9d4ab --- /dev/null +++ b/src/rest/multi.js @@ -0,0 +1,61 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const request = require('request-promise-native'); +const RestRoom = require('./room'); +const {approveConsent} = require('./consent'); + +module.exports = class RestMultiSession { + constructor(sessions) { + this.sessions = sessions; + } + + slice(start, end) { + return new RestMultiSession(this.sessions.slice(start, end)); + } + + pop(userName) { + const idx = this.sessions.findIndex((s) => s.userName() === userName); + if(idx === -1) { + throw new Error(`user ${userName} not found`); + } + const session = this.sessions.splice(idx, 1)[0]; + return session; + } + + async setDisplayName(fn) { + await Promise.all(this.sessions.map((s) => s.setDisplayName(fn(s)))); + } + + async join(roomId) { + const rooms = await Promise.all(this.sessions.map((s) => s.join(roomId))); + return new RestMultiRoom(rooms); + } +} + +class RestMultiRoom { + constructor(rooms) { + this.rooms = rooms; + } + + async talk(message) { + await Promise.all(this.rooms.map((r) => r.talk(message))); + } + + async leave() { + await Promise.all(this.rooms.map((r) => r.leave())); + } +} diff --git a/src/rest/room.js b/src/rest/room.js index b7da1789ff..d8de958a27 100644 --- a/src/rest/room.js +++ b/src/rest/room.js @@ -20,12 +20,12 @@ const uuidv4 = require('uuid/v4'); module.exports = class RestRoom { constructor(session, roomId) { this.session = session; - this.roomId = roomId; + this._roomId = roomId; } async talk(message) { const txId = uuidv4(); - await this.session._put(`/rooms/${this.roomId}/send/m.room.message/${txId}`, { + await this.session._put(`/rooms/${this._roomId}/send/m.room.message/${txId}`, { "msgtype": "m.text", "body": message }); @@ -33,10 +33,10 @@ module.exports = class RestRoom { } async leave() { - await this.session._post(`/rooms/${this.roomId}/leave`); + await this.session._post(`/rooms/${this._roomId}/leave`); } - id() { - return this.roomId; + roomId() { + return this._roomId; } } diff --git a/src/rest/session.js b/src/rest/session.js index caa10a430b..44be15c3ff 100644 --- a/src/rest/session.js +++ b/src/rest/session.js @@ -23,38 +23,12 @@ module.exports = class RestSession { this.credentials = credentials; } - _post(csApiPath, body) { - return this._request("POST", csApiPath, body); + userId() { + return this.credentials.userId; } - _put(csApiPath, body) { - return this._request("PUT", csApiPath, body); - } - - async _request(method, csApiPath, body) { - try { - const responseBody = await request({ - url: `${this.credentials.hsUrl}/_matrix/client/r0${csApiPath}`, - method, - headers: { - "Authorization": `Bearer ${this.credentials.accessToken}` - }, - json: true, - body - }); - return responseBody; - - } catch(err) { - const responseBody = err.response.body; - if (responseBody.errcode === 'M_CONSENT_NOT_GIVEN') { - await approveConsent(responseBody.consent_uri); - return this._request(method, csApiPath, body); - } else if(responseBody && responseBody.error) { - throw new Error(`${method} ${csApiPath}: ${responseBody.error}`); - } else { - throw new Error(`${method} ${csApiPath}: ${err.response.statusCode}`); - } - } + userName() { + return this.credentials.userId.split(":")[0].substr(1); } async setDisplayName(displayName) { @@ -90,4 +64,38 @@ module.exports = class RestSession { const {room_id} = await this._post(`/createRoom`, body); return new RestRoom(this, room_id); } + + _post(csApiPath, body) { + return this._request("POST", csApiPath, body); + } + + _put(csApiPath, body) { + return this._request("PUT", csApiPath, body); + } + + async _request(method, csApiPath, body) { + try { + const responseBody = await request({ + url: `${this.credentials.hsUrl}/_matrix/client/r0${csApiPath}`, + method, + headers: { + "Authorization": `Bearer ${this.credentials.accessToken}` + }, + json: true, + body + }); + return responseBody; + + } catch(err) { + const responseBody = err.response.body; + if (responseBody.errcode === 'M_CONSENT_NOT_GIVEN') { + await approveConsent(responseBody.consent_uri); + return this._request(method, csApiPath, body); + } else if(responseBody && responseBody.error) { + throw new Error(`${method} ${csApiPath}: ${responseBody.error}`); + } else { + throw new Error(`${method} ${csApiPath}: ${err.response.statusCode}`); + } + } + } } From 4a4b1f65aa2d418d8c616b2501b25ceb49f74a5e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 11 Sep 2018 18:28:50 +0200 Subject: [PATCH 0095/2372] wait for the message to be sent --- src/scenario.js | 9 +++++++-- src/session.js | 23 ++++++++++++++++++++--- src/tests/e2e-device.js | 31 ------------------------------- src/tests/send-message.js | 4 +++- start.js | 6 ++++-- synapse/install.sh | 4 +++- 6 files changed, 37 insertions(+), 40 deletions(-) delete mode 100644 src/tests/e2e-device.js diff --git a/src/scenario.js b/src/scenario.js index b468cad823..3145c9471a 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -25,13 +25,13 @@ const receiveMessage = require('./tests/receive-message'); const createRoom = require('./tests/create-room'); const changeRoomSettings = require('./tests/room-settings'); const acceptServerNoticesInviteAndConsent = require('./tests/server-notices-consent'); -const getE2EDeviceFromSettings = require('./tests/e2e-device'); +const {enableLazyLoading, getE2EDeviceFromSettings} = require('./tests/settings'); const verifyDeviceForUser = require("./tests/verify-device"); module.exports = async function scenario(createSession, createRestSession) { async function createUser(username) { const session = await createSession(username); - await signup(session, session.username, 'testtest'); + await signup(session, session.username, 'testtest', session.hsUrl); await acceptServerNoticesInviteAndConsent(session); return session; } @@ -83,3 +83,8 @@ async function createE2ERoomAndTalk(alice, bob) { await sendMessage(bob, bobMessage); await receiveMessage(alice, {sender: "bob", body: bobMessage, encrypted: true}); } + +async function aLLtest(alice, bob) { + await enableLazyLoading(alice); + +} diff --git a/src/session.js b/src/session.js index bcccc9d6cc..60cbfa9099 100644 --- a/src/session.js +++ b/src/session.js @@ -58,9 +58,10 @@ class Logger { } module.exports = class RiotSession { - constructor(browser, page, username, riotserver) { + constructor(browser, page, username, riotserver, hsUrl) { this.browser = browser; this.page = page; + this.hsUrl = hsUrl; this.riotserver = riotserver; this.username = username; this.consoleLog = new LogBuffer(page, "console", (msg) => `${msg.text()}\n`); @@ -72,14 +73,14 @@ module.exports = class RiotSession { this.log = new Logger(this.username); } - static async create(username, puppeteerOptions, riotserver) { + static async create(username, puppeteerOptions, riotserver, hsUrl) { const browser = await puppeteer.launch(puppeteerOptions); const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); - return new RiotSession(browser, page, username, riotserver); + return new RiotSession(browser, page, username, riotserver, hsUrl); } async tryGetInnertext(selector) { @@ -161,6 +162,22 @@ module.exports = class RiotSession { return await this.queryAll(selector); } + waitForReload(timeout = 5000) { + return new Promise((resolve, reject) => { + const timeoutHandle = setTimeout(() => { + this.browser.removeEventListener('domcontentloaded', callback); + reject(new Error(`timeout of ${timeout}ms for waitForReload elapsed`)); + }, timeout); + + const callback = async () => { + clearTimeout(timeoutHandle); + resolve(); + }; + + this.page.once('domcontentloaded', callback); + }); + } + waitForNewPage(timeout = 5000) { return new Promise((resolve, reject) => { const timeoutHandle = setTimeout(() => { diff --git a/src/tests/e2e-device.js b/src/tests/e2e-device.js deleted file mode 100644 index 4be6677396..0000000000 --- a/src/tests/e2e-device.js +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -const assert = require('assert'); - -module.exports = async function getE2EDeviceFromSettings(session) { - session.log.step(`gets e2e device/key from settings`); - const settingsButton = await session.query('.mx_BottomLeftMenu_settings'); - await settingsButton.click(); - const deviceAndKey = await session.waitAndQueryAll(".mx_UserSettings_section.mx_UserSettings_cryptoSection code", 1000); - assert.equal(deviceAndKey.length, 2); - const id = await (await deviceAndKey[0].getProperty("innerText")).jsonValue(); - const key = await (await deviceAndKey[1].getProperty("innerText")).jsonValue(); - const closeButton = await session.query(".mx_RoomHeader_cancelButton"); - await closeButton.click(); - session.log.done(); - return {id, key}; -} diff --git a/src/tests/send-message.js b/src/tests/send-message.js index eb70f5ce23..5bf289b03a 100644 --- a/src/tests/send-message.js +++ b/src/tests/send-message.js @@ -25,5 +25,7 @@ module.exports = async function sendMessage(session, message) { const text = await session.innerText(composer); assert.equal(text.trim(), message.trim()); await composer.press("Enter"); + // wait for the message to appear sent + await session.waitAndQuery(".mx_EventTile_last:not(.mx_EventTile_sending)"); session.log.done(); -} \ No newline at end of file +} diff --git a/start.js b/start.js index 5f6681519b..62ec29d6a1 100644 --- a/start.js +++ b/start.js @@ -26,6 +26,8 @@ program .option('--riot-url [url]', "riot url to test", "http://localhost:5000") .parse(process.argv); +const hsUrl = 'http://localhost:5005'; + async function runTests() { let sessions = []; console.log("running tests ..."); @@ -43,7 +45,7 @@ async function runTests() { const restCreator = new RestSessionCreator( 'synapse/installations/consent', - 'http://localhost:5005', + hsUrl, __dirname ); @@ -52,7 +54,7 @@ async function runTests() { } async function createSession(username) { - const session = await RiotSession.create(username, options, program.riotUrl); + const session = await RiotSession.create(username, options, program.riotUrl, hsUrl); sessions.push(session); return session; } diff --git a/synapse/install.sh b/synapse/install.sh index 3d8172a9f6..a438ea5dc2 100755 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -31,9 +31,11 @@ python -m synapse.app.homeserver \ --generate-config \ --report-stats=no # apply configuration +REGISTRATION_SHARED_SECRET=$(uuidgen) cp -r $BASE_DIR/config-templates/$CONFIG_TEMPLATE/. ./ sed -i "s#{{SYNAPSE_ROOT}}#$(pwd)/#g" homeserver.yaml sed -i "s#{{SYNAPSE_PORT}}#${PORT}#g" homeserver.yaml sed -i "s#{{FORM_SECRET}}#$(uuidgen)#g" homeserver.yaml -sed -i "s#{{REGISTRATION_SHARED_SECRET}}#$(uuidgen)#g" homeserver.yaml +sed -i "s#{{REGISTRATION_SHARED_SECRET}}#${REGISTRATION_SHARED_SECRET}#g" homeserver.yaml sed -i "s#{{MACAROON_SECRET_KEY}}#$(uuidgen)#g" homeserver.yaml +echo REGISTRATION_SHARED_SECRET=$REGISTRATION_SHARED_SECRET= From be4c1cb899e7f9e50e29d3a54aa6f569ce34447e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 11 Sep 2018 18:29:05 +0200 Subject: [PATCH 0096/2372] support setting the room alias --- src/tests/room-settings.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/tests/room-settings.js b/src/tests/room-settings.js index 9f802da711..95c7538431 100644 --- a/src/tests/room-settings.js +++ b/src/tests/room-settings.js @@ -76,6 +76,13 @@ module.exports = async function changeRoomSettings(session, settings) { session.log.done(); } + if (settings.alias) { + session.log.step(`sets alias to ${settings.alias}`); + const aliasField = await session.waitAndQuery(".mx_RoomSettings .mx_EditableItemList .mx_EditableItem_editable"); + await session.replaceInputText(aliasField, settings.alias); + session.log.done(); + } + const saveButton = await session.query(".mx_RoomHeader_wrapper .mx_RoomHeader_textButton"); await saveButton.click(); From 2be413ba6d025813e18ee1f6766ae710b8147439 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 11 Sep 2018 18:29:52 +0200 Subject: [PATCH 0097/2372] allow clients to send messages faster, in order to speed up the test --- synapse/config-templates/consent/homeserver.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/config-templates/consent/homeserver.yaml b/synapse/config-templates/consent/homeserver.yaml index 38aa4747b5..9fa16ebe5f 100644 --- a/synapse/config-templates/consent/homeserver.yaml +++ b/synapse/config-templates/consent/homeserver.yaml @@ -207,10 +207,10 @@ log_config: "{{SYNAPSE_ROOT}}localhost.log.config" ## Ratelimiting ## # Number of messages a client can send per second -rc_messages_per_second: 0.2 +rc_messages_per_second: 100 # Number of message a client can send before being throttled -rc_message_burst_count: 10.0 +rc_message_burst_count: 20.0 # The federation window size in milliseconds federation_rc_window_size: 1000 From ff20bc783dc77e61ad21d024fe72c09be6ae2323 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 11 Sep 2018 18:30:17 +0200 Subject: [PATCH 0098/2372] support joining with a room alias for rest session --- src/rest/session.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rest/session.js b/src/rest/session.js index 44be15c3ff..846e83cc84 100644 --- a/src/rest/session.js +++ b/src/rest/session.js @@ -37,8 +37,8 @@ module.exports = class RestSession { }); } - async join(roomId) { - const {room_id} = await this._post(`/rooms/${roomId}/join`); + async join(roomIdOrAlias) { + const {room_id} = await this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`); return new RestRoom(this, room_id); } From abc7c4c3acb334d6b300a1baa10f7338e0c3e89c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 11 Sep 2018 18:30:57 +0200 Subject: [PATCH 0099/2372] join use cases that touch settings in one file, as selectors are similar --- src/tests/settings.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/tests/settings.js diff --git a/src/tests/settings.js b/src/tests/settings.js new file mode 100644 index 0000000000..5649671e7a --- /dev/null +++ b/src/tests/settings.js @@ -0,0 +1,43 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); + +module.exports.enableLazyLoading = async function(session) { + session.log.step(`enables lazy loading of members in the lab settings`); + const settingsButton = await session.query('.mx_BottomLeftMenu_settings'); + await settingsButton.click(); + const llCheckbox = await session.waitAndQuery("#feature_lazyloading"); + await llCheckbox.click(); + await session.waitForReload(); + const closeButton = await session.waitAndQuery(".mx_RoomHeader_cancelButton"); + await closeButton.click(); + session.log.done(); +} + +module.exports.getE2EDeviceFromSettings = async function(session) { + session.log.step(`gets e2e device/key from settings`); + const settingsButton = await session.query('.mx_BottomLeftMenu_settings'); + await settingsButton.click(); + const deviceAndKey = await session.waitAndQueryAll(".mx_UserSettings_section.mx_UserSettings_cryptoSection code"); + assert.equal(deviceAndKey.length, 2); + const id = await (await deviceAndKey[0].getProperty("innerText")).jsonValue(); + const key = await (await deviceAndKey[1].getProperty("innerText")).jsonValue(); + const closeButton = await session.query(".mx_RoomHeader_cancelButton"); + await closeButton.click(); + session.log.done(); + return {id, key}; +} From 3db32c93d427ae082cf625d17d02109647f5e946 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 11 Sep 2018 18:32:18 +0200 Subject: [PATCH 0100/2372] past rest creator to scenario to also be able to call createSessionRange --- start.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/start.js b/start.js index 62ec29d6a1..3367787905 100644 --- a/start.js +++ b/start.js @@ -49,10 +49,6 @@ async function runTests() { __dirname ); - function createRestSession(username, password) { - return restCreator.createSession(username, password); - } - async function createSession(username) { const session = await RiotSession.create(username, options, program.riotUrl, hsUrl); sessions.push(session); @@ -61,7 +57,7 @@ async function runTests() { let failure = false; try { - await scenario(createSession, createRestSession); + await scenario(createSession, restCreator); } catch(err) { failure = true; console.log('failure: ', err); From dcf96e146167db46fe348ebe5c13eb9762479d45 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 11 Sep 2018 18:32:32 +0200 Subject: [PATCH 0101/2372] WIP for LL test --- src/scenario.js | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/scenario.js b/src/scenario.js index 3145c9471a..24983d8cbf 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -28,7 +28,7 @@ const acceptServerNoticesInviteAndConsent = require('./tests/server-notices-cons const {enableLazyLoading, getE2EDeviceFromSettings} = require('./tests/settings'); const verifyDeviceForUser = require("./tests/verify-device"); -module.exports = async function scenario(createSession, createRestSession) { +module.exports = async function scenario(createSession, restCreator) { async function createUser(username) { const session = await createSession(username); await signup(session, session.username, 'testtest', session.hsUrl); @@ -38,9 +38,26 @@ module.exports = async function scenario(createSession, createRestSession) { const alice = await createUser("alice"); const bob = await createUser("bob"); + const charlies = await createRestUsers(restCreator); - await createDirectoryRoomAndTalk(alice, bob); - await createE2ERoomAndTalk(alice, bob); + // await createDirectoryRoomAndTalk(alice, bob); + // await createE2ERoomAndTalk(alice, bob); + await aLazyLoadingTest(alice, bob, charlies); +} + +function range(start, amount, step = 1) { + const r = []; + for (let i = 0; i < amount; ++i) { + r.push(start + (i * step)); + } + return r; +} + +async function createRestUsers(restCreator) { + const usernames = range(1, 10).map((i) => `charly-${i}`); + const charlies = await restCreator.createSessionRange(usernames, 'testtest'); + await charlies.setDisplayName((s) => `Charly #${s.userName().split('-')[1]}`); + return charlies; } async function createDirectoryRoomAndTalk(alice, bob) { @@ -84,7 +101,18 @@ async function createE2ERoomAndTalk(alice, bob) { await receiveMessage(alice, {sender: "bob", body: bobMessage, encrypted: true}); } -async function aLLtest(alice, bob) { +async function aLazyLoadingTest(alice, bob, charlies) { await enableLazyLoading(alice); - + const room = "Lazy Loading Test"; + const alias = "#lltest:localhost"; + await createRoom(bob, room); + await changeRoomSettings(bob, {directory: true, visibility: "public_no_guests", alias}); + // wait for alias to be set by server after clicking "save" + await bob.delay(500); + await charlies.join(alias); + const messageRange = range(1, 20); + for(let i = 20; i >= 1; --i) { + await sendMessage(bob, `I will only say this ${i} time(s)!`); + } + await join(alice, room); } From 244d5b08514f7f2f518274261e40d6352cc5c5a5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Sep 2018 09:47:57 +0200 Subject: [PATCH 0102/2372] dont show all 20 send messages support muting a logger and chaining calls --- src/scenario.js | 3 +++ src/session.js | 31 ++++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/scenario.js b/src/scenario.js index 24983d8cbf..840cd3e0dc 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -111,8 +111,11 @@ async function aLazyLoadingTest(alice, bob, charlies) { await bob.delay(500); await charlies.join(alias); const messageRange = range(1, 20); + bob.log.step("sends 20 messages").mute(); for(let i = 20; i >= 1; --i) { await sendMessage(bob, `I will only say this ${i} time(s)!`); } + bob.log.unmute().done(); await join(alice, room); + } diff --git a/src/session.js b/src/session.js index 60cbfa9099..0bfe781e61 100644 --- a/src/session.js +++ b/src/session.js @@ -35,25 +35,46 @@ class Logger { constructor(username) { this.indent = 0; this.username = username; + this.muted = false; } startGroup(description) { - const indent = " ".repeat(this.indent * 2); - console.log(`${indent} * ${this.username} ${description}:`); + if (!this.muted) { + const indent = " ".repeat(this.indent * 2); + console.log(`${indent} * ${this.username} ${description}:`); + } this.indent += 1; + return this; } endGroup() { this.indent -= 1; + return this; } step(description) { - const indent = " ".repeat(this.indent * 2); - process.stdout.write(`${indent} * ${this.username} ${description} ... `); + if (!this.muted) { + const indent = " ".repeat(this.indent * 2); + process.stdout.write(`${indent} * ${this.username} ${description} ... `); + } + return this; } done(status = "done") { - process.stdout.write(status + "\n"); + if (!this.muted) { + process.stdout.write(status + "\n"); + } + return this; + } + + mute() { + this.muted = true; + return this; + } + + unmute() { + this.muted = false; + return this; } } From 249cf4f87efb4492ca5098b272ed3704f64dbcf2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Sep 2018 14:49:48 +0200 Subject: [PATCH 0103/2372] implement reading and scrolling timeline, group timeline related code --- src/scenario.js | 6 +- src/tests/receive-message.js | 49 -------------- src/tests/timeline.js | 125 +++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 50 deletions(-) delete mode 100644 src/tests/receive-message.js create mode 100644 src/tests/timeline.js diff --git a/src/scenario.js b/src/scenario.js index 840cd3e0dc..70ceae1471 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -21,7 +21,11 @@ const join = require('./tests/join'); const sendMessage = require('./tests/send-message'); const acceptInvite = require('./tests/accept-invite'); const invite = require('./tests/invite'); -const receiveMessage = require('./tests/receive-message'); +const { + receiveMessage, + checkTimelineContains, + scrollToTimelineTop +} = require('./tests/timeline'); const createRoom = require('./tests/create-room'); const changeRoomSettings = require('./tests/room-settings'); const acceptServerNoticesInviteAndConsent = require('./tests/server-notices-consent'); diff --git a/src/tests/receive-message.js b/src/tests/receive-message.js deleted file mode 100644 index afe4247181..0000000000 --- a/src/tests/receive-message.js +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -const assert = require('assert'); - -module.exports = async function receiveMessage(session, message) { - session.log.step(`receives message "${message.body}" from ${message.sender}`); - // wait for a response to come in that contains the message - // crude, but effective - await session.page.waitForResponse(async (response) => { - if (response.request().url().indexOf("/sync") === -1) { - return false; - } - const body = await response.text(); - if (message.encrypted) { - return body.indexOf(message.sender) !== -1 && - body.indexOf("m.room.encrypted") !== -1; - } else { - return body.indexOf(message.body) !== -1; - } - }); - // wait a bit for the incoming event to be rendered - await session.delay(1000); - let lastTile = await session.query(".mx_EventTile_last"); - const senderElement = await lastTile.$(".mx_SenderProfile_name"); - const bodyElement = await lastTile.$(".mx_EventTile_body"); - const sender = await(await senderElement.getProperty("innerText")).jsonValue(); - const body = await(await bodyElement.getProperty("innerText")).jsonValue(); - if (message.encrypted) { - const e2eIcon = await lastTile.$(".mx_EventTile_e2eIcon"); - assert.ok(e2eIcon); - } - assert.equal(body, message.body); - assert.equal(sender, message.sender); - session.log.done(); -} diff --git a/src/tests/timeline.js b/src/tests/timeline.js new file mode 100644 index 0000000000..1e724a2d39 --- /dev/null +++ b/src/tests/timeline.js @@ -0,0 +1,125 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); + +module.exports.scrollToTimelineTop = async function(session) { + session.log.step(`scrolls to the top of the timeline`); + await session.page.evaluate(() => { + return Promise.resolve().then(async () => { + const timelineScrollView = document.querySelector(".mx_RoomView .gm-scroll-view"); + let timedOut = false; + let timeoutHandle = null; + // set scrollTop to 0 in a loop and check every 50ms + // if content became available (scrollTop not being 0 anymore), + // assume everything is loaded after 1000ms + do { + if (timelineScrollView.scrollTop !== 0) { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + timeoutHandle = setTimeout(() => timedOut = true, 1000); + timelineScrollView.scrollTop = 0; + } else { + await new Promise((resolve) => setTimeout(resolve, 50)); + } + } while (!timedOut) + }); + }) + session.log.done(); +} + +module.exports.receiveMessage = async function(session, expectedMessage) { + session.log.step(`receives message "${expectedMessage.body}" from ${expectedMessage.sender}`); + // wait for a response to come in that contains the message + // crude, but effective + await session.page.waitForResponse(async (response) => { + if (response.request().url().indexOf("/sync") === -1) { + return false; + } + const body = await response.text(); + if (expectedMessage.encrypted) { + return body.indexOf(expectedMessage.sender) !== -1 && + body.indexOf("m.room.encrypted") !== -1; + } else { + return body.indexOf(expectedMessage.body) !== -1; + } + }); + // wait a bit for the incoming event to be rendered + await session.delay(1000); + const lastTile = await getLastEventTile(session); + const foundMessage = await getMessageFromEventTile(lastTile); + assertMessage(foundMessage, expectedMessage); + session.log.done(); +} + + +module.exports.checkTimelineContains = async function (session, expectedMessages, sendersDescription) { + session.log.step(`checks timeline contains ${expectedMessages.length} ` + + `given messages${sendersDescription ? ` from ${sendersDescription}`:""}`); + const eventTiles = await getAllEventTiles(session); + let timelineMessages = await Promise.all(eventTiles.map((eventTile) => { + return getMessageFromEventTile(eventTile); + })); + //filter out tiles that were not messages + timelineMessages = timelineMessages .filter((m) => !!m); + expectedMessages.forEach((expectedMessage) => { + const foundMessage = timelineMessages.find((message) => { + return message.sender === expectedMessage.sender && + message.body === expectedMessage.body; + }); + assertMessage(foundMessage, expectedMessage); + }); + + session.log.done(); +} + +function assertMessage(foundMessage, expectedMessage) { + assert(foundMessage, `message ${JSON.stringify(expectedMessage)} not found in timeline`); + assert.equal(foundMessage.body, expectedMessage.body); + assert.equal(foundMessage.sender, expectedMessage.sender); + if (expectedMessage.hasOwnProperty("encrypted")) { + assert.equal(foundMessage.encrypted, expectedMessage.encrypted); + } +} + +function getLastEventTile(session) { + return session.query(".mx_EventTile_last"); +} + +function getAllEventTiles(session) { + return session.queryAll(".mx_RoomView_MessageList > *"); +} + +async function getMessageFromEventTile(eventTile) { + const senderElement = await eventTile.$(".mx_SenderProfile_name"); + const bodyElement = await eventTile.$(".mx_EventTile_body"); + let sender = null; + if (senderElement) { + sender = await(await senderElement.getProperty("innerText")).jsonValue(); + } + if (!bodyElement) { + return null; + } + const body = await(await bodyElement.getProperty("innerText")).jsonValue(); + const e2eIcon = await eventTile.$(".mx_EventTile_e2eIcon"); + + return { + sender, + body, + encrypted: !!e2eIcon + }; +} From 4057ec8a6accf6bdfa87f4c9e62f31b64a4f14fb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Sep 2018 14:51:00 +0200 Subject: [PATCH 0104/2372] store displayName on RestSession to use it in tests --- src/rest/session.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/rest/session.js b/src/rest/session.js index 846e83cc84..ece04f3352 100644 --- a/src/rest/session.js +++ b/src/rest/session.js @@ -20,19 +20,25 @@ const {approveConsent} = require('./consent'); module.exports = class RestSession { constructor(credentials) { - this.credentials = credentials; + this._credentials = credentials; + this._displayName = null; } userId() { - return this.credentials.userId; + return this._credentials.userId; } userName() { - return this.credentials.userId.split(":")[0].substr(1); + return this._credentials.userId.split(":")[0].substr(1); + } + + displayName() { + return this._displayName; } async setDisplayName(displayName) { - await this._put(`/profile/${this.credentials.userId}/displayname`, { + this._displayName = displayName; + await this._put(`/profile/${this._credentials.userId}/displayname`, { displayname: displayName }); } @@ -76,10 +82,10 @@ module.exports = class RestSession { async _request(method, csApiPath, body) { try { const responseBody = await request({ - url: `${this.credentials.hsUrl}/_matrix/client/r0${csApiPath}`, + url: `${this._credentials.hsUrl}/_matrix/client/r0${csApiPath}`, method, headers: { - "Authorization": `Bearer ${this.credentials.accessToken}` + "Authorization": `Bearer ${this._credentials.accessToken}` }, json: true, body From 29aec256df052b758a0883a397997825ed5114ed Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Sep 2018 14:53:19 +0200 Subject: [PATCH 0105/2372] finish basic LL test to see if display names appear from lazy loaded state --- src/scenario.js | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/scenario.js b/src/scenario.js index 70ceae1471..946a4122ef 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -44,8 +44,8 @@ module.exports = async function scenario(createSession, restCreator) { const bob = await createUser("bob"); const charlies = await createRestUsers(restCreator); - // await createDirectoryRoomAndTalk(alice, bob); - // await createE2ERoomAndTalk(alice, bob); + await createDirectoryRoomAndTalk(alice, bob); + await createE2ERoomAndTalk(alice, bob); await aLazyLoadingTest(alice, bob, charlies); } @@ -106,20 +106,36 @@ async function createE2ERoomAndTalk(alice, bob) { } async function aLazyLoadingTest(alice, bob, charlies) { + console.log(" creating a room for lazy loading member scenarios:"); await enableLazyLoading(alice); const room = "Lazy Loading Test"; const alias = "#lltest:localhost"; + const charlyMsg1 = "hi bob!"; + const charlyMsg2 = "how's it going??"; await createRoom(bob, room); await changeRoomSettings(bob, {directory: true, visibility: "public_no_guests", alias}); // wait for alias to be set by server after clicking "save" + // so the charlies can join it. await bob.delay(500); - await charlies.join(alias); - const messageRange = range(1, 20); + const charlyMembers = await charlies.join(alias); + await charlyMembers.talk(charlyMsg1); + await charlyMembers.talk(charlyMsg2); bob.log.step("sends 20 messages").mute(); for(let i = 20; i >= 1; --i) { await sendMessage(bob, `I will only say this ${i} time(s)!`); } bob.log.unmute().done(); - await join(alice, room); - + await join(alice, alias); + await scrollToTimelineTop(alice); + //alice should see 2 messages from every charly with + //the correct display name + const expectedMessages = [charlyMsg1, charlyMsg2].reduce((messages, msgText) => { + return charlies.sessions.reduce((messages, charly) => { + return messages.concat({ + sender: charly.displayName(), + body: msgText, + }); + }, messages); + }, []); + await checkTimelineContains(alice, expectedMessages, "Charly #1-10"); } From 7bcb255a2c38dcb220d67a85303d84b3fa5a503b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Sep 2018 16:47:24 +0200 Subject: [PATCH 0106/2372] increase timeout here in case this wouldnt be enough for the CI server --- src/tests/timeline.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/timeline.js b/src/tests/timeline.js index 1e724a2d39..beaaec5e5a 100644 --- a/src/tests/timeline.js +++ b/src/tests/timeline.js @@ -25,13 +25,13 @@ module.exports.scrollToTimelineTop = async function(session) { let timeoutHandle = null; // set scrollTop to 0 in a loop and check every 50ms // if content became available (scrollTop not being 0 anymore), - // assume everything is loaded after 1000ms + // assume everything is loaded after 3s do { if (timelineScrollView.scrollTop !== 0) { if (timeoutHandle) { clearTimeout(timeoutHandle); } - timeoutHandle = setTimeout(() => timedOut = true, 1000); + timeoutHandle = setTimeout(() => timedOut = true, 3000); timelineScrollView.scrollTop = 0; } else { await new Promise((resolve) => setTimeout(resolve, 50)); From e843d532ebe38dff7b6b934226c4f355f4689c76 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Sep 2018 16:48:40 +0200 Subject: [PATCH 0107/2372] these changes were not needed in the end --- synapse/install.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/synapse/install.sh b/synapse/install.sh index a438ea5dc2..3d8172a9f6 100755 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -31,11 +31,9 @@ python -m synapse.app.homeserver \ --generate-config \ --report-stats=no # apply configuration -REGISTRATION_SHARED_SECRET=$(uuidgen) cp -r $BASE_DIR/config-templates/$CONFIG_TEMPLATE/. ./ sed -i "s#{{SYNAPSE_ROOT}}#$(pwd)/#g" homeserver.yaml sed -i "s#{{SYNAPSE_PORT}}#${PORT}#g" homeserver.yaml sed -i "s#{{FORM_SECRET}}#$(uuidgen)#g" homeserver.yaml -sed -i "s#{{REGISTRATION_SHARED_SECRET}}#${REGISTRATION_SHARED_SECRET}#g" homeserver.yaml +sed -i "s#{{REGISTRATION_SHARED_SECRET}}#$(uuidgen)#g" homeserver.yaml sed -i "s#{{MACAROON_SECRET_KEY}}#$(uuidgen)#g" homeserver.yaml -echo REGISTRATION_SHARED_SECRET=$REGISTRATION_SHARED_SECRET= From c8fec947e47930f249b0fe31a3119328dbb8bec9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Sep 2018 17:27:51 +0200 Subject: [PATCH 0108/2372] structure flags better and document them --- README.md | 56 ++++++++++++++++++++++--------------------------------- start.js | 15 ++++++++------- 2 files changed, 30 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index b1a4e40aac..acfeed8257 100644 --- a/README.md +++ b/README.md @@ -2,48 +2,36 @@ This repository contains tests for the matrix-react-sdk web app. The tests fire up a headless chrome and simulate user interaction (end-to-end). Note that end-to-end has little to do with the end-to-end encryption matrix supports, just that we test the full stack, going from user interaction to expected DOM in the browser. -## Current tests - - test riot loads (check title) - - signup with custom homeserver - - join preexisting room - -## Roadmap -- get rid of jest, as a test framework won't be helpful to have a continuous flow going from one use case to another (think: do login, create a room, invite a user, ...). a test framework usually assumes the tests are semi-indepedent. -- better error reporting (show console.log, XHR requests, partial DOM, screenshot) on error -- cleanup helper methods -- add more css id's/classes to riot web to make css selectors in test less brittle. -- avoid delay when waiting for location.hash to change -- more tests! -- setup installing & running riot and synapse as part of the tests. - - Run 2 synapse instances to test federation use cases. - - start synapse with clean config/database on every test run -- look into CI(Travis) integration -- create interactive mode, where window is opened, and browser is kept open until Ctrl^C, for easy test debugging. - -## It's broken! How do I see what's happening in the browser? - -Look for this line: -``` -puppeteer.launch(); -``` -Now change it to: -``` -puppeteer.launch({headless: false}); -``` - -## How to run - -### Setup +## Setup Run `./install.sh`. This will: - install synapse, fetches the master branch at the moment. If anything fails here, please refer to the synapse README to see if you're missing one of the prerequisites. - install riot, this fetches the master branch at the moment. - install dependencies (will download copy of chrome) -### Run tests - +## Running the tests + Run tests with `./run.sh`. +### Debug tests locally. + +`./run.sh` will run the tests against the riot copy present in `riot/riot-web` served by a static python http server. You can symlink your `riot-web` develop copy here but that doesn't work well with webpack recompiling. You can run the test runner directly and specify parameters to get more insight into a failure or run the tests against your local webpack server. + +``` +./synapse/stop.sh && \ +./synapse/start.sh && \ +node start.js +``` +It's important to always stop and start synapse before each run of the tests to clear the in-memory sqlite database it uses, as the tests assume a blank slate. + +start.js accepts the following parameters that can help running the tests locally: + + - `--no-logs` dont show the excessive logging show by default (meant for CI), just where the test failed. + - `--riot-url ` don't use the riot copy and static server provided by the tests, but use a running server like the webpack watch server to run the tests against. Make sure to have `welcomeUserId` disabled in your config as the tests assume there is no riot-bot currently. + - `--slow-mo` run the tests a bit slower, so it's easier to follow along with `--windowed`. + - `--windowed` run the tests in an actual browser window Try to limit interacting with the windows while the tests are running. Hovering over the window tends to fail the tests, dragging the title bar should be fine though. + - `--dev-tools` open the devtools in the browser window, only applies if `--windowed` is set as well. + Developer Guide =============== diff --git a/start.js b/start.js index f3eac32f9f..630a3a6d3d 100644 --- a/start.js +++ b/start.js @@ -21,19 +21,20 @@ const scenario = require('./src/scenario'); const program = require('commander'); program .option('--no-logs', "don't output logs, document html on error", false) - .option('--debug', "open browser window and slow down interactions", false) .option('--riot-url [url]', "riot url to test", "http://localhost:5000") + .option('--windowed', "dont run tests headless", false) + .option('--slow-mo', "run tests slower to follow whats going on", false) + .option('--dev-tools', "open chrome devtools in browser window", false) .parse(process.argv); async function runTests() { let sessions = []; console.log("running tests ..."); - const options = {}; - if (program.debug) { - options.slowMo = 20; - options.devtools = true; - options.headless = false; - } + const options = { + slowMo: program.slowMo ? 20 : undefined, + devtools: program.devTools, + headless: !program.windowed, + }; if (process.env.CHROME_PATH) { const path = process.env.CHROME_PATH; console.log(`(using external chrome/chromium at ${path}, make sure it's compatible with puppeteer)`); From 5745e9ed0cfeb745046fc8788dccf012334b6b3a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Sep 2018 18:36:02 +0200 Subject: [PATCH 0109/2372] move Logger and LogBuffer to own module --- src/logbuffer.js | 30 +++++++++++++++++++++++ src/logger.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++ src/session.js | 64 ++---------------------------------------------- 3 files changed, 94 insertions(+), 62 deletions(-) create mode 100644 src/logbuffer.js create mode 100644 src/logger.js diff --git a/src/logbuffer.js b/src/logbuffer.js new file mode 100644 index 0000000000..8bf6285e25 --- /dev/null +++ b/src/logbuffer.js @@ -0,0 +1,30 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +module.exports = class LogBuffer { + constructor(page, eventName, eventMapper, reduceAsync=false, initialValue = "") { + this.buffer = initialValue; + page.on(eventName, (arg) => { + const result = eventMapper(arg); + if (reduceAsync) { + result.then((r) => this.buffer += r); + } + else { + this.buffer += result; + } + }); + } +} diff --git a/src/logger.js b/src/logger.js new file mode 100644 index 0000000000..be3ebde75b --- /dev/null +++ b/src/logger.js @@ -0,0 +1,62 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +module.exports = class Logger { + constructor(username) { + this.indent = 0; + this.username = username; + this.muted = false; + } + + startGroup(description) { + if (!this.muted) { + const indent = " ".repeat(this.indent * 2); + console.log(`${indent} * ${this.username} ${description}:`); + } + this.indent += 1; + return this; + } + + endGroup() { + this.indent -= 1; + return this; + } + + step(description) { + if (!this.muted) { + const indent = " ".repeat(this.indent * 2); + process.stdout.write(`${indent} * ${this.username} ${description} ... `); + } + return this; + } + + done(status = "done") { + if (!this.muted) { + process.stdout.write(status + "\n"); + } + return this; + } + + mute() { + this.muted = true; + return this; + } + + unmute() { + this.muted = false; + return this; + } +} diff --git a/src/session.js b/src/session.js index 0bfe781e61..86cdd05e95 100644 --- a/src/session.js +++ b/src/session.js @@ -15,68 +15,8 @@ limitations under the License. */ const puppeteer = require('puppeteer'); - -class LogBuffer { - constructor(page, eventName, eventMapper, reduceAsync=false, initialValue = "") { - this.buffer = initialValue; - page.on(eventName, (arg) => { - const result = eventMapper(arg); - if (reduceAsync) { - result.then((r) => this.buffer += r); - } - else { - this.buffer += result; - } - }); - } -} - -class Logger { - constructor(username) { - this.indent = 0; - this.username = username; - this.muted = false; - } - - startGroup(description) { - if (!this.muted) { - const indent = " ".repeat(this.indent * 2); - console.log(`${indent} * ${this.username} ${description}:`); - } - this.indent += 1; - return this; - } - - endGroup() { - this.indent -= 1; - return this; - } - - step(description) { - if (!this.muted) { - const indent = " ".repeat(this.indent * 2); - process.stdout.write(`${indent} * ${this.username} ${description} ... `); - } - return this; - } - - done(status = "done") { - if (!this.muted) { - process.stdout.write(status + "\n"); - } - return this; - } - - mute() { - this.muted = true; - return this; - } - - unmute() { - this.muted = false; - return this; - } -} +const Logger = require('./logger'); +const LogBuffer = require('./logbuffer'); module.exports = class RiotSession { constructor(browser, page, username, riotserver, hsUrl) { From 923ae90576d3beb3a1070d75880d1c7aaf3a06c0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Sep 2018 18:38:42 +0200 Subject: [PATCH 0110/2372] move range and delay over to util module --- src/scenario.js | 13 +++---------- src/session.js | 3 ++- src/util.js | 27 +++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 src/util.js diff --git a/src/scenario.js b/src/scenario.js index 946a4122ef..3538fa2d40 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -21,6 +21,7 @@ const join = require('./tests/join'); const sendMessage = require('./tests/send-message'); const acceptInvite = require('./tests/accept-invite'); const invite = require('./tests/invite'); +const {delay, range} = require('./util'); const { receiveMessage, checkTimelineContains, @@ -49,14 +50,6 @@ module.exports = async function scenario(createSession, restCreator) { await aLazyLoadingTest(alice, bob, charlies); } -function range(start, amount, step = 1) { - const r = []; - for (let i = 0; i < amount; ++i) { - r.push(start + (i * step)); - } - return r; -} - async function createRestUsers(restCreator) { const usernames = range(1, 10).map((i) => `charly-${i}`); const charlies = await restCreator.createSessionRange(usernames, 'testtest'); @@ -88,12 +81,12 @@ async function createE2ERoomAndTalk(alice, bob) { const bobDevice = await getE2EDeviceFromSettings(bob); // wait some time for the encryption warning dialog // to appear after closing the settings - await bob.delay(1000); + await delay(1000); await acceptDialogMaybe(bob, "encryption"); const aliceDevice = await getE2EDeviceFromSettings(alice); // wait some time for the encryption warning dialog // to appear after closing the settings - await alice.delay(1000); + await delay(1000); await acceptDialogMaybe(alice, "encryption"); await verifyDeviceForUser(bob, "alice", aliceDevice); await verifyDeviceForUser(alice, "bob", bobDevice); diff --git a/src/session.js b/src/session.js index 86cdd05e95..839b4a495b 100644 --- a/src/session.js +++ b/src/session.js @@ -17,6 +17,7 @@ limitations under the License. const puppeteer = require('puppeteer'); const Logger = require('./logger'); const LogBuffer = require('./logbuffer'); +const {delay} = require('./util'); module.exports = class RiotSession { constructor(browser, page, username, riotserver, hsUrl) { @@ -169,7 +170,7 @@ module.exports = class RiotSession { } delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); + return delay(ms); } close() { diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000000..8080d771be --- /dev/null +++ b/src/util.js @@ -0,0 +1,27 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +module.exports.range = function(start, amount, step = 1) { + const r = []; + for (let i = 0; i < amount; ++i) { + r.push(start + (i * step)); + } + return r; +} + +module.exports.delay = function(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} From 5ec8f6f9b41cd8d65093ae70f6031372ccb30865 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Sep 2018 18:40:25 +0200 Subject: [PATCH 0111/2372] rename tests folder to the more accurate usecases --- src/scenario.js | 24 +++++++++---------- src/{tests => usecases}/accept-invite.js | 0 src/{tests => usecases}/consent.js | 0 src/{tests => usecases}/create-room.js | 0 src/{tests => usecases}/dialog.js | 0 src/{tests => usecases}/invite.js | 0 src/{tests => usecases}/join.js | 0 src/{tests => usecases}/room-settings.js | 0 src/{tests => usecases}/send-message.js | 0 .../server-notices-consent.js | 0 src/{tests => usecases}/settings.js | 0 src/{tests => usecases}/signup.js | 0 src/{tests => usecases}/timeline.js | 0 src/{tests => usecases}/verify-device.js | 0 14 files changed, 12 insertions(+), 12 deletions(-) rename src/{tests => usecases}/accept-invite.js (100%) rename src/{tests => usecases}/consent.js (100%) rename src/{tests => usecases}/create-room.js (100%) rename src/{tests => usecases}/dialog.js (100%) rename src/{tests => usecases}/invite.js (100%) rename src/{tests => usecases}/join.js (100%) rename src/{tests => usecases}/room-settings.js (100%) rename src/{tests => usecases}/send-message.js (100%) rename src/{tests => usecases}/server-notices-consent.js (100%) rename src/{tests => usecases}/settings.js (100%) rename src/{tests => usecases}/signup.js (100%) rename src/{tests => usecases}/timeline.js (100%) rename src/{tests => usecases}/verify-device.js (100%) diff --git a/src/scenario.js b/src/scenario.js index 3538fa2d40..77307689ea 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -15,23 +15,23 @@ limitations under the License. */ -const {acceptDialogMaybe} = require('./tests/dialog'); -const signup = require('./tests/signup'); -const join = require('./tests/join'); -const sendMessage = require('./tests/send-message'); -const acceptInvite = require('./tests/accept-invite'); -const invite = require('./tests/invite'); const {delay, range} = require('./util'); +const {acceptDialogMaybe} = require('./usecases/dialog'); +const signup = require('./usecases/signup'); +const join = require('./usecases/join'); +const sendMessage = require('./usecases/send-message'); +const acceptInvite = require('./usecases/accept-invite'); +const invite = require('./usecases/invite'); const { receiveMessage, checkTimelineContains, scrollToTimelineTop -} = require('./tests/timeline'); -const createRoom = require('./tests/create-room'); -const changeRoomSettings = require('./tests/room-settings'); -const acceptServerNoticesInviteAndConsent = require('./tests/server-notices-consent'); -const {enableLazyLoading, getE2EDeviceFromSettings} = require('./tests/settings'); -const verifyDeviceForUser = require("./tests/verify-device"); +} = require('./usecases/timeline'); +const createRoom = require('./usecases/create-room'); +const changeRoomSettings = require('./usecases/room-settings'); +const acceptServerNoticesInviteAndConsent = require('./usecases/server-notices-consent'); +const {enableLazyLoading, getE2EDeviceFromSettings} = require('./usecases/settings'); +const verifyDeviceForUser = require("./usecases/verify-device"); module.exports = async function scenario(createSession, restCreator) { async function createUser(username) { diff --git a/src/tests/accept-invite.js b/src/usecases/accept-invite.js similarity index 100% rename from src/tests/accept-invite.js rename to src/usecases/accept-invite.js diff --git a/src/tests/consent.js b/src/usecases/consent.js similarity index 100% rename from src/tests/consent.js rename to src/usecases/consent.js diff --git a/src/tests/create-room.js b/src/usecases/create-room.js similarity index 100% rename from src/tests/create-room.js rename to src/usecases/create-room.js diff --git a/src/tests/dialog.js b/src/usecases/dialog.js similarity index 100% rename from src/tests/dialog.js rename to src/usecases/dialog.js diff --git a/src/tests/invite.js b/src/usecases/invite.js similarity index 100% rename from src/tests/invite.js rename to src/usecases/invite.js diff --git a/src/tests/join.js b/src/usecases/join.js similarity index 100% rename from src/tests/join.js rename to src/usecases/join.js diff --git a/src/tests/room-settings.js b/src/usecases/room-settings.js similarity index 100% rename from src/tests/room-settings.js rename to src/usecases/room-settings.js diff --git a/src/tests/send-message.js b/src/usecases/send-message.js similarity index 100% rename from src/tests/send-message.js rename to src/usecases/send-message.js diff --git a/src/tests/server-notices-consent.js b/src/usecases/server-notices-consent.js similarity index 100% rename from src/tests/server-notices-consent.js rename to src/usecases/server-notices-consent.js diff --git a/src/tests/settings.js b/src/usecases/settings.js similarity index 100% rename from src/tests/settings.js rename to src/usecases/settings.js diff --git a/src/tests/signup.js b/src/usecases/signup.js similarity index 100% rename from src/tests/signup.js rename to src/usecases/signup.js diff --git a/src/tests/timeline.js b/src/usecases/timeline.js similarity index 100% rename from src/tests/timeline.js rename to src/usecases/timeline.js diff --git a/src/tests/verify-device.js b/src/usecases/verify-device.js similarity index 100% rename from src/tests/verify-device.js rename to src/usecases/verify-device.js From 1725e7524b6c71482218625703220a36d52bc7fa Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 13 Sep 2018 10:31:15 +0200 Subject: [PATCH 0112/2372] split up scenarios in multiple files as lazy-loading scenarios grow --- src/scenario.js | 101 +++----------------------------- src/scenarios/README.md | 1 + src/scenarios/directory.js | 36 ++++++++++++ src/scenarios/e2e-encryption.js | 55 +++++++++++++++++ src/scenarios/lazy-loading.js | 62 ++++++++++++++++++++ src/session.js | 2 +- src/usecases/README.md | 2 + 7 files changed, 164 insertions(+), 95 deletions(-) create mode 100644 src/scenarios/README.md create mode 100644 src/scenarios/directory.js create mode 100644 src/scenarios/e2e-encryption.js create mode 100644 src/scenarios/lazy-loading.js create mode 100644 src/usecases/README.md diff --git a/src/scenario.js b/src/scenario.js index 77307689ea..f0b4ad988b 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -15,23 +15,12 @@ limitations under the License. */ -const {delay, range} = require('./util'); -const {acceptDialogMaybe} = require('./usecases/dialog'); +const {range} = require('./util'); const signup = require('./usecases/signup'); -const join = require('./usecases/join'); -const sendMessage = require('./usecases/send-message'); -const acceptInvite = require('./usecases/accept-invite'); -const invite = require('./usecases/invite'); -const { - receiveMessage, - checkTimelineContains, - scrollToTimelineTop -} = require('./usecases/timeline'); -const createRoom = require('./usecases/create-room'); -const changeRoomSettings = require('./usecases/room-settings'); const acceptServerNoticesInviteAndConsent = require('./usecases/server-notices-consent'); -const {enableLazyLoading, getE2EDeviceFromSettings} = require('./usecases/settings'); -const verifyDeviceForUser = require("./usecases/verify-device"); +const roomDirectoryScenarios = require('./scenarios/directory'); +const lazyLoadingScenarios = require('./scenarios/lazy-loading'); +const e2eEncryptionScenarios = require('./scenarios/e2e-encryption'); module.exports = async function scenario(createSession, restCreator) { async function createUser(username) { @@ -45,9 +34,9 @@ module.exports = async function scenario(createSession, restCreator) { const bob = await createUser("bob"); const charlies = await createRestUsers(restCreator); - await createDirectoryRoomAndTalk(alice, bob); - await createE2ERoomAndTalk(alice, bob); - await aLazyLoadingTest(alice, bob, charlies); + await roomDirectoryScenarios(alice, bob); + await e2eEncryptionScenarios(alice, bob); + await lazyLoadingScenarios(alice, bob, charlies); } async function createRestUsers(restCreator) { @@ -56,79 +45,3 @@ async function createRestUsers(restCreator) { await charlies.setDisplayName((s) => `Charly #${s.userName().split('-')[1]}`); return charlies; } - -async function createDirectoryRoomAndTalk(alice, bob) { - console.log(" creating a public room and join through directory:"); - const room = 'test'; - await createRoom(alice, room); - await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests"}); - await join(bob, room); - const bobMessage = "hi Alice!"; - await sendMessage(bob, bobMessage); - await receiveMessage(alice, {sender: "bob", body: bobMessage}); - const aliceMessage = "hi Bob, welcome!" - await sendMessage(alice, aliceMessage); - await receiveMessage(bob, {sender: "alice", body: aliceMessage}); -} - -async function createE2ERoomAndTalk(alice, bob) { - console.log(" creating an e2e encrypted room and join through invite:"); - const room = "secrets"; - await createRoom(bob, room); - await changeRoomSettings(bob, {encryption: true}); - await invite(bob, "@alice:localhost"); - await acceptInvite(alice, room); - const bobDevice = await getE2EDeviceFromSettings(bob); - // wait some time for the encryption warning dialog - // to appear after closing the settings - await delay(1000); - await acceptDialogMaybe(bob, "encryption"); - const aliceDevice = await getE2EDeviceFromSettings(alice); - // wait some time for the encryption warning dialog - // to appear after closing the settings - await delay(1000); - await acceptDialogMaybe(alice, "encryption"); - await verifyDeviceForUser(bob, "alice", aliceDevice); - await verifyDeviceForUser(alice, "bob", bobDevice); - const aliceMessage = "Guess what I just heard?!" - await sendMessage(alice, aliceMessage); - await receiveMessage(bob, {sender: "alice", body: aliceMessage, encrypted: true}); - const bobMessage = "You've got to tell me!"; - await sendMessage(bob, bobMessage); - await receiveMessage(alice, {sender: "bob", body: bobMessage, encrypted: true}); -} - -async function aLazyLoadingTest(alice, bob, charlies) { - console.log(" creating a room for lazy loading member scenarios:"); - await enableLazyLoading(alice); - const room = "Lazy Loading Test"; - const alias = "#lltest:localhost"; - const charlyMsg1 = "hi bob!"; - const charlyMsg2 = "how's it going??"; - await createRoom(bob, room); - await changeRoomSettings(bob, {directory: true, visibility: "public_no_guests", alias}); - // wait for alias to be set by server after clicking "save" - // so the charlies can join it. - await bob.delay(500); - const charlyMembers = await charlies.join(alias); - await charlyMembers.talk(charlyMsg1); - await charlyMembers.talk(charlyMsg2); - bob.log.step("sends 20 messages").mute(); - for(let i = 20; i >= 1; --i) { - await sendMessage(bob, `I will only say this ${i} time(s)!`); - } - bob.log.unmute().done(); - await join(alice, alias); - await scrollToTimelineTop(alice); - //alice should see 2 messages from every charly with - //the correct display name - const expectedMessages = [charlyMsg1, charlyMsg2].reduce((messages, msgText) => { - return charlies.sessions.reduce((messages, charly) => { - return messages.concat({ - sender: charly.displayName(), - body: msgText, - }); - }, messages); - }, []); - await checkTimelineContains(alice, expectedMessages, "Charly #1-10"); -} diff --git a/src/scenarios/README.md b/src/scenarios/README.md new file mode 100644 index 0000000000..4eabc8f9ef --- /dev/null +++ b/src/scenarios/README.md @@ -0,0 +1 @@ +scenarios contains the high-level playbook for the test suite diff --git a/src/scenarios/directory.js b/src/scenarios/directory.js new file mode 100644 index 0000000000..3b87d64d78 --- /dev/null +++ b/src/scenarios/directory.js @@ -0,0 +1,36 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + + +const join = require('../usecases/join'); +const sendMessage = require('../usecases/send-message'); +const {receiveMessage} = require('../usecases/timeline'); +const createRoom = require('../usecases/create-room'); +const changeRoomSettings = require('../usecases/room-settings'); + +module.exports = async function roomDirectoryScenarios(alice, bob) { + console.log(" creating a public room and join through directory:"); + const room = 'test'; + await createRoom(alice, room); + await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests"}); + await join(bob, room); //looks up room in directory + const bobMessage = "hi Alice!"; + await sendMessage(bob, bobMessage); + await receiveMessage(alice, {sender: "bob", body: bobMessage}); + const aliceMessage = "hi Bob, welcome!" + await sendMessage(alice, aliceMessage); + await receiveMessage(bob, {sender: "alice", body: aliceMessage}); +} diff --git a/src/scenarios/e2e-encryption.js b/src/scenarios/e2e-encryption.js new file mode 100644 index 0000000000..938dc5e592 --- /dev/null +++ b/src/scenarios/e2e-encryption.js @@ -0,0 +1,55 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + + +const {delay} = require('../util'); +const {acceptDialogMaybe} = require('../usecases/dialog'); +const join = require('../usecases/join'); +const sendMessage = require('../usecases/send-message'); +const acceptInvite = require('../usecases/accept-invite'); +const invite = require('../usecases/invite'); +const {receiveMessage} = require('../usecases/timeline'); +const createRoom = require('../usecases/create-room'); +const changeRoomSettings = require('../usecases/room-settings'); +const {getE2EDeviceFromSettings} = require('../usecases/settings'); +const verifyDeviceForUser = require('../usecases/verify-device'); + +module.exports = async function e2eEncryptionScenarios(alice, bob) { + console.log(" creating an e2e encrypted room and join through invite:"); + const room = "secrets"; + await createRoom(bob, room); + await changeRoomSettings(bob, {encryption: true}); + await invite(bob, "@alice:localhost"); + await acceptInvite(alice, room); + const bobDevice = await getE2EDeviceFromSettings(bob); + // wait some time for the encryption warning dialog + // to appear after closing the settings + await delay(1000); + await acceptDialogMaybe(bob, "encryption"); + const aliceDevice = await getE2EDeviceFromSettings(alice); + // wait some time for the encryption warning dialog + // to appear after closing the settings + await delay(1000); + await acceptDialogMaybe(alice, "encryption"); + await verifyDeviceForUser(bob, "alice", aliceDevice); + await verifyDeviceForUser(alice, "bob", bobDevice); + const aliceMessage = "Guess what I just heard?!" + await sendMessage(alice, aliceMessage); + await receiveMessage(bob, {sender: "alice", body: aliceMessage, encrypted: true}); + const bobMessage = "You've got to tell me!"; + await sendMessage(bob, bobMessage); + await receiveMessage(alice, {sender: "bob", body: bobMessage, encrypted: true}); +} diff --git a/src/scenarios/lazy-loading.js b/src/scenarios/lazy-loading.js new file mode 100644 index 0000000000..f7360622a1 --- /dev/null +++ b/src/scenarios/lazy-loading.js @@ -0,0 +1,62 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + + +const {delay} = require('../util'); +const join = require('../usecases/join'); +const sendMessage = require('../usecases/send-message'); +const { + checkTimelineContains, + scrollToTimelineTop +} = require('../usecases/timeline'); +const createRoom = require('../usecases/create-room'); +const changeRoomSettings = require('../usecases/room-settings'); +const {enableLazyLoading} = require('../usecases/settings'); + +module.exports = async function lazyLoadingScenarios(alice, bob, charlies) { + console.log(" creating a room for lazy loading member scenarios:"); + await enableLazyLoading(alice); + const room = "Lazy Loading Test"; + const alias = "#lltest:localhost"; + const charlyMsg1 = "hi bob!"; + const charlyMsg2 = "how's it going??"; + await createRoom(bob, room); + await changeRoomSettings(bob, {directory: true, visibility: "public_no_guests", alias}); + // wait for alias to be set by server after clicking "save" + // so the charlies can join it. + await bob.delay(500); + const charlyMembers = await charlies.join(alias); + await charlyMembers.talk(charlyMsg1); + await charlyMembers.talk(charlyMsg2); + bob.log.step("sends 20 messages").mute(); + for(let i = 20; i >= 1; --i) { + await sendMessage(bob, `I will only say this ${i} time(s)!`); + } + bob.log.unmute().done(); + await join(alice, alias); + await scrollToTimelineTop(alice); + //alice should see 2 messages from every charly with + //the correct display name + const expectedMessages = [charlyMsg1, charlyMsg2].reduce((messages, msgText) => { + return charlies.sessions.reduce((messages, charly) => { + return messages.concat({ + sender: charly.displayName(), + body: msgText, + }); + }, messages); + }, []); + await checkTimelineContains(alice, expectedMessages, "Charly #1-10"); +} diff --git a/src/session.js b/src/session.js index 839b4a495b..3f233ee8f2 100644 --- a/src/session.js +++ b/src/session.js @@ -124,7 +124,7 @@ module.exports = class RiotSession { return await this.queryAll(selector); } - waitForReload(timeout = 5000) { + waitForReload(timeout = 10000) { return new Promise((resolve, reject) => { const timeoutHandle = setTimeout(() => { this.browser.removeEventListener('domcontentloaded', callback); diff --git a/src/usecases/README.md b/src/usecases/README.md new file mode 100644 index 0000000000..daa990e15c --- /dev/null +++ b/src/usecases/README.md @@ -0,0 +1,2 @@ +use cases contains the detailed DOM interactions to perform a given use case, may also do some assertions. +use cases are often used in multiple scenarios. From 5d06c65ce5d28d64a1efd2dd4e746cbf2c999e29 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 13 Sep 2018 12:02:49 +0200 Subject: [PATCH 0113/2372] split up ll tests in several functions --- src/scenarios/lazy-loading.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/scenarios/lazy-loading.js b/src/scenarios/lazy-loading.js index f7360622a1..0adb3fc793 100644 --- a/src/scenarios/lazy-loading.js +++ b/src/scenarios/lazy-loading.js @@ -29,10 +29,16 @@ const {enableLazyLoading} = require('../usecases/settings'); module.exports = async function lazyLoadingScenarios(alice, bob, charlies) { console.log(" creating a room for lazy loading member scenarios:"); await enableLazyLoading(alice); - const room = "Lazy Loading Test"; - const alias = "#lltest:localhost"; - const charlyMsg1 = "hi bob!"; - const charlyMsg2 = "how's it going??"; + await setupRoomWithBobAliceAndCharlies(alice, bob, charlies); + await checkPaginatedDisplayNames(alice, charlies); +} + +const room = "Lazy Loading Test"; +const alias = "#lltest:localhost"; +const charlyMsg1 = "hi bob!"; +const charlyMsg2 = "how's it going??"; + +async function setupRoomWithBobAliceAndCharlies(alice, bob, charlies) { await createRoom(bob, room); await changeRoomSettings(bob, {directory: true, visibility: "public_no_guests", alias}); // wait for alias to be set by server after clicking "save" @@ -47,6 +53,9 @@ module.exports = async function lazyLoadingScenarios(alice, bob, charlies) { } bob.log.unmute().done(); await join(alice, alias); +} + +async function checkPaginatedDisplayNames(alice, charlies) { await scrollToTimelineTop(alice); //alice should see 2 messages from every charly with //the correct display name From 239e6a4bcef1a92aa69d48923b8f46e072d8c00c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 13 Sep 2018 12:03:29 +0200 Subject: [PATCH 0114/2372] add ll tests to check if all expected members are in memberlist also move verify-device use case to timeline to reuse memberlist query for this test. --- src/scenarios/e2e-encryption.js | 2 +- src/scenarios/lazy-loading.js | 16 ++++++++++++ .../{verify-device.js => memberlist.js} | 26 ++++++++++++------- 3 files changed, 33 insertions(+), 11 deletions(-) rename src/usecases/{verify-device.js => memberlist.js} (68%) diff --git a/src/scenarios/e2e-encryption.js b/src/scenarios/e2e-encryption.js index 938dc5e592..51d8a70236 100644 --- a/src/scenarios/e2e-encryption.js +++ b/src/scenarios/e2e-encryption.js @@ -25,7 +25,7 @@ const {receiveMessage} = require('../usecases/timeline'); const createRoom = require('../usecases/create-room'); const changeRoomSettings = require('../usecases/room-settings'); const {getE2EDeviceFromSettings} = require('../usecases/settings'); -const verifyDeviceForUser = require('../usecases/verify-device'); +const {verifyDeviceForUser} = require('../usecases/memberlist'); module.exports = async function e2eEncryptionScenarios(alice, bob) { console.log(" creating an e2e encrypted room and join through invite:"); diff --git a/src/scenarios/lazy-loading.js b/src/scenarios/lazy-loading.js index 0adb3fc793..bd7c1d1507 100644 --- a/src/scenarios/lazy-loading.js +++ b/src/scenarios/lazy-loading.js @@ -23,14 +23,17 @@ const { scrollToTimelineTop } = require('../usecases/timeline'); const createRoom = require('../usecases/create-room'); +const {getMembersInMemberlist} = require('../usecases/memberlist'); const changeRoomSettings = require('../usecases/room-settings'); const {enableLazyLoading} = require('../usecases/settings'); +const assert = require('assert'); module.exports = async function lazyLoadingScenarios(alice, bob, charlies) { console.log(" creating a room for lazy loading member scenarios:"); await enableLazyLoading(alice); await setupRoomWithBobAliceAndCharlies(alice, bob, charlies); await checkPaginatedDisplayNames(alice, charlies); + await checkMemberList(alice, charlies); } const room = "Lazy Loading Test"; @@ -69,3 +72,16 @@ async function checkPaginatedDisplayNames(alice, charlies) { }, []); await checkTimelineContains(alice, expectedMessages, "Charly #1-10"); } + +async function checkMemberList(alice, charlies) { + alice.log.step("checks the memberlist contains herself, bob and all charlies"); + const displayNames = (await getMembersInMemberlist(alice)).map((m) => m.displayName); + assert(displayNames.includes("alice")); + assert(displayNames.includes("bob")); + charlies.sessions.forEach((charly) => { + assert(displayNames.includes(charly.displayName()), + `${charly.displayName()} should be in the member list, ` + + `only have ${displayNames}`); + }); + alice.log.done(); +} diff --git a/src/usecases/verify-device.js b/src/usecases/memberlist.js similarity index 68% rename from src/usecases/verify-device.js rename to src/usecases/memberlist.js index 7b01e7c756..b018ed552c 100644 --- a/src/usecases/verify-device.js +++ b/src/usecases/memberlist.js @@ -16,16 +16,13 @@ limitations under the License. const assert = require('assert'); -module.exports = async function verifyDeviceForUser(session, name, expectedDevice) { +module.exports.verifyDeviceForUser = async function(session, name, expectedDevice) { session.log.step(`verifies e2e device for ${name}`); - const memberNameElements = await session.queryAll(".mx_MemberList .mx_EntityTile_name"); - const membersAndNames = await Promise.all(memberNameElements.map(async (el) => { - return [el, await session.innerText(el)]; - })); - const matchingMember = membersAndNames.filter(([el, text]) => { - return text === name; - }).map(([el]) => el)[0]; - await matchingMember.click(); + const membersAndNames = await getMembersInMemberlist(session); + const matchingLabel = membersAndNames.filter((m) => { + return m.displayName === name; + }).map((m) => m.label)[0]; + await matchingLabel.click(); const firstVerifyButton = await session.waitAndQuery(".mx_MemberDeviceInfo_verify"); await firstVerifyButton.click(); const dialogCodeFields = await session.waitAndQueryAll(".mx_QuestionDialog code"); @@ -39,4 +36,13 @@ module.exports = async function verifyDeviceForUser(session, name, expectedDevic const closeMemberInfo = await session.query(".mx_MemberInfo_cancel"); await closeMemberInfo.click(); session.log.done(); -} \ No newline at end of file +} + +async function getMembersInMemberlist(session) { + const memberNameElements = await session.waitAndQueryAll(".mx_MemberList .mx_EntityTile_name"); + return Promise.all(memberNameElements.map(async (el) => { + return {label: el, displayName: await session.innerText(el)}; + })); +} + +module.exports.getMembersInMemberlist = getMembersInMemberlist; From 9f4cf776c5de89a84714182b532ac75b7fd159b6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 13 Sep 2018 12:04:18 +0200 Subject: [PATCH 0115/2372] make receiveMessage more robust by checking first if the message is not already in the timeline --- src/usecases/timeline.js | 49 ++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/src/usecases/timeline.js b/src/usecases/timeline.js index beaaec5e5a..32c468048c 100644 --- a/src/usecases/timeline.js +++ b/src/usecases/timeline.js @@ -46,23 +46,38 @@ module.exports.receiveMessage = async function(session, expectedMessage) { session.log.step(`receives message "${expectedMessage.body}" from ${expectedMessage.sender}`); // wait for a response to come in that contains the message // crude, but effective - await session.page.waitForResponse(async (response) => { - if (response.request().url().indexOf("/sync") === -1) { - return false; - } - const body = await response.text(); - if (expectedMessage.encrypted) { - return body.indexOf(expectedMessage.sender) !== -1 && - body.indexOf("m.room.encrypted") !== -1; - } else { - return body.indexOf(expectedMessage.body) !== -1; - } - }); - // wait a bit for the incoming event to be rendered - await session.delay(1000); - const lastTile = await getLastEventTile(session); - const foundMessage = await getMessageFromEventTile(lastTile); - assertMessage(foundMessage, expectedMessage); + + async function assertLastMessage() { + const lastTile = await getLastEventTile(session); + const lastMessage = await getMessageFromEventTile(lastTile); + await assertMessage(lastMessage, expectedMessage); + } + + // first try to see if the message is already the last message in the timeline + let isExpectedMessage = false; + try { + assertLastMessage(); + isExpectedMessage = true; + } catch(ex) {} + + if (!isExpectedMessage) { + await session.page.waitForResponse(async (response) => { + if (response.request().url().indexOf("/sync") === -1) { + return false; + } + const body = await response.text(); + if (expectedMessage.encrypted) { + return body.indexOf(expectedMessage.sender) !== -1 && + body.indexOf("m.room.encrypted") !== -1; + } else { + return body.indexOf(expectedMessage.body) !== -1; + } + }); + // wait a bit for the incoming event to be rendered + await session.delay(1000); + await assertLastMessage(); + } + session.log.done(); } From af255c63866603271c03e4dc80b032fd9f702aae Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 14 Sep 2018 09:52:34 +0200 Subject: [PATCH 0116/2372] dont assert the first time in receiveMessage, as it will show an ugly assert error while everything is fine, just need to wait longer --- src/usecases/timeline.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/usecases/timeline.js b/src/usecases/timeline.js index 32c468048c..466d7fb222 100644 --- a/src/usecases/timeline.js +++ b/src/usecases/timeline.js @@ -47,20 +47,23 @@ module.exports.receiveMessage = async function(session, expectedMessage) { // wait for a response to come in that contains the message // crude, but effective - async function assertLastMessage() { + async function getLastMessage() { const lastTile = await getLastEventTile(session); - const lastMessage = await getMessageFromEventTile(lastTile); - await assertMessage(lastMessage, expectedMessage); + return getMessageFromEventTile(lastTile); } - // first try to see if the message is already the last message in the timeline + let lastMessage = null; let isExpectedMessage = false; try { - assertLastMessage(); - isExpectedMessage = true; + lastMessage = await getLastMessage(); + isExpectedMessage = lastMessage && + lastMessage.body === expectedMessage.body && + lastMessage.sender === expectedMessage.sender; } catch(ex) {} - - if (!isExpectedMessage) { + // first try to see if the message is already the last message in the timeline + if (isExpectedMessage) { + assertMessage(lastMessage, expectedMessage); + } else { await session.page.waitForResponse(async (response) => { if (response.request().url().indexOf("/sync") === -1) { return false; @@ -75,7 +78,8 @@ module.exports.receiveMessage = async function(session, expectedMessage) { }); // wait a bit for the incoming event to be rendered await session.delay(1000); - await assertLastMessage(); + lastMessage = await getLastMessage(); + assertMessage(lastMessage, expectedMessage); } session.log.done(); From 6deb595fecfb66c5d0e62421c8c852cd759462a5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 14 Sep 2018 12:17:22 +0200 Subject: [PATCH 0117/2372] add logging to rest session actions --- src/rest/creator.js | 4 ++-- src/rest/multi.js | 49 ++++++++++++++++++++++++++++++++++++--------- src/rest/room.js | 7 ++++++- src/rest/session.js | 12 +++++++++-- src/scenario.js | 2 +- 5 files changed, 58 insertions(+), 16 deletions(-) diff --git a/src/rest/creator.js b/src/rest/creator.js index 9090a21e70..84b1fbc70a 100644 --- a/src/rest/creator.js +++ b/src/rest/creator.js @@ -27,10 +27,10 @@ module.exports = class RestSessionCreator { this.cwd = cwd; } - async createSessionRange(usernames, password) { + async createSessionRange(usernames, password, groupName) { const sessionPromises = usernames.map((username) => this.createSession(username, password)); const sessions = await Promise.all(sessionPromises); - return new RestMultiSession(sessions); + return new RestMultiSession(sessions, groupName); } async createSession(username, password) { diff --git a/src/rest/multi.js b/src/rest/multi.js index 12ebe9d4ab..35bb11a0cf 100644 --- a/src/rest/multi.js +++ b/src/rest/multi.js @@ -17,14 +17,16 @@ limitations under the License. const request = require('request-promise-native'); const RestRoom = require('./room'); const {approveConsent} = require('./consent'); +const Logger = require('../logger'); module.exports = class RestMultiSession { - constructor(sessions) { + constructor(sessions, groupName) { + this.log = new Logger(groupName); this.sessions = sessions; } - slice(start, end) { - return new RestMultiSession(this.sessions.slice(start, end)); + slice(start, end, groupName) { + return new RestMultiSession(this.sessions.slice(start, end), groupName); } pop(userName) { @@ -37,25 +39,52 @@ module.exports = class RestMultiSession { } async setDisplayName(fn) { - await Promise.all(this.sessions.map((s) => s.setDisplayName(fn(s)))); + this.log.step("set their display name") + await Promise.all(this.sessions.map(async (s) => { + s.log.mute(); + await s.setDisplayName(fn(s)); + s.log.unmute(); + })); + this.log.done(); } - async join(roomId) { - const rooms = await Promise.all(this.sessions.map((s) => s.join(roomId))); - return new RestMultiRoom(rooms); + async join(roomIdOrAlias) { + this.log.step(`join ${roomIdOrAlias}`) + const rooms = await Promise.all(this.sessions.map(async (s) => { + s.log.mute(); + const room = await s.join(roomIdOrAlias); + s.log.unmute(); + return room; + })); + this.log.done(); + return new RestMultiRoom(rooms, roomIdOrAlias, this.log); } } class RestMultiRoom { - constructor(rooms) { + constructor(rooms, roomIdOrAlias, log) { this.rooms = rooms; + this.roomIdOrAlias = roomIdOrAlias; + this.log = log; } async talk(message) { - await Promise.all(this.rooms.map((r) => r.talk(message))); + this.log.step(`say "${message}" in ${this.roomIdOrAlias}`) + await Promise.all(this.rooms.map(async (r) => { + r.log.mute(); + await r.talk(message); + r.log.unmute(); + })); + this.log.done(); } async leave() { - await Promise.all(this.rooms.map((r) => r.leave())); + this.log.step(`leave ${this.roomIdOrAlias}`) + await Promise.all(this.rooms.map(async (r) => { + r.log.mute(); + await r.leave(message); + r.log.unmute(); + })); + this.log.done(); } } diff --git a/src/rest/room.js b/src/rest/room.js index d8de958a27..a7f40af594 100644 --- a/src/rest/room.js +++ b/src/rest/room.js @@ -18,22 +18,27 @@ const uuidv4 = require('uuid/v4'); /* no pun intented */ module.exports = class RestRoom { - constructor(session, roomId) { + constructor(session, roomId, log) { this.session = session; this._roomId = roomId; + this.log = log; } async talk(message) { + this.log.step(`says "${message}" in ${this._roomId}`) const txId = uuidv4(); await this.session._put(`/rooms/${this._roomId}/send/m.room.message/${txId}`, { "msgtype": "m.text", "body": message }); + this.log.done(); return txId; } async leave() { + this.log.step(`leaves ${this._roomId}`) await this.session._post(`/rooms/${this._roomId}/leave`); + this.log.done(); } roomId() { diff --git a/src/rest/session.js b/src/rest/session.js index ece04f3352..21922a69f1 100644 --- a/src/rest/session.js +++ b/src/rest/session.js @@ -15,11 +15,13 @@ limitations under the License. */ const request = require('request-promise-native'); +const Logger = require('../logger'); const RestRoom = require('./room'); const {approveConsent} = require('./consent'); module.exports = class RestSession { constructor(credentials) { + this.log = new Logger(credentials.userId); this._credentials = credentials; this._displayName = null; } @@ -37,18 +39,23 @@ module.exports = class RestSession { } async setDisplayName(displayName) { + this.log.step(`sets their display name to ${displayName}`); this._displayName = displayName; await this._put(`/profile/${this._credentials.userId}/displayname`, { displayname: displayName }); + this.log.done(); } async join(roomIdOrAlias) { + this.log.step(`joins ${roomIdOrAlias}`); const {room_id} = await this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`); - return new RestRoom(this, room_id); + this.log.done(); + return new RestRoom(this, room_id, this.log); } async createRoom(name, options) { + this.log.step(`creates room ${name}`); const body = { name, }; @@ -68,7 +75,8 @@ module.exports = class RestSession { } const {room_id} = await this._post(`/createRoom`, body); - return new RestRoom(this, room_id); + this.log.done(); + return new RestRoom(this, room_id, this.log); } _post(csApiPath, body) { diff --git a/src/scenario.js b/src/scenario.js index f0b4ad988b..12cff7d498 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -41,7 +41,7 @@ module.exports = async function scenario(createSession, restCreator) { async function createRestUsers(restCreator) { const usernames = range(1, 10).map((i) => `charly-${i}`); - const charlies = await restCreator.createSessionRange(usernames, 'testtest'); + const charlies = await restCreator.createSessionRange(usernames, "testtest", "charly-1..10"); await charlies.setDisplayName((s) => `Charly #${s.userName().split('-')[1]}`); return charlies; } From 16b2f09915cce5781cf8da90cf101491158044fc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 14 Sep 2018 12:44:01 +0200 Subject: [PATCH 0118/2372] Test if members joining while user is offline are received after returning online with LL enabled --- src/rest/multi.js | 2 +- src/scenarios/lazy-loading.js | 29 +++++++++++++++++++++++++---- src/session.js | 7 +++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/rest/multi.js b/src/rest/multi.js index 35bb11a0cf..3d24245ddf 100644 --- a/src/rest/multi.js +++ b/src/rest/multi.js @@ -25,7 +25,7 @@ module.exports = class RestMultiSession { this.sessions = sessions; } - slice(start, end, groupName) { + slice(groupName, start, end) { return new RestMultiSession(this.sessions.slice(start, end), groupName); } diff --git a/src/scenarios/lazy-loading.js b/src/scenarios/lazy-loading.js index bd7c1d1507..a606cc0421 100644 --- a/src/scenarios/lazy-loading.js +++ b/src/scenarios/lazy-loading.js @@ -31,9 +31,15 @@ const assert = require('assert'); module.exports = async function lazyLoadingScenarios(alice, bob, charlies) { console.log(" creating a room for lazy loading member scenarios:"); await enableLazyLoading(alice); - await setupRoomWithBobAliceAndCharlies(alice, bob, charlies); - await checkPaginatedDisplayNames(alice, charlies); - await checkMemberList(alice, charlies); + const charly1to5 = charlies.slice("charly-1..5", 0, 5); + const charly6to10 = charlies.slice("charly-6..10", 5); + assert(charly1to5.sessions.length, 5); + assert(charly6to10.sessions.length, 5); + await setupRoomWithBobAliceAndCharlies(alice, bob, charly1to5); + await checkPaginatedDisplayNames(alice, charly1to5); + await checkMemberList(alice, charly1to5); + await joinCharliesWhileAliceIsOffline(alice, charly6to10); + await checkMemberList(alice, charly6to10); } const room = "Lazy Loading Test"; @@ -70,7 +76,7 @@ async function checkPaginatedDisplayNames(alice, charlies) { }); }, messages); }, []); - await checkTimelineContains(alice, expectedMessages, "Charly #1-10"); + await checkTimelineContains(alice, expectedMessages, "Charly #1-5"); } async function checkMemberList(alice, charlies) { @@ -85,3 +91,18 @@ async function checkMemberList(alice, charlies) { }); alice.log.done(); } + +async function joinCharliesWhileAliceIsOffline(alice, charly6to10) { + await alice.setOffline(true); + await delay(1000); + const members6to10 = await charly6to10.join(alias); + const member6 = members6to10.rooms[0]; + member6.log.step("sends 20 messages").mute(); + for(let i = 20; i >= 1; --i) { + await member6.talk("where is charly?"); + } + member6.log.unmute().done(); + await delay(1000); + await alice.setOffline(false); + await delay(1000); +} diff --git a/src/session.js b/src/session.js index 3f233ee8f2..82a66fda39 100644 --- a/src/session.js +++ b/src/session.js @@ -173,6 +173,13 @@ module.exports = class RiotSession { return delay(ms); } + async setOffline(enabled) { + const description = enabled ? "offline" : "back online"; + this.log.step(`goes ${description}`); + await this.page.setOfflineMode(enabled); + this.log.done(); + } + close() { return this.browser.close(); } From 36708cc5db5cc303904884bc9ea50ac76bcb3a5c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 14 Sep 2018 14:45:40 +0200 Subject: [PATCH 0119/2372] wait for next sync before inspecting memberlist before we needed a 10s delay here to make the test work reliable, this should be faster in the best case. --- src/scenarios/lazy-loading.js | 5 +++-- src/session.js | 27 +++++++++++++++++++++++++++ src/usecases/timeline.js | 5 +---- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/scenarios/lazy-loading.js b/src/scenarios/lazy-loading.js index a606cc0421..15826af568 100644 --- a/src/scenarios/lazy-loading.js +++ b/src/scenarios/lazy-loading.js @@ -102,7 +102,8 @@ async function joinCharliesWhileAliceIsOffline(alice, charly6to10) { await member6.talk("where is charly?"); } member6.log.unmute().done(); - await delay(1000); + const catchupPromise = alice.waitForNextSuccessfulSync(); await alice.setOffline(false); - await delay(1000); + await catchupPromise; + await delay(2000); } diff --git a/src/session.js b/src/session.js index 82a66fda39..7ea980bd32 100644 --- a/src/session.js +++ b/src/session.js @@ -161,6 +161,33 @@ module.exports = class RiotSession { }); } + waitForSyncResponseWith(predicate) { + return this.page.waitForResponse(async (response) => { + if (response.request().url().indexOf("/sync") === -1) { + return false; + } + return predicate(response); + }); + } + + /** wait for a /sync request started after this call that gets a 200 response */ + async waitForNextSuccessfulSync() { + const syncUrls = []; + function onRequest(request) { + if (request.url().indexOf("/sync") !== -1) { + syncUrls.push(request.url()); + } + } + + this.page.on('request', onRequest); + + await this.page.waitForResponse((response) => { + return syncUrls.includes(response.request().url()) && response.status() === 200; + }); + + this.page.removeListener('request', onRequest); + } + goto(url) { return this.page.goto(url); } diff --git a/src/usecases/timeline.js b/src/usecases/timeline.js index 466d7fb222..dce0203660 100644 --- a/src/usecases/timeline.js +++ b/src/usecases/timeline.js @@ -64,10 +64,7 @@ module.exports.receiveMessage = async function(session, expectedMessage) { if (isExpectedMessage) { assertMessage(lastMessage, expectedMessage); } else { - await session.page.waitForResponse(async (response) => { - if (response.request().url().indexOf("/sync") === -1) { - return false; - } + await session.waitForSyncResponseWith(async (response) => { const body = await response.text(); if (expectedMessage.encrypted) { return body.indexOf(expectedMessage.sender) !== -1 && From 992a0be4d08f877e731b71db3d0da0b1687ae8fd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 14 Sep 2018 14:46:25 +0200 Subject: [PATCH 0120/2372] DRY usernames --- src/scenarios/lazy-loading.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scenarios/lazy-loading.js b/src/scenarios/lazy-loading.js index 15826af568..c33e83215c 100644 --- a/src/scenarios/lazy-loading.js +++ b/src/scenarios/lazy-loading.js @@ -76,11 +76,11 @@ async function checkPaginatedDisplayNames(alice, charlies) { }); }, messages); }, []); - await checkTimelineContains(alice, expectedMessages, "Charly #1-5"); + await checkTimelineContains(alice, expectedMessages, charlies.log.username); } async function checkMemberList(alice, charlies) { - alice.log.step("checks the memberlist contains herself, bob and all charlies"); + alice.log.step(`checks the memberlist contains herself, bob and ${charlies.log.username}`); const displayNames = (await getMembersInMemberlist(alice)).map((m) => m.displayName); assert(displayNames.includes("alice")); assert(displayNames.includes("bob")); From 8cff961ec82469e5321634992d45aad9a00c2e25 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 14 Sep 2018 14:46:42 +0200 Subject: [PATCH 0121/2372] use develop for now as LL with gappy syncs is fixed on that branch for now --- synapse/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/install.sh b/synapse/install.sh index 3d8172a9f6..37dfd7d7e2 100755 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -1,6 +1,6 @@ #!/bin/bash # config -SYNAPSE_BRANCH=master +SYNAPSE_BRANCH=develop INSTALLATION_NAME=consent SERVER_DIR=installations/$INSTALLATION_NAME CONFIG_TEMPLATE=consent From 42c1b95b7c4f57527c5bed3e04aa5cdd7c369f22 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Sep 2018 10:17:03 +0200 Subject: [PATCH 0122/2372] spit out logs for creating REST users to figure out what is going on with the CI server --- src/rest/creator.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/rest/creator.js b/src/rest/creator.js index 84b1fbc70a..cc87134108 100644 --- a/src/rest/creator.js +++ b/src/rest/creator.js @@ -35,11 +35,12 @@ module.exports = class RestSessionCreator { async createSession(username, password) { await this._register(username, password); + console.log(` * created REST user ${username} ... done`); const authResult = await this._authenticate(username, password); return new RestSession(authResult); } - _register(username, password) { + async _register(username, password) { const registerArgs = [ '-c homeserver.yaml', `-u ${username}`, @@ -55,11 +56,14 @@ module.exports = class RestSessionCreator { registerCmd ].join(';'); - return exec(allCmds, {cwd: this.cwd, encoding: 'utf-8'}).catch((result) => { + try { + await exec(allCmds, {cwd: this.cwd, encoding: 'utf-8'}); + } catch (result) { const lines = result.stdout.trim().split('\n'); const failureReason = lines[lines.length - 1]; - throw new Error(`creating user ${username} failed: ${failureReason}`); - }); + const logs = (await exec("tail -n 100 synapse/installations/consent/homeserver.log")).stdout; + throw new Error(`creating user ${username} failed: ${failureReason}, synapse logs:\n${logs}`); + } } async _authenticate(username, password) { From 0d86b82e3ae19729e72c0a9b0196a8b761cd1949 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Sep 2018 11:13:02 +0200 Subject: [PATCH 0123/2372] increase timeout for server notices room --- src/usecases/server-notices-consent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usecases/server-notices-consent.js b/src/usecases/server-notices-consent.js index 25c3bb3bd5..eb42dcdaf7 100644 --- a/src/usecases/server-notices-consent.js +++ b/src/usecases/server-notices-consent.js @@ -19,7 +19,7 @@ const acceptInvite = require("./accept-invite") module.exports = async function acceptServerNoticesInviteAndConsent(session) { await acceptInvite(session, "Server Notices"); session.log.step(`accepts terms & conditions`); - const consentLink = await session.waitAndQuery(".mx_EventTile_body a"); + const consentLink = await session.waitAndQuery(".mx_EventTile_body a", 10000); const termsPagePromise = session.waitForNewPage(); await consentLink.click(); const termsPage = await termsPagePromise; From a84162ede84a4648c49361cead0c4c40c7fb3801 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Sep 2018 16:11:07 +0200 Subject: [PATCH 0124/2372] use patched synapse so admin rest api works with python 2.7.6 --- synapse/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/install.sh b/synapse/install.sh index 37dfd7d7e2..1ae2212e7e 100755 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -1,6 +1,6 @@ #!/bin/bash # config -SYNAPSE_BRANCH=develop +SYNAPSE_BRANCH=bwindels/adminapibeforepy277 INSTALLATION_NAME=consent SERVER_DIR=installations/$INSTALLATION_NAME CONFIG_TEMPLATE=consent From cf397efed5e46b59b1609f4c16ddb12aab483bdf Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Sep 2018 16:50:33 +0200 Subject: [PATCH 0125/2372] disable LL tests on travis CI --- run.sh | 2 +- src/scenario.js | 15 ++++++++++++--- start.js | 3 ++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/run.sh b/run.sh index 02b2e4cbdf..daa3bd222c 100755 --- a/run.sh +++ b/run.sh @@ -15,5 +15,5 @@ trap 'handle_error' ERR ./synapse/start.sh ./riot/start.sh -node start.js +node start.js --travis stop_servers diff --git a/src/scenario.js b/src/scenario.js index 12cff7d498..2fd52de679 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -22,7 +22,7 @@ const roomDirectoryScenarios = require('./scenarios/directory'); const lazyLoadingScenarios = require('./scenarios/lazy-loading'); const e2eEncryptionScenarios = require('./scenarios/e2e-encryption'); -module.exports = async function scenario(createSession, restCreator) { +module.exports = async function scenario(createSession, restCreator, runningOnTravis) { async function createUser(username) { const session = await createSession(username); await signup(session, session.username, 'testtest', session.hsUrl); @@ -32,11 +32,20 @@ module.exports = async function scenario(createSession, restCreator) { const alice = await createUser("alice"); const bob = await createUser("bob"); - const charlies = await createRestUsers(restCreator); await roomDirectoryScenarios(alice, bob); await e2eEncryptionScenarios(alice, bob); - await lazyLoadingScenarios(alice, bob, charlies); + + // disable LL tests until we can run synapse on anything > than 2.7.7 as + // /admin/register fails with a missing method. + // either switch to python3 on synapse, + // blocked on https://github.com/matrix-org/synapse/issues/3900 + // or use a more recent version of ubuntu + // or switch to circleci? + if (!runningOnTravis) { + const charlies = await createRestUsers(restCreator); + await lazyLoadingScenarios(alice, bob, charlies); + } } async function createRestUsers(restCreator) { diff --git a/start.js b/start.js index 1c3f27bbe3..18ccb438ec 100644 --- a/start.js +++ b/start.js @@ -26,6 +26,7 @@ program .option('--windowed', "dont run tests headless", false) .option('--slow-mo', "run tests slower to follow whats going on", false) .option('--dev-tools', "open chrome devtools in browser window", false) + .option('--travis', "running on travis CI, disable tests known to break on Ubuntu 14.04 LTS", false) .parse(process.argv); const hsUrl = 'http://localhost:5005'; @@ -58,7 +59,7 @@ async function runTests() { let failure = false; try { - await scenario(createSession, restCreator); + await scenario(createSession, restCreator, program.travis); } catch(err) { failure = true; console.log('failure: ', err); From d47f782c214c62b3feebf393613eb8aa3e2c4896 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Sep 2018 16:51:22 +0200 Subject: [PATCH 0126/2372] Revert "increase timeout for server notices room" --- src/usecases/server-notices-consent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usecases/server-notices-consent.js b/src/usecases/server-notices-consent.js index eb42dcdaf7..25c3bb3bd5 100644 --- a/src/usecases/server-notices-consent.js +++ b/src/usecases/server-notices-consent.js @@ -19,7 +19,7 @@ const acceptInvite = require("./accept-invite") module.exports = async function acceptServerNoticesInviteAndConsent(session) { await acceptInvite(session, "Server Notices"); session.log.step(`accepts terms & conditions`); - const consentLink = await session.waitAndQuery(".mx_EventTile_body a", 10000); + const consentLink = await session.waitAndQuery(".mx_EventTile_body a"); const termsPagePromise = session.waitForNewPage(); await consentLink.click(); const termsPage = await termsPagePromise; From 70eb4805534bc81f6052d62464f260093cca60cc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Sep 2018 16:51:50 +0200 Subject: [PATCH 0127/2372] Revert "use patched synapse so admin rest api works with python 2.7.6" --- synapse/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/install.sh b/synapse/install.sh index 1ae2212e7e..37dfd7d7e2 100755 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -1,6 +1,6 @@ #!/bin/bash # config -SYNAPSE_BRANCH=bwindels/adminapibeforepy277 +SYNAPSE_BRANCH=develop INSTALLATION_NAME=consent SERVER_DIR=installations/$INSTALLATION_NAME CONFIG_TEMPLATE=consent From 13b20bb1924690c125c78f159623733c183fdff2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 19 Sep 2018 10:56:39 +0200 Subject: [PATCH 0128/2372] pass parameters through instead of hardcoding --travis --- run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.sh b/run.sh index daa3bd222c..569940e1ae 100755 --- a/run.sh +++ b/run.sh @@ -15,5 +15,5 @@ trap 'handle_error' ERR ./synapse/start.sh ./riot/start.sh -node start.js --travis +node start.js $@ stop_servers From a637ad85ad27eb48fdf39463bcf8d4cbf5ad137e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 21 Sep 2018 11:25:26 +0200 Subject: [PATCH 0129/2372] set a room alias for a public room, as required now --- src/scenarios/directory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scenarios/directory.js b/src/scenarios/directory.js index 3b87d64d78..cfe72ccef3 100644 --- a/src/scenarios/directory.js +++ b/src/scenarios/directory.js @@ -25,7 +25,7 @@ module.exports = async function roomDirectoryScenarios(alice, bob) { console.log(" creating a public room and join through directory:"); const room = 'test'; await createRoom(alice, room); - await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests"}); + await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests", alias: "#test"}); await join(bob, room); //looks up room in directory const bobMessage = "hi Alice!"; await sendMessage(bob, bobMessage); From 8ee7623d9010dc5fa1e35404084747c5b97953f6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Sep 2018 18:01:10 +0100 Subject: [PATCH 0130/2372] current tests need riot develop to set a room alias without a domain name --- riot/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/riot/install.sh b/riot/install.sh index 209926d4c5..9b5a0a0757 100755 --- a/riot/install.sh +++ b/riot/install.sh @@ -1,5 +1,5 @@ #!/bin/bash -RIOT_BRANCH=master +RIOT_BRANCH=develop BASE_DIR=$(readlink -f $(dirname $0)) if [ -d $BASE_DIR/riot-web ]; then From 04b64dbae9f2a4ea0169ed3224921ea051bc91fc Mon Sep 17 00:00:00 2001 From: Tom Lant Date: Tue, 25 Sep 2018 18:45:08 +0100 Subject: [PATCH 0131/2372] Some changes to make the testing script run on mac, too, + a multithreaded server for riot --- riot/install.sh | 21 +++++++++++++++++++-- riot/start.sh | 11 +++++++---- riot/stop.sh | 4 +++- run.sh | 1 + synapse/install.sh | 27 ++++++++++++++++++++------- synapse/start.sh | 6 ++++-- synapse/stop.sh | 4 +++- 7 files changed, 57 insertions(+), 17 deletions(-) diff --git a/riot/install.sh b/riot/install.sh index 209926d4c5..9b85b4cb13 100755 --- a/riot/install.sh +++ b/riot/install.sh @@ -1,12 +1,29 @@ #!/bin/bash -RIOT_BRANCH=master +set -e -BASE_DIR=$(readlink -f $(dirname $0)) +RIOT_BRANCH=master +BASE_DIR=$(cd $(dirname $0) && pwd) if [ -d $BASE_DIR/riot-web ]; then echo "riot is already installed" exit fi +# Install ComplexHttpServer (a drop in replacement for Python's SimpleHttpServer +# but with support for multiple threads) into a virtualenv. +( + virtualenv $BASE_DIR/env + source $BASE_DIR/env/bin/activate + + # Having been bitten by pip SSL fail too many times, I don't trust the existing pip + # to be able to --upgrade itself, so grab a new one fresh from source. + curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py + python get-pip.py + + pip install ComplexHttpServer + + deactivate +) + cd $BASE_DIR curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --output riot.zip unzip -q riot.zip diff --git a/riot/start.sh b/riot/start.sh index 0af9f4faef..be226ed257 100755 --- a/riot/start.sh +++ b/riot/start.sh @@ -1,6 +1,8 @@ -#!/bin/bash +#!/usr/bin/env bash +set -e + PORT=5000 -BASE_DIR=$(readlink -f $(dirname $0)) +BASE_DIR=$(cd $(dirname $0) && pwd) PIDFILE=$BASE_DIR/riot.pid CONFIG_BACKUP=config.e2etests_backup.json @@ -21,7 +23,8 @@ cp $BASE_DIR/config-template/config.json . LOGFILE=$(mktemp) # run web server in the background, showing output on error ( - python -m SimpleHTTPServer $PORT > $LOGFILE 2>&1 & + source $BASE_DIR/env/bin/activate + python -m ComplexHTTPServer $PORT > $LOGFILE 2>&1 & PID=$! echo $PID > $PIDFILE # wait so subshell does not exit @@ -40,7 +43,7 @@ LOGFILE=$(mktemp) )& # to be able to return the exit code for immediate errors (like address already in use) # we wait for a short amount of time in the background and exit when the first -# child process exists +# child process exits sleep 0.5 & # wait the first child process to exit (python or sleep) wait -n; RESULT=$? diff --git a/riot/stop.sh b/riot/stop.sh index a3e07f574f..eb99fa11cc 100755 --- a/riot/stop.sh +++ b/riot/stop.sh @@ -1,5 +1,7 @@ #!/bin/bash -BASE_DIR=$(readlink -f $(dirname $0)) +set -e + +BASE_DIR=$(cd $(dirname $0) && pwd) PIDFILE=riot.pid CONFIG_BACKUP=config.e2etests_backup.json diff --git a/run.sh b/run.sh index 569940e1ae..0e03b733ce 100755 --- a/run.sh +++ b/run.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e stop_servers() { ./riot/stop.sh diff --git a/synapse/install.sh b/synapse/install.sh index 37dfd7d7e2..b80b1ac705 100755 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -1,4 +1,6 @@ #!/bin/bash +set -e + # config SYNAPSE_BRANCH=develop INSTALLATION_NAME=consent @@ -6,7 +8,7 @@ SERVER_DIR=installations/$INSTALLATION_NAME CONFIG_TEMPLATE=consent PORT=5005 # set current directory to script directory -BASE_DIR=$(readlink -f $(dirname $0)) +BASE_DIR=$(cd $(dirname $0) && pwd) if [ -d $BASE_DIR/$SERVER_DIR ]; then echo "synapse is already installed" @@ -22,9 +24,17 @@ mv synapse-$SYNAPSE_BRANCH $SERVER_DIR cd $SERVER_DIR virtualenv -p python2.7 env source env/bin/activate -pip install --upgrade pip + +# Having been bitten by pip SSL fail too many times, I don't trust the existing pip +# to be able to --upgrade itself, so grab a new one fresh from source. +curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py +python get-pip.py + pip install --upgrade setuptools +python synapse/python_dependencies.py | xargs pip install +pip install lxml mock pip install . + python -m synapse.app.homeserver \ --server-name localhost \ --config-path homeserver.yaml \ @@ -32,8 +42,11 @@ python -m synapse.app.homeserver \ --report-stats=no # apply configuration cp -r $BASE_DIR/config-templates/$CONFIG_TEMPLATE/. ./ -sed -i "s#{{SYNAPSE_ROOT}}#$(pwd)/#g" homeserver.yaml -sed -i "s#{{SYNAPSE_PORT}}#${PORT}#g" homeserver.yaml -sed -i "s#{{FORM_SECRET}}#$(uuidgen)#g" homeserver.yaml -sed -i "s#{{REGISTRATION_SHARED_SECRET}}#$(uuidgen)#g" homeserver.yaml -sed -i "s#{{MACAROON_SECRET_KEY}}#$(uuidgen)#g" homeserver.yaml + +# Hashes used instead of slashes because we'll get a value back from $(pwd) that'll be +# full of un-escapable slashes. +sed -i '' "s#{{SYNAPSE_ROOT}}#$(pwd)/#g" homeserver.yaml +sed -i '' "s#{{SYNAPSE_PORT}}#${PORT}#g" homeserver.yaml +sed -i '' "s#{{FORM_SECRET}}#$(uuidgen)#g" homeserver.yaml +sed -i '' "s#{{REGISTRATION_SHARED_SECRET}}#$(uuidgen)#g" homeserver.yaml +sed -i '' "s#{{MACAROON_SECRET_KEY}}#$(uuidgen)#g" homeserver.yaml diff --git a/synapse/start.sh b/synapse/start.sh index 12b89b31ed..379de3850c 100755 --- a/synapse/start.sh +++ b/synapse/start.sh @@ -1,5 +1,7 @@ #!/bin/bash -BASE_DIR=$(readlink -f $(dirname $0)) +set -e + +BASE_DIR=$(cd $(dirname $0) && pwd) cd $BASE_DIR cd installations/consent source env/bin/activate @@ -9,4 +11,4 @@ EXIT_CODE=$? if [ $EXIT_CODE -ne 0 ]; then cat $LOGFILE fi -exit $EXIT_CODE \ No newline at end of file +exit $EXIT_CODE diff --git a/synapse/stop.sh b/synapse/stop.sh index d83ddd0e4a..a7716744ef 100755 --- a/synapse/stop.sh +++ b/synapse/stop.sh @@ -1,5 +1,7 @@ #!/bin/bash -BASE_DIR=$(readlink -f $(dirname $0)) +set -e + +BASE_DIR=$(cd $(dirname $0) && pwd) cd $BASE_DIR cd installations/consent source env/bin/activate From 861af62208b5b6642c8dbee4f4f714d7157d8d0a Mon Sep 17 00:00:00 2001 From: Tom Lant Date: Thu, 27 Sep 2018 13:21:45 +0100 Subject: [PATCH 0132/2372] Make the sed usage cross-platform compatible --- synapse/install.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/synapse/install.sh b/synapse/install.sh index b80b1ac705..c83ca6512a 100755 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -45,8 +45,11 @@ cp -r $BASE_DIR/config-templates/$CONFIG_TEMPLATE/. ./ # Hashes used instead of slashes because we'll get a value back from $(pwd) that'll be # full of un-escapable slashes. -sed -i '' "s#{{SYNAPSE_ROOT}}#$(pwd)/#g" homeserver.yaml -sed -i '' "s#{{SYNAPSE_PORT}}#${PORT}#g" homeserver.yaml -sed -i '' "s#{{FORM_SECRET}}#$(uuidgen)#g" homeserver.yaml -sed -i '' "s#{{REGISTRATION_SHARED_SECRET}}#$(uuidgen)#g" homeserver.yaml -sed -i '' "s#{{MACAROON_SECRET_KEY}}#$(uuidgen)#g" homeserver.yaml +# Manually directing output to .templated file and then manually renaming back on top +# of the original file because -i is a nonstandard sed feature which is implemented +# differently, across os X and ubuntu at least +sed "s#{{SYNAPSE_ROOT}}#$(pwd)/#g" homeserver.yaml > homeserver.yaml.templated && mv homeserver.yaml.templated homeserver.yaml +sed "s#{{SYNAPSE_PORT}}#${PORT}#g" homeserver.yaml > homeserver.yaml.templated && mv homeserver.yaml.templated homeserver.yaml +sed "s#{{FORM_SECRET}}#$(uuidgen)#g" homeserver.yaml > homeserver.yaml.templated && mv homeserver.yaml.templated homeserver.yaml +sed "s#{{REGISTRATION_SHARED_SECRET}}#$(uuidgen)#g" homeserver.yaml > homeserver.yaml.templated && mv homeserver.yaml.templated homeserver.yaml +sed "s#{{MACAROON_SECRET_KEY}}#$(uuidgen)#g" homeserver.yaml > homeserver.yaml.templated && mv homeserver.yaml.templated homeserver.yaml From 1147508c345d7744185ec9619ab7e1e4cc4d9356 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Sep 2018 18:09:27 +0100 Subject: [PATCH 0133/2372] list of tests we want to write --- TODO.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000000..4cbcba801b --- /dev/null +++ b/TODO.md @@ -0,0 +1,4 @@ +join a peekable room by directory +join a peekable room by invite +join a non-peekable room by directory +join a non-peekable room by invite From b2bd134945a907c6bdd29d98785c33737a13db58 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 8 Oct 2018 16:58:01 +0200 Subject: [PATCH 0134/2372] add config file instructions to run with --riot-url --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index acfeed8257..4f4f9217e3 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,10 @@ It's important to always stop and start synapse before each run of the tests to start.js accepts the following parameters that can help running the tests locally: - `--no-logs` dont show the excessive logging show by default (meant for CI), just where the test failed. - - `--riot-url ` don't use the riot copy and static server provided by the tests, but use a running server like the webpack watch server to run the tests against. Make sure to have `welcomeUserId` disabled in your config as the tests assume there is no riot-bot currently. + - `--riot-url ` don't use the riot copy and static server provided by the tests, but use a running server like the webpack watch server to run the tests against. Make sure to have the following local config: + - `welcomeUserId` disabled as the tests assume there is no riot-bot currently. Make sure to set the default homeserver to + - `"default_hs_url": "http://localhost:5005"`, to use the e2e tests synapse (the tests use the default HS to run against atm) + - `"feature_lazyloading": "labs"`, currently assumes lazy loading needs to be turned on in the settings, will change soon. - `--slow-mo` run the tests a bit slower, so it's easier to follow along with `--windowed`. - `--windowed` run the tests in an actual browser window Try to limit interacting with the windows while the tests are running. Hovering over the window tends to fail the tests, dragging the title bar should be fine though. - `--dev-tools` open the devtools in the browser window, only applies if `--windowed` is set as well. From 1a2254677c91dc291180a26a5f736bb3e852e6ca Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 9 Oct 2018 15:15:03 +0200 Subject: [PATCH 0135/2372] test leaving members disappear from memberlist --- src/rest/multi.js | 7 ++++++- src/rest/session.js | 14 +++++++++++++- src/scenarios/lazy-loading.js | 15 +++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/rest/multi.js b/src/rest/multi.js index 3d24245ddf..b930a27c1e 100644 --- a/src/rest/multi.js +++ b/src/rest/multi.js @@ -59,6 +59,11 @@ module.exports = class RestMultiSession { this.log.done(); return new RestMultiRoom(rooms, roomIdOrAlias, this.log); } + + room(roomIdOrAlias) { + const rooms = this.sessions.map(s => s.room(roomIdOrAlias)); + return new RestMultiRoom(rooms, roomIdOrAlias, this.log); + } } class RestMultiRoom { @@ -82,7 +87,7 @@ class RestMultiRoom { this.log.step(`leave ${this.roomIdOrAlias}`) await Promise.all(this.rooms.map(async (r) => { r.log.mute(); - await r.leave(message); + await r.leave(); r.log.unmute(); })); this.log.done(); diff --git a/src/rest/session.js b/src/rest/session.js index 21922a69f1..de05cd4b5c 100644 --- a/src/rest/session.js +++ b/src/rest/session.js @@ -24,6 +24,7 @@ module.exports = class RestSession { this.log = new Logger(credentials.userId); this._credentials = credentials; this._displayName = null; + this._rooms = {}; } userId() { @@ -51,7 +52,18 @@ module.exports = class RestSession { this.log.step(`joins ${roomIdOrAlias}`); const {room_id} = await this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`); this.log.done(); - return new RestRoom(this, room_id, this.log); + const room = new RestRoom(this, room_id, this.log); + this._rooms[room_id] = room; + this._rooms[roomIdOrAlias] = room; + return room; + } + + room(roomIdOrAlias) { + if (this._rooms.hasOwnProperty(roomIdOrAlias)) { + return this._rooms[roomIdOrAlias]; + } else { + throw new Error(`${this._credentials.userId} is not in ${roomIdOrAlias}`); + } } async createRoom(name, options) { diff --git a/src/scenarios/lazy-loading.js b/src/scenarios/lazy-loading.js index c33e83215c..7fd67153f0 100644 --- a/src/scenarios/lazy-loading.js +++ b/src/scenarios/lazy-loading.js @@ -40,6 +40,10 @@ module.exports = async function lazyLoadingScenarios(alice, bob, charlies) { await checkMemberList(alice, charly1to5); await joinCharliesWhileAliceIsOffline(alice, charly6to10); await checkMemberList(alice, charly6to10); + await charlies.room(alias).leave(); + await delay(1000); + await checkMemberListLacksCharlies(alice, charlies); + await checkMemberListLacksCharlies(bob, charlies); } const room = "Lazy Loading Test"; @@ -92,6 +96,17 @@ async function checkMemberList(alice, charlies) { alice.log.done(); } +async function checkMemberListLacksCharlies(session, charlies) { + session.log.step(`checks the memberlist doesn't contain ${charlies.log.username}`); + const displayNames = (await getMembersInMemberlist(session)).map((m) => m.displayName); + charlies.sessions.forEach((charly) => { + assert(!displayNames.includes(charly.displayName()), + `${charly.displayName()} should not be in the member list, ` + + `only have ${displayNames}`); + }); + session.log.done(); +} + async function joinCharliesWhileAliceIsOffline(alice, charly6to10) { await alice.setOffline(true); await delay(1000); From f607cb27027d0f5180ac69b7a040672502912ca2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 1 Nov 2018 17:55:48 -0600 Subject: [PATCH 0136/2372] Fix the registration process to handle m.login.terms auth --- src/usecases/signup.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/usecases/signup.js b/src/usecases/signup.js index b715e111a1..dfd97a975f 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -59,6 +59,12 @@ module.exports = async function signup(session, username, password, homeserver) //confirm dialog saying you cant log back in without e-mail const continueButton = await session.waitAndQuery('.mx_QuestionDialog button.mx_Dialog_primary'); await continueButton.click(); + + //find the privacy policy checkbox and check it + //this should automatically move ahead with registration + const policyCheckbox = await session.waitAndQuery('.mx_Login_box input[type="checkbox"]'); + await policyCheckbox.click(); + //wait for registration to finish so the hash gets set //onhashchange better? await session.delay(2000); From d57a56d7a8e873b57dababd511b103d61b94b593 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 1 Nov 2018 18:20:55 -0600 Subject: [PATCH 0137/2372] There is no more server notices invite on signup --- src/scenario.js | 2 -- src/usecases/consent.js | 27 ---------------------- src/usecases/server-notices-consent.js | 31 -------------------------- src/usecases/signup.js | 1 - 4 files changed, 61 deletions(-) delete mode 100644 src/usecases/consent.js delete mode 100644 src/usecases/server-notices-consent.js diff --git a/src/scenario.js b/src/scenario.js index 2fd52de679..5b9d1f2906 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -17,7 +17,6 @@ limitations under the License. const {range} = require('./util'); const signup = require('./usecases/signup'); -const acceptServerNoticesInviteAndConsent = require('./usecases/server-notices-consent'); const roomDirectoryScenarios = require('./scenarios/directory'); const lazyLoadingScenarios = require('./scenarios/lazy-loading'); const e2eEncryptionScenarios = require('./scenarios/e2e-encryption'); @@ -26,7 +25,6 @@ module.exports = async function scenario(createSession, restCreator, runningOnTr async function createUser(username) { const session = await createSession(username); await signup(session, session.username, 'testtest', session.hsUrl); - await acceptServerNoticesInviteAndConsent(session); return session; } diff --git a/src/usecases/consent.js b/src/usecases/consent.js deleted file mode 100644 index b4a6289fca..0000000000 --- a/src/usecases/consent.js +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -const assert = require('assert'); - -module.exports = async function acceptTerms(session) { - const reviewTermsButton = await session.waitAndQuery('.mx_QuestionDialog button.mx_Dialog_primary'); - const termsPagePromise = session.waitForNewPage(); - await reviewTermsButton.click(); - const termsPage = await termsPagePromise; - const acceptButton = await termsPage.$('input[type=submit]'); - await acceptButton.click(); - await session.delay(1000); //TODO yuck, timers -} diff --git a/src/usecases/server-notices-consent.js b/src/usecases/server-notices-consent.js deleted file mode 100644 index 25c3bb3bd5..0000000000 --- a/src/usecases/server-notices-consent.js +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -const assert = require('assert'); -const acceptInvite = require("./accept-invite") -module.exports = async function acceptServerNoticesInviteAndConsent(session) { - await acceptInvite(session, "Server Notices"); - session.log.step(`accepts terms & conditions`); - const consentLink = await session.waitAndQuery(".mx_EventTile_body a"); - const termsPagePromise = session.waitForNewPage(); - await consentLink.click(); - const termsPage = await termsPagePromise; - const acceptButton = await termsPage.$('input[type=submit]'); - await acceptButton.click(); - await session.delay(1000); //TODO yuck, timers - await termsPage.close(); - session.log.done(); -} diff --git a/src/usecases/signup.js b/src/usecases/signup.js index dfd97a975f..bf2a512a91 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -const acceptTerms = require('./consent'); const assert = require('assert'); module.exports = async function signup(session, username, password, homeserver) { From 1a0f09543bc3bf2141967c9098cfeee6f619124e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 7 Nov 2018 15:26:30 -0700 Subject: [PATCH 0138/2372] Tell synapse to require consent at registration To fix issues where the tests can't correctly test terms auth. --- synapse/config-templates/consent/homeserver.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/synapse/config-templates/consent/homeserver.yaml b/synapse/config-templates/consent/homeserver.yaml index 9fa16ebe5f..4f3837a878 100644 --- a/synapse/config-templates/consent/homeserver.yaml +++ b/synapse/config-templates/consent/homeserver.yaml @@ -674,6 +674,7 @@ user_consent: block_events_error: >- To continue using this homeserver you must review and agree to the terms and conditions at %(consent_uri)s + require_at_registration: true From 2e839d545adb848e5578e6ae53b16a5ebc595710 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Nov 2018 20:23:15 -0700 Subject: [PATCH 0139/2372] Click the 'Accept' button as part of the signup process Part of https://github.com/vector-im/riot-web/issues/7700 --- src/usecases/signup.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/usecases/signup.js b/src/usecases/signup.js index bf2a512a91..825a2c27fa 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -60,10 +60,13 @@ module.exports = async function signup(session, username, password, homeserver) await continueButton.click(); //find the privacy policy checkbox and check it - //this should automatically move ahead with registration const policyCheckbox = await session.waitAndQuery('.mx_Login_box input[type="checkbox"]'); await policyCheckbox.click(); + //now click the 'Accept' button to agree to the privacy policy + const acceptButton = await session.waitAndQuery('.mx_InteractiveAuthEntryComponents_termsSubmit'); + await acceptButton.click(); + //wait for registration to finish so the hash gets set //onhashchange better? await session.delay(2000); From 19c4f4a8c6aa039aa6269299468b0c006e908602 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 21 Dec 2018 18:36:27 -0700 Subject: [PATCH 0140/2372] Install jinja2 --- synapse/install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/synapse/install.sh b/synapse/install.sh index 37dfd7d7e2..1b27f0952d 100755 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -25,6 +25,7 @@ source env/bin/activate pip install --upgrade pip pip install --upgrade setuptools pip install . +pip install jinja2 # We use the ConsentResource, which requires jinja2 python -m synapse.app.homeserver \ --server-name localhost \ --config-path homeserver.yaml \ From 7ac19b0beab275460d03863fd10cba2f43ff6849 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 29 Mar 2019 11:25:21 +0100 Subject: [PATCH 0141/2372] adjust synapse install script for python3 and config file changes --- .../config-templates/consent/homeserver.yaml | 1030 ++++++++++++----- synapse/install.sh | 5 +- 2 files changed, 717 insertions(+), 318 deletions(-) diff --git a/synapse/config-templates/consent/homeserver.yaml b/synapse/config-templates/consent/homeserver.yaml index 4f3837a878..222ddb956f 100644 --- a/synapse/config-templates/consent/homeserver.yaml +++ b/synapse/config-templates/consent/homeserver.yaml @@ -1,19 +1,313 @@ # vim:ft=yaml -# PEM encoded X509 certificate for TLS. -# You can replace the self-signed certificate that synapse -# autogenerates on launch with your own SSL certificate + key pair -# if you like. Any required intermediary certificates can be -# appended after the primary certificate in hierarchical order. -tls_certificate_path: "{{SYNAPSE_ROOT}}localhost.tls.crt" -# PEM encoded private key for TLS -tls_private_key_path: "{{SYNAPSE_ROOT}}localhost.tls.key" +## Server ## -# PEM dh parameters for ephemeral keys -tls_dh_params_path: "{{SYNAPSE_ROOT}}localhost.tls.dh" +# The domain name of the server, with optional explicit port. +# This is used by remote servers to connect to this server, +# e.g. matrix.org, localhost:8080, etc. +# This is also the last part of your UserID. +# +server_name: "localhost" -# Don't bind to the https port -no_tls: True +# When running as a daemon, the file to store the pid in +# +pid_file: {{SYNAPSE_ROOT}}homeserver.pid + +# CPU affinity mask. Setting this restricts the CPUs on which the +# process will be scheduled. It is represented as a bitmask, with the +# lowest order bit corresponding to the first logical CPU and the +# highest order bit corresponding to the last logical CPU. Not all CPUs +# may exist on a given system but a mask may specify more CPUs than are +# present. +# +# For example: +# 0x00000001 is processor #0, +# 0x00000003 is processors #0 and #1, +# 0xFFFFFFFF is all processors (#0 through #31). +# +# Pinning a Python process to a single CPU is desirable, because Python +# is inherently single-threaded due to the GIL, and can suffer a +# 30-40% slowdown due to cache blow-out and thread context switching +# if the scheduler happens to schedule the underlying threads across +# different cores. See +# https://www.mirantis.com/blog/improve-performance-python-programs-restricting-single-cpu/. +# +# This setting requires the affinity package to be installed! +# +#cpu_affinity: 0xFFFFFFFF + +# The path to the web client which will be served at /_matrix/client/ +# if 'webclient' is configured under the 'listeners' configuration. +# +#web_client_location: "/path/to/web/root" + +# The public-facing base URL that clients use to access this HS +# (not including _matrix/...). This is the same URL a user would +# enter into the 'custom HS URL' field on their client. If you +# use synapse with a reverse proxy, this should be the URL to reach +# synapse via the proxy. +# +public_baseurl: http://localhost:{{SYNAPSE_PORT}}/ + +# Set the soft limit on the number of file descriptors synapse can use +# Zero is used to indicate synapse should set the soft limit to the +# hard limit. +# +#soft_file_limit: 0 + +# Set to false to disable presence tracking on this homeserver. +# +#use_presence: false + +# The GC threshold parameters to pass to `gc.set_threshold`, if defined +# +#gc_thresholds: [700, 10, 10] + +# Set the limit on the returned events in the timeline in the get +# and sync operations. The default value is -1, means no upper limit. +# +#filter_timeline_limit: 5000 + +# Whether room invites to users on this server should be blocked +# (except those sent by local server admins). The default is False. +# +#block_non_admin_invites: True + +# Room searching +# +# If disabled, new messages will not be indexed for searching and users +# will receive errors when searching for messages. Defaults to enabled. +# +#enable_search: false + +# Restrict federation to the following whitelist of domains. +# N.B. we recommend also firewalling your federation listener to limit +# inbound federation traffic as early as possible, rather than relying +# purely on this application-layer restriction. If not specified, the +# default is to whitelist everything. +# +#federation_domain_whitelist: +# - lon.example.com +# - nyc.example.com +# - syd.example.com + +# List of ports that Synapse should listen on, their purpose and their +# configuration. +# +# Options for each listener include: +# +# port: the TCP port to bind to +# +# bind_addresses: a list of local addresses to listen on. The default is +# 'all local interfaces'. +# +# type: the type of listener. Normally 'http', but other valid options are: +# 'manhole' (see docs/manhole.md), +# 'metrics' (see docs/metrics-howto.rst), +# 'replication' (see docs/workers.rst). +# +# tls: set to true to enable TLS for this listener. Will use the TLS +# key/cert specified in tls_private_key_path / tls_certificate_path. +# +# x_forwarded: Only valid for an 'http' listener. Set to true to use the +# X-Forwarded-For header as the client IP. Useful when Synapse is +# behind a reverse-proxy. +# +# resources: Only valid for an 'http' listener. A list of resources to host +# on this port. Options for each resource are: +# +# names: a list of names of HTTP resources. See below for a list of +# valid resource names. +# +# compress: set to true to enable HTTP comression for this resource. +# +# additional_resources: Only valid for an 'http' listener. A map of +# additional endpoints which should be loaded via dynamic modules. +# +# Valid resource names are: +# +# client: the client-server API (/_matrix/client). Also implies 'media' and +# 'static'. +# +# consent: user consent forms (/_matrix/consent). See +# docs/consent_tracking.md. +# +# federation: the server-server API (/_matrix/federation). Also implies +# 'media', 'keys', 'openid' +# +# keys: the key discovery API (/_matrix/keys). +# +# media: the media API (/_matrix/media). +# +# metrics: the metrics interface. See docs/metrics-howto.rst. +# +# openid: OpenID authentication. +# +# replication: the HTTP replication API (/_synapse/replication). See +# docs/workers.rst. +# +# static: static resources under synapse/static (/_matrix/static). (Mostly +# useful for 'fallback authentication'.) +# +# webclient: A web client. Requires web_client_location to be set. +# +listeners: + # TLS-enabled listener: for when matrix traffic is sent directly to synapse. + # + # Disabled by default. To enable it, uncomment the following. (Note that you + # will also need to give Synapse a TLS key and certificate: see the TLS section + # below.) + # + #- port: 8448 + # type: http + # tls: true + # resources: + # - names: [client, federation] + + # Unsecure HTTP listener: for when matrix traffic passes through a reverse proxy + # that unwraps TLS. + # + # If you plan to use a reverse proxy, please see + # https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.rst. + # + - port: {{SYNAPSE_PORT}} + tls: false + bind_addresses: ['::1', '127.0.0.1'] + type: http + x_forwarded: true + + resources: + - names: [client, federation] + compress: false + + # example additonal_resources: + # + #additional_resources: + # "/_matrix/my/custom/endpoint": + # module: my_module.CustomRequestHandler + # config: {} + + # Turn on the twisted ssh manhole service on localhost on the given + # port. + # + #- port: 9000 + # bind_addresses: ['::1', '127.0.0.1'] + # type: manhole + + +## Homeserver blocking ## + +# How to reach the server admin, used in ResourceLimitError +# +#admin_contact: 'mailto:admin@server.com' + +# Global blocking +# +#hs_disabled: False +#hs_disabled_message: 'Human readable reason for why the HS is blocked' +#hs_disabled_limit_type: 'error code(str), to help clients decode reason' + +# Monthly Active User Blocking +# +#limit_usage_by_mau: False +#max_mau_value: 50 +#mau_trial_days: 2 + +# If enabled, the metrics for the number of monthly active users will +# be populated, however no one will be limited. If limit_usage_by_mau +# is true, this is implied to be true. +# +#mau_stats_only: False + +# Sometimes the server admin will want to ensure certain accounts are +# never blocked by mau checking. These accounts are specified here. +# +#mau_limit_reserved_threepids: +# - medium: 'email' +# address: 'reserved_user@example.com' + + +## TLS ## + +# PEM-encoded X509 certificate for TLS. +# This certificate, as of Synapse 1.0, will need to be a valid and verifiable +# certificate, signed by a recognised Certificate Authority. +# +# See 'ACME support' below to enable auto-provisioning this certificate via +# Let's Encrypt. +# +# If supplying your own, be sure to use a `.pem` file that includes the +# full certificate chain including any intermediate certificates (for +# instance, if using certbot, use `fullchain.pem` as your certificate, +# not `cert.pem`). +# +#tls_certificate_path: "{{SYNAPSE_ROOT}}localhost.tls.crt" + +# PEM-encoded private key for TLS +# +#tls_private_key_path: "{{SYNAPSE_ROOT}}localhost.tls.key" + +# ACME support: This will configure Synapse to request a valid TLS certificate +# for your configured `server_name` via Let's Encrypt. +# +# Note that provisioning a certificate in this way requires port 80 to be +# routed to Synapse so that it can complete the http-01 ACME challenge. +# By default, if you enable ACME support, Synapse will attempt to listen on +# port 80 for incoming http-01 challenges - however, this will likely fail +# with 'Permission denied' or a similar error. +# +# There are a couple of potential solutions to this: +# +# * If you already have an Apache, Nginx, or similar listening on port 80, +# you can configure Synapse to use an alternate port, and have your web +# server forward the requests. For example, assuming you set 'port: 8009' +# below, on Apache, you would write: +# +# ProxyPass /.well-known/acme-challenge http://localhost:8009/.well-known/acme-challenge +# +# * Alternatively, you can use something like `authbind` to give Synapse +# permission to listen on port 80. +# +acme: + # ACME support is disabled by default. Uncomment the following line + # (and tls_certificate_path and tls_private_key_path above) to enable it. + # + #enabled: true + + # Endpoint to use to request certificates. If you only want to test, + # use Let's Encrypt's staging url: + # https://acme-staging.api.letsencrypt.org/directory + # + #url: https://acme-v01.api.letsencrypt.org/directory + + # Port number to listen on for the HTTP-01 challenge. Change this if + # you are forwarding connections through Apache/Nginx/etc. + # + #port: 80 + + # Local addresses to listen on for incoming connections. + # Again, you may want to change this if you are forwarding connections + # through Apache/Nginx/etc. + # + #bind_addresses: ['::', '0.0.0.0'] + + # How many days remaining on a certificate before it is renewed. + # + #reprovision_threshold: 30 + + # The domain that the certificate should be for. Normally this + # should be the same as your Matrix domain (i.e., 'server_name'), but, + # by putting a file at 'https:///.well-known/matrix/server', + # you can delegate incoming traffic to another server. If you do that, + # you should give the target of the delegation here. + # + # For example: if your 'server_name' is 'example.com', but + # 'https://example.com/.well-known/matrix/server' delegates to + # 'matrix.example.com', you should put 'matrix.example.com' here. + # + # If not set, defaults to your 'server_name'. + # + #domain: matrix.example.com # List of allowed TLS fingerprints for this server to publish along # with the signing keys for this server. Other matrix servers that @@ -40,153 +334,12 @@ no_tls: True # openssl x509 -outform DER | openssl sha256 -binary | base64 | tr -d '=' # or by checking matrix.org/federationtester/api/report?server_name=$host # -tls_fingerprints: [] -# tls_fingerprints: [{"sha256": ""}] +#tls_fingerprints: [{"sha256": ""}] -## Server ## -# The domain name of the server, with optional explicit port. -# This is used by remote servers to connect to this server, -# e.g. matrix.org, localhost:8080, etc. -# This is also the last part of your UserID. -server_name: "localhost" +## Database ## -# When running as a daemon, the file to store the pid in -pid_file: {{SYNAPSE_ROOT}}homeserver.pid - -# CPU affinity mask. Setting this restricts the CPUs on which the -# process will be scheduled. It is represented as a bitmask, with the -# lowest order bit corresponding to the first logical CPU and the -# highest order bit corresponding to the last logical CPU. Not all CPUs -# may exist on a given system but a mask may specify more CPUs than are -# present. -# -# For example: -# 0x00000001 is processor #0, -# 0x00000003 is processors #0 and #1, -# 0xFFFFFFFF is all processors (#0 through #31). -# -# Pinning a Python process to a single CPU is desirable, because Python -# is inherently single-threaded due to the GIL, and can suffer a -# 30-40% slowdown due to cache blow-out and thread context switching -# if the scheduler happens to schedule the underlying threads across -# different cores. See -# https://www.mirantis.com/blog/improve-performance-python-programs-restricting-single-cpu/. -# -# cpu_affinity: 0xFFFFFFFF - -# Whether to serve a web client from the HTTP/HTTPS root resource. -web_client: True - -# The root directory to server for the above web client. -# If left undefined, synapse will serve the matrix-angular-sdk web client. -# Make sure matrix-angular-sdk is installed with pip if web_client is True -# and web_client_location is undefined -# web_client_location: "/path/to/web/root" - -# The public-facing base URL for the client API (not including _matrix/...) -public_baseurl: http://localhost:{{SYNAPSE_PORT}}/ - -# Set the soft limit on the number of file descriptors synapse can use -# Zero is used to indicate synapse should set the soft limit to the -# hard limit. -soft_file_limit: 0 - -# The GC threshold parameters to pass to `gc.set_threshold`, if defined -# gc_thresholds: [700, 10, 10] - -# Set the limit on the returned events in the timeline in the get -# and sync operations. The default value is -1, means no upper limit. -# filter_timeline_limit: 5000 - -# Whether room invites to users on this server should be blocked -# (except those sent by local server admins). The default is False. -# block_non_admin_invites: True - -# Restrict federation to the following whitelist of domains. -# N.B. we recommend also firewalling your federation listener to limit -# inbound federation traffic as early as possible, rather than relying -# purely on this application-layer restriction. If not specified, the -# default is to whitelist everything. -# -# federation_domain_whitelist: -# - lon.example.com -# - nyc.example.com -# - syd.example.com - -# List of ports that Synapse should listen on, their purpose and their -# configuration. -listeners: - # Main HTTPS listener - # For when matrix traffic is sent directly to synapse. - - - # The port to listen for HTTPS requests on. - port: 8448 - - # Local addresses to listen on. - # On Linux and Mac OS, `::` will listen on all IPv4 and IPv6 - # addresses by default. For most other OSes, this will only listen - # on IPv6. - bind_addresses: - - '::' - - '0.0.0.0' - - # This is a 'http' listener, allows us to specify 'resources'. - type: http - - tls: true - - # Use the X-Forwarded-For (XFF) header as the client IP and not the - # actual client IP. - x_forwarded: false - - # List of HTTP resources to serve on this listener. - resources: - - - # List of resources to host on this listener. - names: - - client # The client-server APIs, both v1 and v2 - - webclient # The bundled webclient. - - # Should synapse compress HTTP responses to clients that support it? - # This should be disabled if running synapse behind a load balancer - # that can do automatic compression. - compress: true - - - names: [federation] # Federation APIs - compress: false - - # optional list of additional endpoints which can be loaded via - # dynamic modules - # additional_resources: - # "/_matrix/my/custom/endpoint": - # module: my_module.CustomRequestHandler - # config: {} - - # Unsecure HTTP listener, - # For when matrix traffic passes through loadbalancer that unwraps TLS. - - port: {{SYNAPSE_PORT}} - tls: false - bind_addresses: ['::', '0.0.0.0'] - type: http - - x_forwarded: false - - resources: - - names: [client, webclient, consent] - compress: true - - names: [federation] - compress: false - - # Turn on the twisted ssh manhole service on localhost on the given - # port. - # - port: 9000 - # bind_addresses: ['::1', '127.0.0.1'] - # type: manhole - - -# Database configuration database: # The database engine name name: "sqlite3" @@ -196,98 +349,158 @@ database: database: ":memory:" # Number of events to cache in memory. -event_cache_size: "10K" +# +#event_cache_size: 10K +## Logging ## # A yaml python logging config file +# log_config: "{{SYNAPSE_ROOT}}localhost.log.config" ## Ratelimiting ## # Number of messages a client can send per second -rc_messages_per_second: 100 +# +#rc_messages_per_second: 0.2 # Number of message a client can send before being throttled -rc_message_burst_count: 20.0 +# +#rc_message_burst_count: 10.0 + +# Ratelimiting settings for registration and login. +# +# Each ratelimiting configuration is made of two parameters: +# - per_second: number of requests a client can send per second. +# - burst_count: number of requests a client can send before being throttled. +# +# Synapse currently uses the following configurations: +# - one for registration that ratelimits registration requests based on the +# client's IP address. +# - one for login that ratelimits login requests based on the client's IP +# address. +# - one for login that ratelimits login requests based on the account the +# client is attempting to log into. +# - one for login that ratelimits login requests based on the account the +# client is attempting to log into, based on the amount of failed login +# attempts for this account. +# +# The defaults are as shown below. +# +#rc_registration: +# per_second: 0.17 +# burst_count: 3 +# +#rc_login: +# address: +# per_second: 0.17 +# burst_count: 3 +# account: +# per_second: 0.17 +# burst_count: 3 +# failed_attempts: +# per_second: 0.17 +# burst_count: 3 # The federation window size in milliseconds -federation_rc_window_size: 1000 +# +#federation_rc_window_size: 1000 # The number of federation requests from a single server in a window # before the server will delay processing the request. -federation_rc_sleep_limit: 10 +# +#federation_rc_sleep_limit: 10 # The duration in milliseconds to delay processing events from # remote servers by if they go over the sleep limit. -federation_rc_sleep_delay: 500 +# +#federation_rc_sleep_delay: 500 # The maximum number of concurrent federation requests allowed # from a single server -federation_rc_reject_limit: 50 +# +#federation_rc_reject_limit: 50 # The number of federation requests to concurrently process from a # single server -federation_rc_concurrent: 3 +# +#federation_rc_concurrent: 3 + +# Target outgoing federation transaction frequency for sending read-receipts, +# per-room. +# +# If we end up trying to send out more read-receipts, they will get buffered up +# into fewer transactions. +# +#federation_rr_transactions_per_room_per_second: 50 # Directory where uploaded images and attachments are stored. +# media_store_path: "{{SYNAPSE_ROOT}}media_store" # Media storage providers allow media to be stored in different # locations. -# media_storage_providers: -# - module: file_system -# # Whether to write new local files. -# store_local: false -# # Whether to write new remote media -# store_remote: false -# # Whether to block upload requests waiting for write to this -# # provider to complete -# store_synchronous: false -# config: -# directory: /mnt/some/other/directory +# +#media_storage_providers: +# - module: file_system +# # Whether to write new local files. +# store_local: false +# # Whether to write new remote media +# store_remote: false +# # Whether to block upload requests waiting for write to this +# # provider to complete +# store_synchronous: false +# config: +# directory: /mnt/some/other/directory # Directory where in-progress uploads are stored. +# uploads_path: "{{SYNAPSE_ROOT}}uploads" # The largest allowed upload size in bytes -max_upload_size: "10M" +# +#max_upload_size: 10M # Maximum number of pixels that will be thumbnailed -max_image_pixels: "32M" +# +#max_image_pixels: 32M # Whether to generate new thumbnails on the fly to precisely match # the resolution requested by the client. If true then whenever # a new resolution is requested by the client the server will # generate a new thumbnail. If false the server will pick a thumbnail # from a precalculated list. -dynamic_thumbnails: false +# +#dynamic_thumbnails: false -# List of thumbnail to precalculate when an image is uploaded. -thumbnail_sizes: -- width: 32 - height: 32 - method: crop -- width: 96 - height: 96 - method: crop -- width: 320 - height: 240 - method: scale -- width: 640 - height: 480 - method: scale -- width: 800 - height: 600 - method: scale +# List of thumbnails to precalculate when an image is uploaded. +# +#thumbnail_sizes: +# - width: 32 +# height: 32 +# method: crop +# - width: 96 +# height: 96 +# method: crop +# - width: 320 +# height: 240 +# method: scale +# - width: 640 +# height: 480 +# method: scale +# - width: 800 +# height: 600 +# method: scale # Is the preview URL API enabled? If enabled, you *must* specify # an explicit url_preview_ip_range_blacklist of IPs that the spider is # denied from accessing. -url_preview_enabled: False +# +#url_preview_enabled: false # List of IP address CIDR ranges that the URL preview spider is denied # from accessing. There are no defaults: you must explicitly @@ -297,16 +510,16 @@ url_preview_enabled: False # synapse to issue arbitrary GET requests to your internal services, # causing serious security issues. # -# url_preview_ip_range_blacklist: -# - '127.0.0.0/8' -# - '10.0.0.0/8' -# - '172.16.0.0/12' -# - '192.168.0.0/16' -# - '100.64.0.0/10' -# - '169.254.0.0/16' -# - '::1/128' -# - 'fe80::/64' -# - 'fc00::/7' +#url_preview_ip_range_blacklist: +# - '127.0.0.0/8' +# - '10.0.0.0/8' +# - '172.16.0.0/12' +# - '192.168.0.0/16' +# - '100.64.0.0/10' +# - '169.254.0.0/16' +# - '::1/128' +# - 'fe80::/64' +# - 'fc00::/7' # # List of IP address CIDR ranges that the URL preview spider is allowed # to access even if they are specified in url_preview_ip_range_blacklist. @@ -314,8 +527,8 @@ url_preview_enabled: False # target IP ranges - e.g. for enabling URL previews for a specific private # website only visible in your network. # -# url_preview_ip_range_whitelist: -# - '192.168.1.1' +#url_preview_ip_range_whitelist: +# - '192.168.1.1' # Optional list of URL matches that the URL preview spider is # denied from accessing. You should use url_preview_ip_range_blacklist @@ -333,99 +546,118 @@ url_preview_enabled: False # specified component matches for a given list item succeed, the URL is # blacklisted. # -# url_preview_url_blacklist: -# # blacklist any URL with a username in its URI -# - username: '*' +#url_preview_url_blacklist: +# # blacklist any URL with a username in its URI +# - username: '*' # -# # blacklist all *.google.com URLs -# - netloc: 'google.com' -# - netloc: '*.google.com' +# # blacklist all *.google.com URLs +# - netloc: 'google.com' +# - netloc: '*.google.com' # -# # blacklist all plain HTTP URLs -# - scheme: 'http' +# # blacklist all plain HTTP URLs +# - scheme: 'http' # -# # blacklist http(s)://www.acme.com/foo -# - netloc: 'www.acme.com' -# path: '/foo' +# # blacklist http(s)://www.acme.com/foo +# - netloc: 'www.acme.com' +# path: '/foo' # -# # blacklist any URL with a literal IPv4 address -# - netloc: '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' +# # blacklist any URL with a literal IPv4 address +# - netloc: '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' # The largest allowed URL preview spidering size in bytes -max_spider_size: "10M" - - +# +#max_spider_size: 10M ## Captcha ## # See docs/CAPTCHA_SETUP for full details of configuring this. # This Home Server's ReCAPTCHA public key. -recaptcha_public_key: "YOUR_PUBLIC_KEY" +# +#recaptcha_public_key: "YOUR_PUBLIC_KEY" # This Home Server's ReCAPTCHA private key. -recaptcha_private_key: "YOUR_PRIVATE_KEY" +# +#recaptcha_private_key: "YOUR_PRIVATE_KEY" # Enables ReCaptcha checks when registering, preventing signup # unless a captcha is answered. Requires a valid ReCaptcha # public/private key. -enable_registration_captcha: False +# +#enable_registration_captcha: false # A secret key used to bypass the captcha test entirely. +# #captcha_bypass_secret: "YOUR_SECRET_HERE" # The API endpoint to use for verifying m.login.recaptcha responses. -recaptcha_siteverify_api: "https://www.google.com/recaptcha/api/siteverify" +# +#recaptcha_siteverify_api: "https://www.recaptcha.net/recaptcha/api/siteverify" -## Turn ## +## TURN ## # The public URIs of the TURN server to give to clients -turn_uris: [] +# +#turn_uris: [] # The shared secret used to compute passwords for the TURN server -turn_shared_secret: "YOUR_SHARED_SECRET" +# +#turn_shared_secret: "YOUR_SHARED_SECRET" # The Username and password if the TURN server needs them and # does not use a token +# #turn_username: "TURNSERVER_USERNAME" #turn_password: "TURNSERVER_PASSWORD" # How long generated TURN credentials last -turn_user_lifetime: "1h" +# +#turn_user_lifetime: 1h # Whether guests should be allowed to use the TURN server. # This defaults to True, otherwise VoIP will be unreliable for guests. # However, it does introduce a slight security risk as it allows users to # connect to arbitrary endpoints without having first signed up for a # valid account (e.g. by passing a CAPTCHA). -turn_allow_guests: True +# +#turn_allow_guests: True ## Registration ## +# +# Registration can be rate-limited using the parameters in the "Ratelimiting" +# section of this file. # Enable registration for new users. -enable_registration: True +# +enable_registration: true # The user must provide all of the below types of 3PID when registering. # -# registrations_require_3pid: -# - email -# - msisdn +#registrations_require_3pid: +# - email +# - msisdn + +# Explicitly disable asking for MSISDNs from the registration +# flow (overrides registrations_require_3pid if MSISDNs are set as required) +# +#disable_msisdn_registration: true # Mandate that users are only allowed to associate certain formats of # 3PIDs with accounts on this server. # -# allowed_local_3pids: -# - medium: email -# pattern: ".*@matrix\.org" -# - medium: email -# pattern: ".*@vector\.im" -# - medium: msisdn -# pattern: "\+44" +#allowed_local_3pids: +# - medium: email +# pattern: '.*@matrix\.org' +# - medium: email +# pattern: '.*@vector\.im' +# - medium: msisdn +# pattern: '\+44' -# If set, allows registration by anyone who also has the shared -# secret, even if registration is otherwise disabled. +# If set, allows registration of standard or admin accounts by anyone who +# has the shared secret, even if registration is otherwise disabled. +# registration_shared_secret: "{{REGISTRATION_SHARED_SECRET}}" # Set the number of bcrypt rounds used to generate password hash. @@ -433,64 +665,118 @@ registration_shared_secret: "{{REGISTRATION_SHARED_SECRET}}" # The default number is 12 (which equates to 2^12 rounds). # N.B. that increasing this will exponentially increase the time required # to register or login - e.g. 24 => 2^24 rounds which will take >20 mins. -bcrypt_rounds: 12 +# +#bcrypt_rounds: 12 # Allows users to register as guests without a password/email/etc, and # participate in rooms hosted on this server which have been made # accessible to anonymous users. -allow_guest_access: False +# +#allow_guest_access: false + +# The identity server which we suggest that clients should use when users log +# in on this server. +# +# (By default, no suggestion is made, so it is left up to the client. +# This setting is ignored unless public_baseurl is also set.) +# +#default_identity_server: https://matrix.org # The list of identity servers trusted to verify third party # identifiers by this server. -trusted_third_party_id_servers: - - matrix.org - - vector.im - - riot.im +# +# Also defines the ID server which will be called when an account is +# deactivated (one will be picked arbitrarily). +# +#trusted_third_party_id_servers: +# - matrix.org +# - vector.im # Users who register on this homeserver will automatically be joined -# to these roomsS +# to these rooms +# #auto_join_rooms: -# - "#example:example.com" +# - "#example:example.com" + +# Where auto_join_rooms are specified, setting this flag ensures that the +# the rooms exist by creating them when the first user on the +# homeserver registers. +# Setting to false means that if the rooms are not manually created, +# users cannot be auto-joined since they do not exist. +# +#autocreate_auto_join_rooms: true ## Metrics ### # Enable collection and rendering of performance metrics -enable_metrics: False -report_stats: False +# +#enable_metrics: False + +# Enable sentry integration +# NOTE: While attempts are made to ensure that the logs don't contain +# any sensitive information, this cannot be guaranteed. By enabling +# this option the sentry server may therefore receive sensitive +# information, and it in turn may then diseminate sensitive information +# through insecure notification channels if so configured. +# +#sentry: +# dsn: "..." + +# Whether or not to report anonymized homeserver usage statistics. +report_stats: false ## API Configuration ## # A list of event types that will be included in the room_invite_state -room_invite_state_types: - - "m.room.join_rules" - - "m.room.canonical_alias" - - "m.room.avatar" - - "m.room.name" +# +#room_invite_state_types: +# - "m.room.join_rules" +# - "m.room.canonical_alias" +# - "m.room.avatar" +# - "m.room.encryption" +# - "m.room.name" -# A list of application service config file to use -app_service_config_files: [] +# A list of application service config files to use +# +#app_service_config_files: +# - app_service_1.yaml +# - app_service_2.yaml + +# Uncomment to enable tracking of application service IP addresses. Implicitly +# enables MAU tracking for application service users. +# +#track_appservice_user_ips: True +# a secret which is used to sign access tokens. If none is specified, +# the registration_shared_secret is used, if one is given; otherwise, +# a secret key is derived from the signing key. +# macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" # Used to enable access token expiration. -expire_access_token: False +# +#expire_access_token: False # a secret which is used to calculate HMACs for form values, to stop -# falsification of values +# falsification of values. Must be specified for the User Consent +# forms to work. +# form_secret: "{{FORM_SECRET}}" ## Signing Keys ## # Path to the signing key to sign messages with +# signing_key_path: "{{SYNAPSE_ROOT}}localhost.signing.key" # The keys that the server used to sign messages with but won't use # to sign new messages. E.g. it has lost its private key -old_signing_keys: {} +# +#old_signing_keys: # "ed25519:auto": # # Base64 encoded public key # key: "The public part of your old signing key." @@ -501,31 +787,65 @@ old_signing_keys: {} # Used to set the valid_until_ts in /key/v2 APIs. # Determines how quickly servers will query to check which keys # are still valid. -key_refresh_interval: "1d" # 1 Day.block_non_admin_invites +# +#key_refresh_interval: 1d # The trusted servers to download signing keys from. -perspectives: - servers: - "matrix.org": - verify_keys: - "ed25519:auto": - key: "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw" +# +#perspectives: +# servers: +# "matrix.org": +# verify_keys: +# "ed25519:auto": +# key: "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw" - -# Enable SAML2 for registration and login. Uses pysaml2 -# config_path: Path to the sp_conf.py configuration file -# idp_redirect_url: Identity provider URL which will redirect -# the user back to /login/saml2 with proper info. +# Enable SAML2 for registration and login. Uses pysaml2. +# +# `sp_config` is the configuration for the pysaml2 Service Provider. # See pysaml2 docs for format of config. +# +# Default values will be used for the 'entityid' and 'service' settings, +# so it is not normally necessary to specify them unless you need to +# override them. +# #saml2_config: -# enabled: true -# config_path: "{{SYNAPSE_ROOT}}sp_conf.py" -# idp_redirect_url: "http://localhost/idp" +# sp_config: +# # point this to the IdP's metadata. You can use either a local file or +# # (preferably) a URL. +# metadata: +# #local: ["saml2/idp.xml"] +# remote: +# - url: https://our_idp/metadata.xml +# +# # The rest of sp_config is just used to generate our metadata xml, and you +# # may well not need it, depending on your setup. Alternatively you +# # may need a whole lot more detail - see the pysaml2 docs! +# +# description: ["My awesome SP", "en"] +# name: ["Test SP", "en"] +# +# organization: +# name: Example com +# display_name: +# - ["Example co", "en"] +# url: "http://example.com" +# +# contact_person: +# - given_name: Bob +# sur_name: "the Sysadmin" +# email_address": ["admin@example.com"] +# contact_type": technical +# +# # Instead of putting the config inline as above, you can specify a +# # separate pysaml2 configuration file: +# # +# config_path: "{{SYNAPSE_ROOT}}sp_conf.py" # Enable CAS for registration and login. +# #cas_config: # enabled: true # server_url: "https://cas-server.com" @@ -536,19 +856,21 @@ perspectives: # The JWT needs to contain a globally unique "sub" (subject) claim. # -# jwt_config: -# enabled: true -# secret: "a secret" -# algorithm: "HS256" +#jwt_config: +# enabled: true +# secret: "a secret" +# algorithm: "HS256" - -# Enable password for login. password_config: - enabled: true + # Uncomment to disable password login + # + #enabled: false + # Uncomment and change to a secret random string for extra security. # DO NOT CHANGE THIS AFTER INITIAL SETUP! - #pepper: "" + # + #pepper: "EVEN_MORE_SECRET" @@ -569,27 +891,29 @@ password_config: # require_transport_security: False # notif_from: "Your Friendly %(app)s Home Server " # app_name: Matrix -# template_dir: res/templates +# # if template_dir is unset, uses the example templates that are part of +# # the Synapse distribution. +# #template_dir: res/templates # notif_template_html: notif_mail.html # notif_template_text: notif_mail.txt # notif_for_new_users: True # riot_base_url: "http://localhost/riot" -# password_providers: -# - module: "ldap_auth_provider.LdapAuthProvider" -# config: -# enabled: true -# uri: "ldap://ldap.example.com:389" -# start_tls: true -# base: "ou=users,dc=example,dc=com" -# attributes: -# uid: "cn" -# mail: "email" -# name: "givenName" -# #bind_dn: -# #bind_password: -# #filter: "(objectClass=posixAccount)" +#password_providers: +# - module: "ldap_auth_provider.LdapAuthProvider" +# config: +# enabled: true +# uri: "ldap://ldap.example.com:389" +# start_tls: true +# base: "ou=users,dc=example,dc=com" +# attributes: +# uid: "cn" +# mail: "email" +# name: "givenName" +# #bind_dn: +# #bind_password: +# #filter: "(objectClass=posixAccount)" @@ -600,32 +924,38 @@ password_config: # notification request includes the content of the event (other details # like the sender are still included). For `event_id_only` push, it # has no effect. - +# # For modern android devices the notification content will still appear # because it is loaded by the app. iPhone, however will send a # notification saying only that a message arrived and who it came from. # #push: -# include_content: true +# include_content: true -# spam_checker: -# module: "my_custom_project.SuperSpamChecker" -# config: -# example_option: 'things' +#spam_checker: +# module: "my_custom_project.SuperSpamChecker" +# config: +# example_option: 'things' -# Whether to allow non server admins to create groups on this server -enable_group_creation: false +# Uncomment to allow non-server-admin users to create groups on this server +# +#enable_group_creation: true # If enabled, non server admins can only create groups with local parts # starting with this prefix -# group_creation_prefix: "unofficial/" +# +#group_creation_prefix: "unofficial/" # User Directory configuration # +# 'enabled' defines whether users can search the user directory. If +# false then empty responses are returned to all queries. Defaults to +# true. +# # 'search_all_users' defines whether to search all users visible to your HS # when searching the user directory, rather than limiting to users visible # in public rooms. Defaults to false. If you set it True, you'll have to run @@ -633,7 +963,8 @@ enable_group_creation: false # on your database to tell it to rebuild the user_directory search indexes. # #user_directory: -# search_all_users: false +# enabled: true +# search_all_users: false # User Consent configuration @@ -662,6 +993,14 @@ enable_group_creation: false # until the user consents to the privacy policy. The value of the setting is # used as the text of the error. # +# 'require_at_registration', if enabled, will add a step to the registration +# process, similar to how captcha works. Users will be required to accept the +# policy before their account is created. +# +# 'policy_name' is the display name of the policy users will see when registering +# for an account. Has no effect unless `require_at_registration` is enabled. +# Defaults to "Privacy Policy". +# user_consent: template_dir: res/templates/privacy version: 1.0 @@ -676,8 +1015,6 @@ user_consent: terms and conditions at %(consent_uri)s require_at_registration: true - - # Server Notices room configuration # # Uncomment this section to enable a room which can be used to send notices @@ -696,3 +1033,66 @@ server_notices: system_mxid_display_name: "Server Notices" system_mxid_avatar_url: "mxc://localhost:{{SYNAPSE_PORT}}/oumMVlgDnLYFaPVkExemNVVZ" room_name: "Server Notices" + +# Uncomment to disable searching the public room list. When disabled +# blocks searching local and remote room lists for local and remote +# users by always returning an empty list for all queries. +# +#enable_room_list_search: false + +# The `alias_creation` option controls who's allowed to create aliases +# on this server. +# +# The format of this option is a list of rules that contain globs that +# match against user_id, room_id and the new alias (fully qualified with +# server name). The action in the first rule that matches is taken, +# which can currently either be "allow" or "deny". +# +# Missing user_id/room_id/alias fields default to "*". +# +# If no rules match the request is denied. An empty list means no one +# can create aliases. +# +# Options for the rules include: +# +# user_id: Matches against the creator of the alias +# alias: Matches against the alias being created +# room_id: Matches against the room ID the alias is being pointed at +# action: Whether to "allow" or "deny" the request if the rule matches +# +# The default is: +# +#alias_creation_rules: +# - user_id: "*" +# alias: "*" +# room_id: "*" +# action: allow + +# The `room_list_publication_rules` option controls who can publish and +# which rooms can be published in the public room list. +# +# The format of this option is the same as that for +# `alias_creation_rules`. +# +# If the room has one or more aliases associated with it, only one of +# the aliases needs to match the alias rule. If there are no aliases +# then only rules with `alias: *` match. +# +# If no rules match the request is denied. An empty list means no one +# can publish rooms. +# +# Options for the rules include: +# +# user_id: Matches agaisnt the creator of the alias +# room_id: Matches against the room ID being published +# alias: Matches against any current local or canonical aliases +# associated with the room +# action: Whether to "allow" or "deny" the request if the rule matches +# +# The default is: +# +#room_list_publication_rules: +# - user_id: "*" +# alias: "*" +# room_id: "*" +# action: allow diff --git a/synapse/install.sh b/synapse/install.sh index 1b27f0952d..4761e359fa 100755 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -20,12 +20,11 @@ curl https://codeload.github.com/matrix-org/synapse/zip/$SYNAPSE_BRANCH --output unzip -q synapse.zip mv synapse-$SYNAPSE_BRANCH $SERVER_DIR cd $SERVER_DIR -virtualenv -p python2.7 env +virtualenv -p python3 env source env/bin/activate pip install --upgrade pip pip install --upgrade setuptools -pip install . -pip install jinja2 # We use the ConsentResource, which requires jinja2 +pip install matrix-synapse[all] python -m synapse.app.homeserver \ --server-name localhost \ --config-path homeserver.yaml \ From 3056d936f546c81614f86837096ab0ba373bf00f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 29 Mar 2019 11:55:33 +0100 Subject: [PATCH 0142/2372] use yarn and update dependencies, commit lock file --- .gitignore | 3 +- package.json | 6 +- yarn.lock | 759 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 763 insertions(+), 5 deletions(-) create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore index 8a48fb815d..24cd046858 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ node_modules -package-lock.json -*.png \ No newline at end of file +*.png diff --git a/package.json b/package.json index f3c47ac491..8372039258 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,10 @@ "license": "ISC", "dependencies": { "cheerio": "^1.0.0-rc.2", - "commander": "^2.17.1", - "puppeteer": "^1.6.0", + "commander": "^2.19.0", + "puppeteer": "^1.14.0", "request": "^2.88.0", - "request-promise-native": "^1.0.5", + "request-promise-native": "^1.0.7", "uuid": "^3.3.2" } } diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000000..bdf5608a7e --- /dev/null +++ b/yarn.lock @@ -0,0 +1,759 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@*": + version "11.12.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.12.1.tgz#d90123f6c61fdf2f7cddd286ddae891586dd3488" + integrity sha512-sKDlqv6COJrR7ar0+GqqhrXQDzQlMcqMnF2iEU6m9hLo8kxozoAGUazwPyELHlRVmjsbvlnGXjnzyptSXVmceA== + +agent-base@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" + integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== + dependencies: + es6-promisify "^5.0.0" + +ajv@^6.5.5: + version "6.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" + integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +async-limiter@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" + integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +cheerio@^1.0.0-rc.2: + version "1.0.0-rc.2" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db" + integrity sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs= + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.0" + entities "~1.1.1" + htmlparser2 "^3.9.1" + lodash "^4.15.0" + parse5 "^3.0.1" + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" + integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concat-stream@1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +css-select@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-what@2.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" + integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +dom-serializer@0, dom-serializer@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== + dependencies: + domelementtype "^1.3.0" + entities "^1.1.1" + +domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + +domutils@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^1.5.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +entities@^1.1.1, entities@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +es6-promise@^4.0.3: + version "4.2.6" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f" + integrity sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extract-zip@^1.6.6: + version "1.6.7" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" + integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= + dependencies: + concat-stream "1.6.2" + debug "2.6.9" + mkdirp "0.5.1" + yauzl "2.4.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +fd-slicer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" + integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU= + dependencies: + pend "~1.2.0" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +htmlparser2@^3.9.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-proxy-agent@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" + integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ== + dependencies: + agent-base "^4.1.0" + debug "^3.1.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +lodash@^4.15.0, lodash@^4.17.11: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + +mime-db@~1.38.0: + version "1.38.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" + integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.22" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd" + integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog== + dependencies: + mime-db "~1.38.0" + +mime@^2.0.3: + version "2.4.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.0.tgz#e051fd881358585f3279df333fe694da0bcffdd6" + integrity sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +mkdirp@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +nth-check@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +parse5@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" + integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA== + dependencies: + "@types/node" "*" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== + +progress@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +proxy-from-env@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" + integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4= + +psl@^1.1.24, psl@^1.1.28: + version "1.1.31" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184" + integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw== + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +puppeteer@^1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.14.0.tgz#828c1926b307200d5fc8289b99df4e13e962d339" + integrity sha512-SayS2wUX/8LF8Yo2Rkpc5nkAu4Jg3qu+OLTDSOZtisVQMB2Z5vjlY2TdPi/5CgZKiZroYIiyUN3sRX63El9iaw== + dependencies: + debug "^4.1.0" + extract-zip "^1.6.6" + https-proxy-agent "^2.2.1" + mime "^2.0.3" + progress "^2.0.1" + proxy-from-env "^1.0.0" + rimraf "^2.6.1" + ws "^6.1.0" + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +readable-stream@^2.2.2: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d" + integrity sha512-RV20kLjdmpZuTF1INEb9IA3L68Nmi+Ri7ppZqo78wj//Pn62fCoJyV9zalccNzDD/OuJpMG4f+pfMl8+L6QdGw== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +request-promise-core@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" + integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== + dependencies: + lodash "^4.17.11" + +request-promise-native@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" + integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== + dependencies: + request-promise-core "1.1.2" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + +request@^2.88.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +rimraf@^2.6.1: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stealthy-require@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + +string_decoder@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" + integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w== + dependencies: + safe-buffer "~5.1.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +tough-cookie@^2.3.3: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +ws@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" + integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== + dependencies: + async-limiter "~1.0.0" + +yauzl@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" + integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU= + dependencies: + fd-slicer "~1.0.1" From ab5a2452ee482559068941024d6336b0d247ab8b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 29 Mar 2019 12:04:51 +0100 Subject: [PATCH 0143/2372] fix signup --- src/usecases/signup.js | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/usecases/signup.js b/src/usecases/signup.js index 825a2c27fa..1b9ba2872c 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -19,31 +19,26 @@ const assert = require('assert'); module.exports = async function signup(session, username, password, homeserver) { session.log.step("signs up"); await session.goto(session.url('/#/register')); - //click 'Custom server' radio button + // change the homeserver by clicking the "Change" link. if (homeserver) { - const advancedRadioButton = await session.waitAndQuery('#advanced'); - await advancedRadioButton.click(); + const changeServerDetailsLink = await session.waitAndQuery('.mx_AuthBody_editServerDetails'); + await changeServerDetailsLink.click(); + const hsInputField = await session.query('#mx_ServerConfig_hsUrl'); + await session.replaceInputText(hsInputField, homeserver); + const nextButton = await session.query('.mx_Login_submit'); + await nextButton.click(); } - // wait until register button is visible - await session.waitAndQuery('.mx_Login_submit[value=Register]'); //fill out form - const loginFields = await session.queryAll('.mx_Login_field'); - assert.strictEqual(loginFields.length, 7); - const usernameField = loginFields[2]; - const passwordField = loginFields[3]; - const passwordRepeatField = loginFields[4]; - const hsurlField = loginFields[5]; + const usernameField = await session.waitAndQuery("#mx_RegistrationForm_username"); + const passwordField = await session.waitAndQuery("#mx_RegistrationForm_password"); + const passwordRepeatField = await session.waitAndQuery("#mx_RegistrationForm_passwordConfirm"); await session.replaceInputText(usernameField, username); await session.replaceInputText(passwordField, password); await session.replaceInputText(passwordRepeatField, password); - if (homeserver) { - await session.waitAndQuery('.mx_ServerConfig'); - await session.replaceInputText(hsurlField, homeserver); - } - //wait over a second because Registration/ServerConfig have a 1000ms + //wait over a second because Registration/ServerConfig have a 250ms //delay to internally set the homeserver url //see Registration::render and ServerConfig::props::delayTimeMs - await session.delay(1500); + await session.delay(300); /// focus on the button to make sure error validation /// has happened before checking the form is good to go const registerButton = await session.query('.mx_Login_submit'); @@ -60,7 +55,7 @@ module.exports = async function signup(session, username, password, homeserver) await continueButton.click(); //find the privacy policy checkbox and check it - const policyCheckbox = await session.waitAndQuery('.mx_Login_box input[type="checkbox"]'); + const policyCheckbox = await session.waitAndQuery('.mx_InteractiveAuthEntryComponents_termsPolicy input'); await policyCheckbox.click(); //now click the 'Accept' button to agree to the privacy policy From 65ca1b33eea95a51ee2e461e5d9a648372f86685 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 29 Mar 2019 12:27:48 +0100 Subject: [PATCH 0144/2372] fix creating a room --- src/usecases/create-room.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/usecases/create-room.js b/src/usecases/create-room.js index 7d3488bfbe..79f1848198 100644 --- a/src/usecases/create-room.js +++ b/src/usecases/create-room.js @@ -18,8 +18,16 @@ const assert = require('assert'); module.exports = async function createRoom(session, roomName) { session.log.step(`creates room "${roomName}"`); - //TODO: brittle selector - const createRoomButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Create new room"]'); + const roomListHeaders = await session.queryAll('.mx_RoomSubList_labelContainer'); + const roomListHeaderLabels = await Promise.all(roomListHeaders.map(h => session.innerText(h))); + const roomsIndex = roomListHeaderLabels.findIndex(l => l.toLowerCase().includes("rooms")); + if (roomsIndex === -1) { + throw new Error("could not find room list section that contains rooms in header"); + } + const roomsHeader = roomListHeaders[roomsIndex]; + const addRoomButton = await roomsHeader.$(".mx_RoomSubList_addRoom"); + await addRoomButton.click(); + const createRoomButton = await session.waitAndQuery('.mx_RoomDirectory_createRoom'); await createRoomButton.click(); const roomNameInput = await session.waitAndQuery('.mx_CreateRoomDialog_input'); @@ -30,4 +38,4 @@ module.exports = async function createRoom(session, roomName) { await session.waitAndQuery('.mx_MessageComposer'); session.log.done(); -} \ No newline at end of file +} From a27b92a49a42766d696d7e98d09df995d0dede01 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 29 Mar 2019 13:04:10 +0100 Subject: [PATCH 0145/2372] fix changing the room settings --- src/usecases/room-settings.js | 52 ++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/usecases/room-settings.js b/src/usecases/room-settings.js index 95c7538431..ca0bcf2a95 100644 --- a/src/usecases/room-settings.js +++ b/src/usecases/room-settings.js @@ -17,11 +17,11 @@ limitations under the License. const assert = require('assert'); const {acceptDialog} = require('./dialog'); -async function setCheckboxSetting(session, checkbox, enabled) { - const checked = await session.getElementProperty(checkbox, "checked"); - assert.equal(typeof checked, "boolean"); +async function setSettingsToggle(session, toggle, enabled) { + const className = await session.getElementProperty(toggle, "className"); + const checked = className.includes("mx_ToggleSwitch_on"); if (checked !== enabled) { - await checkbox.click(); + await toggle.click(); session.log.done(); return true; } else { @@ -31,25 +31,40 @@ async function setCheckboxSetting(session, checkbox, enabled) { module.exports = async function changeRoomSettings(session, settings) { session.log.startGroup(`changes the room settings`); - /// XXX delay is needed here, possible because the header is being rerendered + /// XXX delay is needed here, possibly because the header is being rerendered /// click doesn't do anything otherwise await session.delay(1000); const settingsButton = await session.query(".mx_RoomHeader .mx_AccessibleButton[title=Settings]"); await settingsButton.click(); - const checks = await session.waitAndQueryAll(".mx_RoomSettings_settings input[type=checkbox]"); - assert.equal(checks.length, 3); - const e2eEncryptionCheck = checks[0]; - const sendToUnverifiedDevices = checks[1]; - const isDirectory = checks[2]; + //find tabs + const tabButtons = await session.waitAndQueryAll(".mx_RoomSettingsDialog .mx_TabbedView_tabLabel"); + const tabLabels = await Promise.all(tabButtons.map(t => session.innerText(t))); + const securityTabButton = tabButtons[tabLabels.findIndex(l => l.toLowerCase().includes("security"))]; + + const generalSwitches = await session.waitAndQueryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch"); + const isDirectory = generalSwitches[0]; if (typeof settings.directory === "boolean") { session.log.step(`sets directory listing to ${settings.directory}`); - await setCheckboxSetting(session, isDirectory, settings.directory); + await setSettingsToggle(session, isDirectory, settings.directory); } + if (settings.alias) { + session.log.step(`sets alias to ${settings.alias}`); + const aliasField = await session.waitAndQuery(".mx_RoomSettingsDialog .mx_AliasSettings input[type=text]"); + await session.replaceInputText(aliasField, settings.alias); + const addButton = await session.waitAndQuery(".mx_RoomSettingsDialog .mx_AliasSettings .mx_AccessibleButton"); + await addButton.click(); + session.log.done(); + } + + securityTabButton.click(); + const securitySwitches = await session.waitAndQueryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch"); + const e2eEncryptionToggle = securitySwitches[0]; + if (typeof settings.encryption === "boolean") { session.log.step(`sets room e2e encryption to ${settings.encryption}`); - const clicked = await setCheckboxSetting(session, e2eEncryptionCheck, settings.encryption); + const clicked = await setSettingsToggle(session, e2eEncryptionToggle, settings.encryption); // if enabling, accept beta warning dialog if (clicked && settings.encryption) { await acceptDialog(session, "encryption"); @@ -58,7 +73,7 @@ module.exports = async function changeRoomSettings(session, settings) { if (settings.visibility) { session.log.step(`sets visibility to ${settings.visibility}`); - const radios = await session.waitAndQueryAll(".mx_RoomSettings_settings input[type=radio]"); + const radios = await session.waitAndQueryAll(".mx_RoomSettingsDialog input[type=radio]"); assert.equal(radios.length, 7); const inviteOnly = radios[0]; const publicNoGuests = radios[1]; @@ -76,15 +91,8 @@ module.exports = async function changeRoomSettings(session, settings) { session.log.done(); } - if (settings.alias) { - session.log.step(`sets alias to ${settings.alias}`); - const aliasField = await session.waitAndQuery(".mx_RoomSettings .mx_EditableItemList .mx_EditableItem_editable"); - await session.replaceInputText(aliasField, settings.alias); - session.log.done(); - } - - const saveButton = await session.query(".mx_RoomHeader_wrapper .mx_RoomHeader_textButton"); - await saveButton.click(); + const closeButton = await session.query(".mx_RoomSettingsDialog .mx_Dialog_cancelButton"); + await closeButton.click(); session.log.endGroup(); } From fe6a273ba947ae605d507d7f86382cead75660d4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 29 Mar 2019 13:59:42 +0100 Subject: [PATCH 0146/2372] fix joining a room through the room directory --- src/scenarios/directory.js | 2 +- src/usecases/create-room.js | 10 ++++++++-- src/usecases/join.js | 10 ++++------ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/scenarios/directory.js b/src/scenarios/directory.js index cfe72ccef3..582b6867b2 100644 --- a/src/scenarios/directory.js +++ b/src/scenarios/directory.js @@ -18,7 +18,7 @@ limitations under the License. const join = require('../usecases/join'); const sendMessage = require('../usecases/send-message'); const {receiveMessage} = require('../usecases/timeline'); -const createRoom = require('../usecases/create-room'); +const {createRoom} = require('../usecases/create-room'); const changeRoomSettings = require('../usecases/room-settings'); module.exports = async function roomDirectoryScenarios(alice, bob) { diff --git a/src/usecases/create-room.js b/src/usecases/create-room.js index 79f1848198..16d0620879 100644 --- a/src/usecases/create-room.js +++ b/src/usecases/create-room.js @@ -16,8 +16,7 @@ limitations under the License. const assert = require('assert'); -module.exports = async function createRoom(session, roomName) { - session.log.step(`creates room "${roomName}"`); +async function openRoomDirectory(session) { const roomListHeaders = await session.queryAll('.mx_RoomSubList_labelContainer'); const roomListHeaderLabels = await Promise.all(roomListHeaders.map(h => session.innerText(h))); const roomsIndex = roomListHeaderLabels.findIndex(l => l.toLowerCase().includes("rooms")); @@ -27,6 +26,11 @@ module.exports = async function createRoom(session, roomName) { const roomsHeader = roomListHeaders[roomsIndex]; const addRoomButton = await roomsHeader.$(".mx_RoomSubList_addRoom"); await addRoomButton.click(); +} + +async function createRoom(session, roomName) { + session.log.step(`creates room "${roomName}"`); + await openRoomDirectory(session); const createRoomButton = await session.waitAndQuery('.mx_RoomDirectory_createRoom'); await createRoomButton.click(); @@ -39,3 +43,5 @@ module.exports = async function createRoom(session, roomName) { await session.waitAndQuery('.mx_MessageComposer'); session.log.done(); } + +module.exports = {openRoomDirectory, createRoom}; diff --git a/src/usecases/join.js b/src/usecases/join.js index 76b98ca397..cba9e06660 100644 --- a/src/usecases/join.js +++ b/src/usecases/join.js @@ -15,14 +15,12 @@ limitations under the License. */ const assert = require('assert'); +const {openRoomDirectory} = require('./create-room'); module.exports = async function join(session, roomName) { session.log.step(`joins room "${roomName}"`); - //TODO: brittle selector - const directoryButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Room directory"]'); - await directoryButton.click(); - - const roomInput = await session.waitAndQuery('.mx_DirectorySearchBox_input'); + await openRoomDirectory(session); + const roomInput = await session.waitAndQuery('.mx_DirectorySearchBox input'); await session.replaceInputText(roomInput, roomName); const firstRoomLabel = await session.waitAndQuery('.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child', 1000); @@ -33,4 +31,4 @@ module.exports = async function join(session, roomName) { await session.waitAndQuery('.mx_MessageComposer'); session.log.done(); -} \ No newline at end of file +} From 5598214cd20a534bc0aaf3b8d9423ae34081ecb6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 29 Mar 2019 16:59:05 +0100 Subject: [PATCH 0147/2372] fix writing in composer --- src/usecases/send-message.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/usecases/send-message.js b/src/usecases/send-message.js index 5bf289b03a..038171327c 100644 --- a/src/usecases/send-message.js +++ b/src/usecases/send-message.js @@ -21,6 +21,9 @@ module.exports = async function sendMessage(session, message) { // this selector needs to be the element that has contenteditable=true, // not any if its parents, otherwise it behaves flaky at best. const composer = await session.waitAndQuery('.mx_MessageComposer_editor'); + // sometimes the focus that type() does internally doesn't seem to work + // and calling click before seems to fix it 🤷 + await composer.click(); await composer.type(message); const text = await session.innerText(composer); assert.equal(text.trim(), message.trim()); From a1505971fc18eb627dbabb43a22ca23b15cd6057 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 29 Mar 2019 16:59:54 +0100 Subject: [PATCH 0148/2372] missed this when making createRoom export non-default earlier --- src/scenarios/e2e-encryption.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scenarios/e2e-encryption.js b/src/scenarios/e2e-encryption.js index 51d8a70236..7cd9f1ede9 100644 --- a/src/scenarios/e2e-encryption.js +++ b/src/scenarios/e2e-encryption.js @@ -22,7 +22,7 @@ const sendMessage = require('../usecases/send-message'); const acceptInvite = require('../usecases/accept-invite'); const invite = require('../usecases/invite'); const {receiveMessage} = require('../usecases/timeline'); -const createRoom = require('../usecases/create-room'); +const {createRoom} = require('../usecases/create-room'); const changeRoomSettings = require('../usecases/room-settings'); const {getE2EDeviceFromSettings} = require('../usecases/settings'); const {verifyDeviceForUser} = require('../usecases/memberlist'); From 2bf51da73e58ef16b3807302051293faa4b34008 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 2 Apr 2019 14:29:23 +0200 Subject: [PATCH 0149/2372] fix enabling e2e encryption in room settings --- src/usecases/room-settings.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/usecases/room-settings.js b/src/usecases/room-settings.js index ca0bcf2a95..e204d24a52 100644 --- a/src/usecases/room-settings.js +++ b/src/usecases/room-settings.js @@ -59,7 +59,8 @@ module.exports = async function changeRoomSettings(session, settings) { } securityTabButton.click(); - const securitySwitches = await session.waitAndQueryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch"); + await session.delay(500); + const securitySwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch"); const e2eEncryptionToggle = securitySwitches[0]; if (typeof settings.encryption === "boolean") { From ae5dc9d0b37507706479211d217a6f7da4d3f4ab Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 2 Apr 2019 14:29:47 +0200 Subject: [PATCH 0150/2372] fix inviting someone --- src/usecases/invite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usecases/invite.js b/src/usecases/invite.js index 934beb6819..bbfc38cd23 100644 --- a/src/usecases/invite.js +++ b/src/usecases/invite.js @@ -19,7 +19,7 @@ const assert = require('assert'); module.exports = async function invite(session, userId) { session.log.step(`invites "${userId}" to room`); await session.delay(1000); - const inviteButton = await session.waitAndQuery(".mx_RightPanel_invite"); + const inviteButton = await session.waitAndQuery(".mx_MemberList_invite"); await inviteButton.click(); const inviteTextArea = await session.waitAndQuery(".mx_ChatInviteDialog textarea"); await inviteTextArea.type(userId); From 9ab169254417f97638e9019cfaf45943733def12 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 2 Apr 2019 14:30:41 +0200 Subject: [PATCH 0151/2372] fix verification, replace device id/key with SAS verification. --- src/scenarios/e2e-encryption.js | 24 +++++------- src/usecases/dialog.js | 23 ++++++----- src/usecases/memberlist.js | 22 +++++++++++ src/usecases/room-settings.js | 2 +- src/usecases/verify.js | 68 +++++++++++++++++++++++++++++++++ 5 files changed, 114 insertions(+), 25 deletions(-) create mode 100644 src/usecases/verify.js diff --git a/src/scenarios/e2e-encryption.js b/src/scenarios/e2e-encryption.js index 7cd9f1ede9..c7a6a5d085 100644 --- a/src/scenarios/e2e-encryption.js +++ b/src/scenarios/e2e-encryption.js @@ -24,28 +24,24 @@ const invite = require('../usecases/invite'); const {receiveMessage} = require('../usecases/timeline'); const {createRoom} = require('../usecases/create-room'); const changeRoomSettings = require('../usecases/room-settings'); -const {getE2EDeviceFromSettings} = require('../usecases/settings'); -const {verifyDeviceForUser} = require('../usecases/memberlist'); +const {startSasVerifcation, acceptSasVerification} = require('../usecases/verify'); +const assert = require('assert'); module.exports = async function e2eEncryptionScenarios(alice, bob) { console.log(" creating an e2e encrypted room and join through invite:"); const room = "secrets"; await createRoom(bob, room); await changeRoomSettings(bob, {encryption: true}); + // await cancelKeyBackup(bob); await invite(bob, "@alice:localhost"); await acceptInvite(alice, room); - const bobDevice = await getE2EDeviceFromSettings(bob); - // wait some time for the encryption warning dialog - // to appear after closing the settings - await delay(1000); - await acceptDialogMaybe(bob, "encryption"); - const aliceDevice = await getE2EDeviceFromSettings(alice); - // wait some time for the encryption warning dialog - // to appear after closing the settings - await delay(1000); - await acceptDialogMaybe(alice, "encryption"); - await verifyDeviceForUser(bob, "alice", aliceDevice); - await verifyDeviceForUser(alice, "bob", bobDevice); + // do sas verifcation + bob.log.step(`starts SAS verification with ${alice.username}`); + const bobSasPromise = startSasVerifcation(bob, alice.username); + const aliceSasPromise = acceptSasVerification(alice, bob.username); + const [bobSas, aliceSas] = await Promise.all([bobSasPromise, aliceSasPromise]); + assert.deepEqual(bobSas, aliceSas); + bob.log.done(`done, (${bobSas.join(", ")}) matches!`); const aliceMessage = "Guess what I just heard?!" await sendMessage(alice, aliceMessage); await receiveMessage(bob, {sender: "alice", body: aliceMessage, encrypted: true}); diff --git a/src/usecases/dialog.js b/src/usecases/dialog.js index 89c70470d9..b11dac616d 100644 --- a/src/usecases/dialog.js +++ b/src/usecases/dialog.js @@ -16,27 +16,29 @@ limitations under the License. const assert = require('assert'); +async function assertDialog(session, expectedTitle) { + const titleElement = await session.waitAndQuery(".mx_Dialog .mx_Dialog_title"); + const dialogHeader = await session.innerText(titleElement); + assert(dialogHeader, expectedTitle); +} -async function acceptDialog(session, expectedContent) { - const foundDialog = await acceptDialogMaybe(session, expectedContent); +async function acceptDialog(session, expectedTitle) { + const foundDialog = await acceptDialogMaybe(session, expectedTitle); if (!foundDialog) { throw new Error("could not find a dialog"); } } -async function acceptDialogMaybe(session, expectedContent) { - let dialog = null; +async function acceptDialogMaybe(session, expectedTitle) { + let primaryButton = null; try { - dialog = await session.waitAndQuery(".mx_QuestionDialog"); + primaryButton = await session.waitAndQuery(".mx_Dialog [role=dialog] .mx_Dialog_primary"); } catch(err) { return false; } - if (expectedContent) { - const contentElement = await dialog.$(".mx_Dialog_content"); - const content = await (await contentElement.getProperty("innerText")).jsonValue(); - assert.ok(content.indexOf(expectedContent) !== -1); + if (expectedTitle) { + await assertDialog(session, expectedTitle); } - const primaryButton = await dialog.$(".mx_Dialog_primary"); await primaryButton.click(); return true; } @@ -44,4 +46,5 @@ async function acceptDialogMaybe(session, expectedContent) { module.exports = { acceptDialog, acceptDialogMaybe, + assertDialog, }; diff --git a/src/usecases/memberlist.js b/src/usecases/memberlist.js index b018ed552c..0cd8744853 100644 --- a/src/usecases/memberlist.js +++ b/src/usecases/memberlist.js @@ -16,6 +16,16 @@ limitations under the License. const assert = require('assert'); +async function openMemberInfo(session, name) { + const membersAndNames = await getMembersInMemberlist(session); + const matchingLabel = membersAndNames.filter((m) => { + return m.displayName === name; + }).map((m) => m.label)[0]; + await matchingLabel.click(); +}; + +module.exports.openMemberInfo = openMemberInfo; + module.exports.verifyDeviceForUser = async function(session, name, expectedDevice) { session.log.step(`verifies e2e device for ${name}`); const membersAndNames = await getMembersInMemberlist(session); @@ -23,8 +33,20 @@ module.exports.verifyDeviceForUser = async function(session, name, expectedDevic return m.displayName === name; }).map((m) => m.label)[0]; await matchingLabel.click(); + // click verify in member info const firstVerifyButton = await session.waitAndQuery(".mx_MemberDeviceInfo_verify"); await firstVerifyButton.click(); + // expect "Verify device" dialog and click "Begin Verification" + const dialogHeader = await session.innerText(await session.waitAndQuery(".mx_Dialog .mx_Dialog_title")); + assert(dialogHeader, "Verify device"); + const beginVerificationButton = await session.waitAndQuery(".mx_Dialog .mx_Dialog_primary") + await beginVerificationButton.click(); + // get emoji SAS labels + const sasLabelElements = await session.waitAndQueryAll(".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label"); + const sasLabels = await Promise.all(sasLabelElements.map(e => session.innerText(e))); + console.log("my sas labels", sasLabels); + + const dialogCodeFields = await session.waitAndQueryAll(".mx_QuestionDialog code"); assert.equal(dialogCodeFields.length, 2); const deviceId = await session.innerText(dialogCodeFields[0]); diff --git a/src/usecases/room-settings.js b/src/usecases/room-settings.js index e204d24a52..95078d1c87 100644 --- a/src/usecases/room-settings.js +++ b/src/usecases/room-settings.js @@ -68,7 +68,7 @@ module.exports = async function changeRoomSettings(session, settings) { const clicked = await setSettingsToggle(session, e2eEncryptionToggle, settings.encryption); // if enabling, accept beta warning dialog if (clicked && settings.encryption) { - await acceptDialog(session, "encryption"); + await acceptDialog(session, "Enable encryption?"); } } diff --git a/src/usecases/verify.js b/src/usecases/verify.js new file mode 100644 index 0000000000..e9461c225e --- /dev/null +++ b/src/usecases/verify.js @@ -0,0 +1,68 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); +const {openMemberInfo} = require("./memberlist"); +const {assertDialog, acceptDialog} = require("./dialog"); + +async function assertVerified(session) { + const dialogSubTitle = await session.innerText(await session.waitAndQuery(".mx_Dialog h2")); + assert(dialogSubTitle, "Verified!"); +} + +async function startVerification(session, name) { + await openMemberInfo(session, name); + // click verify in member info + const firstVerifyButton = await session.waitAndQuery(".mx_MemberDeviceInfo_verify"); + await firstVerifyButton.click(); +} + +async function getSasCodes(session) { + const sasLabelElements = await session.waitAndQueryAll(".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label"); + const sasLabels = await Promise.all(sasLabelElements.map(e => session.innerText(e))); + return sasLabels; +} + +module.exports.startSasVerifcation = async function(session, name) { + await startVerification(session, name); + // expect "Verify device" dialog and click "Begin Verification" + await assertDialog(session, "Verify device"); + // click "Begin Verification" + await acceptDialog(session); + const sasCodes = await getSasCodes(session); + // click "Verify" + await acceptDialog(session); + await assertVerified(session); + // click "Got it" when verification is done + await acceptDialog(session); + return sasCodes; +}; + +module.exports.acceptSasVerification = async function(session, name) { + await assertDialog(session, "Incoming Verification Request"); + const opponentLabelElement = await session.query(".mx_IncomingSasDialog_opponentProfile h2"); + const opponentLabel = await session.innerText(opponentLabelElement); + assert(opponentLabel, name); + // click "Continue" button + await acceptDialog(session); + const sasCodes = await getSasCodes(session); + // click "Verify" + await acceptDialog(session); + await assertVerified(session); + // click "Got it" when verification is done + await acceptDialog(session); + return sasCodes; +}; From 28bba4952b5d10fec01952b9c150de18f8ecfcb4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 2 Apr 2019 15:12:51 +0200 Subject: [PATCH 0152/2372] fix detecting e2e message in timeline --- src/usecases/timeline.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/usecases/timeline.js b/src/usecases/timeline.js index dce0203660..610d1f4e9b 100644 --- a/src/usecases/timeline.js +++ b/src/usecases/timeline.js @@ -117,11 +117,13 @@ function getLastEventTile(session) { } function getAllEventTiles(session) { - return session.queryAll(".mx_RoomView_MessageList > *"); + return session.queryAll(".mx_RoomView_MessageList .mx_EventTile"); } async function getMessageFromEventTile(eventTile) { const senderElement = await eventTile.$(".mx_SenderProfile_name"); + const className = await (await eventTile.getProperty("className")).jsonValue(); + const classNames = className.split(" "); const bodyElement = await eventTile.$(".mx_EventTile_body"); let sender = null; if (senderElement) { @@ -131,11 +133,10 @@ async function getMessageFromEventTile(eventTile) { return null; } const body = await(await bodyElement.getProperty("innerText")).jsonValue(); - const e2eIcon = await eventTile.$(".mx_EventTile_e2eIcon"); return { sender, body, - encrypted: !!e2eIcon + encrypted: classNames.includes("mx_EventTile_verified") }; } From 34171eab8cc1998a2301af81344cac524e3dbe22 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 2 Apr 2019 15:13:04 +0200 Subject: [PATCH 0153/2372] doing wait for /sync request to receive message, doesn't work well just poll every 200ms, feels way faster as before we were probably missing /sync requests --- src/session.js | 9 --------- src/usecases/timeline.js | 38 ++++++++++++++------------------------ 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/src/session.js b/src/session.js index 7ea980bd32..1363185753 100644 --- a/src/session.js +++ b/src/session.js @@ -161,15 +161,6 @@ module.exports = class RiotSession { }); } - waitForSyncResponseWith(predicate) { - return this.page.waitForResponse(async (response) => { - if (response.request().url().indexOf("/sync") === -1) { - return false; - } - return predicate(response); - }); - } - /** wait for a /sync request started after this call that gets a 200 response */ async waitForNextSuccessfulSync() { const syncUrls = []; diff --git a/src/usecases/timeline.js b/src/usecases/timeline.js index 610d1f4e9b..191117891d 100644 --- a/src/usecases/timeline.js +++ b/src/usecases/timeline.js @@ -54,31 +54,21 @@ module.exports.receiveMessage = async function(session, expectedMessage) { let lastMessage = null; let isExpectedMessage = false; - try { - lastMessage = await getLastMessage(); - isExpectedMessage = lastMessage && - lastMessage.body === expectedMessage.body && - lastMessage.sender === expectedMessage.sender; - } catch(ex) {} - // first try to see if the message is already the last message in the timeline - if (isExpectedMessage) { - assertMessage(lastMessage, expectedMessage); - } else { - await session.waitForSyncResponseWith(async (response) => { - const body = await response.text(); - if (expectedMessage.encrypted) { - return body.indexOf(expectedMessage.sender) !== -1 && - body.indexOf("m.room.encrypted") !== -1; - } else { - return body.indexOf(expectedMessage.body) !== -1; - } - }); - // wait a bit for the incoming event to be rendered - await session.delay(1000); - lastMessage = await getLastMessage(); - assertMessage(lastMessage, expectedMessage); + let totalTime = 0; + while (!isExpectedMessage) { + try { + lastMessage = await getLastMessage(); + isExpectedMessage = lastMessage && + lastMessage.body === expectedMessage.body && + lastMessage.sender === expectedMessage.sender + } catch(err) {} + if (totalTime > 5000) { + throw new Error("timed out after 5000ms"); + } + totalTime += 200; + await session.delay(200); } - + assertMessage(lastMessage, expectedMessage); session.log.done(); } From c1312f09ab5f1ff463f8f592ed7ebe7d900f5caf Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 2 Apr 2019 15:14:08 +0200 Subject: [PATCH 0154/2372] fix import --- src/scenarios/lazy-loading.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scenarios/lazy-loading.js b/src/scenarios/lazy-loading.js index c33e83215c..b1a1695b5c 100644 --- a/src/scenarios/lazy-loading.js +++ b/src/scenarios/lazy-loading.js @@ -22,7 +22,7 @@ const { checkTimelineContains, scrollToTimelineTop } = require('../usecases/timeline'); -const createRoom = require('../usecases/create-room'); +const {createRoom} = require('../usecases/create-room'); const {getMembersInMemberlist} = require('../usecases/memberlist'); const changeRoomSettings = require('../usecases/room-settings'); const {enableLazyLoading} = require('../usecases/settings'); From f197e9f97756e82ef626cab0a6b96fba8d2a5890 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 2 Apr 2019 15:14:24 +0200 Subject: [PATCH 0155/2372] lazy loading is not in labs anymore --- src/scenarios/lazy-loading.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/scenarios/lazy-loading.js b/src/scenarios/lazy-loading.js index b1a1695b5c..deb259a916 100644 --- a/src/scenarios/lazy-loading.js +++ b/src/scenarios/lazy-loading.js @@ -30,7 +30,6 @@ const assert = require('assert'); module.exports = async function lazyLoadingScenarios(alice, bob, charlies) { console.log(" creating a room for lazy loading member scenarios:"); - await enableLazyLoading(alice); const charly1to5 = charlies.slice("charly-1..5", 0, 5); const charly6to10 = charlies.slice("charly-6..10", 5); assert(charly1to5.sessions.length, 5); From 450430d66c02b50a063bb299bef7b8ab3d4ae80b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 2 Apr 2019 15:14:44 +0200 Subject: [PATCH 0156/2372] remove travis flag --- src/scenario.js | 14 +++----------- start.js | 3 +-- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/scenario.js b/src/scenario.js index 5b9d1f2906..4593960c10 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -21,7 +21,7 @@ const roomDirectoryScenarios = require('./scenarios/directory'); const lazyLoadingScenarios = require('./scenarios/lazy-loading'); const e2eEncryptionScenarios = require('./scenarios/e2e-encryption'); -module.exports = async function scenario(createSession, restCreator, runningOnTravis) { +module.exports = async function scenario(createSession, restCreator) { async function createUser(username) { const session = await createSession(username); await signup(session, session.username, 'testtest', session.hsUrl); @@ -34,16 +34,8 @@ module.exports = async function scenario(createSession, restCreator, runningOnTr await roomDirectoryScenarios(alice, bob); await e2eEncryptionScenarios(alice, bob); - // disable LL tests until we can run synapse on anything > than 2.7.7 as - // /admin/register fails with a missing method. - // either switch to python3 on synapse, - // blocked on https://github.com/matrix-org/synapse/issues/3900 - // or use a more recent version of ubuntu - // or switch to circleci? - if (!runningOnTravis) { - const charlies = await createRestUsers(restCreator); - await lazyLoadingScenarios(alice, bob, charlies); - } + const charlies = await createRestUsers(restCreator); + await lazyLoadingScenarios(alice, bob, charlies); } async function createRestUsers(restCreator) { diff --git a/start.js b/start.js index 18ccb438ec..1c3f27bbe3 100644 --- a/start.js +++ b/start.js @@ -26,7 +26,6 @@ program .option('--windowed', "dont run tests headless", false) .option('--slow-mo', "run tests slower to follow whats going on", false) .option('--dev-tools', "open chrome devtools in browser window", false) - .option('--travis', "running on travis CI, disable tests known to break on Ubuntu 14.04 LTS", false) .parse(process.argv); const hsUrl = 'http://localhost:5005'; @@ -59,7 +58,7 @@ async function runTests() { let failure = false; try { - await scenario(createSession, restCreator, program.travis); + await scenario(createSession, restCreator); } catch(err) { failure = true; console.log('failure: ', err); From 2449ddcfeede196fa7f4210b72b39bd8e40e70cf Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 2 Apr 2019 15:14:51 +0200 Subject: [PATCH 0157/2372] "disable" rate limiting for rest users --- .../config-templates/consent/homeserver.yaml | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/synapse/config-templates/consent/homeserver.yaml b/synapse/config-templates/consent/homeserver.yaml index 222ddb956f..80a967b047 100644 --- a/synapse/config-templates/consent/homeserver.yaml +++ b/synapse/config-templates/consent/homeserver.yaml @@ -364,11 +364,11 @@ log_config: "{{SYNAPSE_ROOT}}localhost.log.config" # Number of messages a client can send per second # -#rc_messages_per_second: 0.2 +rc_messages_per_second: 10000 # Number of message a client can send before being throttled # -#rc_message_burst_count: 10.0 +rc_message_burst_count: 10000 # Ratelimiting settings for registration and login. # @@ -389,20 +389,20 @@ log_config: "{{SYNAPSE_ROOT}}localhost.log.config" # # The defaults are as shown below. # -#rc_registration: -# per_second: 0.17 -# burst_count: 3 -# -#rc_login: -# address: -# per_second: 0.17 -# burst_count: 3 -# account: -# per_second: 0.17 -# burst_count: 3 -# failed_attempts: -# per_second: 0.17 -# burst_count: 3 +rc_registration: + per_second: 10000 + burst_count: 10000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 # The federation window size in milliseconds # From d63a0c5aea5ca2053376354e812a70345300fcc5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 2 Apr 2019 15:15:13 +0200 Subject: [PATCH 0158/2372] fix gete2e info and open settings, even though not used currently --- src/usecases/settings.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/usecases/settings.js b/src/usecases/settings.js index 5649671e7a..68a290c29d 100644 --- a/src/usecases/settings.js +++ b/src/usecases/settings.js @@ -16,6 +16,17 @@ limitations under the License. const assert = require('assert'); +async function openSettings(session, section) { + const menuButton = await session.query(".mx_TopLeftMenuButton_name"); + await menuButton.click(); + const settingsItem = await session.waitAndQuery(".mx_TopLeftMenu_icon_settings"); + await settingsItem.click(); + if (section) { + const sectionButton = await session.waitAndQuery(`.mx_UserSettingsDialog .mx_TabbedView_tabLabels .mx_UserSettingsDialog_${section}Icon`); + await sectionButton.click(); + } +} + module.exports.enableLazyLoading = async function(session) { session.log.step(`enables lazy loading of members in the lab settings`); const settingsButton = await session.query('.mx_BottomLeftMenu_settings'); @@ -30,13 +41,12 @@ module.exports.enableLazyLoading = async function(session) { module.exports.getE2EDeviceFromSettings = async function(session) { session.log.step(`gets e2e device/key from settings`); - const settingsButton = await session.query('.mx_BottomLeftMenu_settings'); - await settingsButton.click(); - const deviceAndKey = await session.waitAndQueryAll(".mx_UserSettings_section.mx_UserSettings_cryptoSection code"); + await openSettings(session, "security"); + const deviceAndKey = await session.waitAndQueryAll(".mx_SettingsTab_section .mx_SecurityUserSettingsTab_deviceInfo code"); assert.equal(deviceAndKey.length, 2); const id = await (await deviceAndKey[0].getProperty("innerText")).jsonValue(); const key = await (await deviceAndKey[1].getProperty("innerText")).jsonValue(); - const closeButton = await session.query(".mx_RoomHeader_cancelButton"); + const closeButton = await session.query(".mx_UserSettingsDialog .mx_Dialog_cancelButton"); await closeButton.click(); session.log.done(); return {id, key}; From e147cc9341a1b950760e669ada259d5bdd707d72 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 3 Apr 2019 14:31:31 +0200 Subject: [PATCH 0159/2372] this dialog isn't shown anymore and this was accepting the SAS dialog also lower timeout so we don't wait 5s if there is no dialog --- src/usecases/accept-invite.js | 3 --- src/usecases/dialog.js | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/usecases/accept-invite.js b/src/usecases/accept-invite.js index 8cc1a0b37d..03fbac28fa 100644 --- a/src/usecases/accept-invite.js +++ b/src/usecases/accept-invite.js @@ -34,8 +34,5 @@ module.exports = async function acceptInvite(session, name) { const acceptInvitationLink = await session.waitAndQuery(".mx_RoomPreviewBar_join_text a:first-child"); await acceptInvitationLink.click(); - // accept e2e warning dialog - acceptDialogMaybe(session, "encryption"); - session.log.done(); } diff --git a/src/usecases/dialog.js b/src/usecases/dialog.js index b11dac616d..da71f6b61f 100644 --- a/src/usecases/dialog.js +++ b/src/usecases/dialog.js @@ -32,7 +32,7 @@ async function acceptDialog(session, expectedTitle) { async function acceptDialogMaybe(session, expectedTitle) { let primaryButton = null; try { - primaryButton = await session.waitAndQuery(".mx_Dialog [role=dialog] .mx_Dialog_primary"); + primaryButton = await session.waitAndQuery(".mx_Dialog .mx_Dialog_primary", 50); } catch(err) { return false; } @@ -44,7 +44,7 @@ async function acceptDialogMaybe(session, expectedTitle) { } module.exports = { + assertDialog, acceptDialog, acceptDialogMaybe, - assertDialog, }; From e10e4b0eab4e0fea3d4c66cfdc9ef67f57df4a3a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 3 Apr 2019 14:37:03 +0200 Subject: [PATCH 0160/2372] nicer output, comment --- src/scenarios/e2e-encryption.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/scenarios/e2e-encryption.js b/src/scenarios/e2e-encryption.js index c7a6a5d085..29b97f2047 100644 --- a/src/scenarios/e2e-encryption.js +++ b/src/scenarios/e2e-encryption.js @@ -39,9 +39,10 @@ module.exports = async function e2eEncryptionScenarios(alice, bob) { bob.log.step(`starts SAS verification with ${alice.username}`); const bobSasPromise = startSasVerifcation(bob, alice.username); const aliceSasPromise = acceptSasVerification(alice, bob.username); + // wait in parallel, so they don't deadlock on each other const [bobSas, aliceSas] = await Promise.all([bobSasPromise, aliceSasPromise]); assert.deepEqual(bobSas, aliceSas); - bob.log.done(`done, (${bobSas.join(", ")}) matches!`); + bob.log.done(`done (match for ${bobSas.join(", ")})`); const aliceMessage = "Guess what I just heard?!" await sendMessage(alice, aliceMessage); await receiveMessage(bob, {sender: "alice", body: aliceMessage, encrypted: true}); From 2b2a4867bdbf446795f2980e417253286fabda7a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 3 Apr 2019 15:08:43 +0200 Subject: [PATCH 0161/2372] forgot to add consent listener again, this will fix REST registration --- synapse/config-templates/consent/homeserver.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/config-templates/consent/homeserver.yaml b/synapse/config-templates/consent/homeserver.yaml index 80a967b047..7fdf8a887d 100644 --- a/synapse/config-templates/consent/homeserver.yaml +++ b/synapse/config-templates/consent/homeserver.yaml @@ -177,7 +177,7 @@ listeners: x_forwarded: true resources: - - names: [client, federation] + - names: [client, federation, consent] compress: false # example additonal_resources: From 5939d624999dfc42e7c4f067af1ebe9344c3bab8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 3 Apr 2019 15:15:15 +0200 Subject: [PATCH 0162/2372] fix scrollpanel selector --- src/usecases/timeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usecases/timeline.js b/src/usecases/timeline.js index 191117891d..3298464c8d 100644 --- a/src/usecases/timeline.js +++ b/src/usecases/timeline.js @@ -20,7 +20,7 @@ module.exports.scrollToTimelineTop = async function(session) { session.log.step(`scrolls to the top of the timeline`); await session.page.evaluate(() => { return Promise.resolve().then(async () => { - const timelineScrollView = document.querySelector(".mx_RoomView .gm-scroll-view"); + const timelineScrollView = document.querySelector(".mx_RoomView_timeline .mx_ScrollPanel"); let timedOut = false; let timeoutHandle = null; // set scrollTop to 0 in a loop and check every 50ms From 3affb8f068b011a99f17e6a088ec156524129218 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 3 Apr 2019 15:15:28 +0200 Subject: [PATCH 0163/2372] section for creating rest users --- src/scenario.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scenario.js b/src/scenario.js index 4593960c10..6176d3a171 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -33,7 +33,7 @@ module.exports = async function scenario(createSession, restCreator) { await roomDirectoryScenarios(alice, bob); await e2eEncryptionScenarios(alice, bob); - + console.log("create REST users:"); const charlies = await createRestUsers(restCreator); await lazyLoadingScenarios(alice, bob, charlies); } From 9c41ccce58019cfe2169b82b4a8ec09aff33550a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 3 Apr 2019 15:46:55 +0200 Subject: [PATCH 0164/2372] use shorter .bak suffix approach --- synapse/install.sh | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/synapse/install.sh b/synapse/install.sh index c83ca6512a..2cc68dee03 100755 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -45,11 +45,10 @@ cp -r $BASE_DIR/config-templates/$CONFIG_TEMPLATE/. ./ # Hashes used instead of slashes because we'll get a value back from $(pwd) that'll be # full of un-escapable slashes. -# Manually directing output to .templated file and then manually renaming back on top -# of the original file because -i is a nonstandard sed feature which is implemented -# differently, across os X and ubuntu at least -sed "s#{{SYNAPSE_ROOT}}#$(pwd)/#g" homeserver.yaml > homeserver.yaml.templated && mv homeserver.yaml.templated homeserver.yaml -sed "s#{{SYNAPSE_PORT}}#${PORT}#g" homeserver.yaml > homeserver.yaml.templated && mv homeserver.yaml.templated homeserver.yaml -sed "s#{{FORM_SECRET}}#$(uuidgen)#g" homeserver.yaml > homeserver.yaml.templated && mv homeserver.yaml.templated homeserver.yaml -sed "s#{{REGISTRATION_SHARED_SECRET}}#$(uuidgen)#g" homeserver.yaml > homeserver.yaml.templated && mv homeserver.yaml.templated homeserver.yaml -sed "s#{{MACAROON_SECRET_KEY}}#$(uuidgen)#g" homeserver.yaml > homeserver.yaml.templated && mv homeserver.yaml.templated homeserver.yaml +# Use .bak suffix as using no suffix doesn't work macOS. +sed -i.bak "s#{{SYNAPSE_ROOT}}#$(pwd)/#g" homeserver.yaml +sed -i.bak "s#{{SYNAPSE_PORT}}#${PORT}#g" homeserver.yaml +sed -i.bak "s#{{FORM_SECRET}}#$(uuidgen)#g" homeserver.yaml +sed -i.bak "s#{{REGISTRATION_SHARED_SECRET}}#$(uuidgen)#g" homeserver.yaml +sed -i.bak "s#{{MACAROON_SECRET_KEY}}#$(uuidgen)#g" homeserver.yaml +rm *.bak From 5d4ded05b4e7c91de90f3ea3ab7e4f841d27637d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 3 Apr 2019 16:19:56 +0200 Subject: [PATCH 0165/2372] use yarn --- install.sh | 2 +- riot/install.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index 4008099a3d..e1fed144ce 100755 --- a/install.sh +++ b/install.sh @@ -3,4 +3,4 @@ set -e ./synapse/install.sh ./riot/install.sh -npm install +yarn install diff --git a/riot/install.sh b/riot/install.sh index b89a767446..9422bd225a 100755 --- a/riot/install.sh +++ b/riot/install.sh @@ -30,5 +30,5 @@ unzip -q riot.zip rm riot.zip mv riot-web-${RIOT_BRANCH} riot-web cd riot-web -npm install -npm run build +yarn install +yarn run build From 146549a66a9c2257617591a3613284e8637fcb3d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 3 Apr 2019 16:20:26 +0200 Subject: [PATCH 0166/2372] keep complexhttpserver installation within riot folder and gitignore leftovers --- .gitignore | 1 + riot/install.sh | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 24cd046858..afca1ddcb3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules *.png +riot/env diff --git a/riot/install.sh b/riot/install.sh index 9422bd225a..59f945d51b 100755 --- a/riot/install.sh +++ b/riot/install.sh @@ -8,23 +8,24 @@ if [ -d $BASE_DIR/riot-web ]; then exit fi +cd $BASE_DIR # Install ComplexHttpServer (a drop in replacement for Python's SimpleHttpServer # but with support for multiple threads) into a virtualenv. ( - virtualenv $BASE_DIR/env - source $BASE_DIR/env/bin/activate + virtualenv env + source env/bin/activate # Having been bitten by pip SSL fail too many times, I don't trust the existing pip # to be able to --upgrade itself, so grab a new one fresh from source. curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py python get-pip.py + rm get-pip.py pip install ComplexHttpServer deactivate ) -cd $BASE_DIR curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --output riot.zip unzip -q riot.zip rm riot.zip From d93e6edb1dc2cf96f1ea2d5f074e8999c2ae6475 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 3 Apr 2019 17:01:49 +0200 Subject: [PATCH 0167/2372] use python3 to install riot webserver --- riot/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/riot/install.sh b/riot/install.sh index 59f945d51b..e8eef9427f 100755 --- a/riot/install.sh +++ b/riot/install.sh @@ -12,7 +12,7 @@ cd $BASE_DIR # Install ComplexHttpServer (a drop in replacement for Python's SimpleHttpServer # but with support for multiple threads) into a virtualenv. ( - virtualenv env + virtualenv -p python3 env source env/bin/activate # Having been bitten by pip SSL fail too many times, I don't trust the existing pip From 04e06c3cfaff402f137e4ac9a579f0d145a8439f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 4 Apr 2019 10:20:25 +0200 Subject: [PATCH 0168/2372] PR feedback --- src/usecases/signup.js | 2 +- src/usecases/verify.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/usecases/signup.js b/src/usecases/signup.js index 1b9ba2872c..2522346970 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -35,7 +35,7 @@ module.exports = async function signup(session, username, password, homeserver) await session.replaceInputText(usernameField, username); await session.replaceInputText(passwordField, password); await session.replaceInputText(passwordRepeatField, password); - //wait over a second because Registration/ServerConfig have a 250ms + //wait 300ms because Registration/ServerConfig have a 250ms //delay to internally set the homeserver url //see Registration::render and ServerConfig::props::delayTimeMs await session.delay(300); diff --git a/src/usecases/verify.js b/src/usecases/verify.js index e9461c225e..b08f727553 100644 --- a/src/usecases/verify.js +++ b/src/usecases/verify.js @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +Copyright 2019 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From cfff4a998db3af18e422f86217913ec1f0247cbd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 5 Apr 2019 16:15:38 +0200 Subject: [PATCH 0169/2372] install ComplexHttpServer regardless of whether riot is already installed --- riot/install.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/riot/install.sh b/riot/install.sh index e8eef9427f..fda9537652 100755 --- a/riot/install.sh +++ b/riot/install.sh @@ -2,12 +2,6 @@ set -e RIOT_BRANCH=develop -BASE_DIR=$(cd $(dirname $0) && pwd) -if [ -d $BASE_DIR/riot-web ]; then - echo "riot is already installed" - exit -fi - cd $BASE_DIR # Install ComplexHttpServer (a drop in replacement for Python's SimpleHttpServer # but with support for multiple threads) into a virtualenv. @@ -26,6 +20,12 @@ cd $BASE_DIR deactivate ) +BASE_DIR=$(cd $(dirname $0) && pwd) +if [ -d $BASE_DIR/riot-web ]; then + echo "riot is already installed" + exit +fi + curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --output riot.zip unzip -q riot.zip rm riot.zip From 4e1ddf85a4792e76be7921aec3df4f8647fea34d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 5 Apr 2019 16:26:05 +0200 Subject: [PATCH 0170/2372] c/p error --- riot/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/riot/install.sh b/riot/install.sh index fda9537652..8a942a05ea 100755 --- a/riot/install.sh +++ b/riot/install.sh @@ -2,6 +2,7 @@ set -e RIOT_BRANCH=develop +BASE_DIR=$(cd $(dirname $0) && pwd) cd $BASE_DIR # Install ComplexHttpServer (a drop in replacement for Python's SimpleHttpServer # but with support for multiple threads) into a virtualenv. @@ -20,7 +21,6 @@ cd $BASE_DIR deactivate ) -BASE_DIR=$(cd $(dirname $0) && pwd) if [ -d $BASE_DIR/riot-web ]; then echo "riot is already installed" exit From 53eab479ecd4ffa8a6f53e6d5860bfb84d463a68 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 5 Apr 2019 16:45:07 +0200 Subject: [PATCH 0171/2372] pass --no-sandbox to puppeteer --- start.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/start.js b/start.js index 1c3f27bbe3..099282740d 100644 --- a/start.js +++ b/start.js @@ -26,6 +26,7 @@ program .option('--windowed', "dont run tests headless", false) .option('--slow-mo', "run tests slower to follow whats going on", false) .option('--dev-tools', "open chrome devtools in browser window", false) + .option('--no-sandbox', "same as puppeteer arg", false) .parse(process.argv); const hsUrl = 'http://localhost:5005'; @@ -37,7 +38,11 @@ async function runTests() { slowMo: program.slowMo ? 20 : undefined, devtools: program.devTools, headless: !program.windowed, + args: [], }; + if (!program.sandbox) { + options.args.push('--no-sandbox'); + } if (process.env.CHROME_PATH) { const path = process.env.CHROME_PATH; console.log(`(using external chrome/chromium at ${path}, make sure it's compatible with puppeteer)`); From 7e2d35fdfe48ee0d5360b987ede78b0a431ca096 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 5 Apr 2019 16:55:59 +0200 Subject: [PATCH 0172/2372] moar sandbox flags --- start.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.js b/start.js index 099282740d..8c8b640595 100644 --- a/start.js +++ b/start.js @@ -41,7 +41,7 @@ async function runTests() { args: [], }; if (!program.sandbox) { - options.args.push('--no-sandbox'); + options.args.push('--no-sandbox', '--disable-setuid-sandbox'); } if (process.env.CHROME_PATH) { const path = process.env.CHROME_PATH; From 492d8106b2a7ee6f06406b5c5c929dff9d2f5d55 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 8 Apr 2019 17:00:19 +0200 Subject: [PATCH 0173/2372] support writing logs to file --- start.js | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/start.js b/start.js index 8c8b640595..ab710664ec 100644 --- a/start.js +++ b/start.js @@ -18,6 +18,7 @@ const assert = require('assert'); const RiotSession = require('./src/session'); const scenario = require('./src/scenario'); const RestSessionCreator = require('./src/rest/creator'); +const fs = require("fs"); const program = require('commander'); program @@ -27,6 +28,7 @@ program .option('--slow-mo', "run tests slower to follow whats going on", false) .option('--dev-tools', "open chrome devtools in browser window", false) .option('--no-sandbox', "same as puppeteer arg", false) + .option('--error-log ', 'stdout, or a file to dump html and network logs in when the tests fail') .parse(process.argv); const hsUrl = 'http://localhost:5005'; @@ -67,18 +69,13 @@ async function runTests() { } catch(err) { failure = true; console.log('failure: ', err); - if (program.logs) { - for(let i = 0; i < sessions.length; ++i) { - const session = sessions[i]; - documentHtml = await session.page.content(); - console.log(`---------------- START OF ${session.username} LOGS ----------------`); - console.log('---------------- console.log output:'); - console.log(session.consoleLogs()); - console.log('---------------- network requests:'); - console.log(session.networkLogs()); - console.log('---------------- document html:'); - console.log(documentHtml); - console.log(`---------------- END OF ${session.username} LOGS ----------------`); + if (program.errorLog) { + const logs = await createLogs(sessions); + if (program.errorLog === "stdout") { + process.stdout.write(logs); + } else { + console.log(`wrote logs to "${program.errorLog}"`); + fs.writeFileSync(program.errorLog, logs); } } } @@ -98,6 +95,23 @@ async function runTests() { } } +async function createLogs(sessions) { + let logs = ""; + for(let i = 0; i < sessions.length; ++i) { + const session = sessions[i]; + documentHtml = await session.page.content(); + logs = logs + `---------------- START OF ${session.username} LOGS ----------------\n`; + logs = logs + '\n---------------- console.log output:\n'; + logs = logs + session.consoleLogs(); + logs = logs + '\n---------------- network requests:\n'; + logs = logs + session.networkLogs(); + logs = logs + '\n---------------- document html:\n'; + logs = logs + documentHtml; + logs = logs + `\n---------------- END OF ${session.username} LOGS ----------------\n`; + } + return logs; +} + runTests().catch(function(err) { console.log(err); process.exit(-1); From d1df0d98c67e2623d96a7015970ceea60fbc62de Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 9 Apr 2019 14:58:54 +0200 Subject: [PATCH 0174/2372] avoid ipv6, see if that makes buildkite happy --- synapse/config-templates/consent/homeserver.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/config-templates/consent/homeserver.yaml b/synapse/config-templates/consent/homeserver.yaml index 7fdf8a887d..e07cf585d8 100644 --- a/synapse/config-templates/consent/homeserver.yaml +++ b/synapse/config-templates/consent/homeserver.yaml @@ -172,7 +172,7 @@ listeners: # - port: {{SYNAPSE_PORT}} tls: false - bind_addresses: ['::1', '127.0.0.1'] + bind_addresses: ['127.0.0.1'] type: http x_forwarded: true From 4591b26dab05dffd5db04187eb792837d858724c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 9 Apr 2019 15:30:20 +0200 Subject: [PATCH 0175/2372] show all of create rest user command output on failure --- src/rest/creator.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rest/creator.js b/src/rest/creator.js index cc87134108..35f20badb2 100644 --- a/src/rest/creator.js +++ b/src/rest/creator.js @@ -59,10 +59,10 @@ module.exports = class RestSessionCreator { try { await exec(allCmds, {cwd: this.cwd, encoding: 'utf-8'}); } catch (result) { - const lines = result.stdout.trim().split('\n'); - const failureReason = lines[lines.length - 1]; - const logs = (await exec("tail -n 100 synapse/installations/consent/homeserver.log")).stdout; - throw new Error(`creating user ${username} failed: ${failureReason}, synapse logs:\n${logs}`); + // const lines = result.stdout.trim().split('\n'); + // const failureReason = lines[lines.length - 1]; + // const logs = (await exec("tail -n 100 synapse/installations/consent/homeserver.log")).stdout; + throw new Error(`creating user ${username} failed, script output:\n ${result.stdout.trim()}`); } } From b88dc0ffd5dc492c44e508b65c3a10e81fbb54b4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 9 Apr 2019 15:45:16 +0200 Subject: [PATCH 0176/2372] show browser version when running tests --- src/scenario.js | 6 ++++++ start.js | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/scenario.js b/src/scenario.js index 6176d3a171..cd818fd7bc 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -22,8 +22,14 @@ const lazyLoadingScenarios = require('./scenarios/lazy-loading'); const e2eEncryptionScenarios = require('./scenarios/e2e-encryption'); module.exports = async function scenario(createSession, restCreator) { + let firstUser = true; async function createUser(username) { const session = await createSession(username); + if (firstUser) { + // only show browser version for first browser opened + console.log(`running tests on ${await session.browser.version()} ...`); + firstUser = false; + } await signup(session, session.username, 'testtest', session.hsUrl); return session; } diff --git a/start.js b/start.js index ab710664ec..87a21d565c 100644 --- a/start.js +++ b/start.js @@ -35,7 +35,6 @@ const hsUrl = 'http://localhost:5005'; async function runTests() { let sessions = []; - console.log("running tests ..."); const options = { slowMo: program.slowMo ? 20 : undefined, devtools: program.devTools, From 4c79e3bd0d589aed868b2a46a30fedff2cb56d2e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 9 Apr 2019 15:59:08 +0200 Subject: [PATCH 0177/2372] better error handling when creating rest user fails --- src/rest/creator.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/rest/creator.js b/src/rest/creator.js index 35f20badb2..ca414c097c 100644 --- a/src/rest/creator.js +++ b/src/rest/creator.js @@ -14,12 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -const util = require('util'); -const exec = util.promisify(require('child_process').exec); +const {exec} = require('child_process'); const request = require('request-promise-native'); const RestSession = require('./session'); const RestMultiSession = require('./multi'); +function execAsync(command, options) { + return new Promise((resolve, reject) => { + exec(command, options, (error, stdout, stderr) => { + if (error) { + reject(error); + } else { + resolve({stdout, stderr}); + } + }); + }); +} + module.exports = class RestSessionCreator { constructor(synapseSubdir, hsUrl, cwd) { this.synapseSubdir = synapseSubdir; @@ -56,14 +67,7 @@ module.exports = class RestSessionCreator { registerCmd ].join(';'); - try { - await exec(allCmds, {cwd: this.cwd, encoding: 'utf-8'}); - } catch (result) { - // const lines = result.stdout.trim().split('\n'); - // const failureReason = lines[lines.length - 1]; - // const logs = (await exec("tail -n 100 synapse/installations/consent/homeserver.log")).stdout; - throw new Error(`creating user ${username} failed, script output:\n ${result.stdout.trim()}`); - } + await execAsync(allCmds, {cwd: this.cwd, encoding: 'utf-8'}); } async _authenticate(username, password) { From 7fbfe3159a22857ddb09d8706c577a39dfd2b017 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 9 Apr 2019 16:19:44 +0200 Subject: [PATCH 0178/2372] dont assume bash when creating rest users --- src/rest/creator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rest/creator.js b/src/rest/creator.js index ca414c097c..5330f88bbf 100644 --- a/src/rest/creator.js +++ b/src/rest/creator.js @@ -63,9 +63,9 @@ module.exports = class RestSessionCreator { const registerCmd = `./scripts/register_new_matrix_user ${registerArgs.join(' ')}`; const allCmds = [ `cd ${this.synapseSubdir}`, - "source env/bin/activate", + ". env/bin/activate", registerCmd - ].join(';'); + ].join('&&'); await execAsync(allCmds, {cwd: this.cwd, encoding: 'utf-8'}); } From 200f95b31221e5b6df0f7558651d23748d118904 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 9 Apr 2019 16:32:07 +0200 Subject: [PATCH 0179/2372] rest users dont need to be admin --- src/rest/creator.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rest/creator.js b/src/rest/creator.js index 5330f88bbf..2c15bae792 100644 --- a/src/rest/creator.js +++ b/src/rest/creator.js @@ -56,8 +56,7 @@ module.exports = class RestSessionCreator { '-c homeserver.yaml', `-u ${username}`, `-p ${password}`, - // '--regular-user', - '-a', //until PR gets merged + '--no-admin', this.hsUrl ]; const registerCmd = `./scripts/register_new_matrix_user ${registerArgs.join(' ')}`; From d978ce6b48f3d8de32ed233c8427a37dfa2a7239 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 9 Apr 2019 16:34:03 +0200 Subject: [PATCH 0180/2372] test upload artefacts on failure --- src/scenario.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scenario.js b/src/scenario.js index cd818fd7bc..fc5f62773b 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -42,6 +42,7 @@ module.exports = async function scenario(createSession, restCreator) { console.log("create REST users:"); const charlies = await createRestUsers(restCreator); await lazyLoadingScenarios(alice, bob, charlies); + throw new Error("f41l!!!"); } async function createRestUsers(restCreator) { From 6958fdb6e113bb7b0e5684ee383ffae028def666 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 9 Apr 2019 17:15:25 +0200 Subject: [PATCH 0181/2372] write html, console and network logs to different files --- start.js | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/start.js b/start.js index 87a21d565c..9bd78db4f8 100644 --- a/start.js +++ b/start.js @@ -28,7 +28,7 @@ program .option('--slow-mo', "run tests slower to follow whats going on", false) .option('--dev-tools', "open chrome devtools in browser window", false) .option('--no-sandbox', "same as puppeteer arg", false) - .option('--error-log ', 'stdout, or a file to dump html and network logs in when the tests fail') + .option('--log-directory ', 'a directory to dump html and network logs in when the tests fail') .parse(process.argv); const hsUrl = 'http://localhost:5005'; @@ -68,14 +68,8 @@ async function runTests() { } catch(err) { failure = true; console.log('failure: ', err); - if (program.errorLog) { - const logs = await createLogs(sessions); - if (program.errorLog === "stdout") { - process.stdout.write(logs); - } else { - console.log(`wrote logs to "${program.errorLog}"`); - fs.writeFileSync(program.errorLog, logs); - } + if (program.logDirectory) { + await writeLogs(sessions, program.logDirectory); } } @@ -94,19 +88,19 @@ async function runTests() { } } -async function createLogs(sessions) { +async function writeLogs(sessions, dir) { let logs = ""; for(let i = 0; i < sessions.length; ++i) { const session = sessions[i]; + const userLogDir = `${dir}/${session.username}`; + fs.mkdirSync(userLogDir); + const consoleLogName = `${userLogDir}/console.log`; + const networkLogName = `${userLogDir}/network.log`; + const appHtmlName = `${userLogDir}/app.html`; documentHtml = await session.page.content(); - logs = logs + `---------------- START OF ${session.username} LOGS ----------------\n`; - logs = logs + '\n---------------- console.log output:\n'; - logs = logs + session.consoleLogs(); - logs = logs + '\n---------------- network requests:\n'; - logs = logs + session.networkLogs(); - logs = logs + '\n---------------- document html:\n'; - logs = logs + documentHtml; - logs = logs + `\n---------------- END OF ${session.username} LOGS ----------------\n`; + fs.writeFileSync(appHtmlName, documentHtml); + fs.writeFileSync(networkLogName, session.networkLogs()); + fs.writeFileSync(consoleLogName, session.consoleLogs()); } return logs; } From f55a4487391e5eabfdefc0b9b14f5d5302b04dfc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 9 Apr 2019 17:43:34 +0200 Subject: [PATCH 0182/2372] add screenshots to logs directory upon failure --- start.js | 1 + 1 file changed, 1 insertion(+) diff --git a/start.js b/start.js index 9bd78db4f8..43988095f9 100644 --- a/start.js +++ b/start.js @@ -101,6 +101,7 @@ async function writeLogs(sessions, dir) { fs.writeFileSync(appHtmlName, documentHtml); fs.writeFileSync(networkLogName, session.networkLogs()); fs.writeFileSync(consoleLogName, session.consoleLogs()); + await session.page.screenshot({path: `${userLogDir}/screenshot.png`}); } return logs; } From b0fb36dbb21c015fcfed772895342ed74b59a247 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 9 Apr 2019 18:09:18 +0200 Subject: [PATCH 0183/2372] remove debug error --- src/scenario.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/scenario.js b/src/scenario.js index fc5f62773b..cd818fd7bc 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -42,7 +42,6 @@ module.exports = async function scenario(createSession, restCreator) { console.log("create REST users:"); const charlies = await createRestUsers(restCreator); await lazyLoadingScenarios(alice, bob, charlies); - throw new Error("f41l!!!"); } async function createRestUsers(restCreator) { From be32414214d83f1f92b4e865fd4b7bc85693ee8e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 10 Apr 2019 14:32:13 +0200 Subject: [PATCH 0184/2372] fix broken selector --- src/usecases/invite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usecases/invite.js b/src/usecases/invite.js index bbfc38cd23..03dff7b30f 100644 --- a/src/usecases/invite.js +++ b/src/usecases/invite.js @@ -21,7 +21,7 @@ module.exports = async function invite(session, userId) { await session.delay(1000); const inviteButton = await session.waitAndQuery(".mx_MemberList_invite"); await inviteButton.click(); - const inviteTextArea = await session.waitAndQuery(".mx_ChatInviteDialog textarea"); + const inviteTextArea = await session.waitAndQuery(".mx_AddressPickerDialog textarea"); await inviteTextArea.type(userId); await inviteTextArea.press("Enter"); const confirmButton = await session.query(".mx_Dialog_primary"); From b01e126433d8d366a1f44314752eb2920a54a786 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 16 Apr 2019 10:30:14 +0200 Subject: [PATCH 0185/2372] wait longer to arrive at #home but poll every 100ms --- src/session.js | 13 +++++++++++++ src/usecases/signup.js | 8 +++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/session.js b/src/session.js index 1363185753..126112d7c7 100644 --- a/src/session.js +++ b/src/session.js @@ -201,4 +201,17 @@ module.exports = class RiotSession { close() { return this.browser.close(); } + + async poll(callback, timeout) { + const INTERVAL = 100; + let waited = 0; + while(waited < timeout) { + await this.delay(INTERVAL); + waited += INTERVAL; + if (callback()) { + return true; + } + } + return false; + } } diff --git a/src/usecases/signup.js b/src/usecases/signup.js index 2522346970..1f9605c30e 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -64,9 +64,11 @@ module.exports = async function signup(session, username, password, homeserver) //wait for registration to finish so the hash gets set //onhashchange better? - await session.delay(2000); - const url = session.page.url(); - assert.strictEqual(url, session.url('/#/home')); + const foundHomeUrl = await session.poll(() => { + const url = session.page.url(); + return url === session.url('/#/home'); + }, 5000); + assert(foundHomeUrl); session.log.done(); } From f489f55fd7708c955b78dff5eb9c84e8bf76cda7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 16 Apr 2019 11:09:34 +0200 Subject: [PATCH 0186/2372] increase dialog timeout a bit --- src/usecases/dialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usecases/dialog.js b/src/usecases/dialog.js index da71f6b61f..10b343128a 100644 --- a/src/usecases/dialog.js +++ b/src/usecases/dialog.js @@ -32,7 +32,7 @@ async function acceptDialog(session, expectedTitle) { async function acceptDialogMaybe(session, expectedTitle) { let primaryButton = null; try { - primaryButton = await session.waitAndQuery(".mx_Dialog .mx_Dialog_primary", 50); + primaryButton = await session.waitAndQuery(".mx_Dialog .mx_Dialog_primary", 100); } catch(err) { return false; } From 945daf294c411d597516dad37dcf484f9b9074b3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 16 Apr 2019 11:14:55 +0200 Subject: [PATCH 0187/2372] log messages in timeline when expected not found (debug code) --- src/usecases/timeline.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/usecases/timeline.js b/src/usecases/timeline.js index 3298464c8d..71e47fbfbb 100644 --- a/src/usecases/timeline.js +++ b/src/usecases/timeline.js @@ -87,7 +87,12 @@ module.exports.checkTimelineContains = async function (session, expectedMessages return message.sender === expectedMessage.sender && message.body === expectedMessage.body; }); - assertMessage(foundMessage, expectedMessage); + try { + assertMessage(foundMessage, expectedMessage); + } catch(err) { + console.log("timelineMessages", timelineMessages); + throw err; + } }); session.log.done(); From 9610e9b57e25e4da6f41e070b43ff0cc50c7287c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 16 Apr 2019 11:43:32 +0200 Subject: [PATCH 0188/2372] take into account continuation tiles in when checking timeline messages --- src/usecases/timeline.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/usecases/timeline.js b/src/usecases/timeline.js index 71e47fbfbb..5556660e1c 100644 --- a/src/usecases/timeline.js +++ b/src/usecases/timeline.js @@ -81,7 +81,16 @@ module.exports.checkTimelineContains = async function (session, expectedMessages return getMessageFromEventTile(eventTile); })); //filter out tiles that were not messages - timelineMessages = timelineMessages .filter((m) => !!m); + timelineMessages = timelineMessages.filter((m) => !!m); + timelineMessages.reduce((prevSender, m) => { + if (m.continuation) { + m.sender = prevSender; + return prevSender; + } else { + return m.sender; + } + }); + expectedMessages.forEach((expectedMessage) => { const foundMessage = timelineMessages.find((message) => { return message.sender === expectedMessage.sender && @@ -132,6 +141,7 @@ async function getMessageFromEventTile(eventTile) { return { sender, body, - encrypted: classNames.includes("mx_EventTile_verified") + encrypted: classNames.includes("mx_EventTile_verified"), + continuation: classNames.includes("mx_EventTile_continuation"), }; } From 20c3023b9405f3358a9038bc36da7d3d92118731 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 16 Apr 2019 11:55:33 +0200 Subject: [PATCH 0189/2372] use session.poll as well for polling when receiving a message --- src/session.js | 9 ++++----- src/usecases/signup.js | 2 +- src/usecases/timeline.js | 22 +++++++++------------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/session.js b/src/session.js index 126112d7c7..e1d0daf7fe 100644 --- a/src/session.js +++ b/src/session.js @@ -202,13 +202,12 @@ module.exports = class RiotSession { return this.browser.close(); } - async poll(callback, timeout) { - const INTERVAL = 100; + async poll(callback, timeout, interval = 100) { let waited = 0; while(waited < timeout) { - await this.delay(INTERVAL); - waited += INTERVAL; - if (callback()) { + await this.delay(interval); + waited += interval; + if (await callback()) { return true; } } diff --git a/src/usecases/signup.js b/src/usecases/signup.js index 1f9605c30e..17b9669e6c 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -65,7 +65,7 @@ module.exports = async function signup(session, username, password, homeserver) //wait for registration to finish so the hash gets set //onhashchange better? - const foundHomeUrl = await session.poll(() => { + const foundHomeUrl = await session.poll(async () => { const url = session.page.url(); return url === session.url('/#/home'); }, 5000); diff --git a/src/usecases/timeline.js b/src/usecases/timeline.js index 5556660e1c..7b28328e49 100644 --- a/src/usecases/timeline.js +++ b/src/usecases/timeline.js @@ -52,22 +52,18 @@ module.exports.receiveMessage = async function(session, expectedMessage) { return getMessageFromEventTile(lastTile); } - let lastMessage = null; - let isExpectedMessage = false; - let totalTime = 0; - while (!isExpectedMessage) { + let lastMessage; + await session.poll(async () => { try { lastMessage = await getLastMessage(); - isExpectedMessage = lastMessage && - lastMessage.body === expectedMessage.body && - lastMessage.sender === expectedMessage.sender - } catch(err) {} - if (totalTime > 5000) { - throw new Error("timed out after 5000ms"); + } catch(err) { + return false; } - totalTime += 200; - await session.delay(200); - } + // stop polling when found the expected message + return lastMessage && + lastMessage.body === expectedMessage.body && + lastMessage.sender === expectedMessage.sender; + }, 5000, 200); assertMessage(lastMessage, expectedMessage); session.log.done(); } From ee46c2b030d9806a6f7e1fac06cc8d471aed266f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 16 Apr 2019 12:07:59 +0200 Subject: [PATCH 0190/2372] wait for opponent label to appear in sas dialog --- src/usecases/verify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usecases/verify.js b/src/usecases/verify.js index b08f727553..233bf4f954 100644 --- a/src/usecases/verify.js +++ b/src/usecases/verify.js @@ -53,7 +53,7 @@ module.exports.startSasVerifcation = async function(session, name) { module.exports.acceptSasVerification = async function(session, name) { await assertDialog(session, "Incoming Verification Request"); - const opponentLabelElement = await session.query(".mx_IncomingSasDialog_opponentProfile h2"); + const opponentLabelElement = await session.waitAndQuery(".mx_IncomingSasDialog_opponentProfile h2"); const opponentLabel = await session.innerText(opponentLabelElement); assert(opponentLabel, name); // click "Continue" button From ef59c59f37a90acd8f6f8eac2b6e5762d2fadec3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 16 Apr 2019 12:22:48 +0200 Subject: [PATCH 0191/2372] poll these as well as ci is slowwww --- src/usecases/signup.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/usecases/signup.js b/src/usecases/signup.js index 17b9669e6c..0bfdb27a58 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -23,9 +23,9 @@ module.exports = async function signup(session, username, password, homeserver) if (homeserver) { const changeServerDetailsLink = await session.waitAndQuery('.mx_AuthBody_editServerDetails'); await changeServerDetailsLink.click(); - const hsInputField = await session.query('#mx_ServerConfig_hsUrl'); + const hsInputField = await session.waitAndQuery('#mx_ServerConfig_hsUrl'); await session.replaceInputText(hsInputField, homeserver); - const nextButton = await session.query('.mx_Login_submit'); + const nextButton = await session.waitAndQuery('.mx_Login_submit'); await nextButton.click(); } //fill out form @@ -41,7 +41,7 @@ module.exports = async function signup(session, username, password, homeserver) await session.delay(300); /// focus on the button to make sure error validation /// has happened before checking the form is good to go - const registerButton = await session.query('.mx_Login_submit'); + const registerButton = await session.waitAndQuery('.mx_Login_submit'); await registerButton.focus(); //check no errors const error_text = await session.tryGetInnertext('.mx_Login_error'); From c40f7f6a3c90feb58eeb55e17a9f13a37e33fd34 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 16 Apr 2019 14:59:35 +0200 Subject: [PATCH 0192/2372] add flag to throttle cpu to get failures because of slow ci locally --- src/session.js | 7 ++++++- start.js | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/session.js b/src/session.js index e1d0daf7fe..80e3225f1f 100644 --- a/src/session.js +++ b/src/session.js @@ -35,13 +35,18 @@ module.exports = class RiotSession { this.log = new Logger(this.username); } - static async create(username, puppeteerOptions, riotserver, hsUrl) { + static async create(username, puppeteerOptions, riotserver, hsUrl, throttleCpuFactor = 1) { const browser = await puppeteer.launch(puppeteerOptions); const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); + if (throttleCpuFactor !== 1) { + const client = await page.target().createCDPSession(); + console.log("throttling cpu by a factor of", throttleCpuFactor); + await client.send('Emulation.setCPUThrottlingRate', { rate: throttleCpuFactor }); + } return new RiotSession(browser, page, username, riotserver, hsUrl); } diff --git a/start.js b/start.js index 43988095f9..b98dc7f366 100644 --- a/start.js +++ b/start.js @@ -25,8 +25,9 @@ program .option('--no-logs', "don't output logs, document html on error", false) .option('--riot-url [url]', "riot url to test", "http://localhost:5000") .option('--windowed', "dont run tests headless", false) - .option('--slow-mo', "run tests slower to follow whats going on", false) + .option('--slow-mo', "type at a human speed", false) .option('--dev-tools', "open chrome devtools in browser window", false) + .option('--throttle-cpu [factor]', "factor to slow down the cpu with", parseFloat, 1.0) .option('--no-sandbox', "same as puppeteer arg", false) .option('--log-directory ', 'a directory to dump html and network logs in when the tests fail') .parse(process.argv); @@ -57,7 +58,7 @@ async function runTests() { ); async function createSession(username) { - const session = await RiotSession.create(username, options, program.riotUrl, hsUrl); + const session = await RiotSession.create(username, options, program.riotUrl, hsUrl, program.throttleCpu); sessions.push(session); return session; } From 48c1d46aa71b2d6278065a9f6c2c8fc8792cc834 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 16 Apr 2019 15:35:31 +0200 Subject: [PATCH 0193/2372] remove explicit timeouts from tests for selectors - gets rid of the waitAndQuery vs query distinction, all queries now wait if needed (called query and queryAll) - remove explicit timeouts, as they depend on the speed of the machine the tests run on --- src/session.js | 24 +++++++++++------------- src/usecases/accept-invite.js | 4 ++-- src/usecases/create-room.js | 8 ++++---- src/usecases/dialog.js | 4 ++-- src/usecases/invite.js | 4 ++-- src/usecases/join.js | 8 ++++---- src/usecases/memberlist.js | 12 ++++++------ src/usecases/room-settings.js | 10 +++++----- src/usecases/send-message.js | 4 ++-- src/usecases/settings.js | 10 +++++----- src/usecases/signup.js | 20 ++++++++++---------- src/usecases/verify.js | 8 ++++---- 12 files changed, 57 insertions(+), 59 deletions(-) diff --git a/src/session.js b/src/session.js index 80e3225f1f..4fe882c97d 100644 --- a/src/session.js +++ b/src/session.js @@ -19,6 +19,8 @@ const Logger = require('./logger'); const LogBuffer = require('./logbuffer'); const {delay} = require('./util'); +const DEFAULT_TIMEOUT = 20000; + module.exports = class RiotSession { constructor(browser, page, username, riotserver, hsUrl) { this.browser = browser; @@ -113,23 +115,18 @@ module.exports = class RiotSession { } query(selector) { - return this.page.$(selector); - } - - waitAndQuery(selector, timeout = 5000) { + const timeout = DEFAULT_TIMEOUT; return this.page.waitForSelector(selector, {visible: true, timeout}); } - queryAll(selector) { - return this.page.$$(selector); + async queryAll(selector) { + const timeout = DEFAULT_TIMEOUT; + await this.query(selector, timeout); + return await this.page.$$(selector); } - async waitAndQueryAll(selector, timeout = 5000) { - await this.waitAndQuery(selector, timeout); - return await this.queryAll(selector); - } - - waitForReload(timeout = 10000) { + waitForReload() { + const timeout = DEFAULT_TIMEOUT; return new Promise((resolve, reject) => { const timeoutHandle = setTimeout(() => { this.browser.removeEventListener('domcontentloaded', callback); @@ -145,7 +142,8 @@ module.exports = class RiotSession { }); } - waitForNewPage(timeout = 5000) { + waitForNewPage() { + const timeout = DEFAULT_TIMEOUT; return new Promise((resolve, reject) => { const timeoutHandle = setTimeout(() => { this.browser.removeListener('targetcreated', callback); diff --git a/src/usecases/accept-invite.js b/src/usecases/accept-invite.js index 03fbac28fa..3676f6641b 100644 --- a/src/usecases/accept-invite.js +++ b/src/usecases/accept-invite.js @@ -20,7 +20,7 @@ const {acceptDialogMaybe} = require('./dialog'); module.exports = async function acceptInvite(session, name) { session.log.step(`accepts "${name}" invite`); //TODO: brittle selector - const invitesHandles = await session.waitAndQueryAll('.mx_RoomTile_name.mx_RoomTile_invite'); + const invitesHandles = await session.queryAll('.mx_RoomTile_name.mx_RoomTile_invite'); const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => { const text = await session.innerText(inviteHandle); return {inviteHandle, text}; @@ -31,7 +31,7 @@ module.exports = async function acceptInvite(session, name) { await inviteHandle.click(); - const acceptInvitationLink = await session.waitAndQuery(".mx_RoomPreviewBar_join_text a:first-child"); + const acceptInvitationLink = await session.query(".mx_RoomPreviewBar_join_text a:first-child"); await acceptInvitationLink.click(); session.log.done(); diff --git a/src/usecases/create-room.js b/src/usecases/create-room.js index 16d0620879..4578dbaf0a 100644 --- a/src/usecases/create-room.js +++ b/src/usecases/create-room.js @@ -31,16 +31,16 @@ async function openRoomDirectory(session) { async function createRoom(session, roomName) { session.log.step(`creates room "${roomName}"`); await openRoomDirectory(session); - const createRoomButton = await session.waitAndQuery('.mx_RoomDirectory_createRoom'); + const createRoomButton = await session.query('.mx_RoomDirectory_createRoom'); await createRoomButton.click(); - const roomNameInput = await session.waitAndQuery('.mx_CreateRoomDialog_input'); + const roomNameInput = await session.query('.mx_CreateRoomDialog_input'); await session.replaceInputText(roomNameInput, roomName); - const createButton = await session.waitAndQuery('.mx_Dialog_primary'); + const createButton = await session.query('.mx_Dialog_primary'); await createButton.click(); - await session.waitAndQuery('.mx_MessageComposer'); + await session.query('.mx_MessageComposer'); session.log.done(); } diff --git a/src/usecases/dialog.js b/src/usecases/dialog.js index 10b343128a..58f135de04 100644 --- a/src/usecases/dialog.js +++ b/src/usecases/dialog.js @@ -17,7 +17,7 @@ limitations under the License. const assert = require('assert'); async function assertDialog(session, expectedTitle) { - const titleElement = await session.waitAndQuery(".mx_Dialog .mx_Dialog_title"); + const titleElement = await session.query(".mx_Dialog .mx_Dialog_title"); const dialogHeader = await session.innerText(titleElement); assert(dialogHeader, expectedTitle); } @@ -32,7 +32,7 @@ async function acceptDialog(session, expectedTitle) { async function acceptDialogMaybe(session, expectedTitle) { let primaryButton = null; try { - primaryButton = await session.waitAndQuery(".mx_Dialog .mx_Dialog_primary", 100); + primaryButton = await session.query(".mx_Dialog .mx_Dialog_primary"); } catch(err) { return false; } diff --git a/src/usecases/invite.js b/src/usecases/invite.js index 03dff7b30f..7b3f8a2b48 100644 --- a/src/usecases/invite.js +++ b/src/usecases/invite.js @@ -19,9 +19,9 @@ const assert = require('assert'); module.exports = async function invite(session, userId) { session.log.step(`invites "${userId}" to room`); await session.delay(1000); - const inviteButton = await session.waitAndQuery(".mx_MemberList_invite"); + const inviteButton = await session.query(".mx_MemberList_invite"); await inviteButton.click(); - const inviteTextArea = await session.waitAndQuery(".mx_AddressPickerDialog textarea"); + const inviteTextArea = await session.query(".mx_AddressPickerDialog textarea"); await inviteTextArea.type(userId); await inviteTextArea.press("Enter"); const confirmButton = await session.query(".mx_Dialog_primary"); diff --git a/src/usecases/join.js b/src/usecases/join.js index cba9e06660..cf23fbfb64 100644 --- a/src/usecases/join.js +++ b/src/usecases/join.js @@ -20,15 +20,15 @@ const {openRoomDirectory} = require('./create-room'); module.exports = async function join(session, roomName) { session.log.step(`joins room "${roomName}"`); await openRoomDirectory(session); - const roomInput = await session.waitAndQuery('.mx_DirectorySearchBox input'); + const roomInput = await session.query('.mx_DirectorySearchBox input'); await session.replaceInputText(roomInput, roomName); - const firstRoomLabel = await session.waitAndQuery('.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child', 1000); + const firstRoomLabel = await session.query('.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child'); await firstRoomLabel.click(); - const joinLink = await session.waitAndQuery('.mx_RoomPreviewBar_join_text a'); + const joinLink = await session.query('.mx_RoomPreviewBar_join_text a'); await joinLink.click(); - await session.waitAndQuery('.mx_MessageComposer'); + await session.query('.mx_MessageComposer'); session.log.done(); } diff --git a/src/usecases/memberlist.js b/src/usecases/memberlist.js index 0cd8744853..5858e82bb8 100644 --- a/src/usecases/memberlist.js +++ b/src/usecases/memberlist.js @@ -34,20 +34,20 @@ module.exports.verifyDeviceForUser = async function(session, name, expectedDevic }).map((m) => m.label)[0]; await matchingLabel.click(); // click verify in member info - const firstVerifyButton = await session.waitAndQuery(".mx_MemberDeviceInfo_verify"); + const firstVerifyButton = await session.query(".mx_MemberDeviceInfo_verify"); await firstVerifyButton.click(); // expect "Verify device" dialog and click "Begin Verification" - const dialogHeader = await session.innerText(await session.waitAndQuery(".mx_Dialog .mx_Dialog_title")); + const dialogHeader = await session.innerText(await session.query(".mx_Dialog .mx_Dialog_title")); assert(dialogHeader, "Verify device"); - const beginVerificationButton = await session.waitAndQuery(".mx_Dialog .mx_Dialog_primary") + const beginVerificationButton = await session.query(".mx_Dialog .mx_Dialog_primary") await beginVerificationButton.click(); // get emoji SAS labels - const sasLabelElements = await session.waitAndQueryAll(".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label"); + const sasLabelElements = await session.queryAll(".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label"); const sasLabels = await Promise.all(sasLabelElements.map(e => session.innerText(e))); console.log("my sas labels", sasLabels); - const dialogCodeFields = await session.waitAndQueryAll(".mx_QuestionDialog code"); + const dialogCodeFields = await session.queryAll(".mx_QuestionDialog code"); assert.equal(dialogCodeFields.length, 2); const deviceId = await session.innerText(dialogCodeFields[0]); const deviceKey = await session.innerText(dialogCodeFields[1]); @@ -61,7 +61,7 @@ module.exports.verifyDeviceForUser = async function(session, name, expectedDevic } async function getMembersInMemberlist(session) { - const memberNameElements = await session.waitAndQueryAll(".mx_MemberList .mx_EntityTile_name"); + const memberNameElements = await session.queryAll(".mx_MemberList .mx_EntityTile_name"); return Promise.all(memberNameElements.map(async (el) => { return {label: el, displayName: await session.innerText(el)}; })); diff --git a/src/usecases/room-settings.js b/src/usecases/room-settings.js index 95078d1c87..bf3e60d490 100644 --- a/src/usecases/room-settings.js +++ b/src/usecases/room-settings.js @@ -37,11 +37,11 @@ module.exports = async function changeRoomSettings(session, settings) { const settingsButton = await session.query(".mx_RoomHeader .mx_AccessibleButton[title=Settings]"); await settingsButton.click(); //find tabs - const tabButtons = await session.waitAndQueryAll(".mx_RoomSettingsDialog .mx_TabbedView_tabLabel"); + const tabButtons = await session.queryAll(".mx_RoomSettingsDialog .mx_TabbedView_tabLabel"); const tabLabels = await Promise.all(tabButtons.map(t => session.innerText(t))); const securityTabButton = tabButtons[tabLabels.findIndex(l => l.toLowerCase().includes("security"))]; - const generalSwitches = await session.waitAndQueryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch"); + const generalSwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch"); const isDirectory = generalSwitches[0]; if (typeof settings.directory === "boolean") { @@ -51,9 +51,9 @@ module.exports = async function changeRoomSettings(session, settings) { if (settings.alias) { session.log.step(`sets alias to ${settings.alias}`); - const aliasField = await session.waitAndQuery(".mx_RoomSettingsDialog .mx_AliasSettings input[type=text]"); + const aliasField = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings input[type=text]"); await session.replaceInputText(aliasField, settings.alias); - const addButton = await session.waitAndQuery(".mx_RoomSettingsDialog .mx_AliasSettings .mx_AccessibleButton"); + const addButton = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings .mx_AccessibleButton"); await addButton.click(); session.log.done(); } @@ -74,7 +74,7 @@ module.exports = async function changeRoomSettings(session, settings) { if (settings.visibility) { session.log.step(`sets visibility to ${settings.visibility}`); - const radios = await session.waitAndQueryAll(".mx_RoomSettingsDialog input[type=radio]"); + const radios = await session.queryAll(".mx_RoomSettingsDialog input[type=radio]"); assert.equal(radios.length, 7); const inviteOnly = radios[0]; const publicNoGuests = radios[1]; diff --git a/src/usecases/send-message.js b/src/usecases/send-message.js index 038171327c..d3bd02cae3 100644 --- a/src/usecases/send-message.js +++ b/src/usecases/send-message.js @@ -20,7 +20,7 @@ module.exports = async function sendMessage(session, message) { session.log.step(`writes "${message}" in room`); // this selector needs to be the element that has contenteditable=true, // not any if its parents, otherwise it behaves flaky at best. - const composer = await session.waitAndQuery('.mx_MessageComposer_editor'); + const composer = await session.query('.mx_MessageComposer_editor'); // sometimes the focus that type() does internally doesn't seem to work // and calling click before seems to fix it 🤷 await composer.click(); @@ -29,6 +29,6 @@ module.exports = async function sendMessage(session, message) { assert.equal(text.trim(), message.trim()); await composer.press("Enter"); // wait for the message to appear sent - await session.waitAndQuery(".mx_EventTile_last:not(.mx_EventTile_sending)"); + await session.query(".mx_EventTile_last:not(.mx_EventTile_sending)"); session.log.done(); } diff --git a/src/usecases/settings.js b/src/usecases/settings.js index 68a290c29d..903524e6b8 100644 --- a/src/usecases/settings.js +++ b/src/usecases/settings.js @@ -19,10 +19,10 @@ const assert = require('assert'); async function openSettings(session, section) { const menuButton = await session.query(".mx_TopLeftMenuButton_name"); await menuButton.click(); - const settingsItem = await session.waitAndQuery(".mx_TopLeftMenu_icon_settings"); + const settingsItem = await session.query(".mx_TopLeftMenu_icon_settings"); await settingsItem.click(); if (section) { - const sectionButton = await session.waitAndQuery(`.mx_UserSettingsDialog .mx_TabbedView_tabLabels .mx_UserSettingsDialog_${section}Icon`); + const sectionButton = await session.query(`.mx_UserSettingsDialog .mx_TabbedView_tabLabels .mx_UserSettingsDialog_${section}Icon`); await sectionButton.click(); } } @@ -31,10 +31,10 @@ module.exports.enableLazyLoading = async function(session) { session.log.step(`enables lazy loading of members in the lab settings`); const settingsButton = await session.query('.mx_BottomLeftMenu_settings'); await settingsButton.click(); - const llCheckbox = await session.waitAndQuery("#feature_lazyloading"); + const llCheckbox = await session.query("#feature_lazyloading"); await llCheckbox.click(); await session.waitForReload(); - const closeButton = await session.waitAndQuery(".mx_RoomHeader_cancelButton"); + const closeButton = await session.query(".mx_RoomHeader_cancelButton"); await closeButton.click(); session.log.done(); } @@ -42,7 +42,7 @@ module.exports.enableLazyLoading = async function(session) { module.exports.getE2EDeviceFromSettings = async function(session) { session.log.step(`gets e2e device/key from settings`); await openSettings(session, "security"); - const deviceAndKey = await session.waitAndQueryAll(".mx_SettingsTab_section .mx_SecurityUserSettingsTab_deviceInfo code"); + const deviceAndKey = await session.queryAll(".mx_SettingsTab_section .mx_SecurityUserSettingsTab_deviceInfo code"); assert.equal(deviceAndKey.length, 2); const id = await (await deviceAndKey[0].getProperty("innerText")).jsonValue(); const key = await (await deviceAndKey[1].getProperty("innerText")).jsonValue(); diff --git a/src/usecases/signup.js b/src/usecases/signup.js index 0bfdb27a58..66bf773ca1 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -21,17 +21,17 @@ module.exports = async function signup(session, username, password, homeserver) await session.goto(session.url('/#/register')); // change the homeserver by clicking the "Change" link. if (homeserver) { - const changeServerDetailsLink = await session.waitAndQuery('.mx_AuthBody_editServerDetails'); + const changeServerDetailsLink = await session.query('.mx_AuthBody_editServerDetails'); await changeServerDetailsLink.click(); - const hsInputField = await session.waitAndQuery('#mx_ServerConfig_hsUrl'); + const hsInputField = await session.query('#mx_ServerConfig_hsUrl'); await session.replaceInputText(hsInputField, homeserver); - const nextButton = await session.waitAndQuery('.mx_Login_submit'); + const nextButton = await session.query('.mx_Login_submit'); await nextButton.click(); } //fill out form - const usernameField = await session.waitAndQuery("#mx_RegistrationForm_username"); - const passwordField = await session.waitAndQuery("#mx_RegistrationForm_password"); - const passwordRepeatField = await session.waitAndQuery("#mx_RegistrationForm_passwordConfirm"); + const usernameField = await session.query("#mx_RegistrationForm_username"); + const passwordField = await session.query("#mx_RegistrationForm_password"); + const passwordRepeatField = await session.query("#mx_RegistrationForm_passwordConfirm"); await session.replaceInputText(usernameField, username); await session.replaceInputText(passwordField, password); await session.replaceInputText(passwordRepeatField, password); @@ -41,7 +41,7 @@ module.exports = async function signup(session, username, password, homeserver) await session.delay(300); /// focus on the button to make sure error validation /// has happened before checking the form is good to go - const registerButton = await session.waitAndQuery('.mx_Login_submit'); + const registerButton = await session.query('.mx_Login_submit'); await registerButton.focus(); //check no errors const error_text = await session.tryGetInnertext('.mx_Login_error'); @@ -51,15 +51,15 @@ module.exports = async function signup(session, username, password, homeserver) await registerButton.click(); //confirm dialog saying you cant log back in without e-mail - const continueButton = await session.waitAndQuery('.mx_QuestionDialog button.mx_Dialog_primary'); + const continueButton = await session.query('.mx_QuestionDialog button.mx_Dialog_primary'); await continueButton.click(); //find the privacy policy checkbox and check it - const policyCheckbox = await session.waitAndQuery('.mx_InteractiveAuthEntryComponents_termsPolicy input'); + const policyCheckbox = await session.query('.mx_InteractiveAuthEntryComponents_termsPolicy input'); await policyCheckbox.click(); //now click the 'Accept' button to agree to the privacy policy - const acceptButton = await session.waitAndQuery('.mx_InteractiveAuthEntryComponents_termsSubmit'); + const acceptButton = await session.query('.mx_InteractiveAuthEntryComponents_termsSubmit'); await acceptButton.click(); //wait for registration to finish so the hash gets set diff --git a/src/usecases/verify.js b/src/usecases/verify.js index 233bf4f954..323765bebf 100644 --- a/src/usecases/verify.js +++ b/src/usecases/verify.js @@ -19,19 +19,19 @@ const {openMemberInfo} = require("./memberlist"); const {assertDialog, acceptDialog} = require("./dialog"); async function assertVerified(session) { - const dialogSubTitle = await session.innerText(await session.waitAndQuery(".mx_Dialog h2")); + const dialogSubTitle = await session.innerText(await session.query(".mx_Dialog h2")); assert(dialogSubTitle, "Verified!"); } async function startVerification(session, name) { await openMemberInfo(session, name); // click verify in member info - const firstVerifyButton = await session.waitAndQuery(".mx_MemberDeviceInfo_verify"); + const firstVerifyButton = await session.query(".mx_MemberDeviceInfo_verify"); await firstVerifyButton.click(); } async function getSasCodes(session) { - const sasLabelElements = await session.waitAndQueryAll(".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label"); + const sasLabelElements = await session.queryAll(".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label"); const sasLabels = await Promise.all(sasLabelElements.map(e => session.innerText(e))); return sasLabels; } @@ -53,7 +53,7 @@ module.exports.startSasVerifcation = async function(session, name) { module.exports.acceptSasVerification = async function(session, name) { await assertDialog(session, "Incoming Verification Request"); - const opponentLabelElement = await session.waitAndQuery(".mx_IncomingSasDialog_opponentProfile h2"); + const opponentLabelElement = await session.query(".mx_IncomingSasDialog_opponentProfile h2"); const opponentLabel = await session.innerText(opponentLabelElement); assert(opponentLabel, name); // click "Continue" button From eae830a4d45ad029105cfa92201b6ca6eb90edb3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 16 Apr 2019 15:39:33 +0200 Subject: [PATCH 0194/2372] update readme --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4f4f9217e3..81655000a0 100644 --- a/README.md +++ b/README.md @@ -24,14 +24,12 @@ node start.js ``` It's important to always stop and start synapse before each run of the tests to clear the in-memory sqlite database it uses, as the tests assume a blank slate. -start.js accepts the following parameters that can help running the tests locally: +start.js accepts these parameters (and more, see `node start.js --help`) that can help running the tests locally: - - `--no-logs` dont show the excessive logging show by default (meant for CI), just where the test failed. - `--riot-url ` don't use the riot copy and static server provided by the tests, but use a running server like the webpack watch server to run the tests against. Make sure to have the following local config: - - `welcomeUserId` disabled as the tests assume there is no riot-bot currently. Make sure to set the default homeserver to - - `"default_hs_url": "http://localhost:5005"`, to use the e2e tests synapse (the tests use the default HS to run against atm) - - `"feature_lazyloading": "labs"`, currently assumes lazy loading needs to be turned on in the settings, will change soon. - - `--slow-mo` run the tests a bit slower, so it's easier to follow along with `--windowed`. + - `welcomeUserId` disabled as the tests assume there is no riot-bot currently. + - `--slow-mo` type at a human speed, useful with `--windowed`. + - `--throttle-cpu ` throttle cpu in the browser by the given factor. Useful to reproduce failures because of insufficient timeouts happening on the slower CI server. - `--windowed` run the tests in an actual browser window Try to limit interacting with the windows while the tests are running. Hovering over the window tends to fail the tests, dragging the title bar should be fine though. - `--dev-tools` open the devtools in the browser window, only applies if `--windowed` is set as well. From ee1e585301c686cd40537f1afa0843cc6aadfc69 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 16 Apr 2019 15:41:45 +0200 Subject: [PATCH 0195/2372] remove explicit timeout for polling as well --- src/session.js | 3 ++- src/usecases/signup.js | 2 +- src/usecases/timeline.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/session.js b/src/session.js index 4fe882c97d..8e85509b82 100644 --- a/src/session.js +++ b/src/session.js @@ -205,7 +205,8 @@ module.exports = class RiotSession { return this.browser.close(); } - async poll(callback, timeout, interval = 100) { + async poll(callback, interval = 100) { + const timeout = DEFAULT_TIMEOUT; let waited = 0; while(waited < timeout) { await this.delay(interval); diff --git a/src/usecases/signup.js b/src/usecases/signup.js index 66bf773ca1..cc58bff28b 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -68,7 +68,7 @@ module.exports = async function signup(session, username, password, homeserver) const foundHomeUrl = await session.poll(async () => { const url = session.page.url(); return url === session.url('/#/home'); - }, 5000); + }); assert(foundHomeUrl); session.log.done(); } diff --git a/src/usecases/timeline.js b/src/usecases/timeline.js index 7b28328e49..580d88ee6a 100644 --- a/src/usecases/timeline.js +++ b/src/usecases/timeline.js @@ -63,7 +63,7 @@ module.exports.receiveMessage = async function(session, expectedMessage) { return lastMessage && lastMessage.body === expectedMessage.body && lastMessage.sender === expectedMessage.sender; - }, 5000, 200); + }); assertMessage(lastMessage, expectedMessage); session.log.done(); } From bf0296602af6033c60f48ad2841aba7ee33f4470 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 16 Apr 2019 15:03:12 +0000 Subject: [PATCH 0196/2372] Update TODO.md --- TODO.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/TODO.md b/TODO.md index 4cbcba801b..f5ccebcf77 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,8 @@ -join a peekable room by directory -join a peekable room by invite -join a non-peekable room by directory -join a non-peekable room by invite + + - join a peekable room by directory + - join a peekable room by invite + - join a non-peekable room by directory + - join a non-peekable room by invite + - leave a room and check we move to the "next" room + - get kicked from a room and check that we show the correct message + - get banned " From 20d80e695c5684490032657519961d1f63dff1f5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 17 Apr 2019 11:44:32 +0200 Subject: [PATCH 0197/2372] adjust selectors for join and accept button in room preview bar --- src/usecases/accept-invite.js | 2 +- src/usecases/join.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/usecases/accept-invite.js b/src/usecases/accept-invite.js index 3676f6641b..bcecf41ed2 100644 --- a/src/usecases/accept-invite.js +++ b/src/usecases/accept-invite.js @@ -31,7 +31,7 @@ module.exports = async function acceptInvite(session, name) { await inviteHandle.click(); - const acceptInvitationLink = await session.query(".mx_RoomPreviewBar_join_text a:first-child"); + const acceptInvitationLink = await session.query(".mx_RoomPreviewBar_Invite .mx_AccessibleButton_kind_primary"); await acceptInvitationLink.click(); session.log.done(); diff --git a/src/usecases/join.js b/src/usecases/join.js index cf23fbfb64..9bc7007849 100644 --- a/src/usecases/join.js +++ b/src/usecases/join.js @@ -26,7 +26,7 @@ module.exports = async function join(session, roomName) { const firstRoomLabel = await session.query('.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child'); await firstRoomLabel.click(); - const joinLink = await session.query('.mx_RoomPreviewBar_join_text a'); + const joinLink = await session.query('.mx_RoomPreviewBar_ViewingRoom .mx_AccessibleButton_kind_primary'); await joinLink.click(); await session.query('.mx_MessageComposer'); From 2527995e2e1fa51a9752974dc08fe4611fbec260 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 25 Apr 2019 14:23:10 +0100 Subject: [PATCH 0198/2372] Only install the minimum deps to pass --- synapse/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/install.sh b/synapse/install.sh index c289b0f0ba..0b1d581118 100755 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -31,7 +31,7 @@ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py python get-pip.py pip install --upgrade setuptools -pip install matrix-synapse[all] +pip install matrix-synapse[resources.consent] python -m synapse.app.homeserver \ --server-name localhost \ --config-path homeserver.yaml \ From 1ffe0d1a24b52fd1e09cda145239fadb5a513d18 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 25 Apr 2019 14:23:51 +0100 Subject: [PATCH 0199/2372] Report location of Synpase log --- synapse/start.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/synapse/start.sh b/synapse/start.sh index 379de3850c..f8da097f7d 100755 --- a/synapse/start.sh +++ b/synapse/start.sh @@ -6,6 +6,7 @@ cd $BASE_DIR cd installations/consent source env/bin/activate LOGFILE=$(mktemp) +echo "Synapse log file at $LOGFILE" ./synctl start 2> $LOGFILE EXIT_CODE=$? if [ $EXIT_CODE -ne 0 ]; then From f82f9ecdb20b4730c71c61414abba73e9c88c627 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 25 Apr 2019 14:28:39 +0100 Subject: [PATCH 0200/2372] Use a stronger password --- src/scenario.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scenario.js b/src/scenario.js index cd818fd7bc..1ad177a4f5 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -30,7 +30,7 @@ module.exports = async function scenario(createSession, restCreator) { console.log(`running tests on ${await session.browser.version()} ...`); firstUser = false; } - await signup(session, session.username, 'testtest', session.hsUrl); + await signup(session, session.username, 'testsarefun!!!', session.hsUrl); return session; } From 3e6719e8abe97fec86520684f16d321b10727e49 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 25 Apr 2019 18:10:41 +0100 Subject: [PATCH 0201/2372] Wait for password validation --- src/usecases/signup.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/usecases/signup.js b/src/usecases/signup.js index cc58bff28b..a52baea961 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -43,6 +43,8 @@ module.exports = async function signup(session, username, password, homeserver) /// has happened before checking the form is good to go const registerButton = await session.query('.mx_Login_submit'); await registerButton.focus(); + // Password validation is async, wait for it to complete before submit + await session.query(".mx_Field_valid #mx_RegistrationForm_password"); //check no errors const error_text = await session.tryGetInnertext('.mx_Login_error'); assert.strictEqual(!!error_text, false); From 118e752a1fd8f3b900709db205976feb84b95304 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 12 May 2019 23:24:12 +0100 Subject: [PATCH 0202/2372] Add button to clear all notification counts, sometimes stuck in historical Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/settings/Notifications.js | 22 +++++++++++++++++++ src/i18n/strings/en_EN.json | 1 + 2 files changed, 23 insertions(+) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 9b5688aa6a..9e01d44fb6 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -29,6 +29,7 @@ import { } from '../../../notifications'; import SdkConfig from "../../../SdkConfig"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; +import AccessibleButton from "../elements/AccessibleButton"; // TODO: this "view" component still has far too much application logic in it, // which should be factored out to other files. @@ -654,6 +655,17 @@ module.exports = React.createClass({ MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids})); }, + _onClearNotifications: function() { + const cli = MatrixClientPeg.get(); + + cli.getRooms().forEach(r => { + if (r.getUnreadNotificationCount() > 0) { + const events = r.getLiveTimeline().getEvents(); + if (events.length) cli.sendReadReceipt(events.pop()); + } + }); + }, + _updatePushRuleActions: function(rule, actions, enabled) { const cli = MatrixClientPeg.get(); @@ -746,6 +758,13 @@ module.exports = React.createClass({ label={_t('Enable notifications for this account')}/>; } + let clearNotificationsButton; + if (MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)) { + clearNotificationsButton = + {_t("Clear notifications")} + ; + } + // When enabled, the master rule inhibits all existing rules // So do not show all notification settings if (this.state.masterPushRule && this.state.masterPushRule.enabled) { @@ -756,6 +775,8 @@ module.exports = React.createClass({
{ _t('All notifications are currently disabled for all targets.') }
+ + {clearNotificationsButton} ); } @@ -877,6 +898,7 @@ module.exports = React.createClass({ { devicesSection } + { clearNotificationsButton } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8534091176..c6a12d8d56 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -501,6 +501,7 @@ "Notify for all other messages/rooms": "Notify for all other messages/rooms", "Notify me for anything else": "Notify me for anything else", "Enable notifications for this account": "Enable notifications for this account", + "Clear notifications": "Clear notifications", "All notifications are currently disabled for all targets.": "All notifications are currently disabled for all targets.", "Add an email address to configure email notifications": "Add an email address to configure email notifications", "Enable email notifications": "Enable email notifications", From 7a15acf224c193af725ab8c1901f7eea110540dc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 24 Jun 2019 12:51:05 +0200 Subject: [PATCH 0203/2372] install synapse/develop (and deps) from pip instead of getting dependencies from synapse/master and running synapse/develop from git download --- synapse/install.sh | 10 +++++----- synapse/start.sh | 4 ++-- synapse/stop.sh | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) mode change 100755 => 100644 synapse/install.sh diff --git a/synapse/install.sh b/synapse/install.sh old mode 100755 new mode 100644 index 0b1d581118..8a9bb82e19 --- a/synapse/install.sh +++ b/synapse/install.sh @@ -16,11 +16,7 @@ if [ -d $BASE_DIR/$SERVER_DIR ]; then fi cd $BASE_DIR - -mkdir -p installations/ -curl https://codeload.github.com/matrix-org/synapse/zip/$SYNAPSE_BRANCH --output synapse.zip -unzip -q synapse.zip -mv synapse-$SYNAPSE_BRANCH $SERVER_DIR +mkdir -p $SERVER_DIR cd $SERVER_DIR virtualenv -p python3 env source env/bin/activate @@ -37,7 +33,9 @@ python -m synapse.app.homeserver \ --config-path homeserver.yaml \ --generate-config \ --report-stats=no +pip install https://codeload.github.com/matrix-org/synapse/zip/$SYNAPSE_BRANCH # apply configuration +pushd env/bin/ cp -r $BASE_DIR/config-templates/$CONFIG_TEMPLATE/. ./ # Hashes used instead of slashes because we'll get a value back from $(pwd) that'll be @@ -49,3 +47,5 @@ sed -i.bak "s#{{FORM_SECRET}}#$(uuidgen)#g" homeserver.yaml sed -i.bak "s#{{REGISTRATION_SHARED_SECRET}}#$(uuidgen)#g" homeserver.yaml sed -i.bak "s#{{MACAROON_SECRET_KEY}}#$(uuidgen)#g" homeserver.yaml rm *.bak + +popd diff --git a/synapse/start.sh b/synapse/start.sh index f8da097f7d..2ff6ae69d0 100755 --- a/synapse/start.sh +++ b/synapse/start.sh @@ -3,8 +3,8 @@ set -e BASE_DIR=$(cd $(dirname $0) && pwd) cd $BASE_DIR -cd installations/consent -source env/bin/activate +cd installations/consent/env/bin/ +source activate LOGFILE=$(mktemp) echo "Synapse log file at $LOGFILE" ./synctl start 2> $LOGFILE diff --git a/synapse/stop.sh b/synapse/stop.sh index a7716744ef..08258e5a8c 100755 --- a/synapse/stop.sh +++ b/synapse/stop.sh @@ -3,6 +3,6 @@ set -e BASE_DIR=$(cd $(dirname $0) && pwd) cd $BASE_DIR -cd installations/consent -source env/bin/activate +cd installations/consent/env/bin/ +source activate ./synctl stop From 894a07484c6738ca7f1ad404f173f92e6ed1006a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 24 Jun 2019 12:51:51 +0200 Subject: [PATCH 0204/2372] generate signing keys without generating config file and then overwr. it --- synapse/install.sh | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) mode change 100644 => 100755 synapse/install.sh diff --git a/synapse/install.sh b/synapse/install.sh old mode 100644 new mode 100755 index 8a9bb82e19..077258072c --- a/synapse/install.sh +++ b/synapse/install.sh @@ -27,12 +27,6 @@ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py python get-pip.py pip install --upgrade setuptools -pip install matrix-synapse[resources.consent] -python -m synapse.app.homeserver \ - --server-name localhost \ - --config-path homeserver.yaml \ - --generate-config \ - --report-stats=no pip install https://codeload.github.com/matrix-org/synapse/zip/$SYNAPSE_BRANCH # apply configuration pushd env/bin/ @@ -49,3 +43,5 @@ sed -i.bak "s#{{MACAROON_SECRET_KEY}}#$(uuidgen)#g" homeserver.yaml rm *.bak popd +# generate signing keys for signing_key_path +python -m synapse.app.homeserver --generate-keys --config-dir env/bin/ -c env/bin/homeserver.yaml From d9def18184a0e6c5cfd1058abbde71122d2b6567 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 24 Jun 2019 15:00:51 +0200 Subject: [PATCH 0205/2372] adjust path to register script for pip installation --- src/rest/creator.js | 6 +++--- start.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rest/creator.js b/src/rest/creator.js index 2c15bae792..5897ae2683 100644 --- a/src/rest/creator.js +++ b/src/rest/creator.js @@ -59,12 +59,12 @@ module.exports = class RestSessionCreator { '--no-admin', this.hsUrl ]; - const registerCmd = `./scripts/register_new_matrix_user ${registerArgs.join(' ')}`; + const registerCmd = `./register_new_matrix_user ${registerArgs.join(' ')}`; const allCmds = [ `cd ${this.synapseSubdir}`, - ". env/bin/activate", + ". activate", registerCmd - ].join('&&'); + ].join(' && '); await execAsync(allCmds, {cwd: this.cwd, encoding: 'utf-8'}); } diff --git a/start.js b/start.js index b98dc7f366..d19b232236 100644 --- a/start.js +++ b/start.js @@ -52,7 +52,7 @@ async function runTests() { } const restCreator = new RestSessionCreator( - 'synapse/installations/consent', + 'synapse/installations/consent/env/bin', hsUrl, __dirname ); From a72934556c60c48734d815d629a839bf2183ecba Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 24 Jun 2019 15:30:18 +0200 Subject: [PATCH 0206/2372] look for activate in cwd --- src/rest/creator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rest/creator.js b/src/rest/creator.js index 5897ae2683..31c352b31a 100644 --- a/src/rest/creator.js +++ b/src/rest/creator.js @@ -62,7 +62,7 @@ module.exports = class RestSessionCreator { const registerCmd = `./register_new_matrix_user ${registerArgs.join(' ')}`; const allCmds = [ `cd ${this.synapseSubdir}`, - ". activate", + ". ./activate", registerCmd ].join(' && '); From 8a247e0ed748601502f9f951351e0f32a101b4bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Aug 2019 15:51:23 +0000 Subject: [PATCH 0207/2372] Bump lodash from 4.17.11 to 4.17.15 Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.15. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.15) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index bdf5608a7e..4379b24946 100644 --- a/yarn.lock +++ b/yarn.lock @@ -425,9 +425,9 @@ jsprim@^1.2.2: verror "1.10.0" lodash@^4.15.0, lodash@^4.17.11: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== mime-db@~1.38.0: version "1.38.0" From 7635e93896c84459180afb9b7c899e1f81dd0f6d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 28 Aug 2019 11:41:29 +0100 Subject: [PATCH 0208/2372] Adjust tests for hidden IS field --- src/usecases/signup.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/usecases/signup.js b/src/usecases/signup.js index a52baea961..e43b47b5cd 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -27,6 +27,8 @@ module.exports = async function signup(session, username, password, homeserver) await session.replaceInputText(hsInputField, homeserver); const nextButton = await session.query('.mx_Login_submit'); await nextButton.click(); + await session.query('.mx_ServerConfig_identityServer_shown'); + await nextButton.click(); } //fill out form const usernameField = await session.query("#mx_RegistrationForm_username"); From 67b03b5a0f6ae65ead9999f7174e1df56a7862eb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 16:25:59 +0200 Subject: [PATCH 0209/2372] Fix signup: set custom hs through advanced section, and accept IS step --- src/usecases/signup.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/usecases/signup.js b/src/usecases/signup.js index a52baea961..b6a6d46311 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -21,11 +21,15 @@ module.exports = async function signup(session, username, password, homeserver) await session.goto(session.url('/#/register')); // change the homeserver by clicking the "Change" link. if (homeserver) { - const changeServerDetailsLink = await session.query('.mx_AuthBody_editServerDetails'); - await changeServerDetailsLink.click(); + const advancedButton = await session.query('.mx_ServerTypeSelector_type_Advanced'); + await advancedButton.click(); const hsInputField = await session.query('#mx_ServerConfig_hsUrl'); await session.replaceInputText(hsInputField, homeserver); const nextButton = await session.query('.mx_Login_submit'); + // accept homeserver + await nextButton.click(); + await session.delay(200); + // accept discovered identity server await nextButton.click(); } //fill out form From f66f5bedb676c5191c732d7b086937c7b3349eda Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 16:27:02 +0200 Subject: [PATCH 0210/2372] Adjust how room directory and create room dialog should be opened --- src/usecases/create-room.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/usecases/create-room.js b/src/usecases/create-room.js index 4578dbaf0a..132a4bd782 100644 --- a/src/usecases/create-room.js +++ b/src/usecases/create-room.js @@ -17,6 +17,13 @@ limitations under the License. const assert = require('assert'); async function openRoomDirectory(session) { + const roomDirectoryButton = await session.query('.mx_LeftPanel_explore .mx_AccessibleButton'); + await roomDirectoryButton.click(); +} + +async function createRoom(session, roomName) { + session.log.step(`creates room "${roomName}"`); + const roomListHeaders = await session.queryAll('.mx_RoomSubList_labelContainer'); const roomListHeaderLabels = await Promise.all(roomListHeaders.map(h => session.innerText(h))); const roomsIndex = roomListHeaderLabels.findIndex(l => l.toLowerCase().includes("rooms")); @@ -26,13 +33,7 @@ async function openRoomDirectory(session) { const roomsHeader = roomListHeaders[roomsIndex]; const addRoomButton = await roomsHeader.$(".mx_RoomSubList_addRoom"); await addRoomButton.click(); -} -async function createRoom(session, roomName) { - session.log.step(`creates room "${roomName}"`); - await openRoomDirectory(session); - const createRoomButton = await session.query('.mx_RoomDirectory_createRoom'); - await createRoomButton.click(); const roomNameInput = await session.query('.mx_CreateRoomDialog_input'); await session.replaceInputText(roomNameInput, roomName); From 16591719e33d7e8addc8310943bca769b1ff6777 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Sep 2019 15:40:17 +0000 Subject: [PATCH 0211/2372] Revert "Fix signup: set custom hs through advanced section, and accept IS step" --- src/usecases/signup.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/usecases/signup.js b/src/usecases/signup.js index a4f50b3e32..e43b47b5cd 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -21,15 +21,11 @@ module.exports = async function signup(session, username, password, homeserver) await session.goto(session.url('/#/register')); // change the homeserver by clicking the "Change" link. if (homeserver) { - const advancedButton = await session.query('.mx_ServerTypeSelector_type_Advanced'); - await advancedButton.click(); + const changeServerDetailsLink = await session.query('.mx_AuthBody_editServerDetails'); + await changeServerDetailsLink.click(); const hsInputField = await session.query('#mx_ServerConfig_hsUrl'); await session.replaceInputText(hsInputField, homeserver); const nextButton = await session.query('.mx_Login_submit'); - // accept homeserver - await nextButton.click(); - await session.delay(200); - // accept discovered identity server await nextButton.click(); await session.query('.mx_ServerConfig_identityServer_shown'); await nextButton.click(); From f2a3a136781d4221ea63e2eb213656337e6c59ad Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 11 Sep 2019 16:51:54 +0200 Subject: [PATCH 0212/2372] find new join button in room directory --- src/usecases/join.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/usecases/join.js b/src/usecases/join.js index 9bc7007849..3c14a76143 100644 --- a/src/usecases/join.js +++ b/src/usecases/join.js @@ -23,12 +23,8 @@ module.exports = async function join(session, roomName) { const roomInput = await session.query('.mx_DirectorySearchBox input'); await session.replaceInputText(roomInput, roomName); - const firstRoomLabel = await session.query('.mx_RoomDirectory_table .mx_RoomDirectory_name:first-child'); - await firstRoomLabel.click(); - - const joinLink = await session.query('.mx_RoomPreviewBar_ViewingRoom .mx_AccessibleButton_kind_primary'); - await joinLink.click(); - + const joinFirstLink = await session.query('.mx_RoomDirectory_table .mx_RoomDirectory_join .mx_AccessibleButton'); + await joinFirstLink.click(); await session.query('.mx_MessageComposer'); session.log.done(); } From a25056f21ee38a38a2f7eac6c27b3447bd072fb9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 13 Sep 2019 16:26:50 +0200 Subject: [PATCH 0213/2372] retry getting the scroll panel when retrying to get the scrolltop --- src/usecases/timeline.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/usecases/timeline.js b/src/usecases/timeline.js index 580d88ee6a..1770e0df9f 100644 --- a/src/usecases/timeline.js +++ b/src/usecases/timeline.js @@ -20,14 +20,14 @@ module.exports.scrollToTimelineTop = async function(session) { session.log.step(`scrolls to the top of the timeline`); await session.page.evaluate(() => { return Promise.resolve().then(async () => { - const timelineScrollView = document.querySelector(".mx_RoomView_timeline .mx_ScrollPanel"); let timedOut = false; let timeoutHandle = null; // set scrollTop to 0 in a loop and check every 50ms // if content became available (scrollTop not being 0 anymore), // assume everything is loaded after 3s do { - if (timelineScrollView.scrollTop !== 0) { + const timelineScrollView = document.querySelector(".mx_RoomView_timeline .mx_ScrollPanel"); + if (timelineScrollView && timelineScrollView.scrollTop !== 0) { if (timeoutHandle) { clearTimeout(timeoutHandle); } From 87be5fb9386dc00846ac20854e1b216688fea362 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 13 Sep 2019 16:31:00 +0200 Subject: [PATCH 0214/2372] try to fix selecting all text in Field components --- src/session.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/session.js b/src/session.js index 8e85509b82..f91bce5a46 100644 --- a/src/session.js +++ b/src/session.js @@ -108,6 +108,9 @@ module.exports = class RiotSession { async replaceInputText(input, text) { // click 3 times to select all text await input.click({clickCount: 3}); + // waiting here solves not having selected all the text by the 3x click above, + // presumably because of the Field label animation. + await this.delay(300); // then remove it with backspace await input.press('Backspace'); // and type the new text From c36673b992c61f5ec7848c79bbe9b11da50c713a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 13 Sep 2019 14:40:25 +0000 Subject: [PATCH 0215/2372] Revert "Revert "Fix signup: set custom hs through advanced section, and accept IS step"" --- src/usecases/signup.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/usecases/signup.js b/src/usecases/signup.js index e43b47b5cd..a4f50b3e32 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -21,11 +21,15 @@ module.exports = async function signup(session, username, password, homeserver) await session.goto(session.url('/#/register')); // change the homeserver by clicking the "Change" link. if (homeserver) { - const changeServerDetailsLink = await session.query('.mx_AuthBody_editServerDetails'); - await changeServerDetailsLink.click(); + const advancedButton = await session.query('.mx_ServerTypeSelector_type_Advanced'); + await advancedButton.click(); const hsInputField = await session.query('#mx_ServerConfig_hsUrl'); await session.replaceInputText(hsInputField, homeserver); const nextButton = await session.query('.mx_Login_submit'); + // accept homeserver + await nextButton.click(); + await session.delay(200); + // accept discovered identity server await nextButton.click(); await session.query('.mx_ServerConfig_identityServer_shown'); await nextButton.click(); From 1ea5047607ea6b78b93f8939077e49d2ec81dfe9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 16 Sep 2019 14:02:22 +0200 Subject: [PATCH 0216/2372] only need 2 clicks, not 3 --- src/usecases/signup.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/usecases/signup.js b/src/usecases/signup.js index a4f50b3e32..64cf52668c 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -28,10 +28,8 @@ module.exports = async function signup(session, username, password, homeserver) const nextButton = await session.query('.mx_Login_submit'); // accept homeserver await nextButton.click(); - await session.delay(200); - // accept discovered identity server - await nextButton.click(); await session.query('.mx_ServerConfig_identityServer_shown'); + // accept default identity server await nextButton.click(); } //fill out form From 06af5b3f224cf068ce9e12eb3edb6e5e943a4d6b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 17 Sep 2019 11:40:24 +0200 Subject: [PATCH 0217/2372] look for a change (HS) link in the registration but don't fail if its not there --- src/session.js | 3 +-- src/usecases/signup.js | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/session.js b/src/session.js index f91bce5a46..d3c26c07e4 100644 --- a/src/session.js +++ b/src/session.js @@ -117,8 +117,7 @@ module.exports = class RiotSession { await input.type(text); } - query(selector) { - const timeout = DEFAULT_TIMEOUT; + query(selector, timeout = DEFAULT_TIMEOUT) { return this.page.waitForSelector(selector, {visible: true, timeout}); } diff --git a/src/usecases/signup.js b/src/usecases/signup.js index 64cf52668c..b6fad58260 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -19,10 +19,23 @@ const assert = require('assert'); module.exports = async function signup(session, username, password, homeserver) { session.log.step("signs up"); await session.goto(session.url('/#/register')); - // change the homeserver by clicking the "Change" link. + // change the homeserver by clicking the advanced section if (homeserver) { const advancedButton = await session.query('.mx_ServerTypeSelector_type_Advanced'); await advancedButton.click(); + + // depending on what HS is configured as the default, the advanced registration + // goes the HS/IS entry directly (for matrix.org) or takes you to the user/pass entry (not matrix.org). + // To work with both, we look for the "Change" link in the user/pass entry but don't fail when we can't find it + // As this link should be visible immediately, and to not slow down the case where it isn't present, + // pick a lower timeout of 5000ms + try { + const changeHsField = await session.query('.mx_AuthBody_editServerDetails', 5000); + if (changeHsField) { + await changeHsField.click(); + } + } catch (err) {} + const hsInputField = await session.query('#mx_ServerConfig_hsUrl'); await session.replaceInputText(hsInputField, homeserver); const nextButton = await session.query('.mx_Login_submit'); From 03d928bf5da8dccd06946eace55ee26a669f891b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Sep 2019 17:22:43 +0200 Subject: [PATCH 0218/2372] adjust create room dialog name field selector --- src/usecases/create-room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usecases/create-room.js b/src/usecases/create-room.js index 132a4bd782..88547610f0 100644 --- a/src/usecases/create-room.js +++ b/src/usecases/create-room.js @@ -35,7 +35,7 @@ async function createRoom(session, roomName) { await addRoomButton.click(); - const roomNameInput = await session.query('.mx_CreateRoomDialog_input'); + const roomNameInput = await session.query('.mx_CreateRoomDialog_name input'); await session.replaceInputText(roomNameInput, roomName); const createButton = await session.query('.mx_Dialog_primary'); From fe1b786c507cb5fa8c05693d352bc4dff9d6cab2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 23 Sep 2019 22:33:30 +0100 Subject: [PATCH 0219/2372] unbreak tests; we no longer prompt for IS at register --- src/usecases/signup.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/usecases/signup.js b/src/usecases/signup.js index b6fad58260..c3302a8a7f 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -41,7 +41,6 @@ module.exports = async function signup(session, username, password, homeserver) const nextButton = await session.query('.mx_Login_submit'); // accept homeserver await nextButton.click(); - await session.query('.mx_ServerConfig_identityServer_shown'); // accept default identity server await nextButton.click(); } From f5b605beaa8d97ae4a9049bf9ee2ac7997270c3f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 23 Sep 2019 22:43:48 +0100 Subject: [PATCH 0220/2372] unbreak tests (take 2); we no longer prompt for IS at register --- src/usecases/signup.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/usecases/signup.js b/src/usecases/signup.js index c3302a8a7f..014d2ff786 100644 --- a/src/usecases/signup.js +++ b/src/usecases/signup.js @@ -41,8 +41,6 @@ module.exports = async function signup(session, username, password, homeserver) const nextButton = await session.query('.mx_Login_submit'); // accept homeserver await nextButton.click(); - // accept default identity server - await nextButton.click(); } //fill out form const usernameField = await session.query("#mx_RegistrationForm_username"); From 5b71cf5d8dd6954106d90a65fd2ab5d5b8a02f2c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Oct 2019 11:33:45 +0200 Subject: [PATCH 0221/2372] use correct css class for cider composer --- src/usecases/send-message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/usecases/send-message.js b/src/usecases/send-message.js index d3bd02cae3..38fe6fceb3 100644 --- a/src/usecases/send-message.js +++ b/src/usecases/send-message.js @@ -20,7 +20,7 @@ module.exports = async function sendMessage(session, message) { session.log.step(`writes "${message}" in room`); // this selector needs to be the element that has contenteditable=true, // not any if its parents, otherwise it behaves flaky at best. - const composer = await session.query('.mx_MessageComposer_editor'); + const composer = await session.query('.mx_SendMessageComposer'); // sometimes the focus that type() does internally doesn't seem to work // and calling click before seems to fix it 🤷 await composer.click(); From df02eb8e92ff565eb7a11702a16957baba59f1d1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Oct 2019 16:52:50 +0100 Subject: [PATCH 0222/2372] Add UserInfo panel (consolidation of MemberInfo & GroupMemberInfo) Labs Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .eslintrc.js | 4 + package.json | 2 + res/css/_components.scss | 1 + res/css/views/right_panel/_UserInfo.scss | 175 +++ src/components/structures/RightPanel.js | 44 +- .../views/dialogs/AddressPickerDialog.js | 1 + src/components/views/right_panel/UserInfo.js | 1201 +++++++++++++++++ src/i18n/strings/en_EN.json | 6 + src/settings/Settings.js | 6 + yarn.lock | 10 + 10 files changed, 1445 insertions(+), 5 deletions(-) create mode 100644 res/css/views/right_panel/_UserInfo.scss create mode 100644 src/components/views/right_panel/UserInfo.js diff --git a/.eslintrc.js b/.eslintrc.js index fdf0bb351e..81c3752301 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,6 +12,7 @@ module.exports = { extends: [matrixJsSdkPath + "/.eslintrc.js"], plugins: [ "react", + "react-hooks", "flowtype", "babel" ], @@ -104,6 +105,9 @@ module.exports = { // crashes currently: https://github.com/eslint/eslint/issues/6274 "generator-star-spacing": "off", + + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", }, settings: { flowtype: { diff --git a/package.json b/package.json index d2955f89be..e24901070f 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "test-multi": "karma start" }, "dependencies": { + "@use-it/event-listener": "^0.1.3", "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-runtime": "^6.26.0", "bluebird": "^3.5.0", @@ -134,6 +135,7 @@ "eslint-plugin-babel": "^5.2.1", "eslint-plugin-flowtype": "^2.30.0", "eslint-plugin-react": "^7.7.0", + "eslint-plugin-react-hooks": "^2.0.1", "estree-walker": "^0.5.0", "expect": "^24.1.0", "file-loader": "^3.0.1", diff --git a/res/css/_components.scss b/res/css/_components.scss index f627fe3a29..70d4e8e6ae 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -132,6 +132,7 @@ @import "./views/messages/_TextualEvent.scss"; @import "./views/messages/_UnknownBody.scss"; @import "./views/messages/_ViewSourceEvent.scss"; +@import "./views/right_panel/_UserInfo.scss"; @import "./views/room_settings/_AliasSettings.scss"; @import "./views/room_settings/_ColorSettings.scss"; @import "./views/rooms/_AppsDrawer.scss"; diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss new file mode 100644 index 0000000000..df536a7388 --- /dev/null +++ b/res/css/views/right_panel/_UserInfo.scss @@ -0,0 +1,175 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +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. +*/ + +.mx_UserInfo { + display: flex; + flex-direction: column; + flex: 1; + overflow-y: auto; +} + +.mx_UserInfo_profile .mx_E2EIcon { + display: inline; + margin: auto; + padding-right: 25px; + mask-size: contain; +} + +.mx_UserInfo_cancel { + height: 16px; + width: 16px; + padding: 10px 0 10px 10px; + cursor: pointer; + mask-image: url('$(res)/img/minimise.svg'); + mask-repeat: no-repeat; + mask-position: 16px center; + background-color: $rightpanel-button-color; +} + +.mx_UserInfo_profile h2 { + flex: 1; + overflow-x: auto; + max-height: 50px; +} + +.mx_UserInfo h2 { + font-size: 16px; + font-weight: 600; + margin: 16px 0 8px 0; +} + +.mx_UserInfo_container { + padding: 0 16px 16px 16px; + border-bottom: 1px solid lightgray; +} + +.mx_UserInfo_memberDetailsContainer { + padding-bottom: 0; +} + +.mx_UserInfo .mx_RoomTile_nameContainer { + width: 154px; +} + +.mx_UserInfo .mx_RoomTile_badge { + display: none; +} + +.mx_UserInfo .mx_RoomTile_name { + width: 160px; +} + +.mx_UserInfo_avatar { + background: $tagpanel-bg-color; +} + +.mx_UserInfo_avatar > img { + height: auto; + width: 100%; + max-height: 30vh; + object-fit: contain; + display: block; +} + +.mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image { + cursor: zoom-in; +} + +.mx_UserInfo h3 { + text-transform: uppercase; + color: $input-darker-fg-color; + font-weight: bold; + font-size: 12px; + margin: 4px 0; +} + +.mx_UserInfo_profileField { + font-size: 15px; + position: relative; + text-align: center; +} + +.mx_UserInfo_memberDetails { + text-align: center; +} + +.mx_UserInfo_field { + cursor: pointer; + font-size: 15px; + color: $primary-fg-color; + margin-left: 8px; + line-height: 23px; +} + +.mx_UserInfo_createRoom { + cursor: pointer; + display: flex; + align-items: center; + padding: 0 8px; +} + +.mx_UserInfo_createRoom_label { + width: initial !important; + cursor: pointer; +} + +.mx_UserInfo_statusMessage { + font-size: 11px; + opacity: 0.5; + overflow: hidden; + white-space: nowrap; + text-overflow: clip; +} +.mx_UserInfo .mx_UserInfo_scrollContainer { + flex: 1; + padding-bottom: 16px; +} + +.mx_UserInfo .mx_UserInfo_scrollContainer .mx_UserInfo_container { + padding-top: 16px; + padding-bottom: 0; + border-bottom: none; +} + +.mx_UserInfo_container_header { + display: flex; +} + +.mx_UserInfo_container_header_right { + position: relative; + margin-left: auto; +} + +.mx_UserInfo_newDmButton { + background-color: $roomheader-addroom-bg-color; + border-radius: 10px; // 16/2 + 2 padding + height: 16px; + flex: 0 0 16px; + + &::before { + background-color: $roomheader-addroom-fg-color; + mask: url('$(res)/img/icons-room-add.svg'); + mask-repeat: no-repeat; + mask-position: center; + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } +} diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 31e4788a0b..48d272f6c9 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -27,6 +27,7 @@ import { MatrixClient } from 'matrix-js-sdk'; import RateLimitedFunc from '../../ratelimitedfunc'; import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker'; import GroupStore from '../../stores/GroupStore'; +import SettingsStore from "../../settings/SettingsStore"; export default class RightPanel extends React.Component { static get propTypes() { @@ -165,6 +166,7 @@ export default class RightPanel extends React.Component { render() { const MemberList = sdk.getComponent('rooms.MemberList'); const MemberInfo = sdk.getComponent('rooms.MemberInfo'); + const UserInfo = sdk.getComponent('right_panel.UserInfo'); const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo'); const NotificationPanel = sdk.getComponent('structures.NotificationPanel'); const FilePanel = sdk.getComponent('structures.FilePanel'); @@ -183,14 +185,46 @@ export default class RightPanel extends React.Component { } else if (this.state.phase === RightPanel.Phase.GroupRoomList) { panel = ; } else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) { - panel = ; + if (SettingsStore.isFeatureEnabled("feature_user_info_panel")) { + const onClose = () => { + dis.dispatch({ + action: "view_user", + member: null, + }); + }; + panel = ; + } else { + panel = ; + } } else if (this.state.phase === RightPanel.Phase.Room3pidMemberInfo) { panel = ; } else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) { - panel = ; + if (SettingsStore.isFeatureEnabled("feature_user_info_panel")) { + const onClose = () => { + dis.dispatch({ + action: "view_user", + member: null, + }); + }; + panel = ; + } else { + panel = ( + + ); + } } else if (this.state.phase === RightPanel.Phase.GroupRoomInfo) { panel = +Copyright 2019 The Matrix.org Foundation C.I.C. + +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, {useCallback, useMemo, useState, useEffect} from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import useEventListener from '@use-it/event-listener'; +import {Group, MatrixClient, RoomMember, User} from 'matrix-js-sdk'; +import dis from '../../../dispatcher'; +import Modal from '../../../Modal'; +import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; +import createRoom from '../../../createRoom'; +import DMRoomMap from '../../../utils/DMRoomMap'; +import Unread from '../../../Unread'; +import AccessibleButton from '../elements/AccessibleButton'; +import SdkConfig from '../../../SdkConfig'; +import SettingsStore from "../../../settings/SettingsStore"; +import {EventTimeline} from "matrix-js-sdk"; +import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; +import * as RoomViewStore from "../../../stores/RoomViewStore"; +import MultiInviter from "../../../utils/MultiInviter"; +import GroupStore from "../../../stores/GroupStore"; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import E2EIcon from "../rooms/E2EIcon"; + +const _disambiguateDevices = (devices) => { + const names = Object.create(null); + for (let i = 0; i < devices.length; i++) { + const name = devices[i].getDisplayName(); + const indexList = names[name] || []; + indexList.push(i); + names[name] = indexList; + } + for (const name in names) { + if (names[name].length > 1) { + names[name].forEach((j)=>{ + devices[j].ambiguous = true; + }); + } + } +}; + +const withLegacyMatrixClient = (Component) => class extends React.PureComponent { + static contextTypes = { + matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, + }; + + render() { + return ; + } +}; + +const _getE2EStatus = (devices) => { + const hasUnverifiedDevice = devices.some((device) => device.isUnverified()); + return hasUnverifiedDevice ? "warning" : "verified"; +}; + +const DevicesSection = withLegacyMatrixClient(({devices, userId, loading}) => { + const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo'); + const Spinner = sdk.getComponent("elements.Spinner"); + + if (loading) { + // still loading + return ; + } + if (devices === null) { + return _t("Unable to load device list"); + } + if (devices.length === 0) { + return _t("No devices with registered encryption keys"); + } + + return ( +
+

{ _t("Trust & Devices") }

+
+ { devices.map((device, i) => ) } +
+
+ ); +}); + +const onRoomTileClick = (roomId) => { + dis.dispatch({ + action: 'view_room', + room_id: roomId, + }); +}; + +const DirectChatsSection = withLegacyMatrixClient(({cli, userId, startUpdating, stopUpdating}) => { + const onNewDMClick = async () => { + startUpdating(); + await createRoom({dmUserId: userId}); + stopUpdating(); + }; + + // TODO: Immutable DMs replaces a lot of this + // dmRooms will not include dmRooms that we have been invited into but did not join. + // Because DMRoomMap runs off account_data[m.direct] which is only set on join of dm room. + // XXX: we potentially want DMs we have been invited to, to also show up here :L + // especially as logic below concerns specially if we haven't joined but have been invited + const [dmRooms, setDmRooms] = useState(new DMRoomMap(cli).getDMRoomsForUserId(userId)); + + // TODO bind the below + // cli.on("Room", this.onRoom); + // cli.on("Room.name", this.onRoomName); + // cli.on("deleteRoom", this.onDeleteRoom); + + const accountDataHandler = useCallback((ev) => { + if (ev.getType() === "m.direct") { + const dmRoomMap = new DMRoomMap(cli); + setDmRooms(dmRoomMap.getDMRoomsForUserId(userId)); + } + }, [cli, userId]); + + useEventListener("accountData", accountDataHandler, cli); + + const RoomTile = sdk.getComponent("rooms.RoomTile"); + + const tiles = []; + for (const roomId of dmRooms) { + const room = cli.getRoom(roomId); + if (room) { + const myMembership = room.getMyMembership(); + // not a DM room if we have are not joined + if (myMembership !== 'join') continue; + + const them = room.getMember(userId); + // not a DM room if they are not joined + if (!them || !them.membership || them.membership !== 'join') continue; + + const highlight = room.getUnreadNotificationCount('highlight') > 0; + + tiles.push( + , + ); + } + } + + const labelClasses = classNames({ + mx_UserInfo_createRoom_label: true, + mx_RoomTile_name: true, + }); + + let body = tiles; + if (!body) { + body = ( + +
+ {_t("Start +
+
{ _t("Start a chat") }
+
+ ); + } + + return ( +
+
+

{ _t("Direct messages") }

+ +
+ { body } +
+ ); +}); + +const UserOptionsSection = withLegacyMatrixClient(({cli, member, isIgnored, canInvite}) => { + let ignoreButton = null; + let insertPillButton = null; + let inviteUserButton = null; + let readReceiptButton = null; + + const onShareUserClick = () => { + const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); + Modal.createTrackedDialog('share room member dialog', '', ShareDialog, { + target: member, + }); + }; + + // Only allow the user to ignore the user if its not ourselves + // same goes for jumping to read receipt + if (member.userId !== cli.getUserId()) { + const onIgnoreToggle = () => { + const ignoredUsers = cli.getIgnoredUsers(); + if (isIgnored) { + const index = ignoredUsers.indexOf(member.userId); + if (index !== -1) ignoredUsers.splice(index, 1); + } else { + ignoredUsers.push(member.userId); + } + + cli.setIgnoredUsers(ignoredUsers).then(() => { + // return this.setState({isIgnoring: !this.state.isIgnoring}); + }); + }; + + ignoreButton = ( + + { isIgnored ? _t("Unignore") : _t("Ignore") } + + ); + + if (member.roomId) { + const onReadReceiptButton = function() { + const room = cli.getRoom(member.roomId); + dis.dispatch({ + action: 'view_room', + highlighted: true, + event_id: room.getEventReadUpTo(member.userId), + room_id: member.roomId, + }); + }; + + const onInsertPillButton = function() { + dis.dispatch({ + action: 'insert_mention', + user_id: member.userId, + }); + }; + + readReceiptButton = ( + + { _t('Jump to read receipt') } + + ); + + insertPillButton = ( + + { _t('Mention') } + + ); + } + + if (canInvite && (!member || !member.membership || member.membership === 'leave')) { + const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId(); + const onInviteUserButton = async () => { + try { + // We use a MultiInviter to re-use the invite logic, even though + // we're only inviting one user. + const inviter = new MultiInviter(roomId); + await inviter.invite([member.userId]).then(() => { + if (inviter.getCompletionState(member.userId) !== "invited") { + throw new Error(inviter.getErrorText(member.userId)); + } + }); + } catch (err) { + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { + title: _t('Failed to invite'), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + } + }; + + inviteUserButton = ( + + { _t('Invite') } + + ); + } + } + + const shareUserButton = ( + + { _t('Share Link to User') } + + ); + + return ( +
+

{ _t("User Options") }

+
+ { readReceiptButton } + { shareUserButton } + { insertPillButton } + { ignoreButton } + { inviteUserButton } +
+
+ ); +}); + +const _warnSelfDemote = async () => { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const {finished} = Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, { + title: _t("Demote yourself?"), + description: +
+ { _t("You will not be able to undo this change as you are demoting yourself, " + + "if you are the last privileged user in the room it will be impossible " + + "to regain privileges.") } +
, + button: _t("Demote"), + }); + + const [confirmed] = await finished; + return confirmed; +}; + +const GenericAdminToolsContainer = ({children}) => { + return ( +
+

{ _t("Admin Tools") }

+
+ { children } +
+
+ ); +}; + +const _isMuted = (member, powerLevelContent) => { + if (!powerLevelContent || !member) return false; + + const levelToSend = ( + (powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) || + powerLevelContent.events_default + ); + return member.powerLevel < levelToSend; +}; + +const useRoomPowerLevels = (room) => { + const [powerLevels, setPowerLevels] = useState({}); + + const update = useCallback(() => { + const event = room.currentState.getStateEvents("m.room.power_levels", ""); + if (event) { + setPowerLevels(event.getContent()); + } else { + setPowerLevels({}); + } + return () => { + setPowerLevels({}); + }; + }, [room]); + + useEventListener("RoomState.events", update, room); + useEffect(() => { + update(); + return () => { + setPowerLevels({}); + }; + }, [update]); + return powerLevels; +}; + +const RoomAdminToolsContainer = withLegacyMatrixClient(({cli, room, children, member, startUpdating, stopUpdating}) => { + let kickButton; + let banButton; + let muteButton; + let redactButton; + + const powerLevels = useRoomPowerLevels(room); + const editPowerLevel = ( + (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || + powerLevels.state_default + ); + + const me = room.getMember(cli.getUserId()); + const isMe = me.userId === member.userId; + const canAffectUser = member.powerLevel < me.powerLevel || isMe; + const membership = member.membership; + + if (canAffectUser && me.powerLevel >= powerLevels.kick) { + const onKick = async () => { + const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); + const {finished} = Modal.createTrackedDialog( + 'Confirm User Action Dialog', + 'onKick', + ConfirmUserActionDialog, + { + member, + action: membership === "invite" ? _t("Disinvite") : _t("Kick"), + title: membership === "invite" ? _t("Disinvite this user?") : _t("Kick this user?"), + askReason: membership === "join", + danger: true, + }, + ); + + const [proceed, reason] = await finished; + if (!proceed) return; + + startUpdating(); + cli.kick(member.roomId, member.userId, reason || undefined).then(() => { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Kick success"); + }, function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Kick error: " + err); + Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, { + title: _t("Failed to kick"), + description: ((err && err.message) ? err.message : "Operation failed"), + }); + }).finally(() => { + stopUpdating(); + }); + }; + + const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick"); + kickButton = ( + + { kickLabel } + + ); + } + if (me.powerLevel >= powerLevels.redact) { + const onRedactAllMessages = async () => { + const {roomId, userId} = member; + const room = cli.getRoom(roomId); + if (!room) { + return; + } + let timeline = room.getLiveTimeline(); + let eventsToRedact = []; + while (timeline) { + eventsToRedact = timeline.getEvents().reduce((events, event) => { + if (event.getSender() === userId && !event.isRedacted()) { + return events.concat(event); + } else { + return events; + } + }, eventsToRedact); + timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS); + } + + const count = eventsToRedact.length; + const user = member.name; + + if (count === 0) { + const InfoDialog = sdk.getComponent("dialogs.InfoDialog"); + Modal.createTrackedDialog('No user messages found to remove', '', InfoDialog, { + title: _t("No recent messages by %(user)s found", {user}), + description: +
+

{ _t("Try scrolling up in the timeline to see if there are any earlier ones.") }

+
, + }); + } else { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + const {finished} = Modal.createTrackedDialog('Remove recent messages by user', '', QuestionDialog, { + title: _t("Remove recent messages by %(user)s", {user}), + description: +
+

{ _t("You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", {count, user}) }

+

{ _t("For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.") }

+
, + button: _t("Remove %(count)s messages", {count}), + }); + + const [confirmed] = await finished; + if (!confirmed) { + return; + } + + // Submitting a large number of redactions freezes the UI, + // so first yield to allow to rerender after closing the dialog. + await Promise.resolve(); + + console.info(`Started redacting recent ${count} messages for ${user} in ${roomId}`); + await Promise.all(eventsToRedact.map(async event => { + try { + await cli.redactEvent(roomId, event.getId()); + } catch (err) { + // log and swallow errors + console.error("Could not redact", event.getId()); + console.error(err); + } + })); + console.info(`Finished redacting recent ${count} messages for ${user} in ${roomId}`); + } + }; + + redactButton = ( + + { _t("Remove recent messages") } + + ); + } + if (canAffectUser && me.powerLevel >= powerLevels.ban) { + const onBanOrUnban = async () => { + const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); + const {finished} = Modal.createTrackedDialog( + 'Confirm User Action Dialog', + 'onBanOrUnban', + ConfirmUserActionDialog, + { + member, + action: membership === 'ban' ? _t("Unban") : _t("Ban"), + title: membership === 'ban' ? _t("Unban this user?") : _t("Ban this user?"), + askReason: membership !== 'ban', + danger: membership !== 'ban', + }, + ); + + const [proceed, reason] = await finished; + if (!proceed) return; + + startUpdating(); + let promise; + if (membership === 'ban') { + promise = cli.unban(member.roomId, member.userId); + } else { + promise = cli.ban(member.roomId, member.userId, reason || undefined); + } + promise.then(() => { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Ban success"); + }, function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Ban error: " + err); + Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, { + title: _t("Error"), + description: _t("Failed to ban user"), + }); + }).finally(() => { + stopUpdating(); + }); + }; + + let label = _t("Ban"); + if (membership === 'ban') { + label = _t("Unban"); + } + banButton = ( + + { label } + + ); + } + if (canAffectUser && me.powerLevel >= editPowerLevel) { + const isMuted = _isMuted(member, powerLevels); + const onMuteToggle = async () => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const roomId = member.roomId; + const target = member.userId; + + // if muting self, warn as it may be irreversible + if (target === cli.getUserId()) { + try { + if (!(await _warnSelfDemote())) return; + } catch (e) { + console.error("Failed to warn about self demotion: ", e); + return; + } + } + + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (!powerLevelEvent) return; + + const powerLevels = powerLevelEvent.getContent(); + const levelToSend = ( + (powerLevels.events ? powerLevels.events["m.room.message"] : null) || + powerLevels.events_default + ); + let level; + if (isMuted) { // unmute + level = levelToSend; + } else { // mute + level = levelToSend - 1; + } + level = parseInt(level); + + if (!isNaN(level)) { + startUpdating(); + cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Mute toggle success"); + }, function(err) { + console.error("Mute error: " + err); + Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, { + title: _t("Error"), + description: _t("Failed to mute user"), + }); + }).finally(() => { + stopUpdating(); + }); + } + }; + + const muteLabel = isMuted ? _t("Unmute") : _t("Mute"); + muteButton = ( + + { muteLabel } + + ); + } + + if (kickButton || banButton || muteButton || redactButton || children) { + return + { muteButton } + { kickButton } + { banButton } + { redactButton } + { children } + ; + } + + return
; +}); + +const GroupAdminToolsSection = withLegacyMatrixClient( + ({cli, children, groupId, groupMember, startUpdating, stopUpdating}) => { + const [isPrivileged, setIsPrivileged] = useState(false); + const [isInvited, setIsInvited] = useState(false); + + // Listen to group store changes + useEffect(() => { + let unmounted = false; + + const onGroupStoreUpdated = () => { + if (unmounted) return; + setIsPrivileged(GroupStore.isUserPrivileged(groupId)); + setIsInvited(GroupStore.getGroupInvitedMembers(groupId).some( + (m) => m.userId === groupMember.userId, + )); + }; + + GroupStore.registerListener(groupId, onGroupStoreUpdated); + onGroupStoreUpdated(); + // Handle unmount + return () => { + unmounted = true; + GroupStore.unregisterListener(onGroupStoreUpdated); + }; + }, [groupId, groupMember.userId]); + + if (isPrivileged) { + const _onKick = async () => { + const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); + const {finished} = Modal.createDialog(ConfirmUserActionDialog, { + matrixClient: cli, + groupMember, + action: isInvited ? _t('Disinvite') : _t('Remove from community'), + title: isInvited ? _t('Disinvite this user from community?') + : _t('Remove this user from community?'), + danger: true, + }); + + const [proceed] = await finished; + if (!proceed) return; + + startUpdating(); + cli.removeUserFromGroup(groupId, groupMember.userId).then(() => { + // return to the user list + dis.dispatch({ + action: "view_user", + member: null, + }); + }).catch((e) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, { + title: _t('Error'), + description: isInvited ? + _t('Failed to withdraw invitation') : + _t('Failed to remove user from community'), + }); + console.log(e); + }).finally(() => { + stopUpdating(); + }); + }; + + const kickButton = ( + + { isInvited ? _t('Disinvite') : _t('Remove from community') } + + ); + + // No make/revoke admin API yet + /*const opLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator"); + giveModButton = + {giveOpLabel} + ;*/ + + return + { kickButton } + { children } + ; + } + + return
; + }, +); + +const GroupMember = PropTypes.shape({ + userId: PropTypes.string.isRequired, + displayname: PropTypes.string, // XXX: GroupMember objects are inconsistent :(( + avatarUrl: PropTypes.string, +}); + +const useIsSynapseAdmin = (cli) => { + const [isAdmin, setIsAdmin] = useState(false); + useEffect(() => { + cli.isSynapseAdministrator().then((isAdmin) => { + setIsAdmin(isAdmin); + }, () => { + setIsAdmin(false); + }); + }, []); + return isAdmin; +}; + +// cli is injected by withLegacyMatrixClient +const UserInfo = withLegacyMatrixClient(({cli, user, groupId, roomId, onClose}) => { + // Load room if we are given a room id and memoize it + const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]); + + // only display the devices list if our client supports E2E + const _enableDevices = cli.isCryptoEnabled(); + + // Load whether or not we are a Synapse Admin + const isSynapseAdmin = useIsSynapseAdmin(cli); + + // Check whether the user is ignored + const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId)); + // Recheck if the user or client changes + useEffect(() => { + setIsIgnored(cli.isUserIgnored(user.userId)); + }, [cli, user.userId]); + // Recheck also if we receive new accountData m.ignored_user_list + const accountDataHandler = useCallback((ev) => { + if (ev.getType() === "m.ignored_user_list") { + setIsIgnored(cli.isUserIgnored(user.userId)); + } + }, [cli, user.userId]); + useEventListener("accountData", accountDataHandler, cli); + + // Count of how many operations are currently in progress, if > 0 then show a Spinner + const [pendingUpdateCount, setPendingUpdateCount] = useState(0); + const startUpdating = useCallback(() => { + setPendingUpdateCount(pendingUpdateCount + 1); + }, [pendingUpdateCount]); + const stopUpdating = useCallback(() => { + setPendingUpdateCount(pendingUpdateCount - 1); + }, [pendingUpdateCount]); + + const [roomPermissions, setRoomPermissions] = useState({ + // modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL + modifyLevelMax: -1, + canInvite: false, + }); + const updateRoomPermissions = useCallback(async () => { + if (!room) return; + + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (!powerLevelEvent) return; + const powerLevels = powerLevelEvent.getContent(); + if (!powerLevels) return; + + const me = room.getMember(cli.getUserId()); + if (!me) return; + + const them = user; + const isMe = me.userId === them.userId; + const canAffectUser = them.powerLevel < me.powerLevel || isMe; + + let modifyLevelMax = -1; + if (canAffectUser) { + const editPowerLevel = ( + (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || + powerLevels.state_default + ); + if (me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel)) { + modifyLevelMax = me.powerLevel; + } + } + + setRoomPermissions({ + canInvite: me.powerLevel >= powerLevels.invite, + modifyLevelMax, + }); + }, [cli, user, room]); + useEventListener("RoomState.events", updateRoomPermissions, cli); + useEffect(() => { + updateRoomPermissions(); + return () => { + setRoomPermissions({ + maximalPowerLevel: -1, + canInvite: false, + }); + }; + }, [updateRoomPermissions]); + + const onSynapseDeactivate = useCallback(async () => { + const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog'); + const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, { + title: _t("Deactivate user?"), + description: +
{ _t( + "Deactivating this user will log them out and prevent them from logging back in. Additionally, " + + "they will leave all the rooms they are in. This action cannot be reversed. Are you sure you " + + "want to deactivate this user?", + ) }
, + button: _t("Deactivate user"), + danger: true, + }); + + const [accepted] = await finished; + if (!accepted) return; + try { + cli.deactivateSynapseUser(user.userId); + } catch (err) { + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createTrackedDialog('Failed to deactivate user', '', ErrorDialog, { + title: _t('Failed to deactivate user'), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + } + }, [cli, user.userId]); + + const onPowerChange = useCallback(async (powerLevel) => { + const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { + startUpdating(); + cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( + function() { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Power change success"); + }, function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to change power level " + err); + Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, { + title: _t("Error"), + description: _t("Failed to change power level"), + }); + }, + ).finally(() => { + stopUpdating(); + }).done(); + }; + + const roomId = user.roomId; + const target = user.userId; + + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (!powerLevelEvent) return; + + if (!powerLevelEvent.getContent().users) { + _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); + return; + } + + const myUserId = cli.getUserId(); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + // If we are changing our own PL it can only ever be decreasing, which we cannot reverse. + if (myUserId === target) { + try { + if (!(await _warnSelfDemote())) return; + _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); + } catch (e) { + console.error("Failed to warn about self demotion: ", e); + } + return; + } + + const myPower = powerLevelEvent.getContent().users[myUserId]; + if (parseInt(myPower) === parseInt(powerLevel)) { + const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, { + title: _t("Warning!"), + description: +
+ { _t("You will not be able to undo this change as you are promoting the user " + + "to have the same power level as yourself.") }
+ { _t("Are you sure?") } +
, + button: _t("Continue"), + }); + + const [confirmed] = await finished; + if (confirmed) return; + } + _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); + }, [user.roomId, user.userId, room && room.currentState, cli]); // eslint-disable-line + + const onMemberAvatarClick = useCallback(() => { + const member = user; + const avatarUrl = member.getMxcAvatarUrl(); + if (!avatarUrl) return; + + const httpUrl = cli.mxcUrlToHttp(avatarUrl); + const ImageView = sdk.getComponent("elements.ImageView"); + const params = { + src: httpUrl, + name: member.name, + }; + + Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); + }, [cli, user]); + + let synapseDeactivateButton; + let spinner; + + let directChatsSection; + if (user.userId !== cli.getUserId()) { + directChatsSection = ; + } + + // We don't need a perfect check here, just something to pass as "probably not our homeserver". If + // someone does figure out how to bypass this check the worst that happens is an error. + // FIXME this should be using cli instead of MatrixClientPeg.matrixClient + if (isSynapseAdmin && user.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`)) { + synapseDeactivateButton = ( + + {_t("Deactivate user")} + + ); + } + + let adminToolsContainer; + if (room && user.roomId) { + adminToolsContainer = ( + + { synapseDeactivateButton } + + ); + } else if (groupId) { + adminToolsContainer = ( + + { synapseDeactivateButton } + + ); + } else if (synapseDeactivateButton) { + adminToolsContainer = ( + + { synapseDeactivateButton } + + ); + } + + if (pendingUpdateCount > 0) { + const Loader = sdk.getComponent("elements.Spinner"); + spinner = ; + } + + const displayName = user.name || user.displayname; + + let presenceState; + let presenceLastActiveAgo; + let presenceCurrentlyActive; + let statusMessage; + + if (user instanceof RoomMember) { + presenceState = user.user.presence; + presenceLastActiveAgo = user.user.lastActiveAgo; + presenceCurrentlyActive = user.user.currentlyActive; + + if (SettingsStore.isFeatureEnabled("feature_custom_status")) { + statusMessage = user.user._unstable_statusMessage; + } + } + + const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"]; + let showPresence = true; + if (enablePresenceByHsUrl && enablePresenceByHsUrl[cli.baseUrl] !== undefined) { + showPresence = enablePresenceByHsUrl[cli.baseUrl]; + } + + let presenceLabel = null; + if (showPresence) { + const PresenceLabel = sdk.getComponent('rooms.PresenceLabel'); + presenceLabel = ; + } + + let statusLabel = null; + if (statusMessage) { + statusLabel = { statusMessage }; + } + + let memberDetails = null; + + if (room && user.roomId) { // is in room + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; + + const PowerSelector = sdk.getComponent('elements.PowerSelector'); + memberDetails =
+
+ +
+ +
; + } + + const avatarUrl = user.getMxcAvatarUrl ? user.getMxcAvatarUrl() : user.avatarUrl; + let avatarElement; + if (avatarUrl) { + const httpUrl = cli.mxcUrlToHttp(avatarUrl, 800, 800); + avatarElement =
+ {_t("Profile +
; + } + + let closeButton; + if (onClose) { + closeButton = ; + } + + // undefined means yet to be loaded, null means failed to load, otherwise list of devices + const [devices, setDevices] = useState(undefined); + // Download device lists + useEffect(() => { + setDevices(undefined); + + let cancelled = false; + + async function _downloadDeviceList() { + try { + await cli.downloadKeys([user.userId], true); + const devices = await cli.getStoredDevicesForUser(user.userId); + + if (cancelled) { + // we got cancelled - presumably a different user now + return; + } + + _disambiguateDevices(devices); + setDevices(devices); + } catch (err) { + setDevices(null); + } + } + + _downloadDeviceList(); + + // Handle being unmounted + return () => { + cancelled = true; + }; + }, [cli, user.userId]); + + // Listen to changes + useEffect(() => { + let cancel = false; + const onDeviceVerificationChanged = (_userId, device) => { + if (_userId === user.userId) { + // no need to re-download the whole thing; just update our copy of the list. + + // Promise.resolve to handle transition from static result to promise; can be removed in future + Promise.resolve(cli.getStoredDevicesForUser(user.userId)).then((devices) => { + if (cancel) return; + setDevices(devices); + }); + } + }; + + cli.on("deviceVerificationChanged", onDeviceVerificationChanged); + // Handle being unmounted + return () => { + cancel = true; + cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged); + }; + }, [cli, user.userId]); + + let devicesSection; + const isRoomEncrypted = _enableDevices && room && cli.isRoomEncrypted(room.roomId); + if (isRoomEncrypted) { + devicesSection = ; + } else { + let text; + + if (!_enableDevices) { + text = _t("This client does not support end-to-end encryption."); + } else if (room) { + text = _t("Messages in this room are not end-to-end encrypted."); + } else { + // TODO what to render for GroupMember + } + + if (text) { + devicesSection = ( +
+

{ _t("Trust & Devices") }

+
+ { text } +
+
+ ); + } + } + + let e2eIcon; + if (isRoomEncrypted && devices) { + e2eIcon = ; + } + + return ( +
+ { closeButton } + { avatarElement } + +
+
+
+

+ { e2eIcon } + { displayName } +

+
+
+ { user.userId } +
+
+ {presenceLabel} + {statusLabel} +
+
+
+ + { memberDetails &&
+
+ { memberDetails } +
+
} + + + { devicesSection } + + { directChatsSection } + + + + { adminToolsContainer } + + { spinner } + +
+ ); +}); + +UserInfo.propTypes = { + user: PropTypes.oneOfType([ + PropTypes.instanceOf(User), + PropTypes.instanceOf(RoomMember), + GroupMember, + ]).isRequired, + group: PropTypes.instanceOf(Group), + groupId: PropTypes.string, + roomId: PropTypes.string, + + onClose: PropTypes.func, +}; + +export default UserInfo; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b303b7a94d..03c9070db5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -332,6 +332,7 @@ "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Use the new, consistent UserInfo panel for Room Members and Group Members", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", @@ -1203,6 +1204,11 @@ "This alias is already in use": "This alias is already in use", "Room directory": "Room directory", "And %(count)s more...|other": "And %(count)s more...", + "Trust & Devices": "Trust & Devices", + "Direct messages": "Direct messages", + "Failed to deactivate user": "Failed to deactivate user", + "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", + "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "ex. @bob:example.com": "ex. @bob:example.com", "Add User": "Add User", "Matrix ID": "Matrix ID", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 3c33ae57fe..7470641359 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -120,6 +120,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_user_info_panel": { + isFeature: true, + displayName: _td("Use the new, consistent UserInfo panel for Room Members and Group Members"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "useCiderComposer": { displayName: _td("Use the new, faster, composer for writing messages"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, diff --git a/yarn.lock b/yarn.lock index ba7fea21ba..e40a1272cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -293,6 +293,11 @@ dependencies: "@types/yargs-parser" "*" +"@use-it/event-listener@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@use-it/event-listener/-/event-listener-0.1.3.tgz#a9920b2819d211cf55e68e830997546eec6886d3" + integrity sha512-UCHkLOVU+xj3/1R8jXz8GzDTowkzfIDPESOBlVC2ndgwUSBEqiFdwCoUEs2lcGhJOOiEdmWxF+T23C5+60eEew== + "@webassemblyjs/ast@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" @@ -2891,6 +2896,11 @@ eslint-plugin-flowtype@^2.30.0: dependencies: lodash "^4.17.10" +eslint-plugin-react-hooks@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.0.1.tgz#e898ec26a0a335af6f7b0ad1f0bedda7143ed756" + integrity sha512-xir+3KHKo86AasxlCV8AHRtIZPHljqCRRUYgASkbatmt0fad4+5GgC7zkT7o/06hdKM6MIwp8giHVXqBPaarHQ== + eslint-plugin-react@^7.7.0: version "7.14.3" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.3.tgz#911030dd7e98ba49e1b2208599571846a66bdf13" From 37b122aa16c53c891e02936c10289cbfa7718b57 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Oct 2019 10:50:37 +0100 Subject: [PATCH 0223/2372] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/right_panel/UserInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 94df344db2..59cd61a583 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -732,7 +732,7 @@ const useIsSynapseAdmin = (cli) => { }, () => { setIsAdmin(false); }); - }, []); + }, [cli]); return isAdmin; }; From fa8ad7088039c81b37a8e2684fa8d0585962226c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Oct 2019 11:16:27 +0100 Subject: [PATCH 0224/2372] run i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 03c9070db5..a875cd860c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1018,6 +1018,16 @@ "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", "Members": "Members", "Files": "Files", + "Trust & Devices": "Trust & Devices", + "Direct messages": "Direct messages", + "Remove from community": "Remove from community", + "Disinvite this user from community?": "Disinvite this user from community?", + "Remove this user from community?": "Remove this user from community?", + "Failed to withdraw invitation": "Failed to withdraw invitation", + "Failed to remove user from community": "Failed to remove user from community", + "Failed to deactivate user": "Failed to deactivate user", + "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", + "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", @@ -1062,11 +1072,6 @@ "Removed or unknown message type": "Removed or unknown message type", "Message removed by %(userId)s": "Message removed by %(userId)s", "Message removed": "Message removed", - "Remove from community": "Remove from community", - "Disinvite this user from community?": "Disinvite this user from community?", - "Remove this user from community?": "Remove this user from community?", - "Failed to withdraw invitation": "Failed to withdraw invitation", - "Failed to remove user from community": "Failed to remove user from community", "Failed to load group members": "Failed to load group members", "Filter community members": "Filter community members", "Invite to this community": "Invite to this community", @@ -1204,11 +1209,6 @@ "This alias is already in use": "This alias is already in use", "Room directory": "Room directory", "And %(count)s more...|other": "And %(count)s more...", - "Trust & Devices": "Trust & Devices", - "Direct messages": "Direct messages", - "Failed to deactivate user": "Failed to deactivate user", - "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", - "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "ex. @bob:example.com": "ex. @bob:example.com", "Add User": "Add User", "Matrix ID": "Matrix ID", From 6cb9ef7e65f261bae577c25e0ca76c81c4f9c548 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Oct 2019 16:51:35 +0200 Subject: [PATCH 0225/2372] remove editorconfig --- .editorconfig | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 880331a09e..0000000000 --- a/.editorconfig +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2017 Aviral Dasgupta -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -root = true - -[*] -charset=utf-8 -end_of_line = lf -insert_final_newline = true -indent_style = space -indent_size = 4 -trim_trailing_whitespace = true From ca86969f929014344b42d2d4b80b206f71952c97 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Oct 2019 16:52:48 +0200 Subject: [PATCH 0226/2372] move everything to subfolder to merge into react-sdk --- .gitignore => test/end-to-end-tests/.gitignore | 0 README.md => test/end-to-end-tests/README.md | 0 TODO.md => test/end-to-end-tests/TODO.md | 0 install.sh => test/end-to-end-tests/install.sh | 0 package.json => test/end-to-end-tests/package.json | 0 {riot => test/end-to-end-tests/riot}/.gitignore | 0 {riot => test/end-to-end-tests/riot}/config-template/config.json | 0 {riot => test/end-to-end-tests/riot}/install.sh | 0 {riot => test/end-to-end-tests/riot}/start.sh | 0 {riot => test/end-to-end-tests/riot}/stop.sh | 0 run.sh => test/end-to-end-tests/run.sh | 0 {src => test/end-to-end-tests/src}/logbuffer.js | 0 {src => test/end-to-end-tests/src}/logger.js | 0 {src => test/end-to-end-tests/src}/rest/consent.js | 0 {src => test/end-to-end-tests/src}/rest/creator.js | 0 {src => test/end-to-end-tests/src}/rest/multi.js | 0 {src => test/end-to-end-tests/src}/rest/room.js | 0 {src => test/end-to-end-tests/src}/rest/session.js | 0 {src => test/end-to-end-tests/src}/scenario.js | 0 {src => test/end-to-end-tests/src}/scenarios/README.md | 0 {src => test/end-to-end-tests/src}/scenarios/directory.js | 0 {src => test/end-to-end-tests/src}/scenarios/e2e-encryption.js | 0 {src => test/end-to-end-tests/src}/scenarios/lazy-loading.js | 0 {src => test/end-to-end-tests/src}/session.js | 0 {src => test/end-to-end-tests/src}/usecases/README.md | 0 {src => test/end-to-end-tests/src}/usecases/accept-invite.js | 0 {src => test/end-to-end-tests/src}/usecases/create-room.js | 0 {src => test/end-to-end-tests/src}/usecases/dialog.js | 0 {src => test/end-to-end-tests/src}/usecases/invite.js | 0 {src => test/end-to-end-tests/src}/usecases/join.js | 0 {src => test/end-to-end-tests/src}/usecases/memberlist.js | 0 {src => test/end-to-end-tests/src}/usecases/room-settings.js | 0 {src => test/end-to-end-tests/src}/usecases/send-message.js | 0 {src => test/end-to-end-tests/src}/usecases/settings.js | 0 {src => test/end-to-end-tests/src}/usecases/signup.js | 0 {src => test/end-to-end-tests/src}/usecases/timeline.js | 0 {src => test/end-to-end-tests/src}/usecases/verify.js | 0 {src => test/end-to-end-tests/src}/util.js | 0 start.js => test/end-to-end-tests/start.js | 0 {synapse => test/end-to-end-tests/synapse}/.gitignore | 0 .../synapse}/config-templates/consent/homeserver.yaml | 0 .../config-templates/consent/res/templates/privacy/en/1.0.html | 0 .../consent/res/templates/privacy/en/success.html | 0 {synapse => test/end-to-end-tests/synapse}/install.sh | 0 {synapse => test/end-to-end-tests/synapse}/start.sh | 0 {synapse => test/end-to-end-tests/synapse}/stop.sh | 0 yarn.lock => test/end-to-end-tests/yarn.lock | 0 47 files changed, 0 insertions(+), 0 deletions(-) rename .gitignore => test/end-to-end-tests/.gitignore (100%) rename README.md => test/end-to-end-tests/README.md (100%) rename TODO.md => test/end-to-end-tests/TODO.md (100%) rename install.sh => test/end-to-end-tests/install.sh (100%) rename package.json => test/end-to-end-tests/package.json (100%) rename {riot => test/end-to-end-tests/riot}/.gitignore (100%) rename {riot => test/end-to-end-tests/riot}/config-template/config.json (100%) rename {riot => test/end-to-end-tests/riot}/install.sh (100%) rename {riot => test/end-to-end-tests/riot}/start.sh (100%) rename {riot => test/end-to-end-tests/riot}/stop.sh (100%) rename run.sh => test/end-to-end-tests/run.sh (100%) rename {src => test/end-to-end-tests/src}/logbuffer.js (100%) rename {src => test/end-to-end-tests/src}/logger.js (100%) rename {src => test/end-to-end-tests/src}/rest/consent.js (100%) rename {src => test/end-to-end-tests/src}/rest/creator.js (100%) rename {src => test/end-to-end-tests/src}/rest/multi.js (100%) rename {src => test/end-to-end-tests/src}/rest/room.js (100%) rename {src => test/end-to-end-tests/src}/rest/session.js (100%) rename {src => test/end-to-end-tests/src}/scenario.js (100%) rename {src => test/end-to-end-tests/src}/scenarios/README.md (100%) rename {src => test/end-to-end-tests/src}/scenarios/directory.js (100%) rename {src => test/end-to-end-tests/src}/scenarios/e2e-encryption.js (100%) rename {src => test/end-to-end-tests/src}/scenarios/lazy-loading.js (100%) rename {src => test/end-to-end-tests/src}/session.js (100%) rename {src => test/end-to-end-tests/src}/usecases/README.md (100%) rename {src => test/end-to-end-tests/src}/usecases/accept-invite.js (100%) rename {src => test/end-to-end-tests/src}/usecases/create-room.js (100%) rename {src => test/end-to-end-tests/src}/usecases/dialog.js (100%) rename {src => test/end-to-end-tests/src}/usecases/invite.js (100%) rename {src => test/end-to-end-tests/src}/usecases/join.js (100%) rename {src => test/end-to-end-tests/src}/usecases/memberlist.js (100%) rename {src => test/end-to-end-tests/src}/usecases/room-settings.js (100%) rename {src => test/end-to-end-tests/src}/usecases/send-message.js (100%) rename {src => test/end-to-end-tests/src}/usecases/settings.js (100%) rename {src => test/end-to-end-tests/src}/usecases/signup.js (100%) rename {src => test/end-to-end-tests/src}/usecases/timeline.js (100%) rename {src => test/end-to-end-tests/src}/usecases/verify.js (100%) rename {src => test/end-to-end-tests/src}/util.js (100%) rename start.js => test/end-to-end-tests/start.js (100%) rename {synapse => test/end-to-end-tests/synapse}/.gitignore (100%) rename {synapse => test/end-to-end-tests/synapse}/config-templates/consent/homeserver.yaml (100%) rename {synapse => test/end-to-end-tests/synapse}/config-templates/consent/res/templates/privacy/en/1.0.html (100%) rename {synapse => test/end-to-end-tests/synapse}/config-templates/consent/res/templates/privacy/en/success.html (100%) rename {synapse => test/end-to-end-tests/synapse}/install.sh (100%) rename {synapse => test/end-to-end-tests/synapse}/start.sh (100%) rename {synapse => test/end-to-end-tests/synapse}/stop.sh (100%) rename yarn.lock => test/end-to-end-tests/yarn.lock (100%) diff --git a/.gitignore b/test/end-to-end-tests/.gitignore similarity index 100% rename from .gitignore rename to test/end-to-end-tests/.gitignore diff --git a/README.md b/test/end-to-end-tests/README.md similarity index 100% rename from README.md rename to test/end-to-end-tests/README.md diff --git a/TODO.md b/test/end-to-end-tests/TODO.md similarity index 100% rename from TODO.md rename to test/end-to-end-tests/TODO.md diff --git a/install.sh b/test/end-to-end-tests/install.sh similarity index 100% rename from install.sh rename to test/end-to-end-tests/install.sh diff --git a/package.json b/test/end-to-end-tests/package.json similarity index 100% rename from package.json rename to test/end-to-end-tests/package.json diff --git a/riot/.gitignore b/test/end-to-end-tests/riot/.gitignore similarity index 100% rename from riot/.gitignore rename to test/end-to-end-tests/riot/.gitignore diff --git a/riot/config-template/config.json b/test/end-to-end-tests/riot/config-template/config.json similarity index 100% rename from riot/config-template/config.json rename to test/end-to-end-tests/riot/config-template/config.json diff --git a/riot/install.sh b/test/end-to-end-tests/riot/install.sh similarity index 100% rename from riot/install.sh rename to test/end-to-end-tests/riot/install.sh diff --git a/riot/start.sh b/test/end-to-end-tests/riot/start.sh similarity index 100% rename from riot/start.sh rename to test/end-to-end-tests/riot/start.sh diff --git a/riot/stop.sh b/test/end-to-end-tests/riot/stop.sh similarity index 100% rename from riot/stop.sh rename to test/end-to-end-tests/riot/stop.sh diff --git a/run.sh b/test/end-to-end-tests/run.sh similarity index 100% rename from run.sh rename to test/end-to-end-tests/run.sh diff --git a/src/logbuffer.js b/test/end-to-end-tests/src/logbuffer.js similarity index 100% rename from src/logbuffer.js rename to test/end-to-end-tests/src/logbuffer.js diff --git a/src/logger.js b/test/end-to-end-tests/src/logger.js similarity index 100% rename from src/logger.js rename to test/end-to-end-tests/src/logger.js diff --git a/src/rest/consent.js b/test/end-to-end-tests/src/rest/consent.js similarity index 100% rename from src/rest/consent.js rename to test/end-to-end-tests/src/rest/consent.js diff --git a/src/rest/creator.js b/test/end-to-end-tests/src/rest/creator.js similarity index 100% rename from src/rest/creator.js rename to test/end-to-end-tests/src/rest/creator.js diff --git a/src/rest/multi.js b/test/end-to-end-tests/src/rest/multi.js similarity index 100% rename from src/rest/multi.js rename to test/end-to-end-tests/src/rest/multi.js diff --git a/src/rest/room.js b/test/end-to-end-tests/src/rest/room.js similarity index 100% rename from src/rest/room.js rename to test/end-to-end-tests/src/rest/room.js diff --git a/src/rest/session.js b/test/end-to-end-tests/src/rest/session.js similarity index 100% rename from src/rest/session.js rename to test/end-to-end-tests/src/rest/session.js diff --git a/src/scenario.js b/test/end-to-end-tests/src/scenario.js similarity index 100% rename from src/scenario.js rename to test/end-to-end-tests/src/scenario.js diff --git a/src/scenarios/README.md b/test/end-to-end-tests/src/scenarios/README.md similarity index 100% rename from src/scenarios/README.md rename to test/end-to-end-tests/src/scenarios/README.md diff --git a/src/scenarios/directory.js b/test/end-to-end-tests/src/scenarios/directory.js similarity index 100% rename from src/scenarios/directory.js rename to test/end-to-end-tests/src/scenarios/directory.js diff --git a/src/scenarios/e2e-encryption.js b/test/end-to-end-tests/src/scenarios/e2e-encryption.js similarity index 100% rename from src/scenarios/e2e-encryption.js rename to test/end-to-end-tests/src/scenarios/e2e-encryption.js diff --git a/src/scenarios/lazy-loading.js b/test/end-to-end-tests/src/scenarios/lazy-loading.js similarity index 100% rename from src/scenarios/lazy-loading.js rename to test/end-to-end-tests/src/scenarios/lazy-loading.js diff --git a/src/session.js b/test/end-to-end-tests/src/session.js similarity index 100% rename from src/session.js rename to test/end-to-end-tests/src/session.js diff --git a/src/usecases/README.md b/test/end-to-end-tests/src/usecases/README.md similarity index 100% rename from src/usecases/README.md rename to test/end-to-end-tests/src/usecases/README.md diff --git a/src/usecases/accept-invite.js b/test/end-to-end-tests/src/usecases/accept-invite.js similarity index 100% rename from src/usecases/accept-invite.js rename to test/end-to-end-tests/src/usecases/accept-invite.js diff --git a/src/usecases/create-room.js b/test/end-to-end-tests/src/usecases/create-room.js similarity index 100% rename from src/usecases/create-room.js rename to test/end-to-end-tests/src/usecases/create-room.js diff --git a/src/usecases/dialog.js b/test/end-to-end-tests/src/usecases/dialog.js similarity index 100% rename from src/usecases/dialog.js rename to test/end-to-end-tests/src/usecases/dialog.js diff --git a/src/usecases/invite.js b/test/end-to-end-tests/src/usecases/invite.js similarity index 100% rename from src/usecases/invite.js rename to test/end-to-end-tests/src/usecases/invite.js diff --git a/src/usecases/join.js b/test/end-to-end-tests/src/usecases/join.js similarity index 100% rename from src/usecases/join.js rename to test/end-to-end-tests/src/usecases/join.js diff --git a/src/usecases/memberlist.js b/test/end-to-end-tests/src/usecases/memberlist.js similarity index 100% rename from src/usecases/memberlist.js rename to test/end-to-end-tests/src/usecases/memberlist.js diff --git a/src/usecases/room-settings.js b/test/end-to-end-tests/src/usecases/room-settings.js similarity index 100% rename from src/usecases/room-settings.js rename to test/end-to-end-tests/src/usecases/room-settings.js diff --git a/src/usecases/send-message.js b/test/end-to-end-tests/src/usecases/send-message.js similarity index 100% rename from src/usecases/send-message.js rename to test/end-to-end-tests/src/usecases/send-message.js diff --git a/src/usecases/settings.js b/test/end-to-end-tests/src/usecases/settings.js similarity index 100% rename from src/usecases/settings.js rename to test/end-to-end-tests/src/usecases/settings.js diff --git a/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js similarity index 100% rename from src/usecases/signup.js rename to test/end-to-end-tests/src/usecases/signup.js diff --git a/src/usecases/timeline.js b/test/end-to-end-tests/src/usecases/timeline.js similarity index 100% rename from src/usecases/timeline.js rename to test/end-to-end-tests/src/usecases/timeline.js diff --git a/src/usecases/verify.js b/test/end-to-end-tests/src/usecases/verify.js similarity index 100% rename from src/usecases/verify.js rename to test/end-to-end-tests/src/usecases/verify.js diff --git a/src/util.js b/test/end-to-end-tests/src/util.js similarity index 100% rename from src/util.js rename to test/end-to-end-tests/src/util.js diff --git a/start.js b/test/end-to-end-tests/start.js similarity index 100% rename from start.js rename to test/end-to-end-tests/start.js diff --git a/synapse/.gitignore b/test/end-to-end-tests/synapse/.gitignore similarity index 100% rename from synapse/.gitignore rename to test/end-to-end-tests/synapse/.gitignore diff --git a/synapse/config-templates/consent/homeserver.yaml b/test/end-to-end-tests/synapse/config-templates/consent/homeserver.yaml similarity index 100% rename from synapse/config-templates/consent/homeserver.yaml rename to test/end-to-end-tests/synapse/config-templates/consent/homeserver.yaml diff --git a/synapse/config-templates/consent/res/templates/privacy/en/1.0.html b/test/end-to-end-tests/synapse/config-templates/consent/res/templates/privacy/en/1.0.html similarity index 100% rename from synapse/config-templates/consent/res/templates/privacy/en/1.0.html rename to test/end-to-end-tests/synapse/config-templates/consent/res/templates/privacy/en/1.0.html diff --git a/synapse/config-templates/consent/res/templates/privacy/en/success.html b/test/end-to-end-tests/synapse/config-templates/consent/res/templates/privacy/en/success.html similarity index 100% rename from synapse/config-templates/consent/res/templates/privacy/en/success.html rename to test/end-to-end-tests/synapse/config-templates/consent/res/templates/privacy/en/success.html diff --git a/synapse/install.sh b/test/end-to-end-tests/synapse/install.sh similarity index 100% rename from synapse/install.sh rename to test/end-to-end-tests/synapse/install.sh diff --git a/synapse/start.sh b/test/end-to-end-tests/synapse/start.sh similarity index 100% rename from synapse/start.sh rename to test/end-to-end-tests/synapse/start.sh diff --git a/synapse/stop.sh b/test/end-to-end-tests/synapse/stop.sh similarity index 100% rename from synapse/stop.sh rename to test/end-to-end-tests/synapse/stop.sh diff --git a/yarn.lock b/test/end-to-end-tests/yarn.lock similarity index 100% rename from yarn.lock rename to test/end-to-end-tests/yarn.lock From cad71913e999100aa8059d9d7b7abeb361d24655 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Oct 2019 16:59:00 +0200 Subject: [PATCH 0227/2372] only run riot static server if no riot url has been provided --- test/end-to-end-tests/has_custom_riot.js | 24 ++++++++++++++++++++++++ test/end-to-end-tests/run.sh | 12 +++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 test/end-to-end-tests/has_custom_riot.js diff --git a/test/end-to-end-tests/has_custom_riot.js b/test/end-to-end-tests/has_custom_riot.js new file mode 100644 index 0000000000..ad79c8680b --- /dev/null +++ b/test/end-to-end-tests/has_custom_riot.js @@ -0,0 +1,24 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// used from run.sh as getopts doesn't support long parameters +const idx = process.argv.indexOf("--riot-url"); +let hasRiotUrl = false; +if (idx !== -1) { + const value = process.argv[idx + 1]; + hasRiotUrl = !!value; +} +process.stdout.write(hasRiotUrl ? "1" : "0" ); diff --git a/test/end-to-end-tests/run.sh b/test/end-to-end-tests/run.sh index 0e03b733ce..0498d8197d 100755 --- a/test/end-to-end-tests/run.sh +++ b/test/end-to-end-tests/run.sh @@ -1,9 +1,13 @@ #!/bin/bash set -e +has_custom_riot=$(node has_custom_riot.js $@) + stop_servers() { - ./riot/stop.sh - ./synapse/stop.sh + if [ $has_custom_riot -ne "1" ]; then + ./riot/stop.sh + fi + ./synapse/stop.sh } handle_error() { @@ -15,6 +19,8 @@ handle_error() { trap 'handle_error' ERR ./synapse/start.sh -./riot/start.sh +if [ $has_custom_riot -ne "1" ]; then + ./riot/start.sh +fi node start.js $@ stop_servers From 5b9bfae320de98b7ea3dd2540d1a69c6c67e2fe7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Oct 2019 17:03:01 +0200 Subject: [PATCH 0228/2372] first attempt at running local e2e tests from CI --- scripts/ci/end-to-end-tests.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh index 0ec26df450..4968bc4f72 100644 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -28,9 +28,7 @@ REACT_SDK_DIR=`pwd` echo "--- Building Riot" scripts/ci/build.sh # run end to end tests -echo "--- Fetching end-to-end tests from master" -scripts/fetchdep.sh matrix-org matrix-react-end-to-end-tests master -pushd matrix-react-end-to-end-tests +pushd test/end-to-end-tests ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh # CHROME_PATH=$(which google-chrome-stable) ./run.sh From 59cc36ca652ce71b59194166e6192e1609aa6f49 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Oct 2019 17:34:40 +0200 Subject: [PATCH 0229/2372] don't fetch riot/master by default when installing e2e tests --- test/end-to-end-tests/install.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/end-to-end-tests/install.sh b/test/end-to-end-tests/install.sh index e1fed144ce..bb88785741 100755 --- a/test/end-to-end-tests/install.sh +++ b/test/end-to-end-tests/install.sh @@ -2,5 +2,7 @@ # run with PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true sh install.sh if chrome is already installed set -e ./synapse/install.sh -./riot/install.sh +# both CI and local testing don't need a riot fetched from master, +# so not installing this by default anymore +# ./riot/install.sh yarn install From f8358fa4d0dfeb29057bbd74a4be6349e9285e13 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Oct 2019 17:37:41 +0200 Subject: [PATCH 0230/2372] make e2e test safe to run from anywhere --- test/end-to-end-tests/run.sh | 2 ++ 1 file changed, 2 insertions(+) mode change 100755 => 100644 test/end-to-end-tests/run.sh diff --git a/test/end-to-end-tests/run.sh b/test/end-to-end-tests/run.sh old mode 100755 new mode 100644 index 0498d8197d..162ce461ec --- a/test/end-to-end-tests/run.sh +++ b/test/end-to-end-tests/run.sh @@ -1,6 +1,8 @@ #!/bin/bash set -e +BASE_DIR=$(cd $(dirname $0) && pwd) +pushd $BASE_DIR has_custom_riot=$(node has_custom_riot.js $@) stop_servers() { From ebc2bba0c332095c6068a7d28173a0a41564800e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Oct 2019 17:37:57 +0200 Subject: [PATCH 0231/2372] warn if not installed yet when running e2e tests --- test/end-to-end-tests/run.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) mode change 100644 => 100755 test/end-to-end-tests/run.sh diff --git a/test/end-to-end-tests/run.sh b/test/end-to-end-tests/run.sh old mode 100644 new mode 100755 index 162ce461ec..085e2ba706 --- a/test/end-to-end-tests/run.sh +++ b/test/end-to-end-tests/run.sh @@ -3,8 +3,19 @@ set -e BASE_DIR=$(cd $(dirname $0) && pwd) pushd $BASE_DIR + +if [ ! -d "synapse/installations" ] || [ ! -d "node_modules" ]; then + echo "please, first run $BASE_DIR/install.sh" + exit 1 +fi + has_custom_riot=$(node has_custom_riot.js $@) +if [ ! -d "riot/riot-web" ] && [ $has_custom_riot -ne "1" ]; then + echo "please provide an instance of riot to test against by passing --riot-url or running $BASE_DIR/riot/install.sh" + exit 1 +fi + stop_servers() { if [ $has_custom_riot -ne "1" ]; then ./riot/stop.sh From 3e971e48800ceffd35d8732c6cb663f1f5f8e0d6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Oct 2019 17:38:18 +0200 Subject: [PATCH 0232/2372] provide yarn command to run e2e tests this assumes the riot-web local dev server is running --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index d2955f89be..2d08417bf9 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ "clean": "rimraf lib", "prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt", "test": "karma start --single-run=true --browsers VectorChromeHeadless", - "test-multi": "karma start" + "test-multi": "karma start", + "e2etests": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080" }, "dependencies": { "babel-plugin-syntax-dynamic-import": "^6.18.0", From bd7e9096995345c205c99ffb901d1e62493fb605 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 9 Oct 2019 16:54:05 +0100 Subject: [PATCH 0233/2372] js-sdk rc.1 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index d2955f89be..86b8f74860 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "lolex": "4.2", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "2.4.2-rc.1", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index ba7fea21ba..1a3dc593d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5187,9 +5187,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc" integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "2.4.1" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e024d047e358cf26caa47542c8f6d9a469a11cb2" +matrix-js-sdk@2.4.2-rc.1: + version "2.4.2-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.2-rc.1.tgz#0601c8020e34b6c0ecde6216f86f56664d7a9357" + integrity sha512-s8Bjvw6EkQQshHXC+aM846FJQdXTUIIyh5s+FRpW08yxA3I38/guF9cpJyCD44gZozXr+8c8VFTJ4Af2rRyV7A== dependencies: another-json "^0.2.0" babel-runtime "^6.26.0" From d86cf434f7293f8c5219fd3a141c0076fd97bf96 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 9 Oct 2019 16:57:14 +0100 Subject: [PATCH 0234/2372] Prepare changelog for v1.7.0-rc.1 --- CHANGELOG.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89faf70d42..adf985c7ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,52 @@ +Changes in [1.7.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.0-rc.1) (2019-10-09) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.6.2...v1.7.0-rc.1) + + * Update from Weblate + [\#3539](https://github.com/matrix-org/matrix-react-sdk/pull/3539) + * React error/warning cleanup + [\#3529](https://github.com/matrix-org/matrix-react-sdk/pull/3529) + * Add label to rageshakes for React soft crashes + [\#3535](https://github.com/matrix-org/matrix-react-sdk/pull/3535) + * Support UI Auth on adding email addresses & phone numbers + [\#3534](https://github.com/matrix-org/matrix-react-sdk/pull/3534) + * Unmount React components before stopping the client + [\#3533](https://github.com/matrix-org/matrix-react-sdk/pull/3533) + * Fix soft crash on room join + [\#3532](https://github.com/matrix-org/matrix-react-sdk/pull/3532) + * Fix: Unable to verify email address error + [\#3528](https://github.com/matrix-org/matrix-react-sdk/pull/3528) + * Fix: submit create room dialog when pressing enter + [\#3509](https://github.com/matrix-org/matrix-react-sdk/pull/3509) + * Allow cyclic objects in console logs + [\#3531](https://github.com/matrix-org/matrix-react-sdk/pull/3531) + * Fix: watch emoticon autoreplace setting + [\#3530](https://github.com/matrix-org/matrix-react-sdk/pull/3530) + * Make "remove recent messages" more robust + [\#3508](https://github.com/matrix-org/matrix-react-sdk/pull/3508) + * Label submit button in UI auth password prompt + [\#3527](https://github.com/matrix-org/matrix-react-sdk/pull/3527) + * Null-guard the recaptcha setup + [\#3526](https://github.com/matrix-org/matrix-react-sdk/pull/3526) + * Use a mask instead of an img for "Show image" eye + [\#3513](https://github.com/matrix-org/matrix-react-sdk/pull/3513) + * Only limit the rageshake log size in one place + [\#3523](https://github.com/matrix-org/matrix-react-sdk/pull/3523) + * Rename UPPER_CAMEL_CASE to UPPER_SNAKE_CASE in Coding Style + [\#3525](https://github.com/matrix-org/matrix-react-sdk/pull/3525) + * Revert "Run yarn upgrade" + [\#3524](https://github.com/matrix-org/matrix-react-sdk/pull/3524) + * Run yarn upgrade + [\#3521](https://github.com/matrix-org/matrix-react-sdk/pull/3521) + * Limit Backspace-consuming workaround to just Slate, tidy Keyboard :) + [\#3522](https://github.com/matrix-org/matrix-react-sdk/pull/3522) + * Enable CIDER composer by default + [\#3519](https://github.com/matrix-org/matrix-react-sdk/pull/3519) + * Update from Weblate + [\#3520](https://github.com/matrix-org/matrix-react-sdk/pull/3520) + * Cull some easily fixable errors which make the console a mess + [\#3516](https://github.com/matrix-org/matrix-react-sdk/pull/3516) + Changes in [1.6.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.6.2) (2019-10-04) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.6.2-rc.1...v1.6.2) From 558a8b72f8ab63d4be698b7b8fee3968b0ba8657 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 9 Oct 2019 16:57:14 +0100 Subject: [PATCH 0235/2372] v1.7.0-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 86b8f74860..f1a137f628 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.6.2", + "version": "1.7.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 2d848bba298873962327ab76407baf38b10eeb52 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Oct 2019 17:51:50 +0200 Subject: [PATCH 0236/2372] fix lint --- test/end-to-end-tests/src/logbuffer.js | 5 ++-- test/end-to-end-tests/src/logger.js | 2 +- test/end-to-end-tests/src/rest/creator.js | 10 ++++---- test/end-to-end-tests/src/rest/multi.js | 17 ++++++-------- test/end-to-end-tests/src/rest/room.js | 8 +++---- test/end-to-end-tests/src/rest/session.js | 23 +++++++++---------- test/end-to-end-tests/src/scenario.js | 2 +- .../src/scenarios/directory.js | 4 ++-- .../src/scenarios/e2e-encryption.js | 8 ++----- .../src/scenarios/lazy-loading.js | 9 ++++---- test/end-to-end-tests/src/session.js | 18 +++++++-------- .../src/usecases/accept-invite.js | 5 +--- .../src/usecases/create-room.js | 2 -- test/end-to-end-tests/src/usecases/dialog.js | 2 +- test/end-to-end-tests/src/usecases/invite.js | 4 +--- test/end-to-end-tests/src/usecases/join.js | 3 +-- .../src/usecases/memberlist.js | 9 ++++---- .../src/usecases/room-settings.js | 2 +- .../src/usecases/send-message.js | 2 +- .../end-to-end-tests/src/usecases/settings.js | 7 +++--- test/end-to-end-tests/src/usecases/signup.js | 6 ++--- .../end-to-end-tests/src/usecases/timeline.js | 16 ++++++------- test/end-to-end-tests/src/usecases/verify.js | 3 ++- test/end-to-end-tests/src/util.js | 4 ++-- test/end-to-end-tests/start.js | 13 +++++------ 25 files changed, 84 insertions(+), 100 deletions(-) diff --git a/test/end-to-end-tests/src/logbuffer.js b/test/end-to-end-tests/src/logbuffer.js index 8bf6285e25..d586dc8b84 100644 --- a/test/end-to-end-tests/src/logbuffer.js +++ b/test/end-to-end-tests/src/logbuffer.js @@ -21,10 +21,9 @@ module.exports = class LogBuffer { const result = eventMapper(arg); if (reduceAsync) { result.then((r) => this.buffer += r); - } - else { + } else { this.buffer += result; } }); } -} +}; diff --git a/test/end-to-end-tests/src/logger.js b/test/end-to-end-tests/src/logger.js index be3ebde75b..283d07f163 100644 --- a/test/end-to-end-tests/src/logger.js +++ b/test/end-to-end-tests/src/logger.js @@ -59,4 +59,4 @@ module.exports = class Logger { this.muted = false; return this; } -} +}; diff --git a/test/end-to-end-tests/src/rest/creator.js b/test/end-to-end-tests/src/rest/creator.js index 31c352b31a..fde54014b2 100644 --- a/test/end-to-end-tests/src/rest/creator.js +++ b/test/end-to-end-tests/src/rest/creator.js @@ -57,13 +57,13 @@ module.exports = class RestSessionCreator { `-u ${username}`, `-p ${password}`, '--no-admin', - this.hsUrl + this.hsUrl, ]; const registerCmd = `./register_new_matrix_user ${registerArgs.join(' ')}`; const allCmds = [ `cd ${this.synapseSubdir}`, ". ./activate", - registerCmd + registerCmd, ].join(' && '); await execAsync(allCmds, {cwd: this.cwd, encoding: 'utf-8'}); @@ -74,9 +74,9 @@ module.exports = class RestSessionCreator { "type": "m.login.password", "identifier": { "type": "m.id.user", - "user": username + "user": username, }, - "password": password + "password": password, }; const url = `${this.hsUrl}/_matrix/client/r0/login`; const responseBody = await request.post({url, json: true, body: requestBody}); @@ -88,4 +88,4 @@ module.exports = class RestSessionCreator { hsUrl: this.hsUrl, }; } -} +}; diff --git a/test/end-to-end-tests/src/rest/multi.js b/test/end-to-end-tests/src/rest/multi.js index b930a27c1e..e58b9f3f57 100644 --- a/test/end-to-end-tests/src/rest/multi.js +++ b/test/end-to-end-tests/src/rest/multi.js @@ -14,9 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -const request = require('request-promise-native'); -const RestRoom = require('./room'); -const {approveConsent} = require('./consent'); const Logger = require('../logger'); module.exports = class RestMultiSession { @@ -31,7 +28,7 @@ module.exports = class RestMultiSession { pop(userName) { const idx = this.sessions.findIndex((s) => s.userName() === userName); - if(idx === -1) { + if (idx === -1) { throw new Error(`user ${userName} not found`); } const session = this.sessions.splice(idx, 1)[0]; @@ -39,7 +36,7 @@ module.exports = class RestMultiSession { } async setDisplayName(fn) { - this.log.step("set their display name") + this.log.step("set their display name"); await Promise.all(this.sessions.map(async (s) => { s.log.mute(); await s.setDisplayName(fn(s)); @@ -49,10 +46,10 @@ module.exports = class RestMultiSession { } async join(roomIdOrAlias) { - this.log.step(`join ${roomIdOrAlias}`) + this.log.step(`join ${roomIdOrAlias}`); const rooms = await Promise.all(this.sessions.map(async (s) => { s.log.mute(); - const room = await s.join(roomIdOrAlias); + const room = await s.join(roomIdOrAlias); s.log.unmute(); return room; })); @@ -64,7 +61,7 @@ module.exports = class RestMultiSession { const rooms = this.sessions.map(s => s.room(roomIdOrAlias)); return new RestMultiRoom(rooms, roomIdOrAlias, this.log); } -} +}; class RestMultiRoom { constructor(rooms, roomIdOrAlias, log) { @@ -74,7 +71,7 @@ class RestMultiRoom { } async talk(message) { - this.log.step(`say "${message}" in ${this.roomIdOrAlias}`) + this.log.step(`say "${message}" in ${this.roomIdOrAlias}`); await Promise.all(this.rooms.map(async (r) => { r.log.mute(); await r.talk(message); @@ -84,7 +81,7 @@ class RestMultiRoom { } async leave() { - this.log.step(`leave ${this.roomIdOrAlias}`) + this.log.step(`leave ${this.roomIdOrAlias}`); await Promise.all(this.rooms.map(async (r) => { r.log.mute(); await r.leave(); diff --git a/test/end-to-end-tests/src/rest/room.js b/test/end-to-end-tests/src/rest/room.js index a7f40af594..429a29c31a 100644 --- a/test/end-to-end-tests/src/rest/room.js +++ b/test/end-to-end-tests/src/rest/room.js @@ -25,18 +25,18 @@ module.exports = class RestRoom { } async talk(message) { - this.log.step(`says "${message}" in ${this._roomId}`) + this.log.step(`says "${message}" in ${this._roomId}`); const txId = uuidv4(); await this.session._put(`/rooms/${this._roomId}/send/m.room.message/${txId}`, { "msgtype": "m.text", - "body": message + "body": message, }); this.log.done(); return txId; } async leave() { - this.log.step(`leaves ${this._roomId}`) + this.log.step(`leaves ${this._roomId}`); await this.session._post(`/rooms/${this._roomId}/leave`); this.log.done(); } @@ -44,4 +44,4 @@ module.exports = class RestRoom { roomId() { return this._roomId; } -} +}; diff --git a/test/end-to-end-tests/src/rest/session.js b/test/end-to-end-tests/src/rest/session.js index de05cd4b5c..2e29903800 100644 --- a/test/end-to-end-tests/src/rest/session.js +++ b/test/end-to-end-tests/src/rest/session.js @@ -43,17 +43,17 @@ module.exports = class RestSession { this.log.step(`sets their display name to ${displayName}`); this._displayName = displayName; await this._put(`/profile/${this._credentials.userId}/displayname`, { - displayname: displayName + displayname: displayName, }); this.log.done(); } async join(roomIdOrAlias) { this.log.step(`joins ${roomIdOrAlias}`); - const {room_id} = await this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`); + const roomId = await this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`).room_id; this.log.done(); - const room = new RestRoom(this, room_id, this.log); - this._rooms[room_id] = room; + const room = new RestRoom(this, roomId, this.log); + this._rooms[roomId] = room; this._rooms[roomIdOrAlias] = room; return room; } @@ -86,9 +86,9 @@ module.exports = class RestSession { body.topic = options.topic; } - const {room_id} = await this._post(`/createRoom`, body); + const roomId = await this._post(`/createRoom`, body).room_id; this.log.done(); - return new RestRoom(this, room_id, this.log); + return new RestRoom(this, roomId, this.log); } _post(csApiPath, body) { @@ -105,23 +105,22 @@ module.exports = class RestSession { url: `${this._credentials.hsUrl}/_matrix/client/r0${csApiPath}`, method, headers: { - "Authorization": `Bearer ${this._credentials.accessToken}` + "Authorization": `Bearer ${this._credentials.accessToken}`, }, json: true, - body + body, }); return responseBody; - - } catch(err) { + } catch (err) { const responseBody = err.response.body; if (responseBody.errcode === 'M_CONSENT_NOT_GIVEN') { await approveConsent(responseBody.consent_uri); return this._request(method, csApiPath, body); - } else if(responseBody && responseBody.error) { + } else if (responseBody && responseBody.error) { throw new Error(`${method} ${csApiPath}: ${responseBody.error}`); } else { throw new Error(`${method} ${csApiPath}: ${err.response.statusCode}`); } } } -} +}; diff --git a/test/end-to-end-tests/src/scenario.js b/test/end-to-end-tests/src/scenario.js index 1ad177a4f5..f575fb392e 100644 --- a/test/end-to-end-tests/src/scenario.js +++ b/test/end-to-end-tests/src/scenario.js @@ -42,7 +42,7 @@ module.exports = async function scenario(createSession, restCreator) { console.log("create REST users:"); const charlies = await createRestUsers(restCreator); await lazyLoadingScenarios(alice, bob, charlies); -} +}; async function createRestUsers(restCreator) { const usernames = range(1, 10).map((i) => `charly-${i}`); diff --git a/test/end-to-end-tests/src/scenarios/directory.js b/test/end-to-end-tests/src/scenarios/directory.js index 582b6867b2..3ae728a5b7 100644 --- a/test/end-to-end-tests/src/scenarios/directory.js +++ b/test/end-to-end-tests/src/scenarios/directory.js @@ -30,7 +30,7 @@ module.exports = async function roomDirectoryScenarios(alice, bob) { const bobMessage = "hi Alice!"; await sendMessage(bob, bobMessage); await receiveMessage(alice, {sender: "bob", body: bobMessage}); - const aliceMessage = "hi Bob, welcome!" + const aliceMessage = "hi Bob, welcome!"; await sendMessage(alice, aliceMessage); await receiveMessage(bob, {sender: "alice", body: aliceMessage}); -} +}; diff --git a/test/end-to-end-tests/src/scenarios/e2e-encryption.js b/test/end-to-end-tests/src/scenarios/e2e-encryption.js index 29b97f2047..8df374bacb 100644 --- a/test/end-to-end-tests/src/scenarios/e2e-encryption.js +++ b/test/end-to-end-tests/src/scenarios/e2e-encryption.js @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ - -const {delay} = require('../util'); -const {acceptDialogMaybe} = require('../usecases/dialog'); -const join = require('../usecases/join'); const sendMessage = require('../usecases/send-message'); const acceptInvite = require('../usecases/accept-invite'); const invite = require('../usecases/invite'); @@ -43,10 +39,10 @@ module.exports = async function e2eEncryptionScenarios(alice, bob) { const [bobSas, aliceSas] = await Promise.all([bobSasPromise, aliceSasPromise]); assert.deepEqual(bobSas, aliceSas); bob.log.done(`done (match for ${bobSas.join(", ")})`); - const aliceMessage = "Guess what I just heard?!" + const aliceMessage = "Guess what I just heard?!"; await sendMessage(alice, aliceMessage); await receiveMessage(bob, {sender: "alice", body: aliceMessage, encrypted: true}); const bobMessage = "You've got to tell me!"; await sendMessage(bob, bobMessage); await receiveMessage(alice, {sender: "bob", body: bobMessage, encrypted: true}); -} +}; diff --git a/test/end-to-end-tests/src/scenarios/lazy-loading.js b/test/end-to-end-tests/src/scenarios/lazy-loading.js index f924f78cf1..be5a91bb71 100644 --- a/test/end-to-end-tests/src/scenarios/lazy-loading.js +++ b/test/end-to-end-tests/src/scenarios/lazy-loading.js @@ -20,12 +20,11 @@ const join = require('../usecases/join'); const sendMessage = require('../usecases/send-message'); const { checkTimelineContains, - scrollToTimelineTop + scrollToTimelineTop, } = require('../usecases/timeline'); const {createRoom} = require('../usecases/create-room'); const {getMembersInMemberlist} = require('../usecases/memberlist'); const changeRoomSettings = require('../usecases/room-settings'); -const {enableLazyLoading} = require('../usecases/settings'); const assert = require('assert'); module.exports = async function lazyLoadingScenarios(alice, bob, charlies) { @@ -43,7 +42,7 @@ module.exports = async function lazyLoadingScenarios(alice, bob, charlies) { await delay(1000); await checkMemberListLacksCharlies(alice, charlies); await checkMemberListLacksCharlies(bob, charlies); -} +}; const room = "Lazy Loading Test"; const alias = "#lltest:localhost"; @@ -60,7 +59,7 @@ async function setupRoomWithBobAliceAndCharlies(alice, bob, charlies) { await charlyMembers.talk(charlyMsg1); await charlyMembers.talk(charlyMsg2); bob.log.step("sends 20 messages").mute(); - for(let i = 20; i >= 1; --i) { + for (let i = 20; i >= 1; --i) { await sendMessage(bob, `I will only say this ${i} time(s)!`); } bob.log.unmute().done(); @@ -112,7 +111,7 @@ async function joinCharliesWhileAliceIsOffline(alice, charly6to10) { const members6to10 = await charly6to10.join(alias); const member6 = members6to10.rooms[0]; member6.log.step("sends 20 messages").mute(); - for(let i = 20; i >= 1; --i) { + for (let i = 20; i >= 1; --i) { await member6.talk("where is charly?"); } member6.log.unmute().done(); diff --git a/test/end-to-end-tests/src/session.js b/test/end-to-end-tests/src/session.js index d3c26c07e4..65cec6fef0 100644 --- a/test/end-to-end-tests/src/session.js +++ b/test/end-to-end-tests/src/session.js @@ -42,7 +42,7 @@ module.exports = class RiotSession { const page = await browser.newPage(); await page.setViewport({ width: 1280, - height: 800 + height: 800, }); if (throttleCpuFactor !== 1) { const client = await page.target().createCDPSession(); @@ -55,8 +55,8 @@ module.exports = class RiotSession { async tryGetInnertext(selector) { const field = await this.page.$(selector); if (field != null) { - const text_handle = await field.getProperty('innerText'); - return await text_handle.jsonValue(); + const textHandle = await field.getProperty('innerText'); + return await textHandle.jsonValue(); } return null; } @@ -70,7 +70,7 @@ module.exports = class RiotSession { return this.getElementProperty(field, 'innerText'); } - getOuterHTML(element_handle) { + getOuterHTML(field) { return this.getElementProperty(field, 'outerHTML'); } @@ -97,12 +97,12 @@ module.exports = class RiotSession { return { logs() { return buffer; - } - } + }, + }; } async printElements(label, elements) { - console.log(label, await Promise.all(elements.map(getOuterHTML))); + console.log(label, await Promise.all(elements.map(this.getOuterHTML))); } async replaceInputText(input, text) { @@ -210,7 +210,7 @@ module.exports = class RiotSession { async poll(callback, interval = 100) { const timeout = DEFAULT_TIMEOUT; let waited = 0; - while(waited < timeout) { + while (waited < timeout) { await this.delay(interval); waited += interval; if (await callback()) { @@ -219,4 +219,4 @@ module.exports = class RiotSession { } return false; } -} +}; diff --git a/test/end-to-end-tests/src/usecases/accept-invite.js b/test/end-to-end-tests/src/usecases/accept-invite.js index bcecf41ed2..085c60aa6a 100644 --- a/test/end-to-end-tests/src/usecases/accept-invite.js +++ b/test/end-to-end-tests/src/usecases/accept-invite.js @@ -14,9 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -const assert = require('assert'); -const {acceptDialogMaybe} = require('./dialog'); - module.exports = async function acceptInvite(session, name) { session.log.step(`accepts "${name}" invite`); //TODO: brittle selector @@ -35,4 +32,4 @@ module.exports = async function acceptInvite(session, name) { await acceptInvitationLink.click(); session.log.done(); -} +}; diff --git a/test/end-to-end-tests/src/usecases/create-room.js b/test/end-to-end-tests/src/usecases/create-room.js index 88547610f0..75abdc78f4 100644 --- a/test/end-to-end-tests/src/usecases/create-room.js +++ b/test/end-to-end-tests/src/usecases/create-room.js @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -const assert = require('assert'); - async function openRoomDirectory(session) { const roomDirectoryButton = await session.query('.mx_LeftPanel_explore .mx_AccessibleButton'); await roomDirectoryButton.click(); diff --git a/test/end-to-end-tests/src/usecases/dialog.js b/test/end-to-end-tests/src/usecases/dialog.js index 58f135de04..7b5d4d09fa 100644 --- a/test/end-to-end-tests/src/usecases/dialog.js +++ b/test/end-to-end-tests/src/usecases/dialog.js @@ -33,7 +33,7 @@ async function acceptDialogMaybe(session, expectedTitle) { let primaryButton = null; try { primaryButton = await session.query(".mx_Dialog .mx_Dialog_primary"); - } catch(err) { + } catch (err) { return false; } if (expectedTitle) { diff --git a/test/end-to-end-tests/src/usecases/invite.js b/test/end-to-end-tests/src/usecases/invite.js index 7b3f8a2b48..d7e02a38d8 100644 --- a/test/end-to-end-tests/src/usecases/invite.js +++ b/test/end-to-end-tests/src/usecases/invite.js @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -const assert = require('assert'); - module.exports = async function invite(session, userId) { session.log.step(`invites "${userId}" to room`); await session.delay(1000); @@ -27,4 +25,4 @@ module.exports = async function invite(session, userId) { const confirmButton = await session.query(".mx_Dialog_primary"); await confirmButton.click(); session.log.done(); -} +}; diff --git a/test/end-to-end-tests/src/usecases/join.js b/test/end-to-end-tests/src/usecases/join.js index 3c14a76143..bc292a0768 100644 --- a/test/end-to-end-tests/src/usecases/join.js +++ b/test/end-to-end-tests/src/usecases/join.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -const assert = require('assert'); const {openRoomDirectory} = require('./create-room'); module.exports = async function join(session, roomName) { @@ -27,4 +26,4 @@ module.exports = async function join(session, roomName) { await joinFirstLink.click(); await session.query('.mx_MessageComposer'); session.log.done(); -} +}; diff --git a/test/end-to-end-tests/src/usecases/memberlist.js b/test/end-to-end-tests/src/usecases/memberlist.js index 5858e82bb8..f6b07b3500 100644 --- a/test/end-to-end-tests/src/usecases/memberlist.js +++ b/test/end-to-end-tests/src/usecases/memberlist.js @@ -22,7 +22,7 @@ async function openMemberInfo(session, name) { return m.displayName === name; }).map((m) => m.label)[0]; await matchingLabel.click(); -}; +} module.exports.openMemberInfo = openMemberInfo; @@ -39,10 +39,11 @@ module.exports.verifyDeviceForUser = async function(session, name, expectedDevic // expect "Verify device" dialog and click "Begin Verification" const dialogHeader = await session.innerText(await session.query(".mx_Dialog .mx_Dialog_title")); assert(dialogHeader, "Verify device"); - const beginVerificationButton = await session.query(".mx_Dialog .mx_Dialog_primary") + const beginVerificationButton = await session.query(".mx_Dialog .mx_Dialog_primary"); await beginVerificationButton.click(); // get emoji SAS labels - const sasLabelElements = await session.queryAll(".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label"); + const sasLabelElements = await session.queryAll( + ".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label"); const sasLabels = await Promise.all(sasLabelElements.map(e => session.innerText(e))); console.log("my sas labels", sasLabels); @@ -58,7 +59,7 @@ module.exports.verifyDeviceForUser = async function(session, name, expectedDevic const closeMemberInfo = await session.query(".mx_MemberInfo_cancel"); await closeMemberInfo.click(); session.log.done(); -} +}; async function getMembersInMemberlist(session) { const memberNameElements = await session.queryAll(".mx_MemberList .mx_EntityTile_name"); diff --git a/test/end-to-end-tests/src/usecases/room-settings.js b/test/end-to-end-tests/src/usecases/room-settings.js index bf3e60d490..7655d2d066 100644 --- a/test/end-to-end-tests/src/usecases/room-settings.js +++ b/test/end-to-end-tests/src/usecases/room-settings.js @@ -96,4 +96,4 @@ module.exports = async function changeRoomSettings(session, settings) { await closeButton.click(); session.log.endGroup(); -} +}; diff --git a/test/end-to-end-tests/src/usecases/send-message.js b/test/end-to-end-tests/src/usecases/send-message.js index 38fe6fceb3..764994420c 100644 --- a/test/end-to-end-tests/src/usecases/send-message.js +++ b/test/end-to-end-tests/src/usecases/send-message.js @@ -31,4 +31,4 @@ module.exports = async function sendMessage(session, message) { // wait for the message to appear sent await session.query(".mx_EventTile_last:not(.mx_EventTile_sending)"); session.log.done(); -} +}; diff --git a/test/end-to-end-tests/src/usecases/settings.js b/test/end-to-end-tests/src/usecases/settings.js index 903524e6b8..ec675157f2 100644 --- a/test/end-to-end-tests/src/usecases/settings.js +++ b/test/end-to-end-tests/src/usecases/settings.js @@ -22,7 +22,8 @@ async function openSettings(session, section) { const settingsItem = await session.query(".mx_TopLeftMenu_icon_settings"); await settingsItem.click(); if (section) { - const sectionButton = await session.query(`.mx_UserSettingsDialog .mx_TabbedView_tabLabels .mx_UserSettingsDialog_${section}Icon`); + const sectionButton = await session.query( + `.mx_UserSettingsDialog .mx_TabbedView_tabLabels .mx_UserSettingsDialog_${section}Icon`); await sectionButton.click(); } } @@ -37,7 +38,7 @@ module.exports.enableLazyLoading = async function(session) { const closeButton = await session.query(".mx_RoomHeader_cancelButton"); await closeButton.click(); session.log.done(); -} +}; module.exports.getE2EDeviceFromSettings = async function(session) { session.log.step(`gets e2e device/key from settings`); @@ -50,4 +51,4 @@ module.exports.getE2EDeviceFromSettings = async function(session) { await closeButton.click(); session.log.done(); return {id, key}; -} +}; diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js index 014d2ff786..391ce76441 100644 --- a/test/end-to-end-tests/src/usecases/signup.js +++ b/test/end-to-end-tests/src/usecases/signup.js @@ -60,8 +60,8 @@ module.exports = async function signup(session, username, password, homeserver) // Password validation is async, wait for it to complete before submit await session.query(".mx_Field_valid #mx_RegistrationForm_password"); //check no errors - const error_text = await session.tryGetInnertext('.mx_Login_error'); - assert.strictEqual(!!error_text, false); + const errorText = await session.tryGetInnertext('.mx_Login_error'); + assert.strictEqual(!!errorText, false); //submit form //await page.screenshot({path: "beforesubmit.png", fullPage: true}); await registerButton.click(); @@ -87,4 +87,4 @@ module.exports = async function signup(session, username, password, homeserver) }); assert(foundHomeUrl); session.log.done(); -} +}; diff --git a/test/end-to-end-tests/src/usecases/timeline.js b/test/end-to-end-tests/src/usecases/timeline.js index 1770e0df9f..3ff9e0f5b4 100644 --- a/test/end-to-end-tests/src/usecases/timeline.js +++ b/test/end-to-end-tests/src/usecases/timeline.js @@ -36,11 +36,11 @@ module.exports.scrollToTimelineTop = async function(session) { } else { await new Promise((resolve) => setTimeout(resolve, 50)); } - } while (!timedOut) + } while (!timedOut); }); - }) + }); session.log.done(); -} +}; module.exports.receiveMessage = async function(session, expectedMessage) { session.log.step(`receives message "${expectedMessage.body}" from ${expectedMessage.sender}`); @@ -56,7 +56,7 @@ module.exports.receiveMessage = async function(session, expectedMessage) { await session.poll(async () => { try { lastMessage = await getLastMessage(); - } catch(err) { + } catch (err) { return false; } // stop polling when found the expected message @@ -66,10 +66,10 @@ module.exports.receiveMessage = async function(session, expectedMessage) { }); assertMessage(lastMessage, expectedMessage); session.log.done(); -} +}; -module.exports.checkTimelineContains = async function (session, expectedMessages, sendersDescription) { +module.exports.checkTimelineContains = async function(session, expectedMessages, sendersDescription) { session.log.step(`checks timeline contains ${expectedMessages.length} ` + `given messages${sendersDescription ? ` from ${sendersDescription}`:""}`); const eventTiles = await getAllEventTiles(session); @@ -94,14 +94,14 @@ module.exports.checkTimelineContains = async function (session, expectedMessages }); try { assertMessage(foundMessage, expectedMessage); - } catch(err) { + } catch (err) { console.log("timelineMessages", timelineMessages); throw err; } }); session.log.done(); -} +}; function assertMessage(foundMessage, expectedMessage) { assert(foundMessage, `message ${JSON.stringify(expectedMessage)} not found in timeline`); diff --git a/test/end-to-end-tests/src/usecases/verify.js b/test/end-to-end-tests/src/usecases/verify.js index 323765bebf..11ff98d097 100644 --- a/test/end-to-end-tests/src/usecases/verify.js +++ b/test/end-to-end-tests/src/usecases/verify.js @@ -31,7 +31,8 @@ async function startVerification(session, name) { } async function getSasCodes(session) { - const sasLabelElements = await session.queryAll(".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label"); + const sasLabelElements = await session.queryAll( + ".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label"); const sasLabels = await Promise.all(sasLabelElements.map(e => session.innerText(e))); return sasLabels; } diff --git a/test/end-to-end-tests/src/util.js b/test/end-to-end-tests/src/util.js index 8080d771be..699b11b5ce 100644 --- a/test/end-to-end-tests/src/util.js +++ b/test/end-to-end-tests/src/util.js @@ -20,8 +20,8 @@ module.exports.range = function(start, amount, step = 1) { r.push(start + (i * step)); } return r; -} +}; module.exports.delay = function(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); -} +}; diff --git a/test/end-to-end-tests/start.js b/test/end-to-end-tests/start.js index d19b232236..83bc186356 100644 --- a/test/end-to-end-tests/start.js +++ b/test/end-to-end-tests/start.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -const assert = require('assert'); const RiotSession = require('./src/session'); const scenario = require('./src/scenario'); const RestSessionCreator = require('./src/rest/creator'); @@ -35,7 +34,7 @@ program const hsUrl = 'http://localhost:5005'; async function runTests() { - let sessions = []; + const sessions = []; const options = { slowMo: program.slowMo ? 20 : undefined, devtools: program.devTools, @@ -54,7 +53,7 @@ async function runTests() { const restCreator = new RestSessionCreator( 'synapse/installations/consent/env/bin', hsUrl, - __dirname + __dirname, ); async function createSession(username) { @@ -66,7 +65,7 @@ async function runTests() { let failure = false; try { await scenario(createSession, restCreator); - } catch(err) { + } catch (err) { failure = true; console.log('failure: ', err); if (program.logDirectory) { @@ -90,15 +89,15 @@ async function runTests() { } async function writeLogs(sessions, dir) { - let logs = ""; - for(let i = 0; i < sessions.length; ++i) { + const logs = ""; + for (let i = 0; i < sessions.length; ++i) { const session = sessions[i]; const userLogDir = `${dir}/${session.username}`; fs.mkdirSync(userLogDir); const consoleLogName = `${userLogDir}/console.log`; const networkLogName = `${userLogDir}/network.log`; const appHtmlName = `${userLogDir}/app.html`; - documentHtml = await session.page.content(); + const documentHtml = await session.page.content(); fs.writeFileSync(appHtmlName, documentHtml); fs.writeFileSync(networkLogName, session.networkLogs()); fs.writeFileSync(consoleLogName, session.consoleLogs()); From 09978e40eb9c3ca12704b78764ac6b4952bee2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sava=20Rado=C5=A1?= Date: Wed, 9 Oct 2019 21:55:50 +0000 Subject: [PATCH 0237/2372] Added translation using Weblate (Serbian (latin)) --- src/i18n/strings/sr_Latn.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/i18n/strings/sr_Latn.json diff --git a/src/i18n/strings/sr_Latn.json b/src/i18n/strings/sr_Latn.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/src/i18n/strings/sr_Latn.json @@ -0,0 +1 @@ +{} From 537bd3700d6565809964d53804b5785ce3f81e79 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 9 Oct 2019 22:27:49 +0100 Subject: [PATCH 0238/2372] SettingsFlag always run ToggleSwitch fully-controlled Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/SettingsFlag.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/views/elements/SettingsFlag.js b/src/components/views/elements/SettingsFlag.js index e4df15a096..f557690514 100644 --- a/src/components/views/elements/SettingsFlag.js +++ b/src/components/views/elements/SettingsFlag.js @@ -62,13 +62,6 @@ module.exports = createReactClass({ }, render: function() { - const value = this.props.manualSave ? this.state.value : SettingsStore.getValueAt( - this.props.level, - this.props.name, - this.props.roomId, - this.props.isExplicit, - ); - const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level); let label = this.props.label; @@ -78,7 +71,7 @@ module.exports = createReactClass({ return (
{label} - +
); }, From c1b591dfa2c13db46160f97ed6d20788c4d19c1f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 9 Oct 2019 22:33:14 +0100 Subject: [PATCH 0239/2372] actually always run fully controlled Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/SettingsFlag.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/SettingsFlag.js b/src/components/views/elements/SettingsFlag.js index f557690514..b4f372073c 100644 --- a/src/components/views/elements/SettingsFlag.js +++ b/src/components/views/elements/SettingsFlag.js @@ -48,7 +48,7 @@ module.exports = createReactClass({ if (this.props.group && !checked) return; if (!this.props.manualSave) this.save(checked); - else this.setState({ value: checked }); + this.setState({ value: checked }); if (this.props.onChange) this.props.onChange(checked); }, From 55f4a0cb441241f0cf867bf0fc3ceb5214ec036e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 9 Oct 2019 23:20:59 +0100 Subject: [PATCH 0240/2372] remove SettingsFlag manualSave altogether Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/SettingsFlag.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/elements/SettingsFlag.js b/src/components/views/elements/SettingsFlag.js index b4f372073c..a3a6d18d33 100644 --- a/src/components/views/elements/SettingsFlag.js +++ b/src/components/views/elements/SettingsFlag.js @@ -30,7 +30,6 @@ module.exports = createReactClass({ label: PropTypes.string, // untranslated onChange: PropTypes.func, isExplicit: PropTypes.bool, - manualSave: PropTypes.bool, }, getInitialState: function() { @@ -47,7 +46,7 @@ module.exports = createReactClass({ onChange: function(checked) { if (this.props.group && !checked) return; - if (!this.props.manualSave) this.save(checked); + this.save(checked); this.setState({ value: checked }); if (this.props.onChange) this.props.onChange(checked); }, From 116425f3466921d04ee3eb204d314029ca1fd9c1 Mon Sep 17 00:00:00 2001 From: Claus Conrad Date: Thu, 10 Oct 2019 05:37:54 +0000 Subject: [PATCH 0241/2372] Translated using Weblate (Danish) Currently translated at 29.6% (542 of 1830 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/da/ --- src/i18n/strings/da.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/da.json b/src/i18n/strings/da.json index 7176964aea..94c64a510b 100644 --- a/src/i18n/strings/da.json +++ b/src/i18n/strings/da.json @@ -586,5 +586,15 @@ "Please contact your homeserver administrator.": "Kontakt venligst din homeserver administrator.", "Failed to join room": "Kunne ikke betræde rummet", "Message Pinning": "Fastgørelse af beskeder", - "Custom user status messages": "Tilpassede bruger-statusbeskeder" + "Custom user status messages": "Tilpassede bruger-statusbeskeder", + "Add Email Address": "Tilføj e-mail adresse", + "Add Phone Number": "Tilføj telefonnummer", + "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s ændret af %(senderName)s", + "Group & filter rooms by custom tags (refresh to apply changes)": "Gruppér og filtrér rum efter egne tags (opdater for at anvende ændringerne)", + "Render simple counters in room header": "Vis simple tællere i rumhovedet", + "Multiple integration managers": "Flere integrationsmanagere", + "Use the new, faster, composer for writing messages": "Brug den nye, hurtigere editor for at forfatte beskeder", + "Enable Emoji suggestions while typing": "Aktiver emoji forslag under indtastning", + "Use compact timeline layout": "Brug kompakt tidslinje", + "Show a placeholder for removed messages": "Vis en pladsholder for fjernede beskeder" } From c37ea98cd0c3db0211a82e65b43922b01021e5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Thu, 10 Oct 2019 07:03:21 +0000 Subject: [PATCH 0242/2372] Translated using Weblate (French) Currently translated at 100.0% (1830 of 1830 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 2a7296b2ae..c53603b978 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2240,5 +2240,7 @@ "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "Vous êtes sur le point de supprimer 1 message de %(user)s. Ça ne peut pas être annulé. Voulez-vous continuer ?", "Remove %(count)s messages|one": "Supprimer 1 message", "Your email address hasn't been verified yet": "Votre adresse e-mail n’a pas encore été vérifiée", - "Click the link in the email you received to verify and then click continue again.": "Cliquez sur le lien dans l’e-mail que vous avez reçu pour la vérifier et cliquez encore sur continuer." + "Click the link in the email you received to verify and then click continue again.": "Cliquez sur le lien dans l’e-mail que vous avez reçu pour la vérifier et cliquez encore sur continuer.", + "Add Email Address": "Ajouter une adresse e-mail", + "Add Phone Number": "Ajouter un numéro de téléphone" } From 103d7fe748f80fcf2e1076542e34055b5570cfbb Mon Sep 17 00:00:00 2001 From: jadiof Date: Wed, 9 Oct 2019 22:53:39 +0000 Subject: [PATCH 0243/2372] Translated using Weblate (German) Currently translated at 84.0% (1537 of 1830 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index da5b65c9f3..fb0809b2b6 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1881,7 +1881,7 @@ "Invited by %(sender)s": "Eingeladen von %(sender)s", "Changes your avatar in all rooms": "Verändert dein Profilbild in allen Räumen", "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "Dieses Gerät speichert deine Schlüssel nicht, aber du hast ein bestehendes Backup, welches du wiederherstellen kannst um fortzufahren.", - "Backup has an invalid signature from this device": "Das Backup hat eine ungültige Signatur von diesem Gerät.", + "Backup has an invalid signature from this device": "Das Backup besitzt eine ungültige Signatur von diesem Gerät", "Failed to start chat": "Chat konnte nicht gestartet werden", "Messages": "Nachrichten", "Actions": "Aktionen", From 6427054d66025c26cd71f99606b0b7ad4aef5c62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Thu, 10 Oct 2019 02:50:44 +0000 Subject: [PATCH 0244/2372] Translated using Weblate (Korean) Currently translated at 100.0% (1830 of 1830 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index bedd8f5244..7f1e5d9edc 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2084,5 +2084,7 @@ "Remove %(count)s messages|one": "1개의 메시지 삭제", "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "홈서버 설정에서 캡챠 공개 키가 없습니다. 홈서버 관리자에게 이것을 신고해주세요.", "Your email address hasn't been verified yet": "이메일 주소가 아직 확인되지 않았습니다", - "Click the link in the email you received to verify and then click continue again.": "받은 이메일에 있는 링크를 클릭해서 확인한 후에 계속하기를 클릭하세요." + "Click the link in the email you received to verify and then click continue again.": "받은 이메일에 있는 링크를 클릭해서 확인한 후에 계속하기를 클릭하세요.", + "Add Email Address": "이메일 주소 추가", + "Add Phone Number": "전화번호 추가" } From 7a3bf5639f1a11a348a9f60d6d3dc0099024108f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sava=20Rado=C5=A1?= Date: Wed, 9 Oct 2019 22:10:15 +0000 Subject: [PATCH 0245/2372] Translated using Weblate (Serbian) Currently translated at 55.7% (1020 of 1830 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sr/ --- src/i18n/strings/sr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/sr.json b/src/i18n/strings/sr.json index 6ad74f823d..7fe775a55e 100644 --- a/src/i18n/strings/sr.json +++ b/src/i18n/strings/sr.json @@ -1210,7 +1210,7 @@ "deleted": "обрисано", "underlined": "подвучено", "You have no historical rooms": "Ваша историја соба је празна", - "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "Такође, можете подесити прилагођени сервер идентитета али у том случају нећете моћи да позивате кориснике преко мејла адресе или да сами будете позвани преко мејл адресе.", + "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "Такође, можете подесити прилагођени сервер идентитета али у том случају нећете моћи да позивате кориснике преко мејл адресе или да сами будете позвани преко мејл адресе.", "Whether or not you're logged in (we don't record your username)": "Да ли сте пријављени (не бележимо ваше корисничко име)", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Датотека „%(fileName)s“ премашује ограничење величине отпремања на овом кућном серверу", "Unable to load! Check your network connectivity and try again.": "Нисам могао да учитам! Проверите вашу мрежну везу и пробајте поново.", From eb3e2e5049a9026cba724aac308b866eace6b358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sava=20Rado=C5=A1?= Date: Wed, 9 Oct 2019 21:58:46 +0000 Subject: [PATCH 0246/2372] Translated using Weblate (Serbian (latin)) Currently translated at 1.2% (22 of 1830 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sr_Latn/ --- src/i18n/strings/sr_Latn.json | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sr_Latn.json b/src/i18n/strings/sr_Latn.json index 0967ef424b..7e8c2a6612 100644 --- a/src/i18n/strings/sr_Latn.json +++ b/src/i18n/strings/sr_Latn.json @@ -1 +1,24 @@ -{} +{ + "This email address is already in use": "Ova adresa elektronske pošte se već koristi", + "This phone number is already in use": "Ovaj broj telefona se već koristi", + "Add Email Address": "Dodajte adresu elektronske pošte", + "Failed to verify email address: make sure you clicked the link in the email": "Neuspela provera adrese elektronske pošte: proverite da li ste kliknuli na link u poruci elektronske pošte", + "Add Phone Number": "Dodajte broj telefona", + "The platform you're on": "Platforma koju koristite", + "The version of Riot.im": "Verzija Riot.im", + "Whether or not you're logged in (we don't record your username)": "Da li ste prijavljeni ili ne (ne beležimo vaše korisničko ime)", + "Your language of choice": "Vaš izbor jezika", + "Which officially provided instance you are using, if any": "Koju zvaničnu instancu koristite, ako koristite", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Da li koristite režim bogatog teksta u uređivaču bogatog teksta", + "Your homeserver's URL": "URL vašeg kućnog servera", + "Your identity server's URL": "URL vašeg servera identiteta", + "e.g. %(exampleValue)s": "npr. %(exampleValue)s", + "Dismiss": "Odbaci", + "Your Riot is misconfigured": "Vaš Riot nije dobro podešen", + "Chat with Riot Bot": "Ćaskajte sa Riot botom", + "Sign In": "Prijavite se", + "powered by Matrix": "pokreće Matriks", + "Custom Server Options": "Prilagođene opcije servera", + "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "Takođe, možete podesiti prilagođeni server identiteta, ali tada nećete moći da pozivate korisnike preko adrese elektronske pošte ili da i sami budete pozvani preko adrese elektronske pošte.", + "Explore rooms": "Istražite sobe" +} From 645a9d836f63a7fb4e558c2db8bf0455ef4aac60 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Oct 2019 11:40:04 +0200 Subject: [PATCH 0247/2372] install static webserver for server symlinked riot on CI --- scripts/ci/end-to-end-tests.sh | 2 ++ test/end-to-end-tests/riot/install.sh | 24 +++++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) mode change 100755 => 100644 test/end-to-end-tests/riot/install.sh diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh index 4968bc4f72..85257420e3 100644 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -34,6 +34,8 @@ ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web # CHROME_PATH=$(which google-chrome-stable) ./run.sh echo "--- Install synapse & other dependencies" ./install.sh +# install (only) static webserver to server symlinked local copy of riot +./riot/install.sh --without-riot mkdir logs echo "+++ Running end-to-end tests" TESTS_STARTED=1 diff --git a/test/end-to-end-tests/riot/install.sh b/test/end-to-end-tests/riot/install.sh old mode 100755 new mode 100644 index 8a942a05ea..9495234bcc --- a/test/end-to-end-tests/riot/install.sh +++ b/test/end-to-end-tests/riot/install.sh @@ -2,6 +2,14 @@ set -e RIOT_BRANCH=develop +with_riot=1 + +for i in $@; do + if [ "$i" == "--without-riot" ] ; then + with_riot=0 + fi +done + BASE_DIR=$(cd $(dirname $0) && pwd) cd $BASE_DIR # Install ComplexHttpServer (a drop in replacement for Python's SimpleHttpServer @@ -26,10 +34,12 @@ if [ -d $BASE_DIR/riot-web ]; then exit fi -curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --output riot.zip -unzip -q riot.zip -rm riot.zip -mv riot-web-${RIOT_BRANCH} riot-web -cd riot-web -yarn install -yarn run build +if [ $with_riot -eq 1 ]; then + curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --output riot.zip + unzip -q riot.zip + rm riot.zip + mv riot-web-${RIOT_BRANCH} riot-web + cd riot-web + yarn install + yarn run build +fi From 6f9604992bff56a0da5ea333d971095e9373fa09 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Oct 2019 11:42:59 +0200 Subject: [PATCH 0248/2372] copyright --- test/end-to-end-tests/has_custom_riot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/has_custom_riot.js b/test/end-to-end-tests/has_custom_riot.js index ad79c8680b..95f32d8ad0 100644 --- a/test/end-to-end-tests/has_custom_riot.js +++ b/test/end-to-end-tests/has_custom_riot.js @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 867739e8bebde2356a3da9cb588b2030a3cd0577 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Oct 2019 11:45:35 +0200 Subject: [PATCH 0249/2372] switch e2e tests to xlarge queue --- .buildkite/pipeline.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.buildkite/pipeline.yaml b/.buildkite/pipeline.yaml index af55fe8cb4..639c7420f0 100644 --- a/.buildkite/pipeline.yaml +++ b/.buildkite/pipeline.yaml @@ -15,9 +15,9 @@ steps: - label: ":chains: End-to-End Tests" agents: - # We use a medium sized instance instead of the normal small ones because + # We use a xlarge sized instance instead of the normal small ones because # e2e tests otherwise take +-8min - queue: "medium" + queue: "xlarge" command: # TODO: Remove hacky chmod for BuildKite - "echo '--- Setup'" @@ -96,7 +96,7 @@ steps: image: "node:10" - wait - + - label: "🐴 Trigger riot-web" trigger: "riot-web" branches: "develop" From 4b9a29cb6003ef70e738a93963a41a46539b975c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Oct 2019 11:55:31 +0200 Subject: [PATCH 0250/2372] put exec perms back on install script --- test/end-to-end-tests/riot/install.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 test/end-to-end-tests/riot/install.sh diff --git a/test/end-to-end-tests/riot/install.sh b/test/end-to-end-tests/riot/install.sh old mode 100644 new mode 100755 From 15bbf3a999cdfb2d2b11d166d0b673d534f970e8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Oct 2019 12:12:13 +0200 Subject: [PATCH 0251/2372] fix the lint fix --- test/end-to-end-tests/src/rest/session.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/end-to-end-tests/src/rest/session.js b/test/end-to-end-tests/src/rest/session.js index 2e29903800..5b97824f5c 100644 --- a/test/end-to-end-tests/src/rest/session.js +++ b/test/end-to-end-tests/src/rest/session.js @@ -50,7 +50,7 @@ module.exports = class RestSession { async join(roomIdOrAlias) { this.log.step(`joins ${roomIdOrAlias}`); - const roomId = await this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`).room_id; + const roomId = (await this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`)).room_id; this.log.done(); const room = new RestRoom(this, roomId, this.log); this._rooms[roomId] = room; @@ -86,7 +86,7 @@ module.exports = class RestSession { body.topic = options.topic; } - const roomId = await this._post(`/createRoom`, body).room_id; + const roomId = (await this._post(`/createRoom`, body)).room_id; this.log.done(); return new RestRoom(this, roomId, this.log); } From d20b2ee9eb182b728b76f99c3c32b9b2ed791082 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Oct 2019 16:54:10 +0200 Subject: [PATCH 0252/2372] document how to run the e2e tests locally --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index e944b04ff2..f4c2e5a5d5 100644 --- a/README.md +++ b/README.md @@ -168,3 +168,8 @@ Ensure you've followed the above development instructions and then: ```bash yarn test ``` + +## End-to-End tests + +Make sure you've got your riot development server running (by doing `yarn start` in riot-web), and then in this project, run `yarn run e2etests`. +See `test/end-to-end-tests/README.md` for more information. From efa5ae0aacc38a0549c1d0290230bd857fd13c80 Mon Sep 17 00:00:00 2001 From: random Date: Thu, 10 Oct 2019 10:42:16 +0000 Subject: [PATCH 0253/2372] Translated using Weblate (Italian) Currently translated at 99.9% (1828 of 1830 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 0b927162ba..57552d2c5a 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2183,5 +2183,9 @@ "Use the new, faster, composer for writing messages": "Usa il compositore nuovo e più veloce per scrivere messaggi", "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "Stai per rimuovere 1 messaggio da %(user)s. Non può essere annullato. Vuoi continuare?", "Remove %(count)s messages|one": "Rimuovi 1 messaggio", - "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Chiave pubblica di Captcha mancante nella configurazione dell'homeserver. Segnalalo all'amministratore dell'homeserver." + "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Chiave pubblica di Captcha mancante nella configurazione dell'homeserver. Segnalalo all'amministratore dell'homeserver.", + "Add Email Address": "Aggiungi indirizzo email", + "Add Phone Number": "Aggiungi numero di telefono", + "Your email address hasn't been verified yet": "Il tuo indirizzo email non è ancora stato verificato", + "Click the link in the email you received to verify and then click continue again.": "Clicca il link nell'email che hai ricevuto per verificare e poi clicca di nuovo Continua." } From 9dd93c52b077c59f719486fdaa7309884bb0c1b5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Oct 2019 16:39:41 +0200 Subject: [PATCH 0254/2372] safeguard if the offsetnode is null when determining caret position --- src/editor/dom.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/editor/dom.js b/src/editor/dom.js index 9073eb37a3..e82c3f70ca 100644 --- a/src/editor/dom.js +++ b/src/editor/dom.js @@ -92,6 +92,10 @@ function getSelectionOffsetAndText(editor, selectionNode, selectionOffset) { // gets the caret position details, ignoring and adjusting to // the ZWS if you're typing in a caret node function getCaret(node, offsetToNode, offsetWithinNode) { + // if no node is selected, return an offset at the start + if (!node) { + return new DocumentOffset(0, false); + } let atNodeEnd = offsetWithinNode === node.textContent.length; if (node.nodeType === Node.TEXT_NODE && isCaretNode(node.parentElement)) { const zwsIdx = node.nodeValue.indexOf(CARET_NODE_CHAR); From 004c3900a901a38756ea6a916ec53d1471d23c4f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Oct 2019 16:40:03 +0200 Subject: [PATCH 0255/2372] don't persist caret when selection is missing so caret will be put back at end of editor when remounting --- src/components/views/rooms/EditMessageComposer.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 3430e793ac..aec35a74ad 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -209,9 +209,18 @@ export default class EditMessageComposer extends React.Component { } componentWillUnmount() { + // store caret and serialized parts in the + // editorstate so it can be restored when the remote echo event tile gets rendered + // in case we're currently editing a pending event const sel = document.getSelection(); - const {caret} = getCaretOffsetAndText(this._editorRef, sel); + let caret; + if (sel.focusNode) { + caret = getCaretOffsetAndText(this._editorRef, sel).caret; + } const parts = this.model.serializeParts(); + // if caret is undefined because for some reason there isn't a valid selection, + // then when mounting the editor again with the same editor state, + // it will set the cursor at the end. this.props.editState.setEditorState(caret, parts); } @@ -238,7 +247,7 @@ export default class EditMessageComposer extends React.Component { _getInitialCaretPosition() { const {editState} = this.props; let caretPosition; - if (editState.hasEditorState()) { + if (editState.hasEditorState() && editState.getCaret()) { // if restoring state from a previous editor, // restore caret position from the state const caret = editState.getCaret(); From c663a57dff27f6c41ae45fa24ea4e491edffcb8b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 10 Oct 2019 17:36:22 +0100 Subject: [PATCH 0256/2372] Add some type checking on event body --- src/HtmlUtils.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 7a212b2497..2266522bfe 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -412,11 +412,13 @@ export function bodyToHtml(content, highlights, opts={}) { }; } - let formattedBody = content.formatted_body; - if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody); - strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body; + let formattedBody = typeof content.formatted_body === 'string' ? content.formatted_body : null; + const plainBody = typeof content.body === 'string' ? content.body : null; - bodyHasEmoji = mightContainEmoji(isHtmlMessage ? formattedBody : content.body); + if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody); + strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(plainBody) : plainBody; + + bodyHasEmoji = mightContainEmoji(isHtmlMessage ? formattedBody : plainBody); // Only generate safeBody if the message was sent as org.matrix.custom.html if (isHtmlMessage) { From df01af0f66538c058697edf6f0d39a9ee6989cf5 Mon Sep 17 00:00:00 2001 From: Kaa Jii Date: Thu, 10 Oct 2019 20:46:45 +0000 Subject: [PATCH 0257/2372] Translated using Weblate (Italian) Currently translated at 100.0% (1830 of 1830 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 57552d2c5a..7c998744e9 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2187,5 +2187,7 @@ "Add Email Address": "Aggiungi indirizzo email", "Add Phone Number": "Aggiungi numero di telefono", "Your email address hasn't been verified yet": "Il tuo indirizzo email non è ancora stato verificato", - "Click the link in the email you received to verify and then click continue again.": "Clicca il link nell'email che hai ricevuto per verificare e poi clicca di nuovo Continua." + "Click the link in the email you received to verify and then click continue again.": "Clicca il link nell'email che hai ricevuto per verificare e poi clicca di nuovo Continua.", + "Read Marker lifetime (ms)": "Durata delle conferme di lettura (ms)", + "Read Marker off-screen lifetime (ms)": "Durata della conferma di lettura off-screen (ms)" } From d7631ed9f8f08c912d5e970824bf973aa9b6bff1 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 11 Oct 2019 15:52:15 +0100 Subject: [PATCH 0258/2372] Catch errors in Settings when IS is unreachable A few bits of Settings try to talk to the IS when Settings is opened. This changes them to handle failure by logging warnings to the console. --- .../tabs/user/GeneralUserSettingsTab.js | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 64aafe6046..78961ad663 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -102,7 +102,17 @@ export default class GeneralUserSettingsTab extends React.Component { // Need to get 3PIDs generally for Account section and possibly also for // Discovery (assuming we have an IS and terms are agreed). - const threepids = await getThreepidsWithBindStatus(cli); + let threepids = []; + try { + threepids = await getThreepidsWithBindStatus(cli); + } catch (e) { + const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl(); + console.warn( + `Unable to reach identity server at ${idServerUrl} to check ` + + `for 3PIDs bindings in Settings`, + ); + console.warn(e); + } this.setState({ emails: threepids.filter((a) => a.medium === 'email') }); this.setState({ msisdns: threepids.filter((a) => a.medium === 'msisdn') }); } @@ -115,32 +125,40 @@ export default class GeneralUserSettingsTab extends React.Component { // By starting the terms flow we get the logic for checking which terms the user has signed // for free. So we might as well use that for our own purposes. + const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl(); const authClient = new IdentityAuthClient(); const idAccessToken = await authClient.getAccessToken({ check: false }); - startTermsFlow([new Service( - SERVICE_TYPES.IS, - MatrixClientPeg.get().getIdentityServerUrl(), - idAccessToken, - )], (policiesAndServices, agreedUrls, extraClassNames) => { - return new Promise((resolve, reject) => { - this.setState({ - idServerName: abbreviateUrl(MatrixClientPeg.get().getIdentityServerUrl()), - requiredPolicyInfo: { - hasTerms: true, - policiesAndServices, - agreedUrls, - resolve, - }, - }); + try { + await startTermsFlow([new Service( + SERVICE_TYPES.IS, + idServerUrl, + idAccessToken, + )], (policiesAndServices, agreedUrls, extraClassNames) => { + return new Promise((resolve, reject) => { + this.setState({ + idServerName: abbreviateUrl(idServerUrl), + requiredPolicyInfo: { + hasTerms: true, + policiesAndServices, + agreedUrls, + resolve, + }, + }); + }); }); - }).then(() => { // User accepted all terms this.setState({ requiredPolicyInfo: { hasTerms: false, }, }); - }); + } catch (e) { + console.warn( + `Unable to reach identity server at ${idServerUrl} to check ` + + `for terms in Settings`, + ); + console.warn(e); + } } _onLanguageChange = (newLanguage) => { From 03d1454ee76657968f49f45aca310eec11ae25d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Fri, 11 Oct 2019 14:27:44 +0000 Subject: [PATCH 0259/2372] Translated using Weblate (French) Currently translated at 100.0% (1831 of 1831 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index c53603b978..4080238d8c 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2242,5 +2242,6 @@ "Your email address hasn't been verified yet": "Votre adresse e-mail n’a pas encore été vérifiée", "Click the link in the email you received to verify and then click continue again.": "Cliquez sur le lien dans l’e-mail que vous avez reçu pour la vérifier et cliquez encore sur continuer.", "Add Email Address": "Ajouter une adresse e-mail", - "Add Phone Number": "Ajouter un numéro de téléphone" + "Add Phone Number": "Ajouter un numéro de téléphone", + "%(creator)s created and configured the room.": "%(creator)s a créé et configuré le salon." } From cb11182e2049e637e96d4e3e3bcdf5a7db42a36b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Fri, 11 Oct 2019 09:55:53 +0000 Subject: [PATCH 0260/2372] Translated using Weblate (Korean) Currently translated at 100.0% (1831 of 1831 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 7f1e5d9edc..5bd1f69bb6 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2086,5 +2086,6 @@ "Your email address hasn't been verified yet": "이메일 주소가 아직 확인되지 않았습니다", "Click the link in the email you received to verify and then click continue again.": "받은 이메일에 있는 링크를 클릭해서 확인한 후에 계속하기를 클릭하세요.", "Add Email Address": "이메일 주소 추가", - "Add Phone Number": "전화번호 추가" + "Add Phone Number": "전화번호 추가", + "%(creator)s created and configured the room.": "%(creator)s님이 방을 만드고 설정했습니다." } From b8a3ee1841f3b06a5c97016d4b75a03ab1456ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 11 Oct 2019 16:07:59 +0200 Subject: [PATCH 0261/2372] BasePlatform: Add prototype methods for event indexing. --- src/BasePlatform.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/BasePlatform.js b/src/BasePlatform.js index a97c14bf90..7f5df822e4 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -151,4 +151,44 @@ export default class BasePlatform { async setMinimizeToTrayEnabled(enabled: boolean): void { throw new Error("Unimplemented"); } + + supportsEventIndexing(): boolean { + return false; + } + + async initEventIndex(userId: string): boolean { + throw new Error("Unimplemented"); + } + + async addEventToIndex(ev: {}, profile: {}): void { + throw new Error("Unimplemented"); + } + + indexIsEmpty(): Promise { + throw new Error("Unimplemented"); + } + + async commitLiveEvents(): void { + throw new Error("Unimplemented"); + } + + async searchEventIndex(term: string): Promise<{}> { + throw new Error("Unimplemented"); + } + + async addHistoricEvents(events: [], checkpoint: {} = null, oldCheckpoint: {} = null): Promise { + throw new Error("Unimplemented"); + } + + async addCrawlerCheckpoint(checkpoint: {}): Promise<> { + throw new Error("Unimplemented"); + } + + async removeCrawlerCheckpoint(checkpoint: {}): Promise<> { + throw new Error("Unimplemented"); + } + + async deleteEventIndex(): Promise<> { + throw new Error("Unimplemented"); + } } From 9ce478cb0e29fca8bf0815c7f7a13ef29fe573fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 11 Oct 2019 16:43:53 +0200 Subject: [PATCH 0262/2372] MatrixChat: Create an event index and start crawling for events. This patch adds support to create an event index if the clients platform supports it and starts an event crawler. The event crawler goes through the room history of encrypted rooms and eventually indexes the whole room history of such rooms. It does this by first creating crawling checkpoints and storing them inside a database. A checkpoint consists of a room_id, direction and token. After the checkpoints are added the client starts a crawler method in the background. The crawler goes through checkpoints in a round-robin way and uses them to fetch historic room messages using the rooms/roomId/messages API endpoint. Every time messages are fetched a new checkpoint is created that will be stored in the database with the fetched events in an atomic way, the old checkpoint is deleted at the same time as well. --- src/MatrixClientPeg.js | 4 + src/components/structures/MatrixChat.js | 231 ++++++++++++++++++++++++ 2 files changed, 235 insertions(+) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index bebb254afc..5c5ee6e4ec 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -30,6 +30,7 @@ import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; +import PlatformPeg from "./PlatformPeg"; interface MatrixClientCreds { homeserverUrl: string, @@ -222,6 +223,9 @@ class MatrixClientPeg { this.matrixClient = createMatrixClient(opts); + const platform = PlatformPeg.get(); + if (platform.supportsEventIndexing()) platform.initEventIndex(creds.userId); + // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. this.matrixClient.setMaxListeners(500); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index da67416400..218b7e4d4e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1262,6 +1262,7 @@ export default createReactClass({ // to do the first sync this.firstSyncComplete = false; this.firstSyncPromise = Promise.defer(); + this.crawlerChekpoints = []; const cli = MatrixClientPeg.get(); const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); @@ -1287,6 +1288,75 @@ export default createReactClass({ return self._loggedInView.child.canResetTimelineInRoom(roomId); }); + cli.on('sync', async (state, prevState, data) => { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + if (prevState === null && state === "PREPARED") { + /// Load our stored checkpoints, if any. + self.crawlerChekpoints = await platform.loadCheckpoints(); + console.log("Seshat: Loaded checkpoints", + self.crawlerChekpoints); + return; + } + + if (prevState === "PREPARED" && state === "SYNCING") { + const addInitialCheckpoints = async () => { + const client = MatrixClientPeg.get(); + const rooms = client.getRooms(); + + const isRoomEncrypted = (room) => { + return client.isRoomEncrypted(room.roomId); + }; + + // We only care to crawl the encrypted rooms, non-encrytped + // rooms can use the search provided by the Homeserver. + const encryptedRooms = rooms.filter(isRoomEncrypted); + + console.log("Seshat: Adding initial crawler checkpoints"); + + // Gather the prev_batch tokens and create checkpoints for + // our message crawler. + await Promise.all(encryptedRooms.map(async (room) => { + const timeline = room.getLiveTimeline(); + const token = timeline.getPaginationToken("b"); + + console.log("Seshat: Got token for indexer", + room.roomId, token); + + const backCheckpoint = { + roomId: room.roomId, + token: token, + direction: "b", + }; + + const forwardCheckpoint = { + roomId: room.roomId, + token: token, + direction: "f", + }; + + await platform.addCrawlerCheckpoint(backCheckpoint); + await platform.addCrawlerCheckpoint(forwardCheckpoint); + self.crawlerChekpoints.push(backCheckpoint); + self.crawlerChekpoints.push(forwardCheckpoint); + })); + }; + + // If our indexer is empty we're most likely running Riot the + // first time with indexing support or running it with an + // initial sync. Add checkpoints to crawl our encrypted rooms. + const eventIndexWasEmpty = await platform.isEventIndexEmpty(); + if (eventIndexWasEmpty) await addInitialCheckpoints(); + + // Start our crawler. + const crawlerHandle = {}; + self.crawlerFunc(crawlerHandle); + self.crawlerRef = crawlerHandle; + return; + } + }); + cli.on('sync', function(state, prevState, data) { // LifecycleStore and others cannot directly subscribe to matrix client for // events because flux only allows store state changes during flux dispatches. @@ -1930,4 +2000,165 @@ export default createReactClass({ {view} ; }, + + async crawlerFunc(handle) { + // TODO either put this in a better place or find a library provided + // method that does this. + const sleep = async (ms) => { + return new Promise(resolve => setTimeout(resolve, ms)); + }; + + let cancelled = false; + + console.log("Seshat: Started crawler function"); + + const client = MatrixClientPeg.get(); + const platform = PlatformPeg.get(); + + handle.cancel = () => { + cancelled = true; + }; + + while (!cancelled) { + // This is a low priority task and we don't want to spam our + // Homeserver with /messages requests so we set a hefty 3s timeout + // here. + await sleep(3000); + + if (cancelled) { + break; + } + + const checkpoint = this.crawlerChekpoints.shift(); + + /// There is no checkpoint available currently, one may appear if + // a sync with limited room timelines happens, so go back to sleep. + if (checkpoint === undefined) { + continue; + } + + console.log("Seshat: crawling using checkpoint", checkpoint); + + // We have a checkpoint, let us fetch some messages, again, very + // conservatively to not bother our Homeserver too much. + const eventMapper = client.getEventMapper(); + // TODO we need to ensure to use member lazy loading with this + // request so we get the correct profiles. + const res = await client._createMessagesRequest(checkpoint.roomId, + checkpoint.token, 100, checkpoint.direction); + + if (res.chunk.length === 0) { + // We got to the start/end of our timeline, lets just + // delete our checkpoint and go back to sleep. + await platform.removeCrawlerCheckpoint(checkpoint); + continue; + } + + // Convert the plain JSON events into Matrix events so they get + // decrypted if necessary. + const matrixEvents = res.chunk.map(eventMapper); + const stateEvents = res.state.map(eventMapper); + + const profiles = {}; + + stateEvents.forEach(ev => { + if (ev.event.content && + ev.event.content.membership === "join") { + profiles[ev.event.sender] = { + displayname: ev.event.content.displayname, + avatar_url: ev.event.content.avatar_url, + }; + } + }); + + const decryptionPromises = []; + + matrixEvents.forEach(ev => { + if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { + // TODO the decryption promise is a private property, this + // should either be made public or we should convert the + // event that gets fired when decryption is done into a + // promise using the once event emitter method: + // https://nodejs.org/api/events.html#events_events_once_emitter_name + decryptionPromises.push(ev._decryptionPromise); + } + }); + + // Let us wait for all the events to get decrypted. + await Promise.all(decryptionPromises); + + // We filter out events for which decryption failed, are redacted + // or aren't of a type that we know how to index. + const isValidEvent = (value) => { + return ([ + "m.room.message", + "m.room.name", + "m.room.topic", + ].indexOf(value.getType()) >= 0 + && !value.isRedacted() && !value.isDecryptionFailure() + ); + // TODO do we need to check if the event has all the valid + // attributes? + }; + + // TODO if there ar no events at this point we're missing a lot + // decryption keys, do we wan't to retry this checkpoint at a later + // stage? + const filteredEvents = matrixEvents.filter(isValidEvent); + + // Let us convert the events back into a format that Seshat can + // consume. + const events = filteredEvents.map((ev) => { + const jsonEvent = ev.toJSON(); + + let e; + if (ev.isEncrypted()) e = jsonEvent.decrypted; + else e = jsonEvent; + + let profile = {}; + if (e.sender in profiles) profile = profiles[e.sender]; + const object = { + event: e, + profile: profile, + }; + return object; + }); + + // Create a new checkpoint so we can continue crawling the room for + // messages. + const newCheckpoint = { + roomId: checkpoint.roomId, + token: res.end, + fullCrawl: checkpoint.fullCrawl, + direction: checkpoint.direction, + }; + + console.log( + "Seshat: Crawled room", + client.getRoom(checkpoint.roomId).name, + "and fetched", events.length, "events.", + ); + + try { + const eventsAlreadyAdded = await platform.addHistoricEvents( + events, newCheckpoint, checkpoint); + // If all events were already indexed we assume that we catched + // up with our index and don't need to crawl the room further. + // Let us delete the checkpoint in that case, otherwise push + // the new checkpoint to be used by the crawler. + if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { + await platform.removeCrawlerCheckpoint(newCheckpoint); + } else { + this.crawlerChekpoints.push(newCheckpoint); + } + } catch (e) { + console.log("Seshat: Error durring a crawl", e); + // An error occured, put the checkpoint back so we + // can retry. + this.crawlerChekpoints.push(checkpoint); + } + } + + console.log("Seshat: Stopping crawler function"); + }, }); From b23ba5f8811488c16412b6ebe2d141f1b9e18f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 11 Oct 2019 16:27:01 +0200 Subject: [PATCH 0263/2372] MatrixChat: Stop the crawler function and delete the index when logging out. --- src/components/structures/MatrixChat.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 218b7e4d4e..7eda69ad9b 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1221,7 +1221,15 @@ export default createReactClass({ /** * Called when the session is logged out */ - _onLoggedOut: function() { + _onLoggedOut: async function() { + const platform = PlatformPeg.get(); + + if (platform.supportsEventIndexing()) { + console.log("Seshat: Deleting event index."); + this.crawlerRef.cancel(); + await platform.deleteEventIndex(); + } + this.notifyNewScreen('login'); this.setStateForNewView({ view: VIEWS.LOGIN, From 5e7076e985fd95a7978099322457823f96daff8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 11 Oct 2019 16:28:36 +0200 Subject: [PATCH 0264/2372] MatrixChat: Add live events to the event index as well. --- src/components/structures/MatrixChat.js | 65 +++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 7eda69ad9b..5c4db4a562 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1271,6 +1271,7 @@ export default createReactClass({ this.firstSyncComplete = false; this.firstSyncPromise = Promise.defer(); this.crawlerChekpoints = []; + this.liveEventsForIndex = new Set(); const cli = MatrixClientPeg.get(); const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); @@ -1363,6 +1364,14 @@ export default createReactClass({ self.crawlerRef = crawlerHandle; return; } + + if (prevState === "SYNCING" && state === "SYNCING") { + // A sync was done, presumably we queued up some live events, + // commit them now. + console.log("Seshat: Committing events"); + await platform.commitLiveEvents(); + return; + } }); cli.on('sync', function(state, prevState, data) { @@ -1447,6 +1456,44 @@ export default createReactClass({ }, null, true); }); + cli.on("Room.timeline", async (ev, room, toStartOfTimeline, removed, data) => { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + // We only index encrypted rooms locally. + if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; + + // If it isn't a live event or if it's redacted there's nothing to + // do. + if (toStartOfTimeline || !data || !data.liveEvent + || ev.isRedacted()) { + return; + } + + // If the event is not yet decrypted mark it for the + // Event.decrypted callback. + if (ev.isBeingDecrypted()) { + const eventId = ev.getId(); + self.liveEventsForIndex.add(eventId); + } else { + // If the event is decrypted or is unencrypted add it to the + // index now. + await self.addLiveEventToIndex(ev); + } + }); + + cli.on("Event.decrypted", async (ev, err) => { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + const eventId = ev.getId(); + + // If the event isn't in our live event set, ignore it. + if (!self.liveEventsForIndex.delete(eventId)) return; + if (err) return; + await self.addLiveEventToIndex(ev); + }); + cli.on("accountData", function(ev) { if (ev.getType() === 'im.vector.web.settings') { if (ev.getContent() && ev.getContent().theme) { @@ -2009,6 +2056,24 @@ export default createReactClass({ ; }, + async addLiveEventToIndex(ev) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + if (["m.room.message", "m.room.name", "m.room.topic"] + .indexOf(ev.getType()) == -1) { + return; + } + + const e = ev.toJSON().decrypted; + const profile = { + displayname: ev.sender.rawDisplayName, + avatar_url: ev.sender.getMxcAvatarUrl(), + }; + + platform.addEventToIndex(e, profile); + }, + async crawlerFunc(handle) { // TODO either put this in a better place or find a library provided // method that does this. From 4acec19d40ba57f789f4c1293ebeb6774babc6ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 11 Oct 2019 16:32:55 +0200 Subject: [PATCH 0265/2372] MatrixChat: Add new crawler checkpoints if there was a limited timeline. A sync call may not have all events that happened since the last time the client synced. In such a case the room is marked as limited and events need to be fetched separately. When such a sync call happens our event index will have a gap. To close the gap checkpoints are added to start crawling our room again. Unnecessary full re-crawls are prevented by checking if our current /room/roomId/messages request contains only events that were already present in our event index. --- src/components/structures/MatrixChat.js | 42 ++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 5c4db4a562..d423bbd592 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1281,8 +1281,11 @@ export default createReactClass({ // particularly noticeable when there are lots of 'limited' /sync responses // such as when laptops unsleep. // https://github.com/vector-im/riot-web/issues/3307#issuecomment-282895568 - cli.setCanResetTimelineCallback(function(roomId) { + cli.setCanResetTimelineCallback(async function(roomId) { console.log("Request to reset timeline in room ", roomId, " viewing:", self.state.currentRoomId); + // TODO is there a better place to plug this in + await self.addCheckpointForLimitedRoom(roomId); + if (roomId !== self.state.currentRoomId) { // It is safe to remove events from rooms we are not viewing. return true; @@ -2234,4 +2237,41 @@ export default createReactClass({ console.log("Seshat: Stopping crawler function"); }, + + async addCheckpointForLimitedRoom(roomId) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + if (!MatrixClientPeg.get().isRoomEncrypted(roomId)) return; + + const client = MatrixClientPeg.get(); + const room = client.getRoom(roomId); + + if (room === null) return; + + const timeline = room.getLiveTimeline(); + const token = timeline.getPaginationToken("b"); + + const backwardsCheckpoint = { + roomId: room.roomId, + token: token, + fullCrawl: false, + direction: "b", + }; + + const forwardsCheckpoint = { + roomId: room.roomId, + token: token, + fullCrawl: false, + direction: "f", + }; + + console.log("Seshat: Added checkpoint because of a limited timeline", + backwardsCheckpoint, forwardsCheckpoint); + + await platform.addCrawlerCheckpoint(backwardsCheckpoint); + await platform.addCrawlerCheckpoint(forwardsCheckpoint); + + this.crawlerChekpoints.push(backwardsCheckpoint); + this.crawlerChekpoints.push(forwardsCheckpoint); + }, }); From 3f5369183404be057af45fa248556572804727b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 11 Oct 2019 16:40:10 +0200 Subject: [PATCH 0266/2372] RoomView: Use platform specific search if our platform supports it. This patch extends our search to include our platform specific event index. There are 3 search scenarios and are handled differently when platform support for indexing is present: - Search a single non-encrypted room: Use the server-side search like before. - Search a single encrypted room: Search using our platform specific event index. - Search across all rooms: Search encrypted rooms using our local event index. Search non-encrypted rooms using the classic server-side search. Combine the results. The combined search will result in having twice the amount of search results since comparing the scores fairly wasn't deemed sensible. --- src/components/structures/RoomView.js | 115 ++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 5 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4de573479d..1b44335f51 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -34,6 +34,7 @@ import { _t } from '../../languageHandler'; import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks'; import MatrixClientPeg from '../../MatrixClientPeg'; +import PlatformPeg from "../../PlatformPeg"; import ContentMessages from '../../ContentMessages'; import Modal from '../../Modal'; import sdk from '../../index'; @@ -1140,12 +1141,116 @@ module.exports = createReactClass({ } debuglog("sending search request"); + const platform = PlatformPeg.get(); - const searchPromise = MatrixClientPeg.get().searchRoomEvents({ - filter: filter, - term: term, - }); - this._handleSearchResult(searchPromise).done(); + if (platform.supportsEventIndexing()) { + const combinedSearchFunc = async (searchTerm) => { + // Create two promises, one for the local search, one for the + // server-side search. + const client = MatrixClientPeg.get(); + const serverSidePromise = client.searchRoomEvents({ + term: searchTerm, + }); + const localPromise = localSearchFunc(searchTerm); + + // Wait for both promises to resolve. + await Promise.all([serverSidePromise, localPromise]); + + // Get both search results. + const localResult = await localPromise; + const serverSideResult = await serverSidePromise; + + // Combine the search results into one result. + const result = {}; + + // Our localResult and serverSideResult are both ordered by + // recency separetly, when we combine them the order might not + // be the right one so we need to sort them. + const compare = (a, b) => { + const aEvent = a.context.getEvent().event; + const bEvent = b.context.getEvent().event; + + if (aEvent.origin_server_ts > + bEvent.origin_server_ts) return -1; + if (aEvent.origin_server_ts < + bEvent.origin_server_ts) return 1; + return 0; + }; + + result.count = localResult.count + serverSideResult.count; + result.results = localResult.results.concat( + serverSideResult.results).sort(compare); + result.highlights = localResult.highlights.concat( + serverSideResult.highlights); + + return result; + }; + + const localSearchFunc = async (searchTerm, roomId = undefined) => { + const searchArgs = { + search_term: searchTerm, + before_limit: 1, + after_limit: 1, + order_by_recency: true, + }; + + if (roomId !== undefined) { + searchArgs.room_id = roomId; + } + + const localResult = await platform.searchEventIndex( + searchArgs); + + const response = { + search_categories: { + room_events: localResult, + }, + }; + + const emptyResult = { + results: [], + highlights: [], + }; + + // TODO is there a better way to convert our result into what + // is expected by the handler method. + const result = MatrixClientPeg.get()._processRoomEventsSearch( + emptyResult, response); + + return result; + }; + + let searchPromise; + + if (scope === "Room") { + const roomId = this.state.room.roomId; + + if (MatrixClientPeg.get().isRoomEncrypted(roomId)) { + // The search is for a single encrypted room, use our local + // search method. + searchPromise = localSearchFunc(term, roomId); + } else { + // The search is for a single non-encrypted room, use the + // server-side search. + searchPromise = MatrixClientPeg.get().searchRoomEvents({ + filter: filter, + term: term, + }); + } + } else { + // Search across all rooms, combine a server side search and a + // local search. + searchPromise = combinedSearchFunc(term); + } + + this._handleSearchResult(searchPromise).done(); + } else { + const searchPromise = MatrixClientPeg.get().searchRoomEvents({ + filter: filter, + term: term, + }); + this._handleSearchResult(searchPromise).done(); + } }, _handleSearchResult: function(searchPromise) { From e6a81c5733a901adce38c2da4cd98ca00db6b7ac Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 11 Oct 2019 15:57:12 +0100 Subject: [PATCH 0267/2372] Show warning dialog when changing unreachable IS If the IS is unreachable, this handles the error by showing a warning encouraging the user to check after their personal data and resolve the situation, but still allows them to continue if they want. Fixes https://github.com/vector-im/riot-web/issues/10909 --- src/components/views/settings/SetIdServer.js | 49 +++++++++++++++++--- src/i18n/strings/en_EN.json | 7 ++- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 9ef5fb295e..8359c09d87 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -249,20 +249,55 @@ export default class SetIdServer extends React.Component { }; async _showServerChangeWarning({ title, unboundMessage, button }) { - const threepids = await getThreepidsWithBindStatus(MatrixClientPeg.get()); + const { currentClientIdServer } = this.state; + + let threepids = []; + let currentServerReachable = true; + try { + threepids = await getThreepidsWithBindStatus(MatrixClientPeg.get()); + } catch (e) { + currentServerReachable = false; + console.warn( + `Unable to reach identity server at ${currentClientIdServer} to check ` + + `for 3PIDs during IS change flow`, + ); + console.warn(e); + } const boundThreepids = threepids.filter(tp => tp.bound); let message; let danger = false; - if (boundThreepids.length) { + const messageElements = { + idserver: sub => {abbreviateUrl(currentClientIdServer)}, + b: sub => {sub}, + }; + if (!currentServerReachable) { + message =
+

{_t( + "You should remove your personal data from identity server " + + " before disconnecting. Unfortunately, identity server " + + " is currently offline or cannot be reached.", + {}, messageElements, + )}

+

{_t("You should:")}

+
    +
  • {_t( + "check your browser plugins for anything that might block " + + "the identity server (such as Privacy Badger)", + )}
  • +
  • {_t("contact the administrators of identity server ", {}, { + idserver: messageElements.idserver, + })}
  • +
  • {_t("wait and try again later")}
  • +
+
; + danger = true; + button = _t("Disconnect anyway"); + } else if (boundThreepids.length) { message =

{_t( "You are still sharing your personal data on the identity " + - "server .", {}, - { - idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, - b: sub => {sub}, - }, + "server .", {}, messageElements, )}

{_t( "We recommend that you remove your email addresses and phone numbers " + diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f524a22d4b..2b7ef28ace 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -568,9 +568,14 @@ "Disconnect identity server": "Disconnect identity server", "Disconnect from the identity server ?": "Disconnect from the identity server ?", "Disconnect": "Disconnect", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.", + "You should:": "You should:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "check your browser plugins for anything that might block the identity server (such as Privacy Badger)", + "contact the administrators of identity server ": "contact the administrators of identity server ", + "wait and try again later": "wait and try again later", + "Disconnect anyway": "Disconnect anyway", "You are still sharing your personal data on the identity server .": "You are still sharing your personal data on the identity server .", "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.", - "Disconnect anyway": "Disconnect anyway", "Go back": "Go back", "Identity Server (%(server)s)": "Identity Server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", From 2555fcb38f6d040ffa7a87bf21df70ba5471872f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 13 Oct 2019 13:28:20 +0300 Subject: [PATCH 0268/2372] Fix reply fallback being included in edit m.new_content Signed-off-by: Tulir Asokan --- src/components/views/rooms/EditMessageComposer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 00d2e447af..34b2d92590 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -75,7 +75,7 @@ function createEditContent(model, editedEvent) { const newContent = { "msgtype": isEmote ? "m.emote" : "m.text", - "body": plainPrefix + body, + "body": body, }; const contentBody = { msgtype: newContent.msgtype, @@ -85,7 +85,7 @@ function createEditContent(model, editedEvent) { const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: isReply}); if (formattedBody) { newContent.format = "org.matrix.custom.html"; - newContent.formatted_body = htmlPrefix + formattedBody; + newContent.formatted_body = formattedBody; contentBody.format = newContent.format; contentBody.formatted_body = `${htmlPrefix} * ${formattedBody}`; } From c41d17bb443da4677ac17e88541311e5eee1c7d4 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Sat, 12 Oct 2019 03:41:39 +0000 Subject: [PATCH 0269/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1831 of 1831 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 27f4f31978..c172f28f0a 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2233,5 +2233,8 @@ "Your email address hasn't been verified yet": "您的電子郵件位置尚未被驗證", "Click the link in the email you received to verify and then click continue again.": "點擊您收到的電子郵件中的連結以驗證然後再次點擊繼續。", "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "您將要移除 %(user)s 的 1 則訊息。這無法復原。您想要繼續嗎?", - "Remove %(count)s messages|one": "移除 1 則訊息" + "Remove %(count)s messages|one": "移除 1 則訊息", + "Add Email Address": "新增電子郵件地址", + "Add Phone Number": "新增電話號碼", + "%(creator)s created and configured the room.": "%(creator)s 建立並設定了聊天室。" } From 4d505900d429425830e321026175070cf94ad821 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Sat, 12 Oct 2019 10:27:26 +0000 Subject: [PATCH 0270/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1831 of 1831 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 55acdb3bd8..bfe72b737c 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2229,5 +2229,6 @@ "Your email address hasn't been verified yet": "Az e-mail címed még nincs ellenőrizve", "Click the link in the email you received to verify and then click continue again.": "Ellenőrzéshez kattints a linkre az e-mailben amit kaptál és itt kattints a folytatásra újra.", "Add Email Address": "E-mail cím hozzáadása", - "Add Phone Number": "Telefonszám hozzáadása" + "Add Phone Number": "Telefonszám hozzáadása", + "%(creator)s created and configured the room.": "%(creator)s elkészítette és beállította a szobát." } From 07dd088c1047d1ee2f390fb17f1f2e21920bb64d Mon Sep 17 00:00:00 2001 From: "Nils J. Haugen" Date: Fri, 11 Oct 2019 21:08:26 +0000 Subject: [PATCH 0271/2372] Translated using Weblate (Norwegian Nynorsk) Currently translated at 62.3% (1141 of 1831 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/nn/ --- src/i18n/strings/nn.json | 58 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/nn.json b/src/i18n/strings/nn.json index 2f0b415d2c..3c65f36249 100644 --- a/src/i18n/strings/nn.json +++ b/src/i18n/strings/nn.json @@ -120,9 +120,9 @@ "Unignored user": "Avoversedd brukar", "You are no longer ignoring %(userId)s": "Du overser ikkje %(userId)s no lenger", "Define the power level of a user": "Set ein brukar si makthøgd", - "This email address is already in use": "Denne epostadressa er allereie i bruk", + "This email address is already in use": "Denne e-postadressa er allereie i bruk", "The platform you're on": "Platformen du er på", - "Failed to verify email address: make sure you clicked the link in the email": "Fekk ikkje til å stadfesta epostadressa: sjå til at du klikka på den rette lenkja i eposten", + "Failed to verify email address: make sure you clicked the link in the email": "Fekk ikkje til å stadfesta e-postadressa: sjå til at du klikka på den rette lenkja i e-posten", "Your identity server's URL": "Din identitetstenar si nettadresse", "Every page you use in the app": "Alle sider du brukar i æppen", "e.g. ": "t.d. ", @@ -1227,7 +1227,7 @@ "This homeserver has hit its Monthly Active User limit.": "Heimtenaren har truffe den Månadlege Grensa si for Aktive Brukarar.", "This homeserver has exceeded one of its resource limits.": "Heimtenaren har gått over ei av ressursgrensene sine.", "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "Du kan òg velja ein eigendefinert identitetstenar, men då kjem du ikkje til å innvitere brukarar gjennom e-post, eller verta invitert med e-post sjølv.", - "Whether or not you're logged in (we don't record your username)": "Om du er innlogga eller ikkje (vi lagrar ikkje brukarnamnet ditt)", + "Whether or not you're logged in (we don't record your username)": "Anten du er innlogga eller ikkje ( lagrar vi ikkje brukarnamnet ditt)", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Fila %(fileName)s er større enn heimetenaren si grense for opplastningar", "Unable to load! Check your network connectivity and try again.": "Klarte ikkje lasta! Sjå på nettilkoplinga di og prøv igjen.", "Your Riot is misconfigured": "Riot-klienten din er feilkonfiguert", @@ -1330,5 +1330,55 @@ "Recovery Method Removed": "Gjenopprettingsmetode fjerna", "This device has detected that your recovery passphrase and key for Secure Messages have been removed.": "Denne eininga har oppdaga at gjenopprettingspassfrasen og nøkkelen for sikre meldingar er fjerna.", "If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.": "Gjorde du dette ved eit uhell, kan du sette opp sikre meldingar på denne eininga. Dette vil kryptere all meldingshistorikk med ein ny gjenopprettingsmetode.", - "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Viss du ikkje fjerna gjenopprettingsmetoden, kan ein angripar prøve å bryte seg inn på kontoen din. Endre kontopassordet ditt og sett ein opp ein ny gjenopprettingsmetode umidellbart under Innstillingar." + "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Viss du ikkje fjerna gjenopprettingsmetoden, kan ein angripar prøve å bryte seg inn på kontoen din. Endre kontopassordet ditt og sett ein opp ein ny gjenopprettingsmetode umidellbart under Innstillingar.", + "Add Email Address": "Legg til e-postadresse", + "Add Phone Number": "Legg til telefonnummer", + "Call failed due to misconfigured server": "Kallet gjekk gale fordi tenaren er oppsatt feil", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Spør administratoren for din heimetenar%(homeserverDomain)s om å setje opp ein \"TURN-server\" slik at heimetenaren svarar korrekt.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativt, kan du prøva å nytta den offentlege tenaren på turn.matrix.org, men det kan vera mindre stabilt og IP-adressa di vil bli delt med den tenaren. Du kan og endra på det under Innstillingar.", + "Try using turn.matrix.org": "Prøv med å nytta turn.matrix.org", + "A conference call could not be started because the integrations server is not available": "Ein konferansesamtale kunne ikkje starta fordi integrasjons-tenaren er utilgjengeleg", + "Replying With Files": "Send svar med filer", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Nett no er det ikkje mogleg å senda svar med ei fil. Vil du lasta opp denne fila utan å senda svaret?", + "The file '%(fileName)s' failed to upload.": "Fila '%(fileName)s' vart ikkje lasta opp.", + "The server does not support the room version specified.": "Tenaren støttar ikkje den spesifikke versjonen av rommet.", + "Name or Matrix ID": "Namn eller Matrix ID", + "Registration Required": "Registrering er obligatorisk", + "You need to register to do this. Would you like to register now?": "Du må registrera for å gjera dette. Ynskjer du å registrera no?", + "Email, name or Matrix ID": "E-post, namn eller Matrix ID", + "Failed to start chat": "Fekk ikkje til å starte samtalen", + "Failed to invite users to the room:": "Fekk ikkje til å invitera brukarar til rommet:", + "Messages": "Meldingar", + "Actions": "Handlingar", + "Other": "Anna", + "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Sett inn ¯\\_(ツ)_/¯ i ein rein-tekst melding", + "Sends a message as plain text, without interpreting it as markdown": "Sender ein melding som rein-tekst, utan å tolka den som markdown", + "Upgrades a room to a new version": "Oppgraderer eit rom til ein ny versjon", + "You do not have the required permissions to use this command.": "Du har ikkje tilgang til å utføra denne kommandoen.", + "Room upgrade confirmation": "Stadfesting for rom-oppgradering", + "Upgrading a room can be destructive and isn't always necessary.": "Oppgradering av eit rom kan vera destruktivt og er ikkje alltid naudsynt.", + "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Oppgradering av rom er som regel tilrådd når ein versjon av rommet er å rekne som ustabil. Ustabile rom kan ha kodefeil, mangle funksjonar eller ha sikkerheitproblem.", + "Room upgrades usually only affect server-side processing of the room. If you're having problems with your Riot client, please file an issue with .": "Oppgradering av rom påverkar normalt berre prosessering av rommet på tenar-sida . Om du opplever problem med Riot-klienten din, meld gjerne inn problemet med .", + "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Åtvaring: Oppgradering av eit rom vil ikkje automatisk overføre rom-medlemane til den nye versjonen av rommet. Vi vil leggje ut ein link til det nye romme i den gamle utgåva av rommet - rom-medlemane må då klikka på denne linken for å medlem av det nye rommet.", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Stadfest at du vil fortsetje med å oppgradere dette rommet frå til .", + "Upgrade": "Oppgrader", + "Changes your display nickname in the current room only": "Endrar kallenamnet ditt som er synleg i det gjeldande rommet", + "Changes the avatar of the current room": "Endrar avataren for det gjeldande rommet", + "Changes your avatar in this current room only": "Endrar din avatar for det gjeldande rommet", + "Changes your avatar in all rooms": "Endrar din avatar for alle rom", + "Gets or sets the room topic": "Hentar eller endrar emnefeltet for rommet", + "This room has no topic.": "Dette rommet har ikkje noko emne.", + "Sets the room name": "Sett romnamn", + "Use an identity server": "Bruk ein identitetstenar", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Bruk ein identitetstenar for å invitera via e-post. Klikk for å fortsetja å bruka standard identitetstenar (%(defaultIdentityServerName)s) eller styre dette i innstillingane.", + "Use an identity server to invite by email. Manage in Settings.": "Bruk ein identitetstenar for å invitera via e-post. Styr dette i Innstillingane.", + "Unbans user with given ID": "Ta vekk blokkering av brukar med bestemt ID", + "Adds a custom widget by URL to the room": "Legg til eit tilpassa miniprogram til rommet med ein URL", + "Please supply a https:// or http:// widget URL": "Skriv inn https:// eller http:// URL-en for miniprogrammet", + "You cannot modify widgets in this room.": "Du kan ikkje endra miniprogram i dette rommet.", + "Forces the current outbound group session in an encrypted room to be discarded": "Tvingar i eit kryptert rom kassering av gjeldande utgåande gruppe-sesjon", + "Sends the given message coloured as a rainbow": "Sender den bestemte meldinga farga som ein regnboge", + "Displays list of commands with usages and descriptions": "Viser ei liste over kommandoar med bruksområde og skildringar", + "%(senderName)s made no change.": "%(senderName)s utførde ingen endring.", + "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s oppgraderte dette rommet." } From a160bdf4df5d2f870778e5ac22a623274f74c9f5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 13 Oct 2019 14:04:54 +0300 Subject: [PATCH 0272/2372] Persist code block language when editing Signed-off-by: Tulir Asokan --- src/editor/deserialize.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index d41e413dbc..abcfbf7dd7 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -58,7 +58,16 @@ function parseLink(a, partCreator) { function parseCodeBlock(n, partCreator) { const parts = []; - const preLines = ("```\n" + n.textContent + "```").split("\n"); + let language = ""; + if (n.firstChild && n.firstChild.nodeName === "CODE") { + for (const className of n.firstChild.classList) { + if (className.startsWith("language-")) { + language = className.substr("language-".length); + break; + } + } + } + const preLines = ("```" + language + "\n" + n.textContent + "```").split("\n"); preLines.forEach((l, i) => { parts.push(partCreator.plain(l)); if (i < preLines.length - 1) { From 29367766fd107b1209a1d3965a33df7dbcc081f4 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 13 Oct 2019 14:10:11 +0300 Subject: [PATCH 0273/2372] Fix "decend" typo Signed-off-by: Tulir Asokan --- src/editor/deserialize.js | 16 ++++++++-------- src/editor/dom.js | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index abcfbf7dd7..58e188457a 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -124,19 +124,19 @@ function parseElement(n, partCreator, lastNode, state) { state.listDepth = (state.listDepth || 0) + 1; // es-lint-disable-next-line no-fallthrough default: - // don't textify block nodes we'll decend into - if (!checkDecendInto(n)) { + // don't textify block nodes we'll descend into + if (!checkDescendInto(n)) { return partCreator.plain(n.textContent); } } } -function checkDecendInto(node) { +function checkDescendInto(node) { switch (node.nodeName) { case "PRE": // a code block is textified in parseCodeBlock // as we don't want to preserve markup in it, - // so no need to decend into it + // so no need to descend into it return false; default: return checkBlockNode(node); @@ -212,11 +212,11 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) { parts.push(...newParts); - const decend = checkDecendInto(n); - // when not decending (like for PRE), onNodeLeave won't be called to set lastNode + const descend = checkDescendInto(n); + // when not descending (like for PRE), onNodeLeave won't be called to set lastNode // so do that here. - lastNode = decend ? null : n; - return decend; + lastNode = descend ? null : n; + return descend; } function onNodeLeave(n) { diff --git a/src/editor/dom.js b/src/editor/dom.js index e82c3f70ca..3efc64f1c9 100644 --- a/src/editor/dom.js +++ b/src/editor/dom.js @@ -21,8 +21,8 @@ import DocumentOffset from "./offset"; export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback) { let node = rootNode.firstChild; while (node && node !== rootNode) { - const shouldDecend = enterNodeCallback(node); - if (shouldDecend && node.firstChild) { + const shouldDescend = enterNodeCallback(node); + if (shouldDescend && node.firstChild) { node = node.firstChild; } else if (node.nextSibling) { node = node.nextSibling; From a95f7be22d67a98a7c1df074d12d147090a70899 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 13 Oct 2019 14:27:12 +0300 Subject: [PATCH 0274/2372] Persist list indexes when editing Signed-off-by: Tulir Asokan --- src/editor/deserialize.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index 58e188457a..2662d5a503 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -108,7 +108,9 @@ function parseElement(n, partCreator, lastNode, state) { case "LI": { const indent = " ".repeat(state.listDepth - 1); if (n.parentElement.nodeName === "OL") { - return partCreator.plain(`${indent}1. `); + // The markdown parser doesn't do nested indexed lists at all, but this supports it anyway. + let index = state.listIndex[state.listIndex.length - 1]++; + return partCreator.plain(`${indent}${index}. `); } else { return partCreator.plain(`${indent}- `); } @@ -120,9 +122,11 @@ function parseElement(n, partCreator, lastNode, state) { break; } case "OL": + state.listIndex.push(n.start || 1); + // fallthrough case "UL": state.listDepth = (state.listDepth || 0) + 1; - // es-lint-disable-next-line no-fallthrough + // fallthrough default: // don't textify block nodes we'll descend into if (!checkDescendInto(n)) { @@ -177,7 +181,9 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) { const parts = []; let lastNode; let inQuote = isQuotedMessage; - const state = {}; + const state = { + listIndex: [], + }; function onNodeEnter(n) { if (checkIgnored(n)) { @@ -228,6 +234,8 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) { inQuote = false; break; case "OL": + state.listIndex.pop(); + // fallthrough case "UL": state.listDepth -= 1; break; From 497b779334cd0e5f92ccc068c8a9fb9d4c8fe01a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 14 Oct 2019 00:32:11 +0300 Subject: [PATCH 0275/2372] Add full emoji picker for reactions Signed-off-by: Tulir Asokan --- res/css/_components.scss | 3 +- res/css/views/emojipicker/_EmojiPicker.scss | 157 ++++++++++++++ .../views/messages/_ReactionQuickTooltip.scss | 29 --- .../messages/_ReactionTooltipButton.scss | 31 --- src/components/views/emojipicker/Category.js | 57 +++++ src/components/views/emojipicker/Emoji.js | 41 ++++ .../views/emojipicker/EmojiPicker.js | 154 ++++++++++++++ src/components/views/emojipicker/Header.js | 58 ++++++ src/components/views/emojipicker/Preview.js | 44 ++++ .../views/emojipicker/QuickReactions.js | 82 ++++++++ src/components/views/emojipicker/Search.js | 38 ++++ src/components/views/emojipicker/icons.js | 170 +++++++++++++++ .../views/messages/MessageActionBar.js | 57 +++-- .../views/messages/ReactMessageAction.js | 97 --------- .../views/messages/ReactionTooltipButton.js | 68 ------ .../views/messages/ReactionsQuickTooltip.js | 195 ------------------ src/i18n/strings/en_EN.json | 12 +- 17 files changed, 858 insertions(+), 435 deletions(-) create mode 100644 res/css/views/emojipicker/_EmojiPicker.scss delete mode 100644 res/css/views/messages/_ReactionQuickTooltip.scss delete mode 100644 res/css/views/messages/_ReactionTooltipButton.scss create mode 100644 src/components/views/emojipicker/Category.js create mode 100644 src/components/views/emojipicker/Emoji.js create mode 100644 src/components/views/emojipicker/EmojiPicker.js create mode 100644 src/components/views/emojipicker/Header.js create mode 100644 src/components/views/emojipicker/Preview.js create mode 100644 src/components/views/emojipicker/QuickReactions.js create mode 100644 src/components/views/emojipicker/Search.js create mode 100644 src/components/views/emojipicker/icons.js delete mode 100644 src/components/views/messages/ReactMessageAction.js delete mode 100644 src/components/views/messages/ReactionTooltipButton.js delete mode 100644 src/components/views/messages/ReactionsQuickTooltip.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 4891fd90c0..7e1a280dd3 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -108,6 +108,7 @@ @import "./views/elements/_Tooltip.scss"; @import "./views/elements/_TooltipButton.scss"; @import "./views/elements/_Validation.scss"; +@import "./views/emojipicker/_EmojiPicker.scss"; @import "./views/globals/_MatrixToolbar.scss"; @import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupRoomList.scss"; @@ -122,8 +123,6 @@ @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; -@import "./views/messages/_ReactionQuickTooltip.scss"; -@import "./views/messages/_ReactionTooltipButton.scss"; @import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRowButton.scss"; @import "./views/messages/_ReactionsRowButtonTooltip.scss"; diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss new file mode 100644 index 0000000000..99a75b9d10 --- /dev/null +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -0,0 +1,157 @@ +/* +Copyright 2019 Tulir Asokan + +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. +*/ + +.mx_EmojiPicker { + width: 340px; + height: 450px; + + border-radius: 4px; + + display: flex; + flex-direction: column; +} + +.mx_EmojiPicker_body { + flex: 1; + overflow-y: scroll; + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.2) transparent; +} + +.mx_EmojiPicker_header { + padding: 4px 8px 0; + border-bottom: 1px solid $message-action-bar-border-color; +} + +.mx_EmojiPicker_anchor { + border: none; + padding: 8px 8px 6px; + border-bottom: 2px solid transparent; + background-color: transparent; + border-radius: 4px 4px 0 0; + + svg { + width: 20px; + height: 20px; + fill: $primary-fg-color; + } + + &:hover { + background-color: $focus-bg-color; + border-bottom: 2px solid $button-bg-color; + } + + .mx_EmojiPicker_anchor_selected { + border-bottom: 2px solid $button-bg-color; + } +} + +.mx_EmojiPicker_search { + margin: 8px; + border-radius: 4px; + border: 1px solid $input-border-color; + background-color: $primary-bg-color; + display: flex; + + input { + flex: 1; + border: none; + padding: 8px 12px; + border-radius: 4px 0; + } + + svg { + align-self: center; + width: 16px; + height: 16px; + margin: 8px; + } +} + +.mx_EmojiPicker_category { + padding: 0 12px; +} + +.mx_EmojiPicker_category_label, .mx_EmojiPicker_preview_name { + font-size: 16px; + font-weight: 600; + margin: 0; +} + +.mx_EmojiPicker_list { + padding: 0; + margin: 0; + // TODO the emoji rows need to be center-aligned, but the individual emojis shouldn't be. + //text-align: center; +} + +.mx_EmojiPicker_item { + list-style: none; + display: inline-block; + font-size: 20px; + margin: 1px; + padding: 4px 0; + width: 36px; + box-sizing: border-box; + text-align: center; + border-radius: 4px; + cursor: pointer; + + &:hover { + background-color: $focus-bg-color; + } +} + +.mx_EmojiPicker_footer { + border-top: 1px solid $message-action-bar-border-color; + height: 72px; + + display: flex; + align-items: center; +} + +.mx_EmojiPicker_preview_emoji { + font-size: 32px; + padding: 8px 16px; +} + +.mx_EmojiPicker_preview_text { + display: flex; + flex-direction: column; +} + +.mx_EmojiPicker_name { + text-transform: capitalize; +} + +.mx_EmojiPicker_shortcode { + color: $light-fg-color; + font-size: 14px; + + &::before, &::after { + content: ":"; + } +} + +.mx_EmojiPicker_quick { + flex-direction: column; + align-items: start; + justify-content: space-around; +} + +.mx_EmojiPicker_quick_header .mx_EmojiPicker_name { + margin-right: 4px; +} diff --git a/res/css/views/messages/_ReactionQuickTooltip.scss b/res/css/views/messages/_ReactionQuickTooltip.scss deleted file mode 100644 index 7b1611483b..0000000000 --- a/res/css/views/messages/_ReactionQuickTooltip.scss +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -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. -*/ - -.mx_ReactionsQuickTooltip_buttons { - display: grid; - grid-template-columns: repeat(4, auto); -} - -.mx_ReactionsQuickTooltip_label { - text-align: center; -} - -.mx_ReactionsQuickTooltip_shortcode { - padding-left: 6px; - opacity: 0.7; -} diff --git a/res/css/views/messages/_ReactionTooltipButton.scss b/res/css/views/messages/_ReactionTooltipButton.scss deleted file mode 100644 index 59244ab63b..0000000000 --- a/res/css/views/messages/_ReactionTooltipButton.scss +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -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. -*/ - -.mx_ReactionTooltipButton { - font-size: 16px; - padding: 6px; - user-select: none; - cursor: pointer; - transition: transform 0.25s; - - &:hover { - transform: scale(1.2); - } -} - -.mx_ReactionTooltipButton_selected { - opacity: 0.4; -} diff --git a/src/components/views/emojipicker/Category.js b/src/components/views/emojipicker/Category.js new file mode 100644 index 0000000000..9573a24630 --- /dev/null +++ b/src/components/views/emojipicker/Category.js @@ -0,0 +1,57 @@ +/* +Copyright 2019 Tulir Asokan + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import sdk from '../../../index'; + +class Category extends React.PureComponent { + static propTypes = { + emojis: PropTypes.arrayOf(PropTypes.object).isRequired, + name: PropTypes.string.isRequired, + onMouseEnter: PropTypes.func.isRequired, + onMouseLeave: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + filter: PropTypes.string, + }; + + render() { + const { onClick, onMouseEnter, onMouseLeave, emojis, name, filter } = this.props; + + const Emoji = sdk.getComponent("emojipicker.Emoji"); + const renderedEmojis = (emojis || []).map(emoji => !filter || emoji.filterString.includes(filter) ? ( + + ) : null).filter(component => component !== null); + if (renderedEmojis.length === 0) { + return null; + } + + return ( +

+

+ {name} +

+
    + {renderedEmojis} +
+
+ ) + } +} + +export default Category; diff --git a/src/components/views/emojipicker/Emoji.js b/src/components/views/emojipicker/Emoji.js new file mode 100644 index 0000000000..3bbbe3a771 --- /dev/null +++ b/src/components/views/emojipicker/Emoji.js @@ -0,0 +1,41 @@ +/* +Copyright 2019 Tulir Asokan + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +class Emoji extends React.PureComponent { + static propTypes = { + onClick: PropTypes.func, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, + emoji: PropTypes.object.isRequired, + }; + + render() { + const { onClick, onMouseEnter, onMouseLeave, emoji } = this.props; + return ( +
  • onClick(emoji)} + onMouseEnter={() => onMouseEnter(emoji)} + onMouseLeave={() => onMouseLeave(emoji)} + className="mx_EmojiPicker_item"> + {emoji.unicode} +
  • + ) + } +} + +export default Emoji; diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.js new file mode 100644 index 0000000000..0ffe3a06f7 --- /dev/null +++ b/src/components/views/emojipicker/EmojiPicker.js @@ -0,0 +1,154 @@ +/* +Copyright 2019 Tulir Asokan + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import EMOJIBASE from 'emojibase-data/en/compact.json'; +import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; + +const EMOJIBASE_CATEGORY_IDS = [ + "people", // smileys + "people", // actually people + "control", // modifiers and such, not displayed in picker + "nature", + "foods", + "places", + "activity", + "objects", + "symbols", + "flags", +]; + +const DATA_BY_CATEGORY = { + "people": [], + "nature": [], + "foods": [], + "places": [], + "activity": [], + "objects": [], + "symbols": [], + "flags": [], + "control": [], +}; + +EMOJIBASE.forEach(emoji => { + DATA_BY_CATEGORY[EMOJIBASE_CATEGORY_IDS[emoji.group]].push(emoji); + // This is used as the string to match the query against when filtering emojis. + emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`; +}); + +class EmojiPicker extends React.Component { + static propTypes = { + onChoose: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + filter: "", + previewEmoji: null, + }; + + this.categories = [{ + id: "recent", + name: _t("Frequently Used"), + }, { + id: "people", + name: _t("Smileys & People"), + }, { + id: "nature", + name: _t("Animals & Nature"), + }, { + id: "foods", + name: _t("Food & Drink"), + }, { + id: "activity", + name: _t("Activities"), + }, { + id: "places", + name: _t("Travel & Places"), + }, { + id: "objects", + name: _t("Objects"), + }, { + id: "symbols", + name: _t("Symbols"), + }, { + id: "flags", + name: _t("Flags"), + }]; + + this.onChangeFilter = this.onChangeFilter.bind(this); + this.onHoverEmoji = this.onHoverEmoji.bind(this); + this.onHoverEmojiEnd = this.onHoverEmojiEnd.bind(this); + this.onClickEmoji = this.onClickEmoji.bind(this); + } + + scrollToCategory() { + // TODO + } + + onChangeFilter(ev) { + this.setState({ + filter: ev.target.value, + }); + } + + onHoverEmoji(emoji) { + this.setState({ + previewEmoji: emoji, + }); + } + + onHoverEmojiEnd(emoji) { + this.setState({ + previewEmoji: null, + }); + } + + onClickEmoji(emoji) { + this.props.onChoose(emoji.unicode); + } + + render() { + const Header = sdk.getComponent("emojipicker.Header"); + const Search = sdk.getComponent("emojipicker.Search"); + const Category = sdk.getComponent("emojipicker.Category"); + const Preview = sdk.getComponent("emojipicker.Preview"); + const QuickReactions = sdk.getComponent("emojipicker.QuickReactions"); + return ( +
    +
    + +
    + {this.categories.map(category => ( + + ))} +
    + {this.state.previewEmoji + ? + : } +
    + ) + } +} + +export default EmojiPicker; diff --git a/src/components/views/emojipicker/Header.js b/src/components/views/emojipicker/Header.js new file mode 100644 index 0000000000..d061f8559a --- /dev/null +++ b/src/components/views/emojipicker/Header.js @@ -0,0 +1,58 @@ +/* +Copyright 2019 Tulir Asokan + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import * as icons from "./icons"; + +class Header extends React.Component { + static propTypes = { + categories: PropTypes.arrayOf(PropTypes.object).isRequired, + onAnchorClick: PropTypes.func.isRequired, + defaultCategory: PropTypes.string, + }; + + constructor(props) { + super(props); + this.state = { + selected: props.defaultCategory || props.categories[0].id, + }; + this.handleClick = this.handleClick.bind(this); + } + + handleClick(ev) { + const selected = ev.target.getAttribute("data-category-id"); + this.setState({selected}); + this.props.onAnchorClick(selected); + }; + + render() { + return ( + + ) + } +} + +export default Header; diff --git a/src/components/views/emojipicker/Preview.js b/src/components/views/emojipicker/Preview.js new file mode 100644 index 0000000000..1757a04801 --- /dev/null +++ b/src/components/views/emojipicker/Preview.js @@ -0,0 +1,44 @@ +/* +Copyright 2019 Tulir Asokan + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +class Preview extends React.PureComponent { + static propTypes = { + emoji: PropTypes.object.isRequired, + }; + + render() { + return ( +
    +
    + {this.props.emoji.unicode} +
    +
    +
    + {this.props.emoji.annotation} +
    +
    + {this.props.emoji.shortcodes[0]} +
    +
    +
    + ) + } +} + +export default Preview; diff --git a/src/components/views/emojipicker/QuickReactions.js b/src/components/views/emojipicker/QuickReactions.js new file mode 100644 index 0000000000..58c3095d34 --- /dev/null +++ b/src/components/views/emojipicker/QuickReactions.js @@ -0,0 +1,82 @@ +/* +Copyright 2019 Tulir Asokan + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import EMOJIBASE from 'emojibase-data/en/compact.json'; + +import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; + +const QUICK_REACTIONS = ["👍️", "👎️", "😄", "🎉", "😕", "❤️", "🚀", "👀"]; +EMOJIBASE.forEach(emoji => { + const index = QUICK_REACTIONS.indexOf(emoji.unicode); + if (index !== -1) { + QUICK_REACTIONS[index] = emoji; + } +}); + +class QuickReactions extends React.Component { + static propTypes = { + onClick: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + hover: null, + }; + this.onMouseEnter = this.onMouseEnter.bind(this); + this.onMouseLeave = this.onMouseLeave.bind(this); + } + + onMouseEnter(emoji) { + this.setState({ + hover: emoji, + }); + } + + onMouseLeave() { + this.setState({ + hover: null, + }); + } + + render() { + const Emoji = sdk.getComponent("emojipicker.Emoji"); + + return ( +
    +

    + {!this.state.hover + ? _t("Quick Reactions") + : + {this.state.hover.annotation} + {this.state.hover.shortcodes[0]} + + } +

    +
      + {QUICK_REACTIONS.map(emoji => )} +
    +
    + ) + } +} + +export default QuickReactions; diff --git a/src/components/views/emojipicker/Search.js b/src/components/views/emojipicker/Search.js new file mode 100644 index 0000000000..a0200a29d4 --- /dev/null +++ b/src/components/views/emojipicker/Search.js @@ -0,0 +1,38 @@ +/* +Copyright 2019 Tulir Asokan + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import * as icons from "./icons"; + +class Search extends React.PureComponent { + static propTypes = { + query: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + }; + + render() { + return ( +
    + + {icons.search.search()} +
    + ) + } +} + +export default Search; diff --git a/src/components/views/emojipicker/icons.js b/src/components/views/emojipicker/icons.js new file mode 100644 index 0000000000..b6cf1ad371 --- /dev/null +++ b/src/components/views/emojipicker/icons.js @@ -0,0 +1,170 @@ +// Copyright (c) 2016, Missive +// From https://github.com/missive/emoji-mart/blob/master/src/svgs/index.js +// Licensed under BSD-3-Clause: https://github.com/missive/emoji-mart/blob/master/LICENSE + +import React from 'react' + +const categories = { + activity: () => ( + + + + ), + + custom: () => ( + + + + + + + + ), + + flags: () => ( + + + + ), + + foods: () => ( + + + + ), + + nature: () => ( + + + + + ), + + objects: () => ( + + + + + ), + + people: () => ( + + + + + ), + + places: () => ( + + + + + ), + + recent: () => ( + + + + + ), + + symbols: () => ( + + + + ), +} + +const search = { + search: () => ( + + + + ), + + delete: () => ( + + + + ), +} + +export { categories, search } diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 2b43c5fe2a..95ab57d324 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -25,6 +25,7 @@ import Modal from '../../../Modal'; import { createMenu } from '../../structures/ContextualMenu'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import {RoomContext} from "../../structures/RoomView"; +import MatrixClientPeg from '../../../MatrixClientPeg'; export default class MessageActionBar extends React.PureComponent { static propTypes = { @@ -84,6 +85,45 @@ export default class MessageActionBar extends React.PureComponent { }); }; + onReactClick = (ev) => { + const EmojiPicker = sdk.getComponent('emojipicker.EmojiPicker'); + const buttonRect = ev.target.getBoundingClientRect(); + + const menuOptions = { + reactions: this.props.reactions, + chevronFace: "none", + onFinished: () => this.onFocusChange(false), + onChoose: reaction => { + this.onFocusChange(false); + MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), "m.reaction", { + "m.relates_to": { + "rel_type": "m.annotation", + "event_id": this.props.mxEvent.getId(), + "key": reaction, + }, + }); + }, + }; + + // The window X and Y offsets are to adjust position when zoomed in to page + const buttonRight = buttonRect.right + window.pageXOffset; + const buttonBottom = buttonRect.bottom + window.pageYOffset; + const buttonTop = buttonRect.top + window.pageYOffset; + // Align the right edge of the menu to the right edge of the button + menuOptions.right = window.innerWidth - buttonRight; + // Align the menu vertically on whichever side of the button has more + // space available. + if (buttonBottom < window.innerHeight / 2) { + menuOptions.top = buttonBottom; + } else { + menuOptions.bottom = window.innerHeight - buttonTop; + } + + createMenu(EmojiPicker, menuOptions); + + this.onFocusChange(true); + }; + onOptionsClick = (ev) => { const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); const buttonRect = ev.target.getBoundingClientRect(); @@ -128,17 +168,6 @@ export default class MessageActionBar extends React.PureComponent { this.onFocusChange(true); }; - renderReactButton() { - const ReactMessageAction = sdk.getComponent('messages.ReactMessageAction'); - const { mxEvent, reactions } = this.props; - - return ; - } - render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -148,7 +177,11 @@ export default class MessageActionBar extends React.PureComponent { if (isContentActionable(this.props.mxEvent)) { if (this.context.room.canReact) { - reactButton = this.renderReactButton(); + reactButton = ; } if (this.context.room.canReply) { replyButton = { - if (!this.props.onFocusChange) { - return; - } - this.props.onFocusChange(focused); - } - - componentDidUpdate(prevProps) { - if (prevProps.reactions !== this.props.reactions) { - this.props.reactions.on("Relations.add", this.onReactionsChange); - this.props.reactions.on("Relations.remove", this.onReactionsChange); - this.props.reactions.on("Relations.redaction", this.onReactionsChange); - this.onReactionsChange(); - } - } - - componentWillUnmount() { - if (this.props.reactions) { - this.props.reactions.removeListener( - "Relations.add", - this.onReactionsChange, - ); - this.props.reactions.removeListener( - "Relations.remove", - this.onReactionsChange, - ); - this.props.reactions.removeListener( - "Relations.redaction", - this.onReactionsChange, - ); - } - } - - onReactionsChange = () => { - // Force a re-render of the tooltip because a change in the reactions - // set means the event tile's layout may have changed and possibly - // altered the location where the tooltip should be shown. - this.forceUpdate(); - } - - render() { - const ReactionsQuickTooltip = sdk.getComponent('messages.ReactionsQuickTooltip'); - const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip'); - const { mxEvent, reactions } = this.props; - - const content = ; - - return - - ; - } -} diff --git a/src/components/views/messages/ReactionTooltipButton.js b/src/components/views/messages/ReactionTooltipButton.js deleted file mode 100644 index e09b9ade69..0000000000 --- a/src/components/views/messages/ReactionTooltipButton.js +++ /dev/null @@ -1,68 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import MatrixClientPeg from '../../../MatrixClientPeg'; - -export default class ReactionTooltipButton extends React.PureComponent { - static propTypes = { - mxEvent: PropTypes.object.isRequired, - // The reaction content / key / emoji - content: PropTypes.string.isRequired, - title: PropTypes.string, - // A possible Matrix event if the current user has voted for this type - myReactionEvent: PropTypes.object, - }; - - onClick = (ev) => { - const { mxEvent, myReactionEvent, content } = this.props; - if (myReactionEvent) { - MatrixClientPeg.get().redactEvent( - mxEvent.getRoomId(), - myReactionEvent.getId(), - ); - } else { - MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", { - "m.relates_to": { - "rel_type": "m.annotation", - "event_id": mxEvent.getId(), - "key": content, - }, - }); - } - } - - render() { - const { content, myReactionEvent } = this.props; - - const classes = classNames({ - mx_ReactionTooltipButton: true, - mx_ReactionTooltipButton_selected: !!myReactionEvent, - }); - - return - {content} - ; - } -} diff --git a/src/components/views/messages/ReactionsQuickTooltip.js b/src/components/views/messages/ReactionsQuickTooltip.js deleted file mode 100644 index 0505bbd2df..0000000000 --- a/src/components/views/messages/ReactionsQuickTooltip.js +++ /dev/null @@ -1,195 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import PropTypes from 'prop-types'; - -import { _t } from '../../../languageHandler'; -import sdk from '../../../index'; -import MatrixClientPeg from '../../../MatrixClientPeg'; -import { unicodeToShortcode } from '../../../HtmlUtils'; - -export default class ReactionsQuickTooltip extends React.PureComponent { - static propTypes = { - mxEvent: PropTypes.object.isRequired, - // The Relations model from the JS SDK for reactions to `mxEvent` - reactions: PropTypes.object, - }; - - constructor(props) { - super(props); - - if (props.reactions) { - props.reactions.on("Relations.add", this.onReactionsChange); - props.reactions.on("Relations.remove", this.onReactionsChange); - props.reactions.on("Relations.redaction", this.onReactionsChange); - } - - this.state = { - hoveredItem: null, - myReactions: this.getMyReactions(), - }; - } - - componentDidUpdate(prevProps) { - if (prevProps.reactions !== this.props.reactions) { - this.props.reactions.on("Relations.add", this.onReactionsChange); - this.props.reactions.on("Relations.remove", this.onReactionsChange); - this.props.reactions.on("Relations.redaction", this.onReactionsChange); - this.onReactionsChange(); - } - } - - componentWillUnmount() { - if (this.props.reactions) { - this.props.reactions.removeListener( - "Relations.add", - this.onReactionsChange, - ); - this.props.reactions.removeListener( - "Relations.remove", - this.onReactionsChange, - ); - this.props.reactions.removeListener( - "Relations.redaction", - this.onReactionsChange, - ); - } - } - - onReactionsChange = () => { - this.setState({ - myReactions: this.getMyReactions(), - }); - } - - getMyReactions() { - const reactions = this.props.reactions; - if (!reactions) { - return null; - } - const userId = MatrixClientPeg.get().getUserId(); - const myReactions = reactions.getAnnotationsBySender()[userId]; - if (!myReactions) { - return null; - } - return [...myReactions.values()]; - } - - onMouseOver = (ev) => { - const { key } = ev.target.dataset; - const item = this.items.find(({ content }) => content === key); - this.setState({ - hoveredItem: item, - }); - } - - onMouseOut = (ev) => { - this.setState({ - hoveredItem: null, - }); - } - - get items() { - return [ - { - content: "👍", - title: _t("Agree"), - }, - { - content: "👎", - title: _t("Disagree"), - }, - { - content: "😄", - title: _t("Happy"), - }, - { - content: "🎉", - title: _t("Party Popper"), - }, - { - content: "😕", - title: _t("Confused"), - }, - { - content: "❤️", - title: _t("Heart"), - }, - { - content: "🚀", - title: _t("Rocket"), - }, - { - content: "👀", - title: _t("Eyes"), - }, - ]; - } - - render() { - const { mxEvent } = this.props; - const { myReactions, hoveredItem } = this.state; - const ReactionTooltipButton = sdk.getComponent('messages.ReactionTooltipButton'); - - const buttons = this.items.map(({ content, title }) => { - const myReactionEvent = myReactions && myReactions.find(mxEvent => { - if (mxEvent.isRedacted()) { - return false; - } - return mxEvent.getRelation().key === content; - }); - - return ; - }); - - let label = " "; // non-breaking space to keep layout the same when empty - if (hoveredItem) { - const { content, title } = hoveredItem; - - let shortcodeLabel; - const shortcode = unicodeToShortcode(content); - if (shortcode) { - shortcodeLabel = - {shortcode} - ; - } - - label =
    - - {title} - - {shortcodeLabel} -
    ; - } - - return
    -
    - {buttons} -
    - {label} -
    ; - } -} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f524a22d4b..44812f8bbc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1829,5 +1829,15 @@ "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", - "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", + "Quick Reactions": "Quick Reactions", + "Frequently Used": "Frequently Used", + "Smileys & People": "Smileys & People", + "Animals & Nature": "Animals & Nature", + "Food & Drink": "Food & Drink", + "Activities": "Activities", + "Travel & Places": "Travel & Places", + "Objects": "Objects", + "Symbols": "Symbols", + "Flags": "Flags" } From c0dab5c3cc585fe88aa6e8b1cdd491a1cf72b557 Mon Sep 17 00:00:00 2001 From: Aleksei Perepelkin Date: Mon, 14 Oct 2019 05:39:32 +0000 Subject: [PATCH 0276/2372] Translated using Weblate (Russian) Currently translated at 93.4% (1711 of 1831 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 4a83fdc605..edc556b63f 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -2003,5 +2003,12 @@ "Use an identity server": "Используйте сервер идентификации", "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Используйте идентификационный сервер для приглашения по электронной почте. Нажмите «Продолжить», чтобы использовать сервер идентификации по умолчанию (% (defaultIdentityServerName)s) или измените его в настройках.", "Use an identity server to invite by email. Manage in Settings.": "Используйте идентификационный сервер для приглашения по электронной почте. Управление в меню Настройки.", - "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Разрешить резервный вызов поддержки сервера turn.matrix.org, когда ваш домашний сервер не предлагает такой поддержки (ваш IP-адрес будет использоваться во время вызова)" + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Разрешить резервный вызов поддержки сервера turn.matrix.org, когда ваш домашний сервер не предлагает такой поддержки (ваш IP-адрес будет использоваться во время вызова)", + "Add Email Address": "Добавить адрес Email", + "Add Phone Number": "Добавить номер телефона", + "Changes the avatar of the current room": "Меняет аватарку текущей комнаты", + "Change identity server": "Изменить сервер идентификации", + "Explore": "Исследовать", + "Filter": "Фильтровать", + "Filter rooms…": "Фильтровать комнаты…" } From adbb20f47feb81555ededde8a126f4b2a7cd27af Mon Sep 17 00:00:00 2001 From: Aleksei Perepelkin Date: Mon, 14 Oct 2019 06:19:31 +0000 Subject: [PATCH 0277/2372] Translated using Weblate (Russian) Currently translated at 93.5% (1712 of 1831 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index edc556b63f..e1933dbe59 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -2001,7 +2001,7 @@ "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Кроме того, вы можете попытаться использовать общедоступный сервер по адресу turn.matrix.org , но это не будет настолько надежным, и он предоставит ваш IP-адрес этому серверу. Вы также можете управлять этим в настройках.", "Sends a message as plain text, without interpreting it as markdown": "Посылает сообщение в виде простого текста, не интерпретируя его как разметку", "Use an identity server": "Используйте сервер идентификации", - "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Используйте идентификационный сервер для приглашения по электронной почте. Нажмите «Продолжить», чтобы использовать сервер идентификации по умолчанию (% (defaultIdentityServerName)s) или измените его в настройках.", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Используйте идентификационный сервер для приглашения по электронной почте. Нажмите «Продолжить», чтобы использовать сервер идентификации по умолчанию (%(defaultIdentityServerName)s) или измените его в настройках.", "Use an identity server to invite by email. Manage in Settings.": "Используйте идентификационный сервер для приглашения по электронной почте. Управление в меню Настройки.", "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Разрешить резервный вызов поддержки сервера turn.matrix.org, когда ваш домашний сервер не предлагает такой поддержки (ваш IP-адрес будет использоваться во время вызова)", "Add Email Address": "Добавить адрес Email", @@ -2010,5 +2010,6 @@ "Change identity server": "Изменить сервер идентификации", "Explore": "Исследовать", "Filter": "Фильтровать", - "Filter rooms…": "Фильтровать комнаты…" + "Filter rooms…": "Фильтровать комнаты…", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Если не удаётся найти комнату, то можно запросить приглашение или Создать новую комнату." } From 4273d5e9a9b564e98af1323d2fbf8a928b1bcd08 Mon Sep 17 00:00:00 2001 From: Aleksei Perepelkin Date: Mon, 14 Oct 2019 06:24:42 +0000 Subject: [PATCH 0278/2372] Translated using Weblate (Russian) Currently translated at 93.8% (1718 of 1831 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index e1933dbe59..ebda65764d 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -2011,5 +2011,11 @@ "Explore": "Исследовать", "Filter": "Фильтровать", "Filter rooms…": "Фильтровать комнаты…", - "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Если не удаётся найти комнату, то можно запросить приглашение или Создать новую комнату." + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Если не удаётся найти комнату, то можно запросить приглашение или Создать новую комнату.", + "Room alias": "Псевдоним комнаты", + "Set a room alias to easily share your room with other people.": "Присвоить комнате адрес, чтобы было проще приглашать в неё людей.", + "Create a public room": "Создать публичную комнату", + "Create a private room": "Создать приватную комнату", + "Topic (optional)": "Тема (опционально)", + "Make this room public": "Сделать комнату публичной" } From 0d7e9bb9c735aef82ec6011a09102046118e2546 Mon Sep 17 00:00:00 2001 From: Aleksei Perepelkin Date: Mon, 14 Oct 2019 07:13:14 +0000 Subject: [PATCH 0279/2372] Translated using Weblate (Russian) Currently translated at 93.8% (1718 of 1831 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index ebda65764d..5b02f0307d 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -2009,8 +2009,8 @@ "Changes the avatar of the current room": "Меняет аватарку текущей комнаты", "Change identity server": "Изменить сервер идентификации", "Explore": "Исследовать", - "Filter": "Фильтровать", - "Filter rooms…": "Фильтровать комнаты…", + "Filter": "Поиск", + "Filter rooms…": "Поиск комнат…", "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Если не удаётся найти комнату, то можно запросить приглашение или Создать новую комнату.", "Room alias": "Псевдоним комнаты", "Set a room alias to easily share your room with other people.": "Присвоить комнате адрес, чтобы было проще приглашать в неё людей.", From c882b7f332d24edf46f7eb29e0c924808e74f9cb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 14 Oct 2019 10:37:10 +0100 Subject: [PATCH 0280/2372] Improve A11Y for Autocomplete Commands and DDG Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/CommandProvider.js | 8 +++++--- src/autocomplete/Components.js | 2 +- src/autocomplete/DuckDuckGoProvider.js | 12 +++++++++--- src/i18n/strings/en_EN.json | 2 ++ 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index b13680ece2..da8fa3ed3c 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -78,8 +78,10 @@ export default class CommandProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return
    - { completions } -
    ; + return ( +
    + { completions } +
    + ); } } diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js index ca105bb211..32bbeb46a0 100644 --- a/src/autocomplete/Components.js +++ b/src/autocomplete/Components.js @@ -34,7 +34,7 @@ export class TextualCompletion extends React.Component { ...restProps } = this.props; return ( -
    +
    { title } { subtitle } { description } diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index e25ef16428..49ef7dfb43 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -97,8 +97,14 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return
    - { completions } -
    ; + return ( +
    + { completions } +
    + ); } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f524a22d4b..618ac9092c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1740,8 +1740,10 @@ "Clear personal data": "Clear personal data", "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.", "Commands": "Commands", + "Command Autocomplete": "Command Autocomplete", "Community Autocomplete": "Community Autocomplete", "Results from DuckDuckGo": "Results from DuckDuckGo", + "DuckDuckGo Results": "DuckDuckGo Results", "Emoji": "Emoji", "Emoji Autocomplete": "Emoji Autocomplete", "Notify the whole room": "Notify the whole room", From d3517cdb71dfb7d5b4944f0805376569f796592b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 14 Oct 2019 10:44:42 +0100 Subject: [PATCH 0281/2372] actually pass role="tabpanel" to the DOM for FilePanel and NotifPanel Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/FilePanel.js | 22 +++++++++---------- .../structures/NotificationPanel.js | 20 ++++++++--------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index fb2bdcad42..c7e8295f80 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -117,17 +117,17 @@ const FilePanel = createReactClass({ // console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " + // "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId); return ( - +
    + +
    ); } else { return ( diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.js index 3a07bf2e63..470c7c8728 100644 --- a/src/components/structures/NotificationPanel.js +++ b/src/components/structures/NotificationPanel.js @@ -38,16 +38,16 @@ const NotificationPanel = createReactClass({ const timelineSet = MatrixClientPeg.get().getNotifTimelineSet(); if (timelineSet) { return ( - +
    + +
    ); } else { console.error("No notifTimelineSet available!"); From 2de88449aa617b9ee5f68d0423c5c42fde5aeffd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 14 Oct 2019 16:08:56 +0100 Subject: [PATCH 0282/2372] Clean up RoomSubList from stale unused code paths Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomSubList.js | 57 +++++++++++++----------- src/components/views/rooms/RoomList.js | 40 ++--------------- 2 files changed, 33 insertions(+), 64 deletions(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 3d09c05c43..60be1d7b34 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -57,8 +57,8 @@ const RoomSubList = createReactClass({ onHeaderClick: PropTypes.func, incomingCall: PropTypes.object, isFiltered: PropTypes.bool, - headerItems: PropTypes.node, // content shown in the sublist header extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles + forceExpand: PropTypes.bool, }, getInitialState: function() { @@ -299,21 +299,20 @@ const RoomSubList = createReactClass({ render: function() { const len = this.props.list.length + this.props.extraTiles.length; const isCollapsed = this.state.hidden && !this.props.forceExpand; - if (len) { - const subListClasses = classNames({ - "mx_RoomSubList": true, - "mx_RoomSubList_hidden": isCollapsed, - "mx_RoomSubList_nonEmpty": len && !isCollapsed, - }); + const subListClasses = classNames({ + "mx_RoomSubList": true, + "mx_RoomSubList_hidden": len && isCollapsed, + "mx_RoomSubList_nonEmpty": len && !isCollapsed, + }); + + let content; + if (len) { if (isCollapsed) { - return
    - {this._getHeaderJsx(isCollapsed)} -
    ; + // no body } else if (this._canUseLazyListRendering()) { - return
    - {this._getHeaderJsx(isCollapsed)} - + content = ( + -
    ; + ); } else { const roomTiles = this.props.list.map(r => this.makeRoomTile(r)); const tiles = roomTiles.concat(this.props.extraTiles); - return
    - {this._getHeaderJsx(isCollapsed)} - + content = ( + { tiles } -
    ; + ); } } else { - const Loader = sdk.getComponent("elements.Spinner"); - let content; if (this.props.showSpinner && !isCollapsed) { + const Loader = sdk.getComponent("elements.Spinner"); content = ; } - - return ( -
    - { this._getHeaderJsx(isCollapsed) } - { content } -
    - ); } + + return ( +
    + { this._getHeaderJsx(isCollapsed) } + { content } +
    + ); }, }); diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 6c031563cd..d8092eae22 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -49,21 +49,6 @@ function labelForTagName(tagName) { return tagName; } -function phraseForSection(section) { - switch (section) { - case 'm.favourite': - return _t('Drop here to favourite'); - case 'im.vector.fake.direct': - return _t('Drop here to tag direct chat'); - case 'im.vector.fake.recent': - return _t('Drop here to restore'); - case 'm.lowpriority': - return _t('Drop here to demote'); - default: - return _t('Drop here to tag %(section)s', {section: section}); - } -} - module.exports = createReactClass({ displayName: 'RoomList', @@ -203,7 +188,7 @@ module.exports = createReactClass({ this.resizer.setClassNames({ handle: "mx_ResizeHandle", vertical: "mx_ResizeHandle_vertical", - reverse: "mx_ResizeHandle_reverse" + reverse: "mx_ResizeHandle_reverse", }); this._layout.update( this._layoutSections, @@ -584,23 +569,6 @@ module.exports = createReactClass({ } }, - _getHeaderItems: function(section) { - const StartChatButton = sdk.getComponent('elements.StartChatButton'); - const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton'); - const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton'); - switch (section) { - case 'im.vector.fake.direct': - return - - ; - case 'im.vector.fake.recent': - return - - - ; - } - }, - _makeGroupInviteTiles(filter) { const ret = []; const lcFilter = filter && filter.toLowerCase(); @@ -676,7 +644,7 @@ module.exports = createReactClass({ props = Object.assign({}, defaultProps, props); const isLast = i === subListsProps.length - 1; const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0); - const {key, label, onHeaderClick, ... otherProps} = props; + const {key, label, onHeaderClick, ...otherProps} = props; const chosenKey = key || label; const onSubListHeaderClick = (collapsed) => { this._handleCollapsedState(chosenKey, collapsed); @@ -746,16 +714,14 @@ module.exports = createReactClass({ list: this.state.lists['im.vector.fake.direct'], label: _t('People'), tagName: "im.vector.fake.direct", - headerItems: this._getHeaderItems('im.vector.fake.direct'), order: "recent", incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'), - onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})}, + onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});}, addRoomLabel: _t("Start chat"), }, { list: this.state.lists['im.vector.fake.recent'], label: _t('Rooms'), - headerItems: this._getHeaderItems('im.vector.fake.recent'), order: "recent", incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'), onAddRoom: () => {dis.dispatch({action: 'view_create_room'});}, From 088c9bff9ede44bde253a8ecd1d96b70e8b83675 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 14 Oct 2019 19:40:57 +0300 Subject: [PATCH 0283/2372] Add recently used section and scroll to category Signed-off-by: Tulir Asokan --- res/css/views/emojipicker/_EmojiPicker.scss | 20 ++++-- src/components/views/emojipicker/Category.js | 16 ++--- .../views/emojipicker/EmojiPicker.js | 61 +++++++++++++++---- src/components/views/emojipicker/Header.js | 7 +-- src/components/views/emojipicker/Search.js | 7 ++- src/components/views/emojipicker/recent.js | 35 +++++++++++ src/i18n/strings/en_EN.json | 3 +- 7 files changed, 115 insertions(+), 34 deletions(-) create mode 100644 src/components/views/emojipicker/recent.js diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index 99a75b9d10..50eeb4281c 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -49,7 +49,11 @@ limitations under the License. fill: $primary-fg-color; } - &:hover { + &:disabled svg { + fill: $focus-bg-color; + } + + &:not(:disabled):hover { background-color: $focus-bg-color; border-bottom: 2px solid $button-bg-color; } @@ -73,11 +77,17 @@ limitations under the License. border-radius: 4px 0; } - svg { - align-self: center; - width: 16px; - height: 16px; + button { + border: none; + background-color: inherit; + padding: 0; margin: 8px; + + svg { + align-self: center; + width: 16px; + height: 16px; + } } } diff --git a/src/components/views/emojipicker/Category.js b/src/components/views/emojipicker/Category.js index 9573a24630..629dd3e570 100644 --- a/src/components/views/emojipicker/Category.js +++ b/src/components/views/emojipicker/Category.js @@ -23,31 +23,27 @@ class Category extends React.PureComponent { static propTypes = { emojis: PropTypes.arrayOf(PropTypes.object).isRequired, name: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, onMouseEnter: PropTypes.func.isRequired, onMouseLeave: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired, - filter: PropTypes.string, }; render() { const { onClick, onMouseEnter, onMouseLeave, emojis, name, filter } = this.props; - - const Emoji = sdk.getComponent("emojipicker.Emoji"); - const renderedEmojis = (emojis || []).map(emoji => !filter || emoji.filterString.includes(filter) ? ( - - ) : null).filter(component => component !== null); - if (renderedEmojis.length === 0) { + if (!emojis || emojis.length === 0) { return null; } + const Emoji = sdk.getComponent("emojipicker.Emoji"); return ( -
    +

    {name}

      - {renderedEmojis} + {emojis.map(emoji => )}
    ) diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.js index 0ffe3a06f7..d1f784f062 100644 --- a/src/components/views/emojipicker/EmojiPicker.js +++ b/src/components/views/emojipicker/EmojiPicker.js @@ -16,11 +16,13 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; - import EMOJIBASE from 'emojibase-data/en/compact.json'; + import sdk from '../../../index'; import { _t } from '../../../languageHandler'; +import * as recent from './recent'; + const EMOJIBASE_CATEGORY_IDS = [ "people", // smileys "people", // actually people @@ -43,11 +45,15 @@ const DATA_BY_CATEGORY = { "objects": [], "symbols": [], "flags": [], - "control": [], }; +const DATA_BY_EMOJI = {}; EMOJIBASE.forEach(emoji => { - DATA_BY_CATEGORY[EMOJIBASE_CATEGORY_IDS[emoji.group]].push(emoji); + DATA_BY_EMOJI[emoji.unicode] = emoji; + const categoryId = EMOJIBASE_CATEGORY_IDS[emoji.group]; + if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) { + DATA_BY_CATEGORY[categoryId].push(emoji); + } // This is used as the string to match the query against when filtering emojis. emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`; }); @@ -65,49 +71,76 @@ class EmojiPicker extends React.Component { previewEmoji: null, }; + this.bodyRef = React.createRef(); + + this.recentlyUsed = recent.get().map(unicode => DATA_BY_EMOJI[unicode]); + this.memoizedDataByCategory = { + recent: this.recentlyUsed, + ...DATA_BY_CATEGORY, + }; + this.categories = [{ id: "recent", name: _t("Frequently Used"), + enabled: this.recentlyUsed.length > 0, }, { id: "people", name: _t("Smileys & People"), + enabled: true, }, { id: "nature", name: _t("Animals & Nature"), + enabled: true, }, { id: "foods", name: _t("Food & Drink"), + enabled: true, }, { id: "activity", name: _t("Activities"), + enabled: true, }, { id: "places", name: _t("Travel & Places"), + enabled: true, }, { id: "objects", name: _t("Objects"), + enabled: true, }, { id: "symbols", name: _t("Symbols"), + enabled: true, }, { id: "flags", name: _t("Flags"), + enabled: true, }]; this.onChangeFilter = this.onChangeFilter.bind(this); this.onHoverEmoji = this.onHoverEmoji.bind(this); this.onHoverEmojiEnd = this.onHoverEmojiEnd.bind(this); this.onClickEmoji = this.onClickEmoji.bind(this); + this.scrollToCategory = this.scrollToCategory.bind(this); + + window.bodyRef = this.bodyRef; } - scrollToCategory() { - // TODO + scrollToCategory(category) { + const index = this.categories.findIndex(cat => cat.id === category); + this.bodyRef.current.querySelector(`[data-category-id="${category}"]`).scrollIntoView(); } - onChangeFilter(ev) { - this.setState({ - filter: ev.target.value, - }); + onChangeFilter(filter) { + for (let [id, emojis] of Object.entries(this.memoizedDataByCategory)) { + // If the new filter string includes the old filter string, we don't have to re-filter the whole dataset. + if (!filter.includes(this.state.filter)) { + emojis = id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[id]; + } + this.memoizedDataByCategory[id] = emojis.filter(emoji => emoji.filterString.includes(filter)); + this.categories.find(cat => cat.id === id).enabled = this.memoizedDataByCategory[id].length > 0; + } + this.setState({ filter }); } onHoverEmoji(emoji) { @@ -124,6 +157,10 @@ class EmojiPicker extends React.Component { onClickEmoji(emoji) { this.props.onChoose(emoji.unicode); + recent.add(emoji.unicode); + this.recentlyUsed = recent.get().map(unicode => DATA_BY_EMOJI[unicode]); + this.memoizedDataByCategory.recent = this.recentlyUsed.filter(emoji => + emoji.filterString.includes(this.state.filter)) } render() { @@ -136,10 +173,10 @@ class EmojiPicker extends React.Component {
    -
    +
    {this.categories.map(category => ( - ))}
    diff --git a/src/components/views/emojipicker/Header.js b/src/components/views/emojipicker/Header.js index d061f8559a..95af68f4a6 100644 --- a/src/components/views/emojipicker/Header.js +++ b/src/components/views/emojipicker/Header.js @@ -34,8 +34,7 @@ class Header extends React.Component { this.handleClick = this.handleClick.bind(this); } - handleClick(ev) { - const selected = ev.target.getAttribute("data-category-id"); + handleClick(selected) { this.setState({selected}); this.props.onAnchorClick(selected); }; @@ -44,9 +43,9 @@ class Header extends React.Component { return (
    - ) + ); } } diff --git a/src/components/views/emojipicker/Emoji.js b/src/components/views/emojipicker/Emoji.js index 3bbbe3a771..3db5882fb3 100644 --- a/src/components/views/emojipicker/Emoji.js +++ b/src/components/views/emojipicker/Emoji.js @@ -23,18 +23,20 @@ class Emoji extends React.PureComponent { onMouseEnter: PropTypes.func, onMouseLeave: PropTypes.func, emoji: PropTypes.object.isRequired, + selectedEmojis: PropTypes.instanceOf(Set), }; render() { - const { onClick, onMouseEnter, onMouseLeave, emoji } = this.props; + const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props; + const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode); return (
  • onClick(emoji)} onMouseEnter={() => onMouseEnter(emoji)} onMouseLeave={() => onMouseLeave(emoji)} - className="mx_EmojiPicker_item"> + className={`mx_EmojiPicker_item ${isSelected ? 'mx_EmojiPicker_item_selected' : ''}`}> {emoji.unicode}
  • - ) + ); } } diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.js index d6d79d7b8c..1d5b11edb1 100644 --- a/src/components/views/emojipicker/EmojiPicker.js +++ b/src/components/views/emojipicker/EmojiPicker.js @@ -61,7 +61,8 @@ EMOJIBASE.forEach(emoji => { class EmojiPicker extends React.Component { static propTypes = { onChoose: PropTypes.func.isRequired, - closeMenu: PropTypes.func, + selectedEmojis: PropTypes.instanceOf(Set), + showQuickReactions: PropTypes.bool, }; constructor(props) { @@ -204,10 +205,8 @@ class EmojiPicker extends React.Component { } onClickEmoji(emoji) { - recent.add(emoji.unicode); - this.props.onChoose(emoji.unicode); - if (this.props.closeMenu) { - this.props.closeMenu(); + if (this.props.onChoose(emoji.unicode) !== false) { + recent.add(emoji.unicode); } } @@ -225,14 +224,15 @@ class EmojiPicker extends React.Component { {this.categories.map(category => ( + onMouseEnter={this.onHoverEmoji} onMouseLeave={this.onHoverEmojiEnd} + selectedEmojis={this.props.selectedEmojis} /> ))}
    - {this.state.previewEmoji + {this.state.previewEmoji || !this.props.showQuickReactions ? - : } + : }
    - ) + ); } } diff --git a/src/components/views/emojipicker/Preview.js b/src/components/views/emojipicker/Preview.js index 1757a04801..75d3e35f31 100644 --- a/src/components/views/emojipicker/Preview.js +++ b/src/components/views/emojipicker/Preview.js @@ -19,21 +19,26 @@ import PropTypes from 'prop-types'; class Preview extends React.PureComponent { static propTypes = { - emoji: PropTypes.object.isRequired, + emoji: PropTypes.object, }; render() { + const { + unicode = "", + annotation = "", + shortcodes: [shortcode = ""] + } = this.props.emoji || {}; return (
    - {this.props.emoji.unicode} + {unicode}
    - {this.props.emoji.annotation} + {annotation}
    - {this.props.emoji.shortcodes[0]} + {shortcode}
    diff --git a/src/components/views/emojipicker/QuickReactions.js b/src/components/views/emojipicker/QuickReactions.js index 58c3095d34..2357345460 100644 --- a/src/components/views/emojipicker/QuickReactions.js +++ b/src/components/views/emojipicker/QuickReactions.js @@ -32,6 +32,7 @@ EMOJIBASE.forEach(emoji => { class QuickReactions extends React.Component { static propTypes = { onClick: PropTypes.func.isRequired, + selectedEmojis: PropTypes.instanceOf(Set), }; constructor(props) { @@ -72,7 +73,8 @@ class QuickReactions extends React.Component {
      {QUICK_REACTIONS.map(emoji => )} + onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} + selectedEmojis={this.props.selectedEmojis}/>)}
    ) diff --git a/src/components/views/emojipicker/ReactionPicker.js b/src/components/views/emojipicker/ReactionPicker.js new file mode 100644 index 0000000000..d539fa30e6 --- /dev/null +++ b/src/components/views/emojipicker/ReactionPicker.js @@ -0,0 +1,120 @@ +/* +Copyright 2019 Tulir Asokan + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from "prop-types"; +import EmojiPicker from "./EmojiPicker"; +import MatrixClientPeg from "../../../MatrixClientPeg"; + +class ReactionPicker extends React.Component { + static propTypes = { + mxEvent: PropTypes.object.isRequired, + onFinished: PropTypes.func.isRequired, + closeMenu: PropTypes.func.isRequired, + reactions: PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + selectedEmojis: new Set(Object.keys(this.getReactions())), + }; + this.onChoose = this.onChoose.bind(this); + this.onReactionsChange = this.onReactionsChange.bind(this); + this.addListeners(); + } + + componentDidUpdate(prevProps) { + if (prevProps.reactions !== this.props.reactions) { + this.addListeners(); + this.onReactionsChange(); + } + } + + addListeners() { + if (this.props.reactions) { + this.props.reactions.on("Relations.add", this.onReactionsChange); + this.props.reactions.on("Relations.remove", this.onReactionsChange); + this.props.reactions.on("Relations.redaction", this.onReactionsChange); + } + } + + componentWillUnmount() { + if (this.props.reactions) { + this.props.reactions.removeListener( + "Relations.add", + this.onReactionsChange, + ); + this.props.reactions.removeListener( + "Relations.remove", + this.onReactionsChange, + ); + this.props.reactions.removeListener( + "Relations.redaction", + this.onReactionsChange, + ); + } + } + + getReactions() { + if (!this.props.reactions) { + return {}; + } + const userId = MatrixClientPeg.get().getUserId(); + const myAnnotations = this.props.reactions.getAnnotationsBySender()[userId]; + return Object.fromEntries([...myAnnotations] + .filter(event => !event.isRedacted()) + .map(event => [event.getRelation().key, event.getId()])); + }; + + onReactionsChange() { + this.setState({ + selectedEmojis: new Set(Object.keys(this.getReactions())) + }); + } + + onChoose(reaction) { + this.componentWillUnmount(); + this.props.closeMenu(); + this.props.onFinished(); + const myReactions = this.getReactions(); + if (myReactions.hasOwnProperty(reaction)) { + MatrixClientPeg.get().redactEvent( + this.props.mxEvent.getRoomId(), + myReactions[reaction], + ); + // Tell the emoji picker not to bump this in the more frequently used list. + return false; + } else { + MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), "m.reaction", { + "m.relates_to": { + "rel_type": "m.annotation", + "event_id": this.props.mxEvent.getId(), + "key": reaction, + }, + }); + return true; + } + } + + render() { + return + } +} + +export default ReactionPicker diff --git a/src/components/views/emojipicker/Search.js b/src/components/views/emojipicker/Search.js index 19da7c2e6c..547f673815 100644 --- a/src/components/views/emojipicker/Search.js +++ b/src/components/views/emojipicker/Search.js @@ -45,7 +45,7 @@ class Search extends React.PureComponent { {this.props.query ? icons.search.delete() : icons.search.search()}
    - ) + ); } } diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 48554d8cc0..df1bc9a294 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -85,45 +85,9 @@ export default class MessageActionBar extends React.PureComponent { }); }; - onReactClick = (ev) => { - const EmojiPicker = sdk.getComponent('emojipicker.EmojiPicker'); + getMenuOptions = (ev) => { + const menuOptions = {}; const buttonRect = ev.target.getBoundingClientRect(); - - const getReactions = () => { - if (!this.props.reactions) { - return []; - } - const userId = MatrixClientPeg.get().getUserId(); - const myAnnotations = this.props.reactions.getAnnotationsBySender()[userId]; - return Object.fromEntries([...myAnnotations] - .filter(event => !event.isRedacted()) - .map(event => [event.getRelation().key, event.getId()])); - }; - - const menuOptions = { - reactions: this.props.reactions, - chevronFace: "none", - onFinished: () => this.onFocusChange(false), - onChoose: reaction => { - this.onFocusChange(false); - const myReactions = getReactions(); - if (myReactions.hasOwnProperty(reaction)) { - MatrixClientPeg.get().redactEvent( - this.props.mxEvent.getRoomId(), - myReactions[reaction], - ); - } else { - MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), "m.reaction", { - "m.relates_to": { - "rel_type": "m.annotation", - "event_id": this.props.mxEvent.getId(), - "key": reaction, - }, - }); - } - }, - }; - // The window X and Y offsets are to adjust position when zoomed in to page const buttonRight = buttonRect.right + window.pageXOffset; const buttonBottom = buttonRect.bottom + window.pageYOffset; @@ -137,15 +101,27 @@ export default class MessageActionBar extends React.PureComponent { } else { menuOptions.bottom = window.innerHeight - buttonTop; } + return menuOptions; + }; - createMenu(EmojiPicker, menuOptions); + onReactClick = (ev) => { + const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker'); + + const menuOptions = { + ...this.getMenuOptions(ev), + mxEvent: this.props.mxEvent, + reactions: this.props.reactions, + chevronFace: "none", + onFinished: () => this.onFocusChange(false), + }; + + createMenu(ReactionPicker, menuOptions); this.onFocusChange(true); }; onOptionsClick = (ev) => { const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); - const buttonRect = ev.target.getBoundingClientRect(); const { getTile, getReplyThread } = this.props; const tile = getTile && getTile(); @@ -157,6 +133,7 @@ export default class MessageActionBar extends React.PureComponent { } const menuOptions = { + ...this.getMenuOptions(ev), mxEvent: this.props.mxEvent, chevronFace: "none", permalinkCreator: this.props.permalinkCreator, @@ -168,20 +145,6 @@ export default class MessageActionBar extends React.PureComponent { }, }; - // The window X and Y offsets are to adjust position when zoomed in to page - const buttonRight = buttonRect.right + window.pageXOffset; - const buttonBottom = buttonRect.bottom + window.pageYOffset; - const buttonTop = buttonRect.top + window.pageYOffset; - // Align the right edge of the menu to the right edge of the button - menuOptions.right = window.innerWidth - buttonRight; - // Align the menu vertically on whichever side of the button has more - // space available. - if (buttonBottom < window.innerHeight / 2) { - menuOptions.top = buttonBottom; - } else { - menuOptions.bottom = window.innerHeight - buttonTop; - } - createMenu(MessageContextMenu, menuOptions); this.onFocusChange(true); From 6ce2e3d796ecd9d048b591eac3937c0428534bf1 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 15 Oct 2019 19:10:02 +0300 Subject: [PATCH 0292/2372] Make selected emojis more transparent Signed-off-by: Tulir Asokan --- res/css/views/emojipicker/_EmojiPicker.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index 549a7c3621..ddb3e82eca 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -134,7 +134,7 @@ limitations under the License. } .mx_EmojiPicker_item_selected { - color: rgba(0, 0, 0, .75); + color: rgba(0, 0, 0, .5); border: 1px solid $input-valid-border-color; margin: 0; } From 30ffd65b6c9e68d5934c2b72c80d8a8095ce2337 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 16 Oct 2019 21:35:08 +0300 Subject: [PATCH 0293/2372] Fix reacting to messages with reactions from other users Signed-off-by: Tulir Asokan --- src/components/views/emojipicker/ReactionPicker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/emojipicker/ReactionPicker.js b/src/components/views/emojipicker/ReactionPicker.js index d539fa30e6..4dd7ea0da7 100644 --- a/src/components/views/emojipicker/ReactionPicker.js +++ b/src/components/views/emojipicker/ReactionPicker.js @@ -75,7 +75,7 @@ class ReactionPicker extends React.Component { return {}; } const userId = MatrixClientPeg.get().getUserId(); - const myAnnotations = this.props.reactions.getAnnotationsBySender()[userId]; + const myAnnotations = this.props.reactions.getAnnotationsBySender()[userId] || []; return Object.fromEntries([...myAnnotations] .filter(event => !event.isRedacted()) .map(event => [event.getRelation().key, event.getId()])); From 3400808f6ef6497968c32561e925f8246ae878e1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 17 Oct 2019 15:53:39 +0100 Subject: [PATCH 0294/2372] Use navigation treeview aria pattern for roomlist sublists and tiles Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_RoomTile.scss | 2 + src/Keyboard.js | 2 + src/components/structures/LeftPanel.js | 1 + src/components/structures/RoomSubList.js | 74 +++++++++++++++---- .../views/elements/AccessibleButton.js | 15 +++- src/components/views/rooms/RoomList.js | 2 +- src/components/views/rooms/RoomTile.js | 3 +- src/i18n/strings/en_EN.json | 7 +- 8 files changed, 81 insertions(+), 25 deletions(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 2acddc233c..1814919b61 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -143,6 +143,8 @@ limitations under the License. // toggle menuButton and badge on hover/menu displayed .mx_RoomTile_menuDisplayed, +// or on keyboard focus of room tile +.mx_RoomTile.focus-visible:focus-within, .mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:hover { .mx_RoomTile_menuButton { display: block; diff --git a/src/Keyboard.js b/src/Keyboard.js index 738da478e4..f63956777f 100644 --- a/src/Keyboard.js +++ b/src/Keyboard.js @@ -69,6 +69,8 @@ export const Key = { BACKSPACE: "Backspace", ARROW_UP: "ArrowUp", ARROW_DOWN: "ArrowDown", + ARROW_LEFT: "ArrowLeft", + ARROW_RIGHT: "ArrowRight", TAB: "Tab", ESCAPE: "Escape", ENTER: "Enter", diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 36dd3a7a61..d1d3bb1b63 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -186,6 +186,7 @@ const LeftPanel = createReactClass({ } } while (element && !( classes.contains("mx_RoomTile") || + classes.contains("mx_RoomSubList_label") || classes.contains("mx_textinput_search"))); if (element) { diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 60be1d7b34..92b9d91e0e 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -2,6 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018, 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {createRef} from 'react'; import createReactClass from 'create-react-class'; import classNames from 'classnames'; import sdk from '../../index'; @@ -25,7 +26,7 @@ import Unread from '../../Unread'; import * as RoomNotifs from '../../RoomNotifs'; import * as FormattingUtils from '../../utils/FormattingUtils'; import IndicatorScrollbar from './IndicatorScrollbar'; -import { KeyCode } from '../../Keyboard'; +import {Key, KeyCode} from '../../Keyboard'; import { Group } from 'matrix-js-sdk'; import PropTypes from 'prop-types'; import RoomTile from "../views/rooms/RoomTile"; @@ -56,7 +57,6 @@ const RoomSubList = createReactClass({ collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed? onHeaderClick: PropTypes.func, incomingCall: PropTypes.object, - isFiltered: PropTypes.bool, extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles forceExpand: PropTypes.bool, }, @@ -80,6 +80,7 @@ const RoomSubList = createReactClass({ }, componentWillMount: function() { + this._headerButton = createRef(); this.dispatcherRef = dis.register(this.onAction); }, @@ -87,9 +88,9 @@ const RoomSubList = createReactClass({ dis.unregister(this.dispatcherRef); }, - // The header is collapsable if it is hidden or not stuck + // The header is collapsible if it is hidden or not stuck // The dataset elements are added in the RoomList _initAndPositionStickyHeaders method - isCollapsableOnClick: function() { + isCollapsibleOnClick: function() { const stuck = this.refs.header.dataset.stuck; if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) { return true; @@ -114,8 +115,8 @@ const RoomSubList = createReactClass({ }, onClick: function(ev) { - if (this.isCollapsableOnClick()) { - // The header isCollapsable, so the click is to be interpreted as collapse and truncation logic + if (this.isCollapsibleOnClick()) { + // The header isCollapsible, so the click is to be interpreted as collapse and truncation logic const isHidden = !this.state.hidden; this.setState({hidden: isHidden}, () => { this.props.onHeaderClick(isHidden); @@ -124,6 +125,30 @@ const RoomSubList = createReactClass({ // The header is stuck, so the click is to be interpreted as a scroll to the header this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition); } + this._headerButton.current.focus(); + }, + + onKeyDown: function(ev) { + switch (ev.key) { + case Key.TAB: + // Prevent LeftPanel handling Tab if focus is on the sublist header itself + ev.stopPropagation(); + break; + case Key.ARROW_LEFT: + ev.stopPropagation(); + if (!this.state.hidden && !this.props.forceExpand) { + this.onClick(); + } + break; + case Key.ARROW_RIGHT: + ev.stopPropagation(); + if (this.state.hidden && !this.props.forceExpand) { + this.onClick(); + } else { + // TODO go to first element in subtree + } + break; + } }, onRoomTileClick(roomId, ev) { @@ -193,6 +218,11 @@ const RoomSubList = createReactClass({ } }, + onAddRoom: function(e) { + e.stopPropagation(); + if (this.props.onAddRoom) this.props.onAddRoom(); + }, + _getHeaderJsx: function(isCollapsed) { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton'); @@ -209,12 +239,18 @@ const RoomSubList = createReactClass({ 'mx_RoomSubList_badgeHighlight': subListNotifHighlight, }); if (subListNotifCount > 0) { - badge =
    - { FormattingUtils.formatCount(subListNotifCount) } -
    ; + badge = ( + + { FormattingUtils.formatCount(subListNotifCount) } + + ); } else if (this.props.isInvite && this.props.list.length) { // no notifications but highlight anyway because this is an invite badge - badge =
    {this.props.list.length}
    ; + badge = ( + + { this.props.list.length } + + ); } } @@ -237,7 +273,7 @@ const RoomSubList = createReactClass({ if (this.props.onAddRoom) { addRoomButton = ( @@ -255,10 +291,17 @@ const RoomSubList = createReactClass({ chevron = (
    ); } - const tabindex = this.props.isFiltered ? "0" : "-1"; return ( -
    - +
    + { chevron } {this.props.label} { incomingCall } @@ -344,6 +387,7 @@ const RoomSubList = createReactClass({ role="group" aria-label={this.props.label} aria-expanded={!isCollapsed} + onKeyDown={this.onKeyDown} > { this._getHeaderJsx(isCollapsed) } { content } diff --git a/src/components/views/elements/AccessibleButton.js b/src/components/views/elements/AccessibleButton.js index bfc3e45246..1ccb7d0796 100644 --- a/src/components/views/elements/AccessibleButton.js +++ b/src/components/views/elements/AccessibleButton.js @@ -67,8 +67,6 @@ export default function AccessibleButton(props) { restProps.ref = restProps.inputRef; delete restProps.inputRef; - restProps.tabIndex = restProps.tabIndex || "0"; - restProps.role = restProps.role || "button"; restProps.className = (restProps.className ? restProps.className + " " : "") + "mx_AccessibleButton"; if (kind) { @@ -93,19 +91,30 @@ export default function AccessibleButton(props) { */ AccessibleButton.propTypes = { children: PropTypes.node, - inputRef: PropTypes.func, + inputRef: PropTypes.oneOfType([ + // Either a function + PropTypes.func, + // Or the instance of a DOM native element + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]), element: PropTypes.string, onClick: PropTypes.func.isRequired, // The kind of button, similar to how Bootstrap works. // See available classes for AccessibleButton for options. kind: PropTypes.string, + // The ARIA role + role: PropTypes.string, + // The tabIndex + tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), disabled: PropTypes.bool, }; AccessibleButton.defaultProps = { element: 'div', + role: 'button', + tabIndex: "0", }; AccessibleButton.displayName = "AccessibleButton"; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index d8092eae22..036f50d899 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -771,7 +771,7 @@ module.exports = createReactClass({ const subListComponents = this._mapSubListProps(subLists); return ( -
    { subListComponents }
    diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index b727abd261..1398e03b10 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -398,7 +398,8 @@ module.exports = createReactClass({ onMouseLeave={this.onMouseLeave} onContextMenu={this.onContextMenu} aria-label={ariaLabel} - role="option" + aria-selected={this.state.selected} + role="treeitem" >
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f524a22d4b..13945a9ce8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -901,11 +901,6 @@ "Forget room": "Forget room", "Search": "Search", "Share room": "Share room", - "Drop here to favourite": "Drop here to favourite", - "Drop here to tag direct chat": "Drop here to tag direct chat", - "Drop here to restore": "Drop here to restore", - "Drop here to demote": "Drop here to demote", - "Drop here to tag %(section)s": "Drop here to tag %(section)s", "Community Invites": "Community Invites", "Invites": "Invites", "Favourites": "Favourites", @@ -1653,6 +1648,8 @@ "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", "Active call": "Active call", "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", + "Jump to first unread room.": "Jump to first unread room.", + "Jump to first invite.": "Jump to first invite.", "Add room": "Add room", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", From 8b5d3b93f4b84fa7c6625ab58d27543387d99995 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 17 Oct 2019 15:59:32 +0100 Subject: [PATCH 0295/2372] Prevent double read of ARIA expanded Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomSubList.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 92b9d91e0e..d218fdf1e8 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -386,7 +386,6 @@ const RoomSubList = createReactClass({ className={subListClasses} role="group" aria-label={this.props.label} - aria-expanded={!isCollapsed} onKeyDown={this.onKeyDown} > { this._getHeaderJsx(isCollapsed) } From f09a3b42812d8216c16f534215c3d935ddd76932 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 17 Oct 2019 16:21:33 +0100 Subject: [PATCH 0296/2372] Fix outline on RoomSubList badges Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/structures/_RoomSubList.scss | 4 ++-- src/components/structures/RoomSubList.js | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index fc61395bf9..0e0d5c68af 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -67,7 +67,7 @@ limitations under the License. margin-left: 8px; } -.mx_RoomSubList_badge { +.mx_RoomSubList_badge > div { flex: 0 0 auto; border-radius: 8px; font-weight: 600; @@ -103,7 +103,7 @@ limitations under the License. } } -.mx_RoomSubList_badgeHighlight { +.mx_RoomSubList_badgeHighlight > div { color: $accent-fg-color; background-color: $warning-color; } diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index d218fdf1e8..bca6b8ad42 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -238,17 +238,22 @@ const RoomSubList = createReactClass({ 'mx_RoomSubList_badge': true, 'mx_RoomSubList_badgeHighlight': subListNotifHighlight, }); + // Wrap the contents in a div and apply styles to the child div so that the browser default outline works if (subListNotifCount > 0) { badge = ( - { FormattingUtils.formatCount(subListNotifCount) } +
    + { FormattingUtils.formatCount(subListNotifCount) } +
    ); } else if (this.props.isInvite && this.props.list.length) { // no notifications but highlight anyway because this is an invite badge badge = ( - { this.props.list.length } +
    + { this.props.list.length } +
    ); } From 3eef1bf87ec5ebdfbdcc71ee734b2d1598eabada Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 17 Oct 2019 17:03:37 +0100 Subject: [PATCH 0297/2372] Fix tabbing through room sublist Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomSubList.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index bca6b8ad42..843be3e50b 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -128,7 +128,7 @@ const RoomSubList = createReactClass({ this._headerButton.current.focus(); }, - onKeyDown: function(ev) { + onHeaderKeyDown: function(ev) { switch (ev.key) { case Key.TAB: // Prevent LeftPanel handling Tab if focus is on the sublist header itself @@ -297,7 +297,7 @@ const RoomSubList = createReactClass({ } return ( -
    +
    { this._getHeaderJsx(isCollapsed) } { content } From 1286cf287e3a50ab96f6e6f772628dfc25f64fea Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 17 Oct 2019 17:09:43 +0100 Subject: [PATCH 0298/2372] remove TODO for now Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomSubList.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 843be3e50b..a3c28a74bb 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -144,8 +144,6 @@ const RoomSubList = createReactClass({ ev.stopPropagation(); if (this.state.hidden && !this.props.forceExpand) { this.onClick(); - } else { - // TODO go to first element in subtree } break; } From afe2226cb8dd3a0f23b0d119e3371189889d4f31 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 17 Oct 2019 17:14:00 +0100 Subject: [PATCH 0299/2372] Handle ARROW_LEFT correctly on any room tile in sublist Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomSubList.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index a3c28a74bb..f97b0e5112 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -125,7 +125,6 @@ const RoomSubList = createReactClass({ // The header is stuck, so the click is to be interpreted as a scroll to the header this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition); } - this._headerButton.current.focus(); }, onHeaderKeyDown: function(ev) { @@ -134,21 +133,28 @@ const RoomSubList = createReactClass({ // Prevent LeftPanel handling Tab if focus is on the sublist header itself ev.stopPropagation(); break; - case Key.ARROW_LEFT: - ev.stopPropagation(); - if (!this.state.hidden && !this.props.forceExpand) { - this.onClick(); - } - break; case Key.ARROW_RIGHT: ev.stopPropagation(); if (this.state.hidden && !this.props.forceExpand) { this.onClick(); + } else { + // TODO go to first element in subtree } break; } }, + onKeyDown: function(ev) { + // On ARROW_LEFT collapse the room sublist + if (ev.key === Key.ARROW_LEFT) { + ev.stopPropagation(); + if (!this.state.hidden && !this.props.forceExpand) { + this.onClick(); + this._headerButton.current.focus(); + } + } + }, + onRoomTileClick(roomId, ev) { dis.dispatch({ action: 'view_room', @@ -389,6 +395,7 @@ const RoomSubList = createReactClass({ className={subListClasses} role="group" aria-label={this.props.label} + onKeyDown={this.onKeyDown} > { this._getHeaderJsx(isCollapsed) } { content } From 1586ed85f11ca67b17a094ccebec189f21b37770 Mon Sep 17 00:00:00 2001 From: Osoitz Date: Sun, 13 Oct 2019 12:16:04 +0000 Subject: [PATCH 0300/2372] Translated using Weblate (Basque) Currently translated at 100.0% (1831 of 1831 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eu/ --- src/i18n/strings/eu.json | 114 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index 341dc52484..ef2dd9fe8b 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -301,7 +301,7 @@ "%(senderName)s made future room history visible to all room members.": "%(senderName)s erabiltzaileak etorkizuneko gelaren historiala ikusgai jarri du gelako kide guztientzat.", "%(senderName)s made future room history visible to anyone.": "%(senderName)s erabiltzaileak etorkizuneko gelaren historiala ikusgai jarri du edonorentzat.", "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s erabiltzaileak etorkizuneko gelaren historiala ikusgai jarri du ezezagunentzat (%(visibility)s).", - "Manage Integrations": "Kudeatu interakzioak", + "Manage Integrations": "Kudeatu integrazioak", "Markdown is disabled": "Markdown desgaituta dago", "Markdown is enabled": "Markdown gaituta dago", "matrix-react-sdk version:": "matrix-react-sdk bertsioa:", @@ -695,7 +695,7 @@ "Add rooms to this community": "Gehitu gelak komunitate honetara", "Call Failed": "Deiak huts egin du", "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Gailu ezezagunak daude gela honetan: hauek egiaztatu gabe aurrera jarraituz gero, posiblea litzateke inork zure deia entzutea.", - "Review Devices": "Aztertu gailuak", + "Review Devices": "Berrikusi gailuak", "Call Anyway": "Deitu hala ere", "Answer Anyway": "Erantzun hala ere", "Call": "Deitu", @@ -2023,7 +2023,7 @@ "Terms": "Baldintzak", "Call failed due to misconfigured server": "Deiak huts egin du zerbitzaria gaizki konfiguratuta dagoelako", "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Eskatu zure hasiera-zerbitzariaren administratzaileari (%(homeserverDomain)s) TURN zerbitzari bat konfiguratu dezala deiek ondo funtzionatzeko.", - "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Bestela, turn.matrix.org zerbitzari publikoa erabili dezakezu, baina hau ez dahorren fidagarria izango, eta zure IP-a partekatuko du zerbitzari horrekin. Hau ezarpenetan ere kudeatu dezakezu.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Bestela, turn.matrix.org zerbitzari publikoa erabili dezakezu, baina hau ez da hain fidagarria izango, eta zure IP-a partekatuko du zerbitzari horrekin. Hau ezarpenetan ere kudeatu dezakezu.", "Try using turn.matrix.org": "Saiatu turn.matrix.org erabiltzen", "Failed to start chat": "Huts egin du txata hastean", "Messages": "Mezuak", @@ -2039,7 +2039,7 @@ "Disconnect": "Deskonektatu", "Identity Server (%(server)s)": "Identitate-zerbitzaria (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": " erabiltzen ari zara kontaktua aurkitzeko eta aurkigarria izateko. Zure identitate-zerbitzaria aldatu dezakezu azpian.", - "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Orain ez duzu identitate-zerbitzaririk aurkitzen. Kontaktuak aurkitzeko eta aurkigarria izateko, gehitu bat azpian.", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Orain ez duzu identitate-zerbitzaririk erabiltzen. Kontaktuak aurkitzeko eta aurkigarria izateko, gehitu bat azpian.", "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "Orain zerbitzariarekin partekatzen dituzu e-mail helbideak edo telefono zenbakiak. zerbitzarira konektatu beharko zara partekatzeari uzteko.", "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Zure identitate-zerbitzaritik deskonektatzean ez zara beste erabiltzaileentzat aurkigarria izango eta ezin izango dituzu besteak gonbidatu e-mail helbidea edo telefono zenbakia erabiliz.", "Integration manager offline or not accessible.": "Integrazio kudeatzailea lineaz kanpo edo ez eskuragarri.", @@ -2060,7 +2060,7 @@ "Unable to revoke sharing for phone number": "Ezin izan da partekatzea indargabetu telefono zenbakiarentzat", "Unable to share phone number": "Ezin izan da telefono zenbakia partekatu", "Please enter verification code sent via text.": "Sartu SMS bidez bidalitako egiaztatze kodea.", - "Discovery options will appear once you have added a phone number above.": "Aurkitze aukerak behin goian telefono zenbaki bat bat gehitu duzunean agertuko dira.", + "Discovery options will appear once you have added a phone number above.": "Aurkitze aukerak behin goian telefono zenbaki bat gehitu duzunean agertuko dira.", "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "SMS mezu bat bidali zaizu +%(msisdn)s zenbakira. Sartu hemen mezu horrek daukan egiaztatze-kodea.", "To verify that this device can be trusted, please check that the key you see in User Settings on that device matches the key below:": "Gailu hau fidagarria dela egiaztatzeko, egiaztatu gailu horretako Erabiltzaile ezarpenetan ikusi dezakezun gakoa beheko hau bera dela:", "Command Help": "Aginduen laguntza", @@ -2086,5 +2086,107 @@ "Deactivate user?": "Desaktibatu erabiltzailea?", "Deactivate user": "Desaktibatu erabiltzailea", "Link this email with your account in Settings to receive invites directly in Riot.": "Lotu e-mail hau zure kontuarekin gonbidapenak zuzenean Riot-en jasotzeko.", - "This invite to %(roomName)s was sent to %(email)s": "%(roomName)s gelara gonbidapen hau %(email)s helbidera bidali da" + "This invite to %(roomName)s was sent to %(email)s": "%(roomName)s gelara gonbidapen hau %(email)s helbidera bidali da", + "Add Email Address": "Gehitu e-mail helbidea", + "Add Phone Number": "Gehitu telefono zenbakia", + "Changes the avatar of the current room": "Uneko gelaren abatarra aldatzen du", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Erabili identitate-zerbitzari bat e-mail bidez gonbidatzeko. Sakatu jarraitu lehenetsitakoa erabiltzeko (%(defaultIdentityServerName)s) edo aldatu ezarpenetan.", + "Use an identity server to invite by email. Manage in Settings.": "Erabili identitate-zerbitzari bat e-mail bidez gonbidatzeko. Kudeatu ezarpenetan.", + "Use the new, faster, composer for writing messages": "Erabili mezuak idazteko tresna berri eta azkarragoa", + "Send read receipts for messages (requires compatible homeserver to disable)": "Bidali mezuentzako irakurragiriak (Hasiera-zerbitzari bateragarria behar da desgaitzeko)", + "Show previews/thumbnails for images": "Erakutsi irudien aurrebista/iruditxoak", + "Change identity server": "Aldatu identitate-zerbitzaria", + "Disconnect from the identity server and connect to instead?": "Deskonektatu identitate-zerbitzaritik eta konektatu zerbitzarira?", + "Identity server has no terms of service": "Identitate-zerbitzariak ez du erabilera baldintzarik", + "The identity server you have chosen does not have any terms of service.": "Hautatu duzun identitate-zerbitzariak ez du erabilera baldintzarik.", + "Disconnect identity server": "Deskonektatu identitate-zerbitzaritik", + "You are still sharing your personal data on the identity server .": "Oraindik informazio pertsonala partekatzen duzu identitate zerbitzarian.", + "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Deskonektatu aurretik identitate-zerbitzaritik e-mail helbideak eta telefonoak kentzea aholkatzen dizugu.", + "Disconnect anyway": "Deskonektatu hala ere", + "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "Ez baduzu erabili nahi jendea aurkitzeko eta zure kontaktuek zu aurkitzeko, idatzi beste identitate-zerbitzari bat behean.", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Identitate-zerbitzari bat erabiltzea aukerazkoa da. Identitate-zerbitzari bat ez erabiltzea erabakitzen baduzu, ezin izango zaituztete e-mail edo telefonoa erabilita aurkitu eta ezin izango dituzu besteak e-mail edo telefonoa erabiliz gonbidatu.", + "Terms of service not accepted or the integration manager is invalid.": "Erabilera baldintzak ez dira onartu edo integrazio kudeatzailea baliogabea da.", + "Integration manager has no terms of service": "Integrazio kudeatzaileak ez du erabilera baldintzarik", + "The integration manager you have chosen does not have any terms of service.": "Aukeratu duzun integrazio zerbitzariak ez du erabilera baldintzarik zehaztu.", + "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Onartu %(serverName)s identitate-zerbitzariaren erabilera baldintzak besteek zu e-mail helbidea edo telefonoa erabiliz aurkitzea ahalbidetzeko.", + "Clear cache and reload": "Garbitu cachea eta birkargatu", + "Read Marker lifetime (ms)": "Orri-markagailuaren biziraupena (ms)", + "Read Marker off-screen lifetime (ms)": "Orri-markagailuaren biziraupena pantailaz kanpo (ms)", + "A device's public name is visible to people you communicate with": "Gailuaren izen publikoa zurekin komunikatzen den jendeak ikusi dezake", + "Error changing power level requirement": "Errorea botere-maila eskaria aldatzean", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Errore bat gertatu da gelaren botere-maila eskariak aldatzean. Baieztatu baimen bahikoa duzula eta saiatu berriro.", + "Error changing power level": "Errorea botere-maila aldatzean", + "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Errore bat gertatu da erabiltzailearen botere-maila aldatzean. Baieztatu baimen nahikoa duzula eta saiatu berriro.", + "Your email address hasn't been verified yet": "Zure e-mail helbidea egiaztatu gabe dago oraindik", + "Click the link in the email you received to verify and then click continue again.": "Sakatu jaso duzun e-maileko estekan egiaztatzeko eta gero sakatu jarraitu berriro.", + "Verify the link in your inbox": "Egiaztatu zure sarrera ontzian dagoen esteka", + "Complete": "Burutu", + "No recent messages by %(user)s found": "Ez da %(user)s erabiltzailearen azken mezurik aurkitu", + "Try scrolling up in the timeline to see if there are any earlier ones.": "Saiatu denbora-lerroa gora korritzen aurreko besterik dagoen ikusteko.", + "Remove recent messages by %(user)s": "Kendu %(user)s erabiltzailearen azken mezuak", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "%(user)s erabiltzailearen %(count)s mezu kentzekotan zaude. Hau ezin da desegin. Jarraitu nahi duzu?", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "%(user)s erabiltzailearen mezu 1 kentzekotan zaude. Hau ezin da desegin. Jarraitu nahi duzu?", + "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Mezu kopuru handientzako, honek denbora behar lezake. Ez freskatu zure bezeroa bitartean.", + "Remove %(count)s messages|other": "Kendu %(count)s mezu", + "Remove %(count)s messages|one": "Kendu mezu 1", + "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Erabiltzailea desaktibatzean saioa itxiko zaio eta ezin izango du berriro hasi. Gainera, dauden gela guztietatik aterako da. Ekintza hau ezin da aurreko egoera batera ekarri. Ziur erabiltzaile hau desaktibatu nahi duzula?", + "Remove recent messages": "Kendu azken mezuak", + "Bold": "Lodia", + "Italics": "Etzana", + "Strikethrough": "Marratua", + "Code block": "Kode blokea", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "Errore bat jaso da (%(errcode)s) zure gonbidapena balioztatzen saiatzean. Informazio hau gelaren administratzaile bati pasatzen saiatu zaitezke.", + "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "%(roomName)s gelarako gonbidapena zure kontuarekin lotuta ez dagoen %(email)s helbidera bidali da", + "Use an identity server in Settings to receive invites directly in Riot.": "Erabili identitate zerbitzari bat ezarpenetan gonbidapenak zuzenean Riot-en jasotzeko.", + "Share this email in Settings to receive invites directly in Riot.": "Partekatu e-mail hau ezarpenetan gonbidapenak zuzenean Riot-en jasotzeko.", + "%(count)s unread messages including mentions.|other": "irakurri gabeko %(count)s mezu aipamenak barne.", + "%(count)s unread messages.|other": "irakurri gabeko %(count)s mezu.", + "Unread mentions.": "Irakurri gabeko aipamenak.", + "Show image": "Erakutsi irudia", + "Please create a new issue on GitHub so that we can investigate this bug.": "Sortu txosten berri bat GitHub zerbitzarian arazo hau ikertu dezagun.", + "Room alias": "Gelaren ezizena", + "e.g. my-room": "adib. nire-gela", + "Please provide a room alias": "Sartu gelaren ezizena", + "This alias is available to use": "Ezizen hau eskuragarri dago", + "This alias is already in use": "Ezizen hau jada hartuta dago", + "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "Erabili identitate-zerbitzari bat e-mail bidez gonbidatzeko. Erabili lehenetsitakoa (%(defaultIdentityServerName)s) edo gehitu bat Ezarpenak atalean.", + "Use an identity server to invite by email. Manage in Settings.": "Erabili identitate-zerbitzari bat e-mail bidez gonbidatzeko. Kudeatu Ezarpenak atalean.", + "Close dialog": "Itxi elkarrizketa-koadroa", + "Please enter a name for the room": "Sartu gelaren izena", + "Set a room alias to easily share your room with other people.": "Ezarri ezizen bat gelarentzat besteekin erraz partekatzeko.", + "This room is private, and can only be joined by invitation.": "Gela hau pribatua da, eta gonbidapena behar da elkartzeko.", + "Create a public room": "Sortu gela publikoa", + "Create a private room": "Sortu gela pribatua", + "Topic (optional)": "Mintzagaia (aukerakoa)", + "Make this room public": "Bihurtu publiko gela hau", + "Hide advanced": "Ezkutatu aurreratua", + "Show advanced": "Erakutsi aurreratua", + "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Eragotzi beste matrix hasiera-zerbitzarietako erabiltzaileak gela honetara elkartzea (Ezarpen hau ezin da gero aldatu!)", + "Please fill why you're reporting.": "Idatzi zergatik salatzen duzun.", + "Report Content to Your Homeserver Administrator": "Salatu edukia zure hasiera-zerbitzariko administratzaileari", + "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Mezu hau salatzeak bere 'gertaera ID'-a bidaliko dio hasiera-zerbitzariko administratzaileari. Gela honetako mezuak zifratuta badaude, zure hasiera-zerbitzariko administratzaileak ezin izango du mezuaren testua irakurri edo irudirik ikusi.", + "Send report": "Bidali salaketa", + "To continue you need to accept the terms of this service.": "Jarraitzeko erabilera baldintzak onartu behar dituzu.", + "Document": "Dokumentua", + "Report Content": "Salatu edukia", + "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Captcha-ren gako publikoa falta da hasiera-zerbitzariaren konfigurazioan. Eman honen berri hasiera-zerbitzariaren administratzaileari.", + "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Ezarri E-mail bat kontua berreskuratzeko. Erabili E-mail edo telefonoa aukeran zure kontaktuek aurkitu zaitzaten.", + "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Ezarri E-mail bat kontua berreskuratzeko. Erabili E-maila aukeran zure kontaktuek aurkitu zaitzaten.", + "Enter your custom homeserver URL What does this mean?": "Sartu zure hasiera-zerbitzari pertsonalizatuaren URL-a Zer esan nahi du?", + "Enter your custom identity server URL What does this mean?": "Sartu zure identitate-zerbitzari pertsonalizatuaren URL-a Zer esan nahi du?", + "Explore": "Arakatu", + "Filter": "Iragazi", + "Filter rooms…": "Iragazi gelak…", + "%(creator)s created and configured the room.": "%(creator)s erabiltzaileak gela sortu eta konfiguratu du.", + "Preview": "Aurrebista", + "View": "Ikusi", + "Find a room…": "Bilatu gela bat…", + "Find a room… (e.g. %(exampleRoom)s)": "Bilatu gela bat… (adib. %(exampleRoom)s)", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Ezin baduzu bila ari zaren gela aurkitu, eskatu gonbidapen bat edo Sortu gela berri bat.", + "Explore rooms": "Arakatu gelak", + "Community Autocomplete": "Komunitate osatze automatikoa", + "Emoji Autocomplete": "Emoji osatze automatikoa", + "Notification Autocomplete": "Jakinarazpen osatze automatikoa", + "Room Autocomplete": "Gela osatze automatikoa", + "User Autocomplete": "Erabiltzaile osatze automatikoa" } From 86d3c5e56a0102e00c8c9ae72595b3f8956c8f7f Mon Sep 17 00:00:00 2001 From: "J. A. Durieux" Date: Tue, 15 Oct 2019 17:02:28 +0000 Subject: [PATCH 0301/2372] Translated using Weblate (Dutch) Currently translated at 100.0% (1831 of 1831 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/nl/ --- src/i18n/strings/nl.json | 241 ++++++++++++++++++++------------------- 1 file changed, 123 insertions(+), 118 deletions(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 241d37b7c7..825f3c6a48 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -91,7 +91,7 @@ "Mute": "Dempen", "Notifications": "Meldingen", "Operation failed": "Handeling is mislukt", - "powered by Matrix": "mogelijk gemaakt door Matrix", + "powered by Matrix": "draait op Matrix", "Remove": "Verwijderen", "Room directory": "Gesprekscatalogus", "Settings": "Instellingen", @@ -122,7 +122,7 @@ "People": "Personen", "Permissions": "Toestemmingen", "Phone": "Telefoonnummer", - "%(senderName)s placed a %(callType)s call.": "%(senderName)s heeft een %(callType)s-oproep gemaakt.", + "%(senderName)s placed a %(callType)s call.": "%(senderName)s pleegt een %(callType)s-oproep.", "Privacy warning": "Privacywaarschuwing", "Private Chat": "Privégesprek", "Privileged Users": "Bevoorrechte gebruikers", @@ -182,7 +182,7 @@ "Deactivate Account": "Account deactiveren", "Deactivate my account": "Mijn account deactiveren", "Decline": "Weigeren", - "Decrypt %(text)s": "%(text)s ontsleutelen", + "Decrypt %(text)s": "%(text)s ontcijferen", "Decryption error": "Ontsleutelingsfout", "Delete": "Verwijderen", "Device already verified!": "Apparaat reeds geverifieerd!", @@ -208,7 +208,7 @@ "Custom level": "Aangepast niveau", "Deops user with given id": "Ontmachtigt gebruiker met de gegeven ID", "Default": "Standaard", - "Displays action": "Geeft actie weer", + "Displays action": "Toont actie", "Drop here to tag %(section)s": "Versleep hierheen om %(section)s te labellen", "Email, name or matrix ID": "E-mailadres, naam of matrix-ID", "Emoji": "Emoticons", @@ -225,12 +225,12 @@ "End-to-end encryption is in beta and may not be reliable": "End-to-endbeveiliging is nog in bèta en kan onbetrouwbaar zijn", "Enter Code": "Voer code in", "Enter passphrase": "Voer wachtwoord in", - "Error decrypting attachment": "Fout bij het ontsleutelen van de bijlage", + "Error decrypting attachment": "Fout bij het ontcijferen van de bijlage", "Error: Problem communicating with the given homeserver.": "Fout: probleem bij communicatie met de gegeven thuisserver.", "Event information": "Gebeurtenisinformatie", "Existing Call": "Bestaande oproep", - "Export": "Exporteren", - "Export E2E room keys": "E2E-gesprekssleutels exporteren", + "Export": "Wegschrijven", + "Export E2E room keys": "E2E-gesprekssleutels wegschrijven", "Failed to ban user": "Verbannen van gebruiker is mislukt", "Failed to change power level": "Wijzigen van machtsniveau is mislukt", "Failed to fetch avatar URL": "Ophalen van avatar-URL is mislukt", @@ -270,8 +270,8 @@ "Homeserver is": "Thuisserver is", "Identity Server is": "Identiteitsserver is", "I have verified my email address": "Ik heb mijn e-mailadres geverifieerd", - "Import": "Importeren", - "Import E2E room keys": "E2E-gesprekssleutels importeren", + "Import": "Inlezen", + "Import E2E room keys": "E2E-gesprekssleutels inlezen", "Incoming call from %(name)s": "Inkomende oproep van %(name)s", "Incoming video call from %(name)s": "Inkomende video-oproep van %(name)s", "Incoming voice call from %(name)s": "Inkomende spraakoproep van %(name)s", @@ -294,7 +294,7 @@ "Join as voice or video.": "Deelnemen met spraak of video.", "Join Room": "Gesprek toetreden", "%(targetName)s joined the room.": "%(targetName)s is tot het gesprek toegetreden.", - "Joins room with given alias": "Treedt tot het gesprek toe met de gegeven bijnaam", + "Joins room with given alias": "Neemt met de gegeven bijnaam aan het gesprek deel", "Jump to first unread message.": "Spring naar het eerste ongelezen bericht.", "Labs": "Experimenteel", "Last seen": "Laatst gezien", @@ -326,9 +326,9 @@ "New passwords must match each other.": "Nieuwe wachtwoorden moeten overeenkomen.", "Once encryption is enabled for a room it cannot be turned off again (for now)": "Zodra versleuteling in een ruimte is ingeschakeld kan het niet meer worden uitgeschakeld (kan later wijzigen)", "Only people who have been invited": "Alleen personen die zijn uitgenodigd", - "Please check your email and click on the link it contains. Once this is done, click continue.": "Bekijk uw e-mail en klik op de koppeling erin. Klik zodra u dit gedaan heeft op ‘Verdergaan’.", + "Please check your email and click on the link it contains. Once this is done, click continue.": "Bekijk uw e-mail en klik op de koppeling daarin. Klik vervolgens op ‘Verder gaan’.", "Power level must be positive integer.": "Machtsniveau moet een positief geheel getal zijn.", - "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s heeft zijn/haar weergavenaam (%(oldDisplayName)s) verwijderd.", + "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s heeft de weergavenaam (%(oldDisplayName)s) afgelegd.", "%(senderName)s removed their profile picture.": "%(senderName)s heeft zijn/haar profielfoto verwijderd.", "Failed to kick": "Uit het gesprek zetten is mislukt", "Press to start a chat with someone": "Druk op om een gesprek met iemand te starten", @@ -337,12 +337,12 @@ "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.": "Het wachtwoord veranderen betekent momenteel dat alle end-to-endbeveiligingssleutels op alle apparaten veranderen waardoor versleutelde gespreksgeschiedenis onleesbaar wordt, behalve als je eerst de ruimte sleutels exporteert en daarna opnieuw importeert. Dit zal in de toekomst verbeterd worden.", "Results from DuckDuckGo": "Resultaten van DuckDuckGo", "Return to login screen": "Terug naar het aanmeldscherm", - "Riot does not have permission to send you notifications - please check your browser settings": "Riot heeft geen toestemming om u meldingen te versturen - controleer uw browserinstellingen", - "Riot was not given permission to send notifications - please try again": "Riot heeft geen toestemming gekregen u meldingen te versturen - probeer het opnieuw", + "Riot does not have permission to send you notifications - please check your browser settings": "Riot heeft geen toestemming u meldingen te versturen - controleer uw browserinstellingen", + "Riot was not given permission to send notifications - please try again": "Riot kreeg geen toestemming u meldingen te sturen - probeer het opnieuw", "riot-web version:": "riot-web-versie:", "Room %(roomId)s not visible": "Gesprek %(roomId)s is niet zichtbaar", "Room Colour": "Gesprekskleur", - "Room contains unknown devices": "Het gesprek bevat onbekende apparaten", + "Room contains unknown devices": "Aan het gesprek nemen onbekende apparaten deel", "Room name (optional)": "Gespreksnaam (optioneel)", "%(roomName)s does not exist.": "%(roomName)s bestaat niet.", "%(roomName)s is not accessible at this time.": "%(roomName)s is op dit moment niet toegankelijk.", @@ -369,7 +369,7 @@ "Kick": "Uit het gesprek sturen", "Kicks user with given id": "Stuurt de gebruiker met de gegeven ID uit het gesprek", "%(senderName)s set a profile picture.": "%(senderName)s heeft een profielfoto ingesteld.", - "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s heeft zijn/haar weergavenaam ingesteld op %(displayName)s.", + "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s heeft %(displayName)s als weergavenaam aangenomen.", "Show panel": "Paneel weergeven", "Show Text Formatting Toolbar": "Tekstopmaakwerkbalk weergeven", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Tijd in 12-uursformaat weergeven (bv. 2:30pm)", @@ -445,9 +445,9 @@ "User Interface": "Gebruikersinterface", "User name": "Gebruikersnaam", "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (macht %(powerLevelNumber)s)", - "Username invalid: %(errMessage)s": "Gebruikersnaam ongeldig: %(errMessage)s", + "Username invalid: %(errMessage)s": "Ongeldige gebruikersnaam: %(errMessage)s", "Users": "Gebruikers", - "Verification Pending": "Verificatie in afwachting", + "Verification Pending": "Contrôle in afwachting", "Verification": "Verificatie", "verified": "geverifieerd", "Verified": "Geverifieerd", @@ -457,7 +457,7 @@ "VoIP conference finished.": "VoIP-vergadering beëindigd.", "VoIP conference started.": "VoIP-vergadering gestart.", "VoIP is unsupported": "VoIP wordt niet ondersteund", - "(could not connect media)": "(kan media niet verbinden)", + "(could not connect media)": "(mediaverbinding mislukt)", "(no answer)": "(geen antwoord)", "(unknown failure: %(reason)s)": "(onbekende fout: %(reason)s)", "(warning: cannot be disabled again!)": "(waarschuwing: kan niet meer uitgezet worden!)", @@ -486,10 +486,10 @@ "You have no visible notifications": "U heeft geen zichtbare meldingen", "You may wish to login with a different account, or add this email to this account.": "U kunt zich met een andere account aanmelden, of dit e-mailadres aan uw account toevoegen.", "You must register to use this functionality": "U dient u te registreren om deze functie te gebruiken", - "You need to be able to invite users to do that.": "Hiervoor moet u gebruikers kunnen uitnodigen.", + "You need to be able to invite users to do that.": "Dit vereist de bevoegdheid gebruikers uit te nodigen.", "You need to be logged in.": "Hiervoor dient u aangemeld te zijn.", "You need to enter a user name.": "Je moet een gebruikersnaam invoeren.", - "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Het lijkt erop dat uw e-mailadres op deze thuisserver niet aan een Matrix-ID gekoppeld is.", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Zo te zien is uw e-mailadres op deze thuisserver niet aan een Matrix-ID gekoppeld.", "Your password has been reset": "Je wachtwoord is gereset", "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Uw wachtwoord is gewijzigd. U zult op andere apparaten pas weer pushmeldingen ontvangen nadat u zich er opnieuw op aangemeld heeft", "You seem to be in a call, are you sure you want to quit?": "Het ziet er naar uit dat u in gesprek bent, weet u zeker dat u wilt afsluiten?", @@ -536,20 +536,20 @@ "Riot collects anonymous analytics to allow us to improve the application.": "Riot verzamelt anonieme analysegegevens die het mogelijk maken de toepassing te verbeteren.", "Passphrases must match": "Wachtwoorden moeten overeenkomen", "Passphrase must not be empty": "Wachtwoord mag niet leeg zijn", - "Export room keys": "Gesprekssleutels exporteren", + "Export room keys": "Gesprekssleutels wegschrijven", "Confirm passphrase": "Bevestig wachtwoord", - "Import room keys": "Gesprekssleutels importeren", - "File to import": "Te importeren bestand", - "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Hiermee kunt u de sleutels van uw ontvangen berichten in versleutelde gesprekken naar een lokaal bestand exporteren. U kunt dit bestand later in een andere Matrix-cliënt importeren, zodat ook die cliënt deze berichten zal kunnen ontsleutelen.", - "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Iedereen die het geëxporteerde bestand kan lezen, kan daarmee alle versleutelde berichten die u kunt zien ontsleutelen. Wees dus voorzichtig en bewaar dit bestand op een veilige plaats. Daartoe kunt u hieronder een wachtwoord invoeren, dat dan gebruikt zal worden om de geëxporteerde gegevens te versleutelen. Het is dan enkel mogelijk de gegevens in te lezen met hetzelfde wachtwoord.", - "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Hiermee kunt u de versleutelingssleutels die u uit een andere Matrix-cliënt had geëxporteerd importeren, zodat u alle berichten die het andere programma kon ontsleutelen ook hier zult kunnen lezen.", - "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Het geëxporteerde bestand is beveiligd met een wachtwoord. Voer dat wachtwoord hier in om het bestand te ontsleutelen.", - "You must join the room to see its files": "U moet tot het gesprek toetreden om de bestanden te kunnen zien", + "Import room keys": "Gesprekssleutels inlezen", + "File to import": "In te lezen bestand", + "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Hiermee kunt u de sleutels van uw ontvangen berichten in versleutelde gesprekken naar een lokaal bestand wegschrijven. Als u dat bestand dan in een andere Matrix-cliënt inleest kan die ook die berichten ontcijferen.", + "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Wie het weggeschreven bestand kan lezen, kan daarmee ook alle versleutelde berichten die u kunt zien ontcijferen - ga er dus zorgvuldig mee om! Daartoe kunt u hieronder een wachtwoord invoeren, dat dan gebruikt zal worden om het bestand te versleutelen. Het is dan enkel mogelijk de gegevens in te lezen met hetzelfde wachtwoord.", + "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Hiermee kunt u vanuit een andere Matrix-cliënt weggeschreven ontcijferingssleutels inlezen, zodat u alle berichten die de andere cliënt kon ontcijferen ook hier kunt lezen.", + "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Het weggeschreven bestand is beveiligd met een wachtwoord. Voer dat wachtwoord hier in om het bestand te ontcijferen.", + "You must join the room to see its files": "Slechts na toetreding tot het gesprek toetreden zult u de bestanden kunnen zien", "Reject all %(invitedRooms)s invites": "Alle %(invitedRooms)s-uitnodigingen weigeren", "Start new chat": "Nieuw gesprek beginnen", "Failed to invite": "Uitnodigen is mislukt", "Failed to invite user": "Uitnodigen van gebruiker is mislukt", - "Failed to invite the following users to the %(roomName)s room:": "Uitnodigen van volgende gebruikers tot gesprek %(roomName)s is mislukt:", + "Failed to invite the following users to the %(roomName)s room:": "Kon de volgende gebruikers niet uitnodigen voor gesprek %(roomName)s:", "Confirm Removal": "Verwijdering bevestigen", "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Weet u zeker dat u deze gebeurtenis wilt verwijderen? Wees u er wel van bewust dat als u een gespreksnaam of onderwerpswijziging verwijdert, u de verandering mogelijk ongedaan maakt.", "Unknown error": "Onbekende fout", @@ -561,15 +561,15 @@ "Device key": "Apparaatssleutel", "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "Klik hieronder op de knop ‘Verifiëren’ als de sleutels overeenkomen. Zo niet drukt u op de knop ‘Blokkeren’, want dan onderschept iemand berichten naar dit apparaat.", "Blacklist": "Blokkeren", - "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "Momenteel sluit u ongeverifieerde apparaten uit; om berichten naar deze apparaten te versturen moet u ze verifiëren.", + "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "Momenteel sluit u ongeverifieerde apparaten uit; om daar berichten aan te sturen dient u ze te verifiëren.", "Unblacklist": "Deblokkeren", "In future this verification process will be more sophisticated.": "In de toekomst zal dit verificatie proces meer geraffineerd zijn.", "Verify device": "Apparaat verifiëren", "I verify that the keys match": "Ik verifieer dat de sleutels overeenkomen", - "Unable to restore session": "Het is niet mogelijk de sessie te herstellen", - "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Als u reeds gebruik heeft gemaakt van een recentere versie van Riot, is uw sessie misschien onverenigbaar met deze versie. Sluit dit venster en ga terug naar de recentere versie.", - "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "We raden u aan ieder apparaat te verifiëren om vast te stellen of ze tot de rechtmatige eigenaar behoren, maar u kunt het bericht ook zonder verificatie versturen.", - "\"%(RoomName)s\" contains devices that you haven't seen before.": "‘%(RoomName)s’ bevat apparaten die u nog niet eerder heeft gezien.", + "Unable to restore session": "Sessieherstel lukt niet", + "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Als u reeds een recentere versie van Riot heeft gebruikt is uw sessie mogelijk onverenigbaar met deze versie. Sluit dit venster en ga terug naar die recentere versie.", + "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "We raden u aan ieder apparaat te verifiëren om zo vast te stellen of ze tot de rechtmatige eigenaar behoren, maar u kunt het bericht desgewenst ook zonder verificatie versturen.", + "\"%(RoomName)s\" contains devices that you haven't seen before.": "Aan ‘%(RoomName)s’ nemen apparaten deel die u nog niet eerder heeft gezien.", "Unknown devices": "Onbekende apparaten", "Unknown Address": "Onbekend adres", "Unverify": "Ontverifiëren", @@ -591,9 +591,9 @@ "Home server URL": "Thuisserver-URL", "Identity server URL": "Identiteitsserver-URL", "What does this mean?": "Wat betekent dit?", - "Error decrypting audio": "Fout bij het ontsleutelen van de audio", - "Error decrypting image": "Fout bij het ontsleutelen van de afbeelding", - "Error decrypting video": "Fout bij het ontsleutelen van de video", + "Error decrypting audio": "Fout bij het ontcijferen van de audio", + "Error decrypting image": "Fout bij het ontcijferen van de afbeelding", + "Error decrypting video": "Fout bij het ontcijferen van de video", "Add an Integration": "Voeg een integratie toe", "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "U wordt zo dadelijk naar een derdepartijwebsite gebracht zodat u de account kunt legitimeren voor gebruik met %(integrationsUrl)s. Wilt u doorgaan?", "Removed or unknown message type": "Verwijderd of onbekend berichttype", @@ -622,7 +622,7 @@ "Authentication check failed: incorrect password?": "Aanmeldingscontrole mislukt: onjuist wachtwoord?", "Disable Peer-to-Peer for 1:1 calls": "Peer-to-Peer voor 1:1 oproepen uitschakelen", "Do you want to set an email address?": "Wilt u een e-mailadres instellen?", - "This will allow you to reset your password and receive notifications.": "Hierdoor zult u uw wachtwoord opnieuw kunnen instellen en meldingen ontvangen.", + "This will allow you to reset your password and receive notifications.": "Zo kunt u een nieuw wachtwoord instellen en meldingen ontvangen.", "To return to your account in future you need to set a password": "Om in de toekomst naar je account terug te gaan moet je een wachtwoord instellen", "Skip": "Overslaan", "Start verification": "Verificatie starten", @@ -649,7 +649,7 @@ "Revoke widget access": "Widget-toegang intrekken", "Sets the room topic": "Wijzigt het ruimte-onderwerp", "The maximum permitted number of widgets have already been added to this room.": "Het maximum aan toegestane widgets voor dit gesprek is al bereikt.", - "To get started, please pick a username!": "Kies allereerst een gebruikersnaam!", + "To get started, please pick a username!": "Kies eerst een gebruikersnaam!", "Unable to create widget.": "Kan widget niet aanmaken.", "Unbans user with given id": "Ontbant de gebruiker met de gegeven ID", "You are not in this room.": "U maakt geen deel uit van dit gesprek.", @@ -835,8 +835,8 @@ "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s is weggegaan en weer toegetreden", "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s hebben hun uitnodigingen %(count)s keer afgewezen", "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s hebben hun uitnodigingen afgewezen", - "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s heeft zijn/haar uitnodiging %(count)s keer afgewezen", - "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)s heeft zijn/haar uitnodiging afgewezen", + "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s heeft de uitnodiging %(count)smaal afgewezen", + "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)s heeft de uitnodiging afgeslagen", "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "De uitnodigingen van %(severalUsers)s zijn %(count)s keer ingetrokken", "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "De uitnodigingen van %(severalUsers)s zijn ingetrokken", "%(oneUser)shad their invitation withdrawn %(count)s times|other": "De uitnodiging van %(oneUser)s is %(count)s keer ingetrokken", @@ -858,12 +858,12 @@ "was kicked %(count)s times|one": "is uit het gesprek gezet", "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s hebben hun naam %(count)s keer gewijzigd", "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s hebben hun naam gewijzigd", - "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s heeft zijn/haar naam %(count)s keer gewijzigd", - "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s heeft zijn/haar naam gewijzigd", + "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s is %(count)smaal van naam veranderd", + "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s is van naam veranderd", "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s hebben hun avatar %(count)s keer gewijzigd", "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s hebben hun avatar gewijzigd", - "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s heeft zijn/haar avatar %(count)s keer gewijzigd", - "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s heeft zijn/haar avatar gewijzigd", + "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s is %(count)smaal van profielfoto veranderd", + "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s is van profielfoto veranderd", "%(items)s and %(count)s others|other": "%(items)s en %(count)s andere", "%(items)s and %(count)s others|one": "%(items)s en één ander", "collapse": "dichtvouwen", @@ -907,7 +907,7 @@ "Leave %(groupName)s?": "%(groupName)s verlaten?", "Leave": "Verlaten", "Community Settings": "Gemeenschapsinstellingen", - "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Deze gesprekken worden aan gemeenschapsleden getoond op de gemeenschapspagina. Gemeenschapsleden kunnen tot de gesprekken toetreden door er op te klikken.", + "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Op de gemeenschapspagina worden deze gesprekken getoond aan gemeenschapsleden, die er dan aan kunnen deelnemen door erop te klikken.", "%(inviter)s has invited you to join this community": "%(inviter)s heeft u uitgenodigd in deze gemeenschap", "You are an administrator of this community": "U bent een beheerder van deze gemeenschap", "You are a member of this community": "U bent lid van deze gemeenschap", @@ -918,7 +918,7 @@ "This Home server does not support communities": "Deze Thuisserver ondersteunt geen gemeenschappen", "Failed to load %(groupId)s": "Laden van %(groupId)s is mislukt", "Old cryptography data detected": "Oude cryptografiegegevens gedetecteerd", - "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Er zijn gegevens van een oudere versie van Riot gedetecteerd. Dit zal problemen veroorzaakt hebben met de eind-tot-eind-versleuteling in de oude versie. Eind-tot-eind-versleutelde berichten die recent uitgewisseld zijn met de oude versie zijn mogelijk niet te ontsleutelen in deze versie. Dit zou er ook voor kunnen zorgen dat berichten die uitgewisseld zijn in deze versie falen. Meld u opnieuw aan als u problemen zou ervaren. Exporteer de sleutels en importeer ze achteraf weer om de berichtgeschiedenis te behouden.", + "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Er zijn gegevens van een oudere versie van Riot gevonden, die problemen veroorzaakt hebben met de eind-tot-eind-versleuteling in de oude versie. Onlangs vanuit de oude versie verzonden eind-tot-eind-versleutelde berichten zijn mogelijk onontcijferbaar in deze versie. Ook kunnen berichten die met deze versie gewisseld zijn falen. Mocht u problemen ervaren, meld u dan opnieuw aan. Schrijf uw de sleutels weg en lees ze weer in om uw berichtgeschiedenis te behouden.", "Your Communities": "Uw gemeenschappen", "Error whilst fetching joined communities": "Er is een fout opgetreden bij het ophalen van de gemeenschappen waarvan u lid bent", "Create a new community": "Maak een nieuwe gemeenschap aan", @@ -956,11 +956,11 @@ "In reply to ": "Als antwoord op ", "This room is not public. You will not be able to rejoin without an invite.": "Dit is geen openbaar gesprek. Slechts op uitnodiging zult u opnieuw kunnen toetreden.", "were unbanned %(count)s times|one": "zijn ontbannen", - "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s heeft zijn/haar weergavenaam gewijzigd naar %(displayName)s.", + "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s heeft %(displayName)s als weergavenaam aangenomen.", "Disable Community Filter Panel": "Gemeenschapsfilterpaneel uitzetten", "Your key share request has been sent - please check your other devices for key share requests.": "Uw sleuteldeelverzoek is verstuurd - controleer uw andere apparaten voor sleuteldeelverzoeken.", "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "Sleuteldeelverzoeken worden automatisch naar andere apparaten verstuurd. Als u het verzoek heeft afgewezen of gesloten, klik dan hier om de sleutels van deze sessie opnieuw aan te vragen.", - "If your other devices do not have the key for this message you will not be able to decrypt them.": "U zult dit bericht niet kunnen ontsleutelen als geen van uw andere apparaten er de sleutel voor heeft.", + "If your other devices do not have the key for this message you will not be able to decrypt them.": "U zult dit bericht niet kunnen ontcijferen als geen van uw andere apparaten er de sleutel voor heeft.", "Key request sent.": "Sleutelverzoek verstuurd.", "Re-request encryption keys from your other devices.": "Versleutelingssleutels opnieuw aanvragen van uw andere apparaten.", "%(user)s is a %(userRole)s": "%(user)s is een %(userRole)s", @@ -985,7 +985,7 @@ "Everyone": "Iedereen", "Leave this community": "Deze gemeenschap verlaten", "Debug Logs Submission": "Debug Logs Indienen", - "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Als u een bug via GitHub heeft ingediend, kunnen foutopsporingslogboeken ons helpen het probleem te vinden. Foutopsporingslogboeken bevatten gebruiksgegevens van de toepassing, waaronder uw gebruikersnaam, de ID’s of bijnamen van de gesprekken en groepen die u heeft bezocht, evenals de gebruikersnamen van andere gebruikers. Ze bevatten geen berichten.", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Bij het oplossen van in GitHub gemelde problemen helpen foutopsporingslogboeken ons enorm. Deze bevatten wel gebruiksgegevens (waaronder uw gebruikersnaam, de ID’s of bijnamen van de gesprekken en groepen die u heeft bezocht, en de namen van andere gebruikers), maar geen berichten.", "Submit debug logs": "Foutopsporingslogboeken indienen", "Opens the Developer Tools dialog": "Opent het dialoogvenster met ontwikkelaarsgereedschap", "Fetching third party location failed": "Het ophalen van de locatie van de derde partij is mislukt", @@ -993,7 +993,7 @@ "I understand the risks and wish to continue": "Ik begrijp de risico’s en wil graag verdergaan", "Couldn't load home page": "Kon de home pagina niet laden", "Send Account Data": "Accountgegevens versturen", - "All notifications are currently disabled for all targets.": "Alle meldingen zijn momenteel uitgeschakeld voor alle bestemmingen.", + "All notifications are currently disabled for all targets.": "Alle meldingen voor alle bestemmingen staan momenteel uit.", "Uploading report": "Rapport wordt geüpload", "Sunday": "Zondag", "Notification targets": "Meldingsbestemmingen", @@ -1013,7 +1013,7 @@ "Send Custom Event": "Aangepaste gebeurtenis versturen", "Advanced notification settings": "Geavanceerde meldingsinstellingen", "delete the alias.": "verwijder de bijnaam.", - "To return to your account in future you need to set a password": "Om in de toekomst naar uw account terug te kunnen dient u een wachtwoord in te stellen", + "To return to your account in future you need to set a password": "Tenzij u een wachtwoord instelt zult u uw account niet meer kunnen benaderen", "Forget": "Vergeten", "#example": "#voorbeeld", "Hide panel": "Paneel verbergen", @@ -1173,7 +1173,7 @@ "Clear Storage and Sign Out": "Opslag wissen en afmelden", "Send Logs": "Logboek versturen", "Refresh": "Herladen", - "We encountered an error trying to restore your previous session.": "Er is een fout opgetreden bij het herstellen van uw vorige sessie.", + "We encountered an error trying to restore your previous session.": "Het herstel van uw vorige sessie is mislukt.", "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Het legen van de opslag van uw browser zal het probleem misschien verhelpen, maar zal u ook afmelden en uw gehele versleutelde gespreksgeschiedenis onleesbaar maken.", "Collapse Reply Thread": "Reactieketting dichtvouwen", "Can't leave Server Notices room": "Kan servermeldingsgesprek niet verlaten", @@ -1217,7 +1217,7 @@ "Share User": "Gebruiker delen", "Share Community": "Gemeenschap delen", "Share Room Message": "Bericht uit gesprek delen", - "Link to selected message": "Koppeling naar geselecteerde bericht", + "Link to selected message": "Koppeling naar geselecteerd bericht", "COPY": "KOPIËREN", "Share Message": "Bericht delen", "You can't send any messages until you review and agree to our terms and conditions.": "U kunt geen berichten sturen totdat u onze algemene voorwaarden heeft gelezen en aanvaard.", @@ -1243,7 +1243,7 @@ "Gets or sets the room topic": "Verkrijgt het onderwerp van het gesprek of stelt het in", "This room has no topic.": "Dit gesprek heeft geen onderwerp.", "Sets the room name": "Stelt de gespreksnaam in", - "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s heeft dit gesprek geactualiseerd.", + "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s heeft dit gesprek bijgewerkt.", "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s heeft het gesprek toegankelijk gemaakt voor iedereen die de verwijzing kent.", "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s heeft het gesprek enkel op uitnodiging toegankelijk gemaakt.", "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s heeft de toegangsregel veranderd naar ‘%(rule)s’", @@ -1275,14 +1275,14 @@ "Unknown server error": "Onbekende serverfout", "Use a few words, avoid common phrases": "Gebruik enkele woorden - maar geen bekende uitdrukkingen", "No need for symbols, digits, or uppercase letters": "Hoofdletters, cijfers of speciale tekens hoeven niet, mogen wel", - "Use a longer keyboard pattern with more turns": "Gebruik een langer patroon met meer variatie", + "Use a longer keyboard pattern with more turns": "Gebruik een langere en onvoorspelbaardere tekenreeks", "Avoid repeated words and characters": "Vermijd herhaling van woorden of tekens", "Avoid sequences": "Vermijd rijtjes", "Avoid recent years": "Vermijd recente jaren", "Avoid years that are associated with you": "Vermijd jaren die op uzelf betrekking hebben", "Avoid dates and years that are associated with you": "Vermijd data en jaren die op uzelf betrekking hebben", "Capitalization doesn't help very much": "Schrijven in hoofdletters maakt niet veel uit", - "All-uppercase is almost as easy to guess as all-lowercase": "Tekst in hoofdletters is vrijwel even gemakkelijk te raden als tekst in kleine letters", + "All-uppercase is almost as easy to guess as all-lowercase": "Enkel hoofdletters is nauwelijks moeilijker te raden als enkel kleine letters", "Reversed words aren't much harder to guess": "Omgedraaide woorden zijn bijna even gemakkelijk te raden", "Predictable substitutions like '@' instead of 'a' don't help very much": "Voorspelbare vervangingen (zoals '@' i.p.v. 'a') zijn niet erg zinvol", "Add another word or two. Uncommon words are better.": "Voeg nog een paar (liefst weinig gebruikte) woorden toe.", @@ -1353,7 +1353,7 @@ "Flower": "Bloem", "Tree": "Boom", "Cactus": "Cactus", - "Mushroom": "Paddestoel", + "Mushroom": "Paddenstoel", "Globe": "Wereldbol", "Moon": "Maan", "Cloud": "Wolk", @@ -1420,8 +1420,8 @@ "This backup is trusted because it has been restored on this device": "Deze back-up wordt vertrouwd, omdat hij op dit apparaat hersteld is", "Backup version: ": "Back-upversie: ", "Algorithm: ": "Algoritme: ", - "Chat with Riot Bot": "Chatten met Riot-robot", - "Forces the current outbound group session in an encrypted room to be discarded": "Dwingt de huidige uitwaartse groepssessie in een versleuteld gesprek om verworpen te worden", + "Chat with Riot Bot": "Met Riot-robot chatten", + "Forces the current outbound group session in an encrypted room to be discarded": "Dwingt tot verwerping van de huidige uitwaartse groepssessie in een versleuteld gesprek", "Backup has a signature from unknown device with ID %(deviceId)s.": "De back-up heeft een ondertekening van een onbekend apparaat met ID %(deviceId)s.", "Backup has a valid signature from verified device ": "De back-up heeft een geldige ondertekening van een geverifieerd apparaat ", "Backup has a valid signature from unverified device ": "De back-up heeft een geldige ondertekening van een ongeverifieerd apparaat ", @@ -1435,7 +1435,7 @@ "Phone Number": "Telefoonnummer", "Profile picture": "Profielfoto", "Upload profile picture": "Profielfoto uploaden", - "Upgrade to your own domain": "Opwaardeer naar uw eigen domein", + "Upgrade to your own domain": "Waardeer op naar uw eigen domein", "Display Name": "Weergavenaam", "Set a new account password...": "Stel een nieuw accountwachtwoord in…", "Email addresses": "E-mailadressen", @@ -1492,7 +1492,7 @@ "Roles & Permissions": "Rollen & toestemmingen", "Select the roles required to change various parts of the room": "Selecteer de rollen vereist om verschillende delen van het gesprek te wijzigen", "Enable encryption?": "Versleuteling inschakelen?", - "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Van zodra versleuteling voor een gesprek is ingeschakeld, kan dit niet meer worden uitgeschakeld. Berichten die in een versleuteld gesprek worden verstuurd worden niet gezien door de server, enkel door de deelnemers aan het gesprek. Door versleuteling in te schakelen kunnen veel robots en overbruggingen niet correct functioneren. Lees meer over versleuteling.", + "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Gespreksversleuteling is onomkeerbaar. Berichten in versleutelde gesprekken zijn niet leesbaar voor de server; enkel voor de gespreksdeelnemers. Veel robots en overbruggingen werken niet correct in versleutelde gesprekken. Lees meer over versleuteling.", "To link to this room, please add an alias.": "Voeg een bijnaam toe om naar dit gesprek te verwijzen.", "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Wijzigingen aan wie de geschiedenis kan lezen gelden enkel voor toekomstige berichten in dit gesprek. De zichtbaarheid van de bestaande geschiedenis blijft ongewijzigd.", "Encryption": "Versleuteling", @@ -1546,10 +1546,10 @@ "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Kan geen profielen voor de Matrix-ID’s hieronder vinden - wilt u ze toch uitnodigen?", "Invite anyway and never warn me again": "Alsnog uitnodigen en mij nooit meer waarschuwen", "Invite anyway": "Alsnog uitnodigen", - "Before submitting logs, you must create a GitHub issue to describe your problem.": "Vooraleer u logboeken indient, dient u een melding te openen op GitHub waarin u uw probleem beschrijft.", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "Vooraleer u logboeken indient, dient u uw probleem te melden op GitHub.", "What GitHub issue are these logs for?": "Voor welke melding op GitHub zijn deze logboeken?", "Unable to load commit detail: %(msg)s": "Kan commitdetail niet laden: %(msg)s", - "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Om uw gespreksgeschiedenis niet te verliezen, moet u uw gesprekssleutels exporteren vooraleer u zich afmeldt. Hiertoe zult u moeten terugkeren naar de nieuwere versie van Riot", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Om uw gespreksgeschiedenis niet te verliezen, moet u vóór het afmelden uw gesprekssleutels wegschrijven. Dat moet vanuit de nieuwere versie van Riot", "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "U heeft eerder een nieuwere versie van Riot op %(host)s gebruikt. Om deze versie opnieuw met eind-tot-eind-versleuteling te gebruiken, zult u zich moeten afmelden en opnieuw aanmelden. ", "Incompatible Database": "Incompatibele database", "Continue With Encryption Disabled": "Verdergaan met versleuteling uitgeschakeld", @@ -1571,34 +1571,34 @@ "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot verbruikt nu 3-5x minder geheugen, door informatie over andere gebruikers enkel te laden wanneer nodig. Even geduld, we hersynchroniseren met de server!", "Updating Riot": "Riot wordt bijgewerkt", "I don't want my encrypted messages": "Ik wil mijn versleutelde berichten niet", - "Manually export keys": "Sleutels handmatig exporteren", + "Manually export keys": "Sleutels handmatig wegschrijven", "You'll lose access to your encrypted messages": "U zult de toegang tot uw versleutelde berichten verliezen", "Are you sure you want to sign out?": "Weet u zeker dat u zich wilt afmelden?", "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "Als u fouten zou tegenkomen of voorstellen zou hebben, laat het ons dan weten op GitHub.", - "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "Bekijk eerst de bestaande meldingen (en voeg een +1 toe waar u dat wenst), of maak een nieuwe melding aan indien u er geen kunt vinden, zo voorkomt u dat u een duplicate melding indient.", + "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "Voorkom dubbele meldingen: doorzoek eerst de bestaande meldingen (en voeg desgewenst een +1 toe). Maak enkel een nieuwe melding aan indien u niets kunt vinden.", "Report bugs & give feedback": "Fouten melden & feedback geven", "Go back": "Terug", "Room Settings - %(roomName)s": "Gespreksinstellingen - %(roomName)s", - "Failed to upgrade room": "Actualiseren van gesprek is mislukt", - "The room upgrade could not be completed": "De gespreksactualisering kon niet voltooid worden", - "Upgrade this room to version %(version)s": "Actualiseer dit gesprek naar versie %(version)s", - "Upgrade Room Version": "Gespreksversie actualiseren", + "Failed to upgrade room": "Gesprek bijwerken mislukt", + "The room upgrade could not be completed": "Het bijwerken van het gesprek kon niet voltooid worden", + "Upgrade this room to version %(version)s": "Werk dit gesprek bij tot versie %(version)s", + "Upgrade Room Version": "Gespreksversie bijwerken", "Upgrading this room requires closing down the current instance of the room and creating a new room it its place. To give room members the best possible experience, we will:": "Actualisatie van dit gesprek zal de huidige versie ervan sluiten, en in plaats daarvan een nieuw gesprek aanmaken. Om dit voor de deelnemers zo vlot mogelijk te laten verlopen zullen we:", "Create a new room with the same name, description and avatar": "Een nieuw gesprek aanmaken met dezelfde naam, beschrijving en avatar", - "Update any local room aliases to point to the new room": "Alle lokale gespreksbijnamen bijwerken om naar het nieuwe gesprek te verwijzen", - "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Gebruikers verhinderen van te praten in de oude versie van het gesprek, en er een bericht in plaatsen waarin de gebruikers worden aanbevolen zich naar het nieuwe gesprek te begeven", - "Put a link back to the old room at the start of the new room so people can see old messages": "Een verwijzing naar het oude gesprek plaatsen aan het begin van het nieuwe gesprek, zodat mensen oude berichten kunnen zien", - "A username can only contain lower case letters, numbers and '=_-./'": "Een gebruikersnaam kan enkel kleine letters, cijfers en ‘=_-./’ bevatten", + "Update any local room aliases to point to the new room": "Alle lokale gespreksbijnamen naar het nieuwe gesprek laten verwijzen", + "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Gebruikers verhinderen aan de oude versie van het gesprek bij te dragen, en daar een bericht plaatsen dat de gebruikers verwijst naar het nieuwe gesprek", + "Put a link back to the old room at the start of the new room so people can see old messages": "Bovenaan het nieuwe gesprek naar het oude te verwijzen, om oude berichten te lezen", + "A username can only contain lower case letters, numbers and '=_-./'": "Een gebruikersnaam mag enkel kleine letters, cijfers en ‘=_-./’ bevatten", "Checking...": "Bezig met controleren…", "Unable to load backup status": "Kan back-upstatus niet laden", "Recovery Key Mismatch": "Herstelsleutel komt niet overeen", - "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "De back-up kon met deze sleutel niet ontsleuteld worden: controleer of u de juiste herstelsleutel heeft ingevoerd.", + "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "De back-up kon met deze sleutel niet ontcijferd worden: controleer of u de juiste herstelsleutel heeft ingevoerd.", "Incorrect Recovery Passphrase": "Onjuist herstelwachtwoord", - "Backup could not be decrypted with this passphrase: please verify that you entered the correct recovery passphrase.": "De back-up kon met dit wachtwoord niet ontsleuteld worden: controleer of u het juiste herstelwachtwoord heeft ingevoerd.", - "Unable to restore backup": "Kan back-up niet herstellen", + "Backup could not be decrypted with this passphrase: please verify that you entered the correct recovery passphrase.": "De back-up kon met dit wachtwoord niet ontcijferd worden: controleer of u het juiste herstelwachtwoord heeft ingevoerd.", + "Unable to restore backup": "Kan back-up niet terugzetten", "No backup found!": "Geen back-up gevonden!", "Backup Restored": "Back-up hersteld", - "Failed to decrypt %(failedCount)s sessions!": "Ontsleutelen van %(failedCount)s sessies is mislukt!", + "Failed to decrypt %(failedCount)s sessions!": "Ontcijferen van %(failedCount)s sessies is mislukt!", "Restored %(sessionCount)s session keys": "%(sessionCount)s sessiesleutels hersteld", "Enter Recovery Passphrase": "Voer het herstelwachtwoord in", "Warning: you should only set up key backup from a trusted computer.": "Let op: stel sleutelback-up enkel in op een vertrouwde computer.", @@ -1618,7 +1618,7 @@ "Hide": "Verbergen", "This homeserver would like to make sure you are not a robot.": "Deze thuisserver wil graag weten of u geen robot bent.", "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use this app with an existing Matrix account on a different homeserver.": "U kunt de aangepaste serveropties gebruiken om u aan te melden bij andere Matrix-servers, door een andere thuisserver-URL op te geven. Dit biedt u de mogelijkheid om deze toepassing te gebruiken met een bestaande Matrix-account op een andere thuisserver.", - "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "U kunt ook een aangepaste identiteitsserver instellen, maar u zult geen gebruikers kunnen uitnodigen via e-mail, of zelf via e-mail uitgenodigd worden.", + "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "U kunt ook een aangepaste identiteitsserver instellen, maar u kunt dan geen anderen uitnodigen of zelf uitgenodigd worden op e-mailadres.", "Please review and accept all of the homeserver's policies": "Gelieve het beleid van de thuisserver te doornemen en aanvaarden", "Please review and accept the policies of this homeserver:": "Gelieve het beleid van deze thuisserver te doornemen en aanvaarden:", "Your Modular server": "Uw Modular-server", @@ -1737,22 +1737,22 @@ "This device has detected that your recovery passphrase and key for Secure Messages have been removed.": "Dit apparaat heeft gedetecteerd dat uw herstelwachtwoord en -sleutel voor beveiligde berichten verwijderd zijn.", "If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.": "Als u dit per ongeluk heeft gedaan, kunt u beveiligde berichten instellen op dit apparaat, waarmee de berichtgeschiedenis van dit apparaat herversleuteld zal worden met een nieuwe herstelmethode.", "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Als u de herstelmethode niet heeft verwijderd, is het mogelijk dat er een aanvaller toegang tot uw account probeert te verkrijgen. Wijzig onmiddellijk uw accountwachtwoord en stel een nieuwe herstelmethode in in de instellingen.", - "Room upgrade confirmation": "Bevestiging voor actualiseren van gesprek", - "Upgrading a room can be destructive and isn't always necessary.": "Het opwaarderen van een gesprek is mogelijk destructief en is niet altijd noodzakelijk.", + "Room upgrade confirmation": "Bevestiging voor bijwerken van gesprek", + "Upgrading a room can be destructive and isn't always necessary.": "Het bijwerken van een gesprek is niet altijd nodig, en soms destructief.", "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Gespreksopwaarderingen worden meestal aanbevolen wanneer een bepaalde groepsgespreksversie als onstabiel wordt beschouwd. Onstabiele groepsgespreksversies bevatten mogelijk bugs of beveiligingsproblemen, of beschikken niet over alle functies.", "Room upgrades usually only affect server-side processing of the room. If you're having problems with your Riot client, please file an issue with .": "Gespreksopwaarderingen beïnvloeden meestal enkel de verwerking van het gesprek aan serverzijde. Indien u problemen ondervindt met uw Riot-cliënt, gelieve dit dan te melden op .", - "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Let op: het opwaarderen van een gesprek zal gespreksleden niet automatisch verplaatsen naar de nieuwe versie van het gesprek. We zullen een koppeling naar het nieuwe gesprek in de oude versie van het gesprek plaatsen - gespreksleden zullen dan op deze koppeling moeten klikken om het nieuwe gesprek toe te treden.", + "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Let op: gesprekken bijwerken voegt gespreksleden niet automatisch toe aan de nieuwe versie van het gesprek. Er komt in het oude gesprek een koppeling naar het nieuwe, waarop gespreksleden moeten klikken om aan het nieuwe gesprek deel te nemen.", "Please confirm that you'd like to go forward with upgrading this room from to ": "Gelieve te bevestigen dat u dit gesprek wilt opwaarderen van naar ", - "Upgrade": "Actualiseren", + "Upgrade": "Bijwerken", "Adds a custom widget by URL to the room": "Voegt met een URL een aangepaste widget toe aan het gesprek", "Please supply a https:// or http:// widget URL": "Voer een https://- of http://-widget-URL in", "You cannot modify widgets in this room.": "U kunt de widgets in dit gesprek niet aanpassen.", "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s heeft de uitnodiging voor %(targetDisplayName)s om toe te treden tot het gesprek ingetrokken.", "Enable desktop notifications for this device": "Bureaubladmeldingen inschakelen voor dit apparaat", "Enable audible notifications for this device": "Geluidsmeldingen inschakelen voor dit apparaat", - "Upgrade this room to the recommended room version": "Actualiseer dit gesprek naar de aanbevolen gespreksversie", + "Upgrade this room to the recommended room version": "Werk dit gesprek bij tot de aanbevolen versie", "This room is running room version , which this homeserver has marked as unstable.": "Dit gesprek draait op groepsgespreksversie , die door deze thuisserver als onstabiel is gemarkeerd.", - "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Het actualiseren van dit gesprek zal de huidige versie ervan sluiten, en een geactualiseerde versie onder dezelfde naam aanmaken.", + "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Bijwerken zal de huidige versie van dit gesprek sluiten, en onder dezelfde naam een bijgewerkte versie starten.", "Failed to revoke invite": "Intrekken van uitnodiging is mislukt", "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Kon de uitnodiging niet intrekken. De server ondervindt mogelijk een tijdelijk probleem, of u heeft niet het recht de uitnodiging in te trekken.", "Revoke invite": "Uitnodiging intrekken", @@ -1762,16 +1762,16 @@ "Riot has run into a problem which makes it difficult to show you your messages right now. Nothing has been lost and reloading the app should fix this for you. In order to assist us in troubleshooting the problem, we'd like to take a look at your debug logs. You do not need to send your logs unless you want to, but we would really appreciate it if you did. We'd also like to apologize for having to show this message to you - we hope your debug logs are the key to solving the issue once and for all. If you'd like more information on the bug you've accidentally run into, please visit the issue.": "Riot heeft een probleem ondervonden waardoor het uw berichten momenteel moeilijk kan weergeven. Geen zorgen, er is niets verloren geraakt, en de toepassing opnieuw opstarten zou dit voor u moeten oplossen. Om ons te helpen bij het onderzoeken van het probleem, zouden we graag uw foutopsporingslogboeken inkijken. U hoeft ons deze niet te tsuren, maar als u dit doet zouden we u erg dankbaar zijn. We willen ons ook verontschuldigen dat u dit bericht te zien krijgt - we hopen dat uw logboeken ons kunnen helpen om dit probleem eens en voor altijd op te lossen. Als u meer informatie wilt over het probleem dat u bent tegengekomen, bekijk dan deze foutmelding.", "Send debug logs and reload Riot": "Foutopsporingslogboeken versturen en Riot herstarten", "Reload Riot without sending logs": "Riot herstarten zonder logboeken te versturen", - "A widget would like to verify your identity": "Een widget zou graag uw identiteit willen verifiëren", - "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "Een widget op %(widgetUrl)s zou graag uw identiteit willen verifiëren. Door dit toe te staan, zal de widget uw gebruikers-ID kunnen verifiëren, maar geen handelingen als u kunnen uitvoeren.", + "A widget would like to verify your identity": "Een widget wil uw identiteit nagaan", + "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "Een widget op %(widgetUrl)s wil uw identiteit nagaan. Staat u dit toe, dan zal de widget wel uw gebruikers-ID kunnen nagaan, maar niet als u kunnen handelen.", "Remember my selection for this widget": "Onthoud mijn keuze voor deze widget", "Deny": "Weigeren", "Riot failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "Riot kon de protocollijst niet ophalen van de thuisserver. Mogelijk is de thuisserver te oud om derdepartijnetwerken te ondersteunen.", "Riot failed to get the public room list.": "Riot kon de lijst met openbare gesprekken niet verkrijgen.", "The homeserver may be unavailable or overloaded.": "De thuisserver is mogelijk onbereikbaar of overbelast.", - "You have %(count)s unread notifications in a prior version of this room.|other": "U heeft %(count)s ongelezen meldingen in een eerdere versie van dit gesprek.", - "You have %(count)s unread notifications in a prior version of this room.|one": "U heeft %(count)s ongelezen melding in een eerdere versie van dit gesprek.", - "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Of u de 'broodkruimels'-functie al dan niet gebruikt (avatars boven de gesprekslijst)", + "You have %(count)s unread notifications in a prior version of this room.|other": "U heeft %(count)s ongelezen meldingen in een vorige versie van dit gesprek.", + "You have %(count)s unread notifications in a prior version of this room.|one": "U heeft %(count)s ongelezen meldingen in een vorige versie van dit gesprek.", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Of u de icoontjes voor recente gesprekken (boven de gesprekkenlijst) al dan niet gebruikt", "Replying With Files": "Beantwoorden met bestanden", "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Momenteel is het niet mogelijk om met een bestand te antwoorden. Wilt u dit bestand uploaden zonder te antwoorden?", "The file '%(fileName)s' failed to upload.": "Het bestand ‘%(fileName)s’ kon niet geüpload worden.", @@ -1780,27 +1780,27 @@ "Rotate clockwise": "Met de klok mee draaien", "GitHub issue": "GitHub-melding", "Notes": "Opmerkingen", - "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Indien extra context zou kunnen helpen het probleem te analyseren (wat u aan het doen was, relevante gespreks-ID’s, gebruikers-ID’s, enz.), gelieve deze informatie dan hier mee te geven.", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Gelieve alle verdere informatie die zou kunnen helpen het probleem te analyseren (wat u aan het doen was, relevante gespreks-ID’s, gebruikers-ID’s, enz.) bij te voegen.", "Sign out and remove encryption keys?": "Afmelden en versleutelingssleutels verwijderen?", "To help us prevent this in future, please send us logs.": "Gelieve ons logboeken te sturen om dit in de toekomst te helpen voorkomen.", "Missing session data": "Sessiegegevens ontbreken", - "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Sommige sessiegegevens, inclusief sleutels voor versleutelde berichten, ontbreken. Meld u af en weer aan om dit op te lossen, en herstel de sleutels uit de back-up.", - "Your browser likely removed this data when running low on disk space.": "Uw browser heeft deze gegevens mogelijk verwijderd toen de beschikbare opslagruimte vol was.", - "Upload files (%(current)s of %(total)s)": "Bestanden worden geüpload (%(current)s van %(total)s)", - "Upload files": "Bestanden uploaden", - "Upload": "Uploaden", - "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Dit bestand is te groot om te uploaden. De bestandsgroottelimiet is %(limit)s, maar dit bestand is %(sizeOfThisFile)s.", - "These files are too large to upload. The file size limit is %(limit)s.": "Deze bestanden zijn te groot om te uploaden. De bestandsgroottelimiet is %(limit)s.", - "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Sommige bestanden zijn te groot om te uploaden. De bestandsgroottelimiet is %(limit)s.", - "Upload %(count)s other files|other": "%(count)s overige bestanden uploaden", - "Upload %(count)s other files|one": "%(count)s overig bestand uploaden", + "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Sommige sessiegegevens, waaronder sleutels voor versleutelde berichten, ontbreken. Herstel de sleutels uit uw back-up door u af en weer aan te melden.", + "Your browser likely removed this data when running low on disk space.": "Uw browser heeft deze gegevens wellicht verwijderd toen de beschikbare opslagruimte vol was.", + "Upload files (%(current)s of %(total)s)": "Bestanden versturen (%(current)s van %(total)s)", + "Upload files": "Bestanden versturen", + "Upload": "Versturen", + "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Dit bestand is te groot om te versturen. De bestandsgroottelimiet is %(limit)s, maar dit bestand is %(sizeOfThisFile)s.", + "These files are too large to upload. The file size limit is %(limit)s.": "Deze bestanden zijn te groot om te versturen. De bestandsgroottelimiet is %(limit)s.", + "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Sommige bestanden zijn te groot om te versturen. De bestandsgroottelimiet is %(limit)s.", + "Upload %(count)s other files|other": "%(count)s overige bestanden versturen", + "Upload %(count)s other files|one": "%(count)s overig bestanden versturen", "Cancel All": "Alles annuleren", - "Upload Error": "Uploadfout", + "Upload Error": "Fout bij versturen bestand", "A conference call could not be started because the integrations server is not available": "Daar de integratieserver onbereikbaar is kon het groepsaudiogesprek niet gestart worden", "The server does not support the room version specified.": "De server ondersteunt deze versie van gesprekken niet.", "Name or Matrix ID": "Naam of Matrix-ID", "Email, name or Matrix ID": "E-mailadres, naam, of matrix-ID", - "Please confirm that you'd like to go forward with upgrading this room from to .": "Bevestig dat u dit gesprek van wilt actualiseren tot .", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Gelieve het bijwerken van dit gesprek van tot te bevestigen.", "Changes your avatar in this current room only": "Verandert uw avatar enkel in het huidige gesprek", "Unbans user with given ID": "Ontbant de gebruiker met de gegeven ID", "Sends the given message coloured as a rainbow": "Verstuurt het gegeven bericht in regenboogkleuren", @@ -1811,7 +1811,7 @@ "Edit messages after they have been sent (refresh to apply changes)": "Bewerk berichten nadat ze verstuurd zijn (vernieuw om de wijzigingen toe te passen)", "React to messages with emoji (refresh to apply changes)": "Reageer op berichten met emoticons (vernieuw om de wijzigingen toe te passen)", "Show hidden events in timeline": "Verborgen gebeurtenissen op de tijdslijn weergeven", - "When rooms are upgraded": "Wanneer gesprekken opgewaardeerd worden", + "When rooms are upgraded": "Wanneer gesprekken bijgewerkt worden", "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "Dit apparaat maakt geen back-up van uw sleutels, maar u heeft wel een bestaande back-up die u kunt herstellen, en waaraan u vervolgens nieuwe sleutels kunt toevoegen.", "Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.": "Verbind dit apparaat met de sleutelback-up vooraleer u zich afmeldt om sleutels die zich enkel op dit apparaat bevinden niet te verliezen.", "Connect this device to Key Backup": "Dit apparaat verbinden met sleutelback-up", @@ -1845,7 +1845,7 @@ "This room doesn't exist. Are you sure you're at the right place?": "Dit gesprek bestaat niet. Weet u zeker dat u zich op de juiste plaats bevindt?", "Try again later, or ask a room admin to check if you have access.": "Probeer het later opnieuw, of vraag een gespreksbeheerder om te controleren of u wel toegang heeft.", "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "De foutcode %(errcode)s is weergegeven bij het toetreden van het gesprek. Als u meent dat u dit bericht foutief te zien krijgt, gelieve dan een foutmelding in te dienen.", - "This room has already been upgraded.": "Dit gesprek is reeds opgewaardeerd.", + "This room has already been upgraded.": "Dit gesprek is reeds bijgewerkt.", "Agree or Disagree": "Akkoord of niet akkoord", "Like or Dislike": "Duim omhoog of omlaag", "reacted with %(shortName)s": "heeft gereageerd met %(shortName)s", @@ -1894,7 +1894,7 @@ "Browse": "Bladeren", "Cannot reach homeserver": "Kan thuisserver niet bereiken", "Ensure you have a stable internet connection, or get in touch with the server admin": "Zorg dat u een stabiele internetverbinding heeft, of neem contact op met de systeembeheerder", - "Your Riot is misconfigured": "Uw Riot is verkeerd geconfigureerd", + "Your Riot is misconfigured": "Uw Riot is onjuist geconfigureerd", "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Vraag uw Riot-beheerder om uw configuratie na te kijken op onjuiste of duplicate items.", "Unexpected error resolving identity server configuration": "Onverwachte fout bij het oplossen van de identiteitsserverconfiguratie", "Use lowercase letters, numbers, dashes and underscores only": "Gebruik enkel letters, cijfers, streepjes en underscores", @@ -1915,7 +1915,7 @@ "Unnamed camera": "Naamloze camera", "Failed to connect to integrations server": "Verbinden met integratieserver mislukt", "No integrations server is configured to manage stickers with": "Er is geen integratieserver geconfigureerd om stickers mee te beheren", - "Upload all": "Alles uploaden", + "Upload all": "Alles versturen", "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Uw nieuwe account (%(newAccountId)s) is geregistreerd, maar u bent reeds aangemeld met een andere account (%(loggedInUserId)s).", "Continue with previous account": "Doorgaan met vorige account", "Loading room preview": "Gespreksweergave wordt geladen", @@ -1957,7 +1957,7 @@ "Identity Server": "Identiteitsserver", "Integrations Manager": "Integratiebeheerder", "Find others by phone or email": "Vind anderen via telefoonnummer of e-mailadres", - "Be found by phone or email": "Word gevonden via telefoonnummer of e-mailadres", + "Be found by phone or email": "Wees vindbaar via telefoonnummer of e-mailadres", "Use bots, bridges, widgets and sticker packs": "Gebruik robots, bruggen, widgets en stickerpakketten", "Terms of Service": "Gebruiksvoorwaarden", "To continue you need to accept the Terms of this service.": "Om verder te gaan dient u de gebruiksvoorwaarden van deze dienst te aanvaarden.", @@ -1980,8 +1980,8 @@ "Disconnect from the identity server ?": "Wilt u de verbinding met de identiteitsserver verbreken?", "Disconnect": "Verbinding verbreken", "Identity Server (%(server)s)": "Identiteitsserver (%(server)s)", - "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "U gebruikt momenteel om door uw contacten gevonden te kunnen worden, en om hen te kunnen vinden. U kunt hieronder uw identiteitsserver wijzigen.", - "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "U gebruikt momenteel geen identiteitsserver. Voeg er hieronder één toe om door uw contacten gevonden te worden en om hen te kunnen vinden.", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Om bekenden te kunnen vinden en door hen vindbaar te zijn gebruikt u momenteel . U kunt die identiteitsserver hieronder wijzigen.", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "U gebruikt momenteel geen identiteitsserver. Voeg er hieronder één toe om bekenden te kunnen vinden en voor hen vindbaar te zijn.", "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "De verbinding met uw identiteitsserver verbreken zal ertoe leiden dat u niet door andere gebruikers gevonden zal kunnen worden, en dat u anderen niet via e-mail of telefoon zal kunnen uitnodigen.", "Integration manager offline or not accessible.": "Integratiebeheerder is offline of onbereikbaar.", "Failed to update integration manager": "Bijwerken van integratiebeheerder is mislukt", @@ -2042,14 +2042,14 @@ "You are still sharing your personal data on the identity server .": "U deelt nog steeds uw persoonlijke gegevens op de identiteitsserver .", "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "We raden u aan uw e-mailadressen en telefoonnummers van de identiteitsserver te verwijderen vooraleer u de verbinding verbreekt.", "Disconnect anyway": "Verbinding toch verbreken", - "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "Indien u niet wilt gebruiken om door uw contacten gevonden te kunnen worden of om hen te kunnen vinden, voer dan hieronder een andere identiteitsserver in.", - "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Het gebruik van een identiteitsserver is optioneel. Als u ervoor kiest om geen identiteitsserver te gebruiken, zult u niet door uw contacten gevonden kunnen worden en hen niet kunnen vinden, via e-mailadres of telefoonnummer.", + "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "Mocht u om bekenden te zoeken en vindbaar te zijn niet willen gebruiken, voer dan hieronder een andere identiteitsserver in.", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Een identiteitsserver is niet verplicht, maar zonder identiteitsserver zult u geen bekenden op e-mailadres of telefoonnummer kunnen zoeken, noch door hen vindbaar zijn.", "Do not use an identity server": "Geen identiteitsserver gebruiken", - "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Aanvaard de gebruiksvoorwaarden van de identiteitsserver (%(serverName)s) om door uw contacten gevonden te kunnen worden of om hen te kunnen vinden via e-mailadres of telefoonnumer.", + "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Aanvaard de gebruiksvoorwaarden van de identiteitsserver (%(serverName)s) om vindbaar te zijn op e-mailadres of telefoonnummer.", "Read Marker lifetime (ms)": "Levensduur van leesbevestigingen (ms)", "Read Marker off-screen lifetime (ms)": "Levensduur van levensbevestigingen, niet op scherm (ms)", "A device's public name is visible to people you communicate with": "De openbare naam van een apparaat is zichtbaar aan alle mensen met wie u communiceert", - "Upgrade the room": "Opwaardeer het gesprek", + "Upgrade the room": "Werk het gesprek bij", "Enable room encryption": "Gespreksversleuteling inschakelen", "Error changing power level requirement": "Fout bij wijzigen van machtsniveauvereiste", "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Er is een fout opgetreden bij het wijzigen van de machtsniveauvereisten van het gesprek. Zorg ervoor dat u over voldoende machtigingen beschikt en probeer het opnieuw.", @@ -2079,7 +2079,7 @@ "Share this email in Settings to receive invites directly in Riot.": "Deel dit e-mailadres in de instellingen om uitnodigingen automatisch te ontvangen in Riot.", "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "Gebruik een identiteitsserver om uit te nodigen op e-mailadres. Gebruik de standaardserver (%(defaultIdentityServerName)s) of beheer de server in de Instellingen.", "Use an identity server to invite by email. Manage in Settings.": "Gebruik een identiteitsserver om anderen uit te nodigen via e-mail. Beheer de server in de Instellingen.", - "Please fill why you're reporting.": "Gelieve te vermelden waarom u deze melding indient.", + "Please fill why you're reporting.": "Gelieve aan te geven waarom u deze melding indient.", "Report Content to Your Homeserver Administrator": "Inhoud melden aan de beheerder van uw thuisserver", "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Dit bericht melden zal zijn unieke ‘gebeurtenis-ID’ versturen naar de beheerder van uw thuisserver. Als de berichten in dit gesprek versleuteld zijn, zal de beheerder van uw thuisserver het bericht niet kunnen lezen, noch enige bestanden of afbeeldingen zien.", "Send report": "Rapport versturen", @@ -2128,7 +2128,12 @@ "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Publieke sleutel van captcha ontbreekt in thuisserverconfiguratie. Gelieve dit te melden aan de beheerder van uw thuisserver.", "Community Autocomplete": "Gemeenschappen automatisch aanvullen", "Emoji Autocomplete": "Emoji’s automatisch aanvullen", - "Notification Autocomplete": "Meldingen automatisch aanvullen", + "Notification Autocomplete": "Meldingen automatisch voltooien", "Room Autocomplete": "Gesprekken automatisch aanvullen", - "User Autocomplete": "Gebruikers automatisch aanvullen" + "User Autocomplete": "Gebruikers automatisch aanvullen", + "Add Email Address": "Emailadres toevoegen", + "Add Phone Number": "Telefoonnummer toevoegen", + "Your email address hasn't been verified yet": "Uw emailadres is nog niet gecontroleerd", + "Click the link in the email you received to verify and then click continue again.": "Open de link in de ontvangen contrôle-email, en klik dan op \"Doorgaan\".", + "%(creator)s created and configured the room.": "Gesprek gestart en ingesteld door %(creator)s." } From 9c3b85b9ee3baf0a4c382aa7931989b534497fd4 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 15 Oct 2019 05:54:22 +0000 Subject: [PATCH 0302/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1831 of 1831 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index bfe72b737c..8251f729f3 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1910,7 +1910,7 @@ "Joining room …": "Szobához csatlakozás …", "Loading …": "Betöltés …", "Rejecting invite …": "Meghívó elutasítása …", - "Join the conversation with an account": "Beszélgetéshez csatlakozás felhasználói fiókkal", + "Join the conversation with an account": "Beszélgetéshez való csatlakozás felhasználói fiókkal lehetséges", "Sign Up": "Fiók készítés", "Sign In": "Bejelentkezés", "You were kicked from %(roomName)s by %(memberName)s": "Téged kirúgott %(memberName)s ebből a szobából: %(roomName)s", From 86dad09b361a7565e8039a7ed1c8b608698ae207 Mon Sep 17 00:00:00 2001 From: random Date: Thu, 17 Oct 2019 09:14:37 +0000 Subject: [PATCH 0303/2372] Translated using Weblate (Italian) Currently translated at 100.0% (1831 of 1831 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 7c998744e9..b4b60c952e 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2189,5 +2189,6 @@ "Your email address hasn't been verified yet": "Il tuo indirizzo email non è ancora stato verificato", "Click the link in the email you received to verify and then click continue again.": "Clicca il link nell'email che hai ricevuto per verificare e poi clicca di nuovo Continua.", "Read Marker lifetime (ms)": "Durata delle conferme di lettura (ms)", - "Read Marker off-screen lifetime (ms)": "Durata della conferma di lettura off-screen (ms)" + "Read Marker off-screen lifetime (ms)": "Durata della conferma di lettura off-screen (ms)", + "%(creator)s created and configured the room.": "%(creator)s ha creato e configurato la stanza." } From e0d534e8e41bf50b536539525b30024dadb6530c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sava=20Rado=C5=A1?= Date: Mon, 14 Oct 2019 19:59:44 +0000 Subject: [PATCH 0304/2372] Translated using Weblate (Serbian (latin)) Currently translated at 3.9% (72 of 1831 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sr_Latn/ --- src/i18n/strings/sr_Latn.json | 52 ++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sr_Latn.json b/src/i18n/strings/sr_Latn.json index 7e8c2a6612..96454cb00b 100644 --- a/src/i18n/strings/sr_Latn.json +++ b/src/i18n/strings/sr_Latn.json @@ -20,5 +20,55 @@ "powered by Matrix": "pokreće Matriks", "Custom Server Options": "Prilagođene opcije servera", "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "Takođe, možete podesiti prilagođeni server identiteta, ali tada nećete moći da pozivate korisnike preko adrese elektronske pošte ili da i sami budete pozvani preko adrese elektronske pošte.", - "Explore rooms": "Istražite sobe" + "Explore rooms": "Istražite sobe", + "Send anyway": "Ipak pošalji", + "Send": "Pošalji", + "Sun": "Ned", + "Mon": "Pon", + "Tue": "Uto", + "Wed": "Sre", + "Thu": "Čet", + "Fri": "Pet", + "Sat": "Sub", + "Jan": "Jan", + "Feb": "Feb", + "Mar": "Mar", + "Apr": "Apr", + "May": "Maj", + "Jun": "Jun", + "Jul": "Jul", + "Aug": "Avg", + "Sep": "Sep", + "Oct": "Okt", + "Nov": "Nov", + "Dec": "Dec", + "PM": "poslepodne", + "AM": "prepodne", + "Name or Matrix ID": "Ime ili Matriks ID", + "Unnamed Room": "Soba bez imena", + "Error": "Greška", + "Unable to load! Check your network connectivity and try again.": "Neuspelo učitavanje! Proverite vašu mrežu i pokušajte ponovo.", + "Riot does not have permission to send you notifications - please check your browser settings": "Riot nema dozvolu da vam šalje obaveštenja. Molim proverite podešavanja vašeg internet pregledača", + "Riot was not given permission to send notifications - please try again": "Riot nije dobio dozvolu da šalje obaveštenja. Molim pokušajte ponovo", + "This email address was not found": "Ova adresa elektronske pošte nije pronađena", + "Registration Required": "Potrebna je registracija", + "You need to register to do this. Would you like to register now?": "Morate biti registrovani da bi uradili ovo. Da li želite da se registrujete sad?", + "Register": "Registruj se", + "Default": "Podrazumevano", + "Restricted": "Ograničeno", + "Moderator": "Moderator", + "Admin": "Administrator", + "Start a chat": "Pokreni ćaskanje", + "Who would you like to communicate with?": "Sa kim bi želeli da komunicirate?", + "Email, name or Matrix ID": "Adresa elektronske pošte, ime ili Matriks ID", + "Start Chat": "Započni ćaskanje", + "Invite new room members": "Pozovi nove članove u sobu", + "Send Invites": "Poštalji pozivnice", + "Failed to start chat": "Započinjanje ćaskanja nije uspelo", + "Operation failed": "Operacija nije uspela", + "Failed to invite": "Slanje pozivnice nije uspelo", + "Failed to invite users to the room:": "Nije uspelo pozivanje korisnika u sobu:", + "You need to be logged in.": "Morate biti prijavljeni", + "You need to be able to invite users to do that.": "Mora vam biti dozvoljeno da pozovete korisnike kako bi to uradili.", + "Failed to send request.": "Slanje zahteva nije uspelo." } From f72ff95efb25f8c5b75dd74503e862859d2a5834 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 17 Oct 2019 17:30:37 +0100 Subject: [PATCH 0305/2372] Handle ARROW_RIGHT on group node in treelist as per aria suggestions Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomSubList.js | 42 ++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index f97b0e5112..d9fa553782 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -133,14 +133,50 @@ const RoomSubList = createReactClass({ // Prevent LeftPanel handling Tab if focus is on the sublist header itself ev.stopPropagation(); break; - case Key.ARROW_RIGHT: + case Key.ARROW_RIGHT: { ev.stopPropagation(); if (this.state.hidden && !this.props.forceExpand) { + // sublist is collapsed, expand it this.onClick(); - } else { - // TODO go to first element in subtree + } else if (!this.props.forceExpand) { + // sublist is expanded, go to first room + let element = document.activeElement; + let descending = true; + let classes; + + do { + const child = element.firstElementChild; + const sibling = element.nextElementSibling; + + if (descending) { + if (child) { + element = child; + } else if (sibling) { + element = sibling; + } else { + descending = false; + element = element.parentElement; + } + } else { + if (sibling) { + element = sibling; + descending = true; + } else { + element = element.parentElement; + } + } + + if (element) { + classes = element.classList; + } + } while (element && !classes.contains("mx_RoomTile")); + + if (element) { + element.focus(); + } } break; + } } }, From fe46925c00a457ef726d07803c6ff52f46c3e7a2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 17 Oct 2019 17:49:28 +0100 Subject: [PATCH 0306/2372] Handle LEFT Arrow as expected by Aria Treeview Widget pattern Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomSubList.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index d9fa553782..4ff273303a 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -133,6 +133,13 @@ const RoomSubList = createReactClass({ // Prevent LeftPanel handling Tab if focus is on the sublist header itself ev.stopPropagation(); break; + case Key.ARROW_LEFT: + // On ARROW_LEFT collapse the room sublist + if (!this.state.hidden && !this.props.forceExpand) { + this.onClick(); + } + ev.stopPropagation(); + break; case Key.ARROW_RIGHT: { ev.stopPropagation(); if (this.state.hidden && !this.props.forceExpand) { @@ -181,13 +188,10 @@ const RoomSubList = createReactClass({ }, onKeyDown: function(ev) { - // On ARROW_LEFT collapse the room sublist + // On ARROW_LEFT go to the sublist header if (ev.key === Key.ARROW_LEFT) { ev.stopPropagation(); - if (!this.state.hidden && !this.props.forceExpand) { - this.onClick(); - this._headerButton.current.focus(); - } + this._headerButton.current.focus(); } }, From 2494af37c862144742c34780e85464366113090f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 17 Oct 2019 19:08:40 +0100 Subject: [PATCH 0307/2372] Break UserInfo:RoomAdminToolsContainer into smaller components Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/right_panel/UserInfo.js | 452 ++++++++++--------- 1 file changed, 234 insertions(+), 218 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 59cd61a583..292cf87f47 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -374,6 +374,230 @@ const useRoomPowerLevels = (room) => { return powerLevels; }; +const RoomKickButton = withLegacyMatrixClient(({cli, member, startUpdating, stopUpdating}) => { + const onKick = async () => { + const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); + const {finished} = Modal.createTrackedDialog( + 'Confirm User Action Dialog', + 'onKick', + ConfirmUserActionDialog, + { + member, + action: member.membership === "invite" ? _t("Disinvite") : _t("Kick"), + title: member.membership === "invite" ? _t("Disinvite this user?") : _t("Kick this user?"), + askReason: member.membership === "join", + danger: true, + }, + ); + + const [proceed, reason] = await finished; + if (!proceed) return; + + startUpdating(); + cli.kick(member.roomId, member.userId, reason || undefined).then(() => { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Kick success"); + }, function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Kick error: " + err); + Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, { + title: _t("Failed to kick"), + description: ((err && err.message) ? err.message : "Operation failed"), + }); + }).finally(() => { + stopUpdating(); + }); + }; + + const kickLabel = member.membership === "invite" ? _t("Disinvite") : _t("Kick"); + return + { kickLabel } + ; +}); + +const RedactMessagesButton = withLegacyMatrixClient(({cli, member}) => { + const onRedactAllMessages = async () => { + const {roomId, userId} = member; + const room = cli.getRoom(roomId); + if (!room) { + return; + } + let timeline = room.getLiveTimeline(); + let eventsToRedact = []; + while (timeline) { + eventsToRedact = timeline.getEvents().reduce((events, event) => { + if (event.getSender() === userId && !event.isRedacted()) { + return events.concat(event); + } else { + return events; + } + }, eventsToRedact); + timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS); + } + + const count = eventsToRedact.length; + const user = member.name; + + if (count === 0) { + const InfoDialog = sdk.getComponent("dialogs.InfoDialog"); + Modal.createTrackedDialog('No user messages found to remove', '', InfoDialog, { + title: _t("No recent messages by %(user)s found", {user}), + description: +
    +

    { _t("Try scrolling up in the timeline to see if there are any earlier ones.") }

    +
    , + }); + } else { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + const {finished} = Modal.createTrackedDialog('Remove recent messages by user', '', QuestionDialog, { + title: _t("Remove recent messages by %(user)s", {user}), + description: +
    +

    { _t("You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", {count, user}) }

    +

    { _t("For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.") }

    +
    , + button: _t("Remove %(count)s messages", {count}), + }); + + const [confirmed] = await finished; + if (!confirmed) { + return; + } + + // Submitting a large number of redactions freezes the UI, + // so first yield to allow to rerender after closing the dialog. + await Promise.resolve(); + + console.info(`Started redacting recent ${count} messages for ${user} in ${roomId}`); + await Promise.all(eventsToRedact.map(async event => { + try { + await cli.redactEvent(roomId, event.getId()); + } catch (err) { + // log and swallow errors + console.error("Could not redact", event.getId()); + console.error(err); + } + })); + console.info(`Finished redacting recent ${count} messages for ${user} in ${roomId}`); + } + }; + + return + { _t("Remove recent messages") } + ; +}); + +const BanToggleButton = withLegacyMatrixClient(({cli, member, startUpdating, stopUpdating}) => { + const onBanOrUnban = async () => { + const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); + const {finished} = Modal.createTrackedDialog( + 'Confirm User Action Dialog', + 'onBanOrUnban', + ConfirmUserActionDialog, + { + member, + action: member.membership === 'ban' ? _t("Unban") : _t("Ban"), + title: member.membership === 'ban' ? _t("Unban this user?") : _t("Ban this user?"), + askReason: member.membership !== 'ban', + danger: member.membership !== 'ban', + }, + ); + + const [proceed, reason] = await finished; + if (!proceed) return; + + startUpdating(); + let promise; + if (member.membership === 'ban') { + promise = cli.unban(member.roomId, member.userId); + } else { + promise = cli.ban(member.roomId, member.userId, reason || undefined); + } + promise.then(() => { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Ban success"); + }, function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Ban error: " + err); + Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, { + title: _t("Error"), + description: _t("Failed to ban user"), + }); + }).finally(() => { + stopUpdating(); + }); + }; + + let label = _t("Ban"); + if (member.membership === 'ban') { + label = _t("Unban"); + } + + return + { label } + ; +}); + +const MuteToggleButton = withLegacyMatrixClient(({cli, member, room, powerLevels, startUpdating, stopUpdating}) => { + const isMuted = _isMuted(member, powerLevels); + const onMuteToggle = async () => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const roomId = member.roomId; + const target = member.userId; + + // if muting self, warn as it may be irreversible + if (target === cli.getUserId()) { + try { + if (!(await _warnSelfDemote())) return; + } catch (e) { + console.error("Failed to warn about self demotion: ", e); + return; + } + } + + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (!powerLevelEvent) return; + + const powerLevels = powerLevelEvent.getContent(); + const levelToSend = ( + (powerLevels.events ? powerLevels.events["m.room.message"] : null) || + powerLevels.events_default + ); + let level; + if (isMuted) { // unmute + level = levelToSend; + } else { // mute + level = levelToSend - 1; + } + level = parseInt(level); + + if (!isNaN(level)) { + startUpdating(); + cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Mute toggle success"); + }, function(err) { + console.error("Mute error: " + err); + Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, { + title: _t("Error"), + description: _t("Failed to mute user"), + }); + }).finally(() => { + stopUpdating(); + }); + } + }; + + const muteLabel = isMuted ? _t("Unmute") : _t("Mute"); + return + { muteLabel } + ; +}); + const RoomAdminToolsContainer = withLegacyMatrixClient(({cli, room, children, member, startUpdating, stopUpdating}) => { let kickButton; let banButton; @@ -389,235 +613,27 @@ const RoomAdminToolsContainer = withLegacyMatrixClient(({cli, room, children, me const me = room.getMember(cli.getUserId()); const isMe = me.userId === member.userId; const canAffectUser = member.powerLevel < me.powerLevel || isMe; - const membership = member.membership; if (canAffectUser && me.powerLevel >= powerLevels.kick) { - const onKick = async () => { - const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); - const {finished} = Modal.createTrackedDialog( - 'Confirm User Action Dialog', - 'onKick', - ConfirmUserActionDialog, - { - member, - action: membership === "invite" ? _t("Disinvite") : _t("Kick"), - title: membership === "invite" ? _t("Disinvite this user?") : _t("Kick this user?"), - askReason: membership === "join", - danger: true, - }, - ); - - const [proceed, reason] = await finished; - if (!proceed) return; - - startUpdating(); - cli.kick(member.roomId, member.userId, reason || undefined).then(() => { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Kick success"); - }, function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Kick error: " + err); - Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, { - title: _t("Failed to kick"), - description: ((err && err.message) ? err.message : "Operation failed"), - }); - }).finally(() => { - stopUpdating(); - }); - }; - - const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick"); - kickButton = ( - - { kickLabel } - - ); + kickButton = ; } if (me.powerLevel >= powerLevels.redact) { - const onRedactAllMessages = async () => { - const {roomId, userId} = member; - const room = cli.getRoom(roomId); - if (!room) { - return; - } - let timeline = room.getLiveTimeline(); - let eventsToRedact = []; - while (timeline) { - eventsToRedact = timeline.getEvents().reduce((events, event) => { - if (event.getSender() === userId && !event.isRedacted()) { - return events.concat(event); - } else { - return events; - } - }, eventsToRedact); - timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS); - } - - const count = eventsToRedact.length; - const user = member.name; - - if (count === 0) { - const InfoDialog = sdk.getComponent("dialogs.InfoDialog"); - Modal.createTrackedDialog('No user messages found to remove', '', InfoDialog, { - title: _t("No recent messages by %(user)s found", {user}), - description: -
    -

    { _t("Try scrolling up in the timeline to see if there are any earlier ones.") }

    -
    , - }); - } else { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - - const {finished} = Modal.createTrackedDialog('Remove recent messages by user', '', QuestionDialog, { - title: _t("Remove recent messages by %(user)s", {user}), - description: -
    -

    { _t("You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", {count, user}) }

    -

    { _t("For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.") }

    -
    , - button: _t("Remove %(count)s messages", {count}), - }); - - const [confirmed] = await finished; - if (!confirmed) { - return; - } - - // Submitting a large number of redactions freezes the UI, - // so first yield to allow to rerender after closing the dialog. - await Promise.resolve(); - - console.info(`Started redacting recent ${count} messages for ${user} in ${roomId}`); - await Promise.all(eventsToRedact.map(async event => { - try { - await cli.redactEvent(roomId, event.getId()); - } catch (err) { - // log and swallow errors - console.error("Could not redact", event.getId()); - console.error(err); - } - })); - console.info(`Finished redacting recent ${count} messages for ${user} in ${roomId}`); - } - }; - redactButton = ( - - { _t("Remove recent messages") } - + ); } if (canAffectUser && me.powerLevel >= powerLevels.ban) { - const onBanOrUnban = async () => { - const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); - const {finished} = Modal.createTrackedDialog( - 'Confirm User Action Dialog', - 'onBanOrUnban', - ConfirmUserActionDialog, - { - member, - action: membership === 'ban' ? _t("Unban") : _t("Ban"), - title: membership === 'ban' ? _t("Unban this user?") : _t("Ban this user?"), - askReason: membership !== 'ban', - danger: membership !== 'ban', - }, - ); - - const [proceed, reason] = await finished; - if (!proceed) return; - - startUpdating(); - let promise; - if (membership === 'ban') { - promise = cli.unban(member.roomId, member.userId); - } else { - promise = cli.ban(member.roomId, member.userId, reason || undefined); - } - promise.then(() => { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Ban success"); - }, function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Ban error: " + err); - Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, { - title: _t("Error"), - description: _t("Failed to ban user"), - }); - }).finally(() => { - stopUpdating(); - }); - }; - - let label = _t("Ban"); - if (membership === 'ban') { - label = _t("Unban"); - } - banButton = ( - - { label } - - ); + banButton = ; } if (canAffectUser && me.powerLevel >= editPowerLevel) { - const isMuted = _isMuted(member, powerLevels); - const onMuteToggle = async () => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - const roomId = member.roomId; - const target = member.userId; - - // if muting self, warn as it may be irreversible - if (target === cli.getUserId()) { - try { - if (!(await _warnSelfDemote())) return; - } catch (e) { - console.error("Failed to warn about self demotion: ", e); - return; - } - } - - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - if (!powerLevelEvent) return; - - const powerLevels = powerLevelEvent.getContent(); - const levelToSend = ( - (powerLevels.events ? powerLevels.events["m.room.message"] : null) || - powerLevels.events_default - ); - let level; - if (isMuted) { // unmute - level = levelToSend; - } else { // mute - level = levelToSend - 1; - } - level = parseInt(level); - - if (!isNaN(level)) { - startUpdating(); - cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Mute toggle success"); - }, function(err) { - console.error("Mute error: " + err); - Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, { - title: _t("Error"), - description: _t("Failed to mute user"), - }); - }).finally(() => { - stopUpdating(); - }); - } - }; - - const muteLabel = isMuted ? _t("Unmute") : _t("Mute"); muteButton = ( - - { muteLabel } - + ); } From 93429d7c2ee515872fc18c653c8cdae0636511cc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 17 Oct 2019 19:13:37 +0100 Subject: [PATCH 0308/2372] Break withLegacyMatrixClient into a util module Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/right_panel/UserInfo.js | 35 ++++++++------------ src/utils/withLegacyMatrixClient.js | 31 +++++++++++++++++ 2 files changed, 44 insertions(+), 22 deletions(-) create mode 100644 src/utils/withLegacyMatrixClient.js diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 292cf87f47..e5ac7d4665 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -21,7 +21,7 @@ import React, {useCallback, useMemo, useState, useEffect} from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import useEventListener from '@use-it/event-listener'; -import {Group, MatrixClient, RoomMember, User} from 'matrix-js-sdk'; +import {Group, RoomMember, User} from 'matrix-js-sdk'; import dis from '../../../dispatcher'; import Modal from '../../../Modal'; import sdk from '../../../index'; @@ -39,6 +39,7 @@ import MultiInviter from "../../../utils/MultiInviter"; import GroupStore from "../../../stores/GroupStore"; import MatrixClientPeg from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; +import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient"; const _disambiguateDevices = (devices) => { const names = Object.create(null); @@ -57,22 +58,12 @@ const _disambiguateDevices = (devices) => { } }; -const withLegacyMatrixClient = (Component) => class extends React.PureComponent { - static contextTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, - }; - - render() { - return ; - } -}; - const _getE2EStatus = (devices) => { const hasUnverifiedDevice = devices.some((device) => device.isUnverified()); return hasUnverifiedDevice ? "warning" : "verified"; }; -const DevicesSection = withLegacyMatrixClient(({devices, userId, loading}) => { +const DevicesSection = ({devices, userId, loading}) => { const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo'); const Spinner = sdk.getComponent("elements.Spinner"); @@ -95,7 +86,7 @@ const DevicesSection = withLegacyMatrixClient(({devices, userId, loading}) => {
    ); -}); +}; const onRoomTileClick = (roomId) => { dis.dispatch({ @@ -104,7 +95,7 @@ const onRoomTileClick = (roomId) => { }); }; -const DirectChatsSection = withLegacyMatrixClient(({cli, userId, startUpdating, stopUpdating}) => { +const DirectChatsSection = withLegacyMatrixClient(({matrixClient: cli, userId, startUpdating, stopUpdating}) => { const onNewDMClick = async () => { startUpdating(); await createRoom({dmUserId: userId}); @@ -195,7 +186,7 @@ const DirectChatsSection = withLegacyMatrixClient(({cli, userId, startUpdating, ); }); -const UserOptionsSection = withLegacyMatrixClient(({cli, member, isIgnored, canInvite}) => { +const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite}) => { let ignoreButton = null; let insertPillButton = null; let inviteUserButton = null; @@ -374,7 +365,7 @@ const useRoomPowerLevels = (room) => { return powerLevels; }; -const RoomKickButton = withLegacyMatrixClient(({cli, member, startUpdating, stopUpdating}) => { +const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, startUpdating, stopUpdating}) => { const onKick = async () => { const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); const {finished} = Modal.createTrackedDialog( @@ -416,7 +407,7 @@ const RoomKickButton = withLegacyMatrixClient(({cli, member, startUpdating, stop ; }); -const RedactMessagesButton = withLegacyMatrixClient(({cli, member}) => { +const RedactMessagesButton = withLegacyMatrixClient(({matrixClient: cli, member}) => { const onRedactAllMessages = async () => { const {roomId, userId} = member; const room = cli.getRoom(roomId); @@ -489,7 +480,7 @@ const RedactMessagesButton = withLegacyMatrixClient(({cli, member}) => { ; }); -const BanToggleButton = withLegacyMatrixClient(({cli, member, startUpdating, stopUpdating}) => { +const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, startUpdating, stopUpdating}) => { const onBanOrUnban = async () => { const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); const {finished} = Modal.createTrackedDialog( @@ -541,7 +532,7 @@ const BanToggleButton = withLegacyMatrixClient(({cli, member, startUpdating, sto ; }); -const MuteToggleButton = withLegacyMatrixClient(({cli, member, room, powerLevels, startUpdating, stopUpdating}) => { +const MuteToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, room, powerLevels, startUpdating, stopUpdating}) => { const isMuted = _isMuted(member, powerLevels); const onMuteToggle = async () => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -598,7 +589,7 @@ const MuteToggleButton = withLegacyMatrixClient(({cli, member, room, powerLevels ; }); -const RoomAdminToolsContainer = withLegacyMatrixClient(({cli, room, children, member, startUpdating, stopUpdating}) => { +const RoomAdminToolsContainer = withLegacyMatrixClient(({matrixClient: cli, room, children, member, startUpdating, stopUpdating}) => { let kickButton; let banButton; let muteButton; @@ -651,7 +642,7 @@ const RoomAdminToolsContainer = withLegacyMatrixClient(({cli, room, children, me }); const GroupAdminToolsSection = withLegacyMatrixClient( - ({cli, children, groupId, groupMember, startUpdating, stopUpdating}) => { + ({matrixClient: cli, children, groupId, groupMember, startUpdating, stopUpdating}) => { const [isPrivileged, setIsPrivileged] = useState(false); const [isInvited, setIsInvited] = useState(false); @@ -753,7 +744,7 @@ const useIsSynapseAdmin = (cli) => { }; // cli is injected by withLegacyMatrixClient -const UserInfo = withLegacyMatrixClient(({cli, user, groupId, roomId, onClose}) => { +const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => { // Load room if we are given a room id and memoize it const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]); diff --git a/src/utils/withLegacyMatrixClient.js b/src/utils/withLegacyMatrixClient.js new file mode 100644 index 0000000000..af6a930a88 --- /dev/null +++ b/src/utils/withLegacyMatrixClient.js @@ -0,0 +1,31 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import PropTypes from "prop-types"; +import {MatrixClient} from "matrix-js-sdk"; + +// Higher Order Component to allow use of legacy MatrixClient React Context +// in Functional Components which do not otherwise support legacy React Contexts +export default (Component) => class extends React.PureComponent { + static contextTypes = { + matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, + }; + + render() { + return ; + } +}; From 7e4d429fa3b7918a0f40fd9e3379ebb9074315a4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 17 Oct 2019 19:18:14 +0100 Subject: [PATCH 0309/2372] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/right_panel/UserInfo.js | 202 ++++++++++--------- 1 file changed, 103 insertions(+), 99 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index e5ac7d4665..3abe97dd75 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -532,114 +532,118 @@ const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, star ; }); -const MuteToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, room, powerLevels, startUpdating, stopUpdating}) => { - const isMuted = _isMuted(member, powerLevels); - const onMuteToggle = async () => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - const roomId = member.roomId; - const target = member.userId; +const MuteToggleButton = withLegacyMatrixClient( + ({matrixClient: cli, member, room, powerLevels, startUpdating, stopUpdating}) => { + const isMuted = _isMuted(member, powerLevels); + const onMuteToggle = async () => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const roomId = member.roomId; + const target = member.userId; - // if muting self, warn as it may be irreversible - if (target === cli.getUserId()) { - try { - if (!(await _warnSelfDemote())) return; - } catch (e) { - console.error("Failed to warn about self demotion: ", e); - return; + // if muting self, warn as it may be irreversible + if (target === cli.getUserId()) { + try { + if (!(await _warnSelfDemote())) return; + } catch (e) { + console.error("Failed to warn about self demotion: ", e); + return; + } } - } - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - if (!powerLevelEvent) return; + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (!powerLevelEvent) return; - const powerLevels = powerLevelEvent.getContent(); - const levelToSend = ( - (powerLevels.events ? powerLevels.events["m.room.message"] : null) || - powerLevels.events_default - ); - let level; - if (isMuted) { // unmute - level = levelToSend; - } else { // mute - level = levelToSend - 1; - } - level = parseInt(level); + const powerLevels = powerLevelEvent.getContent(); + const levelToSend = ( + (powerLevels.events ? powerLevels.events["m.room.message"] : null) || + powerLevels.events_default + ); + let level; + if (isMuted) { // unmute + level = levelToSend; + } else { // mute + level = levelToSend - 1; + } + level = parseInt(level); - if (!isNaN(level)) { - startUpdating(); - cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Mute toggle success"); - }, function(err) { - console.error("Mute error: " + err); - Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, { - title: _t("Error"), - description: _t("Failed to mute user"), + if (!isNaN(level)) { + startUpdating(); + cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Mute toggle success"); + }, function(err) { + console.error("Mute error: " + err); + Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, { + title: _t("Error"), + description: _t("Failed to mute user"), + }); + }).finally(() => { + stopUpdating(); }); - }).finally(() => { - stopUpdating(); - }); + } + }; + + const muteLabel = isMuted ? _t("Unmute") : _t("Mute"); + return + { muteLabel } + ; + }, +); + +const RoomAdminToolsContainer = withLegacyMatrixClient( + ({matrixClient: cli, room, children, member, startUpdating, stopUpdating}) => { + let kickButton; + let banButton; + let muteButton; + let redactButton; + + const powerLevels = useRoomPowerLevels(room); + const editPowerLevel = ( + (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || + powerLevels.state_default + ); + + const me = room.getMember(cli.getUserId()); + const isMe = me.userId === member.userId; + const canAffectUser = member.powerLevel < me.powerLevel || isMe; + + if (canAffectUser && me.powerLevel >= powerLevels.kick) { + kickButton = ; + } + if (me.powerLevel >= powerLevels.redact) { + redactButton = ( + + ); + } + if (canAffectUser && me.powerLevel >= powerLevels.ban) { + banButton = ; + } + if (canAffectUser && me.powerLevel >= editPowerLevel) { + muteButton = ( + + ); } - }; - const muteLabel = isMuted ? _t("Unmute") : _t("Mute"); - return - { muteLabel } - ; -}); + if (kickButton || banButton || muteButton || redactButton || children) { + return + { muteButton } + { kickButton } + { banButton } + { redactButton } + { children } + ; + } -const RoomAdminToolsContainer = withLegacyMatrixClient(({matrixClient: cli, room, children, member, startUpdating, stopUpdating}) => { - let kickButton; - let banButton; - let muteButton; - let redactButton; - - const powerLevels = useRoomPowerLevels(room); - const editPowerLevel = ( - (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || - powerLevels.state_default - ); - - const me = room.getMember(cli.getUserId()); - const isMe = me.userId === member.userId; - const canAffectUser = member.powerLevel < me.powerLevel || isMe; - - if (canAffectUser && me.powerLevel >= powerLevels.kick) { - kickButton = ; - } - if (me.powerLevel >= powerLevels.redact) { - redactButton = ( - - ); - } - if (canAffectUser && me.powerLevel >= powerLevels.ban) { - banButton = ; - } - if (canAffectUser && me.powerLevel >= editPowerLevel) { - muteButton = ( - - ); - } - - if (kickButton || banButton || muteButton || redactButton || children) { - return - { muteButton } - { kickButton } - { banButton } - { redactButton } - { children } - ; - } - - return
    ; -}); + return
    ; + }, +); const GroupAdminToolsSection = withLegacyMatrixClient( ({matrixClient: cli, children, groupId, groupMember, startUpdating, stopUpdating}) => { From 579ada3ca27ffd4ef299f02140c581dc9bcda89e Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 18 Oct 2019 12:40:50 +0100 Subject: [PATCH 0310/2372] Add an overall reachability timeout of 10s This adds a reachability timeout of 10s when checking the IS for 3PID bindings. This ensures we stop in a reasonable time, rather than waiting for a long list of requests to eventually timeout via some general mechanism. Part of https://github.com/vector-im/riot-web/issues/10909 --- src/components/views/settings/SetIdServer.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 8359c09d87..7f4a50d391 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -28,6 +28,9 @@ import {SERVICE_TYPES} from "matrix-js-sdk"; import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils"; import { getDefaultIdentityServerUrl } from '../../../utils/IdentityServerUtils'; +// We'll wait up to this long when checking for 3PID bindings on the IS. +const REACHABILITY_TIMEOUT = 10000; // ms + /** * Check an IS URL is valid, including liveness check * @@ -254,7 +257,14 @@ export default class SetIdServer extends React.Component { let threepids = []; let currentServerReachable = true; try { - threepids = await getThreepidsWithBindStatus(MatrixClientPeg.get()); + threepids = await Promise.race([ + getThreepidsWithBindStatus(MatrixClientPeg.get()), + new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error("Timeout attempting to reach identity server")); + }, REACHABILITY_TIMEOUT); + }), + ]); } catch (e) { currentServerReachable = false; console.warn( @@ -263,7 +273,6 @@ export default class SetIdServer extends React.Component { ); console.warn(e); } - const boundThreepids = threepids.filter(tp => tp.bound); let message; let danger = false; From 683947e0b764ed025613abff34a484c9cae80ac2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Oct 2019 12:27:43 +0000 Subject: [PATCH 0311/2372] Update test/end-to-end-tests/README.md Co-Authored-By: J. Ryan Stinnett --- test/end-to-end-tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/README.md b/test/end-to-end-tests/README.md index 81655000a0..8b0d0fc670 100644 --- a/test/end-to-end-tests/README.md +++ b/test/end-to-end-tests/README.md @@ -1,4 +1,4 @@ -# Matrix React Web App End-to-End tests +# Matrix React SDK End-to-End tests This repository contains tests for the matrix-react-sdk web app. The tests fire up a headless chrome and simulate user interaction (end-to-end). Note that end-to-end has little to do with the end-to-end encryption matrix supports, just that we test the full stack, going from user interaction to expected DOM in the browser. From 6236909d93520c7e946089b4d56ec9f322968f13 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Oct 2019 12:27:52 +0000 Subject: [PATCH 0312/2372] Update test/end-to-end-tests/README.md Co-Authored-By: J. Ryan Stinnett --- test/end-to-end-tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/README.md b/test/end-to-end-tests/README.md index 8b0d0fc670..5e95afc1d5 100644 --- a/test/end-to-end-tests/README.md +++ b/test/end-to-end-tests/README.md @@ -1,6 +1,6 @@ # Matrix React SDK End-to-End tests -This repository contains tests for the matrix-react-sdk web app. The tests fire up a headless chrome and simulate user interaction (end-to-end). Note that end-to-end has little to do with the end-to-end encryption matrix supports, just that we test the full stack, going from user interaction to expected DOM in the browser. +This directory contains tests for matrix-react-sdk. The tests fire up a headless Chrome and simulate user interaction (end-to-end). Note that end-to-end has little to do with the end-to-end encryption Matrix supports, just that we test the full stack, going from user interaction to expected DOM in the browser. ## Setup From 8a028029eea3a2e0b797ff746823bcd648744317 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Oct 2019 12:28:04 +0000 Subject: [PATCH 0313/2372] Update test/end-to-end-tests/install.sh Co-Authored-By: J. Ryan Stinnett --- test/end-to-end-tests/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/install.sh b/test/end-to-end-tests/install.sh index bb88785741..de73c3868c 100755 --- a/test/end-to-end-tests/install.sh +++ b/test/end-to-end-tests/install.sh @@ -2,7 +2,7 @@ # run with PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true sh install.sh if chrome is already installed set -e ./synapse/install.sh -# both CI and local testing don't need a riot fetched from master, +# both CI and local testing don't need a Riot fetched from master, # so not installing this by default anymore # ./riot/install.sh yarn install From 5025a0ffea004d26a70efc83e015d3a53972a8c9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Oct 2019 12:28:12 +0000 Subject: [PATCH 0314/2372] Update test/end-to-end-tests/README.md Co-Authored-By: J. Ryan Stinnett --- test/end-to-end-tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/README.md b/test/end-to-end-tests/README.md index 5e95afc1d5..cea6411228 100644 --- a/test/end-to-end-tests/README.md +++ b/test/end-to-end-tests/README.md @@ -26,7 +26,7 @@ It's important to always stop and start synapse before each run of the tests to start.js accepts these parameters (and more, see `node start.js --help`) that can help running the tests locally: - - `--riot-url ` don't use the riot copy and static server provided by the tests, but use a running server like the webpack watch server to run the tests against. Make sure to have the following local config: + - `--riot-url ` don't use the Riot copy and static server provided by the tests, but use a running server like the Webpack watch server to run the tests against. Make sure to have the following local config: - `welcomeUserId` disabled as the tests assume there is no riot-bot currently. - `--slow-mo` type at a human speed, useful with `--windowed`. - `--throttle-cpu ` throttle cpu in the browser by the given factor. Useful to reproduce failures because of insufficient timeouts happening on the slower CI server. From 06e69d114fbc79b0cd55615d730307ff313505a3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Oct 2019 12:28:31 +0000 Subject: [PATCH 0315/2372] Update test/end-to-end-tests/README.md Co-Authored-By: J. Ryan Stinnett --- test/end-to-end-tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/README.md b/test/end-to-end-tests/README.md index cea6411228..1431fdaf97 100644 --- a/test/end-to-end-tests/README.md +++ b/test/end-to-end-tests/README.md @@ -22,7 +22,7 @@ Run tests with `./run.sh`. ./synapse/start.sh && \ node start.js ``` -It's important to always stop and start synapse before each run of the tests to clear the in-memory sqlite database it uses, as the tests assume a blank slate. +It's important to always stop and start Synapse before each run of the tests to clear the in-memory SQLite database it uses, as the tests assume a blank slate. start.js accepts these parameters (and more, see `node start.js --help`) that can help running the tests locally: From 142a32b52831fc67aecd74f1674a8a789ce442c3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Oct 2019 12:28:47 +0000 Subject: [PATCH 0316/2372] Update test/end-to-end-tests/README.md Co-Authored-By: J. Ryan Stinnett --- test/end-to-end-tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/README.md b/test/end-to-end-tests/README.md index 1431fdaf97..6132ee0dbf 100644 --- a/test/end-to-end-tests/README.md +++ b/test/end-to-end-tests/README.md @@ -6,7 +6,7 @@ This directory contains tests for matrix-react-sdk. The tests fire up a headless Run `./install.sh`. This will: - install synapse, fetches the master branch at the moment. If anything fails here, please refer to the synapse README to see if you're missing one of the prerequisites. - - install riot, this fetches the master branch at the moment. + - install Riot, this fetches the master branch at the moment. - install dependencies (will download copy of chrome) ## Running the tests From 76c7f58235e11bf892499ed75961277619b64955 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Oct 2019 12:28:58 +0000 Subject: [PATCH 0317/2372] Update test/end-to-end-tests/README.md Co-Authored-By: J. Ryan Stinnett --- test/end-to-end-tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/README.md b/test/end-to-end-tests/README.md index 6132ee0dbf..a47983de4c 100644 --- a/test/end-to-end-tests/README.md +++ b/test/end-to-end-tests/README.md @@ -15,7 +15,7 @@ Run tests with `./run.sh`. ### Debug tests locally. -`./run.sh` will run the tests against the riot copy present in `riot/riot-web` served by a static python http server. You can symlink your `riot-web` develop copy here but that doesn't work well with webpack recompiling. You can run the test runner directly and specify parameters to get more insight into a failure or run the tests against your local webpack server. +`./run.sh` will run the tests against the Riot copy present in `riot/riot-web` served by a static Python HTTP server. You can symlink your `riot-web` develop copy here but that doesn't work well with Webpack recompiling. You can run the test runner directly and specify parameters to get more insight into a failure or run the tests against your local Webpack server. ``` ./synapse/stop.sh && \ From 15a75737ff063f88117ef1c0bf2d98364297a42c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Oct 2019 12:29:09 +0000 Subject: [PATCH 0318/2372] Update test/end-to-end-tests/README.md Co-Authored-By: J. Ryan Stinnett --- test/end-to-end-tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/README.md b/test/end-to-end-tests/README.md index a47983de4c..8794ef6c9b 100644 --- a/test/end-to-end-tests/README.md +++ b/test/end-to-end-tests/README.md @@ -5,7 +5,7 @@ This directory contains tests for matrix-react-sdk. The tests fire up a headless ## Setup Run `./install.sh`. This will: - - install synapse, fetches the master branch at the moment. If anything fails here, please refer to the synapse README to see if you're missing one of the prerequisites. + - install Synapse, fetches the master branch at the moment. If anything fails here, please refer to the Synapse README to see if you're missing one of the prerequisites. - install Riot, this fetches the master branch at the moment. - install dependencies (will download copy of chrome) From e2e7303cc35fea15b701e08128fe17b84322bb4f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Oct 2019 12:29:17 +0000 Subject: [PATCH 0319/2372] Update README.md Co-Authored-By: J. Ryan Stinnett --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f4c2e5a5d5..1265c2bd77 100644 --- a/README.md +++ b/README.md @@ -171,5 +171,5 @@ yarn test ## End-to-End tests -Make sure you've got your riot development server running (by doing `yarn start` in riot-web), and then in this project, run `yarn run e2etests`. +Make sure you've got your Riot development server running (by doing `yarn start` in riot-web), and then in this project, run `yarn run e2etests`. See `test/end-to-end-tests/README.md` for more information. From 3d15026da32ef4ef2f3738313aec1a659a88a0c6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Oct 2019 12:33:32 +0000 Subject: [PATCH 0320/2372] Update test/end-to-end-tests/run.sh --- test/end-to-end-tests/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/run.sh b/test/end-to-end-tests/run.sh index 085e2ba706..f61094cf75 100755 --- a/test/end-to-end-tests/run.sh +++ b/test/end-to-end-tests/run.sh @@ -5,7 +5,7 @@ BASE_DIR=$(cd $(dirname $0) && pwd) pushd $BASE_DIR if [ ! -d "synapse/installations" ] || [ ! -d "node_modules" ]; then - echo "please, first run $BASE_DIR/install.sh" +echo "Please first run $BASE_DIR/install.sh" exit 1 fi From dca968375de6d6333ec43c2dc998da9005dd8bd7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Oct 2019 12:33:42 +0000 Subject: [PATCH 0321/2372] Update test/end-to-end-tests/run.sh --- test/end-to-end-tests/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/run.sh b/test/end-to-end-tests/run.sh index f61094cf75..b9d589eed9 100755 --- a/test/end-to-end-tests/run.sh +++ b/test/end-to-end-tests/run.sh @@ -12,7 +12,7 @@ fi has_custom_riot=$(node has_custom_riot.js $@) if [ ! -d "riot/riot-web" ] && [ $has_custom_riot -ne "1" ]; then - echo "please provide an instance of riot to test against by passing --riot-url or running $BASE_DIR/riot/install.sh" + echo "Please provide an instance of riot to test against by passing --riot-url or running $BASE_DIR/riot/install.sh" exit 1 fi From 21bb1dc837eb747fa782a9005b4b80fcad882637 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 18 Oct 2019 14:43:08 +0100 Subject: [PATCH 0322/2372] Upgrade to JS SDK v2.4.2 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f1a137f628..7ef68ff8f1 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "lolex": "4.2", - "matrix-js-sdk": "2.4.2-rc.1", + "matrix-js-sdk": "2.4.2", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index 1a3dc593d0..ac23c2983a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5187,10 +5187,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc" integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw== -matrix-js-sdk@2.4.2-rc.1: - version "2.4.2-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.2-rc.1.tgz#0601c8020e34b6c0ecde6216f86f56664d7a9357" - integrity sha512-s8Bjvw6EkQQshHXC+aM846FJQdXTUIIyh5s+FRpW08yxA3I38/guF9cpJyCD44gZozXr+8c8VFTJ4Af2rRyV7A== +matrix-js-sdk@2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.2.tgz#e9c3c929469e0d885463d22f927f7ac38ad5f209" + integrity sha512-DkkUk6IX56Pkz9S7RYLn2XeTRVMrLiFOAavZvzWHs/m+k8JFtjDmJ8JVJLDA12+kL9h6rXYDN80/99RfFZH3hA== dependencies: another-json "^0.2.0" babel-runtime "^6.26.0" From 2557d893857d3b3dd59d7d1799762b3af93b7845 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 18 Oct 2019 14:48:33 +0100 Subject: [PATCH 0323/2372] Prepare changelog for v1.7.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index adf985c7ce..b95cc03595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +Changes in [1.7.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.0) (2019-10-18) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.0-rc.1...v1.7.0) + + * Upgrade to JS SDK v2.4.2 + * Fix: edit unmount when no selection + [\#3545](https://github.com/matrix-org/matrix-react-sdk/pull/3545) + * "SettingsFlag always run ToggleSwitch fully controlled" to release + [\#3542](https://github.com/matrix-org/matrix-react-sdk/pull/3542) + Changes in [1.7.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.0-rc.1) (2019-10-09) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.6.2...v1.7.0-rc.1) From 86a564b2f0b10e55665a2f9c78145b0fcff6130d Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 18 Oct 2019 14:48:34 +0100 Subject: [PATCH 0324/2372] v1.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7ef68ff8f1..7c3e4a493e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.0-rc.1", + "version": "1.7.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 0c6daf4f1971447e71439db7f4ded1436b9bc039 Mon Sep 17 00:00:00 2001 From: Walter Date: Fri, 18 Oct 2019 09:26:01 +0000 Subject: [PATCH 0325/2372] Translated using Weblate (Russian) Currently translated at 95.2% (1743 of 1831 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 5b02f0307d..dbba680e98 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -62,8 +62,8 @@ "For security, this session has been signed out. Please sign in again.": "Для обеспечения безопасности ваша сессия была завершена. Пожалуйста, войдите снова.", "Hangup": "Повесить трубку", "Historical": "Архив", - "Homeserver is": "Домашний сервер это", - "Identity Server is": "Сервер идентификации это", + "Homeserver is": "Домашний сервер:", + "Identity Server is": "Сервер идентификации:", "I have verified my email address": "Я подтвердил свой email", "Import E2E room keys": "Импорт ключей шифрования", "Invalid Email Address": "Недопустимый email", @@ -1194,7 +1194,7 @@ "COPY": "КОПИРОВАТЬ", "Jitsi Conference Calling": "Конференц-связь Jitsi", "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "В зашифрованных комнатах, подобных этой, предварительный просмотр URL-адресов отключен по умолчанию, чтобы гарантировать, что ваш сервер (где создаются предварительные просмотры) не может собирать информацию о ссылках, которые вы видите в этой комнате.", - "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Когда кто-то вставляет URL-адрес в свое сообщение, может быть отображен предварительный просмотр URL-адреса, чтобы предоставить дополнительную информацию об этой ссылке, такую как название, описание и изображение с веб-сайта.", + "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Когда кто-то вставляет URL-адрес в свое сообщение, то можно просмотреть его, чтобы получить дополнительную информацию об этой ссылке, такую как название, описание и изображение с веб-сайта.", "The email field must not be blank.": "Поле email не должно быть пустым.", "The user name field must not be blank.": "Поле имени пользователя не должно быть пустым.", "The phone number field must not be blank.": "Поле номера телефона не должно быть пустым.", @@ -1353,7 +1353,7 @@ "Lazy loading is not supported by your current homeserver.": "Ленивая подгрузка не поддерживается вашим сервером.", "Preferences": "Параметры", "Room list": "Список комнат", - "Timeline": "Временна́я шкала", + "Timeline": "Временная шкала", "Autocomplete delay (ms)": "Задержка автодополнения (мс)", "Roles & Permissions": "Роли и права", "Security & Privacy": "Безопасность и конфиденциальность", @@ -2017,5 +2017,31 @@ "Create a public room": "Создать публичную комнату", "Create a private room": "Создать приватную комнату", "Topic (optional)": "Тема (опционально)", - "Make this room public": "Сделать комнату публичной" + "Make this room public": "Сделать комнату публичной", + "Use the new, faster, composer for writing messages": "Используйте новый, более быстрый, composer для написания сообщений.", + "Send read receipts for messages (requires compatible homeserver to disable)": "Отправлять подтверждения о прочтении сообщений (требуется отключение совместимого домашнего сервера)", + "Show previews/thumbnails for images": "Показать превью / миниатюры для изображений", + "Disconnect from the identity server and connect to instead?": "Отключиться от сервера идентификации и вместо этого подключиться к ?", + "Disconnect identity server": "Отключить идентификационный сервер", + "You are still sharing your personal data on the identity server .": "Вы все еще делитесь своими личными данными на сервере идентификации .", + "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Мы рекомендуем вам удалить свои адреса электронной почты и номера телефонов с сервера идентификации перед отключением.", + "Disconnect anyway": "Отключить в любом случае", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "В настоящее время вы используете для обнаружения и быть найденным существующими контактами, которые вы знаете. Вы можете изменить ваш сервер идентификации ниже.", + "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "Если вы не хотите использовать для обнаружения вас и быть обнаруженным вашими существующими контактами, введите другой идентификационный сервер ниже.", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Вы в настоящее время не используете идентификационный сервер. Чтобы обнаружить и быть обнаруженным существующими контактами, которых вы знаете, добавьте один ниже.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Отключение от сервера идентификации будет означать, что другие пользователи не смогут вас обнаружить, и вы не сможете приглашать других по электронной почте или по телефону.", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Использование сервера идентификации не обязательно. Если вы решите не использовать сервер идентификации, другие пользователи не смогут обнаружить вас, и вы не сможете пригласить других по электронной почте или телефону.", + "The integration manager you have chosen does not have any terms of service.": "Менеджер по интеграции, которого вы выбрали, не имеет никаких условий предоставления услуг.", + "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "В настоящее время вы используете %(serverName)s для управления вашими ботами, виджетами и стикирами.", + "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Добавьте менеджер интеграции, который будет управлять вашими ботами, виджетами и стикирами.", + "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Подтвердите условия предоставления услуг сервера идентификации (%(serverName)s), чтобы вас можно было обнаружить по адресу электронной почты или номеру телефона.", + "Discovery": "Обнаружение", + "Deactivate account": "Деактивировать аккаунт", + "Clear cache and reload": "Очистить кэш и перезагрузить", + "Always show the window menu bar": "Всегда показывать строку меню", + "Read Marker lifetime (ms)": "Читать маркер время жизни (мс)", + "Read Marker off-screen lifetime (ms)": "Читать маркер вне экрана время жизни (мс)", + "A device's public name is visible to people you communicate with": "Публичное имя устройства видно людям, с которыми вы общаетесь", + "Upgrade the room": "Обновить эту комнату", + "Enable room encryption": "Включить шифрование комнаты" } From 1b63886a6baca1a4191f83992609e58e5e6dc43c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 18 Oct 2019 16:31:39 +0200 Subject: [PATCH 0326/2372] MatrixChat: Add more detailed logging to the event crawler. --- src/components/structures/MatrixChat.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d423bbd592..3558cda586 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -2101,7 +2101,10 @@ export default createReactClass({ // here. await sleep(3000); + console.log("Seshat: Running the crawler loop."); + if (cancelled) { + console.log("Seshat: Cancelling the crawler."); break; } @@ -2124,6 +2127,7 @@ export default createReactClass({ checkpoint.token, 100, checkpoint.direction); if (res.chunk.length === 0) { + console.log("Seshat: Done with the checkpoint", checkpoint) // We got to the start/end of our timeline, lets just // delete our checkpoint and go back to sleep. await platform.removeCrawlerCheckpoint(checkpoint); @@ -2223,6 +2227,7 @@ export default createReactClass({ // Let us delete the checkpoint in that case, otherwise push // the new checkpoint to be used by the crawler. if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { + console.log("Seshat: Checkpoint had already all events added, stopping the crawl", checkpoint); await platform.removeCrawlerCheckpoint(newCheckpoint); } else { this.crawlerChekpoints.push(newCheckpoint); From 89f14e55a2bb31959893f138813957acd957e032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 18 Oct 2019 16:32:43 +0200 Subject: [PATCH 0327/2372] MatrixChat: Catch errors when fetching room messages in the crawler. --- src/components/structures/MatrixChat.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 3558cda586..2f9e64efa9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -2123,8 +2123,17 @@ export default createReactClass({ const eventMapper = client.getEventMapper(); // TODO we need to ensure to use member lazy loading with this // request so we get the correct profiles. - const res = await client._createMessagesRequest(checkpoint.roomId, - checkpoint.token, 100, checkpoint.direction); + let res; + + try { + res = await client._createMessagesRequest( + checkpoint.roomId, checkpoint.token, 100, + checkpoint.direction); + } catch (e) { + console.log("Seshat: Error crawling events:", e) + this.crawlerChekpoints.push(checkpoint); + continue + } if (res.chunk.length === 0) { console.log("Seshat: Done with the checkpoint", checkpoint) From 64061173e19507ce40241989a1fb55ac705cd648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 18 Oct 2019 16:33:07 +0200 Subject: [PATCH 0328/2372] MatrixChat: Check if our state array is empty in the crawled messages response. --- src/components/structures/MatrixChat.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 2f9e64efa9..51cf92da5f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -2146,7 +2146,10 @@ export default createReactClass({ // Convert the plain JSON events into Matrix events so they get // decrypted if necessary. const matrixEvents = res.chunk.map(eventMapper); - const stateEvents = res.state.map(eventMapper); + let stateEvents = []; + if (res.state !== undefined) { + stateEvents = res.state.map(eventMapper); + } const profiles = {}; From 53332018234fc9067c6200babb794ab3538a0791 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 18 Oct 2019 16:12:20 +0100 Subject: [PATCH 0329/2372] Change back to develop branch for deps --- package.json | 2 +- yarn.lock | 29 +++++++++-------------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 7c3e4a493e..eac77bc0d7 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "lolex": "4.2", - "matrix-js-sdk": "2.4.2", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index 982e812711..1af984cfa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1567,7 +1567,7 @@ blob@0.0.5: resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== -bluebird@^3.3.0, bluebird@^3.5.0, bluebird@^3.5.5: +bluebird@3.5.5, bluebird@^3.3.0, bluebird@^3.5.0, bluebird@^3.5.5: version "3.5.5" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w== @@ -5051,10 +5051,10 @@ log4js@^4.0.0: rfdc "^1.1.4" streamroller "^1.0.6" -loglevel@1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" - integrity sha1-4PyVEztu8nbNyIh82vJKpvFW+Po= +loglevel@^1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.4.tgz#f408f4f006db8354d0577dcf6d33485b3cb90d56" + integrity sha512-p0b6mOGKcGa+7nnmKbpzR6qloPbrgLcnio++E+14Vo/XffOGwZtRpUhr8dTH/x2oCMmEoIU0Zwm3ZauhvYD17g== lolex@4.2, lolex@^4.1.0: version "4.2.0" @@ -5187,18 +5187,17 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc" integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw== -matrix-js-sdk@2.4.2: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "2.4.2" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.2.tgz#e9c3c929469e0d885463d22f927f7ac38ad5f209" - integrity sha512-DkkUk6IX56Pkz9S7RYLn2XeTRVMrLiFOAavZvzWHs/m+k8JFtjDmJ8JVJLDA12+kL9h6rXYDN80/99RfFZH3hA== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/46d7e4c7075386f1330d6a49941e9979fc26be0a" dependencies: another-json "^0.2.0" babel-runtime "^6.26.0" - bluebird "^3.5.0" + bluebird "3.5.5" browser-request "^0.3.3" bs58 "^4.0.1" content-type "^1.0.2" - loglevel "1.6.1" + loglevel "^1.6.4" qs "^6.5.2" request "^2.88.0" unhomoglyph "^1.0.2" @@ -6583,16 +6582,6 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== -react-is@^16.9.0: - version "16.9.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" - integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== - -react-is@^16.9.0: - version "16.9.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" - integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== - react-lifecycles-compat@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" From b03ebb964b12901514ca820a423e98984f602f26 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 18 Oct 2019 17:18:18 +0200 Subject: [PATCH 0330/2372] split up installing static webserver and riot copy so we can just do the latter for the e2e tests on CI --- scripts/ci/end-to-end-tests.sh | 4 +- test/end-to-end-tests/install.sh | 5 +-- .../riot/install-webserver.sh | 21 +++++++++ test/end-to-end-tests/riot/install.sh | 43 +++---------------- 4 files changed, 32 insertions(+), 41 deletions(-) create mode 100755 test/end-to-end-tests/riot/install-webserver.sh diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh index 85257420e3..567c853c2b 100644 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -34,8 +34,8 @@ ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web # CHROME_PATH=$(which google-chrome-stable) ./run.sh echo "--- Install synapse & other dependencies" ./install.sh -# install (only) static webserver to server symlinked local copy of riot -./riot/install.sh --without-riot +# install static webserver to server symlinked local copy of riot +./riot/install-webserver.sh mkdir logs echo "+++ Running end-to-end tests" TESTS_STARTED=1 diff --git a/test/end-to-end-tests/install.sh b/test/end-to-end-tests/install.sh index de73c3868c..937ef1f5eb 100755 --- a/test/end-to-end-tests/install.sh +++ b/test/end-to-end-tests/install.sh @@ -2,7 +2,6 @@ # run with PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true sh install.sh if chrome is already installed set -e ./synapse/install.sh -# both CI and local testing don't need a Riot fetched from master, -# so not installing this by default anymore -# ./riot/install.sh +# local testing doesn't need a Riot fetched from master, +# so not installing that by default yarn install diff --git a/test/end-to-end-tests/riot/install-webserver.sh b/test/end-to-end-tests/riot/install-webserver.sh new file mode 100755 index 0000000000..5c38a1eea3 --- /dev/null +++ b/test/end-to-end-tests/riot/install-webserver.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -e + +BASE_DIR=$(cd $(dirname $0) && pwd) +cd $BASE_DIR +# Install ComplexHttpServer (a drop in replacement for Python's SimpleHttpServer +# but with support for multiple threads) into a virtualenv. +( + virtualenv -p python3 env + source env/bin/activate + + # Having been bitten by pip SSL fail too many times, I don't trust the existing pip + # to be able to --upgrade itself, so grab a new one fresh from source. + curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py + python get-pip.py + rm get-pip.py + + pip install ComplexHttpServer + + deactivate +) diff --git a/test/end-to-end-tests/riot/install.sh b/test/end-to-end-tests/riot/install.sh index 9495234bcc..f66ab3224e 100755 --- a/test/end-to-end-tests/riot/install.sh +++ b/test/end-to-end-tests/riot/install.sh @@ -2,44 +2,15 @@ set -e RIOT_BRANCH=develop -with_riot=1 - -for i in $@; do - if [ "$i" == "--without-riot" ] ; then - with_riot=0 - fi -done - -BASE_DIR=$(cd $(dirname $0) && pwd) -cd $BASE_DIR -# Install ComplexHttpServer (a drop in replacement for Python's SimpleHttpServer -# but with support for multiple threads) into a virtualenv. -( - virtualenv -p python3 env - source env/bin/activate - - # Having been bitten by pip SSL fail too many times, I don't trust the existing pip - # to be able to --upgrade itself, so grab a new one fresh from source. - curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py - python get-pip.py - rm get-pip.py - - pip install ComplexHttpServer - - deactivate -) - if [ -d $BASE_DIR/riot-web ]; then echo "riot is already installed" exit fi -if [ $with_riot -eq 1 ]; then - curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --output riot.zip - unzip -q riot.zip - rm riot.zip - mv riot-web-${RIOT_BRANCH} riot-web - cd riot-web - yarn install - yarn run build -fi +curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --output riot.zip +unzip -q riot.zip +rm riot.zip +mv riot-web-${RIOT_BRANCH} riot-web +cd riot-web +yarn install +yarn run build From 75bcc3f84913c61960b9734cd60929df3d4e369b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 18 Oct 2019 19:58:55 +0300 Subject: [PATCH 0331/2372] Update src/editor/deserialize.js Co-Authored-By: Bruno Windels --- src/editor/deserialize.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index 2662d5a503..6636c9971e 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -109,7 +109,8 @@ function parseElement(n, partCreator, lastNode, state) { const indent = " ".repeat(state.listDepth - 1); if (n.parentElement.nodeName === "OL") { // The markdown parser doesn't do nested indexed lists at all, but this supports it anyway. - let index = state.listIndex[state.listIndex.length - 1]++; + let index = state.listIndex[state.listIndex.length - 1]; + state.listIndex[state.listIndex.length - 1] += 1; return partCreator.plain(`${indent}${index}. `); } else { return partCreator.plain(`${indent}- `); From 3a428efb6081716cd81cd316a7dc55709cf132ca Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 18 Oct 2019 18:01:57 +0100 Subject: [PATCH 0332/2372] Abort scroll updates when already unmounted This checks whether we're unmounted before updating scroll state, as we use async functions and timeouts in this area. Fixes https://github.com/vector-im/riot-web/issues/11150 --- src/components/structures/ScrollPanel.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index cb29305dd3..32efad1e05 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -677,6 +677,11 @@ module.exports = createReactClass({ debuglog("updateHeight getting straight to business, no scrolling going on."); } + // We might have unmounted since the timer finished, so abort if so. + if (this.unmounted) { + return; + } + const sn = this._getScrollNode(); const itemlist = this.refs.itemlist; const contentHeight = this._getMessagesHeight(); From 0e6359ab24a7ead4c1c57ca36c303686a06edc2a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 19 Oct 2019 16:39:06 +0100 Subject: [PATCH 0333/2372] replace @use-it/event-listener as it doesn't like Node EE's --- package.json | 1 - src/components/views/right_panel/UserInfo.js | 15 ++++----- src/hooks/useEventEmitter.js | 35 ++++++++++++++++++++ yarn.lock | 5 --- 4 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 src/hooks/useEventEmitter.js diff --git a/package.json b/package.json index e24901070f..ef7041a58f 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "test-multi": "karma start" }, "dependencies": { - "@use-it/event-listener": "^0.1.3", "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-runtime": "^6.26.0", "bluebird": "^3.5.0", diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 59cd61a583..9d0ef7cf15 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -20,7 +20,6 @@ limitations under the License. import React, {useCallback, useMemo, useState, useEffect} from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import useEventListener from '@use-it/event-listener'; import {Group, MatrixClient, RoomMember, User} from 'matrix-js-sdk'; import dis from '../../../dispatcher'; import Modal from '../../../Modal'; @@ -39,6 +38,7 @@ import MultiInviter from "../../../utils/MultiInviter"; import GroupStore from "../../../stores/GroupStore"; import MatrixClientPeg from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; +import {useEventEmitter} from "../../../hooks/useEventEmitter"; const _disambiguateDevices = (devices) => { const names = Object.create(null); @@ -129,8 +129,7 @@ const DirectChatsSection = withLegacyMatrixClient(({cli, userId, startUpdating, setDmRooms(dmRoomMap.getDMRoomsForUserId(userId)); } }, [cli, userId]); - - useEventListener("accountData", accountDataHandler, cli); + useEventEmitter(cli, "accountData", accountDataHandler); const RoomTile = sdk.getComponent("rooms.RoomTile"); @@ -220,9 +219,7 @@ const UserOptionsSection = withLegacyMatrixClient(({cli, member, isIgnored, canI ignoredUsers.push(member.userId); } - cli.setIgnoredUsers(ignoredUsers).then(() => { - // return this.setState({isIgnoring: !this.state.isIgnoring}); - }); + cli.setIgnoredUsers(ignoredUsers); }; ignoreButton = ( @@ -364,7 +361,7 @@ const useRoomPowerLevels = (room) => { }; }, [room]); - useEventListener("RoomState.events", update, room); + useEventEmitter(room, "RoomState.events", update); useEffect(() => { update(); return () => { @@ -759,7 +756,7 @@ const UserInfo = withLegacyMatrixClient(({cli, user, groupId, roomId, onClose}) setIsIgnored(cli.isUserIgnored(user.userId)); } }, [cli, user.userId]); - useEventListener("accountData", accountDataHandler, cli); + useEventEmitter(cli, "accountData", accountDataHandler); // Count of how many operations are currently in progress, if > 0 then show a Spinner const [pendingUpdateCount, setPendingUpdateCount] = useState(0); @@ -806,7 +803,7 @@ const UserInfo = withLegacyMatrixClient(({cli, user, groupId, roomId, onClose}) modifyLevelMax, }); }, [cli, user, room]); - useEventListener("RoomState.events", updateRoomPermissions, cli); + useEventEmitter(cli, "RoomState.events", updateRoomPermissions); useEffect(() => { updateRoomPermissions(); return () => { diff --git a/src/hooks/useEventEmitter.js b/src/hooks/useEventEmitter.js new file mode 100644 index 0000000000..0cd57bc209 --- /dev/null +++ b/src/hooks/useEventEmitter.js @@ -0,0 +1,35 @@ +import {useRef, useEffect} from "react"; + +// Hook to wrap event emitter on and removeListener in hook lifecycle +export const useEventEmitter = (emitter, eventName, handler) => { + // Create a ref that stores handler + const savedHandler = useRef(); + + // Update ref.current value if handler changes. + // This allows our effect below to always get latest handler ... + // ... without us needing to pass it in effect deps array ... + // ... and potentially cause effect to re-run every render. + useEffect(() => { + savedHandler.current = handler; + }, [handler]); + + useEffect( + () => { + // Make sure element supports on + const isSupported = emitter && emitter.on; + if (!isSupported) return; + + // Create event listener that calls handler function stored in ref + const eventListener = event => savedHandler.current(event); + + // Add event listener + emitter.on(eventName, eventListener); + + // Remove event listener on cleanup + return () => { + emitter.removeListener(eventName, eventListener); + }; + }, + [eventName, emitter], // Re-run if eventName or emitter changes + ); +}; diff --git a/yarn.lock b/yarn.lock index e40a1272cf..2bee2661b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -293,11 +293,6 @@ dependencies: "@types/yargs-parser" "*" -"@use-it/event-listener@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@use-it/event-listener/-/event-listener-0.1.3.tgz#a9920b2819d211cf55e68e830997546eec6886d3" - integrity sha512-UCHkLOVU+xj3/1R8jXz8GzDTowkzfIDPESOBlVC2ndgwUSBEqiFdwCoUEs2lcGhJOOiEdmWxF+T23C5+60eEew== - "@webassemblyjs/ast@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" From 4de0b3c177f55c375368d0ab6c8e5798167a1964 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 19 Oct 2019 16:48:39 +0100 Subject: [PATCH 0334/2372] Clean up useEventEmitter --- src/hooks/useEventEmitter.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/hooks/useEventEmitter.js b/src/hooks/useEventEmitter.js index 0cd57bc209..56676bf871 100644 --- a/src/hooks/useEventEmitter.js +++ b/src/hooks/useEventEmitter.js @@ -1,3 +1,19 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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 {useRef, useEffect} from "react"; // Hook to wrap event emitter on and removeListener in hook lifecycle @@ -6,19 +22,12 @@ export const useEventEmitter = (emitter, eventName, handler) => { const savedHandler = useRef(); // Update ref.current value if handler changes. - // This allows our effect below to always get latest handler ... - // ... without us needing to pass it in effect deps array ... - // ... and potentially cause effect to re-run every render. useEffect(() => { savedHandler.current = handler; }, [handler]); useEffect( () => { - // Make sure element supports on - const isSupported = emitter && emitter.on; - if (!isSupported) return; - // Create event listener that calls handler function stored in ref const eventListener = event => savedHandler.current(event); From be829980f68aed3d466de0e863ed5d888d9fda30 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 20 Oct 2019 12:13:32 +0300 Subject: [PATCH 0335/2372] Split inline SVGs to their own files Signed-off-by: Tulir Asokan --- res/css/views/emojipicker/_EmojiPicker.scss | 49 ++++-- res/img/emojipicker/activity.svg | 14 ++ res/img/emojipicker/custom.svg | 34 ++++ res/img/emojipicker/delete.svg | 15 ++ res/img/emojipicker/flags.svg | 14 ++ res/img/emojipicker/foods.svg | 14 ++ res/img/emojipicker/nature.svg | 15 ++ res/img/emojipicker/objects.svg | 15 ++ res/img/emojipicker/people.svg | 15 ++ res/img/emojipicker/places.svg | 15 ++ res/img/emojipicker/recent.svg | 15 ++ res/img/emojipicker/search.svg | 15 ++ res/img/emojipicker/symbols.svg | 14 ++ src/components/views/emojipicker/Header.js | 11 +- src/components/views/emojipicker/Search.js | 9 +- src/components/views/emojipicker/icons.js | 170 -------------------- 16 files changed, 241 insertions(+), 193 deletions(-) create mode 100644 res/img/emojipicker/activity.svg create mode 100644 res/img/emojipicker/custom.svg create mode 100644 res/img/emojipicker/delete.svg create mode 100644 res/img/emojipicker/flags.svg create mode 100644 res/img/emojipicker/foods.svg create mode 100644 res/img/emojipicker/nature.svg create mode 100644 res/img/emojipicker/objects.svg create mode 100644 res/img/emojipicker/people.svg create mode 100644 res/img/emojipicker/places.svg create mode 100644 res/img/emojipicker/recent.svg create mode 100644 res/img/emojipicker/search.svg create mode 100644 res/img/emojipicker/symbols.svg delete mode 100644 src/components/views/emojipicker/icons.js diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index ddb3e82eca..5f3cfb8133 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -36,16 +36,6 @@ limitations under the License. border-bottom: 1px solid $message-action-bar-border-color; } -.mx_EmojiPicker button > svg { - width: 100%; - height: 100%; - fill: $primary-fg-color; -} - -.mx_EmojiPicker button:disabled > svg { - fill: $focus-bg-color; -} - .mx_EmojiPicker_anchor { border: none; padding: 8px 8px 6px; @@ -66,6 +56,31 @@ limitations under the License. } } +.mx_EmojiPicker_anchor::before { + background-color: $primary-fg-color; + content: ''; + display: inline-block; + mask-size: 100%; + mask-repeat: no-repeat; + width: 100%; + height: 100%; +} + +.mx_EmojiPicker_anchor:disabled::before { + background-color: $focus-bg-color; +} + +.mx_EmojiPicker_anchor_activity::before { mask-image: url('$(res)/img/emojipicker/activity.svg') } +.mx_EmojiPicker_anchor_custom::before { mask-image: url('$(res)/img/emojipicker/custom.svg') } +.mx_EmojiPicker_anchor_flags::before { mask-image: url('$(res)/img/emojipicker/flags.svg') } +.mx_EmojiPicker_anchor_foods::before { mask-image: url('$(res)/img/emojipicker/foods.svg') } +.mx_EmojiPicker_anchor_nature::before { mask-image: url('$(res)/img/emojipicker/nature.svg') } +.mx_EmojiPicker_anchor_objects::before { mask-image: url('$(res)/img/emojipicker/objects.svg') } +.mx_EmojiPicker_anchor_people::before { mask-image: url('$(res)/img/emojipicker/people.svg') } +.mx_EmojiPicker_anchor_places::before { mask-image: url('$(res)/img/emojipicker/places.svg') } +.mx_EmojiPicker_anchor_recent::before { mask-image: url('$(res)/img/emojipicker/recent.svg') } +.mx_EmojiPicker_anchor_symbols::before { mask-image: url('$(res)/img/emojipicker/symbols.svg') } + .mx_EmojiPicker_anchor_visible { border-bottom: 2px solid $button-bg-color; } @@ -99,6 +114,20 @@ limitations under the License. cursor: pointer; } +.mx_EmojiPicker_search_icon::after { + mask: url('$(res)/img/emojipicker/search.svg') no-repeat; + mask-size: 100%; + background-color: $primary-fg-color; + content: ''; + display: inline-block; + width: 100%; + height: 100%; +} + +.mx_EmojiPicker_search_clear::after { + mask-image: url('$(res)/img/emojipicker/delete.svg'); +} + .mx_EmojiPicker_category { padding: 0 12px; display: flex; diff --git a/res/img/emojipicker/activity.svg b/res/img/emojipicker/activity.svg new file mode 100644 index 0000000000..d921667e7a --- /dev/null +++ b/res/img/emojipicker/activity.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/res/img/emojipicker/custom.svg b/res/img/emojipicker/custom.svg new file mode 100644 index 0000000000..814cd8ec13 --- /dev/null +++ b/res/img/emojipicker/custom.svg @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/res/img/emojipicker/delete.svg b/res/img/emojipicker/delete.svg new file mode 100644 index 0000000000..5f5d4e52eb --- /dev/null +++ b/res/img/emojipicker/delete.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/res/img/emojipicker/flags.svg b/res/img/emojipicker/flags.svg new file mode 100644 index 0000000000..bd0a935265 --- /dev/null +++ b/res/img/emojipicker/flags.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/res/img/emojipicker/foods.svg b/res/img/emojipicker/foods.svg new file mode 100644 index 0000000000..57a15976d8 --- /dev/null +++ b/res/img/emojipicker/foods.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/res/img/emojipicker/nature.svg b/res/img/emojipicker/nature.svg new file mode 100644 index 0000000000..a4778be927 --- /dev/null +++ b/res/img/emojipicker/nature.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/res/img/emojipicker/objects.svg b/res/img/emojipicker/objects.svg new file mode 100644 index 0000000000..e0d39f985a --- /dev/null +++ b/res/img/emojipicker/objects.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/res/img/emojipicker/people.svg b/res/img/emojipicker/people.svg new file mode 100644 index 0000000000..c2fdb579f6 --- /dev/null +++ b/res/img/emojipicker/people.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/res/img/emojipicker/places.svg b/res/img/emojipicker/places.svg new file mode 100644 index 0000000000..0947baaf04 --- /dev/null +++ b/res/img/emojipicker/places.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/res/img/emojipicker/recent.svg b/res/img/emojipicker/recent.svg new file mode 100644 index 0000000000..2fdcc65cd2 --- /dev/null +++ b/res/img/emojipicker/recent.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/res/img/emojipicker/search.svg b/res/img/emojipicker/search.svg new file mode 100644 index 0000000000..b5f660b3ac --- /dev/null +++ b/res/img/emojipicker/search.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/res/img/emojipicker/symbols.svg b/res/img/emojipicker/symbols.svg new file mode 100644 index 0000000000..a2b86d9ec8 --- /dev/null +++ b/res/img/emojipicker/symbols.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/src/components/views/emojipicker/Header.js b/src/components/views/emojipicker/Header.js index 04addfc81d..05cbebbfb1 100644 --- a/src/components/views/emojipicker/Header.js +++ b/src/components/views/emojipicker/Header.js @@ -17,8 +17,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import * as icons from "./icons"; - class Header extends React.PureComponent { static propTypes = { categories: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -31,13 +29,12 @@ class Header extends React.PureComponent { - ) + ); } } diff --git a/src/components/views/emojipicker/Search.js b/src/components/views/emojipicker/Search.js index 547f673815..dbdb91ff10 100644 --- a/src/components/views/emojipicker/Search.js +++ b/src/components/views/emojipicker/Search.js @@ -17,8 +17,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import * as icons from "./icons"; - class Search extends React.PureComponent { static propTypes = { query: PropTypes.string.isRequired, @@ -39,11 +37,10 @@ class Search extends React.PureComponent { return (
    this.props.onChange(ev.target.value)} ref={this.inputRef}/> + onChange={ev => this.props.onChange(ev.target.value)} ref={this.inputRef} /> + className={`mx_EmojiPicker_search_icon ${this.props.query ? "mx_EmojiPicker_search_clear" : ""}`} + title={this.props.query ? "Cancel search" : "Search"} />
    ); } diff --git a/src/components/views/emojipicker/icons.js b/src/components/views/emojipicker/icons.js deleted file mode 100644 index b6cf1ad371..0000000000 --- a/src/components/views/emojipicker/icons.js +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) 2016, Missive -// From https://github.com/missive/emoji-mart/blob/master/src/svgs/index.js -// Licensed under BSD-3-Clause: https://github.com/missive/emoji-mart/blob/master/LICENSE - -import React from 'react' - -const categories = { - activity: () => ( - - - - ), - - custom: () => ( - - - - - - - - ), - - flags: () => ( - - - - ), - - foods: () => ( - - - - ), - - nature: () => ( - - - - - ), - - objects: () => ( - - - - - ), - - people: () => ( - - - - - ), - - places: () => ( - - - - - ), - - recent: () => ( - - - - - ), - - symbols: () => ( - - - - ), -} - -const search = { - search: () => ( - - - - ), - - delete: () => ( - - - - ), -} - -export { categories, search } From 10732e8e7341a0cb60a7919ea4904c208428dc7c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 20 Oct 2019 12:13:42 +0300 Subject: [PATCH 0336/2372] Fix license headers Signed-off-by: Tulir Asokan --- src/components/views/emojipicker/Category.js | 2 +- src/components/views/emojipicker/Emoji.js | 2 +- src/components/views/emojipicker/EmojiPicker.js | 2 +- src/components/views/emojipicker/Header.js | 2 +- src/components/views/emojipicker/Preview.js | 2 +- src/components/views/emojipicker/QuickReactions.js | 2 +- src/components/views/emojipicker/ReactionPicker.js | 2 +- src/components/views/emojipicker/Search.js | 2 +- src/components/views/emojipicker/recent.js | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/views/emojipicker/Category.js b/src/components/views/emojipicker/Category.js index b8e56488d8..ba48c8842b 100644 --- a/src/components/views/emojipicker/Category.js +++ b/src/components/views/emojipicker/Category.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +Copyright 2019 Tulir Asokan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/emojipicker/Emoji.js b/src/components/views/emojipicker/Emoji.js index 3db5882fb3..8d6ffe122b 100644 --- a/src/components/views/emojipicker/Emoji.js +++ b/src/components/views/emojipicker/Emoji.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +Copyright 2019 Tulir Asokan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.js index 1d5b11edb1..6bf79d2623 100644 --- a/src/components/views/emojipicker/EmojiPicker.js +++ b/src/components/views/emojipicker/EmojiPicker.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +Copyright 2019 Tulir Asokan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/emojipicker/Header.js b/src/components/views/emojipicker/Header.js index 05cbebbfb1..b98e90e9b1 100644 --- a/src/components/views/emojipicker/Header.js +++ b/src/components/views/emojipicker/Header.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +Copyright 2019 Tulir Asokan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/emojipicker/Preview.js b/src/components/views/emojipicker/Preview.js index 75d3e35f31..618b9473c7 100644 --- a/src/components/views/emojipicker/Preview.js +++ b/src/components/views/emojipicker/Preview.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +Copyright 2019 Tulir Asokan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/emojipicker/QuickReactions.js b/src/components/views/emojipicker/QuickReactions.js index 2357345460..820865dc88 100644 --- a/src/components/views/emojipicker/QuickReactions.js +++ b/src/components/views/emojipicker/QuickReactions.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +Copyright 2019 Tulir Asokan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/emojipicker/ReactionPicker.js b/src/components/views/emojipicker/ReactionPicker.js index 4dd7ea0da7..d027ae6fd3 100644 --- a/src/components/views/emojipicker/ReactionPicker.js +++ b/src/components/views/emojipicker/ReactionPicker.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +Copyright 2019 Tulir Asokan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/emojipicker/Search.js b/src/components/views/emojipicker/Search.js index dbdb91ff10..17fbde648b 100644 --- a/src/components/views/emojipicker/Search.js +++ b/src/components/views/emojipicker/Search.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +Copyright 2019 Tulir Asokan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/emojipicker/recent.js b/src/components/views/emojipicker/recent.js index f37fbdb235..1d2106fbfb 100644 --- a/src/components/views/emojipicker/recent.js +++ b/src/components/views/emojipicker/recent.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +Copyright 2019 Tulir Asokan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 438ad54701202c544bae72af341e19c8bff337db Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 20 Oct 2019 12:31:28 +0300 Subject: [PATCH 0337/2372] Remove space between emojis in picker Signed-off-by: Tulir Asokan --- res/css/views/emojipicker/_EmojiPicker.scss | 17 +++++++++++------ src/components/views/emojipicker/Emoji.js | 6 ++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index 5f3cfb8133..6dcc4d75b9 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -145,17 +145,22 @@ limitations under the License. margin: 0; } -.mx_EmojiPicker_item { +.mx_EmojiPicker_item_wrapper { + display: inline-block; list-style: none; + width: 38px; + cursor: pointer; +} + +.mx_EmojiPicker_item { display: inline-block; font-size: 20px; - margin: 1px; - padding: 4px 0; - width: 36px; + padding: 5px; + width: 100%; + height: 100%; box-sizing: border-box; text-align: center; border-radius: 4px; - cursor: pointer; &:hover { background-color: $focus-bg-color; @@ -165,7 +170,7 @@ limitations under the License. .mx_EmojiPicker_item_selected { color: rgba(0, 0, 0, .5); border: 1px solid $input-valid-border-color; - margin: 0; + padding: 4px; } .mx_EmojiPicker_category_label, .mx_EmojiPicker_preview_name { diff --git a/src/components/views/emojipicker/Emoji.js b/src/components/views/emojipicker/Emoji.js index 8d6ffe122b..75f23c5761 100644 --- a/src/components/views/emojipicker/Emoji.js +++ b/src/components/views/emojipicker/Emoji.js @@ -33,8 +33,10 @@ class Emoji extends React.PureComponent {
  • onClick(emoji)} onMouseEnter={() => onMouseEnter(emoji)} onMouseLeave={() => onMouseLeave(emoji)} - className={`mx_EmojiPicker_item ${isSelected ? 'mx_EmojiPicker_item_selected' : ''}`}> - {emoji.unicode} + className="mx_EmojiPicker_item_wrapper"> +
    + {emoji.unicode} +
  • ); } From b2deb548d309c29826198fddff4485ae5aa4b80c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 20 Oct 2019 12:40:55 +0300 Subject: [PATCH 0338/2372] Translate search button titles Signed-off-by: Tulir Asokan --- src/components/views/emojipicker/Search.js | 3 ++- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/emojipicker/Search.js b/src/components/views/emojipicker/Search.js index 17fbde648b..8646559fed 100644 --- a/src/components/views/emojipicker/Search.js +++ b/src/components/views/emojipicker/Search.js @@ -16,6 +16,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import { _t } from '../../../languageHandler'; class Search extends React.PureComponent { static propTypes = { @@ -40,7 +41,7 @@ class Search extends React.PureComponent { onChange={ev => this.props.onChange(ev.target.value)} ref={this.inputRef} />
    ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4b4330cf7a..add5af02c4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1840,5 +1840,6 @@ "Objects": "Objects", "Symbols": "Symbols", "Flags": "Flags", - "React": "React" + "React": "React", + "Cancel search": "Cancel search" } From 1613c39a7062e748086ed8f0ea90de80ee5ff5b8 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Sat, 19 Oct 2019 09:29:44 +0000 Subject: [PATCH 0339/2372] Translated using Weblate (Albanian) Currently translated at 99.7% (1833 of 1838 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 6a20183b1d..4f25162491 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -1830,7 +1830,7 @@ "Reason: %(reason)s": "Arsye: %(reason)s", "Forget this room": "Harroje këtë dhomë", "Re-join": "Rihyni", - "You were banned from %(roomName)s by %(memberName)s": "Jeni dëbuar prej %(roomName)s nga %(userName)s", + "You were banned from %(roomName)s by %(memberName)s": "Jeni dëbuar prej %(roomName)s nga %(memberName)s", "Join the discussion": "Merrni pjesë në diskutim", "Try to join anyway": "Provoni të merrni pjesë, sido qoftë", "Do you want to chat with %(user)s?": "Doni të bisedoni me %(user)s?", @@ -2178,5 +2178,32 @@ "It has %(count)s unread messages including mentions.|other": "Ka %(count)s mesazhe të palexuar, përfshi përmendje.", "It has %(count)s unread messages.|other": "Ka %(count)s mesazhe të palexuar.", "It has unread mentions.": "Ka përmendje të palexuara.", - "Community Autocomplete": "Vetëplotësim Nga të Bashkësisë" + "Community Autocomplete": "Vetëplotësim Nga të Bashkësisë", + "Add Email Address": "Shtoni Adresë Email", + "Add Phone Number": "Shtoni Numër Telefoni", + "Use the new, faster, composer for writing messages": "Përdorni për shkrim mesazhesh hartuesin e ri, më të shpejtë", + "Show previews/thumbnails for images": "Shfaq për figurat paraparje/miniatura", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Përpara shkëputjes, duhet të hiqni të dhënat tuaja personale nga shërbyesi i identiteteve . Mjerisht, shërbyesi i identiteteve hëpërhë është jashtë funksionimi dhe s’mund të kapet.", + "You should:": "Duhet:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "të kontrolloni shtojcat e shfletuesit tuaj për çfarëdo që mund të bllokojë shërbyesin e identiteteve (bie fjala, Privacy Badger)", + "contact the administrators of identity server ": "të lidheni me përgjegjësit e shërbyesit të identiteteve ", + "wait and try again later": "të prisni dhe të riprovoni më vonë", + "Clear cache and reload": "Të pastroni fshehtinën dhe të ringarkoni", + "Your email address hasn't been verified yet": "Adresa juaj email s’është verifikuar ende", + "Click the link in the email you received to verify and then click continue again.": "Për verifkim, klikoni lidhjen te email që morët dhe mandej vazhdoni sërish.", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "Ju ndan një hap nga heqja e 1 mesazhi prej %(user)s. Kjo s’mund të zhbëhet. Doni të vazhdohet?", + "Remove %(count)s messages|one": "Hiq 1 mesazh", + "%(count)s unread messages including mentions.|other": "%(count)s mesazhe të palexuar, përfshi përmendje.", + "%(count)s unread messages.|other": "%(count)s mesazhe të palexuar.", + "Unread mentions.": "Përmendje të palexuara.", + "Show image": "Shfaq figurë", + "Please create a new issue on GitHub so that we can investigate this bug.": "Ju lutemi, krijoni një çështje të re në GitHub, që të mund ta hetojmë këtë të metë.", + "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Mungon kyç publik captcha-je te formësimi i shërbyesit Home. Ju lutemi, njoftojani këtë përgjegjësit të shërbyesit tuaj Home.", + "%(creator)s created and configured the room.": "%(creator)s krijoi dhe formësoi dhomën.", + "Command Autocomplete": "Vetëplotësim Urdhrash", + "DuckDuckGo Results": "Përfundime nga DuckDuckGo", + "Emoji Autocomplete": "Vetëplotësim Emoji-sh", + "Notification Autocomplete": "Vetëplotësim NJoftimesh", + "Room Autocomplete": "Vetëplotësim Dhomash", + "User Autocomplete": "Vetëplotësim Përdoruesish" } From dcb45d30b476625279d5b9841e5b899ac0ee6e5d Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Sun, 20 Oct 2019 03:28:11 +0000 Subject: [PATCH 0340/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1838 of 1838 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index c172f28f0a..5d128e71e8 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2236,5 +2236,12 @@ "Remove %(count)s messages|one": "移除 1 則訊息", "Add Email Address": "新增電子郵件地址", "Add Phone Number": "新增電話號碼", - "%(creator)s created and configured the room.": "%(creator)s 建立並設定了聊天室。" + "%(creator)s created and configured the room.": "%(creator)s 建立並設定了聊天室。", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "您應該在斷線前從身份識別伺服器 移除您的個人資料。不幸的是,身份識別伺服器 目前離線中或無法連線。", + "You should:": "您應該:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "檢查您的瀏覽器,看有沒有任何可能阻擋身份識別伺服器的外掛程式(如 Privacy Badger)", + "contact the administrators of identity server ": "聯絡身份識別伺服器 的管理員", + "wait and try again later": "稍候並再試一次", + "Command Autocomplete": "指令自動完成", + "DuckDuckGo Results": "DuckDuckGo 結果" } From 014866c63f79a6cef646cd91d7196c68cb92b642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Sat, 19 Oct 2019 08:44:03 +0000 Subject: [PATCH 0341/2372] Translated using Weblate (French) Currently translated at 100.0% (1838 of 1838 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 4080238d8c..ba5a1aa0c1 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2243,5 +2243,12 @@ "Click the link in the email you received to verify and then click continue again.": "Cliquez sur le lien dans l’e-mail que vous avez reçu pour la vérifier et cliquez encore sur continuer.", "Add Email Address": "Ajouter une adresse e-mail", "Add Phone Number": "Ajouter un numéro de téléphone", - "%(creator)s created and configured the room.": "%(creator)s a créé et configuré le salon." + "%(creator)s created and configured the room.": "%(creator)s a créé et configuré le salon.", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Vous devriez supprimer vos données personnelles du serveur d’identité avant de vous déconnecter. Malheureusement, le serveur d’identité est actuellement hors ligne ou injoignable.", + "You should:": "Vous devriez :", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "vérifier qu’aucune des extensions de votre navigateur ne bloque le serveur d’identité (comme Privacy Badger)", + "contact the administrators of identity server ": "contacter les administrateurs du serveur d’identité ", + "wait and try again later": "attendre et réessayer plus tard", + "Command Autocomplete": "Autocomplétion de commande", + "DuckDuckGo Results": "Résultats de DuckDuckGo" } From 9c1c8fe89c7b5cc7b75bfc62df5c0e77ca13ede4 Mon Sep 17 00:00:00 2001 From: Michael Albert Date: Sat, 19 Oct 2019 15:40:20 +0000 Subject: [PATCH 0342/2372] Translated using Weblate (German) Currently translated at 83.8% (1540 of 1838 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index fb0809b2b6..1edfebbc40 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1825,7 +1825,7 @@ "The file '%(fileName)s' failed to upload.": "Die Datei \"%(fileName)s\" konnte nicht hochgeladen werden.", "Please confirm that you'd like to go forward with upgrading this room from to .": "Bitte bestätige, dass du mit dem Raum-Upgrade von auf fortfahren möchtest.", "Upgrade": "Upgrade", - "Changes your avatar in this current room only": "Ändert deinen Avatar für den diesen Raum", + "Changes your avatar in this current room only": "Ändert deinen Avatar für diesen Raum", "Unbans user with given ID": "Entbannt den Benutzer mit der angegebenen ID", "Sends the given message coloured as a rainbow": "Sendet die Nachricht in Regenbogenfarben", "Adds a custom widget by URL to the room": "Fügt ein Benutzer-Widget über eine URL zum Raum hinzu", @@ -1917,5 +1917,8 @@ "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Warnung: Deine persönlichen Daten (inkl. Verschlüsselungsschlüssel) sind noch auf diesem Gerät gespeichert. Lösche diese Daten, wenn du mit der Nutzung auf diesem Gerät fertig bist oder dich in einen anderen Account einloggen möchtest.", "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Wenn du die Verbindung zu deinem Identitätsserver trennst, heißt das, dass du nicht mehr von anderen Benutzern gefunden werden und auch andere nicht mehr per E-Mail oder Telefonnummer einladen kannst.", "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Bitte frage den Administrator deines Heimservers (%(homeserverDomain)s) darum, einen TURN-Server einzurichten, damit Anrufe zuverlässig funktionieren.", - "Disconnect from the identity server ?": "Verbindung zum Identitätsserver trennen?" + "Disconnect from the identity server ?": "Verbindung zum Identitätsserver trennen?", + "Add Email Address": "E-Mail-Adresse hinzufügen", + "Add Phone Number": "Telefonnummer hinzufügen", + "Changes the avatar of the current room": "Ändert den Avatar für diesen Raum" } From 392d112a883e86e9f06056f7ab6aeba9b7d812f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Sat, 19 Oct 2019 07:28:48 +0000 Subject: [PATCH 0343/2372] Translated using Weblate (Korean) Currently translated at 100.0% (1838 of 1838 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 5bd1f69bb6..00a914d843 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2087,5 +2087,12 @@ "Click the link in the email you received to verify and then click continue again.": "받은 이메일에 있는 링크를 클릭해서 확인한 후에 계속하기를 클릭하세요.", "Add Email Address": "이메일 주소 추가", "Add Phone Number": "전화번호 추가", - "%(creator)s created and configured the room.": "%(creator)s님이 방을 만드고 설정했습니다." + "%(creator)s created and configured the room.": "%(creator)s님이 방을 만드고 설정했습니다.", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "계정을 해제하기 전에 ID 서버 에서 개인 정보를 삭제해야 합니다. 불행하게도 ID 서버 가 현재 오프라인이거나 접근할 수 없는 상태입니다.", + "You should:": "이렇게 하세요:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "브라우저 플러그인을 확인하고 (Privacy Badger같은) ID 서버를 막는 것이 있는지 확인하세요", + "contact the administrators of identity server ": "ID 서버 의 관리자와 연락하세요", + "wait and try again later": "기다리고 나중에 다시 시도하세요", + "Command Autocomplete": "명령어 자동 완성", + "DuckDuckGo Results": "DuckDuckGo 결과" } From 43cd00c73d0bbf64b9b2f4e28b71c96e883077dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Sun, 20 Oct 2019 11:52:57 +0000 Subject: [PATCH 0344/2372] Translated using Weblate (French) Currently translated at 100.0% (1850 of 1850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index ba5a1aa0c1..3a962cc1c9 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2250,5 +2250,17 @@ "contact the administrators of identity server ": "contacter les administrateurs du serveur d’identité ", "wait and try again later": "attendre et réessayer plus tard", "Command Autocomplete": "Autocomplétion de commande", - "DuckDuckGo Results": "Résultats de DuckDuckGo" + "DuckDuckGo Results": "Résultats de DuckDuckGo", + "Quick Reactions": "Réactions rapides", + "Frequently Used": "Utilisé fréquemment", + "Smileys & People": "Émoticônes & personnes", + "Animals & Nature": "Animaux & nature", + "Food & Drink": "Nourriture & boisson", + "Activities": "Activités", + "Travel & Places": "Voyage & lieux", + "Objects": "Objets", + "Symbols": "Symboles", + "Flags": "Drapeaux", + "React": "Réagir", + "Cancel search": "Annuler la recherche" } From caafd6e5c94dd1eeb6103b9928b4a011b8c31e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Mon, 21 Oct 2019 03:27:39 +0000 Subject: [PATCH 0345/2372] Translated using Weblate (Korean) Currently translated at 100.0% (1850 of 1850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 00a914d843..f7c0b0680d 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2094,5 +2094,17 @@ "contact the administrators of identity server ": "ID 서버 의 관리자와 연락하세요", "wait and try again later": "기다리고 나중에 다시 시도하세요", "Command Autocomplete": "명령어 자동 완성", - "DuckDuckGo Results": "DuckDuckGo 결과" + "DuckDuckGo Results": "DuckDuckGo 결과", + "Quick Reactions": "빠른 리액션", + "Frequently Used": "자주 사용함", + "Smileys & People": "표정 & 사람", + "Animals & Nature": "동물 & 자연", + "Food & Drink": "음식 & 음료", + "Activities": "활동", + "Travel & Places": "여행 & 장소", + "Objects": "물건", + "Symbols": "기호", + "Flags": "깃발", + "React": "리액션", + "Cancel search": "검색 취소" } From 292f8fb2622d7d2fc63580e6268b351787af4778 Mon Sep 17 00:00:00 2001 From: Michael Albert Date: Mon, 21 Oct 2019 11:29:43 +0000 Subject: [PATCH 0346/2372] Translated using Weblate (German) Currently translated at 83.3% (1541 of 1850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 1edfebbc40..85b9693ee9 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1920,5 +1920,6 @@ "Disconnect from the identity server ?": "Verbindung zum Identitätsserver trennen?", "Add Email Address": "E-Mail-Adresse hinzufügen", "Add Phone Number": "Telefonnummer hinzufügen", - "Changes the avatar of the current room": "Ändert den Avatar für diesen Raum" + "Changes the avatar of the current room": "Ändert den Avatar für diesen Raum", + "Deactivate account": "Benutzerkonto schließen" } From ae6e39b233a662320f7b03dcd291ccc8d2e8b82f Mon Sep 17 00:00:00 2001 From: Walter Date: Mon, 21 Oct 2019 12:22:50 +0000 Subject: [PATCH 0347/2372] Translated using Weblate (Russian) Currently translated at 97.3% (1800 of 1850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 67 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index dbba680e98..b14a4864c6 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -636,7 +636,7 @@ "Cannot add any more widgets": "Невозможно добавить больше виджетов", "Changes colour scheme of current room": "Изменяет цветовую схему текущей комнаты", "Delete widget": "Удалить виджет", - "Define the power level of a user": "Определить уровень доступа пользователя", + "Define the power level of a user": "Определить уровень прав пользователя", "Do you want to load widget from URL:": "Вы собираетесь загрузить виджет по URL-адресу:", "Edit": "Изменить", "Enable automatic language detection for syntax highlighting": "Автоматически определять язык подсветки синтаксиса", @@ -1654,7 +1654,7 @@ "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s включено для %(groups)s в этой комнате.", "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s выключено для %(groups)s в этой комнате.", "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s включено для %(newGroups)s и отключено для %(oldGroups)s в этой комнате.", - "Once enabled, encryption cannot be disabled.": "После включения шифрование не может быть отключено.", + "Once enabled, encryption cannot be disabled.": "После включения, шифрование не может быть отключено.", "Some devices for this user are not trusted": "Некоторые устройства для этого пользователя не являются доверенными", "Some devices in this encrypted room are not trusted": "Некоторые устройства в этой зашифрованной комнате являются недоверенными", "All devices for this user are trusted": "Все устройства для этого пользователя являются доверенными", @@ -1721,7 +1721,7 @@ "Rotate Right": "Повернуть вправо", "Rotate clockwise": "Повернуть по часовой стрелке", "Edit message": "Редактировать сообщение", - "Power level": "Уровень мощности", + "Power level": "Уровень прав", "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Не удалось найти профили для Matrix ID, перечисленных ниже. Вы всё равно хотите их пригласить?", "Invite anyway": "Пригласить в любом случае", "GitHub issue": "GitHub вопрос", @@ -2043,5 +2043,64 @@ "Read Marker off-screen lifetime (ms)": "Читать маркер вне экрана время жизни (мс)", "A device's public name is visible to people you communicate with": "Публичное имя устройства видно людям, с которыми вы общаетесь", "Upgrade the room": "Обновить эту комнату", - "Enable room encryption": "Включить шифрование комнаты" + "Enable room encryption": "Включить шифрование комнаты", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Вы должны удалить свои личные данные с сервера идентификации перед отключением. К сожалению, идентификационный сервер в данный момент отключен или недоступен.", + "You should:": "Вам следует:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "проверяйте плагины браузера на наличие всего, что может заблокировать сервер идентификации (например, Privacy Badger)", + "contact the administrators of identity server ": "связаться с администраторами сервера идентификации ", + "wait and try again later": "Подождите и повторите попытку позже", + "Error changing power level requirement": "Ошибка изменения требования к уровню прав", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Произошла ошибка при изменении требований к уровню прав комнаты. Убедитесь, что у вас достаточно прав и попробуйте снова.", + "Error changing power level": "Ошибка изменения уровня прав", + "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Произошла ошибка при изменении уровня прав пользователя. Убедитесь, что у вас достаточно прав и попробуйте снова.", + "Unable to revoke sharing for email address": "Не удается отменить общий доступ к адресу электронной почты", + "Unable to share email address": "Невозможно поделиться адресом электронной почты", + "Your email address hasn't been verified yet": "Ваш адрес электронной почты еще не проверен", + "Click the link in the email you received to verify and then click continue again.": "Нажмите на ссылку в электронном письме, которое вы получили, чтобы подтвердить, и затем нажмите продолжить снова.", + "Verify the link in your inbox": "Проверьте ссылку в вашем почтовом ящике(папка \"Входящие\")", + "Complete": "Выполнено", + "Revoke": "Отмена", + "Share": "Делиться", + "Discovery options will appear once you have added an email above.": "Параметры обнаружения появятся после добавления электронной почты выше.", + "Unable to revoke sharing for phone number": "Не удалось отменить общий доступ к номеру телефона", + "Unable to share phone number": "Не удается предоставить общий доступ к номеру телефона", + "Please enter verification code sent via text.": "Пожалуйста, введите проверочный код, высланный с помощью текста.", + "Discovery options will appear once you have added a phone number above.": "Параметры обнаружения появятся после добавления вышеуказанного номера телефона.", + "Remove %(email)s?": "Удалить %(email)s?", + "Remove %(phone)s?": "Удалить %(phone)s?", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "Текстовое сообщение было отправлено +%(msisdn)s. Пожалуйста, введите проверочный код, который он содержит.", + "No recent messages by %(user)s found": "Последние сообщения от %(user)s не найдены", + "Try scrolling up in the timeline to see if there are any earlier ones.": "Попробуйте прокрутить вверх по временной шкале, чтобы увидеть, есть ли более ранние.", + "Remove recent messages by %(user)s": "Удалить последние сообщения от %(user)s", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "Вы собираетесь удалить %(count)s сообщений от %(user)s. Это безповоротно. Вы хотите продолжить?", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "Вы собираетесь удалить 1 сообщение от %(user)s. Это безповоротно. Вы хотите продолжить?", + "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Для большого количества сообщений это может занять некоторое время. Пожалуйста, не обновляйте своего клиента в это время.", + "Remove %(count)s messages|other": "Удалить %(count)s сообщений", + "Remove %(count)s messages|one": "Удалить 1 сообщение", + "Deactivate user?": "Деактивировать пользователя?", + "Deactivate user": "Пользователь деактивирован", + "Remove recent messages": "Удалить последние сообщения", + "Bold": "Жирный", + "Italics": "Курсив", + "Strikethrough": "Перечёркнутый", + "Code block": "Блок кода", + "%(count)s unread messages.|other": "%(count)s непрочитанные сообщения.", + "Unread mentions.": "Непрочитанные упоминания.", + "Show image": "Показать изображение", + "e.g. my-room": "например, моя-комната", + "Please provide a room alias": "Пожалуйста, укажите псевдоним комнаты", + "This alias is available to use": "Этот псевдоним доступен для использования", + "This alias is already in use": "Этот псевдоним уже используется", + "Close dialog": "Закрыть диалог", + "Please enter a name for the room": "Пожалуйста, введите название комнаты", + "This room is private, and can only be joined by invitation.": "Эта комната приватная и может быть присоединена только по приглашению.", + "Hide advanced": "Скрыть расширения", + "Show advanced": "Показать расширения", + "Please fill why you're reporting.": "Пожалуйста, заполните, почему вы сообщаете.", + "Report Content to Your Homeserver Administrator": "Сообщите о содержании своему администратору домашнего сервера", + "Send report": "Отослать отчёт", + "Command Help": "Помощь команды", + "To continue you need to accept the terms of this service.": "Для продолжения Вам необходимо принять условия данного сервиса.", + "Document": "Документ", + "Report Content": "Содержание отчета" } From 2a3655e9b832ba632049d57a39e5b6dff8901c6e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Oct 2019 11:10:25 +0100 Subject: [PATCH 0348/2372] Focus highlight room sublist label, catch right arrow and simplify code Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/structures/_RoomSubList.scss | 7 +++- src/components/structures/RoomSubList.js | 48 ++++++------------------ 2 files changed, 17 insertions(+), 38 deletions(-) diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index 0e0d5c68af..39ccf9bef2 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -46,10 +46,15 @@ limitations under the License. flex-direction: row; align-items: center; flex: 0 0 auto; - margin: 0 16px; + margin: 0 8px; + padding: 0 8px; height: 36px; } +.mx_RoomSubList_labelContainer:focus-within { + background-color: $roomtile-focused-bg-color; +} + .mx_RoomSubList_label { flex: 1; cursor: pointer; diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 4ff273303a..8054ef01be 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -147,37 +147,7 @@ const RoomSubList = createReactClass({ this.onClick(); } else if (!this.props.forceExpand) { // sublist is expanded, go to first room - let element = document.activeElement; - let descending = true; - let classes; - - do { - const child = element.firstElementChild; - const sibling = element.nextElementSibling; - - if (descending) { - if (child) { - element = child; - } else if (sibling) { - element = sibling; - } else { - descending = false; - element = element.parentElement; - } - } else { - if (sibling) { - element = sibling; - descending = true; - } else { - element = element.parentElement; - } - } - - if (element) { - classes = element.classList; - } - } while (element && !classes.contains("mx_RoomTile")); - + const element = this.refs.subList && this.refs.subList.querySelector(".mx_RoomTile"); if (element) { element.focus(); } @@ -188,10 +158,15 @@ const RoomSubList = createReactClass({ }, onKeyDown: function(ev) { - // On ARROW_LEFT go to the sublist header - if (ev.key === Key.ARROW_LEFT) { - ev.stopPropagation(); - this._headerButton.current.focus(); + switch (ev.key) { + // On ARROW_LEFT go to the sublist header + case Key.ARROW_LEFT: + ev.stopPropagation(); + this._headerButton.current.focus(); + break; + // Consume ARROW_RIGHT so it doesn't cause focus to get sent to composer + case Key.ARROW_RIGHT: + ev.stopPropagation(); } }, @@ -348,8 +323,7 @@ const RoomSubList = createReactClass({ tabIndex={0} aria-expanded={!isCollapsed} inputRef={this._headerButton} - // cancel out role so this button behaves as the toggle-header of this group - role="none" + role="treeitem" > { chevron } {this.props.label} From ebc562878283389117cc082a21926ac243ff5659 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Oct 2019 11:30:47 +0100 Subject: [PATCH 0349/2372] fix typo Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/structures/_RoomSubList.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index 39ccf9bef2..b39504593a 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -20,7 +20,7 @@ limitations under the License. so they ideally wouldn't affect each other. lowest category: .mx_RoomSubList flex-shrink: 10000000 - distribute size of items within the same categery by their size + distribute size of items within the same category by their size middle category: .mx_RoomSubList.resized-sized flex-shrink: 1000 applied when using the resizer, will have a max-height set to it, From d01258b45857776cbd9540b24503d4b9e913700c Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Tue, 22 Oct 2019 03:35:47 +0000 Subject: [PATCH 0350/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1856 of 1856 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 5d128e71e8..31e6c39902 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2243,5 +2243,23 @@ "contact the administrators of identity server ": "聯絡身份識別伺服器 的管理員", "wait and try again later": "稍候並再試一次", "Command Autocomplete": "指令自動完成", - "DuckDuckGo Results": "DuckDuckGo 結果" + "DuckDuckGo Results": "DuckDuckGo 結果", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "為聊天室成員與群組成員使用新的、較具一致性的使用者資訊面板", + "Trust & Devices": "信任與裝置", + "Direct messages": "直接訊息", + "Failed to deactivate user": "停用使用者失敗", + "This client does not support end-to-end encryption.": "此客戶端不支援端到端加密。", + "Messages in this room are not end-to-end encrypted.": "此聊天室中的訊息沒有端到端加密。", + "Quick Reactions": "快速反應", + "Frequently Used": "經常使用", + "Smileys & People": "笑臉與人", + "Animals & Nature": "動物與自然", + "Food & Drink": "飲食", + "Activities": "活動", + "Travel & Places": "旅遊與地點", + "Objects": "物件", + "Symbols": "符號", + "Flags": "旗幟", + "React": "反應", + "Cancel search": "取消搜尋" } From edda60db341085389c156b7ec464f21ea3d40ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Tue, 22 Oct 2019 06:55:26 +0000 Subject: [PATCH 0351/2372] Translated using Weblate (French) Currently translated at 100.0% (1856 of 1856 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 3a962cc1c9..7841334332 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2262,5 +2262,11 @@ "Symbols": "Symboles", "Flags": "Drapeaux", "React": "Réagir", - "Cancel search": "Annuler la recherche" + "Cancel search": "Annuler la recherche", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Utilisez le nouveau panneau d’informations utilisateur uniforme, pour les membres des salons et des groupes", + "Trust & Devices": "Confiance & appareils", + "Direct messages": "Messages directs", + "Failed to deactivate user": "Échec de la désactivation de l’utilisateur", + "This client does not support end-to-end encryption.": "Ce client ne prend pas en charge le chiffrement de bout en bout.", + "Messages in this room are not end-to-end encrypted.": "Les messages dans ce salon ne sont pas chiffrés de bout en bout." } From d713ff82b909d81e0f70eae8de7e25feac805c33 Mon Sep 17 00:00:00 2001 From: Michael Albert Date: Mon, 21 Oct 2019 17:40:55 +0000 Subject: [PATCH 0352/2372] Translated using Weblate (German) Currently translated at 83.5% (1549 of 1856 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 85b9693ee9..67e854d518 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1333,7 +1333,7 @@ "Copy to clipboard": "In Zwischenablage kopieren", "Download": "Herunterladen", "I've made a copy": "Ich habe eine Kopie gemacht", - "Print it and store it somewhere safe": "Drucke ihn aus und lagere ihn, wo er sicher ist", + "Print it and store it somewhere safe": "Drucke ihn aus und lagere ihn an einem sicheren Ort", "Save it on a USB key or backup drive": "Speichere ihn auf einem USB-Schlüssel oder Sicherungsslaufwerk", "Copy it to your personal cloud storage": "Kopiere ihn in deinen persönlichen Cloud-Speicher", "Got it": "Verstanden", @@ -1921,5 +1921,12 @@ "Add Email Address": "E-Mail-Adresse hinzufügen", "Add Phone Number": "Telefonnummer hinzufügen", "Changes the avatar of the current room": "Ändert den Avatar für diesen Raum", - "Deactivate account": "Benutzerkonto schließen" + "Deactivate account": "Benutzerkonto schließen", + "Show previews/thumbnails for images": "Zeige eine Vorschau für Bilder", + "A device's public name is visible to people you communicate with": "Der Gerätename ist sichtbar für die Personen mit denen du kommunizierst", + "Clear all data on this device?": "Alle Daten auf diesem Gerät löschen?", + "View": "Vorschau", + "Find a room…": "Suche einen Raum…", + "Find a room… (e.g. %(exampleRoom)s)": "Suche einen Raum… (z.B. %(exampleRoom)s)", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Wenn du den gesuchten Raum nicht finden kannst, frage nach einer Einladung für den Raum oder Erstelle einen neuen Raum." } From 6e2fe2a3f9237e94901c776f197617e3be475cd5 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Mon, 21 Oct 2019 19:26:54 +0000 Subject: [PATCH 0353/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1856 of 1856 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 8251f729f3..d68eebf7a4 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2230,5 +2230,30 @@ "Click the link in the email you received to verify and then click continue again.": "Ellenőrzéshez kattints a linkre az e-mailben amit kaptál és itt kattints a folytatásra újra.", "Add Email Address": "E-mail cím hozzáadása", "Add Phone Number": "Telefonszám hozzáadása", - "%(creator)s created and configured the room.": "%(creator)s elkészítette és beállította a szobát." + "%(creator)s created and configured the room.": "%(creator)s elkészítette és beállította a szobát.", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Használd az új és konzisztens FelhasználóInfó panelt szoba-, és csoport tagsághoz", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Először töröld a személyes adatokat az azonosítási szerverről () mielőtt lecsatlakozol. Sajnos az azonosítási szerver () jelenleg elérhetetlen.", + "You should:": "Ezt kellene tedd:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "ellenőrizd a böngésződ kiegészítéseit, hogy nem blokkolja-e valami az azonosítási szervert (mint például Privacy Badger)", + "contact the administrators of identity server ": "vedd fel a kapcsolatot az azonosítási szerver () adminisztrátorával", + "wait and try again later": "várj és próbáld újra", + "Trust & Devices": "Megbízhatóság és Eszközök", + "Direct messages": "Közvetlen üzenetek", + "Failed to deactivate user": "A felhasználó felfüggesztése nem sikerült", + "This client does not support end-to-end encryption.": "A kliens nem támogatja a végponttól végpontig való titkosítást.", + "Messages in this room are not end-to-end encrypted.": "Az üzenetek a szobában nincsenek végponttól végpontig titkosítva.", + "Command Autocomplete": "Parancs Automatikus kiegészítés", + "DuckDuckGo Results": "DuckDuckGo találatok", + "Quick Reactions": "Gyors Reakció", + "Frequently Used": "Gyakran Használt", + "Smileys & People": "Smiley-k és emberek", + "Animals & Nature": "Állatok és természet", + "Food & Drink": "Étel és ital", + "Activities": "Mozgás", + "Travel & Places": "Utazás és helyek", + "Objects": "Tárgyak", + "Symbols": "Szimbólumok", + "Flags": "Zászlók", + "React": "Reakció", + "Cancel search": "Keresés megszakítása" } From 420d20a0ae7d12d213382ac179be0371bf551a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Tue, 22 Oct 2019 02:16:30 +0000 Subject: [PATCH 0354/2372] Translated using Weblate (Korean) Currently translated at 100.0% (1856 of 1856 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index f7c0b0680d..8a310849bb 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2079,7 +2079,7 @@ "%(count)s unread messages.|other": "%(count)s개의 읽지 않은 메시지.", "Unread mentions.": "읽지 않은 언급.", "Please create a new issue on GitHub so that we can investigate this bug.": "이 버그를 조사할 수 있도록 GitHub에 새 이슈를 추가해주세요.", - "Use the new, faster, composer for writing messages": "메시지 입력으로 새로운, 더 빠른 작성기를 사용", + "Use the new, faster, composer for writing messages": "메시지 입력으로 새롭고 더 빠른 작성기를 사용하기", "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "%(user)s님의 1개의 메시지를 삭제합니다. 이것은 되돌릴 수 없습니다. 계속하겠습니까?", "Remove %(count)s messages|one": "1개의 메시지 삭제", "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "홈서버 설정에서 캡챠 공개 키가 없습니다. 홈서버 관리자에게 이것을 신고해주세요.", @@ -2106,5 +2106,11 @@ "Symbols": "기호", "Flags": "깃발", "React": "리액션", - "Cancel search": "검색 취소" + "Cancel search": "검색 취소", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "방 구성원과 그룹 구성원을 위한 새롭고 일관적인 사용자 정보 사용하기", + "Trust & Devices": "신뢰 & 기기", + "Direct messages": "다이렉트 메시지", + "Failed to deactivate user": "사용자 비활성화에 실패함", + "This client does not support end-to-end encryption.": "이 클라이언트는 종단간 암호화를 지원하지 않습니다.", + "Messages in this room are not end-to-end encrypted.": "이 방의 메시지는 종단간 암호화가 되지 않았습니다." } From 79b6117c4d5102357ff1ca98a96c93bed2f6f901 Mon Sep 17 00:00:00 2001 From: Walter Date: Tue, 22 Oct 2019 07:19:56 +0000 Subject: [PATCH 0355/2372] Translated using Weblate (Russian) Currently translated at 99.8% (1853 of 1856 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 55 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index b14a4864c6..665b96c199 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -2102,5 +2102,58 @@ "Command Help": "Помощь команды", "To continue you need to accept the terms of this service.": "Для продолжения Вам необходимо принять условия данного сервиса.", "Document": "Документ", - "Report Content": "Содержание отчета" + "Report Content": "Содержание отчета", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Используйте новую панель UserInfo для участников комнат и групп", + "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Деактивация этого пользователя приведет к его выходу из системы и запрету повторного входа. Кроме того, они оставит все комнаты, в которых он участник. Это действие безповоротно. Вы уверены, что хотите деактивировать этого пользователя?", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "При попытке подтвердить приглашение была возвращена ошибка (%(errcode)s). Вы можете попробовать передать эту информацию администратору комнаты.", + "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "Это приглашение в %(roomName)s было отправлено на %(email)s, которое не связано с вашей учетной записью", + "Link this email with your account in Settings to receive invites directly in Riot.": "Свяжите это письмо с вашей учетной записью в Настройках, чтобы получать приглашения непосредственно в Riot.", + "This invite to %(roomName)s was sent to %(email)s": "Это приглашение в %(roomName)s было отправлено на %(email)s", + "Use an identity server in Settings to receive invites directly in Riot.": "Используйте сервер идентификации в Настройках для получения приглашений непосредственно в Riot.", + "Share this email in Settings to receive invites directly in Riot.": "Введите адрес эл.почты в Настройках, чтобы получать приглашения прямо в Riot.", + "%(count)s unread messages including mentions.|other": "%(count)s непрочитанные сообщения, включая упоминания.", + "Trust & Devices": "Доверия & Устройства", + "Direct messages": "Прямые сообщения", + "Failed to deactivate user": "Не удалось деактивировать пользователя", + "This client does not support end-to-end encryption.": "Этот клиент не поддерживает сквозное шифрование.", + "Messages in this room are not end-to-end encrypted.": "Сообщения в этой комнате не шифруются сквозным шифрованием.", + "Please create a new issue on GitHub so that we can investigate this bug.": "Пожалуйста, создайте новую проблему/вопрос на GitHub, чтобы мы могли расследовать эту ошибку.", + "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "Используйте идентификационный сервер для приглашения по электронной почте. Используйте значение по умолчанию (%(defaultIdentityServerName)s) или управляйте в Настройках.", + "Use an identity server to invite by email. Manage in Settings.": "Используйте идентификационный сервер для приглашения по электронной почте. Управление в Настройки.", + "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Запретить пользователям других Matrix-Серверов присоединяться к этой комнате (этот параметр нельзя изменить позже!)", + "To verify that this device can be trusted, please check that the key you see in User Settings on that device matches the key below:": "Чтобы убедиться, что этому устройству можно доверять, проверьте ключ, который вы видите в настройках пользователя на этом устройстве, соответствует указанному ниже ключу:", + "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Отчет о данном сообщении отправит свой уникальный 'event ID' администратору вашего домашнего сервера. Если сообщения в этой комнате зашифрованы, администратор вашего домашнего сервера не сможет прочитать текст сообщения или просмотреть какие-либо файлы или изображения.", + "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Отсутствует Капча открытого ключа в конфигурации домашнего сервера. Пожалуйста, сообщите об этом администратору вашего домашнего сервера.", + "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "Сервер идентификации не настроен, поэтому вы не можете добавить адрес электронной почты, чтобы в будущем сбросить пароль.", + "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Установка адреса электронной почты для восстановления учетной записи. Используйте электронную почту или телефон, чтобы опционально быть обнаруженными существующими контактами.", + "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Установите адрес электронной почты для восстановления аккаунта. Используйте электронную почту, чтобы опционально быть обнаруженными существующими контактами.", + "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "Сервер идентификации не настроен: адреса электронной почты не могут быть добавлены. Вы не сможете сбросить свой пароль.", + "Enter your custom homeserver URL What does this mean?": "Введите URL-адрес вашего собственного домашнего сервера Что это значит? ", + "Enter your custom identity server URL What does this mean?": "Введите URL-адрес вашего собственного сервера идентификации Что это значит?", + "%(creator)s created and configured the room.": "%(creator)s создал и настроил комнату.", + "Preview": "Предпросмотр", + "View": "Просмотр", + "Find a room…": "Найди комнату…", + "Find a room… (e.g. %(exampleRoom)s)": "Найди комнату... (напр. %(exampleRoom)s)", + "Explore rooms": "Исследуйте комнаты", + "No identity server is configured: add one in server settings to reset your password.": "Идентификационный сервер не настроен: добавьте его в настройки сервера, чтобы сбросить пароль.", + "Command Autocomplete": "Автозаполнение команды", + "Community Autocomplete": "Автозаполнение сообщества", + "DuckDuckGo Results": "DuckDuckGo результаты", + "Emoji Autocomplete": "Emoji Автозаполнение", + "Notification Autocomplete": "Автозаполнение уведомлений", + "Room Autocomplete": "Автозаполнение комнаты", + "User Autocomplete": "Автозаполнение пользователя", + "Quick Reactions": "Быстрая реакция", + "Frequently Used": "Часто используемый", + "Smileys & People": "Смайлики & Люди", + "Animals & Nature": "Животные & Природа", + "Food & Drink": "Еда & Напитки", + "Activities": "Действия", + "Travel & Places": "Путешествия & Места", + "Objects": "Объекты", + "Symbols": "Символы", + "Flags": "Флаги", + "React": "Реакция", + "Cancel search": "Отмена поиска" } From 93ecc9839bfe3a157e2760b2e1106b06a76bf146 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Oct 2019 13:49:02 +0100 Subject: [PATCH 0356/2372] Fix linty failures --- .../views/elements/EventListSummary.js | 2 +- .../views/emojipicker/EmojiPicker.js | 4 +-- src/components/views/emojipicker/Preview.js | 4 +-- .../views/emojipicker/QuickReactions.js | 4 +-- .../views/emojipicker/ReactionPicker.js | 13 +++++--- .../views/messages/MessageActionBar.js | 1 - src/editor/deserialize.js | 2 +- src/i18n/strings/en_EN.json | 32 ++++++++----------- 8 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/components/views/elements/EventListSummary.js b/src/components/views/elements/EventListSummary.js index d6971334d4..79712ebb45 100644 --- a/src/components/views/elements/EventListSummary.js +++ b/src/components/views/elements/EventListSummary.js @@ -29,7 +29,7 @@ const EventListSummary = ({events, children, threshold=3, onToggle, startExpande if (onToggle) { onToggle(); } - }, [expanded]); + }, [expanded]); // eslint-disable-line react-hooks/exhaustive-deps const eventIds = events.map((e) => e.getId()).join(','); diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.js index 6bf79d2623..6d34804187 100644 --- a/src/components/views/emojipicker/EmojiPicker.js +++ b/src/components/views/emojipicker/EmojiPicker.js @@ -218,8 +218,8 @@ class EmojiPicker extends React.Component { const QuickReactions = sdk.getComponent("emojipicker.QuickReactions"); return (
    -
    - +
    +
    {this.categories.map(category => ( @@ -42,7 +42,7 @@ class Preview extends React.PureComponent {
    - ) + ); } } diff --git a/src/components/views/emojipicker/QuickReactions.js b/src/components/views/emojipicker/QuickReactions.js index 820865dc88..66248730f9 100644 --- a/src/components/views/emojipicker/QuickReactions.js +++ b/src/components/views/emojipicker/QuickReactions.js @@ -74,10 +74,10 @@ class QuickReactions extends React.Component { {QUICK_REACTIONS.map(emoji => )} + selectedEmojis={this.props.selectedEmojis} />)} - ) + ); } } diff --git a/src/components/views/emojipicker/ReactionPicker.js b/src/components/views/emojipicker/ReactionPicker.js index d027ae6fd3..01c04529ed 100644 --- a/src/components/views/emojipicker/ReactionPicker.js +++ b/src/components/views/emojipicker/ReactionPicker.js @@ -79,11 +79,11 @@ class ReactionPicker extends React.Component { return Object.fromEntries([...myAnnotations] .filter(event => !event.isRedacted()) .map(event => [event.getRelation().key, event.getId()])); - }; + } onReactionsChange() { this.setState({ - selectedEmojis: new Set(Object.keys(this.getReactions())) + selectedEmojis: new Set(Object.keys(this.getReactions())), }); } @@ -112,9 +112,12 @@ class ReactionPicker extends React.Component { } render() { - return + return ; } } -export default ReactionPicker +export default ReactionPicker; diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index df1bc9a294..565c66410e 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -25,7 +25,6 @@ import Modal from '../../../Modal'; import { createMenu } from '../../structures/ContextualMenu'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import {RoomContext} from "../../structures/RoomView"; -import MatrixClientPeg from '../../../MatrixClientPeg'; export default class MessageActionBar extends React.PureComponent { static propTypes = { diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index 6636c9971e..1fdbf9490c 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -109,7 +109,7 @@ function parseElement(n, partCreator, lastNode, state) { const indent = " ".repeat(state.listDepth - 1); if (n.parentElement.nodeName === "OL") { // The markdown parser doesn't do nested indexed lists at all, but this supports it anyway. - let index = state.listIndex[state.listIndex.length - 1]; + const index = state.listIndex[state.listIndex.length - 1]; state.listIndex[state.listIndex.length - 1] += 1; return partCreator.plain(`${indent}${index}. `); } else { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e5c6043c7f..2683d6f10a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1045,6 +1045,7 @@ "Yesterday": "Yesterday", "View Source": "View Source", "Error decrypting audio": "Error decrypting audio", + "React": "React", "Reply": "Reply", "Edit": "Edit", "Options": "Options", @@ -1056,12 +1057,6 @@ "Error decrypting image": "Error decrypting image", "Show image": "Show image", "Error decrypting video": "Error decrypting video", - "Agree": "Agree", - "Disagree": "Disagree", - "Happy": "Happy", - "Party Popper": "Party Popper", - "Confused": "Confused", - "Eyes": "Eyes", "Show all": "Show all", "reacted with %(shortName)s": "reacted with %(shortName)s", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s", @@ -1113,6 +1108,17 @@ "Checking for an update...": "Checking for an update...", "No update available.": "No update available.", "Downloading update...": "Downloading update...", + "Frequently Used": "Frequently Used", + "Smileys & People": "Smileys & People", + "Animals & Nature": "Animals & Nature", + "Food & Drink": "Food & Drink", + "Activities": "Activities", + "Travel & Places": "Travel & Places", + "Objects": "Objects", + "Symbols": "Symbols", + "Flags": "Flags", + "Quick Reactions": "Quick Reactions", + "Cancel search": "Cancel search", "Unknown Address": "Unknown Address", "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "Warning: This widget might use cookies.": "Warning: This widget might use cookies.", @@ -1839,17 +1845,5 @@ "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", - "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", - "Quick Reactions": "Quick Reactions", - "Frequently Used": "Frequently Used", - "Smileys & People": "Smileys & People", - "Animals & Nature": "Animals & Nature", - "Food & Drink": "Food & Drink", - "Activities": "Activities", - "Travel & Places": "Travel & Places", - "Objects": "Objects", - "Symbols": "Symbols", - "Flags": "Flags", - "React": "React", - "Cancel search": "Cancel search" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" } From 616ff5ce761b448e6288f515c4fe44306efc6de4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 22 Oct 2019 15:43:40 +0200 Subject: [PATCH 0357/2372] adjust list item numbers in test that are now preserved --- test/editor/deserialize-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/editor/deserialize-test.js b/test/editor/deserialize-test.js index bf670b0fd8..ae25e45126 100644 --- a/test/editor/deserialize-test.js +++ b/test/editor/deserialize-test.js @@ -208,9 +208,9 @@ describe('editor/deserialize', function() { expect(parts.length).toBe(5); expect(parts[0]).toStrictEqual({type: "plain", text: "1. Start"}); expect(parts[1]).toStrictEqual({type: "newline", text: "\n"}); - expect(parts[2]).toStrictEqual({type: "plain", text: "1. Continue"}); + expect(parts[2]).toStrictEqual({type: "plain", text: "2. Continue"}); expect(parts[3]).toStrictEqual({type: "newline", text: "\n"}); - expect(parts[4]).toStrictEqual({type: "plain", text: "1. Finish"}); + expect(parts[4]).toStrictEqual({type: "plain", text: "3. Finish"}); }); it('mx-reply is stripped', function() { const html = "foobar"; From 190fccea151e0af64fe019208e950e89127a4527 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Oct 2019 16:35:36 +0100 Subject: [PATCH 0358/2372] delint scss --- res/css/views/emojipicker/_EmojiPicker.scss | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index 6dcc4d75b9..8f57d97833 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -70,16 +70,16 @@ limitations under the License. background-color: $focus-bg-color; } -.mx_EmojiPicker_anchor_activity::before { mask-image: url('$(res)/img/emojipicker/activity.svg') } -.mx_EmojiPicker_anchor_custom::before { mask-image: url('$(res)/img/emojipicker/custom.svg') } -.mx_EmojiPicker_anchor_flags::before { mask-image: url('$(res)/img/emojipicker/flags.svg') } -.mx_EmojiPicker_anchor_foods::before { mask-image: url('$(res)/img/emojipicker/foods.svg') } -.mx_EmojiPicker_anchor_nature::before { mask-image: url('$(res)/img/emojipicker/nature.svg') } -.mx_EmojiPicker_anchor_objects::before { mask-image: url('$(res)/img/emojipicker/objects.svg') } -.mx_EmojiPicker_anchor_people::before { mask-image: url('$(res)/img/emojipicker/people.svg') } -.mx_EmojiPicker_anchor_places::before { mask-image: url('$(res)/img/emojipicker/places.svg') } -.mx_EmojiPicker_anchor_recent::before { mask-image: url('$(res)/img/emojipicker/recent.svg') } -.mx_EmojiPicker_anchor_symbols::before { mask-image: url('$(res)/img/emojipicker/symbols.svg') } +.mx_EmojiPicker_anchor_activity::before { mask-image: url('$(res)/img/emojipicker/activity.svg'); } +.mx_EmojiPicker_anchor_custom::before { mask-image: url('$(res)/img/emojipicker/custom.svg'); } +.mx_EmojiPicker_anchor_flags::before { mask-image: url('$(res)/img/emojipicker/flags.svg'); } +.mx_EmojiPicker_anchor_foods::before { mask-image: url('$(res)/img/emojipicker/foods.svg'); } +.mx_EmojiPicker_anchor_nature::before { mask-image: url('$(res)/img/emojipicker/nature.svg'); } +.mx_EmojiPicker_anchor_objects::before { mask-image: url('$(res)/img/emojipicker/objects.svg'); } +.mx_EmojiPicker_anchor_people::before { mask-image: url('$(res)/img/emojipicker/people.svg'); } +.mx_EmojiPicker_anchor_places::before { mask-image: url('$(res)/img/emojipicker/places.svg'); } +.mx_EmojiPicker_anchor_recent::before { mask-image: url('$(res)/img/emojipicker/recent.svg'); } +.mx_EmojiPicker_anchor_symbols::before { mask-image: url('$(res)/img/emojipicker/symbols.svg'); } .mx_EmojiPicker_anchor_visible { border-bottom: 2px solid $button-bg-color; From d8252e6223aaf8f958f29fa737798e870723fd86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Tue, 22 Oct 2019 15:22:18 +0000 Subject: [PATCH 0359/2372] Translated using Weblate (Korean) Currently translated at 100.0% (1853 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 8a310849bb..6f976d8b0e 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -228,7 +228,7 @@ "Join Room": "방에 참가", "%(targetName)s joined the room.": "%(targetName)s님이 방에 참가했습니다.", "Joins room with given alias": "받은 별칭으로 방에 들어가기", - "Jump to first unread message.": "읽지 않은 첫 메시지로 이동하려면 누르세요.", + "Jump to first unread message.": "읽지 않은 첫 메시지로 건너뜁니다.", "%(senderName)s kicked %(targetName)s.": "%(senderName)s님이 %(targetName)s님을 추방했습니다.", "Kick": "추방", "Kicks user with given id": "받은 ID로 사용자 추방하기", @@ -2112,5 +2112,7 @@ "Direct messages": "다이렉트 메시지", "Failed to deactivate user": "사용자 비활성화에 실패함", "This client does not support end-to-end encryption.": "이 클라이언트는 종단간 암호화를 지원하지 않습니다.", - "Messages in this room are not end-to-end encrypted.": "이 방의 메시지는 종단간 암호화가 되지 않았습니다." + "Messages in this room are not end-to-end encrypted.": "이 방의 메시지는 종단간 암호화가 되지 않았습니다.", + "Jump to first unread room.": "읽지 않은 첫 방으로 건너뜁니다.", + "Jump to first invite.": "첫 초대로 건너뜁니다." } From 744fc5ca6a945c1cbc62f612a9f0ce741e79208a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Oct 2019 10:52:35 +0100 Subject: [PATCH 0360/2372] Specify aria-level="1" on Room List tree RoomSubList Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomSubList.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 8054ef01be..0bb5c9e9be 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -324,6 +324,7 @@ const RoomSubList = createReactClass({ aria-expanded={!isCollapsed} inputRef={this._headerButton} role="treeitem" + aria-level="1" > { chevron } {this.props.label} From 4dd0f6d902d7f1b6790730c750b011675c38f35b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Oct 2019 12:11:37 +0100 Subject: [PATCH 0361/2372] Make breadcrumbs more accessible Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomBreadcrumbs.js | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index 2cbdc31464..d1d407ea00 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -346,8 +346,15 @@ export default class RoomBreadcrumbs extends React.Component { } return ( - this._viewRoom(r.room, i)} - onMouseEnter={() => this._onMouseEnter(r.room)} onMouseLeave={() => this._onMouseLeave(r.room)}> + this._viewRoom(r.room, i)} + onMouseEnter={() => this._onMouseEnter(r.room)} + onMouseLeave={() => this._onMouseLeave(r.room)} + aria-label={_t("Room %(name)s", {name: r.room.name})} + role="listitem" + > {badge} {dmIndicator} @@ -356,10 +363,16 @@ export default class RoomBreadcrumbs extends React.Component { ); }); return ( - - { avatars } - +
    + + { avatars } + +
    ); } } From d7a64fcacd05f74bc659d3c8a06fe0dd1edc66b7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Oct 2019 12:12:11 +0100 Subject: [PATCH 0362/2372] Move Jitsi widget to bottom and fix keyboard navigation of left panel Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/structures/_LeftPanel.scss | 6 +- src/components/structures/LeftPanel.js | 77 +++++++++++--------------- 2 files changed, 36 insertions(+), 47 deletions(-) diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 85fdfa092d..e55ebb42d7 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -48,12 +48,16 @@ limitations under the License. .mx_LeftPanel { flex: 1; - overflow-x: hidden; + overflow: hidden; display: flex; flex-direction: column; min-height: 0; } +.mx_LeftPanel .mx_LeftPanel_Rooms { + flex: 1 1 0; +} + .mx_LeftPanel .mx_AppTile_mini { height: 132px; } diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index d1d3bb1b63..54fad45713 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,7 +20,7 @@ import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { MatrixClient } from 'matrix-js-sdk'; -import { KeyCode } from '../../Keyboard'; +import { Key } from '../../Keyboard'; import sdk from '../../index'; import dis from '../../dispatcher'; import VectorConferenceHandler from '../../VectorConferenceHandler'; @@ -117,35 +118,21 @@ const LeftPanel = createReactClass({ _onKeyDown: function(ev) { if (!this.focusedElement) return; - let handled = true; - switch (ev.keyCode) { - case KeyCode.TAB: - this._onMoveFocus(ev.shiftKey); + switch (ev.key) { + case Key.TAB: + this._onMoveFocus(ev, ev.shiftKey); break; - case KeyCode.UP: - this._onMoveFocus(true); + case Key.ARROW_UP: + this._onMoveFocus(ev, true, true); break; - case KeyCode.DOWN: - this._onMoveFocus(false); + case Key.ARROW_DOWN: + this._onMoveFocus(ev, false, true); break; - case KeyCode.ENTER: - this._onMoveFocus(false); - if (this.focusedElement) { - this.focusedElement.click(); - } - break; - default: - handled = false; - } - - if (handled) { - ev.stopPropagation(); - ev.preventDefault(); } }, - _onMoveFocus: function(up) { + _onMoveFocus: function(ev, up, trap) { let element = this.focusedElement; // unclear why this isn't needed @@ -179,29 +166,24 @@ const LeftPanel = createReactClass({ if (element) { classes = element.classList; - if (classes.contains("mx_LeftPanel")) { // we hit the top - element = up ? element.lastElementChild : element.firstElementChild; - descending = true; - } } } while (element && !( classes.contains("mx_RoomTile") || classes.contains("mx_RoomSubList_label") || - classes.contains("mx_textinput_search"))); + classes.contains("mx_LeftPanel_filterRooms"))); if (element) { + ev.stopPropagation(); + ev.preventDefault(); element.focus(); this.focusedElement = element; - this.focusedDescending = descending; + } else if (trap) { + // if navigation is via up/down arrow-keys, trap in the widget so it doesn't send to composer + ev.stopPropagation(); + ev.preventDefault(); } }, - onHideClick: function() { - dis.dispatch({ - action: 'hide_left_panel', - }); - }, - onSearch: function(term) { this.setState({ searchFilter: term }); }, @@ -269,6 +251,7 @@ const LeftPanel = createReactClass({ } const searchBox = ( { tagPanelContainer } -
    ); From c6023ca461f2c8f101c32002cfe51699251801b3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Oct 2019 12:29:53 +0100 Subject: [PATCH 0363/2372] Gen i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomBreadcrumbs.js | 2 +- src/i18n/strings/en_EN.json | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index d1d407ea00..8bee87728b 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -363,7 +363,7 @@ export default class RoomBreadcrumbs extends React.Component { ); }); return ( -
    +
    Date: Wed, 23 Oct 2019 13:05:02 +0100 Subject: [PATCH 0364/2372] Move Jitsi widget above Explore/Filter Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/structures/_LeftPanel.scss | 6 +----- src/components/structures/LeftPanel.js | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index e55ebb42d7..85fdfa092d 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -48,16 +48,12 @@ limitations under the License. .mx_LeftPanel { flex: 1; - overflow: hidden; + overflow-x: hidden; display: flex; flex-direction: column; min-height: 0; } -.mx_LeftPanel .mx_LeftPanel_Rooms { - flex: 1 1 0; -} - .mx_LeftPanel .mx_AppTile_mini { height: 132px; } diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 54fad45713..8a21b017fd 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -272,6 +272,7 @@ const LeftPanel = createReactClass({
    ); From a0d9abd7d77014e43dc2e785068ab36c8f4265fd Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 23 Oct 2019 07:15:51 +0000 Subject: [PATCH 0365/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1847 of 1847 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 31e6c39902..e5fc53f406 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2261,5 +2261,7 @@ "Symbols": "符號", "Flags": "旗幟", "React": "反應", - "Cancel search": "取消搜尋" + "Cancel search": "取消搜尋", + "Jump to first unread room.": "跳到第一個未讀的聊天室。", + "Jump to first invite.": "跳到第一個邀請。" } From cd09935d6d1f87a95ed879840d293ec327273677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Wed, 23 Oct 2019 06:26:15 +0000 Subject: [PATCH 0366/2372] Translated using Weblate (French) Currently translated at 100.0% (1847 of 1847 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 7841334332..15cd767904 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2268,5 +2268,7 @@ "Direct messages": "Messages directs", "Failed to deactivate user": "Échec de la désactivation de l’utilisateur", "This client does not support end-to-end encryption.": "Ce client ne prend pas en charge le chiffrement de bout en bout.", - "Messages in this room are not end-to-end encrypted.": "Les messages dans ce salon ne sont pas chiffrés de bout en bout." + "Messages in this room are not end-to-end encrypted.": "Les messages dans ce salon ne sont pas chiffrés de bout en bout.", + "Jump to first unread room.": "Sauter au premier salon non lu.", + "Jump to first invite.": "Sauter à la première invitation." } From b9ec12fdf6e3d3a81c5cfabdb2684203ec30848e Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 22 Oct 2019 16:43:25 +0000 Subject: [PATCH 0367/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1847 of 1847 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index d68eebf7a4..79c820d51c 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2255,5 +2255,7 @@ "Symbols": "Szimbólumok", "Flags": "Zászlók", "React": "Reakció", - "Cancel search": "Keresés megszakítása" + "Cancel search": "Keresés megszakítása", + "Jump to first unread room.": "Az első olvasatlan szobába ugrás.", + "Jump to first invite.": "Az első meghívóra ugrás." } From 967594950ea853416eecc1ec72276d649562577e Mon Sep 17 00:00:00 2001 From: random Date: Wed, 23 Oct 2019 12:18:13 +0000 Subject: [PATCH 0368/2372] Translated using Weblate (Italian) Currently translated at 100.0% (1847 of 1847 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index b4b60c952e..583160e28f 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2190,5 +2190,32 @@ "Click the link in the email you received to verify and then click continue again.": "Clicca il link nell'email che hai ricevuto per verificare e poi clicca di nuovo Continua.", "Read Marker lifetime (ms)": "Durata delle conferme di lettura (ms)", "Read Marker off-screen lifetime (ms)": "Durata della conferma di lettura off-screen (ms)", - "%(creator)s created and configured the room.": "%(creator)s ha creato e configurato la stanza." + "%(creator)s created and configured the room.": "%(creator)s ha creato e configurato la stanza.", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Usa il nuovo pannello InfoUtente per i membri della stanza e del gruppo", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Dovresti rimuovere i tuoi dati personali dal server di identità prima di disconnetterti. Sfortunatamente, il server di identità attualmente è offline o non raggiungibile.", + "You should:": "Dovresti:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "cercare tra i plugin del browser se qualcosa potrebbe bloccare il server di identità (come Privacy Badger)", + "contact the administrators of identity server ": "contattare l'amministratore del server di identità ", + "wait and try again later": "attendere e riprovare più tardi", + "Trust & Devices": "Fiducia e Dispositivi", + "Direct messages": "Messaggi diretti", + "Failed to deactivate user": "Disattivazione utente fallita", + "This client does not support end-to-end encryption.": "Questo client non supporta la cifratura end-to-end.", + "Messages in this room are not end-to-end encrypted.": "I messaggi in questa stanza non sono cifrati end-to-end.", + "React": "Reagisci", + "Frequently Used": "Usati di frequente", + "Smileys & People": "Faccine e Persone", + "Animals & Nature": "Animali e Natura", + "Food & Drink": "Cibo e Bevande", + "Activities": "Attività", + "Travel & Places": "Viaggi e Luoghi", + "Objects": "Oggetti", + "Symbols": "Simboli", + "Flags": "Bandiere", + "Quick Reactions": "Reazioni rapide", + "Cancel search": "Annulla ricerca", + "Jump to first unread room.": "Salta alla prima stanza non letta.", + "Jump to first invite.": "Salta al primo invito.", + "Command Autocomplete": "Autocompletamento comando", + "DuckDuckGo Results": "Risultati DuckDuckGo" } From cd05038d84295005b7c6012d275e6723a90fd795 Mon Sep 17 00:00:00 2001 From: Aleksei Perepelkin Date: Wed, 23 Oct 2019 02:29:48 +0000 Subject: [PATCH 0369/2372] Translated using Weblate (Russian) Currently translated at 99.7% (1842 of 1847 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 665b96c199..d2bec741ec 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -2008,7 +2008,7 @@ "Add Phone Number": "Добавить номер телефона", "Changes the avatar of the current room": "Меняет аватарку текущей комнаты", "Change identity server": "Изменить сервер идентификации", - "Explore": "Исследовать", + "Explore": "Обзор", "Filter": "Поиск", "Filter rooms…": "Поиск комнат…", "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Если не удаётся найти комнату, то можно запросить приглашение или Создать новую комнату.", From 8d9dc195d58c06bd477489006330330f3c39dd5c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 21 Oct 2019 16:51:40 +0100 Subject: [PATCH 0370/2372] Make ARIA happier with DateSeparator and tidy ELS Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/EventListSummary.js | 10 +++++----- src/components/views/messages/DateSeparator.js | 7 ++++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/EventListSummary.js b/src/components/views/elements/EventListSummary.js index 79712ebb45..7a69398071 100644 --- a/src/components/views/elements/EventListSummary.js +++ b/src/components/views/elements/EventListSummary.js @@ -62,12 +62,12 @@ const EventListSummary = ({events, children, threshold=3, onToggle, startExpande
    - - { avatars } - + + { avatars } + - { summaryText } - + { summaryText } +
    diff --git a/src/components/views/messages/DateSeparator.js b/src/components/views/messages/DateSeparator.js index 9cea4c5cd6..900fd61914 100644 --- a/src/components/views/messages/DateSeparator.js +++ b/src/components/views/messages/DateSeparator.js @@ -56,6 +56,11 @@ export default class DateSeparator extends React.Component { } render() { - return


    { this.getLabel() }

    ; + // ARIA treats
    s as separators, here we abuse them slightly so manually treat this entire thing as one + return

    +
    +
    { this.getLabel() }
    +
    +

    ; } } From bc639312ecd41351520a2d872b68808027492843 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Oct 2019 18:30:29 +0100 Subject: [PATCH 0371/2372] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/EventTile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 5152ffde3e..6af2860399 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -705,7 +705,7 @@ module.exports = createReactClass({ { timestamp }
    -
    +
    -
    +
    Date: Wed, 23 Oct 2019 18:31:00 +0100 Subject: [PATCH 0372/2372] Remove wrapping div around RoomList to fix regression with scrollbars Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/LeftPanel.js | 38 ++++++++++++++++++-------- src/components/structures/SearchBox.js | 2 ++ src/components/views/rooms/RoomList.js | 12 ++++++-- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 8a21b017fd..f5687f0595 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -116,6 +116,20 @@ const LeftPanel = createReactClass({ this.focusedElement = null; }, + _onFilterKeyDown: function(ev) { + if (!this.focusedElement) return; + + switch (ev.key) { + case Key.ENTER: { + const firstRoom = ev.target.closest(".mx_LeftPanel").querySelector(".mx_RoomTile"); + if (firstRoom) { + firstRoom.click(); + } + break; + } + } + }, + _onKeyDown: function(ev) { if (!this.focusedElement) return; @@ -255,6 +269,7 @@ const LeftPanel = createReactClass({ enableRoomSearchFocus={true} blurredPlaceholder={ _t('Filter') } placeholder={ _t('Filter rooms…') } + onKeyDown={this._onFilterKeyDown} onSearch={ this.onSearch } onCleared={ this.onSearchCleared } onFocus={this._onSearchFocus} @@ -273,18 +288,19 @@ const LeftPanel = createReactClass({ { breadcrumbs } -
    -
    - { exploreButton } - { searchBox } -
    - +
    + { exploreButton } + { searchBox }
    +
    ); diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index de9a86c3a6..21613733db 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -30,6 +30,7 @@ module.exports = createReactClass({ propTypes: { onSearch: PropTypes.func, onCleared: PropTypes.func, + onKeyDown: PropTypes.func, className: PropTypes.string, placeholder: PropTypes.string.isRequired, @@ -93,6 +94,7 @@ module.exports = createReactClass({ this._clearSearch("keyboard"); break; } + if (this.props.onKeyDown) this.props.onKeyDown(ev); }, _onFocus: function(ev) { diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 036f50d899..80a03e7a73 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -770,9 +770,17 @@ module.exports = createReactClass({ const subListComponents = this._mapSubListProps(subLists); + const {resizeNotifier, collapsed, searchFilter, ConferenceHandler, ...props} = this.props; // eslint-disable-line return ( -
    +
    { subListComponents }
    ); From 3ee43dc2eb4dc365f1d33d1f996812659027af35 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Oct 2019 18:38:43 +0100 Subject: [PATCH 0373/2372] correct EmojiPicker.ReactionPicker reactions PropType validity Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/emojipicker/ReactionPicker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/emojipicker/ReactionPicker.js b/src/components/views/emojipicker/ReactionPicker.js index 01c04529ed..5e506f39d1 100644 --- a/src/components/views/emojipicker/ReactionPicker.js +++ b/src/components/views/emojipicker/ReactionPicker.js @@ -24,7 +24,7 @@ class ReactionPicker extends React.Component { mxEvent: PropTypes.object.isRequired, onFinished: PropTypes.func.isRequired, closeMenu: PropTypes.func.isRequired, - reactions: PropTypes.object.isRequired, + reactions: PropTypes.object, }; constructor(props) { From 2e6899be933f89fec9965b80341a55d9f4f68fa2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Oct 2019 18:39:39 +0100 Subject: [PATCH 0374/2372] Improve Accessibility of the new Emoji picker Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/emojipicker/_EmojiPicker.scss | 9 +++++++++ src/components/views/emojipicker/Search.js | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index 8f57d97833..3bf6845c85 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -114,6 +114,15 @@ limitations under the License. cursor: pointer; } +.mx_EmojiPicker_search_icon { + width: 16px; + margin: 8px; +} + +.mx_EmojiPicker_search_icon:not(.mx_EmojiPicker_search_clear) { + pointer-events: none; +} + .mx_EmojiPicker_search_icon::after { mask: url('$(res)/img/emojipicker/search.svg') no-repeat; mask-size: 100%; diff --git a/src/components/views/emojipicker/Search.js b/src/components/views/emojipicker/Search.js index 8646559fed..3432dadea8 100644 --- a/src/components/views/emojipicker/Search.js +++ b/src/components/views/emojipicker/Search.js @@ -35,13 +35,22 @@ class Search extends React.PureComponent { } render() { + let rightButton; + if (this.props.query) { + rightButton = ( +
    ); } From fac82745592820d2b835fed798ef44369c27eab3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Oct 2019 18:45:04 +0100 Subject: [PATCH 0375/2372] Add comment Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/LeftPanel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index f5687f0595..a0ad2b5c81 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -120,6 +120,7 @@ const LeftPanel = createReactClass({ if (!this.focusedElement) return; switch (ev.key) { + // On enter of rooms filter select and activate first room if such one exists case Key.ENTER: { const firstRoom = ev.target.closest(".mx_LeftPanel").querySelector(".mx_RoomTile"); if (firstRoom) { From 157d5a013089d864202c1cff63d070d1ea14126b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 23 Oct 2019 13:13:13 -0600 Subject: [PATCH 0376/2372] Update ServerTypeSelector for new matrix.org CS API URL This was missed in https://github.com/vector-im/riot-web/pull/11112 and causes problems where matrix.org isn't pre-selected. --- src/components/views/auth/ServerTypeSelector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/ServerTypeSelector.js b/src/components/views/auth/ServerTypeSelector.js index fa76bc8512..ebc2ea6d37 100644 --- a/src/components/views/auth/ServerTypeSelector.js +++ b/src/components/views/auth/ServerTypeSelector.js @@ -35,7 +35,7 @@ export const TYPES = { logo: () => , description: () => _t('Join millions for free on the largest public server'), serverConfig: makeType(ValidatedServerConfig, { - hsUrl: "https://matrix.org", + hsUrl: "https://matrix-client.matrix.org", hsName: "matrix.org", hsNameIsDifferent: false, isUrl: "https://vector.im", From cece1227da740c6ae8366f0b524e11396120a93b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Wed, 23 Oct 2019 18:55:27 +0000 Subject: [PATCH 0377/2372] Translated using Weblate (French) Currently translated at 100.0% (1849 of 1849 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 15cd767904..12f1590bb1 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2270,5 +2270,7 @@ "This client does not support end-to-end encryption.": "Ce client ne prend pas en charge le chiffrement de bout en bout.", "Messages in this room are not end-to-end encrypted.": "Les messages dans ce salon ne sont pas chiffrés de bout en bout.", "Jump to first unread room.": "Sauter au premier salon non lu.", - "Jump to first invite.": "Sauter à la première invitation." + "Jump to first invite.": "Sauter à la première invitation.", + "Room %(name)s": "Salon %(name)s", + "Recent rooms": "Salons récents" } From d4fd34c3e14391af03d7de24ebe76e7c7743e4c3 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 24 Oct 2019 13:11:28 +0000 Subject: [PATCH 0378/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1849 of 1849 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index e5fc53f406..7f80904b9f 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2263,5 +2263,7 @@ "React": "反應", "Cancel search": "取消搜尋", "Jump to first unread room.": "跳到第一個未讀的聊天室。", - "Jump to first invite.": "跳到第一個邀請。" + "Jump to first invite.": "跳到第一個邀請。", + "Room %(name)s": "聊天室 %(name)s", + "Recent rooms": "最近的聊天室" } From d28478a71c8dee928ae2e326900f808a4feb34c9 Mon Sep 17 00:00:00 2001 From: Madison Scott-Clary Date: Thu, 24 Oct 2019 09:31:51 +0000 Subject: [PATCH 0379/2372] Translated using Weblate (Esperanto) Currently translated at 93.8% (1735 of 1849 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eo/ --- src/i18n/strings/eo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 5603ccfc5f..7e4599aaea 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -1776,7 +1776,7 @@ "Clear all data": "Vakigi ĉiujn datumojn", "Community IDs cannot be empty.": "Identigilo de komunumo ne estu malplena.", "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Por eviti perdon de via babila historio, vi devas elporti la ŝlosilojn de viaj ĉambroj antaŭ adiaŭo. Por tio vi bezonos reveni al la pli nova versio de Riot", - "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Vi antaŭe uzis pli novan version de Riot je %(host)s. Por ree uzi ĉi tiun version kun ĉifrado, vi devos adiaŭi kaj resaluti", + "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Vi antaŭe uzis pli novan version de Riot je %(host)s. Por ree uzi ĉi tiun version kun ĉifrado, vi devos adiaŭi kaj resaluti. ", "Incompatible Database": "Nekongrua datumbazo", "View Servers in Room": "Montri servilojn en ĉambro", "You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.": "Vi antaŭe uzis Riot-on je %(host)s kun ŝaltita malfrua enlegado de anoj. En ĉi tiu versio, malfrua enlegado estas malŝaltita. Ĉar la loka kaŝmemoro de ambaŭ versioj ne kongruas, Riot bezonas respeguli vian konton.", @@ -1933,7 +1933,7 @@ "Messages": "Mesaĝoj", "Actions": "Agoj", "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Uzu identigan servilon por inviti retpoŝte. Klaku al »[…]« por uzi la implicitan identigan servilon (%(defaultIdentityServerName)s) aŭ administru tion en Agordoj.", - "Displays list of commands with usages and descriptions": "Montras liston de komandoj kun priskribo de uzo.", + "Displays list of commands with usages and descriptions": "Montras liston de komandoj kun priskribo de uzo", "Use the new, faster, but still experimental composer for writing messages (requires refresh)": "Uzi la novan, pli rapidan, sed ankoraŭ eksperimentan komponilon de mesaĝoj (bezonas aktualigon)", "Send read receipts for messages (requires compatible homeserver to disable)": "Sendi legokonfirmojn de mesaĝoj (bezonas akordan hejmservilon por malŝalto)", "Accept to continue:": "Akceptu por daŭrigi:", From 79683c052dbe67c4ccdb5164638da1270f2a6094 Mon Sep 17 00:00:00 2001 From: Michael Albert Date: Thu, 24 Oct 2019 10:26:36 +0000 Subject: [PATCH 0380/2372] Translated using Weblate (German) Currently translated at 83.5% (1544 of 1849 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 67e854d518..6c6dcbd0e4 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1730,13 +1730,13 @@ "Registration has been disabled on this homeserver.": "Registrierungen wurden auf diesem Heimserver deaktiviert.", "You need to enter a username.": "Du musst einen Benutzernamen angeben.", "Keep going...": "Fortfahren...", - "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "Wir werden eine verschlüsselte Kopie deiner Schlüssel auf deinem Server speichern. Behüte deine Sicherung mit einer Passphrase um es sicher zu halten.", + "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "Wir werden deine Schlüsselsicherung verschlüsselt auf dem Server speichern. Schütze dafür deine Sicherung mit einer Passphrase.", "For maximum security, this should be different from your account password.": "Für maximale Sicherheit, sollte dies anders als dein Konto-Passwort sein.", "Set up with a Recovery Key": "Wiederherstellungsschlüssel einrichten", - "Please enter your passphrase a second time to confirm.": "Bitte gebe eine Passphrase ein weiteres mal zu Bestätigung ein.", - "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.": "Dein Wiederherstellungsschlüssel ist ein Sicherungsnetz - Du kannst es benutzen um Zugang zu deinen verschlüsselten Nachrichten zu bekommen, wenn du deine Passphrase vergisst.", - "Keep your recovery key somewhere very secure, like a password manager (or a safe)": "Behalte deinen Wiederherstellungsschlüssel irgendwo, wo er sicher ist. Das kann ein Passwort-Manager oder Safe sein", - "A new recovery passphrase and key for Secure Messages have been detected.": "Eine neue Wiederherstellungspassphrase und -Schlüssel wir Sichere Nachrichten wurde festgestellt.", + "Please enter your passphrase a second time to confirm.": "Bitte gebe deine Passphrase ein weiteres mal zur Bestätigung ein.", + "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.": "Dein Wiederherstellungsschlüssel ist ein Sicherungsnetz - Du kannst ihn benutzen um Zugang zu deinen verschlüsselten Nachrichten zu bekommen, wenn du deine Passphrase vergisst.", + "Keep your recovery key somewhere very secure, like a password manager (or a safe)": "Speichere deinen Wiederherstellungsschlüssel irgendwo, wo er sicher ist. Das kann ein Passwort-Manager oder Safe sein", + "A new recovery passphrase and key for Secure Messages have been detected.": "Eine neue Wiederherstellungspassphrase und -Schlüssel für sichere Nachrichten wurde festgestellt.", "This device is encrypting history using the new recovery method.": "Dieses Gerät verschlüsselt die Historie mit einer neuen Wiederherstellungsmethode.", "Recovery Method Removed": "Wiederherstellungsmethode gelöscht", "This device has detected that your recovery passphrase and key for Secure Messages have been removed.": "Dieses Gerät hat bemerkt, dass deine Wiederherstellungspassphrase und -Schlüssel für Sichere Nachrichten gelöscht wurde.", @@ -1745,7 +1745,7 @@ "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "Sicherung konnte mit diesem Schlüssel nicht entschlüsselt werden: Bitte stelle sicher, dass du den richtigen Wiederherstellungsschlüssel eingegeben hast.", "Incorrect Recovery Passphrase": "Falsche Wiederherstellungspassphrase", "Backup could not be decrypted with this passphrase: please verify that you entered the correct recovery passphrase.": "Sicherung konnte nicht mit dieser Passphrase entschlüsselt werden: Bitte stelle sicher, dass du die richtige Wiederherstellungspassphrase eingegeben hast.", - "Warning: you should only set up key backup from a trusted computer.": "Warnung: Du solltest die Schlüsselsicherung nur von einem vertrautem Gerät einrichten.", + "Warning: you should only set up key backup from a trusted computer.": "Warnung: Du solltest die Schlüsselsicherung nur auf einem vertrauenswürdigen Gerät einrichten.", "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use this app with an existing Matrix account on a different homeserver.": "Du kannst die angepassten Serveroptionen benutzen um dich an einem anderen Matrixserver anzumelden indem du eine andere Heimserver-Adresse angibst. Dies erlaubt dir diese Anwendung mit einem anderen Matrixkonto auf einem anderen Heimserver zu nutzen.", "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of modular.im.": "Gib die Adresse deines Modular-Heimservers an. Es kann deine eigene Domain oder eine Subdomain von modular.im sein.", "Failed to get protocol list from homeserver": "Konnte Protokollliste vom Heimserver nicht abrufen", From 5fc11709985c3d1eff36a00baa4afeb07342f805 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Thu, 24 Oct 2019 08:48:33 +0000 Subject: [PATCH 0381/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1849 of 1849 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 79c820d51c..b84ed2d7aa 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2257,5 +2257,7 @@ "React": "Reakció", "Cancel search": "Keresés megszakítása", "Jump to first unread room.": "Az első olvasatlan szobába ugrás.", - "Jump to first invite.": "Az első meghívóra ugrás." + "Jump to first invite.": "Az első meghívóra ugrás.", + "Room %(name)s": "Szoba: %(name)s", + "Recent rooms": "Legutóbbi szobák" } From 7b3dbc431f9cb97251b1dacebb118fd2a343d182 Mon Sep 17 00:00:00 2001 From: random Date: Thu, 24 Oct 2019 13:10:17 +0000 Subject: [PATCH 0382/2372] Translated using Weblate (Italian) Currently translated at 100.0% (1849 of 1849 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 583160e28f..fcbb21e841 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2217,5 +2217,7 @@ "Jump to first unread room.": "Salta alla prima stanza non letta.", "Jump to first invite.": "Salta al primo invito.", "Command Autocomplete": "Autocompletamento comando", - "DuckDuckGo Results": "Risultati DuckDuckGo" + "DuckDuckGo Results": "Risultati DuckDuckGo", + "Room %(name)s": "Stanza %(name)s", + "Recent rooms": "Stanze recenti" } From 858dd6ea34929ab25a0e35268300a1f3bc5f3e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Thu, 24 Oct 2019 12:02:47 +0000 Subject: [PATCH 0383/2372] Translated using Weblate (Korean) Currently translated at 100.0% (1849 of 1849 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 6f976d8b0e..ee6e53b6cd 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2114,5 +2114,7 @@ "This client does not support end-to-end encryption.": "이 클라이언트는 종단간 암호화를 지원하지 않습니다.", "Messages in this room are not end-to-end encrypted.": "이 방의 메시지는 종단간 암호화가 되지 않았습니다.", "Jump to first unread room.": "읽지 않은 첫 방으로 건너뜁니다.", - "Jump to first invite.": "첫 초대로 건너뜁니다." + "Jump to first invite.": "첫 초대로 건너뜁니다.", + "Room %(name)s": "%(name)s 방", + "Recent rooms": "최근 방" } From 31b49864dcf021dc74c9f04ed5a9aead3238e90d Mon Sep 17 00:00:00 2001 From: MamasLT Date: Thu, 24 Oct 2019 02:58:10 +0000 Subject: [PATCH 0384/2372] Translated using Weblate (Lithuanian) Currently translated at 43.9% (811 of 1849 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/lt/ --- src/i18n/strings/lt.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index f60ac73069..d7f5e2bf74 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -7,7 +7,7 @@ "Whether or not you're logged in (we don't record your user name)": "Nesvarbu ar esate prisijungę ar ne (mes neįrašome jūsų naudotojo vardo)", "Your language of choice": "Jūsų pasirinkta kalba", "Which officially provided instance you are using, if any": "Kurį oficialiai pateiktą egzempliorių naudojate", - "Whether or not you're using the Richtext mode of the Rich Text Editor": "Nesvarbu jūs naudojate ar ne raiškiojo teksto režimą Raiškiojo tekto redaktoriuje", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Ar jūs naudojate Raiškiojo Teksto Redaktoriaus Raiškiojo Teksto režimą ar ne", "Your homeserver's URL": "Jūsų serverio URL adresas", "Your identity server's URL": "Jūsų identifikavimo serverio URL adresas", "Analytics": "Statistika", @@ -991,5 +991,11 @@ "Got it": "Supratau", "Backup created": "Atsarginė kopija sukurta", "Recovery Key": "Atkūrimo raktas", - "Retry": "Bandyti dar kartą" + "Retry": "Bandyti dar kartą", + "Add Email Address": "Pridėti el. pašto adresą", + "Add Phone Number": "Pridėti telefono numerį", + "Whether or not you're logged in (we don't record your username)": "Nepriklausomai nuo to ar jūs prisijungę (mes neįrašome jūsų vartotojo vardo)", + "Chat with Riot Bot": "Kalbėtis su Riot botu", + "Sign In": "Prisijungti", + "Explore rooms": "Peržiūrėti kambarius" } From 3e360c156ad3f21ebbb22a87788b60ea54770567 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 24 Oct 2019 12:45:20 +0200 Subject: [PATCH 0385/2372] bring LazyRenderList up to React 16 standards, cleanup & docs --- .../views/elements/LazyRenderList.js | 99 +++++++++++++------ 1 file changed, 68 insertions(+), 31 deletions(-) diff --git a/src/components/views/elements/LazyRenderList.js b/src/components/views/elements/LazyRenderList.js index d7d2a0ab99..0fc0ef6733 100644 --- a/src/components/views/elements/LazyRenderList.js +++ b/src/components/views/elements/LazyRenderList.js @@ -15,9 +15,7 @@ limitations under the License. */ import React from "react"; - -const OVERFLOW_ITEMS = 20; -const OVERFLOW_MARGIN = 5; +import PropTypes from 'prop-types'; class ItemRange { constructor(topCount, renderCount, bottomCount) { @@ -27,11 +25,22 @@ class ItemRange { } contains(range) { + // don't contain empty ranges + // as it will prevent clearing the list + // once it is scrolled far enough out of view + if (!range.renderCount && this.renderCount) { + return false; + } return range.topCount >= this.topCount && (range.topCount + range.renderCount) <= (this.topCount + this.renderCount); } expand(amount) { + // don't expand ranges that won't render anything + if (this.renderCount === 0) { + return this; + } + const topGrow = Math.min(amount, this.topCount); const bottomGrow = Math.min(amount, this.bottomCount); return new ItemRange( @@ -40,52 +49,80 @@ class ItemRange { this.bottomCount - bottomGrow, ); } + + totalSize() { + return this.topCount + this.renderCount + this.bottomCount; + } } export default class LazyRenderList extends React.Component { - constructor(props) { - super(props); - const renderRange = LazyRenderList.getVisibleRangeFromProps(props).expand(OVERFLOW_ITEMS); - this.state = {renderRange}; + static getDerivedStateFromProps(props, state) { + const range = LazyRenderList.getVisibleRangeFromProps(props); + const intersectRange = range.expand(props.overflowMargin); + const renderRange = range.expand(props.overflowItems); + const listHasChangedSize = !!state && renderRange.totalSize() !== state.renderRange.totalSize(); + // only update render Range if the list has shrunk/grown and we need to adjust padding OR + // if the new range + overflowMargin isn't contained by the old anymore + if (listHasChangedSize || !state || !state.renderRange.contains(intersectRange)) { + return {renderRange}; + } + return null; } static getVisibleRangeFromProps(props) { const {items, itemHeight, scrollTop, height} = props; const length = items ? items.length : 0; - const topCount = Math.max(0, Math.floor(scrollTop / itemHeight)); + const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length); const itemsAfterTop = length - topCount; - const renderCount = Math.min(Math.ceil(height / itemHeight), itemsAfterTop); + const visibleItems = height !== 0 ? Math.ceil(height / itemHeight) : 0; + const renderCount = Math.min(visibleItems, itemsAfterTop); const bottomCount = itemsAfterTop - renderCount; return new ItemRange(topCount, renderCount, bottomCount); } - componentWillReceiveProps(props) { - const state = this.state; - const range = LazyRenderList.getVisibleRangeFromProps(props); - const intersectRange = range.expand(OVERFLOW_MARGIN); - - const prevSize = this.props.items ? this.props.items.length : 0; - const listHasChangedSize = props.items.length !== prevSize; - // only update renderRange if the list has shrunk/grown and we need to adjust padding or - // if the new range isn't contained by the old anymore - if (listHasChangedSize || !state.renderRange || !state.renderRange.contains(intersectRange)) { - this.setState({renderRange: range.expand(OVERFLOW_ITEMS)}); - } - } - render() { const {itemHeight, items, renderItem} = this.props; - const {renderRange} = this.state; - const paddingTop = renderRange.topCount * itemHeight; - const paddingBottom = renderRange.bottomCount * itemHeight; + const {topCount, renderCount, bottomCount} = renderRange; + + const paddingTop = topCount * itemHeight; + const paddingBottom = bottomCount * itemHeight; const renderedItems = (items || []).slice( - renderRange.topCount, - renderRange.topCount + renderRange.renderCount, + topCount, + topCount + renderCount, ); - return (
    - { renderedItems.map(renderItem) } -
    ); + const element = this.props.element || "div"; + const elementProps = { + "style": {paddingTop: `${paddingTop}px`, paddingBottom: `${paddingBottom}px`}, + "className": this.props.className, + }; + return React.createElement(element, elementProps, renderedItems.map(renderItem)); } } + +LazyRenderList.defaultProps = { + overflowItems: 20, + overflowMargin: 5, +}; + +LazyRenderList.propTypes = { + // height in pixels of the component returned by `renderItem` + itemHeight: PropTypes.number.isRequired, + // function to turn an element of `items` into a react component + renderItem: PropTypes.func.isRequired, + // scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s) + scrollTop: PropTypes.number.isRequired, + // the height of the viewport this content is scrolled in + height: PropTypes.number.isRequired, + // all items for the list. These should not be react components, see `renderItem`. + items: PropTypes.array, + // the amount of items to scroll before causing a rerender, + // should typically be less than `overflowItems` unless applying + // margins in the parent component when using multiple LazyRenderList in one viewport. + // use 0 to only rerender when items will come into view. + overflowMargin: PropTypes.number, + // the amount of items to add at the top and bottom to render, + // so not every scroll of causes a rerender. + overflowItems: PropTypes.number, +}; From 00b1816986f252fe42d83b978a93154e81c7548b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 24 Oct 2019 16:01:34 +0200 Subject: [PATCH 0386/2372] use LazyRenderList in emoji picker Category --- src/components/views/emojipicker/Category.js | 48 +++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/components/views/emojipicker/Category.js b/src/components/views/emojipicker/Category.js index ba48c8842b..ba525b76e2 100644 --- a/src/components/views/emojipicker/Category.js +++ b/src/components/views/emojipicker/Category.js @@ -16,9 +16,11 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; - +import { CATEGORY_HEADER_HEIGHT, EMOJI_HEIGHT, EMOJIS_PER_ROW } from "./EmojiPicker"; import sdk from '../../../index'; +const OVERFLOW_ROWS = 3; + class Category extends React.PureComponent { static propTypes = { emojis: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -30,22 +32,54 @@ class Category extends React.PureComponent { selectedEmojis: PropTypes.instanceOf(Set), }; + _renderEmojiRow = (rowIndex) => { + const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props; + const emojisForRow = emojis.slice(rowIndex * 8, (rowIndex + 1) * 8); + const Emoji = sdk.getComponent("emojipicker.Emoji"); + return (
    { + emojisForRow.map(emoji => + ) + }
    ); + }; + render() { - const { onClick, onMouseEnter, onMouseLeave, emojis, name, selectedEmojis } = this.props; + const { emojis, name, heightBefore, viewportHeight, scrollTop } = this.props; if (!emojis || emojis.length === 0) { return null; } + const rows = new Array(Math.ceil(emojis.length / EMOJIS_PER_ROW)); + for (let counter = 0; counter < rows.length; ++counter) { + rows[counter] = counter; + } + const LazyRenderList = sdk.getComponent('elements.LazyRenderList'); + + const viewportTop = scrollTop; + const viewportBottom = viewportTop + viewportHeight; + const listTop = heightBefore + CATEGORY_HEADER_HEIGHT; + const listBottom = listTop + (rows.length * EMOJI_HEIGHT); + const top = Math.max(viewportTop, listTop); + const bottom = Math.min(viewportBottom, listBottom); + // the viewport height and scrollTop passed to the LazyRenderList + // is capped at the intersection with the real viewport, so lists + // out of view are passed height 0, so they won't render any items. + const localHeight = Math.max(0, bottom - top); + const localScrollTop = Math.max(0, scrollTop - listTop); - const Emoji = sdk.getComponent("emojipicker.Emoji"); return (

    {name}

    -
      - {emojis.map(emoji => )} -
    + +
    ); } From ea24b8bd58a4e3040e4f88c353b645d816540554 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 24 Oct 2019 16:02:27 +0200 Subject: [PATCH 0387/2372] pass heightBefore, scrollTop and viewportHeight to Category to support having multiple LazyRenderLists in one viewport --- .../views/emojipicker/EmojiPicker.js | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.js index 6d34804187..48713b90d8 100644 --- a/src/components/views/emojipicker/EmojiPicker.js +++ b/src/components/views/emojipicker/EmojiPicker.js @@ -58,6 +58,10 @@ EMOJIBASE.forEach(emoji => { emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`; }); +export const CATEGORY_HEADER_HEIGHT = 22; +export const EMOJI_HEIGHT = 37; +export const EMOJIS_PER_ROW = 8; + class EmojiPicker extends React.Component { static propTypes = { onChoose: PropTypes.func.isRequired, @@ -71,6 +75,11 @@ class EmojiPicker extends React.Component { this.state = { filter: "", previewEmoji: null, + scrollTop: 0, + // initial estimation of height, dialog is hardcoded to 450px height. + // should be enough to never have blank rows of emojis as + // 3 rows of overflow are also rendered. The actual value is updated on scroll. + viewportHeight: 280, }; this.recentlyUsed = recent.get().map(unicode => DATA_BY_EMOJI[unicode]); @@ -145,10 +154,20 @@ class EmojiPicker extends React.Component { this.updateVisibility = this.updateVisibility.bind(this); } + onScroll = () => { + const body = this.bodyRef.current; + this.setState({ + scrollTop: body.scrollTop, + viewportHeight: body.clientHeight, + }); + this.updateVisibility(); + }; + updateVisibility() { - const rect = this.bodyRef.current.getBoundingClientRect(); + const body = this.bodyRef.current; + const rect = body.getBoundingClientRect(); for (const cat of this.categories) { - const elem = this.bodyRef.current.querySelector(`[data-category-id="${cat.id}"]`); + const elem = body.querySelector(`[data-category-id="${cat.id}"]`); if (!elem) { cat.visible = false; cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible"); @@ -210,23 +229,36 @@ class EmojiPicker extends React.Component { } } + _categoryHeightForEmojiCount(count) { + if (count === 0) { + return 0; + } + return CATEGORY_HEADER_HEIGHT + (Math.ceil(count / EMOJIS_PER_ROW) * EMOJI_HEIGHT); + } + render() { const Header = sdk.getComponent("emojipicker.Header"); const Search = sdk.getComponent("emojipicker.Search"); const Category = sdk.getComponent("emojipicker.Category"); const Preview = sdk.getComponent("emojipicker.Preview"); const QuickReactions = sdk.getComponent("emojipicker.QuickReactions"); + let heightBefore = 0; return (
    -
    - {this.categories.map(category => ( - + {this.categories.map(category => { + const emojis = this.memoizedDataByCategory[category.id]; + const categoryElement = ( - ))} + selectedEmojis={this.props.selectedEmojis} />); + const height = this._categoryHeightForEmojiCount(emojis.length); + heightBefore += height; + return categoryElement; + })}
    {this.state.previewEmoji || !this.props.showQuickReactions ? From 5a401d98ad52925a6d263ab127b20614c7896b25 Mon Sep 17 00:00:00 2001 From: Tuomas Hietala Date: Thu, 24 Oct 2019 17:35:50 +0000 Subject: [PATCH 0388/2372] Translated using Weblate (Finnish) Currently translated at 97.6% (1805 of 1849 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fi/ --- src/i18n/strings/fi.json | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 0560dddfcc..757f9a0337 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -2079,5 +2079,34 @@ "Show image": "Näytä kuva", "Please create a new issue on GitHub so that we can investigate this bug.": "Luo uusi issue GitHubissa, jotta voimme tutkia tätä ongelmaa.", "Close dialog": "Sulje dialogi", - "To continue you need to accept the terms of this service.": "Sinun täytyy hyväksyä palvelun käyttöehdot jatkaaksesi." + "To continue you need to accept the terms of this service.": "Sinun täytyy hyväksyä palvelun käyttöehdot jatkaaksesi.", + "Add Email Address": "Lisää sähköpostiosoite", + "Add Phone Number": "Lisää puhelinnumero", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Suosittelemme poistamaan henkilökohtaiset tietosi identiteettipalvelimelta ennen yhteyden katkaisemista. Valitettavasti identiteettipalvelin on parhaillaan poissa verkosta tai siihen ei saada yhteyttä.", + "You should:": "Sinun tulisi:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "tarkistaa, että selaimen lisäosat (kuten Privacy Badger) eivät estä identiteettipalvelinta", + "contact the administrators of identity server ": "ottaa yhteyttä identiteettipalvelimen ylläpitäjiin", + "wait and try again later": "odottaa ja yrittää uudelleen myöhemmin", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "Olet poistamassa yhtä käyttäjän %(user)s kirjoittamaa viestiä. Tätä toimintoa ei voi kumota. Haluatko jatkaa?", + "Remove %(count)s messages|one": "Poista yksi viesti", + "Room %(name)s": "Huone %(name)s", + "Recent rooms": "Viimeaikaiset huoneet", + "Trust & Devices": "Luottamus ja laitteet", + "Direct messages": "Yksityisviestit", + "React": "Reagoi", + "Frequently Used": "Usein käytetyt", + "Smileys & People": "Hymiöt ja ihmiset", + "Animals & Nature": "Eläimet ja luonto", + "Food & Drink": "Ruoka ja juoma", + "Activities": "Aktiviteetit", + "Travel & Places": "Matkustaminen ja paikat", + "Objects": "Esineet", + "Symbols": "Symbolit", + "Flags": "Liput", + "Quick Reactions": "Pikareaktiot", + "Cancel search": "Peruuta haku", + "%(creator)s created and configured the room.": "%(creator)s loi ja määritti huoneen.", + "Jump to first unread room.": "Siirry ensimmäiseen lukemattomaan huoneeseen.", + "Jump to first invite.": "Siirry ensimmäiseen kutsuun.", + "DuckDuckGo Results": "DuckDuckGo-tulokset" } From 821ad5703fe037bccd8467862bcf5fefdaccb7de Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 25 Oct 2019 16:37:57 +0100 Subject: [PATCH 0389/2372] LifeCycle onLoggedOut unmount before stopping client Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Lifecycle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 7490c5d464..13f3abccb1 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -610,7 +610,7 @@ export function onLoggedOut() { // Ensure that we dispatch a view change **before** stopping the client so // so that React components unmount first. This avoids React soft crashes // that can occur when components try to use a null client. - dis.dispatch({action: 'on_logged_out'}); + dis.dispatch({action: 'on_logged_out'}, true); stopMatrixClient(); _clearStorage().done(); } From 3b8cb421081b16cad549ea5b30131ea727ca7ef2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 26 Oct 2019 09:31:45 +0100 Subject: [PATCH 0390/2372] Fix Room Create ELS using MXID instead of newly set Displayname/Avatar --- src/components/structures/MessagePanel.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 7254059734..cf2a5b1738 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -443,15 +443,17 @@ module.exports = createReactClass({ // one was itself. This way, the timestamp of the previous event === the // timestamp of the current event, and no DateSeparator is inserted. return this._getTilesForEvent(e, e, e === lastShownEvent); - }).reduce((a, b) => a.concat(b)); + }).reduce((a, b) => a.concat(b), []); + // Get sender profile from the latest event in the summary as the m.room.create doesn't contain one + const ev = this.props.events[i]; ret.push( { eventTiles } @@ -529,7 +531,7 @@ module.exports = createReactClass({ // one was itself. This way, the timestamp of the previous event === the // timestamp of the current event, and no DateSeparator is inserted. return this._getTilesForEvent(e, e, e === lastShownEvent); - }).reduce((a, b) => a.concat(b)); + }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { eventTiles = null; From 6a8bc45d83c35531646d59655beac3433864a49a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sim=C3=B3=20Albert=20i=20Beltran?= Date: Fri, 25 Oct 2019 17:08:16 +0000 Subject: [PATCH 0391/2372] Translated using Weblate (Catalan) Currently translated at 43.4% (802 of 1849 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ca/ --- src/i18n/strings/ca.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/i18n/strings/ca.json b/src/i18n/strings/ca.json index 5f3f40b6d9..1114fa675c 100644 --- a/src/i18n/strings/ca.json +++ b/src/i18n/strings/ca.json @@ -98,14 +98,14 @@ "Name or matrix ID": "Nom o ID de Matrix", "Invite to Community": "Convida a la comunitat", "Which rooms would you like to add to this community?": "Quines sales voleu afegir a aquesta comunitat?", - "Show these rooms to non-members on the community page and room list?": "Voleu mostrar a la llista de sales i a la pàgina de la comunitat, aquestes sales, als qui no en siguin membres?", + "Show these rooms to non-members on the community page and room list?": "Voleu mostrar aquestes sales als que no son membres a la pàgina de la comunitat i a la llista de sales?", "Add rooms to the community": "Afegeix sales a la comunitat", "Room name or alias": "Nom de la sala o àlies", "Add to community": "Afegeix a la comunitat", "Failed to invite the following users to %(groupId)s:": "No s'ha pogut convidar a %(groupId)s els següents usuaris:", "Failed to invite users to community": "No s'ha pogut convidar als usuaris a la comunitat", "Failed to invite users to %(groupId)s": "No s'ha pogut convidar els usuaris a %(groupId)s", - "Failed to add the following rooms to %(groupId)s:": "No s'ha pogut afegir al grup %(groupId)s les següents sales:", + "Failed to add the following rooms to %(groupId)s:": "No s'ha pogut afegir les següents sales al %(groupId)s:", "Riot does not have permission to send you notifications - please check your browser settings": "Riot no té permís per enviar-vos notificacions. Comproveu la configuració del vostre navegador", "Riot was not given permission to send notifications - please try again": "Riot no ha rebut cap permís per enviar notificacions. Torneu-ho a provar", "Unable to enable Notifications": "No s'ha pogut activar les notificacions", @@ -135,7 +135,7 @@ "You do not have permission to do that in this room.": "No teniu el permís per realitzar aquesta acció en aquesta sala.", "Missing room_id in request": "Falta l'ID de la sala en la vostra sol·licitud", "Room %(roomId)s not visible": "La sala %(roomId)s no és visible", - "Missing user_id in request": "Falta l'ID d'usuari a la vostre sol·licitud", + "Missing user_id in request": "Falta el user_id a la sol·licitud", "Usage": "Ús", "/ddg is not a command": "/ddg no és un comandament", "To use it, just wait for autocomplete results to load and tab through them.": "Per utilitzar-lo, simplement espereu que es completin els resultats automàticament i seleccioneu-ne el desitjat.", @@ -182,15 +182,15 @@ "(no answer)": "(sense resposta)", "(unknown failure: %(reason)s)": "(error desconegut: %(reason)s)", "%(senderName)s ended the call.": "%(senderName)s ha penjat.", - "%(senderName)s placed a %(callType)s call.": "%(senderName)s ha col·locat una trucada de %(callType)s.", - "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s ha enviat una invitació a %(targetDisplayName)s a entrar a aquesta sala.", + "%(senderName)s placed a %(callType)s call.": "%(senderName)s ha fet una trucada de %(callType)s.", + "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s ha convidat a %(targetDisplayName)s a entrar a la sala.", "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s ha fet visible l'històric futur de la sala per a tots els membres, a partir de que hi són convidats.", "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s ha fet visible l'històric futur de la sala a tots els membres, des de que entren a la sala.", "%(senderName)s made future room history visible to all room members.": "%(senderName)s ha fet visible l'històric futur de la sala a tots els membres de la sala.", - "%(senderName)s made future room history visible to anyone.": "%(senderName)s ha fet visible l´historial de la sala per a tothom.", - "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s ha fet visible l'històric de la sala per a desconeguts (%(visibility)s).", + "%(senderName)s made future room history visible to anyone.": "%(senderName)s ha fet visible el futur historial de la sala per a tothom.", + "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s ha fet visible el futur historial de la sala per a desconeguts (%(visibility)s).", "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s ha activat el xifratge d'extrem a extrem (algoritme %(algorithm)s).", - "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s de %(fromPowerLevel)s fins %(toPowerLevel)s", + "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s de %(fromPowerLevel)s a %(toPowerLevel)s", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s ha canviat el nivell de poders de %(powerLevelDiffText)s.", "%(senderName)s changed the pinned messages for the room.": "%(senderName)s ha canviat els missatges fixats de la sala.", "%(widgetName)s widget modified by %(senderName)s": "%(senderName)s ha modificat el giny %(widgetName)s", @@ -811,7 +811,7 @@ "Remove %(threePid)s?": "Esborrar %(threePid)s?", "Interface Language": "Idioma de l'interfície", "User Interface": "Interfície d'usuari", - "Import E2E room keys": "Importar claus E2E de la sala", + "Import E2E room keys": "Importar claus E2E de sala", "Cryptography": "Criptografia", "Device ID:": "ID del dispositiu:", "Device key:": "Clau del dispositiu:", @@ -854,7 +854,7 @@ "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s ha canviat el seu nom visible a %(displayName)s.", "Server may be unavailable or overloaded": "El servidor pot estar inaccessible o sobrecarregat", "Display name": "Nom visible", - "Identity Server is": "El servidor d'identitat es", + "Identity Server is": "El servidor d'identitat és", "Submit debug logs": "Enviar logs de depuració", "The platform you're on": "La plataforma a la que estàs", "Whether or not you're logged in (we don't record your user name)": "Si estàs identificat o no (no desem el teu nom d'usuari)", From 2d3836d73184f9c36fa7c821526f5d83df047ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Fri, 25 Oct 2019 11:03:23 +0000 Subject: [PATCH 0392/2372] Translated using Weblate (Korean) Currently translated at 100.0% (1849 of 1849 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 48 ++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index ee6e53b6cd..7aa907708a 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -44,8 +44,8 @@ "Attachment": "첨부 파일", "Are you sure you want to upload the following files?": "다음 파일들을 올리시겠어요?", "Autoplay GIFs and videos": "GIF와 동영상을 자동으로 재생하기", - "Ban": "차단", - "Banned users": "차단된 사용자", + "Ban": "출입 금지", + "Banned users": "출입 금지된 사용자", "Blacklisted": "블랙리스트 대상", "Can't load user settings": "사용사 설정을 불러올 수 없습니다.", "Change Password": "비밀번호 바꾸기", @@ -89,8 +89,8 @@ "Anyone who knows the room's link, apart from guests": "손님을 제외하고, 방의 주소를 아는 누구나", "Anyone who knows the room's link, including guests": "손님을 포함하여, 방의 주소를 아는 누구나", "Are you sure you want to reject the invitation?": "초대를 거절하시겠어요?", - "%(senderName)s banned %(targetName)s.": "%(senderName)s님이 %(targetName)s님을 차단했습니다.", - "Bans user with given id": "받은 ID로 사용자 차단하기", + "%(senderName)s banned %(targetName)s.": "%(senderName)s님이 %(targetName)s님을 출입 금지했습니다.", + "Bans user with given id": "받은 ID로 사용자 출입 금지하기", "Bulk Options": "대규모 설정", "Call Timeout": "전화 대기 시간 초과", "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "홈서버에 연결할 수 없음 - 연결 상태를 확인하거나, 홈서버의 SSL 인증서가 믿을 수 있는지 확인하고, 브라우저 확장 기능이 요청을 막고 있는지 확인해주세요.", @@ -162,7 +162,7 @@ "Existing Call": "기존 전화", "Export": "내보내기", "Export E2E room keys": "종단간 암호화 방 열쇠 내보내기", - "Failed to ban user": "사용자 차단에 실패함", + "Failed to ban user": "사용자 출입 금지에 실패함", "Failed to change power level": "권한 등급 변경에 실패함", "Failed to fetch avatar URL": "아바타 URL 가져오기에 실패함", "Failed to join room": "방에 들어가지 못했습니다", @@ -179,7 +179,7 @@ "Failed to set display name": "표시 이름을 설정하지 못함", "Failed to set up conference call": "전화 회의를 시작하지 못했습니다.", "Failed to toggle moderator status": "조정자 상태 설정에 실패함", - "Failed to unban": "차단 해제 실패함", + "Failed to unban": "출입 금지 풀기에 실패함", "Failed to upload file": "파일을 올리지 못했습니다.", "Failed to upload profile picture!": "프로필 사진 업로드에 실패함!", "Failed to verify email address: make sure you clicked the link in the email": "이메일 주소를 인증하지 못했습니다. 메일에 나온 주소를 눌렀는지 확인해 보세요", @@ -388,8 +388,8 @@ "Unable to add email address": "이메일 주소를 추가할 수 없음", "Unable to remove contact information": "연락처 정보를 제거할 수 없음", "Unable to verify email address.": "이메일 주소를 인증할 수 없습니다.", - "Unban": "차단 해제", - "%(senderName)s unbanned %(targetName)s.": "%(senderName)s님이 %(targetName)s님에 대한 차단을 해제했습니다.", + "Unban": "출입 금지 풀기", + "%(senderName)s unbanned %(targetName)s.": "%(senderName)s님이 %(targetName)s님에 대한 출입 금지를 풀었습니다.", "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "이 이매알 주소가 초대를 받은 계정과 연결된 주소가 맞는지 확인할 수 없습니다.", "Unable to capture screen": "화면을 찍을 수 없음", "Unable to enable Notifications": "알림을 사용할 수 없음", @@ -783,7 +783,7 @@ "Hide display name changes": "별명 변경 내역 숨기기", "This event could not be displayed": "이 이벤트를 표시할 수 없음", "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "%(displayName)s(%(userName)s)님이 %(dateTime)s에 확인함", - "Banned by %(displayName)s": "%(displayName)s님에 의해 차단됨", + "Banned by %(displayName)s": "%(displayName)s님에 의해 출입 금지됨", "Display your community flair in rooms configured to show it.": "커뮤니티 재능이 보이도록 설정된 방에서 커뮤니티 재능을 표시할 수 있습니다.", "The user '%(displayName)s' could not be removed from the summary.": "사용자 %(displayName)s님을 요약에서 제거하지 못했습니다.", "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "이 방들은 커뮤니티 페이지에서 커뮤니티 구성원에게 보여집니다. 커뮤니티 구성원은 방을 클릭해 참가할 수 있습니다.", @@ -907,7 +907,7 @@ "Encrypted, not sent": "암호화 됨, 보내지지 않음", "Disinvite this user?": "이 사용자에 대한 초대를 취소할까요?", "Kick this user?": "이 사용자를 추방할까요?", - "Unban this user?": "이 사용자를 차단 해제할까요?", + "Unban this user?": "이 사용자를 출입 금지에서 풀까요?", "%(duration)ss": "%(duration)s초", "%(duration)sm": "%(duration)s분", "%(duration)sh": "%(duration)s시간", @@ -936,16 +936,16 @@ "Ignored Users": "무시된 사용자", "Demote": "강등", "Demote yourself?": "자신을 강등하시겠습니까?", - "Ban this user?": "이 사용자를 차단할까요?", + "Ban this user?": "이 사용자를 출입 금지할까요?", "To ban users, you must be a": "사용자를 차단하기 위해서 필요한 권한:", - "were banned %(count)s times|other": "이 %(count)s번 차단당했습니다", - "were banned %(count)s times|one": "이 차단당했습니다", - "was banned %(count)s times|other": "님이 %(count)s번 차단당했습니다", - "was banned %(count)s times|one": "님이 차단당했습니다", - "were unbanned %(count)s times|other": "의 차단이 %(count)s번 풀렸습니다", - "were unbanned %(count)s times|one": "의 차단이 풀렸습니다", - "was unbanned %(count)s times|other": "님의 차단이 %(count)s번 풀렸습니다", - "was unbanned %(count)s times|one": "님의 차단이 풀렸습니다", + "were banned %(count)s times|other": "이 %(count)s번 출입 금지 당했습니다", + "were banned %(count)s times|one": "이 출입 금지 당했습니다", + "was banned %(count)s times|other": "님이 %(count)s번 출입 금지 당했습니다", + "was banned %(count)s times|one": "님이 출입 금지 당했습니다", + "were unbanned %(count)s times|other": "의 출입 금지이 %(count)s번 풀렸습니다", + "were unbanned %(count)s times|one": "의 출입 금지이 풀렸습니다", + "was unbanned %(count)s times|other": "님의 출입 금지이 %(count)s번 풀렸습니다", + "was unbanned %(count)s times|one": "님의 출입 금지이 풀렸습니다", "Delete %(count)s devices|other": "%(count)s개의 기기 삭제", "Drop here to restore": "복구하려면 여기에 놓기", "Drop here to favourite": "즐겨찾기에 추가하려면 여기에 놓기", @@ -1271,7 +1271,7 @@ "Gets or sets the room topic": "방 주제를 얻거나 설정하기", "This room has no topic.": "이 방은 주제가 없습니다.", "Sets the room name": "방 이름 설정하기", - "Unbans user with given ID": "받은 ID로 사용자 차단 풀기", + "Unbans user with given ID": "받은 ID로 사용자 출입 금지 풀기", "Adds a custom widget by URL to the room": "URL로 방에 맞춤 위젯 추가하기", "Please supply a https:// or http:// widget URL": "https:// 혹은 http:// 위젯 URL을 제공하세요", "You cannot modify widgets in this room.": "이 방에서 위젯을 수정할 수 없습니다.", @@ -1320,7 +1320,7 @@ "User %(userId)s is already in the room": "사용자 %(userId)s님이 이미 이 방에 있습니다", "User %(user_id)s does not exist": "사용자 %(user_id)s님이 존재하지 않습니다", "User %(user_id)s may or may not exist": "사용자 %(user_id)s님이 있거나 없을 수 있습니다", - "The user must be unbanned before they can be invited.": "초대하려면 사용자가 차단되지 않은 상태여야 합니다.", + "The user must be unbanned before they can be invited.": "초대하려면 사용자가 출입 금지되지 않은 상태여야 합니다.", "The user's homeserver does not support the version of the room.": "사용자의 홈서버가 방의 버전을 호환하지 않습니다.", "Unknown server error": "알 수 없는 서버 오류", "Use a few words, avoid common phrases": "몇 단어를 사용하되, 흔한 단어는 피하세요", @@ -1426,7 +1426,7 @@ "Multiple integration managers": "여러 통합 관리자", "Enable Emoji suggestions while typing": "입력 중 이모지 제안 켜기", "Show a placeholder for removed messages": "감춘 메시지의 자리 표시하기", - "Show join/leave messages (invites/kicks/bans unaffected)": "참가/떠남 메시지 보이기 (초대/추방/차단은 영향 없음)", + "Show join/leave messages (invites/kicks/bans unaffected)": "참가/떠남 메시지 보이기 (초대/추방/출입 금지는 영향 없음)", "Show avatar changes": "아바타 변경 사항 보이기", "Show display name changes": "표시 이름 변경 사항 보이기", "Show read receipts sent by other users": "다른 사용자가 읽은 기록 보이기", @@ -1595,7 +1595,7 @@ "Invite users": "사용자 초대", "Change settings": "설정 변경", "Kick users": "사용자 추방", - "Ban users": "사용자 차단", + "Ban users": "사용자 출입 금지", "Remove messages": "메시지 감추기", "Notify everyone": "모두에게 알림", "Send %(eventType)s events": "%(eventType)s 이벤트 보내기", @@ -1642,7 +1642,7 @@ "Reason: %(reason)s": "이유: %(reason)s", "Forget this room": "이 방 지우기", "Re-join": "다시 참가", - "You were banned from %(roomName)s by %(memberName)s": "%(roomName)s 방에서 %(memberName)s님에 의해 차단당했습니다", + "You were banned from %(roomName)s by %(memberName)s": "%(roomName)s 방에서 %(memberName)s님에 의해 출입 금지 당했습니다", "Something went wrong with your invite to %(roomName)s": "%(roomName)s 방으로의 초대에 문제가 있음", "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "초대를 확인하는 동안 %(errcode)s가 반환됬습니다. 이 정보를 방 관리자에게 전달할 수 있습니다.", "You can only join it with a working invite.": "초대받은 사람만 참가할 수 있습니다.", From 8eefb5bf025e6eec1b6250901c18758a09859165 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 28 Oct 2019 09:53:30 +0000 Subject: [PATCH 0393/2372] If ToS gets rejected/any Scalar error then don't make Jitsi widget --- src/CallHandler.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index f6b3e18538..3efd800499 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -387,7 +387,7 @@ async function _startCallApp(roomId, type) { // work for us. Better that the user knows before everyone else in the // room sees it. const managers = IntegrationManagers.sharedInstance(); - let haveScalar = true; + let haveScalar = false; if (managers.hasManager()) { try { const scalarClient = managers.getPrimaryManager().getScalarClient(); @@ -396,8 +396,6 @@ async function _startCallApp(roomId, type) { } catch (e) { // ignore } - } else { - haveScalar = false; } if (!haveScalar) { From e661e3b11577bd51663c5cd95c045f109c0a5739 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 28 Oct 2019 13:17:59 +0000 Subject: [PATCH 0394/2372] Fix quick reactions to be aligned with other emoji This uses center alignment for quick reactions as well, so that they line up with the larger emoji categories above. --- res/css/views/emojipicker/_EmojiPicker.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index 3bf6845c85..5d9b3f2687 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -221,7 +221,6 @@ limitations under the License. .mx_EmojiPicker_quick { flex-direction: column; - align-items: start; justify-content: space-around; } From 88d0ae05726652dcc90cbf9966d88fc554412206 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 28 Oct 2019 14:13:24 +0000 Subject: [PATCH 0395/2372] Remove messages implying you need an identity server for email recovery This tweaks logic that shows some warning messages saying you need an identity server for email recovery. Assuming you have a modern homeserver, no identity server is need for these activities, so the warnings were confusing. Fixes https://github.com/vector-im/riot-web/issues/11100 --- src/components/views/auth/RegistrationForm.js | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 5d4dadca9d..1ea66176ff 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -97,16 +97,16 @@ module.exports = createReactClass({ const haveIs = Boolean(this.props.serverConfig.isUrl); let desc; - if (haveIs) { - desc = _t( - "If you don't specify an email address, you won't be able to reset your password. " + - "Are you sure?", - ); - } else { + if (this.props.serverRequiresIdServer && !haveIs) { desc = _t( "No Identity Server is configured so you cannot add add an email address in order to " + "reset your password in the future.", ); + } else { + desc = _t( + "If you don't specify an email address, you won't be able to reset your password. " + + "Are you sure?", + ); } const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); @@ -439,7 +439,10 @@ module.exports = createReactClass({ _showEmail() { const haveIs = Boolean(this.props.serverConfig.isUrl); - if ((this.props.serverRequiresIdServer && !haveIs) || !this._authStepIsUsed('m.login.email.identity')) { + if ( + (this.props.serverRequiresIdServer && !haveIs) || + !this._authStepIsUsed('m.login.email.identity') + ) { return false; } return true; @@ -448,8 +451,11 @@ module.exports = createReactClass({ _showPhoneNumber() { const threePidLogin = !SdkConfig.get().disable_3pid_login; const haveIs = Boolean(this.props.serverConfig.isUrl); - const haveRequiredIs = this.props.serverRequiresIdServer && !haveIs; - if (!threePidLogin || haveRequiredIs || !this._authStepIsUsed('m.login.msisdn')) { + if ( + !threePidLogin || + (this.props.serverRequiresIdServer && !haveIs) || + !this._authStepIsUsed('m.login.msisdn') + ) { return false; } return true; @@ -592,12 +598,15 @@ module.exports = createReactClass({ } } const haveIs = Boolean(this.props.serverConfig.isUrl); - const noIsText = haveIs ? null :
    - {_t( - "No Identity Server is configured: no email addreses can be added. " + - "You will be unable to reset your password.", - )} -
    ; + let noIsText = null; + if (this.props.serverRequiresIdServer && !haveIs) { + noIsText =
    + {_t( + "No Identity Server is configured: no email addreses can be added. " + + "You will be unable to reset your password.", + )} +
    ; + } return (
    From 99fa100b92bd660e0e9c6cf11f9161a17f301686 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 28 Oct 2019 14:23:11 +0000 Subject: [PATCH 0396/2372] Tweak identity server warning text --- src/components/views/auth/RegistrationForm.js | 6 +++--- src/i18n/strings/en_EN.json | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 1ea66176ff..03fb74462c 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -99,7 +99,7 @@ module.exports = createReactClass({ let desc; if (this.props.serverRequiresIdServer && !haveIs) { desc = _t( - "No Identity Server is configured so you cannot add add an email address in order to " + + "No identity server is configured so you cannot add an email address in order to " + "reset your password in the future.", ); } else { @@ -602,8 +602,8 @@ module.exports = createReactClass({ if (this.props.serverRequiresIdServer && !haveIs) { noIsText =
    {_t( - "No Identity Server is configured: no email addreses can be added. " + - "You will be unable to reset your password.", + "No identity server is configured so you cannot add an email address in order to " + + "reset your password in the future.", )}
    ; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1cb773dcc5..31602d2dab 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1522,8 +1522,8 @@ "Phone": "Phone", "Not sure of your password? Set a new one": "Not sure of your password? Set a new one", "Sign in with": "Sign in with", + "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "No identity server is configured so you cannot add an email address in order to reset your password in the future.", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?", - "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.": "No Identity Server is configured so you cannot add add an email address in order to reset your password in the future.", "Use an email address to recover your account": "Use an email address to recover your account", "Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)", "Doesn't look like a valid email address": "Doesn't look like a valid email address", @@ -1544,7 +1544,6 @@ "Create your Matrix account on ": "Create your Matrix account on ", "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.", "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.", - "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.": "No Identity Server is configured: no email addreses can be added. You will be unable to reset your password.", "Enter your custom homeserver URL What does this mean?": "Enter your custom homeserver URL What does this mean?", "Homeserver URL": "Homeserver URL", "Enter your custom identity server URL What does this mean?": "Enter your custom identity server URL What does this mean?", From 085a309f5e83dbfc72fdc5c1bac68745cb58a798 Mon Sep 17 00:00:00 2001 From: Tuomas Hietala Date: Mon, 28 Oct 2019 14:26:34 +0000 Subject: [PATCH 0397/2372] Translated using Weblate (Finnish) Currently translated at 97.9% (1810 of 1849 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fi/ --- src/i18n/strings/fi.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 757f9a0337..406ddf1b47 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -2108,5 +2108,10 @@ "%(creator)s created and configured the room.": "%(creator)s loi ja määritti huoneen.", "Jump to first unread room.": "Siirry ensimmäiseen lukemattomaan huoneeseen.", "Jump to first invite.": "Siirry ensimmäiseen kutsuun.", - "DuckDuckGo Results": "DuckDuckGo-tulokset" + "DuckDuckGo Results": "DuckDuckGo-tulokset", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "Tekstiviesti on lähetetty numeroon +%(msisdn)s. Syötä siinä oleva varmistuskoodi.", + "Failed to deactivate user": "Käyttäjän deaktivointi epäonnistui", + "Hide advanced": "Piilota edistyneet", + "Show advanced": "Näytä edistyneet", + "Document": "Asiakirja" } From e3a55508d90a00e9ce3ce8bf4d1f9e261025722b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 28 Oct 2019 15:37:22 +0000 Subject: [PATCH 0398/2372] Remove unneeded help about identity servers The custom server path no longer shows an identity server field (for modern homeservers), so it's confusing to have the help dialog reference it. Part of https://github.com/vector-im/riot-web/issues/11236 --- src/components/views/auth/CustomServerDialog.js | 6 +----- src/i18n/strings/en_EN.json | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/views/auth/CustomServerDialog.js b/src/components/views/auth/CustomServerDialog.js index ae1054e0d9..a9a3a53f02 100644 --- a/src/components/views/auth/CustomServerDialog.js +++ b/src/components/views/auth/CustomServerDialog.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -34,11 +35,6 @@ module.exports = createReactClass({ "allows you to use this app with an existing Matrix account on a " + "different homeserver.", )}

    -

    {_t( - "You can also set a custom identity server, but you won't be " + - "able to invite users by email address, or be invited by email " + - "address yourself.", - )}

    - + - { sender.name } + { senderProfile ? senderProfile.name : sender } { formatFullDate(new Date(this.props.mxEvent.getTs())) } From 425ecc2fbb3c5c012a9143de1f27fa3027b31d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Thu, 31 Oct 2019 11:25:44 +0000 Subject: [PATCH 0429/2372] Translated using Weblate (French) Currently translated at 100.0% (1850 of 1850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 0e8f00e9c4..20f88e4d08 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2276,5 +2276,6 @@ "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Aucun serveur d’identité n’est configuré donc vous ne pouvez pas ajouter une adresse e-mail afin de réinitialiser votre mot de passe dans l’avenir.", "%(count)s unread messages including mentions.|one": "1 mention non lue.", "%(count)s unread messages.|one": "1 message non lu.", - "Unread messages.": "Messages non lus." + "Unread messages.": "Messages non lus.", + "Show tray icon and minimize window to it on close": "Afficher l’icône dans la barre d’état et minimiser la fenêtre lors de la fermeture" } From 4ed253b33d2de52e31c061eb2d8d4cb59fbd892d Mon Sep 17 00:00:00 2001 From: Szimszon Date: Wed, 30 Oct 2019 22:08:00 +0000 Subject: [PATCH 0430/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1850 of 1850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index dc8c42b109..d901ff4d85 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2263,5 +2263,6 @@ "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Azonosítási szerver nincs beállítva, így nem tudsz hozzáadni e-mail címet amivel vissza lehetne állítani a jelszót a későbbiekben.", "%(count)s unread messages including mentions.|one": "1 olvasatlan megemlítés.", "%(count)s unread messages.|one": "1 olvasatlan üzenet.", - "Unread messages.": "Olvasatlan üzenetek." + "Unread messages.": "Olvasatlan üzenetek.", + "Show tray icon and minimize window to it on close": "Tálcaikon mutatása és az ablak összecsukása bezáráskor" } From 3a1b065da2e0786e3904dd6e1bc6b6fe563ff071 Mon Sep 17 00:00:00 2001 From: Tim Stahel Date: Thu, 31 Oct 2019 09:49:30 +0000 Subject: [PATCH 0431/2372] Translated using Weblate (Swedish) Currently translated at 77.7% (1437 of 1850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sv/ --- src/i18n/strings/sv.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index f322affd1b..5c98ea50ba 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -1812,5 +1812,7 @@ "Connect this device to Key Backup": "Anslut den här enheten till nyckelsäkerhetskopiering", "Backing up %(sessionsRemaining)s keys...": "Säkerhetskopierar %(sessionsRemaining)s nycklar...", "All keys backed up": "Alla nycklar säkerhetskopierade", - "Backup has a signature from unknown device with ID %(deviceId)s.": "Säkerhetskopian har en signatur från okänd enhet med ID %(deviceId)s." + "Backup has a signature from unknown device with ID %(deviceId)s.": "Säkerhetskopian har en signatur från okänd enhet med ID %(deviceId)s.", + "Add Email Address": "Lägg till e-postadress", + "Add Phone Number": "Lägg till telefonnummer" } From 0fc51088175db7092e948d69c0c2f0e88eb5df0f Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 31 Oct 2019 11:58:31 +0000 Subject: [PATCH 0432/2372] Add a prompt when interacting with an identity server without terms This adds a prompt whenever we are about to perform some action on a default identity server (from homeserver .well-known or Riot app config) without terms. This allows the user to abort or trust the server (storing it in account data). Fixes https://github.com/vector-im/riot-web/issues/10557 --- src/IdentityAuthClient.js | 54 +++++++++++++++++++- src/components/views/settings/SetIdServer.js | 18 ++----- src/i18n/strings/en_EN.json | 6 ++- src/utils/IdentityServerUtils.js | 22 ++++++++ 4 files changed, 81 insertions(+), 19 deletions(-) diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 7cbad074bf..563e5e0441 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -17,7 +17,18 @@ limitations under the License. import { createClient, SERVICE_TYPES } from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; +import Modal from './Modal'; +import sdk from './index'; +import { _t } from './languageHandler'; import { Service, startTermsFlow, TermsNotSignedError } from './Terms'; +import { + doesAccountDataHaveIdentityServer, + doesIdentityServerHaveTerms, + useDefaultIdentityServer, +} from './utils/IdentityServerUtils'; +import { abbreviateUrl } from './utils/UrlUtils'; + +export class AbortedIdentityActionError extends Error {} export default class IdentityAuthClient { /** @@ -89,7 +100,10 @@ export default class IdentityAuthClient { try { await this._checkToken(token); } catch (e) { - if (e instanceof TermsNotSignedError) { + if ( + e instanceof TermsNotSignedError || + e instanceof AbortedIdentityActionError + ) { // Retrying won't help this throw e; } @@ -106,6 +120,8 @@ export default class IdentityAuthClient { } async _checkToken(token) { + const identityServerUrl = this._matrixClient.getIdentityServerUrl(); + try { await this._matrixClient.getIdentityAccount(token); } catch (e) { @@ -113,7 +129,7 @@ export default class IdentityAuthClient { console.log("Identity Server requires new terms to be agreed to"); await startTermsFlow([new Service( SERVICE_TYPES.IS, - this._matrixClient.getIdentityServerUrl(), + identityServerUrl, token, )]); return; @@ -121,6 +137,40 @@ export default class IdentityAuthClient { throw e; } + if ( + !this.tempClient && + !doesAccountDataHaveIdentityServer() && + !await doesIdentityServerHaveTerms(identityServerUrl) + ) { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '', + QuestionDialog, { + title: _t("Identity server has no terms of service"), + description:
    +

    {_t( + "This action requires accessing the default identity server " + + " to validate an email address or phone number, but the server " + + "does not have any terms of service.", {}, + { + server: () => {abbreviateUrl(identityServerUrl)}, + }, + )}

    +

    {_t( + "Only continue if you trust the owner of the server.", + )}

    +
    , + button: _t("Trust"), + }); + const [confirmed] = await finished; + if (confirmed) { + useDefaultIdentityServer(); + } else { + throw new AbortedIdentityActionError( + "User aborted identity server action without terms", + ); + } + } + // We should ensure the token in `localStorage` is cleared // appropriately. We already clear storage on sign out, but we'll need // additional clearing when changing ISes in settings as part of future diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 7f4a50d391..126cdc9557 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -24,9 +24,8 @@ import Modal from '../../../Modal'; import dis from "../../../dispatcher"; import { getThreepidsWithBindStatus } from '../../../boundThreepids'; import IdentityAuthClient from "../../../IdentityAuthClient"; -import {SERVICE_TYPES} from "matrix-js-sdk"; import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils"; -import { getDefaultIdentityServerUrl } from '../../../utils/IdentityServerUtils'; +import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils'; // We'll wait up to this long when checking for 3PID bindings on the IS. const REACHABILITY_TIMEOUT = 10000; // ms @@ -162,19 +161,8 @@ export default class SetIdServer extends React.Component { let save = true; // Double check that the identity server even has terms of service. - let terms; - try { - terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl); - } catch (e) { - console.error(e); - if (e.cors === "rejected" || e.httpStatus === 404) { - terms = null; - } else { - throw e; - } - } - - if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) { + const hasTerms = await doesIdentityServerHaveTerms(fullUrl); + if (!hasTerms) { const [confirmed] = await this._showNoTermsWarning(fullUrl); save = confirmed; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 31602d2dab..69f68b2497 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -99,6 +99,10 @@ "Failed to invite users to %(groupId)s": "Failed to invite users to %(groupId)s", "Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:", "Unnamed Room": "Unnamed Room", + "Identity server has no terms of service": "Identity server has no terms of service", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.", + "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.", + "Trust": "Trust", "Error": "Error", "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", "Dismiss": "Dismiss", @@ -563,9 +567,7 @@ "Change identity server": "Change identity server", "Disconnect from the identity server and connect to instead?": "Disconnect from the identity server and connect to instead?", "Terms of service not accepted or the identity server is invalid.": "Terms of service not accepted or the identity server is invalid.", - "Identity server has no terms of service": "Identity server has no terms of service", "The identity server you have chosen does not have any terms of service.": "The identity server you have chosen does not have any terms of service.", - "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.", "Disconnect identity server": "Disconnect identity server", "Disconnect from the identity server ?": "Disconnect from the identity server ?", "Disconnect": "Disconnect", diff --git a/src/utils/IdentityServerUtils.js b/src/utils/IdentityServerUtils.js index 883bd52149..cf180e3026 100644 --- a/src/utils/IdentityServerUtils.js +++ b/src/utils/IdentityServerUtils.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { SERVICE_TYPES } from 'matrix-js-sdk'; import SdkConfig from '../SdkConfig'; import MatrixClientPeg from '../MatrixClientPeg'; @@ -28,3 +29,24 @@ export function useDefaultIdentityServer() { base_url: url, }); } + +export async function doesIdentityServerHaveTerms(fullUrl) { + let terms; + try { + terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl); + } catch (e) { + console.error(e); + if (e.cors === "rejected" || e.httpStatus === 404) { + terms = null; + } else { + throw e; + } + } + + return terms && terms["policies"] && (Object.keys(terms["policies"]).length > 0); +} + +export function doesAccountDataHaveIdentityServer() { + const event = MatrixClientPeg.get().getAccountData("m.identity_server"); + return event && event.getContent() && event.getContent()['base_url']; +} From b38c7fc41131a5d47d0a261c3b76fbeba4d6ec5a Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 31 Oct 2019 14:50:21 +0000 Subject: [PATCH 0433/2372] Guard against misconfigured homeservers when adding / binding phone numbers This ensures we only fallback to submitting MSISDN tokens to the identity server when we expect to do. Unexpected cases will now throw an error. Fixes https://github.com/vector-im/riot-web/issues/10936 --- src/AddThreepid.js | 8 ++++++-- .../views/auth/InteractiveAuthEntryComponents.js | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/AddThreepid.js b/src/AddThreepid.js index 548ed4ce48..694c2e124c 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -236,6 +236,8 @@ export default class AddThreepid { */ async haveMsisdnToken(msisdnToken) { const authClient = new IdentityAuthClient(); + const supportsSeparateAddAndBind = + await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind(); let result; if (this.submitUrl) { @@ -245,19 +247,21 @@ export default class AddThreepid { this.clientSecret, msisdnToken, ); - } else { + } else if (this.bind || !supportsSeparateAddAndBind) { result = await MatrixClientPeg.get().submitMsisdnToken( this.sessionId, this.clientSecret, msisdnToken, await authClient.getAccessToken(), ); + } else { + throw new Error("The add / bind with MSISDN flow is misconfigured"); } if (result.errcode) { throw result; } - if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { + if (supportsSeparateAddAndBind) { if (this.bind) { await MatrixClientPeg.get().bindThreePid({ sid: this.sessionId, diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index 3eb6c111e0..d19ce95b33 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -475,22 +475,26 @@ export const MsisdnAuthEntry = createReactClass({ }); try { + const requiresIdServerParam = + await this.props.matrixClient.doesServerRequireIdServerParam(); let result; if (this._submitUrl) { result = await this.props.matrixClient.submitMsisdnTokenOtherUrl( this._submitUrl, this._sid, this.props.clientSecret, this.state.token, ); - } else { + } else if (requiresIdServerParam) { result = await this.props.matrixClient.submitMsisdnToken( this._sid, this.props.clientSecret, this.state.token, ); + } else { + throw new Error("The registration with MSISDN flow is misconfigured"); } if (result.success) { const creds = { sid: this._sid, client_secret: this.props.clientSecret, }; - if (await this.props.matrixClient.doesServerRequireIdServerParam()) { + if (requiresIdServerParam) { const idServerParsedUrl = url.parse( this.props.matrixClient.getIdentityServerUrl(), ); From 3f5b7b3b92e8a2e5bac4f9bfd9e9863bb8bba01a Mon Sep 17 00:00:00 2001 From: random Date: Thu, 31 Oct 2019 14:26:14 +0000 Subject: [PATCH 0434/2372] Translated using Weblate (Italian) Currently translated at 100.0% (1850 of 1850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index f1f66b44d2..6262315012 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2223,5 +2223,6 @@ "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Nessun server di identità configurato, perciò non puoi aggiungere un indirizzo email per ripristinare la tua password in futuro.", "%(count)s unread messages including mentions.|one": "1 citazione non letta.", "%(count)s unread messages.|one": "1 messaggio non letto.", - "Unread messages.": "Messaggi non letti." + "Unread messages.": "Messaggi non letti.", + "Show tray icon and minimize window to it on close": "Mostra icona in tray e usala alla chiusura della finestra" } From 23383419e803f6916c6636de10865b386a240f73 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 13:19:54 -0600 Subject: [PATCH 0435/2372] Add settings base for Mjolnir rules --- .../views/dialogs/_UserSettingsDialog.scss | 4 + .../views/dialogs/UserSettingsDialog.js | 30 ++++++++ .../tabs/user/MjolnirUserSettingsTab.js | 74 +++++++++++++++++++ src/i18n/strings/en_EN.json | 11 ++- src/settings/Settings.js | 14 ++++ 5 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js diff --git a/res/css/views/dialogs/_UserSettingsDialog.scss b/res/css/views/dialogs/_UserSettingsDialog.scss index 2a046ff501..4d831d7858 100644 --- a/res/css/views/dialogs/_UserSettingsDialog.scss +++ b/res/css/views/dialogs/_UserSettingsDialog.scss @@ -45,6 +45,10 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/flag.svg'); } +.mx_UserSettingsDialog_mjolnirIcon::before { + mask-image: url('$(res)/img/feather-customised/face.svg'); +} + .mx_UserSettingsDialog_flairIcon::before { mask-image: url('$(res)/img/feather-customised/flair.svg'); } diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index fb9045f05a..6e324ad3fb 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -29,12 +30,34 @@ import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab"; import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab"; import sdk from "../../../index"; import SdkConfig from "../../../SdkConfig"; +import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab"; export default class UserSettingsDialog extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, }; + constructor() { + super(); + + this.state = { + mjolnirEnabled: SettingsStore.isFeatureEnabled("feature_mjolnir"), + } + } + + componentDidMount(): void { + this._mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this._mjolnirChanged.bind(this)); + } + + componentWillUnmount(): void { + SettingsStore.unwatchSetting(this._mjolnirWatcher); + } + + _mjolnirChanged(settingName, roomId, atLevel, newValue) { + // We can cheat because we know what levels a feature is tracked at, and how it is tracked + this.setState({mjolnirEnabled: newValue}); + } + _getTabs() { const tabs = []; @@ -75,6 +98,13 @@ export default class UserSettingsDialog extends React.Component { , )); } + if (this.state.mjolnirEnabled) { + tabs.push(new Tab( + _td("Ignored users"), + "mx_UserSettingsDialog_mjolnirIcon", + , + )); + } tabs.push(new Tab( _td("Help & About"), "mx_UserSettingsDialog_helpIcon", diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js new file mode 100644 index 0000000000..02e64c0bc1 --- /dev/null +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -0,0 +1,74 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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"; +const sdk = require("../../../../.."); + +export default class MjolnirUserSettingsTab extends React.Component { + constructor() { + super(); + } + + render() { + return ( +
    +
    {_t("Ignored users")}
    +
    +
    + {_t("⚠ These settings are meant for advanced users.")}
    +
    + {_t( + "Add users and servers you want to ignore here. Use asterisks " + + "to have Riot match any characters. For example, @bot:* " + + "would ignore all users that have the name 'bot' on any server.", + {}, {code: (s) => {s}}, + )}
    +
    + {_t( + "Ignoring people is done through ban lists which contain rules for " + + "who to ban. Subscribing to a ban list means the users/servers blocked by " + + "that list will be hidden from you." + )} +
    +
    +
    + {_t("Personal ban list")} +
    + {_t( + "Your personal ban list holds all the users/servers you personally don't " + + "want to see messages from. After ignoring your first user/server, a new room " + + "will show up in your room list named 'My Ban List' - stay in this room to keep " + + "the ban list in effect.", + )} +
    +

    TODO

    +
    +
    + {_t("Subscribed lists")} +
    + {_t("Subscribing to a ban list will cause you to join it!")} +   + {_t( + "If this isn't what you want, please use a different tool to ignore users.", + )} +
    +

    TODO

    +
    +
    + ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 524a8a1abf..e909f49159 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -335,6 +335,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Use the new, consistent UserInfo panel for Room Members and Group Members": "Use the new, consistent UserInfo panel for Room Members and Group Members", + "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", @@ -637,6 +638,15 @@ "Access Token:": "Access Token:", "click to reveal": "click to reveal", "Labs": "Labs", + "Ignored users": "Ignored users", + "⚠ These settings are meant for advanced users.": "⚠ These settings are meant for advanced users.", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.", + "Personal ban list": "Personal ban list", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.", + "Subscribed lists": "Subscribed lists", + "Subscribing to a ban list will cause you to join it!": "Subscribing to a ban list will cause you to join it!", + "If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.", "Notifications": "Notifications", "Start automatically after system login": "Start automatically after system login", "Always show the window menu bar": "Always show the window menu bar", @@ -654,7 +664,6 @@ "Cryptography": "Cryptography", "Device ID:": "Device ID:", "Device key:": "Device key:", - "Ignored users": "Ignored users", "Bulk options": "Bulk options", "Accept all %(invitedRooms)s invites": "Accept all %(invitedRooms)s invites", "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 7470641359..1cfff0182e 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -126,6 +126,20 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_mjolnir": { + isFeature: true, + displayName: _td("Try out new ways to ignore people (experimental)"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, + "mjolnirRooms": { + supportedLevels: ['account'], + default: [], + }, + "mjolnirPersonalRoom": { + supportedLevels: ['account'], + default: null, + }, "useCiderComposer": { displayName: _td("Use the new, faster, composer for writing messages"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, From e6e12df82d1e801019f3ea993b35ae0b2b61f04c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 13:20:08 -0600 Subject: [PATCH 0436/2372] Add structural base for handling Mjolnir lists --- package.json | 1 + src/i18n/strings/en_EN.json | 2 + src/mjolnir/BanList.js | 98 +++++++++++++++++++++++++++++ src/mjolnir/ListRule.js | 63 +++++++++++++++++++ src/mjolnir/Mjolnir.js | 122 ++++++++++++++++++++++++++++++++++++ src/utils/MatrixGlob.js | 54 ++++++++++++++++ yarn.lock | 5 ++ 7 files changed, 345 insertions(+) create mode 100644 src/mjolnir/BanList.js create mode 100644 src/mjolnir/ListRule.js create mode 100644 src/mjolnir/Mjolnir.js create mode 100644 src/utils/MatrixGlob.js diff --git a/package.json b/package.json index e709662020..745f82d7bc 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566", "gfm.css": "^1.1.1", "glob": "^5.0.14", + "glob-to-regexp": "^0.4.1", "highlight.js": "^9.15.8", "is-ip": "^2.0.0", "isomorphic-fetch": "^2.2.1", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e909f49159..770f4723ef 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -389,6 +389,8 @@ "Call invitation": "Call invitation", "Messages sent by bot": "Messages sent by bot", "When rooms are upgraded": "When rooms are upgraded", + "My Ban List": "My Ban List", + "This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!", "Active call (%(roomName)s)": "Active call (%(roomName)s)", "unknown caller": "unknown caller", "Incoming voice call from %(name)s": "Incoming voice call from %(name)s", diff --git a/src/mjolnir/BanList.js b/src/mjolnir/BanList.js new file mode 100644 index 0000000000..6ebc0a7e36 --- /dev/null +++ b/src/mjolnir/BanList.js @@ -0,0 +1,98 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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. +*/ + +// Inspiration largely taken from Mjolnir itself + +import {ListRule, RECOMMENDATION_BAN, recommendationToStable} from "./ListRule"; +import MatrixClientPeg from "../MatrixClientPeg"; + +export const RULE_USER = "m.room.rule.user"; +export const RULE_ROOM = "m.room.rule.room"; +export const RULE_SERVER = "m.room.rule.server"; + +export const USER_RULE_TYPES = [RULE_USER, "org.matrix.mjolnir.rule.user"]; +export const ROOM_RULE_TYPES = [RULE_ROOM, "org.matrix.mjolnir.rule.room"]; +export const SERVER_RULE_TYPES = [RULE_SERVER, "org.matrix.mjolnir.rule.server"]; +export const ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES]; + +export function ruleTypeToStable(rule: string, unstable = true): string { + if (USER_RULE_TYPES.includes(rule)) return unstable ? USER_RULE_TYPES[USER_RULE_TYPES.length - 1] : RULE_USER; + if (ROOM_RULE_TYPES.includes(rule)) return unstable ? ROOM_RULE_TYPES[ROOM_RULE_TYPES.length - 1] : RULE_ROOM; + if (SERVER_RULE_TYPES.includes(rule)) return unstable ? SERVER_RULE_TYPES[SERVER_RULE_TYPES.length - 1] : RULE_SERVER; + return null; +} + +export class BanList { + _rules: ListRule[] = []; + _roomId: string; + + constructor(roomId: string) { + this._roomId = roomId; + this.updateList(); + } + + get roomId(): string { + return this._roomId; + } + + get serverRules(): ListRule[] { + return this._rules.filter(r => r.kind === RULE_SERVER); + } + + get userRules(): ListRule[] { + return this._rules.filter(r => r.kind === RULE_USER); + } + + get roomRules(): ListRule[] { + return this._rules.filter(r => r.kind === RULE_ROOM); + } + + banEntity(kind: string, entity: string, reason: string): Promise { + return MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), { + entity: entity, + reason: reason, + recommendation: recommendationToStable(RECOMMENDATION_BAN, true), + }, "rule:" + entity); + } + + unbanEntity(kind: string, entity: string): Promise { + // Empty state event is effectively deleting it. + return MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), {}, "rule:" + entity); + } + + updateList() { + this._rules = []; + + const room = MatrixClientPeg.get().getRoom(this._roomId); + if (!room) return; + + for (const eventType of ALL_RULE_TYPES) { + const events = room.currentState.getStateEvents(eventType, undefined); + for (const ev of events) { + if (!ev['state_key']) continue; + + const kind = ruleTypeToStable(eventType, false); + + const entity = ev.getContent()['entity']; + const recommendation = ev.getContent()['recommendation']; + const reason = ev.getContent()['reason']; + if (!entity || !recommendation || !reason) continue; + + this._rules.push(new ListRule(entity, recommendation, reason, kind)); + } + } + } +} diff --git a/src/mjolnir/ListRule.js b/src/mjolnir/ListRule.js new file mode 100644 index 0000000000..d33248d24c --- /dev/null +++ b/src/mjolnir/ListRule.js @@ -0,0 +1,63 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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 {MatrixGlob} from "../utils/MatrixGlob"; + +// Inspiration largely taken from Mjolnir itself + +export const RECOMMENDATION_BAN = "m.ban"; +export const RECOMMENDATION_BAN_TYPES = [RECOMMENDATION_BAN, "org.matrix.mjolnir.ban"]; + +export function recommendationToStable(recommendation: string, unstable = true): string { + if (RECOMMENDATION_BAN_TYPES.includes(recommendation)) return unstable ? RECOMMENDATION_BAN_TYPES[RECOMMENDATION_BAN_TYPES.length - 1] : RECOMMENDATION_BAN; + return null; +} + +export class ListRule { + _glob: MatrixGlob; + _entity: string; + _action: string; + _reason: string; + _kind: string; + + constructor(entity: string, action: string, reason: string, kind: string) { + this._glob = new MatrixGlob(entity); + this._entity = entity; + this._action = recommendationToStable(action, false); + this._reason = reason; + this._kind = kind; + } + + get entity(): string { + return this._entity; + } + + get reason(): string { + return this._reason; + } + + get kind(): string { + return this._kind; + } + + get recommendation(): string { + return this._action; + } + + isMatch(entity: string): boolean { + return this._glob.test(entity); + } +} diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js new file mode 100644 index 0000000000..a12534592d --- /dev/null +++ b/src/mjolnir/Mjolnir.js @@ -0,0 +1,122 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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 MatrixClientPeg from "../MatrixClientPeg"; +import {ALL_RULE_TYPES, BanList} from "./BanList"; +import SettingsStore, {SettingLevel} from "../settings/SettingsStore"; +import {_t} from "../languageHandler"; + +// TODO: Move this and related files to the js-sdk or something once finalized. + +export class Mjolnir { + static _instance: Mjolnir = null; + + _lists: BanList[] = []; + _roomIds: string[] = []; + _mjolnirWatchRef = null; + + constructor() { + } + + start() { + this._updateLists(SettingsStore.getValue("mjolnirRooms")); + this._mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this._onListsChanged.bind(this)); + + MatrixClientPeg.get().on("RoomState.events", this._onEvent.bind(this)); + } + + stop() { + SettingsStore.unwatchSetting(this._mjolnirWatchRef); + MatrixClientPeg.get().removeListener("RoomState.events", this._onEvent.bind(this)); + } + + async getOrCreatePersonalList(): Promise { + let personalRoomId = SettingsStore.getValue("mjolnirPersonalRoom"); + if (!personalRoomId) { + const resp = await MatrixClientPeg.get().createRoom({ + name: _t("My Ban List"), + topic: _t("This is your list of users/servers you have blocked - don't leave the room!"), + preset: "private_chat" + }); + personalRoomId = resp['room_id']; + SettingsStore.setValue("mjolnirPersonalRoom", null, SettingLevel.ACCOUNT, personalRoomId); + SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, [personalRoomId, ...this._roomIds]); + } + if (!personalRoomId) { + throw new Error("Error finding a room ID to use"); + } + + let list = this._lists.find(b => b.roomId === personalRoomId); + if (!list) list = new BanList(personalRoomId); + // we don't append the list to the tracked rooms because it should already be there. + // we're just trying to get the caller some utility access to the list + + return list; + } + + _onEvent(event) { + if (!this._roomIds.includes(event.getRoomId())) return; + if (!ALL_RULE_TYPES.includes(event.getType())) return; + + this._updateLists(this._roomIds); + } + + _onListsChanged(settingName, roomId, atLevel, newValue) { + // We know that ban lists are only recorded at one level so we don't need to re-eval them + this._updateLists(newValue); + } + + _updateLists(listRoomIds: string[]) { + this._lists = []; + this._roomIds = listRoomIds || []; + if (!listRoomIds) return; + + for (const roomId of listRoomIds) { + // Creating the list updates it + this._lists.push(new BanList(roomId)); + } + } + + isServerBanned(serverName: string): boolean { + for (const list of this._lists) { + for (const rule of list.serverRules) { + if (rule.isMatch(serverName)) { + return true; + } + } + } + return false; + } + + isUserBanned(userId: string): boolean { + for (const list of this._lists) { + for (const rule of list.userRules) { + if (rule.isMatch(userId)) { + return true; + } + } + } + return false; + } + + static sharedInstance(): Mjolnir { + if (!Mjolnir._instance) { + Mjolnir._instance = new Mjolnir(); + } + return Mjolnir._instance; + } +} + diff --git a/src/utils/MatrixGlob.js b/src/utils/MatrixGlob.js new file mode 100644 index 0000000000..cf55040625 --- /dev/null +++ b/src/utils/MatrixGlob.js @@ -0,0 +1,54 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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 * as globToRegexp from "glob-to-regexp"; + +// Taken with permission from matrix-bot-sdk: +// https://github.com/turt2live/matrix-js-bot-sdk/blob/eb148c2ecec7bf3ade801d73deb43df042d55aef/src/MatrixGlob.ts + +/** + * Represents a common Matrix glob. This is commonly used + * for server ACLs and similar functions. + */ +export class MatrixGlob { + _regex: RegExp; + + /** + * Creates a new Matrix Glob + * @param {string} glob The glob to convert. Eg: "*.example.org" + */ + constructor(glob: string) { + const globRegex = globToRegexp(glob, { + extended: false, + globstar: false, + }); + + // We need to convert `?` manually because globToRegexp's extended mode + // does more than we want it to. + const replaced = globRegex.toString().replace(/\\\?/g, "."); + this._regex = new RegExp(replaced.substring(1, replaced.length - 1)); + } + + /** + * Tests the glob against a value, returning true if it matches. + * @param {string} val The value to test. + * @returns {boolean} True if the value matches the glob, false otherwise. + */ + test(val: string): boolean { + return this._regex.test(val); + } + +} diff --git a/yarn.lock b/yarn.lock index aa0a06e588..a2effb975c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3674,6 +3674,11 @@ glob-to-regexp@^0.3.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + glob@7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" From e9c8a31e1f07031e1b315020d48bb97434f40f41 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 13:28:00 -0600 Subject: [PATCH 0437/2372] Start and stop Mjolnir with the lifecycle --- src/Lifecycle.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 13f3abccb1..f2b50d7f2d 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -36,6 +36,7 @@ import * as StorageManager from './utils/StorageManager'; import SettingsStore from "./settings/SettingsStore"; import TypingStore from "./stores/TypingStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; +import {Mjolnir} from "./mjolnir/Mjolnir"; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -585,6 +586,11 @@ async function startMatrixClient(startSyncing=true) { IntegrationManagers.sharedInstance().startWatching(); ActiveWidgetStore.start(); + // Start Mjolnir even though we haven't checked the feature flag yet. Starting + // the thing just wastes CPU cycles, but should result in no actual functionality + // being exposed to the user. + Mjolnir.sharedInstance().start(); + if (startSyncing) { await MatrixClientPeg.start(); } else { @@ -645,6 +651,7 @@ export function stopMatrixClient(unsetClient=true) { Presence.stop(); ActiveWidgetStore.stop(); IntegrationManagers.sharedInstance().stopWatching(); + Mjolnir.sharedInstance().stop(); if (DMRoomMap.shared()) DMRoomMap.shared().stop(); const cli = MatrixClientPeg.get(); if (cli) { From 7d6643664e334e4ff3c8576ccc273bc95b1890d0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 31 Oct 2019 19:42:41 +0000 Subject: [PATCH 0438/2372] Fix bug where rooms would not appear when filtering We need to reset the scroll offset otherwise the component may be scrolled past the only content it has (Chrome just corrected the scroll offset but Firefox scrolled it anyway). NB. Introducing the new deriveStateFromProps method seems to means that react no longer calls componentWillMount so I've had to change it to componentDidMount (which it should have been anyway). Fixes https://github.com/vector-im/riot-web/issues/11263 --- src/components/structures/RoomSubList.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 0bb5c9e9be..921680b678 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -67,6 +67,9 @@ const RoomSubList = createReactClass({ // some values to get LazyRenderList starting scrollerHeight: 800, scrollTop: 0, + // React 16's getDerivedStateFromProps(props, state) doesn't give the previous props so + // we have to store the length of the list here so we can see if it's changed or not... + listLength: null, }; }, @@ -79,11 +82,20 @@ const RoomSubList = createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._headerButton = createRef(); this.dispatcherRef = dis.register(this.onAction); }, + statics: { + getDerivedStateFromProps: function(props, state) { + return { + listLength: props.list.length, + scrollTop: props.list.length === state.listLength ? state.scrollTop : 0, + }; + }, + }, + componentWillUnmount: function() { dis.unregister(this.dispatcherRef); }, From b93508728a1e4abd3dd8fa411eb6760119bf6f7d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 14:24:51 -0600 Subject: [PATCH 0439/2372] Add personal list management to Mjolnir section --- res/css/_components.scss | 1 + .../tabs/user/_MjolnirUserSettingsTab.scss | 23 ++++ .../tabs/user/MjolnirUserSettingsTab.js | 117 +++++++++++++++++- src/i18n/strings/en_EN.json | 11 +- src/mjolnir/BanList.js | 16 ++- src/mjolnir/Mjolnir.js | 44 ++++++- src/utils/MatrixGlob.js | 2 +- 7 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 29c4d2c84c..a0e5881201 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -182,6 +182,7 @@ @import "./views/settings/tabs/room/_SecurityRoomSettingsTab.scss"; @import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss"; @import "./views/settings/tabs/user/_HelpUserSettingsTab.scss"; +@import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss"; @import "./views/settings/tabs/user/_NotificationUserSettingsTab.scss"; @import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss"; @import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss"; diff --git a/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss new file mode 100644 index 0000000000..930dbeb440 --- /dev/null +++ b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss @@ -0,0 +1,23 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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. +*/ + +.mx_MjolnirUserSettingsTab .mx_Field { + @mixin mx_Settings_fullWidthField; +} + +.mx_MjolnirUserSettingsTab_personalRule { + margin-bottom: 2px; +} diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js index 02e64c0bc1..97f92bb0b2 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -16,28 +16,115 @@ limitations under the License. import React from 'react'; import {_t} from "../../../../../languageHandler"; +import {Mjolnir} from "../../../../../mjolnir/Mjolnir"; +import {ListRule} from "../../../../../mjolnir/ListRule"; +import {RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList"; +import Modal from "../../../../../Modal"; + const sdk = require("../../../../.."); export default class MjolnirUserSettingsTab extends React.Component { constructor() { super(); + + this.state = { + busy: false, + newPersonalRule: "", + }; + } + + _onPersonalRuleChanged = (e) => { + this.setState({newPersonalRule: e.target.value}); + }; + + _onAddPersonalRule = async (e) => { + e.preventDefault(); + e.stopPropagation(); + + let kind = RULE_SERVER; + if (this.state.newPersonalRule.startsWith("@")) { + kind = RULE_USER; + } + + this.setState({busy: true}); + try { + const list = await Mjolnir.sharedInstance().getOrCreatePersonalList(); + await list.banEntity(kind, this.state.newPersonalRule, _t("Ignored/Blocked")); + this.setState({newPersonalRule: ""}); // this will also cause the new rule to be rendered + } catch (e) { + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, { + title: _t('Error removing ignored user/server'), + description: _t('Something went wrong. Please try again or view your console for hints.'), + }); + } finally { + this.setState({busy: false}); + } + }; + + async _removePersonalRule(rule: ListRule) { + this.setState({busy: true}); + try { + const list = Mjolnir.sharedInstance().getPersonalList(); + await list.unbanEntity(rule.kind, rule.entity); + } catch (e) { + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, { + title: _t('Error removing ignored user/server'), + description: _t('Something went wrong. Please try again or view your console for hints.'), + }); + } finally { + this.setState({busy: false}); + } + } + + _renderPersonalBanListRules() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + const list = Mjolnir.sharedInstance().getPersonalList(); + const rules = list ? [...list.userRules, ...list.serverRules] : []; + if (!list || rules.length <= 0) return {_t("You have not ignored anyone.")}; + + const tiles = []; + for (const rule of rules) { + tiles.push( +
  • + this._removePersonalRule(rule)} + disabled={this.state.busy}> + {_t("Remove")} +   + {rule.entity} +
  • , + ); + } + + return

    {_t("You are currently ignoring:")}

    +
      {tiles}
    +
    ; } render() { + const Field = sdk.getComponent('elements.Field'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return ( -
    +
    {_t("Ignored users")}
    - {_t("⚠ These settings are meant for advanced users.")}
    -
    + {_t("⚠ These settings are meant for advanced users.")}
    +
    {_t( "Add users and servers you want to ignore here. Use asterisks " + "to have Riot match any characters. For example, @bot:* " + "would ignore all users that have the name 'bot' on any server.", {}, {code: (s) => {s}}, - )}
    -
    + )}
    +
    {_t( "Ignoring people is done through ban lists which contain rules for " + "who to ban. Subscribing to a ban list means the users/servers blocked by " + @@ -55,7 +142,25 @@ export default class MjolnirUserSettingsTab extends React.Component { "the ban list in effect.", )}
    -

    TODO

    +
    + {this._renderPersonalBanListRules()} +
    +
    +
    + + + {_t("Ignore")} + + +
    {_t("Subscribed lists")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 770f4723ef..fa15433a1a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -640,12 +640,21 @@ "Access Token:": "Access Token:", "click to reveal": "click to reveal", "Labs": "Labs", + "Ignored/Blocked": "Ignored/Blocked", + "Error removing ignored user/server": "Error removing ignored user/server", + "Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.", + "You have not ignored anyone.": "You have not ignored anyone.", + "Remove": "Remove", + "You are currently ignoring:": "You are currently ignoring:", "Ignored users": "Ignored users", "⚠ These settings are meant for advanced users.": "⚠ These settings are meant for advanced users.", "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.", "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.", "Personal ban list": "Personal ban list", "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.", + "Server or user ID to ignore": "Server or user ID to ignore", + "eg: @bot:* or example.org": "eg: @bot:* or example.org", + "Ignore": "Ignore", "Subscribed lists": "Subscribed lists", "Subscribing to a ban list will cause you to join it!": "Subscribing to a ban list will cause you to join it!", "If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.", @@ -776,7 +785,6 @@ "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", "Unable to remove contact information": "Unable to remove contact information", "Remove %(email)s?": "Remove %(email)s?", - "Remove": "Remove", "Invalid Email Address": "Invalid Email Address", "This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address", "Unable to add email address": "Unable to add email address", @@ -843,7 +851,6 @@ "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", "Are you sure?": "Are you sure?", "No devices with registered encryption keys": "No devices with registered encryption keys", - "Ignore": "Ignore", "Jump to read receipt": "Jump to read receipt", "Mention": "Mention", "Invite": "Invite", diff --git a/src/mjolnir/BanList.js b/src/mjolnir/BanList.js index 6ebc0a7e36..026005420a 100644 --- a/src/mjolnir/BanList.js +++ b/src/mjolnir/BanList.js @@ -60,17 +60,23 @@ export class BanList { return this._rules.filter(r => r.kind === RULE_ROOM); } - banEntity(kind: string, entity: string, reason: string): Promise { - return MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), { + async banEntity(kind: string, entity: string, reason: string): Promise { + await MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), { entity: entity, reason: reason, recommendation: recommendationToStable(RECOMMENDATION_BAN, true), }, "rule:" + entity); + this._rules.push(new ListRule(entity, RECOMMENDATION_BAN, reason, ruleTypeToStable(kind, false))); } - unbanEntity(kind: string, entity: string): Promise { + async unbanEntity(kind: string, entity: string): Promise { // Empty state event is effectively deleting it. - return MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), {}, "rule:" + entity); + await MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), {}, "rule:" + entity); + this._rules = this._rules.filter(r => { + if (r.kind !== ruleTypeToStable(kind, false)) return true; + if (r.entity !== entity) return true; + return false; // we just deleted this rule + }); } updateList() { @@ -82,7 +88,7 @@ export class BanList { for (const eventType of ALL_RULE_TYPES) { const events = room.currentState.getStateEvents(eventType, undefined); for (const ev of events) { - if (!ev['state_key']) continue; + if (!ev.getStateKey()) continue; const kind = ruleTypeToStable(eventType, false); diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js index a12534592d..d90ea9cd04 100644 --- a/src/mjolnir/Mjolnir.js +++ b/src/mjolnir/Mjolnir.js @@ -18,6 +18,7 @@ import MatrixClientPeg from "../MatrixClientPeg"; import {ALL_RULE_TYPES, BanList} from "./BanList"; import SettingsStore, {SettingLevel} from "../settings/SettingsStore"; import {_t} from "../languageHandler"; +import dis from "../dispatcher"; // TODO: Move this and related files to the js-sdk or something once finalized. @@ -27,19 +28,39 @@ export class Mjolnir { _lists: BanList[] = []; _roomIds: string[] = []; _mjolnirWatchRef = null; + _dispatcherRef = null; constructor() { } start() { - this._updateLists(SettingsStore.getValue("mjolnirRooms")); this._mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this._onListsChanged.bind(this)); + this._dispatcherRef = dis.register(this._onAction); + dis.dispatch({ + action: 'do_after_sync_prepared', + deferred_action: {action: 'setup_mjolnir'}, + }); + } + + _onAction = (payload) => { + if (payload['action'] === 'setup_mjolnir') { + console.log("Setting up Mjolnir: after sync"); + this.setup(); + } + }; + + setup() { + if (!MatrixClientPeg.get()) return; + this._updateLists(SettingsStore.getValue("mjolnirRooms")); MatrixClientPeg.get().on("RoomState.events", this._onEvent.bind(this)); } stop() { SettingsStore.unwatchSetting(this._mjolnirWatchRef); + dis.unregister(this._dispatcherRef); + + if (!MatrixClientPeg.get()) return; MatrixClientPeg.get().removeListener("RoomState.events", this._onEvent.bind(this)); } @@ -52,8 +73,8 @@ export class Mjolnir { preset: "private_chat" }); personalRoomId = resp['room_id']; - SettingsStore.setValue("mjolnirPersonalRoom", null, SettingLevel.ACCOUNT, personalRoomId); - SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, [personalRoomId, ...this._roomIds]); + await SettingsStore.setValue("mjolnirPersonalRoom", null, SettingLevel.ACCOUNT, personalRoomId); + await SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, [personalRoomId, ...this._roomIds]); } if (!personalRoomId) { throw new Error("Error finding a room ID to use"); @@ -67,7 +88,21 @@ export class Mjolnir { return list; } + // get without creating the list + getPersonalList(): BanList { + const personalRoomId = SettingsStore.getValue("mjolnirPersonalRoom"); + if (!personalRoomId) return null; + + let list = this._lists.find(b => b.roomId === personalRoomId); + if (!list) list = new BanList(personalRoomId); + // we don't append the list to the tracked rooms because it should already be there. + // we're just trying to get the caller some utility access to the list + + return list; + } + _onEvent(event) { + if (!MatrixClientPeg.get()) return; if (!this._roomIds.includes(event.getRoomId())) return; if (!ALL_RULE_TYPES.includes(event.getType())) return; @@ -80,6 +115,9 @@ export class Mjolnir { } _updateLists(listRoomIds: string[]) { + if (!MatrixClientPeg.get()) return; + + console.log("Updating Mjolnir ban lists to: " + listRoomIds); this._lists = []; this._roomIds = listRoomIds || []; if (!listRoomIds) return; diff --git a/src/utils/MatrixGlob.js b/src/utils/MatrixGlob.js index cf55040625..b18e20ecf4 100644 --- a/src/utils/MatrixGlob.js +++ b/src/utils/MatrixGlob.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as globToRegexp from "glob-to-regexp"; +import globToRegexp from "glob-to-regexp"; // Taken with permission from matrix-bot-sdk: // https://github.com/turt2live/matrix-js-bot-sdk/blob/eb148c2ecec7bf3ade801d73deb43df042d55aef/src/MatrixGlob.ts From 39b657ce7c4c3402802c836acce8d2c095c0bb9a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 15:53:18 -0600 Subject: [PATCH 0440/2372] Add basic structure for (un)subscribing from lists --- .../tabs/user/_MjolnirUserSettingsTab.scss | 2 +- .../tabs/user/MjolnirUserSettingsTab.js | 145 ++++++++++++++++-- src/i18n/strings/en_EN.json | 13 +- src/mjolnir/Mjolnir.js | 20 +++ 4 files changed, 166 insertions(+), 14 deletions(-) diff --git a/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss index 930dbeb440..c60cbc5dea 100644 --- a/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss @@ -18,6 +18,6 @@ limitations under the License. @mixin mx_Settings_fullWidthField; } -.mx_MjolnirUserSettingsTab_personalRule { +.mx_MjolnirUserSettingsTab_listItem { margin-bottom: 2px; } diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js index 97f92bb0b2..4e05b57567 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -18,8 +18,9 @@ import React from 'react'; import {_t} from "../../../../../languageHandler"; import {Mjolnir} from "../../../../../mjolnir/Mjolnir"; import {ListRule} from "../../../../../mjolnir/ListRule"; -import {RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList"; +import {BanList, RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList"; import Modal from "../../../../../Modal"; +import MatrixClientPeg from "../../../../../MatrixClientPeg"; const sdk = require("../../../../.."); @@ -30,6 +31,7 @@ export default class MjolnirUserSettingsTab extends React.Component { this.state = { busy: false, newPersonalRule: "", + newList: "", }; } @@ -37,6 +39,10 @@ export default class MjolnirUserSettingsTab extends React.Component { this.setState({newPersonalRule: e.target.value}); }; + _onNewListChanged = (e) => { + this.setState({newList: e.target.value}); + }; + _onAddPersonalRule = async (e) => { e.preventDefault(); e.stopPropagation(); @@ -55,8 +61,8 @@ export default class MjolnirUserSettingsTab extends React.Component { console.error(e); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, { - title: _t('Error removing ignored user/server'), + Modal.createTrackedDialog('Failed to add Mjolnir rule', '', ErrorDialog, { + title: _t('Error adding ignored user/server'), description: _t('Something went wrong. Please try again or view your console for hints.'), }); } finally { @@ -64,6 +70,28 @@ export default class MjolnirUserSettingsTab extends React.Component { } }; + _onSubscribeList = async (e) => { + e.preventDefault(); + e.stopPropagation(); + + this.setState({busy: true}); + try { + const room = await MatrixClientPeg.get().joinRoom(this.state.newList); + await Mjolnir.sharedInstance().subscribeToList(room.roomId); + this.setState({newList: ""}); // this will also cause the new rule to be rendered + } catch (e) { + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to subscribe to Mjolnir list', '', ErrorDialog, { + title: _t('Error subscribing to list'), + description: _t('Please verify the room ID or alias and try again.'), + }); + } finally { + this.setState({busy: false}); + } + }; + async _removePersonalRule(rule: ListRule) { this.setState({busy: true}); try { @@ -82,6 +110,28 @@ export default class MjolnirUserSettingsTab extends React.Component { } } + async _unsubscribeFromList(list: BanList) { + this.setState({busy: true}); + try { + await Mjolnir.sharedInstance().unsubscribeFromList(list.roomId); + await MatrixClientPeg.get().leave(list.roomId); + } catch (e) { + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to unsubscribe from Mjolnir list', '', ErrorDialog, { + title: _t('Error unsubscribing from list'), + description: _t('Please try again or view your console for hints.'), + }); + } finally { + this.setState({busy: false}); + } + } + + _viewListRules(list: BanList) { + // TODO + } + _renderPersonalBanListRules() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -92,9 +142,12 @@ export default class MjolnirUserSettingsTab extends React.Component { const tiles = []; for (const rule of rules) { tiles.push( -
  • - this._removePersonalRule(rule)} - disabled={this.state.busy}> +
  • + this._removePersonalRule(rule)} + disabled={this.state.busy} + > {_t("Remove")}   {rule.entity} @@ -102,9 +155,52 @@ export default class MjolnirUserSettingsTab extends React.Component { ); } - return

    {_t("You are currently ignoring:")}

    -
      {tiles}
    -
    ; + return ( +
    +

    {_t("You are currently ignoring:")}

    +
      {tiles}
    +
    + ); + } + + _renderSubscribedBanLists() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + const personalList = Mjolnir.sharedInstance().getPersonalList(); + const lists = Mjolnir.sharedInstance().lists.filter(b => personalList ? personalList.roomId !== b.roomId : true); + if (!lists || lists.length <= 0) return {_t("You are not subscribed to any lists")}; + + const tiles = []; + for (const list of lists) { + const room = MatrixClientPeg.get().getRoom(list.roomId); + const name = room ? {room.name} ({list.roomId}) : list.roomId; + tiles.push( +
  • + this._unsubscribeFromList(list)} + disabled={this.state.busy} + > + {_t("Unsubscribe")} +   + this._viewListRules(list)} + disabled={this.state.busy} + > + {_t("View rules")} +   + {name} +
  • , + ); + } + + return ( +
    +

    {_t("You are currently subscribed to:")}

    +
      {tiles}
    +
    + ); } render() { @@ -155,8 +251,12 @@ export default class MjolnirUserSettingsTab extends React.Component { value={this.state.newPersonalRule} onChange={this._onPersonalRuleChanged} /> - + {_t("Ignore")} @@ -171,7 +271,28 @@ export default class MjolnirUserSettingsTab extends React.Component { "If this isn't what you want, please use a different tool to ignore users.", )}
    -

    TODO

    +
    + {this._renderSubscribedBanLists()} +
    +
    +
    + + + {_t("Subscribe")} + + +
    ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fa15433a1a..561dbc4da9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -641,11 +641,20 @@ "click to reveal": "click to reveal", "Labs": "Labs", "Ignored/Blocked": "Ignored/Blocked", - "Error removing ignored user/server": "Error removing ignored user/server", + "Error adding ignored user/server": "Error adding ignored user/server", "Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.", + "Error subscribing to list": "Error subscribing to list", + "Please verify the room ID or alias and try again.": "Please verify the room ID or alias and try again.", + "Error removing ignored user/server": "Error removing ignored user/server", + "Error unsubscribing from list": "Error unsubscribing from list", + "Please try again or view your console for hints.": "Please try again or view your console for hints.", "You have not ignored anyone.": "You have not ignored anyone.", "Remove": "Remove", "You are currently ignoring:": "You are currently ignoring:", + "You are not subscribed to any lists": "You are not subscribed to any lists", + "Unsubscribe": "Unsubscribe", + "View rules": "View rules", + "You are currently subscribed to:": "You are currently subscribed to:", "Ignored users": "Ignored users", "⚠ These settings are meant for advanced users.": "⚠ These settings are meant for advanced users.", "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.", @@ -658,6 +667,8 @@ "Subscribed lists": "Subscribed lists", "Subscribing to a ban list will cause you to join it!": "Subscribing to a ban list will cause you to join it!", "If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.", + "Room ID or alias of ban list": "Room ID or alias of ban list", + "Subscribe": "Subscribe", "Notifications": "Notifications", "Start automatically after system login": "Start automatically after system login", "Always show the window menu bar": "Always show the window menu bar", diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js index d90ea9cd04..5edfe3750e 100644 --- a/src/mjolnir/Mjolnir.js +++ b/src/mjolnir/Mjolnir.js @@ -33,6 +33,14 @@ export class Mjolnir { constructor() { } + get roomIds(): string[] { + return this._roomIds; + } + + get lists(): BanList[] { + return this._lists; + } + start() { this._mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this._onListsChanged.bind(this)); @@ -101,6 +109,18 @@ export class Mjolnir { return list; } + async subscribeToList(roomId: string) { + const roomIds = [...this._roomIds, roomId]; + await SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, roomIds); + this._lists.push(new BanList(roomId)); + } + + async unsubscribeFromList(roomId: string) { + const roomIds = this._roomIds.filter(r => r !== roomId); + await SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, roomIds); + this._lists = this._lists.filter(b => b.roomId !== roomId); + } + _onEvent(event) { if (!MatrixClientPeg.get()) return; if (!this._roomIds.includes(event.getRoomId())) return; From b420fd675857d6c3e212caafa1c56d2ddc4a16da Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 16:00:31 -0600 Subject: [PATCH 0441/2372] Add a view rules dialog --- .../tabs/user/MjolnirUserSettingsTab.js | 29 ++++++++++++++++++- src/i18n/strings/en_EN.json | 6 +++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js index 4e05b57567..a02ca2c570 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -129,7 +129,34 @@ export default class MjolnirUserSettingsTab extends React.Component { } _viewListRules(list: BanList) { - // TODO + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + const room = MatrixClientPeg.get().getRoom(list.roomId); + const name = room ? room.name : list.roomId; + + const renderRules = (rules: ListRule[]) => { + if (rules.length === 0) return {_t("None")}; + + const tiles = []; + for (const rule of rules) { + tiles.push(
  • {rule.entity}
  • ); + } + return
      {tiles}
    ; + }; + + Modal.createTrackedDialog('View Mjolnir list rules', '', QuestionDialog, { + title: _t("Ban list rules - %(roomName)s", {roomName: name}), + description: ( +
    +

    {_t("Server rules")}

    + {renderRules(list.serverRules)} +

    {_t("User rules")}

    + {renderRules(list.userRules)} +
    + ), + button: _t("Close"), + hasCancelButton: false, + }); } _renderPersonalBanListRules() { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 561dbc4da9..58fa564250 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -648,6 +648,11 @@ "Error removing ignored user/server": "Error removing ignored user/server", "Error unsubscribing from list": "Error unsubscribing from list", "Please try again or view your console for hints.": "Please try again or view your console for hints.", + "None": "None", + "Ban list rules - %(roomName)s": "Ban list rules - %(roomName)s", + "Server rules": "Server rules", + "User rules": "User rules", + "Close": "Close", "You have not ignored anyone.": "You have not ignored anyone.", "Remove": "Remove", "You are currently ignoring:": "You are currently ignoring:", @@ -874,7 +879,6 @@ "Revoke Moderator": "Revoke Moderator", "Make Moderator": "Make Moderator", "Admin Tools": "Admin Tools", - "Close": "Close", "and %(count)s others...|other": "and %(count)s others...", "and %(count)s others...|one": "and one other...", "Invite to this room": "Invite to this room", From 11068d189cf03e309cccca75b83ee8674fb01796 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 16:19:42 -0600 Subject: [PATCH 0442/2372] Hide messages blocked by ban lists --- res/css/_components.scss | 1 + res/css/views/messages/_MjolnirBody.scss | 19 ++++++++ src/components/views/messages/MessageEvent.js | 24 +++++++++- src/components/views/messages/MjolnirBody.js | 47 +++++++++++++++++++ src/i18n/strings/en_EN.json | 1 + 5 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 res/css/views/messages/_MjolnirBody.scss create mode 100644 src/components/views/messages/MjolnirBody.js diff --git a/res/css/_components.scss b/res/css/_components.scss index a0e5881201..788e22a766 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -123,6 +123,7 @@ @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; +@import "./views/messages/_MjolnirBody.scss"; @import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRowButton.scss"; @import "./views/messages/_ReactionsRowButtonTooltip.scss"; diff --git a/res/css/views/messages/_MjolnirBody.scss b/res/css/views/messages/_MjolnirBody.scss new file mode 100644 index 0000000000..80be7429e5 --- /dev/null +++ b/res/css/views/messages/_MjolnirBody.scss @@ -0,0 +1,19 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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. +*/ + +.mx_MjolnirBody { + opacity: 0.4; +} diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index a616dd96ed..2e353794d7 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -18,6 +18,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import sdk from '../../../index'; +import SettingsStore from "../../../settings/SettingsStore"; +import {Mjolnir} from "../../../mjolnir/Mjolnir"; module.exports = createReactClass({ displayName: 'MessageEvent', @@ -49,6 +51,10 @@ module.exports = createReactClass({ return this.refs.body && this.refs.body.getEventTileOps ? this.refs.body.getEventTileOps() : null; }, + onTileUpdate: function() { + this.forceUpdate(); + }, + render: function() { const UnknownBody = sdk.getComponent('messages.UnknownBody'); @@ -81,6 +87,20 @@ module.exports = createReactClass({ } } + if (SettingsStore.isFeatureEnabled("feature_mjolnir")) { + const allowRender = localStorage.getItem(`mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`) === "true"; + + if (!allowRender) { + const userDomain = this.props.mxEvent.getSender().split(':').slice(1).join(':'); + const userBanned = Mjolnir.sharedInstance().isUserBanned(this.props.mxEvent.getSender()); + const serverBanned = Mjolnir.sharedInstance().isServerBanned(userDomain); + + if (userBanned || serverBanned) { + BodyType = sdk.getComponent('messages.MjolnirBody'); + } + } + } + return ; + onHeightChanged={this.props.onHeightChanged} + onTileUpdate={this.onTileUpdate} + />; }, }); diff --git a/src/components/views/messages/MjolnirBody.js b/src/components/views/messages/MjolnirBody.js new file mode 100644 index 0000000000..994642863b --- /dev/null +++ b/src/components/views/messages/MjolnirBody.js @@ -0,0 +1,47 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {_t} from '../../../languageHandler'; + +export default class MjolnirBody extends React.Component { + static propTypes = { + mxEvent: PropTypes.object.isRequired, + onTileUpdate: PropTypes.func.isRequired, + }; + + constructor() { + super(); + } + + _onAllowClick = (e) => { + e.preventDefault(); + e.stopPropagation(); + + localStorage.setItem(`mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`, "true"); + this.props.onTileUpdate(); + }; + + render() { + return ( +
    {_t( + "You have ignored this user, so their message is hidden. Show anyways.", + {}, {a: (sub) => {sub}}, + )}
    + ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 58fa564250..74433a9c04 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1094,6 +1094,7 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Show image": "Show image", + "You have ignored this user, so their message is hidden. Show anyways.": "You have ignored this user, so their message is hidden. Show anyways.", "Error decrypting video": "Error decrypting video", "Show all": "Show all", "reacted with %(shortName)s": "reacted with %(shortName)s", From 3e4a721111f6bb6a17e219ea97ead4dfe4589792 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 16:27:45 -0600 Subject: [PATCH 0443/2372] Appease the linter --- src/components/views/dialogs/UserSettingsDialog.js | 2 +- src/components/views/messages/MessageEvent.js | 3 ++- src/components/views/messages/MjolnirBody.js | 3 ++- .../settings/tabs/user/MjolnirUserSettingsTab.js | 14 ++++++++------ src/mjolnir/BanList.js | 12 +++++++++--- src/mjolnir/ListRule.js | 4 +++- src/mjolnir/Mjolnir.js | 8 +++++--- src/utils/MatrixGlob.js | 1 - 8 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index 6e324ad3fb..d3ab2b8722 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -42,7 +42,7 @@ export default class UserSettingsDialog extends React.Component { this.state = { mjolnirEnabled: SettingsStore.isFeatureEnabled("feature_mjolnir"), - } + }; } componentDidMount(): void { diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 2e353794d7..0d22658884 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -88,7 +88,8 @@ module.exports = createReactClass({ } if (SettingsStore.isFeatureEnabled("feature_mjolnir")) { - const allowRender = localStorage.getItem(`mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`) === "true"; + const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`; + const allowRender = localStorage.getItem(key) === "true"; if (!allowRender) { const userDomain = this.props.mxEvent.getSender().split(':').slice(1).join(':'); diff --git a/src/components/views/messages/MjolnirBody.js b/src/components/views/messages/MjolnirBody.js index 994642863b..d03c6c658d 100644 --- a/src/components/views/messages/MjolnirBody.js +++ b/src/components/views/messages/MjolnirBody.js @@ -32,7 +32,8 @@ export default class MjolnirBody extends React.Component { e.preventDefault(); e.stopPropagation(); - localStorage.setItem(`mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`, "true"); + const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`; + localStorage.setItem(key, "true"); this.props.onTileUpdate(); }; diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js index a02ca2c570..608be0b129 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -194,7 +194,9 @@ export default class MjolnirUserSettingsTab extends React.Component { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const personalList = Mjolnir.sharedInstance().getPersonalList(); - const lists = Mjolnir.sharedInstance().lists.filter(b => personalList ? personalList.roomId !== b.roomId : true); + const lists = Mjolnir.sharedInstance().lists.filter(b => { + return personalList? personalList.roomId !== b.roomId : true; + }); if (!lists || lists.length <= 0) return {_t("You are not subscribed to any lists")}; const tiles = []; @@ -239,19 +241,19 @@ export default class MjolnirUserSettingsTab extends React.Component {
    {_t("Ignored users")}
    - {_t("⚠ These settings are meant for advanced users.")}
    -
    + {_t("⚠ These settings are meant for advanced users.")}
    +
    {_t( "Add users and servers you want to ignore here. Use asterisks " + "to have Riot match any characters. For example, @bot:* " + "would ignore all users that have the name 'bot' on any server.", {}, {code: (s) => {s}}, - )}
    -
    + )}
    +
    {_t( "Ignoring people is done through ban lists which contain rules for " + "who to ban. Subscribing to a ban list means the users/servers blocked by " + - "that list will be hidden from you." + "that list will be hidden from you.", )}
    diff --git a/src/mjolnir/BanList.js b/src/mjolnir/BanList.js index 026005420a..60a924a52b 100644 --- a/src/mjolnir/BanList.js +++ b/src/mjolnir/BanList.js @@ -29,9 +29,15 @@ export const SERVER_RULE_TYPES = [RULE_SERVER, "org.matrix.mjolnir.rule.server"] export const ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES]; export function ruleTypeToStable(rule: string, unstable = true): string { - if (USER_RULE_TYPES.includes(rule)) return unstable ? USER_RULE_TYPES[USER_RULE_TYPES.length - 1] : RULE_USER; - if (ROOM_RULE_TYPES.includes(rule)) return unstable ? ROOM_RULE_TYPES[ROOM_RULE_TYPES.length - 1] : RULE_ROOM; - if (SERVER_RULE_TYPES.includes(rule)) return unstable ? SERVER_RULE_TYPES[SERVER_RULE_TYPES.length - 1] : RULE_SERVER; + if (USER_RULE_TYPES.includes(rule)) { + return unstable ? USER_RULE_TYPES[USER_RULE_TYPES.length - 1] : RULE_USER; + } + if (ROOM_RULE_TYPES.includes(rule)) { + return unstable ? ROOM_RULE_TYPES[ROOM_RULE_TYPES.length - 1] : RULE_ROOM; + } + if (SERVER_RULE_TYPES.includes(rule)) { + return unstable ? SERVER_RULE_TYPES[SERVER_RULE_TYPES.length - 1] : RULE_SERVER; + } return null; } diff --git a/src/mjolnir/ListRule.js b/src/mjolnir/ListRule.js index d33248d24c..1d472e06d6 100644 --- a/src/mjolnir/ListRule.js +++ b/src/mjolnir/ListRule.js @@ -22,7 +22,9 @@ export const RECOMMENDATION_BAN = "m.ban"; export const RECOMMENDATION_BAN_TYPES = [RECOMMENDATION_BAN, "org.matrix.mjolnir.ban"]; export function recommendationToStable(recommendation: string, unstable = true): string { - if (RECOMMENDATION_BAN_TYPES.includes(recommendation)) return unstable ? RECOMMENDATION_BAN_TYPES[RECOMMENDATION_BAN_TYPES.length - 1] : RECOMMENDATION_BAN; + if (RECOMMENDATION_BAN_TYPES.includes(recommendation)) { + return unstable ? RECOMMENDATION_BAN_TYPES[RECOMMENDATION_BAN_TYPES.length - 1] : RECOMMENDATION_BAN; + } return null; } diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js index 5edfe3750e..9177c621d1 100644 --- a/src/mjolnir/Mjolnir.js +++ b/src/mjolnir/Mjolnir.js @@ -78,11 +78,13 @@ export class Mjolnir { const resp = await MatrixClientPeg.get().createRoom({ name: _t("My Ban List"), topic: _t("This is your list of users/servers you have blocked - don't leave the room!"), - preset: "private_chat" + preset: "private_chat", }); personalRoomId = resp['room_id']; - await SettingsStore.setValue("mjolnirPersonalRoom", null, SettingLevel.ACCOUNT, personalRoomId); - await SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, [personalRoomId, ...this._roomIds]); + await SettingsStore.setValue( + "mjolnirPersonalRoom", null, SettingLevel.ACCOUNT, personalRoomId); + await SettingsStore.setValue( + "mjolnirRooms", null, SettingLevel.ACCOUNT, [personalRoomId, ...this._roomIds]); } if (!personalRoomId) { throw new Error("Error finding a room ID to use"); diff --git a/src/utils/MatrixGlob.js b/src/utils/MatrixGlob.js index b18e20ecf4..e07aaab541 100644 --- a/src/utils/MatrixGlob.js +++ b/src/utils/MatrixGlob.js @@ -50,5 +50,4 @@ export class MatrixGlob { test(val: string): boolean { return this._regex.test(val); } - } From 3c45a39caaab2c13f8b687e08679ead3adca7b85 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 16:30:51 -0600 Subject: [PATCH 0444/2372] Appease the other linter --- res/css/views/messages/_MjolnirBody.scss | 2 +- res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/res/css/views/messages/_MjolnirBody.scss b/res/css/views/messages/_MjolnirBody.scss index 80be7429e5..2760adfd7e 100644 --- a/res/css/views/messages/_MjolnirBody.scss +++ b/res/css/views/messages/_MjolnirBody.scss @@ -15,5 +15,5 @@ limitations under the License. */ .mx_MjolnirBody { - opacity: 0.4; + opacity: 0.4; } diff --git a/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss index c60cbc5dea..2a3fd12f31 100644 --- a/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss @@ -15,9 +15,9 @@ limitations under the License. */ .mx_MjolnirUserSettingsTab .mx_Field { - @mixin mx_Settings_fullWidthField; + @mixin mx_Settings_fullWidthField; } .mx_MjolnirUserSettingsTab_listItem { - margin-bottom: 2px; + margin-bottom: 2px; } From 07b8e128d2adc198767d9978329448ea59dad868 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 16:43:03 -0600 Subject: [PATCH 0445/2372] Bypass the tests being weird They run kinda-but-not-really async, which can lead to early/late calls to `stop()` --- src/mjolnir/Mjolnir.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js index 9177c621d1..7539dfafb0 100644 --- a/src/mjolnir/Mjolnir.js +++ b/src/mjolnir/Mjolnir.js @@ -66,7 +66,14 @@ export class Mjolnir { stop() { SettingsStore.unwatchSetting(this._mjolnirWatchRef); - dis.unregister(this._dispatcherRef); + + try { + if (this._dispatcherRef) dis.unregister(this._dispatcherRef); + } catch (e) { + console.error(e); + // Only the tests cause problems with this particular block of code. We should + // never be here in production. + } if (!MatrixClientPeg.get()) return; MatrixClientPeg.get().removeListener("RoomState.events", this._onEvent.bind(this)); From 28d72840ec771d2abd48a1d31850c9c842c31bb5 Mon Sep 17 00:00:00 2001 From: MamasLT Date: Fri, 1 Nov 2019 04:19:08 +0000 Subject: [PATCH 0446/2372] Translated using Weblate (Lithuanian) Currently translated at 44.0% (814 of 1850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/lt/ --- src/i18n/strings/lt.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index d7f5e2bf74..6a8076ac96 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -181,7 +181,7 @@ "%(count)s Members|one": "%(count)s narys", "Developer Tools": "Programuotojo įrankiai", "Unhide Preview": "Rodyti paržiūrą", - "Custom Server Options": "Tinkinto serverio parametrai", + "Custom Server Options": "Pasirinktiniai Serverio Nustatymai", "Event Content": "Įvykio turinys", "Thank you!": "Ačiū!", "Collapse panel": "Suskleisti skydelį", @@ -997,5 +997,8 @@ "Whether or not you're logged in (we don't record your username)": "Nepriklausomai nuo to ar jūs prisijungę (mes neįrašome jūsų vartotojo vardo)", "Chat with Riot Bot": "Kalbėtis su Riot botu", "Sign In": "Prisijungti", - "Explore rooms": "Peržiūrėti kambarius" + "Explore rooms": "Peržiūrėti kambarius", + "Your Riot is misconfigured": "Jūsų Riot yra neteisingai sukonfigūruotas", + "Sign in to your Matrix account on %(serverName)s": "Prisijunkite prie savo paskyros %(serverName)s serveryje", + "Sign in to your Matrix account on ": "Prisijunkite prie savo paskyros serveryje" } From 6aa96ef82f6fbe28321afa6cb64fe3cc12592ae6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 31 Oct 2019 19:42:41 +0000 Subject: [PATCH 0447/2372] Fix bug where rooms would not appear when filtering We need to reset the scroll offset otherwise the component may be scrolled past the only content it has (Chrome just corrected the scroll offset but Firefox scrolled it anyway). NB. Introducing the new deriveStateFromProps method seems to means that react no longer calls componentWillMount so I've had to change it to componentDidMount (which it should have been anyway). Fixes https://github.com/vector-im/riot-web/issues/11263 --- src/components/structures/RoomSubList.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 0bb5c9e9be..921680b678 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -67,6 +67,9 @@ const RoomSubList = createReactClass({ // some values to get LazyRenderList starting scrollerHeight: 800, scrollTop: 0, + // React 16's getDerivedStateFromProps(props, state) doesn't give the previous props so + // we have to store the length of the list here so we can see if it's changed or not... + listLength: null, }; }, @@ -79,11 +82,20 @@ const RoomSubList = createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._headerButton = createRef(); this.dispatcherRef = dis.register(this.onAction); }, + statics: { + getDerivedStateFromProps: function(props, state) { + return { + listLength: props.list.length, + scrollTop: props.list.length === state.listLength ? state.scrollTop : 0, + }; + }, + }, + componentWillUnmount: function() { dis.unregister(this.dispatcherRef); }, From bc7da4f6c8fab44ac6e90a5f6a1a629026bbb719 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 1 Nov 2019 10:17:53 +0000 Subject: [PATCH 0448/2372] Prepare changelog for v1.7.1-rc.2 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d8998ecbe..b4e97b737c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +Changes in [1.7.1-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.1-rc.2) (2019-11-01) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.1-rc.1...v1.7.1-rc.2) + + * Fix bug where rooms would not appear when filtering + [\#3586](https://github.com/matrix-org/matrix-react-sdk/pull/3586) + Changes in [1.7.1-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.1-rc.1) (2019-10-30) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.0...v1.7.1-rc.1) From 050ef95cd6676bdd90e558ed79b2c454fb0e6bc7 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 1 Nov 2019 10:17:54 +0000 Subject: [PATCH 0449/2372] v1.7.1-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 865efab2c2..0bd712df7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.1-rc.1", + "version": "1.7.1-rc.2", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From fd4cdd0dec78d2ea3085ab8b752a394c3f08bea9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 1 Nov 2019 10:50:58 +0000 Subject: [PATCH 0450/2372] Improve A11Y of timeline. Show TS & Actions on focus-within --- res/css/views/rooms/_EventTile.scss | 7 ++++++- src/components/views/messages/DateSeparator.js | 2 +- src/components/views/messages/MessageActionBar.js | 3 ++- src/components/views/messages/MessageTimestamp.js | 2 +- src/components/views/rooms/EventTile.js | 9 +++++++-- src/i18n/strings/en_EN.json | 1 + 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index fafd34f8ca..a30b219016 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -138,11 +138,13 @@ limitations under the License. // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) .mx_EventTile_last > div > a > .mx_MessageTimestamp, .mx_EventTile:hover > div > a > .mx_MessageTimestamp, +.mx_EventTile:focus-within > div > a > .mx_MessageTimestamp, .mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp { visibility: visible; } .mx_EventTile:hover .mx_MessageActionBar, +.mx_EventTile:focus-within .mx_MessageActionBar, .mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar { visibility: visible; } @@ -166,6 +168,7 @@ limitations under the License. } .mx_EventTile:hover .mx_EventTile_line, +.mx_EventTile:focus-within .mx_EventTile_line, .mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line { background-color: $event-selected-color; } @@ -465,7 +468,8 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { } } -.mx_EventTile:hover .mx_EventTile_body pre { +.mx_EventTile:hover .mx_EventTile_body pre, +.mx_EventTile:focus-within .mx_EventTile_body pre { border: 1px solid #e5e5e5; // deliberate constant as we're behind an invert filter } @@ -487,6 +491,7 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { background-image: url($copy-button-url); } +.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_copyButton, .mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton { visibility: visible; } diff --git a/src/components/views/messages/DateSeparator.js b/src/components/views/messages/DateSeparator.js index 900fd61914..88b59d0c26 100644 --- a/src/components/views/messages/DateSeparator.js +++ b/src/components/views/messages/DateSeparator.js @@ -57,7 +57,7 @@ export default class DateSeparator extends React.Component { render() { // ARIA treats
    s as separators, here we abuse them slightly so manually treat this entire thing as one - return

    + return


    { this.getLabel() }

    diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 565c66410e..acd8263410 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -180,7 +180,8 @@ export default class MessageActionBar extends React.PureComponent { />; } - return
    + // aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive. + return
    {reactButton} {replyButton} {editButton} diff --git a/src/components/views/messages/MessageTimestamp.js b/src/components/views/messages/MessageTimestamp.js index 0bbb3f631e..199a6f47ce 100644 --- a/src/components/views/messages/MessageTimestamp.js +++ b/src/components/views/messages/MessageTimestamp.js @@ -28,7 +28,7 @@ export default class MessageTimestamp extends React.Component { render() { const date = new Date(this.props.ts); return ( - + { formatTime(date, this.props.showTwelveHour) } ); diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index ca83dd1814..bc502d0674 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -32,6 +32,7 @@ const TextForEvent = require('../../../TextForEvent'); import dis from '../../../dispatcher'; import SettingsStore from "../../../settings/SettingsStore"; import {EventStatus, MatrixClient} from 'matrix-js-sdk'; +import {formatTime} from "../../../DateUtils"; const ObjectUtils = require('../../../ObjectUtils'); @@ -787,13 +788,17 @@ module.exports = createReactClass({ 'replyThread', ); return ( -
    +
    { readAvatars }
    { sender }
    - + { timestamp } { this._renderE2EPadlock() } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 524a8a1abf..86521f2594 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1053,6 +1053,7 @@ "React": "React", "Reply": "Reply", "Edit": "Edit", + "Message Actions": "Message Actions", "Options": "Options", "Attachment": "Attachment", "Error decrypting attachment": "Error decrypting attachment", From 446e21c2e129ae8387b5eaf6f6fce198731887a2 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 1 Nov 2019 10:44:30 +0000 Subject: [PATCH 0451/2372] Relax identity server discovery error handling If discovery results in a warning for the identity server (as in can't be found or is malformed), this allows you to continue signing in and shows the warning above the form. Fixes https://github.com/vector-im/riot-web/issues/11102 --- src/components/structures/auth/Login.js | 15 ++++++++++-- src/utils/AutoDiscoveryUtils.js | 31 +++++++++++++++---------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index ad77ed49a5..af308e1407 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -378,8 +378,19 @@ module.exports = createReactClass({ // Do a quick liveliness check on the URLs try { - await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl); - this.setState({serverIsAlive: true, errorText: ""}); + const { warning } = + await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl); + if (warning) { + this.setState({ + ...AutoDiscoveryUtils.authComponentStateForError(warning), + errorText: "", + }); + } else { + this.setState({ + serverIsAlive: true, + errorText: "", + }); + } } catch (e) { this.setState({ busy: false, diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js index e94c454a3e..49898aae90 100644 --- a/src/utils/AutoDiscoveryUtils.js +++ b/src/utils/AutoDiscoveryUtils.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -33,6 +34,8 @@ export class ValidatedServerConfig { isUrl: string; isDefault: boolean; + + warning: string; } export default class AutoDiscoveryUtils { @@ -56,7 +59,14 @@ export default class AutoDiscoveryUtils { * implementation for known values. * @returns {*} The state for the component, given the error. */ - static authComponentStateForError(err: Error, pageName="login"): Object { + static authComponentStateForError(err: string | Error | null, pageName = "login"): Object { + if (!err) { + return { + serverIsAlive: true, + serverErrorIsFatal: false, + serverDeadError: null, + }; + } let title = _t("Cannot reach homeserver"); let body = _t("Ensure you have a stable internet connection, or get in touch with the server admin"); if (!AutoDiscoveryUtils.isLivelinessError(err)) { @@ -153,11 +163,9 @@ export default class AutoDiscoveryUtils { /** * Validates a server configuration, using a homeserver domain name as input. * @param {string} serverName The homeserver domain name (eg: "matrix.org") to validate. - * @param {boolean} syntaxOnly If true, errors relating to liveliness of the servers will - * not be raised. * @returns {Promise} Resolves to the validated configuration. */ - static async validateServerName(serverName: string, syntaxOnly=false): ValidatedServerConfig { + static async validateServerName(serverName: string): ValidatedServerConfig { const result = await AutoDiscovery.findClientConfig(serverName); return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result); } @@ -186,7 +194,7 @@ export default class AutoDiscoveryUtils { const defaultConfig = SdkConfig.get()["validated_server_config"]; // Validate the identity server first because an invalid identity server causes - // and invalid homeserver, which may not be picked up correctly. + // an invalid homeserver, which may not be picked up correctly. // Note: In the cases where we rely on the default IS from the config (namely // lack of identity server provided by the discovery method), we intentionally do not @@ -197,20 +205,18 @@ export default class AutoDiscoveryUtils { preferredIdentityUrl = isResult["base_url"]; } else if (isResult && isResult.state !== AutoDiscovery.PROMPT) { console.error("Error determining preferred identity server URL:", isResult); - if (!syntaxOnly || !AutoDiscoveryUtils.isLivelinessError(isResult.error)) { + if (isResult.state === AutoDiscovery.FAIL_ERROR) { if (AutoDiscovery.ALL_ERRORS.indexOf(isResult.error) !== -1) { throw newTranslatableError(isResult.error); } throw newTranslatableError(_td("Unexpected error resolving identity server configuration")); } // else the error is not related to syntax - continue anyways. - // rewrite homeserver error if we don't care about problems - if (syntaxOnly) { - hsResult.error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER; + // rewrite homeserver error since we don't care about problems + hsResult.error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER; - // Also use the user's supplied identity server if provided - if (isResult["base_url"]) preferredIdentityUrl = isResult["base_url"]; - } + // Also use the user's supplied identity server if provided + if (isResult["base_url"]) preferredIdentityUrl = isResult["base_url"]; } if (hsResult.state !== AutoDiscovery.SUCCESS) { @@ -241,6 +247,7 @@ export default class AutoDiscoveryUtils { hsNameIsDifferent: url.hostname !== preferredHomeserverName, isUrl: preferredIdentityUrl, isDefault: false, + warning: hsResult.error, }); } } From 10a63ada48ec5dc9b7cfeeb324a97f5404a80611 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 1 Nov 2019 14:46:30 +0000 Subject: [PATCH 0452/2372] Fix SVG mask-image usage in a bunch of places for correct outlining --- res/css/structures/_GroupView.scss | 18 +++++++++++---- res/css/structures/_RightPanel.scss | 22 +++++++++--------- res/css/views/rooms/_MessageComposer.scss | 28 +++++++++++++++-------- res/css/views/rooms/_RoomHeader.scss | 27 ++++++++++++++-------- 4 files changed, 59 insertions(+), 36 deletions(-) diff --git a/res/css/structures/_GroupView.scss b/res/css/structures/_GroupView.scss index ae86f68fd0..4ec53a3c9a 100644 --- a/res/css/structures/_GroupView.scss +++ b/res/css/structures/_GroupView.scss @@ -44,21 +44,29 @@ limitations under the License. } .mx_GroupHeader_button { + position: relative; margin-left: 5px; margin-right: 5px; cursor: pointer; height: 20px; width: 20px; - background-color: $groupheader-button-color; - mask-repeat: no-repeat; - mask-size: contain; + + &::before { + content: ''; + position: absolute; + height: 20px; + width: 20px; + background-color: $groupheader-button-color; + mask-repeat: no-repeat; + mask-size: contain; + } } -.mx_GroupHeader_editButton { +.mx_GroupHeader_editButton::before { mask-image: url('$(res)/img/icons-settings-room.svg'); } -.mx_GroupHeader_shareButton { +.mx_GroupHeader_shareButton::before { mask-image: url('$(res)/img/icons-share.svg'); } diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index c63db5d274..973f6fe9b3 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -50,18 +50,18 @@ limitations under the License. height: 20px; width: 20px; position: relative; -} -.mx_RightPanel_headerButton::before { - content: ''; - position: absolute; - top: 0; - left: 0; - height: 20px; - width: 20px; - background-color: $rightpanel-button-color; - mask-repeat: no-repeat; - mask-size: contain; + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + height: 20px; + width: 20px; + background-color: $rightpanel-button-color; + mask-repeat: no-repeat; + mask-size: contain; + } } .mx_RightPanel_membersButton::before { diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 5b4a9b764b..e9f33183f5 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -180,34 +180,42 @@ limitations under the License. } .mx_MessageComposer_button { + position: relative; margin-right: 12px; cursor: pointer; - padding-top: 4px; height: 20px; width: 20px; - background-color: $composer-button-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; + + &::before { + content: ''; + position: absolute; + + height: 20px; + width: 20px; + background-color: $composer-button-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } } -.mx_MessageComposer_upload { +.mx_MessageComposer_upload::before { mask-image: url('$(res)/img/feather-customised/paperclip.svg'); } -.mx_MessageComposer_hangup { +.mx_MessageComposer_hangup::before { mask-image: url('$(res)/img/hangup.svg'); } -.mx_MessageComposer_voicecall { +.mx_MessageComposer_voicecall::before { mask-image: url('$(res)/img/feather-customised/phone.svg'); } -.mx_MessageComposer_videocall { +.mx_MessageComposer_videocall::before { mask-image: url('$(res)/img/feather-customised/video.svg'); } -.mx_MessageComposer_stickers { +.mx_MessageComposer_stickers::before { mask-image: url('$(res)/img/feather-customised/face.svg'); } diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 2ee991cac7..5da8ff76b9 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -192,33 +192,41 @@ limitations under the License. } .mx_RoomHeader_button { + position: relative; margin-left: 10px; cursor: pointer; height: 20px; width: 20px; - background-color: $roomheader-button-color; - mask-repeat: no-repeat; - mask-size: contain; + + &::before { + content: ''; + position: absolute; + height: 20px; + width: 20px; + background-color: $roomheader-button-color; + mask-repeat: no-repeat; + mask-size: contain; + } } -.mx_RoomHeader_settingsButton { +.mx_RoomHeader_settingsButton::before { mask-image: url('$(res)/img/feather-customised/settings.svg'); } -.mx_RoomHeader_forgetButton { +.mx_RoomHeader_forgetButton::before { mask-image: url('$(res)/img/leave.svg'); width: 26px; } -.mx_RoomHeader_searchButton { +.mx_RoomHeader_searchButton::before { mask-image: url('$(res)/img/feather-customised/search.svg'); } -.mx_RoomHeader_shareButton { +.mx_RoomHeader_shareButton::before { mask-image: url('$(res)/img/feather-customised/share.svg'); } -.mx_RoomHeader_manageIntegsButton { +.mx_RoomHeader_manageIntegsButton::before { mask-image: url('$(res)/img/feather-customised/grid.svg'); } @@ -234,8 +242,7 @@ limitations under the License. margin-top: 18px; } -.mx_RoomHeader_pinnedButton { - position: relative; +.mx_RoomHeader_pinnedButton::before { mask-image: url('$(res)/img/icons-pin.svg'); } From 0c82d9e7e0c85db359c7917bc50c83e689078dc7 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 1 Nov 2019 16:35:16 +0000 Subject: [PATCH 0453/2372] Align start and end tags --- src/IdentityAuthClient.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 563e5e0441..f21db12c51 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -146,19 +146,21 @@ export default class IdentityAuthClient { const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '', QuestionDialog, { title: _t("Identity server has no terms of service"), - description:
    -

    {_t( - "This action requires accessing the default identity server " + - " to validate an email address or phone number, but the server " + - "does not have any terms of service.", {}, - { - server: () => {abbreviateUrl(identityServerUrl)}, - }, - )}

    -

    {_t( - "Only continue if you trust the owner of the server.", - )}

    -
    , + description: ( +
    +

    {_t( + "This action requires accessing the default identity server " + + " to validate an email address or phone number, " + + "but the server does not have any terms of service.", {}, + { + server: () => {abbreviateUrl(identityServerUrl)}, + }, + )}

    +

    {_t( + "Only continue if you trust the owner of the server.", + )}

    +
    + ), button: _t("Trust"), }); const [confirmed] = await finished; From a8b89840a34bc0f1754bc013db1fe8933884dd30 Mon Sep 17 00:00:00 2001 From: Osoitz Date: Fri, 1 Nov 2019 09:21:09 +0000 Subject: [PATCH 0454/2372] Translated using Weblate (Basque) Currently translated at 100.0% (1850 of 1850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eu/ --- src/i18n/strings/eu.json | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index ef2dd9fe8b..72b6fff50d 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -2188,5 +2188,39 @@ "Emoji Autocomplete": "Emoji osatze automatikoa", "Notification Autocomplete": "Jakinarazpen osatze automatikoa", "Room Autocomplete": "Gela osatze automatikoa", - "User Autocomplete": "Erabiltzaile osatze automatikoa" + "User Autocomplete": "Erabiltzaile osatze automatikoa", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Erabili erabiltzaile-informazio panel berria gelako eta taldeko kideentzat", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Zure datu pribatuak kendu beharko zenituzke identitate-zerbitzaritik deskonektatu aurretik. Zoritxarrez identitate-zerbitzaria lineaz kanpo dago eta ezin da atzitu.", + "You should:": "Hau egin beharko zenuke:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "egiaztatu zure nabigatzailearen gehigarriren batek ez duela identitate-zerbitzaria blokeatzen (esaterako Privacy Badger)", + "contact the administrators of identity server ": " identitate-zerbitzariko administratzaileekin kontaktuak jarri", + "wait and try again later": "itxaron eta berriro saiatu", + "Show tray icon and minimize window to it on close": "Erakutsi egoera-barrako ikonoa eta minimizatu leihoa itxi ordez", + "Room %(name)s": "%(name)s gela", + "Recent rooms": "Azken gelak", + "%(count)s unread messages including mentions.|one": "Irakurri gabeko aipamen 1.", + "%(count)s unread messages.|one": "Irakurri gabeko mezu 1.", + "Unread messages.": "Irakurri gabeko mezuak.", + "Trust & Devices": "Fidagarritasuna eta gailuak", + "Direct messages": "Mezu zuzenak", + "Failed to deactivate user": "Huts egin du erabiltzailea desaktibatzeak", + "This client does not support end-to-end encryption.": "Bezero honek ez du muturretik muturrerako zifratzea onartzen.", + "Messages in this room are not end-to-end encrypted.": "Gela honetako mezuak ez daude muturretik muturrera zifratuta.", + "React": "Erreakzioa", + "Frequently Used": "Maiz erabilia", + "Smileys & People": "Irribartxoak eta jendea", + "Animals & Nature": "Animaliak eta natura", + "Food & Drink": "Jana eta edana", + "Activities": "Jarduerak", + "Travel & Places": "Bidaiak eta tokiak", + "Objects": "Objektuak", + "Symbols": "Ikurrak", + "Flags": "Banderak", + "Quick Reactions": "Erreakzio azkarrak", + "Cancel search": "Ezeztatu bilaketa", + "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Ez da identitate-zerbitzaririk konfiguratu, beraz ezin duzu e-mail helbide bat gehitu etorkizunean pasahitza berrezartzeko.", + "Jump to first unread room.": "Jauzi irakurri gabeko lehen gelara.", + "Jump to first invite.": "Jauzi lehen gonbidapenera.", + "Command Autocomplete": "Aginduak auto-osatzea", + "DuckDuckGo Results": "DuckDuckGo emaitzak" } From aaf86b198513a3cf11ae6a337af643b0de3ddf39 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Fri, 1 Nov 2019 01:28:18 +0000 Subject: [PATCH 0455/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1850 of 1850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index bb383c7f5a..3c580f22ff 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2269,5 +2269,6 @@ "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "未設定身份識別伺服器,所以您無法新增電子郵件以在未來重設您的密碼。", "%(count)s unread messages including mentions.|one": "1 則未讀的提及。", "%(count)s unread messages.|one": "1 則未讀的訊息。", - "Unread messages.": "未讀的訊息。" + "Unread messages.": "未讀的訊息。", + "Show tray icon and minimize window to it on close": "顯示系統匣圖示並在關閉視窗時將其最小化至其中" } From b877b81041e1ebde1fe7e955ba20b2176c4149ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Fri, 1 Nov 2019 14:11:54 +0000 Subject: [PATCH 0456/2372] Translated using Weblate (Korean) Currently translated at 100.0% (1850 of 1850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 4e94a06dc3..f06d72d01e 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2120,5 +2120,6 @@ "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "설정된 ID 서버가 없어서 이후 비밀번호를 초기화하기 위한 이메일 주소를 추가할 수 없습니다.", "%(count)s unread messages including mentions.|one": "1개의 읽지 않은 언급.", "%(count)s unread messages.|one": "1개의 읽지 않은 메시지.", - "Unread messages.": "읽지 않은 메시지." + "Unread messages.": "읽지 않은 메시지.", + "Show tray icon and minimize window to it on close": "닫을 때 창을 최소화하고 트레이 아이콘으로 표시하기" } From 661c77cfd9bbaf6e652b8533bb254f4007e10277 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Fri, 1 Nov 2019 17:15:53 +0000 Subject: [PATCH 0457/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1852 of 1852 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index d901ff4d85..4946c7b14f 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2264,5 +2264,7 @@ "%(count)s unread messages including mentions.|one": "1 olvasatlan megemlítés.", "%(count)s unread messages.|one": "1 olvasatlan üzenet.", "Unread messages.": "Olvasatlan üzenetek.", - "Show tray icon and minimize window to it on close": "Tálcaikon mutatása és az ablak összecsukása bezáráskor" + "Show tray icon and minimize window to it on close": "Tálcaikon mutatása és az ablak összecsukása bezáráskor", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Ez a művelet az e-mail cím vagy telefonszám ellenőrzése miatt hozzáférést igényel az alapértelmezett azonosítási szerverhez (), de a szervernek nincsen semmilyen felhasználási feltétele.", + "Trust": "Megbízom benne" } From d3dd0cb91afde4e0bee0d3e323c2bf267807364c Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Sun, 3 Nov 2019 07:47:15 +0000 Subject: [PATCH 0458/2372] Translated using Weblate (Bulgarian) Currently translated at 97.6% (1808 of 1852 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/bg/ --- src/i18n/strings/bg.json | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index 29771f6f2b..cf08dba77f 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -2189,5 +2189,20 @@ "Hide advanced": "Скрий разширени настройки", "Show advanced": "Покажи разширени настройки", "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Блокирай присъединяването на потребители от други Matrix сървъри в тази стая (Тази настройка не може да се промени по-късно!)", - "Close dialog": "Затвори прозореца" + "Close dialog": "Затвори прозореца", + "Add Email Address": "Добави имейл адрес", + "Add Phone Number": "Добави телефонен номер", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Това действие изисква връзка със сървъра за самоличност за валидиране на имейл адреса или телефонния номер, но сървърът не предоставя условия за ползване.", + "Trust": "Довери се", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Използвай новия UserInfo панел за членове на стаи и групи", + "Use the new, faster, composer for writing messages": "Използвай новия, по-бърз редактор за писане на съобщения", + "Show previews/thumbnails for images": "Показвай преглед (умален размер) на снимки", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Би било добре да премахнете личните си данни от сървъра за самоличност преди прекъсване на връзката. За съжаление, сървърът за самоличност в момента не е достъпен.", + "You should:": "Ще е добре да:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "проверите браузър добавките за всичко, което може да блокира връзката със сървъра за самоличност (например Privacy Badge)", + "contact the administrators of identity server ": "се свържете с администратора на сървъра за самоличност ", + "wait and try again later": "изчакате и опитате пак", + "Clear cache and reload": "Изчисти кеша и презареди", + "Show tray icon and minimize window to it on close": "Показвай икона в лентата и минимизирай прозореца там при затваряне", + "Your email address hasn't been verified yet": "Имейл адресът ви все още не е потвърден" } From 97831277246855749990b42df178b69c89f1122d Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Sat, 2 Nov 2019 13:40:15 +0000 Subject: [PATCH 0459/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1852 of 1852 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 3c580f22ff..94183ef83f 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2270,5 +2270,7 @@ "%(count)s unread messages including mentions.|one": "1 則未讀的提及。", "%(count)s unread messages.|one": "1 則未讀的訊息。", "Unread messages.": "未讀的訊息。", - "Show tray icon and minimize window to it on close": "顯示系統匣圖示並在關閉視窗時將其最小化至其中" + "Show tray icon and minimize window to it on close": "顯示系統匣圖示並在關閉視窗時將其最小化至其中", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "此動作需要存取預設的身份識別伺服器 以驗證電子郵件或電話號碼,但伺服器沒有任何服務條款。", + "Trust": "信任" } From d6d2505346f3513bb294855d3e5324ee647c60b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Sat, 2 Nov 2019 11:03:53 +0000 Subject: [PATCH 0460/2372] Translated using Weblate (French) Currently translated at 100.0% (1852 of 1852 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 20f88e4d08..7807facb1c 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2277,5 +2277,7 @@ "%(count)s unread messages including mentions.|one": "1 mention non lue.", "%(count)s unread messages.|one": "1 message non lu.", "Unread messages.": "Messages non lus.", - "Show tray icon and minimize window to it on close": "Afficher l’icône dans la barre d’état et minimiser la fenêtre lors de la fermeture" + "Show tray icon and minimize window to it on close": "Afficher l’icône dans la barre d’état et minimiser la fenêtre lors de la fermeture", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Cette action nécessite l’accès au serveur d’identité par défaut afin de valider une adresse e-mail ou un numéro de téléphone, mais le serveur n’a aucune condition de service.", + "Trust": "Confiance" } From 3a44cf2191f4dccd1bfb79cd618865e5ba873fe1 Mon Sep 17 00:00:00 2001 From: shuji narazaki Date: Sun, 3 Nov 2019 04:55:28 +0000 Subject: [PATCH 0461/2372] Translated using Weblate (Japanese) Currently translated at 58.8% (1089 of 1852 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ja/ --- src/i18n/strings/ja.json | 42 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 47e53e2ffe..23199094e8 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -1303,5 +1303,45 @@ "Upgrade": "アップグレード", "Sets the room name": "部屋名を設定する", "Change room name": "部屋名を変える", - "Room Name": "部屋名" + "Room Name": "部屋名", + "Add Email Address": "メールアドレスの追加", + "Add Phone Number": "電話番号の追加", + "Call failed due to misconfigured server": "サーバの誤設定により呼び出し失敗", + "Try using turn.matrix.org": "turn.matrix.orgで試してみる", + "A conference call could not be started because the integrations server is not available": "統合サーバーが利用できないので電話会議を開始できませんでした", + "Replying With Files": "ファイルを添付して返信", + "The file '%(fileName)s' failed to upload.": "ファイル '%(fileName)s' のアップロードに失敗しました。", + "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "ファイル '%(fileName)s' はこのホームサーバのアップロードのサイズ上限を超えています", + "The server does not support the room version specified.": "このサーバは指定された部屋バージョンに対応していません。", + "Name or Matrix ID": "名前またはMatrix ID", + "Identity server has no terms of service": "IDサーバーは利用規約を持っていません", + "Email, name or Matrix ID": "メールアドレス、名前、またはMatrix ID", + "Failed to start chat": "対話開始に失敗しました", + "Messages": "メッセージ", + "Actions": "アクション", + "Other": "その他", + "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "¯\\_(ツ)_/¯ を平文メッセージの前に挿入する", + "Sends a message as plain text, without interpreting it as markdown": "メッセージをマークダウンと解釈せずプレーンテキストとして送信する", + "Upgrades a room to a new version": "部屋を新しいバージョンへアップグレードする", + "You do not have the required permissions to use this command.": "このコマンドを実行するのに必要な権限がありません。", + "Room upgrade confirmation": "部屋のアップグレードの確認", + "Changes your display nickname in the current room only": "表示されるニックネームをこの部屋に関してのみ変更する", + "Changes the avatar of the current room": "現在の部屋のアバターを変更する", + "Changes your avatar in this current room only": "アバターをこの部屋に関してのみ変更する", + "Changes your avatar in all rooms": "全ての部屋に対するアバターを変更する", + "Gets or sets the room topic": "部屋のトピック情報を取得または設定する", + "This room has no topic.": "この部屋はトピックを持ちません。", + "Use an identity server": "IDサーバーを使用する", + "Unbans user with given ID": "与えられたIDを持つユーザーの追放を解除する", + "Adds a custom widget by URL to the room": "部屋にURLで指定したカスタムウィジェットを追加する", + "Please supply a https:// or http:// widget URL": "https:// または http:// で始まるウィジェット URLを指定してください", + "You cannot modify widgets in this room.": "あなたはこの部屋のウィジェットを変更できません。", + "Sends the given message coloured as a rainbow": "与えられたメッセージを虹色にして送信する", + "Sends the given emote coloured as a rainbow": "与えられたエモートを虹色で送信する", + "Displays list of commands with usages and descriptions": "使い方と説明付きのコマンド一覧を表示する", + "%(senderName)s made no change.": "%(senderName)s は変更されませんでした。", + "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s はこの部屋をアップグレードしました。", + "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s はこの部屋をリンクを知っている人全てに公開しました。", + "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s はこの部屋を招待者のみに変更しました。", + "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s はゲストがこの部屋に参加できるようにしました。" } From 8b63a37cdd9622e5f98efd327a19e3b7e52e0624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Sat, 2 Nov 2019 10:06:34 +0000 Subject: [PATCH 0462/2372] Translated using Weblate (Korean) Currently translated at 100.0% (1852 of 1852 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index f06d72d01e..1aebd0ce17 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2121,5 +2121,7 @@ "%(count)s unread messages including mentions.|one": "1개의 읽지 않은 언급.", "%(count)s unread messages.|one": "1개의 읽지 않은 메시지.", "Unread messages.": "읽지 않은 메시지.", - "Show tray icon and minimize window to it on close": "닫을 때 창을 최소화하고 트레이 아이콘으로 표시하기" + "Show tray icon and minimize window to it on close": "닫을 때 창을 최소화하고 트레이 아이콘으로 표시하기", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "이 작업에는 이메일 주소 또는 전화번호를 확인하기 위해 기본 ID 서버 에 접근해야 합니다. 하지만 서버가 서비스 약관을 갖고 있지 않습니다.", + "Trust": "신뢰함" } From 2fc1c3235bfdf6f127dc051a8c17970e7602bdb7 Mon Sep 17 00:00:00 2001 From: Philip Johansson Date: Sat, 2 Nov 2019 10:23:46 +0000 Subject: [PATCH 0463/2372] Translated using Weblate (Swedish) Currently translated at 77.8% (1441 of 1852 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sv/ --- src/i18n/strings/sv.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 5c98ea50ba..6d611be464 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -946,7 +946,7 @@ "Verified key": "Verifierad nyckel", "Unrecognised command:": "Oigenkänt kommando:", "Unbans user with given id": "Avbannar användare med givet id", - "Verifies a user, device, and pubkey tuple": "Verifierar en användare, enhet och nycklar", + "Verifies a user, device, and pubkey tuple": "Verifierar en användare, enhet och offentlig nyckel-tupel", "VoIP conference started.": "VoIP-konferens startad.", "VoIP conference finished.": "VoIP-konferens avslutad.", "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s gjorde framtida rumshistorik synligt för okänd (%(visibility)s).", @@ -1814,5 +1814,8 @@ "All keys backed up": "Alla nycklar säkerhetskopierade", "Backup has a signature from unknown device with ID %(deviceId)s.": "Säkerhetskopian har en signatur från okänd enhet med ID %(deviceId)s.", "Add Email Address": "Lägg till e-postadress", - "Add Phone Number": "Lägg till telefonnummer" + "Add Phone Number": "Lägg till telefonnummer", + "Identity server has no terms of service": "Identitetsserver har inga användarvillkor", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Den här åtgärden kräver åtkomst till standardidentitetsservern för att validera en e-postadress eller telefonnummer, men servern har inga användarvillkor.", + "Trust": "Förtroende" } From 6d3b5631199154d83432f292d95f6dd2ae824f6e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 4 Nov 2019 10:16:16 +0000 Subject: [PATCH 0464/2372] Add comments regarding tab-index=-1 --- src/components/views/messages/DateSeparator.js | 1 + src/components/views/rooms/EventTile.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/views/messages/DateSeparator.js b/src/components/views/messages/DateSeparator.js index 88b59d0c26..56faa670b2 100644 --- a/src/components/views/messages/DateSeparator.js +++ b/src/components/views/messages/DateSeparator.js @@ -57,6 +57,7 @@ export default class DateSeparator extends React.Component { render() { // ARIA treats
    s as separators, here we abuse them slightly so manually treat this entire thing as one + // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return


    { this.getLabel() }
    diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index bc502d0674..9497324f5a 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -787,6 +787,7 @@ module.exports = createReactClass({ this.props.permalinkCreator, 'replyThread', ); + // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return (
    From 2a1f26a44ff4003204ba05bc7f7c9d52a9eedc7a Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 4 Nov 2019 11:50:49 +0000 Subject: [PATCH 0465/2372] Translated using Weblate (Bulgarian) Currently translated at 97.6% (1808 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/bg/ --- src/i18n/strings/bg.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index cf08dba77f..2287c5b295 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -2199,7 +2199,7 @@ "Show previews/thumbnails for images": "Показвай преглед (умален размер) на снимки", "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Би било добре да премахнете личните си данни от сървъра за самоличност преди прекъсване на връзката. За съжаление, сървърът за самоличност в момента не е достъпен.", "You should:": "Ще е добре да:", - "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "проверите браузър добавките за всичко, което може да блокира връзката със сървъра за самоличност (например Privacy Badge)", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "проверите браузър добавките за всичко, което може да блокира връзката със сървъра за самоличност (например Privacy Badger)", "contact the administrators of identity server ": "се свържете с администратора на сървъра за самоличност ", "wait and try again later": "изчакате и опитате пак", "Clear cache and reload": "Изчисти кеша и презареди", From aba557f02352e04a2276a69fe4d51e15ac351881 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 4 Nov 2019 15:09:19 +0000 Subject: [PATCH 0466/2372] Released react-sdk --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0bd712df7f..a27279645f 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "lolex": "4.2", - "matrix-js-sdk": "2.4.3-rc.1", + "matrix-js-sdk": "2.4.3", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index bc3ee0282f..fa7868d270 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5192,10 +5192,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc" integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw== -matrix-js-sdk@2.4.3-rc.1: - version "2.4.3-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.3-rc.1.tgz#c06452b89c74976ac0bae0732325c0b359e6f2fa" - integrity sha512-aV70H10lSpjAOmnWDXIWc2CP5D1OylwSSfyc61QzjvGhECEYaiQi4rxH4ZFhX9AL3ezPHse7SY6AmKOCfqBQiw== +matrix-js-sdk@2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.3.tgz#23b78cc707a02eb0ce7eecb3aa50129e46dd5b6e" + integrity sha512-8qTqILd/NmTWF24tpaxmDIzkTk/bZhPD5N8h69PlvJ5Y6kMFctpRj+Tud5zZjl5/yhO07+g+JCyDzg+AagiM/A== dependencies: another-json "^0.2.0" babel-runtime "^6.26.0" From 4454be909218de4050028cd4a85a56f4025f300c Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 4 Nov 2019 15:12:54 +0000 Subject: [PATCH 0467/2372] Prepare changelog for v1.7.1 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4e97b737c..ee71ea7f68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [1.7.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.1) (2019-11-04) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.1-rc.2...v1.7.1) + + * No changes since rc.2 + Changes in [1.7.1-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.1-rc.2) (2019-11-01) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.1-rc.1...v1.7.1-rc.2) From 2f4f15d26ccc084aebac223a1d33a7cc8f03c14a Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 4 Nov 2019 15:12:54 +0000 Subject: [PATCH 0468/2372] v1.7.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a27279645f..5cc23a9fe3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.1-rc.2", + "version": "1.7.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 99c8a909b31f50e33b4c1c9a43e80208936377c9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 4 Nov 2019 16:47:20 +0000 Subject: [PATCH 0469/2372] Fix focus-within on EventTile and more showing onClick --- res/css/structures/_RoomSubList.scss | 2 +- res/css/views/rooms/_EventTile.scss | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index b39504593a..cd3a822d4d 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -51,7 +51,7 @@ limitations under the License. height: 36px; } -.mx_RoomSubList_labelContainer:focus-within { +.mx_RoomSubList_labelContainer.focus-visible:focus-within { background-color: $roomtile-focused-bg-color; } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index a30b219016..ac3eeb7b81 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -138,13 +138,13 @@ limitations under the License. // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) .mx_EventTile_last > div > a > .mx_MessageTimestamp, .mx_EventTile:hover > div > a > .mx_MessageTimestamp, -.mx_EventTile:focus-within > div > a > .mx_MessageTimestamp, +.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp, .mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp { visibility: visible; } .mx_EventTile:hover .mx_MessageActionBar, -.mx_EventTile:focus-within .mx_MessageActionBar, +.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar, .mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar { visibility: visible; } @@ -168,7 +168,7 @@ limitations under the License. } .mx_EventTile:hover .mx_EventTile_line, -.mx_EventTile:focus-within .mx_EventTile_line, +.mx_EventTile.focus-visible:focus-within .mx_EventTile_line, .mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line { background-color: $event-selected-color; } @@ -469,7 +469,7 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { } .mx_EventTile:hover .mx_EventTile_body pre, -.mx_EventTile:focus-within .mx_EventTile_body pre { +.mx_EventTile.focus-visible:focus-within .mx_EventTile_body pre { border: 1px solid #e5e5e5; // deliberate constant as we're behind an invert filter } From bc924bbd82e2274fb6a663935201da88f2064fff Mon Sep 17 00:00:00 2001 From: sha-265 <4103710+sha-265@users.noreply.github.com> Date: Mon, 4 Nov 2019 17:37:36 +0000 Subject: [PATCH 0470/2372] Support RTL language in message composer --- src/components/views/rooms/BasicMessageComposer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 0cd08cce5a..c7659e89fb 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -566,6 +566,7 @@ export default class BasicMessageEditor extends React.Component { aria-haspopup="listbox" aria-expanded={Boolean(this.state.autoComplete)} aria-activedescendant={completionIndex >= 0 ? generateCompletionDomId(completionIndex) : undefined} + dir="auto" />
    ); } From 888da3ad84c260ed4c33f2bdd53df33dac163fb8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 5 Nov 2019 12:01:30 +0000 Subject: [PATCH 0471/2372] delint stylesheet --- res/css/views/rooms/_EventTile.scss | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index ac3eeb7b81..36db29d47e 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -138,14 +138,14 @@ limitations under the License. // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) .mx_EventTile_last > div > a > .mx_MessageTimestamp, .mx_EventTile:hover > div > a > .mx_MessageTimestamp, -.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp, -.mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp { +.mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp, +.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp { visibility: visible; } .mx_EventTile:hover .mx_MessageActionBar, -.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar, -.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar { +.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar, +.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar { visibility: visible; } @@ -168,8 +168,8 @@ limitations under the License. } .mx_EventTile:hover .mx_EventTile_line, -.mx_EventTile.focus-visible:focus-within .mx_EventTile_line, -.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line { +.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line, +.mx_EventTile.focus-visible:focus-within .mx_EventTile_line { background-color: $event-selected-color; } From eb4220a836972ca470b3bffd5916921631410e55 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 6 Nov 2019 10:31:05 +0100 Subject: [PATCH 0472/2372] scroll panels should be in flex container so they don't grow w/ content --- res/css/structures/_FilePanel.scss | 1 + res/css/structures/_NotificationPanel.scss | 1 + 2 files changed, 2 insertions(+) diff --git a/res/css/structures/_FilePanel.scss b/res/css/structures/_FilePanel.scss index 703e90f402..87e885e668 100644 --- a/res/css/structures/_FilePanel.scss +++ b/res/css/structures/_FilePanel.scss @@ -18,6 +18,7 @@ limitations under the License. order: 2; flex: 1 1 0; overflow-y: auto; + display: flex; } .mx_FilePanel .mx_RoomView_messageListWrapper { diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss index 78b3522d4e..c9e0261ec9 100644 --- a/res/css/structures/_NotificationPanel.scss +++ b/res/css/structures/_NotificationPanel.scss @@ -18,6 +18,7 @@ limitations under the License. order: 2; flex: 1 1 0; overflow-y: auto; + display: flex; } .mx_NotificationPanel .mx_RoomView_messageListWrapper { From 9fa7990996ec89f2301f6cc4f3d4ab3860a7b7cd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 6 Nov 2019 10:31:56 +0100 Subject: [PATCH 0473/2372] prevent error for empty list --- src/components/structures/ScrollPanel.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 32efad1e05..1d5c520285 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -759,8 +759,10 @@ module.exports = createReactClass({ _getMessagesHeight() { const itemlist = this.refs.itemlist; const lastNode = itemlist.lastElementChild; + const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0; + const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0; // 18 is itemlist padding - return (lastNode.offsetTop + lastNode.clientHeight) - itemlist.firstElementChild.offsetTop + (18 * 2); + return lastNodeBottom - firstNodeTop + (18 * 2); }, _topFromBottom(node) { From 842bf77409184a028e721cb50abb5a50c7b655fb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 6 Nov 2019 10:32:20 +0100 Subject: [PATCH 0474/2372] prevent error when nextProps is null, cleanup As the FilePanel is now rendered as part of the RoomView, we don't need to respond to room changes, as RoomView has a key of the roomId, so the whole subtree would be recreated. --- src/components/structures/FilePanel.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index c7e8295f80..f5a5912dd5 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -39,23 +39,10 @@ const FilePanel = createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this.updateTimelineSet(this.props.roomId); }, - componentWillReceiveProps: function(nextProps) { - if (nextProps.roomId !== this.props.roomId) { - // otherwise we race between re-rendering the TimelinePanel and setting the new timelineSet. - // - // FIXME: this race only happens because of the promise returned by getOrCreateFilter(). - // We should only need to create the containsUrl filter once per login session, so in practice - // it shouldn't be being done here at all. Then we could just update the timelineSet directly - // without resetting it first, and speed up room-change. - this.setState({ timelineSet: null }); - this.updateTimelineSet(nextProps.roomId); - } - }, - updateTimelineSet: function(roomId) { const client = MatrixClientPeg.get(); const room = client.getRoom(roomId); From d403ed751389ee2bc8d4f26a75f6a727bf49f596 Mon Sep 17 00:00:00 2001 From: N-Pex Date: Wed, 6 Nov 2019 10:41:14 +0100 Subject: [PATCH 0475/2372] Fix linkify imports VECTOR_URL_PATTERN was 'undefined' inside Permalinks.tryTransformPermalinkToLocalHref() --- src/linkify-matrix.js | 2 +- src/utils/permalinks/Permalinks.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index fabd9d15ad..889bad682c 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -243,4 +243,4 @@ matrixLinkify.options = { }, }; -module.exports = matrixLinkify; +export default matrixLinkify; diff --git a/src/utils/permalinks/Permalinks.js b/src/utils/permalinks/Permalinks.js index 19dcbd062a..aec7243236 100644 --- a/src/utils/permalinks/Permalinks.js +++ b/src/utils/permalinks/Permalinks.js @@ -20,7 +20,7 @@ import utils from 'matrix-js-sdk/lib/utils'; import SpecPermalinkConstructor, {baseUrl as matrixtoBaseUrl} from "./SpecPermalinkConstructor"; import PermalinkConstructor, {PermalinkParts} from "./PermalinkConstructor"; import RiotPermalinkConstructor from "./RiotPermalinkConstructor"; -import * as matrixLinkify from "../../linkify-matrix"; +import matrixLinkify from "../../linkify-matrix"; const SdkConfig = require("../../SdkConfig"); From 328fc5a02dd5013a42825ee2e1c3bb99d67b8eb1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 6 Nov 2019 10:41:10 +0000 Subject: [PATCH 0476/2372] fix style lint --- res/css/views/rooms/_EventTile.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 36db29d47e..db34200b16 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -167,6 +167,10 @@ limitations under the License. } } +.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { + padding-left: 78px; +} + .mx_EventTile:hover .mx_EventTile_line, .mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line, .mx_EventTile.focus-visible:focus-within .mx_EventTile_line { @@ -384,10 +388,6 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { padding-left: 5px; } -.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { - padding-left: 78px; -} - .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { padding-left: 60px; From 0464b094a6fa911204891a15a6b8af4cef0fa09b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 6 Nov 2019 11:44:32 +0000 Subject: [PATCH 0477/2372] Fix softcrash if editing silly events If you sent an event with a body of the empty json object, riot would then softcrash when you pressed the up arrow because it would try to treat a json object as a string and run split on it. --- src/utils/EventUtils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.js index ffc47e2277..29af5ca9b5 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.js @@ -52,6 +52,7 @@ export function canEditContent(mxEvent) { const content = mxEvent.getOriginalContent(); const {msgtype} = content; return (msgtype === "m.text" || msgtype === "m.emote") && + content.body && typeof content.body === 'string' && mxEvent.getSender() === MatrixClientPeg.get().getUserId(); } From c73b553831a6a3976a4aeaa69183483ff1cef9ca Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 6 Nov 2019 14:17:09 +0000 Subject: [PATCH 0478/2372] Prepare changelog for v1.7.2 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee71ea7f68..7c46530fad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +Changes in [1.7.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.2) (2019-11-06) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.1...v1.7.2) + + * Fix softcrash if editing silly events + [\#3596](https://github.com/matrix-org/matrix-react-sdk/pull/3596) + * Fix: file and notifications panel back-paginating forever. + [\#3594](https://github.com/matrix-org/matrix-react-sdk/pull/3594) + * Fix focus-within on EventTile and more showing onClick + [\#3591](https://github.com/matrix-org/matrix-react-sdk/pull/3591) + * Support RTL language in message composer + [\#3592](https://github.com/matrix-org/matrix-react-sdk/pull/3592) + * Update from Weblate + [\#3590](https://github.com/matrix-org/matrix-react-sdk/pull/3590) + * Improve A11Y of timeline. Show timestamp & Actions on focus-within + [\#3587](https://github.com/matrix-org/matrix-react-sdk/pull/3587) + * Fix SVG mask-image usage in a bunch of places for correct outlining + [\#3589](https://github.com/matrix-org/matrix-react-sdk/pull/3589) + * Handle breadcrumbs, integration manager provisioning, and allowed widgets + Riot settings + [\#3577](https://github.com/matrix-org/matrix-react-sdk/pull/3577) + * Add a prompt when interacting with an identity server without terms + [\#3582](https://github.com/matrix-org/matrix-react-sdk/pull/3582) + * Fix bug where rooms would not appear when filtering + [\#3584](https://github.com/matrix-org/matrix-react-sdk/pull/3584) + * Guard against misconfigured homeservers when adding / binding phone numbers + [\#3583](https://github.com/matrix-org/matrix-react-sdk/pull/3583) + * Fix error message which is shown when unknown slash command attempted + [\#3580](https://github.com/matrix-org/matrix-react-sdk/pull/3580) + * Attempt to fix soft crash on some pinned events by null guarding member + [\#3581](https://github.com/matrix-org/matrix-react-sdk/pull/3581) + Changes in [1.7.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.1) (2019-11-04) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.1-rc.2...v1.7.1) From a0909df26923249291b2addfa815ad3728b9db4b Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 6 Nov 2019 14:17:09 +0000 Subject: [PATCH 0479/2372] v1.7.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5cc23a9fe3..0caff285e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.1", + "version": "1.7.2", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 86be607e92dbf148498e284d083e62b8716be2a8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 6 Nov 2019 10:52:00 -0700 Subject: [PATCH 0480/2372] onTileUpdate -> onMessageAllowed We keep onTileUpdate in MessgeEvent because it's a generic thing for the class to handle. onMessageAllowed is slightly different than onShowAllowed because "show allowed" doesn't quite make sense on its own, imo. --- src/components/views/messages/MessageEvent.js | 2 +- src/components/views/messages/MjolnirBody.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 0d22658884..e75bcc4332 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -112,7 +112,7 @@ module.exports = createReactClass({ replacingEventId={this.props.replacingEventId} editState={this.props.editState} onHeightChanged={this.props.onHeightChanged} - onTileUpdate={this.onTileUpdate} + onMessageAllowed={this.onTileUpdate} />; }, }); diff --git a/src/components/views/messages/MjolnirBody.js b/src/components/views/messages/MjolnirBody.js index d03c6c658d..baaee91657 100644 --- a/src/components/views/messages/MjolnirBody.js +++ b/src/components/views/messages/MjolnirBody.js @@ -21,7 +21,7 @@ import {_t} from '../../../languageHandler'; export default class MjolnirBody extends React.Component { static propTypes = { mxEvent: PropTypes.object.isRequired, - onTileUpdate: PropTypes.func.isRequired, + onMessageAllowed: PropTypes.func.isRequired, }; constructor() { @@ -34,7 +34,7 @@ export default class MjolnirBody extends React.Component { const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`; localStorage.setItem(key, "true"); - this.props.onTileUpdate(); + this.props.onMessageAllowed(); }; render() { From c00974d22d9e8b5b934b84980a30ea58a21e9590 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 7 Nov 2019 10:16:57 +0000 Subject: [PATCH 0481/2372] Now that part of spacing is padding, make it smaller when collapsed Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/structures/_RoomSubList.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index cd3a822d4d..be44563cfb 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -151,6 +151,7 @@ limitations under the License. .mx_RoomSubList_labelContainer { margin-right: 8px; margin-left: 2px; + padding: 0; } .mx_RoomSubList_addRoom { From 91f3b75d41d9f3fc2f88818ec5afb545e15c2850 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 7 Nov 2019 10:19:17 +0000 Subject: [PATCH 0482/2372] Remove variation selectors from quick reactions This fixes a regression introduced by the full emoji picker which inserted empty variation selectors in the thumbs up / down quick reactions. Since this makes them different characters, it would cause us to not aggregate thumbs from web vs. mobile together. Regressed by https://github.com/matrix-org/matrix-react-sdk/pull/3554 Fixes https://github.com/vector-im/riot-web/issues/11335 --- src/components/views/emojipicker/QuickReactions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/emojipicker/QuickReactions.js b/src/components/views/emojipicker/QuickReactions.js index 66248730f9..71a53984cc 100644 --- a/src/components/views/emojipicker/QuickReactions.js +++ b/src/components/views/emojipicker/QuickReactions.js @@ -21,7 +21,7 @@ import EMOJIBASE from 'emojibase-data/en/compact.json'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; -const QUICK_REACTIONS = ["👍️", "👎️", "😄", "🎉", "😕", "❤️", "🚀", "👀"]; +const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"]; EMOJIBASE.forEach(emoji => { const index = QUICK_REACTIONS.indexOf(emoji.unicode); if (index !== -1) { From ec2f3d36ea2d705bc330a949a0a665873ab66a03 Mon Sep 17 00:00:00 2001 From: Marco Zehe Date: Thu, 7 Nov 2019 14:41:01 +0100 Subject: [PATCH 0483/2372] Fix breadcrumbs so the bar is a toolbar and the buttons are buttons. Signed-off-by: Marco Zehe --- src/components/views/rooms/RoomBreadcrumbs.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index 8bee87728b..6529b5b1da 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -353,7 +353,6 @@ export default class RoomBreadcrumbs extends React.Component { onMouseEnter={() => this._onMouseEnter(r.room)} onMouseLeave={() => this._onMouseLeave(r.room)} aria-label={_t("Room %(name)s", {name: r.room.name})} - role="listitem" > {badge} @@ -363,7 +362,7 @@ export default class RoomBreadcrumbs extends React.Component { ); }); return ( -
    +
    Date: Thu, 7 Nov 2019 17:49:25 +0000 Subject: [PATCH 0484/2372] Restore thumbs after variation selector removal This more thorough change adjusts emoji processing to deal with variation selectors appropriately and revives the missing thumbs. Regressed by https://github.com/matrix-org/matrix-react-sdk/pull/3598 Fixes https://github.com/vector-im/riot-web/issues/11344 --- src/HtmlUtils.js | 21 ++++++++++++++----- .../views/emojipicker/EmojiPicker.js | 12 ++++++++++- .../views/emojipicker/QuickReactions.js | 14 +++++++------ 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 2266522bfe..2b7384a5aa 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -53,7 +53,6 @@ const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g"); const WHITESPACE_REGEX = new RegExp("\\s", "g"); const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); -const SINGLE_EMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})$`, 'i'); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; @@ -72,6 +71,21 @@ function mightContainEmoji(str) { return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str); } +/** + * Find emoji data in emojibase by character. + * + * @param {String} char The emoji character + * @return {Object} The emoji data + */ +export function findEmojiData(char) { + // Check against both the char and the char with an empty variation selector + // appended because that's how emojibase stores its base emojis which have + // variations. + // See also https://github.com/vector-im/riot-web/issues/9785. + const emptyVariation = char + VARIATION_SELECTOR; + return EMOJIBASE.find(e => e.unicode === char || e.unicode === emptyVariation); +} + /** * Returns the shortcode for an emoji character. * @@ -79,10 +93,7 @@ function mightContainEmoji(str) { * @return {String} The shortcode (such as :thumbup:) */ export function unicodeToShortcode(char) { - // Check against both the char and the char with an empty variation selector appended because that's how - // emoji-base stores its base emojis which have variations. https://github.com/vector-im/riot-web/issues/9785 - const emptyVariation = char + VARIATION_SELECTOR; - const data = EMOJIBASE.find(e => e.unicode === char || e.unicode === emptyVariation); + const data = findEmojiData(char); return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : ''); } diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.js index 48713b90d8..9fe8b4c81e 100644 --- a/src/components/views/emojipicker/EmojiPicker.js +++ b/src/components/views/emojipicker/EmojiPicker.js @@ -48,7 +48,14 @@ const DATA_BY_CATEGORY = { }; const DATA_BY_EMOJI = {}; +const VARIATION_SELECTOR = String.fromCharCode(0xFE0F); EMOJIBASE.forEach(emoji => { + if (emoji.unicode.includes(VARIATION_SELECTOR)) { + // Clone data into variation-less version + emoji = Object.assign({}, emoji, { + unicode: emoji.unicode.replace(VARIATION_SELECTOR, ""), + }); + } DATA_BY_EMOJI[emoji.unicode] = emoji; const categoryId = EMOJIBASE_CATEGORY_IDS[emoji.group]; if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) { @@ -82,7 +89,10 @@ class EmojiPicker extends React.Component { viewportHeight: 280, }; - this.recentlyUsed = recent.get().map(unicode => DATA_BY_EMOJI[unicode]); + // Convert recent emoji characters to emoji data, removing unknowns. + this.recentlyUsed = recent.get() + .map(unicode => DATA_BY_EMOJI[unicode]) + .filter(data => !!data); this.memoizedDataByCategory = { recent: this.recentlyUsed, ...DATA_BY_CATEGORY, diff --git a/src/components/views/emojipicker/QuickReactions.js b/src/components/views/emojipicker/QuickReactions.js index 71a53984cc..8444fb2d9c 100644 --- a/src/components/views/emojipicker/QuickReactions.js +++ b/src/components/views/emojipicker/QuickReactions.js @@ -16,17 +16,19 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import EMOJIBASE from 'emojibase-data/en/compact.json'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; +import { findEmojiData } from '../../../HtmlUtils'; -const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"]; -EMOJIBASE.forEach(emoji => { - const index = QUICK_REACTIONS.indexOf(emoji.unicode); - if (index !== -1) { - QUICK_REACTIONS[index] = emoji; +const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map(emoji => { + const data = findEmojiData(emoji); + if (!data) { + throw new Error(`Emoji ${emoji} doesn't exist in emojibase`); } + // Prefer our unicode value for quick reactions (which does not have + // variation selectors). + return Object.assign({}, data, { unicode: emoji }); }); class QuickReactions extends React.Component { From 9c4470e599006e15214d45267c1e551026dbc6ed Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 17:36:16 +0100 Subject: [PATCH 0485/2372] helper class to track the state of the verification as we will have 2 tiles, and both need to track the status of the verification request, I've put the logic for tracking the state in this helper class to use from both tiles. --- src/utils/KeyVerificationStateObserver.js | 153 ++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 src/utils/KeyVerificationStateObserver.js diff --git a/src/utils/KeyVerificationStateObserver.js b/src/utils/KeyVerificationStateObserver.js new file mode 100644 index 0000000000..b049b5d426 --- /dev/null +++ b/src/utils/KeyVerificationStateObserver.js @@ -0,0 +1,153 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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 MatrixClientPeg from '../MatrixClientPeg'; +import { _t } from '../languageHandler'; + +const SUB_EVENT_TYPES_OF_INTEREST = ["start", "cancel", "done"]; + +export default class KeyVerificationStateObserver { + constructor(requestEvent, client, updateCallback) { + this._requestEvent = requestEvent; + this._client = client; + this._updateCallback = updateCallback; + this.accepted = false; + this.done = false; + this.cancelled = false; + this._updateVerificationState(); + } + + attach() { + this._requestEvent.on("Event.relationsCreated", this._onRelationsCreated); + for (const phaseName of SUB_EVENT_TYPES_OF_INTEREST) { + this._tryListenOnRelationsForType(`m.key.verification.${phaseName}`); + } + } + + detach() { + const roomId = this._requestEvent.getRoomId(); + const room = this._client.getRoom(roomId); + + for (const phaseName of SUB_EVENT_TYPES_OF_INTEREST) { + const relations = room.getUnfilteredTimelineSet() + .getRelationsForEvent(this._requestEvent.getId(), "m.reference", `m.key.verification.${phaseName}`); + if (relations) { + relations.removeListener("Relations.add", this._onRelationsUpdated); + relations.removeListener("Relations.remove", this._onRelationsUpdated); + relations.removeListener("Relations.redaction", this._onRelationsUpdated); + } + } + this._requestEvent.removeListener("Event.relationsCreated", this._onRelationsCreated); + } + + _onRelationsCreated = (relationType, eventType) => { + if (relationType !== "m.reference") { + return; + } + if ( + eventType !== "m.key.verification.start" && + eventType !== "m.key.verification.cancel" && + eventType !== "m.key.verification.done" + ) { + return; + } + this._tryListenOnRelationsForType(eventType); + this._updateVerificationState(); + this._updateCallback(); + }; + + _tryListenOnRelationsForType(eventType) { + const roomId = this._requestEvent.getRoomId(); + const room = this._client.getRoom(roomId); + const relations = room.getUnfilteredTimelineSet() + .getRelationsForEvent(this._requestEvent.getId(), "m.reference", eventType); + if (relations) { + relations.on("Relations.add", this._onRelationsUpdated); + relations.on("Relations.remove", this._onRelationsUpdated); + relations.on("Relations.redaction", this._onRelationsUpdated); + } + } + + _onRelationsUpdated = (event) => { + this._updateVerificationState(); + this._updateCallback(); + }; + + _updateVerificationState() { + const roomId = this._requestEvent.getRoomId(); + const room = this._client.getRoom(roomId); + const timelineSet = room.getUnfilteredTimelineSet(); + const fromUserId = this._requestEvent.getSender(); + const content = this._requestEvent.getContent(); + const toUserId = content.to; + + this.cancelled = false; + this.done = false; + this.accepted = false; + this.otherPartyUserId = null; + this.cancelPartyUserId = null; + + const startRelations = timelineSet.getRelationsForEvent( + this._requestEvent.getId(), "m.reference", "m.key.verification.start"); + if (startRelations) { + for (const startEvent of startRelations.getRelations()) { + if (startEvent.getSender() === toUserId) { + this.accepted = true; + } + } + } + + const doneRelations = timelineSet.getRelationsForEvent( + this._requestEvent.getId(), "m.reference", "m.key.verification.done"); + if (doneRelations) { + let senderDone = false; + let receiverDone = false; + for (const doneEvent of doneRelations.getRelations()) { + if (doneEvent.getSender() === toUserId) { + receiverDone = true; + } else if (doneEvent.getSender() === fromUserId) { + senderDone = true; + } + } + if (senderDone && receiverDone) { + this.done = true; + } + } + + if (!this.done) { + const cancelRelations = timelineSet.getRelationsForEvent( + this._requestEvent.getId(), "m.reference", "m.key.verification.cancel"); + + if (cancelRelations) { + let earliestCancelEvent; + for (const cancelEvent of cancelRelations.getRelations()) { + // only accept cancellation from the users involved + if (cancelEvent.getSender() === toUserId || cancelEvent.getSender() === fromUserId) { + this.cancelled = true; + if (!earliestCancelEvent || cancelEvent.getTs() < earliestCancelEvent.getTs()) { + earliestCancelEvent = cancelEvent; + } + } + } + if (earliestCancelEvent) { + this.cancelPartyUserId = earliestCancelEvent.getSender(); + } + } + } + + this.otherPartyUserId = fromUserId === this._client.getUserId() ? toUserId : fromUserId; + } +} From 5c9e80a0ba12478e6358487538dc2ced2d2b2f8a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 17:38:52 +0100 Subject: [PATCH 0486/2372] add feature flag and send verification using DM from dialog if enabled --- .../views/dialogs/DeviceVerifyDialog.js | 59 ++++++++++++++++--- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 6 ++ 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index 710a92aa39..0e191cc192 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -24,6 +24,10 @@ import sdk from '../../../index'; import * as FormattingUtils from '../../../utils/FormattingUtils'; import { _t } from '../../../languageHandler'; import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; +import DMRoomMap from '../../../utils/DMRoomMap'; +import createRoom from "../../../createRoom"; +import dis from "../../../dispatcher"; +import SettingsStore from '../../../settings/SettingsStore'; const MODE_LEGACY = 'legacy'; const MODE_SAS = 'sas'; @@ -86,25 +90,37 @@ export default class DeviceVerifyDialog extends React.Component { this.props.onFinished(confirm); } - _onSasRequestClick = () => { + _onSasRequestClick = async () => { this.setState({ phase: PHASE_WAIT_FOR_PARTNER_TO_ACCEPT, }); - this._verifier = MatrixClientPeg.get().beginKeyVerification( - verificationMethods.SAS, this.props.userId, this.props.device.deviceId, - ); - this._verifier.on('show_sas', this._onVerifierShowSas); - this._verifier.verify().then(() => { + const client = MatrixClientPeg.get(); + const verifyingOwnDevice = this.props.userId === client.getUserId(); + try { + if (!verifyingOwnDevice && SettingsStore.getValue("feature_dm_verification")) { + const roomId = await ensureDMExistsAndOpen(this.props.userId); + // throws upon cancellation before having started + this._verifier = await client.requestVerificationDM( + this.props.userId, roomId, [verificationMethods.SAS], + ); + } else { + this._verifier = client.beginKeyVerification( + verificationMethods.SAS, this.props.userId, this.props.device.deviceId, + ); + } + this._verifier.on('show_sas', this._onVerifierShowSas); + // throws upon cancellation + await this._verifier.verify(); this.setState({phase: PHASE_VERIFIED}); this._verifier.removeListener('show_sas', this._onVerifierShowSas); this._verifier = null; - }).catch((e) => { + } catch (e) { console.log("Verification failed", e); this.setState({ phase: PHASE_CANCELLED, }); this._verifier = null; - }); + } } _onSasMatchesClick = () => { @@ -299,3 +315,30 @@ export default class DeviceVerifyDialog extends React.Component { } } +async function ensureDMExistsAndOpen(userId) { + const client = MatrixClientPeg.get(); + const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId); + const rooms = roomIds.map(id => client.getRoom(id)); + const suitableDMRooms = rooms.filter(r => { + if (r && r.getMyMembership() === "join") { + const member = r.getMember(userId); + return member && (member.membership === "invite" || member.membership === "join"); + } + return false; + }); + let roomId; + if (suitableDMRooms.length) { + const room = suitableDMRooms[0]; + roomId = room.roomId; + } else { + roomId = await createRoom({dmUserId: userId, spinner: false, andView: false}); + } + // don't use andView and spinner in createRoom, together, they cause this dialog to close and reopen, + // we causes us to loose the verifier and restart, and we end up having two verification requests + dis.dispatch({ + action: 'view_room', + room_id: roomId, + should_peek: false, + }); + return roomId; +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5af7e26b79..bcf68f8e4b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -339,6 +339,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Use the new, consistent UserInfo panel for Room Members and Group Members": "Use the new, consistent UserInfo panel for Room Members and Group Members", + "Send verification requests in direct message": "Send verification requests in direct message", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 2220435cb9..b169a0f29c 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -126,6 +126,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_dm_verification": { + isFeature: true, + displayName: _td("Send verification requests in direct message"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "useCiderComposer": { displayName: _td("Use the new, faster, composer for writing messages"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, From 0d2f9c42152c870d93f2259fc587c22d5822dc45 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 17:39:50 +0100 Subject: [PATCH 0487/2372] add verification request tile + styling --- res/css/_components.scss | 1 + .../messages/_MKeyVerificationRequest.scss | 93 ++++++++++++ res/css/views/rooms/_EventTile.scss | 25 ++++ res/themes/light/css/_light.scss | 2 + .../views/messages/MKeyVerificationRequest.js | 141 ++++++++++++++++++ src/utils/KeyVerificationStateObserver.js | 17 +++ 6 files changed, 279 insertions(+) create mode 100644 res/css/views/messages/_MKeyVerificationRequest.scss create mode 100644 src/components/views/messages/MKeyVerificationRequest.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 29c4d2c84c..5d26185393 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -118,6 +118,7 @@ @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; +@import "./views/messages/_MKeyVerificationRequest.scss"; @import "./views/messages/_MNoticeBody.scss"; @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; diff --git a/res/css/views/messages/_MKeyVerificationRequest.scss b/res/css/views/messages/_MKeyVerificationRequest.scss new file mode 100644 index 0000000000..aff44e4109 --- /dev/null +++ b/res/css/views/messages/_MKeyVerificationRequest.scss @@ -0,0 +1,93 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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. +*/ + +.mx_KeyVerification { + + display: grid; + grid-template-columns: 24px minmax(0, 1fr) min-content; + + &.mx_KeyVerification_icon::after { + grid-column: 1; + grid-row: 1 / 3; + width: 12px; + height: 16px; + content: ""; + mask: url("$(res)/img/e2e/verified.svg"); + mask-repeat: no-repeat; + mask-size: 100%; + margin-top: 4px; + background-color: $primary-fg-color; + } + + &.mx_KeyVerification_icon_verified::after { + background-color: $accent-color; + } + + .mx_KeyVerification_title, .mx_KeyVerification_subtitle, .mx_KeyVerification_state { + overflow-wrap: break-word; + } + + .mx_KeyVerification_title { + font-weight: 600; + font-size: 15px; + grid-column: 2; + grid-row: 1; + } + + .mx_KeyVerification_subtitle { + grid-column: 2; + grid-row: 2; + } + + .mx_KeyVerification_state, .mx_KeyVerification_subtitle { + font-size: 12px; + } + + .mx_KeyVerification_state, .mx_KeyVerification_buttons { + grid-column: 3; + grid-row: 1 / 3; + } + + .mx_KeyVerification_buttons { + align-items: center; + display: flex; + + .mx_AccessibleButton_kind_decline { + color: $notice-primary-color; + background-color: $notice-primary-bg-color; + } + + .mx_AccessibleButton_kind_accept { + color: $accent-color; + background-color: $accent-bg-color; + } + + [role=button] { + margin: 10px; + padding: 7px 15px; + border-radius: 5px; + height: min-content; + } + } + + .mx_KeyVerification_state { + width: 130px; + padding: 10px 20px; + margin: auto 0; + text-align: center; + color: $notice-secondary-color; + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index db34200b16..04c1065092 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -22,6 +22,15 @@ limitations under the License. position: relative; } +.mx_EventTile_bubble { + background-color: $dark-panel-bg-color; + padding: 10px; + border-radius: 5px; + margin: 10px auto; + max-width: 75%; + box-sizing: border-box; +} + .mx_EventTile.mx_EventTile_info { padding-top: 0px; } @@ -112,6 +121,21 @@ limitations under the License. line-height: 22px; } +.mx_EventTile_bubbleContainer.mx_EventTile_bubbleContainer { + display: grid; + grid-template-columns: 1fr 100px; + + .mx_EventTile_line { + margin-right: 0px; + grid-column: 1 / 3; + padding: 0; + } + + .mx_EventTile_msgOption { + grid-column: 2; + } +} + .mx_EventTile_reply { margin-right: 10px; } @@ -617,4 +641,5 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { } } } + /* stylelint-enable no-descending-specificity */ diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index b412261d10..dcd7ce166e 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -12,7 +12,9 @@ $monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emo // unified palette // try to use these colors when possible $accent-color: #03b381; +$accent-bg-color: rgba(115, 247, 91, 0.08); $notice-primary-color: #ff4b55; +$notice-primary-bg-color: rgba(255, 75, 85, 0.08); $notice-secondary-color: #61708b; $header-panel-bg-color: #f3f8fd; diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.js new file mode 100644 index 0000000000..21d82309ed --- /dev/null +++ b/src/components/views/messages/MKeyVerificationRequest.js @@ -0,0 +1,141 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; +import sdk from '../../../index'; +import Modal from "../../../Modal"; +import { _t } from '../../../languageHandler'; +import KeyVerificationStateObserver, {getNameForEventRoom, userLabelForEventRoom} + from '../../../utils/KeyVerificationStateObserver'; + +export default class MKeyVerificationRequest extends React.Component { + constructor(props) { + super(props); + this.keyVerificationState = new KeyVerificationStateObserver(this.props.mxEvent, MatrixClientPeg.get(), () => { + this.setState(this._copyState()); + }); + this.state = this._copyState(); + } + + _copyState() { + const {accepted, done, cancelled, cancelPartyUserId, otherPartyUserId} = this.keyVerificationState; + return {accepted, done, cancelled, cancelPartyUserId, otherPartyUserId}; + } + + componentDidMount() { + this.keyVerificationState.attach(); + } + + componentWillUnmount() { + this.keyVerificationState.detach(); + } + + _onAcceptClicked = () => { + const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); + // todo: validate event, for example if it has sas in the methods. + const verifier = MatrixClientPeg.get().acceptVerificationDM(this.props.mxEvent, verificationMethods.SAS); + Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { + verifier, + }); + }; + + _onRejectClicked = () => { + // todo: validate event, for example if it has sas in the methods. + const verifier = MatrixClientPeg.get().acceptVerificationDM(this.props.mxEvent, verificationMethods.SAS); + verifier.cancel("User declined"); + }; + + _acceptedLabel(userId) { + const client = MatrixClientPeg.get(); + const myUserId = client.getUserId(); + if (userId === myUserId) { + return _t("You accepted"); + } else { + return _t("%(name)s accepted", {name: getNameForEventRoom(userId, this.props.mxEvent)}); + } + } + + _cancelledLabel(userId) { + const client = MatrixClientPeg.get(); + const myUserId = client.getUserId(); + if (userId === myUserId) { + return _t("You cancelled"); + } else { + return _t("%(name)s cancelled", {name: getNameForEventRoom(userId, this.props.mxEvent)}); + } + } + + render() { + const {mxEvent} = this.props; + const fromUserId = mxEvent.getSender(); + const content = mxEvent.getContent(); + const toUserId = content.to; + const client = MatrixClientPeg.get(); + const myUserId = client.getUserId(); + const isOwn = fromUserId === myUserId; + + let title; + let subtitle; + let stateNode; + + if (this.state.accepted || this.state.cancelled) { + let stateLabel; + if (this.state.accepted) { + stateLabel = this._acceptedLabel(toUserId); + } else if (this.state.cancelled) { + stateLabel = this._cancelledLabel(this.state.cancelPartyUserId); + } + stateNode = (
    {stateLabel}
    ); + } + + if (toUserId === myUserId) { // request sent to us + title = (
    { + _t("%(name)s wants to verify", {name: getNameForEventRoom(fromUserId, mxEvent)})}
    ); + subtitle = (
    { + userLabelForEventRoom(fromUserId, mxEvent)}
    ); + const isResolved = !(this.state.accepted || this.state.cancelled || this.state.done); + if (isResolved) { + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + stateNode = (
    + {_t("Decline")} + {_t("Accept")} +
    ); + } + } else if (isOwn) { // request sent by us + title = (
    { + _t("You sent a verification request")}
    ); + subtitle = (
    { + userLabelForEventRoom(this.state.otherPartyUserId, mxEvent)}
    ); + } + + if (title) { + return (
    + {title} + {subtitle} + {stateNode} +
    ); + } + return null; + } +} + +MKeyVerificationRequest.propTypes = { + /* the MatrixEvent to show */ + mxEvent: PropTypes.object.isRequired, +}; diff --git a/src/utils/KeyVerificationStateObserver.js b/src/utils/KeyVerificationStateObserver.js index b049b5d426..7de50ec4bf 100644 --- a/src/utils/KeyVerificationStateObserver.js +++ b/src/utils/KeyVerificationStateObserver.js @@ -151,3 +151,20 @@ export default class KeyVerificationStateObserver { this.otherPartyUserId = fromUserId === this._client.getUserId() ? toUserId : fromUserId; } } + +export function getNameForEventRoom(userId, mxEvent) { + const roomId = mxEvent.getRoomId(); + const client = MatrixClientPeg.get(); + const room = client.getRoom(roomId); + const member = room.getMember(userId); + return member ? member.name : userId; +} + +export function userLabelForEventRoom(userId, mxEvent) { + const name = getNameForEventRoom(userId, mxEvent); + if (name !== userId) { + return _t("%(name)s (%(userId)s)", {name, userId}); + } else { + return userId; + } +} From e8c21a341cf8dc96f3f57b8824bf5d78663ad660 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 17:40:22 +0100 Subject: [PATCH 0488/2372] add key verification conclusion tile --- .../messages/MKeyVerificationConclusion.js | 130 ++++++++++++++++++ src/i18n/strings/en_EN.json | 10 ++ 2 files changed, 140 insertions(+) create mode 100644 src/components/views/messages/MKeyVerificationConclusion.js diff --git a/src/components/views/messages/MKeyVerificationConclusion.js b/src/components/views/messages/MKeyVerificationConclusion.js new file mode 100644 index 0000000000..e955d6159d --- /dev/null +++ b/src/components/views/messages/MKeyVerificationConclusion.js @@ -0,0 +1,130 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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 classNames from 'classnames'; +import PropTypes from 'prop-types'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import { _t } from '../../../languageHandler'; +import KeyVerificationStateObserver, {getNameForEventRoom, userLabelForEventRoom} + from '../../../utils/KeyVerificationStateObserver'; + +export default class MKeyVerificationConclusion extends React.Component { + constructor(props) { + super(props); + this.keyVerificationState = null; + this.state = { + done: false, + cancelled: false, + otherPartyUserId: null, + cancelPartyUserId: null, + }; + const rel = this.props.mxEvent.getRelation(); + if (rel) { + const client = MatrixClientPeg.get(); + const room = client.getRoom(this.props.mxEvent.getRoomId()); + const requestEvent = room.findEventById(rel.event_id); + if (requestEvent) { + this._createStateObserver(requestEvent, client); + this.state = this._copyState(); + } else { + const findEvent = event => { + if (event.getId() === rel.event_id) { + this._createStateObserver(event, client); + this.setState(this._copyState()); + room.removeListener("Room.timeline", findEvent); + } + }; + room.on("Room.timeline", findEvent); + } + } + } + + _createStateObserver(requestEvent, client) { + this.keyVerificationState = new KeyVerificationStateObserver(requestEvent, client, () => { + this.setState(this._copyState()); + }); + } + + _copyState() { + const {done, cancelled, otherPartyUserId, cancelPartyUserId} = this.keyVerificationState; + return {done, cancelled, otherPartyUserId, cancelPartyUserId}; + } + + componentDidMount() { + if (this.keyVerificationState) { + this.keyVerificationState.attach(); + } + } + + componentWillUnmount() { + if (this.keyVerificationState) { + this.keyVerificationState.detach(); + } + } + + _getName(userId) { + const roomId = this.props.mxEvent.getRoomId(); + const client = MatrixClientPeg.get(); + const room = client.getRoom(roomId); + const member = room.getMember(userId); + return member ? member.name : userId; + } + + _userLabel(userId) { + const name = this._getName(userId); + if (name !== userId) { + return _t("%(name)s (%(userId)s)", {name, userId}); + } else { + return userId; + } + } + + render() { + const {mxEvent} = this.props; + const client = MatrixClientPeg.get(); + const myUserId = client.getUserId(); + let title; + + if (this.state.done) { + title = _t("You verified %(name)s", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); + } else if (this.state.cancelled) { + if (mxEvent.getSender() === myUserId) { + title = _t("You cancelled verifying %(name)s", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); + } else if (mxEvent.getSender() === this.state.otherPartyUserId) { + title = _t("%(name)s cancelled verifying", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); + } + } + + if (title) { + const subtitle = userLabelForEventRoom(this.state.otherPartyUserId, mxEvent); + const classes = classNames("mx_EventTile_bubble", "mx_KeyVerification", "mx_KeyVerification_icon", { + mx_KeyVerification_icon_verified: this.state.done, + }); + return (
    +
    {title}
    +
    {subtitle}
    +
    ); + } + + return null; + } +} + +MKeyVerificationConclusion.propTypes = { + /* the MatrixEvent to show */ + mxEvent: PropTypes.object.isRequired, +}; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index bcf68f8e4b..1dcd2a5129 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1065,6 +1065,16 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Show image": "Show image", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "You verified %(name)s": "You verified %(name)s", + "You cancelled verifying %(name)s": "You cancelled verifying %(name)s", + "%(name)s cancelled verifying": "%(name)s cancelled verifying", + "You accepted": "You accepted", + "%(name)s accepted": "%(name)s accepted", + "You cancelled": "You cancelled", + "%(name)s cancelled": "%(name)s cancelled", + "%(name)s wants to verify": "%(name)s wants to verify", + "You sent a verification request": "You sent a verification request", "Error decrypting video": "Error decrypting video", "Show all": "Show all", "reacted with %(shortName)s": "reacted with %(shortName)s", From 9d67fa9fa185ded789cedb0abbdf7943d7c58f63 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 17:42:06 +0100 Subject: [PATCH 0489/2372] render verification request with correct tile only if the request was send by or to us, otherwise ignore. --- src/components/views/rooms/EventTile.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 9497324f5a..fca77fcaf6 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -33,6 +33,7 @@ import dis from '../../../dispatcher'; import SettingsStore from "../../../settings/SettingsStore"; import {EventStatus, MatrixClient} from 'matrix-js-sdk'; import {formatTime} from "../../../DateUtils"; +import MatrixClientPeg from '../../../MatrixClientPeg'; const ObjectUtils = require('../../../ObjectUtils'); @@ -68,6 +69,21 @@ const stateEventTileTypes = { function getHandlerTile(ev) { const type = ev.getType(); + + // don't show verification requests we're not involved in, + // not even when showing hidden events + if (type === "m.room.message") { + const content = ev.getContent(); + if (content && content.msgtype === "m.key.verification.request") { + const client = MatrixClientPeg.get(); + const me = client && client.getUserId(); + if (ev.getSender() !== me && content.to !== me) { + return undefined; + } else { + return "messages.MKeyVerificationRequest"; + } + } + } return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type]; } From d7f5252f9afa237841f9c78baac9ae33c91230b8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 17:43:50 +0100 Subject: [PATCH 0490/2372] render done and cancel event as conclusion tile don't render any done events not sent by us, as done events are sent by both parties and we don't want to render two conclusion tiles. cancel events should be only sent by one party. --- src/components/views/rooms/EventTile.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index fca77fcaf6..de13fa30e2 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -40,6 +40,8 @@ const ObjectUtils = require('../../../ObjectUtils'); const eventTileTypes = { 'm.room.message': 'messages.MessageEvent', 'm.sticker': 'messages.MessageEvent', + 'm.key.verification.cancel': 'messages.MKeyVerificationConclusion', + 'm.key.verification.done': 'messages.MKeyVerificationConclusion', 'm.call.invite': 'messages.TextualEvent', 'm.call.answer': 'messages.TextualEvent', 'm.call.hangup': 'messages.TextualEvent', @@ -84,6 +86,16 @@ function getHandlerTile(ev) { } } } + // these events are sent by both parties during verification, but we only want to render one + // tile once the verification concludes, so filter out the one from the other party. + if (type === "m.key.verification.done") { + const client = MatrixClientPeg.get(); + const me = client && client.getUserId(); + if (ev.getSender() !== me) { + return undefined; + } + } + return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type]; } From 805c83779a2bf1420fb40118041a197b6f26e828 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 17:44:53 +0100 Subject: [PATCH 0491/2372] support bubble tile style for verification tiles --- src/components/views/rooms/EventTile.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index de13fa30e2..7105ee2635 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -555,8 +555,10 @@ module.exports = createReactClass({ const eventType = this.props.mxEvent.getType(); // Info messages are basically information about commands processed on a room + const isBubbleMessage = eventType.startsWith("m.key.verification") || + (eventType === "m.room.message" && msgtype.startsWith("m.key.verification")); let isInfoMessage = ( - eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create' + !isBubbleMessage && eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create' ); let tileHandler = getHandlerTile(this.props.mxEvent); @@ -589,6 +591,7 @@ module.exports = createReactClass({ const isEditing = !!this.props.editState; const classes = classNames({ + mx_EventTile_bubbleContainer: isBubbleMessage, mx_EventTile: true, mx_EventTile_isEditing: isEditing, mx_EventTile_info: isInfoMessage, @@ -624,7 +627,7 @@ module.exports = createReactClass({ if (this.props.tileShape === "notif") { avatarSize = 24; needsSenderProfile = true; - } else if (tileHandler === 'messages.RoomCreate') { + } else if (tileHandler === 'messages.RoomCreate' || isBubbleMessage) { avatarSize = 0; needsSenderProfile = false; } else if (isInfoMessage) { @@ -822,7 +825,7 @@ module.exports = createReactClass({ { readAvatars }
    { sender } -
    +
    Date: Thu, 7 Nov 2019 20:01:33 +0100 Subject: [PATCH 0492/2372] string has moved in i18n apparently --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1dcd2a5129..5f6e327944 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -291,6 +291,7 @@ "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", "%(items)s and %(count)s others|one": "%(items)s and one other", "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Not a valid Riot keyfile": "Not a valid Riot keyfile", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", @@ -1065,7 +1066,6 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Show image": "Show image", - "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", "You verified %(name)s": "You verified %(name)s", "You cancelled verifying %(name)s": "You cancelled verifying %(name)s", "%(name)s cancelled verifying": "%(name)s cancelled verifying", From d83f3632f6f39f7f6d017dc2d54a3e514aea3434 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 20:04:36 +0100 Subject: [PATCH 0493/2372] make the linter happy --- src/components/views/messages/MKeyVerificationConclusion.js | 6 ++++-- src/components/views/rooms/EventTile.js | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/MKeyVerificationConclusion.js b/src/components/views/messages/MKeyVerificationConclusion.js index e955d6159d..0bd8e2d3d8 100644 --- a/src/components/views/messages/MKeyVerificationConclusion.js +++ b/src/components/views/messages/MKeyVerificationConclusion.js @@ -103,9 +103,11 @@ export default class MKeyVerificationConclusion extends React.Component { title = _t("You verified %(name)s", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); } else if (this.state.cancelled) { if (mxEvent.getSender() === myUserId) { - title = _t("You cancelled verifying %(name)s", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); + title = _t("You cancelled verifying %(name)s", + {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); } else if (mxEvent.getSender() === this.state.otherPartyUserId) { - title = _t("%(name)s cancelled verifying", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); + title = _t("%(name)s cancelled verifying", + {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); } } diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 7105ee2635..786a72f5b3 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -558,7 +558,8 @@ module.exports = createReactClass({ const isBubbleMessage = eventType.startsWith("m.key.verification") || (eventType === "m.room.message" && msgtype.startsWith("m.key.verification")); let isInfoMessage = ( - !isBubbleMessage && eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create' + !isBubbleMessage && eventType !== 'm.room.message' && + eventType !== 'm.sticker' && eventType != 'm.room.create' ); let tileHandler = getHandlerTile(this.props.mxEvent); From 2516d8ee61de0a9f76ee4c44fc8084de2b7befa4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 20:11:31 +0100 Subject: [PATCH 0494/2372] fix repeated css class --- res/css/views/rooms/_EventTile.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 04c1065092..98bfa248ff 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -121,7 +121,7 @@ limitations under the License. line-height: 22px; } -.mx_EventTile_bubbleContainer.mx_EventTile_bubbleContainer { +.mx_EventTile_bubbleContainer { display: grid; grid-template-columns: 1fr 100px; From cf80cb559e35b5c35694664921c98dd143fc4863 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 7 Nov 2019 15:09:23 -0700 Subject: [PATCH 0495/2372] Match identity server registration to the IS r0.3.0 spec The returned field is `token` for the spec, but we somehow got through with `access_token` on Sydent. --- src/IdentityAuthClient.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index f21db12c51..24f11be474 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -183,8 +183,10 @@ export default class IdentityAuthClient { async registerForToken(check=true) { try { const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken(); - const { access_token: identityAccessToken } = + // XXX: The spec is `token`, but we used `access_token` for a Sydent release. + const { access_token, token } = await this._matrixClient.registerWithIdentityServer(hsOpenIdToken); + let identityAccessToken = token ? token : access_token; if (check) await this._checkToken(identityAccessToken); return identityAccessToken; } catch (e) { From f0e02f59b46b04f01a3ea1ea09e9aa4f30129874 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 7 Nov 2019 15:12:55 -0700 Subject: [PATCH 0496/2372] Appease the linter --- src/IdentityAuthClient.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 24f11be474..c82c93e7a6 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -184,9 +184,9 @@ export default class IdentityAuthClient { try { const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken(); // XXX: The spec is `token`, but we used `access_token` for a Sydent release. - const { access_token, token } = + const { access_token: accessToken, token } = await this._matrixClient.registerWithIdentityServer(hsOpenIdToken); - let identityAccessToken = token ? token : access_token; + const identityAccessToken = token ? token : accessToken; if (check) await this._checkToken(identityAccessToken); return identityAccessToken; } catch (e) { From 4283f9ec74b50748ebcc18360c68dab5e0c86eae Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Nov 2019 16:01:53 +0000 Subject: [PATCH 0497/2372] Split CSS rule to fix descending specificity lint error --- res/css/views/rooms/_EventTile.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 98bfa248ff..81924d2be3 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -130,10 +130,6 @@ limitations under the License. grid-column: 1 / 3; padding: 0; } - - .mx_EventTile_msgOption { - grid-column: 2; - } } .mx_EventTile_reply { @@ -278,6 +274,10 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { margin-right: 10px; } +.mx_EventTile_bubbleContainer.mx_EventTile_msgOption { + grid-column: 2; +} + .mx_EventTile_msgOption a { text-decoration: none; } From 3070ee6d7b370ae13f9d0d2b6e7f759daf83bb0a Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Nov 2019 16:10:51 +0000 Subject: [PATCH 0498/2372] Put back the grouped rule & disable the linting rule instead --- .stylelintrc.js | 1 + res/css/views/rooms/_EventTile.scss | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.stylelintrc.js b/.stylelintrc.js index f028c76cc0..1690f2186f 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -15,6 +15,7 @@ module.exports = { "number-leading-zero": null, "selector-list-comma-newline-after": null, "at-rule-no-unknown": null, + "no-descending-specificity": null, "scss/at-rule-no-unknown": [true, { // https://github.com/vector-im/riot-web/issues/10544 "ignoreAtRules": ["define-mixin"], diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 81924d2be3..98bfa248ff 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -130,6 +130,10 @@ limitations under the License. grid-column: 1 / 3; padding: 0; } + + .mx_EventTile_msgOption { + grid-column: 2; + } } .mx_EventTile_reply { @@ -274,10 +278,6 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { margin-right: 10px; } -.mx_EventTile_bubbleContainer.mx_EventTile_msgOption { - grid-column: 2; -} - .mx_EventTile_msgOption a { text-decoration: none; } From 06ab9efed639c8944c6181290c6683122151540a Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Tue, 5 Nov 2019 07:16:59 +0000 Subject: [PATCH 0499/2372] Translated using Weblate (Bulgarian) Currently translated at 100.0% (1853 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/bg/ --- src/i18n/strings/bg.json | 47 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index 2287c5b295..6f198b2e5a 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -2204,5 +2204,50 @@ "wait and try again later": "изчакате и опитате пак", "Clear cache and reload": "Изчисти кеша и презареди", "Show tray icon and minimize window to it on close": "Показвай икона в лентата и минимизирай прозореца там при затваряне", - "Your email address hasn't been verified yet": "Имейл адресът ви все още не е потвърден" + "Your email address hasn't been verified yet": "Имейл адресът ви все още не е потвърден", + "Click the link in the email you received to verify and then click continue again.": "Кликнете на връзката получена по имейл за да потвърдите, а след това натиснете продължи отново.", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "На път сте да премахнете 1 съобщение от %(user)s. Това е необратимо. Искате ли да продължите?", + "Remove %(count)s messages|one": "Премахни 1 съобщение", + "Room %(name)s": "Стая %(name)s", + "Recent rooms": "Скорошни стаи", + "%(count)s unread messages including mentions.|other": "%(count)s непрочетени съобщения, включително споменавания.", + "%(count)s unread messages including mentions.|one": "1 непрочетено споменаване.", + "%(count)s unread messages.|other": "%(count)s непрочетени съобщения.", + "%(count)s unread messages.|one": "1 непрочетено съобщение.", + "Unread mentions.": "Непрочетени споменавания.", + "Unread messages.": "Непрочетени съобщения.", + "Trust & Devices": "Доверие и устройства", + "Direct messages": "Директни съобщения", + "Failed to deactivate user": "Неуспешно деактивиране на потребител", + "This client does not support end-to-end encryption.": "Този клиент не поддържа шифроване от край до край.", + "Messages in this room are not end-to-end encrypted.": "Съобщенията в тази стая не са шифровани от край до край.", + "React": "Реагирай", + "Message Actions": "Действия със съобщението", + "Show image": "Покажи снимката", + "Frequently Used": "Често използвани", + "Smileys & People": "Усмивки и хора", + "Animals & Nature": "Животни и природа", + "Food & Drink": "Храна и напитки", + "Activities": "Действия", + "Travel & Places": "Пътуване и места", + "Objects": "Обекти", + "Symbols": "Символи", + "Flags": "Знамена", + "Quick Reactions": "Бързи реакции", + "Cancel search": "Отмени търсенето", + "Please create a new issue on GitHub so that we can investigate this bug.": "Моля, отворете нов проблем в GitHub за да проучим проблема.", + "To continue you need to accept the terms of this service.": "За да продължите, трябва да приемете условията за ползване.", + "Document": "Документ", + "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Липсва публичния ключ за catcha в конфигурацията на сървъра. Съобщете това на администратора на сървъра.", + "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Не е конфигуриран сървър за самоличност, така че не можете да добавите имейл адрес за възстановяване на паролата в бъдеще.", + "%(creator)s created and configured the room.": "%(creator)s създаде и настрой стаята.", + "Jump to first unread room.": "Отиди до първата непрочетена стая.", + "Jump to first invite.": "Отиди до първата покана.", + "Command Autocomplete": "Подсказка за команди", + "Community Autocomplete": "Подсказка за общности", + "DuckDuckGo Results": "DuckDuckGo резултати", + "Emoji Autocomplete": "Подсказка за емоджита", + "Notification Autocomplete": "Подсказка за уведомления", + "Room Autocomplete": "Подсказка за стаи", + "User Autocomplete": "Подсказка за потребители" } From dc5abbe3809831e00a8d2d1f2ed85d8c77bf364a Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Tue, 5 Nov 2019 03:58:00 +0000 Subject: [PATCH 0500/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1853 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 94183ef83f..58dca89415 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2272,5 +2272,6 @@ "Unread messages.": "未讀的訊息。", "Show tray icon and minimize window to it on close": "顯示系統匣圖示並在關閉視窗時將其最小化至其中", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "此動作需要存取預設的身份識別伺服器 以驗證電子郵件或電話號碼,但伺服器沒有任何服務條款。", - "Trust": "信任" + "Trust": "信任", + "Message Actions": "訊息動作" } From 6d4971c29eb2b760f15dae41085d5b164d5dc8e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Mon, 4 Nov 2019 12:28:24 +0000 Subject: [PATCH 0501/2372] Translated using Weblate (French) Currently translated at 100.0% (1853 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 7807facb1c..328c3b7f9e 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2279,5 +2279,6 @@ "Unread messages.": "Messages non lus.", "Show tray icon and minimize window to it on close": "Afficher l’icône dans la barre d’état et minimiser la fenêtre lors de la fermeture", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Cette action nécessite l’accès au serveur d’identité par défaut afin de valider une adresse e-mail ou un numéro de téléphone, mais le serveur n’a aucune condition de service.", - "Trust": "Confiance" + "Trust": "Confiance", + "Message Actions": "Actions de message" } From 4eb39190b493159f863debf7987b86980f8cd5b7 Mon Sep 17 00:00:00 2001 From: dreamerchris Date: Tue, 5 Nov 2019 12:12:12 +0000 Subject: [PATCH 0502/2372] Translated using Weblate (Greek) Currently translated at 39.7% (735 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/el/ --- src/i18n/strings/el.json | 91 +++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index c5d6468881..a2438cc22c 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -57,7 +57,7 @@ "%(senderDisplayName)s removed the room name.": "Ο %(senderDisplayName)s διέγραψε το όνομα του δωματίου.", "Changes your display nickname": "Αλλάζει το ψευδώνυμο χρήστη", "Conference call failed.": "Η κλήση συνδιάσκεψης απέτυχε.", - "powered by Matrix": "με τη βοήθεια του Matrix", + "powered by Matrix": "λειτουργεί με το Matrix", "Confirm password": "Επιβεβαίωση κωδικού πρόσβασης", "Confirm your new password": "Επιβεβαίωση του νέου κωδικού πρόσβασης", "Continue": "Συνέχεια", @@ -567,7 +567,7 @@ "numbullet": "απαρίθμηση", "You must join the room to see its files": "Πρέπει να συνδεθείτε στο δωμάτιο για να δείτε τα αρχεία του", "Reject all %(invitedRooms)s invites": "Απόρριψη όλων των προσκλήσεων %(invitedRooms)s", - "Failed to invite the following users to the %(roomName)s room:": "Δεν ήταν δυνατή η πρόσκληση των χρηστών στο δωμάτιο %(roomName)s:", + "Failed to invite the following users to the %(roomName)s room:": "Δεν ήταν δυνατή η πρόσκληση των παρακάτω χρηστών στο δωμάτιο %(roomName)s:", "Deops user with given id": "Deop χρήστη με το συγκεκριμένο αναγνωριστικό", "Drop here to tag %(section)s": "Απόθεση εδώ για ορισμό ετικέτας στο %(section)s", "Join as voice or video.": "Συμμετάσχετε με φωνή ή βίντεο.", @@ -575,7 +575,7 @@ "Show timestamps in 12 hour format (e.g. 2:30pm)": "Εμφάνιση χρονικών σημάνσεων σε 12ωρη μορφή ώρας (π.χ. 2:30 μ.μ.)", "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Το κλειδί υπογραφής που δώσατε αντιστοιχεί στο κλειδί υπογραφής που λάβατε από τη συσκευή %(userId)s %(deviceId)s. Η συσκευή έχει επισημανθεί ως επιβεβαιωμένη.", "To link to a room it must have an address.": "Για να συνδεθείτε σε ένα δωμάτιο πρέπει να έχετε μια διεύθυνση.", - "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Η διεύθυνση ηλεκτρονικής αλληλογραφίας σας δεν φαίνεται να συσχετίζεται με Matrix ID σε αυτόν τον διακομιστή.", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Η διεύθυνση της ηλ. αλληλογραφίας σας δεν φαίνεται να συσχετίζεται με μια ταυτότητα Matrix σε αυτόν τον Διακομιστή Φιλοξενίας.", "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Ο κωδικός πρόσβασής σας άλλαξε επιτυχώς. Δεν θα λάβετε ειδοποιήσεις push σε άλλες συσκευές μέχρι να συνδεθείτε ξανά σε αυτές", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "Δεν θα μπορέσετε να αναιρέσετε αυτήν την αλλαγή καθώς προωθείτε τον χρήστη να έχει το ίδιο επίπεδο δύναμης με τον εαυτό σας.", "Sent messages will be stored until your connection has returned.": "Τα απεσταλμένα μηνύματα θα αποθηκευτούν μέχρι να αακτηθεί η σύνδεσή σας.", @@ -765,7 +765,7 @@ "The platform you're on": "Η πλατφόρμα στην οποία βρίσκεστε", "The version of Riot.im": "Η έκδοση του Riot.im", "Your language of choice": "Η γλώσσα επιλογής σας", - "Your homeserver's URL": "Το URL του διακομιστή σας", + "Your homeserver's URL": "Το URL του διακομιστή φιλοξενίας σας", "Every page you use in the app": "Κάθε σελίδα που χρησιμοποιείτε στην εφαρμογή", "e.g. ": "π.χ. ", "Your device resolution": "Η ανάλυση της συσκευής σας", @@ -774,7 +774,7 @@ "Whether or not you're logged in (we don't record your user name)": "Εάν είστε συνδεδεμένος/η ή όχι (δεν καταγράφουμε το όνομα χρήστη σας)", "e.g. %(exampleValue)s": "π.χ. %(exampleValue)s", "Review Devices": "Ανασκόπηση συσκευών", - "Call Anyway": "Κλήση όπως και να 'χει", + "Call Anyway": "Πραγματοποίηση Κλήσης όπως και να 'χει", "Answer Anyway": "Απάντηση όπως και να 'χει", "Call": "Κλήση", "Answer": "Απάντηση", @@ -785,20 +785,20 @@ "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Προσοχή: κάθε άτομο που προσθέτετε στην κοινότητα θε είναι δημοσίως ορατό σε οποιονδήποτε γνωρίζει το αναγνωριστικό της κοινότητας", "Invite new community members": "Προσκαλέστε νέα μέλη στην κοινότητα", "Name or matrix ID": "Όνομα ή αναγνωριστικό του matrix", - "Invite to Community": "Πρόσκληση στην κοινότητα", + "Invite to Community": "Προσκαλέστε στην κοινότητα", "Which rooms would you like to add to this community?": "Ποια δωμάτια θα θέλατε να προσθέσετε σε αυτή την κοινότητα;", "Add rooms to the community": "Προσθήκη δωματίων στην κοινότητα", "Add to community": "Προσθήκη στην κοινότητα", - "Failed to invite the following users to %(groupId)s:": "Αποτυχία πρόσκλησης των ακόλουθων χρηστών στο %(groupId)s :", + "Failed to invite the following users to %(groupId)s:": "Αποτυχία πρόσκλησης στο %(groupId)s των χρηστών:", "Failed to invite users to community": "Αποτυχία πρόσκλησης χρηστών στην κοινότητα", "Failed to invite users to %(groupId)s": "Αποτυχία πρόσκλησης χρηστών στο %(groupId)s", - "Failed to add the following rooms to %(groupId)s:": "Αποτυχία προσθήκης των ακόλουθων δωματίων στο %(groupId)s:", + "Failed to add the following rooms to %(groupId)s:": "Αποτυχία προσθήκης στο %(groupId)s των δωματίων:", "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Υπάρχουν άγνωστες συσκευές στο δωμάτιο: εάν συνεχίσετε χωρίς να τις επιβεβαιώσετε, θα μπορούσε κάποιος να κρυφακούει την κλήση σας.", "Show these rooms to non-members on the community page and room list?": "Εμφάνιση αυτών των δωματίων σε μη-μέλη στην σελίδα της κοινότητας και στη λίστα δωματίων;", "Room name or alias": "Όνομα η ψευδώνυμο δωματίου", - "Restricted": "Περιορισμένο", - "Unable to create widget.": "Αδυναμία δημιουργίας widget.", - "Reload widget": "Ανανέωση widget", + "Restricted": "Περιορισμένο/η", + "Unable to create widget.": "Αδυναμία δημιουργίας γραφικού στοιχείου.", + "Reload widget": "Επαναφόρτωση γραφικού στοιχείου", "You are not in this room.": "Δεν είστε μέλος αυτού του δωματίου.", "You do not have permission to do that in this room.": "Δεν έχετε την άδεια να το κάνετε αυτό σε αυτό το δωμάτιο.", "You are now ignoring %(userId)s": "Τώρα αγνοείτε τον/την %(userId)s", @@ -818,14 +818,14 @@ "Delete %(count)s devices|other": "Διαγραφή %(count)s συσκευών", "Delete %(count)s devices|one": "Διαγραφή συσκευής", "Select devices": "Επιλογή συσκευών", - "Cannot add any more widgets": "Δεν είναι δυνατή η προσθήκη άλλων widget", - "The maximum permitted number of widgets have already been added to this room.": "Ο μέγιστος επιτρεπτός αριθμός widget έχει ήδη προστεθεί σε αυτό το δωμάτιο.", - "Add a widget": "Προσθήκη widget", + "Cannot add any more widgets": "Δεν είναι δυνατή η προσθήκη άλλων γραφικών στοιχείων", + "The maximum permitted number of widgets have already been added to this room.": "Ο μέγιστος επιτρεπτός αριθμός γραφικών στοιχείων έχει ήδη προστεθεί σε αυτό το δωμάτιο.", + "Add a widget": "Προσθέστε ένα γραφικό στοιχείο", "%(senderName)s sent an image": "Ο/Η %(senderName)s έστειλε μία εικόνα", "%(senderName)s sent a video": "Ο/Η %(senderName)s έστειλε ένα βίντεο", "%(senderName)s uploaded a file": "Ο/Η %(senderName)s αναφόρτωσε ένα αρχείο", "If your other devices do not have the key for this message you will not be able to decrypt them.": "Εάν οι άλλες συσκευές σας δεν έχουν το κλειδί για αυτό το μήνυμα, τότε δεν θα μπορείτε να το αποκρυπτογραφήσετε.", - "Disinvite this user?": "Ακύρωση πρόσκλησης αυτού του χρήστη;", + "Disinvite this user?": "Απόσυρση της πρόσκλησης αυτού του χρήστη;", "Mention": "Αναφορά", "Invite": "Πρόσκληση", "User Options": "Επιλογές Χρήστη", @@ -849,5 +849,64 @@ "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Διαβάστηκε από τον/την %(displayName)s (%(userName)s) στις %(dateTime)s", "Room Notification": "Ειδοποίηση Δωματίου", "Notify the whole room": "Ειδοποιήστε όλο το δωμάτιο", - "Sets the room topic": "Ορίζει το θέμα του δωματίου" + "Sets the room topic": "Ορίζει το θέμα του δωματίου", + "Add Email Address": "Προσθήκη Διεύθυνσης Ηλ. Ταχυδρομείου", + "Add Phone Number": "Προσθήκη Τηλεφωνικού Αριθμού", + "Whether or not you're logged in (we don't record your username)": "Χωρίς να έχει σημασία εάν είστε συνδεδεμένοι (δεν καταγράφουμε το όνομα χρήστη σας)", + "Which officially provided instance you are using, if any": "Ποιά επίσημα παρεχόμενη έκδοση χρησιμοποιείτε, εάν χρησιμοποιείτε κάποια", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Χωρίς να έχει σημασία εάν χρησιμοποιείτε την λειτουργία \"Πλούσιο Κείμενο\" του Επεξεργαστή Πλουσίου Κειμένου", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Χωρίς να έχει σημασία εάν χρησιμοποιείτε το χαρακτηριστικό 'ψίχουλα' (τα άβαταρ πάνω από την λίστα δωματίων)", + "Your User Agent": "Ο Πράκτορας Χρήστη σας", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Όπου αυτή η σελίδα περιέχει αναγνωρίσιμες πληροφορίες, όπως ταυτότητα δωματίου, χρήστη ή ομάδας, αυτά τα δεδομένα αφαιρούνται πριν πραγματοποιηθεί αποστολή στον διακομιστή.", + "Call failed due to misconfigured server": "Η κλήση απέτυχε λόγω της λανθασμένης διάρθρωσης του διακομιστή", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Παρακαλείστε να ρωτήσετε τον διαχειριστή του διακομιστή φιλοξενίας σας (%(homeserverDomain)s) να ρυθμίσουν έναν διακομιστή πρωτοκόλλου TURN ώστε οι κλήσεις να λειτουργούν απρόσκοπτα.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Εναλλακτικά, δοκιμάστε να χρησιμοποιήσετε τον δημόσιο διακομιστή στο turn.matrix.org, αλλά δεν θα είναι το ίδιο απρόσκοπτο, και θα κοινοποιεί την διεύθυνση IP σας με τον διακομιστή. Μπορείτε επίσης να το διαχειριστείτε στις Ρυθμίσεις.", + "Try using turn.matrix.org": "Δοκιμάστε το turn.matrix.org", + "A conference call could not be started because the integrations server is not available": "Μια κλήση συνδιάσκεψης δεν μπορούσε να ξεκινήσει διότι ο διακομιστής ενσωμάτωσης είναι μη διαθέσιμος", + "Call in Progress": "Κλήση σε Εξέλιξη", + "A call is currently being placed!": "Μια κλήση πραγματοποιείτε τώρα!", + "A call is already in progress!": "Μια κλήση είναι σε εξέλιξη ήδη!", + "Permission Required": "Απαιτείται Άδεια", + "You do not have permission to start a conference call in this room": "Δεν έχετε άδεια για να ξεκινήσετε μια κλήση συνδιάσκεψης σε αυτό το δωμάτιο", + "Replying With Files": "Απαντώντας Με Αρχεία", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Αυτήν την στιγμή δεν είναι δυνατό να απαντήσετε με αρχείο. Θα θέλατε να ανεβάσετε το αρχείο χωρίς να απαντήσετε;", + "The file '%(fileName)s' failed to upload.": "Απέτυχε το ανέβασμα του αρχείου '%(fileName)s'.", + "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Το αρχείο '%(fileName)s' ξεπερνάει το όριο μεγέθους ανεβάσματος αυτού του διακομιστή φιλοξενίας", + "The server does not support the room version specified.": "Ο διακομιστής δεν υποστηρίζει την έκδοση του δωματίου που ορίστηκε.", + "Name or Matrix ID": "Όνομα ή ταυτότητα Matrix", + "Identity server has no terms of service": "Ο διακομιστής ταυτοποίησης δεν έχει όρους χρήσης", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Αυτή η δράση απαιτεί την πρόσβαση στο προκαθορισμένο διακομιστή ταυτοποίησης για να επιβεβαιώσει μια διεύθυνση ηλ. ταχυδρομείου ή αριθμό τηλεφώνου, αλλά ο διακομιστής δεν έχει όρους χρήσης.", + "Only continue if you trust the owner of the server.": "Συνεχίστε μόνο εάν εμπιστεύεστε τον ιδιοκτήτη του διακομιστή.", + "Trust": "Εμπιστοσύνη", + "Unable to load! Check your network connectivity and try again.": "Αδυναμία φόρτωσης! Ελέγξτε την σύνδεση του δικτύου και προσπαθήστε ξανά.", + "Registration Required": "Απαιτείτε Εγγραφή", + "You need to register to do this. Would you like to register now?": "Χρειάζεται να γίνει εγγραφή για αυτό. Θα θέλατε να κάνετε εγγραφή τώρα;", + "Email, name or Matrix ID": "Ηλ. ταχυδρομείο, όνομα ή ταυτότητα Matrix", + "Failed to start chat": "Αποτυχία αρχικοποίησης συνομιλίας", + "Failed to invite users to the room:": "Αποτυχία πρόσκλησης χρηστών στο δωμάτιο:", + "Missing roomId.": "Λείπει η ταυτότητα δωματίου.", + "Messages": "Μηνύματα", + "Actions": "Δράσεις", + "Other": "Άλλα", + "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Προ-εισάγει ¯\\_(ツ)_/¯ σε ένα μήνυμα απλού κειμένου", + "Sends a message as plain text, without interpreting it as markdown": "Αποστέλλει ένα μήνυμα ως απλό κείμενο, χωρίς να το ερμηνεύει ως \"markdown\"", + "Upgrades a room to a new version": "Αναβαθμίζει το δωμάτιο σε μια καινούργια έκδοση", + "You do not have the required permissions to use this command.": "Δεν διαθέτετε τις απαιτούμενες άδειες για να χρησιμοποιήσετε αυτήν την εντολή.", + "Room upgrade confirmation": "Επιβεβαίωση αναβάθμισης δωματίου", + "Upgrading a room can be destructive and isn't always necessary.": "Η αναβάθμιση ενός δωματίου μπορεί να είναι καταστροφική και δεν είναι πάντα απαραίτητη.", + "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Οι αναβαθμίσεις δωματίου είναι συνήθως προτεινόμενες όταν μια έκδοση δωματίου θεωρείτε ασταθής. Ασταθείς εκδόσεις δωματίων μπορεί να έχουν σφάλματα, ελλειπή χαρακτηριστικά, ή αδυναμίες ασφαλείας.", + "Room upgrades usually only affect server-side processing of the room. If you're having problems with your Riot client, please file an issue with .": "Οι αναβαθμίσεις δωματίων συνήθως επηρεάζουν μόνο την επεξεργασία του δωματίου από την πλευρά του διακομιστή. Εάν έχετε προβλήματα με το πρόγραμμα-πελάτη Riot, παρακαλώ αρχειοθετήστε ένα πρόβλημα μέσω .", + "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Προσοχή: Αναβαθμίζοντας ένα δωμάτιο δεν θα μεταφέρει αυτόματα τα μέλη του δωματίου στη νέα έκδοση του δωματίου. Θα αναρτήσουμε ένα σύνδεσμο προς το νέο δωμάτιο στη παλιά έκδοση του δωματίου - τα μέλη του δωματίου θα πρέπει να πατήσουν στον σύνδεσμο για να μπουν στο νέο δωμάτιο.", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Παρακαλώ επιβεβαιώστε ότι θα θέλατε να προχωρήσετε με την αναβάθμιση του δωματίου από σε .", + "Upgrade": "Αναβάθμιση", + "Changes your display nickname in the current room only": "Αλλάζει το εμφανιζόμενο ψευδώνυμο μόνο στο παρόν δωμάτιο", + "Changes the avatar of the current room": "Αλλάζει το άβαταρ αυτού του δωματίου", + "Changes your avatar in this current room only": "Αλλάζει το άβαταρ σας μόνο στο παρόν δωμάτιο", + "Changes your avatar in all rooms": "Αλλάζει το άβαταρ σας σε όλα τα δωμάτια", + "Gets or sets the room topic": "Λαμβάνει ή θέτει το θέμα του δωματίου", + "This room has no topic.": "Το δωμάτιο αυτό δεν έχει κανένα θέμα.", + "Sets the room name": "Θέτει το θέμα του δωματίου", + "Use an identity server": "Χρησιμοποιήστε ένα διακομιστή ταυτοτήτων", + "Your Riot is misconfigured": "Οι παράμετροι του Riot σας είναι λανθασμένα ρυθμισμένοι", + "Explore rooms": "Εξερευνήστε δωμάτια" } From 84d23676d6002005c7ff5b906e383eeef1300647 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Mon, 4 Nov 2019 21:14:13 +0000 Subject: [PATCH 0503/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1853 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 4946c7b14f..5d21a17e37 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2266,5 +2266,6 @@ "Unread messages.": "Olvasatlan üzenetek.", "Show tray icon and minimize window to it on close": "Tálcaikon mutatása és az ablak összecsukása bezáráskor", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Ez a művelet az e-mail cím vagy telefonszám ellenőrzése miatt hozzáférést igényel az alapértelmezett azonosítási szerverhez (), de a szervernek nincsen semmilyen felhasználási feltétele.", - "Trust": "Megbízom benne" + "Trust": "Megbízom benne", + "Message Actions": "Üzenet Műveletek" } From 8b3844f83b6d47053d01ffa391c51b35255c9118 Mon Sep 17 00:00:00 2001 From: shuji narazaki Date: Thu, 7 Nov 2019 23:12:55 +0000 Subject: [PATCH 0504/2372] Translated using Weblate (Japanese) Currently translated at 60.8% (1126 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ja/ --- src/i18n/strings/ja.json | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 23199094e8..5e3fecaed9 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -1343,5 +1343,42 @@ "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s はこの部屋をアップグレードしました。", "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s はこの部屋をリンクを知っている人全てに公開しました。", "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s はこの部屋を招待者のみに変更しました。", - "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s はゲストがこの部屋に参加できるようにしました。" + "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s はゲストがこの部屋に参加できるようにしました。", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "「パンくずリスト」機能(部屋リストの上のアバター)を使っているかどうか", + "Only continue if you trust the owner of the server.": "そのサーバーの所有者を信頼する場合のみ続ける。", + "Trust": "信頼", + "Use an identity server to invite by email. Manage in Settings.": "メールによる招待のためにIDサーバーを用いる。設定画面で管理する。", + "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s は参加ルールを %(rule)s に変更しました", + "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s はゲストの部屋への参加を差し止めています。", + "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s はゲストのアクセスを %(rule)s に変更しました", + "%(displayName)s is typing …": "%(displayName)s が入力中 …", + "%(names)s and %(count)s others are typing …|other": "%(names)s と他 %(count)s 名が入力中 …", + "%(names)s and %(count)s others are typing …|one": "%(names)s ともう一人が入力中 …", + "%(names)s and %(lastPerson)s are typing …": "%(names)s と %(lastPerson)s が入力中 …", + "Cannot reach homeserver": "ホームサーバーに接続できません", + "Your Riot is misconfigured": "あなたのRiotは設定が間違っています", + "Cannot reach identity server": "IDサーバーに接続できません", + "No homeserver URL provided": "ホームサーバーのURLが与えられていません", + "User %(userId)s is already in the room": "ユーザー %(userId)s はすでにその部屋にいます", + "User %(user_id)s does not exist": "ユーザー %(user_id)s は存在しません", + "The user's homeserver does not support the version of the room.": "そのユーザーのホームサーバーはその部屋のバージョンに対応していません。", + "Use a few words, avoid common phrases": "ありふれた語句を避けて、いくつかの単語を使ってください", + "This is a top-10 common password": "これがよく使われるパスワードの上位10個です", + "This is a top-100 common password": "これがよく使われるパスワードの上位100個です", + "This is a very common password": "これはとてもよく使われるパスワードです", + "This is similar to a commonly used password": "これはよく使われるパスワードに似ています", + "A word by itself is easy to guess": "単語一つだけだと簡単に特定されます", + "Custom user status messages": "ユーザーステータスのメッセージをカスタマイズする", + "Render simple counters in room header": "部屋のヘッダーに簡単なカウンターを表示する", + "Use the new, faster, composer for writing messages": "メッセージの編集に新しい高速なコンポーザーを使う", + "Enable Emoji suggestions while typing": "入力中の絵文字提案機能を有効にする", + "Show avatar changes": "アバターの変更を表示する", + "Show display name changes": "表示名の変更を表示する", + "Show read receipts sent by other users": "他の人の既読情報を表示する", + "Enable big emoji in chat": "チャットで大きな絵文字を有効にする", + "Send typing notifications": "入力中であることを通知する", + "Enable Community Filter Panel": "コミュニティーフィルターパネルを有効にする", + "Show recently visited rooms above the room list": "最近訪問した部屋をリストの上位に表示する", + "Low bandwidth mode": "低帯域通信モード", + "Trust & Devices": "信頼と端末" } From defe3fb5f81e2d34d4c91fcd43e820e30c2b2f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Mon, 4 Nov 2019 15:39:35 +0000 Subject: [PATCH 0505/2372] Translated using Weblate (Korean) Currently translated at 100.0% (1853 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 1aebd0ce17..54425657cf 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2123,5 +2123,6 @@ "Unread messages.": "읽지 않은 메시지.", "Show tray icon and minimize window to it on close": "닫을 때 창을 최소화하고 트레이 아이콘으로 표시하기", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "이 작업에는 이메일 주소 또는 전화번호를 확인하기 위해 기본 ID 서버 에 접근해야 합니다. 하지만 서버가 서비스 약관을 갖고 있지 않습니다.", - "Trust": "신뢰함" + "Trust": "신뢰함", + "Message Actions": "메시지 동작" } From 824a26549644de047d6dd75d46b9bde0babd47a6 Mon Sep 17 00:00:00 2001 From: MamasLT Date: Mon, 4 Nov 2019 22:55:04 +0000 Subject: [PATCH 0506/2372] Translated using Weblate (Lithuanian) Currently translated at 50.4% (933 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/lt/ --- src/i18n/strings/lt.json | 271 ++++++++++++++++++++++++++++----------- 1 file changed, 194 insertions(+), 77 deletions(-) diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index 6a8076ac96..2aeb207387 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -1,17 +1,17 @@ { "This email address is already in use": "Šis el. pašto adresas jau naudojamas", "This phone number is already in use": "Šis telefono numeris jau naudojamas", - "Failed to verify email address: make sure you clicked the link in the email": "Nepavyko patvirtinti el. pašto adreso: įsitikinkite, kad gautame el. laiške spustelėjote nuorodą", + "Failed to verify email address: make sure you clicked the link in the email": "Nepavyko patvirtinti el. pašto adreso: įsitikinkite, kad paspaudėte nuorodą el. laiške", "The platform you're on": "Jūsų naudojama platforma", "The version of Riot.im": "Riot.im versija", "Whether or not you're logged in (we don't record your user name)": "Nesvarbu ar esate prisijungę ar ne (mes neįrašome jūsų naudotojo vardo)", "Your language of choice": "Jūsų pasirinkta kalba", - "Which officially provided instance you are using, if any": "Kurį oficialiai pateiktą egzempliorių naudojate", - "Whether or not you're using the Richtext mode of the Rich Text Editor": "Ar jūs naudojate Raiškiojo Teksto Redaktoriaus Raiškiojo Teksto režimą ar ne", - "Your homeserver's URL": "Jūsų serverio URL adresas", - "Your identity server's URL": "Jūsų identifikavimo serverio URL adresas", + "Which officially provided instance you are using, if any": "Kurią oficialiai teikiamą instanciją naudojate, jei tokių yra", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Nepriklausomai nuo to ar jūs naudojate Raiškiojo Teksto Redaktoriaus Raiškiojo Teksto režimą", + "Your homeserver's URL": "Jūsų serverio URL", + "Your identity server's URL": "Jūsų tapatybės serverio URL", "Analytics": "Statistika", - "The information being sent to us to help make Riot.im better includes:": "Informacijoje, kuri yra siunčiama Riot.im tobulinimui yra:", + "The information being sent to us to help make Riot.im better includes:": "Informacija, siunčiama mums, kad padėtų tobulinti Riot.im, apima:", "Fetching third party location failed": "Nepavyko gauti trečios šalies vietos", "A new version of Riot is available.": "Yra prieinama nauja Riot versija.", "I understand the risks and wish to continue": "Aš suprantu riziką ir noriu tęsti", @@ -196,7 +196,7 @@ "Answer Anyway": "Vis tiek atsiliepti", "Call": "Skambinti", "Answer": "Atsiliepti", - "Unable to capture screen": "Nepavyko nufotografuoti ekraną", + "Unable to capture screen": "Nepavyko nufotografuoti ekrano", "You are already in a call.": "Jūs jau dalyvaujate skambutyje.", "VoIP is unsupported": "VoIP yra nepalaikoma", "Could not connect to the integration server": "Nepavyko prisijungti prie integracijos serverio", @@ -210,18 +210,18 @@ "Thu": "Ket", "Fri": "Pen", "Sat": "Šeš", - "Jan": "Sau", + "Jan": "Sausis", "Feb": "Vas", - "Mar": "Kov", + "Mar": "Kovas", "Apr": "Bal", "May": "Geg", - "Jun": "Bir", - "Jul": "Lie", - "Aug": "Rgp", - "Sep": "Rgs", - "Oct": "Spa", - "Nov": "Lap", - "Dec": "Gru", + "Jun": "Birž", + "Jul": "Liepa", + "Aug": "Rugpj", + "Sep": "Rugs", + "Oct": "Spalis", + "Nov": "Lapkr", + "Dec": "Gruodis", "PM": "PM", "AM": "AM", "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", @@ -229,16 +229,16 @@ "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(fullYear)s %(monthName)s %(day)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(fullYear)s %(monthName)s %(day)s %(time)s", "Who would you like to add to this community?": "Ką norėtumėte pridėti į šią bendruomenę?", - "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Įspėjimas: bet kuris pridėtas asmuo bus matomas visiems, žinantiems bendruomenės ID", + "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Įspėjimas: bet kuris jūsų pridėtas asmuo bus viešai matomas visiems, žinantiems bendruomenės ID", "Name or matrix ID": "Vardas ar matrix ID", "Invite to Community": "Pakviesti į bendruomenę", "Which rooms would you like to add to this community?": "Kuriuos kambarius norėtumėte pridėti į šią bendruomenę?", "Add rooms to the community": "Pridėti kambarius į bendruomenę", "Add to community": "Pridėti į bendruomenę", - "Failed to invite the following users to %(groupId)s:": "Nepavyko pakviesti šių naudotojų į %(groupId)s:", - "Failed to invite users to community": "Nepavyko pakviesti naudotojus į bendruomenę", - "Failed to invite users to %(groupId)s": "Nepavyko pakviesti naudotojų į %(groupId)s", - "Failed to add the following rooms to %(groupId)s:": "Nepavyko pridėti šiuos kambarius į %(groupId)s:", + "Failed to invite the following users to %(groupId)s:": "Nepavyko pakviesti šių vartotojų į %(groupId)s:", + "Failed to invite users to community": "Nepavyko pakviesti vartotojų į bendruomenę", + "Failed to invite users to %(groupId)s": "Nepavyko pakviesti vartotojų į %(groupId)s", + "Failed to add the following rooms to %(groupId)s:": "Nepavyko pridėti šių kambarių į %(groupId)s:", "Riot does not have permission to send you notifications - please check your browser settings": "Riot neturi leidimo siųsti jums pranešimus - patikrinkite savo naršyklės nustatymus", "Riot was not given permission to send notifications - please try again": "Riot nebuvo suteiktas leidimas siųsti pranešimus - bandykite dar kartą", "Unable to enable Notifications": "Nepavyko įjungti Pranešimus", @@ -251,7 +251,7 @@ "Send Invites": "Siųsti pakvietimus", "Failed to invite user": "Nepavyko pakviesti naudotojo", "Failed to invite": "Nepavyko pakviesti", - "Failed to invite the following users to the %(roomName)s room:": "Nepavyko pakviesti šių naudotojų į kambarį %(roomName)s :", + "Failed to invite the following users to the %(roomName)s room:": "Nepavyko pakviesti šių vartotojų į kambarį %(roomName)s:", "You need to be logged in.": "Turite būti prisijungę.", "Unable to create widget.": "Nepavyko sukurti valdiklio.", "Failed to send request.": "Nepavyko išsiųsti užklausos.", @@ -263,8 +263,8 @@ "Changes your display nickname": "Pakeičia jūsų rodomą slapyvardį", "Sets the room topic": "Nustato kambario temą", "Invites user with given id to current room": "Pakviečia naudotoją su nurodytu id į esamą kambarį", - "You are now ignoring %(userId)s": "Dabar nepaisote %(userId)s", - "Opens the Developer Tools dialog": "Atveria kūrėjo įrankių dialogą", + "You are now ignoring %(userId)s": "Dabar ignoruojate %(userId)s", + "Opens the Developer Tools dialog": "Atveria programuotojo įrankių dialogą", "Unknown (user, device) pair:": "Nežinoma pora (naudotojas, įrenginys):", "Device already verified!": "Įrenginys jau patvirtintas!", "WARNING: Device already verified, but keys do NOT MATCH!": "ĮSPĖJIMAS: Įrenginys jau patvirtintas, tačiau raktai NESUTAMPA!", @@ -273,26 +273,26 @@ "Unrecognised command:": "Neatpažinta komanda:", "Reason": "Priežastis", "%(targetName)s accepted an invitation.": "%(targetName)s priėmė pakvietimą.", - "%(senderName)s invited %(targetName)s.": "%(senderName)s pakvietė naudotoją %(targetName)s.", - "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s pasikeitė savo rodomą vardą į %(displayName)s.", - "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s nusistatė savo rodomą vardą į %(displayName)s.", - "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s pašalino savo rodomą vardą (%(oldDisplayName)s).", + "%(senderName)s invited %(targetName)s.": "%(senderName)s pakvietė %(targetName)s.", + "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s pakeitė savo vardą į %(displayName)s.", + "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s nustatė savo vardą į %(displayName)s.", + "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s pašalino savo vardą (%(oldDisplayName)s).", "%(senderName)s removed their profile picture.": "%(senderName)s pašalino savo profilio paveikslą.", - "%(senderName)s changed their profile picture.": "%(senderName)s pasikeitė savo profilio paveikslą.", - "%(senderName)s set a profile picture.": "%(senderName)s nusistatė profilio paveikslą.", + "%(senderName)s changed their profile picture.": "%(senderName)s pakeitė savo profilio paveikslą.", + "%(senderName)s set a profile picture.": "%(senderName)s nustatė profilio paveikslą.", "%(targetName)s rejected the invitation.": "%(targetName)s atmetė pakvietimą.", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s pakeitė temą į \"%(topic)s\".", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s pakeitė kambario pavadinimą į %(roomName)s.", "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s išsiuntė paveikslą.", "Someone": "Kažkas", "%(senderName)s answered the call.": "%(senderName)s atsiliepė į skambutį.", - "(unknown failure: %(reason)s)": "(nežinoma lemtingoji klaida: %(reason)s)", + "(unknown failure: %(reason)s)": "(nežinoma klaida: %(reason)s)", "%(senderName)s ended the call.": "%(senderName)s užbaigė skambutį.", "%(displayName)s is typing": "%(displayName)s rašo", "%(names)s and %(count)s others are typing|other": "%(names)s ir dar kiti %(count)s rašo", "%(names)s and %(lastPerson)s are typing": "%(names)s ir %(lastPerson)s rašo", "Send anyway": "Vis tiek siųsti", - "Unnamed Room": "Kambarys be pavadinimo", + "Unnamed Room": "Bevardis kambarys", "Hide removed messages": "Slėpti pašalintas žinutes", "Hide display name changes": "Slėpti rodomo vardo pakeitimus", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Rodyti laiko žymas 12 valandų formatu (pvz., 2:30pm)", @@ -372,7 +372,7 @@ "Settings": "Nustatymai", "Show panel": "Rodyti skydelį", "Press to start a chat with someone": "Norėdami pradėti su kuo nors pokalbį, paspauskite ", - "Community Invites": "", + "Community Invites": "Bendruomenės pakvietimai", "People": "Žmonės", "Reason: %(reasonText)s": "Priežastis: %(reasonText)s", "%(roomName)s does not exist.": "%(roomName)s nėra.", @@ -576,27 +576,27 @@ "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Eksportavimo failas bus apsaugotas slaptafraze. Norėdami iššifruoti failą, čia turėtumėte įvesti slaptafrazę.", "File to import": "Failas, kurį importuoti", "Import": "Importuoti", - "Your User Agent": "Jūsų naudotojo agentas", + "Your User Agent": "Jūsų vartotojo agentas", "Review Devices": "Peržiūrėti įrenginius", "You do not have permission to start a conference call in this room": "Jūs neturite leidimo šiame kambaryje pradėti konferencinį pokalbį", "The file '%(fileName)s' exceeds this home server's size limit for uploads": "Failas \"%(fileName)s\" viršija šio namų serverio įkeliamų failų dydžio apribojimą", "Room name or alias": "Kambario pavadinimas ar slapyvardis", - "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Neatrodo, kad jūsų el. pašto adresas šiame namų serveryje būtų susietas su Matrix ID.", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Neatrodo, kad jūsų el. pašto adresas šiame serveryje būtų susietas su Matrix ID.", "Who would you like to communicate with?": "Su kuo norėtumėte susisiekti?", "Missing room_id in request": "Užklausoje trūksta room_id", "Missing user_id in request": "Užklausoje trūksta user_id", "Unrecognised room alias:": "Neatpažintas kambario slapyvardis:", - "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "ĮSPĖJIMAS: RAKTO PATVIRTINIMAS NEPAVYKO! Pasirašymo raktas, skirtas %(userId)s ir įrenginiui %(deviceId)s yra \"%(fprint)s\", o tai nesutampa su pateiktu raktu \"%(fingerprint)s\". Tai gali reikšti, kad kažkas perima jūsų komunikavimą!", - "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Jūsų pateiktas pasirašymo raktas sutampa su pasirašymo raktus, kuris gautas iš naudotojo %(userId)s įrenginio %(deviceId)s. Įrenginys pažymėtas kaip patvirtintas.", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "ĮSPĖJIMAS: RAKTO PATVIRTINIMAS NEPAVYKO! Pasirašymo raktas, skirtas %(userId)s ir įrenginiui %(deviceId)s yra \"%(fprint)s\", o tai nesutampa su pateiktu raktu \"%(fingerprint)s\". Tai gali reikšti, kad jūsų komunikacijos yra perimamos!", + "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Jūsų pateiktas pasirašymo raktas sutampa su pasirašymo raktu, kurį gavote iš vartotojo %(userId)s įrenginio %(deviceId)s. Įrenginys pažymėtas kaip patvirtintas.", "VoIP conference started.": "VoIP konferencija pradėta.", "VoIP conference finished.": "VoIP konferencija užbaigta.", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s pašalino kambario pavadinimą.", - "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s įjungė ištisinį šifravimą (%(algorithm)s algoritmas).", + "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s įjungė end-to-end šifravimą (%(algorithm)s algoritmas).", "%(widgetName)s widget modified by %(senderName)s": "%(senderName)s modifikavo %(widgetName)s valdiklį", "%(widgetName)s widget added by %(senderName)s": "%(senderName)s pridėjo %(widgetName)s valdiklį", "%(widgetName)s widget removed by %(senderName)s": "%(senderName)s pašalino %(widgetName)s valdiklį", "Failure to create room": "Nepavyko sukurti kambarį", - "Server may be unavailable, overloaded, or you hit a bug.": "Gali būti, kad serveris neprieinamas, perkrautas arba susidūrėte su klaida.", + "Server may be unavailable, overloaded, or you hit a bug.": "Serveris gali būti neprieinamas, per daug apkrautas, arba susidūrėte su klaida.", "Use compact timeline layout": "Naudoti kompaktišką laiko juostos išdėstymą", "Autoplay GIFs and videos": "Automatiškai atkurti GIF ir vaizdo įrašus", "Never send encrypted messages to unverified devices from this device": "Niekada nesiųsti iš šio įrenginio šifruotų žinučių į nepatvirtintus įrenginius", @@ -624,30 +624,30 @@ "Invited": "Pakviestas", "Filter room members": "Filtruoti kambario dalyvius", "Server unavailable, overloaded, or something else went wrong.": "Serveris neprieinamas, perkrautas arba nutiko kažkas kito.", - "%(duration)ss": "%(duration)s sek.", - "%(duration)sm": "%(duration)s min.", - "%(duration)sh": "%(duration)s val.", - "%(duration)sd": "%(duration)s d.", + "%(duration)ss": "%(duration)s sek", + "%(duration)sm": "%(duration)s min", + "%(duration)sh": "%(duration)s val", + "%(duration)sd": "%(duration)s d", "Seen by %(userName)s at %(dateTime)s": "%(userName)s matė ties %(dateTime)s", "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "%(displayName)s (%(userName)s) matė ties %(dateTime)s", - "Show these rooms to non-members on the community page and room list?": "Ar rodyti šiuos kambarius ne dalyviams bendruomenės puslapyje ir kambarių sąraše?", + "Show these rooms to non-members on the community page and room list?": "Rodyti šiuos kambarius ne nariams bendruomenės puslapyje ir kambarių sąraše?", "Invite new room members": "Pakviesti naujus kambario dalyvius", "Changes colour scheme of current room": "Pakeičia esamo kambario spalvų rinkinį", - "Kicks user with given id": "Išmeta naudotoją su nurodytu id", - "Bans user with given id": "Užblokuoja naudotoja su nurodytu id", + "Kicks user with given id": "Išmeta vartotoją su nurodytu id", + "Bans user with given id": "Užblokuoja vartotoją su nurodytu id", "Unbans user with given id": "Atblokuoja naudotoją su nurodytu id", - "%(senderName)s banned %(targetName)s.": "%(senderName)s užblokavo naudotoją %(targetName)s.", - "%(senderName)s unbanned %(targetName)s.": "%(senderName)s atblokavo naudotoją %(targetName)s.", - "%(senderName)s kicked %(targetName)s.": "%(senderName)s išmetė naudotoją %(targetName)s.", + "%(senderName)s banned %(targetName)s.": "%(senderName)s užblokavo %(targetName)s.", + "%(senderName)s unbanned %(targetName)s.": "%(senderName)s atblokavo %(targetName)s.", + "%(senderName)s kicked %(targetName)s.": "%(senderName)s išmetė %(targetName)s.", "(not supported by this browser)": "(nėra palaikoma šios naršyklės)", "(no answer)": "(nėra atsakymo)", - "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s padarė kambario ateities istoriją matomą visiems kambario dalyviams nuo to laiko, kai jie buvo pakviesti.", - "%(senderName)s made future room history visible to all room members.": "%(senderName)s padarė kambario ateities istoriją matomą visiems kambario dalyviams.", - "%(senderName)s made future room history visible to anyone.": "%(senderName)s padarė kambario ateities istoriją matomą bet kam.", + "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s padarė būsimą kambario istoriją matomą visiems kambario dalyviams, nuo pat jų pakvietimo.", + "%(senderName)s made future room history visible to all room members.": "%(senderName)s padarė būsimą kambario istoriją matomą visiems kambario dalyviams.", + "%(senderName)s made future room history visible to anyone.": "%(senderName)s padarė būsimą kambario istoriją matomą bet kam.", "%(names)s and %(count)s others are typing|one": "%(names)s ir dar vienas naudotojas rašo", "Your browser does not support the required cryptography extensions": "Jūsų naršyklė nepalaiko reikalingų kriptografijos plėtinių", "Not a valid Riot keyfile": "Negaliojantis Riot rakto failas", - "Authentication check failed: incorrect password?": "Tapatybės nustatymo patikrinimas patyrė nesėkmę: neteisingas slaptažodis?", + "Authentication check failed: incorrect password?": "Autentifikavimo patikra nepavyko: neteisingas slaptažodis?", "Send analytics data": "Siųsti analitinius duomenis", "Incoming voice call from %(name)s": "Gaunamasis balso skambutis nuo %(name)s", "Incoming video call from %(name)s": "Gaunamasis vaizdo skambutis nuo %(name)s", @@ -672,14 +672,14 @@ "Failed to set avatar.": "Nepavyko nustatyti avataro.", "Forget room": "Pamiršti kambarį", "Share room": "Bendrinti kambarį", - "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Šiame kambaryje yra nepatvirtintų įrenginių: jeigu tęsite jų nepatvirtinę, tuomet kas nors galės slapta klausytis jūsų skambučio.", + "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Šiame kambaryje yra nežinomų įrenginių: jei tęsite jų nepatvirtinę, kam nors bus įmanoma slapta klausytis jūsų skambučio.", "Usage": "Naudojimas", "Searches DuckDuckGo for results": "Atlieka rezultatų paiešką sistemoje DuckDuckGo", - "To use it, just wait for autocomplete results to load and tab through them.": "Norėdami tai naudoti, tiesiog, palaukite, kol bus įkelti automatiškai užbaigti rezultatai, o tuomet, pereikite per juos naudodami Tab klavišą.", + "To use it, just wait for autocomplete results to load and tab through them.": "Norėdami tai naudoti, tiesiog palaukite, kol bus įkelti automatiškai užbaigti rezultatai, tuomet pereikite per juos naudodami Tab klavišą.", "%(targetName)s left the room.": "%(targetName)s išėjo iš kambario.", "%(senderName)s changed the pinned messages for the room.": "%(senderName)s pakeitė prisegtas kambario žinutes.", - "Sorry, your homeserver is too old to participate in this room.": "Atleiskite, jūsų namų serveris yra per senas dalyvauti šiame kambaryje.", - "Please contact your homeserver administrator.": "Prašome susisiekti su savo namų serverio administratoriumi.", + "Sorry, your homeserver is too old to participate in this room.": "Atleiskite, jūsų serverio versija yra per sena dalyvauti šiame kambaryje.", + "Please contact your homeserver administrator.": "Prašome susisiekti su savo serverio administratoriumi.", "Enable inline URL previews by default": "Įjungti tiesiogines URL nuorodų peržiūras pagal numatymą", "Enable URL previews for this room (only affects you)": "Įjungti URL nuorodų peržiūras šiame kambaryje (įtakoja tik jus)", "Enable URL previews by default for participants in this room": "Įjungti URL nuorodų peržiūras pagal numatymą dalyviams šiame kambaryje", @@ -731,7 +731,7 @@ "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Jeigu nenurodysite savo el. pašto adreso, negalėsite atstatyti savo slaptažodį. Ar esate tikri?", "Home server URL": "Namų serverio URL", "Identity server URL": "Tapatybės serverio URL", - "Please contact your service administrator to continue using the service.": "Norėdami tęsti naudotis paslauga, susisiekite su savo paslaugos administratoriumi.", + "Please contact your service administrator to continue using the service.": "Norėdami toliau naudotis šia paslauga, susisiekite su savo paslaugos administratoriumi.", "Reload widget": "Įkelti valdiklį iš naujo", "Picture": "Paveikslas", "Create new room": "Sukurti naują kambarį", @@ -787,12 +787,12 @@ "You cannot place a call with yourself.": "Negalite skambinti patys sau.", "Registration Required": "Reikalinga registracija", "You need to register to do this. Would you like to register now?": "Norėdami tai atlikti, turite užsiregistruoti. Ar norėtumėte užsiregistruoti dabar?", - "Missing roomId.": "Trūksta kambario ID (roomId).", + "Missing roomId.": "Trūksta kambario ID.", "Leave room": "Išeiti iš kambario", "(could not connect media)": "(nepavyko prijungti medijos)", - "This homeserver has hit its Monthly Active User limit.": "Šis namų serveris pasiekė savo mėnesinį aktyvių naudotojų limitą.", - "This homeserver has exceeded one of its resource limits.": "Šis namų serveris viršijo vieno iš savo išteklių limitą.", - "Unable to connect to Homeserver. Retrying...": "Nepavyksta prisijungti prie namų serverio. Bandoma iš naujo...", + "This homeserver has hit its Monthly Active User limit.": "Šis serveris pasiekė savo mėnesinį aktyvių naudotojų limitą.", + "This homeserver has exceeded one of its resource limits.": "Šis serveris viršijo vieno iš savo išteklių limitą.", + "Unable to connect to Homeserver. Retrying...": "Nepavyksta prisijungti prie serverio. Bandoma iš naujo...", "Hide avatar changes": "Slėpti avatarų pasikeitimus", "Disable Community Filter Panel": "Išjungti bendruomenės filtro skydelį", "Enable widget screenshots on supported widgets": "Palaikomuose valdikliuose įjungti valdiklių ekrano kopijas", @@ -854,12 +854,12 @@ "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s pasikeitė avatarą %(count)s kartų(-us)", "And %(count)s more...|other": "Ir dar %(count)s...", "Existing Call": "Esamas skambutis", - "A call is already in progress!": "Skambutis jau yra inicijuojamas!", + "A call is already in progress!": "Skambutis jau vyksta!", "Default": "Numatytasis", "Restricted": "Apribotas", "Moderator": "Moderatorius", - "Ignores a user, hiding their messages from you": "Nepaiso naudotojo, paslepiant nuo jūsų jo žinutes", - "Stops ignoring a user, showing their messages going forward": "Sustabdo naudotojo nepaisymą, rodant jo tolimesnes žinutes", + "Ignores a user, hiding their messages from you": "Ignoruoja vartotoją, slepiant nuo jūsų jo žinutes", + "Stops ignoring a user, showing their messages going forward": "Sustabdo vartotojo ignoravimą, rodant jums jo tolimesnes žinutes", "Hide avatars in user and room mentions": "Slėpti avatarus naudotojų ir kambarių paminėjimuose", "Revoke Moderator": "Panaikinti moderatorių", "deleted": "perbrauktas", @@ -871,13 +871,13 @@ "Invites": "Pakvietimai", "You have no historical rooms": "Jūs neturite istorinių kambarių", "Historical": "Istoriniai", - "Every page you use in the app": "Kiekvienas puslapis, kurį naudoji programoje", + "Every page you use in the app": "Kiekvienas puslapis, kurį jūs naudojate programoje", "Call Timeout": "Skambučio laikas baigėsi", - "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s kaip šio kambario adresus pridėjo %(addedAddresses)s.", - "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s kaip šio kambario adresą pridėjo %(addedAddresses)s.", - "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s kaip šio kambario adresus pašalino %(removedAddresses)s.", - "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": "%(senderName)s kaip šio kambario adresą pašalino %(removedAddresses)s.", - "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s kaip šio kambario adresus pridėjo %(addedAddresses)s ir pašalino %(removedAddresses)s.", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s pridėjo %(addedAddresses)s, kaip šio kambario adresus.", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s pridėjo %(addedAddresses)s, kaip šio kambario adresą.", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s pašalino %(removedAddresses)s, kaip šio kambario adresus.", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": "%(senderName)s pašalino %(removedAddresses)s, kaip šio kambario adresą.", + "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s pridėjo %(addedAddresses)s ir pašalino %(removedAddresses)s, kaip šio kambario adresus.", "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s nustatė pagrindinį šio kambario adresą į %(address)s.", "%(senderName)s removed the main address for this room.": "%(senderName)s pašalino pagrindinį šio kambario adresą.", "Disinvite": "Atšaukti pakvietimą", @@ -885,8 +885,8 @@ "Unknown for %(duration)s": "Nežinoma jau %(duration)s", "(warning: cannot be disabled again!)": "(įspėjimas: nebeįmanoma bus išjungti!)", "Unable to load! Check your network connectivity and try again.": "Nepavyko įkelti! Patikrinkite savo tinklo ryšį ir bandykite dar kartą.", - "%(targetName)s joined the room.": "%(targetName)s atėjo į kambarį.", - "User %(user_id)s does not exist": "Naudotojo %(user_id)s nėra", + "%(targetName)s joined the room.": "%(targetName)s prisijungė prie kambario.", + "User %(user_id)s does not exist": "Vartotojas %(user_id)s neegzistuoja", "Unknown server error": "Nežinoma serverio klaida", "Avoid sequences": "Venkite sekų", "Avoid recent years": "Venkite paskiausių metų", @@ -896,7 +896,7 @@ "All-uppercase is almost as easy to guess as all-lowercase": "Visas didžiąsias raides taip pat lengva atspėti kaip ir visas mažąsias", "Reversed words aren't much harder to guess": "Žodžius atvirkštine tvarka nėra sunkiau atspėti", "Predictable substitutions like '@' instead of 'a' don't help very much": "Nuspėjami pakaitalai, tokie kaip \"@\" vietoj \"a\", nelabai padeda", - "Add another word or two. Uncommon words are better.": "Pridėkite dar vieną žodį ar du. Geriau nedažnai vartojamus žodžius.", + "Add another word or two. Uncommon words are better.": "Pridėkite dar vieną ar du žodžius. Geriau nedažnai vartojamus žodžius.", "Repeats like \"aaa\" are easy to guess": "Tokius pasikartojimus kaip \"aaa\" yra lengva atspėti", "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Tokius pasikartojimus kaip \"abcabcabc\" yra tik truputėlį sunkiau atspėti nei \"abc\"", "Sequences like abc or 6543 are easy to guess": "Tokias sekas kaip \"abc\" ar \"6543\" yra lengva atspėti", @@ -906,13 +906,13 @@ "This is a top-100 common password": "Tai yra vienas iš 100 dažniausiai naudojamų slaptažodžių", "This is a very common password": "Tai yra labai dažnai naudojamas slaptažodis", "This is similar to a commonly used password": "Šis yra panašus į dažnai naudojamą slaptažodį", - "A word by itself is easy to guess": "Patį žodį savaime yra lengva atspėti", + "A word by itself is easy to guess": "Pats žodis yra lengvai atspėjamas", "Names and surnames by themselves are easy to guess": "Pačius vardus ar pavardes yra lengva atspėti", "Common names and surnames are easy to guess": "Dažnai naudojamus vardus ar pavardes yra lengva atspėti", "Straight rows of keys are easy to guess": "Klavišų eilę yra lengva atspėti", "Short keyboard patterns are easy to guess": "Trumpus klaviatūros šablonus yra lengva atspėti", "Avoid repeated words and characters": "Venkite pasikartojančių žodžių ir simbolių", - "Use a few words, avoid common phrases": "Naudokite kelis žodžius, venkite dažnai naudojamų frazių", + "Use a few words, avoid common phrases": "Naudokite keletą žodžių, venkite dažnai naudojamų frazių", "No need for symbols, digits, or uppercase letters": "Nereikia simbolių, skaitmenų ar didžiųjų raidžių", "Encrypted messages in group chats": "Šifruotos žinutės grupės pokalbiuose", "Delete Backup": "Ištrinti atsarginę kopiją", @@ -1000,5 +1000,122 @@ "Explore rooms": "Peržiūrėti kambarius", "Your Riot is misconfigured": "Jūsų Riot yra neteisingai sukonfigūruotas", "Sign in to your Matrix account on %(serverName)s": "Prisijunkite prie savo paskyros %(serverName)s serveryje", - "Sign in to your Matrix account on ": "Prisijunkite prie savo paskyros serveryje" + "Sign in to your Matrix account on ": "Prisijunkite prie savo paskyros serveryje", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Nepriklausomai nuo to ar jūs naudojate 'duonos trupinių' funkciją (avatarai virš kambarių sąrašo)", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kur šis puslapis įtraukia identifikuojamą informaciją, kaip kambarys, vartotojas ar grupės ID, tie duomenys yra pašalinami prieš siunčiant į serverį.", + "The remote side failed to pick up": "Nuotolinėi pusėi nepavyko atsiliepti", + "Call failed due to misconfigured server": "Skambutis nepavyko dėl neteisingai sukonfigūruoto serverio", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Paprašykite savo serverio administratoriaus (%(homeserverDomain)s) sukonfiguruoti TURN serverį, kad skambučiai veiktų patikimai.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatyviai, jūs galite bandyti naudoti viešą serverį turn.matrix.org, bet tai nebus taip patikima, ir tai atskleis jūsų IP adresą šiam serveriui. Jūs taip pat galite tvarkyti tai Nustatymuose.", + "Try using turn.matrix.org": "Bandykite naudoti turn.matrix.org", + "A conference call could not be started because the integrations server is not available": "Konferencinio skambučio nebuvo galima pradėti, nes nėra integracijų serverio", + "Call in Progress": "Vykstantis skambutis", + "A call is currently being placed!": "Šiuo metu skambinama!", + "Replying With Files": "Atsakyti su failais", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Šiuo metu neįmanoma atsakyti su failu. Ar norite įkelti šį failą neatsakydami?", + "The file '%(fileName)s' failed to upload.": "Failo '%(fileName)s' nepavyko įkelti.", + "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Failas '%(fileName)s' viršyja šio serverio įkeliamų failų dydžio limitą", + "The server does not support the room version specified.": "Serveris nepalaiko nurodytos kambario versijos.", + "Invite new community members": "Pakviesti naujus bendruomenės narius", + "Name or Matrix ID": "Vardas arba Matrix ID", + "Identity server has no terms of service": "Tapatybės serveris neturi paslaugų teikimo sąlygų", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Šiam veiksmui reikalinga prieiti numatytąjį tapatybės serverį , kad patvirtinti el. pašto adresą arba telefono numerį, bet serveris neturi jokių paslaugos teikimo sąlygų.", + "Only continue if you trust the owner of the server.": "Tęskite tik tada, jei pasitikite serverio savininku.", + "Trust": "Pasitikėti", + "Email, name or Matrix ID": "El. paštas, vardas arba Matrix ID", + "Failed to start chat": "Nepavyko pradėti pokalbio", + "Failed to invite users to the room:": "Nepavyko pakviesti vartotojų į kambarį:", + "You need to be able to invite users to do that.": "Norėdami tai atlikti jūs turite turėti galimybę pakviesti vartotojus.", + "Power level must be positive integer.": "Galios lygis privalo būti teigiamas sveikasis skaičius.", + "Messages": "Žinutės", + "Actions": "Veiksmai", + "Other": "Kitas", + "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Prideda ¯\\_(ツ)_/¯ prie paprasto teksto pranešimo", + "Sends a message as plain text, without interpreting it as markdown": "SIunčia žinutę, kaip paprastą tekstą, jo neinterpretuodamas kaip pažymėto", + "Upgrades a room to a new version": "Atnaujina kambarį į naują versiją", + "You do not have the required permissions to use this command.": "Jūs neturite reikalingų leidimų naudoti šią komandą.", + "Room upgrade confirmation": "Kambario atnaujinimo patvirtinimas", + "Upgrading a room can be destructive and isn't always necessary.": "Kambario atnaujinimas gali būti destruktyvus ir nėra visada reikalingas.", + "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Kambario atnaujinimai paprastai rekomenduojami, kada kambario versija yra laikoma nestabili. Nestabilios kambario versijos gali turėti klaidų, trūkstamų funkcijų, arba saugumo spragų.", + "Room upgrades usually only affect server-side processing of the room. If you're having problems with your Riot client, please file an issue with .": "Kambario atnaujinimai paprastai paveikia tik serverio pusės kambario apdorojimą. Jei jūs turite problemų su jūsų Riot klientu, prašome užregistruoti problemą .", + "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Įspėjimas: Kambario atnaujinimas automatiškai nemigruos kambario dalyvių į naują kambario versiją. Mes paskelbsime nuorodą į naują kambarį senojoje kambario versijoje - kambario dalyviai turės ją paspausti, norėdami prisijungti prie naujo kambario.", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Prašome patvirtinti, kad jūs norite tęsti šio kambario atnaujinimą iš į .", + "Upgrade": "Atnaujinti", + "Changes your display nickname in the current room only": "Pakeičia jūsų rodomą slapyvardį tik esamame kambaryje", + "Changes the avatar of the current room": "Pakeičia esamo kambario avatarą", + "Changes your avatar in this current room only": "Pakeičia jūsų avatarą tik esamame kambaryje", + "Changes your avatar in all rooms": "Pakeičia jūsų avatarą visuose kambariuose", + "Gets or sets the room topic": "Gauna arba nustato kambario temą", + "This room has no topic.": "Šis kambarys neturi temos.", + "Sets the room name": "Nustato kambario pavadinimą", + "Use an identity server": "Naudoti tapatybės serverį", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Norėdami pakviesti nurodydami el. paštą, naudokite tapatybės serverį. Tam, kad būtų naudojamas numatytasis tapatybės serveris %(defaultIdentityServerName)s, spauskite tęsti, arba tvarkykite nustatymuose.", + "Use an identity server to invite by email. Manage in Settings.": "Norėdami pakviesti nurodydami el. paštą, naudokite tapatybės serverį. Tvarkykite nustatymuose.", + "Joins room with given alias": "Prisijungia prie kambario su nurodytu slapyvardžiu", + "Unbans user with given ID": "Atblokuoja vartotoją su nurodytu id", + "Ignored user": "Ignoruojamas vartotojas", + "Unignored user": "Nebeignoruojamas vartotojas", + "You are no longer ignoring %(userId)s": "Dabar nebeignoruojate %(userId)s", + "Define the power level of a user": "Nustatykite vartotojo galios lygį", + "Deops user with given id": "Deop'ina vartotoją su nurodytu id", + "Adds a custom widget by URL to the room": "Į kambarį prideda pasirinktinį valdiklį pagal URL", + "Please supply a https:// or http:// widget URL": "Prašome pateikti https:// arba http:// valdiklio URL", + "You cannot modify widgets in this room.": "Jūs negalite keisti valdiklių šiame kambaryje.", + "Verifies a user, device, and pubkey tuple": "Patikrina vartotoją, įrenginį ir pubkey seką", + "Forces the current outbound group session in an encrypted room to be discarded": "Priverčia išmesti esamą užsibaigiančią grupės sesiją šifruotame kambaryje", + "Sends the given message coloured as a rainbow": "Išsiunčia nurodytą žinutę nuspalvintą kaip vaivorykštė", + "Sends the given emote coloured as a rainbow": "Išsiunčia nurodytą emociją nuspalvintą kaip vaivorykštė", + "Displays list of commands with usages and descriptions": "Parodo komandų sąrašą su naudojimo būdais ir aprašymais", + "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s priėmė pakvietimą %(displayName)s.", + "%(senderName)s requested a VoIP conference.": "%(senderName)s pageidauja VoIP konferencijos.", + "%(senderName)s made no change.": "%(senderName)s neatliko pakeitimo.", + "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s atšaukė %(targetName)s pakvietimą.", + "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s atnaujino šį kambarį.", + "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s padarė kambarį viešą visiems žinantiems nuorodą.", + "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s padarė kambarį tik pakviestiems.", + "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s pakeitė prisijungimo normą į %(rule)s", + "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s leido svečiams prisijungti prie kambario.", + "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s uždraudė svečiams prisijungti prie kambario.", + "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s pakeitė svečių prieigą prie %(rule)s", + "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s įjungė ženkliukus bendruomenėi %(groups)s šiame kambaryje.", + "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s išjungė ženkliukus bendruomenėi %(groups)s šiame kambaryje.", + "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s įjungė ženkliukus bendruomenėi %(newGroups)s ir išjungė ženkliukus bendruomenėi %(oldGroups)s šiame kambaryje.", + "%(senderName)s placed a %(callType)s call.": "%(senderName)s pradėjo %(callType)s skambutį.", + "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s atšaukė pakvietimą %(targetDisplayName)s prisijungti prie kambario.", + "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s išsiuntė pakvietimą %(targetDisplayName)s prisijungti prie kambario.", + "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s padarė būsimą kambario istoriją matomą visiems kambario dalyviams, nuo pat jų prisijungimo.", + "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s padarė būsimą kambario istoriją matomą nežinomam (%(visibility)s).", + "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s iš %(fromPowerLevel)s į %(toPowerLevel)s", + "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s pakeitė %(powerLevelDiffText)s galios lygį.", + "%(displayName)s is typing …": "%(displayName)s rašo …", + "%(names)s and %(count)s others are typing …|other": "%(names)s ir %(count)s kiti rašo …", + "%(names)s and %(count)s others are typing …|one": "%(names)s ir vienas kitas rašo …", + "%(names)s and %(lastPerson)s are typing …": "%(names)s ir %(lastPerson)s rašo …", + "Cannot reach homeserver": "Serveris nepasiekiamas", + "Ensure you have a stable internet connection, or get in touch with the server admin": "Įsitikinkite, kad jūsų interneto ryšys yra stabilus, arba susisiekite su serverio administratoriumi", + "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Paprašykite savo Riot administratoriaus patikrinti ar jūsų konfigūracijoje nėra neteisingų arba pasikartojančių įrašų.", + "Cannot reach identity server": "Tapatybės serveris nepasiekiamas", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Jūs galite registruotis, tačiau kai kurios funkcijos bus nepasiekiamos, kol tapatybės serveris prisijungs. Jei ir toliau matote šį įspėjimą, patikrinkite savo konfigūraciją arba susisiekite su serverio administratoriumi.", + "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Jūs galite iš naujo nustatyti savo slaptažodį, tačiau kai kurios funkcijos bus nepasiekiamos, kol tapatybės serveris prisijungs. Jei ir toliau matote šį įspėjimą, patikrinkite savo konfigūraciją arba susisiekite su serverio administratoriumi.", + "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Jūs galite prisijungti, tačiau kai kurios funkcijos bus nepasiekiamos, kol tapatybės serveris prisijungs. Jei ir toliau matote šį įspėjimą, patikrinkite savo konfigūraciją arba susisiekite su serverio administratoriumi.", + "No homeserver URL provided": "Nepateiktas serverio URL", + "Unexpected error resolving homeserver configuration": "Netikėta klaida nustatant serverio konfigūraciją", + "Unexpected error resolving identity server configuration": "Netikėta klaida nustatant tapatybės serverio konfigūraciją", + "%(items)s and %(count)s others|other": "%(items)s ir %(count)s kiti", + "%(items)s and %(count)s others|one": "%(items)s ir vienas kitas", + "%(items)s and %(lastItem)s": "%(items)s ir %(lastItem)s", + "Unrecognised address": "Neatpažintas adresas", + "You do not have permission to invite people to this room.": "Jūs neturite leidimo pakviesti žmones į šį kambarį.", + "User %(userId)s is already in the room": "Vartotojas %(userId)s jau yra kambaryje", + "User %(user_id)s may or may not exist": "Vartotojas %(user_id)s gali ir neegzistuoti", + "The user must be unbanned before they can be invited.": "Norint pakviesti vartotoją, pirmiausia turi būti pašalintas draudimas.", + "The user's homeserver does not support the version of the room.": "Vartotojo serveris nepalaiko kambario versijos.", + "Use a longer keyboard pattern with more turns": "Naudokite ilgesnį klaviatūros modelį su daugiau vijų", + "There was an error joining the room": "Prisijungiant prie kambario įvyko klaida", + "Failed to join room": "Prisijungti prie kambario nepavyko", + "Message Pinning": "Žinutės prisegimas", + "Custom user status messages": "Pasirinktinės vartotojo būsenos žinutės", + "Group & filter rooms by custom tags (refresh to apply changes)": "Grupuoti ir filtruoti kambarius pagal pasirinktines žymas (atnaujinkite, kad pritaikytumėte pakeitimus)", + "Render simple counters in room header": "Užkrauti paprastus skaitiklius kambario antraštėje", + "Multiple integration managers": "Daugialypiai integracijų valdikliai" } From bb5f532eeb5d074c16439f50af7d2a62996e6249 Mon Sep 17 00:00:00 2001 From: fenuks Date: Wed, 6 Nov 2019 00:32:52 +0000 Subject: [PATCH 0507/2372] Translated using Weblate (Polish) Currently translated at 74.6% (1382 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pl/ --- src/i18n/strings/pl.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index dd09059da8..31f82bc2dd 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -1645,5 +1645,13 @@ "Chat with Riot Bot": "Rozmowa z Botem Riota", "FAQ": "Najczęściej zadawane pytania", "Always show the window menu bar": "Zawsze pokazuj pasek menu okna", - "Close button should minimize window to tray": "Przycisk zamknięcia minimalizuje okno do zasobnika" + "Close button should minimize window to tray": "Przycisk zamknięcia minimalizuje okno do zasobnika", + "Add Email Address": "Dodaj adres e-mail", + "Add Phone Number": "Dodaj numer telefonu", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Ta czynność wymaga dostępu do domyślnego serwera tożsamości do walidacji adresu e-mail, czy numeru telefonu, ale serwer nie określa warunków korzystania z usługi.", + "Sends a message as plain text, without interpreting it as markdown": "Wysyła wiadomość jako zwykły tekst, bez jego interpretacji jako markdown", + "You do not have the required permissions to use this command.": "Nie posiadasz wymaganych uprawnień do użycia tego polecenia.", + "Changes the avatar of the current room": "Zmienia awatar dla obecnego pokoju", + "Use an identity server": "Użyj serwera tożsamości", + "Show previews/thumbnails for images": "Pokaż podgląd/miniatury obrazów" } From 4fe95b0075832725f9e11a64cd3d44be29ee1ceb Mon Sep 17 00:00:00 2001 From: Walter Date: Thu, 7 Nov 2019 18:22:05 +0000 Subject: [PATCH 0508/2372] Translated using Weblate (Russian) Currently translated at 99.8% (1849 of 1853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index b22c627213..01065a9e96 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -1299,7 +1299,7 @@ "Two-way device verification using short text": "Двусторонняя проверка устройства используя короткий текст", "Enable Emoji suggestions while typing": "Включить предложения эмоджи при наборе", "Show a placeholder for removed messages": "Показывать плашки вместо удалённых сообщений", - "Show join/leave messages (invites/kicks/bans unaffected)": "Показывать сообщения о вступлении/выходе (не влияет на приглашения, исключения и запреты)", + "Show join/leave messages (invites/kicks/bans unaffected)": "Показывать сообщения о вступлении | выходе (не влияет на приглашения, исключения и запреты)", "Show avatar changes": "Показывать изменения аватара", "Show display name changes": "Показывать изменения отображаемого имени", "Show read receipts": "Показывать уведомления о прочтении", @@ -1347,7 +1347,7 @@ "Account management": "Управление аккаунтом", "Deactivating your account is a permanent action - be careful!": "Деактивация вашей учётной записи — это необратимое действие. Будьте осторожны!", "Chat with Riot Bot": "Чат с ботом Riot", - "Help & About": "Помощь & о программе", + "Help & About": "Помощь & О программе", "FAQ": "Часто задаваемые вопросы", "Versions": "Версии", "Lazy loading is not supported by your current homeserver.": "Ленивая подгрузка не поддерживается вашим сервером.", @@ -1391,7 +1391,7 @@ "Backing up %(sessionsRemaining)s keys...": "Резервное копирование %(sessionsRemaining)s ключей...", "All keys backed up": "Все ключи сохранены", "Developer options": "Параметры разработчика", - "General": "Общий", + "General": "Общие", "Set a new account password...": "Установить новый пароль учётной записи...", "Legal": "Законный", "At this time it is not possible to reply with an emote.": "В настоящее время невозможно ответить с помощью эмоции.", @@ -2018,7 +2018,7 @@ "Create a private room": "Создать приватную комнату", "Topic (optional)": "Тема (опционально)", "Make this room public": "Сделать комнату публичной", - "Use the new, faster, composer for writing messages": "Используйте новый, более быстрый, composer для написания сообщений.", + "Use the new, faster, composer for writing messages": "Используйте новый, более быстрый, редактор для написания сообщений.", "Send read receipts for messages (requires compatible homeserver to disable)": "Отправлять подтверждения о прочтении сообщений (требуется отключение совместимого домашнего сервера)", "Show previews/thumbnails for images": "Показать превью / миниатюры для изображений", "Disconnect from the identity server and connect to instead?": "Отключиться от сервера идентификации и вместо этого подключиться к ?", @@ -2160,5 +2160,10 @@ "Recent rooms": "Недавние комнаты", "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Сервер идентификации не настроен, поэтому вы не можете добавить адрес электронной почты, чтобы в будущем сбросить пароль.", "Jump to first unread room.": "Перейти в первую непрочитанную комнату.", - "Jump to first invite.": "Перейти к первому приглашению." + "Jump to first invite.": "Перейти к первому приглашению.", + "Trust": "Доверие", + "%(count)s unread messages including mentions.|one": "1 непрочитанное упоминание.", + "%(count)s unread messages.|one": "1 непрочитанное сообщение.", + "Unread messages.": "Непрочитанные сообщения.", + "Message Actions": "Сообщение действий" } From 2a5dc9bfac61ec792efed970c71be470588e2f3f Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 8 Nov 2019 16:35:40 +0000 Subject: [PATCH 0509/2372] Remove lint comments about no-descending-specificity We have disabled the `no-descending-specificity` stylelint rule, so we no longer need these block comments. --- res/css/structures/_AutoHideScrollbar.scss | 9 ++++----- res/css/views/dialogs/_DevtoolsDialog.scss | 4 ---- res/css/views/elements/_Field.scss | 4 ---- res/css/views/rooms/_EventTile.scss | 5 ----- 4 files changed, 4 insertions(+), 18 deletions(-) diff --git a/res/css/structures/_AutoHideScrollbar.scss b/res/css/structures/_AutoHideScrollbar.scss index db86a6fbd6..6e4484157c 100644 --- a/res/css/structures/_AutoHideScrollbar.scss +++ b/res/css/structures/_AutoHideScrollbar.scss @@ -14,12 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -/* This file has CSS for both native and non-native scrollbars in an - * order that's fairly logic to read but violates stylelints descending - * specificity rule, so turn it off for this file. It also duplicates - * a selector to separate the hiding/showing from the sizing. +/* This file has CSS for both native and non-native scrollbars in an order + * that's fairly logical to read but duplicates a selector to separate the + * hiding/showing from the sizing. */ -/* stylelint-disable no-descending-specificity, no-duplicate-selectors */ +/* stylelint-disable no-duplicate-selectors */ /* 1. for browsers that support native overlay auto-hiding scrollbars diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index 417d0d6744..9d58c999c3 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -135,9 +135,6 @@ limitations under the License. } } -/* Ordering this block by specificity would require breaking it up into several - chunks, which seems like it would be more confusing to read. */ -/* stylelint-disable no-descending-specificity */ .mx_DevTools_tgl-flip { + .mx_DevTools_tgl-btn { padding: 2px; @@ -192,4 +189,3 @@ limitations under the License. } } } -/* stylelint-enable no-descending-specificity */ diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index da896f947d..4d012a136e 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -148,9 +148,6 @@ limitations under the License. color: $greyed-fg-color; } -/* Ordering this block by specificity would require breaking it up into several - chunks, which seems like it would be more confusing to read. */ -/* stylelint-disable no-descending-specificity */ .mx_Field_valid { &.mx_Field, &.mx_Field:focus-within { @@ -174,7 +171,6 @@ limitations under the License. color: $input-invalid-border-color; } } -/* stylelint-enable no-descending-specificity */ .mx_Field_tooltip { margin-top: -12px; diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 98bfa248ff..fb85b9cf88 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -561,9 +561,6 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { /* end of overrides */ -/* Ordering this block by specificity would require breaking it up into several - chunks, which seems like it would be more confusing to read. */ -/* stylelint-disable no-descending-specificity */ .mx_MatrixChat_useCompactLayout { .mx_EventTile { padding-top: 4px; @@ -641,5 +638,3 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { } } } - -/* stylelint-enable no-descending-specificity */ From 5a5ebee9189e7fb9fbc333797b99cca5d4ebd44b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 8 Nov 2019 10:39:38 -0700 Subject: [PATCH 0510/2372] Check for a message type before assuming it is a room message Redacted messages do not have message types, despite being room messages. Fixes https://github.com/vector-im/riot-web/issues/11352 Regressed in https://github.com/matrix-org/matrix-react-sdk/pull/3601 Click-to-ping being broken (as mentioned by https://github.com/vector-im/riot-web/issues/11353) is a side effect of the react stack falling over. Once one room crashes, click-to-ping is broken everywhere. --- src/components/views/rooms/EventTile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 786a72f5b3..22f1f914b6 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -556,10 +556,10 @@ module.exports = createReactClass({ // Info messages are basically information about commands processed on a room const isBubbleMessage = eventType.startsWith("m.key.verification") || - (eventType === "m.room.message" && msgtype.startsWith("m.key.verification")); + (eventType === "m.room.message" && msgtype && msgtype.startsWith("m.key.verification")); let isInfoMessage = ( !isBubbleMessage && eventType !== 'm.room.message' && - eventType !== 'm.sticker' && eventType != 'm.room.create' + eventType !== 'm.sticker' && eventType !== 'm.room.create' ); let tileHandler = getHandlerTile(this.props.mxEvent); From e161e99b63c5ea18fcebc7c244c72ba20fd4cceb Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 8 Nov 2019 17:46:08 +0000 Subject: [PATCH 0511/2372] Fix rounded corners for the formatting toolbar The formatting toolbar is meant to have rounded corners like the message action bar. Fixes https://github.com/vector-im/riot-web/issues/11203 --- res/css/views/rooms/_MessageComposerFormatBar.scss | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/res/css/views/rooms/_MessageComposerFormatBar.scss b/res/css/views/rooms/_MessageComposerFormatBar.scss index 43a2fb257e..80909529ee 100644 --- a/res/css/views/rooms/_MessageComposerFormatBar.scss +++ b/res/css/views/rooms/_MessageComposerFormatBar.scss @@ -41,6 +41,18 @@ limitations under the License. &:hover { border-color: $message-action-bar-hover-border-color; } + + &:first-child { + border-radius: 3px 0 0 3px; + } + + &:last-child { + border-radius: 0 3px 3px 0; + } + + &:only-child { + border-radius: 3px; + } } .mx_MessageComposerFormatBar_button { From c4d45e87ea251f62f3ffc8c33c916613d256a9b3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 8 Nov 2019 15:54:48 -0700 Subject: [PATCH 0512/2372] Use a ternary operator instead of relying on AND semantics in EditHIstoryDialog Fixes https://github.com/vector-im/riot-web/issues/11334 (probably). `allEvents` should never have a boolean in it, so given the stack trace and the code this is my best estimate for what the problem could be. I can't reproduce the problem. --- src/components/views/dialogs/MessageEditHistoryDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js index 6014cb941c..b5e4daa1c1 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.js +++ b/src/components/views/dialogs/MessageEditHistoryDialog.js @@ -116,7 +116,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent { nodes.push(( Date: Fri, 8 Nov 2019 16:07:11 -0700 Subject: [PATCH 0513/2372] Fix HTML fallback in replies Correctly encode the `body` to avoid problems down the line. We also convert newlines to `
    ` to better represent the message as a fallback. Fixes https://github.com/vector-im/riot-web/issues/9413 --- package.json | 1 + src/components/views/elements/ReplyThread.js | 15 +++++++++++++-- yarn.lock | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 0caff285e8..95b294c8a5 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "diff-match-patch": "^1.0.4", "emojibase-data": "^4.0.2", "emojibase-regex": "^3.0.0", + "escape-html": "^1.0.3", "file-saver": "^1.3.3", "filesize": "3.5.6", "flux": "2.1.1", diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index fac0a71617..55fd028980 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -1,6 +1,7 @@ /* Copyright 2017 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,6 +24,7 @@ import {wantsDateSeparator} from '../../../DateUtils'; import {MatrixEvent, MatrixClient} from 'matrix-js-sdk'; import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import SettingsStore from "../../../settings/SettingsStore"; +import escapeHtml from "escape-html"; // This component does no cycle detection, simply because the only way to make such a cycle would be to // craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would @@ -101,6 +103,15 @@ export default class ReplyThread extends React.Component { if (html) html = this.stripHTMLReply(html); } + if (!body) body = ""; // Always ensure we have a body, for reasons. + + // Escape the body to use as HTML below. + // We also run a nl2br over the result to fix the fallback representation. We do this + // after converting the text to safe HTML to avoid user-provided BR's from being converted. + if (!html) html = escapeHtml(body).replace(/\n/g, '
    '); + + // dev note: do not rely on `body` being safe for HTML usage below. + const evLink = permalinkCreator.forEvent(ev.getId()); const userLink = makeUserPermalink(ev.getSender()); const mxid = ev.getSender(); @@ -110,7 +121,7 @@ export default class ReplyThread extends React.Component { case 'm.text': case 'm.notice': { html = `
    In reply to ${mxid}` - + `
    ${html || body}
    `; + + `
    ${html}`; const lines = body.trim().split('\n'); if (lines.length > 0) { lines[0] = `<${mxid}> ${lines[0]}`; @@ -140,7 +151,7 @@ export default class ReplyThread extends React.Component { break; case 'm.emote': { html = `
    In reply to * ` - + `${mxid}
    ${html || body}
    `; + + `${mxid}
    ${html}`; const lines = body.trim().split('\n'); if (lines.length > 0) { lines[0] = `* <${mxid}> ${lines[0]}`; diff --git a/yarn.lock b/yarn.lock index fa7868d270..60fef54c16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2862,7 +2862,7 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -escape-html@~1.0.3: +escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= From c46a3764c3dc8db7ec8ba8b928ad620abb8400d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Fri, 8 Nov 2019 18:40:35 +0000 Subject: [PATCH 0514/2372] Translated using Weblate (French) Currently translated at 100.0% (1864 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 328c3b7f9e..40840c8d58 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2280,5 +2280,16 @@ "Show tray icon and minimize window to it on close": "Afficher l’icône dans la barre d’état et minimiser la fenêtre lors de la fermeture", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Cette action nécessite l’accès au serveur d’identité par défaut afin de valider une adresse e-mail ou un numéro de téléphone, mais le serveur n’a aucune condition de service.", "Trust": "Confiance", - "Message Actions": "Actions de message" + "Message Actions": "Actions de message", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Send verification requests in direct message": "Envoyer les demandes de vérification en message direct", + "You verified %(name)s": "Vous avez vérifié %(name)s", + "You cancelled verifying %(name)s": "Vous avez annulé la vérification de %(name)s", + "%(name)s cancelled verifying": "%(name)s a annulé la vérification", + "You accepted": "Vous avez accepté", + "%(name)s accepted": "%(name)s a accepté", + "You cancelled": "Vous avez annulé", + "%(name)s cancelled": "%(name)s a annulé", + "%(name)s wants to verify": "%(name)s veut vérifier", + "You sent a verification request": "Vous avez envoyé une demande de vérification" } From 949ba89b4a5cc657d953ca3566badb5b24779c4e Mon Sep 17 00:00:00 2001 From: Elwyn Malethan Date: Sat, 9 Nov 2019 19:00:33 +0000 Subject: [PATCH 0515/2372] Added translation using Weblate (Welsh) --- src/i18n/strings/cy.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/i18n/strings/cy.json diff --git a/src/i18n/strings/cy.json b/src/i18n/strings/cy.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/src/i18n/strings/cy.json @@ -0,0 +1 @@ +{} From def4f90257bd486b41305b6e34c3087b29e72a46 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Sun, 10 Nov 2019 18:11:53 +0000 Subject: [PATCH 0516/2372] Translated using Weblate (Albanian) Currently translated at 99.8% (1860 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 43 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 4f25162491..1d80a90a2b 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2205,5 +2205,46 @@ "Emoji Autocomplete": "Vetëplotësim Emoji-sh", "Notification Autocomplete": "Vetëplotësim NJoftimesh", "Room Autocomplete": "Vetëplotësim Dhomash", - "User Autocomplete": "Vetëplotësim Përdoruesish" + "User Autocomplete": "Vetëplotësim Përdoruesish", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Ky veprim lyp hyrje te shërbyesi parazgjedhje i identiteteve për të vlerësuar një adresë email ose një numër telefoni, por shërbyesi nuk ka ndonjë kusht shërbimesh.", + "Trust": "Besim", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Përdorni panelin e ri UserInfo për Anëtarë Dhome dhe Anëtarë Grupi", + "Send verification requests in direct message": "Dërgo kërkesa verifikimi në mesazh të drejtpërdrejt", + "Show tray icon and minimize window to it on close": "Me mbylljen, shfaq ikonë paneli dhe minimizo dritaren", + "Room %(name)s": "Dhoma %(name)s", + "Recent rooms": "Dhoma së fundi", + "%(count)s unread messages including mentions.|one": "1 përmendje e palexuar.", + "%(count)s unread messages.|one": "1 mesazh i palexuar.", + "Unread messages.": "Mesazhe të palexuar.", + "Trust & Devices": "Besim & Pajisje", + "Direct messages": "Mesazhe të drejtpërdrejtë", + "Failed to deactivate user": "S’u arrit të çaktivizohet përdorues", + "This client does not support end-to-end encryption.": "Ky klient nuk mbulon fshehtëzim skaj-më-skaj.", + "Messages in this room are not end-to-end encrypted.": "Mesazhet në këtë dhomë nuk janë të fshehtëzuara skaj-më-skaj.", + "React": "Reagoni", + "Message Actions": "Veprime Mesazhesh", + "You verified %(name)s": "Verifikuat %(name)s", + "You cancelled verifying %(name)s": "Anuluat verifikimin e %(name)s", + "%(name)s cancelled verifying": "%(name)s anuloi verifikimin", + "You accepted": "Pranuat", + "%(name)s accepted": "%(name)s pranoi", + "You cancelled": "Anuluat", + "%(name)s cancelled": "%(name)s anuloi", + "%(name)s wants to verify": "%(name)s dëshiron të verifikojë", + "You sent a verification request": "Dërguat një kërkesë verifikimi", + "Frequently Used": "Përdorur Shpesh", + "Smileys & People": "Emotikone & Persona", + "Animals & Nature": "Kafshë & Natyrë", + "Food & Drink": "Ushqim & Pije", + "Activities": "Veprimtari", + "Travel & Places": "Udhëtim & Vende", + "Objects": "Objekte", + "Symbols": "Simbole", + "Flags": "Flamuj", + "Quick Reactions": "Reagime të Shpejta", + "Cancel search": "Anulo kërkimin", + "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "S’ka shërbyes identitetesh të formësuar, ndaj s’mund të shtoni një adresë email që të mund të ricaktoni fjalëkalimin tuaj në të ardhmen.", + "Jump to first unread room.": "Hidhu te dhoma e parë e palexuar.", + "Jump to first invite.": "Hidhu te ftesa e parë." } From eaac3fe3b8000423f195edcc865fa32cb3ff2deb Mon Sep 17 00:00:00 2001 From: Osoitz Date: Sat, 9 Nov 2019 11:12:46 +0000 Subject: [PATCH 0517/2372] Translated using Weblate (Basque) Currently translated at 100.0% (1864 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eu/ --- src/i18n/strings/eu.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index 72b6fff50d..888f6984e7 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -2222,5 +2222,19 @@ "Jump to first unread room.": "Jauzi irakurri gabeko lehen gelara.", "Jump to first invite.": "Jauzi lehen gonbidapenera.", "Command Autocomplete": "Aginduak auto-osatzea", - "DuckDuckGo Results": "DuckDuckGo emaitzak" + "DuckDuckGo Results": "DuckDuckGo emaitzak", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Ekintza honek lehenetsitako identitate-zerbitzaria atzitzea eskatzen du, e-mail helbidea edo telefono zenbakia balioztatzeko, baina zerbitzariak ez du erabilera baldintzarik.", + "Trust": "Jo fidagarritzat", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Send verification requests in direct message": "Bidali egiaztaketa eskariak mezu zuzen bidez", + "Message Actions": "Mezu-ekintzak", + "You verified %(name)s": "%(name)s egiaztatu duzu", + "You cancelled verifying %(name)s": "%(name)s egiaztatzeari utzi diozu", + "%(name)s cancelled verifying": "%(name)s(e)k egiaztaketa utzi du", + "You accepted": "Onartu duzu", + "%(name)s accepted": "%(name)s onartuta", + "You cancelled": "Utzi duzu", + "%(name)s cancelled": "%(name)s utzita", + "%(name)s wants to verify": "%(name)s(e)k egiaztatu nahi du", + "You sent a verification request": "Egiaztaketa eskari bat bidali duzu" } From a4a0dc9c2d7e14c32c1d3cba6e3663f277e443a4 Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Mon, 11 Nov 2019 06:47:26 +0000 Subject: [PATCH 0518/2372] Translated using Weblate (Bulgarian) Currently translated at 100.0% (1864 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/bg/ --- src/i18n/strings/bg.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index 6f198b2e5a..3697cc635c 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -2249,5 +2249,16 @@ "Emoji Autocomplete": "Подсказка за емоджита", "Notification Autocomplete": "Подсказка за уведомления", "Room Autocomplete": "Подсказка за стаи", - "User Autocomplete": "Подсказка за потребители" + "User Autocomplete": "Подсказка за потребители", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Send verification requests in direct message": "Изпращай заявки за потвърждение чрез директни съобщения", + "You verified %(name)s": "Потвърдихте %(name)s", + "You cancelled verifying %(name)s": "Отказахте потвърждаването за %(name)s", + "%(name)s cancelled verifying": "%(name)s отказа потвърждаването", + "You accepted": "Приехте", + "%(name)s accepted": "%(name)s прие", + "You cancelled": "Отказахте потвърждаването", + "%(name)s cancelled": "%(name)s отказа", + "%(name)s wants to verify": "%(name)s иска да извърши потвърждение", + "You sent a verification request": "Изпратихте заявка за потвърждение" } From d8ea25403acb33a166f7f62ab668146c51cf306a Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Mon, 11 Nov 2019 03:26:55 +0000 Subject: [PATCH 0519/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1864 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 58dca89415..185026aad5 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2273,5 +2273,16 @@ "Show tray icon and minimize window to it on close": "顯示系統匣圖示並在關閉視窗時將其最小化至其中", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "此動作需要存取預設的身份識別伺服器 以驗證電子郵件或電話號碼,但伺服器沒有任何服務條款。", "Trust": "信任", - "Message Actions": "訊息動作" + "Message Actions": "訊息動作", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Send verification requests in direct message": "在直接訊息中傳送驗證請求", + "You verified %(name)s": "您驗證了 %(name)s", + "You cancelled verifying %(name)s": "您已取消驗證 %(name)s", + "%(name)s cancelled verifying": "%(name)s 已取消驗證", + "You accepted": "您已接受", + "%(name)s accepted": "%(name)s 已接受", + "You cancelled": "您已取消", + "%(name)s cancelled": "%(name)s 已取消", + "%(name)s wants to verify": "%(name)s 想要驗證", + "You sent a verification request": "您已傳送了驗證請求" } From 163f9f057fd39db2beb45ecd060a34bdb49e5f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20Luke=C5=A1?= Date: Sun, 10 Nov 2019 22:20:21 +0000 Subject: [PATCH 0520/2372] Translated using Weblate (Czech) Currently translated at 99.9% (1863 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/cs/ --- src/i18n/strings/cs.json | 179 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 174 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 2d3088c279..e4e01b0116 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -1716,7 +1716,7 @@ "Remove messages": "Odstranit zprávy", "Notify everyone": "Upozornění pro celou místnost", "Enable encryption?": "Povolit šifrování?", - "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Po zapnutí už nelze šifrování v této místnosti zakázat. Zprávy v šifrovaných místostech můžou číst jenom členové místnosti, server se k obsahu nedostane. Šifrování místností nepodporuje většina botů a bridgů. Více informací o šifrování.", + "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Po zapnutí už nelze šifrování v této místnosti zakázat. Zprávy v šifrovaných místostech můžou číst jenom členové místnosti, server se k obsahu nedostane. Šifrování místností nepodporuje většina botů a propojení. Více informací o šifrování.", "Error updating main address": "Nepovedlo se změnit hlavní adresu", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "Nastala chyba při pokusu o nastavení hlavní adresy místnosti. Mohl to zakázat server, nebo to může být dočasná chyba.", "Error creating alias": "Nepovedlo se vyrobit alias", @@ -1957,8 +1957,8 @@ "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "Pokud nechcete na hledání existujících kontaktů používat server , zvolte si jiný server.", "Identity Server": "Server identit", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Aktuálně nepoužíváte žádný server identit. Na hledání existujících kontaktů a přidání se do registru kontatů přidejte server identit níže.", - "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Odpojení ze serveru identit znamená, že vás nepůjde najít podle emailové adresy a telefonního čísla and vy taky nebudete moct hledat ostatní.", - "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Použití serveru identit je volitelné. Nemusíte server identit používat, ale nepůjde vás pak najít podle emailové adresy a telefonního čísla a vy také nebudete moct hledat ostatní.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Odpojení ze serveru identit znamená, že vás nepůjde najít podle emailové adresy ani telefonního čísla and vy také nebudete moct hledat ostatní.", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Použití serveru identit je volitelné. Nemusíte server identit používat, ale nepůjde vás pak najít podle emailové adresy ani telefonního čísla a vy také nebudete moct hledat ostatní.", "Do not use an identity server": "Nepoužívat server identit", "Enter a new identity server": "Zadejte nový server identit", "Failed to update integration manager": "Nepovedlo se aktualizovat správce integrací", @@ -1969,7 +1969,7 @@ "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "Aktálně používáte %(serverName)s na správu robotů, widgetů, and balíků samolepek.", "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Zadejte kterého správce integrací chcete používat na správu robotů, widgetů a balíků samolepek.", "Integration Manager": "Správce integrací", - "Enter a new integration manager": "Přidat nový správce integrací", + "Enter a new integration manager": "Přidat nového správce integrací", "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Musíte odsouhlasit podmínky použití serveru (%(serverName)s) abyste se mohli zapsat do registru emailových adres a telefonních čísel.", "Deactivate account": "Deaktivovat účet", "Always show the window menu bar": "Vždy zobrazovat horní lištu okna", @@ -2002,5 +2002,174 @@ "Command Help": "Nápověda příkazu", "Integrations Manager": "Správce integrací", "Find others by phone or email": "Hledat ostatní pomocí emailu nebo telefonu", - "Be found by phone or email": "Umožnit ostatním mě nalézt pomocí emailu nebo telefonu" + "Be found by phone or email": "Umožnit ostatním mě nalézt pomocí emailu nebo telefonu", + "Add Email Address": "Přidat emailovou adresu", + "Add Phone Number": "Přidat telefonní číslo", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Tato akce vyžaduje přístup k výchozímu serveru identity aby šlo ověřit email a telefon, ale server nemá podmínky použití.", + "Trust": "Důvěra", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Multiple integration managers": "Více správců integrací", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Používat nový, konzistentní panel s informaci o uživalelích pro členy místností and skupin", + "Send verification requests in direct message": "Poslat žádost o verifikaci jako přímou zprávu", + "Use the new, faster, composer for writing messages": "Použít nový, rychlejší editor zpráv", + "Show previews/thumbnails for images": "Zobrazovat náhledy obrázků", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Před odpojením byste měli smazat osobní údaje ze serveru identit . Bohužel, server je offline nebo neodpovídá.", + "You should:": "Měli byste:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "zkontrolujte, jestli nemáte v prohlížeči nějaký doplněk blokující server identit (například Privacy Badger)", + "contact the administrators of identity server ": "konaktujte administrátora serveru identit ", + "wait and try again later": "počkejte z zkuste to znovu později", + "Discovery": "Nalezitelnost", + "Clear cache and reload": "Smazat mezipaměť a načíst znovu", + "Show tray icon and minimize window to it on close": "Zobrazovat systémovou ikonu a minimalizovat při zavření", + "Read Marker lifetime (ms)": "životnost značky přečteno (ms)", + "Read Marker off-screen lifetime (ms)": "životnost značky přečteno mimo obrazovku (ms)", + "A device's public name is visible to people you communicate with": "Veřejné jméno zařízení je viditelné pro lidi se kterými komunikujete", + "Upgrade the room": "Upgradovat místnost", + "Enable room encryption": "Povolit v místnosti šifrování", + "Error changing power level requirement": "Chyba změny požadavku na úroveň oprávnění", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Došlo k chybě při změně požadované úrovně oprávnění v místnosti. Ubezpečte se, že na to máte dostatečná práva a zkuste to znovu.", + "Error changing power level": "Chyba při změně úrovně oprávnění", + "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Došlo k chybě při změně úrovně oprávnění uživatele. Ubezpečte se, že na to máte dostatečná práva a zkuste to znovu.", + "Unable to revoke sharing for email address": "Nepovedlo se zrušit sdílení emailové adresy", + "Unable to share email address": "Nepovedlo se nasdílet emailovou adresu", + "Your email address hasn't been verified yet": "Vaše emailová adresa nebyla zatím verifikována", + "Click the link in the email you received to verify and then click continue again.": "Pro verifikaci a pokračování klikněte na odkaz v emailu, který vím přišel.", + "Verify the link in your inbox": "Ověřte odkaz v emailové schánce", + "Complete": "Dokončit", + "Revoke": "Zneplatnit", + "Share": "Sdílet", + "Discovery options will appear once you have added an email above.": "Možnosti nalezitelnosti se objeví až výše přidáte svou emailovou adresu.", + "Unable to revoke sharing for phone number": "Nepovedlo se zrušit sdílení telefonního čísla", + "Unable to share phone number": "Nepovedlo se nasdílet telefonní číslo", + "Please enter verification code sent via text.": "Zadejte prosím ověřovací SMS kód.", + "Discovery options will appear once you have added a phone number above.": "Možnosti nalezitelnosti se objeví až zadáte telefonní číslo výše.", + "Remove %(email)s?": "Odstranit %(email)s?", + "Remove %(phone)s?": "Odstranit %(phone)s?", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "SMS zpráva byla odeslána na +%(msisdn)s. Zadejte prosím ověřovací kód, který obsahuje.", + "No recent messages by %(user)s found": "Nebyly nalezeny žádné nedávné zprávy od uživatele %(user)s", + "Try scrolling up in the timeline to see if there are any earlier ones.": "Zkuste zascrollovat nahoru, jestli tam nejsou nějaké dřívější.", + "Remove recent messages by %(user)s": "Odstranit nedávné zprávy od uživatele %(user)s", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "Odstraňujeme %(count)s zpráv od %(user)s. Nelze to vzít zpět. Chcete pokračovat?", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "Odstraňujeme jednu zprávu od %(user)s. Nelze to vzít zpět. Chcete pokračovat?", + "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Pro větší množství zpráv to může nějakou dobu trvat. V průběhu prosím neobnovujte klienta.", + "Remove %(count)s messages|other": "Odstranit %(count)s zpráv", + "Remove %(count)s messages|one": "Odstranit zprávu", + "Deactivate user?": "Deaktivovat uživatele?", + "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deaktivování uživatele ho odhlásí a zabrání mu v opětovném přihlášení. Navíc bude ostraněn ze všech místností. Akci nelze vzít zpět. Jste si jistí, že chcete uživatele deaktivovat?", + "Deactivate user": "Deaktivovat uživatele", + "Remove recent messages": "Odstranit nedávné zprávy", + "Bold": "Tučně", + "Italics": "Kurzívou", + "Strikethrough": "Přešktnutě", + "Code block": "Blok kódu", + "Room %(name)s": "Místnost %(name)s", + "Recent rooms": "Nedávné místnosti", + "Loading room preview": "Načítání náhdledu místnosti", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "Při ověřování pozvánky jsme dostali chybu (%(errcode)s). Můžete zkusit tuto informaci předat administrátorovi místnosti.", + "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "Pozvánka do místnosti %(roomName)s byla poslána na %(email)s, který není přidaný k tomuto účtu", + "Link this email with your account in Settings to receive invites directly in Riot.": "Přidejte si tento email k účtu v Nastavení, abyste dostávali pozvání přímo v Riotu.", + "This invite to %(roomName)s was sent to %(email)s": "Pozvánka do %(roomName)s byla odeslána na %(email)s", + "Use an identity server in Settings to receive invites directly in Riot.": "Používat server identit z nastavení k přijímání pozvánek přímo v Riotu.", + "Share this email in Settings to receive invites directly in Riot.": "Sdílet tento email v nastavení, abyste mohli dostávat pozvánky přímo v Riotu.", + "%(count)s unread messages including mentions.|other": "%(count)s nepřečtených zpráv a zmínek.", + "%(count)s unread messages including mentions.|one": "Nepřečtená zmínka.", + "%(count)s unread messages.|other": "%(count)s nepřečtených zpráv.", + "%(count)s unread messages.|one": "Nepřečtená zpráva.", + "Unread mentions.": "Nepřečtená zmínka.", + "Unread messages.": "Nepřečtené zprávy.", + "Failed to connect to integrations server": "Nepovedlo se připojit k serveru integrací", + "No integrations server is configured to manage stickers with": "Žádný server integrací není nakonfigurován aby spravoval samolepky", + "Trust & Devices": "Důvěra & Zařízení", + "Direct messages": "Přímé zprávy", + "Failed to deactivate user": "Deaktivace uživatele se nezdařila", + "This client does not support end-to-end encryption.": "Tento klient nepodporuje end-to-end šifrování.", + "Messages in this room are not end-to-end encrypted.": "Zprávy nejsou end-to-end šifrované.", + "React": "Reagovat", + "Message Actions": "Akce zprávy", + "Show image": "Zobrazit obrázek", + "You verified %(name)s": "Ověřili jste %(name)s", + "You cancelled verifying %(name)s": "Zrušili jste ověření %(name)s", + "%(name)s cancelled verifying": "%(name)s zrušil/a ověření", + "You accepted": "Přijali jste", + "%(name)s accepted": "%(name)s přijal/a", + "You cancelled": "Zrušili jste", + "%(name)s cancelled": "%(name)s zrušil/a", + "%(name)s wants to verify": "%(name)s chce ověřit", + "You sent a verification request": "Poslali jste požadavek na ověření", + "Show all": "Zobrazit vše", + "Edited at %(date)s. Click to view edits.": "Pozměněno v %(date)s. Klinutím zobrazíte změny.", + "Frequently Used": "Často používané", + "Smileys & People": "Obličeje & Lidé", + "Animals & Nature": "Zvířata & Příroda", + "Food & Drink": "Jídlo & Nápoje", + "Activities": "Aktivity", + "Travel & Places": "Cestování & Místa", + "Objects": "Objekty", + "Symbols": "Symboly", + "Flags": "Vlajky", + "Quick Reactions": "Rychlé reakce", + "Cancel search": "Zrušit hledání", + "Please create a new issue on GitHub so that we can investigate this bug.": "Vyrobte prosím nové issue na GitHubu abychom mohli chybu opravit.", + "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s neudělali %(count)s krát žádnou změnu", + "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)sneudělali žádnou změnu", + "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)sneudělal %(count)s krát žádnou změnu", + "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)sneudělal žádnou změnu", + "Room alias": "Alias místnosti", + "e.g. my-room": "například moje-mistost", + "Please provide a room alias": "Zadejte prosím alias místnosti", + "This alias is available to use": "Tento alias je volný", + "This alias is already in use": "Tento alias už je používán", + "Use bots, bridges, widgets and sticker packs": "Použít roboty, propojení, widgety a balíky samolepek", + "Terms of Service": "Podmínky použití", + "To continue you need to accept the terms of this service.": "Musíte souhlasit s podmínkami použití, abychom mohli pokračovat.", + "Service": "Služba", + "Summary": "Shrnutí", + "Document": "Dokument", + "Upload all": "Nahrát vše", + "Resend edit": "Odeslat změnu znovu", + "Resend %(unsentCount)s reaction(s)": "Poslat %(unsentCount)s reakcí znovu", + "Resend removal": "Odeslat smazání znovu", + "Report Content": "Nahlásit obsah", + "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Na domovském serveru chybí veřejný klíč pro captcha. Nahlaste to prosím administrátorovi serveru.", + "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Žádný server identit není nakonfigurován, takže nemůžete přidat emailovou adresu pro obnovení hesla.", + "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Nastavit emailovou adresu pro obnovení hesla. Také můžete použít email nebo telefon, aby vás vaši přátelé snadno nalezli.", + "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Nastavit emailovou adresu pro obnovení hesla. Také můžete použít email, aby vás vaši přátelé snadno nalezli.", + "Enter your custom homeserver URL What does this mean?": "Zadejte adresu domovského serveru Co to znamená?", + "Enter your custom identity server URL What does this mean?": "Zadejte adresu serveru identit Co to znamená?", + "Explore": "Prohlížet", + "Filter": "Filtrovat", + "Filter rooms…": "Filtrovat místnosti…", + "%(creator)s created and configured the room.": "%(creator)s vytvořil a nakonfiguroval místnost.", + "Preview": "Náhled", + "View": "Zobrazit", + "Find a room…": "Najít místnost…", + "Find a room… (e.g. %(exampleRoom)s)": "Najít místnost… (například %(exampleRoom)s)", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Pokud nemůžete nelézt místnost, kterou hledáte, napište o pozvánku nebo si jí Vytvořte.", + "Explore rooms": "Prohlížet místnosti", + "Jump to first unread room.": "Skočit na první nepřečtenou místnost.", + "Jump to first invite.": "Skočit na první pozvánku.", + "No identity server is configured: add one in server settings to reset your password.": "Žádný server identit není nakonfigurován: přidejte si ho v nastavení, abyste mohli obnovit heslo.", + "This account has been deactivated.": "Tento účet byl deaktivován.", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "nový účet (%(newAccountId)s) je registrován, ale už jste přihlášeni pod jiným účtem (%(loggedInUserId)s).", + "Continue with previous account": "Pokračovat s předchozím účtem", + "Log in to your new account.": "Přihlaste se svým novým účtem.", + "You can now close this window or log in to your new account.": "Teď můžete okno zavřít, nebo se přihlásit svým novým účtem.", + "Registration Successful": "Úspěšná registrace", + "Failed to re-authenticate due to a homeserver problem": "Kvůli problémům s domovským server se nepovedlo autentifikovat znovu", + "Failed to re-authenticate": "Nepovedlo se autentifikovat", + "Regain access to your account and recover encryption keys stored on this device. Without them, you won’t be able to read all of your secure messages on any device.": "Získejte znovu přístup k účtu a obnovte si šifrovací klíče uložené na tomto zařízení. Bez nich nebudete schopni číst zabezpečené zprávy na některých zařízeních.", + "Enter your password to sign in and regain access to your account.": "Zadejte heslo pro přihlášení a obnovte si přístup k účtu.", + "Forgotten your password?": "Zapomněli jste heslo?", + "Sign in and regain access to your account.": "Přihlaste se a získejte přístup ke svému účtu.", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "Nemůžete se přihlásit do svého účtu. Kontaktujte administrátora domovského serveru pro více informací.", + "You're signed out": "Jste odhlášeni", + "Clear personal data": "Smazat osobní data", + "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Varování: Vaše osobní data (včetně šifrovacích klíčů) jsou pořád uložena na tomto zařízení. Smažte je, pokud už toto zařízení nehodláte používat nebo se přihlašte pod jiný účet.", + "Command Autocomplete": "Automatické doplňování příkazů", + "Community Autocomplete": "Automatické doplňování komunit", + "DuckDuckGo Results": "Výsledky hledání DuckDuckGo", + "Emoji Autocomplete": "Automatické doplňování Emodži", + "Notification Autocomplete": "Automatické doplňování upozornění", + "Room Autocomplete": "Automatické doplňování místností", + "User Autocomplete": "Automatické doplňování uživatelů" } From 0bfbf34c3956c8364134739a78a3c983597af814 Mon Sep 17 00:00:00 2001 From: Tirifto Date: Sun, 10 Nov 2019 21:24:20 +0000 Subject: [PATCH 0521/2372] Translated using Weblate (Esperanto) Currently translated at 99.9% (1862 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eo/ --- src/i18n/strings/eo.json | 155 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 143 insertions(+), 12 deletions(-) diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 7e4599aaea..bbed6773b5 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -182,7 +182,7 @@ "Answer": "Respondi", "Send anyway": "Tamen sendi", "Send": "Sendi", - "Enable automatic language detection for syntax highlighting": "Ŝalti memfaran rekonon de lingvo por sintaksa markado", + "Enable automatic language detection for syntax highlighting": "Ŝalti memagan rekonon de lingvo por sintaksa markado", "Hide avatars in user and room mentions": "Kaŝi profilbildojn en mencioj de uzantoj kaj babilejoj", "Disable big emoji in chat": "Malŝalti grandajn mienetojn en babilo", "Don't send typing notifications": "Ne elsendi sciigojn pri tajpado", @@ -1270,7 +1270,7 @@ "Ignored users": "Malatentaj uzantoj", "Key backup": "Sekurkopio de ŝlosilo", "Security & Privacy": "Sekureco & Privateco", - "Voice & Video": "Voĉo k Vido", + "Voice & Video": "Voĉo kaj vido", "Upgrade room to version %(ver)s": "Ĝisdatigi ĉambron al versio %(ver)s", "Room information": "Ĉambraj informoj", "Internal room ID:": "Ena ĉambra identigilo:", @@ -1507,7 +1507,7 @@ "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "Ŝanĝoj al viaj komunumaj nomo kaj profilbildo eble ne montriĝos al aliaj uzantoj ĝis 30 minutoj.", "Who can join this community?": "Kiu povas aliĝi al tiu ĉi komunumo?", "This room is not public. You will not be able to rejoin without an invite.": "Ĉi tiu ĉambro ne estas publika. Vi ne povos realiĝi sen invito.", - "Can't leave Server Notices room": "Ne eblas eliri el ĉambro «  Server Notices  »", + "Can't leave Server Notices room": "Ne eblas eliri el ĉambro « Server Notices »", "Revoke invite": "Nuligi inviton", "Invited by %(sender)s": "Invitita de %(sender)s", "Error updating main address": "Ĝisdatigo de la ĉefa adreso eraris", @@ -1710,7 +1710,7 @@ "Connect this device to Key Backup": "Konekti ĉi tiun aparaton al Savkopiado de ŝlosiloj", "Start using Key Backup": "Ekuzi Savkopiadon de ŝlosiloj", "Timeline": "Historio", - "Autocomplete delay (ms)": "Prokrasto de memfara kompletigo", + "Autocomplete delay (ms)": "Prokrasto de memaga kompletigo", "Upgrade this room to the recommended room version": "Gradaltigi ĉi tiun ĉambron al rekomendata ĉambra versio", "Open Devtools": "Malfermi programistajn ilojn", "Uploaded sound": "Alŝutita sono", @@ -1747,7 +1747,7 @@ "Go to Settings": "Iri al agordoj", "Flair": "Etikedo", "No Audio Outputs detected": "Neniu soneligo troviĝis", - "Send %(eventType)s events": "Sendi okazojn de tipo «%(eventType)s»", + "Send %(eventType)s events": "Sendi okazojn de tipo « %(eventType)s »", "Select the roles required to change various parts of the room": "Elektu la rolojn postulatajn por ŝanĝado de diversaj partoj de la ĉambro", "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Post ŝalto, ĉifrado de ĉambro ne povas esti malŝaltita. Mesaĝoj senditaj al ĉifrata ĉambro ne estas videblaj por la servilo, nur por la partoprenantoj de la ĉambro. Ŝalto de ĉifrado eble malfunkciigos iujn robotojn kaj pontojn. Eksciu plion pri ĉifrado.", "To link to this room, please add an alias.": "Por ligili al la ĉambro, bonvolu aldoni kromnomon.", @@ -1756,7 +1756,7 @@ "Once enabled, encryption cannot be disabled.": "Post ŝalto, ne plu eblas malŝalti ĉifradon.", "Encrypted": "Ĉifrata", "Some devices in this encrypted room are not trusted": "Iuj aparatoj en ĉi tiu ĉifrata ĉambro ne estas fidataj", - "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "Petoj pri kunhavigo de ŝlosiloj sendiĝas al viaj aliaj aparatoj memfare. Se vi rifuzis aŭ forlasis la peton en viaj aliaj aparatoj, klaku ĉi tien por repeti la ŝlosilojn por tiu ĉi kunsido.", + "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "Petoj pri kunhavigo de ŝlosiloj sendiĝas al viaj aliaj aparatoj memage. Se vi rifuzis aŭ forlasis la peton en viaj aliaj aparatoj, klaku ĉi tien por repeti la ŝlosilojn por tiu ĉi kunsido.", "The conversation continues here.": "La interparolo pluas ĉi tie.", "This room has been replaced and is no longer active.": "Ĉi tiu ĉambro estas anstataŭita, kaj ne plu aktivas.", "Loading room preview": "Preparas antaŭrigardon al la ĉambro", @@ -1790,7 +1790,7 @@ "Message edits": "Redaktoj de mesaĝoj", "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "Se vi renkontas problemojn aŭ havas prikomentojn, bonvolu sciigi nin per GitHub.", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "Por eviti duoblajn raportojn, bonvolu unue rigardi jamajn raportojn (kaj meti +1) aŭ raporti novan problemon se vi neniun trovos.", - "Report bugs & give feedback": "Raporti erarojn ϗ sendi prikomentojn", + "Report bugs & give feedback": "Raporti erarojn kaj sendi prikomentojn", "Clear Storage and Sign Out": "Vakigi memoron kaj adiaŭi", "We encountered an error trying to restore your previous session.": "Ni renkontis eraron provante rehavi vian antaŭan kunsidon.", "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Vakigo de la memoro de via foliumilo eble korektos la problemon, sed adiaŭigos vin, kaj malebligos legadon de historio de ĉifritaj babiloj.", @@ -1865,7 +1865,7 @@ "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "Gradaltigo de ĉi tiu ĉambro bezonas fermi ĝin, kaj krei novan por anstataŭi ĝin. Por plejbonigi sperton de la ĉambranoj, ni:", "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.": "Vi adiaŭis ĉiujn aparatojn kaj ne plu ricevados sciigojn. Por reŝalti ilin, resalutu per ĉiu el la aparatoj.", "Invalid homeserver discovery response": "Nevalida eltrova respondo de hejmservilo", - "Failed to get autodiscovery configuration from server": "Malsukcesis akiri agordojn de memfara eltrovado de la servilo", + "Failed to get autodiscovery configuration from server": "Malsukcesis akiri agordojn de memaga eltrovado de la servilo", "Homeserver URL does not appear to be a valid Matrix homeserver": "URL por hejmservilo ŝajne ne ligas al valida hejmservilo de Matrix", "Invalid identity server discovery response": "Nevalida eltrova respondo de identiga servilo", "Identity server URL does not appear to be a valid identity server": "URL por identiga servilo ŝajne ne ligas al valida identiga servilo", @@ -1932,7 +1932,7 @@ "Failed to start chat": "Malsukcesis komenci babilon", "Messages": "Mesaĝoj", "Actions": "Agoj", - "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Uzu identigan servilon por inviti retpoŝte. Klaku al »[…]« por uzi la implicitan identigan servilon (%(defaultIdentityServerName)s) aŭ administru tion en Agordoj.", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Uzu identigan servilon por inviti retpoŝte. Klaku al « daŭrigi » por uzi la norman identigan servilon (%(defaultIdentityServerName)s) aŭ administru tion en Agordoj.", "Displays list of commands with usages and descriptions": "Montras liston de komandoj kun priskribo de uzo", "Use the new, faster, but still experimental composer for writing messages (requires refresh)": "Uzi la novan, pli rapidan, sed ankoraŭ eksperimentan komponilon de mesaĝoj (bezonas aktualigon)", "Send read receipts for messages (requires compatible homeserver to disable)": "Sendi legokonfirmojn de mesaĝoj (bezonas akordan hejmservilon por malŝalto)", @@ -1957,8 +1957,8 @@ "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Vi nun uzas servilon por trovi kontaktojn, kaj troviĝi de ili. Vi povas ŝanĝi vian identigan servilon sube.", "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "Se vi ne volas uzi servilon por trovi kontaktojn kaj troviĝi mem, enigu alian identigan servilon sube.", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Vi nun ne uzas identigan servilon. Por trovi kontaktojn kaj troviĝi de ili mem, aldonu iun sube.", - "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Malkonektiĝo de via identiga servilo signifas, ke vi ne povos troviĝi de aliaj uzantoj, kaj vi ne povos memfare inviti aliajn per retpoŝto aŭ telefono.", - "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Vi ne devas uzi identigan servilon. Se vi tion elektos, vi ne povos troviĝi de aliaj uzantoj, kaj vi ne povos memfare inviti ilin per retpoŝto aŭ telefono.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Malkonektiĝo de via identiga servilo signifas, ke vi ne povos troviĝi de aliaj uzantoj, kaj vi ne povos memage inviti aliajn per retpoŝto aŭ telefono.", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Vi ne devas uzi identigan servilon. Se vi tion elektos, vi ne povos troviĝi de aliaj uzantoj, kaj vi ne povos memage inviti ilin per retpoŝto aŭ telefono.", "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Konsentu al uzkondiĉoj de la identiga servilo (%(serverName)s) por esti trovita per retpoŝtadreso aŭ telefonnumero.", "Discovery": "Trovado", "Deactivate account": "Malaktivigi konton", @@ -1996,5 +1996,136 @@ "View": "Rigardo", "Find a room…": "Trovi ĉambron…", "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Se vi ne povas travi la serĉatan ĉambron, petu inviton aŭ kreu novan ĉambron.", - "Explore rooms": "Esplori ĉambrojn" + "Explore rooms": "Esplori ĉambrojn", + "Add Email Address": "Aldoni retpoŝtadreson", + "Add Phone Number": "Aldoni telefonnumeron", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Ĉi tiu ago bezonas atingi la norman identigan servilon por kontroli retpoŝtadreson aŭ telefonnumeron, sed la servilo ne havas uzokondiĉojn.", + "Trust": "Fido", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Multiple integration managers": "Pluraj kunigiloj", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Uzi la novan, koheran informan panelon por ĉambranoj kaj grupanoj", + "Send verification requests in direct message": "Sendi kontrolajn petojn per rekta mesaĝo", + "Use the new, faster, composer for writing messages": "Uzi la novan, pli rapidan verkilon de mesaĝoj", + "Show previews/thumbnails for images": "Montri antaŭrigardojn/bildetojn je bildoj", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Vi forigu viajn personajn datumojn de identiga servilo antaŭ ol vi malkonektiĝos. Bedaŭrinde, identiga servilo estas nuntempe eksterreta kaj ne eblas ĝin atingi.", + "You should:": "Vi:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "kontrolu kromprogramojn de via foliumilo je ĉio, kio povus malhelpi konekton al la identiga servilo (ekzemple « Privacy Badger »)", + "contact the administrators of identity server ": "kontaktu la administrantojn de la identiga servilo ", + "wait and try again later": "atendu kaj reprovu pli poste", + "Failed to update integration manager": "Malsukcesis ĝisdatigi kunigilon", + "Integration manager offline or not accessible.": "Kunigilo estas eksterreta aŭ alie neatingebla", + "Terms of service not accepted or the integration manager is invalid.": "Aŭ zokondiĉoj ne akceptiĝis, aŭ la kunigilo estas nevalida.", + "Integration manager has no terms of service": "Kunigilo havas neniujn uzokondiĉojn", + "The integration manager you have chosen does not have any terms of service.": "La elektita kunigilo havas neniujn uzokondiĉojn.", + "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "Vi nun mastrumas viajn robotojn, fenestaĵojn, kaj glumarkarojn per %(serverName)s.", + "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Aldonu kiun kunigilon vi volas por mastrumi viajn robotojn, fenestraĵojn, kaj glumarkarojn.", + "Integration Manager": "Kunigilo", + "Enter a new integration manager": "Metu novan kunigilon", + "Clear cache and reload": "Vakigi kaŝmemoron kaj relegi", + "Show tray icon and minimize window to it on close": "Montri pletan bildsimbolon kaj tien plejetigi la fenestron je fermo", + "Read Marker lifetime (ms)": "Vivodaŭro de legomarko (ms)", + "Read Marker off-screen lifetime (ms)": "Vivodaŭro de eksterekrana legomarko (ms)", + "Unable to revoke sharing for email address": "Ne povas senvalidigi havigadon je retpoŝtadreso", + "Unable to share email address": "Ne povas havigi vian retpoŝtadreson", + "Your email address hasn't been verified yet": "Via retpoŝtadreso ankoraŭ ne kontroliĝis", + "Click the link in the email you received to verify and then click continue again.": "Klaku la ligilon en la ricevita retletero por kontroli, kaj poste reklaku al « daŭrigi ».", + "Verify the link in your inbox": "Kontrolu la ligilon en via ricevujo.", + "Complete": "Fini", + "Revoke": "Senvalidigi", + "Share": "Havigi", + "Discovery options will appear once you have added an email above.": "Eltrovaj agordoj aperos kiam vi aldonos supre retpoŝtadreson.", + "Unable to revoke sharing for phone number": "Ne povas senvalidigi havigadon je telefonnumero", + "Unable to share phone number": "Ne povas havigi telefonnumeron", + "Please enter verification code sent via text.": "Bonvolu enigi kontrolan kodon senditan per tekstmesaĝo.", + "Discovery options will appear once you have added a phone number above.": "Eltrovaj agordoj aperos kiam vi aldonos telefonnumeron supre.", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "Tekstmesaĝo sendiĝis al +%(msisdn)s. Bonvolu enigi la kontrolan kodon enhavitan.", + "Try scrolling up in the timeline to see if there are any earlier ones.": "Provu rulumi supren tra la historio por kontroli, ĉu ne estas iuj pli fruaj.", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "Vi estas forigonta 1 mesaĝon de %(user)s. Ne eblas tion malfari. Ĉu vi volas pluigi?", + "Remove %(count)s messages|one": "Forigi 1 mesaĝon", + "Room %(name)s": "Ĉambro %(name)s", + "Recent rooms": "Freŝaj vizititaj ĉambroj", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "Eraris (%(errcode)s) validigo de via invito. Vi povas transdoni ĉi tiun informon al ĉambra administranto.", + "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "Ĉi tiu invito al %(roomName)s sendiĝis al %(email)s, kiu ne estas ligita al via konto", + "Link this email with your account in Settings to receive invites directly in Riot.": "Ligu ĉi tiun retpoŝtadreson al via konto en Agordoj por ricevadi invitojn rekte per Riot.", + "This invite to %(roomName)s was sent to %(email)s": "La invito al %(roomName)s sendiĝis al %(email)s", + "Use an identity server in Settings to receive invites directly in Riot.": "Uzu identigan servilon en Agordoj por ricevadi invitojn rekte per Riot.", + "Share this email in Settings to receive invites directly in Riot.": "Havigu ĉi tiun retpoŝtadreson per Agordoj por ricevadi invitojn rekte per Riot.", + "%(count)s unread messages including mentions.|other": "%(count)s nelegitaj mesaĝoj, inkluzive menciojn.", + "%(count)s unread messages including mentions.|one": "1 nelegita mencio.", + "%(count)s unread messages.|other": "%(count)s nelegitaj mesaĝoj.", + "%(count)s unread messages.|one": "1 nelegita mesaĝo.", + "Unread mentions.": "Nelegitaj mencioj.", + "Unread messages.": "Nelegitaj mesaĝoj.", + "Trust & Devices": "Fido kaj Aparatoj", + "Direct messages": "Rektaj mesaĝoj", + "Failed to deactivate user": "Malsukcesis malaktivigi uzanton", + "This client does not support end-to-end encryption.": "Ĉi tiu kliento ne subtenas tutvojan ĉifradon.", + "Messages in this room are not end-to-end encrypted.": "Mesaĝoj en ĉi tiu ĉambro ne estas tutvoje ĉifrataj.", + "React": "Reagi", + "Message Actions": "Mesaĝaj agoj", + "Show image": "Montri bildon", + "You verified %(name)s": "Vi kontrolis %(name)s", + "You cancelled verifying %(name)s": "Vi nuligis kontrolon de %(name)s", + "%(name)s cancelled verifying": "%(name)s nuligis kontrolon", + "You accepted": "Vi akceptis", + "%(name)s accepted": "%(name)s akceptis", + "You cancelled": "Vi nuligis", + "%(name)s cancelled": "%(name)s nuligis", + "%(name)s wants to verify": "%(name)s volas kontroli", + "You sent a verification request": "Vi sendis peton de kontrolo", + "Frequently Used": "Ofte uzataj", + "Smileys & People": "Mienoj kaj homoj", + "Animals & Nature": "Bestoj kaj naturo", + "Food & Drink": "Manĝaĵoj kaj trinkaĵoj", + "Activities": "Agadoj", + "Travel & Places": "Lokoj kaj vojaĝado", + "Objects": "Aĵoj", + "Symbols": "Simboloj", + "Flags": "Flagoj", + "Quick Reactions": "Rapidaj reagoj", + "Cancel search": "Nuligi serĉon", + "Please create a new issue on GitHub so that we can investigate this bug.": "Bonvolu raporti novan problemon je GitHub, por ke ni povu ĝin esplori.", + "Room alias": "Kromnomo de ĉambro", + "e.g. my-room": "ekzemple mia-chambro", + "Please provide a room alias": "Bonvolu doni kromnomon de ĉambro", + "This alias is available to use": "Ĉi tiu kromnomo estas disponebla", + "This alias is already in use": "Ĉi tiu kromnomo jam estas uzata", + "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "Uzu identigan servilon por inviti per retpoŝto. Uzu la norman (%(defaultIdentityServerName)s) aŭ mastrumu per Agordoj.", + "Use an identity server to invite by email. Manage in Settings.": "Uzu identigan servilon por inviti per retpoŝto. Mastrumu per Agordoj.", + "Close dialog": "Fermi interagujon", + "Please enter a name for the room": "Bonvolu enigi nomon por la ĉambro", + "Set a room alias to easily share your room with other people.": "Agordu kromnomon de la ĉambro por facile ĝin kunhavigi al aliaj homoj.", + "This room is private, and can only be joined by invitation.": "Ĉi tiu ĉambro estas privata, kaj eblas aliĝi nur per invito.", + "Create a public room": "Krei publikan ĉambron", + "Create a private room": "Krei privatan ĉambron", + "Topic (optional)": "Temo (malnepra)", + "Make this room public": "Publikigi ĉi tiun ĉambron", + "Hide advanced": "Kaŝi specialajn", + "Show advanced": "Montri specialajn", + "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Bloki aliĝojn al ĉi tiu ĉambro de uzantoj el aliaj Matrix-serviloj (Ĉi tiun agordon ne eblas poste ŝanĝi!)", + "To verify that this device can be trusted, please check that the key you see in User Settings on that device matches the key below:": "Por kontroli ke tiu ĉi aparato estas fidinda, bonvolu kontroli, ke la ŝlosilo, kiun vi vidas en viaj Agordoj de uzanto je tiu aparato, akordas kun la ŝlosilo sube:", + "Please fill why you're reporting.": "Bonvolu skribi, kial vi raportas.", + "Report Content to Your Homeserver Administrator": "Raporti enhavon al la administrantode via hejmservilo", + "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Per raporto de ĝi tiu mesaĝo vi sendos ĝian unikan « eventan identigilon » al la administranto de via hejmservilo. Se mesaĝoj en ĉi tiu ĉambro estas ĉifrataj, la administranto de via hejmservilo ne povos legi la tekston de la mesaĝo, nek rigardi dosierojn aŭ bildojn.", + "Send report": "Sendi raporton", + "Command Help": "Helpo pri komando", + "Integrations Manager": "Kunigilo", + "To continue you need to accept the terms of this service.": "Por pluigi, vi devas akcepti la uzokondiĉojn de ĉi tiu servo.", + "Document": "Dokumento", + "Report Content": "Raporti enhavon", + "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Neniu identiga servilo estas agordita, kaj tial vi ne povas aldoni retpoŝtadreson por ose restarigi vian pasvorton.", + "Enter your custom homeserver URL What does this mean?": "Enigu vian propran hejmservilan URL-on. Kion tio signifas?", + "Enter your custom identity server URL What does this mean?": "Enigu vian propran URL-on de identiga servilo. Kion tio signifas?", + "%(creator)s created and configured the room.": "%(creator)s kreis kaj agordis la ĉambron.", + "Find a room… (e.g. %(exampleRoom)s)": "Trovi ĉambron… (ekzemple (%(exampleRoom)s)", + "Jump to first unread room.": "Salti al unua nelegita ĉambro.", + "Jump to first invite.": "Salti al unua invito.", + "No identity server is configured: add one in server settings to reset your password.": "Neniu identiga servilo estas agordita: aldonu iun per la servilaj agordoj, por restarigi vian pasvorton.", + "Command Autocomplete": "Memkompletigo de komandoj", + "Community Autocomplete": "Memkompletigo de komunumoj", + "DuckDuckGo Results": "Rezultoj de DuckDuckGo", + "Emoji Autocomplete": "Memkompletigo de mienetoj", + "Notification Autocomplete": "Memkompletigo de sciigoj", + "Room Autocomplete": "Memkompletigo de ĉambroj", + "User Autocomplete": "Memkompletigo de uzantoj" } From d545a1e0b2655a55766ef9bb3016830536ea3b0b Mon Sep 17 00:00:00 2001 From: Szimszon Date: Sun, 10 Nov 2019 12:40:40 +0000 Subject: [PATCH 0522/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1864 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 5d21a17e37..67af8a6a57 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2267,5 +2267,16 @@ "Show tray icon and minimize window to it on close": "Tálcaikon mutatása és az ablak összecsukása bezáráskor", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Ez a művelet az e-mail cím vagy telefonszám ellenőrzése miatt hozzáférést igényel az alapértelmezett azonosítási szerverhez (), de a szervernek nincsen semmilyen felhasználási feltétele.", "Trust": "Megbízom benne", - "Message Actions": "Üzenet Műveletek" + "Message Actions": "Üzenet Műveletek", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Send verification requests in direct message": "Ellenőrzés kérés küldése közvetlen üzenetben", + "You verified %(name)s": "Ellenőrizted: %(name)s", + "You cancelled verifying %(name)s": "Az ellenőrzést megszakítottad ehhez: %(name)s", + "%(name)s cancelled verifying": "%(name)s megszakította az ellenőrzést", + "You accepted": "Elfogadtad", + "%(name)s accepted": "%(name)s elfogadta", + "You cancelled": "Megszakítottad", + "%(name)s cancelled": "%(name)s megszakította", + "%(name)s wants to verify": "%(name)s ellenőrizni szeretné", + "You sent a verification request": "Ellenőrzési kérést küldtél" } From ab9f3780194637574b28f97c4f36719b054edee3 Mon Sep 17 00:00:00 2001 From: Elwyn Malethan Date: Sat, 9 Nov 2019 19:01:03 +0000 Subject: [PATCH 0523/2372] Translated using Weblate (Welsh) Currently translated at 0.5% (9 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/cy/ --- src/i18n/strings/cy.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cy.json b/src/i18n/strings/cy.json index 0967ef424b..951468c747 100644 --- a/src/i18n/strings/cy.json +++ b/src/i18n/strings/cy.json @@ -1 +1,11 @@ -{} +{ + "This email address is already in use": "Mae'r cyfeiriad e-bost yma yn cael ei ddefnyddio eisoes", + "This phone number is already in use": "Mae'r rhif ffôn yma yn cael ei ddefnyddio eisoes", + "Add Email Address": "Ychwanegu Cyfeiriad E-bost", + "Failed to verify email address: make sure you clicked the link in the email": "Methiant gwirio cyfeiriad e-bost: gwnewch yn siŵr eich bod wedi clicio'r ddolen yn yr e-bost", + "Add Phone Number": "Ychwanegu Rhif Ffôn", + "The platform you're on": "Y platfform rydych chi arno", + "The version of Riot.im": "Fersiwn Riot.im", + "Whether or not you're logged in (we don't record your username)": "Os ydych wedi mewngofnodi ai peidio (nid ydym yn cofnodi'ch enw defnyddiwr)", + "Your language of choice": "Eich iaith o ddewis" +} From ef0529413308504b6fde2a1d177a6db00c98dfac Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 11 Nov 2019 10:24:40 +0000 Subject: [PATCH 0524/2372] Fix draw order when hovering composer format buttons This ensures all 4 sides of a button show the hover border colour as intended. Another part of https://github.com/vector-im/riot-web/issues/11203 --- res/css/views/rooms/_MessageComposerFormatBar.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_MessageComposerFormatBar.scss b/res/css/views/rooms/_MessageComposerFormatBar.scss index 80909529ee..1b5a21bed0 100644 --- a/res/css/views/rooms/_MessageComposerFormatBar.scss +++ b/res/css/views/rooms/_MessageComposerFormatBar.scss @@ -40,6 +40,7 @@ limitations under the License. &:hover { border-color: $message-action-bar-hover-border-color; + z-index: 1; } &:first-child { From 2c5565e5020edfe0306836fb37d49a8f410df2ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 18 Oct 2019 16:36:31 +0200 Subject: [PATCH 0525/2372] MatrixChat: Add some missing semicolons. --- src/components/structures/MatrixChat.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 51cf92da5f..402790df98 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -2130,13 +2130,13 @@ export default createReactClass({ checkpoint.roomId, checkpoint.token, 100, checkpoint.direction); } catch (e) { - console.log("Seshat: Error crawling events:", e) + console.log("Seshat: Error crawling events:", e); this.crawlerChekpoints.push(checkpoint); continue } if (res.chunk.length === 0) { - console.log("Seshat: Done with the checkpoint", checkpoint) + console.log("Seshat: Done with the checkpoint", checkpoint); // We got to the start/end of our timeline, lets just // delete our checkpoint and go back to sleep. await platform.removeCrawlerCheckpoint(checkpoint); @@ -2239,7 +2239,8 @@ export default createReactClass({ // Let us delete the checkpoint in that case, otherwise push // the new checkpoint to be used by the crawler. if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { - console.log("Seshat: Checkpoint had already all events added, stopping the crawl", checkpoint); + console.log("Seshat: Checkpoint had already all events", + "added, stopping the crawl", checkpoint); await platform.removeCrawlerCheckpoint(newCheckpoint); } else { this.crawlerChekpoints.push(newCheckpoint); From 1c4d89f2d77f605e2f1b6dd991b086faa10c7fd6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 11 Nov 2019 17:53:17 +0000 Subject: [PATCH 0526/2372] Migrate all standard Context Menus over to new custom framework --- res/css/views/context_menus/_TopLeftMenu.scss | 14 +- src/components/structures/ContextualMenu.js | 332 +++++++++++++++++- .../structures/TopLeftMenuButton.js | 73 ++-- .../GroupInviteTileContextMenu.js | 10 +- .../context_menus/RoomTileContextMenu.js | 180 +++++----- .../views/context_menus/TagTileContextMenu.js | 27 +- .../views/context_menus/TopLeftMenu.js | 29 +- src/components/views/elements/TagTile.js | 121 ++++--- .../views/emojipicker/ReactionPicker.js | 2 - .../views/groups/GroupInviteTile.js | 100 +++--- .../views/messages/MessageActionBar.js | 224 +++++++----- src/components/views/rooms/RoomTile.js | 141 ++++---- src/i18n/strings/en_EN.json | 1 + 13 files changed, 830 insertions(+), 424 deletions(-) diff --git a/res/css/views/context_menus/_TopLeftMenu.scss b/res/css/views/context_menus/_TopLeftMenu.scss index 9d258bcf55..d17d683e7e 100644 --- a/res/css/views/context_menus/_TopLeftMenu.scss +++ b/res/css/views/context_menus/_TopLeftMenu.scss @@ -49,23 +49,23 @@ limitations under the License. padding: 0; list-style: none; - li.mx_TopLeftMenu_icon_home::after { + .mx_TopLeftMenu_icon_home::after { mask-image: url('$(res)/img/feather-customised/home.svg'); } - li.mx_TopLeftMenu_icon_settings::after { + .mx_TopLeftMenu_icon_settings::after { mask-image: url('$(res)/img/feather-customised/settings.svg'); } - li.mx_TopLeftMenu_icon_signin::after { + .mx_TopLeftMenu_icon_signin::after { mask-image: url('$(res)/img/feather-customised/sign-in.svg'); } - li.mx_TopLeftMenu_icon_signout::after { + .mx_TopLeftMenu_icon_signout::after { mask-image: url('$(res)/img/feather-customised/sign-out.svg'); } - li::after { + .mx_AccessibleButton::after { mask-repeat: no-repeat; mask-position: 0 center; mask-size: 16px; @@ -78,14 +78,14 @@ limitations under the License. background-color: $primary-fg-color; } - li { + .mx_AccessibleButton { position: relative; cursor: pointer; white-space: nowrap; padding: 5px 20px 5px 43px; } - li:hover { + .mx_AccessibleButton:hover { background-color: $menu-selected-color; } } diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index 3f8c87efef..6d046c08d4 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -21,7 +21,9 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import {focusCapturedRef} from "../../utils/Accessibility"; -import {KeyCode} from "../../Keyboard"; +import {Key, KeyCode} from "../../Keyboard"; +import {_t} from "../../languageHandler"; +import sdk from "../../index"; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and @@ -229,6 +231,334 @@ export default class ContextualMenu extends React.Component { } } +const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]); + +class ContextualMenu2 extends React.Component { + propTypes: { + top: PropTypes.number, + bottom: PropTypes.number, + left: PropTypes.number, + right: PropTypes.number, + menuWidth: PropTypes.number, + menuHeight: PropTypes.number, + chevronOffset: PropTypes.number, + chevronFace: PropTypes.string, // top, bottom, left, right or none + // Function to be called on menu close + onFinished: PropTypes.func, + menuPaddingTop: PropTypes.number, + menuPaddingRight: PropTypes.number, + menuPaddingBottom: PropTypes.number, + menuPaddingLeft: PropTypes.number, + zIndex: PropTypes.number, + + // If true, insert an invisible screen-sized element behind the + // menu that when clicked will close it. + hasBackground: PropTypes.bool, + + // on resize callback + windowResize: PropTypes.func, + }; + + constructor() { + super(); + this.state = { + contextMenuRect: null, + }; + + // persist what had focus when we got initialized so we can return it after + this.initialFocus = document.activeElement; + } + + componentWillUnmount() { + // return focus to the thing which had it before us + this.initialFocus.focus(); + } + + collectContextMenuRect = (element) => { + // We don't need to clean up when unmounting, so ignore + if (!element) return; + + const first = element.querySelector('[role^="menuitem"]'); + if (first) { + first.focus(); + } + + this.setState({ + contextMenuRect: element.getBoundingClientRect(), + }); + }; + + onContextMenu = (e) => { + if (this.props.closeMenu) { + this.props.closeMenu(); + + e.preventDefault(); + const x = e.clientX; + const y = e.clientY; + + // XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst + // a context menu and its click-guard are up without completely rewriting how the context menus work. + setImmediate(() => { + const clickEvent = document.createEvent('MouseEvents'); + clickEvent.initMouseEvent( + 'contextmenu', true, true, window, 0, + 0, 0, x, y, false, false, + false, false, 0, null, + ); + document.elementFromPoint(x, y).dispatchEvent(clickEvent); + }); + } + }; + + _onMoveFocus = (element, up) => { + let descending = false; // are we currently descending or ascending through the DOM tree? + + do { + const child = up ? element.lastElementChild : element.firstElementChild; + const sibling = up ? element.previousElementSibling : element.nextElementSibling; + + if (descending) { + if (child) { + element = child; + } else if (sibling) { + element = sibling; + } else { + descending = false; + element = element.parentElement; + } + } else { + if (sibling) { + element = sibling; + descending = true; + } else { + element = element.parentElement; + } + } + + if (element) { + if (element.classList.contains("mx_ContextualMenu")) { // we hit the top + element = up ? element.lastElementChild : element.firstElementChild; + descending = true; + } + } + } while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role"))); + + if (element) { + element.focus(); + } + }; + + _onKeyDown = (ev) => { + let handled = true; + + switch (ev.key) { + case Key.TAB: + case Key.ESCAPE: + this.props.closeMenu(); + break; + case Key.ARROW_UP: + this._onMoveFocus(ev.target, true); + break; + case Key.ARROW_DOWN: + this._onMoveFocus(ev.target, false); + break; + default: + handled = false; + } + + if (handled) { + ev.stopPropagation(); + ev.preventDefault(); + } + }; + + render() { + const position = {}; + let chevronFace = null; + const props = this.props; + + if (props.top) { + position.top = props.top; + } else { + position.bottom = props.bottom; + } + + if (props.left) { + position.left = props.left; + chevronFace = 'left'; + } else { + position.right = props.right; + chevronFace = 'right'; + } + + const contextMenuRect = this.state.contextMenuRect || null; + const padding = 10; + + const chevronOffset = {}; + if (props.chevronFace) { + chevronFace = props.chevronFace; + } + const hasChevron = chevronFace && chevronFace !== "none"; + + if (chevronFace === 'top' || chevronFace === 'bottom') { + chevronOffset.left = props.chevronOffset; + } else { + const target = position.top; + + // By default, no adjustment is made + let adjusted = target; + + // If we know the dimensions of the context menu, adjust its position + // such that it does not leave the (padded) window. + if (contextMenuRect) { + adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding); + } + + position.top = adjusted; + chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted); + } + + let chevron; + if (hasChevron) { + chevron =
    ; + } + + const menuClasses = classNames({ + 'mx_ContextualMenu': true, + 'mx_ContextualMenu_left': !hasChevron && position.left, + 'mx_ContextualMenu_right': !hasChevron && position.right, + 'mx_ContextualMenu_top': !hasChevron && position.top, + 'mx_ContextualMenu_bottom': !hasChevron && position.bottom, + 'mx_ContextualMenu_withChevron_left': chevronFace === 'left', + 'mx_ContextualMenu_withChevron_right': chevronFace === 'right', + 'mx_ContextualMenu_withChevron_top': chevronFace === 'top', + 'mx_ContextualMenu_withChevron_bottom': chevronFace === 'bottom', + }); + + const menuStyle = {}; + if (props.menuWidth) { + menuStyle.width = props.menuWidth; + } + + if (props.menuHeight) { + menuStyle.height = props.menuHeight; + } + + if (!isNaN(Number(props.menuPaddingTop))) { + menuStyle["paddingTop"] = props.menuPaddingTop; + } + if (!isNaN(Number(props.menuPaddingLeft))) { + menuStyle["paddingLeft"] = props.menuPaddingLeft; + } + if (!isNaN(Number(props.menuPaddingBottom))) { + menuStyle["paddingBottom"] = props.menuPaddingBottom; + } + if (!isNaN(Number(props.menuPaddingRight))) { + menuStyle["paddingRight"] = props.menuPaddingRight; + } + + const wrapperStyle = {}; + if (!isNaN(Number(props.zIndex))) { + menuStyle["zIndex"] = props.zIndex + 1; + wrapperStyle["zIndex"] = props.zIndex; + } + + let background; + if (props.hasBackground) { + background = ( +
    + ); + } + + return ( +
    +
    + { chevron } + { props.children } +
    + { background } +
    + ); + } +} + +// Generic ContextMenu Portal wrapper +// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1} +// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines. + +export const ContextMenu = ({children, onFinished, props, hasBackground=true}) => { + const menu = + { children } + ; + + return ReactDOM.createPortal(menu, getOrCreateContainer()); +}; + +// Semantic component for representing a role=menuitem +export const MenuItem = ({children, label, ...props}) => { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return ( + + { children } + + ); +}; +MenuItem.propTypes = { + label: PropTypes.string, // optional + className: PropTypes.string, // optional + onClick: PropTypes.func.isRequired, +}; + +// Semantic component for representing a role=group for grouping menu radios/checkboxes +export const MenuGroup = ({children, label, ...props}) => { + return
    + { children } +
    ; +}; +MenuGroup.propTypes = { + label: PropTypes.string.isRequired, + className: PropTypes.string, // optional +}; + +// Semantic component for representing a role=menuitemcheckbox +export const MenuItemCheckbox = ({children, label, active=false, disabled=false, ...props}) => { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return ( + + { children } + + ); +}; +MenuItemCheckbox.propTypes = { + label: PropTypes.string, // optional + active: PropTypes.bool.isRequired, + disabled: PropTypes.bool, // optional + className: PropTypes.string, // optional + onClick: PropTypes.func.isRequired, +}; + +// Semantic component for representing a role=menuitemradio +export const MenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return ( + + { children } + + ); +}; +MenuItemRadio.propTypes = { + label: PropTypes.string, // optional + active: PropTypes.bool.isRequired, + disabled: PropTypes.bool, // optional + className: PropTypes.string, // optional + onClick: PropTypes.func.isRequired, +}; + export function createMenu(ElementClass, props, hasBackground=true) { const closeMenu = function(...args) { ReactDOM.unmountComponentAtNode(getOrCreateContainer()); diff --git a/src/components/structures/TopLeftMenuButton.js b/src/components/structures/TopLeftMenuButton.js index 42b8623e56..e0875efb42 100644 --- a/src/components/structures/TopLeftMenuButton.js +++ b/src/components/structures/TopLeftMenuButton.js @@ -17,15 +17,14 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import * as ContextualMenu from './ContextualMenu'; import {TopLeftMenu} from '../views/context_menus/TopLeftMenu'; -import AccessibleButton from '../views/elements/AccessibleButton'; import BaseAvatar from '../views/avatars/BaseAvatar'; import MatrixClientPeg from '../../MatrixClientPeg'; import Avatar from '../../Avatar'; import { _t } from '../../languageHandler'; import dis from "../../dispatcher"; -import {focusCapturedRef} from "../../utils/Accessibility"; +import {ContextMenu} from "./ContextualMenu"; +import sdk from "../../index"; const AVATAR_SIZE = 28; @@ -40,11 +39,8 @@ export default class TopLeftMenuButton extends React.Component { super(); this.state = { menuDisplayed: false, - menuFunctions: null, // should be { close: fn } profileInfo: null, }; - - this.onToggleMenu = this.onToggleMenu.bind(this); } async _getProfileInfo() { @@ -95,7 +91,21 @@ export default class TopLeftMenuButton extends React.Component { } } + openMenu = (e) => { + e.preventDefault(); + e.stopPropagation(); + this.setState({ menuDisplayed: true }); + }; + + closeMenu = () => { + this.setState({ + menuDisplayed: false, + }); + }; + render() { + const cli = MatrixClientPeg.get().getUserId(); + const name = this._getDisplayName(); let nameElement; let chevronElement; @@ -106,10 +116,28 @@ export default class TopLeftMenuButton extends React.Component { chevronElement = ; } - return ( + let contextMenu; + if (this.state.menuDisplayed) { + const elementRect = this._buttonRef.getBoundingClientRect(); + const x = elementRect.left; + const y = elementRect.top + elementRect.height; + + const props = { + chevronFace: "none", + left: x, + top: y, + }; + + contextMenu = + + ; + } + + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return this._buttonRef = r} aria-label={_t("Your profile")} aria-haspopup={true} @@ -126,33 +154,8 @@ export default class TopLeftMenuButton extends React.Component { { nameElement } { chevronElement } - ); - } - onToggleMenu(e) { - e.preventDefault(); - e.stopPropagation(); - - if (this.state.menuDisplayed && this.state.menuFunctions) { - this.state.menuFunctions.close(); - return; - } - - const elementRect = e.currentTarget.getBoundingClientRect(); - const x = elementRect.left; - const y = elementRect.top + elementRect.height; - - const menuFunctions = ContextualMenu.createMenu(TopLeftMenu, { - chevronFace: "none", - left: x, - top: y, - userId: MatrixClientPeg.get().getUserId(), - displayName: this._getDisplayName(), - containerRef: focusCapturedRef, // Focus the TopLeftMenu on first render - onFinished: () => { - this.setState({ menuDisplayed: false, menuFunctions: null }); - }, - }); - this.setState({ menuDisplayed: true, menuFunctions }); + { contextMenu } + ; } } diff --git a/src/components/views/context_menus/GroupInviteTileContextMenu.js b/src/components/views/context_menus/GroupInviteTileContextMenu.js index 8c0d9c215b..093e6045c7 100644 --- a/src/components/views/context_menus/GroupInviteTileContextMenu.js +++ b/src/components/views/context_menus/GroupInviteTileContextMenu.js @@ -22,6 +22,7 @@ import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; import {Group} from 'matrix-js-sdk'; import GroupStore from "../../../stores/GroupStore"; +import {MenuItem} from "../../structures/ContextualMenu"; export default class GroupInviteTileContextMenu extends React.Component { static propTypes = { @@ -36,7 +37,7 @@ export default class GroupInviteTileContextMenu extends React.Component { this._onClickReject = this._onClickReject.bind(this); } - componentWillMount() { + componentDidMount() { this._unmounted = false; } @@ -78,12 +79,11 @@ export default class GroupInviteTileContextMenu extends React.Component { } render() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return
    - - + + { _t('Reject') } - +
    ; } } diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 9bb573026f..6b1149cca1 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -32,6 +32,36 @@ import * as RoomNotifs from '../../../RoomNotifs'; import Modal from '../../../Modal'; import RoomListActions from '../../../actions/RoomListActions'; import RoomViewStore from '../../../stores/RoomViewStore'; +import {MenuItem, MenuItemCheckbox, MenuItemRadio} from "../../structures/ContextualMenu"; + +const RoomTagOption = ({active, onClick, src, srcSet, label}) => { + const classes = classNames('mx_RoomTileContextMenu_tag_field', { + 'mx_RoomTileContextMenu_tag_fieldSet': active, + 'mx_RoomTileContextMenu_tag_fieldDisabled': false, + }); + + return ( + + + + { label } + + ); +}; + +const NotifOption = ({active, onClick, src, label}) => { + const classes = classNames('mx_RoomTileContextMenu_notif_field', { + 'mx_RoomTileContextMenu_notif_fieldSet': active, + }); + + return ( + + + + { label } + + ); +}; module.exports = createReactClass({ displayName: 'RoomTileContextMenu', @@ -228,53 +258,36 @@ module.exports = createReactClass({ }, _renderNotifMenu: function() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - - const alertMeClasses = classNames({ - 'mx_RoomTileContextMenu_notif_field': true, - 'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES_LOUD, - }); - - const allNotifsClasses = classNames({ - 'mx_RoomTileContextMenu_notif_field': true, - 'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES, - }); - - const mentionsClasses = classNames({ - 'mx_RoomTileContextMenu_notif_field': true, - 'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.MENTIONS_ONLY, - }); - - const muteNotifsClasses = classNames({ - 'mx_RoomTileContextMenu_notif_field': true, - 'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.MUTE, - }); - return ( -
    -
    - +
    +
    +
    - - - - { _t('All messages (noisy)') } - - - - - { _t('All messages') } - - - - - { _t('Mentions only') } - - - - - { _t('Mute') } - + + + + +
    ); }, @@ -290,13 +303,12 @@ module.exports = createReactClass({ }, _renderSettingsMenu: function() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return (
    - - + + { _t('Settings') } - +
    ); }, @@ -329,52 +341,38 @@ module.exports = createReactClass({ return (
    - - + + { leaveText } - +
    ); }, _renderRoomTagMenu: function() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - - const favouriteClasses = classNames({ - 'mx_RoomTileContextMenu_tag_field': true, - 'mx_RoomTileContextMenu_tag_fieldSet': this.state.isFavourite, - 'mx_RoomTileContextMenu_tag_fieldDisabled': false, - }); - - const lowPriorityClasses = classNames({ - 'mx_RoomTileContextMenu_tag_field': true, - 'mx_RoomTileContextMenu_tag_fieldSet': this.state.isLowPriority, - 'mx_RoomTileContextMenu_tag_fieldDisabled': false, - }); - - const dmClasses = classNames({ - 'mx_RoomTileContextMenu_tag_field': true, - 'mx_RoomTileContextMenu_tag_fieldSet': this.state.isDirectMessage, - 'mx_RoomTileContextMenu_tag_fieldDisabled': false, - }); - return (
    - - - - { _t('Favourite') } - - - - - { _t('Low Priority') } - - - - - { _t('Direct Chat') } - + + +
    ); }, @@ -386,11 +384,11 @@ module.exports = createReactClass({ case 'join': return
    { this._renderNotifMenu() } -
    +
    { this._renderLeaveMenu(myMembership) } -
    +
    { this._renderRoomTagMenu() } -
    +
    { this._renderSettingsMenu() }
    ; case 'invite': @@ -400,7 +398,7 @@ module.exports = createReactClass({ default: return
    { this._renderLeaveMenu(myMembership) } -
    +
    { this._renderSettingsMenu() }
    ; } diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js index c0203a3ac8..3cfad4efe4 100644 --- a/src/components/views/context_menus/TagTileContextMenu.js +++ b/src/components/views/context_menus/TagTileContextMenu.js @@ -1,5 +1,6 @@ /* Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,11 +17,12 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import { MatrixClient } from 'matrix-js-sdk'; import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher'; import TagOrderActions from '../../../actions/TagOrderActions'; -import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; +import {MenuItem} from "../../structures/ContextualMenu"; export default class TagTileContextMenu extends React.Component { static propTypes = { @@ -29,6 +31,10 @@ export default class TagTileContextMenu extends React.Component { onFinished: PropTypes.func.isRequired, }; + static contextTypes = { + matrixClient: PropTypes.instanceOf(MatrixClient), + }; + constructor() { super(); @@ -45,18 +51,15 @@ export default class TagTileContextMenu extends React.Component { } _onRemoveClick() { - dis.dispatch(TagOrderActions.removeTag( - // XXX: Context menus don't have a MatrixClient context - MatrixClientPeg.get(), - this.props.tag, - )); + dis.dispatch(TagOrderActions.removeTag(this.context.matrixClient, this.props.tag)); this.props.onFinished(); } render() { const TintableSvg = sdk.getComponent("elements.TintableSvg"); + return
    -
    + { _t('View Community') } -
    -
    -
    - + +
    + + { _t('Hide') } -
    +
    ; } } diff --git a/src/components/views/context_menus/TopLeftMenu.js b/src/components/views/context_menus/TopLeftMenu.js index 815d0a5f55..d5b6817b00 100644 --- a/src/components/views/context_menus/TopLeftMenu.js +++ b/src/components/views/context_menus/TopLeftMenu.js @@ -25,6 +25,7 @@ import SdkConfig from '../../../SdkConfig'; import { getHostingLink } from '../../../utils/HostingLink'; import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from "../../../index"; +import {MenuItem} from "../../structures/ContextualMenu"; export class TopLeftMenu extends React.Component { static propTypes = { @@ -58,8 +59,6 @@ export class TopLeftMenu extends React.Component { } render() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const isGuest = MatrixClientPeg.get().isGuest(); const hostingSignupLink = getHostingLink('user-context-menu'); @@ -69,10 +68,10 @@ export class TopLeftMenu extends React.Component { {_t( "Upgrade to your own domain", {}, { - a: sub => {sub}, + a: sub => {sub}, }, )} - +
    ; @@ -81,40 +80,40 @@ export class TopLeftMenu extends React.Component { let homePageItem = null; if (this.hasHomePage()) { homePageItem = ( - + {_t("Home")} - + ); } let signInOutItem; if (isGuest) { signInOutItem = ( - + {_t("Sign in")} - + ); } else { signInOutItem = ( - + {_t("Sign out")} - + ); } const settingsItem = ( - + {_t("Settings")} - + ); - return
    -
    + return
    +
    {this.props.displayName}
    {this.props.userId}
    {hostingSignup}
    -
      +
        {homePageItem} {settingsItem} {signInOutItem} diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js index 2355cae820..03d7f61204 100644 --- a/src/components/views/elements/TagTile.js +++ b/src/components/views/elements/TagTile.js @@ -1,6 +1,7 @@ /* Copyright 2017 New Vector Ltd. Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import classNames from 'classnames'; @@ -23,12 +24,12 @@ import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard'; -import * as ContextualMenu from '../../structures/ContextualMenu'; import * as FormattingUtils from '../../../utils/FormattingUtils'; import FlairStore from '../../../stores/FlairStore'; import GroupStore from '../../../stores/GroupStore'; import TagOrderStore from '../../../stores/TagOrderStore'; +import {ContextMenu} from "../../structures/ContextualMenu"; // A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents // a thing to click on for the user to filter the visible rooms in the RoomList to: @@ -58,6 +59,8 @@ export default createReactClass({ }, componentDidMount() { + this._contextMenuButton = createRef(); + this.unmounted = false; if (this.props.tag[0] === '+') { FlairStore.addListener('updateGroupProfile', this._onFlairStoreUpdated); @@ -107,53 +110,33 @@ export default createReactClass({ } }, - _openContextMenu: function(x, y, chevronOffset) { - // Hide the (...) immediately - this.setState({ hover: false }); - - const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu'); - ContextualMenu.createMenu(TagTileContextMenu, { - chevronOffset: chevronOffset, - left: x, - top: y, - tag: this.props.tag, - onFinished: () => { - this.setState({ menuDisplayed: false }); - }, - }); - this.setState({ menuDisplayed: true }); - }, - - onContextButtonClick: function(e) { - e.preventDefault(); - e.stopPropagation(); - - const elementRect = e.target.getBoundingClientRect(); - - // The window X and Y offsets are to adjust position when zoomed in to page - const x = elementRect.right + window.pageXOffset + 3; - const chevronOffset = 12; - let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); - y = y - (chevronOffset + 8); // where 8 is half the height of the chevron - - this._openContextMenu(x, y, chevronOffset); - }, - - onContextMenu: function(e) { - e.preventDefault(); - - const chevronOffset = 12; - this._openContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset); - }, - onMouseOver: function() { + console.log("DEBUG onMouseOver"); this.setState({hover: true}); }, onMouseOut: function() { + console.log("DEBUG onMouseOut"); this.setState({hover: false}); }, + openMenu: function(e) { + // Prevent the TagTile onClick event firing as well + e.stopPropagation(); + e.preventDefault(); + + this.setState({ + menuDisplayed: true, + hover: false, + }); + }, + + closeMenu: function() { + this.setState({ + menuDisplayed: false, + }); + }, + render: function() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -184,23 +167,47 @@ export default createReactClass({ const tip = this.state.hover ? :
        ; + // FIXME: this ought to use AccessibleButton for a11y but that causes onMouseOut/onMouseOver to fire too much const contextButton = this.state.hover || this.state.menuDisplayed ? -
        +
        { "\u00B7\u00B7\u00B7" } -
        :
        ; - return -
        - - { tip } - { contextButton } - { badgeElement } -
        -
        ; +
        :
        ; + + let contextMenu; + if (this.state.menuDisplayed) { + const elementRect = this._contextMenuButton.current.getBoundingClientRect(); + + // The window X and Y offsets are to adjust position when zoomed in to page + const left = elementRect.right + window.pageXOffset + 3; + const chevronOffset = 12; + let top = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); + top = top - (chevronOffset + 8); // where 8 is half the height of the chevron + + const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu'); + contextMenu = ( + + + + ); + } + + return + +
        + + { tip } + { contextButton } + { badgeElement } +
        +
        + + { contextMenu } +
        ; }, }); diff --git a/src/components/views/emojipicker/ReactionPicker.js b/src/components/views/emojipicker/ReactionPicker.js index 5e506f39d1..c051ab40bb 100644 --- a/src/components/views/emojipicker/ReactionPicker.js +++ b/src/components/views/emojipicker/ReactionPicker.js @@ -23,7 +23,6 @@ class ReactionPicker extends React.Component { static propTypes = { mxEvent: PropTypes.object.isRequired, onFinished: PropTypes.func.isRequired, - closeMenu: PropTypes.func.isRequired, reactions: PropTypes.object, }; @@ -89,7 +88,6 @@ class ReactionPicker extends React.Component { onChoose(reaction) { this.componentWillUnmount(); - this.props.closeMenu(); this.props.onFinished(); const myReactions = this.getReactions(); if (myReactions.hasOwnProperty(reaction)) { diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index 7d7275c55b..99427acb03 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -1,6 +1,7 @@ /* Copyright 2017, 2018 New Vector Ltd Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,16 +16,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; import dis from '../../../dispatcher'; -import AccessibleButton from '../elements/AccessibleButton'; import classNames from 'classnames'; import MatrixClientPeg from "../../../MatrixClientPeg"; -import {createMenu} from "../../structures/ContextualMenu"; +import {ContextMenu} from "../../structures/ContextualMenu"; export default createReactClass({ displayName: 'GroupInviteTile', @@ -46,6 +46,10 @@ export default createReactClass({ }); }, + componentDidMount: function() { + this._contextMenuButton = createRef(); + }, + onClick: function(e) { dis.dispatch({ action: 'view_group', @@ -69,54 +73,34 @@ export default createReactClass({ }); }, - _showContextMenu: function(x, y, chevronOffset) { - const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu'); - - createMenu(GroupInviteTileContextMenu, { - chevronOffset, - left: x, - top: y, - group: this.props.group, - onFinished: () => { - this.setState({ menuDisplayed: false }); - }, - }); - this.setState({ menuDisplayed: true }); - }, - - onContextMenu: function(e) { - // Prevent the RoomTile onClick event firing as well - e.preventDefault(); + openMenu: function(e) { // Only allow non-guests to access the context menu if (MatrixClientPeg.get().isGuest()) return; - const chevronOffset = 12; - this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset); - }, - - onBadgeClicked: function(e) { - // Prevent the RoomTile onClick event firing as well + // Prevent the GroupInviteTile onClick event firing as well e.stopPropagation(); - // Only allow non-guests to access the context menu - if (MatrixClientPeg.get().isGuest()) return; + e.preventDefault(); + + const state = { + menuDisplayed: true, + }; // If the badge is clicked, then no longer show tooltip if (this.props.collapsed) { - this.setState({ hover: false }); + state.hover = false; } - const elementRect = e.target.getBoundingClientRect(); + this.setState(state); + }, - // The window X and Y offsets are to adjust position when zoomed in to page - const x = elementRect.right + window.pageXOffset + 3; - const chevronOffset = 12; - let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); - y = y - (chevronOffset + 8); // where 8 is half the height of the chevron - - this._showContextMenu(x, y, chevronOffset); + closeMenu: function() { + this.setState({ + menuDisplayed: false, + }); }, render: function() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const groupName = this.props.group.name || this.props.group.groupId; @@ -139,7 +123,11 @@ export default createReactClass({ }); const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!'; - const badge =
        { badgeContent }
        ; + const badge = ( + + { badgeContent } + + ); let tooltip; if (this.props.collapsed && this.state.hover) { @@ -153,12 +141,30 @@ export default createReactClass({ 'mx_GroupInviteTile': true, }); - return ( - + + + ); + } + + return +
        { av } @@ -169,6 +175,8 @@ export default createReactClass({
        { tooltip }
        - ); + + { contextMenu } +
        ; }, }); diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index acd8263410..b67107eebf 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -1,6 +1,7 @@ /* Copyright 2019 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,17 +16,146 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {useState, useEffect, useRef} from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import Modal from '../../../Modal'; -import { createMenu } from '../../structures/ContextualMenu'; +import {ContextMenu} from '../../structures/ContextualMenu'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import {RoomContext} from "../../structures/RoomView"; +const useContextMenu = () => { + const _button = useRef(null); + const [isOpen, setIsOpen] = useState(false); + const open = () => { + setIsOpen(true); + }; + const close = () => { + setIsOpen(false); + }; + + return [isOpen, _button, open, close, setIsOpen]; +}; + +const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => { + const [menuDisplayed, _button, openMenu, closeMenu] = useContextMenu(); + useEffect(() => { + onFocusChange(menuDisplayed); + }, [onFocusChange, menuDisplayed]); + + let contextMenu; + if (menuDisplayed) { + const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); + + const tile = getTile && getTile(); + const replyThread = getReplyThread && getReplyThread(); + + const onCryptoClick = () => { + Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', + import('../../../async-components/views/dialogs/EncryptedEventDialog'), + {event: mxEvent}, + ); + }; + + let e2eInfoCallback = null; + if (mxEvent.isEncrypted()) { + e2eInfoCallback = onCryptoClick; + } + + const menuOptions = { + chevronFace: "none", + }; + + const buttonRect = _button.current.getBoundingClientRect(); + // The window X and Y offsets are to adjust position when zoomed in to page + const buttonRight = buttonRect.right + window.pageXOffset; + const buttonBottom = buttonRect.bottom + window.pageYOffset; + const buttonTop = buttonRect.top + window.pageYOffset; + // Align the right edge of the menu to the right edge of the button + menuOptions.right = window.innerWidth - buttonRight; + // Align the menu vertically on whichever side of the button has more + // space available. + if (buttonBottom < window.innerHeight / 2) { + menuOptions.top = buttonBottom; + } else { + menuOptions.bottom = window.innerHeight - buttonTop; + } + + contextMenu = + + ; + } + + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return + + + { contextMenu } + ; +}; + +const ReactButton = ({mxEvent, reactions}) => { + const [menuDisplayed, _button, openMenu, closeMenu] = useContextMenu(); + + let contextMenu; + if (menuDisplayed) { + const menuOptions = { + chevronFace: "none", + }; + + const buttonRect = _button.current.getBoundingClientRect(); + // The window X and Y offsets are to adjust position when zoomed in to page + const buttonRight = buttonRect.right + window.pageXOffset; + const buttonBottom = buttonRect.bottom + window.pageYOffset; + const buttonTop = buttonRect.top + window.pageYOffset; + // Align the right edge of the menu to the right edge of the button + menuOptions.right = window.innerWidth - buttonRight; + // Align the menu vertically on whichever side of the button has more + // space available. + if (buttonBottom < window.innerHeight / 2) { + menuOptions.top = buttonBottom; + } else { + menuOptions.bottom = window.innerHeight - buttonTop; + } + + const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker'); + contextMenu = + + ; + } + + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return + + + { contextMenu } + ; +}; + export default class MessageActionBar extends React.PureComponent { static propTypes = { mxEvent: PropTypes.object.isRequired, @@ -62,14 +192,6 @@ export default class MessageActionBar extends React.PureComponent { this.props.onFocusChange(focused); }; - onCryptoClick = () => { - const event = this.props.mxEvent; - Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', - import('../../../async-components/views/dialogs/EncryptedEventDialog'), - {event}, - ); - }; - onReplyClick = (ev) => { dis.dispatch({ action: 'reply_to_event', @@ -84,71 +206,6 @@ export default class MessageActionBar extends React.PureComponent { }); }; - getMenuOptions = (ev) => { - const menuOptions = {}; - const buttonRect = ev.target.getBoundingClientRect(); - // The window X and Y offsets are to adjust position when zoomed in to page - const buttonRight = buttonRect.right + window.pageXOffset; - const buttonBottom = buttonRect.bottom + window.pageYOffset; - const buttonTop = buttonRect.top + window.pageYOffset; - // Align the right edge of the menu to the right edge of the button - menuOptions.right = window.innerWidth - buttonRight; - // Align the menu vertically on whichever side of the button has more - // space available. - if (buttonBottom < window.innerHeight / 2) { - menuOptions.top = buttonBottom; - } else { - menuOptions.bottom = window.innerHeight - buttonTop; - } - return menuOptions; - }; - - onReactClick = (ev) => { - const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker'); - - const menuOptions = { - ...this.getMenuOptions(ev), - mxEvent: this.props.mxEvent, - reactions: this.props.reactions, - chevronFace: "none", - onFinished: () => this.onFocusChange(false), - }; - - createMenu(ReactionPicker, menuOptions); - - this.onFocusChange(true); - }; - - onOptionsClick = (ev) => { - const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); - - const { getTile, getReplyThread } = this.props; - const tile = getTile && getTile(); - const replyThread = getReplyThread && getReplyThread(); - - let e2eInfoCallback = null; - if (this.props.mxEvent.isEncrypted()) { - e2eInfoCallback = () => this.onCryptoClick(); - } - - const menuOptions = { - ...this.getMenuOptions(ev), - mxEvent: this.props.mxEvent, - chevronFace: "none", - permalinkCreator: this.props.permalinkCreator, - eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined, - collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined, - e2eInfoCallback: e2eInfoCallback, - onFinished: () => { - this.onFocusChange(false); - }, - }; - - createMenu(MessageContextMenu, menuOptions); - - this.onFocusChange(true); - }; - render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -158,11 +215,7 @@ export default class MessageActionBar extends React.PureComponent { if (isContentActionable(this.props.mxEvent)) { if (this.context.room.canReact) { - reactButton = ; + reactButton = ; } if (this.context.room.canReply) { replyButton =
        ; } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index dc893f0049..9702b24eff 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -17,8 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ - -import React from 'react'; +import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import classNames from 'classnames'; @@ -26,10 +25,9 @@ import dis from '../../../dispatcher'; import MatrixClientPeg from '../../../MatrixClientPeg'; import DMRoomMap from '../../../utils/DMRoomMap'; import sdk from '../../../index'; -import {createMenu} from '../../structures/ContextualMenu'; +import {ContextMenu} from '../../structures/ContextualMenu'; import * as RoomNotifs from '../../../RoomNotifs'; import * as FormattingUtils from '../../../utils/FormattingUtils'; -import AccessibleButton from '../elements/AccessibleButton'; import ActiveRoomObserver from '../../../ActiveRoomObserver'; import RoomViewStore from '../../../stores/RoomViewStore'; import SettingsStore from "../../../settings/SettingsStore"; @@ -147,6 +145,8 @@ module.exports = createReactClass({ }, componentDidMount: function() { + this._contextMenuButton = createRef(); + const cli = MatrixClientPeg.get(); cli.on("accountData", this.onAccountData); cli.on("Room.name", this.onRoomName); @@ -229,32 +229,6 @@ module.exports = createReactClass({ this.badgeOnMouseLeave(); }, - _showContextMenu: function(x, y, chevronOffset) { - const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu'); - - createMenu(RoomTileContextMenu, { - chevronOffset, - left: x, - top: y, - room: this.props.room, - onFinished: () => { - this.setState({ menuDisplayed: false }); - this.props.refreshSubList(); - }, - }); - this.setState({ menuDisplayed: true }); - }, - - onContextMenu: function(e) { - // Prevent the RoomTile onClick event firing as well - e.preventDefault(); - // Only allow non-guests to access the context menu - if (MatrixClientPeg.get().isGuest()) return; - - const chevronOffset = 12; - this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset); - }, - badgeOnMouseEnter: function() { // Only allow non-guests to access the context menu // and only change it if it needs to change @@ -267,26 +241,31 @@ module.exports = createReactClass({ this.setState( { badgeHover: false } ); }, - onOpenMenu: function(e) { - // Prevent the RoomTile onClick event firing as well - e.stopPropagation(); + openMenu: function(e) { // Only allow non-guests to access the context menu if (MatrixClientPeg.get().isGuest()) return; + // Prevent the RoomTile onClick event firing as well + e.stopPropagation(); + e.preventDefault(); + + const state = { + menuDisplayed: true, + }; + // If the badge is clicked, then no longer show tooltip if (this.props.collapsed) { - this.setState({ hover: false }); + state.hover = false; } - const elementRect = e.target.getBoundingClientRect(); + this.setState(state); + }, - // The window X and Y offsets are to adjust position when zoomed in to page - const x = elementRect.right + window.pageXOffset + 3; - const chevronOffset = 12; - let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); - y = y - (chevronOffset + 8); // where 8 is half the height of the chevron - - this._showContextMenu(x, y, chevronOffset); + closeMenu: function() { + this.setState({ + menuDisplayed: false, + }); + this.props.refreshSubList(); }, render: function() { @@ -360,9 +339,13 @@ module.exports = createReactClass({ // incomingCallBox = ; //} + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + let contextMenuButton; if (!MatrixClientPeg.get().isGuest()) { - contextMenuButton = ; + contextMenuButton = ( + + ); } const RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); @@ -393,32 +376,54 @@ module.exports = createReactClass({ ariaLabel += " " + _t("Unread messages."); } - return -
        -
        - - { dmIndicator } + let contextMenu; + if (this.state.menuDisplayed) { + const elementRect = this._contextMenuButton.current.getBoundingClientRect(); + // The window X and Y offsets are to adjust position when zoomed in to page + const left = elementRect.right + window.pageXOffset + 3; + const chevronOffset = 12; + let top = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); + top = top - (chevronOffset + 8); // where 8 is half the height of the chevron + + const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu'); + contextMenu = ( + + + + ); + } + + return + +
        +
        + + { dmIndicator } +
        -
        -
        -
        - { label } - { subtextLabel } +
        +
        + { label } + { subtextLabel } +
        + { contextMenuButton } + { badge }
        - { contextMenuButton } - { badge } -
        - { /* { incomingCallBox } */ } - { tooltip } - ; + { /* { incomingCallBox } */ } + { tooltip } + + + { contextMenu } + ; }, }); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5f6e327944..75be4205df 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1494,6 +1494,7 @@ "Report Content": "Report Content", "Failed to set Direct Message status of room": "Failed to set Direct Message status of room", "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s", + "Notification settings": "Notification settings", "All messages (noisy)": "All messages (noisy)", "All messages": "All messages", "Mentions only": "Mentions only", From 2eddb6ca01dc17d41ee0601d3803461471cb78d4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:24:14 +0000 Subject: [PATCH 0527/2372] DRY context menu placement algorithms --- src/components/structures/ContextualMenu.js | 8 +++ src/components/views/elements/TagTile.js | 11 +--- .../views/groups/GroupInviteTile.js | 10 +--- .../views/messages/MessageActionBar.js | 60 +++++++------------ src/components/views/rooms/RoomTile.js | 10 +--- 5 files changed, 36 insertions(+), 63 deletions(-) diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index 6d046c08d4..c172daf991 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -559,6 +559,14 @@ MenuItemRadio.propTypes = { onClick: PropTypes.func.isRequired, }; +// Placement method for to position context menu to right of elementRect with chevronOffset +export const toRightOf = (elementRect, chevronOffset=12) => { + const left = elementRect.right + window.pageXOffset + 3; + let top = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); + top = top - (chevronOffset + 8); // where 8 is half the height of the chevron + return {left, top}; +}; + export function createMenu(ElementClass, props, hasBackground=true) { const closeMenu = function(...args) { ReactDOM.unmountComponentAtNode(getOrCreateContainer()); diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js index 03d7f61204..c9afd487cb 100644 --- a/src/components/views/elements/TagTile.js +++ b/src/components/views/elements/TagTile.js @@ -29,7 +29,7 @@ import * as FormattingUtils from '../../../utils/FormattingUtils'; import FlairStore from '../../../stores/FlairStore'; import GroupStore from '../../../stores/GroupStore'; import TagOrderStore from '../../../stores/TagOrderStore'; -import {ContextMenu} from "../../structures/ContextualMenu"; +import {ContextMenu, toRightOf} from "../../structures/ContextualMenu"; // A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents // a thing to click on for the user to filter the visible rooms in the RoomList to: @@ -176,16 +176,9 @@ export default createReactClass({ let contextMenu; if (this.state.menuDisplayed) { const elementRect = this._contextMenuButton.current.getBoundingClientRect(); - - // The window X and Y offsets are to adjust position when zoomed in to page - const left = elementRect.right + window.pageXOffset + 3; - const chevronOffset = 12; - let top = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); - top = top - (chevronOffset + 8); // where 8 is half the height of the chevron - const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu'); contextMenu = ( - + ); diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index 99427acb03..cfc50cc008 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -24,7 +24,7 @@ import sdk from '../../../index'; import dis from '../../../dispatcher'; import classNames from 'classnames'; import MatrixClientPeg from "../../../MatrixClientPeg"; -import {ContextMenu} from "../../structures/ContextualMenu"; +import {ContextMenu, toRightOf} from "../../structures/ContextualMenu"; export default createReactClass({ displayName: 'GroupInviteTile', @@ -144,15 +144,9 @@ export default createReactClass({ let contextMenu; if (this.state.menuDisplayed) { const elementRect = this._contextMenuButton.current.getBoundingClientRect(); - // The window X and Y offsets are to adjust position when zoomed in to page - const left = elementRect.right + window.pageXOffset + 3; - const chevronOffset = 12; - let top = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); - top = top - (chevronOffset + 8); // where 8 is half the height of the chevron - const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu'); contextMenu = ( - + ); diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index b67107eebf..d59e74623a 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -27,6 +27,26 @@ import {ContextMenu} from '../../structures/ContextualMenu'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import {RoomContext} from "../../structures/RoomView"; +const contextMenuProps = (elementRect) => { + const menuOptions = { + chevronFace: "none", + }; + + const buttonRight = elementRect.right + window.pageXOffset; + const buttonBottom = elementRect.bottom + window.pageYOffset; + const buttonTop = elementRect.top + window.pageYOffset; + // Align the right edge of the menu to the right edge of the button + menuOptions.right = window.innerWidth - buttonRight; + // Align the menu vertically on whichever side of the button has more space available. + if (buttonBottom < window.innerHeight / 2) { + menuOptions.top = buttonBottom; + } else { + menuOptions.bottom = window.innerHeight - buttonTop; + } + + return menuOptions; +}; + const useContextMenu = () => { const _button = useRef(null); const [isOpen, setIsOpen] = useState(false); @@ -65,26 +85,8 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo e2eInfoCallback = onCryptoClick; } - const menuOptions = { - chevronFace: "none", - }; - const buttonRect = _button.current.getBoundingClientRect(); - // The window X and Y offsets are to adjust position when zoomed in to page - const buttonRight = buttonRect.right + window.pageXOffset; - const buttonBottom = buttonRect.bottom + window.pageYOffset; - const buttonTop = buttonRect.top + window.pageYOffset; - // Align the right edge of the menu to the right edge of the button - menuOptions.right = window.innerWidth - buttonRight; - // Align the menu vertically on whichever side of the button has more - // space available. - if (buttonBottom < window.innerHeight / 2) { - menuOptions.top = buttonBottom; - } else { - menuOptions.bottom = window.innerHeight - buttonTop; - } - - contextMenu = + contextMenu = { let contextMenu; if (menuDisplayed) { - const menuOptions = { - chevronFace: "none", - }; - const buttonRect = _button.current.getBoundingClientRect(); - // The window X and Y offsets are to adjust position when zoomed in to page - const buttonRight = buttonRect.right + window.pageXOffset; - const buttonBottom = buttonRect.bottom + window.pageYOffset; - const buttonTop = buttonRect.top + window.pageYOffset; - // Align the right edge of the menu to the right edge of the button - menuOptions.right = window.innerWidth - buttonRight; - // Align the menu vertically on whichever side of the button has more - // space available. - if (buttonBottom < window.innerHeight / 2) { - menuOptions.top = buttonBottom; - } else { - menuOptions.bottom = window.innerHeight - buttonTop; - } - const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker'); - contextMenu = + contextMenu = ; } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 9702b24eff..edd50d0330 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -25,7 +25,7 @@ import dis from '../../../dispatcher'; import MatrixClientPeg from '../../../MatrixClientPeg'; import DMRoomMap from '../../../utils/DMRoomMap'; import sdk from '../../../index'; -import {ContextMenu} from '../../structures/ContextualMenu'; +import {ContextMenu, toRightOf} from '../../structures/ContextualMenu'; import * as RoomNotifs from '../../../RoomNotifs'; import * as FormattingUtils from '../../../utils/FormattingUtils'; import ActiveRoomObserver from '../../../ActiveRoomObserver'; @@ -379,15 +379,9 @@ module.exports = createReactClass({ let contextMenu; if (this.state.menuDisplayed) { const elementRect = this._contextMenuButton.current.getBoundingClientRect(); - // The window X and Y offsets are to adjust position when zoomed in to page - const left = elementRect.right + window.pageXOffset + 3; - const chevronOffset = 12; - let top = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); - top = top - (chevronOffset + 8); // where 8 is half the height of the chevron - const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu'); contextMenu = ( - + ); From 44401d73b44b6de4daeb91613456d2760d50456d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:40:38 +0000 Subject: [PATCH 0528/2372] Replace all trivial Promise.defer usages with regular Promises --- src/ContentMessages.js | 174 +++++++++--------- src/Lifecycle.js | 8 +- src/components/views/rooms/Autocomplete.js | 29 ++- .../views/settings/ChangePassword.js | 9 +- src/rageshake/submit-rageshake.js | 40 ++-- 5 files changed, 122 insertions(+), 138 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 2d58622db8..dab8de2465 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -59,40 +59,38 @@ export class UploadCanceledError extends Error {} * and a thumbnail key. */ function createThumbnail(element, inputWidth, inputHeight, mimeType) { - const deferred = Promise.defer(); + return new Promise((resolve) => { + let targetWidth = inputWidth; + let targetHeight = inputHeight; + if (targetHeight > MAX_HEIGHT) { + targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); + targetHeight = MAX_HEIGHT; + } + if (targetWidth > MAX_WIDTH) { + targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); + targetWidth = MAX_WIDTH; + } - let targetWidth = inputWidth; - let targetHeight = inputHeight; - if (targetHeight > MAX_HEIGHT) { - targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); - targetHeight = MAX_HEIGHT; - } - if (targetWidth > MAX_WIDTH) { - targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); - targetWidth = MAX_WIDTH; - } - - const canvas = document.createElement("canvas"); - canvas.width = targetWidth; - canvas.height = targetHeight; - canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); - canvas.toBlob(function(thumbnail) { - deferred.resolve({ - info: { - thumbnail_info: { - w: targetWidth, - h: targetHeight, - mimetype: thumbnail.type, - size: thumbnail.size, + const canvas = document.createElement("canvas"); + canvas.width = targetWidth; + canvas.height = targetHeight; + canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); + canvas.toBlob(function(thumbnail) { + resolve({ + info: { + thumbnail_info: { + w: targetWidth, + h: targetHeight, + mimetype: thumbnail.type, + size: thumbnail.size, + }, + w: inputWidth, + h: inputHeight, }, - w: inputWidth, - h: inputHeight, - }, - thumbnail: thumbnail, - }); - }, mimeType); - - return deferred.promise; + thumbnail: thumbnail, + }); + }, mimeType); + }); } /** @@ -179,30 +177,29 @@ function infoForImageFile(matrixClient, roomId, imageFile) { * @return {Promise} A promise that resolves with the video image element. */ function loadVideoElement(videoFile) { - const deferred = Promise.defer(); + return new Promise((resolve, reject) => { + // Load the file into an html element + const video = document.createElement("video"); - // Load the file into an html element - const video = document.createElement("video"); + const reader = new FileReader(); - const reader = new FileReader(); - reader.onload = function(e) { - video.src = e.target.result; + reader.onload = function(e) { + video.src = e.target.result; - // Once ready, returns its size - // Wait until we have enough data to thumbnail the first frame. - video.onloadeddata = function() { - deferred.resolve(video); + // Once ready, returns its size + // Wait until we have enough data to thumbnail the first frame. + video.onloadeddata = function() { + resolve(video); + }; + video.onerror = function(e) { + reject(e); + }; }; - video.onerror = function(e) { - deferred.reject(e); + reader.onerror = function(e) { + reject(e); }; - }; - reader.onerror = function(e) { - deferred.reject(e); - }; - reader.readAsDataURL(videoFile); - - return deferred.promise; + reader.readAsDataURL(videoFile); + }); } /** @@ -236,16 +233,16 @@ function infoForVideoFile(matrixClient, roomId, videoFile) { * is read. */ function readFileAsArrayBuffer(file) { - const deferred = Promise.defer(); - const reader = new FileReader(); - reader.onload = function(e) { - deferred.resolve(e.target.result); - }; - reader.onerror = function(e) { - deferred.reject(e); - }; - reader.readAsArrayBuffer(file); - return deferred.promise; + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = function(e) { + resolve(e.target.result); + }; + reader.onerror = function(e) { + reject(e); + }; + reader.readAsArrayBuffer(file); + }); } /** @@ -461,33 +458,34 @@ export default class ContentMessages { content.info.mimetype = file.type; } - const def = Promise.defer(); - if (file.type.indexOf('image/') == 0) { - content.msgtype = 'm.image'; - infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ - extend(content.info, imageInfo); - def.resolve(); - }, (error)=>{ - console.error(error); + const prom = new Promise((resolve) => { + if (file.type.indexOf('image/') == 0) { + content.msgtype = 'm.image'; + infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ + extend(content.info, imageInfo); + resolve(); + }, (error)=>{ + console.error(error); + content.msgtype = 'm.file'; + resolve(); + }); + } else if (file.type.indexOf('audio/') == 0) { + content.msgtype = 'm.audio'; + resolve(); + } else if (file.type.indexOf('video/') == 0) { + content.msgtype = 'm.video'; + infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ + extend(content.info, videoInfo); + resolve(); + }, (error)=>{ + content.msgtype = 'm.file'; + resolve(); + }); + } else { content.msgtype = 'm.file'; - def.resolve(); - }); - } else if (file.type.indexOf('audio/') == 0) { - content.msgtype = 'm.audio'; - def.resolve(); - } else if (file.type.indexOf('video/') == 0) { - content.msgtype = 'm.video'; - infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ - extend(content.info, videoInfo); - def.resolve(); - }, (error)=>{ - content.msgtype = 'm.file'; - def.resolve(); - }); - } else { - content.msgtype = 'm.file'; - def.resolve(); - } + resolve(); + } + }); const upload = { fileName: file.name || 'Attachment', @@ -509,7 +507,7 @@ export default class ContentMessages { dis.dispatch({action: 'upload_progress', upload: upload}); } - return def.promise.then(function() { + return prom.then(function() { // XXX: upload.promise must be the promise that // is returned by uploadFile as it has an abort() // method hacked onto it. diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 13f3abccb1..53a9b7a998 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -312,18 +312,14 @@ async function _restoreFromLocalStorage(opts) { function _handleLoadSessionFailure(e) { console.error("Unable to load session", e); - const def = Promise.defer(); const SessionRestoreErrorDialog = sdk.getComponent('views.dialogs.SessionRestoreErrorDialog'); - Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { + const modal = Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { error: e.message, - onFinished: (success) => { - def.resolve(success); - }, }); - return def.promise.then((success) => { + return modal.finished.then(([success]) => { if (success) { // user clicked continue. _clearStorage(); diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index ad5fa198a3..d4b51081f4 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -26,6 +26,7 @@ import { Room } from 'matrix-js-sdk'; import SettingsStore from "../../../settings/SettingsStore"; import Autocompleter from '../../../autocomplete/Autocompleter'; +import {sleep} from "../../../utils/promise"; const COMPOSER_SELECTED = 0; @@ -105,13 +106,11 @@ export default class Autocomplete extends React.Component { autocompleteDelay = 0; } - const deferred = Promise.defer(); - this.debounceCompletionsRequest = setTimeout(() => { - this.processQuery(query, selection).then(() => { - deferred.resolve(); - }); - }, autocompleteDelay); - return deferred.promise; + return new Promise((resolve) => { + this.debounceCompletionsRequest = setTimeout(() => { + resolve(this.processQuery(query, selection)); + }, autocompleteDelay); + }); } processQuery(query, selection) { @@ -197,16 +196,16 @@ export default class Autocomplete extends React.Component { } forceComplete() { - const done = Promise.defer(); - this.setState({ - forceComplete: true, - hide: false, - }, () => { - this.complete(this.props.query, this.props.selection).then(() => { - done.resolve(this.countCompletions()); + return new Promise((resolve) => { + this.setState({ + forceComplete: true, + hide: false, + }, () => { + this.complete(this.props.query, this.props.selection).then(() => { + resolve(this.countCompletions()); + }); }); }); - return done.promise; } onCompletionClicked(selectionOffset: number): boolean { diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index a086efaa6d..91292b19f9 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -178,17 +178,12 @@ module.exports = createReactClass({ }, _optionallySetEmail: function() { - const deferred = Promise.defer(); // Ask for an email otherwise the user has no way to reset their password const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog"); - Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, { + const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, { title: _t('Do you want to set an email address?'), - onFinished: (confirmed) => { - // ignore confirmed, setting an email is optional - deferred.resolve(confirmed); - }, }); - return deferred.promise; + return modal.finished.then(([confirmed]) => confirmed); }, _onExportE2eKeysClicked: function() { diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index 99c412a6ab..e772912e48 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -105,26 +105,22 @@ export default async function sendBugReport(bugReportEndpoint, opts) { } function _submitReport(endpoint, body, progressCallback) { - const deferred = Promise.defer(); - - const req = new XMLHttpRequest(); - req.open("POST", endpoint); - req.timeout = 5 * 60 * 1000; - req.onreadystatechange = function() { - if (req.readyState === XMLHttpRequest.LOADING) { - progressCallback(_t("Waiting for response from server")); - } else if (req.readyState === XMLHttpRequest.DONE) { - on_done(); - } - }; - req.send(body); - return deferred.promise; - - function on_done() { - if (req.status < 200 || req.status >= 400) { - deferred.reject(new Error(`HTTP ${req.status}`)); - return; - } - deferred.resolve(); - } + return new Promise((resolve, reject) => { + const req = new XMLHttpRequest(); + req.open("POST", endpoint); + req.timeout = 5 * 60 * 1000; + req.onreadystatechange = function() { + if (req.readyState === XMLHttpRequest.LOADING) { + progressCallback(_t("Waiting for response from server")); + } else if (req.readyState === XMLHttpRequest.DONE) { + // on done + if (req.status < 200 || req.status >= 400) { + reject(new Error(`HTTP ${req.status}`)); + return; + } + resolve(); + } + }; + req.send(body); + }); } From 6850c147393ba7be8b98d97dfc8d7244fa503461 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:45:28 +0000 Subject: [PATCH 0529/2372] Replace rest of defer usages using small shim. Add homebrew promise utils --- src/Modal.js | 3 +- src/components/structures/MatrixChat.js | 5 +-- src/utils/MultiInviter.js | 3 +- src/utils/promise.js | 46 +++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 src/utils/promise.js diff --git a/src/Modal.js b/src/Modal.js index 26c9da8bbb..cb19731f01 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -24,6 +24,7 @@ import sdk from './index'; import dis from './dispatcher'; import { _t } from './languageHandler'; import Promise from "bluebird"; +import {defer} from "./utils/promise"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; @@ -202,7 +203,7 @@ class ModalManager { } _getCloseFn(modal, props) { - const deferred = Promise.defer(); + const deferred = defer(); return [(...args) => { deferred.resolve(args); if (props && props.onFinished) props.onFinished.apply(null, args); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index da67416400..d12eba88f7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -60,6 +60,7 @@ import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; import DMRoomMap from '../../utils/DMRoomMap'; import { countRoomsWithNotif } from '../../RoomNotifs'; import { setTheme } from "../../theme"; +import {defer} from "../../utils/promise"; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -236,7 +237,7 @@ export default createReactClass({ // Used by _viewRoom before getting state from sync this.firstSyncComplete = false; - this.firstSyncPromise = Promise.defer(); + this.firstSyncPromise = defer(); if (this.props.config.sync_timeline_limit) { MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; @@ -1261,7 +1262,7 @@ export default createReactClass({ // since we're about to start the client and therefore about // to do the first sync this.firstSyncComplete = false; - this.firstSyncPromise = Promise.defer(); + this.firstSyncPromise = defer(); const cli = MatrixClientPeg.get(); const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index e8995b46d7..de5c2e7610 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -24,6 +24,7 @@ import {_t} from "../languageHandler"; import sdk from "../index"; import Modal from "../Modal"; import SettingsStore from "../settings/SettingsStore"; +import {defer} from "./promise"; /** * Invites multiple addresses to a room or group, handling rate limiting from the server @@ -71,7 +72,7 @@ export default class MultiInviter { }; } } - this.deferred = Promise.defer(); + this.deferred = defer(); this._inviteMore(0); return this.deferred.promise; diff --git a/src/utils/promise.js b/src/utils/promise.js new file mode 100644 index 0000000000..dd10f7fdd7 --- /dev/null +++ b/src/utils/promise.js @@ -0,0 +1,46 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// @flow + +// Returns a promise which resolves with a given value after the given number of ms +export const sleep = (ms: number, value: any): Promise => new Promise((resolve => { setTimeout(resolve, ms, value); })); + +// Returns a promise which resolves when the input promise resolves with its value +// or when the timeout of ms is reached with the value of given timeoutValue +export async function timeout(promise: Promise, timeoutValue: any, ms: number): Promise { + const timeoutPromise = new Promise((resolve) => { + const timeoutId = setTimeout(resolve, ms, timeoutValue); + promise.then(() => { + clearTimeout(timeoutId); + }); + }); + + return Promise.race([promise, timeoutPromise]); +} + +// Returns a Deferred +export function defer(): {resolve: () => {}, reject: () => {}, promise: Promise} { + let resolve; + let reject; + + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + + return {resolve, reject, promise}; +} From 0a21957b2cac0cc2d0169f428187bc2e468251a9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:46:58 +0000 Subject: [PATCH 0530/2372] Replace Promise.delay with promise utils sleep --- src/components/structures/GroupView.js | 9 +++++---- .../views/context_menus/RoomTileContextMenu.js | 7 ++++--- src/components/views/dialogs/AddressPickerDialog.js | 3 ++- .../views/settings/tabs/user/SecurityUserSettingsTab.js | 3 ++- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 4d8f47003c..4056557a7c 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -38,6 +38,7 @@ import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks"; import {Group} from "matrix-js-sdk"; +import {sleep} from "../../utils/promise"; const LONG_DESC_PLACEHOLDER = _td( `

        HTML for your community's page

        @@ -692,7 +693,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.acceptGroupInvite(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -711,7 +712,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.leaveGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -735,7 +736,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.joinGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -787,7 +788,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.leaveGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 9bb573026f..541daef27f 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -32,6 +32,7 @@ import * as RoomNotifs from '../../../RoomNotifs'; import Modal from '../../../Modal'; import RoomListActions from '../../../actions/RoomListActions'; import RoomViewStore from '../../../stores/RoomViewStore'; +import {sleep} from "../../../utils/promise"; module.exports = createReactClass({ displayName: 'RoomTileContextMenu', @@ -62,7 +63,7 @@ module.exports = createReactClass({ _toggleTag: function(tagNameOn, tagNameOff) { if (!MatrixClientPeg.get().isGuest()) { - Promise.delay(500).then(() => { + sleep(500).then(() => { dis.dispatch(RoomListActions.tagRoom( MatrixClientPeg.get(), this.props.room, @@ -119,7 +120,7 @@ module.exports = createReactClass({ Rooms.guessAndSetDMRoom( this.props.room, newIsDirectMessage, - ).delay(500).finally(() => { + ).then(sleep(500)).finally(() => { // Close the context menu if (this.props.onFinished) { this.props.onFinished(); @@ -193,7 +194,7 @@ module.exports = createReactClass({ RoomNotifs.setRoomNotifsState(roomId, newState).done(() => { // delay slightly so that the user can see their state change // before closing the menu - return Promise.delay(500).then(() => { + return sleep(500).then(() => { if (this._unmounted) return; // Close the context menu if (this.props.onFinished) { diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index fb779fa96f..dc61f23956 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -32,6 +32,7 @@ import * as Email from '../../../email'; import IdentityAuthClient from '../../../IdentityAuthClient'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils'; import { abbreviateUrl } from '../../../utils/UrlUtils'; +import {sleep} from "../../../utils/promise"; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -533,7 +534,7 @@ module.exports = createReactClass({ }; // wait a bit to let the user finish typing - await Promise.delay(500); + await sleep(500); if (cancelled) return null; try { diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index e619791b01..222af48fa1 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -25,6 +25,7 @@ import Analytics from "../../../../../Analytics"; import Promise from "bluebird"; import Modal from "../../../../../Modal"; import sdk from "../../../../.."; +import {sleep} from "../../../../../utils/promise"; export class IgnoredUser extends React.Component { static propTypes = { @@ -129,7 +130,7 @@ export default class SecurityUserSettingsTab extends React.Component { if (e.errcode === "M_LIMIT_EXCEEDED") { // Add a delay between each invite change in order to avoid rate // limiting by the server. - await Promise.delay(e.retry_after_ms || 2500); + await sleep(e.retry_after_ms || 2500); // Redo last action i--; From 09a8fec26187f12fe9b2d8d201c4b4fcb86af82a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:51:23 +0000 Subject: [PATCH 0531/2372] s/.done(/.then(/ since modern es6 track unhandled promise exceptions --- src/Lifecycle.js | 4 ++-- src/Notifier.js | 2 +- src/Resend.js | 2 +- src/ScalarMessaging.js | 8 ++++---- src/components/structures/GroupView.js | 4 ++-- src/components/structures/InteractiveAuth.js | 2 +- src/components/structures/MatrixChat.js | 8 ++++---- src/components/structures/MyGroups.js | 2 +- src/components/structures/RoomDirectory.js | 8 ++++---- src/components/structures/RoomView.js | 8 ++++---- src/components/structures/TimelinePanel.js | 4 ++-- src/components/structures/auth/ForgotPassword.js | 2 +- src/components/structures/auth/Login.js | 4 ++-- src/components/structures/auth/PostRegistration.js | 2 +- src/components/structures/auth/Registration.js | 2 +- .../views/auth/InteractiveAuthEntryComponents.js | 2 +- .../views/context_menus/RoomTileContextMenu.js | 4 ++-- .../views/dialogs/AddressPickerDialog.js | 4 ++-- src/components/views/dialogs/CreateGroupDialog.js | 2 +- src/components/views/dialogs/KeyShareDialog.js | 2 +- src/components/views/dialogs/SetEmailDialog.js | 4 ++-- src/components/views/elements/AppTile.js | 2 +- .../views/elements/EditableTextContainer.js | 4 ++-- src/components/views/elements/ErrorBoundary.js | 2 +- src/components/views/elements/ImageView.js | 2 +- src/components/views/elements/LanguageDropdown.js | 2 +- src/components/views/groups/GroupUserSettings.js | 2 +- src/components/views/messages/MAudioBody.js | 2 +- src/components/views/messages/MImageBody.js | 2 +- src/components/views/messages/MVideoBody.js | 2 +- src/components/views/right_panel/UserInfo.js | 2 +- src/components/views/rooms/LinkPreviewWidget.js | 2 +- src/components/views/rooms/MemberInfo.js | 6 +++--- src/components/views/settings/ChangeAvatar.js | 2 +- src/components/views/settings/ChangePassword.js | 2 +- src/components/views/settings/DevicesPanel.js | 2 +- src/components/views/settings/Notifications.js | 14 +++++++------- .../settings/tabs/user/HelpUserSettingsTab.js | 2 +- src/stores/RoomViewStore.js | 4 ++-- .../views/dialogs/InteractiveAuthDialog-test.js | 3 ++- .../views/elements/MemberEventListSummary-test.js | 2 +- .../views/rooms/MessageComposerInput-test.js | 3 ++- test/i18n-test/languageHandler-test.js | 2 +- 43 files changed, 74 insertions(+), 72 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 53a9b7a998..9bada98168 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -524,7 +524,7 @@ export function logout() { console.log("Failed to call logout API: token will not be invalidated"); onLoggedOut(); }, - ).done(); + ).then(); } export function softLogout() { @@ -608,7 +608,7 @@ export function onLoggedOut() { // that can occur when components try to use a null client. dis.dispatch({action: 'on_logged_out'}, true); stopMatrixClient(); - _clearStorage().done(); + _clearStorage().then(); } /** diff --git a/src/Notifier.js b/src/Notifier.js index cca0ea2b89..edb9850dfe 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -198,7 +198,7 @@ const Notifier = { if (enable) { // Attempt to get permission from user - plaf.requestNotificationPermission().done((result) => { + plaf.requestNotificationPermission().then((result) => { if (result !== 'granted') { // The permission request was dismissed or denied // TODO: Support alternative branding in messaging diff --git a/src/Resend.js b/src/Resend.js index 4eaee16d1b..51ec804c01 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -35,7 +35,7 @@ module.exports = { }, resend: function(event) { const room = MatrixClientPeg.get().getRoom(event.getRoomId()); - MatrixClientPeg.get().resendEvent(event, room).done(function(res) { + MatrixClientPeg.get().resendEvent(event, room).then(function(res) { dis.dispatch({ action: 'message_sent', event: event, diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 910a6c4f13..c0ffc3022d 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -279,7 +279,7 @@ function inviteUser(event, roomId, userId) { } } - client.invite(roomId, userId).done(function() { + client.invite(roomId, userId).then(function() { sendResponse(event, { success: true, }); @@ -398,7 +398,7 @@ function setPlumbingState(event, roomId, status) { sendError(event, _t('You need to be logged in.')); return; } - client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).done(() => { + client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).then(() => { sendResponse(event, { success: true, }); @@ -414,7 +414,7 @@ function setBotOptions(event, roomId, userId) { sendError(event, _t('You need to be logged in.')); return; } - client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => { + client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).then(() => { sendResponse(event, { success: true, }); @@ -444,7 +444,7 @@ function setBotPower(event, roomId, userId, level) { }, ); - client.setPowerLevel(roomId, userId, level, powerEvent).done(() => { + client.setPowerLevel(roomId, userId, level, powerEvent).then(() => { sendResponse(event, { success: true, }); diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 4056557a7c..b97d76d72a 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -639,7 +639,7 @@ export default createReactClass({ title: _t('Error'), description: _t('Failed to upload image'), }); - }).done(); + }).then(); }, _onJoinableChange: function(ev) { @@ -678,7 +678,7 @@ export default createReactClass({ this.setState({ avatarChanged: false, }); - }).done(); + }).then(); }, _saveGroup: async function() { diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 5e06d124c4..86fc351515 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -121,7 +121,7 @@ export default createReactClass({ this.setState({ errorText: msg, }); - }).done(); + }).then(); this._intervalId = null; if (this.props.poll) { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d12eba88f7..c7bf2f181f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -541,7 +541,7 @@ export default createReactClass({ const Loader = sdk.getComponent("elements.Spinner"); const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); - MatrixClientPeg.get().leave(payload.room_id).done(() => { + MatrixClientPeg.get().leave(payload.room_id).then(() => { modal.close(); if (this.state.currentRoomId === payload.room_id) { dis.dispatch({action: 'view_next_room'}); @@ -862,7 +862,7 @@ export default createReactClass({ waitFor = this.firstSyncPromise.promise; } - waitFor.done(() => { + waitFor.then(() => { let presentedId = roomInfo.room_alias || roomInfo.room_id; const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { @@ -974,7 +974,7 @@ export default createReactClass({ const [shouldCreate, createOpts] = await modal.finished; if (shouldCreate) { - createRoom({createOpts}).done(); + createRoom({createOpts}).then(); } }, @@ -1750,7 +1750,7 @@ export default createReactClass({ return; } - cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => { + cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => { dis.dispatch({action: 'message_sent'}); }, (err) => { dis.dispatch({action: 'message_send_failed'}); diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 2de15a5444..63ae14ba09 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -47,7 +47,7 @@ export default createReactClass({ }, _fetch: function() { - this.context.matrixClient.getJoinedGroups().done((result) => { + this.context.matrixClient.getJoinedGroups().then((result) => { this.setState({groups: result.groups, error: null}); }, (err) => { if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') { diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 84f402e484..941381726d 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -89,7 +89,7 @@ module.exports = createReactClass({ this.setState({protocolsLoading: false}); return; } - MatrixClientPeg.get().getThirdpartyProtocols().done((response) => { + MatrixClientPeg.get().getThirdpartyProtocols().then((response) => { this.protocols = response; this.setState({protocolsLoading: false}); }, (err) => { @@ -135,7 +135,7 @@ module.exports = createReactClass({ publicRooms: [], loading: true, }); - this.getMoreRooms().done(); + this.getMoreRooms().then(); }, getMoreRooms: function() { @@ -246,7 +246,7 @@ module.exports = createReactClass({ if (!alias) return; step = _t('delete the alias.'); return MatrixClientPeg.get().deleteAlias(alias); - }).done(() => { + }).then(() => { modal.close(); this.refreshRoomList(); }, (err) => { @@ -348,7 +348,7 @@ module.exports = createReactClass({ }); return; } - MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => { + MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).then((resp) => { if (resp.length > 0 && resp[0].alias) { this.showRoomAlias(resp[0].alias, true); } else { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4de573479d..d3ba517264 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1101,7 +1101,7 @@ module.exports = createReactClass({ } ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get()) - .done(undefined, (error) => { + .then(undefined, (error) => { if (error.name === "UnknownDeviceError") { // Let the staus bar handle this return; @@ -1145,7 +1145,7 @@ module.exports = createReactClass({ filter: filter, term: term, }); - this._handleSearchResult(searchPromise).done(); + this._handleSearchResult(searchPromise).then(); }, _handleSearchResult: function(searchPromise) { @@ -1316,7 +1316,7 @@ module.exports = createReactClass({ }, onForgetClick: function() { - MatrixClientPeg.get().forget(this.state.room.roomId).done(function() { + MatrixClientPeg.get().forget(this.state.room.roomId).then(function() { dis.dispatch({ action: 'view_next_room' }); }, function(err) { const errCode = err.errcode || _t("unknown error code"); @@ -1333,7 +1333,7 @@ module.exports = createReactClass({ this.setState({ rejecting: true, }); - MatrixClientPeg.get().leave(this.state.roomId).done(function() { + MatrixClientPeg.get().leave(this.state.roomId).then(function() { dis.dispatch({ action: 'view_next_room' }); self.setState({ rejecting: false, diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index faa6f2564a..573d82bb9d 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -462,7 +462,7 @@ const TimelinePanel = createReactClass({ // timeline window. // // see https://github.com/vector-im/vector-web/issues/1035 - this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => { + this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => { if (this.unmounted) { return; } const { events, liveEvents } = this._getEvents(); @@ -1088,7 +1088,7 @@ const TimelinePanel = createReactClass({ prom = prom.then(onLoaded, onError); } - prom.done(); + prom.then(); }, // handle the completion of a timeline load or localEchoUpdate, by diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 46a5fa7bd7..6f68293caa 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -105,7 +105,7 @@ module.exports = createReactClass({ phase: PHASE_SENDING_EMAIL, }); this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl); - this.reset.resetPassword(email, password).done(() => { + this.reset.resetPassword(email, password).then(() => { this.setState({ phase: PHASE_EMAIL_SENT, }); diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index ad77ed49a5..84209e514f 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -253,7 +253,7 @@ module.exports = createReactClass({ this.setState({ busy: false, }); - }).done(); + }).then(); }, onUsernameChanged: function(username) { @@ -424,7 +424,7 @@ module.exports = createReactClass({ this.setState({ busy: false, }); - }).done(); + }).then(); }, _isSupportedFlow: function(flow) { diff --git a/src/components/structures/auth/PostRegistration.js b/src/components/structures/auth/PostRegistration.js index 66075c80f7..760163585d 100644 --- a/src/components/structures/auth/PostRegistration.js +++ b/src/components/structures/auth/PostRegistration.js @@ -43,7 +43,7 @@ module.exports = createReactClass({ const cli = MatrixClientPeg.get(); this.setState({busy: true}); const self = this; - cli.getProfileInfo(cli.credentials.userId).done(function(result) { + cli.getProfileInfo(cli.credentials.userId).then(function(result) { self.setState({ avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url), busy: false, diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 6321028457..6e0fc246d2 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -371,7 +371,7 @@ module.exports = createReactClass({ if (pushers[i].kind === 'email') { const emailPusher = pushers[i]; emailPusher.data = { brand: this.props.brand }; - matrixClient.setPusher(emailPusher).done(() => { + matrixClient.setPusher(emailPusher).then(() => { console.log("Set email branding to " + this.props.brand); }, (error) => { console.error("Couldn't set email branding: " + error); diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index d19ce95b33..d61035210b 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -441,7 +441,7 @@ export const MsisdnAuthEntry = createReactClass({ this.props.fail(e); }).finally(() => { this.setState({requestingToken: false}); - }).done(); + }).then(); }, /* diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 541daef27f..98628979e5 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -161,7 +161,7 @@ module.exports = createReactClass({ _onClickForget: function() { // FIXME: duplicated with RoomSettings (and dead code in RoomView) - MatrixClientPeg.get().forget(this.props.room.roomId).done(() => { + MatrixClientPeg.get().forget(this.props.room.roomId).then(() => { // Switch to another room view if we're currently viewing the // historical room if (RoomViewStore.getRoomId() === this.props.room.roomId) { @@ -191,7 +191,7 @@ module.exports = createReactClass({ this.setState({ roomNotifState: newState, }); - RoomNotifs.setRoomNotifsState(roomId, newState).done(() => { + RoomNotifs.setRoomNotifsState(roomId, newState).then(() => { // delay slightly so that the user can see their state change // before closing the menu return sleep(500).then(() => { diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index dc61f23956..caf6bc18c5 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -267,7 +267,7 @@ module.exports = createReactClass({ this.setState({ searchError: err.errcode ? err.message : _t('Something went wrong!'), }); - }).done(() => { + }).then(() => { this.setState({ busy: false, }); @@ -380,7 +380,7 @@ module.exports = createReactClass({ // Do a local search immediately this._doLocalSearch(query); } - }).done(() => { + }).then(() => { this.setState({ busy: false, }); diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 11f4c21366..191d797a1e 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -93,7 +93,7 @@ export default createReactClass({ this.setState({createError: e}); }).finally(() => { this.setState({creating: false}); - }).done(); + }).then(); }, _onCancel: function() { diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index a10c25a0fb..51b02f1adf 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -78,7 +78,7 @@ export default createReactClass({ true, ); } - }).done(); + }).then(); }, componentWillUnmount: function() { diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.js index bedf713c4e..b527abffc9 100644 --- a/src/components/views/dialogs/SetEmailDialog.js +++ b/src/components/views/dialogs/SetEmailDialog.js @@ -62,7 +62,7 @@ export default createReactClass({ return; } this._addThreepid = new AddThreepid(); - this._addThreepid.addEmailAddress(emailAddress).done(() => { + this._addThreepid.addEmailAddress(emailAddress).then(() => { Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { title: _t("Verification Pending"), description: _t( @@ -96,7 +96,7 @@ export default createReactClass({ }, verifyEmailAddress: function() { - this._addThreepid.checkEmailLinkClicked().done(() => { + this._addThreepid.checkEmailLinkClicked().then(() => { this.props.onFinished(true); }, (err) => { this.setState({emailBusy: false}); diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 260b63dfd4..453630413c 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -205,7 +205,7 @@ export default class AppTile extends React.Component { if (!this._scalarClient) { this._scalarClient = defaultManager.getScalarClient(); } - this._scalarClient.getScalarToken().done((token) => { + this._scalarClient.getScalarToken().then((token) => { // Append scalar_token as a query param if not already present this._scalarClient.scalarToken = token; const u = url.parse(this._addWurlParams(this.props.url)); diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.js index 3bf37df951..cc49c3c67f 100644 --- a/src/components/views/elements/EditableTextContainer.js +++ b/src/components/views/elements/EditableTextContainer.js @@ -51,7 +51,7 @@ export default class EditableTextContainer extends React.Component { this.setState({busy: true}); - this.props.getInitialValue().done( + this.props.getInitialValue().then( (result) => { if (this._unmounted) { return; } this.setState({ @@ -83,7 +83,7 @@ export default class EditableTextContainer extends React.Component { errorString: null, }); - this.props.onSubmit(value).done( + this.props.onSubmit(value).then( () => { if (this._unmounted) { return; } this.setState({ diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.js index e53e1ec0fa..e36464c4ef 100644 --- a/src/components/views/elements/ErrorBoundary.js +++ b/src/components/views/elements/ErrorBoundary.js @@ -54,7 +54,7 @@ export default class ErrorBoundary extends React.PureComponent { if (!PlatformPeg.get()) return; MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().store.deleteAllData().done(() => { + MatrixClientPeg.get().store.deleteAllData().then(() => { PlatformPeg.get().reload(); }); }; diff --git a/src/components/views/elements/ImageView.js b/src/components/views/elements/ImageView.js index 2772363bd0..09e0ff0e54 100644 --- a/src/components/views/elements/ImageView.js +++ b/src/components/views/elements/ImageView.js @@ -84,7 +84,7 @@ export default class ImageView extends React.Component { title: _t('Error'), description: _t('You cannot delete this image. (%(code)s)', {code: code}), }); - }).done(); + }).then(); }, }); }; diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index 365f9ded61..0c4b2b9d6a 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -49,7 +49,7 @@ export default class LanguageDropdown extends React.Component { this.setState({langs}); }).catch(() => { this.setState({langs: ['en']}); - }).done(); + }).then(); if (!this.props.value) { // If no value is given, we start with the first diff --git a/src/components/views/groups/GroupUserSettings.js b/src/components/views/groups/GroupUserSettings.js index 7d80bdd209..3cd5731b99 100644 --- a/src/components/views/groups/GroupUserSettings.js +++ b/src/components/views/groups/GroupUserSettings.js @@ -36,7 +36,7 @@ export default createReactClass({ }, componentWillMount: function() { - this.context.matrixClient.getJoinedGroups().done((result) => { + this.context.matrixClient.getJoinedGroups().then((result) => { this.setState({groups: result.groups || [], error: null}); }, (err) => { console.error(err); diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index b4f26d0cbd..0246d28542 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -55,7 +55,7 @@ export default class MAudioBody extends React.Component { decryptFile(content.file).then(function(blob) { decryptedBlob = blob; return URL.createObjectURL(decryptedBlob); - }).done((url) => { + }).then((url) => { this.setState({ decryptedUrl: url, decryptedBlob: decryptedBlob, diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 640baa1966..abfd8b64cd 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -289,7 +289,7 @@ export default class MImageBody extends React.Component { this.setState({ error: err, }); - }).done(); + }).then(); } // Remember that the user wanted to show this particular image diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index d277b6eae9..bc4dbf3374 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -115,7 +115,7 @@ module.exports = createReactClass({ this.setState({ error: err, }); - }).done(); + }).then(); } }, diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 207bf29998..192efcdd8a 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -870,7 +870,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room }, ).finally(() => { stopUpdating(); - }).done(); + }).then(); }; const roomId = user.roomId; diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index d93fe76b46..b06a9b9a30 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -53,7 +53,7 @@ module.exports = createReactClass({ ); }, (error)=>{ console.error("Failed to get URL preview: " + error); - }).done(); + }).then(); }, componentDidMount: function() { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 2ea6392e96..68e494d5eb 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -248,7 +248,7 @@ module.exports = createReactClass({ return client.getStoredDevicesForUser(member.userId); }).finally(function() { self._cancelDeviceList = null; - }).done(function(devices) { + }).then(function(devices) { if (cancelled) { // we got cancelled - presumably a different user now return; @@ -572,7 +572,7 @@ module.exports = createReactClass({ }, ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); - }).done(); + }).then(); }, onPowerChange: async function(powerLevel) { @@ -629,7 +629,7 @@ module.exports = createReactClass({ this.setState({ updating: this.state.updating + 1 }); createRoom({dmUserId: this.props.member.userId}).finally(() => { this.setState({ updating: this.state.updating - 1 }); - }).done(); + }).then(); }, onLeaveClick: function() { diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 32521006c7..904b17b15f 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -112,7 +112,7 @@ module.exports = createReactClass({ } }); - httpPromise.done(function() { + httpPromise.then(function() { self.setState({ phase: self.Phases.Display, avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl), diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 91292b19f9..15aa6203d7 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -174,7 +174,7 @@ module.exports = createReactClass({ newPassword: "", newPasswordConfirm: "", }); - }).done(); + }).then(); }, _optionallySetEmail: function() { diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index 30f507ea18..cb5db10be4 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -52,7 +52,7 @@ export default class DevicesPanel extends React.Component { } _loadDevices() { - MatrixClientPeg.get().getDevices().done( + MatrixClientPeg.get().getDevices().then( (resp) => { if (this._unmounted) { return; } this.setState({devices: resp.devices || []}); diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index e3b4cfe122..e67c61dff5 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -97,7 +97,7 @@ module.exports = createReactClass({ phase: this.phases.LOADING, }); - MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).done(function() { + MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).then(function() { self._refreshFromServer(); }); }, @@ -170,7 +170,7 @@ module.exports = createReactClass({ emailPusher.kind = null; emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher); } - emailPusherPromise.done(() => { + emailPusherPromise.then(() => { this._refreshFromServer(); }, (error) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -274,7 +274,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).done(function() { + Promise.all(deferreds).then(function() { self._refreshFromServer(); }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -343,7 +343,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).done(function(resps) { + Promise.all(deferreds).then(function(resps) { self._refreshFromServer(); }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -398,7 +398,7 @@ module.exports = createReactClass({ }; // Then, add the new ones - Promise.all(removeDeferreds).done(function(resps) { + Promise.all(removeDeferreds).then(function(resps) { const deferreds = []; let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState; @@ -434,7 +434,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).done(function(resps) { + Promise.all(deferreds).then(function(resps) { self._refreshFromServer(); }, onError); }, onError); @@ -650,7 +650,7 @@ module.exports = createReactClass({ externalContentRules: self.state.externalContentRules, externalPushRules: self.state.externalPushRules, }); - }).done(); + }).then(); MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids})); }, diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index fbad327078..875f0bfc10 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -75,7 +75,7 @@ export default class HelpUserSettingsTab extends React.Component { // stopping in the middle of the logs. console.log("Clear cache & reload clicked"); MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().store.deleteAllData().done(() => { + MatrixClientPeg.get().store.deleteAllData().then(() => { PlatformPeg.get().reload(); }); }; diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 7e1b06c0bf..177b88c3f2 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -186,7 +186,7 @@ class RoomViewStore extends Store { roomLoading: true, roomLoadError: null, }); - MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).done( + MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).then( (result) => { dis.dispatch({ action: 'view_room', @@ -223,7 +223,7 @@ class RoomViewStore extends Store { }); MatrixClientPeg.get().joinRoom( this._state.roomAlias || this._state.roomId, payload.opts, - ).done(() => { + ).then(() => { // We don't actually need to do anything here: we do *not* // clear the 'joining' flag because the Room object and/or // our 'joined' member event may not have come down the sync diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index b14ea7c242..7612b43b48 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -26,6 +26,7 @@ import sdk from 'matrix-react-sdk'; import MatrixClientPeg from '../../../../src/MatrixClientPeg'; import * as test_utils from '../../../test-utils'; +import {sleep} from "../../../../src/utils/promise"; const InteractiveAuthDialog = sdk.getComponent( 'views.dialogs.InteractiveAuthDialog', @@ -107,7 +108,7 @@ describe('InteractiveAuthDialog', function() { }, })).toBe(true); // let the request complete - return Promise.delay(1); + return sleep(1); }).then(() => { expect(onFinished.callCount).toEqual(1); expect(onFinished.calledWithExactly(true, {a: 1})).toBe(true); diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index 95f7e7999a..a31cbdebb5 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -91,7 +91,7 @@ describe('MemberEventListSummary', function() { testUtils.beforeEach(this); sandbox = testUtils.stubClient(); - languageHandler.setLanguage('en').done(done); + languageHandler.setLanguage('en').then(done); languageHandler.setMissingEntryGenerator(function(key) { return key.split('|', 2)[1]; }); diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index 1105a4af17..ed25c4d607 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -8,6 +8,7 @@ import * as testUtils from '../../../test-utils'; import sdk from 'matrix-react-sdk'; const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput'); import MatrixClientPeg from '../../../../src/MatrixClientPeg'; +import {sleep} from "../../../../src/utils/promise"; function addTextToDraft(text) { const components = document.getElementsByClassName('public-DraftEditor-content'); @@ -49,7 +50,7 @@ xdescribe('MessageComposerInput', () => { // warnings // (please can we make the components not setState() after // they are unmounted?) - Promise.delay(10).done(() => { + sleep(10).then(() => { if (parentDiv) { ReactDOM.unmountComponentAtNode(parentDiv); parentDiv.remove(); diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index 0d96bc15ab..8f21638703 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -11,7 +11,7 @@ describe('languageHandler', function() { testUtils.beforeEach(this); sandbox = testUtils.stubClient(); - languageHandler.setLanguage('en').done(done); + languageHandler.setLanguage('en').then(done); }); afterEach(function() { From d72dedb0cee9868792256bc8e34ee1e76e38c8dc Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 12 Nov 2019 11:43:18 +0000 Subject: [PATCH 0532/2372] Cache room alias to room ID mapping in memory This adds very basic cache (literally just a `Map` for now) to store room alias to room ID mappings. The improves the perceived performance of Riot when switching rooms via browser navigation (back / forward), as we no longer try to resolve the room alias every time. The cache is only in memory, so reloading manually or as part of the clear cache process will start afresh. Fixes https://github.com/vector-im/riot-web/issues/10020 --- src/RoomAliasCache.js | 35 +++++++++++++++++++++++++ src/components/structures/MatrixChat.js | 8 +++++- src/stores/RoomViewStore.js | 30 ++++++++++++++++----- 3 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 src/RoomAliasCache.js diff --git a/src/RoomAliasCache.js b/src/RoomAliasCache.js new file mode 100644 index 0000000000..bb511ba4d7 --- /dev/null +++ b/src/RoomAliasCache.js @@ -0,0 +1,35 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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 is meant to be a cache of room alias to room ID so that moving between + * rooms happens smoothly (for example using browser back / forward buttons). + * + * For the moment, it's in memory only and so only applies for the current + * session for simplicity, but could be extended further in the future. + * + * A similar thing could also be achieved via `pushState` with a state object, + * but keeping it separate like this seems easier in case we do want to extend. + */ +const aliasToIDMap = new Map(); + +export function storeRoomAliasInCache(alias, id) { + aliasToIDMap.set(alias, id); +} + +export function getCachedRoomIDForAlias(alias) { + return aliasToIDMap.get(alias); +} diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index da67416400..6cc86bf6d7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -60,6 +60,7 @@ import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; import DMRoomMap from '../../utils/DMRoomMap'; import { countRoomsWithNotif } from '../../RoomNotifs'; import { setTheme } from "../../theme"; +import { storeRoomAliasInCache } from '../../RoomAliasCache'; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -866,7 +867,12 @@ export default createReactClass({ const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { const theAlias = Rooms.getDisplayAliasForRoom(room); - if (theAlias) presentedId = theAlias; + if (theAlias) { + presentedId = theAlias; + // Store display alias of the presented room in cache to speed future + // navigation. + storeRoomAliasInCache(theAlias, room.roomId); + } // Store this as the ID of the last room accessed. This is so that we can // persist which room is being stored across refreshes and browser quits. diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 7e1b06c0bf..e860ed8b24 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -20,6 +20,7 @@ import MatrixClientPeg from '../MatrixClientPeg'; import sdk from '../index'; import Modal from '../Modal'; import { _t } from '../languageHandler'; +import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCache'; const INITIAL_STATE = { // Whether we're joining the currently viewed room (see isJoining()) @@ -137,7 +138,7 @@ class RoomViewStore extends Store { } } - _viewRoom(payload) { + async _viewRoom(payload) { if (payload.room_id) { const newState = { roomId: payload.room_id, @@ -176,6 +177,22 @@ class RoomViewStore extends Store { this._joinRoom(payload); } } else if (payload.room_alias) { + // Try the room alias to room ID navigation cache first to avoid + // blocking room navigation on the homeserver. + const roomId = getCachedRoomIDForAlias(payload.room_alias); + if (roomId) { + dis.dispatch({ + action: 'view_room', + room_id: roomId, + event_id: payload.event_id, + highlighted: payload.highlighted, + room_alias: payload.room_alias, + auto_join: payload.auto_join, + oob_data: payload.oob_data, + }); + return; + } + // Room alias cache miss, so let's ask the homeserver. // Resolve the alias and then do a second dispatch with the room ID acquired this._setState({ roomId: null, @@ -186,8 +203,9 @@ class RoomViewStore extends Store { roomLoading: true, roomLoadError: null, }); - MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).done( - (result) => { + try { + const result = await MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias); + storeRoomAliasInCache(payload.room_alias, result.room_id); dis.dispatch({ action: 'view_room', room_id: result.room_id, @@ -197,14 +215,14 @@ class RoomViewStore extends Store { auto_join: payload.auto_join, oob_data: payload.oob_data, }); - }, (err) => { + } catch (err) { dis.dispatch({ action: 'view_room_error', room_id: null, room_alias: payload.room_alias, - err: err, + err, }); - }); + } } } From 168b1b68bb5b9700da9ad22b692b3db866f12128 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:56:21 +0000 Subject: [PATCH 0533/2372] Revert "s/.done(/.then(/ since modern es6 track unhandled promise exceptions" This reverts commit 09a8fec2 --- src/Lifecycle.js | 4 ++-- src/Notifier.js | 2 +- src/Resend.js | 2 +- src/ScalarMessaging.js | 8 ++++---- src/components/structures/GroupView.js | 4 ++-- src/components/structures/InteractiveAuth.js | 2 +- src/components/structures/MatrixChat.js | 8 ++++---- src/components/structures/MyGroups.js | 2 +- src/components/structures/RoomDirectory.js | 8 ++++---- src/components/structures/RoomView.js | 8 ++++---- src/components/structures/TimelinePanel.js | 4 ++-- src/components/structures/auth/ForgotPassword.js | 2 +- src/components/structures/auth/Login.js | 4 ++-- src/components/structures/auth/PostRegistration.js | 2 +- src/components/structures/auth/Registration.js | 2 +- .../views/auth/InteractiveAuthEntryComponents.js | 2 +- .../views/context_menus/RoomTileContextMenu.js | 4 ++-- .../views/dialogs/AddressPickerDialog.js | 4 ++-- src/components/views/dialogs/CreateGroupDialog.js | 2 +- src/components/views/dialogs/KeyShareDialog.js | 2 +- src/components/views/dialogs/SetEmailDialog.js | 4 ++-- src/components/views/elements/AppTile.js | 2 +- .../views/elements/EditableTextContainer.js | 4 ++-- src/components/views/elements/ErrorBoundary.js | 2 +- src/components/views/elements/ImageView.js | 2 +- src/components/views/elements/LanguageDropdown.js | 2 +- src/components/views/groups/GroupUserSettings.js | 2 +- src/components/views/messages/MAudioBody.js | 2 +- src/components/views/messages/MImageBody.js | 2 +- src/components/views/messages/MVideoBody.js | 2 +- src/components/views/right_panel/UserInfo.js | 2 +- src/components/views/rooms/LinkPreviewWidget.js | 2 +- src/components/views/rooms/MemberInfo.js | 6 +++--- src/components/views/settings/ChangeAvatar.js | 2 +- src/components/views/settings/ChangePassword.js | 2 +- src/components/views/settings/DevicesPanel.js | 2 +- src/components/views/settings/Notifications.js | 14 +++++++------- .../settings/tabs/user/HelpUserSettingsTab.js | 2 +- src/stores/RoomViewStore.js | 4 ++-- .../views/dialogs/InteractiveAuthDialog-test.js | 3 +-- .../views/elements/MemberEventListSummary-test.js | 2 +- .../views/rooms/MessageComposerInput-test.js | 3 +-- test/i18n-test/languageHandler-test.js | 2 +- 43 files changed, 72 insertions(+), 74 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 9bada98168..53a9b7a998 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -524,7 +524,7 @@ export function logout() { console.log("Failed to call logout API: token will not be invalidated"); onLoggedOut(); }, - ).then(); + ).done(); } export function softLogout() { @@ -608,7 +608,7 @@ export function onLoggedOut() { // that can occur when components try to use a null client. dis.dispatch({action: 'on_logged_out'}, true); stopMatrixClient(); - _clearStorage().then(); + _clearStorage().done(); } /** diff --git a/src/Notifier.js b/src/Notifier.js index edb9850dfe..cca0ea2b89 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -198,7 +198,7 @@ const Notifier = { if (enable) { // Attempt to get permission from user - plaf.requestNotificationPermission().then((result) => { + plaf.requestNotificationPermission().done((result) => { if (result !== 'granted') { // The permission request was dismissed or denied // TODO: Support alternative branding in messaging diff --git a/src/Resend.js b/src/Resend.js index 51ec804c01..4eaee16d1b 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -35,7 +35,7 @@ module.exports = { }, resend: function(event) { const room = MatrixClientPeg.get().getRoom(event.getRoomId()); - MatrixClientPeg.get().resendEvent(event, room).then(function(res) { + MatrixClientPeg.get().resendEvent(event, room).done(function(res) { dis.dispatch({ action: 'message_sent', event: event, diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index c0ffc3022d..910a6c4f13 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -279,7 +279,7 @@ function inviteUser(event, roomId, userId) { } } - client.invite(roomId, userId).then(function() { + client.invite(roomId, userId).done(function() { sendResponse(event, { success: true, }); @@ -398,7 +398,7 @@ function setPlumbingState(event, roomId, status) { sendError(event, _t('You need to be logged in.')); return; } - client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).then(() => { + client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).done(() => { sendResponse(event, { success: true, }); @@ -414,7 +414,7 @@ function setBotOptions(event, roomId, userId) { sendError(event, _t('You need to be logged in.')); return; } - client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).then(() => { + client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => { sendResponse(event, { success: true, }); @@ -444,7 +444,7 @@ function setBotPower(event, roomId, userId, level) { }, ); - client.setPowerLevel(roomId, userId, level, powerEvent).then(() => { + client.setPowerLevel(roomId, userId, level, powerEvent).done(() => { sendResponse(event, { success: true, }); diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index b97d76d72a..4056557a7c 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -639,7 +639,7 @@ export default createReactClass({ title: _t('Error'), description: _t('Failed to upload image'), }); - }).then(); + }).done(); }, _onJoinableChange: function(ev) { @@ -678,7 +678,7 @@ export default createReactClass({ this.setState({ avatarChanged: false, }); - }).then(); + }).done(); }, _saveGroup: async function() { diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 86fc351515..5e06d124c4 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -121,7 +121,7 @@ export default createReactClass({ this.setState({ errorText: msg, }); - }).then(); + }).done(); this._intervalId = null; if (this.props.poll) { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index c7bf2f181f..d12eba88f7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -541,7 +541,7 @@ export default createReactClass({ const Loader = sdk.getComponent("elements.Spinner"); const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); - MatrixClientPeg.get().leave(payload.room_id).then(() => { + MatrixClientPeg.get().leave(payload.room_id).done(() => { modal.close(); if (this.state.currentRoomId === payload.room_id) { dis.dispatch({action: 'view_next_room'}); @@ -862,7 +862,7 @@ export default createReactClass({ waitFor = this.firstSyncPromise.promise; } - waitFor.then(() => { + waitFor.done(() => { let presentedId = roomInfo.room_alias || roomInfo.room_id; const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { @@ -974,7 +974,7 @@ export default createReactClass({ const [shouldCreate, createOpts] = await modal.finished; if (shouldCreate) { - createRoom({createOpts}).then(); + createRoom({createOpts}).done(); } }, @@ -1750,7 +1750,7 @@ export default createReactClass({ return; } - cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => { + cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => { dis.dispatch({action: 'message_sent'}); }, (err) => { dis.dispatch({action: 'message_send_failed'}); diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 63ae14ba09..2de15a5444 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -47,7 +47,7 @@ export default createReactClass({ }, _fetch: function() { - this.context.matrixClient.getJoinedGroups().then((result) => { + this.context.matrixClient.getJoinedGroups().done((result) => { this.setState({groups: result.groups, error: null}); }, (err) => { if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') { diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 941381726d..84f402e484 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -89,7 +89,7 @@ module.exports = createReactClass({ this.setState({protocolsLoading: false}); return; } - MatrixClientPeg.get().getThirdpartyProtocols().then((response) => { + MatrixClientPeg.get().getThirdpartyProtocols().done((response) => { this.protocols = response; this.setState({protocolsLoading: false}); }, (err) => { @@ -135,7 +135,7 @@ module.exports = createReactClass({ publicRooms: [], loading: true, }); - this.getMoreRooms().then(); + this.getMoreRooms().done(); }, getMoreRooms: function() { @@ -246,7 +246,7 @@ module.exports = createReactClass({ if (!alias) return; step = _t('delete the alias.'); return MatrixClientPeg.get().deleteAlias(alias); - }).then(() => { + }).done(() => { modal.close(); this.refreshRoomList(); }, (err) => { @@ -348,7 +348,7 @@ module.exports = createReactClass({ }); return; } - MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).then((resp) => { + MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => { if (resp.length > 0 && resp[0].alias) { this.showRoomAlias(resp[0].alias, true); } else { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index d3ba517264..4de573479d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1101,7 +1101,7 @@ module.exports = createReactClass({ } ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get()) - .then(undefined, (error) => { + .done(undefined, (error) => { if (error.name === "UnknownDeviceError") { // Let the staus bar handle this return; @@ -1145,7 +1145,7 @@ module.exports = createReactClass({ filter: filter, term: term, }); - this._handleSearchResult(searchPromise).then(); + this._handleSearchResult(searchPromise).done(); }, _handleSearchResult: function(searchPromise) { @@ -1316,7 +1316,7 @@ module.exports = createReactClass({ }, onForgetClick: function() { - MatrixClientPeg.get().forget(this.state.room.roomId).then(function() { + MatrixClientPeg.get().forget(this.state.room.roomId).done(function() { dis.dispatch({ action: 'view_next_room' }); }, function(err) { const errCode = err.errcode || _t("unknown error code"); @@ -1333,7 +1333,7 @@ module.exports = createReactClass({ this.setState({ rejecting: true, }); - MatrixClientPeg.get().leave(this.state.roomId).then(function() { + MatrixClientPeg.get().leave(this.state.roomId).done(function() { dis.dispatch({ action: 'view_next_room' }); self.setState({ rejecting: false, diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 573d82bb9d..faa6f2564a 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -462,7 +462,7 @@ const TimelinePanel = createReactClass({ // timeline window. // // see https://github.com/vector-im/vector-web/issues/1035 - this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => { + this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => { if (this.unmounted) { return; } const { events, liveEvents } = this._getEvents(); @@ -1088,7 +1088,7 @@ const TimelinePanel = createReactClass({ prom = prom.then(onLoaded, onError); } - prom.then(); + prom.done(); }, // handle the completion of a timeline load or localEchoUpdate, by diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 6f68293caa..46a5fa7bd7 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -105,7 +105,7 @@ module.exports = createReactClass({ phase: PHASE_SENDING_EMAIL, }); this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl); - this.reset.resetPassword(email, password).then(() => { + this.reset.resetPassword(email, password).done(() => { this.setState({ phase: PHASE_EMAIL_SENT, }); diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 84209e514f..ad77ed49a5 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -253,7 +253,7 @@ module.exports = createReactClass({ this.setState({ busy: false, }); - }).then(); + }).done(); }, onUsernameChanged: function(username) { @@ -424,7 +424,7 @@ module.exports = createReactClass({ this.setState({ busy: false, }); - }).then(); + }).done(); }, _isSupportedFlow: function(flow) { diff --git a/src/components/structures/auth/PostRegistration.js b/src/components/structures/auth/PostRegistration.js index 760163585d..66075c80f7 100644 --- a/src/components/structures/auth/PostRegistration.js +++ b/src/components/structures/auth/PostRegistration.js @@ -43,7 +43,7 @@ module.exports = createReactClass({ const cli = MatrixClientPeg.get(); this.setState({busy: true}); const self = this; - cli.getProfileInfo(cli.credentials.userId).then(function(result) { + cli.getProfileInfo(cli.credentials.userId).done(function(result) { self.setState({ avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url), busy: false, diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 6e0fc246d2..6321028457 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -371,7 +371,7 @@ module.exports = createReactClass({ if (pushers[i].kind === 'email') { const emailPusher = pushers[i]; emailPusher.data = { brand: this.props.brand }; - matrixClient.setPusher(emailPusher).then(() => { + matrixClient.setPusher(emailPusher).done(() => { console.log("Set email branding to " + this.props.brand); }, (error) => { console.error("Couldn't set email branding: " + error); diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index d61035210b..d19ce95b33 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -441,7 +441,7 @@ export const MsisdnAuthEntry = createReactClass({ this.props.fail(e); }).finally(() => { this.setState({requestingToken: false}); - }).then(); + }).done(); }, /* diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 98628979e5..541daef27f 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -161,7 +161,7 @@ module.exports = createReactClass({ _onClickForget: function() { // FIXME: duplicated with RoomSettings (and dead code in RoomView) - MatrixClientPeg.get().forget(this.props.room.roomId).then(() => { + MatrixClientPeg.get().forget(this.props.room.roomId).done(() => { // Switch to another room view if we're currently viewing the // historical room if (RoomViewStore.getRoomId() === this.props.room.roomId) { @@ -191,7 +191,7 @@ module.exports = createReactClass({ this.setState({ roomNotifState: newState, }); - RoomNotifs.setRoomNotifsState(roomId, newState).then(() => { + RoomNotifs.setRoomNotifsState(roomId, newState).done(() => { // delay slightly so that the user can see their state change // before closing the menu return sleep(500).then(() => { diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index caf6bc18c5..dc61f23956 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -267,7 +267,7 @@ module.exports = createReactClass({ this.setState({ searchError: err.errcode ? err.message : _t('Something went wrong!'), }); - }).then(() => { + }).done(() => { this.setState({ busy: false, }); @@ -380,7 +380,7 @@ module.exports = createReactClass({ // Do a local search immediately this._doLocalSearch(query); } - }).then(() => { + }).done(() => { this.setState({ busy: false, }); diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 191d797a1e..11f4c21366 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -93,7 +93,7 @@ export default createReactClass({ this.setState({createError: e}); }).finally(() => { this.setState({creating: false}); - }).then(); + }).done(); }, _onCancel: function() { diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index 51b02f1adf..a10c25a0fb 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -78,7 +78,7 @@ export default createReactClass({ true, ); } - }).then(); + }).done(); }, componentWillUnmount: function() { diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.js index b527abffc9..bedf713c4e 100644 --- a/src/components/views/dialogs/SetEmailDialog.js +++ b/src/components/views/dialogs/SetEmailDialog.js @@ -62,7 +62,7 @@ export default createReactClass({ return; } this._addThreepid = new AddThreepid(); - this._addThreepid.addEmailAddress(emailAddress).then(() => { + this._addThreepid.addEmailAddress(emailAddress).done(() => { Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { title: _t("Verification Pending"), description: _t( @@ -96,7 +96,7 @@ export default createReactClass({ }, verifyEmailAddress: function() { - this._addThreepid.checkEmailLinkClicked().then(() => { + this._addThreepid.checkEmailLinkClicked().done(() => { this.props.onFinished(true); }, (err) => { this.setState({emailBusy: false}); diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 453630413c..260b63dfd4 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -205,7 +205,7 @@ export default class AppTile extends React.Component { if (!this._scalarClient) { this._scalarClient = defaultManager.getScalarClient(); } - this._scalarClient.getScalarToken().then((token) => { + this._scalarClient.getScalarToken().done((token) => { // Append scalar_token as a query param if not already present this._scalarClient.scalarToken = token; const u = url.parse(this._addWurlParams(this.props.url)); diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.js index cc49c3c67f..3bf37df951 100644 --- a/src/components/views/elements/EditableTextContainer.js +++ b/src/components/views/elements/EditableTextContainer.js @@ -51,7 +51,7 @@ export default class EditableTextContainer extends React.Component { this.setState({busy: true}); - this.props.getInitialValue().then( + this.props.getInitialValue().done( (result) => { if (this._unmounted) { return; } this.setState({ @@ -83,7 +83,7 @@ export default class EditableTextContainer extends React.Component { errorString: null, }); - this.props.onSubmit(value).then( + this.props.onSubmit(value).done( () => { if (this._unmounted) { return; } this.setState({ diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.js index e36464c4ef..e53e1ec0fa 100644 --- a/src/components/views/elements/ErrorBoundary.js +++ b/src/components/views/elements/ErrorBoundary.js @@ -54,7 +54,7 @@ export default class ErrorBoundary extends React.PureComponent { if (!PlatformPeg.get()) return; MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().store.deleteAllData().then(() => { + MatrixClientPeg.get().store.deleteAllData().done(() => { PlatformPeg.get().reload(); }); }; diff --git a/src/components/views/elements/ImageView.js b/src/components/views/elements/ImageView.js index 09e0ff0e54..2772363bd0 100644 --- a/src/components/views/elements/ImageView.js +++ b/src/components/views/elements/ImageView.js @@ -84,7 +84,7 @@ export default class ImageView extends React.Component { title: _t('Error'), description: _t('You cannot delete this image. (%(code)s)', {code: code}), }); - }).then(); + }).done(); }, }); }; diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index 0c4b2b9d6a..365f9ded61 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -49,7 +49,7 @@ export default class LanguageDropdown extends React.Component { this.setState({langs}); }).catch(() => { this.setState({langs: ['en']}); - }).then(); + }).done(); if (!this.props.value) { // If no value is given, we start with the first diff --git a/src/components/views/groups/GroupUserSettings.js b/src/components/views/groups/GroupUserSettings.js index 3cd5731b99..7d80bdd209 100644 --- a/src/components/views/groups/GroupUserSettings.js +++ b/src/components/views/groups/GroupUserSettings.js @@ -36,7 +36,7 @@ export default createReactClass({ }, componentWillMount: function() { - this.context.matrixClient.getJoinedGroups().then((result) => { + this.context.matrixClient.getJoinedGroups().done((result) => { this.setState({groups: result.groups || [], error: null}); }, (err) => { console.error(err); diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index 0246d28542..b4f26d0cbd 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -55,7 +55,7 @@ export default class MAudioBody extends React.Component { decryptFile(content.file).then(function(blob) { decryptedBlob = blob; return URL.createObjectURL(decryptedBlob); - }).then((url) => { + }).done((url) => { this.setState({ decryptedUrl: url, decryptedBlob: decryptedBlob, diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index abfd8b64cd..640baa1966 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -289,7 +289,7 @@ export default class MImageBody extends React.Component { this.setState({ error: err, }); - }).then(); + }).done(); } // Remember that the user wanted to show this particular image diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index bc4dbf3374..d277b6eae9 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -115,7 +115,7 @@ module.exports = createReactClass({ this.setState({ error: err, }); - }).then(); + }).done(); } }, diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 192efcdd8a..207bf29998 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -870,7 +870,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room }, ).finally(() => { stopUpdating(); - }).then(); + }).done(); }; const roomId = user.roomId; diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index b06a9b9a30..d93fe76b46 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -53,7 +53,7 @@ module.exports = createReactClass({ ); }, (error)=>{ console.error("Failed to get URL preview: " + error); - }).then(); + }).done(); }, componentDidMount: function() { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 68e494d5eb..2ea6392e96 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -248,7 +248,7 @@ module.exports = createReactClass({ return client.getStoredDevicesForUser(member.userId); }).finally(function() { self._cancelDeviceList = null; - }).then(function(devices) { + }).done(function(devices) { if (cancelled) { // we got cancelled - presumably a different user now return; @@ -572,7 +572,7 @@ module.exports = createReactClass({ }, ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); - }).then(); + }).done(); }, onPowerChange: async function(powerLevel) { @@ -629,7 +629,7 @@ module.exports = createReactClass({ this.setState({ updating: this.state.updating + 1 }); createRoom({dmUserId: this.props.member.userId}).finally(() => { this.setState({ updating: this.state.updating - 1 }); - }).then(); + }).done(); }, onLeaveClick: function() { diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 904b17b15f..32521006c7 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -112,7 +112,7 @@ module.exports = createReactClass({ } }); - httpPromise.then(function() { + httpPromise.done(function() { self.setState({ phase: self.Phases.Display, avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl), diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 15aa6203d7..91292b19f9 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -174,7 +174,7 @@ module.exports = createReactClass({ newPassword: "", newPasswordConfirm: "", }); - }).then(); + }).done(); }, _optionallySetEmail: function() { diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index cb5db10be4..30f507ea18 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -52,7 +52,7 @@ export default class DevicesPanel extends React.Component { } _loadDevices() { - MatrixClientPeg.get().getDevices().then( + MatrixClientPeg.get().getDevices().done( (resp) => { if (this._unmounted) { return; } this.setState({devices: resp.devices || []}); diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index e67c61dff5..e3b4cfe122 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -97,7 +97,7 @@ module.exports = createReactClass({ phase: this.phases.LOADING, }); - MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).then(function() { + MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).done(function() { self._refreshFromServer(); }); }, @@ -170,7 +170,7 @@ module.exports = createReactClass({ emailPusher.kind = null; emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher); } - emailPusherPromise.then(() => { + emailPusherPromise.done(() => { this._refreshFromServer(); }, (error) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -274,7 +274,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).then(function() { + Promise.all(deferreds).done(function() { self._refreshFromServer(); }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -343,7 +343,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).then(function(resps) { + Promise.all(deferreds).done(function(resps) { self._refreshFromServer(); }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -398,7 +398,7 @@ module.exports = createReactClass({ }; // Then, add the new ones - Promise.all(removeDeferreds).then(function(resps) { + Promise.all(removeDeferreds).done(function(resps) { const deferreds = []; let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState; @@ -434,7 +434,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).then(function(resps) { + Promise.all(deferreds).done(function(resps) { self._refreshFromServer(); }, onError); }, onError); @@ -650,7 +650,7 @@ module.exports = createReactClass({ externalContentRules: self.state.externalContentRules, externalPushRules: self.state.externalPushRules, }); - }).then(); + }).done(); MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids})); }, diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index 875f0bfc10..fbad327078 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -75,7 +75,7 @@ export default class HelpUserSettingsTab extends React.Component { // stopping in the middle of the logs. console.log("Clear cache & reload clicked"); MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().store.deleteAllData().then(() => { + MatrixClientPeg.get().store.deleteAllData().done(() => { PlatformPeg.get().reload(); }); }; diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 177b88c3f2..7e1b06c0bf 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -186,7 +186,7 @@ class RoomViewStore extends Store { roomLoading: true, roomLoadError: null, }); - MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).then( + MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).done( (result) => { dis.dispatch({ action: 'view_room', @@ -223,7 +223,7 @@ class RoomViewStore extends Store { }); MatrixClientPeg.get().joinRoom( this._state.roomAlias || this._state.roomId, payload.opts, - ).then(() => { + ).done(() => { // We don't actually need to do anything here: we do *not* // clear the 'joining' flag because the Room object and/or // our 'joined' member event may not have come down the sync diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index 7612b43b48..b14ea7c242 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -26,7 +26,6 @@ import sdk from 'matrix-react-sdk'; import MatrixClientPeg from '../../../../src/MatrixClientPeg'; import * as test_utils from '../../../test-utils'; -import {sleep} from "../../../../src/utils/promise"; const InteractiveAuthDialog = sdk.getComponent( 'views.dialogs.InteractiveAuthDialog', @@ -108,7 +107,7 @@ describe('InteractiveAuthDialog', function() { }, })).toBe(true); // let the request complete - return sleep(1); + return Promise.delay(1); }).then(() => { expect(onFinished.callCount).toEqual(1); expect(onFinished.calledWithExactly(true, {a: 1})).toBe(true); diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index a31cbdebb5..95f7e7999a 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -91,7 +91,7 @@ describe('MemberEventListSummary', function() { testUtils.beforeEach(this); sandbox = testUtils.stubClient(); - languageHandler.setLanguage('en').then(done); + languageHandler.setLanguage('en').done(done); languageHandler.setMissingEntryGenerator(function(key) { return key.split('|', 2)[1]; }); diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index ed25c4d607..1105a4af17 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -8,7 +8,6 @@ import * as testUtils from '../../../test-utils'; import sdk from 'matrix-react-sdk'; const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput'); import MatrixClientPeg from '../../../../src/MatrixClientPeg'; -import {sleep} from "../../../../src/utils/promise"; function addTextToDraft(text) { const components = document.getElementsByClassName('public-DraftEditor-content'); @@ -50,7 +49,7 @@ xdescribe('MessageComposerInput', () => { // warnings // (please can we make the components not setState() after // they are unmounted?) - sleep(10).then(() => { + Promise.delay(10).done(() => { if (parentDiv) { ReactDOM.unmountComponentAtNode(parentDiv); parentDiv.remove(); diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index 8f21638703..0d96bc15ab 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -11,7 +11,7 @@ describe('languageHandler', function() { testUtils.beforeEach(this); sandbox = testUtils.stubClient(); - languageHandler.setLanguage('en').then(done); + languageHandler.setLanguage('en').done(done); }); afterEach(function() { From f9d6ed63f0e82056905fa8fcee1914d3de4deaf1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:56:32 +0000 Subject: [PATCH 0534/2372] Revert "Replace Promise.delay with promise utils sleep" This reverts commit 0a21957b --- src/components/structures/GroupView.js | 9 ++++----- .../views/context_menus/RoomTileContextMenu.js | 7 +++---- src/components/views/dialogs/AddressPickerDialog.js | 3 +-- .../views/settings/tabs/user/SecurityUserSettingsTab.js | 3 +-- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 4056557a7c..4d8f47003c 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -38,7 +38,6 @@ import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks"; import {Group} from "matrix-js-sdk"; -import {sleep} from "../../utils/promise"; const LONG_DESC_PLACEHOLDER = _td( `

        HTML for your community's page

        @@ -693,7 +692,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await sleep(500); + await Promise.delay(500); GroupStore.acceptGroupInvite(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -712,7 +711,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await sleep(500); + await Promise.delay(500); GroupStore.leaveGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -736,7 +735,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await sleep(500); + await Promise.delay(500); GroupStore.joinGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -788,7 +787,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await sleep(500); + await Promise.delay(500); GroupStore.leaveGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 541daef27f..9bb573026f 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -32,7 +32,6 @@ import * as RoomNotifs from '../../../RoomNotifs'; import Modal from '../../../Modal'; import RoomListActions from '../../../actions/RoomListActions'; import RoomViewStore from '../../../stores/RoomViewStore'; -import {sleep} from "../../../utils/promise"; module.exports = createReactClass({ displayName: 'RoomTileContextMenu', @@ -63,7 +62,7 @@ module.exports = createReactClass({ _toggleTag: function(tagNameOn, tagNameOff) { if (!MatrixClientPeg.get().isGuest()) { - sleep(500).then(() => { + Promise.delay(500).then(() => { dis.dispatch(RoomListActions.tagRoom( MatrixClientPeg.get(), this.props.room, @@ -120,7 +119,7 @@ module.exports = createReactClass({ Rooms.guessAndSetDMRoom( this.props.room, newIsDirectMessage, - ).then(sleep(500)).finally(() => { + ).delay(500).finally(() => { // Close the context menu if (this.props.onFinished) { this.props.onFinished(); @@ -194,7 +193,7 @@ module.exports = createReactClass({ RoomNotifs.setRoomNotifsState(roomId, newState).done(() => { // delay slightly so that the user can see their state change // before closing the menu - return sleep(500).then(() => { + return Promise.delay(500).then(() => { if (this._unmounted) return; // Close the context menu if (this.props.onFinished) { diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index dc61f23956..fb779fa96f 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -32,7 +32,6 @@ import * as Email from '../../../email'; import IdentityAuthClient from '../../../IdentityAuthClient'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils'; import { abbreviateUrl } from '../../../utils/UrlUtils'; -import {sleep} from "../../../utils/promise"; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -534,7 +533,7 @@ module.exports = createReactClass({ }; // wait a bit to let the user finish typing - await sleep(500); + await Promise.delay(500); if (cancelled) return null; try { diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index 222af48fa1..e619791b01 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -25,7 +25,6 @@ import Analytics from "../../../../../Analytics"; import Promise from "bluebird"; import Modal from "../../../../../Modal"; import sdk from "../../../../.."; -import {sleep} from "../../../../../utils/promise"; export class IgnoredUser extends React.Component { static propTypes = { @@ -130,7 +129,7 @@ export default class SecurityUserSettingsTab extends React.Component { if (e.errcode === "M_LIMIT_EXCEEDED") { // Add a delay between each invite change in order to avoid rate // limiting by the server. - await sleep(e.retry_after_ms || 2500); + await Promise.delay(e.retry_after_ms || 2500); // Redo last action i--; From 7a512f7299f5316aba434cc639a01c4b65a5a3aa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:56:43 +0000 Subject: [PATCH 0535/2372] Revert "Replace rest of defer usages using small shim. Add homebrew promise utils" This reverts commit 6850c147 --- src/Modal.js | 3 +- src/components/structures/MatrixChat.js | 5 ++- src/utils/MultiInviter.js | 3 +- src/utils/promise.js | 46 ------------------------- 4 files changed, 4 insertions(+), 53 deletions(-) delete mode 100644 src/utils/promise.js diff --git a/src/Modal.js b/src/Modal.js index cb19731f01..26c9da8bbb 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -24,7 +24,6 @@ import sdk from './index'; import dis from './dispatcher'; import { _t } from './languageHandler'; import Promise from "bluebird"; -import {defer} from "./utils/promise"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; @@ -203,7 +202,7 @@ class ModalManager { } _getCloseFn(modal, props) { - const deferred = defer(); + const deferred = Promise.defer(); return [(...args) => { deferred.resolve(args); if (props && props.onFinished) props.onFinished.apply(null, args); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d12eba88f7..da67416400 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -60,7 +60,6 @@ import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; import DMRoomMap from '../../utils/DMRoomMap'; import { countRoomsWithNotif } from '../../RoomNotifs'; import { setTheme } from "../../theme"; -import {defer} from "../../utils/promise"; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -237,7 +236,7 @@ export default createReactClass({ // Used by _viewRoom before getting state from sync this.firstSyncComplete = false; - this.firstSyncPromise = defer(); + this.firstSyncPromise = Promise.defer(); if (this.props.config.sync_timeline_limit) { MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; @@ -1262,7 +1261,7 @@ export default createReactClass({ // since we're about to start the client and therefore about // to do the first sync this.firstSyncComplete = false; - this.firstSyncPromise = defer(); + this.firstSyncPromise = Promise.defer(); const cli = MatrixClientPeg.get(); const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index de5c2e7610..e8995b46d7 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -24,7 +24,6 @@ import {_t} from "../languageHandler"; import sdk from "../index"; import Modal from "../Modal"; import SettingsStore from "../settings/SettingsStore"; -import {defer} from "./promise"; /** * Invites multiple addresses to a room or group, handling rate limiting from the server @@ -72,7 +71,7 @@ export default class MultiInviter { }; } } - this.deferred = defer(); + this.deferred = Promise.defer(); this._inviteMore(0); return this.deferred.promise; diff --git a/src/utils/promise.js b/src/utils/promise.js deleted file mode 100644 index dd10f7fdd7..0000000000 --- a/src/utils/promise.js +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// @flow - -// Returns a promise which resolves with a given value after the given number of ms -export const sleep = (ms: number, value: any): Promise => new Promise((resolve => { setTimeout(resolve, ms, value); })); - -// Returns a promise which resolves when the input promise resolves with its value -// or when the timeout of ms is reached with the value of given timeoutValue -export async function timeout(promise: Promise, timeoutValue: any, ms: number): Promise { - const timeoutPromise = new Promise((resolve) => { - const timeoutId = setTimeout(resolve, ms, timeoutValue); - promise.then(() => { - clearTimeout(timeoutId); - }); - }); - - return Promise.race([promise, timeoutPromise]); -} - -// Returns a Deferred -export function defer(): {resolve: () => {}, reject: () => {}, promise: Promise} { - let resolve; - let reject; - - const promise = new Promise((_resolve, _reject) => { - resolve = _resolve; - reject = _reject; - }); - - return {resolve, reject, promise}; -} From 548e38cba9fc24748b4fde73895ac9b30e53d2bf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:56:53 +0000 Subject: [PATCH 0536/2372] Revert "Replace all trivial Promise.defer usages with regular Promises" This reverts commit 44401d73 --- src/ContentMessages.js | 174 +++++++++--------- src/Lifecycle.js | 8 +- src/components/views/rooms/Autocomplete.js | 29 +-- .../views/settings/ChangePassword.js | 9 +- src/rageshake/submit-rageshake.js | 40 ++-- 5 files changed, 138 insertions(+), 122 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index dab8de2465..2d58622db8 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -59,38 +59,40 @@ export class UploadCanceledError extends Error {} * and a thumbnail key. */ function createThumbnail(element, inputWidth, inputHeight, mimeType) { - return new Promise((resolve) => { - let targetWidth = inputWidth; - let targetHeight = inputHeight; - if (targetHeight > MAX_HEIGHT) { - targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); - targetHeight = MAX_HEIGHT; - } - if (targetWidth > MAX_WIDTH) { - targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); - targetWidth = MAX_WIDTH; - } + const deferred = Promise.defer(); - const canvas = document.createElement("canvas"); - canvas.width = targetWidth; - canvas.height = targetHeight; - canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); - canvas.toBlob(function(thumbnail) { - resolve({ - info: { - thumbnail_info: { - w: targetWidth, - h: targetHeight, - mimetype: thumbnail.type, - size: thumbnail.size, - }, - w: inputWidth, - h: inputHeight, + let targetWidth = inputWidth; + let targetHeight = inputHeight; + if (targetHeight > MAX_HEIGHT) { + targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); + targetHeight = MAX_HEIGHT; + } + if (targetWidth > MAX_WIDTH) { + targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); + targetWidth = MAX_WIDTH; + } + + const canvas = document.createElement("canvas"); + canvas.width = targetWidth; + canvas.height = targetHeight; + canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); + canvas.toBlob(function(thumbnail) { + deferred.resolve({ + info: { + thumbnail_info: { + w: targetWidth, + h: targetHeight, + mimetype: thumbnail.type, + size: thumbnail.size, }, - thumbnail: thumbnail, - }); - }, mimeType); - }); + w: inputWidth, + h: inputHeight, + }, + thumbnail: thumbnail, + }); + }, mimeType); + + return deferred.promise; } /** @@ -177,29 +179,30 @@ function infoForImageFile(matrixClient, roomId, imageFile) { * @return {Promise} A promise that resolves with the video image element. */ function loadVideoElement(videoFile) { - return new Promise((resolve, reject) => { - // Load the file into an html element - const video = document.createElement("video"); + const deferred = Promise.defer(); - const reader = new FileReader(); + // Load the file into an html element + const video = document.createElement("video"); - reader.onload = function(e) { - video.src = e.target.result; + const reader = new FileReader(); + reader.onload = function(e) { + video.src = e.target.result; - // Once ready, returns its size - // Wait until we have enough data to thumbnail the first frame. - video.onloadeddata = function() { - resolve(video); - }; - video.onerror = function(e) { - reject(e); - }; + // Once ready, returns its size + // Wait until we have enough data to thumbnail the first frame. + video.onloadeddata = function() { + deferred.resolve(video); }; - reader.onerror = function(e) { - reject(e); + video.onerror = function(e) { + deferred.reject(e); }; - reader.readAsDataURL(videoFile); - }); + }; + reader.onerror = function(e) { + deferred.reject(e); + }; + reader.readAsDataURL(videoFile); + + return deferred.promise; } /** @@ -233,16 +236,16 @@ function infoForVideoFile(matrixClient, roomId, videoFile) { * is read. */ function readFileAsArrayBuffer(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = function(e) { - resolve(e.target.result); - }; - reader.onerror = function(e) { - reject(e); - }; - reader.readAsArrayBuffer(file); - }); + const deferred = Promise.defer(); + const reader = new FileReader(); + reader.onload = function(e) { + deferred.resolve(e.target.result); + }; + reader.onerror = function(e) { + deferred.reject(e); + }; + reader.readAsArrayBuffer(file); + return deferred.promise; } /** @@ -458,34 +461,33 @@ export default class ContentMessages { content.info.mimetype = file.type; } - const prom = new Promise((resolve) => { - if (file.type.indexOf('image/') == 0) { - content.msgtype = 'm.image'; - infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ - extend(content.info, imageInfo); - resolve(); - }, (error)=>{ - console.error(error); - content.msgtype = 'm.file'; - resolve(); - }); - } else if (file.type.indexOf('audio/') == 0) { - content.msgtype = 'm.audio'; - resolve(); - } else if (file.type.indexOf('video/') == 0) { - content.msgtype = 'm.video'; - infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ - extend(content.info, videoInfo); - resolve(); - }, (error)=>{ - content.msgtype = 'm.file'; - resolve(); - }); - } else { + const def = Promise.defer(); + if (file.type.indexOf('image/') == 0) { + content.msgtype = 'm.image'; + infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ + extend(content.info, imageInfo); + def.resolve(); + }, (error)=>{ + console.error(error); content.msgtype = 'm.file'; - resolve(); - } - }); + def.resolve(); + }); + } else if (file.type.indexOf('audio/') == 0) { + content.msgtype = 'm.audio'; + def.resolve(); + } else if (file.type.indexOf('video/') == 0) { + content.msgtype = 'm.video'; + infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ + extend(content.info, videoInfo); + def.resolve(); + }, (error)=>{ + content.msgtype = 'm.file'; + def.resolve(); + }); + } else { + content.msgtype = 'm.file'; + def.resolve(); + } const upload = { fileName: file.name || 'Attachment', @@ -507,7 +509,7 @@ export default class ContentMessages { dis.dispatch({action: 'upload_progress', upload: upload}); } - return prom.then(function() { + return def.promise.then(function() { // XXX: upload.promise must be the promise that // is returned by uploadFile as it has an abort() // method hacked onto it. diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 53a9b7a998..13f3abccb1 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -312,14 +312,18 @@ async function _restoreFromLocalStorage(opts) { function _handleLoadSessionFailure(e) { console.error("Unable to load session", e); + const def = Promise.defer(); const SessionRestoreErrorDialog = sdk.getComponent('views.dialogs.SessionRestoreErrorDialog'); - const modal = Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { + Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { error: e.message, + onFinished: (success) => { + def.resolve(success); + }, }); - return modal.finished.then(([success]) => { + return def.promise.then((success) => { if (success) { // user clicked continue. _clearStorage(); diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index d4b51081f4..ad5fa198a3 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -26,7 +26,6 @@ import { Room } from 'matrix-js-sdk'; import SettingsStore from "../../../settings/SettingsStore"; import Autocompleter from '../../../autocomplete/Autocompleter'; -import {sleep} from "../../../utils/promise"; const COMPOSER_SELECTED = 0; @@ -106,11 +105,13 @@ export default class Autocomplete extends React.Component { autocompleteDelay = 0; } - return new Promise((resolve) => { - this.debounceCompletionsRequest = setTimeout(() => { - resolve(this.processQuery(query, selection)); - }, autocompleteDelay); - }); + const deferred = Promise.defer(); + this.debounceCompletionsRequest = setTimeout(() => { + this.processQuery(query, selection).then(() => { + deferred.resolve(); + }); + }, autocompleteDelay); + return deferred.promise; } processQuery(query, selection) { @@ -196,16 +197,16 @@ export default class Autocomplete extends React.Component { } forceComplete() { - return new Promise((resolve) => { - this.setState({ - forceComplete: true, - hide: false, - }, () => { - this.complete(this.props.query, this.props.selection).then(() => { - resolve(this.countCompletions()); - }); + const done = Promise.defer(); + this.setState({ + forceComplete: true, + hide: false, + }, () => { + this.complete(this.props.query, this.props.selection).then(() => { + done.resolve(this.countCompletions()); }); }); + return done.promise; } onCompletionClicked(selectionOffset: number): boolean { diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 91292b19f9..a086efaa6d 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -178,12 +178,17 @@ module.exports = createReactClass({ }, _optionallySetEmail: function() { + const deferred = Promise.defer(); // Ask for an email otherwise the user has no way to reset their password const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog"); - const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, { + Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, { title: _t('Do you want to set an email address?'), + onFinished: (confirmed) => { + // ignore confirmed, setting an email is optional + deferred.resolve(confirmed); + }, }); - return modal.finished.then(([confirmed]) => confirmed); + return deferred.promise; }, _onExportE2eKeysClicked: function() { diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index e772912e48..99c412a6ab 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -105,22 +105,26 @@ export default async function sendBugReport(bugReportEndpoint, opts) { } function _submitReport(endpoint, body, progressCallback) { - return new Promise((resolve, reject) => { - const req = new XMLHttpRequest(); - req.open("POST", endpoint); - req.timeout = 5 * 60 * 1000; - req.onreadystatechange = function() { - if (req.readyState === XMLHttpRequest.LOADING) { - progressCallback(_t("Waiting for response from server")); - } else if (req.readyState === XMLHttpRequest.DONE) { - // on done - if (req.status < 200 || req.status >= 400) { - reject(new Error(`HTTP ${req.status}`)); - return; - } - resolve(); - } - }; - req.send(body); - }); + const deferred = Promise.defer(); + + const req = new XMLHttpRequest(); + req.open("POST", endpoint); + req.timeout = 5 * 60 * 1000; + req.onreadystatechange = function() { + if (req.readyState === XMLHttpRequest.LOADING) { + progressCallback(_t("Waiting for response from server")); + } else if (req.readyState === XMLHttpRequest.DONE) { + on_done(); + } + }; + req.send(body); + return deferred.promise; + + function on_done() { + if (req.status < 200 || req.status >= 400) { + deferred.reject(new Error(`HTTP ${req.status}`)); + return; + } + deferred.resolve(); + } } From 217dfc3eed0c5f018a986ddc4f1c05a1a31b5960 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:40:38 +0000 Subject: [PATCH 0537/2372] Replace all trivial Promise.defer usages with regular Promises (cherry picked from commit 44401d73b44b6de4daeb91613456d2760d50456d) --- src/ContentMessages.js | 174 +++++++++--------- src/Lifecycle.js | 8 +- src/components/views/rooms/Autocomplete.js | 29 ++- .../views/settings/ChangePassword.js | 9 +- src/rageshake/submit-rageshake.js | 40 ++-- 5 files changed, 122 insertions(+), 138 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 2d58622db8..dab8de2465 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -59,40 +59,38 @@ export class UploadCanceledError extends Error {} * and a thumbnail key. */ function createThumbnail(element, inputWidth, inputHeight, mimeType) { - const deferred = Promise.defer(); + return new Promise((resolve) => { + let targetWidth = inputWidth; + let targetHeight = inputHeight; + if (targetHeight > MAX_HEIGHT) { + targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); + targetHeight = MAX_HEIGHT; + } + if (targetWidth > MAX_WIDTH) { + targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); + targetWidth = MAX_WIDTH; + } - let targetWidth = inputWidth; - let targetHeight = inputHeight; - if (targetHeight > MAX_HEIGHT) { - targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); - targetHeight = MAX_HEIGHT; - } - if (targetWidth > MAX_WIDTH) { - targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); - targetWidth = MAX_WIDTH; - } - - const canvas = document.createElement("canvas"); - canvas.width = targetWidth; - canvas.height = targetHeight; - canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); - canvas.toBlob(function(thumbnail) { - deferred.resolve({ - info: { - thumbnail_info: { - w: targetWidth, - h: targetHeight, - mimetype: thumbnail.type, - size: thumbnail.size, + const canvas = document.createElement("canvas"); + canvas.width = targetWidth; + canvas.height = targetHeight; + canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); + canvas.toBlob(function(thumbnail) { + resolve({ + info: { + thumbnail_info: { + w: targetWidth, + h: targetHeight, + mimetype: thumbnail.type, + size: thumbnail.size, + }, + w: inputWidth, + h: inputHeight, }, - w: inputWidth, - h: inputHeight, - }, - thumbnail: thumbnail, - }); - }, mimeType); - - return deferred.promise; + thumbnail: thumbnail, + }); + }, mimeType); + }); } /** @@ -179,30 +177,29 @@ function infoForImageFile(matrixClient, roomId, imageFile) { * @return {Promise} A promise that resolves with the video image element. */ function loadVideoElement(videoFile) { - const deferred = Promise.defer(); + return new Promise((resolve, reject) => { + // Load the file into an html element + const video = document.createElement("video"); - // Load the file into an html element - const video = document.createElement("video"); + const reader = new FileReader(); - const reader = new FileReader(); - reader.onload = function(e) { - video.src = e.target.result; + reader.onload = function(e) { + video.src = e.target.result; - // Once ready, returns its size - // Wait until we have enough data to thumbnail the first frame. - video.onloadeddata = function() { - deferred.resolve(video); + // Once ready, returns its size + // Wait until we have enough data to thumbnail the first frame. + video.onloadeddata = function() { + resolve(video); + }; + video.onerror = function(e) { + reject(e); + }; }; - video.onerror = function(e) { - deferred.reject(e); + reader.onerror = function(e) { + reject(e); }; - }; - reader.onerror = function(e) { - deferred.reject(e); - }; - reader.readAsDataURL(videoFile); - - return deferred.promise; + reader.readAsDataURL(videoFile); + }); } /** @@ -236,16 +233,16 @@ function infoForVideoFile(matrixClient, roomId, videoFile) { * is read. */ function readFileAsArrayBuffer(file) { - const deferred = Promise.defer(); - const reader = new FileReader(); - reader.onload = function(e) { - deferred.resolve(e.target.result); - }; - reader.onerror = function(e) { - deferred.reject(e); - }; - reader.readAsArrayBuffer(file); - return deferred.promise; + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = function(e) { + resolve(e.target.result); + }; + reader.onerror = function(e) { + reject(e); + }; + reader.readAsArrayBuffer(file); + }); } /** @@ -461,33 +458,34 @@ export default class ContentMessages { content.info.mimetype = file.type; } - const def = Promise.defer(); - if (file.type.indexOf('image/') == 0) { - content.msgtype = 'm.image'; - infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ - extend(content.info, imageInfo); - def.resolve(); - }, (error)=>{ - console.error(error); + const prom = new Promise((resolve) => { + if (file.type.indexOf('image/') == 0) { + content.msgtype = 'm.image'; + infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ + extend(content.info, imageInfo); + resolve(); + }, (error)=>{ + console.error(error); + content.msgtype = 'm.file'; + resolve(); + }); + } else if (file.type.indexOf('audio/') == 0) { + content.msgtype = 'm.audio'; + resolve(); + } else if (file.type.indexOf('video/') == 0) { + content.msgtype = 'm.video'; + infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ + extend(content.info, videoInfo); + resolve(); + }, (error)=>{ + content.msgtype = 'm.file'; + resolve(); + }); + } else { content.msgtype = 'm.file'; - def.resolve(); - }); - } else if (file.type.indexOf('audio/') == 0) { - content.msgtype = 'm.audio'; - def.resolve(); - } else if (file.type.indexOf('video/') == 0) { - content.msgtype = 'm.video'; - infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ - extend(content.info, videoInfo); - def.resolve(); - }, (error)=>{ - content.msgtype = 'm.file'; - def.resolve(); - }); - } else { - content.msgtype = 'm.file'; - def.resolve(); - } + resolve(); + } + }); const upload = { fileName: file.name || 'Attachment', @@ -509,7 +507,7 @@ export default class ContentMessages { dis.dispatch({action: 'upload_progress', upload: upload}); } - return def.promise.then(function() { + return prom.then(function() { // XXX: upload.promise must be the promise that // is returned by uploadFile as it has an abort() // method hacked onto it. diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 13f3abccb1..53a9b7a998 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -312,18 +312,14 @@ async function _restoreFromLocalStorage(opts) { function _handleLoadSessionFailure(e) { console.error("Unable to load session", e); - const def = Promise.defer(); const SessionRestoreErrorDialog = sdk.getComponent('views.dialogs.SessionRestoreErrorDialog'); - Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { + const modal = Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { error: e.message, - onFinished: (success) => { - def.resolve(success); - }, }); - return def.promise.then((success) => { + return modal.finished.then(([success]) => { if (success) { // user clicked continue. _clearStorage(); diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index ad5fa198a3..d4b51081f4 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -26,6 +26,7 @@ import { Room } from 'matrix-js-sdk'; import SettingsStore from "../../../settings/SettingsStore"; import Autocompleter from '../../../autocomplete/Autocompleter'; +import {sleep} from "../../../utils/promise"; const COMPOSER_SELECTED = 0; @@ -105,13 +106,11 @@ export default class Autocomplete extends React.Component { autocompleteDelay = 0; } - const deferred = Promise.defer(); - this.debounceCompletionsRequest = setTimeout(() => { - this.processQuery(query, selection).then(() => { - deferred.resolve(); - }); - }, autocompleteDelay); - return deferred.promise; + return new Promise((resolve) => { + this.debounceCompletionsRequest = setTimeout(() => { + resolve(this.processQuery(query, selection)); + }, autocompleteDelay); + }); } processQuery(query, selection) { @@ -197,16 +196,16 @@ export default class Autocomplete extends React.Component { } forceComplete() { - const done = Promise.defer(); - this.setState({ - forceComplete: true, - hide: false, - }, () => { - this.complete(this.props.query, this.props.selection).then(() => { - done.resolve(this.countCompletions()); + return new Promise((resolve) => { + this.setState({ + forceComplete: true, + hide: false, + }, () => { + this.complete(this.props.query, this.props.selection).then(() => { + resolve(this.countCompletions()); + }); }); }); - return done.promise; } onCompletionClicked(selectionOffset: number): boolean { diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index a086efaa6d..91292b19f9 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -178,17 +178,12 @@ module.exports = createReactClass({ }, _optionallySetEmail: function() { - const deferred = Promise.defer(); // Ask for an email otherwise the user has no way to reset their password const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog"); - Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, { + const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, { title: _t('Do you want to set an email address?'), - onFinished: (confirmed) => { - // ignore confirmed, setting an email is optional - deferred.resolve(confirmed); - }, }); - return deferred.promise; + return modal.finished.then(([confirmed]) => confirmed); }, _onExportE2eKeysClicked: function() { diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index 99c412a6ab..e772912e48 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -105,26 +105,22 @@ export default async function sendBugReport(bugReportEndpoint, opts) { } function _submitReport(endpoint, body, progressCallback) { - const deferred = Promise.defer(); - - const req = new XMLHttpRequest(); - req.open("POST", endpoint); - req.timeout = 5 * 60 * 1000; - req.onreadystatechange = function() { - if (req.readyState === XMLHttpRequest.LOADING) { - progressCallback(_t("Waiting for response from server")); - } else if (req.readyState === XMLHttpRequest.DONE) { - on_done(); - } - }; - req.send(body); - return deferred.promise; - - function on_done() { - if (req.status < 200 || req.status >= 400) { - deferred.reject(new Error(`HTTP ${req.status}`)); - return; - } - deferred.resolve(); - } + return new Promise((resolve, reject) => { + const req = new XMLHttpRequest(); + req.open("POST", endpoint); + req.timeout = 5 * 60 * 1000; + req.onreadystatechange = function() { + if (req.readyState === XMLHttpRequest.LOADING) { + progressCallback(_t("Waiting for response from server")); + } else if (req.readyState === XMLHttpRequest.DONE) { + // on done + if (req.status < 200 || req.status >= 400) { + reject(new Error(`HTTP ${req.status}`)); + return; + } + resolve(); + } + }; + req.send(body); + }); } From 2ea239d1923170ef9d226eeae99dcfc12432dc30 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:45:28 +0000 Subject: [PATCH 0538/2372] Replace rest of defer usages using small shim. Add homebrew promise utils (cherry picked from commit 6850c147393ba7be8b98d97dfc8d7244fa503461) --- src/Modal.js | 3 +- src/components/structures/MatrixChat.js | 5 +-- src/utils/MultiInviter.js | 3 +- src/utils/promise.js | 46 +++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 src/utils/promise.js diff --git a/src/Modal.js b/src/Modal.js index 26c9da8bbb..cb19731f01 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -24,6 +24,7 @@ import sdk from './index'; import dis from './dispatcher'; import { _t } from './languageHandler'; import Promise from "bluebird"; +import {defer} from "./utils/promise"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; @@ -202,7 +203,7 @@ class ModalManager { } _getCloseFn(modal, props) { - const deferred = Promise.defer(); + const deferred = defer(); return [(...args) => { deferred.resolve(args); if (props && props.onFinished) props.onFinished.apply(null, args); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index da67416400..d12eba88f7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -60,6 +60,7 @@ import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; import DMRoomMap from '../../utils/DMRoomMap'; import { countRoomsWithNotif } from '../../RoomNotifs'; import { setTheme } from "../../theme"; +import {defer} from "../../utils/promise"; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -236,7 +237,7 @@ export default createReactClass({ // Used by _viewRoom before getting state from sync this.firstSyncComplete = false; - this.firstSyncPromise = Promise.defer(); + this.firstSyncPromise = defer(); if (this.props.config.sync_timeline_limit) { MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; @@ -1261,7 +1262,7 @@ export default createReactClass({ // since we're about to start the client and therefore about // to do the first sync this.firstSyncComplete = false; - this.firstSyncPromise = Promise.defer(); + this.firstSyncPromise = defer(); const cli = MatrixClientPeg.get(); const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index e8995b46d7..de5c2e7610 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -24,6 +24,7 @@ import {_t} from "../languageHandler"; import sdk from "../index"; import Modal from "../Modal"; import SettingsStore from "../settings/SettingsStore"; +import {defer} from "./promise"; /** * Invites multiple addresses to a room or group, handling rate limiting from the server @@ -71,7 +72,7 @@ export default class MultiInviter { }; } } - this.deferred = Promise.defer(); + this.deferred = defer(); this._inviteMore(0); return this.deferred.promise; diff --git a/src/utils/promise.js b/src/utils/promise.js new file mode 100644 index 0000000000..dd10f7fdd7 --- /dev/null +++ b/src/utils/promise.js @@ -0,0 +1,46 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// @flow + +// Returns a promise which resolves with a given value after the given number of ms +export const sleep = (ms: number, value: any): Promise => new Promise((resolve => { setTimeout(resolve, ms, value); })); + +// Returns a promise which resolves when the input promise resolves with its value +// or when the timeout of ms is reached with the value of given timeoutValue +export async function timeout(promise: Promise, timeoutValue: any, ms: number): Promise { + const timeoutPromise = new Promise((resolve) => { + const timeoutId = setTimeout(resolve, ms, timeoutValue); + promise.then(() => { + clearTimeout(timeoutId); + }); + }); + + return Promise.race([promise, timeoutPromise]); +} + +// Returns a Deferred +export function defer(): {resolve: () => {}, reject: () => {}, promise: Promise} { + let resolve; + let reject; + + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + + return {resolve, reject, promise}; +} From 2b34cf4362ab03b0409da6086199ef2eab54dee7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 11:46:58 +0000 Subject: [PATCH 0539/2372] Replace Promise.delay with promise utils sleep (cherry picked from commit 0a21957b2cac0cc2d0169f428187bc2e468251a9) --- src/components/structures/GroupView.js | 9 +++++---- .../views/context_menus/RoomTileContextMenu.js | 7 ++++--- src/components/views/dialogs/AddressPickerDialog.js | 3 ++- .../views/settings/tabs/user/SecurityUserSettingsTab.js | 3 ++- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 4d8f47003c..4056557a7c 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -38,6 +38,7 @@ import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks"; import {Group} from "matrix-js-sdk"; +import {sleep} from "../../utils/promise"; const LONG_DESC_PLACEHOLDER = _td( `

        HTML for your community's page

        @@ -692,7 +693,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.acceptGroupInvite(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -711,7 +712,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.leaveGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -735,7 +736,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.joinGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync @@ -787,7 +788,7 @@ export default createReactClass({ // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the // spinner disappearing after we have fetched new group data. - await Promise.delay(500); + await sleep(500); GroupStore.leaveGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 9bb573026f..541daef27f 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -32,6 +32,7 @@ import * as RoomNotifs from '../../../RoomNotifs'; import Modal from '../../../Modal'; import RoomListActions from '../../../actions/RoomListActions'; import RoomViewStore from '../../../stores/RoomViewStore'; +import {sleep} from "../../../utils/promise"; module.exports = createReactClass({ displayName: 'RoomTileContextMenu', @@ -62,7 +63,7 @@ module.exports = createReactClass({ _toggleTag: function(tagNameOn, tagNameOff) { if (!MatrixClientPeg.get().isGuest()) { - Promise.delay(500).then(() => { + sleep(500).then(() => { dis.dispatch(RoomListActions.tagRoom( MatrixClientPeg.get(), this.props.room, @@ -119,7 +120,7 @@ module.exports = createReactClass({ Rooms.guessAndSetDMRoom( this.props.room, newIsDirectMessage, - ).delay(500).finally(() => { + ).then(sleep(500)).finally(() => { // Close the context menu if (this.props.onFinished) { this.props.onFinished(); @@ -193,7 +194,7 @@ module.exports = createReactClass({ RoomNotifs.setRoomNotifsState(roomId, newState).done(() => { // delay slightly so that the user can see their state change // before closing the menu - return Promise.delay(500).then(() => { + return sleep(500).then(() => { if (this._unmounted) return; // Close the context menu if (this.props.onFinished) { diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index fb779fa96f..dc61f23956 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -32,6 +32,7 @@ import * as Email from '../../../email'; import IdentityAuthClient from '../../../IdentityAuthClient'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils'; import { abbreviateUrl } from '../../../utils/UrlUtils'; +import {sleep} from "../../../utils/promise"; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -533,7 +534,7 @@ module.exports = createReactClass({ }; // wait a bit to let the user finish typing - await Promise.delay(500); + await sleep(500); if (cancelled) return null; try { diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index e619791b01..222af48fa1 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -25,6 +25,7 @@ import Analytics from "../../../../../Analytics"; import Promise from "bluebird"; import Modal from "../../../../../Modal"; import sdk from "../../../../.."; +import {sleep} from "../../../../../utils/promise"; export class IgnoredUser extends React.Component { static propTypes = { @@ -129,7 +130,7 @@ export default class SecurityUserSettingsTab extends React.Component { if (e.errcode === "M_LIMIT_EXCEEDED") { // Add a delay between each invite change in order to avoid rate // limiting by the server. - await Promise.delay(e.retry_after_ms || 2500); + await sleep(e.retry_after_ms || 2500); // Redo last action i--; From f5d467b3917849cb1f17445b75fe9ea1ba38098e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 12:26:07 +0000 Subject: [PATCH 0540/2372] delint --- src/components/views/context_menus/RoomTileContextMenu.js | 1 - src/components/views/dialogs/AddressPickerDialog.js | 1 - .../views/settings/tabs/user/SecurityUserSettingsTab.js | 1 - 3 files changed, 3 deletions(-) diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 541daef27f..fb056ee47f 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -17,7 +17,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index dc61f23956..24d8b96e0c 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -25,7 +25,6 @@ import { _t, _td } from '../../../languageHandler'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import dis from '../../../dispatcher'; -import Promise from 'bluebird'; import { addressTypes, getAddressType } from '../../../UserAddress.js'; import GroupStore from '../../../stores/GroupStore'; import * as Email from '../../../email'; diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index 222af48fa1..0732bcf926 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -22,7 +22,6 @@ import MatrixClientPeg from "../../../../../MatrixClientPeg"; import * as FormattingUtils from "../../../../../utils/FormattingUtils"; import AccessibleButton from "../../../elements/AccessibleButton"; import Analytics from "../../../../../Analytics"; -import Promise from "bluebird"; import Modal from "../../../../../Modal"; import sdk from "../../../../.."; import {sleep} from "../../../../../utils/promise"; From cfdcf45ac6eb019242b5d969ce8018fae195caec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 12 Nov 2019 13:29:07 +0100 Subject: [PATCH 0541/2372] MatrixChat: Move the event indexing logic into separate modules. --- src/EventIndexPeg.js | 74 +++++ src/EventIndexing.js | 404 ++++++++++++++++++++++++ src/Lifecycle.js | 2 + src/MatrixClientPeg.js | 4 +- src/components/structures/MatrixChat.js | 371 ++-------------------- 5 files changed, 499 insertions(+), 356 deletions(-) create mode 100644 src/EventIndexPeg.js create mode 100644 src/EventIndexing.js diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js new file mode 100644 index 0000000000..794450e4b7 --- /dev/null +++ b/src/EventIndexPeg.js @@ -0,0 +1,74 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* + * Holds the current Platform object used by the code to do anything + * specific to the platform we're running on (eg. web, electron) + * Platforms are provided by the app layer. + * This allows the app layer to set a Platform without necessarily + * having to have a MatrixChat object + */ + +import PlatformPeg from "./PlatformPeg"; +import EventIndex from "./EventIndexing"; +import MatrixClientPeg from "./MatrixClientPeg"; + +class EventIndexPeg { + constructor() { + this.index = null; + } + + /** + * Returns the current Event index object for the application. Can be null + * if the platform doesn't support event indexing. + */ + get() { + return this.index; + } + + /** Create a new EventIndex and initialize it if the platform supports it. + * Returns true if an EventIndex was successfully initialized, false + * otherwise. + */ + async init() { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return false; + + let index = new EventIndex(); + + const userId = MatrixClientPeg.get().getUserId(); + // TODO log errors here and return false if it errors out. + await index.init(userId); + this.index = index; + + return true + } + + async stop() { + if (this.index == null) return; + index.stopCrawler(); + } + + async deleteEventIndex() { + if (this.index == null) return; + index.deleteEventIndex(); + } +} + +if (!global.mxEventIndexPeg) { + global.mxEventIndexPeg = new EventIndexPeg(); +} +module.exports = global.mxEventIndexPeg; diff --git a/src/EventIndexing.js b/src/EventIndexing.js new file mode 100644 index 0000000000..21ee8f3da6 --- /dev/null +++ b/src/EventIndexing.js @@ -0,0 +1,404 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import PlatformPeg from "./PlatformPeg"; +import MatrixClientPeg from "./MatrixClientPeg"; + +/** + * Event indexing class that wraps the platform specific event indexing. + */ +export default class EventIndexer { + constructor() { + this.crawlerChekpoints = []; + // The time that the crawler will wait between /rooms/{room_id}/messages + // requests + this._crawler_timeout = 3000; + this._crawlerRef = null; + this.liveEventsForIndex = new Set(); + } + + async init(userId) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return false; + platform.initEventIndex(userId); + } + + async onSync(state, prevState, data) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + if (prevState === null && state === "PREPARED") { + // Load our stored checkpoints, if any. + this.crawlerChekpoints = await platform.loadCheckpoints(); + console.log("Seshat: Loaded checkpoints", + this.crawlerChekpoints); + return; + } + + if (prevState === "PREPARED" && state === "SYNCING") { + const addInitialCheckpoints = async () => { + const client = MatrixClientPeg.get(); + const rooms = client.getRooms(); + + const isRoomEncrypted = (room) => { + return client.isRoomEncrypted(room.roomId); + }; + + // We only care to crawl the encrypted rooms, non-encrytped + // rooms can use the search provided by the Homeserver. + const encryptedRooms = rooms.filter(isRoomEncrypted); + + console.log("Seshat: Adding initial crawler checkpoints"); + + // Gather the prev_batch tokens and create checkpoints for + // our message crawler. + await Promise.all(encryptedRooms.map(async (room) => { + const timeline = room.getLiveTimeline(); + const token = timeline.getPaginationToken("b"); + + console.log("Seshat: Got token for indexer", + room.roomId, token); + + const backCheckpoint = { + roomId: room.roomId, + token: token, + direction: "b", + }; + + const forwardCheckpoint = { + roomId: room.roomId, + token: token, + direction: "f", + }; + + await platform.addCrawlerCheckpoint(backCheckpoint); + await platform.addCrawlerCheckpoint(forwardCheckpoint); + this.crawlerChekpoints.push(backCheckpoint); + this.crawlerChekpoints.push(forwardCheckpoint); + })); + }; + + // If our indexer is empty we're most likely running Riot the + // first time with indexing support or running it with an + // initial sync. Add checkpoints to crawl our encrypted rooms. + const eventIndexWasEmpty = await platform.isEventIndexEmpty(); + if (eventIndexWasEmpty) await addInitialCheckpoints(); + + // Start our crawler. + this.startCrawler(); + return; + } + + if (prevState === "SYNCING" && state === "SYNCING") { + // A sync was done, presumably we queued up some live events, + // commit them now. + console.log("Seshat: Committing events"); + await platform.commitLiveEvents(); + return; + } + } + + async onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + // We only index encrypted rooms locally. + if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; + + // If it isn't a live event or if it's redacted there's nothing to + // do. + if (toStartOfTimeline || !data || !data.liveEvent + || ev.isRedacted()) { + return; + } + + // If the event is not yet decrypted mark it for the + // Event.decrypted callback. + if (ev.isBeingDecrypted()) { + const eventId = ev.getId(); + this.liveEventsForIndex.add(eventId); + } else { + // If the event is decrypted or is unencrypted add it to the + // index now. + await this.addLiveEventToIndex(ev); + } + } + + async onEventDecrypted(ev, err) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + const eventId = ev.getId(); + + // If the event isn't in our live event set, ignore it. + if (!this.liveEventsForIndex.delete(eventId)) return; + if (err) return; + await this.addLiveEventToIndex(ev); + } + + async addLiveEventToIndex(ev) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + + if (["m.room.message", "m.room.name", "m.room.topic"] + .indexOf(ev.getType()) == -1) { + return; + } + + const e = ev.toJSON().decrypted; + const profile = { + displayname: ev.sender.rawDisplayName, + avatar_url: ev.sender.getMxcAvatarUrl(), + }; + + platform.addEventToIndex(e, profile); + } + + async crawlerFunc(handle) { + // TODO either put this in a better place or find a library provided + // method that does this. + const sleep = async (ms) => { + return new Promise(resolve => setTimeout(resolve, ms)); + }; + + let cancelled = false; + + console.log("Seshat: Started crawler function"); + + const client = MatrixClientPeg.get(); + const platform = PlatformPeg.get(); + + handle.cancel = () => { + cancelled = true; + }; + + while (!cancelled) { + // This is a low priority task and we don't want to spam our + // Homeserver with /messages requests so we set a hefty timeout + // here. + await sleep(this._crawler_timeout); + + console.log("Seshat: Running the crawler loop."); + + if (cancelled) { + console.log("Seshat: Cancelling the crawler."); + break; + } + + const checkpoint = this.crawlerChekpoints.shift(); + + /// There is no checkpoint available currently, one may appear if + // a sync with limited room timelines happens, so go back to sleep. + if (checkpoint === undefined) { + continue; + } + + console.log("Seshat: crawling using checkpoint", checkpoint); + + // We have a checkpoint, let us fetch some messages, again, very + // conservatively to not bother our Homeserver too much. + const eventMapper = client.getEventMapper(); + // TODO we need to ensure to use member lazy loading with this + // request so we get the correct profiles. + let res; + + try { + res = await client._createMessagesRequest( + checkpoint.roomId, checkpoint.token, 100, + checkpoint.direction); + } catch (e) { + console.log("Seshat: Error crawling events:", e); + this.crawlerChekpoints.push(checkpoint); + continue + } + + if (res.chunk.length === 0) { + console.log("Seshat: Done with the checkpoint", checkpoint); + // We got to the start/end of our timeline, lets just + // delete our checkpoint and go back to sleep. + await platform.removeCrawlerCheckpoint(checkpoint); + continue; + } + + // Convert the plain JSON events into Matrix events so they get + // decrypted if necessary. + const matrixEvents = res.chunk.map(eventMapper); + let stateEvents = []; + if (res.state !== undefined) { + stateEvents = res.state.map(eventMapper); + } + + const profiles = {}; + + stateEvents.forEach(ev => { + if (ev.event.content && + ev.event.content.membership === "join") { + profiles[ev.event.sender] = { + displayname: ev.event.content.displayname, + avatar_url: ev.event.content.avatar_url, + }; + } + }); + + const decryptionPromises = []; + + matrixEvents.forEach(ev => { + if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { + // TODO the decryption promise is a private property, this + // should either be made public or we should convert the + // event that gets fired when decryption is done into a + // promise using the once event emitter method: + // https://nodejs.org/api/events.html#events_events_once_emitter_name + decryptionPromises.push(ev._decryptionPromise); + } + }); + + // Let us wait for all the events to get decrypted. + await Promise.all(decryptionPromises); + + // We filter out events for which decryption failed, are redacted + // or aren't of a type that we know how to index. + const isValidEvent = (value) => { + return ([ + "m.room.message", + "m.room.name", + "m.room.topic", + ].indexOf(value.getType()) >= 0 + && !value.isRedacted() && !value.isDecryptionFailure() + ); + // TODO do we need to check if the event has all the valid + // attributes? + }; + + // TODO if there ar no events at this point we're missing a lot + // decryption keys, do we wan't to retry this checkpoint at a later + // stage? + const filteredEvents = matrixEvents.filter(isValidEvent); + + // Let us convert the events back into a format that Seshat can + // consume. + const events = filteredEvents.map((ev) => { + const jsonEvent = ev.toJSON(); + + let e; + if (ev.isEncrypted()) e = jsonEvent.decrypted; + else e = jsonEvent; + + let profile = {}; + if (e.sender in profiles) profile = profiles[e.sender]; + const object = { + event: e, + profile: profile, + }; + return object; + }); + + // Create a new checkpoint so we can continue crawling the room for + // messages. + const newCheckpoint = { + roomId: checkpoint.roomId, + token: res.end, + fullCrawl: checkpoint.fullCrawl, + direction: checkpoint.direction, + }; + + console.log( + "Seshat: Crawled room", + client.getRoom(checkpoint.roomId).name, + "and fetched", events.length, "events.", + ); + + try { + const eventsAlreadyAdded = await platform.addHistoricEvents( + events, newCheckpoint, checkpoint); + // If all events were already indexed we assume that we catched + // up with our index and don't need to crawl the room further. + // Let us delete the checkpoint in that case, otherwise push + // the new checkpoint to be used by the crawler. + if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { + console.log("Seshat: Checkpoint had already all events", + "added, stopping the crawl", checkpoint); + await platform.removeCrawlerCheckpoint(newCheckpoint); + } else { + this.crawlerChekpoints.push(newCheckpoint); + } + } catch (e) { + console.log("Seshat: Error durring a crawl", e); + // An error occured, put the checkpoint back so we + // can retry. + this.crawlerChekpoints.push(checkpoint); + } + } + + console.log("Seshat: Stopping crawler function"); + } + + async addCheckpointForLimitedRoom(roomId) { + const platform = PlatformPeg.get(); + if (!platform.supportsEventIndexing()) return; + if (!MatrixClientPeg.get().isRoomEncrypted(roomId)) return; + + const client = MatrixClientPeg.get(); + const room = client.getRoom(roomId); + + if (room === null) return; + + const timeline = room.getLiveTimeline(); + const token = timeline.getPaginationToken("b"); + + const backwardsCheckpoint = { + roomId: room.roomId, + token: token, + fullCrawl: false, + direction: "b", + }; + + const forwardsCheckpoint = { + roomId: room.roomId, + token: token, + fullCrawl: false, + direction: "f", + }; + + console.log("Seshat: Added checkpoint because of a limited timeline", + backwardsCheckpoint, forwardsCheckpoint); + + await platform.addCrawlerCheckpoint(backwardsCheckpoint); + await platform.addCrawlerCheckpoint(forwardsCheckpoint); + + this.crawlerChekpoints.push(backwardsCheckpoint); + this.crawlerChekpoints.push(forwardsCheckpoint); + } + + async deleteEventIndex() { + if (platform.supportsEventIndexing()) { + console.log("Seshat: Deleting event index."); + this.crawlerRef.cancel(); + await platform.deleteEventIndex(); + } + } + + startCrawler() { + const crawlerHandle = {}; + this.crawlerFunc(crawlerHandle); + this.crawlerRef = crawlerHandle; + } + + stopCrawler() { + this._crawlerRef.cancel(); + this._crawlerRef = null; + } +} diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 7490c5d464..0b44f2ed84 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -20,6 +20,7 @@ import Promise from 'bluebird'; import Matrix from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; +import EventIndexPeg from './EventIndexPeg'; import createMatrixClient from './utils/createMatrixClient'; import Analytics from './Analytics'; import Notifier from './Notifier'; @@ -587,6 +588,7 @@ async function startMatrixClient(startSyncing=true) { if (startSyncing) { await MatrixClientPeg.start(); + await EventIndexPeg.init(); } else { console.warn("Caller requested only auxiliary services be started"); await MatrixClientPeg.assign(); diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 5c5ee6e4ec..6c5b465bb0 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -31,6 +31,7 @@ import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientB import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; import PlatformPeg from "./PlatformPeg"; +import EventIndexPeg from "./EventIndexPeg"; interface MatrixClientCreds { homeserverUrl: string, @@ -223,9 +224,6 @@ class MatrixClientPeg { this.matrixClient = createMatrixClient(opts); - const platform = PlatformPeg.get(); - if (platform.supportsEventIndexing()) platform.initEventIndex(creds.userId); - // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. this.matrixClient.setMaxListeners(500); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 402790df98..d006247151 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -31,6 +31,7 @@ import Analytics from "../../Analytics"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; import MatrixClientPeg from "../../MatrixClientPeg"; import PlatformPeg from "../../PlatformPeg"; +import EventIndexPeg from "../../EventIndexPeg"; import SdkConfig from "../../SdkConfig"; import * as RoomListSorter from "../../RoomListSorter"; import dis from "../../dispatcher"; @@ -1224,12 +1225,6 @@ export default createReactClass({ _onLoggedOut: async function() { const platform = PlatformPeg.get(); - if (platform.supportsEventIndexing()) { - console.log("Seshat: Deleting event index."); - this.crawlerRef.cancel(); - await platform.deleteEventIndex(); - } - this.notifyNewScreen('login'); this.setStateForNewView({ view: VIEWS.LOGIN, @@ -1270,8 +1265,6 @@ export default createReactClass({ // to do the first sync this.firstSyncComplete = false; this.firstSyncPromise = Promise.defer(); - this.crawlerChekpoints = []; - this.liveEventsForIndex = new Set(); const cli = MatrixClientPeg.get(); const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); @@ -1284,7 +1277,10 @@ export default createReactClass({ cli.setCanResetTimelineCallback(async function(roomId) { console.log("Request to reset timeline in room ", roomId, " viewing:", self.state.currentRoomId); // TODO is there a better place to plug this in - await self.addCheckpointForLimitedRoom(roomId); + const eventIndex = EventIndexPeg.get(); + if (eventIndex !== null) { + await eventIndex.addCheckpointForLimitedRoom(roomId); + } if (roomId !== self.state.currentRoomId) { // It is safe to remove events from rooms we are not viewing. @@ -1301,80 +1297,21 @@ export default createReactClass({ }); cli.on('sync', async (state, prevState, data) => { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; + const eventIndex = EventIndexPeg.get(); + if (eventIndex === null) return; + await eventIndex.onSync(state, prevState, data); + }); - if (prevState === null && state === "PREPARED") { - /// Load our stored checkpoints, if any. - self.crawlerChekpoints = await platform.loadCheckpoints(); - console.log("Seshat: Loaded checkpoints", - self.crawlerChekpoints); - return; - } + cli.on("Room.timeline", async (ev, room, toStartOfTimeline, removed, data) => { + const eventIndex = EventIndexPeg.get(); + if (eventIndex === null) return; + await eventIndex.onRoomTimeline(ev, room, toStartOfTimeline, removed, data); + }); - if (prevState === "PREPARED" && state === "SYNCING") { - const addInitialCheckpoints = async () => { - const client = MatrixClientPeg.get(); - const rooms = client.getRooms(); - - const isRoomEncrypted = (room) => { - return client.isRoomEncrypted(room.roomId); - }; - - // We only care to crawl the encrypted rooms, non-encrytped - // rooms can use the search provided by the Homeserver. - const encryptedRooms = rooms.filter(isRoomEncrypted); - - console.log("Seshat: Adding initial crawler checkpoints"); - - // Gather the prev_batch tokens and create checkpoints for - // our message crawler. - await Promise.all(encryptedRooms.map(async (room) => { - const timeline = room.getLiveTimeline(); - const token = timeline.getPaginationToken("b"); - - console.log("Seshat: Got token for indexer", - room.roomId, token); - - const backCheckpoint = { - roomId: room.roomId, - token: token, - direction: "b", - }; - - const forwardCheckpoint = { - roomId: room.roomId, - token: token, - direction: "f", - }; - - await platform.addCrawlerCheckpoint(backCheckpoint); - await platform.addCrawlerCheckpoint(forwardCheckpoint); - self.crawlerChekpoints.push(backCheckpoint); - self.crawlerChekpoints.push(forwardCheckpoint); - })); - }; - - // If our indexer is empty we're most likely running Riot the - // first time with indexing support or running it with an - // initial sync. Add checkpoints to crawl our encrypted rooms. - const eventIndexWasEmpty = await platform.isEventIndexEmpty(); - if (eventIndexWasEmpty) await addInitialCheckpoints(); - - // Start our crawler. - const crawlerHandle = {}; - self.crawlerFunc(crawlerHandle); - self.crawlerRef = crawlerHandle; - return; - } - - if (prevState === "SYNCING" && state === "SYNCING") { - // A sync was done, presumably we queued up some live events, - // commit them now. - console.log("Seshat: Committing events"); - await platform.commitLiveEvents(); - return; - } + cli.on("Event.decrypted", async (ev, err) => { + const eventIndex = EventIndexPeg.get(); + if (eventIndex === null) return; + await eventIndex.onEventDecrypted(ev, err); }); cli.on('sync', function(state, prevState, data) { @@ -1459,44 +1396,6 @@ export default createReactClass({ }, null, true); }); - cli.on("Room.timeline", async (ev, room, toStartOfTimeline, removed, data) => { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; - - // We only index encrypted rooms locally. - if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; - - // If it isn't a live event or if it's redacted there's nothing to - // do. - if (toStartOfTimeline || !data || !data.liveEvent - || ev.isRedacted()) { - return; - } - - // If the event is not yet decrypted mark it for the - // Event.decrypted callback. - if (ev.isBeingDecrypted()) { - const eventId = ev.getId(); - self.liveEventsForIndex.add(eventId); - } else { - // If the event is decrypted or is unencrypted add it to the - // index now. - await self.addLiveEventToIndex(ev); - } - }); - - cli.on("Event.decrypted", async (ev, err) => { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; - - const eventId = ev.getId(); - - // If the event isn't in our live event set, ignore it. - if (!self.liveEventsForIndex.delete(eventId)) return; - if (err) return; - await self.addLiveEventToIndex(ev); - }); - cli.on("accountData", function(ev) { if (ev.getType() === 'im.vector.web.settings') { if (ev.getContent() && ev.getContent().theme) { @@ -2058,238 +1957,4 @@ export default createReactClass({ {view} ; }, - - async addLiveEventToIndex(ev) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; - - if (["m.room.message", "m.room.name", "m.room.topic"] - .indexOf(ev.getType()) == -1) { - return; - } - - const e = ev.toJSON().decrypted; - const profile = { - displayname: ev.sender.rawDisplayName, - avatar_url: ev.sender.getMxcAvatarUrl(), - }; - - platform.addEventToIndex(e, profile); - }, - - async crawlerFunc(handle) { - // TODO either put this in a better place or find a library provided - // method that does this. - const sleep = async (ms) => { - return new Promise(resolve => setTimeout(resolve, ms)); - }; - - let cancelled = false; - - console.log("Seshat: Started crawler function"); - - const client = MatrixClientPeg.get(); - const platform = PlatformPeg.get(); - - handle.cancel = () => { - cancelled = true; - }; - - while (!cancelled) { - // This is a low priority task and we don't want to spam our - // Homeserver with /messages requests so we set a hefty 3s timeout - // here. - await sleep(3000); - - console.log("Seshat: Running the crawler loop."); - - if (cancelled) { - console.log("Seshat: Cancelling the crawler."); - break; - } - - const checkpoint = this.crawlerChekpoints.shift(); - - /// There is no checkpoint available currently, one may appear if - // a sync with limited room timelines happens, so go back to sleep. - if (checkpoint === undefined) { - continue; - } - - console.log("Seshat: crawling using checkpoint", checkpoint); - - // We have a checkpoint, let us fetch some messages, again, very - // conservatively to not bother our Homeserver too much. - const eventMapper = client.getEventMapper(); - // TODO we need to ensure to use member lazy loading with this - // request so we get the correct profiles. - let res; - - try { - res = await client._createMessagesRequest( - checkpoint.roomId, checkpoint.token, 100, - checkpoint.direction); - } catch (e) { - console.log("Seshat: Error crawling events:", e); - this.crawlerChekpoints.push(checkpoint); - continue - } - - if (res.chunk.length === 0) { - console.log("Seshat: Done with the checkpoint", checkpoint); - // We got to the start/end of our timeline, lets just - // delete our checkpoint and go back to sleep. - await platform.removeCrawlerCheckpoint(checkpoint); - continue; - } - - // Convert the plain JSON events into Matrix events so they get - // decrypted if necessary. - const matrixEvents = res.chunk.map(eventMapper); - let stateEvents = []; - if (res.state !== undefined) { - stateEvents = res.state.map(eventMapper); - } - - const profiles = {}; - - stateEvents.forEach(ev => { - if (ev.event.content && - ev.event.content.membership === "join") { - profiles[ev.event.sender] = { - displayname: ev.event.content.displayname, - avatar_url: ev.event.content.avatar_url, - }; - } - }); - - const decryptionPromises = []; - - matrixEvents.forEach(ev => { - if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { - // TODO the decryption promise is a private property, this - // should either be made public or we should convert the - // event that gets fired when decryption is done into a - // promise using the once event emitter method: - // https://nodejs.org/api/events.html#events_events_once_emitter_name - decryptionPromises.push(ev._decryptionPromise); - } - }); - - // Let us wait for all the events to get decrypted. - await Promise.all(decryptionPromises); - - // We filter out events for which decryption failed, are redacted - // or aren't of a type that we know how to index. - const isValidEvent = (value) => { - return ([ - "m.room.message", - "m.room.name", - "m.room.topic", - ].indexOf(value.getType()) >= 0 - && !value.isRedacted() && !value.isDecryptionFailure() - ); - // TODO do we need to check if the event has all the valid - // attributes? - }; - - // TODO if there ar no events at this point we're missing a lot - // decryption keys, do we wan't to retry this checkpoint at a later - // stage? - const filteredEvents = matrixEvents.filter(isValidEvent); - - // Let us convert the events back into a format that Seshat can - // consume. - const events = filteredEvents.map((ev) => { - const jsonEvent = ev.toJSON(); - - let e; - if (ev.isEncrypted()) e = jsonEvent.decrypted; - else e = jsonEvent; - - let profile = {}; - if (e.sender in profiles) profile = profiles[e.sender]; - const object = { - event: e, - profile: profile, - }; - return object; - }); - - // Create a new checkpoint so we can continue crawling the room for - // messages. - const newCheckpoint = { - roomId: checkpoint.roomId, - token: res.end, - fullCrawl: checkpoint.fullCrawl, - direction: checkpoint.direction, - }; - - console.log( - "Seshat: Crawled room", - client.getRoom(checkpoint.roomId).name, - "and fetched", events.length, "events.", - ); - - try { - const eventsAlreadyAdded = await platform.addHistoricEvents( - events, newCheckpoint, checkpoint); - // If all events were already indexed we assume that we catched - // up with our index and don't need to crawl the room further. - // Let us delete the checkpoint in that case, otherwise push - // the new checkpoint to be used by the crawler. - if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { - console.log("Seshat: Checkpoint had already all events", - "added, stopping the crawl", checkpoint); - await platform.removeCrawlerCheckpoint(newCheckpoint); - } else { - this.crawlerChekpoints.push(newCheckpoint); - } - } catch (e) { - console.log("Seshat: Error durring a crawl", e); - // An error occured, put the checkpoint back so we - // can retry. - this.crawlerChekpoints.push(checkpoint); - } - } - - console.log("Seshat: Stopping crawler function"); - }, - - async addCheckpointForLimitedRoom(roomId) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; - if (!MatrixClientPeg.get().isRoomEncrypted(roomId)) return; - - const client = MatrixClientPeg.get(); - const room = client.getRoom(roomId); - - if (room === null) return; - - const timeline = room.getLiveTimeline(); - const token = timeline.getPaginationToken("b"); - - const backwardsCheckpoint = { - roomId: room.roomId, - token: token, - fullCrawl: false, - direction: "b", - }; - - const forwardsCheckpoint = { - roomId: room.roomId, - token: token, - fullCrawl: false, - direction: "f", - }; - - console.log("Seshat: Added checkpoint because of a limited timeline", - backwardsCheckpoint, forwardsCheckpoint); - - await platform.addCrawlerCheckpoint(backwardsCheckpoint); - await platform.addCrawlerCheckpoint(forwardsCheckpoint); - - this.crawlerChekpoints.push(backwardsCheckpoint); - this.crawlerChekpoints.push(forwardsCheckpoint); - }, }); From c3df2f941dccf5e135ac48408fccc25cf3c5da30 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 12:30:05 +0000 Subject: [PATCH 0542/2372] attach promise utils atop bluebird --- src/utils/promise.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils/promise.js b/src/utils/promise.js index dd10f7fdd7..f7a2e7c3e7 100644 --- a/src/utils/promise.js +++ b/src/utils/promise.js @@ -14,6 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ +// This is only here to allow access to methods like done for the time being +import Promise from "bluebird"; + // @flow // Returns a promise which resolves with a given value after the given number of ms From b73d0ca92acda404f3faf3afe1e0b661662972a6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Nov 2019 12:37:47 +0000 Subject: [PATCH 0543/2372] delint and run i18n --- src/components/structures/ContextualMenu.js | 1 - src/components/views/context_menus/RoomTileContextMenu.js | 2 -- src/components/views/context_menus/TopLeftMenu.js | 1 - src/i18n/strings/en_EN.json | 2 +- 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index c172daf991..dcf670f01d 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -22,7 +22,6 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import {focusCapturedRef} from "../../utils/Accessibility"; import {Key, KeyCode} from "../../Keyboard"; -import {_t} from "../../languageHandler"; import sdk from "../../index"; // Shamelessly ripped off Modal.js. There's probably a better way diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 6b1149cca1..e00e3dcdbb 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -318,8 +318,6 @@ module.exports = createReactClass({ return null; } - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - let leaveClickHandler = null; let leaveText = null; diff --git a/src/components/views/context_menus/TopLeftMenu.js b/src/components/views/context_menus/TopLeftMenu.js index d5b6817b00..2232090698 100644 --- a/src/components/views/context_menus/TopLeftMenu.js +++ b/src/components/views/context_menus/TopLeftMenu.js @@ -24,7 +24,6 @@ import Modal from "../../../Modal"; import SdkConfig from '../../../SdkConfig'; import { getHostingLink } from '../../../utils/HostingLink'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import sdk from "../../../index"; import {MenuItem} from "../../structures/ContextualMenu"; export class TopLeftMenu extends React.Component { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 75be4205df..4f73f4d2d4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1054,11 +1054,11 @@ "Yesterday": "Yesterday", "View Source": "View Source", "Error decrypting audio": "Error decrypting audio", + "Options": "Options", "React": "React", "Reply": "Reply", "Edit": "Edit", "Message Actions": "Message Actions", - "Options": "Options", "Attachment": "Attachment", "Error decrypting attachment": "Error decrypting attachment", "Decrypt %(text)s": "Decrypt %(text)s", From 3f2b77189e31c0cb3617d78105987190f10502a9 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 12 Nov 2019 13:29:01 +0000 Subject: [PATCH 0544/2372] Simplify dispatch blocks --- src/stores/RoomViewStore.js | 75 +++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 41 deletions(-) diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index e860ed8b24..6a405124f4 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -179,50 +179,43 @@ class RoomViewStore extends Store { } else if (payload.room_alias) { // Try the room alias to room ID navigation cache first to avoid // blocking room navigation on the homeserver. - const roomId = getCachedRoomIDForAlias(payload.room_alias); - if (roomId) { - dis.dispatch({ - action: 'view_room', - room_id: roomId, - event_id: payload.event_id, - highlighted: payload.highlighted, - room_alias: payload.room_alias, - auto_join: payload.auto_join, - oob_data: payload.oob_data, + let roomId = getCachedRoomIDForAlias(payload.room_alias); + if (!roomId) { + // Room alias cache miss, so let's ask the homeserver. Resolve the alias + // and then do a second dispatch with the room ID acquired. + this._setState({ + roomId: null, + initialEventId: null, + initialEventPixelOffset: null, + isInitialEventHighlighted: null, + roomAlias: payload.room_alias, + roomLoading: true, + roomLoadError: null, }); - return; + try { + const result = await MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias); + storeRoomAliasInCache(payload.room_alias, result.room_id); + roomId = result.room_id; + } catch (err) { + dis.dispatch({ + action: 'view_room_error', + room_id: null, + room_alias: payload.room_alias, + err, + }); + return; + } } - // Room alias cache miss, so let's ask the homeserver. - // Resolve the alias and then do a second dispatch with the room ID acquired - this._setState({ - roomId: null, - initialEventId: null, - initialEventPixelOffset: null, - isInitialEventHighlighted: null, - roomAlias: payload.room_alias, - roomLoading: true, - roomLoadError: null, + + dis.dispatch({ + action: 'view_room', + room_id: roomId, + event_id: payload.event_id, + highlighted: payload.highlighted, + room_alias: payload.room_alias, + auto_join: payload.auto_join, + oob_data: payload.oob_data, }); - try { - const result = await MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias); - storeRoomAliasInCache(payload.room_alias, result.room_id); - dis.dispatch({ - action: 'view_room', - room_id: result.room_id, - event_id: payload.event_id, - highlighted: payload.highlighted, - room_alias: payload.room_alias, - auto_join: payload.auto_join, - oob_data: payload.oob_data, - }); - } catch (err) { - dis.dispatch({ - action: 'view_room_error', - room_id: null, - room_alias: payload.room_alias, - err, - }); - } } } From e296fd05c0048e95a98c8777209ecb2990d787f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 12 Nov 2019 15:39:26 +0100 Subject: [PATCH 0545/2372] RoomView: Move the search logic into a separate module. --- src/EventIndexing.js | 5 + src/Searching.js | 137 ++++++++++++++++++++++++++ src/components/structures/RoomView.js | 125 +---------------------- 3 files changed, 147 insertions(+), 120 deletions(-) create mode 100644 src/Searching.js diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 21ee8f3da6..29f9c48842 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -401,4 +401,9 @@ export default class EventIndexer { this._crawlerRef.cancel(); this._crawlerRef = null; } + + async search(searchArgs) { + const platform = PlatformPeg.get(); + return platform.searchEventIndex(searchArgs) + } } diff --git a/src/Searching.js b/src/Searching.js new file mode 100644 index 0000000000..cd06d9bc67 --- /dev/null +++ b/src/Searching.js @@ -0,0 +1,137 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import EventIndexPeg from "./EventIndexPeg"; +import MatrixClientPeg from "./MatrixClientPeg"; + +function serverSideSearch(term, roomId = undefined) { + let filter; + if (roomId !== undefined) { + filter = { + // XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :( + rooms: [roomId], + }; + } + + let searchPromise = MatrixClientPeg.get().searchRoomEvents({ + filter: filter, + term: term, + }); + + return searchPromise; +} + +function eventIndexSearch(term, roomId = undefined) { + const combinedSearchFunc = async (searchTerm) => { + // Create two promises, one for the local search, one for the + // server-side search. + const client = MatrixClientPeg.get(); + const serverSidePromise = serverSideSearch(searchTerm); + const localPromise = localSearchFunc(searchTerm); + + // Wait for both promises to resolve. + await Promise.all([serverSidePromise, localPromise]); + + // Get both search results. + const localResult = await localPromise; + const serverSideResult = await serverSidePromise; + + // Combine the search results into one result. + const result = {}; + + // Our localResult and serverSideResult are both ordered by + // recency separetly, when we combine them the order might not + // be the right one so we need to sort them. + const compare = (a, b) => { + const aEvent = a.context.getEvent().event; + const bEvent = b.context.getEvent().event; + + if (aEvent.origin_server_ts > + bEvent.origin_server_ts) return -1; + if (aEvent.origin_server_ts < + bEvent.origin_server_ts) return 1; + return 0; + }; + + result.count = localResult.count + serverSideResult.count; + result.results = localResult.results.concat( + serverSideResult.results).sort(compare); + result.highlights = localResult.highlights.concat( + serverSideResult.highlights); + + return result; + }; + + const localSearchFunc = async (searchTerm, roomId = undefined) => { + const searchArgs = { + search_term: searchTerm, + before_limit: 1, + after_limit: 1, + order_by_recency: true, + }; + + if (roomId !== undefined) { + searchArgs.room_id = roomId; + } + + const eventIndex = EventIndexPeg.get(); + + const localResult = await eventIndex.search(searchArgs); + + const response = { + search_categories: { + room_events: localResult, + }, + }; + + const emptyResult = { + results: [], + highlights: [], + }; + + const result = MatrixClientPeg.get()._processRoomEventsSearch( + emptyResult, response); + + return result; + }; + + let searchPromise; + + if (roomId !== undefined) { + if (MatrixClientPeg.get().isRoomEncrypted(roomId)) { + // The search is for a single encrypted room, use our local + // search method. + searchPromise = localSearchFunc(term, roomId); + } else { + // The search is for a single non-encrypted room, use the + // server-side search. + searchPromise = serverSideSearch(term, roomId); + } + } else { + // Search across all rooms, combine a server side search and a + // local search. + searchPromise = combinedSearchFunc(term); + } + + return searchPromise +} + +export default function eventSearch(term, roomId = undefined) { + const eventIndex = EventIndexPeg.get(); + + if (eventIndex === null) return serverSideSearch(term, roomId); + else return eventIndexSearch(term, roomId); +} diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 1b44335f51..9fe54ad164 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -34,7 +34,6 @@ import { _t } from '../../languageHandler'; import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks'; import MatrixClientPeg from '../../MatrixClientPeg'; -import PlatformPeg from "../../PlatformPeg"; import ContentMessages from '../../ContentMessages'; import Modal from '../../Modal'; import sdk from '../../index'; @@ -44,6 +43,7 @@ import Tinter from '../../Tinter'; import rate_limited_func from '../../ratelimitedfunc'; import ObjectUtils from '../../ObjectUtils'; import * as Rooms from '../../Rooms'; +import eventSearch from '../../Searching'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; @@ -1130,127 +1130,12 @@ module.exports = createReactClass({ // todo: should cancel any previous search requests. this.searchId = new Date().getTime(); - let filter; - if (scope === "Room") { - filter = { - // XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :( - rooms: [ - this.state.room.roomId, - ], - }; - } + let roomId; + if (scope === "Room") roomId = this.state.room.roomId, debuglog("sending search request"); - const platform = PlatformPeg.get(); - - if (platform.supportsEventIndexing()) { - const combinedSearchFunc = async (searchTerm) => { - // Create two promises, one for the local search, one for the - // server-side search. - const client = MatrixClientPeg.get(); - const serverSidePromise = client.searchRoomEvents({ - term: searchTerm, - }); - const localPromise = localSearchFunc(searchTerm); - - // Wait for both promises to resolve. - await Promise.all([serverSidePromise, localPromise]); - - // Get both search results. - const localResult = await localPromise; - const serverSideResult = await serverSidePromise; - - // Combine the search results into one result. - const result = {}; - - // Our localResult and serverSideResult are both ordered by - // recency separetly, when we combine them the order might not - // be the right one so we need to sort them. - const compare = (a, b) => { - const aEvent = a.context.getEvent().event; - const bEvent = b.context.getEvent().event; - - if (aEvent.origin_server_ts > - bEvent.origin_server_ts) return -1; - if (aEvent.origin_server_ts < - bEvent.origin_server_ts) return 1; - return 0; - }; - - result.count = localResult.count + serverSideResult.count; - result.results = localResult.results.concat( - serverSideResult.results).sort(compare); - result.highlights = localResult.highlights.concat( - serverSideResult.highlights); - - return result; - }; - - const localSearchFunc = async (searchTerm, roomId = undefined) => { - const searchArgs = { - search_term: searchTerm, - before_limit: 1, - after_limit: 1, - order_by_recency: true, - }; - - if (roomId !== undefined) { - searchArgs.room_id = roomId; - } - - const localResult = await platform.searchEventIndex( - searchArgs); - - const response = { - search_categories: { - room_events: localResult, - }, - }; - - const emptyResult = { - results: [], - highlights: [], - }; - - // TODO is there a better way to convert our result into what - // is expected by the handler method. - const result = MatrixClientPeg.get()._processRoomEventsSearch( - emptyResult, response); - - return result; - }; - - let searchPromise; - - if (scope === "Room") { - const roomId = this.state.room.roomId; - - if (MatrixClientPeg.get().isRoomEncrypted(roomId)) { - // The search is for a single encrypted room, use our local - // search method. - searchPromise = localSearchFunc(term, roomId); - } else { - // The search is for a single non-encrypted room, use the - // server-side search. - searchPromise = MatrixClientPeg.get().searchRoomEvents({ - filter: filter, - term: term, - }); - } - } else { - // Search across all rooms, combine a server side search and a - // local search. - searchPromise = combinedSearchFunc(term); - } - - this._handleSearchResult(searchPromise).done(); - } else { - const searchPromise = MatrixClientPeg.get().searchRoomEvents({ - filter: filter, - term: term, - }); - this._handleSearchResult(searchPromise).done(); - } + const searchPromise = eventSearch(term, roomId); + this._handleSearchResult(searchPromise).done(); }, _handleSearchResult: function(searchPromise) { From d911055f5d8016ebd2f036d68a9c1ee3f7343af2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 12 Nov 2019 15:39:54 +0100 Subject: [PATCH 0546/2372] MatrixChat: Move the indexing limited room logic to a different event. --- src/components/structures/MatrixChat.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d006247151..0d3d5abd55 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1276,11 +1276,6 @@ export default createReactClass({ // https://github.com/vector-im/riot-web/issues/3307#issuecomment-282895568 cli.setCanResetTimelineCallback(async function(roomId) { console.log("Request to reset timeline in room ", roomId, " viewing:", self.state.currentRoomId); - // TODO is there a better place to plug this in - const eventIndex = EventIndexPeg.get(); - if (eventIndex !== null) { - await eventIndex.addCheckpointForLimitedRoom(roomId); - } if (roomId !== self.state.currentRoomId) { // It is safe to remove events from rooms we are not viewing. @@ -1314,6 +1309,13 @@ export default createReactClass({ await eventIndex.onEventDecrypted(ev, err); }); + cli.on("Room.timelineReset", async (room, timelineSet, resetAllTimelines) => { + const eventIndex = EventIndexPeg.get(); + if (eventIndex === null) return; + if (resetAllTimelines === true) return; + await eventIndex.addCheckpointForLimitedRoom(roomId); + }); + cli.on('sync', function(state, prevState, data) { // LifecycleStore and others cannot directly subscribe to matrix client for // events because flux only allows store state changes during flux dispatches. From ecbc47c5488bf60b5cc068b09d4a51672b9a5c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 12 Nov 2019 15:40:49 +0100 Subject: [PATCH 0547/2372] EventIndexing: Rename the stop method. --- src/EventIndexPeg.js | 9 +++++---- src/EventIndexing.js | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 794450e4b7..86fb889c7a 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -57,13 +57,14 @@ class EventIndexPeg { return true } - async stop() { - if (this.index == null) return; - index.stopCrawler(); + stop() { + if (this.index === null) return; + index.stop(); + this.index = null; } async deleteEventIndex() { - if (this.index == null) return; + if (this.index === null) return; index.deleteEventIndex(); } } diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 29f9c48842..92a3a5a1f8 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -34,6 +34,7 @@ export default class EventIndexer { const platform = PlatformPeg.get(); if (!platform.supportsEventIndexing()) return false; platform.initEventIndex(userId); + return true; } async onSync(state, prevState, data) { @@ -397,7 +398,7 @@ export default class EventIndexer { this.crawlerRef = crawlerHandle; } - stopCrawler() { + stop() { this._crawlerRef.cancel(); this._crawlerRef = null; } From d69eb78b661764e9241d14a0c08dff23906245c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 12 Nov 2019 15:41:14 +0100 Subject: [PATCH 0548/2372] EventIndexing: Add a missing platform getting. --- src/EventIndexing.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 92a3a5a1f8..ebd2ffe983 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -385,6 +385,7 @@ export default class EventIndexer { } async deleteEventIndex() { + const platform = PlatformPeg.get(); if (platform.supportsEventIndexing()) { console.log("Seshat: Deleting event index."); this.crawlerRef.cancel(); From 3502454c615f1a7bc74588f3512661278604d2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 12 Nov 2019 15:58:38 +0100 Subject: [PATCH 0549/2372] LifeCycle: Stop the crawler and delete the index when whe log out. --- src/Lifecycle.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 0b44f2ed84..7360cd3231 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -613,6 +613,7 @@ export function onLoggedOut() { // so that React components unmount first. This avoids React soft crashes // that can occur when components try to use a null client. dis.dispatch({action: 'on_logged_out'}); + EventIndexPeg.deleteEventIndex().done(); stopMatrixClient(); _clearStorage().done(); } @@ -648,6 +649,7 @@ export function stopMatrixClient(unsetClient=true) { ActiveWidgetStore.stop(); IntegrationManagers.sharedInstance().stopWatching(); if (DMRoomMap.shared()) DMRoomMap.shared().stop(); + EventIndexPeg.stop(); const cli = MatrixClientPeg.get(); if (cli) { cli.stopClient(); From 87f9e0d5650b42383f85804623c2b40aaba50854 Mon Sep 17 00:00:00 2001 From: take100yen Date: Tue, 12 Nov 2019 11:28:20 +0000 Subject: [PATCH 0550/2372] Translated using Weblate (Japanese) Currently translated at 62.7% (1168 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ja/ --- src/i18n/strings/ja.json | 53 +++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 5e3fecaed9..22b092c06b 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -17,12 +17,12 @@ "Hide read receipts": "既読を表示しない", "Invited": "招待中", "%(displayName)s is typing": "%(displayName)s 文字入力中", - "%(targetName)s joined the room.": "%(targetName)s 部屋に参加しました。", + "%(targetName)s joined the room.": "%(targetName)s が部屋に参加しました。", "Low priority": "低優先度", "Mute": "通知しない", "New password": "新しいパスワード", "Notifications": "通知", - "Cancel": "取消", + "Cancel": "キャンセル", "Create new room": "新しい部屋を作成", "Room directory": "公開部屋一覧", "Search": "検索", @@ -490,7 +490,7 @@ "Delete %(count)s devices|one": "端末を削除する", "Device ID": "端末ID", "Device Name": "端末名", - "Last seen": "最後のシーン", + "Last seen": "最後に表示した時刻", "Select devices": "端末の選択", "Failed to set display name": "表示名の設定に失敗しました", "Disable Notifications": "通知を無効にする", @@ -1162,8 +1162,8 @@ "No media permissions": "メディア権限がありません", "You may need to manually permit Riot to access your microphone/webcam": "自分のマイク/ウェブカメラにアクセスするために手動でRiotを許可する必要があるかもしれません", "Missing Media Permissions, click here to request.": "メディアアクセス権がありません。ここをクリックしてリクエストしてください。", - "No Audio Outputs detected": "オーディオ出力が検出されなかった", - "Default Device": "標準端末", + "No Audio Outputs detected": "オーディオ出力が検出されませんでした", + "Default Device": "既定のデバイス", "Audio Output": "音声出力", "VoIP": "VoIP", "Email": "Eメール", @@ -1380,5 +1380,46 @@ "Enable Community Filter Panel": "コミュニティーフィルターパネルを有効にする", "Show recently visited rooms above the room list": "最近訪問した部屋をリストの上位に表示する", "Low bandwidth mode": "低帯域通信モード", - "Trust & Devices": "信頼と端末" + "Trust & Devices": "信頼と端末", + "Public Name": "パブリック名", + "Upload profile picture": "プロフィール画像をアップロード", + "Upgrade to your own domain": "あなた自身のドメインにアップグレード", + "Phone numbers": "電話番号", + "Set a new account password...": "アカウントの新しいパスワードを設定...", + "Language and region": "言語と地域", + "Theme": "テーマ", + "General": "一般", + "Preferences": "環境設定", + "Security & Privacy": "セキュリティとプライバシー", + "A device's public name is visible to people you communicate with": "デバイスのパブリック名はあなたと会話するすべての人が閲覧できます", + "Room information": "部屋の情報", + "Internal room ID:": "内部 部屋ID:", + "Room version": "部屋のバージョン", + "Room version:": "部屋のバージョン:", + "Developer options": "開発者オプション", + "Room Addresses": "部屋のアドレス", + "Sounds": "音", + "Notification sound": "通知音", + "Reset": "リセット", + "Set a new custom sound": "カスタム音を設定", + "Browse": "参照", + "Roles & Permissions": "役割と権限", + "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "誰が履歴を読み取れるかに関する変更は、今後送信されるメッセージにのみ適用されます。既に存在する履歴の表示は変更されません。", + "Encryption": "暗号化", + "Once enabled, encryption cannot be disabled.": "もし有効化された場合、二度と無効化できません。", + "Encrypted": "暗号化", + "Email Address": "メールアドレス", + "Main address": "メインアドレス", + "Join": "参加", + "This room is private, and can only be joined by invitation.": "この部屋はプライベートです。招待によってのみ参加できます。", + "Create a private room": "プライベートな部屋を作成", + "Topic (optional)": "トピック (オプション)", + "Hide advanced": "高度な設定を非表示", + "Show advanced": "高度な設定を表示", + "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "他の Matrix ホームサーバーからの参加を禁止する (この設定はあとから変更できません!)", + "Room Settings - %(roomName)s": "部屋の設定 - %(roomName)s", + "Explore": "探索", + "Filter": "フィルター", + "Find a room… (e.g. %(exampleRoom)s)": "部屋を探す… (例: %(exampleRoom)s)", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "もしお探しの部屋が見つからない場合、招待してもらうか部屋を作成しましょう。" } From 5fa4bc7192d22cdb0c811f676b9479b3ff99ad6a Mon Sep 17 00:00:00 2001 From: shuji narazaki Date: Tue, 12 Nov 2019 11:52:45 +0000 Subject: [PATCH 0551/2372] Translated using Weblate (Japanese) Currently translated at 62.7% (1168 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ja/ --- src/i18n/strings/ja.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 22b092c06b..54e2c29a21 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -389,8 +389,8 @@ "%(senderName)s kicked %(targetName)s.": "%(senderName)s は %(targetName)s を追放しました。", "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s は %(targetName)s の招待を撤回しました。", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s はトピックを \"%(topic)s\" に変更しました。", - "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s はルーム名を削除しました。", - "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s はルーム名を %(roomName)s に変更しました。", + "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s は部屋名を削除しました。", + "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s は部屋名を %(roomName)s に変更しました。", "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s はイメージを送信しました。", "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s はこの部屋のアドレスとして %(addedAddresses)s を追加しました。", "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s はこの部屋のアドレスとして %(addedAddresses)s を追加しました。", @@ -1421,5 +1421,6 @@ "Explore": "探索", "Filter": "フィルター", "Find a room… (e.g. %(exampleRoom)s)": "部屋を探す… (例: %(exampleRoom)s)", - "If you can't find the room you're looking for, ask for an invite or Create a new room.": "もしお探しの部屋が見つからない場合、招待してもらうか部屋を作成しましょう。" + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "もしお探しの部屋が見つからない場合、招待してもらうか部屋を作成しましょう。", + "Enable room encryption": "部屋の暗号化を有効にする" } From 523e17838de98b9a0744b229db36741a21d22996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Tue, 12 Nov 2019 12:15:45 +0000 Subject: [PATCH 0552/2372] Translated using Weblate (Korean) Currently translated at 100.0% (1864 of 1864 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 47 +++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 54425657cf..3d01d5be67 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -128,8 +128,8 @@ "Deactivate Account": "계정 비활성화", "Deactivate my account": "계정 정지하기", "Decline": "거절", - "Decrypt %(text)s": "%(text)s 해독", - "Decryption error": "암호 해독 오류", + "Decrypt %(text)s": "%(text)s 복호화", + "Decryption error": "암호 복호화 오류", "Delete": "지우기", "Deops user with given id": "받은 ID로 사용자의 등급을 낮추기", "Device ID:": "기기 ID:", @@ -156,7 +156,7 @@ "End-to-end encryption is in beta and may not be reliable": "종단 간 암호화는 베타 테스트 중이며 신뢰하기 힘들 수 있습니다.", "Enter Code": "코드를 입력하세요", "Enter passphrase": "암호 입력", - "Error decrypting attachment": "첨부 파일 해독 중 오류", + "Error decrypting attachment": "첨부 파일 복호화 중 오류", "Error: Problem communicating with the given homeserver.": "오류: 지정한 홈서버와 통신에 문제가 있습니다.", "Event information": "이벤트 정보", "Existing Call": "기존 전화", @@ -394,7 +394,7 @@ "Unable to capture screen": "화면을 찍을 수 없음", "Unable to enable Notifications": "알림을 사용할 수 없음", "Unable to load device list": "기기 목록을 불러올 수 없음", - "Undecryptable": "해독할 수 없음", + "Undecryptable": "복호화할 수 없음", "Unencrypted room": "암호화하지 않은 방", "unencrypted": "암호화하지 않음", "Unencrypted message": "암호화하지 않은 메시지", @@ -549,10 +549,10 @@ "Unknown error": "알 수 없는 오류", "Incorrect password": "맞지 않는 비밀번호", "To continue, please enter your password.": "계속하려면, 비밀번호를 입력해주세요.", - "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "이 과정으로 암호화한 방에서 받은 메시지의 키를 로컬 파일로 내보낼 수 있습니다. 그런 다음 나중에 다른 Matrix 클라이언트에서 파일을 가져와서, 해당 클라이언트에서도 이 메시지를 해독할 수 있도록 할 수 있습니다.", - "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "내보낸 파일이 있으면 누구든 암호화한 메시지를 해독해서 읽을 수 있으므로, 보안에 신경을 써야 합니다. 이런 이유로 내보낸 파일을 암호화하도록 아래에 암호를 입력하는 것을 추천합니다. 같은 암호를 사용해야 데이터를 불러올 수 있을 것입니다.", - "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "이 과정으로 다른 Matrix 클라이언트에서 내보낸 암호화 키를 가져올 수 있습니다. 그런 다음 이전 클라이언트에서 해독할 수 있는 모든 메시지를 해독할 수 있습니다.", - "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "내보낸 파일이 암호로 보호되어 있습니다. 파일을 해독하려면, 여기에 암호를 입력해야 합니다.", + "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "이 과정으로 암호화한 방에서 받은 메시지의 키를 로컬 파일로 내보낼 수 있습니다. 그런 다음 나중에 다른 Matrix 클라이언트에서 파일을 가져와서, 해당 클라이언트에서도 이 메시지를 복호화할 수 있도록 할 수 있습니다.", + "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "내보낸 파일이 있으면 누구든 암호화한 메시지를 복호화해서 읽을 수 있으므로, 보안에 신경을 써야 합니다. 이런 이유로 내보낸 파일을 암호화하도록 아래에 암호를 입력하는 것을 추천합니다. 같은 암호를 사용해야 데이터를 불러올 수 있을 것입니다.", + "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "이 과정으로 다른 Matrix 클라이언트에서 내보낸 암호화 키를 가져올 수 있습니다. 그런 다음 이전 클라이언트에서 복호화할 수 있는 모든 메시지를 복호화할 수 있습니다.", + "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "내보낸 파일이 암호로 보호되어 있습니다. 파일을 복호화하려면, 여기에 암호를 입력해야 합니다.", "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "이 이벤트를 감추길(삭제하길) 원하세요? 방 이름을 삭제하거나 주제를 바꾸면, 다시 생길 수도 있습니다.", "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:": "이 기기를 신뢰할 수 있는 지 인증하려면, 다른 방법(예를 들자면 직접 만나거나 전화를 걸어서)으로 소유자 분에게 연락해, 사용자 설정에 있는 키가 아래 키와 같은지 물어보세요:", "Device name": "기기 이름", @@ -590,9 +590,9 @@ "Custom server": "사용자 지정 서버", "Home server URL": "홈 서버 URL", "Identity server URL": "ID 서버 URL", - "Error decrypting audio": "음성 해독 오류", - "Error decrypting image": "사진 해독 중 오류", - "Error decrypting video": "영상 해독 중 오류", + "Error decrypting audio": "음성 복호화 오류", + "Error decrypting image": "사진 복호화 중 오류", + "Error decrypting video": "영상 복호화 중 오류", "Add an Integration": "통합 추가", "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "%(integrationsUrl)s에서 쓸 수 있도록 계정을 인증하려고 다른 사이트로 이동하고 있습니다. 계속하겠습니까?", "Removed or unknown message type": "감췄거나 알 수 없는 메시지 유형", @@ -670,7 +670,7 @@ "Messages containing my display name": "내 표시 이름이 포함된 메시지", "Messages in one-to-one chats": "1:1 대화 메시지", "Unavailable": "이용할 수 없음", - "View Decrypted Source": "해독된 소스 보기", + "View Decrypted Source": "복호화된 소스 보기", "Send": "보내기", "remove %(name)s from the directory.": "목록에서 %(name)s 방을 제거했습니다.", "Notifications on the following keywords follow rules which can’t be displayed here:": "여기에 표시할 수 없는 규칙에 따르는 다음 키워드에 대한 알림:", @@ -779,7 +779,7 @@ "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s님이 아바타를 %(count)s번 바꿨습니다", "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s님이 아바타를 바꿨습니다", "This setting cannot be changed later!": "이 설정은 나중에 바꿀 수 없습니다!", - "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "이전 버전 Riot의 데이터가 감지됬습니다. 이 때문에 이전 버전에서 종단간 암호화가 작동하지 않을 수 있습니다. 이전 버전을 사용하면서 최근에 교환한 종단간 암호화 메시지를 이 버전에서는 해독할 수 없습니다. 이 버전에서 메시지를 교환할 수 없을 수도 있습니다. 문제가 발생하면 로그아웃한 후 다시 로그인하세요. 메시지 기록을 유지하려면 키를 내보낸 후 다시 가져오세요.", + "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "이전 버전 Riot의 데이터가 감지됬습니다. 이 때문에 이전 버전에서 종단간 암호화가 작동하지 않을 수 있습니다. 이전 버전을 사용하면서 최근에 교환한 종단간 암호화 메시지를 이 버전에서는 복호화할 수 없습니다. 이 버전에서 메시지를 교환할 수 없을 수도 있습니다. 문제가 발생하면 로그아웃한 후 다시 로그인하세요. 메시지 기록을 유지하려면 키를 내보낸 후 다시 가져오세요.", "Hide display name changes": "별명 변경 내역 숨기기", "This event could not be displayed": "이 이벤트를 표시할 수 없음", "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "%(displayName)s(%(userName)s)님이 %(dateTime)s에 확인함", @@ -902,7 +902,7 @@ "%(senderName)s sent a video": "%(senderName)s님이 영상을 보냄", "%(senderName)s uploaded a file": "%(senderName)s님이 파일을 업로드함", "Key request sent.": "키 요청을 보냈습니다.", - "If your other devices do not have the key for this message you will not be able to decrypt them.": "당신의 다른 기기에 이 메시지를 읽기 위한 키가 없다면 메시지를 해독할 수 없습니다.", + "If your other devices do not have the key for this message you will not be able to decrypt them.": "당신의 다른 기기에 이 메시지를 읽기 위한 키가 없다면 메시지를 복호화할 수 없습니다.", "Encrypting": "암호화 중", "Encrypted, not sent": "암호화 됨, 보내지지 않음", "Disinvite this user?": "이 사용자에 대한 초대를 취소할까요?", @@ -1811,13 +1811,13 @@ "Deny": "거부", "Unable to load backup status": "백업 상태 불러올 수 없음", "Recovery Key Mismatch": "복구 키가 맞지 않음", - "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "이 키로 백업을 해독할 수 없음: 맞는 복구 키를 입력해서 인증해주세요.", + "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "이 키로 백업을 복호화할 수 없음: 맞는 복구 키를 입력해서 인증해주세요.", "Incorrect Recovery Passphrase": "맞지 않은 복구 암호", - "Backup could not be decrypted with this passphrase: please verify that you entered the correct recovery passphrase.": "이 암호로 백업을 해독할 수 없음: 맞는 암호를 입력해서 입증해주세요.", + "Backup could not be decrypted with this passphrase: please verify that you entered the correct recovery passphrase.": "이 암호로 백업을 복호화할 수 없음: 맞는 암호를 입력해서 입증해주세요.", "Unable to restore backup": "백업을 복구할 수 없음", "No backup found!": "백업을 찾을 수 없습니다!", "Backup Restored": "백업 복구됨", - "Failed to decrypt %(failedCount)s sessions!": "%(failedCount)s개의 세션 해독에 실패했습니다!", + "Failed to decrypt %(failedCount)s sessions!": "%(failedCount)s개의 세션 복호화에 실패했습니다!", "Restored %(sessionCount)s session keys": "%(sessionCount)s개의 세션 키 복구됨", "Enter Recovery Passphrase": "복구 암호 입력", "Warning: you should only set up key backup from a trusted computer.": "경고: 신뢰할 수 있는 컴퓨터에서만 키 백업을 설정해야 합니다.", @@ -2124,5 +2124,16 @@ "Show tray icon and minimize window to it on close": "닫을 때 창을 최소화하고 트레이 아이콘으로 표시하기", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "이 작업에는 이메일 주소 또는 전화번호를 확인하기 위해 기본 ID 서버 에 접근해야 합니다. 하지만 서버가 서비스 약관을 갖고 있지 않습니다.", "Trust": "신뢰함", - "Message Actions": "메시지 동작" + "Message Actions": "메시지 동작", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Send verification requests in direct message": "다이렉트 메시지에서 확인 요청 보내기", + "You verified %(name)s": "%(name)s님을 확인했습니다", + "You cancelled verifying %(name)s": "%(name)s님의 확인을 취소했습니다", + "%(name)s cancelled verifying": "%(name)s님이 확인을 취소했습니다", + "You accepted": "당신이 수락했습니다", + "%(name)s accepted": "%(name)s님이 수락했습니다", + "You cancelled": "당신이 취소했습니다", + "%(name)s cancelled": "%(name)s님이 취소했습니다", + "%(name)s wants to verify": "%(name)s님이 확인을 요청합니다", + "You sent a verification request": "확인 요청을 보냈습니다" } From fd28cf7a4c9e284ffe3bca8921e7402aacdd2924 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 12 Nov 2019 15:12:54 -0700 Subject: [PATCH 0553/2372] Move notification count to in front of the room name in the page title Fixes https://github.com/vector-im/riot-web/issues/10943 --- src/components/structures/MatrixChat.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 6cc86bf6d7..cd5b27f2b9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1767,10 +1767,12 @@ export default createReactClass({ const client = MatrixClientPeg.get(); const room = client && client.getRoom(this.state.currentRoomId); if (room) { - subtitle = `| ${ room.name } ${subtitle}`; + subtitle = `${this.subTitleStatus} | ${ room.name } ${subtitle}`; } + } else { + subtitle = `${this.subTitleStatus} ${subtitle}`; } - document.title = `${SdkConfig.get().brand || 'Riot'} ${subtitle} ${this.subTitleStatus}`; + document.title = `${SdkConfig.get().brand || 'Riot'} ${subtitle}`; }, updateStatusIndicator: function(state, prevState) { From 1aa0ab13e682fd73c2c9b7d46d77202339593a1e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 12 Nov 2019 15:38:24 -0700 Subject: [PATCH 0554/2372] Add some logging/recovery for lost rooms Zero inserts is not normal, so we apply the same recovery technique from the categorization logic above this block: insert it to be the very first room and hope that someone complains that the room is ordered incorrectly. There's some additional logging to try and identify what went wrong because it should definitely be inserted. The `!== 1` check is not supposed to be called, ever. Logging for https://github.com/vector-im/riot-web/issues/11303 --- src/stores/RoomListStore.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 980753551a..134870398f 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -515,7 +515,21 @@ class RoomListStore extends Store { } if (count !== 1) { - console.warn(`!! Room ${room.roomId} inserted ${count} times`); + console.warn(`!! Room ${room.roomId} inserted ${count} times to ${targetTag}`); + } + + // This is a workaround for https://github.com/vector-im/riot-web/issues/11303 + // The logging is to try and identify what happened exactly. + if (count === 0) { + // Something went very badly wrong - try to recover the room. + // We don't bother checking how the target list is ordered - we're expecting + // to just insert it. + console.warn(`!! Recovering ${room.roomId} for tag ${targetTag} at position 0`); + if (!listsClone[targetTag]) { + console.warn(`!! List for tag ${targetTag} does not exist - creating`); + listsClone[targetTag] = []; + } + listsClone[targetTag].splice(0, 0, {room, category}); } } From fa6e02fafb30b44b0ac0d833335780b18fc80851 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 12 Nov 2019 15:38:24 -0700 Subject: [PATCH 0555/2372] Revert "Add some logging/recovery for lost rooms" This reverts commit 1aa0ab13e682fd73c2c9b7d46d77202339593a1e. --- src/stores/RoomListStore.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 134870398f..980753551a 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -515,21 +515,7 @@ class RoomListStore extends Store { } if (count !== 1) { - console.warn(`!! Room ${room.roomId} inserted ${count} times to ${targetTag}`); - } - - // This is a workaround for https://github.com/vector-im/riot-web/issues/11303 - // The logging is to try and identify what happened exactly. - if (count === 0) { - // Something went very badly wrong - try to recover the room. - // We don't bother checking how the target list is ordered - we're expecting - // to just insert it. - console.warn(`!! Recovering ${room.roomId} for tag ${targetTag} at position 0`); - if (!listsClone[targetTag]) { - console.warn(`!! List for tag ${targetTag} does not exist - creating`); - listsClone[targetTag] = []; - } - listsClone[targetTag].splice(0, 0, {room, category}); + console.warn(`!! Room ${room.roomId} inserted ${count} times`); } } From 651098b2cabbb6c9211a571a9beff480f8b8b1bf Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Tue, 12 Nov 2019 17:46:17 +0000 Subject: [PATCH 0556/2372] Translated using Weblate (Albanian) Currently translated at 99.8% (1894 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 1d80a90a2b..2efe4ddd68 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2063,7 +2063,7 @@ "Discovery options will appear once you have added a phone number above.": "Mundësitë e zbulimit do të shfaqen sapo të keni shtuar më sipër një numër telefoni.", "Call failed due to misconfigured server": "Thirrja dështoi për shkak shërbyesi të keqformësuar", "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Që thirrjet të funksionojnë pa probleme, ju lutemi, kërkojini përgjegjësit të shërbyesit tuaj Home (%(homeserverDomain)s) të formësojë një shërbyes TURN.", - "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Ndryshe, mund të provoni të përdorni shërbyesin publik te turn.matrix.org, por kjo s’do të jetë edhe aq e qëndrueshme, dhe adresa juaj IP do t’i bëhet e njohur atij shërbyesi.Këtë mund ta bëni edhe që nga Rregullimet.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Ndryshe, mund të provoni të përdorni shërbyesin publik te turn.matrix.org, por kjo s’do të jetë edhe aq e qëndrueshme, dhe adresa juaj IP do t’i bëhet e njohur atij shërbyesi. Këtë mund ta bëni edhe që nga Rregullimet.", "Try using turn.matrix.org": "Provo të përdorësh turn.matrix.org", "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Lejoni shërbyes rrugëzgjidhje asistimi thirrjesh turn.matrix.org kur shërbyesi juaj Home nuk ofron një të tillë (gjatë thirrjes, adresa juaj IP do t’i bëhet e ditur)", "ID": "ID", @@ -2246,5 +2246,39 @@ "Cancel search": "Anulo kërkimin", "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "S’ka shërbyes identitetesh të formësuar, ndaj s’mund të shtoni një adresë email që të mund të ricaktoni fjalëkalimin tuaj në të ardhmen.", "Jump to first unread room.": "Hidhu te dhoma e parë e palexuar.", - "Jump to first invite.": "Hidhu te ftesa e parë." + "Jump to first invite.": "Hidhu te ftesa e parë.", + "Try out new ways to ignore people (experimental)": "Provoni rrugë të reja për shpërfillje personash (eksperimentale)", + "My Ban List": "Lista Ime e Dëbimeve", + "This is your list of users/servers you have blocked - don't leave the room!": "Kjo është lista juaj e përdoruesve/shërbyesve që keni bllokuar - mos dilni nga dhoma!", + "Ignored/Blocked": "Të shpërfillur/Të bllokuar", + "Error adding ignored user/server": "Gabim shtimi përdoruesi/shërbyesi të shpërfillur", + "Something went wrong. Please try again or view your console for hints.": "Diç shkoi ters. Ju lutemi, riprovoni ose, për ndonjë ide, shihni konsolën tuaj.", + "Error subscribing to list": "Gabim pajtimi te lista", + "Please verify the room ID or alias and try again.": "Ju lutemi, verifikoni ID-në ose aliasin e dhomës dhe riprovoni.", + "Error removing ignored user/server": "Gabim në heqje përdoruesi/shërbyes të shpërfillur", + "Error unsubscribing from list": "Gabim shpajtimi nga lista", + "Please try again or view your console for hints.": "Ju lutemi, riprovoni, ose shihni konsolën tuaj, për ndonjë ide.", + "None": "Asnjë", + "Ban list rules - %(roomName)s": "Rregulla liste dëbimesh - %(roomName)s", + "Server rules": "Rregulla shërbyesi", + "User rules": "Rregulla përdoruesi", + "You have not ignored anyone.": "S’keni shpërfillur ndonjë.", + "You are currently ignoring:": "Aktualisht shpërfillni:", + "You are not subscribed to any lists": "S’jeni pajtuar te ndonjë listë", + "Unsubscribe": "Shpajtohuni", + "View rules": "Shihni rregulla", + "You are currently subscribed to:": "Jeni i pajtuar te:", + "⚠ These settings are meant for advanced users.": "⚠ Këto rregullime janë menduar për përdorues të përparuar.", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Shtoni këtu përdorues dhe shërbyes që doni të shpërfillen. Që Riot të kërkojë për përputhje me çfarëdo shkronjash, përdorni yllthin. Për shembull, @bot:* do të shpërfillë krej përdoruesit që kanë emrin 'bot' në çfarëdo shërbyesi.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Shpërfillja e personave kryhet përmes listash dëbimi, të cilat përmbajnë rregulla se cilët të dëbohen. Pajtimi te një listë dëbimesh do të thotë se përdoruesit/shërbyesit e bllokuar nga ajo listë do t’ju fshihen juve.", + "Personal ban list": "Listë personale dëbimesh", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Lista juaj personale e dëbimeve mban krejt përdoruesit/shërbyesit prej të cilëve ju personalisht s’dëshironi të shihni mesazhe. Pas shpërfilljes së përdoruesit/shërbyesit tuaj të parë, te lista juaj e dhomave do të shfaqet një dhomë e re e quajtur 'Lista Ime e Dëbimeve' - qëndroni në këtë dhomë që ta mbani listën e dëbimeve në fuqi.", + "Server or user ID to ignore": "Shërbyes ose ID përdoruesi për t’u shpërfillur", + "eg: @bot:* or example.org": "p.sh.: @bot:* ose example.org", + "Subscribed lists": "Lista me pajtim", + "Subscribing to a ban list will cause you to join it!": "Pajtimi te një listë dëbimesh do të shkaktojë pjesëmarrjen tuaj në të!", + "If this isn't what you want, please use a different tool to ignore users.": "Nëse kjo s’është ajo çka doni, ju lutemi, përdorni një tjetër mjet për të shpërfillur përdorues.", + "Room ID or alias of ban list": "ID dhome ose alias e listës së dëbimeve", + "Subscribe": "Pajtohuni", + "You have ignored this user, so their message is hidden. Show anyways.": "E keni shpërfillur këtë përdorues, ndaj mesazhi i tij është fshehur. Shfaqe, sido qoftë." } From 6b8d8d13f241d996bab6b2345db7e084a287adee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Gr=C3=B6nroos?= Date: Tue, 12 Nov 2019 20:28:10 +0000 Subject: [PATCH 0557/2372] Translated using Weblate (Finnish) Currently translated at 95.6% (1814 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fi/ --- src/i18n/strings/fi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 5331bdb5c8..a1163f564b 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -1533,7 +1533,7 @@ "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Lisää ”¯\\_(ツ)_/¯” viestin alkuun", "User %(userId)s is already in the room": "Käyttäjä %(userId)s on jo huoneessa", "The user must be unbanned before they can be invited.": "Käyttäjän porttikielto täytyy poistaa ennen kutsumista.", - "Upgrade to your own domain": "Päivitä omaan verkkotunnukseesi", + "Upgrade to your own domain": "Päivitä omaan verkkotunnukseen", "Accept all %(invitedRooms)s invites": "Hyväksy kaikki %(invitedRooms)s kutsut", "Change room avatar": "Vaihda huoneen kuva", "Change room name": "Vaihda huoneen nimi", From a0ff1e9e9b547de4386ac38a6900db5b612be840 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 12 Nov 2019 20:08:29 +0000 Subject: [PATCH 0558/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1898 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 67af8a6a57..29747bb1b6 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2278,5 +2278,39 @@ "You cancelled": "Megszakítottad", "%(name)s cancelled": "%(name)s megszakította", "%(name)s wants to verify": "%(name)s ellenőrizni szeretné", - "You sent a verification request": "Ellenőrzési kérést küldtél" + "You sent a verification request": "Ellenőrzési kérést küldtél", + "Try out new ways to ignore people (experimental)": "Emberek figyelmen kívül hagyásához próbálj ki új utakat (kísérleti)", + "My Ban List": "Tiltólistám", + "This is your list of users/servers you have blocked - don't leave the room!": "Ez az általad tiltott felhasználók/szerverek listája - ne hagyd el ezt a szobát!", + "Ignored/Blocked": "Figyelmen kívül hagyott/Tiltott", + "Error adding ignored user/server": "Hiba a felhasználó/szerver hozzáadásánál a figyelmen kívül hagyandók listájához", + "Something went wrong. Please try again or view your console for hints.": "Valami nem sikerült. Kérjük próbáld újra vagy nézd meg a konzolt a hiba okának felderítéséhez.", + "Error subscribing to list": "A listára való feliratkozásnál hiba történt", + "Please verify the room ID or alias and try again.": "Kérünk ellenőrizd a szoba azonosítóját vagy alternatív nevét és próbáld újra.", + "Error removing ignored user/server": "Hiba a felhasználó/szerver törlésénél a figyelmen kívül hagyandók listájából", + "Error unsubscribing from list": "A listáról való leiratkozásnál hiba történt", + "Please try again or view your console for hints.": "Kérjük próbáld újra vagy nézd meg a konzolt a hiba okának felderítéséhez.", + "None": "Semmi", + "Ban list rules - %(roomName)s": "Tiltási lista szabályok - %(roomName)s", + "Server rules": "Szerver szabályok", + "User rules": "Felhasználói szabályok", + "You have not ignored anyone.": "Senkit nem hagysz figyelmen kívül.", + "You are currently ignoring:": "Jelenleg őket hagyod figyelmen kívül:", + "You are not subscribed to any lists": "Nem iratkoztál fel egyetlen listára sem", + "Unsubscribe": "Leiratkozás", + "View rules": "Szabályok megtekintése", + "You are currently subscribed to:": "Jelenleg ezekre iratkoztál fel:", + "⚠ These settings are meant for advanced users.": "⚠ Ezek a beállítások haladó felhasználók számára vannak.", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Adj hozzá olyan felhasználókat és szervereket akiket figyelmen kívül kívánsz hagyni. Használj csillagot ahol bármilyen karakter állhat. Például: @bot:* minden „bot” nevű felhasználót figyelmen kívül fog hagyni akármelyik szerverről.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Emberek figyelmen kívül hagyása tiltólistán keresztül történik ami arról tartalmaz szabályokat, hogy kiket kell kitiltani. A feliratkozás a tiltólistára azt jelenti, hogy azok a felhasználók/szerverek amik tiltva vannak a lista által, azoknak az üzenetei rejtve maradnak számodra.", + "Personal ban list": "Személyes tiltó lista", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "A személyes tiltólistád tartalmazza azokat a személyeket/szervereket akiktől nem szeretnél üzeneteket látni. Az első felhasználó/szerver figyelmen kívül hagyása után egy új szoba jelenik meg a szobák listájában „Tiltólistám” névvel - ahhoz, hogy a lista érvényben maradjon maradj a szobában.", + "Server or user ID to ignore": "Figyelmen kívül hagyandó szerver vagy felhasználói azonosító", + "eg: @bot:* or example.org": "pl.: @bot:* vagy example.org", + "Subscribed lists": "Feliratkozott listák", + "Subscribing to a ban list will cause you to join it!": "A feliratkozás egy tiltó listára azzal jár, hogy csatlakozol hozzá!", + "If this isn't what you want, please use a different tool to ignore users.": "Ha nem ez az amit szeretnél, kérlek használj más eszközt a felhasználók figyelmen kívül hagyásához.", + "Room ID or alias of ban list": "Tiltó lista szoba azonosítója vagy alternatív neve", + "Subscribe": "Feliratkozás", + "You have ignored this user, so their message is hidden. Show anyways.": "Ezt a felhasználót figyelmen kívül hagyod, így az üzenetei el lesznek rejtve. Mindenképpen megmutat." } From 3dcc92b79da7458e25b6511f6d0a478da746714b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 12 Nov 2019 15:38:24 -0700 Subject: [PATCH 0559/2372] Add some logging/recovery for lost rooms Zero inserts is not normal, so we apply the same recovery technique from the categorization logic above this block: insert it to be the very first room and hope that someone complains that the room is ordered incorrectly. There's some additional logging to try and identify what went wrong because it should definitely be inserted. The `!== 1` check is not supposed to be called, ever. Logging for https://github.com/vector-im/riot-web/issues/11303 --- src/stores/RoomListStore.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 980753551a..134870398f 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -515,7 +515,21 @@ class RoomListStore extends Store { } if (count !== 1) { - console.warn(`!! Room ${room.roomId} inserted ${count} times`); + console.warn(`!! Room ${room.roomId} inserted ${count} times to ${targetTag}`); + } + + // This is a workaround for https://github.com/vector-im/riot-web/issues/11303 + // The logging is to try and identify what happened exactly. + if (count === 0) { + // Something went very badly wrong - try to recover the room. + // We don't bother checking how the target list is ordered - we're expecting + // to just insert it. + console.warn(`!! Recovering ${room.roomId} for tag ${targetTag} at position 0`); + if (!listsClone[targetTag]) { + console.warn(`!! List for tag ${targetTag} does not exist - creating`); + listsClone[targetTag] = []; + } + listsClone[targetTag].splice(0, 0, {room, category}); } } From 008554463d0478e9b06f0da35dce1af83d08eb55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 09:52:59 +0100 Subject: [PATCH 0560/2372] Lifecycle: Move the event index deletion into the clear storage method. --- src/Lifecycle.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 7360cd3231..1e68bcc062 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -613,7 +613,6 @@ export function onLoggedOut() { // so that React components unmount first. This avoids React soft crashes // that can occur when components try to use a null client. dis.dispatch({action: 'on_logged_out'}); - EventIndexPeg.deleteEventIndex().done(); stopMatrixClient(); _clearStorage().done(); } @@ -633,7 +632,13 @@ function _clearStorage() { // we'll never make any requests, so can pass a bogus HS URL baseUrl: "", }); - return cli.clearStores(); + + const clear = async() => { + await EventIndexPeg.deleteEventIndex(); + await cli.clearStores(); + } + + return clear(); } /** From 1cc64f2426bc049257985b06855b9ba9dbcd0113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 10:10:35 +0100 Subject: [PATCH 0561/2372] Searching: Move the small helper functions out of the eventIndexSearch function. --- src/Searching.js | 146 +++++++++++++++++++++++------------------------ 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index cd06d9bc67..cff5742b04 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -34,80 +34,80 @@ function serverSideSearch(term, roomId = undefined) { return searchPromise; } +async function combinedSearchFunc(searchTerm) { + // Create two promises, one for the local search, one for the + // server-side search. + const client = MatrixClientPeg.get(); + const serverSidePromise = serverSideSearch(searchTerm); + const localPromise = localSearchFunc(searchTerm); + + // Wait for both promises to resolve. + await Promise.all([serverSidePromise, localPromise]); + + // Get both search results. + const localResult = await localPromise; + const serverSideResult = await serverSidePromise; + + // Combine the search results into one result. + const result = {}; + + // Our localResult and serverSideResult are both ordered by + // recency separetly, when we combine them the order might not + // be the right one so we need to sort them. + const compare = (a, b) => { + const aEvent = a.context.getEvent().event; + const bEvent = b.context.getEvent().event; + + if (aEvent.origin_server_ts > + bEvent.origin_server_ts) return -1; + if (aEvent.origin_server_ts < + bEvent.origin_server_ts) return 1; + return 0; + }; + + result.count = localResult.count + serverSideResult.count; + result.results = localResult.results.concat( + serverSideResult.results).sort(compare); + result.highlights = localResult.highlights.concat( + serverSideResult.highlights); + + return result; +} + +async function localSearchFunc(searchTerm, roomId = undefined) { + const searchArgs = { + search_term: searchTerm, + before_limit: 1, + after_limit: 1, + order_by_recency: true, + }; + + if (roomId !== undefined) { + searchArgs.room_id = roomId; + } + + const eventIndex = EventIndexPeg.get(); + + const localResult = await eventIndex.search(searchArgs); + + const response = { + search_categories: { + room_events: localResult, + }, + }; + + const emptyResult = { + results: [], + highlights: [], + }; + + const result = MatrixClientPeg.get()._processRoomEventsSearch( + emptyResult, response); + + return result; +} + function eventIndexSearch(term, roomId = undefined) { - const combinedSearchFunc = async (searchTerm) => { - // Create two promises, one for the local search, one for the - // server-side search. - const client = MatrixClientPeg.get(); - const serverSidePromise = serverSideSearch(searchTerm); - const localPromise = localSearchFunc(searchTerm); - - // Wait for both promises to resolve. - await Promise.all([serverSidePromise, localPromise]); - - // Get both search results. - const localResult = await localPromise; - const serverSideResult = await serverSidePromise; - - // Combine the search results into one result. - const result = {}; - - // Our localResult and serverSideResult are both ordered by - // recency separetly, when we combine them the order might not - // be the right one so we need to sort them. - const compare = (a, b) => { - const aEvent = a.context.getEvent().event; - const bEvent = b.context.getEvent().event; - - if (aEvent.origin_server_ts > - bEvent.origin_server_ts) return -1; - if (aEvent.origin_server_ts < - bEvent.origin_server_ts) return 1; - return 0; - }; - - result.count = localResult.count + serverSideResult.count; - result.results = localResult.results.concat( - serverSideResult.results).sort(compare); - result.highlights = localResult.highlights.concat( - serverSideResult.highlights); - - return result; - }; - - const localSearchFunc = async (searchTerm, roomId = undefined) => { - const searchArgs = { - search_term: searchTerm, - before_limit: 1, - after_limit: 1, - order_by_recency: true, - }; - - if (roomId !== undefined) { - searchArgs.room_id = roomId; - } - - const eventIndex = EventIndexPeg.get(); - - const localResult = await eventIndex.search(searchArgs); - - const response = { - search_categories: { - room_events: localResult, - }, - }; - - const emptyResult = { - results: [], - highlights: [], - }; - - const result = MatrixClientPeg.get()._processRoomEventsSearch( - emptyResult, response); - - return result; - }; - let searchPromise; if (roomId !== undefined) { From 1df28c75262e113ea0111a6cc0dccb74a512e93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 10:30:38 +0100 Subject: [PATCH 0562/2372] Fix some lint errors. --- src/EventIndexPeg.js | 12 +++++++----- src/Lifecycle.js | 4 ++-- src/MatrixClientPeg.js | 2 -- src/Searching.js | 5 ++--- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 86fb889c7a..15d34ea230 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -32,7 +32,9 @@ class EventIndexPeg { } /** - * Returns the current Event index object for the application. Can be null + * Get the current event index. + * + * @Returns The EventIndex object for the application. Can be null * if the platform doesn't support event indexing. */ get() { @@ -47,25 +49,25 @@ class EventIndexPeg { const platform = PlatformPeg.get(); if (!platform.supportsEventIndexing()) return false; - let index = new EventIndex(); + const index = new EventIndex(); const userId = MatrixClientPeg.get().getUserId(); // TODO log errors here and return false if it errors out. await index.init(userId); this.index = index; - return true + return true; } stop() { if (this.index === null) return; - index.stop(); + this.index.stop(); this.index = null; } async deleteEventIndex() { if (this.index === null) return; - index.deleteEventIndex(); + this.index.deleteEventIndex(); } } diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 1e68bcc062..aa900c81a1 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -633,10 +633,10 @@ function _clearStorage() { baseUrl: "", }); - const clear = async() => { + const clear = async () => { await EventIndexPeg.deleteEventIndex(); await cli.clearStores(); - } + }; return clear(); } diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 6c5b465bb0..bebb254afc 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -30,8 +30,6 @@ import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; -import PlatformPeg from "./PlatformPeg"; -import EventIndexPeg from "./EventIndexPeg"; interface MatrixClientCreds { homeserverUrl: string, diff --git a/src/Searching.js b/src/Searching.js index cff5742b04..84e73b91f4 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -26,7 +26,7 @@ function serverSideSearch(term, roomId = undefined) { }; } - let searchPromise = MatrixClientPeg.get().searchRoomEvents({ + const searchPromise = MatrixClientPeg.get().searchRoomEvents({ filter: filter, term: term, }); @@ -37,7 +37,6 @@ function serverSideSearch(term, roomId = undefined) { async function combinedSearchFunc(searchTerm) { // Create two promises, one for the local search, one for the // server-side search. - const client = MatrixClientPeg.get(); const serverSidePromise = serverSideSearch(searchTerm); const localPromise = localSearchFunc(searchTerm); @@ -126,7 +125,7 @@ function eventIndexSearch(term, roomId = undefined) { searchPromise = combinedSearchFunc(term); } - return searchPromise + return searchPromise; } export default function eventSearch(term, roomId = undefined) { From 54b352f69cd1e9d82fff759c6838af1affca4f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 10:37:20 +0100 Subject: [PATCH 0563/2372] MatrixChat: Fix the limited timeline checkpoint adding. --- src/EventIndexing.js | 9 ++------- src/components/structures/MatrixChat.js | 4 +--- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index ebd2ffe983..bf3f50690f 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -347,15 +347,10 @@ export default class EventIndexer { console.log("Seshat: Stopping crawler function"); } - async addCheckpointForLimitedRoom(roomId) { + async addCheckpointForLimitedRoom(room) { const platform = PlatformPeg.get(); if (!platform.supportsEventIndexing()) return; - if (!MatrixClientPeg.get().isRoomEncrypted(roomId)) return; - - const client = MatrixClientPeg.get(); - const room = client.getRoom(roomId); - - if (room === null) return; + if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; const timeline = room.getLiveTimeline(); const token = timeline.getPaginationToken("b"); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 0d3d5abd55..ccc8b5e1d6 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1223,8 +1223,6 @@ export default createReactClass({ * Called when the session is logged out */ _onLoggedOut: async function() { - const platform = PlatformPeg.get(); - this.notifyNewScreen('login'); this.setStateForNewView({ view: VIEWS.LOGIN, @@ -1313,7 +1311,7 @@ export default createReactClass({ const eventIndex = EventIndexPeg.get(); if (eventIndex === null) return; if (resetAllTimelines === true) return; - await eventIndex.addCheckpointForLimitedRoom(roomId); + await eventIndex.addCheckpointForLimitedRoom(room); }); cli.on('sync', function(state, prevState, data) { From 80b28004e15821bd127bee3121baabd1cf6226a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 11:02:54 +0100 Subject: [PATCH 0564/2372] Searching: Define the room id in the const object. --- src/Searching.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Searching.js b/src/Searching.js index 84e73b91f4..ee46a66fb8 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -79,6 +79,7 @@ async function localSearchFunc(searchTerm, roomId = undefined) { before_limit: 1, after_limit: 1, order_by_recency: true, + room_id: undefined, }; if (roomId !== undefined) { From f453fea24acf110d0b297d8374234a8c873bec80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 12:25:16 +0100 Subject: [PATCH 0565/2372] BasePlatform: Move the event indexing methods into a separate class. --- src/BaseEventIndexManager.js | 208 +++++++++++++++++++++++++++++++++++ src/BasePlatform.js | 41 +------ src/EventIndexPeg.js | 6 +- src/EventIndexing.js | 62 +++++------ 4 files changed, 246 insertions(+), 71 deletions(-) create mode 100644 src/BaseEventIndexManager.js diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js new file mode 100644 index 0000000000..cd7a735e8d --- /dev/null +++ b/src/BaseEventIndexManager.js @@ -0,0 +1,208 @@ +// @flow + +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export interface MatrixEvent { + type: string; + sender: string; + content: {}; + event_id: string; + origin_server_ts: number; + unsigned: ?{}; + room_id: string; +} + +export interface MatrixProfile { + avatar_url: string; + displayname: string; +} + +export interface CrawlerCheckpoint { + roomId: string; + token: string; + fullCrawl: boolean; + direction: string; +} + +export interface ResultContext { + events_before: [MatrixEvent]; + events_after: [MatrixEvent]; + profile_info: Map; +} + +export interface ResultsElement { + rank: number; + result: MatrixEvent; + context: ResultContext; +} + +export interface SearchResult { + count: number; + results: [ResultsElement]; + highlights: [string]; +} + +export interface SearchArgs { + search_term: string; + before_limit: number; + after_limit: number; + order_by_recency: boolean; + room_id: ?string; +} + +export interface HistoricEvent { + event: MatrixEvent; + profile: MatrixProfile; +} + +/** + * Base class for classes that provide platform-specific event indexing. + * + * Instances of this class are provided by the application. + */ +export default class BaseEventIndexManager { + /** + * Initialize the event index for the given user. + * + * @param {string} userId The unique identifier of the logged in user that + * owns the index. + * + * @return {Promise} A promise that will resolve when the event index is + * initialized. + */ + async initEventIndex(userId: string): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Queue up an event to be added to the index. + * + * @param {MatrixEvent} ev The event that should be added to the index. + * @param {MatrixProfile} profile The profile of the event sender at the + * time of the event receival. + * + * @return {Promise} A promise that will resolve when the was queued up for + * addition. + */ + async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Check if our event index is empty. + * + * @return {Promise} A promise that will resolve to true if the + * event index is empty, false otherwise. + */ + indexIsEmpty(): Promise { + throw new Error("Unimplemented"); + } + + /** + * Commit the previously queued up events to the index. + * + * @return {Promise} A promise that will resolve once the queued up events + * were added to the index. + */ + async commitLiveEvents(): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Search the event index using the given term for matching events. + * + * @param {SearchArgs} searchArgs The search configuration sets what should + * be searched for and what should be contained in the search result. + * + * @return {Promise<[SearchResult]>} A promise that will resolve to an array + * of search results once the search is done. + */ + async searchEventIndex(searchArgs: SearchArgs): Promise { + throw new Error("Unimplemented"); + } + + /** + * Add events from the room history to the event index. + * + * This is used to add a batch of events to the index. + * + * @param {[HistoricEvent]} events The list of events and profiles that + * should be added to the event index. + * @param {[CrawlerCheckpoint]} checkpoint A new crawler checkpoint that + * should be stored in the index which should be used to continue crawling + * the room. + * @param {[CrawlerCheckpoint]} oldCheckpoint The checkpoint that was used + * to fetch the current batch of events. This checkpoint will be removed + * from the index. + * + * @return {Promise} A promise that will resolve to true if all the events + * were already added to the index, false otherwise. + */ + async addHistoricEvents( + events: [HistoricEvent], + checkpoint: CrawlerCheckpoint | null = null, + oldCheckpoint: CrawlerCheckpoint | null = null, + ): Promise { + throw new Error("Unimplemented"); + } + + /** + * Add a new crawler checkpoint to the index. + * + * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be added + * to the index. + * + * @return {Promise} A promise that will resolve once the checkpoint has + * been stored. + */ + async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Add a new crawler checkpoint to the index. + * + * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be + * removed from the index. + * + * @return {Promise} A promise that will resolve once the checkpoint has + * been removed. + */ + async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<> { + throw new Error("Unimplemented"); + } + + /** + * Load the stored checkpoints from the index. + * + * @return {Promise<[CrawlerCheckpoint]>} A promise that will resolve to an + * array of crawler checkpoints once they have been loaded from the index. + */ + async loadCheckpoints(): Promise<[CrawlerCheckpoint]> { + throw new Error("Unimplemented"); + } + + /** + * Delete our current event index. + * + * @return {Promise} A promise that will resolve once the event index has + * been deleted. + */ + async deleteEventIndex(): Promise<> { + throw new Error("Unimplemented"); + } +} diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 7f5df822e4..582ac24cb0 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -19,6 +19,7 @@ limitations under the License. */ import dis from './dispatcher'; +import BaseEventIndexManager from './BaseEventIndexManager'; /** * Base class for classes that provide platform-specific functionality @@ -152,43 +153,7 @@ export default class BasePlatform { throw new Error("Unimplemented"); } - supportsEventIndexing(): boolean { - return false; - } - - async initEventIndex(userId: string): boolean { - throw new Error("Unimplemented"); - } - - async addEventToIndex(ev: {}, profile: {}): void { - throw new Error("Unimplemented"); - } - - indexIsEmpty(): Promise { - throw new Error("Unimplemented"); - } - - async commitLiveEvents(): void { - throw new Error("Unimplemented"); - } - - async searchEventIndex(term: string): Promise<{}> { - throw new Error("Unimplemented"); - } - - async addHistoricEvents(events: [], checkpoint: {} = null, oldCheckpoint: {} = null): Promise { - throw new Error("Unimplemented"); - } - - async addCrawlerCheckpoint(checkpoint: {}): Promise<> { - throw new Error("Unimplemented"); - } - - async removeCrawlerCheckpoint(checkpoint: {}): Promise<> { - throw new Error("Unimplemented"); - } - - async deleteEventIndex(): Promise<> { - throw new Error("Unimplemented"); + getEventIndexingManager(): BaseEventIndexManager | null { + return null; } } diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 15d34ea230..bec3f075b6 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -46,9 +46,11 @@ class EventIndexPeg { * otherwise. */ async init() { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return false; + const indexManager = PlatformPeg.get().getEventIndexingManager(); + console.log("Initializing event index, got {}", indexManager); + if (indexManager === null) return false; + console.log("Seshat: Creatingnew EventIndex object", indexManager); const index = new EventIndex(); const userId = MatrixClientPeg.get().getUserId(); diff --git a/src/EventIndexing.js b/src/EventIndexing.js index bf3f50690f..60482b76b5 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -31,19 +31,19 @@ export default class EventIndexer { } async init(userId) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return false; - platform.initEventIndex(userId); + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (indexManager === null) return false; + indexManager.initEventIndex(userId); return true; } async onSync(state, prevState, data) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (indexManager === null) return; if (prevState === null && state === "PREPARED") { // Load our stored checkpoints, if any. - this.crawlerChekpoints = await platform.loadCheckpoints(); + this.crawlerChekpoints = await indexManager.loadCheckpoints(); console.log("Seshat: Loaded checkpoints", this.crawlerChekpoints); return; @@ -85,8 +85,8 @@ export default class EventIndexer { direction: "f", }; - await platform.addCrawlerCheckpoint(backCheckpoint); - await platform.addCrawlerCheckpoint(forwardCheckpoint); + await indexManager.addCrawlerCheckpoint(backCheckpoint); + await indexManager.addCrawlerCheckpoint(forwardCheckpoint); this.crawlerChekpoints.push(backCheckpoint); this.crawlerChekpoints.push(forwardCheckpoint); })); @@ -95,7 +95,7 @@ export default class EventIndexer { // If our indexer is empty we're most likely running Riot the // first time with indexing support or running it with an // initial sync. Add checkpoints to crawl our encrypted rooms. - const eventIndexWasEmpty = await platform.isEventIndexEmpty(); + const eventIndexWasEmpty = await indexManager.isEventIndexEmpty(); if (eventIndexWasEmpty) await addInitialCheckpoints(); // Start our crawler. @@ -107,14 +107,14 @@ export default class EventIndexer { // A sync was done, presumably we queued up some live events, // commit them now. console.log("Seshat: Committing events"); - await platform.commitLiveEvents(); + await indexManager.commitLiveEvents(); return; } } async onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (indexManager === null) return; // We only index encrypted rooms locally. if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; @@ -139,8 +139,8 @@ export default class EventIndexer { } async onEventDecrypted(ev, err) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (indexManager === null) return; const eventId = ev.getId(); @@ -151,8 +151,8 @@ export default class EventIndexer { } async addLiveEventToIndex(ev) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (indexManager === null) return; if (["m.room.message", "m.room.name", "m.room.topic"] .indexOf(ev.getType()) == -1) { @@ -165,7 +165,7 @@ export default class EventIndexer { avatar_url: ev.sender.getMxcAvatarUrl(), }; - platform.addEventToIndex(e, profile); + indexManager.addEventToIndex(e, profile); } async crawlerFunc(handle) { @@ -180,7 +180,7 @@ export default class EventIndexer { console.log("Seshat: Started crawler function"); const client = MatrixClientPeg.get(); - const platform = PlatformPeg.get(); + const indexManager = PlatformPeg.get().getEventIndexingManager(); handle.cancel = () => { cancelled = true; @@ -223,14 +223,14 @@ export default class EventIndexer { } catch (e) { console.log("Seshat: Error crawling events:", e); this.crawlerChekpoints.push(checkpoint); - continue + continue; } if (res.chunk.length === 0) { console.log("Seshat: Done with the checkpoint", checkpoint); // We got to the start/end of our timeline, lets just // delete our checkpoint and go back to sleep. - await platform.removeCrawlerCheckpoint(checkpoint); + await indexManager.removeCrawlerCheckpoint(checkpoint); continue; } @@ -323,7 +323,7 @@ export default class EventIndexer { ); try { - const eventsAlreadyAdded = await platform.addHistoricEvents( + const eventsAlreadyAdded = await indexManager.addHistoricEvents( events, newCheckpoint, checkpoint); // If all events were already indexed we assume that we catched // up with our index and don't need to crawl the room further. @@ -332,7 +332,7 @@ export default class EventIndexer { if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { console.log("Seshat: Checkpoint had already all events", "added, stopping the crawl", checkpoint); - await platform.removeCrawlerCheckpoint(newCheckpoint); + await indexManager.removeCrawlerCheckpoint(newCheckpoint); } else { this.crawlerChekpoints.push(newCheckpoint); } @@ -348,8 +348,8 @@ export default class EventIndexer { } async addCheckpointForLimitedRoom(room) { - const platform = PlatformPeg.get(); - if (!platform.supportsEventIndexing()) return; + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (indexManager === null) return; if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; const timeline = room.getLiveTimeline(); @@ -372,19 +372,19 @@ export default class EventIndexer { console.log("Seshat: Added checkpoint because of a limited timeline", backwardsCheckpoint, forwardsCheckpoint); - await platform.addCrawlerCheckpoint(backwardsCheckpoint); - await platform.addCrawlerCheckpoint(forwardsCheckpoint); + await indexManager.addCrawlerCheckpoint(backwardsCheckpoint); + await indexManager.addCrawlerCheckpoint(forwardsCheckpoint); this.crawlerChekpoints.push(backwardsCheckpoint); this.crawlerChekpoints.push(forwardsCheckpoint); } async deleteEventIndex() { - const platform = PlatformPeg.get(); - if (platform.supportsEventIndexing()) { + const indexManager = PlatformPeg.get().getEventIndexingManager(); + if (indexManager !== null) { console.log("Seshat: Deleting event index."); this.crawlerRef.cancel(); - await platform.deleteEventIndex(); + await indexManager.deleteEventIndex(); } } @@ -400,7 +400,7 @@ export default class EventIndexer { } async search(searchArgs) { - const platform = PlatformPeg.get(); - return platform.searchEventIndex(searchArgs) + const indexManager = PlatformPeg.get().getEventIndexingManager(); + return indexManager.searchEventIndex(searchArgs); } } From 1316e04776b90ec7cc3d7770822b400795de171b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 15:23:08 +0100 Subject: [PATCH 0566/2372] EventIndexing: Check if there is a room when resetting the timeline. --- src/EventIndexing.js | 13 ++----------- src/components/structures/MatrixChat.js | 4 ++-- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 60482b76b5..4817df4b32 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -347,7 +347,7 @@ export default class EventIndexer { console.log("Seshat: Stopping crawler function"); } - async addCheckpointForLimitedRoom(room) { + async onLimitedTimeline(room) { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (indexManager === null) return; if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; @@ -362,21 +362,12 @@ export default class EventIndexer { direction: "b", }; - const forwardsCheckpoint = { - roomId: room.roomId, - token: token, - fullCrawl: false, - direction: "f", - }; - console.log("Seshat: Added checkpoint because of a limited timeline", - backwardsCheckpoint, forwardsCheckpoint); + backwardsCheckpoint); await indexManager.addCrawlerCheckpoint(backwardsCheckpoint); - await indexManager.addCrawlerCheckpoint(forwardsCheckpoint); this.crawlerChekpoints.push(backwardsCheckpoint); - this.crawlerChekpoints.push(forwardsCheckpoint); } async deleteEventIndex() { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ccc8b5e1d6..f78bb5c168 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1310,8 +1310,8 @@ export default createReactClass({ cli.on("Room.timelineReset", async (room, timelineSet, resetAllTimelines) => { const eventIndex = EventIndexPeg.get(); if (eventIndex === null) return; - if (resetAllTimelines === true) return; - await eventIndex.addCheckpointForLimitedRoom(room); + if (room === null) return; + await eventIndex.onLimitedTimeline(room); }); cli.on('sync', function(state, prevState, data) { From ab7f34b45a66748fde1ee361faa7f31bc86db0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 15:26:27 +0100 Subject: [PATCH 0567/2372] EventIndexing: Don't mention Seshat in the logs. --- src/EventIndexing.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 4817df4b32..f67d4c9eb3 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -44,7 +44,7 @@ export default class EventIndexer { if (prevState === null && state === "PREPARED") { // Load our stored checkpoints, if any. this.crawlerChekpoints = await indexManager.loadCheckpoints(); - console.log("Seshat: Loaded checkpoints", + console.log("EventIndex: Loaded checkpoints", this.crawlerChekpoints); return; } @@ -62,7 +62,7 @@ export default class EventIndexer { // rooms can use the search provided by the Homeserver. const encryptedRooms = rooms.filter(isRoomEncrypted); - console.log("Seshat: Adding initial crawler checkpoints"); + console.log("EventIndex: Adding initial crawler checkpoints"); // Gather the prev_batch tokens and create checkpoints for // our message crawler. @@ -70,7 +70,7 @@ export default class EventIndexer { const timeline = room.getLiveTimeline(); const token = timeline.getPaginationToken("b"); - console.log("Seshat: Got token for indexer", + console.log("EventIndex: Got token for indexer", room.roomId, token); const backCheckpoint = { @@ -106,7 +106,7 @@ export default class EventIndexer { if (prevState === "SYNCING" && state === "SYNCING") { // A sync was done, presumably we queued up some live events, // commit them now. - console.log("Seshat: Committing events"); + console.log("EventIndex: Committing events"); await indexManager.commitLiveEvents(); return; } @@ -177,7 +177,7 @@ export default class EventIndexer { let cancelled = false; - console.log("Seshat: Started crawler function"); + console.log("EventIndex: Started crawler function"); const client = MatrixClientPeg.get(); const indexManager = PlatformPeg.get().getEventIndexingManager(); @@ -192,10 +192,10 @@ export default class EventIndexer { // here. await sleep(this._crawler_timeout); - console.log("Seshat: Running the crawler loop."); + console.log("EventIndex: Running the crawler loop."); if (cancelled) { - console.log("Seshat: Cancelling the crawler."); + console.log("EventIndex: Cancelling the crawler."); break; } @@ -207,7 +207,7 @@ export default class EventIndexer { continue; } - console.log("Seshat: crawling using checkpoint", checkpoint); + console.log("EventIndex: crawling using checkpoint", checkpoint); // We have a checkpoint, let us fetch some messages, again, very // conservatively to not bother our Homeserver too much. @@ -221,13 +221,13 @@ export default class EventIndexer { checkpoint.roomId, checkpoint.token, 100, checkpoint.direction); } catch (e) { - console.log("Seshat: Error crawling events:", e); + console.log("EventIndex: Error crawling events:", e); this.crawlerChekpoints.push(checkpoint); continue; } if (res.chunk.length === 0) { - console.log("Seshat: Done with the checkpoint", checkpoint); + console.log("EventIndex: Done with the checkpoint", checkpoint); // We got to the start/end of our timeline, lets just // delete our checkpoint and go back to sleep. await indexManager.removeCrawlerCheckpoint(checkpoint); @@ -289,7 +289,7 @@ export default class EventIndexer { // stage? const filteredEvents = matrixEvents.filter(isValidEvent); - // Let us convert the events back into a format that Seshat can + // Let us convert the events back into a format that EventIndex can // consume. const events = filteredEvents.map((ev) => { const jsonEvent = ev.toJSON(); @@ -317,7 +317,7 @@ export default class EventIndexer { }; console.log( - "Seshat: Crawled room", + "EventIndex: Crawled room", client.getRoom(checkpoint.roomId).name, "and fetched", events.length, "events.", ); @@ -330,21 +330,21 @@ export default class EventIndexer { // Let us delete the checkpoint in that case, otherwise push // the new checkpoint to be used by the crawler. if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { - console.log("Seshat: Checkpoint had already all events", + console.log("EventIndex: Checkpoint had already all events", "added, stopping the crawl", checkpoint); await indexManager.removeCrawlerCheckpoint(newCheckpoint); } else { this.crawlerChekpoints.push(newCheckpoint); } } catch (e) { - console.log("Seshat: Error durring a crawl", e); + console.log("EventIndex: Error durring a crawl", e); // An error occured, put the checkpoint back so we // can retry. this.crawlerChekpoints.push(checkpoint); } } - console.log("Seshat: Stopping crawler function"); + console.log("EventIndex: Stopping crawler function"); } async onLimitedTimeline(room) { @@ -362,7 +362,7 @@ export default class EventIndexer { direction: "b", }; - console.log("Seshat: Added checkpoint because of a limited timeline", + console.log("EventIndex: Added checkpoint because of a limited timeline", backwardsCheckpoint); await indexManager.addCrawlerCheckpoint(backwardsCheckpoint); @@ -373,7 +373,7 @@ export default class EventIndexer { async deleteEventIndex() { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (indexManager !== null) { - console.log("Seshat: Deleting event index."); + console.log("EventIndex: Deleting event index."); this.crawlerRef.cancel(); await indexManager.deleteEventIndex(); } From c33f5ba0ca8292116e1623a9d0c932aac62479a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 15:39:06 +0100 Subject: [PATCH 0568/2372] BaseEventIndexManager: Add a method to perform runtime checks for indexing support. --- src/BaseEventIndexManager.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index cd7a735e8d..a74eac658a 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -75,6 +75,19 @@ export interface HistoricEvent { * Instances of this class are provided by the application. */ export default class BaseEventIndexManager { + /** + * Does our EventIndexManager support event indexing. + * + * If an EventIndexManager imlpementor has runtime dependencies that + * optionally enable event indexing they may override this method to perform + * the necessary runtime checks here. + * + * @return {Promise} A promise that will resolve to true if event indexing + * is supported, false otherwise. + */ + async supportsEventIndexing(): Promise { + return true; + } /** * Initialize the event index for the given user. * From bf558b46c3cfc9ee7b19dbe7a92ac79ed118e498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 15:39:39 +0100 Subject: [PATCH 0569/2372] EventIndexPeg: Clean up the event index initialization. --- src/EventIndexPeg.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index bec3f075b6..3ce88339eb 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -47,15 +47,25 @@ class EventIndexPeg { */ async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); - console.log("Initializing event index, got {}", indexManager); if (indexManager === null) return false; - console.log("Seshat: Creatingnew EventIndex object", indexManager); - const index = new EventIndex(); + if (await indexManager.supportsEventIndexing() !== true) { + console.log("EventIndex: Platform doesn't support event indexing,", + "not initializing."); + return false; + } + const index = new EventIndex(); const userId = MatrixClientPeg.get().getUserId(); - // TODO log errors here and return false if it errors out. - await index.init(userId); + + try { + await index.init(userId); + } catch (e) { + console.log("EventIndex: Error initializing the event index", e); + } + + console.log("EventIndex: Successfully initialized the event index"); + this.index = index; return true; From c26df9d9efc836b1a6b5d660edd702448a22b3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 15:57:12 +0100 Subject: [PATCH 0570/2372] EventIndexing: Fix a typo. --- src/EventIndexing.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index f67d4c9eb3..af77979040 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -22,7 +22,7 @@ import MatrixClientPeg from "./MatrixClientPeg"; */ export default class EventIndexer { constructor() { - this.crawlerChekpoints = []; + this.crawlerCheckpoints = []; // The time that the crawler will wait between /rooms/{room_id}/messages // requests this._crawler_timeout = 3000; @@ -43,9 +43,9 @@ export default class EventIndexer { if (prevState === null && state === "PREPARED") { // Load our stored checkpoints, if any. - this.crawlerChekpoints = await indexManager.loadCheckpoints(); + this.crawlerCheckpoints = await indexManager.loadCheckpoints(); console.log("EventIndex: Loaded checkpoints", - this.crawlerChekpoints); + this.crawlerCheckpoints); return; } @@ -87,8 +87,8 @@ export default class EventIndexer { await indexManager.addCrawlerCheckpoint(backCheckpoint); await indexManager.addCrawlerCheckpoint(forwardCheckpoint); - this.crawlerChekpoints.push(backCheckpoint); - this.crawlerChekpoints.push(forwardCheckpoint); + this.crawlerCheckpoints.push(backCheckpoint); + this.crawlerCheckpoints.push(forwardCheckpoint); })); }; @@ -199,7 +199,7 @@ export default class EventIndexer { break; } - const checkpoint = this.crawlerChekpoints.shift(); + const checkpoint = this.crawlerCheckpoints.shift(); /// There is no checkpoint available currently, one may appear if // a sync with limited room timelines happens, so go back to sleep. @@ -222,7 +222,7 @@ export default class EventIndexer { checkpoint.direction); } catch (e) { console.log("EventIndex: Error crawling events:", e); - this.crawlerChekpoints.push(checkpoint); + this.crawlerCheckpoints.push(checkpoint); continue; } @@ -334,13 +334,13 @@ export default class EventIndexer { "added, stopping the crawl", checkpoint); await indexManager.removeCrawlerCheckpoint(newCheckpoint); } else { - this.crawlerChekpoints.push(newCheckpoint); + this.crawlerCheckpoints.push(newCheckpoint); } } catch (e) { console.log("EventIndex: Error durring a crawl", e); // An error occured, put the checkpoint back so we // can retry. - this.crawlerChekpoints.push(checkpoint); + this.crawlerCheckpoints.push(checkpoint); } } @@ -367,7 +367,7 @@ export default class EventIndexer { await indexManager.addCrawlerCheckpoint(backwardsCheckpoint); - this.crawlerChekpoints.push(backwardsCheckpoint); + this.crawlerCheckpoints.push(backwardsCheckpoint); } async deleteEventIndex() { From f2f8a82876a4dc46701ee71b79fa47bf697f7002 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 13 Nov 2019 03:31:51 +0000 Subject: [PATCH 0571/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1898 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 36 ++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 185026aad5..a4898bd328 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2284,5 +2284,39 @@ "You cancelled": "您已取消", "%(name)s cancelled": "%(name)s 已取消", "%(name)s wants to verify": "%(name)s 想要驗證", - "You sent a verification request": "您已傳送了驗證請求" + "You sent a verification request": "您已傳送了驗證請求", + "Try out new ways to ignore people (experimental)": "試用新的方式來忽略人們(實驗性)", + "My Ban List": "我的封鎖清單", + "This is your list of users/servers you have blocked - don't leave the room!": "這是您已封鎖的的使用者/伺服器清單,不要離開聊天室!", + "Ignored/Blocked": "已忽略/已封鎖", + "Error adding ignored user/server": "新增要忽略的使用者/伺服器錯誤", + "Something went wrong. Please try again or view your console for hints.": "有東西出問題了。請重試或檢視您的主控臺以取得更多資訊。", + "Error subscribing to list": "訂閱清單發生錯誤", + "Please verify the room ID or alias and try again.": "請驗證聊天室 ID 或別名並再試一次。", + "Error removing ignored user/server": "移除要忽略的使用者/伺服器發生錯誤", + "Error unsubscribing from list": "從清單取消訂閱時發生錯誤", + "Please try again or view your console for hints.": "請重試或檢視您的主控臺以取得更多資訊。", + "None": "無", + "Ban list rules - %(roomName)s": "封鎖清單規則 - %(roomName)s", + "Server rules": "伺服器規則", + "User rules": "使用者規則", + "You have not ignored anyone.": "您尚未忽略任何人。", + "You are currently ignoring:": "您目前正在忽略:", + "You are not subscribed to any lists": "您尚未訂閱任何清單", + "Unsubscribe": "取消訂閱", + "View rules": "檢視規則", + "You are currently subscribed to:": "您目前已訂閱:", + "⚠ These settings are meant for advanced users.": "⚠ 這些設定適用於進階使用者。", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "在此新增您想要忽略的使用者與伺服器。使用星號以讓 Riot 核對所有字元。舉例來說,@bot:* 將會忽略在任何伺服器上,所有有 'bot' 名稱的使用者。", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "忽略人們已透過封鎖清單完成,其中包含了誰要被封鎖的規則。訂閱封鎖清單代表被此清單封鎖的使用者/伺服器會對您隱藏。", + "Personal ban list": "個人封鎖清單", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "您的個人封鎖清單包含了您個人不想要看到的所有使用者/伺服器。在忽略您的第一個使用者/伺服器後,您的聊天室清單中會出現新的聊天室,其名為「我的封鎖清單」,留在這個聊天室裡面以讓封鎖清單生效。", + "Server or user ID to ignore": "要忽略的伺服器或使用者 ID", + "eg: @bot:* or example.org": "例子:@bot:* 或 example.org", + "Subscribed lists": "已訂閱的清單", + "Subscribing to a ban list will cause you to join it!": "訂閱封鎖清單會讓您加入它!", + "If this isn't what you want, please use a different tool to ignore users.": "如果這不是您想要的,請使用不同的工具來忽略使用者。", + "Room ID or alias of ban list": "聊天室 ID 或封鎖清單的別名", + "Subscribe": "訂閱", + "You have ignored this user, so their message is hidden. Show anyways.": "您已經忽略了這個使用者,所以他們的訊息會隱藏。無論如何都顯示。" } From 95b8e83cd3f97cf6ce3f2ff144bc505518e155c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Wed, 13 Nov 2019 10:31:31 +0000 Subject: [PATCH 0572/2372] Translated using Weblate (French) Currently translated at 100.0% (1898 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 40840c8d58..f4e889d955 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2291,5 +2291,39 @@ "You cancelled": "Vous avez annulé", "%(name)s cancelled": "%(name)s a annulé", "%(name)s wants to verify": "%(name)s veut vérifier", - "You sent a verification request": "Vous avez envoyé une demande de vérification" + "You sent a verification request": "Vous avez envoyé une demande de vérification", + "Try out new ways to ignore people (experimental)": "Essayez de nouvelles façons d’ignorer les gens (expérimental)", + "My Ban List": "Ma liste de bannissement", + "This is your list of users/servers you have blocked - don't leave the room!": "C’est la liste des utilisateurs/serveurs que vous avez bloqués − ne quittez pas le salon !", + "Ignored/Blocked": "Ignoré/bloqué", + "Error adding ignored user/server": "Erreur lors de l’ajout de l’utilisateur/du serveur ignoré", + "Something went wrong. Please try again or view your console for hints.": "Une erreur est survenue. Réessayez ou consultez votre console pour des indices.", + "Error subscribing to list": "Erreur lors de l’inscription à la liste", + "Please verify the room ID or alias and try again.": "Vérifiez l’identifiant ou l’alias du salon et réessayez.", + "Error removing ignored user/server": "Erreur lors de la suppression de l’utilisateur/du serveur ignoré", + "Error unsubscribing from list": "Erreur lors de la désinscription de la liste", + "Please try again or view your console for hints.": "Réessayez ou consultez votre console pour des indices.", + "None": "Aucun", + "Ban list rules - %(roomName)s": "Règles de la liste de bannissement − %(roomName)s", + "Server rules": "Règles de serveur", + "User rules": "Règles d’utilisateur", + "You have not ignored anyone.": "Vous n’avez ignoré personne.", + "You are currently ignoring:": "Vous ignorez actuellement :", + "You are not subscribed to any lists": "Vous n’êtes inscrit(e) à aucune liste", + "Unsubscribe": "Se désinscrire", + "View rules": "Voir les règles", + "You are currently subscribed to:": "Vous êtes actuellement inscrit(e) à :", + "⚠ These settings are meant for advanced users.": "⚠ Ces paramètres sont prévus pour les utilisateurs avancés.", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Ajoutez les utilisateurs et les serveurs que vous voulez ignorer ici. Utilisez des astérisques pour remplacer n’importe quel caractère. Par exemple, @bot:* ignorerait tous les utilisateurs qui ont le nom « bot » sur n’importe quel serveur.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignorer les gens est possible grâce à des listes de bannissement qui contiennent des règles sur les personnes à bannir. L’inscription à une liste de bannissement signifie que les utilisateurs/serveurs bloqués par cette liste seront cachés pour vous.", + "Personal ban list": "Liste de bannissement personnelle", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Votre liste de bannissement personnelle contient tous les utilisateurs/serveurs dont vous ne voulez pas voir les messages personnellement. Quand vous aurez ignoré votre premier utilisateur/serveur, un nouveau salon nommé « Ma liste de bannissement » apparaîtra dans la liste de vos salons − restez dans ce salon pour que la liste de bannissement soit effective.", + "Server or user ID to ignore": "Serveur ou identifiant d’utilisateur à ignorer", + "eg: @bot:* or example.org": "par ex. : @bot:* ou exemple.org", + "Subscribed lists": "Listes souscrites", + "Subscribing to a ban list will cause you to join it!": "En vous inscrivant à une liste de bannissement, vous la rejoindrez !", + "If this isn't what you want, please use a different tool to ignore users.": "Si ce n’est pas ce que vous voulez, utilisez un autre outil pour ignorer les utilisateurs.", + "Room ID or alias of ban list": "Identifiant ou alias du salon de la liste de bannissement", + "Subscribe": "S’inscrire", + "You have ignored this user, so their message is hidden. Show anyways.": "Vous avez ignoré cet utilisateur, donc ses messages sont cachés. Les montrer quand même." } From f0696dcc3f7922a007bf9b3f2b547de9d6b528d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Wed, 13 Nov 2019 01:12:50 +0000 Subject: [PATCH 0573/2372] Translated using Weblate (Korean) Currently translated at 98.4% (1868 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 3d01d5be67..01ef0e8ae1 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2135,5 +2135,9 @@ "You cancelled": "당신이 취소했습니다", "%(name)s cancelled": "%(name)s님이 취소했습니다", "%(name)s wants to verify": "%(name)s님이 확인을 요청합니다", - "You sent a verification request": "확인 요청을 보냈습니다" + "You sent a verification request": "확인 요청을 보냈습니다", + "Try out new ways to ignore people (experimental)": "새 방식으로 사람들을 무시하기 (실험)", + "My Ban List": "차단 목록", + "This is your list of users/servers you have blocked - don't leave the room!": "차단한 사용자/서버 목록입니다 - 방을 떠나지 마세요!", + "Ignored/Blocked": "무시됨/차단됨" } From cc2ee53824b955e513def52cf4a08118d853e646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 16:21:26 +0100 Subject: [PATCH 0574/2372] EventIndex: Add some more docs and fix some lint issues. --- src/BaseEventIndexManager.js | 2 +- src/BasePlatform.js | 6 ++++++ src/EventIndexPeg.js | 20 ++++++++++++++++---- src/components/structures/RoomView.js | 2 +- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index a74eac658a..48a96c4d88 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -168,7 +168,7 @@ export default class BaseEventIndexManager { async addHistoricEvents( events: [HistoricEvent], checkpoint: CrawlerCheckpoint | null = null, - oldCheckpoint: CrawlerCheckpoint | null = null, + oldCheckpoint: CrawlerCheckpoint | null = null, ): Promise { throw new Error("Unimplemented"); } diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 582ac24cb0..f6301fd173 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -153,6 +153,12 @@ export default class BasePlatform { throw new Error("Unimplemented"); } + /** + * Get our platform specific EventIndexManager. + * + * @return {BaseEventIndexManager} The EventIndex manager for our platform, + * can be null if the platform doesn't support event indexing. + */ getEventIndexingManager(): BaseEventIndexManager | null { return null; } diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 3ce88339eb..1b380e273f 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -34,16 +34,16 @@ class EventIndexPeg { /** * Get the current event index. * - * @Returns The EventIndex object for the application. Can be null - * if the platform doesn't support event indexing. + * @return {EventIndex} The current event index. */ get() { return this.index; } /** Create a new EventIndex and initialize it if the platform supports it. - * Returns true if an EventIndex was successfully initialized, false - * otherwise. + * + * @return {Promise} A promise that will resolve to true if an + * EventIndex was successfully initialized, false otherwise. */ async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); @@ -71,15 +71,27 @@ class EventIndexPeg { return true; } + /** + * Stop our event indexer. + */ stop() { if (this.index === null) return; this.index.stop(); this.index = null; } + /** + * Delete our event indexer. + * + * After a call to this the init() method will need to be called again. + * + * @return {Promise} A promise that will resolve once the event index is + * deleted. + */ async deleteEventIndex() { if (this.index === null) return; this.index.deleteEventIndex(); + this.index = null; } } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9fe54ad164..6dee60bec7 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1131,7 +1131,7 @@ module.exports = createReactClass({ this.searchId = new Date().getTime(); let roomId; - if (scope === "Room") roomId = this.state.room.roomId, + if (scope === "Room") roomId = this.state.room.roomId; debuglog("sending search request"); const searchPromise = eventSearch(term, roomId); From 368a77ec3ef318f7e0e55832bf97877b8575f737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 16:35:04 +0100 Subject: [PATCH 0575/2372] EventIndexing: Fix a style issue. --- src/EventIndexing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index af77979040..5830106e84 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -25,7 +25,7 @@ export default class EventIndexer { this.crawlerCheckpoints = []; // The time that the crawler will wait between /rooms/{room_id}/messages // requests - this._crawler_timeout = 3000; + this._crawlerTimeout = 3000; this._crawlerRef = null; this.liveEventsForIndex = new Set(); } From d4b31cb7e037301b0786372d3ae643c96b2b48e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 16:35:26 +0100 Subject: [PATCH 0576/2372] EventIndexing: Move the max events per crawl constant into the class. --- src/EventIndexing.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 5830106e84..77c4022480 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -26,6 +26,9 @@ export default class EventIndexer { // The time that the crawler will wait between /rooms/{room_id}/messages // requests this._crawlerTimeout = 3000; + // The maximum number of events our crawler should fetch in a single + // crawl. + this._eventsPerCrawl = 100; this._crawlerRef = null; this.liveEventsForIndex = new Set(); } @@ -218,7 +221,7 @@ export default class EventIndexer { try { res = await client._createMessagesRequest( - checkpoint.roomId, checkpoint.token, 100, + checkpoint.roomId, checkpoint.token, this._eventsPerCrawl, checkpoint.direction); } catch (e) { console.log("EventIndex: Error crawling events:", e); From 9b32ec10b43cc274df28d938610fbf8c4b53479b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 13 Nov 2019 16:47:21 +0100 Subject: [PATCH 0577/2372] EventIndexing: Use the correct timeout value. --- src/EventIndexing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 77c4022480..67bd894c67 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -193,7 +193,7 @@ export default class EventIndexer { // This is a low priority task and we don't want to spam our // Homeserver with /messages requests so we set a hefty timeout // here. - await sleep(this._crawler_timeout); + await sleep(this._crawlerTimeout); console.log("EventIndex: Running the crawler loop."); From 56ad164c69ab497efed2949de62b7282fe86da2e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 13 Nov 2019 14:01:07 -0700 Subject: [PATCH 0578/2372] Add a function to get the "base" theme for a theme Useful for trying to load the right assets first. See https://github.com/vector-im/riot-web/pull/11381 --- src/theme.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/theme.js b/src/theme.js index d479170792..8a15c606d7 100644 --- a/src/theme.js +++ b/src/theme.js @@ -60,6 +60,22 @@ function getCustomTheme(themeName) { return customTheme; } +/** + * Gets the underlying theme name for the given theme. This is usually the theme or + * CSS resource that the theme relies upon to load. + * @param {string} theme The theme name to get the base of. + * @returns {string} The base theme (typically "light" or "dark"). + */ +export function getBaseTheme(theme) { + if (!theme) return "light"; + if (theme.startsWith("custom-")) { + const customTheme = getCustomTheme(theme.substr(7)); + return customTheme.is_dark ? "dark-custom" : "light-custom"; + } + + return theme; // it's probably a base theme +} + /** * Called whenever someone changes the theme * From bc90789c71b5b6d90e445061b1692269c93dbf3c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Nov 2019 00:39:48 +0000 Subject: [PATCH 0579/2372] Remove unused promise utils method Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/utils/promise.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/utils/promise.js b/src/utils/promise.js index f7a2e7c3e7..8842bfa1b7 100644 --- a/src/utils/promise.js +++ b/src/utils/promise.js @@ -22,19 +22,6 @@ import Promise from "bluebird"; // Returns a promise which resolves with a given value after the given number of ms export const sleep = (ms: number, value: any): Promise => new Promise((resolve => { setTimeout(resolve, ms, value); })); -// Returns a promise which resolves when the input promise resolves with its value -// or when the timeout of ms is reached with the value of given timeoutValue -export async function timeout(promise: Promise, timeoutValue: any, ms: number): Promise { - const timeoutPromise = new Promise((resolve) => { - const timeoutId = setTimeout(resolve, ms, timeoutValue); - promise.then(() => { - clearTimeout(timeoutId); - }); - }); - - return Promise.race([promise, timeoutPromise]); -} - // Returns a Deferred export function defer(): {resolve: () => {}, reject: () => {}, promise: Promise} { let resolve; From 5c24547ef5215b00333da3e9e7b61a55f222f7f6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Nov 2019 09:37:26 +0000 Subject: [PATCH 0580/2372] re-add and actually use promise timeout util Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/settings/SetIdServer.js | 12 +++++------- src/utils/promise.js | 13 +++++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 126cdc9557..a7a2e01c22 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -26,6 +26,7 @@ import { getThreepidsWithBindStatus } from '../../../boundThreepids'; import IdentityAuthClient from "../../../IdentityAuthClient"; import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils"; import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils'; +import {timeout} from "../../../utils/promise"; // We'll wait up to this long when checking for 3PID bindings on the IS. const REACHABILITY_TIMEOUT = 10000; // ms @@ -245,14 +246,11 @@ export default class SetIdServer extends React.Component { let threepids = []; let currentServerReachable = true; try { - threepids = await Promise.race([ + threepids = await timeout( getThreepidsWithBindStatus(MatrixClientPeg.get()), - new Promise((resolve, reject) => { - setTimeout(() => { - reject(new Error("Timeout attempting to reach identity server")); - }, REACHABILITY_TIMEOUT); - }), - ]); + Promise.reject(new Error("Timeout attempting to reach identity server")), + REACHABILITY_TIMEOUT, + ); } catch (e) { currentServerReachable = false; console.warn( diff --git a/src/utils/promise.js b/src/utils/promise.js index 8842bfa1b7..f7a2e7c3e7 100644 --- a/src/utils/promise.js +++ b/src/utils/promise.js @@ -22,6 +22,19 @@ import Promise from "bluebird"; // Returns a promise which resolves with a given value after the given number of ms export const sleep = (ms: number, value: any): Promise => new Promise((resolve => { setTimeout(resolve, ms, value); })); +// Returns a promise which resolves when the input promise resolves with its value +// or when the timeout of ms is reached with the value of given timeoutValue +export async function timeout(promise: Promise, timeoutValue: any, ms: number): Promise { + const timeoutPromise = new Promise((resolve) => { + const timeoutId = setTimeout(resolve, ms, timeoutValue); + promise.then(() => { + clearTimeout(timeoutId); + }); + }); + + return Promise.race([promise, timeoutPromise]); +} + // Returns a Deferred export function defer(): {resolve: () => {}, reject: () => {}, promise: Promise} { let resolve; From 28d2e658a4d184d7f51d2423cc0cde5c6ad41986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 14 Nov 2019 14:13:49 +0100 Subject: [PATCH 0581/2372] EventIndexing: Don't scope the event index per user. --- src/BaseEventIndexManager.js | 5 +---- src/EventIndexPeg.js | 3 +-- src/EventIndexing.js | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index 48a96c4d88..073bdbec81 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -91,13 +91,10 @@ export default class BaseEventIndexManager { /** * Initialize the event index for the given user. * - * @param {string} userId The unique identifier of the logged in user that - * owns the index. - * * @return {Promise} A promise that will resolve when the event index is * initialized. */ - async initEventIndex(userId: string): Promise<> { + async initEventIndex(): Promise<> { throw new Error("Unimplemented"); } diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 1b380e273f..ff1b2099f2 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -56,10 +56,9 @@ class EventIndexPeg { } const index = new EventIndex(); - const userId = MatrixClientPeg.get().getUserId(); try { - await index.init(userId); + await index.init(); } catch (e) { console.log("EventIndex: Error initializing the event index", e); } diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 67bd894c67..1fc9197082 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -33,10 +33,10 @@ export default class EventIndexer { this.liveEventsForIndex = new Set(); } - async init(userId) { + async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (indexManager === null) return false; - indexManager.initEventIndex(userId); + indexManager.initEventIndex(); return true; } From 54dcaf130255c39abc89de8d67fab06f1d0bf712 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Nov 2019 13:52:17 +0000 Subject: [PATCH 0582/2372] Replace bluebird specific promise things. Fix uses of sync promise code. Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/GroupAddressPicker.js | 5 +++-- src/autocomplete/Autocompleter.js | 26 +++++++++------------- src/components/structures/GroupView.js | 12 +++++----- src/components/structures/TimelinePanel.js | 13 +++++------ src/rageshake/rageshake.js | 18 ++++++++++----- src/utils/promise.js | 17 ++++++++++++++ 6 files changed, 53 insertions(+), 38 deletions(-) diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index 7da37b6df1..793f5c9227 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -21,6 +21,7 @@ import MultiInviter from './utils/MultiInviter'; import { _t } from './languageHandler'; import MatrixClientPeg from './MatrixClientPeg'; import GroupStore from './stores/GroupStore'; +import {allSettled} from "./utils/promise"; export function showGroupInviteDialog(groupId) { return new Promise((resolve, reject) => { @@ -118,7 +119,7 @@ function _onGroupInviteFinished(groupId, addrs) { function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) { const matrixClient = MatrixClientPeg.get(); const errorList = []; - return Promise.all(addrs.map((addr) => { + return allSettled(addrs.map((addr) => { return GroupStore .addRoomToGroup(groupId, addr.address, addRoomsPublicly) .catch(() => { errorList.push(addr.address); }) @@ -138,7 +139,7 @@ function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) { groups.push(groupId); return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.related_groups', {groups}, ''); } - }).reflect(); + }); })).then(() => { if (errorList.length === 0) { return; diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index af2744950f..c385e13878 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -27,6 +27,7 @@ import UserProvider from './UserProvider'; import EmojiProvider from './EmojiProvider'; import NotifProvider from './NotifProvider'; import Promise from 'bluebird'; +import {timeout} from "../utils/promise"; export type SelectionRange = { beginning: boolean, // whether the selection is in the first block of the editor or not @@ -77,23 +78,16 @@ export default class Autocompleter { while the user is interacting with the list, which makes it difficult to predict whether an action will actually do what is intended */ - const completionsList = await Promise.all( - // Array of inspections of promises that might timeout. Instead of allowing a - // single timeout to reject the Promise.all, reflect each one and once they've all - // settled, filter for the fulfilled ones - this.providers.map(provider => - provider - .getCompletions(query, selection, force) - .timeout(PROVIDER_COMPLETION_TIMEOUT) - .reflect(), - ), - ); + const completionsList = await Promise.all(this.providers.map(provider => { + return timeout(provider.getCompletions(query, selection, force), null, PROVIDER_COMPLETION_TIMEOUT); + })); + + // map then filter to maintain the index for the map-operation, for this.providers to line up + return completionsList.map((completions, i) => { + if (!completions || !completions.length) return; - return completionsList.filter( - (inspection) => inspection.isFulfilled(), - ).map((completionsState, i) => { return { - completions: completionsState.value(), + completions, provider: this.providers[i], /* the currently matched "command" the completer tried to complete @@ -102,6 +96,6 @@ export default class Autocompleter { */ command: this.providers[i].getCurrentCommand(query, selection, force), }; - }); + }).filter(Boolean); } } diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 4056557a7c..776e7f0d6d 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -38,7 +38,7 @@ import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks"; import {Group} from "matrix-js-sdk"; -import {sleep} from "../../utils/promise"; +import {allSettled, sleep} from "../../utils/promise"; const LONG_DESC_PLACEHOLDER = _td( `

        HTML for your community's page

        @@ -99,11 +99,10 @@ const CategoryRoomList = createReactClass({ onFinished: (success, addrs) => { if (!success) return; const errorList = []; - Promise.all(addrs.map((addr) => { + allSettled(addrs.map((addr) => { return GroupStore .addRoomToGroupSummary(this.props.groupId, addr.address) - .catch(() => { errorList.push(addr.address); }) - .reflect(); + .catch(() => { errorList.push(addr.address); }); })).then(() => { if (errorList.length === 0) { return; @@ -276,11 +275,10 @@ const RoleUserList = createReactClass({ onFinished: (success, addrs) => { if (!success) return; const errorList = []; - Promise.all(addrs.map((addr) => { + allSettled(addrs.map((addr) => { return GroupStore .addUserToGroupSummary(addr.address) - .catch(() => { errorList.push(addr.address); }) - .reflect(); + .catch(() => { errorList.push(addr.address); }); })).then(() => { if (errorList.length === 0) { return; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index faa6f2564a..3dd5ea761e 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1064,8 +1064,6 @@ const TimelinePanel = createReactClass({ }); }; - let prom = this._timelineWindow.load(eventId, INITIAL_SIZE); - // if we already have the event in question, TimelineWindow.load // returns a resolved promise. // @@ -1074,9 +1072,13 @@ const TimelinePanel = createReactClass({ // quite slow. So we detect that situation and shortcut straight to // calling _reloadEvents and updating the state. - if (prom.isFulfilled()) { + const timeline = this.props.timelineSet.getTimelineForEvent(eventId); + if (timeline) { + // This is a hot-path optimization by skipping a promise tick + // by repeating a no-op sync branch in TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline onLoaded(); } else { + const prom = this._timelineWindow.load(eventId, INITIAL_SIZE); this.setState({ events: [], liveEvents: [], @@ -1084,11 +1086,8 @@ const TimelinePanel = createReactClass({ canForwardPaginate: false, timelineLoading: true, }); - - prom = prom.then(onLoaded, onError); + prom.then(onLoaded, onError); } - - prom.done(); }, // handle the completion of a timeline load or localEchoUpdate, by diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index d61956c925..ee1aed2294 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -136,6 +136,8 @@ class IndexedDBLogStore { this.id = "instance-" + Math.random() + Date.now(); this.index = 0; this.db = null; + + // these promises are cleared as soon as fulfilled this.flushPromise = null; // set if flush() is called whilst one is ongoing this.flushAgainPromise = null; @@ -208,15 +210,15 @@ class IndexedDBLogStore { */ flush() { // check if a flush() operation is ongoing - if (this.flushPromise && this.flushPromise.isPending()) { - if (this.flushAgainPromise && this.flushAgainPromise.isPending()) { - // this is the 3rd+ time we've called flush() : return the same - // promise. + if (this.flushPromise) { + if (this.flushAgainPromise) { + // this is the 3rd+ time we've called flush() : return the same promise. return this.flushAgainPromise; } - // queue up a flush to occur immediately after the pending one - // completes. + // queue up a flush to occur immediately after the pending one completes. this.flushAgainPromise = this.flushPromise.then(() => { + // clear this.flushAgainPromise + this.flushAgainPromise = null; return this.flush(); }); return this.flushAgainPromise; @@ -232,12 +234,16 @@ class IndexedDBLogStore { } const lines = this.logger.flush(); if (lines.length === 0) { + // clear this.flushPromise + this.flushPromise = null; resolve(); return; } const txn = this.db.transaction(["logs", "logslastmod"], "readwrite"); const objStore = txn.objectStore("logs"); txn.oncomplete = (event) => { + // clear this.flushPromise + this.flushPromise = null; resolve(); }; txn.onerror = (event) => { diff --git a/src/utils/promise.js b/src/utils/promise.js index f7a2e7c3e7..e6e6ccb5c8 100644 --- a/src/utils/promise.js +++ b/src/utils/promise.js @@ -47,3 +47,20 @@ export function defer(): {resolve: () => {}, reject: () => {}, promise: Promise} return {resolve, reject, promise}; } + +// Promise.allSettled polyfill until browser support is stable in Firefox +export function allSettled(promises: Promise[]): {status: string, value?: any, reason?: any}[] { + if (Promise.allSettled) { + return Promise.allSettled(promises); + } + + return Promise.all(promises.map((promise) => { + return promise.then(value => ({ + status: "fulfilled", + value, + })).catch(reason => ({ + status: "rejected", + reason, + })); + })); +} From 41f4f3ef823646bdb82b8de5e0d29f9d35d0b7ee Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Nov 2019 14:04:50 +0000 Subject: [PATCH 0583/2372] make end-to-end test failure more verbose Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- test/end-to-end-tests/src/usecases/signup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js index 391ce76441..fd2b948572 100644 --- a/test/end-to-end-tests/src/usecases/signup.js +++ b/test/end-to-end-tests/src/usecases/signup.js @@ -61,7 +61,7 @@ module.exports = async function signup(session, username, password, homeserver) await session.query(".mx_Field_valid #mx_RegistrationForm_password"); //check no errors const errorText = await session.tryGetInnertext('.mx_Login_error'); - assert.strictEqual(!!errorText, false); + assert.strictEqual(errorText, null); //submit form //await page.screenshot({path: "beforesubmit.png", fullPage: true}); await registerButton.click(); From b3760cdd6e10b387c4e534c3b8b611870463b7c5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Nov 2019 14:25:54 +0000 Subject: [PATCH 0584/2372] Replace usages of Promise.delay(...) with own utils Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- test/components/views/dialogs/InteractiveAuthDialog-test.js | 3 ++- test/components/views/rooms/MessageComposerInput-test.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index b14ea7c242..7612b43b48 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -26,6 +26,7 @@ import sdk from 'matrix-react-sdk'; import MatrixClientPeg from '../../../../src/MatrixClientPeg'; import * as test_utils from '../../../test-utils'; +import {sleep} from "../../../../src/utils/promise"; const InteractiveAuthDialog = sdk.getComponent( 'views.dialogs.InteractiveAuthDialog', @@ -107,7 +108,7 @@ describe('InteractiveAuthDialog', function() { }, })).toBe(true); // let the request complete - return Promise.delay(1); + return sleep(1); }).then(() => { expect(onFinished.callCount).toEqual(1); expect(onFinished.calledWithExactly(true, {a: 1})).toBe(true); diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index 1105a4af17..04a5c83ed0 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -8,6 +8,7 @@ import * as testUtils from '../../../test-utils'; import sdk from 'matrix-react-sdk'; const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput'); import MatrixClientPeg from '../../../../src/MatrixClientPeg'; +import {sleep} from "../../../../src/utils/promise"; function addTextToDraft(text) { const components = document.getElementsByClassName('public-DraftEditor-content'); @@ -49,7 +50,7 @@ xdescribe('MessageComposerInput', () => { // warnings // (please can we make the components not setState() after // they are unmounted?) - Promise.delay(10).done(() => { + sleep(10).done(() => { if (parentDiv) { ReactDOM.unmountComponentAtNode(parentDiv); parentDiv.remove(); From 51a65f388bc3339e7378636c7c75808b63061e3d Mon Sep 17 00:00:00 2001 From: random Date: Thu, 14 Nov 2019 14:15:42 +0000 Subject: [PATCH 0585/2372] Translated using Weblate (Italian) Currently translated at 99.9% (1896 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 48 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 6262315012..8c7edbadd8 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2224,5 +2224,51 @@ "%(count)s unread messages including mentions.|one": "1 citazione non letta.", "%(count)s unread messages.|one": "1 messaggio non letto.", "Unread messages.": "Messaggi non letti.", - "Show tray icon and minimize window to it on close": "Mostra icona in tray e usala alla chiusura della finestra" + "Show tray icon and minimize window to it on close": "Mostra icona in tray e usala alla chiusura della finestra", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Questa azione richiede l'accesso al server di identità predefinito per verificare un indirizzo email o numero di telefono, ma il server non ha termini di servizio.", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Try out new ways to ignore people (experimental)": "Prova nuovi metodi per ignorare persone (sperimentale)", + "Send verification requests in direct message": "Invia richieste di verifica in un messaggio diretto", + "My Ban List": "Mia lista ban", + "This is your list of users/servers you have blocked - don't leave the room!": "Questa è la lista degli utenti/server che hai bloccato - non lasciare la stanza!", + "Error adding ignored user/server": "Errore di aggiunta utente/server ignorato", + "Something went wrong. Please try again or view your console for hints.": "Qualcosa è andato storto. Riprova o controlla la console per suggerimenti.", + "Error subscribing to list": "Errore di iscrizione alla lista", + "Please verify the room ID or alias and try again.": "Verifica l'ID della stanza o l'alias e riprova.", + "Error removing ignored user/server": "Errore di rimozione utente/server ignorato", + "Error unsubscribing from list": "Errore di disiscrizione dalla lista", + "Please try again or view your console for hints.": "Riprova o controlla la console per suggerimenti.", + "None": "Nessuno", + "Ban list rules - %(roomName)s": "Regole lista banditi - %(roomName)s", + "Server rules": "Regole server", + "User rules": "Regole utente", + "You have not ignored anyone.": "Non hai ignorato nessuno.", + "You are currently ignoring:": "Attualmente stai ignorando:", + "You are not subscribed to any lists": "Non sei iscritto ad alcuna lista", + "Unsubscribe": "Disiscriviti", + "View rules": "Vedi regole", + "You are currently subscribed to:": "Attualmente sei iscritto a:", + "⚠ These settings are meant for advanced users.": "⚠ Queste opzioni sono pensate per utenti esperti.", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Aggiungi qui gli utenti e i server che vuoi ignorare. Usa l'asterisco perchè Riot consideri qualsiasi carattere. Ad esempio, @bot:* ignorerà tutti gli utenti che hanno il nome 'bot' su qualsiasi server.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Si possono ignorare persone attraverso liste di ban contenenti regole per chi bandire. Iscriversi ad una lista di ban significa che gli utenti/server bloccati da quella lista ti verranno nascosti.", + "Personal ban list": "Lista di ban personale", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "La tua lista personale di ban contiene tutti gli utenti/server da cui non vuoi vedere messaggi. Dopo aver ignorato il tuo primo utente/server, apparirà una nuova stanza nel tuo elenco stanze chiamata 'Mia lista ban' - resta in questa stanza per mantenere effettiva la lista ban.", + "Server or user ID to ignore": "Server o ID utente da ignorare", + "eg: @bot:* or example.org": "es: @bot:* o esempio.org", + "Subscribed lists": "Liste sottoscritte", + "Subscribing to a ban list will cause you to join it!": "Iscriversi ad una lista di ban implica di unirsi ad essa!", + "If this isn't what you want, please use a different tool to ignore users.": "Se non è ciò che vuoi, usa uno strumento diverso per ignorare utenti.", + "Room ID or alias of ban list": "ID stanza o alias della lista di ban", + "Subscribe": "Iscriviti", + "Message Actions": "Azioni messaggio", + "You have ignored this user, so their message is hidden. Show anyways.": "Hai ignorato questo utente, perciò il suo messaggio è nascosto. Mostra comunque.", + "You verified %(name)s": "Hai verificato %(name)s", + "You cancelled verifying %(name)s": "Hai annullato la verifica di %(name)s", + "%(name)s cancelled verifying": "%(name)s ha annullato la verifica", + "You accepted": "Hai accettato", + "%(name)s accepted": "%(name)s ha accettato", + "You cancelled": "Hai annullato", + "%(name)s cancelled": "%(name)s ha annullato", + "%(name)s wants to verify": "%(name)s vuole verificare", + "You sent a verification request": "Hai inviato una richiesta di verifica" } From 154fb7ecacc795facc748adf1e9be3a8dd05efde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Thu, 14 Nov 2019 03:08:03 +0000 Subject: [PATCH 0586/2372] Translated using Weblate (Korean) Currently translated at 99.4% (1887 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 01ef0e8ae1..fe8c929acd 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2139,5 +2139,24 @@ "Try out new ways to ignore people (experimental)": "새 방식으로 사람들을 무시하기 (실험)", "My Ban List": "차단 목록", "This is your list of users/servers you have blocked - don't leave the room!": "차단한 사용자/서버 목록입니다 - 방을 떠나지 마세요!", - "Ignored/Blocked": "무시됨/차단됨" + "Ignored/Blocked": "무시됨/차단됨", + "Error adding ignored user/server": "무시한 사용자/서버 추가 중 오류", + "Something went wrong. Please try again or view your console for hints.": "무언가 잘못되었습니다. 다시 시도하거나 콘솔을 통해 원인을 알아봐주세요.", + "Error subscribing to list": "목록으로 구독하는 중 오류", + "Please verify the room ID or alias and try again.": "방 ID나 별칭을 확인한 후 다시 시도해주세요.", + "Error removing ignored user/server": "무시한 사용자/서버를 지우는 중 오류", + "Error unsubscribing from list": "목록에서 구독 해제 중 오류", + "Please try again or view your console for hints.": "다시 시도하거나 콘솔을 통해 원인을 알아봐주세요.", + "None": "없음", + "Ban list rules - %(roomName)s": "차단 목록 규칙 - %(roomName)s", + "Server rules": "서버 규칙", + "User rules": "사용자 규칙", + "You have not ignored anyone.": "아무도 무시하고 있지 않습니다.", + "You are currently ignoring:": "현재 무시하고 있음:", + "You are not subscribed to any lists": "어느 목록에도 구독하고 있지 않습니다", + "Unsubscribe": "구독 해제", + "View rules": "규칙 보기", + "You are currently subscribed to:": "현재 구독 중임:", + "⚠ These settings are meant for advanced users.": "⚠ 이 설정은 고급 사용자를 위한 것입니다.", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "무시하고 싶은 사용자와 서버를 여기에 추가하세요. 별표(*)를 사용해서 Riot이 이름과 문자를 맞춰볼 수 있습니다. 예를 들어, @bot:*이라면 모든 서버에서 'bot'이라는 문자를 가진 이름의 모든 사용자를 무시합니다." } From bcb7ec081453b15fc2ea7640b5d1ace3e3e5e1f5 Mon Sep 17 00:00:00 2001 From: andriusign Date: Wed, 13 Nov 2019 20:45:07 +0000 Subject: [PATCH 0587/2372] Translated using Weblate (Lithuanian) Currently translated at 49.2% (934 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/lt/ --- src/i18n/strings/lt.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index 2aeb207387..5f3a76caa0 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -1117,5 +1117,6 @@ "Custom user status messages": "Pasirinktinės vartotojo būsenos žinutės", "Group & filter rooms by custom tags (refresh to apply changes)": "Grupuoti ir filtruoti kambarius pagal pasirinktines žymas (atnaujinkite, kad pritaikytumėte pakeitimus)", "Render simple counters in room header": "Užkrauti paprastus skaitiklius kambario antraštėje", - "Multiple integration managers": "Daugialypiai integracijų valdikliai" + "Multiple integration managers": "Daugialypiai integracijų valdikliai", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)" } From 448c9a82908b9e1504ed28a66dd1a68cb9daf9c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 14 Nov 2019 16:01:14 +0100 Subject: [PATCH 0588/2372] EventIndexPeg: Add a missing return statement. --- src/EventIndexPeg.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index ff1b2099f2..a4ab1815c9 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -61,6 +61,7 @@ class EventIndexPeg { await index.init(); } catch (e) { console.log("EventIndex: Error initializing the event index", e); + return false; } console.log("EventIndex: Successfully initialized the event index"); From 7516f2724aeb34f13ae379f7d5c2124beca1b5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 14 Nov 2019 16:13:22 +0100 Subject: [PATCH 0589/2372] EventIndexing: Rework the index initialization and deletion. --- src/BaseEventIndexManager.js | 10 +++++++++ src/EventIndexPeg.js | 43 ++++++++++++++++++++++++------------ src/EventIndexing.js | 40 +++++++++++++++++++-------------- src/Lifecycle.js | 1 + 4 files changed, 63 insertions(+), 31 deletions(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index 073bdbec81..4e52344e76 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -206,6 +206,16 @@ export default class BaseEventIndexManager { throw new Error("Unimplemented"); } + /** + * close our event index. + * + * @return {Promise} A promise that will resolve once the event index has + * been closed. + */ + async closeEventIndex(): Promise<> { + throw new Error("Unimplemented"); + } + /** * Delete our current event index. * diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index a4ab1815c9..dc25b11cf7 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -31,15 +31,6 @@ class EventIndexPeg { this.index = null; } - /** - * Get the current event index. - * - * @return {EventIndex} The current event index. - */ - get() { - return this.index; - } - /** Create a new EventIndex and initialize it if the platform supports it. * * @return {Promise} A promise that will resolve to true if an @@ -72,11 +63,30 @@ class EventIndexPeg { } /** - * Stop our event indexer. + * Get the current event index. + * + * @return {EventIndex} The current event index. */ + get() { + return this.index; + } + stop() { if (this.index === null) return; - this.index.stop(); + this.index.stopCrawler(); + } + + /** + * Unset our event store + * + * After a call to this the init() method will need to be called again. + * + * @return {Promise} A promise that will resolve once the event index is + * closed. + */ + async unset() { + if (this.index === null) return; + this.index.close(); this.index = null; } @@ -89,9 +99,14 @@ class EventIndexPeg { * deleted. */ async deleteEventIndex() { - if (this.index === null) return; - this.index.deleteEventIndex(); - this.index = null; + const indexManager = PlatformPeg.get().getEventIndexingManager(); + + if (indexManager !== null) { + this.stop(); + console.log("EventIndex: Deleting event index."); + await indexManager.deleteEventIndex(); + this.index = null; + } } } diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 1fc9197082..37167cf600 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -35,9 +35,7 @@ export default class EventIndexer { async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager === null) return false; - indexManager.initEventIndex(); - return true; + return indexManager.initEventIndex(); } async onSync(state, prevState, data) { @@ -198,7 +196,6 @@ export default class EventIndexer { console.log("EventIndex: Running the crawler loop."); if (cancelled) { - console.log("EventIndex: Cancelling the crawler."); break; } @@ -373,26 +370,35 @@ export default class EventIndexer { this.crawlerCheckpoints.push(backwardsCheckpoint); } + startCrawler() { + if (this._crawlerRef !== null) return; + + const crawlerHandle = {}; + this.crawlerFunc(crawlerHandle); + this._crawlerRef = crawlerHandle; + } + + stopCrawler() { + if (this._crawlerRef === null) return; + + this._crawlerRef.cancel(); + this._crawlerRef = null; + } + + async close() { + const indexManager = PlatformPeg.get().getEventIndexingManager(); + this.stopCrawler(); + return indexManager.closeEventIndex(); + } + async deleteEventIndex() { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (indexManager !== null) { - console.log("EventIndex: Deleting event index."); - this.crawlerRef.cancel(); + this.stopCrawler(); await indexManager.deleteEventIndex(); } } - startCrawler() { - const crawlerHandle = {}; - this.crawlerFunc(crawlerHandle); - this.crawlerRef = crawlerHandle; - } - - stop() { - this._crawlerRef.cancel(); - this._crawlerRef = null; - } - async search(searchArgs) { const indexManager = PlatformPeg.get().getEventIndexingManager(); return indexManager.searchEventIndex(searchArgs); diff --git a/src/Lifecycle.js b/src/Lifecycle.js index aa900c81a1..1d38934ade 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -662,6 +662,7 @@ export function stopMatrixClient(unsetClient=true) { if (unsetClient) { MatrixClientPeg.unset(); + EventIndexPeg.unset().done(); } } } From d82d4246e92800588c77ed74f3e4f957a554ffbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 14 Nov 2019 16:17:50 +0100 Subject: [PATCH 0590/2372] BaseEventIndexManager: Remove a return from a docstring. --- src/BaseEventIndexManager.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index 4e52344e76..fe59cee673 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -114,9 +114,6 @@ export default class BaseEventIndexManager { /** * Check if our event index is empty. - * - * @return {Promise} A promise that will resolve to true if the - * event index is empty, false otherwise. */ indexIsEmpty(): Promise { throw new Error("Unimplemented"); From eb0b0a400f72d8ada1e9018192eff00c42dcf250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 14 Nov 2019 16:18:36 +0100 Subject: [PATCH 0591/2372] EventIndexPeg: Remove the now unused import of MatrixClientPeg. --- src/EventIndexPeg.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index dc25b11cf7..da5c5425e4 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -24,7 +24,6 @@ limitations under the License. import PlatformPeg from "./PlatformPeg"; import EventIndex from "./EventIndexing"; -import MatrixClientPeg from "./MatrixClientPeg"; class EventIndexPeg { constructor() { From 413b90328fe3fc916045c194522cb0ba1721d4a4 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 14 Nov 2019 15:23:04 +0000 Subject: [PATCH 0592/2372] Show server details on login for unreachable homeserver This fixes the login page to be more helpful when the current homeserver is unreachable: it reveals the server change field, so you have some chance to progress forward. Fixes https://github.com/vector-im/riot-web/issues/11077 --- src/components/structures/auth/Login.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index ad77ed49a5..b35110bf6b 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -386,7 +386,11 @@ module.exports = createReactClass({ ...AutoDiscoveryUtils.authComponentStateForError(e), }); if (this.state.serverErrorIsFatal) { - return; // Server is dead - do not continue. + // Server is dead: show server details prompt instead + this.setState({ + phase: PHASE_SERVER_DETAILS, + }); + return; } } From 84f78ae7269400eba84ab42c37e8815c0db23fb6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Nov 2019 16:05:09 +0000 Subject: [PATCH 0593/2372] Revert ripping bluebird out of rageshake.js for time being Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/rageshake/rageshake.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index ee1aed2294..d61956c925 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -136,8 +136,6 @@ class IndexedDBLogStore { this.id = "instance-" + Math.random() + Date.now(); this.index = 0; this.db = null; - - // these promises are cleared as soon as fulfilled this.flushPromise = null; // set if flush() is called whilst one is ongoing this.flushAgainPromise = null; @@ -210,15 +208,15 @@ class IndexedDBLogStore { */ flush() { // check if a flush() operation is ongoing - if (this.flushPromise) { - if (this.flushAgainPromise) { - // this is the 3rd+ time we've called flush() : return the same promise. + if (this.flushPromise && this.flushPromise.isPending()) { + if (this.flushAgainPromise && this.flushAgainPromise.isPending()) { + // this is the 3rd+ time we've called flush() : return the same + // promise. return this.flushAgainPromise; } - // queue up a flush to occur immediately after the pending one completes. + // queue up a flush to occur immediately after the pending one + // completes. this.flushAgainPromise = this.flushPromise.then(() => { - // clear this.flushAgainPromise - this.flushAgainPromise = null; return this.flush(); }); return this.flushAgainPromise; @@ -234,16 +232,12 @@ class IndexedDBLogStore { } const lines = this.logger.flush(); if (lines.length === 0) { - // clear this.flushPromise - this.flushPromise = null; resolve(); return; } const txn = this.db.transaction(["logs", "logslastmod"], "readwrite"); const objStore = txn.objectStore("logs"); txn.oncomplete = (event) => { - // clear this.flushPromise - this.flushPromise = null; resolve(); }; txn.onerror = (event) => { From b05dabe2b7746975ba285d6e52b7594e27a4b8b6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 Nov 2019 12:02:16 -0700 Subject: [PATCH 0594/2372] Add better error handling to Synapse user deactivation Also clearly flag it as a Synapse user deactivation in the analytics, so we don't get confused. Fixes https://github.com/vector-im/riot-web/issues/10986 --- src/components/views/right_panel/UserInfo.js | 7 +++++-- src/components/views/rooms/MemberInfo.js | 11 ++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 207bf29998..7a88c80ce5 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -842,10 +842,13 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room const [accepted] = await finished; if (!accepted) return; try { - cli.deactivateSynapseUser(user.userId); + await cli.deactivateSynapseUser(user.userId); } catch (err) { + console.error("Failed to deactivate user"); + console.error(err); + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - Modal.createTrackedDialog('Failed to deactivate user', '', ErrorDialog, { + Modal.createTrackedDialog('Failed to deactivate Synapse user', '', ErrorDialog, { title: _t('Failed to deactivate user'), description: ((err && err.message) ? err.message : _t("Operation failed")), }); diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 2ea6392e96..9364f2f49d 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -550,7 +550,16 @@ module.exports = createReactClass({ danger: true, onFinished: (accepted) => { if (!accepted) return; - this.context.matrixClient.deactivateSynapseUser(this.props.member.userId); + this.context.matrixClient.deactivateSynapseUser(this.props.member.userId).catch(e => { + console.error("Failed to deactivate user"); + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to deactivate Synapse user', '', ErrorDialog, { + title: _t('Failed to deactivate user'), + description: ((e && e.message) ? e.message : _t("Operation failed")), + }); + }); }, }); }, From 0f2f500a16078b23e818340f4bc2491e2d0e3d56 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 Nov 2019 12:08:04 -0700 Subject: [PATCH 0595/2372] i18n update --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dc9773ad21..8f1344d5c9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -867,6 +867,7 @@ "Deactivate user?": "Deactivate user?", "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?", "Deactivate user": "Deactivate user", + "Failed to deactivate user": "Failed to deactivate user", "Failed to change power level": "Failed to change power level", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", "Are you sure?": "Are you sure?", @@ -1073,7 +1074,6 @@ "Remove this user from community?": "Remove this user from community?", "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", - "Failed to deactivate user": "Failed to deactivate user", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Sunday": "Sunday", From af4ad488bdf753edb514e13b88957a2ef103dcda Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 11 Nov 2019 16:54:12 +0100 Subject: [PATCH 0596/2372] Restyle Avatar Make it a circle with the profile picture centered, with a max height/width of 30vh --- res/css/views/right_panel/_UserInfo.scss | 27 +++++++++++++++----- src/components/views/right_panel/UserInfo.js | 18 +++++++++++-- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index df536a7388..db08fe18bf 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -38,6 +38,7 @@ limitations under the License. mask-repeat: no-repeat; mask-position: 16px center; background-color: $rightpanel-button-color; + position: absolute; } .mx_UserInfo_profile h2 { @@ -47,7 +48,7 @@ limitations under the License. } .mx_UserInfo h2 { - font-size: 16px; + font-size: 18px; font-weight: 600; margin: 16px 0 8px 0; } @@ -74,15 +75,27 @@ limitations under the License. } .mx_UserInfo_avatar { - background: $tagpanel-bg-color; + margin: 24px 32px 0 32px; } -.mx_UserInfo_avatar > img { - height: auto; - width: 100%; +.mx_UserInfo_avatar > div { + max-width: 30vh; + margin: 0 auto; +} + +.mx_UserInfo_avatar > div > div { + /* use padding-top instead of height to make this element square, + as the % in padding is a % of the width (including margin, + that's why we had to put the margin to center on a parent div), + and not a % of the parent height. */ + padding-top: 100%; + height: 0; + border-radius: 100%; max-height: 30vh; - object-fit: contain; - display: block; + box-sizing: content-box; + background-repeat: no-repeat; + background-size: cover; + background-position: center; } .mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image { diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 207bf29998..e55aae74f5 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -40,6 +40,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import {ContentRepo} from 'matrix-js-sdk'; const _disambiguateDevices = (devices) => { const names = Object.create(null); @@ -917,6 +918,12 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); }, [user.roomId, user.userId, room && room.currentState, cli]); // eslint-disable-line + const onMemberAvatarKey = e => { + if (e.key === "Enter") { + onMemberAvatarClick(); + } + }; + const onMemberAvatarClick = useCallback(() => { const member = user; const avatarUrl = member.getMxcAvatarUrl(); @@ -1045,8 +1052,15 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room let avatarElement; if (avatarUrl) { const httpUrl = cli.mxcUrlToHttp(avatarUrl, 800, 800); - avatarElement =
        - {_t("Profile + avatarElement =
        +
        ; } From f4988392f9cdec443ddd01c09bfb931f1beead7f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 16:25:38 +0100 Subject: [PATCH 0597/2372] restyle e2e icons --- res/css/views/rooms/_E2EIcon.scss | 53 +++++++++++++++++++++++---- res/img/e2e/verified.svg | 13 ++++++- res/img/e2e/warning.svg | 16 +++++--- src/components/views/rooms/E2EIcon.js | 8 +++- 4 files changed, 75 insertions(+), 15 deletions(-) diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index 84a16611de..c609d70f4c 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -17,17 +17,56 @@ limitations under the License. .mx_E2EIcon { width: 25px; height: 25px; - mask-repeat: no-repeat; - mask-position: center 0; margin: 0 9px; + position: relative; + display: block; } -.mx_E2EIcon_verified { - mask-image: url('$(res)/img/e2e/lock-verified.svg'); - background-color: $accent-color; +.mx_E2EIcon_verified::before, .mx_E2EIcon_warning::before { + content: ""; + display: block; + /* the symbols in the shield icons are cut out the make the themeable with css masking. + if they appear on a different background than white, the symbol wouldn't be white though, so we + add a rectangle here below the masked element to shine through the symbol cutout. + hardcoding white and not using a theme variable as this would probably be white for any theme. */ + background-color: white; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; } -.mx_E2EIcon_warning { - mask-image: url('$(res)/img/e2e/lock-warning.svg'); +.mx_E2EIcon_verified::after, .mx_E2EIcon_warning::after { + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + mask-repeat: no-repeat; + mask-size: contain; +} + +.mx_E2EIcon_verified::before { + /* white rectangle below checkmark of shield */ + margin: 25% 28% 38% 25%; +} + + +.mx_E2EIcon_verified::after { + mask-image: url('$(res)/img/e2e/verified.svg'); + background-color: $warning-color; +} + + +.mx_E2EIcon_warning::before { + /* white rectangle below "!" of shield */ + margin: 18% 40% 25% 40%; +} + +.mx_E2EIcon_warning::after { + mask-image: url('$(res)/img/e2e/warning.svg'); background-color: $warning-color; } diff --git a/res/img/e2e/verified.svg b/res/img/e2e/verified.svg index 459a552a40..af6bb92297 100644 --- a/res/img/e2e/verified.svg +++ b/res/img/e2e/verified.svg @@ -1,3 +1,12 @@ - - + + + diff --git a/res/img/e2e/warning.svg b/res/img/e2e/warning.svg index 3d5fba550c..2501da6ab3 100644 --- a/res/img/e2e/warning.svg +++ b/res/img/e2e/warning.svg @@ -1,6 +1,12 @@ - - - - - + + + diff --git a/src/components/views/rooms/E2EIcon.js b/src/components/views/rooms/E2EIcon.js index 54260e4ee2..d6baa30c8e 100644 --- a/src/components/views/rooms/E2EIcon.js +++ b/src/components/views/rooms/E2EIcon.js @@ -36,7 +36,13 @@ export default function(props) { _t("All devices for this user are trusted") : _t("All devices in this encrypted room are trusted"); } - const icon = (
        ); + + let style = null; + if (props.size) { + style = {width: `${props.size}px`, height: `${props.size}px`}; + } + + const icon = (
        ); if (props.onClick) { return ({ icon }); } else { From 3e356756aae08459f22e71e25e33fcbc50d880b7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 16:26:26 +0100 Subject: [PATCH 0598/2372] style profile info --- res/css/views/right_panel/_UserInfo.scss | 40 +++++++++++--------- src/components/views/right_panel/UserInfo.js | 12 +++--- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index db08fe18bf..aee0252c4e 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -22,13 +22,6 @@ limitations under the License. overflow-y: auto; } -.mx_UserInfo_profile .mx_E2EIcon { - display: inline; - margin: auto; - padding-right: 25px; - mask-size: contain; -} - .mx_UserInfo_cancel { height: 16px; width: 16px; @@ -41,16 +34,10 @@ limitations under the License. position: absolute; } -.mx_UserInfo_profile h2 { - flex: 1; - overflow-x: auto; - max-height: 50px; -} - .mx_UserInfo h2 { font-size: 18px; font-weight: 600; - margin: 16px 0 8px 0; + margin: 0; } .mx_UserInfo_container { @@ -76,6 +63,7 @@ limitations under the License. .mx_UserInfo_avatar { margin: 24px 32px 0 32px; + cursor: pointer; } .mx_UserInfo_avatar > div { @@ -110,12 +98,30 @@ limitations under the License. margin: 4px 0; } -.mx_UserInfo_profileField { - font-size: 15px; - position: relative; +.mx_UserInfo_profile { + font-size: 12px; text-align: center; + + h2 { + flex: 1; + overflow-x: auto; + max-height: 50px; + display: flex; + justify-self: ; + justify-content: center; + align-items: center; + + .mx_E2EIcon { + margin: 5px; + } + } + + .mx_UserInfo_profileStatus { + margin-top: 12px; + } } + .mx_UserInfo_memberDetails { text-align: center; } diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index e55aae74f5..43c5833faa 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1157,7 +1157,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room let e2eIcon; if (isRoomEncrypted && devices) { - e2eIcon = ; + e2eIcon = ; } return ( @@ -1167,16 +1167,14 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
        -
        -

        +
        +

        { e2eIcon } { displayName }

        -
        - { user.userId } -
        -
        +
        { user.userId }
        +
        {presenceLabel} {statusLabel}
        From b475bc9e912f0764f6a759e2434ea806fedf1e5b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 16:40:07 +0100 Subject: [PATCH 0599/2372] Add direct message button While we don't have canonical DMs yet, it takes you to the most recently active DM room --- src/components/views/right_panel/UserInfo.js | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 43c5833faa..0c058a8859 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -186,6 +186,26 @@ const DirectChatsSection = withLegacyMatrixClient(({matrixClient: cli, userId, s ); }); +function openDMForUser(cli, userId) { + const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); + const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { + const room = cli.getRoom(roomId); + if (!lastActiveRoom || (room && lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp())) { + return room; + } + return lastActiveRoom; + }, null); + + if (lastActiveRoom) { + dis.dispatch({ + action: 'view_room', + room_id: lastActiveRoom.roomId, + }); + } else { + createRoom({dmUserId: userId}); + } +} + const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite}) => { let ignoreButton = null; let insertPillButton = null; @@ -286,10 +306,20 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i ); + let directMessageButton; + if (!isMe) { + directMessageButton = ( + openDMForUser(cli, member.userId)} className="mx_UserInfo_field"> + { _t('Direct message') } + + ); + } + return (

        { _t("User Options") }

        + { directMessageButton } { readReceiptButton } { shareUserButton } { insertPillButton } From 0a2255ce7303d0fbdcfefb6f7a107b168c0c533a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 17:32:34 +0100 Subject: [PATCH 0600/2372] fixup: bring back margin above display name --- res/css/views/right_panel/_UserInfo.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index aee0252c4e..07b8ed2879 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -37,7 +37,7 @@ limitations under the License. .mx_UserInfo h2 { font-size: 18px; font-weight: 600; - margin: 0; + margin: 18px 0 0 0; } .mx_UserInfo_container { From 8dd7d8e5c0fdb9f4166a85a15e8bf3a0e2b62ab8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 17:33:24 +0100 Subject: [PATCH 0601/2372] fixup: don't consider left DM rooms --- src/components/views/right_panel/UserInfo.js | 7 +++++-- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 0c058a8859..c008cfe1f0 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -190,7 +190,10 @@ function openDMForUser(cli, userId) { const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { const room = cli.getRoom(roomId); - if (!lastActiveRoom || (room && lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp())) { + if (!room || room.getMyMembership() === "leave") { + return lastActiveRoom; + } + if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) { return room; } return lastActiveRoom; @@ -317,7 +320,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i return (
        -

        { _t("User Options") }

        +

        { _t("Options") }

        { directMessageButton } { readReceiptButton } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dc9773ad21..4b86c399a4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1068,6 +1068,8 @@ "Files": "Files", "Trust & Devices": "Trust & Devices", "Direct messages": "Direct messages", + "Direct message": "Direct message", + "Options": "Options", "Remove from community": "Remove from community", "Disinvite this user from community?": "Disinvite this user from community?", "Remove this user from community?": "Remove this user from community?", @@ -1091,7 +1093,6 @@ "Reply": "Reply", "Edit": "Edit", "Message Actions": "Message Actions", - "Options": "Options", "Attachment": "Attachment", "Error decrypting attachment": "Error decrypting attachment", "Decrypt %(text)s": "Decrypt %(text)s", From 238555f4ec20ed6fa60be311e18d52cf0d447a4e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 17:34:35 +0100 Subject: [PATCH 0602/2372] fixup: isMe --- src/components/views/right_panel/UserInfo.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index c008cfe1f0..1964b5601c 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -215,6 +215,10 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i let inviteUserButton = null; let readReceiptButton = null; + const isMe = member.userId === cli.getUserId(); + + + const onShareUserClick = () => { const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); Modal.createTrackedDialog('share room member dialog', '', ShareDialog, { @@ -224,7 +228,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i // Only allow the user to ignore the user if its not ourselves // same goes for jumping to read receipt - if (member.userId !== cli.getUserId()) { + if (!isMe) { const onIgnoreToggle = () => { const ignoredUsers = cli.getIgnoredUsers(); if (isIgnored) { From bd2bf4500adf8d753b857b210c15e849faf8b682 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 17:35:38 +0100 Subject: [PATCH 0603/2372] remove direct message list from UserInfo --- src/components/views/right_panel/UserInfo.js | 107 ------------------- 1 file changed, 107 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 1964b5601c..412bf92831 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -89,103 +89,6 @@ const DevicesSection = ({devices, userId, loading}) => { ); }; -const onRoomTileClick = (roomId) => { - dis.dispatch({ - action: 'view_room', - room_id: roomId, - }); -}; - -const DirectChatsSection = withLegacyMatrixClient(({matrixClient: cli, userId, startUpdating, stopUpdating}) => { - const onNewDMClick = async () => { - startUpdating(); - await createRoom({dmUserId: userId}); - stopUpdating(); - }; - - // TODO: Immutable DMs replaces a lot of this - // dmRooms will not include dmRooms that we have been invited into but did not join. - // Because DMRoomMap runs off account_data[m.direct] which is only set on join of dm room. - // XXX: we potentially want DMs we have been invited to, to also show up here :L - // especially as logic below concerns specially if we haven't joined but have been invited - const [dmRooms, setDmRooms] = useState(new DMRoomMap(cli).getDMRoomsForUserId(userId)); - - // TODO bind the below - // cli.on("Room", this.onRoom); - // cli.on("Room.name", this.onRoomName); - // cli.on("deleteRoom", this.onDeleteRoom); - - const accountDataHandler = useCallback((ev) => { - if (ev.getType() === "m.direct") { - const dmRoomMap = new DMRoomMap(cli); - setDmRooms(dmRoomMap.getDMRoomsForUserId(userId)); - } - }, [cli, userId]); - useEventEmitter(cli, "accountData", accountDataHandler); - - const RoomTile = sdk.getComponent("rooms.RoomTile"); - - const tiles = []; - for (const roomId of dmRooms) { - const room = cli.getRoom(roomId); - if (room) { - const myMembership = room.getMyMembership(); - // not a DM room if we have are not joined - if (myMembership !== 'join') continue; - - const them = room.getMember(userId); - // not a DM room if they are not joined - if (!them || !them.membership || them.membership !== 'join') continue; - - const highlight = room.getUnreadNotificationCount('highlight') > 0; - - tiles.push( - , - ); - } - } - - const labelClasses = classNames({ - mx_UserInfo_createRoom_label: true, - mx_RoomTile_name: true, - }); - - let body = tiles; - if (!body) { - body = ( - -
        - {_t("Start -
        -
        { _t("Start a chat") }
        -
        - ); - } - - return ( -
        -
        -

        { _t("Direct messages") }

        - -
        - { body } -
        - ); -}); - function openDMForUser(cli, userId) { const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { @@ -217,8 +120,6 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i const isMe = member.userId === cli.getUserId(); - - const onShareUserClick = () => { const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); Modal.createTrackedDialog('share room member dialog', '', ShareDialog, { @@ -979,11 +880,6 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room let synapseDeactivateButton; let spinner; - let directChatsSection; - if (user.userId !== cli.getUserId()) { - directChatsSection = ; - } - // We don't need a perfect check here, just something to pass as "probably not our homeserver". If // someone does figure out how to bypass this check the worst that happens is an error. // FIXME this should be using cli instead of MatrixClientPeg.matrixClient @@ -1226,9 +1122,6 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room { devicesSection } - - { directChatsSection } - Date: Wed, 13 Nov 2019 12:09:20 +0100 Subject: [PATCH 0604/2372] update when room encryption is turned on also don't download devices as long as room is not encrypted --- src/components/views/right_panel/UserInfo.js | 31 +++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 412bf92831..28b5af358a 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -64,6 +64,18 @@ const _getE2EStatus = (devices) => { return hasUnverifiedDevice ? "warning" : "verified"; }; +function useIsEncrypted(cli, room) { + const [isEncrypted, setIsEncrypted] = useState(cli.isRoomEncrypted(room.roomId)); + + const update = useCallback((event) => { + if (event.getType() === "m.room.encryption") { + setIsEncrypted(cli.isRoomEncrypted(room.roomId)); + } + }, [cli, room]); + useEventEmitter(room.currentState, "RoomState.events", update); + return isEncrypted; +} + const DevicesSection = ({devices, userId, loading}) => { const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo'); const Spinner = sdk.getComponent("elements.Spinner"); @@ -1005,6 +1017,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room title={_t('Close')} />; } + const isRoomEncrypted = useIsEncrypted(cli, room); // undefined means yet to be loaded, null means failed to load, otherwise list of devices const [devices, setDevices] = useState(undefined); // Download device lists @@ -1029,14 +1042,15 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room setDevices(null); } } - - _downloadDeviceList(); + if (isRoomEncrypted) { + _downloadDeviceList(); + } // Handle being unmounted return () => { cancelled = true; }; - }, [cli, user.userId]); + }, [cli, user.userId, isRoomEncrypted]); // Listen to changes useEffect(() => { @@ -1053,16 +1067,19 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room } }; - cli.on("deviceVerificationChanged", onDeviceVerificationChanged); + if (isRoomEncrypted) { + cli.on("deviceVerificationChanged", onDeviceVerificationChanged); + } // Handle being unmounted return () => { cancel = true; - cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged); + if (isRoomEncrypted) { + cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged); + } }; - }, [cli, user.userId]); + }, [cli, user.userId, isRoomEncrypted]); let devicesSection; - const isRoomEncrypted = _enableDevices && room && cli.isRoomEncrypted(room.roomId); if (isRoomEncrypted) { devicesSection = ; } else { From e32a948d5d15a44b6dd2cb6a1615a69e8fa8fd8e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 12:10:30 +0100 Subject: [PATCH 0605/2372] add "unverify user" action to user info --- src/components/views/right_panel/UserInfo.js | 23 +++++++++++++++++++- src/i18n/strings/en_EN.json | 1 + 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 28b5af358a..c61746293e 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -64,6 +64,17 @@ const _getE2EStatus = (devices) => { return hasUnverifiedDevice ? "warning" : "verified"; }; +async function unverifyUser(matrixClient, userId) { + const devices = await matrixClient.getStoredDevicesForUser(userId); + for (const device of devices) { + if (device.isVerified()) { + matrixClient.setDeviceVerified( + userId, device.deviceId, false, + ); + } + } +} + function useIsEncrypted(cli, room) { const [isEncrypted, setIsEncrypted] = useState(cli.isRoomEncrypted(room.roomId)); @@ -124,7 +135,7 @@ function openDMForUser(cli, userId) { } } -const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite}) => { +const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => { let ignoreButton = null; let insertPillButton = null; let inviteUserButton = null; @@ -234,6 +245,14 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i ); } + let unverifyButton; + if (devices && devices.some(device => device.isVerified())) { + unverifyButton = ( + unverifyUser(cli, member.userId)} className="mx_UserInfo_field"> + { _t('Unverify user') } + + ); + } return (
        @@ -245,6 +264,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i { insertPillButton } { ignoreButton } { inviteUserButton } + { unverifyButton }
        ); @@ -1140,6 +1160,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room { devicesSection } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4b86c399a4..fcf43af31f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1069,6 +1069,7 @@ "Trust & Devices": "Trust & Devices", "Direct messages": "Direct messages", "Direct message": "Direct message", + "Unverify user": "Unverify user", "Options": "Options", "Remove from community": "Remove from community", "Disinvite this user from community?": "Disinvite this user from community?", From 4a1dc5567341c478fefd275010dabf81329f3efb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 12:11:06 +0100 Subject: [PATCH 0606/2372] fixup: rearrange openDMForUser --- src/components/views/right_panel/UserInfo.js | 46 ++++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index c61746293e..e7277a52e2 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -75,6 +75,29 @@ async function unverifyUser(matrixClient, userId) { } } +function openDMForUser(matrixClient, userId) { + const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); + const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { + const room = matrixClient.getRoom(roomId); + if (!room || room.getMyMembership() === "leave") { + return lastActiveRoom; + } + if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) { + return room; + } + return lastActiveRoom; + }, null); + + if (lastActiveRoom) { + dis.dispatch({ + action: 'view_room', + room_id: lastActiveRoom.roomId, + }); + } else { + createRoom({dmUserId: userId}); + } +} + function useIsEncrypted(cli, room) { const [isEncrypted, setIsEncrypted] = useState(cli.isRoomEncrypted(room.roomId)); @@ -112,29 +135,6 @@ const DevicesSection = ({devices, userId, loading}) => { ); }; -function openDMForUser(cli, userId) { - const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); - const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { - const room = cli.getRoom(roomId); - if (!room || room.getMyMembership() === "leave") { - return lastActiveRoom; - } - if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) { - return room; - } - return lastActiveRoom; - }, null); - - if (lastActiveRoom) { - dis.dispatch({ - action: 'view_room', - room_id: lastActiveRoom.roomId, - }); - } else { - createRoom({dmUserId: userId}); - } -} - const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => { let ignoreButton = null; let insertPillButton = null; From 6afeeddb36cba22823b815d37a20fb6de158ca27 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 15:59:49 +0100 Subject: [PATCH 0607/2372] hide verified devices by default with expand button --- src/components/views/right_panel/UserInfo.js | 66 +++++++++++++------- src/i18n/strings/en_EN.json | 6 +- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index e7277a52e2..1e59ec2c44 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -114,6 +114,8 @@ const DevicesSection = ({devices, userId, loading}) => { const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo'); const Spinner = sdk.getComponent("elements.Spinner"); + const [isExpanded, setExpanded] = useState(false); + if (loading) { // still loading return ; @@ -121,16 +123,37 @@ const DevicesSection = ({devices, userId, loading}) => { if (devices === null) { return _t("Unable to load device list"); } - if (devices.length === 0) { - return _t("No devices with registered encryption keys"); + + const unverifiedDevices = devices.filter(d => !d.isVerified()); + const verifiedDevices = devices.filter(d => d.isVerified()); + + let expandButton; + if (verifiedDevices.length) { + if (isExpanded) { + expandButton = ( setExpanded(false)}> + {_t("Hide verified Sign-In's")} + ); + } else { + expandButton = ( setExpanded(true)}> + {_t("%(count)s verified Sign-In's", {count: verifiedDevices.length})} + ); + } + } + + let deviceList = unverifiedDevices.map((device, i) => { + return (); + }); + if (isExpanded) { + const keyStart = unverifiedDevices.length; + deviceList = deviceList.concat(verifiedDevices.map((device, i) => { + return (); + })); } return ( -
        -

        { _t("Trust & Devices") }

        -
        - { devices.map((device, i) => ) } -
        +
        +
        {deviceList}
        +
        {expandButton}
        ); }; @@ -1099,12 +1122,8 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room }; }, [cli, user.userId, isRoomEncrypted]); - let devicesSection; - if (isRoomEncrypted) { - devicesSection = ; - } else { - let text; - + let text; + if (!isRoomEncrypted) { if (!_enableDevices) { text = _t("This client does not support end-to-end encryption."); } else if (room) { @@ -1112,19 +1131,18 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room } else { // TODO what to render for GroupMember } - - if (text) { - devicesSection = ( -
        -

        { _t("Trust & Devices") }

        -
        - { text } -
        -
        - ); - } + } else { + text = _t("Messages in this room are end-to-end encrypted."); } + const devicesSection = ( +
        +

        { _t("Security") }

        +

        { text }

        + +
        + ); + let e2eIcon; if (isRoomEncrypted && devices) { e2eIcon = ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fcf43af31f..9322e71b19 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1066,8 +1066,10 @@ "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", "Members": "Members", "Files": "Files", - "Trust & Devices": "Trust & Devices", "Direct messages": "Direct messages", + "Hide verified Sign-In's": "Hide verified Sign-In's", + "%(count)s verified Sign-In's|one": "1 verified Sign-In", + "%(count)s verified Sign-In's|other": "%(count)s verified Sign-In's", "Direct message": "Direct message", "Unverify user": "Unverify user", "Options": "Options", @@ -1079,6 +1081,8 @@ "Failed to deactivate user": "Failed to deactivate user", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", + "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", + "Security": "Security", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", From 04731d0ae331420123683ba6cbaa366ffd99c44b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 16:00:13 +0100 Subject: [PATCH 0608/2372] RoomState.events fired on RoomState object, not room --- src/components/views/right_panel/UserInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 1e59ec2c44..c1a6442409 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -346,7 +346,7 @@ const useRoomPowerLevels = (room) => { }; }, [room]); - useEventEmitter(room, "RoomState.events", update); + useEventEmitter(room.currentState, "RoomState.events", update); useEffect(() => { update(); return () => { From 73b6575082820216f0d62ff70d634224a3aa6ae2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 17:56:04 +0100 Subject: [PATCH 0609/2372] fixup: verified shield should be green --- res/css/views/rooms/_E2EIcon.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index c609d70f4c..c8b1be47f9 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -57,7 +57,7 @@ limitations under the License. .mx_E2EIcon_verified::after { mask-image: url('$(res)/img/e2e/verified.svg'); - background-color: $warning-color; + background-color: $accent-color; } From 0bd1e7112df59a3dabde0946c4926347d2c6779f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 17:59:22 +0100 Subject: [PATCH 0610/2372] style security section as per design --- res/css/views/right_panel/_UserInfo.scss | 325 ++++++++++--------- src/components/views/right_panel/UserInfo.js | 53 ++- src/i18n/strings/en_EN.json | 5 +- 3 files changed, 213 insertions(+), 170 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 07b8ed2879..79211bb38a 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -20,175 +20,182 @@ limitations under the License. flex-direction: column; flex: 1; overflow-y: auto; -} - -.mx_UserInfo_cancel { - height: 16px; - width: 16px; - padding: 10px 0 10px 10px; - cursor: pointer; - mask-image: url('$(res)/img/minimise.svg'); - mask-repeat: no-repeat; - mask-position: 16px center; - background-color: $rightpanel-button-color; - position: absolute; -} - -.mx_UserInfo h2 { - font-size: 18px; - font-weight: 600; - margin: 18px 0 0 0; -} - -.mx_UserInfo_container { - padding: 0 16px 16px 16px; - border-bottom: 1px solid lightgray; -} - -.mx_UserInfo_memberDetailsContainer { - padding-bottom: 0; -} - -.mx_UserInfo .mx_RoomTile_nameContainer { - width: 154px; -} - -.mx_UserInfo .mx_RoomTile_badge { - display: none; -} - -.mx_UserInfo .mx_RoomTile_name { - width: 160px; -} - -.mx_UserInfo_avatar { - margin: 24px 32px 0 32px; - cursor: pointer; -} - -.mx_UserInfo_avatar > div { - max-width: 30vh; - margin: 0 auto; -} - -.mx_UserInfo_avatar > div > div { - /* use padding-top instead of height to make this element square, - as the % in padding is a % of the width (including margin, - that's why we had to put the margin to center on a parent div), - and not a % of the parent height. */ - padding-top: 100%; - height: 0; - border-radius: 100%; - max-height: 30vh; - box-sizing: content-box; - background-repeat: no-repeat; - background-size: cover; - background-position: center; -} - -.mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image { - cursor: zoom-in; -} - -.mx_UserInfo h3 { - text-transform: uppercase; - color: $input-darker-fg-color; - font-weight: bold; font-size: 12px; - margin: 4px 0; -} -.mx_UserInfo_profile { - font-size: 12px; - text-align: center; + .mx_UserInfo_cancel { + height: 16px; + width: 16px; + padding: 10px 0 10px 10px; + cursor: pointer; + mask-image: url('$(res)/img/minimise.svg'); + mask-repeat: no-repeat; + mask-position: 16px center; + background-color: $rightpanel-button-color; + position: absolute; + } h2 { - flex: 1; - overflow-x: auto; - max-height: 50px; - display: flex; - justify-self: ; - justify-content: center; - align-items: center; + font-size: 18px; + font-weight: 600; + margin: 18px 0 0 0; + } - .mx_E2EIcon { - margin: 5px; + .mx_UserInfo_container { + padding: 0 16px 16px 16px; + border-bottom: 1px solid lightgray; + } + + .mx_UserInfo_memberDetailsContainer { + padding-bottom: 0; + } + + .mx_RoomTile_nameContainer { + width: 154px; + } + + .mx_RoomTile_badge { + display: none; + } + + .mx_RoomTile_name { + width: 160px; + } + + .mx_UserInfo_avatar { + margin: 24px 32px 0 32px; + cursor: pointer; + } + + .mx_UserInfo_avatar > div { + max-width: 30vh; + margin: 0 auto; + } + + .mx_UserInfo_avatar > div > div { + /* use padding-top instead of height to make this element square, + as the % in padding is a % of the width (including margin, + that's why we had to put the margin to center on a parent div), + and not a % of the parent height. */ + padding-top: 100%; + height: 0; + border-radius: 100%; + max-height: 30vh; + box-sizing: content-box; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + } + + .mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image { + cursor: zoom-in; + } + + h3 { + text-transform: uppercase; + color: $notice-secondary-color; + font-weight: bold; + font-size: 12px; + margin: 4px 0; + } + + p { + margin: 5px 0; + } + + .mx_UserInfo_profile { + text-align: center; + + h2 { + font-size: 18px; + line-height: 25px; + flex: 1; + overflow-x: auto; + max-height: 50px; + display: flex; + justify-self: ; + justify-content: center; + align-items: center; + + .mx_E2EIcon { + margin: 5px; + } + } + + .mx_UserInfo_profileStatus { + margin-top: 12px; } } - .mx_UserInfo_profileStatus { - margin-top: 12px; + .mx_UserInfo_memberDetails { + text-align: center; } -} + .mx_UserInfo_field { + cursor: pointer; + color: $accent-color; + line-height: 16px; + margin: 8px 0; -.mx_UserInfo_memberDetails { - text-align: center; -} - -.mx_UserInfo_field { - cursor: pointer; - font-size: 15px; - color: $primary-fg-color; - margin-left: 8px; - line-height: 23px; -} - -.mx_UserInfo_createRoom { - cursor: pointer; - display: flex; - align-items: center; - padding: 0 8px; -} - -.mx_UserInfo_createRoom_label { - width: initial !important; - cursor: pointer; -} - -.mx_UserInfo_statusMessage { - font-size: 11px; - opacity: 0.5; - overflow: hidden; - white-space: nowrap; - text-overflow: clip; -} -.mx_UserInfo .mx_UserInfo_scrollContainer { - flex: 1; - padding-bottom: 16px; -} - -.mx_UserInfo .mx_UserInfo_scrollContainer .mx_UserInfo_container { - padding-top: 16px; - padding-bottom: 0; - border-bottom: none; -} - -.mx_UserInfo_container_header { - display: flex; -} - -.mx_UserInfo_container_header_right { - position: relative; - margin-left: auto; -} - -.mx_UserInfo_newDmButton { - background-color: $roomheader-addroom-bg-color; - border-radius: 10px; // 16/2 + 2 padding - height: 16px; - flex: 0 0 16px; - - &::before { - background-color: $roomheader-addroom-fg-color; - mask: url('$(res)/img/icons-room-add.svg'); - mask-repeat: no-repeat; - mask-position: center; - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; } + + .mx_UserInfo_statusMessage { + font-size: 11px; + opacity: 0.5; + overflow: hidden; + white-space: nowrap; + text-overflow: clip; + } + + .mx_UserInfo_scrollContainer { + flex: 1; + padding-bottom: 16px; + } + + .mx_UserInfo_scrollContainer .mx_UserInfo_container { + padding-top: 16px; + padding-bottom: 0; + border-bottom: none; + + >:not(h3) { + margin-left: 8px; + } + } + + .mx_UserInfo_devices { + .mx_UserInfo_device { + display: flex; + + &.mx_UserInfo_device_verified { + .mx_UserInfo_device_trusted { + color: $accent-color; + } + } + &.mx_UserInfo_device_unverified { + .mx_UserInfo_device_trusted { + color: $warning-color; + } + } + + .mx_UserInfo_device_name { + flex: 1; + margin-right: 5px; + } + } + + // both for icon in expand button and device item + .mx_E2EIcon { + // don't squeeze + flex: 0 0 auto; + margin: 2px 5px 0 0; + width: 12px; + height: 12px; + } + + .mx_UserInfo_expand { + display: flex; + margin-top: 11px; + color: $accent-color; + } + } + } diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index c1a6442409..12a38c468e 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -110,8 +110,42 @@ function useIsEncrypted(cli, room) { return isEncrypted; } -const DevicesSection = ({devices, userId, loading}) => { - const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo'); +function verifyDevice(userId, device) { + const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); + Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, { + userId: userId, + device: device, + }); +} + +function DeviceItem({userId, device}) { + const classes = classNames("mx_UserInfo_device", { + mx_UserInfo_device_verified: device.isVerified(), + mx_UserInfo_device_unverified: !device.isVerified(), + }); + const iconClasses = classNames("mx_E2EIcon", { + mx_E2EIcon_verified: device.isVerified(), + mx_E2EIcon_warning: !device.isVerified(), + }); + + const onDeviceClick = () => { + if (!device.isVerified()) { + verifyDevice(userId, device); + } + }; + + const deviceName = device.ambiguous ? + (device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" : + device.getDisplayName(); + const trustedLabel = device.isVerified() ? _t("Trusted") : _t("Not trusted"); + return ( +
        +
        {deviceName}
        +
        {trustedLabel}
        + ); +} + +function DevicesSection({devices, userId, loading}) { const Spinner = sdk.getComponent("elements.Spinner"); const [isExpanded, setExpanded] = useState(false); @@ -130,23 +164,24 @@ const DevicesSection = ({devices, userId, loading}) => { let expandButton; if (verifiedDevices.length) { if (isExpanded) { - expandButton = ( setExpanded(false)}> - {_t("Hide verified Sign-In's")} + expandButton = ( setExpanded(false)}> +
        {_t("Hide verified Sign-In's")}
        ); } else { - expandButton = ( setExpanded(true)}> - {_t("%(count)s verified Sign-In's", {count: verifiedDevices.length})} + expandButton = ( setExpanded(true)}> +
        +
        {_t("%(count)s verified Sign-In's", {count: verifiedDevices.length})}
        ); } } let deviceList = unverifiedDevices.map((device, i) => { - return (); + return (); }); if (isExpanded) { const keyStart = unverifiedDevices.length; deviceList = deviceList.concat(verifiedDevices.map((device, i) => { - return (); + return (); })); } @@ -156,7 +191,7 @@ const DevicesSection = ({devices, userId, loading}) => {
        {expandButton}
        ); -}; +} const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => { let ignoreButton = null; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9322e71b19..a7bcf29407 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1066,10 +1066,11 @@ "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", "Members": "Members", "Files": "Files", - "Direct messages": "Direct messages", + "Trusted": "Trusted", + "Not trusted": "Not trusted", "Hide verified Sign-In's": "Hide verified Sign-In's", - "%(count)s verified Sign-In's|one": "1 verified Sign-In", "%(count)s verified Sign-In's|other": "%(count)s verified Sign-In's", + "%(count)s verified Sign-In's|one": "1 verified Sign-In", "Direct message": "Direct message", "Unverify user": "Unverify user", "Options": "Options", From 030827f77d78eead4f9c613e93299ac4b7ad75eb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 18:00:35 +0100 Subject: [PATCH 0611/2372] mark destructive actions in red --- res/css/views/right_panel/_UserInfo.scss | 3 +++ src/components/views/right_panel/UserInfo.js | 24 +++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 79211bb38a..49f52d3387 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -136,6 +136,9 @@ limitations under the License. line-height: 16px; margin: 8px 0; + &.mx_UserInfo_destructive { + color: $warning-color; + } } .mx_UserInfo_statusMessage { diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 12a38c468e..0b8fb8782c 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -224,7 +224,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i }; ignoreButton = ( - + { isIgnored ? _t("Unignore") : _t("Ignore") } ); @@ -306,7 +306,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i let unverifyButton; if (devices && devices.some(device => device.isVerified())) { unverifyButton = ( - unverifyUser(cli, member.userId)} className="mx_UserInfo_field"> + unverifyUser(cli, member.userId)} className="mx_UserInfo_field mx_UserInfo_destructive"> { _t('Unverify user') } ); @@ -428,7 +428,7 @@ const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, start }; const kickLabel = member.membership === "invite" ? _t("Disinvite") : _t("Kick"); - return + return { kickLabel } ; }); @@ -501,7 +501,7 @@ const RedactMessagesButton = withLegacyMatrixClient(({matrixClient: cli, member} } }; - return + return { _t("Remove recent messages") } ; }); @@ -553,7 +553,11 @@ const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, star label = _t("Unban"); } - return + const classes = classNames("mx_UserInfo_field", { + mx_UserInfo_destructive: member.membership !== 'ban', + }); + + return { label } ; }); @@ -610,8 +614,12 @@ const MuteToggleButton = withLegacyMatrixClient( } }; + const classes = classNames("mx_UserInfo_field", { + mx_UserInfo_destructive: !isMuted, + }); + const muteLabel = isMuted ? _t("Unmute") : _t("Mute"); - return + return { muteLabel } ; }, @@ -734,7 +742,7 @@ const GroupAdminToolsSection = withLegacyMatrixClient( }; const kickButton = ( - + { isInvited ? _t('Disinvite') : _t('Remove from community') } ); @@ -975,7 +983,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room // FIXME this should be using cli instead of MatrixClientPeg.matrixClient if (isSynapseAdmin && user.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`)) { synapseDeactivateButton = ( - + {_t("Deactivate user")} ); From 9e8a2eda1fd77c29fad4067bf3192464aa876069 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 18:00:57 +0100 Subject: [PATCH 0612/2372] small fixes --- src/components/views/right_panel/UserInfo.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 0b8fb8782c..52caa69fcf 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -40,7 +40,6 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; -import {ContentRepo} from 'matrix-js-sdk'; const _disambiguateDevices = (devices) => { const names = Object.create(null); @@ -315,13 +314,13 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i return (

        { _t("Options") }

        -
        +
        { directMessageButton } { readReceiptButton } { shareUserButton } { insertPillButton } - { ignoreButton } { inviteUserButton } + { ignoreButton } { unverifyButton }
        From ca12e6c0106942747f5923b625561788d7a1e9ac Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 18:37:25 +0100 Subject: [PATCH 0613/2372] don't render unverified state on bubbles as they are only used for verification right now, and verification events will be unverified by definition, so no need to alarm users needlessly. Also, this breaks the bubble layout on hover due to e2e icons and verified left border style. --- src/components/views/rooms/EventTile.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 22f1f914b6..5fcf1e4491 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -606,8 +606,8 @@ module.exports = createReactClass({ mx_EventTile_last: this.props.last, mx_EventTile_contextual: this.props.contextual, mx_EventTile_actionBarFocused: this.state.actionBarFocused, - mx_EventTile_verified: this.state.verified === true, - mx_EventTile_unverified: this.state.verified === false, + mx_EventTile_verified: !isBubbleMessage && this.state.verified === true, + mx_EventTile_unverified: !isBubbleMessage && this.state.verified === false, mx_EventTile_bad: isEncryptionFailure, mx_EventTile_emote: msgtype === 'm.emote', mx_EventTile_redacted: isRedacted, @@ -800,7 +800,7 @@ module.exports = createReactClass({ { timestamp } - { this._renderE2EPadlock() } + { !isBubbleMessage && this._renderE2EPadlock() } { thread } { sender } -
        +
        { timestamp } - { this._renderE2EPadlock() } + { !isBubbleMessage && this._renderE2EPadlock() } { thread } Date: Wed, 13 Nov 2019 18:38:55 +0100 Subject: [PATCH 0614/2372] don't need this, as it takes height from the constrained width --- res/css/views/right_panel/_UserInfo.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 49f52d3387..a5dae148f4 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -79,7 +79,6 @@ limitations under the License. padding-top: 100%; height: 0; border-radius: 100%; - max-height: 30vh; box-sizing: content-box; background-repeat: no-repeat; background-size: cover; From e3f7fe51dc6be1768d76c707ef95b7dd71ab795c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 18:45:12 +0100 Subject: [PATCH 0615/2372] use normal shield for verification requests --- res/css/views/messages/_MKeyVerificationRequest.scss | 3 ++- res/img/e2e/normal.svg | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 res/img/e2e/normal.svg diff --git a/res/css/views/messages/_MKeyVerificationRequest.scss b/res/css/views/messages/_MKeyVerificationRequest.scss index aff44e4109..b4cde4e7ef 100644 --- a/res/css/views/messages/_MKeyVerificationRequest.scss +++ b/res/css/views/messages/_MKeyVerificationRequest.scss @@ -25,7 +25,7 @@ limitations under the License. width: 12px; height: 16px; content: ""; - mask: url("$(res)/img/e2e/verified.svg"); + mask: url("$(res)/img/e2e/normal.svg"); mask-repeat: no-repeat; mask-size: 100%; margin-top: 4px; @@ -33,6 +33,7 @@ limitations under the License. } &.mx_KeyVerification_icon_verified::after { + mask: url("$(res)/img/e2e/verified.svg"); background-color: $accent-color; } diff --git a/res/img/e2e/normal.svg b/res/img/e2e/normal.svg new file mode 100644 index 0000000000..5b848bc27f --- /dev/null +++ b/res/img/e2e/normal.svg @@ -0,0 +1,3 @@ + + + From 942a1c9a56dcef794db5d8c2ce4e9650a551d035 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 18:55:11 +0100 Subject: [PATCH 0616/2372] fix e2e icon in composer having wrong colors --- res/css/views/rooms/_MessageComposer.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index e9f33183f5..14562fe7ed 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -78,7 +78,10 @@ limitations under the License. .mx_MessageComposer_e2eIcon.mx_E2EIcon { position: absolute; left: 60px; - background-color: $composer-e2e-icon-color; + + &::after { + background-color: $composer-e2e-icon-color; + } } .mx_MessageComposer_noperm_error { From b278531f2fbabfdee31bd46434f30f9e462e56ea Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 14 Nov 2019 16:58:07 +0100 Subject: [PATCH 0617/2372] add IconButton as in design --- res/css/_components.scss | 1 + res/css/views/elements/_IconButton.scss | 55 +++++++++++++++++++++ res/img/feather-customised/edit.svg | 4 ++ src/components/views/elements/IconButton.js | 34 +++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 res/css/views/elements/_IconButton.scss create mode 100644 res/img/feather-customised/edit.svg create mode 100644 src/components/views/elements/IconButton.js diff --git a/res/css/_components.scss b/res/css/_components.scss index c8ea237dcd..40a2c576d0 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -90,6 +90,7 @@ @import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_EventListSummary.scss"; @import "./views/elements/_Field.scss"; +@import "./views/elements/_IconButton.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_InteractiveTooltip.scss"; diff --git a/res/css/views/elements/_IconButton.scss b/res/css/views/elements/_IconButton.scss new file mode 100644 index 0000000000..d8ebbeb65e --- /dev/null +++ b/res/css/views/elements/_IconButton.scss @@ -0,0 +1,55 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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. +*/ + +.mx_IconButton { + width: 32px; + height: 32px; + border-radius: 100%; + background-color: $accent-bg-color; + // don't shrink or grow if in a flex container + flex: 0 0 auto; + + &.mx_AccessibleButton_disabled { + background-color: none; + + &::before { + background-color: lightgrey; + } + } + + &:hover { + opacity: 90%; + } + + &::before { + content: ""; + display: block; + width: 100%; + height: 100%; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 55%; + background-color: $accent-color; + } + + &.mx_IconButton_icon_check::before { + mask-image: url('$(res)/img/feather-customised/check.svg'); + } + + &.mx_IconButton_icon_edit::before { + mask-image: url('$(res)/img/feather-customised/edit.svg'); + } +} diff --git a/res/img/feather-customised/edit.svg b/res/img/feather-customised/edit.svg new file mode 100644 index 0000000000..f511aa1477 --- /dev/null +++ b/res/img/feather-customised/edit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/views/elements/IconButton.js b/src/components/views/elements/IconButton.js new file mode 100644 index 0000000000..9f5bf77426 --- /dev/null +++ b/src/components/views/elements/IconButton.js @@ -0,0 +1,34 @@ +/* + Copyright 2016 Jani Mustonen + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import AccessibleButton from "./AccessibleButton"; + +export default function IconButton(props) { + const {icon, className, ...restProps} = props; + + let newClassName = (className || "") + " mx_IconButton"; + newClassName = newClassName + " mx_IconButton_icon_" + icon; + + const allProps = Object.assign({}, restProps, {className: newClassName}); + + return React.createElement(AccessibleButton, allProps); +} + +IconButton.propTypes = Object.assign({ + icon: PropTypes.string, +}, AccessibleButton.propTypes); From d0914f9208f44e624eb91bff5bc654c4a9f25bfe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 14 Nov 2019 16:58:37 +0100 Subject: [PATCH 0618/2372] allow label to be empty on power selector --- src/components/views/elements/PowerSelector.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index 5bc8eeba58..e6babded32 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -129,10 +129,11 @@ module.exports = createReactClass({ render: function() { let picker; + const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label; if (this.state.custom) { picker = ( ); @@ -151,7 +152,7 @@ module.exports = createReactClass({ picker = ( {options} From 91e02aa623e91f1d09d8cea2ad447f8cd5222dd0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 14 Nov 2019 16:58:56 +0100 Subject: [PATCH 0619/2372] hide PL numbers on labels --- src/Roles.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Roles.js b/src/Roles.js index 10c4ceaf1e..4c0d2ab4e6 100644 --- a/src/Roles.js +++ b/src/Roles.js @@ -28,8 +28,8 @@ export function levelRoleMap(usersDefault) { export function textualPowerLevel(level, usersDefault) { const LEVEL_ROLE_MAP = levelRoleMap(usersDefault); if (LEVEL_ROLE_MAP[level]) { - return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${usersDefault})`); + return LEVEL_ROLE_MAP[level]; } else { - return level; + return _t("Custom %(level)s", {level}); } } From 6db162a3a7a5a13e99d4e31b43091c25831a0223 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 14 Nov 2019 16:59:09 +0100 Subject: [PATCH 0620/2372] add PL edit mode, don't show selector by default still saves when changing the selector though --- res/css/views/right_panel/_UserInfo.scss | 30 ++- src/components/views/right_panel/UserInfo.js | 192 +++++++++++-------- src/i18n/strings/en_EN.json | 2 + 3 files changed, 141 insertions(+), 83 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index a5dae148f4..9e4d4dc471 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -125,8 +125,34 @@ limitations under the License. } } - .mx_UserInfo_memberDetails { - text-align: center; + .mx_UserInfo_memberDetails .mx_UserInfo_profileField { + display: flex; + justify-content: center; + align-items: center; + + margin: 6px 0; + + .mx_IconButton { + margin-left: 6px; + width: 16px; + height: 16px; + + &::before { + mask-size: 80%; + } + } + + .mx_UserInfo_roleDescription { + display: flex; + justify-content: center; + align-items: center; + // try to make it the same height as the dropdown + margin: 11px 0 12px 0; + } + + .mx_Field { + margin: 0; + } } .mx_UserInfo_field { diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 52caa69fcf..379292c152 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -27,7 +27,6 @@ import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import createRoom from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; -import Unread from '../../../Unread'; import AccessibleButton from '../elements/AccessibleButton'; import SdkConfig from '../../../SdkConfig'; import SettingsStore from "../../../settings/SettingsStore"; @@ -40,6 +39,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import {textualPowerLevel} from '../../../Roles'; const _disambiguateDevices = (devices) => { const names = Object.create(null); @@ -780,43 +780,11 @@ const useIsSynapseAdmin = (cli) => { return isAdmin; }; -// cli is injected by withLegacyMatrixClient -const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => { - // Load room if we are given a room id and memoize it - const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]); - - // only display the devices list if our client supports E2E - const _enableDevices = cli.isCryptoEnabled(); - - // Load whether or not we are a Synapse Admin - const isSynapseAdmin = useIsSynapseAdmin(cli); - - // Check whether the user is ignored - const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId)); - // Recheck if the user or client changes - useEffect(() => { - setIsIgnored(cli.isUserIgnored(user.userId)); - }, [cli, user.userId]); - // Recheck also if we receive new accountData m.ignored_user_list - const accountDataHandler = useCallback((ev) => { - if (ev.getType() === "m.ignored_user_list") { - setIsIgnored(cli.isUserIgnored(user.userId)); - } - }, [cli, user.userId]); - useEventEmitter(cli, "accountData", accountDataHandler); - - // Count of how many operations are currently in progress, if > 0 then show a Spinner - const [pendingUpdateCount, setPendingUpdateCount] = useState(0); - const startUpdating = useCallback(() => { - setPendingUpdateCount(pendingUpdateCount + 1); - }, [pendingUpdateCount]); - const stopUpdating = useCallback(() => { - setPendingUpdateCount(pendingUpdateCount - 1); - }, [pendingUpdateCount]); - +function useRoomPermissions(cli, room, user) { const [roomPermissions, setRoomPermissions] = useState({ // modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL modifyLevelMax: -1, + canAffectUser: false, canInvite: false, }); const updateRoomPermissions = useCallback(async () => { @@ -847,6 +815,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room setRoomPermissions({ canInvite: me.powerLevel >= powerLevels.invite, + canAffectUser, modifyLevelMax, }); }, [cli, user, room]); @@ -856,38 +825,16 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room return () => { setRoomPermissions({ maximalPowerLevel: -1, + canAffectUser: false, canInvite: false, }); }; }, [updateRoomPermissions]); - const onSynapseDeactivate = useCallback(async () => { - const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog'); - const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, { - title: _t("Deactivate user?"), - description: -
        { _t( - "Deactivating this user will log them out and prevent them from logging back in. Additionally, " + - "they will leave all the rooms they are in. This action cannot be reversed. Are you sure you " + - "want to deactivate this user?", - ) }
        , - button: _t("Deactivate user"), - danger: true, - }); - - const [accepted] = await finished; - if (!accepted) return; - try { - cli.deactivateSynapseUser(user.userId); - } catch (err) { - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - Modal.createTrackedDialog('Failed to deactivate user', '', ErrorDialog, { - title: _t('Failed to deactivate user'), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - } - }, [cli, user.userId]); + return roomPermissions; +} +const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, startUpdating, stopUpdating, roomPermissions}) => { const onPowerChange = useCallback(async (powerLevel) => { const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { startUpdating(); @@ -953,6 +900,104 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); }, [user.roomId, user.userId, room && room.currentState, cli]); // eslint-disable-line + + const [isEditingPL, setEditingPL] = useState(false); + if (room && user.roomId) { // is in room + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; + const powerLevel = parseInt(user.powerLevel); + const IconButton = sdk.getComponent('elements.IconButton'); + if (isEditingPL) { + const PowerSelector = sdk.getComponent('elements.PowerSelector'); + return ( +
        + + setEditingPL(false)} /> +
        + ); + } else { + const modifyButton = roomPermissions.canAffectUser ? + ( setEditingPL(true)} />) : null; + const role = textualPowerLevel(powerLevel, powerLevelUsersDefault); + const label = _t("%(role)s in %(roomName)s", + {role, roomName: room.name}, + {strong: label => {label}}, + ); + return (
        {label}{modifyButton}
        ); + } + } +}); + +// cli is injected by withLegacyMatrixClient +const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => { + // Load room if we are given a room id and memoize it + const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]); + + // only display the devices list if our client supports E2E + const _enableDevices = cli.isCryptoEnabled(); + + // Load whether or not we are a Synapse Admin + const isSynapseAdmin = useIsSynapseAdmin(cli); + + // Check whether the user is ignored + const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId)); + // Recheck if the user or client changes + useEffect(() => { + setIsIgnored(cli.isUserIgnored(user.userId)); + }, [cli, user.userId]); + // Recheck also if we receive new accountData m.ignored_user_list + const accountDataHandler = useCallback((ev) => { + if (ev.getType() === "m.ignored_user_list") { + setIsIgnored(cli.isUserIgnored(user.userId)); + } + }, [cli, user.userId]); + useEventEmitter(cli, "accountData", accountDataHandler); + + // Count of how many operations are currently in progress, if > 0 then show a Spinner + const [pendingUpdateCount, setPendingUpdateCount] = useState(0); + const startUpdating = useCallback(() => { + setPendingUpdateCount(pendingUpdateCount + 1); + }, [pendingUpdateCount]); + const stopUpdating = useCallback(() => { + setPendingUpdateCount(pendingUpdateCount - 1); + }, [pendingUpdateCount]); + + const roomPermissions = useRoomPermissions(cli, room, user); + + const onSynapseDeactivate = useCallback(async () => { + const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog'); + const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, { + title: _t("Deactivate user?"), + description: +
        { _t( + "Deactivating this user will log them out and prevent them from logging back in. Additionally, " + + "they will leave all the rooms they are in. This action cannot be reversed. Are you sure you " + + "want to deactivate this user?", + ) }
        , + button: _t("Deactivate user"), + danger: true, + }); + + const [accepted] = await finished; + if (!accepted) return; + try { + cli.deactivateSynapseUser(user.userId); + } catch (err) { + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createTrackedDialog('Failed to deactivate user', '', ErrorDialog, { + title: _t('Failed to deactivate user'), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + } + }, [cli, user.userId]); + + const onMemberAvatarKey = e => { if (e.key === "Enter") { onMemberAvatarClick(); @@ -1058,26 +1103,6 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room statusLabel = { statusMessage }; } - let memberDetails = null; - - if (room && user.roomId) { // is in room - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; - - const PowerSelector = sdk.getComponent('elements.PowerSelector'); - memberDetails =
        -
        - -
        - -
        ; - } - const avatarUrl = user.getMxcAvatarUrl ? user.getMxcAvatarUrl() : user.avatarUrl; let avatarElement; if (avatarUrl) { @@ -1102,6 +1127,11 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room title={_t('Close')} />; } + const memberDetails = ; + const isRoomEncrypted = useIsEncrypted(cli, room); // undefined means yet to be loaded, null means failed to load, otherwise list of devices const [devices, setDevices] = useState(undefined); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a7bcf29407..46ad7d5135 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -118,6 +118,7 @@ "Restricted": "Restricted", "Moderator": "Moderator", "Admin": "Admin", + "Custom %(level)s": "Custom %(level)s", "Start a chat": "Start a chat", "Who would you like to communicate with?": "Who would you like to communicate with?", "Email, name or Matrix ID": "Email, name or Matrix ID", @@ -1080,6 +1081,7 @@ "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", "Failed to deactivate user": "Failed to deactivate user", + "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", From bd853b3102a3da670a705aae338b11cf4b4da9f8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 12:10:56 +0100 Subject: [PATCH 0621/2372] listen for RoomState.members instead of RoomState.events as the powerlevel on the member is not yet updated at the time RoomState.events is emitted. Also listen on the client for this event as the currentState object can change when the timeline is reset. --- src/components/views/right_panel/UserInfo.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 379292c152..8931b3ed1f 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -365,10 +365,13 @@ const _isMuted = (member, powerLevelContent) => { return member.powerLevel < levelToSend; }; -const useRoomPowerLevels = (room) => { +const useRoomPowerLevels = (cli, room) => { const [powerLevels, setPowerLevels] = useState({}); const update = useCallback(() => { + if (!room) { + return; + } const event = room.currentState.getStateEvents("m.room.power_levels", ""); if (event) { setPowerLevels(event.getContent()); @@ -380,7 +383,7 @@ const useRoomPowerLevels = (room) => { }; }, [room]); - useEventEmitter(room.currentState, "RoomState.events", update); + useEventEmitter(cli, "RoomState.members", update); useEffect(() => { update(); return () => { @@ -819,7 +822,7 @@ function useRoomPermissions(cli, room, user) { modifyLevelMax, }); }, [cli, user, room]); - useEventEmitter(cli, "RoomState.events", updateRoomPermissions); + useEventEmitter(cli, "RoomState.members", updateRoomPermissions); useEffect(() => { updateRoomPermissions(); return () => { From e86ceb986fc913e958c6eb72cb0f237b86ebeb07 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:13:22 +0100 Subject: [PATCH 0622/2372] pass powerlevels state to power level section and admin section --- src/components/views/right_panel/UserInfo.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 8931b3ed1f..a0a256e4c8 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -628,13 +628,12 @@ const MuteToggleButton = withLegacyMatrixClient( ); const RoomAdminToolsContainer = withLegacyMatrixClient( - ({matrixClient: cli, room, children, member, startUpdating, stopUpdating}) => { + ({matrixClient: cli, room, children, member, startUpdating, stopUpdating, powerLevels}) => { let kickButton; let banButton; let muteButton; let redactButton; - const powerLevels = useRoomPowerLevels(room); const editPowerLevel = ( (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || powerLevels.state_default @@ -837,8 +836,7 @@ function useRoomPermissions(cli, room, user) { return roomPermissions; } -const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, startUpdating, stopUpdating, roomPermissions}) => { - const onPowerChange = useCallback(async (powerLevel) => { +const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, powerLevels}) => { const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { startUpdating(); cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( @@ -945,6 +943,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room // only display the devices list if our client supports E2E const _enableDevices = cli.isCryptoEnabled(); + const powerLevels = useRoomPowerLevels(cli, room); // Load whether or not we are a Synapse Admin const isSynapseAdmin = useIsSynapseAdmin(cli); @@ -1040,6 +1039,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room if (room && user.roomId) { adminToolsContainer = ( ; } - const memberDetails = ; const isRoomEncrypted = useIsEncrypted(cli, room); From 48b1207c6ed2ea07d764f96db5429c2a6c72f24d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:16:23 +0100 Subject: [PATCH 0623/2372] split up PowerLevelEditor into two components one while editing (PowerLevelEditor) and one while not editing (PowerLevelSection). Also save when pressing the button, not when changing the power dropdown. Also show the spinner next to the dropdown when saving, not at the bottom of the component. --- res/css/views/right_panel/_UserInfo.scss | 8 +- src/Roles.js | 2 +- src/components/views/right_panel/UserInfo.js | 190 +++++++++++-------- src/i18n/strings/en_EN.json | 4 +- 4 files changed, 122 insertions(+), 82 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 9e4d4dc471..2b2add49ee 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -132,8 +132,8 @@ limitations under the License. margin: 6px 0; - .mx_IconButton { - margin-left: 6px; + .mx_IconButton, .mx_Spinner { + margin-left: 20px; width: 16px; height: 16px; @@ -148,6 +148,10 @@ limitations under the License. align-items: center; // try to make it the same height as the dropdown margin: 11px 0 12px 0; + + .mx_IconButton { + margin-left: 6px; + } } .mx_Field { diff --git a/src/Roles.js b/src/Roles.js index 4c0d2ab4e6..7cc3c880d7 100644 --- a/src/Roles.js +++ b/src/Roles.js @@ -30,6 +30,6 @@ export function textualPowerLevel(level, usersDefault) { if (LEVEL_ROLE_MAP[level]) { return LEVEL_ROLE_MAP[level]; } else { - return _t("Custom %(level)s", {level}); + return _t("Custom (%(level)s)", {level}); } } diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index a0a256e4c8..589eca9a08 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -837,9 +837,46 @@ function useRoomPermissions(cli, room, user) { } const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, powerLevels}) => { + const [isEditing, setEditing] = useState(false); + if (room && user.roomId) { // is in room + if (isEditing) { + return ( setEditing(false)} />); + } else { + const IconButton = sdk.getComponent('elements.IconButton'); + const powerLevelUsersDefault = powerLevels.users_default || 0; + const powerLevel = parseInt(user.powerLevel, 10); + const modifyButton = roomPermissions.canEdit ? + ( setEditing(true)} />) : null; + const role = textualPowerLevel(powerLevel, powerLevelUsersDefault); + const label = _t("%(role)s in %(roomName)s", + {role, roomName: room.name}, + {strong: label => {label}}, + ); + return ( +
        +
        {label}{modifyButton}
        +
        + ); + } + } else { + return null; + } +}); + +const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, onFinished}) => { + const [isUpdating, setIsUpdating] = useState(false); + const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10)); + const [isDirty, setIsDirty] = useState(false); + const onPowerChange = useCallback((powerLevel) => { + setIsDirty(true); + setSelectedPowerLevel(parseInt(powerLevel, 10)); + }, [setSelectedPowerLevel, setIsDirty]); + + const changePowerLevel = useCallback(async () => { const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { - startUpdating(); - cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( + return cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( function() { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! @@ -852,87 +889,86 @@ const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room description: _t("Failed to change power level"), }); }, - ).finally(() => { - stopUpdating(); - }).done(); + ); }; - const roomId = user.roomId; - const target = user.userId; - - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - if (!powerLevelEvent) return; - - if (!powerLevelEvent.getContent().users) { - _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - return; - } - - const myUserId = cli.getUserId(); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - - // If we are changing our own PL it can only ever be decreasing, which we cannot reverse. - if (myUserId === target) { - try { - if (!(await _warnSelfDemote())) return; - _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - } catch (e) { - console.error("Failed to warn about self demotion: ", e); + try { + if (!isDirty) { + return; } - return; + + setIsUpdating(true); + + const powerLevel = selectedPowerLevel; + + const roomId = user.roomId; + const target = user.userId; + + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (!powerLevelEvent) return; + + if (!powerLevelEvent.getContent().users) { + _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); + return; + } + + const myUserId = cli.getUserId(); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + // If we are changing our own PL it can only ever be decreasing, which we cannot reverse. + if (myUserId === target) { + try { + if (!(await _warnSelfDemote())) return; + } catch (e) { + console.error("Failed to warn about self demotion: ", e); + } + await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); + return; + } + + const myPower = powerLevelEvent.getContent().users[myUserId]; + if (parseInt(myPower) === parseInt(powerLevel)) { + const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, { + title: _t("Warning!"), + description: +
        + { _t("You will not be able to undo this change as you are promoting the user " + + "to have the same power level as yourself.") }
        + { _t("Are you sure?") } +
        , + button: _t("Continue"), + }); + + const [confirmed] = await finished; + if (confirmed) return; + } + await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); + } finally { + onFinished(); } + }, [user.roomId, user.userId, cli, selectedPowerLevel, isDirty, setIsUpdating, onFinished, room]); - const myPower = powerLevelEvent.getContent().users[myUserId]; - if (parseInt(myPower) === parseInt(powerLevel)) { - const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, { - title: _t("Warning!"), - description: -
        - { _t("You will not be able to undo this change as you are promoting the user " + - "to have the same power level as yourself.") }
        - { _t("Are you sure?") } -
        , - button: _t("Continue"), - }); + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; + const IconButton = sdk.getComponent('elements.IconButton'); + const Spinner = sdk.getComponent("elements.Spinner"); + const buttonOrSpinner = isUpdating ? : + ; - const [confirmed] = await finished; - if (confirmed) return; - } - _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - }, [user.roomId, user.userId, room && room.currentState, cli]); // eslint-disable-line - - - const [isEditingPL, setEditingPL] = useState(false); - if (room && user.roomId) { // is in room - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; - const powerLevel = parseInt(user.powerLevel); - const IconButton = sdk.getComponent('elements.IconButton'); - if (isEditingPL) { - const PowerSelector = sdk.getComponent('elements.PowerSelector'); - return ( -
        - - setEditingPL(false)} /> -
        - ); - } else { - const modifyButton = roomPermissions.canAffectUser ? - ( setEditingPL(true)} />) : null; - const role = textualPowerLevel(powerLevel, powerLevelUsersDefault); - const label = _t("%(role)s in %(roomName)s", - {role, roomName: room.name}, - {strong: label => {label}}, - ); - return (
        {label}{modifyButton}
        ); - } - } + const PowerSelector = sdk.getComponent('elements.PowerSelector'); + return ( +
        + + {buttonOrSpinner} +
        + ); }); // cli is injected by withLegacyMatrixClient diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 46ad7d5135..029000e9d2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -118,7 +118,7 @@ "Restricted": "Restricted", "Moderator": "Moderator", "Admin": "Admin", - "Custom %(level)s": "Custom %(level)s", + "Custom (%(level)s)": "Custom (%(level)s)", "Start a chat": "Start a chat", "Who would you like to communicate with?": "Who would you like to communicate with?", "Email, name or Matrix ID": "Email, name or Matrix ID", @@ -1080,8 +1080,8 @@ "Remove this user from community?": "Remove this user from community?", "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", - "Failed to deactivate user": "Failed to deactivate user", "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", + "Failed to deactivate user": "Failed to deactivate user", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", From 264c8181c2f6c6b52d673e16bbf76708f06c3f30 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:18:04 +0100 Subject: [PATCH 0624/2372] add canEdit to permission state, more explicit than maxLevel >= 0 --- src/components/views/right_panel/UserInfo.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 589eca9a08..681336df7c 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -786,7 +786,7 @@ function useRoomPermissions(cli, room, user) { const [roomPermissions, setRoomPermissions] = useState({ // modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL modifyLevelMax: -1, - canAffectUser: false, + canEdit: false, canInvite: false, }); const updateRoomPermissions = useCallback(async () => { @@ -817,7 +817,7 @@ function useRoomPermissions(cli, room, user) { setRoomPermissions({ canInvite: me.powerLevel >= powerLevels.invite, - canAffectUser, + canEdit: modifyLevelMax >= 0, modifyLevelMax, }); }, [cli, user, room]); @@ -827,7 +827,7 @@ function useRoomPermissions(cli, room, user) { return () => { setRoomPermissions({ maximalPowerLevel: -1, - canAffectUser: false, + canEdit: false, canInvite: false, }); }; From 92237f10452eb77b077f3e49373a497791b6bb7e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:18:36 +0100 Subject: [PATCH 0625/2372] cleanup --- src/components/views/right_panel/UserInfo.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 681336df7c..a194b89eee 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -789,8 +789,10 @@ function useRoomPermissions(cli, room, user) { canEdit: false, canInvite: false, }); - const updateRoomPermissions = useCallback(async () => { - if (!room) return; + const updateRoomPermissions = useCallback(() => { + if (!room) { + return; + } const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); if (!powerLevelEvent) return; From 53019c5e9135b2ad0bc45b23438407fff2763272 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:19:16 +0100 Subject: [PATCH 0626/2372] don't show devices section when not encrypted, as it just shows spinner --- src/components/views/right_panel/UserInfo.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index a194b89eee..88cc41766d 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1248,11 +1248,13 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room text = _t("Messages in this room are end-to-end encrypted."); } - const devicesSection = ( + const devicesSection = isRoomEncrypted ? + () : null; + const securitySection = (

        { _t("Security") }

        { text }

        - + { devicesSection }
        ); @@ -1289,7 +1291,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
        } - { devicesSection } + { securitySection } Date: Fri, 15 Nov 2019 15:23:23 +0100 Subject: [PATCH 0627/2372] prevent https://github.com/vector-im/riot-web/issues/11338 --- src/components/views/right_panel/UserInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 88cc41766d..7355153ec7 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1115,7 +1115,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room let presenceCurrentlyActive; let statusMessage; - if (user instanceof RoomMember) { + if (user instanceof RoomMember && user.user) { presenceState = user.user.presence; presenceLastActiveAgo = user.user.lastActiveAgo; presenceCurrentlyActive = user.user.currentlyActive; From 1162d1ee43862b994245641f118c2d2bade438e2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:29:23 +0100 Subject: [PATCH 0628/2372] Fix user info scroll container growing larger than available height making whole room view grow taller than viewport --- res/css/views/right_panel/_UserInfo.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 2b2add49ee..c5c0d9f42f 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -179,7 +179,7 @@ limitations under the License. } .mx_UserInfo_scrollContainer { - flex: 1; + flex: 1 1 0; padding-bottom: 16px; } From edd5d3c9150679a5b7c0b08e06da3a886ac16a80 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:37:43 +0100 Subject: [PATCH 0629/2372] make custom power level not grow too wide --- res/css/views/elements/_Field.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index 4d012a136e..b260d4b097 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -49,6 +49,7 @@ limitations under the License. color: $primary-fg-color; background-color: $primary-bg-color; flex: 1; + min-width: 0; } .mx_Field select { From ecc842629a989ff824d8114c22acd73affd4f243 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:48:04 +0100 Subject: [PATCH 0630/2372] fix css lint errors --- res/css/views/right_panel/_UserInfo.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index c5c0d9f42f..7fda114a79 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -111,7 +111,6 @@ limitations under the License. overflow-x: auto; max-height: 50px; display: flex; - justify-self: ; justify-content: center; align-items: center; @@ -188,7 +187,7 @@ limitations under the License. padding-bottom: 0; border-bottom: none; - >:not(h3) { + > :not(h3) { margin-left: 8px; } } @@ -229,5 +228,4 @@ limitations under the License. color: $accent-color; } } - } From d416ba2c0ceea8f7e8eca9c0f871552ec8a6220c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 16:04:00 +0100 Subject: [PATCH 0631/2372] add verify button while we don't have the verification in right panel --- res/css/views/right_panel/_UserInfo.scss | 10 ++++++++++ src/components/views/right_panel/UserInfo.js | 1 + src/i18n/strings/en_EN.json | 1 + 3 files changed, 12 insertions(+) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 7fda114a79..c68f3ffd37 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -228,4 +228,14 @@ limitations under the License. color: $accent-color; } } + + .mx_UserInfo_verify { + display: block; + background-color: $accent-color; + color: $accent-fg-color; + border-radius: 4px; + padding: 7px 1.5em; + text-align: center; + margin: 16px 0; + } } diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 7355153ec7..53a87ed1c6 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1254,6 +1254,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room

        { _t("Security") }

        { text }

        + verifyDevice(user.userId, null)}>{_t("Verify")} { devicesSection }
        ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 029000e9d2..a90af471c2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1086,6 +1086,7 @@ "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", "Security": "Security", + "Verify": "Verify", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", From 0f39a9f72dd20f4e4dfa372b9356c0f468fbfcbd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 17:29:38 +0100 Subject: [PATCH 0632/2372] fix pr feedback --- res/css/views/rooms/_E2EIcon.scss | 4 ++-- src/components/views/elements/IconButton.js | 22 ++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index c8b1be47f9..bc11ac6e1c 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -25,9 +25,9 @@ limitations under the License. .mx_E2EIcon_verified::before, .mx_E2EIcon_warning::before { content: ""; display: block; - /* the symbols in the shield icons are cut out the make the themeable with css masking. + /* the symbols in the shield icons are cut out to make it themeable with css masking. if they appear on a different background than white, the symbol wouldn't be white though, so we - add a rectangle here below the masked element to shine through the symbol cutout. + add a rectangle here below the masked element to shine through the symbol cut-out. hardcoding white and not using a theme variable as this would probably be white for any theme. */ background-color: white; position: absolute; diff --git a/src/components/views/elements/IconButton.js b/src/components/views/elements/IconButton.js index 9f5bf77426..ef7b4a8399 100644 --- a/src/components/views/elements/IconButton.js +++ b/src/components/views/elements/IconButton.js @@ -1,18 +1,18 @@ /* - Copyright 2016 Jani Mustonen +Copyright 2019 The Matrix.org Foundation C.I.C. - 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 +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 +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. - */ +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ import React from 'react'; import PropTypes from 'prop-types'; From 5f31efadb68c84a2d54599a4324e919e71d17d63 Mon Sep 17 00:00:00 2001 From: Victor Grousset Date: Fri, 15 Nov 2019 09:33:19 +0000 Subject: [PATCH 0633/2372] Translated using Weblate (Esperanto) Currently translated at 98.1% (1862 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eo/ --- src/i18n/strings/eo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index bbed6773b5..b160c5390b 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -1995,7 +1995,7 @@ "Preview": "Antaŭrigardo", "View": "Rigardo", "Find a room…": "Trovi ĉambron…", - "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Se vi ne povas travi la serĉatan ĉambron, petu inviton aŭ kreu novan ĉambron.", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Se vi ne povas trovi la serĉatan ĉambron, petu inviton aŭ kreu novan ĉambron.", "Explore rooms": "Esplori ĉambrojn", "Add Email Address": "Aldoni retpoŝtadreson", "Add Phone Number": "Aldoni telefonnumeron", From 21dc9b9f25dfe3d21ed0970e8eef2a91c439ec5f Mon Sep 17 00:00:00 2001 From: Tuomas Hietala Date: Fri, 15 Nov 2019 11:47:01 +0000 Subject: [PATCH 0634/2372] Translated using Weblate (Finnish) Currently translated at 96.4% (1830 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fi/ --- src/i18n/strings/fi.json | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index a1163f564b..80fbb9b138 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -2121,5 +2121,21 @@ "Emoji Autocomplete": "Emojien automaattinen täydennys", "Notification Autocomplete": "Ilmoitusten automaattinen täydennys", "Room Autocomplete": "Huoneiden automaattinen täydennys", - "User Autocomplete": "Käyttäjien automaattinen täydennys" + "User Autocomplete": "Käyttäjien automaattinen täydennys", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Tämä toiminto vaatii oletusidentiteettipalvelimen käyttämistä sähköpostiosoitteen tai puhelinnumeron validointiin, mutta palvelimella ei ole käyttöehtoja.", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "My Ban List": "Tekemäni estot", + "This is your list of users/servers you have blocked - don't leave the room!": "Tämä on luettelo käyttäjistä ja palvelimista, jotka olet estänyt - älä poistu huoneesta!", + "Something went wrong. Please try again or view your console for hints.": "Jotain meni vikaan. Yritä uudelleen tai katso vihjeitä konsolista.", + "Please verify the room ID or alias and try again.": "Varmista huoneen tunnus tai alias ja yritä uudelleen.", + "Please try again or view your console for hints.": "Yritä uudelleen tai katso vihjeitä konsolista.", + "⚠ These settings are meant for advanced users.": "⚠ Nämä asetukset on tarkoitettu edistyneille käyttäjille.", + "eg: @bot:* or example.org": "esim. @bot:* tai esimerkki.org", + "Show tray icon and minimize window to it on close": "Näytä ilmaisinalueen kuvake ja pienennä ikkuna siihen suljettaessa", + "Your email address hasn't been verified yet": "Sähköpostiosoitettasi ei ole vielä varmistettu", + "Verify the link in your inbox": "Varmista sähköpostiisi saapunut linkki", + "%(count)s unread messages including mentions.|one": "Yksi lukematon maininta.", + "%(count)s unread messages.|one": "Yksi lukematon viesti.", + "Unread messages.": "Lukemattomat viestit.", + "Message Actions": "Viestitoiminnot" } From 9a92f3d38eb237ab808c8887391ad6f65dd94811 Mon Sep 17 00:00:00 2001 From: Volodymyr Kostyrko Date: Thu, 14 Nov 2019 20:50:50 +0000 Subject: [PATCH 0635/2372] Translated using Weblate (Ukrainian) Currently translated at 29.0% (551 of 1898 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/uk/ --- src/i18n/strings/uk.json | 73 +++++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 7b1c9e1126..4c03a7019d 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -23,8 +23,8 @@ "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "Текстове повідомлення було надіслано +%(msisdn)s. Введіть, будь ласка, код підтвердження з цього повідомлення", "Accept": "Прийняти", "Account": "Обліковка", - "%(targetName)s accepted an invitation.": "%(targetName)s прийняв/ла запрошення.", - "%(targetName)s accepted the invitation for %(displayName)s.": "Запрошення від %(displayName)s прийнято %(targetName)s.", + "%(targetName)s accepted an invitation.": "%(targetName)s приймає запрошення.", + "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s приймає запрошення від %(displayName)s.", "Access Token:": "Токен доступу:", "Active call (%(roomName)s)": "Активний виклик (%(roomName)s)", "Add": "Додати", @@ -71,7 +71,7 @@ "%(senderName)s banned %(targetName)s.": "%(senderName)s заблокував/ла %(targetName)s.", "Ban": "Заблокувати", "Banned users": "Заблоковані користувачі", - "Bans user with given id": "Блокує користувача з вказаним ID", + "Bans user with given id": "Блокує користувача з вказаним ідентифікатором", "Blacklisted": "В чорному списку", "Bulk Options": "Групові параметри", "Call Timeout": "Час очікування виклика", @@ -389,15 +389,15 @@ "Changes colour scheme of current room": "Змінює кольорову схему кімнати", "Sets the room topic": "Встановлює тему кімнати", "Invites user with given id to current room": "Запрошує користувача з вказаним ідентифікатором до кімнати", - "Joins room with given alias": "Приєднується до кімнати з поданим ідентифікатором", + "Joins room with given alias": "Приєднується до кімнати під іншим псевдонімом", "Leave room": "Покинути кімнату", - "Unrecognised room alias:": "Кімнату не знайдено:", - "Kicks user with given id": "Викинути з кімнати користувача з вказаним ідентифікатором", + "Unrecognised room alias:": "Не розпізнано псевдонім кімнати:", + "Kicks user with given id": "Вилучити з кімнати користувача з вказаним ідентифікатором", "Unbans user with given id": "Розблоковує користувача з вказаним ідентифікатором", - "Ignores a user, hiding their messages from you": "Ігнорувати користувача (приховує повідомлення від них)", + "Ignores a user, hiding their messages from you": "Ігнорує користувача, приховуючи повідомлення від них", "Ignored user": "Користувача ігноровано", "You are now ignoring %(userId)s": "Ви ігноруєте %(userId)s", - "Stops ignoring a user, showing their messages going forward": "Припинити ігнорувати користувача (показує їхні повідомлення від цього моменту)", + "Stops ignoring a user, showing their messages going forward": "Припиняє ігнорувати користувача, від цього моменту показуючи їхні повідомлення", "Unignored user": "Припинено ігнорування користувача", "You are no longer ignoring %(userId)s": "Ви більше не ігноруєте %(userId)s", "Define the power level of a user": "Вказати рівень прав користувача", @@ -407,9 +407,9 @@ "Unknown (user, device) pair:": "Невідома комбінація користувача і пристрою:", "Device already verified!": "Пристрій вже перевірено!", "WARNING: Device already verified, but keys do NOT MATCH!": "УВАГА: Пристрій уже перевірено, але ключі НЕ ЗБІГАЮТЬСЯ!", - "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "УВАГА: КЛЮЧ НЕ ПРОЙШОВ ПЕРЕВІРКУ! Підписний ключ %(userId)s на пристрої %(deviceId)s — це «%(fprint)s», і він не збігається з наданим ключем «%(fingerprint)s». Це може означати, що ваші повідомлення перехоплюють!", - "Verified key": "Ключ перевірено", - "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Підписний ключ, який ви вказали, збігається з підписним ключем, отриманим від пристрою %(deviceId)s користувача %(userId)s. Пристрій позначено як перевірений.", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "УВАГА: КЛЮЧ НЕ ПРОЙШОВ ПЕРЕВІРКУ! Ключ підпису %(userId)s на пристрої %(deviceId)s — це «%(fprint)s», і він не збігається з наданим ключем «%(fingerprint)s». Це може означати, що ваші повідомлення перехоплюють!", + "Verified key": "Перевірений ключ", + "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Ключ підпису, який ви вказали, збігається з ключем підпису, отриманим від пристрою %(deviceId)s користувача %(userId)s. Пристрій позначено як перевірений.", "Displays action": "Показує дію", "Unrecognised command:": "Невідома команда:", "Reason": "Причина", @@ -579,5 +579,54 @@ "A conference call could not be started because the integrations server is not available": "Конференц-дзвінок не можна розпочати оскільки інтеграційний сервер недоступний", "The file '%(fileName)s' failed to upload.": "Файл '%(fileName)s' не вийшло відвантажити.", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Файл '%(fileName)s' перевищує ліміт розміру для відвантажень домашнього сервера", - "The server does not support the room version specified.": "Сервер не підтримує вказану версію кімнати." + "The server does not support the room version specified.": "Сервер не підтримує вказану версію кімнати.", + "Add Email Address": "Додати адресу е-пошти", + "Add Phone Number": "Додати номер телефону", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Чи використовуєте ви «хлібні крихти» (аватари на списком кімнат)", + "Call failed due to misconfigured server": "Виклик не вдався через неправильне налаштування сервера", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Запропонуйте адміністратору вашого домашнього серверу (%(homeserverDomain)s) налаштувати сервер TURN для надійної роботи викликів.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Також ви можете спробувати використати публічний сервер turn.matrix.org, але це буде не настільки надійно, а також цей сервер матиме змогу бачити вашу IP-адресу. Ви можете керувати цим у налаштуваннях.", + "Try using turn.matrix.org": "Спробуйте використати turn.matrix.org", + "Replying With Files": "Відповісти файлами", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Зараз неможливо відповісти файлом. Хочете завантажити цей файл без відповіді?", + "Name or Matrix ID": "Імʼя або Matrix ID", + "Identity server has no terms of service": "Сервер ідентифікації не має умов надання послуг", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Щоб підтвердити адресу е-пошту або телефон ця дія потребує доступу до типового серверу ідентифікації , але сервер не має жодних умов надання послуг.", + "Only continue if you trust the owner of the server.": "Продовжуйте тільки якщо довіряєте власнику сервера.", + "Trust": "Довіра", + "Unable to load! Check your network connectivity and try again.": "Завантаження неможливе! Перевірте інтернет-зʼєднання та спробуйте ще.", + "Email, name or Matrix ID": "Е-пошта, імʼя або Matrix ID", + "Failed to start chat": "Не вдалося розпочати чат", + "Failed to invite users to the room:": "Не вдалося запросити користувачів до кімнати:", + "Messages": "Повідомлення", + "Actions": "Дії", + "Other": "Інше", + "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Додає ¯\\_(ツ)_/¯ на початку текстового повідомлення", + "Sends a message as plain text, without interpreting it as markdown": "Надсилає повідомлення як чистий текст, не використовуючи markdown", + "Upgrades a room to a new version": "Покращує кімнату до нової версії", + "You do not have the required permissions to use this command.": "Вам бракує дозволу на використання цієї команди.", + "Room upgrade confirmation": "Підтвердження покращення кімнати", + "Upgrading a room can be destructive and isn't always necessary.": "Покращення кімнати може призвести до втрати даних та не є обовʼязковим.", + "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Рекомендується покращувати кімнату, якщо поточна її версія вважається нестабільною. Нестабільні версії кімнат можуть мати вади, відсутні функції або вразливості безпеки.", + "Room upgrades usually only affect server-side processing of the room. If you're having problems with your Riot client, please file an issue with .": "Покращення кімнати загалом впливає лише на роботу з кімнатою на сервері. Якщо ви маєте проблему із вашим клієнтом Riot, надішліть свою проблему на .", + "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Увага!: Покращення кімнати не перенесе автоматично усіх учасників до нової версії кімнати. Ми опублікуємо посилання на нову кімнату у старій версії кімнати, а учасники мають власноруч клацнути це посилання, щоб приєднатися до нової кімнати.", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Підтвердьте, що ви згодні продовжити покращення цієї кімнати з до .", + "Upgrade": "Покращення", + "Changes your display nickname in the current room only": "Змінює ваше псевдо тільки для поточної кімнати", + "Changes the avatar of the current room": "Змінює аватар поточної кімнати", + "Changes your avatar in this current room only": "Змінює ваш аватар для поточної кімнати", + "Changes your avatar in all rooms": "Змінює ваш аватар для усіх кімнат", + "Gets or sets the room topic": "Показує чи встановлює тему кімнати", + "This room has no topic.": "Ця кімната не має теми.", + "Sets the room name": "Встановлює назву кімнати", + "Use an identity server": "Використовувати сервер ідентифікації", + "Use an identity server to invite by email. Manage in Settings.": "Використовувати ідентифікаційний сервер для запрошення через е-пошту. Керування у настройках.", + "Unbans user with given ID": "Розблоковує користувача з вказаним ідентифікатором", + "Adds a custom widget by URL to the room": "Додає власний віджет до кімнати за посиланням", + "Please supply a https:// or http:// widget URL": "Вкажіть посилання на віджет — https:// або http://", + "You cannot modify widgets in this room.": "Ви не можете змінювати віджети у цій кімнаті.", + "Forces the current outbound group session in an encrypted room to be discarded": "Примусово відкидає поточний вихідний груповий сеанс у шифрованій кімнаті", + "Sends the given message coloured as a rainbow": "Надсилає вказане повідомлення розфарбоване веселкою", + "Your Riot is misconfigured": "Ваш Riot налаштовано неправильно", + "Join the discussion": "Приєднатися до обговорення" } From 61454bcf32d6d547bbcce8b3466778f87cf30c24 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 15 Nov 2019 10:25:58 -0700 Subject: [PATCH 0636/2372] Fix i18n after merge --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f0ff0275f7..655c7030c4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1082,7 +1082,6 @@ "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", - "Failed to deactivate user": "Failed to deactivate user", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", From 6b726a8e13786f6f10222cac3c1655f47c213354 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 15 Nov 2019 14:25:53 -0700 Subject: [PATCH 0637/2372] Implement the bulk of the new widget permission prompt design Part 1 of https://github.com/vector-im/riot-web/issues/11262 This is all the visual changes - the actual wiring of the UI to the right places is for another PR (though this PR still works independently). The help icon is known to be weird here - it's a bug in the svg we have. The tooltip also goes right instead of up because making the tooltip go up is not easy work for this PR - maybe a future one if we *really* want it to go up. --- res/css/_common.scss | 16 ++ res/css/views/rooms/_AppsDrawer.scss | 62 ++++--- .../views/elements/AppPermission.js | 152 +++++++++++------- src/components/views/elements/AppTile.js | 4 +- .../views/elements/TextWithTooltip.js | 4 +- src/i18n/strings/en_EN.json | 17 +- 6 files changed, 169 insertions(+), 86 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 70ab2457f1..5987275f7f 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -550,6 +550,22 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { color: $username-variant8-color; } +@define-mixin mx_Tooltip_dark { + box-shadow: none; + background-color: $tooltip-timeline-bg-color; + color: $tooltip-timeline-fg-color; + border: none; + border-radius: 3px; + padding: 6px 8px; +} + +// This is a workaround for our mixins not supporting child selectors +.mx_Tooltip_dark { + .mx_Tooltip_chevron::after { + border-right-color: $tooltip-timeline-bg-color; + } +} + @define-mixin mx_Settings_fullWidthField { margin-right: 100px; } diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index 9ca6954af7..6f5e3abade 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -294,49 +294,61 @@ form.mx_Custom_Widget_Form div { .mx_AppPermissionWarning { text-align: center; - background-color: $primary-bg-color; + background-color: $widget-menu-bar-bg-color; display: flex; height: 100%; flex-direction: column; justify-content: center; align-items: center; + font-size: 16px; } -.mx_AppPermissionWarningImage { - margin: 10px 0; +.mx_AppPermissionWarning_row { + margin-bottom: 12px; } -.mx_AppPermissionWarningImage img { - width: 100px; +.mx_AppPermissionWarning_smallText { + font-size: 12px; } -.mx_AppPermissionWarningText { - max-width: 90%; - margin: 10px auto 10px auto; - color: $primary-fg-color; +.mx_AppPermissionWarning_bolder { + font-weight: 600; } -.mx_AppPermissionWarningTextLabel { - font-weight: bold; - display: block; +.mx_AppPermissionWarning h4 { + margin: 0; + padding: 0; } -.mx_AppPermissionWarningTextURL { +.mx_AppPermissionWarning_helpIcon { + margin-top: 1px; + margin-right: 2px; + width: 10px; + height: 10px; display: inline-block; - max-width: 100%; - color: $accent-color; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; } -.mx_AppPermissionButton { - border: none; - padding: 5px 20px; - border-radius: 5px; - background-color: $button-bg-color; - color: $button-fg-color; - cursor: pointer; +.mx_AppPermissionWarning_helpIcon::before { + display: inline-block; + background-color: $accent-color; + mask-repeat: no-repeat; + mask-size: 12px; + width: 12px; + height: 12px; + mask-position: center; + content: ''; + vertical-align: middle; + mask-image: url('$(res)/img/feather-customised/help-circle.svg'); +} + +.mx_AppPermissionWarning_tooltip { + @mixin mx_Tooltip_dark; + + ul { + list-style-position: inside; + padding-left: 2px; + margin-left: 0; + } } .mx_AppLoading { diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js index 1e019c0287..422427d4c4 100644 --- a/src/components/views/elements/AppPermission.js +++ b/src/components/views/elements/AppPermission.js @@ -19,79 +19,123 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import url from 'url'; +import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import WidgetUtils from "../../../utils/WidgetUtils"; +import MatrixClientPeg from "../../../MatrixClientPeg"; export default class AppPermission extends React.Component { + static propTypes = { + url: PropTypes.string.isRequired, + creatorUserId: PropTypes.string.isRequired, + roomId: PropTypes.string.isRequired, + onPermissionGranted: PropTypes.func.isRequired, + }; + + static defaultProps = { + onPermissionGranted: () => {}, + }; + constructor(props) { super(props); - const curlBase = this.getCurlBase(); - this.state = { curlBase: curlBase}; + // The first step is to pick apart the widget so we can render information about it + const urlInfo = this.parseWidgetUrl(); + + // The second step is to find the user's profile so we can show it on the prompt + const room = MatrixClientPeg.get().getRoom(this.props.roomId); + let roomMember; + if (room) roomMember = room.getMember(this.props.creatorUserId); + + // Set all this into the initial state + this.state = { + ...urlInfo, + roomMember, + }; } - // Return string representation of content URL without query parameters - getCurlBase() { - const wurl = url.parse(this.props.url); - let curl; - let curlString; + parseWidgetUrl() { + const widgetUrl = url.parse(this.props.url); + const params = new URLSearchParams(widgetUrl.search); - const searchParams = new URLSearchParams(wurl.search); - - if (WidgetUtils.isScalarUrl(wurl) && searchParams && searchParams.get('url')) { - curl = url.parse(searchParams.get('url')); - if (curl) { - curl.search = curl.query = ""; - curlString = curl.format(); - } + // HACK: We're relying on the query params when we should be relying on the widget's `data`. + // This is a workaround for Scalar. + if (WidgetUtils.isScalarUrl(widgetUrl) && params && params.get('url')) { + const unwrappedUrl = url.parse(params.get('url')); + return { + widgetDomain: unwrappedUrl.host || unwrappedUrl.hostname, + isWrapped: true, + }; + } else { + return { + widgetDomain: widgetUrl.host || widgetUrl.hostname, + isWrapped: false, + }; } - if (!curl && wurl) { - wurl.search = wurl.query = ""; - curlString = wurl.format(); - } - return curlString; } render() { - let e2eWarningText; - if (this.props.isRoomEncrypted) { - e2eWarningText = - { _t('NOTE: Apps are not end-to-end encrypted') }; - } - const cookieWarning = - - { _t('Warning: This widget might use cookies.') } - ; + const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); + const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); + const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); + const TextWithTooltip = sdk.getComponent("views.elements.TextWithTooltip"); + + const displayName = this.state.roomMember ? this.state.roomMember.name : this.props.creatorUserId; + const userId = displayName === this.props.creatorUserId ? null : this.props.creatorUserId; + + const avatar = this.state.roomMember + ? + : ; + + const warningTooltipText = ( +
        + {_t("Any of the following data may be shared:")} +
          +
        • {_t("Your display name")}
        • +
        • {_t("Your avatar URL")}
        • +
        • {_t("Your user ID")}
        • +
        • {_t("Your theme")}
        • +
        • {_t("Riot URL")}
        • +
        • {_t("Room ID")}
        • +
        • {_t("Widget ID")}
        • +
        +
        + ); + const warningTooltip = ( + + + + ); + + // Due to i18n limitations, we can't dedupe the code for variables in these two messages. + const warning = this.state.isWrapped + ? _t("Using this widget may share data with %(widgetDomain)s & your Integration Manager.", + {widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip}) + : _t("Using this widget may share data with %(widgetDomain)s.", + {widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip}); + return (
        -
        - {_t('Warning!')} +
        + {_t("Widget added by")}
        -
        - {_t('Do you want to load widget from URL:')} - {this.state.curlBase} - { e2eWarningText } - { cookieWarning } +
        + {avatar} +

        {displayName}

        +
        {userId}
        +
        +
        + {warning} +
        +
        + {_t("This widget may use cookies.")} +
        +
        + + {_t("Continue")} +
        -
        ); } } - -AppPermission.propTypes = { - isRoomEncrypted: PropTypes.bool, - url: PropTypes.string.isRequired, - onPermissionGranted: PropTypes.func.isRequired, -}; -AppPermission.defaultProps = { - isRoomEncrypted: false, - onPermissionGranted: function() {}, -}; diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 260b63dfd4..ffd9d73cca 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -569,11 +569,11 @@ export default class AppTile extends React.Component {
        ); if (!this.state.hasPermissionToLoad) { - const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); appTileBody = (
        diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.js index 61c3a2125a..f6cef47117 100644 --- a/src/components/views/elements/TextWithTooltip.js +++ b/src/components/views/elements/TextWithTooltip.js @@ -21,7 +21,8 @@ import sdk from '../../../index'; export default class TextWithTooltip extends React.Component { static propTypes = { class: PropTypes.string, - tooltip: PropTypes.string.isRequired, + tooltipClass: PropTypes.string, + tooltip: PropTypes.node.isRequired, }; constructor() { @@ -49,6 +50,7 @@ export default class TextWithTooltip extends React.Component { ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 655c7030c4..37383b7e4e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1183,10 +1183,18 @@ "Quick Reactions": "Quick Reactions", "Cancel search": "Cancel search", "Unknown Address": "Unknown Address", - "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", - "Warning: This widget might use cookies.": "Warning: This widget might use cookies.", - "Do you want to load widget from URL:": "Do you want to load widget from URL:", - "Allow": "Allow", + "Any of the following data may be shared:": "Any of the following data may be shared:", + "Your display name": "Your display name", + "Your avatar URL": "Your avatar URL", + "Your user ID": "Your user ID", + "Your theme": "Your theme", + "Riot URL": "Riot URL", + "Room ID": "Room ID", + "Widget ID": "Widget ID", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Using this widget may share data with %(widgetDomain)s & your Integration Manager.", + "Using this widget may share data with %(widgetDomain)s.": "Using this widget may share data with %(widgetDomain)s.", + "Widget added by": "Widget added by", + "This widget may use cookies.": "This widget may use cookies.", "Delete Widget": "Delete Widget", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", "Delete widget": "Delete widget", @@ -1494,6 +1502,7 @@ "A widget would like to verify your identity": "A widget would like to verify your identity", "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.", "Remember my selection for this widget": "Remember my selection for this widget", + "Allow": "Allow", "Deny": "Deny", "Unable to load backup status": "Unable to load backup status", "Recovery Key Mismatch": "Recovery Key Mismatch", From d56ae702873a8a655e0d8aff0f5327f60b15f204 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Sat, 16 Nov 2019 08:37:59 +0000 Subject: [PATCH 0638/2372] Translated using Weblate (Albanian) Currently translated at 99.8% (1904 of 1908 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 2efe4ddd68..2bf5732131 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2280,5 +2280,17 @@ "If this isn't what you want, please use a different tool to ignore users.": "Nëse kjo s’është ajo çka doni, ju lutemi, përdorni një tjetër mjet për të shpërfillur përdorues.", "Room ID or alias of ban list": "ID dhome ose alias e listës së dëbimeve", "Subscribe": "Pajtohuni", - "You have ignored this user, so their message is hidden. Show anyways.": "E keni shpërfillur këtë përdorues, ndaj mesazhi i tij është fshehur. Shfaqe, sido qoftë." + "You have ignored this user, so their message is hidden. Show anyways.": "E keni shpërfillur këtë përdorues, ndaj mesazhi i tij është fshehur. Shfaqe, sido qoftë.", + "Custom (%(level)s)": "Vetjak (%(level)s)", + "Trusted": "E besuar", + "Not trusted": "Jo e besuar", + "Hide verified Sign-In's": "Fshihi Hyrjet e verifikuara", + "%(count)s verified Sign-In's|other": "%(count)s Hyrje të verifikuara", + "%(count)s verified Sign-In's|one": "1 Hyrje e verifikuar", + "Direct message": "Mesazh i Drejtpërdrejtë", + "Unverify user": "Hiqi verifikimin përdoruesit", + "%(role)s in %(roomName)s": "%(role)s në %(roomName)s", + "Messages in this room are end-to-end encrypted.": "Mesazhet në këtë dhomë janë të fshehtëzuara skaj-më-skaj.", + "Security": "Siguri", + "Verify": "Verifikoje" } From a9ef6bde6333e425c50cb4bf6ca60cd5b1050e36 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Sat, 16 Nov 2019 13:09:01 +0000 Subject: [PATCH 0639/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1908 of 1908 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index a4898bd328..5c6e69c864 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2318,5 +2318,17 @@ "If this isn't what you want, please use a different tool to ignore users.": "如果這不是您想要的,請使用不同的工具來忽略使用者。", "Room ID or alias of ban list": "聊天室 ID 或封鎖清單的別名", "Subscribe": "訂閱", - "You have ignored this user, so their message is hidden. Show anyways.": "您已經忽略了這個使用者,所以他們的訊息會隱藏。無論如何都顯示。" + "You have ignored this user, so their message is hidden. Show anyways.": "您已經忽略了這個使用者,所以他們的訊息會隱藏。無論如何都顯示。", + "Custom (%(level)s)": "自訂 (%(level)s)", + "Trusted": "已信任", + "Not trusted": "不信任", + "Hide verified Sign-In's": "隱藏已驗證的登入", + "%(count)s verified Sign-In's|other": "%(count)s 個已驗證的登入", + "%(count)s verified Sign-In's|one": "1 個已驗證的登入", + "Direct message": "直接訊息", + "Unverify user": "未驗證的使用者", + "%(role)s in %(roomName)s": "%(role)s 在 %(roomName)s", + "Messages in this room are end-to-end encrypted.": "在此聊天室中的訊息為端到端加密。", + "Security": "安全", + "Verify": "驗證" } From 78c36e593650ec6fac3b592d7aea5647603b04ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Fri, 15 Nov 2019 21:17:15 +0000 Subject: [PATCH 0640/2372] Translated using Weblate (French) Currently translated at 100.0% (1908 of 1908 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index f4e889d955..eef9438761 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2325,5 +2325,17 @@ "If this isn't what you want, please use a different tool to ignore users.": "Si ce n’est pas ce que vous voulez, utilisez un autre outil pour ignorer les utilisateurs.", "Room ID or alias of ban list": "Identifiant ou alias du salon de la liste de bannissement", "Subscribe": "S’inscrire", - "You have ignored this user, so their message is hidden. Show anyways.": "Vous avez ignoré cet utilisateur, donc ses messages sont cachés. Les montrer quand même." + "You have ignored this user, so their message is hidden. Show anyways.": "Vous avez ignoré cet utilisateur, donc ses messages sont cachés. Les montrer quand même.", + "Custom (%(level)s)": "Personnalisé (%(level)s)", + "Trusted": "Fiable", + "Not trusted": "Non vérifié", + "Hide verified Sign-In's": "Masquer les connexions vérifiées", + "%(count)s verified Sign-In's|other": "%(count)s connexions vérifiées", + "%(count)s verified Sign-In's|one": "1 connexion vérifiée", + "Direct message": "Message direct", + "Unverify user": "Ne plus marquer l’utilisateur comme vérifié", + "%(role)s in %(roomName)s": "%(role)s dans %(roomName)s", + "Messages in this room are end-to-end encrypted.": "Les messages dans ce salon sont chiffrés de bout en bout.", + "Security": "Sécurité", + "Verify": "Vérifier" } From 16a107a1634c9f3071810172f57c74528789d49b Mon Sep 17 00:00:00 2001 From: Szimszon Date: Sat, 16 Nov 2019 09:23:56 +0000 Subject: [PATCH 0641/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1908 of 1908 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 29747bb1b6..3c049cc321 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2312,5 +2312,17 @@ "If this isn't what you want, please use a different tool to ignore users.": "Ha nem ez az amit szeretnél, kérlek használj más eszközt a felhasználók figyelmen kívül hagyásához.", "Room ID or alias of ban list": "Tiltó lista szoba azonosítója vagy alternatív neve", "Subscribe": "Feliratkozás", - "You have ignored this user, so their message is hidden. Show anyways.": "Ezt a felhasználót figyelmen kívül hagyod, így az üzenetei el lesznek rejtve. Mindenképpen megmutat." + "You have ignored this user, so their message is hidden. Show anyways.": "Ezt a felhasználót figyelmen kívül hagyod, így az üzenetei el lesznek rejtve. Mindenképpen megmutat.", + "Custom (%(level)s)": "Egyedi (%(level)s)", + "Trusted": "Megbízható", + "Not trusted": "Megbízhatatlan", + "Hide verified Sign-In's": "Ellenőrzött belépések elrejtése", + "%(count)s verified Sign-In's|other": "%(count)s ellenőrzött belépés", + "%(count)s verified Sign-In's|one": "1 ellenőrzött belépés", + "Direct message": "Közvetlen beszélgetés", + "Unverify user": "Ellenőrizetlen felhasználó", + "%(role)s in %(roomName)s": "%(role)s a szobában: %(roomName)s", + "Messages in this room are end-to-end encrypted.": "Az üzenetek a szobában végponttól végpontig titkosítottak.", + "Security": "Biztonság", + "Verify": "Ellenőriz" } From 20dd731ed017439929a4f8908e004bcd11a0e1e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Sat, 16 Nov 2019 04:00:39 +0000 Subject: [PATCH 0642/2372] Translated using Weblate (Korean) Currently translated at 100.0% (1908 of 1908 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index fe8c929acd..8d34fab025 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2158,5 +2158,28 @@ "View rules": "규칙 보기", "You are currently subscribed to:": "현재 구독 중임:", "⚠ These settings are meant for advanced users.": "⚠ 이 설정은 고급 사용자를 위한 것입니다.", - "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "무시하고 싶은 사용자와 서버를 여기에 추가하세요. 별표(*)를 사용해서 Riot이 이름과 문자를 맞춰볼 수 있습니다. 예를 들어, @bot:*이라면 모든 서버에서 'bot'이라는 문자를 가진 이름의 모든 사용자를 무시합니다." + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "무시하고 싶은 사용자와 서버를 여기에 추가하세요. 별표(*)를 사용해서 Riot이 이름과 문자를 맞춰볼 수 있습니다. 예를 들어, @bot:*이라면 모든 서버에서 'bot'이라는 문자를 가진 이름의 모든 사용자를 무시합니다.", + "Custom (%(level)s)": "맞춤 (%(level)s)", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "차단당하는 사람은 규칙에 따라 차단 목록을 통해 무시됩니다. 차단 목록을 구독하면 그 목록에서 차단당한 사용자/서버를 당신으로부터 감추게됩니다.", + "Personal ban list": "개인 차단 목록", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "개인 차단 목록은 개인적으로 보고 싶지 않은 메시지를 보낸 모든 사용자/서버를 담고 있습니다. 처음 사용자/서버를 무시했다면, 방 목록에 '나의 차단 목록'이라는 이름의 새 방이 나타납니다 - 차단 목록의 효력을 유지하려면 이 방을 그대로 두세요.", + "Server or user ID to ignore": "무시할 서버 또는 사용자 ID", + "eg: @bot:* or example.org": "예: @bot:* 또는 example.org", + "Subscribed lists": "구독 목록", + "Subscribing to a ban list will cause you to join it!": "차단 목록을 구독하면 차단 목록에 참여하게 됩니다!", + "If this isn't what you want, please use a different tool to ignore users.": "이것을 원한 것이 아니라면, 사용자를 무시하는 다른 도구를 사용해주세요.", + "Room ID or alias of ban list": "방 ID 또는 차단 목록의 별칭", + "Subscribe": "구독", + "Trusted": "신뢰함", + "Not trusted": "신뢰하지 않음", + "Hide verified Sign-In's": "확인 로그인 숨기기", + "%(count)s verified Sign-In's|other": "확인된 %(count)s개의 로그인", + "%(count)s verified Sign-In's|one": "확인된 1개의 로그인", + "Direct message": "다이렉트 메시지", + "Unverify user": "사용자 확인 취소", + "%(role)s in %(roomName)s": "%(roomName)s 방의 %(role)s", + "Messages in this room are end-to-end encrypted.": "이 방의 메시지는 종단간 암호화되었습니다.", + "Security": "보안", + "Verify": "확인", + "You have ignored this user, so their message is hidden. Show anyways.": "이 사용자를 무시했습니다. 사용자의 메시지는 숨겨집니다. 무시하고 보이기." } From 5d3a84db855bb8c3e89d8759b35c6feefa06eb47 Mon Sep 17 00:00:00 2001 From: Nobbele Date: Sun, 17 Nov 2019 11:58:39 +0000 Subject: [PATCH 0643/2372] Translated using Weblate (Swedish) Currently translated at 76.4% (1457 of 1908 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sv/ --- src/i18n/strings/sv.json | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 6d611be464..a1c31faf57 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -757,7 +757,7 @@ "Device Name": "Enhetsnamn", "Select devices": "Välj enheter", "Disable Emoji suggestions while typing": "Inaktivera Emoji-förslag medan du skriver", - "Use compact timeline layout": "Använd kompakt chattlayout", + "Use compact timeline layout": "Använd kompakt tidslinjelayout", "Not a valid Riot keyfile": "Inte en giltig Riot-nyckelfil", "Authentication check failed: incorrect password?": "Autentiseringskontroll misslyckades: felaktigt lösenord?", "Always show encryption icons": "Visa alltid krypteringsikoner", @@ -1019,7 +1019,7 @@ "Add rooms to the community": "Lägg till rum i communityn", "Add to community": "Lägg till i community", "Failed to invite users to community": "Det gick inte att bjuda in användare till communityn", - "Mirror local video feed": "Spegelvänd lokal video", + "Mirror local video feed": "Speglad lokal-video", "Disable Community Filter Panel": "Inaktivera community-filterpanel", "Community Invites": "Community-inbjudningar", "Invalid community ID": "Ogiltigt community-ID", @@ -1353,9 +1353,9 @@ "Show read receipts sent by other users": "Visa läskvitton som skickats av andra användare", "Show avatars in user and room mentions": "Visa avatarer i användar- och rumsnämningar", "Enable big emoji in chat": "Aktivera stor emoji i chatt", - "Send typing notifications": "Skicka \"skriver\"-status", + "Send typing notifications": "Skicka \"skriver\"-statusar", "Enable Community Filter Panel": "Aktivera community-filterpanel", - "Allow Peer-to-Peer for 1:1 calls": "Tillåt enhet-till-enhet-kommunikation för direktsamtal (mellan två personer)", + "Allow Peer-to-Peer for 1:1 calls": "Tillåt peer-to-peer kommunikation för 1:1 samtal", "Messages containing my username": "Meddelanden som innehåller mitt användarnamn", "Messages containing @room": "Meddelanden som innehåller @room", "Encrypted messages in one-to-one chats": "Krypterade meddelanden i privata chattar", @@ -1564,7 +1564,7 @@ "Please supply a https:// or http:// widget URL": "Ange en widget-URL med https:// eller http://", "You cannot modify widgets in this room.": "Du kan inte ändra widgets i detta rum.", "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s återkallade inbjudan för %(targetDisplayName)s att gå med i rummet.", - "Show a reminder to enable Secure Message Recovery in encrypted rooms": "", + "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Visa en påminnelse för att sätta på säker meddelande återhämtning i krypterade rum", "The other party cancelled the verification.": "Den andra parten avbröt verifieringen.", "Verified!": "Verifierad!", "You've successfully verified this user.": "Du har verifierat den här användaren.", @@ -1817,5 +1817,11 @@ "Add Phone Number": "Lägg till telefonnummer", "Identity server has no terms of service": "Identitetsserver har inga användarvillkor", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Den här åtgärden kräver åtkomst till standardidentitetsservern för att validera en e-postadress eller telefonnummer, men servern har inga användarvillkor.", - "Trust": "Förtroende" + "Trust": "Förtroende", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Använd den nya, konsistenta UserInfo panelen för rum medlemmar och grupp medlemmar", + "Try out new ways to ignore people (experimental)": "Testa nya sätt att ignorera personer (experimentalt)", + "Send verification requests in direct message": "Skicka verifikations begäran i direkt meddelanden", + "Use the new, faster, composer for writing messages": "Använd den nya, snabbare kompositören för att skriva meddelanden", + "Show previews/thumbnails for images": "Visa förhandsvisning/tumnagel för bilder" } From 47948812b083f903b4be8d4e249f61e229ff9cb9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Nov 2019 09:47:37 +0000 Subject: [PATCH 0644/2372] Attempt number two at ripping out Bluebird from rageshake.js --- src/rageshake/rageshake.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index d61956c925..820550af88 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -136,6 +136,8 @@ class IndexedDBLogStore { this.id = "instance-" + Math.random() + Date.now(); this.index = 0; this.db = null; + + // these promises are cleared as soon as fulfilled this.flushPromise = null; // set if flush() is called whilst one is ongoing this.flushAgainPromise = null; @@ -208,16 +210,16 @@ class IndexedDBLogStore { */ flush() { // check if a flush() operation is ongoing - if (this.flushPromise && this.flushPromise.isPending()) { - if (this.flushAgainPromise && this.flushAgainPromise.isPending()) { - // this is the 3rd+ time we've called flush() : return the same - // promise. + if (this.flushPromise) { + if (this.flushAgainPromise) { + // this is the 3rd+ time we've called flush() : return the same promise. return this.flushAgainPromise; } - // queue up a flush to occur immediately after the pending one - // completes. + // queue up a flush to occur immediately after the pending one completes. this.flushAgainPromise = this.flushPromise.then(() => { return this.flush(); + }).then(() => { + this.flushAgainPromise = null; }); return this.flushAgainPromise; } @@ -225,8 +227,7 @@ class IndexedDBLogStore { // a brand new one, destroying the chain which may have been built up. this.flushPromise = new Promise((resolve, reject) => { if (!this.db) { - // not connected yet or user rejected access for us to r/w to - // the db. + // not connected yet or user rejected access for us to r/w to the db. reject(new Error("No connected database")); return; } @@ -251,6 +252,8 @@ class IndexedDBLogStore { objStore.add(this._generateLogEntry(lines)); const lastModStore = txn.objectStore("logslastmod"); lastModStore.put(this._generateLastModifiedTime()); + }).then(() => { + this.flushPromise = null; }); return this.flushPromise; } From 30d4dd36a7d2086123b52d95123cbd3e43122754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:05:11 +0100 Subject: [PATCH 0645/2372] BaseEventIndexManager: Remove the flow annotation. --- src/BaseEventIndexManager.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index fe59cee673..c5a3273a45 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -1,5 +1,3 @@ -// @flow - /* Copyright 2019 New Vector Ltd From ab93745460501c13ec9f68fe118fbaa9f2c06480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:16:29 +0100 Subject: [PATCH 0646/2372] Fix the copyright headers from New Vector to The Matrix Foundation. --- src/BaseEventIndexManager.js | 2 +- src/EventIndexPeg.js | 2 +- src/EventIndexing.js | 2 +- src/Searching.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index c5a3273a45..7cefb023d1 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index da5c5425e4..54d9c40079 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 37167cf600..f2c3c5c433 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/Searching.js b/src/Searching.js index ee46a66fb8..cb641ec72a 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 9fa8e8238a8cc1e406ed2e6ef471bfb452d707c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:18:09 +0100 Subject: [PATCH 0647/2372] BaseEventIndexManager: Fix a typo. --- src/BaseEventIndexManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index 7cefb023d1..61c556a0ff 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -76,7 +76,7 @@ export default class BaseEventIndexManager { /** * Does our EventIndexManager support event indexing. * - * If an EventIndexManager imlpementor has runtime dependencies that + * If an EventIndexManager implementor has runtime dependencies that * optionally enable event indexing they may override this method to perform * the necessary runtime checks here. * From 5149164010f1237e9e07e3a1a03b5cc0738e9b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:23:04 +0100 Subject: [PATCH 0648/2372] MatrixChat: Revert the unnecessary changes in the MatrixChat class. --- src/components/structures/MatrixChat.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index f78bb5c168..b45884e64f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1222,7 +1222,7 @@ export default createReactClass({ /** * Called when the session is logged out */ - _onLoggedOut: async function() { + _onLoggedOut: function() { this.notifyNewScreen('login'); this.setStateForNewView({ view: VIEWS.LOGIN, @@ -1272,9 +1272,8 @@ export default createReactClass({ // particularly noticeable when there are lots of 'limited' /sync responses // such as when laptops unsleep. // https://github.com/vector-im/riot-web/issues/3307#issuecomment-282895568 - cli.setCanResetTimelineCallback(async function(roomId) { + cli.setCanResetTimelineCallback(function(roomId) { console.log("Request to reset timeline in room ", roomId, " viewing:", self.state.currentRoomId); - if (roomId !== self.state.currentRoomId) { // It is safe to remove events from rooms we are not viewing. return true; From 910c3ac08db4bbdf8097e998cb486e6cdfef1a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:26:17 +0100 Subject: [PATCH 0649/2372] BaseEventIndexManager: Fix some type annotations. --- src/BaseEventIndexManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BaseEventIndexManager.js b/src/BaseEventIndexManager.js index 61c556a0ff..5e8ca668ad 100644 --- a/src/BaseEventIndexManager.js +++ b/src/BaseEventIndexManager.js @@ -159,8 +159,8 @@ export default class BaseEventIndexManager { */ async addHistoricEvents( events: [HistoricEvent], - checkpoint: CrawlerCheckpoint | null = null, - oldCheckpoint: CrawlerCheckpoint | null = null, + checkpoint: CrawlerCheckpoint | null, + oldCheckpoint: CrawlerCheckpoint | null, ): Promise { throw new Error("Unimplemented"); } From ddb536e94a69485360611458cf70341720a3f604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:27:10 +0100 Subject: [PATCH 0650/2372] EventIndexPeg: Move a docstring to the correct place. --- src/EventIndexPeg.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 54d9c40079..4d0e518ab8 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -30,7 +30,8 @@ class EventIndexPeg { this.index = null; } - /** Create a new EventIndex and initialize it if the platform supports it. + /** + * Create a new EventIndex and initialize it if the platform supports it. * * @return {Promise} A promise that will resolve to true if an * EventIndex was successfully initialized, false otherwise. From 050e52ce461de709c01b1697379e6f8ad7fac18a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:34:48 +0100 Subject: [PATCH 0651/2372] EventIndexPeg: Treat both cases of unavailable platform support the same. --- src/EventIndexPeg.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 4d0e518ab8..f1841b3f2b 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -38,9 +38,7 @@ class EventIndexPeg { */ async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager === null) return false; - - if (await indexManager.supportsEventIndexing() !== true) { + if (!indexManager || await indexManager.supportsEventIndexing() !== true) { console.log("EventIndex: Platform doesn't support event indexing,", "not initializing."); return false; From 3b06c684d23fd4e5cd012ccc197926b612fef63b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:35:57 +0100 Subject: [PATCH 0652/2372] EventIndexing: Don't capitalize homeserver. --- src/EventIndexing.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index f2c3c5c433..05d5fd03da 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -59,8 +59,8 @@ export default class EventIndexer { return client.isRoomEncrypted(room.roomId); }; - // We only care to crawl the encrypted rooms, non-encrytped - // rooms can use the search provided by the Homeserver. + // We only care to crawl the encrypted rooms, non-encrypted. + // rooms can use the search provided by the homeserver. const encryptedRooms = rooms.filter(isRoomEncrypted); console.log("EventIndex: Adding initial crawler checkpoints"); @@ -189,7 +189,7 @@ export default class EventIndexer { while (!cancelled) { // This is a low priority task and we don't want to spam our - // Homeserver with /messages requests so we set a hefty timeout + // homeserver with /messages requests so we set a hefty timeout // here. await sleep(this._crawlerTimeout); @@ -210,7 +210,7 @@ export default class EventIndexer { console.log("EventIndex: crawling using checkpoint", checkpoint); // We have a checkpoint, let us fetch some messages, again, very - // conservatively to not bother our Homeserver too much. + // conservatively to not bother our homeserver too much. const eventMapper = client.getEventMapper(); // TODO we need to ensure to use member lazy loading with this // request so we get the correct profiles. From b4a6123295c896b49952302e4ced6ce166034419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:48:18 +0100 Subject: [PATCH 0653/2372] Searching: Move a comment to the correct place. --- src/Searching.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Searching.js b/src/Searching.js index cb641ec72a..4e6c8b9b4d 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -20,8 +20,9 @@ import MatrixClientPeg from "./MatrixClientPeg"; function serverSideSearch(term, roomId = undefined) { let filter; if (roomId !== undefined) { + // XXX: it's unintuitive that the filter for searching doesn't have + // the same shape as the v2 filter API :( filter = { - // XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :( rooms: [roomId], }; } From a4ad8151f8415bf76bd1fd16b64ee167cc1bec0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:55:02 +0100 Subject: [PATCH 0654/2372] Searching: Use the short form to build the search arguments object. --- src/Searching.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index 4e6c8b9b4d..eb7137e221 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -28,8 +28,8 @@ function serverSideSearch(term, roomId = undefined) { } const searchPromise = MatrixClientPeg.get().searchRoomEvents({ - filter: filter, - term: term, + filter, + term, }); return searchPromise; From 0e3a0008df387bb036867177bb92451702f3fff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:56:57 +0100 Subject: [PATCH 0655/2372] Searching: Remove the func suffix from our search functions. --- src/Searching.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index eb7137e221..601da56f86 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -35,11 +35,11 @@ function serverSideSearch(term, roomId = undefined) { return searchPromise; } -async function combinedSearchFunc(searchTerm) { +async function combinedSearch(searchTerm) { // Create two promises, one for the local search, one for the // server-side search. const serverSidePromise = serverSideSearch(searchTerm); - const localPromise = localSearchFunc(searchTerm); + const localPromise = localSearch(searchTerm); // Wait for both promises to resolve. await Promise.all([serverSidePromise, localPromise]); @@ -74,7 +74,7 @@ async function combinedSearchFunc(searchTerm) { return result; } -async function localSearchFunc(searchTerm, roomId = undefined) { +async function localSearch(searchTerm, roomId = undefined) { const searchArgs = { search_term: searchTerm, before_limit: 1, @@ -115,7 +115,7 @@ function eventIndexSearch(term, roomId = undefined) { if (MatrixClientPeg.get().isRoomEncrypted(roomId)) { // The search is for a single encrypted room, use our local // search method. - searchPromise = localSearchFunc(term, roomId); + searchPromise = localSearch(term, roomId); } else { // The search is for a single non-encrypted room, use the // server-side search. @@ -124,7 +124,7 @@ function eventIndexSearch(term, roomId = undefined) { } else { // Search across all rooms, combine a server side search and a // local search. - searchPromise = combinedSearchFunc(term); + searchPromise = combinedSearch(term); } return searchPromise; From 2bb331cdf0c635728d2a08f993e2cb186a89e381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 10:57:23 +0100 Subject: [PATCH 0656/2372] Searching: Fix a typo. --- src/Searching.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Searching.js b/src/Searching.js index 601da56f86..ca3e7f041f 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -52,7 +52,7 @@ async function combinedSearch(searchTerm) { const result = {}; // Our localResult and serverSideResult are both ordered by - // recency separetly, when we combine them the order might not + // recency separately, when we combine them the order might not // be the right one so we need to sort them. const compare = (a, b) => { const aEvent = a.context.getEvent().event; From d4d51dc61f75bcaab50184e066da23fc5cabfbdc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Nov 2019 10:03:05 +0000 Subject: [PATCH 0657/2372] Rip out the remainder of Bluebird --- .babelrc | 1 - package.json | 2 -- src/ContentMessages.js | 1 - src/Lifecycle.js | 5 ++--- src/Modal.js | 1 - src/Notifier.js | 2 +- src/Resend.js | 2 +- src/RoomNotifs.js | 1 - src/Rooms.js | 1 - src/ScalarAuthClient.js | 1 - src/ScalarMessaging.js | 8 ++++---- src/SlashCommands.js | 1 - src/Terms.js | 1 - src/ToWidgetPostMessageApi.js | 2 -- src/VectorConferenceHandler.js | 1 - src/autocomplete/Autocompleter.js | 1 - src/components/structures/GroupView.js | 5 ++--- src/components/structures/InteractiveAuth.js | 2 +- src/components/structures/MatrixChat.js | 10 ++++------ src/components/structures/MyGroups.js | 2 +- src/components/structures/RoomDirectory.js | 9 ++++----- src/components/structures/RoomView.js | 9 ++++----- src/components/structures/ScrollPanel.js | 1 - src/components/structures/TimelinePanel.js | 3 +-- src/components/structures/auth/ForgotPassword.js | 2 +- src/components/structures/auth/Login.js | 4 ++-- .../structures/auth/PostRegistration.js | 2 +- src/components/structures/auth/Registration.js | 3 +-- .../views/auth/InteractiveAuthEntryComponents.js | 2 +- .../views/context_menus/RoomTileContextMenu.js | 4 ++-- .../views/dialogs/AddressPickerDialog.js | 4 ++-- .../views/dialogs/CreateGroupDialog.js | 2 +- src/components/views/dialogs/KeyShareDialog.js | 2 +- src/components/views/dialogs/SetEmailDialog.js | 4 ++-- src/components/views/dialogs/SetMxIdDialog.js | 1 - src/components/views/elements/AppTile.js | 2 +- .../views/elements/EditableTextContainer.js | 5 ++--- src/components/views/elements/ErrorBoundary.js | 2 +- src/components/views/elements/ImageView.js | 2 +- .../views/elements/LanguageDropdown.js | 2 +- src/components/views/groups/GroupUserSettings.js | 2 +- src/components/views/messages/MAudioBody.js | 2 +- src/components/views/messages/MImageBody.js | 3 +-- src/components/views/messages/MVideoBody.js | 3 +-- src/components/views/right_panel/UserInfo.js | 2 +- .../views/room_settings/ColorSettings.js | 1 - src/components/views/rooms/Autocomplete.js | 1 - src/components/views/rooms/LinkPreviewWidget.js | 2 +- src/components/views/rooms/MemberInfo.js | 6 +++--- src/components/views/settings/ChangeAvatar.js | 2 +- src/components/views/settings/ChangePassword.js | 3 +-- src/components/views/settings/DevicesPanel.js | 2 +- src/components/views/settings/Notifications.js | 15 +++++++-------- .../settings/tabs/user/HelpUserSettingsTab.js | 2 +- src/createRoom.js | 1 - src/languageHandler.js | 1 - src/rageshake/rageshake.js | 2 -- src/rageshake/submit-rageshake.js | 1 - src/settings/handlers/DeviceSettingsHandler.js | 1 - src/settings/handlers/LocalEchoWrapper.js | 1 - .../handlers/RoomDeviceSettingsHandler.js | 1 - src/settings/handlers/SettingsHandler.js | 2 -- src/stores/FlairStore.js | 1 - src/stores/RoomViewStore.js | 2 +- src/utils/DecryptFile.js | 1 - src/utils/MultiInviter.js | 2 -- src/utils/promise.js | 3 --- .../views/dialogs/InteractiveAuthDialog-test.js | 1 - .../elements/MemberEventListSummary-test.js | 2 +- .../views/rooms/MessageComposerInput-test.js | 1 - test/components/views/rooms/RoomSettings-test.js | 1 - test/i18n-test/languageHandler-test.js | 2 +- test/stores/RoomViewStore-test.js | 2 -- test/test-utils.js | 1 - yarn.lock | 16 +++------------- 75 files changed, 71 insertions(+), 135 deletions(-) diff --git a/.babelrc b/.babelrc index 3fb847ad18..abe7e1ef3f 100644 --- a/.babelrc +++ b/.babelrc @@ -13,7 +13,6 @@ ], "transform-class-properties", "transform-object-rest-spread", - "transform-async-to-bluebird", "transform-runtime", "add-module-exports", "syntax-dynamic-import" diff --git a/package.json b/package.json index eb234e0573..620b323af7 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "dependencies": { "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-runtime": "^6.26.0", - "bluebird": "^3.5.0", "blueimp-canvas-to-blob": "^3.5.0", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", @@ -120,7 +119,6 @@ "babel-eslint": "^10.0.1", "babel-loader": "^7.1.5", "babel-plugin-add-module-exports": "^0.2.1", - "babel-plugin-transform-async-to-bluebird": "^1.1.1", "babel-plugin-transform-builtin-extend": "^1.1.2", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-object-rest-spread": "^6.26.0", diff --git a/src/ContentMessages.js b/src/ContentMessages.js index dab8de2465..6908a6a18e 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -17,7 +17,6 @@ limitations under the License. 'use strict'; -import Promise from 'bluebird'; import extend from './extend'; import dis from './dispatcher'; import MatrixClientPeg from './MatrixClientPeg'; diff --git a/src/Lifecycle.js b/src/Lifecycle.js index ffd5baace4..c519e52872 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -16,7 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import Matrix from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; @@ -525,7 +524,7 @@ export function logout() { console.log("Failed to call logout API: token will not be invalidated"); onLoggedOut(); }, - ).done(); + ); } export function softLogout() { @@ -614,7 +613,7 @@ export function onLoggedOut() { // that can occur when components try to use a null client. dis.dispatch({action: 'on_logged_out'}, true); stopMatrixClient(); - _clearStorage().done(); + _clearStorage(); } /** diff --git a/src/Modal.js b/src/Modal.js index cb19731f01..4fc9fdcb02 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -23,7 +23,6 @@ import Analytics from './Analytics'; import sdk from './index'; import dis from './dispatcher'; import { _t } from './languageHandler'; -import Promise from "bluebird"; import {defer} from "./utils/promise"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; diff --git a/src/Notifier.js b/src/Notifier.js index cca0ea2b89..edb9850dfe 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -198,7 +198,7 @@ const Notifier = { if (enable) { // Attempt to get permission from user - plaf.requestNotificationPermission().done((result) => { + plaf.requestNotificationPermission().then((result) => { if (result !== 'granted') { // The permission request was dismissed or denied // TODO: Support alternative branding in messaging diff --git a/src/Resend.js b/src/Resend.js index 4eaee16d1b..51ec804c01 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -35,7 +35,7 @@ module.exports = { }, resend: function(event) { const room = MatrixClientPeg.get().getRoom(event.getRoomId()); - MatrixClientPeg.get().resendEvent(event, room).done(function(res) { + MatrixClientPeg.get().resendEvent(event, room).then(function(res) { dis.dispatch({ action: 'message_sent', event: event, diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js index 2d5e4b3136..5bef4afd25 100644 --- a/src/RoomNotifs.js +++ b/src/RoomNotifs.js @@ -17,7 +17,6 @@ limitations under the License. import MatrixClientPeg from './MatrixClientPeg'; import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; -import Promise from 'bluebird'; export const ALL_MESSAGES_LOUD = 'all_messages_loud'; export const ALL_MESSAGES = 'all_messages'; diff --git a/src/Rooms.js b/src/Rooms.js index c8f90ec39a..239e348b58 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -15,7 +15,6 @@ limitations under the License. */ import MatrixClientPeg from './MatrixClientPeg'; -import Promise from 'bluebird'; /** * Given a room object, return the alias we should use for it, diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index 3623d47f8e..92f0ff6340 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -16,7 +16,6 @@ limitations under the License. */ import url from 'url'; -import Promise from 'bluebird'; import SettingsStore from "./settings/SettingsStore"; import { Service, startTermsFlow, TermsNotSignedError } from './Terms'; const request = require('browser-request'); diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 910a6c4f13..c0ffc3022d 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -279,7 +279,7 @@ function inviteUser(event, roomId, userId) { } } - client.invite(roomId, userId).done(function() { + client.invite(roomId, userId).then(function() { sendResponse(event, { success: true, }); @@ -398,7 +398,7 @@ function setPlumbingState(event, roomId, status) { sendError(event, _t('You need to be logged in.')); return; } - client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).done(() => { + client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).then(() => { sendResponse(event, { success: true, }); @@ -414,7 +414,7 @@ function setBotOptions(event, roomId, userId) { sendError(event, _t('You need to be logged in.')); return; } - client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => { + client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).then(() => { sendResponse(event, { success: true, }); @@ -444,7 +444,7 @@ function setBotPower(event, roomId, userId, level) { }, ); - client.setPowerLevel(roomId, userId, level, powerEvent).done(() => { + client.setPowerLevel(roomId, userId, level, powerEvent).then(() => { sendResponse(event, { success: true, }); diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 1a491da54f..31e7ca4f39 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -28,7 +28,6 @@ import { linkifyAndSanitizeHtml } from './HtmlUtils'; import QuestionDialog from "./components/views/dialogs/QuestionDialog"; import WidgetUtils from "./utils/WidgetUtils"; import {textToHtmlRainbow} from "./utils/colour"; -import Promise from "bluebird"; import { getAddressType } from './UserAddress'; import { abbreviateUrl } from './utils/UrlUtils'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils'; diff --git a/src/Terms.js b/src/Terms.js index 685a39709c..14a7ccb65e 100644 --- a/src/Terms.js +++ b/src/Terms.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import classNames from 'classnames'; import MatrixClientPeg from './MatrixClientPeg'; diff --git a/src/ToWidgetPostMessageApi.js b/src/ToWidgetPostMessageApi.js index def4af56ae..00309d252c 100644 --- a/src/ToWidgetPostMessageApi.js +++ b/src/ToWidgetPostMessageApi.js @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from "bluebird"; - // const OUTBOUND_API_NAME = 'toWidget'; // Initiate requests using the "toWidget" postMessage API and handle responses diff --git a/src/VectorConferenceHandler.js b/src/VectorConferenceHandler.js index 37b3a7ddad..e0e333a371 100644 --- a/src/VectorConferenceHandler.js +++ b/src/VectorConferenceHandler.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import {createNewMatrixCall, Room} from "matrix-js-sdk"; import CallHandler from './CallHandler'; import MatrixClientPeg from "./MatrixClientPeg"; diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index c385e13878..a26eb6033b 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -26,7 +26,6 @@ import RoomProvider from './RoomProvider'; import UserProvider from './UserProvider'; import EmojiProvider from './EmojiProvider'; import NotifProvider from './NotifProvider'; -import Promise from 'bluebird'; import {timeout} from "../utils/promise"; export type SelectionRange = { diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 776e7f0d6d..a0aa36803f 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -19,7 +19,6 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; -import Promise from 'bluebird'; import MatrixClientPeg from '../../MatrixClientPeg'; import sdk from '../../index'; import dis from '../../dispatcher'; @@ -637,7 +636,7 @@ export default createReactClass({ title: _t('Error'), description: _t('Failed to upload image'), }); - }).done(); + }); }, _onJoinableChange: function(ev) { @@ -676,7 +675,7 @@ export default createReactClass({ this.setState({ avatarChanged: false, }); - }).done(); + }); }, _saveGroup: async function() { diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 5e06d124c4..e1b02f653b 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -121,7 +121,7 @@ export default createReactClass({ this.setState({ errorText: msg, }); - }).done(); + }); this._intervalId = null; if (this.props.poll) { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b499cb6e42..455f039896 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -17,8 +17,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; - import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; @@ -542,7 +540,7 @@ export default createReactClass({ const Loader = sdk.getComponent("elements.Spinner"); const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); - MatrixClientPeg.get().leave(payload.room_id).done(() => { + MatrixClientPeg.get().leave(payload.room_id).then(() => { modal.close(); if (this.state.currentRoomId === payload.room_id) { dis.dispatch({action: 'view_next_room'}); @@ -863,7 +861,7 @@ export default createReactClass({ waitFor = this.firstSyncPromise.promise; } - waitFor.done(() => { + waitFor.then(() => { let presentedId = roomInfo.room_alias || roomInfo.room_id; const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { @@ -980,7 +978,7 @@ export default createReactClass({ const [shouldCreate, createOpts] = await modal.finished; if (shouldCreate) { - createRoom({createOpts}).done(); + createRoom({createOpts}); } }, @@ -1756,7 +1754,7 @@ export default createReactClass({ return; } - cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => { + cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => { dis.dispatch({action: 'message_sent'}); }, (err) => { dis.dispatch({action: 'message_send_failed'}); diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 2de15a5444..63ae14ba09 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -47,7 +47,7 @@ export default createReactClass({ }, _fetch: function() { - this.context.matrixClient.getJoinedGroups().done((result) => { + this.context.matrixClient.getJoinedGroups().then((result) => { this.setState({groups: result.groups, error: null}); }, (err) => { if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') { diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 84f402e484..efca8d12a8 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -27,7 +27,6 @@ const dis = require('../../dispatcher'); import { linkifyAndSanitizeHtml } from '../../HtmlUtils'; import PropTypes from 'prop-types'; -import Promise from 'bluebird'; import { _t } from '../../languageHandler'; import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils'; import Analytics from '../../Analytics'; @@ -89,7 +88,7 @@ module.exports = createReactClass({ this.setState({protocolsLoading: false}); return; } - MatrixClientPeg.get().getThirdpartyProtocols().done((response) => { + MatrixClientPeg.get().getThirdpartyProtocols().then((response) => { this.protocols = response; this.setState({protocolsLoading: false}); }, (err) => { @@ -135,7 +134,7 @@ module.exports = createReactClass({ publicRooms: [], loading: true, }); - this.getMoreRooms().done(); + this.getMoreRooms(); }, getMoreRooms: function() { @@ -246,7 +245,7 @@ module.exports = createReactClass({ if (!alias) return; step = _t('delete the alias.'); return MatrixClientPeg.get().deleteAlias(alias); - }).done(() => { + }).then(() => { modal.close(); this.refreshRoomList(); }, (err) => { @@ -348,7 +347,7 @@ module.exports = createReactClass({ }); return; } - MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => { + MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).then((resp) => { if (resp.length > 0 && resp[0].alias) { this.showRoomAlias(resp[0].alias, true); } else { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4de573479d..ca558f2456 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -27,7 +27,6 @@ import React from 'react'; import createReactClass from 'create-react-class'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; -import Promise from 'bluebird'; import classNames from 'classnames'; import {Room} from "matrix-js-sdk"; import { _t } from '../../languageHandler'; @@ -1101,7 +1100,7 @@ module.exports = createReactClass({ } ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get()) - .done(undefined, (error) => { + .then(undefined, (error) => { if (error.name === "UnknownDeviceError") { // Let the staus bar handle this return; @@ -1145,7 +1144,7 @@ module.exports = createReactClass({ filter: filter, term: term, }); - this._handleSearchResult(searchPromise).done(); + this._handleSearchResult(searchPromise); }, _handleSearchResult: function(searchPromise) { @@ -1316,7 +1315,7 @@ module.exports = createReactClass({ }, onForgetClick: function() { - MatrixClientPeg.get().forget(this.state.room.roomId).done(function() { + MatrixClientPeg.get().forget(this.state.room.roomId).then(function() { dis.dispatch({ action: 'view_next_room' }); }, function(err) { const errCode = err.errcode || _t("unknown error code"); @@ -1333,7 +1332,7 @@ module.exports = createReactClass({ this.setState({ rejecting: true, }); - MatrixClientPeg.get().leave(this.state.roomId).done(function() { + MatrixClientPeg.get().leave(this.state.roomId).then(function() { dis.dispatch({ action: 'view_next_room' }); self.setState({ rejecting: false, diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 1d5c520285..8a67e70467 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -17,7 +17,6 @@ limitations under the License. import React from "react"; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; -import Promise from 'bluebird'; import { KeyCode } from '../../Keyboard'; import Timer from '../../utils/Timer'; import AutoHideScrollbar from "./AutoHideScrollbar"; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 3dd5ea761e..7b0791ff1d 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -23,7 +23,6 @@ import React from 'react'; import createReactClass from 'create-react-class'; import ReactDOM from "react-dom"; import PropTypes from 'prop-types'; -import Promise from 'bluebird'; const Matrix = require("matrix-js-sdk"); const EventTimeline = Matrix.EventTimeline; @@ -462,7 +461,7 @@ const TimelinePanel = createReactClass({ // timeline window. // // see https://github.com/vector-im/vector-web/issues/1035 - this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => { + this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => { if (this.unmounted) { return; } const { events, liveEvents } = this._getEvents(); diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 46a5fa7bd7..6f68293caa 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -105,7 +105,7 @@ module.exports = createReactClass({ phase: PHASE_SENDING_EMAIL, }); this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl); - this.reset.resetPassword(email, password).done(() => { + this.reset.resetPassword(email, password).then(() => { this.setState({ phase: PHASE_EMAIL_SENT, }); diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index ad77ed49a5..2cdf5890cf 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -253,7 +253,7 @@ module.exports = createReactClass({ this.setState({ busy: false, }); - }).done(); + }); }, onUsernameChanged: function(username) { @@ -424,7 +424,7 @@ module.exports = createReactClass({ this.setState({ busy: false, }); - }).done(); + }); }, _isSupportedFlow: function(flow) { diff --git a/src/components/structures/auth/PostRegistration.js b/src/components/structures/auth/PostRegistration.js index 66075c80f7..760163585d 100644 --- a/src/components/structures/auth/PostRegistration.js +++ b/src/components/structures/auth/PostRegistration.js @@ -43,7 +43,7 @@ module.exports = createReactClass({ const cli = MatrixClientPeg.get(); this.setState({busy: true}); const self = this; - cli.getProfileInfo(cli.credentials.userId).done(function(result) { + cli.getProfileInfo(cli.credentials.userId).then(function(result) { self.setState({ avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url), busy: false, diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 6321028457..3578d745f5 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -18,7 +18,6 @@ limitations under the License. */ import Matrix from 'matrix-js-sdk'; -import Promise from 'bluebird'; import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; @@ -371,7 +370,7 @@ module.exports = createReactClass({ if (pushers[i].kind === 'email') { const emailPusher = pushers[i]; emailPusher.data = { brand: this.props.brand }; - matrixClient.setPusher(emailPusher).done(() => { + matrixClient.setPusher(emailPusher).then(() => { console.log("Set email branding to " + this.props.brand); }, (error) => { console.error("Couldn't set email branding: " + error); diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index d19ce95b33..cc3f9f96c4 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -441,7 +441,7 @@ export const MsisdnAuthEntry = createReactClass({ this.props.fail(e); }).finally(() => { this.setState({requestingToken: false}); - }).done(); + }); }, /* diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index fb056ee47f..97433e1f77 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -160,7 +160,7 @@ module.exports = createReactClass({ _onClickForget: function() { // FIXME: duplicated with RoomSettings (and dead code in RoomView) - MatrixClientPeg.get().forget(this.props.room.roomId).done(() => { + MatrixClientPeg.get().forget(this.props.room.roomId).then(() => { // Switch to another room view if we're currently viewing the // historical room if (RoomViewStore.getRoomId() === this.props.room.roomId) { @@ -190,7 +190,7 @@ module.exports = createReactClass({ this.setState({ roomNotifState: newState, }); - RoomNotifs.setRoomNotifsState(roomId, newState).done(() => { + RoomNotifs.setRoomNotifsState(roomId, newState).then(() => { // delay slightly so that the user can see their state change // before closing the menu return sleep(500).then(() => { diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 24d8b96e0c..a40495893d 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -266,7 +266,7 @@ module.exports = createReactClass({ this.setState({ searchError: err.errcode ? err.message : _t('Something went wrong!'), }); - }).done(() => { + }).then(() => { this.setState({ busy: false, }); @@ -379,7 +379,7 @@ module.exports = createReactClass({ // Do a local search immediately this._doLocalSearch(query); } - }).done(() => { + }).then(() => { this.setState({ busy: false, }); diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 11f4c21366..3430a12e71 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -93,7 +93,7 @@ export default createReactClass({ this.setState({createError: e}); }).finally(() => { this.setState({creating: false}); - }).done(); + }); }, _onCancel: function() { diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index a10c25a0fb..01e3479bb1 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -78,7 +78,7 @@ export default createReactClass({ true, ); } - }).done(); + }); }, componentWillUnmount: function() { diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.js index bedf713c4e..b527abffc9 100644 --- a/src/components/views/dialogs/SetEmailDialog.js +++ b/src/components/views/dialogs/SetEmailDialog.js @@ -62,7 +62,7 @@ export default createReactClass({ return; } this._addThreepid = new AddThreepid(); - this._addThreepid.addEmailAddress(emailAddress).done(() => { + this._addThreepid.addEmailAddress(emailAddress).then(() => { Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { title: _t("Verification Pending"), description: _t( @@ -96,7 +96,7 @@ export default createReactClass({ }, verifyEmailAddress: function() { - this._addThreepid.checkEmailLinkClicked().done(() => { + this._addThreepid.checkEmailLinkClicked().then(() => { this.props.onFinished(true); }, (err) => { this.setState({emailBusy: false}); diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 3bc6f5597e..598d0ce354 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 260b63dfd4..453630413c 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -205,7 +205,7 @@ export default class AppTile extends React.Component { if (!this._scalarClient) { this._scalarClient = defaultManager.getScalarClient(); } - this._scalarClient.getScalarToken().done((token) => { + this._scalarClient.getScalarToken().then((token) => { // Append scalar_token as a query param if not already present this._scalarClient.scalarToken = token; const u = url.parse(this._addWurlParams(this.props.url)); diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.js index 3bf37df951..5cba98470c 100644 --- a/src/components/views/elements/EditableTextContainer.js +++ b/src/components/views/elements/EditableTextContainer.js @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; -import Promise from 'bluebird'; /** * A component which wraps an EditableText, with a spinner while updates take @@ -51,7 +50,7 @@ export default class EditableTextContainer extends React.Component { this.setState({busy: true}); - this.props.getInitialValue().done( + this.props.getInitialValue().then( (result) => { if (this._unmounted) { return; } this.setState({ @@ -83,7 +82,7 @@ export default class EditableTextContainer extends React.Component { errorString: null, }); - this.props.onSubmit(value).done( + this.props.onSubmit(value).then( () => { if (this._unmounted) { return; } this.setState({ diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.js index e53e1ec0fa..e36464c4ef 100644 --- a/src/components/views/elements/ErrorBoundary.js +++ b/src/components/views/elements/ErrorBoundary.js @@ -54,7 +54,7 @@ export default class ErrorBoundary extends React.PureComponent { if (!PlatformPeg.get()) return; MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().store.deleteAllData().done(() => { + MatrixClientPeg.get().store.deleteAllData().then(() => { PlatformPeg.get().reload(); }); }; diff --git a/src/components/views/elements/ImageView.js b/src/components/views/elements/ImageView.js index 2772363bd0..b2f6d0abbb 100644 --- a/src/components/views/elements/ImageView.js +++ b/src/components/views/elements/ImageView.js @@ -84,7 +84,7 @@ export default class ImageView extends React.Component { title: _t('Error'), description: _t('You cannot delete this image. (%(code)s)', {code: code}), }); - }).done(); + }); }, }); }; diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index 365f9ded61..451c97d958 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -49,7 +49,7 @@ export default class LanguageDropdown extends React.Component { this.setState({langs}); }).catch(() => { this.setState({langs: ['en']}); - }).done(); + }); if (!this.props.value) { // If no value is given, we start with the first diff --git a/src/components/views/groups/GroupUserSettings.js b/src/components/views/groups/GroupUserSettings.js index 7d80bdd209..3cd5731b99 100644 --- a/src/components/views/groups/GroupUserSettings.js +++ b/src/components/views/groups/GroupUserSettings.js @@ -36,7 +36,7 @@ export default createReactClass({ }, componentWillMount: function() { - this.context.matrixClient.getJoinedGroups().done((result) => { + this.context.matrixClient.getJoinedGroups().then((result) => { this.setState({groups: result.groups || [], error: null}); }, (err) => { console.error(err); diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index b4f26d0cbd..0246d28542 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -55,7 +55,7 @@ export default class MAudioBody extends React.Component { decryptFile(content.file).then(function(blob) { decryptedBlob = blob; return URL.createObjectURL(decryptedBlob); - }).done((url) => { + }).then((url) => { this.setState({ decryptedUrl: url, decryptedBlob: decryptedBlob, diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 640baa1966..b12957a7df 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -24,7 +24,6 @@ import MFileBody from './MFileBody'; import Modal from '../../../Modal'; import sdk from '../../../index'; import { decryptFile } from '../../../utils/DecryptFile'; -import Promise from 'bluebird'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; @@ -289,7 +288,7 @@ export default class MImageBody extends React.Component { this.setState({ error: err, }); - }).done(); + }); } // Remember that the user wanted to show this particular image diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index d277b6eae9..43e4f2dd75 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -20,7 +20,6 @@ import createReactClass from 'create-react-class'; import MFileBody from './MFileBody'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { decryptFile } from '../../../utils/DecryptFile'; -import Promise from 'bluebird'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; @@ -115,7 +114,7 @@ module.exports = createReactClass({ this.setState({ error: err, }); - }).done(); + }); } }, diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 207bf29998..8c4d5a3586 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -870,7 +870,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room }, ).finally(() => { stopUpdating(); - }).done(); + }); }; const roomId = user.roomId; diff --git a/src/components/views/room_settings/ColorSettings.js b/src/components/views/room_settings/ColorSettings.js index aab6c04f53..952c49828b 100644 --- a/src/components/views/room_settings/ColorSettings.js +++ b/src/components/views/room_settings/ColorSettings.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index d4b51081f4..76a3a19e00 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -21,7 +21,6 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import flatMap from 'lodash/flatMap'; import type {Completion} from '../../../autocomplete/Autocompleter'; -import Promise from 'bluebird'; import { Room } from 'matrix-js-sdk'; import SettingsStore from "../../../settings/SettingsStore"; diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index d93fe76b46..3826c410bf 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -53,7 +53,7 @@ module.exports = createReactClass({ ); }, (error)=>{ console.error("Failed to get URL preview: " + error); - }).done(); + }); }, componentDidMount: function() { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 2ea6392e96..cd6de64a5a 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -248,7 +248,7 @@ module.exports = createReactClass({ return client.getStoredDevicesForUser(member.userId); }).finally(function() { self._cancelDeviceList = null; - }).done(function(devices) { + }).then(function(devices) { if (cancelled) { // we got cancelled - presumably a different user now return; @@ -572,7 +572,7 @@ module.exports = createReactClass({ }, ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); - }).done(); + }); }, onPowerChange: async function(powerLevel) { @@ -629,7 +629,7 @@ module.exports = createReactClass({ this.setState({ updating: this.state.updating + 1 }); createRoom({dmUserId: this.props.member.userId}).finally(() => { this.setState({ updating: this.state.updating - 1 }); - }).done(); + }); }, onLeaveClick: function() { diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 32521006c7..904b17b15f 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -112,7 +112,7 @@ module.exports = createReactClass({ } }); - httpPromise.done(function() { + httpPromise.then(function() { self.setState({ phase: self.Phases.Display, avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl), diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 91292b19f9..a317c46cec 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -25,7 +25,6 @@ const Modal = require("../../../Modal"); const sdk = require("../../../index"); import dis from "../../../dispatcher"; -import Promise from 'bluebird'; import AccessibleButton from '../elements/AccessibleButton'; import { _t } from '../../../languageHandler'; @@ -174,7 +173,7 @@ module.exports = createReactClass({ newPassword: "", newPasswordConfirm: "", }); - }).done(); + }); }, _optionallySetEmail: function() { diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index 30f507ea18..cb5db10be4 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -52,7 +52,7 @@ export default class DevicesPanel extends React.Component { } _loadDevices() { - MatrixClientPeg.get().getDevices().done( + MatrixClientPeg.get().getDevices().then( (resp) => { if (this._unmounted) { return; } this.setState({devices: resp.devices || []}); diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index e3b4cfe122..6c71101eb8 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -16,7 +16,6 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; -import Promise from 'bluebird'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; @@ -97,7 +96,7 @@ module.exports = createReactClass({ phase: this.phases.LOADING, }); - MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).done(function() { + MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).then(function() { self._refreshFromServer(); }); }, @@ -170,7 +169,7 @@ module.exports = createReactClass({ emailPusher.kind = null; emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher); } - emailPusherPromise.done(() => { + emailPusherPromise.then(() => { this._refreshFromServer(); }, (error) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -274,7 +273,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).done(function() { + Promise.all(deferreds).then(function() { self._refreshFromServer(); }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -343,7 +342,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).done(function(resps) { + Promise.all(deferreds).then(function(resps) { self._refreshFromServer(); }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -398,7 +397,7 @@ module.exports = createReactClass({ }; // Then, add the new ones - Promise.all(removeDeferreds).done(function(resps) { + Promise.all(removeDeferreds).then(function(resps) { const deferreds = []; let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState; @@ -434,7 +433,7 @@ module.exports = createReactClass({ } } - Promise.all(deferreds).done(function(resps) { + Promise.all(deferreds).then(function(resps) { self._refreshFromServer(); }, onError); }, onError); @@ -650,7 +649,7 @@ module.exports = createReactClass({ externalContentRules: self.state.externalContentRules, externalPushRules: self.state.externalPushRules, }); - }).done(); + }); MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids})); }, diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index fbad327078..875f0bfc10 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -75,7 +75,7 @@ export default class HelpUserSettingsTab extends React.Component { // stopping in the middle of the logs. console.log("Clear cache & reload clicked"); MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().store.deleteAllData().done(() => { + MatrixClientPeg.get().store.deleteAllData().then(() => { PlatformPeg.get().reload(); }); }; diff --git a/src/createRoom.js b/src/createRoom.js index 120043247d..0ee90beba8 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -21,7 +21,6 @@ import { _t } from './languageHandler'; import dis from "./dispatcher"; import * as Rooms from "./Rooms"; -import Promise from 'bluebird'; import {getAddressType} from "./UserAddress"; /** diff --git a/src/languageHandler.js b/src/languageHandler.js index 179bb2d1d0..c56e5378df 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -19,7 +19,6 @@ limitations under the License. import request from 'browser-request'; import counterpart from 'counterpart'; -import Promise from 'bluebird'; import React from 'react'; import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index 820550af88..47bab38079 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -16,8 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; - // This module contains all the code needed to log the console, persist it to // disk and submit bug reports. Rationale is as follows: // - Monkey-patching the console is preferable to having a log library because diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index e772912e48..457958eb82 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -17,7 +17,6 @@ limitations under the License. */ import pako from 'pako'; -import Promise from 'bluebird'; import MatrixClientPeg from '../MatrixClientPeg'; import PlatformPeg from '../PlatformPeg'; diff --git a/src/settings/handlers/DeviceSettingsHandler.js b/src/settings/handlers/DeviceSettingsHandler.js index 780815efd1..76c518b97b 100644 --- a/src/settings/handlers/DeviceSettingsHandler.js +++ b/src/settings/handlers/DeviceSettingsHandler.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import SettingsHandler from "./SettingsHandler"; import MatrixClientPeg from "../../MatrixClientPeg"; import {SettingLevel} from "../SettingsStore"; diff --git a/src/settings/handlers/LocalEchoWrapper.js b/src/settings/handlers/LocalEchoWrapper.js index e6964f9bf7..4cbe4891be 100644 --- a/src/settings/handlers/LocalEchoWrapper.js +++ b/src/settings/handlers/LocalEchoWrapper.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from "bluebird"; import SettingsHandler from "./SettingsHandler"; /** diff --git a/src/settings/handlers/RoomDeviceSettingsHandler.js b/src/settings/handlers/RoomDeviceSettingsHandler.js index a0981ffbab..a9cf686c4c 100644 --- a/src/settings/handlers/RoomDeviceSettingsHandler.js +++ b/src/settings/handlers/RoomDeviceSettingsHandler.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from 'bluebird'; import SettingsHandler from "./SettingsHandler"; import {SettingLevel} from "../SettingsStore"; diff --git a/src/settings/handlers/SettingsHandler.js b/src/settings/handlers/SettingsHandler.js index d1566d6bfa..7d987fc136 100644 --- a/src/settings/handlers/SettingsHandler.js +++ b/src/settings/handlers/SettingsHandler.js @@ -15,8 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Promise from "bluebird"; - /** * Represents the base class for all level handlers. This class performs no logic * and should be overridden. diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index c8b4d75010..94b81c1ba5 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -15,7 +15,6 @@ limitations under the License. */ import EventEmitter from 'events'; -import Promise from 'bluebird'; const BULK_REQUEST_DEBOUNCE_MS = 200; diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 6a405124f4..a3caf876ef 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -234,7 +234,7 @@ class RoomViewStore extends Store { }); MatrixClientPeg.get().joinRoom( this._state.roomAlias || this._state.roomId, payload.opts, - ).done(() => { + ).then(() => { // We don't actually need to do anything here: we do *not* // clear the 'joining' flag because the Room object and/or // our 'joined' member event may not have come down the sync diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.js index ea0e4c3fb0..f193bd7709 100644 --- a/src/utils/DecryptFile.js +++ b/src/utils/DecryptFile.js @@ -21,7 +21,6 @@ import encrypt from 'browser-encrypt-attachment'; import 'isomorphic-fetch'; // Grab the client so that we can turn mxc:// URLs into https:// URLS. import MatrixClientPeg from '../MatrixClientPeg'; -import Promise from 'bluebird'; // WARNING: We have to be very careful about what mime-types we allow into blobs, // as for performance reasons these are now rendered via URL.createObjectURL() diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index de5c2e7610..8b952a2b5b 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -15,11 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; import MatrixClientPeg from '../MatrixClientPeg'; import {getAddressType} from '../UserAddress'; import GroupStore from '../stores/GroupStore'; -import Promise from 'bluebird'; import {_t} from "../languageHandler"; import sdk from "../index"; import Modal from "../Modal"; diff --git a/src/utils/promise.js b/src/utils/promise.js index e6e6ccb5c8..d7e8d2eae1 100644 --- a/src/utils/promise.js +++ b/src/utils/promise.js @@ -14,9 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// This is only here to allow access to methods like done for the time being -import Promise from "bluebird"; - // @flow // Returns a promise which resolves with a given value after the given number of ms diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index 7612b43b48..5f90e0f21c 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -15,7 +15,6 @@ limitations under the License. */ import expect from 'expect'; -import Promise from 'bluebird'; import React from 'react'; import ReactDOM from 'react-dom'; import ReactTestUtils from 'react-dom/test-utils'; diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index 95f7e7999a..a31cbdebb5 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -91,7 +91,7 @@ describe('MemberEventListSummary', function() { testUtils.beforeEach(this); sandbox = testUtils.stubClient(); - languageHandler.setLanguage('en').done(done); + languageHandler.setLanguage('en').then(done); languageHandler.setMissingEntryGenerator(function(key) { return key.split('|', 2)[1]; }); diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index 04a5c83ed0..60380eecd2 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -3,7 +3,6 @@ import ReactTestUtils from 'react-dom/test-utils'; import ReactDOM from 'react-dom'; import expect from 'expect'; import sinon from 'sinon'; -import Promise from 'bluebird'; import * as testUtils from '../../../test-utils'; import sdk from 'matrix-react-sdk'; const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput'); diff --git a/test/components/views/rooms/RoomSettings-test.js b/test/components/views/rooms/RoomSettings-test.js index dd91e812bc..1c0bfd95dc 100644 --- a/test/components/views/rooms/RoomSettings-test.js +++ b/test/components/views/rooms/RoomSettings-test.js @@ -3,7 +3,6 @@ // import ReactDOM from 'react-dom'; // import expect from 'expect'; // import jest from 'jest-mock'; -// import Promise from 'bluebird'; // import * as testUtils from '../../../test-utils'; // import sdk from 'matrix-react-sdk'; // const WrappedRoomSettings = testUtils.wrapInMatrixClientContext(sdk.getComponent('views.rooms.RoomSettings')); diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index 0d96bc15ab..8f21638703 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -11,7 +11,7 @@ describe('languageHandler', function() { testUtils.beforeEach(this); sandbox = testUtils.stubClient(); - languageHandler.setLanguage('en').done(done); + languageHandler.setLanguage('en').then(done); }); afterEach(function() { diff --git a/test/stores/RoomViewStore-test.js b/test/stores/RoomViewStore-test.js index be598de8da..77dfb37b0a 100644 --- a/test/stores/RoomViewStore-test.js +++ b/test/stores/RoomViewStore-test.js @@ -1,13 +1,11 @@ import expect from 'expect'; -import dis from '../../src/dispatcher'; import RoomViewStore from '../../src/stores/RoomViewStore'; import peg from '../../src/MatrixClientPeg'; import * as testUtils from '../test-utils'; -import Promise from 'bluebird'; const dispatch = testUtils.getDispatchForStore(RoomViewStore); diff --git a/test/test-utils.js b/test/test-utils.js index ff800132b9..64704fc610 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -1,7 +1,6 @@ "use strict"; import sinon from 'sinon'; -import Promise from 'bluebird'; import React from 'react'; import PropTypes from 'prop-types'; import peg from '../src/MatrixClientPeg'; diff --git a/yarn.lock b/yarn.lock index 3e43c29ef6..95d9adb573 100644 --- a/yarn.lock +++ b/yarn.lock @@ -899,7 +899,7 @@ babel-helper-explode-assignable-expression@^6.24.1: babel-traverse "^6.24.1" babel-types "^6.24.1" -babel-helper-function-name@^6.24.1, babel-helper-function-name@^6.8.0: +babel-helper-function-name@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" integrity sha1-00dbjAPtmCQqJbSDUasYOZ01gKk= @@ -1042,16 +1042,6 @@ babel-plugin-syntax-trailing-function-commas@^6.22.0: resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" integrity sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM= -babel-plugin-transform-async-to-bluebird@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-bluebird/-/babel-plugin-transform-async-to-bluebird-1.1.1.tgz#46ea3e7c5af629782ac9f1ed1b7cd38f8425afd4" - integrity sha1-Ruo+fFr2KXgqyfHtG3zTj4Qlr9Q= - dependencies: - babel-helper-function-name "^6.8.0" - babel-plugin-syntax-async-functions "^6.8.0" - babel-template "^6.9.0" - babel-traverse "^6.10.4" - babel-plugin-transform-async-to-generator@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" @@ -1442,7 +1432,7 @@ babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtim core-js "^2.4.0" regenerator-runtime "^0.11.0" -babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0, babel-template@^6.9.0: +babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= @@ -1453,7 +1443,7 @@ babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0, babel-tem babylon "^6.18.0" lodash "^4.17.4" -babel-traverse@^6.10.4, babel-traverse@^6.24.1, babel-traverse@^6.26.0: +babel-traverse@^6.24.1, babel-traverse@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= From e144f1c368c6bd56935caf56df985fdf22ceceea Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Nov 2019 10:37:29 +0000 Subject: [PATCH 0658/2372] remove Promise.config --- src/components/structures/MatrixChat.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 1fb1065e82..a2f2601e75 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -61,10 +61,6 @@ import { setTheme } from "../../theme"; import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { defer } from "../../utils/promise"; -// Disable warnings for now: we use deprecated bluebird functions -// and need to migrate, but they spam the console with warnings. -Promise.config({warnings: false}); - /** constants for MatrixChat.state.view */ const VIEWS = { // a special initial state which is only used at startup, while we are From 579cbef7b0ed38f298fb35ad82b3d73096f080f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 14:29:03 +0100 Subject: [PATCH 0659/2372] EventIndexPeg: Rewrite the module documentation. --- src/EventIndexPeg.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index f1841b3f2b..a289c9e629 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -15,11 +15,8 @@ limitations under the License. */ /* - * Holds the current Platform object used by the code to do anything - * specific to the platform we're running on (eg. web, electron) - * Platforms are provided by the app layer. - * This allows the app layer to set a Platform without necessarily - * having to have a MatrixChat object + * Object holding the global EventIndex object. Can only be initialized if the + * platform supports event indexing. */ import PlatformPeg from "./PlatformPeg"; From 45e7aab41e3767026aa1e207640850f935f2aacb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 14:30:07 +0100 Subject: [PATCH 0660/2372] EventIndexing: Rename our EventIndexer class. --- src/EventIndexing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EventIndexing.js b/src/EventIndexing.js index 05d5fd03da..38d610bac7 100644 --- a/src/EventIndexing.js +++ b/src/EventIndexing.js @@ -20,7 +20,7 @@ import MatrixClientPeg from "./MatrixClientPeg"; /** * Event indexing class that wraps the platform specific event indexing. */ -export default class EventIndexer { +export default class EventIndex { constructor() { this.crawlerCheckpoints = []; // The time that the crawler will wait between /rooms/{room_id}/messages From b983eaa3f9321a245c0f2f63d16a153dc13c9b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 14:36:08 +0100 Subject: [PATCH 0661/2372] EventIndex: Rename the file to be consistent with the class. --- src/{EventIndexing.js => EventIndex.js} | 0 src/EventIndexPeg.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{EventIndexing.js => EventIndex.js} (100%) diff --git a/src/EventIndexing.js b/src/EventIndex.js similarity index 100% rename from src/EventIndexing.js rename to src/EventIndex.js diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index a289c9e629..7530dd1a99 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -20,7 +20,7 @@ limitations under the License. */ import PlatformPeg from "./PlatformPeg"; -import EventIndex from "./EventIndexing"; +import EventIndex from "./EventIndex"; class EventIndexPeg { constructor() { From c48ccf9761d1f481da21b661bf88cbebef04da0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 14:40:04 +0100 Subject: [PATCH 0662/2372] EventIndex: Remove some unnecessary checks if event indexing is supported. --- src/EventIndex.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/EventIndex.js b/src/EventIndex.js index 38d610bac7..e7aee6189e 100644 --- a/src/EventIndex.js +++ b/src/EventIndex.js @@ -40,7 +40,6 @@ export default class EventIndex { async onSync(state, prevState, data) { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager === null) return; if (prevState === null && state === "PREPARED") { // Load our stored checkpoints, if any. @@ -115,7 +114,6 @@ export default class EventIndex { async onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager === null) return; // We only index encrypted rooms locally. if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; @@ -141,7 +139,6 @@ export default class EventIndex { async onEventDecrypted(ev, err) { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager === null) return; const eventId = ev.getId(); @@ -153,7 +150,6 @@ export default class EventIndex { async addLiveEventToIndex(ev) { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager === null) return; if (["m.room.message", "m.room.name", "m.room.topic"] .indexOf(ev.getType()) == -1) { @@ -349,7 +345,6 @@ export default class EventIndex { async onLimitedTimeline(room) { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager === null) return; if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; const timeline = room.getLiveTimeline(); From 8d7e7d0cc404c8d2df9d9602a5f526e3bb01924b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 14:40:38 +0100 Subject: [PATCH 0663/2372] EventIndex: Remove the unused deleteEventIndex method. We need to support the deletion of the event index even if it's not currently initialized, therefore the deletion ended up in the EventIndexPeg class. --- src/EventIndex.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/EventIndex.js b/src/EventIndex.js index e7aee6189e..6d8f265661 100644 --- a/src/EventIndex.js +++ b/src/EventIndex.js @@ -386,14 +386,6 @@ export default class EventIndex { return indexManager.closeEventIndex(); } - async deleteEventIndex() { - const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (indexManager !== null) { - this.stopCrawler(); - await indexManager.deleteEventIndex(); - } - } - async search(searchArgs) { const indexManager = PlatformPeg.get().getEventIndexingManager(); return indexManager.searchEventIndex(searchArgs); From 4a6623bc00f9047256daaec382e3386b8f83741c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 15:04:22 +0100 Subject: [PATCH 0664/2372] EventIndex: Rework the crawler cancellation. --- src/EventIndex.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/EventIndex.js b/src/EventIndex.js index 6d8f265661..75e3cda4f2 100644 --- a/src/EventIndex.js +++ b/src/EventIndex.js @@ -29,7 +29,7 @@ export default class EventIndex { // The maximum number of events our crawler should fetch in a single // crawl. this._eventsPerCrawl = 100; - this._crawlerRef = null; + this._crawler = null; this.liveEventsForIndex = new Set(); } @@ -165,7 +165,7 @@ export default class EventIndex { indexManager.addEventToIndex(e, profile); } - async crawlerFunc(handle) { + async crawlerFunc() { // TODO either put this in a better place or find a library provided // method that does this. const sleep = async (ms) => { @@ -179,7 +179,9 @@ export default class EventIndex { const client = MatrixClientPeg.get(); const indexManager = PlatformPeg.get().getEventIndexingManager(); - handle.cancel = () => { + this._crawler = {}; + + this._crawler.cancel = () => { cancelled = true; }; @@ -340,6 +342,8 @@ export default class EventIndex { } } + this._crawler = null; + console.log("EventIndex: Stopping crawler function"); } @@ -366,18 +370,13 @@ export default class EventIndex { } startCrawler() { - if (this._crawlerRef !== null) return; - - const crawlerHandle = {}; - this.crawlerFunc(crawlerHandle); - this._crawlerRef = crawlerHandle; + if (this._crawler !== null) return; + this.crawlerFunc(); } stopCrawler() { - if (this._crawlerRef === null) return; - - this._crawlerRef.cancel(); - this._crawlerRef = null; + if (this._crawler === null) return; + this._crawler.cancel(); } async close() { From 21f00aaeb1c6c47f314c9aed1543b3ba41208811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 18 Nov 2019 15:04:44 +0100 Subject: [PATCH 0665/2372] EventIndex: Fix some spelling errors. --- src/EventIndex.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EventIndex.js b/src/EventIndex.js index 75e3cda4f2..c96fe25fc8 100644 --- a/src/EventIndex.js +++ b/src/EventIndex.js @@ -282,8 +282,8 @@ export default class EventIndex { // attributes? }; - // TODO if there ar no events at this point we're missing a lot - // decryption keys, do we wan't to retry this checkpoint at a later + // TODO if there are no events at this point we're missing a lot + // decryption keys, do we want to retry this checkpoint at a later // stage? const filteredEvents = matrixEvents.filter(isValidEvent); @@ -336,7 +336,7 @@ export default class EventIndex { } } catch (e) { console.log("EventIndex: Error durring a crawl", e); - // An error occured, put the checkpoint back so we + // An error occurred, put the checkpoint back so we // can retry. this.crawlerCheckpoints.push(checkpoint); } From 50cccd3212b384d3b26460544f2e8e54651f259f Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 15 Nov 2019 10:57:20 +0000 Subject: [PATCH 0666/2372] Add cross-signing feature flag Fixes https://github.com/vector-im/riot-web/issues/11407 --- src/MatrixClientPeg.js | 10 ++++++++++ src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index bebb254afc..ef0130ec15 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -220,6 +220,16 @@ class MatrixClientPeg { identityServer: new IdentityAuthClient(), }; + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { + // TODO: Cross-signing keys are temporarily in memory only. A + // separate task in the cross-signing project will build from here. + const keys = []; + opts.cryptoCallbacks = { + getCrossSigningKey: k => keys[k], + saveCrossSigningKeys: newKeys => Object.assign(keys, newKeys), + }; + } + this.matrixClient = createMatrixClient(opts); // we're going to add eventlisteners for each matrix event tile, so the diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dc9773ad21..8469f62d5c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -342,6 +342,7 @@ "Use the new, consistent UserInfo panel for Room Members and Group Members": "Use the new, consistent UserInfo panel for Room Members and Group Members", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Send verification requests in direct message": "Send verification requests in direct message", + "Enable cross-signing to verify per-user instead of per-device": "Enable cross-signing to verify per-user instead of per-device", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 973d389ba6..c63217775a 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -146,6 +146,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_cross_signing": { + isFeature: true, + displayName: _td("Enable cross-signing to verify per-user instead of per-device"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "useCiderComposer": { displayName: _td("Use the new, faster, composer for writing messages"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, From b8c0a0fe7238675dc818d152e0e63720a0a04bf6 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 15 Nov 2019 11:31:19 +0000 Subject: [PATCH 0667/2372] Reload automatically when changing cross-signing flag --- src/settings/Settings.js | 5 +++-- ...LowBandwidthController.js => ReloadOnChangeController.js} | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) rename src/settings/controllers/{LowBandwidthController.js => ReloadOnChangeController.js} (91%) diff --git a/src/settings/Settings.js b/src/settings/Settings.js index c63217775a..89bca043bd 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -23,7 +23,7 @@ import { } from "./controllers/NotificationControllers"; import CustomStatusController from "./controllers/CustomStatusController"; import ThemeController from './controllers/ThemeController'; -import LowBandwidthController from "./controllers/LowBandwidthController"; +import ReloadOnChangeController from "./controllers/ReloadOnChangeController"; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config']; @@ -151,6 +151,7 @@ export const SETTINGS = { displayName: _td("Enable cross-signing to verify per-user instead of per-device"), supportedLevels: LEVELS_FEATURE, default: false, + controller: new ReloadOnChangeController(), }, "useCiderComposer": { displayName: _td("Use the new, faster, composer for writing messages"), @@ -433,7 +434,7 @@ export const SETTINGS = { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, displayName: _td('Low bandwidth mode'), default: false, - controller: new LowBandwidthController(), + controller: new ReloadOnChangeController(), }, "fallbackICEServerAllowed": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, diff --git a/src/settings/controllers/LowBandwidthController.js b/src/settings/controllers/ReloadOnChangeController.js similarity index 91% rename from src/settings/controllers/LowBandwidthController.js rename to src/settings/controllers/ReloadOnChangeController.js index c7796a425a..eadaee89ca 100644 --- a/src/settings/controllers/LowBandwidthController.js +++ b/src/settings/controllers/ReloadOnChangeController.js @@ -17,7 +17,7 @@ limitations under the License. import SettingController from "./SettingController"; import PlatformPeg from "../../PlatformPeg"; -export default class LowBandwidthController extends SettingController { +export default class ReloadOnChangeController extends SettingController { onChange(level, roomId, newValue) { PlatformPeg.get().reload(); } From 52e7d3505009732015f14be8743e58b4f650797f Mon Sep 17 00:00:00 2001 From: random Date: Mon, 18 Nov 2019 10:19:11 +0000 Subject: [PATCH 0668/2372] Translated using Weblate (Italian) Currently translated at 99.9% (1906 of 1908 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 8c7edbadd8..10227d447a 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2270,5 +2270,17 @@ "You cancelled": "Hai annullato", "%(name)s cancelled": "%(name)s ha annullato", "%(name)s wants to verify": "%(name)s vuole verificare", - "You sent a verification request": "Hai inviato una richiesta di verifica" + "You sent a verification request": "Hai inviato una richiesta di verifica", + "Custom (%(level)s)": "Personalizzato (%(level)s)", + "Trusted": "Fidato", + "Not trusted": "Non fidato", + "Hide verified Sign-In's": "Nascondi accessi verificati", + "%(count)s verified Sign-In's|other": "%(count)s accessi verificati", + "%(count)s verified Sign-In's|one": "1 accesso verificato", + "Direct message": "Messaggio diretto", + "Unverify user": "Revoca verifica utente", + "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", + "Messages in this room are end-to-end encrypted.": "I messaggi in questa stanza sono cifrati end-to-end.", + "Security": "Sicurezza", + "Verify": "Verifica" } From fd5e2398852b8b201c62fb7b0ac0d1b8f93b65e2 Mon Sep 17 00:00:00 2001 From: Walter Date: Mon, 18 Nov 2019 13:35:58 +0000 Subject: [PATCH 0669/2372] Translated using Weblate (Russian) Currently translated at 97.2% (1855 of 1908 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 01065a9e96..7806ea731b 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -2018,7 +2018,7 @@ "Create a private room": "Создать приватную комнату", "Topic (optional)": "Тема (опционально)", "Make this room public": "Сделать комнату публичной", - "Use the new, faster, composer for writing messages": "Используйте новый, более быстрый, редактор для написания сообщений.", + "Use the new, faster, composer for writing messages": "Используйте новый, более быстрый, редактор для написания сообщений", "Send read receipts for messages (requires compatible homeserver to disable)": "Отправлять подтверждения о прочтении сообщений (требуется отключение совместимого домашнего сервера)", "Show previews/thumbnails for images": "Показать превью / миниатюры для изображений", "Disconnect from the identity server and connect to instead?": "Отключиться от сервера идентификации и вместо этого подключиться к ?", @@ -2050,7 +2050,7 @@ "contact the administrators of identity server ": "связаться с администраторами сервера идентификации ", "wait and try again later": "Подождите и повторите попытку позже", "Error changing power level requirement": "Ошибка изменения требования к уровню прав", - "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Произошла ошибка при изменении требований к уровню прав комнаты. Убедитесь, что у вас достаточно прав и попробуйте снова.", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Произошла ошибка при изменении требований к уровню доступа комнаты. Убедитесь, что у вас достаточно прав и попробуйте снова.", "Error changing power level": "Ошибка изменения уровня прав", "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Произошла ошибка при изменении уровня прав пользователя. Убедитесь, что у вас достаточно прав и попробуйте снова.", "Unable to revoke sharing for email address": "Не удается отменить общий доступ к адресу электронной почты", @@ -2165,5 +2165,14 @@ "%(count)s unread messages including mentions.|one": "1 непрочитанное упоминание.", "%(count)s unread messages.|one": "1 непрочитанное сообщение.", "Unread messages.": "Непрочитанные сообщения.", - "Message Actions": "Сообщение действий" + "Message Actions": "Сообщение действий", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Это действие требует по умолчанию доступа к серверу идентификации для подтверждения адреса электронной почты или номера телефона, но у сервера нет никакого пользовательского соглашения.", + "Custom (%(level)s)": "Пользовательский (%(level)s)", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Try out new ways to ignore people (experimental)": "Попробуйте новые способы игнорировать людей (экспериментальные)", + "Send verification requests in direct message": "Отправить запросы на подтверждение в прямом сообщении", + "My Ban List": "Мой список запрещенных", + "Ignored/Blocked": "Игнорируемые/Заблокированные", + "Error adding ignored user/server": "Ошибка добавления игнорируемого пользователя/сервера", + "Error subscribing to list": "Ошибка при подписке на список" } From af2302265af29751f6dd6d6022d4618ca8770756 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 15 Nov 2019 13:36:59 +0000 Subject: [PATCH 0670/2372] Convert CreateKeyBackupDialog to class --- .../keybackup/CreateKeyBackupDialog.js | 130 +++++++++--------- 1 file changed, 66 insertions(+), 64 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 4953cdff68..c43fdb0626 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -1,5 +1,6 @@ /* Copyright 2018, 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; import { scorePassword } from '../../../../utils/PasswordScorer'; @@ -49,9 +49,11 @@ function selectText(target) { * Walks the user through the process of creating an e2e key backup * on the server. */ -export default createReactClass({ - getInitialState: function() { - return { +export default class CreateKeyBackupDialog extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { phase: PHASE_PASSPHRASE, passPhrase: '', passPhraseConfirm: '', @@ -60,25 +62,25 @@ export default createReactClass({ zxcvbnResult: null, setPassPhrase: false, }; - }, + } - componentWillMount: function() { + componentWillMount() { this._recoveryKeyNode = null; this._keyBackupInfo = null; this._setZxcvbnResultTimeout = null; - }, + } - componentWillUnmount: function() { + componentWillUnmount() { if (this._setZxcvbnResultTimeout !== null) { clearTimeout(this._setZxcvbnResultTimeout); } - }, + } - _collectRecoveryKeyNode: function(n) { + _collectRecoveryKeyNode = (n) => { this._recoveryKeyNode = n; - }, + } - _onCopyClick: function() { + _onCopyClick = () => { selectText(this._recoveryKeyNode); const successful = document.execCommand('copy'); if (successful) { @@ -87,9 +89,9 @@ export default createReactClass({ phase: PHASE_KEEPITSAFE, }); } - }, + } - _onDownloadClick: function() { + _onDownloadClick = () => { const blob = new Blob([this._keyBackupInfo.recovery_key], { type: 'text/plain;charset=us-ascii', }); @@ -99,9 +101,9 @@ export default createReactClass({ downloaded: true, phase: PHASE_KEEPITSAFE, }); - }, + } - _createBackup: async function() { + _createBackup = async () => { this.setState({ phase: PHASE_BACKINGUP, error: null, @@ -128,38 +130,38 @@ export default createReactClass({ error: e, }); } - }, + } - _onCancel: function() { + _onCancel = () => { this.props.onFinished(false); - }, + } - _onDone: function() { + _onDone = () => { this.props.onFinished(true); - }, + } - _onOptOutClick: function() { + _onOptOutClick = () => { this.setState({phase: PHASE_OPTOUT_CONFIRM}); - }, + } - _onSetUpClick: function() { + _onSetUpClick = () => { this.setState({phase: PHASE_PASSPHRASE}); - }, + } - _onSkipPassPhraseClick: async function() { + _onSkipPassPhraseClick = async () => { this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(); this.setState({ copied: false, downloaded: false, phase: PHASE_SHOWKEY, }); - }, + } - _onPassPhraseNextClick: function() { + _onPassPhraseNextClick = () => { this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); - }, + } - _onPassPhraseKeyPress: async function(e) { + _onPassPhraseKeyPress = async (e) => { if (e.key === 'Enter') { // If we're waiting for the timeout before updating the result at this point, // skip ahead and do it now, otherwise we'll deny the attempt to proceed @@ -177,9 +179,9 @@ export default createReactClass({ this._onPassPhraseNextClick(); } } - }, + } - _onPassPhraseConfirmNextClick: async function() { + _onPassPhraseConfirmNextClick = async () => { this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase); this.setState({ setPassPhrase: true, @@ -187,30 +189,30 @@ export default createReactClass({ downloaded: false, phase: PHASE_SHOWKEY, }); - }, + } - _onPassPhraseConfirmKeyPress: function(e) { + _onPassPhraseConfirmKeyPress = (e) => { if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) { this._onPassPhraseConfirmNextClick(); } - }, + } - _onSetAgainClick: function() { + _onSetAgainClick = () => { this.setState({ passPhrase: '', passPhraseConfirm: '', phase: PHASE_PASSPHRASE, zxcvbnResult: null, }); - }, + } - _onKeepItSafeBackClick: function() { + _onKeepItSafeBackClick = () => { this.setState({ phase: PHASE_SHOWKEY, }); - }, + } - _onPassPhraseChange: function(e) { + _onPassPhraseChange = (e) => { this.setState({ passPhrase: e.target.value, }); @@ -227,19 +229,19 @@ export default createReactClass({ zxcvbnResult: scorePassword(this.state.passPhrase), }); }, PASSPHRASE_FEEDBACK_DELAY); - }, + } - _onPassPhraseConfirmChange: function(e) { + _onPassPhraseConfirmChange = (e) => { this.setState({ passPhraseConfirm: e.target.value, }); - }, + } - _passPhraseIsValid: function() { + _passPhraseIsValid() { return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; - }, + } - _renderPhasePassPhrase: function() { + _renderPhasePassPhrase() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); let strengthMeter; @@ -305,9 +307,9 @@ export default createReactClass({

        ; - }, + } - _renderPhasePassPhraseConfirm: function() { + _renderPhasePassPhraseConfirm() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let matchText; @@ -361,9 +363,9 @@ export default createReactClass({ disabled={this.state.passPhrase !== this.state.passPhraseConfirm} />
        ; - }, + } - _renderPhaseShowKey: function() { + _renderPhaseShowKey() { let bodyText; if (this.state.setPassPhrase) { bodyText = _t( @@ -402,9 +404,9 @@ export default createReactClass({
        ; - }, + } - _renderPhaseKeepItSafe: function() { + _renderPhaseKeepItSafe() { let introText; if (this.state.copied) { introText = _t( @@ -431,16 +433,16 @@ export default createReactClass({
        ; - }, + } - _renderBusyPhase: function(text) { + _renderBusyPhase(text) { const Spinner = sdk.getComponent('views.elements.Spinner'); return
        ; - }, + } - _renderPhaseDone: function() { + _renderPhaseDone() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

        {_t( @@ -451,9 +453,9 @@ export default createReactClass({ hasCancel={false} />

        ; - }, + } - _renderPhaseOptOutConfirm: function() { + _renderPhaseOptOutConfirm() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
        {_t( @@ -467,9 +469,9 @@ export default createReactClass({
        ; - }, + } - _titleForPhase: function(phase) { + _titleForPhase(phase) { switch (phase) { case PHASE_PASSPHRASE: return _t('Secure your backup with a passphrase'); @@ -488,9 +490,9 @@ export default createReactClass({ default: return _t("Create Key Backup"); } - }, + } - render: function() { + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); let content; @@ -543,5 +545,5 @@ export default createReactClass({
        ); - }, -}); + } +} From cf26f14644172c167f80ce21ed1c9af0d1d34f03 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 18 Nov 2019 12:50:54 +0000 Subject: [PATCH 0671/2372] Switch to function properties to avoid manual binding in KeyBackupPanel --- src/components/views/settings/KeyBackupPanel.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index ec1e52a90c..3d00695e73 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -25,13 +25,6 @@ export default class KeyBackupPanel extends React.PureComponent { constructor(props) { super(props); - this._startNewBackup = this._startNewBackup.bind(this); - this._deleteBackup = this._deleteBackup.bind(this); - this._onKeyBackupSessionsRemaining = - this._onKeyBackupSessionsRemaining.bind(this); - this._onKeyBackupStatus = this._onKeyBackupStatus.bind(this); - this._restoreBackup = this._restoreBackup.bind(this); - this._unmounted = false; this.state = { loading: true, @@ -63,13 +56,13 @@ export default class KeyBackupPanel extends React.PureComponent { } } - _onKeyBackupSessionsRemaining(sessionsRemaining) { + _onKeyBackupSessionsRemaining = (sessionsRemaining) => { this.setState({ sessionsRemaining, }); } - _onKeyBackupStatus() { + _onKeyBackupStatus = () => { // This just loads the current backup status rather than forcing // a re-check otherwise we risk causing infinite loops this._loadBackupStatus(); @@ -120,7 +113,7 @@ export default class KeyBackupPanel extends React.PureComponent { } } - _startNewBackup() { + _startNewBackup = () => { Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'), { @@ -131,7 +124,7 @@ export default class KeyBackupPanel extends React.PureComponent { ); } - _deleteBackup() { + _deleteBackup = () => { const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, { title: _t('Delete Backup'), @@ -151,7 +144,7 @@ export default class KeyBackupPanel extends React.PureComponent { }); } - _restoreBackup() { + _restoreBackup = () => { const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { }); From f9d1fed74ac0f787ebae41d9d856e47129d6d6f5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Nov 2019 19:00:22 +0000 Subject: [PATCH 0672/2372] re-add missing case of codepath --- src/components/structures/TimelinePanel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 3dd5ea761e..e8e23c2f76 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1076,6 +1076,7 @@ const TimelinePanel = createReactClass({ if (timeline) { // This is a hot-path optimization by skipping a promise tick // by repeating a no-op sync branch in TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline + this._timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time onLoaded(); } else { const prom = this._timelineWindow.load(eventId, INITIAL_SIZE); From 6f8129419b5e3548209599fd1f6eb7baa537fa05 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Mon, 18 Nov 2019 19:47:10 +0000 Subject: [PATCH 0673/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1918 of 1918 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 3c049cc321..003af8240c 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2324,5 +2324,18 @@ "%(role)s in %(roomName)s": "%(role)s a szobában: %(roomName)s", "Messages in this room are end-to-end encrypted.": "Az üzenetek a szobában végponttól végpontig titkosítottak.", "Security": "Biztonság", - "Verify": "Ellenőriz" + "Verify": "Ellenőriz", + "Enable cross-signing to verify per-user instead of per-device": "Kereszt-aláírás engedélyezése eszköz alapú ellenőrzés helyett felhasználó alapú ellenőrzéshez", + "Any of the following data may be shared:": "Az alábbi adatok közül bármelyik megosztásra kerülhet:", + "Your display name": "Megjelenítési neved", + "Your avatar URL": "Profilképed URL-je", + "Your user ID": "Felhasználói azonosítód", + "Your theme": "Témád", + "Riot URL": "Riot URL", + "Room ID": "Szoba azonosító", + "Widget ID": "Kisalkalmazás azon.", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg %(widgetDomain)s domain-nel és az Integrációs Menedzserrel.", + "Using this widget may share data with %(widgetDomain)s.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg %(widgetDomain)s domain-nel.", + "Widget added by": "A kisalkalmazást hozzáadta", + "This widget may use cookies.": "Ez a kisalkalmazás sütiket használhat." } From f5ec9eb8f470eb0d55c98a9fcc36a3dd6d7e3e47 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 18 Nov 2019 13:16:36 -0700 Subject: [PATCH 0674/2372] Ensure widgets always have a sender associated with them Fixes https://github.com/vector-im/riot-web/issues/11419 --- src/components/views/elements/PersistentApp.js | 2 +- src/components/views/rooms/AppsDrawer.js | 2 +- src/utils/WidgetUtils.js | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js index d6931850be..391e7728f6 100644 --- a/src/components/views/elements/PersistentApp.js +++ b/src/components/views/elements/PersistentApp.js @@ -67,7 +67,7 @@ module.exports = createReactClass({ return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId(); }); const app = WidgetUtils.makeAppConfig( - appEvent.getStateKey(), appEvent.getContent(), appEvent.sender, persistentWidgetInRoomId, + appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), persistentWidgetInRoomId, ); const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId); const AppTile = sdk.getComponent('elements.AppTile'); diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 2a0a7569fb..8e6319e315 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -107,7 +107,7 @@ module.exports = createReactClass({ this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room), ); return widgets.map((ev) => { - return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.sender); + return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.getSender()); }); }, diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 36907da5ab..eb26ff1484 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -400,7 +400,7 @@ export default class WidgetUtils { return client.setAccountData('m.widgets', userWidgets); } - static makeAppConfig(appId, app, sender, roomId) { + static makeAppConfig(appId, app, senderUserId, roomId) { const myUserId = MatrixClientPeg.get().credentials.userId; const user = MatrixClientPeg.get().getUser(myUserId); const params = { @@ -413,6 +413,11 @@ export default class WidgetUtils { '$theme': SettingsStore.getValue("theme"), }; + if (!senderUserId) { + throw new Error("Widgets must be created by someone - provide a senderUserId"); + } + app.creatorUserId = senderUserId; + app.id = appId; app.name = app.name || app.type; @@ -425,7 +430,6 @@ export default class WidgetUtils { } app.url = encodeUri(app.url, params); - app.creatorUserId = (sender && sender.userId) ? sender.userId : null; return app; } From 8d25952dbbacdcf139b46977199491c449235768 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 18 Nov 2019 14:17:31 -0700 Subject: [PATCH 0675/2372] Add a bit more safety around breadcrumbs Fixes https://github.com/vector-im/riot-web/issues/11420 --- src/settings/handlers/AccountSettingsHandler.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index f738bf7971..9c39d98990 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -126,6 +126,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa if (!content || !content['recent_rooms']) { content = this._getSettings(BREADCRUMBS_LEGACY_EVENT_TYPE); } + if (!content) content = {}; // If we still don't have content, make some content['recent_rooms'] = newValue; return MatrixClientPeg.get().setAccountData(BREADCRUMBS_EVENT_TYPE, content); @@ -167,7 +168,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa // This seems fishy - try and get the event for the new rooms const newType = this._getSettings(BREADCRUMBS_EVENT_TYPE); if (newType) val = newType['recent_rooms']; - else val = event.getContent()['rooms']; + else val = event.getContent()['rooms'] || []; } else if (event.getType() === BREADCRUMBS_EVENT_TYPE) { val = event.getContent()['recent_rooms']; } else { From 2f89f284965951013e8716df89aae5b8b622349f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 18 Nov 2019 14:25:04 -0700 Subject: [PATCH 0676/2372] Remove extraneous paranoia The value is nullchecked later on. --- src/settings/handlers/AccountSettingsHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index 9c39d98990..7b05ad0c1b 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -168,7 +168,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa // This seems fishy - try and get the event for the new rooms const newType = this._getSettings(BREADCRUMBS_EVENT_TYPE); if (newType) val = newType['recent_rooms']; - else val = event.getContent()['rooms'] || []; + else val = event.getContent()['rooms']; } else if (event.getType() === BREADCRUMBS_EVENT_TYPE) { val = event.getContent()['recent_rooms']; } else { From b185eed46256edbfc1f287424f5f027dd4e38812 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 18 Nov 2019 17:56:33 -0700 Subject: [PATCH 0677/2372] Wire up the widget permission prompt to the cross-platform setting This doesn't have any backwards compatibility with anyone who has already clicked "Allow". We kinda want everyone to read the new prompt, so what better way to do it than effectively revoke all widget permissions? Part of https://github.com/vector-im/riot-web/issues/11262 --- src/components/views/elements/AppTile.js | 53 ++++++++++++------- .../views/elements/PersistentApp.js | 3 +- src/components/views/rooms/AppsDrawer.js | 3 +- src/utils/WidgetUtils.js | 3 +- 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index ffd9d73cca..db5978c792 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -34,7 +34,7 @@ import dis from '../../../dispatcher'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import classNames from 'classnames'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; -import SettingsStore from "../../../settings/SettingsStore"; +import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ENABLE_REACT_PERF = false; @@ -69,8 +69,11 @@ export default class AppTile extends React.Component { * @return {Object} Updated component state to be set with setState */ _getNewState(newProps) { - const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_'); - const hasPermissionToLoad = localStorage.getItem(widgetPermissionId); + // This is a function to make the impact of calling SettingsStore slightly less + const hasPermissionToLoad = () => { + const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId); + return !!currentlyAllowedWidgets[newProps.eventId]; + }; const PersistedElement = sdk.getComponent("elements.PersistedElement"); return { @@ -78,10 +81,9 @@ export default class AppTile extends React.Component { // True while the iframe content is loading loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey), widgetUrl: this._addWurlParams(newProps.url), - widgetPermissionId: widgetPermissionId, // Assume that widget has permission to load if we are the user who // added it to the room, or if explicitly granted by the user - hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId, + hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(), error: null, deleting: false, widgetPageTitle: newProps.widgetPageTitle, @@ -446,24 +448,38 @@ export default class AppTile extends React.Component { }); } - /* TODO -- Store permission in account data so that it is persisted across multiple devices */ _grantWidgetPermission() { - console.warn('Granting permission to load widget - ', this.state.widgetUrl); - localStorage.setItem(this.state.widgetPermissionId, true); - this.setState({hasPermissionToLoad: true}); - // Now that we have permission, fetch the IM token - this.setScalarToken(); + const roomId = this.props.room.roomId; + console.info("Granting permission for widget to load: " + this.props.eventId); + const current = SettingsStore.getValue("allowedWidgets", roomId); + current[this.props.eventId] = true; + SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => { + this.setState({hasPermissionToLoad: true}); + + // Fetch a token for the integration manager, now that we're allowed to + this.setScalarToken(); + }).catch(err => { + console.error(err); + // We don't really need to do anything about this - the user will just hit the button again. + }); } _revokeWidgetPermission() { - console.warn('Revoking permission to load widget - ', this.state.widgetUrl); - localStorage.removeItem(this.state.widgetPermissionId); - this.setState({hasPermissionToLoad: false}); + const roomId = this.props.room.roomId; + console.info("Revoking permission for widget to load: " + this.props.eventId); + const current = SettingsStore.getValue("allowedWidgets", roomId); + current[this.props.eventId] = false; + SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => { + this.setState({hasPermissionToLoad: false}); - // Force the widget to be non-persistent - ActiveWidgetStore.destroyPersistentWidget(this.props.id); - const PersistedElement = sdk.getComponent("elements.PersistedElement"); - PersistedElement.destroyElement(this._persistKey); + // Force the widget to be non-persistent (able to be deleted/forgotten) + ActiveWidgetStore.destroyPersistentWidget(this.props.id); + const PersistedElement = sdk.getComponent("elements.PersistedElement"); + PersistedElement.destroyElement(this._persistKey); + }).catch(err => { + console.error(err); + // We don't really need to do anything about this - the user will just hit the button again. + }); } formatAppTileName() { @@ -720,6 +736,7 @@ AppTile.displayName ='AppTile'; AppTile.propTypes = { id: PropTypes.string.isRequired, + eventId: PropTypes.string, // required for room widgets url: PropTypes.string.isRequired, name: PropTypes.string.isRequired, room: PropTypes.object.isRequired, diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js index 391e7728f6..47783a45c3 100644 --- a/src/components/views/elements/PersistentApp.js +++ b/src/components/views/elements/PersistentApp.js @@ -67,13 +67,14 @@ module.exports = createReactClass({ return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId(); }); const app = WidgetUtils.makeAppConfig( - appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), persistentWidgetInRoomId, + appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), persistentWidgetInRoomId, appEvent.getId(), ); const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId); const AppTile = sdk.getComponent('elements.AppTile'); return { - return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.getSender()); + return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.getSender(), ev.getRoomId(), ev.getId()); }); }, @@ -159,6 +159,7 @@ module.exports = createReactClass({ return ( Date: Mon, 18 Nov 2019 18:02:47 -0700 Subject: [PATCH 0678/2372] Appease the linter --- src/components/views/elements/PersistentApp.js | 3 ++- src/components/views/rooms/AppsDrawer.js | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js index 47783a45c3..19e4be6083 100644 --- a/src/components/views/elements/PersistentApp.js +++ b/src/components/views/elements/PersistentApp.js @@ -67,7 +67,8 @@ module.exports = createReactClass({ return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId(); }); const app = WidgetUtils.makeAppConfig( - appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), persistentWidgetInRoomId, appEvent.getId(), + appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), + persistentWidgetInRoomId, appEvent.getId(), ); const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId); const AppTile = sdk.getComponent('elements.AppTile'); diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 618536ef7c..e53570dc5b 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -107,7 +107,9 @@ module.exports = createReactClass({ this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room), ); return widgets.map((ev) => { - return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.getSender(), ev.getRoomId(), ev.getId()); + return WidgetUtils.makeAppConfig( + ev.getStateKey(), ev.getContent(), ev.getSender(), ev.getRoomId(), ev.getId(), + ); }); }, From d2a99183595df253cdf4d97eee9c494e890fc4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 19 Nov 2019 09:26:46 +0100 Subject: [PATCH 0679/2372] EventIndex: Remove some unused variables and some trailing whitespace. --- src/EventIndex.js | 4 ---- src/EventIndexPeg.js | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/EventIndex.js b/src/EventIndex.js index c96fe25fc8..7ed43ad31c 100644 --- a/src/EventIndex.js +++ b/src/EventIndex.js @@ -113,8 +113,6 @@ export default class EventIndex { } async onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { - const indexManager = PlatformPeg.get().getEventIndexingManager(); - // We only index encrypted rooms locally. if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; @@ -138,8 +136,6 @@ export default class EventIndex { } async onEventDecrypted(ev, err) { - const indexManager = PlatformPeg.get().getEventIndexingManager(); - const eventId = ev.getId(); // If the event isn't in our live event set, ignore it. diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 7530dd1a99..266b8f2d53 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -15,7 +15,7 @@ limitations under the License. */ /* - * Object holding the global EventIndex object. Can only be initialized if the + * Object holding the global EventIndex object. Can only be initialized if the * platform supports event indexing. */ From 92292003c8fcbe741fbe95e50bb54e5cee68fdb6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 19 Nov 2019 12:51:02 +0100 Subject: [PATCH 0680/2372] make shield on verification request scale correctly by not overriding `mask-size` using `mask` for `mask-image` --- res/css/views/messages/_MKeyVerificationRequest.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/messages/_MKeyVerificationRequest.scss b/res/css/views/messages/_MKeyVerificationRequest.scss index b4cde4e7ef..87a75dee82 100644 --- a/res/css/views/messages/_MKeyVerificationRequest.scss +++ b/res/css/views/messages/_MKeyVerificationRequest.scss @@ -25,7 +25,7 @@ limitations under the License. width: 12px; height: 16px; content: ""; - mask: url("$(res)/img/e2e/normal.svg"); + mask-image: url("$(res)/img/e2e/normal.svg"); mask-repeat: no-repeat; mask-size: 100%; margin-top: 4px; @@ -33,7 +33,7 @@ limitations under the License. } &.mx_KeyVerification_icon_verified::after { - mask: url("$(res)/img/e2e/verified.svg"); + mask-image: url("$(res)/img/e2e/verified.svg"); background-color: $accent-color; } From de15965c4a59495cc47e364833710410d0adfd19 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 19 Nov 2019 12:51:37 +0100 Subject: [PATCH 0681/2372] improve device list layout --- res/css/views/right_panel/_UserInfo.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index c68f3ffd37..df7d0a5f87 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -195,6 +195,8 @@ limitations under the License. .mx_UserInfo_devices { .mx_UserInfo_device { display: flex; + margin: 8px 0; + &.mx_UserInfo_device_verified { .mx_UserInfo_device_trusted { @@ -210,6 +212,7 @@ limitations under the License. .mx_UserInfo_device_name { flex: 1; margin-right: 5px; + word-break: break-word; } } From 39939de04ffbc29c66e68789f0d70372afcbc72e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 19 Nov 2019 12:51:46 +0100 Subject: [PATCH 0682/2372] =?UTF-8?q?remove=20white=20background=20on=20!?= =?UTF-8?q?=20and=20=E2=9C=85=20so=20it=20looks=20better=20on=20dark=20the?= =?UTF-8?q?me?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- res/css/views/rooms/_E2EIcon.scss | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index bc11ac6e1c..1ee5008888 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -22,21 +22,6 @@ limitations under the License. display: block; } -.mx_E2EIcon_verified::before, .mx_E2EIcon_warning::before { - content: ""; - display: block; - /* the symbols in the shield icons are cut out to make it themeable with css masking. - if they appear on a different background than white, the symbol wouldn't be white though, so we - add a rectangle here below the masked element to shine through the symbol cut-out. - hardcoding white and not using a theme variable as this would probably be white for any theme. */ - background-color: white; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; -} - .mx_E2EIcon_verified::after, .mx_E2EIcon_warning::after { content: ""; display: block; @@ -49,23 +34,11 @@ limitations under the License. mask-size: contain; } -.mx_E2EIcon_verified::before { - /* white rectangle below checkmark of shield */ - margin: 25% 28% 38% 25%; -} - - .mx_E2EIcon_verified::after { mask-image: url('$(res)/img/e2e/verified.svg'); background-color: $accent-color; } - -.mx_E2EIcon_warning::before { - /* white rectangle below "!" of shield */ - margin: 18% 40% 25% 40%; -} - .mx_E2EIcon_warning::after { mask-image: url('$(res)/img/e2e/warning.svg'); background-color: $warning-color; From 5f7b0fef334763df3a83aac563bf533574006101 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 19 Nov 2019 12:55:28 +0100 Subject: [PATCH 0683/2372] scale (new) icons to fit available size fixes https://github.com/vector-im/riot-web/issues/11399 --- res/css/views/rooms/_MemberDeviceInfo.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_MemberDeviceInfo.scss b/res/css/views/rooms/_MemberDeviceInfo.scss index 951d1945b1..e73e6c58f1 100644 --- a/res/css/views/rooms/_MemberDeviceInfo.scss +++ b/res/css/views/rooms/_MemberDeviceInfo.scss @@ -25,6 +25,7 @@ limitations under the License. width: 12px; height: 12px; mask-repeat: no-repeat; + mask-size: 100%; } .mx_MemberDeviceInfo_icon_blacklisted { mask-image: url('$(res)/img/e2e/blacklisted.svg'); From 6017473caf8e23e31666924a17fb5f6ce60af42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 19 Nov 2019 10:46:18 +0100 Subject: [PATCH 0684/2372] EventIndex: Move the event listener registration into the EventIndex class. --- src/EventIndex.js | 41 +++++++++++++++++++++++-- src/EventIndexPeg.js | 3 +- src/components/structures/MatrixChat.js | 26 ---------------- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/EventIndex.js b/src/EventIndex.js index 7ed43ad31c..b6784cd331 100644 --- a/src/EventIndex.js +++ b/src/EventIndex.js @@ -31,11 +31,45 @@ export default class EventIndex { this._eventsPerCrawl = 100; this._crawler = null; this.liveEventsForIndex = new Set(); + + this.boundOnSync = async (state, prevState, data) => { + await this.onSync(state, prevState, data); + }; + this.boundOnRoomTimeline = async ( ev, room, toStartOfTimeline, removed, + data) => { + await this.onRoomTimeline(ev, room, toStartOfTimeline, removed, data); + }; + this.boundOnEventDecrypted = async (ev, err) => { + await this.onEventDecrypted(ev, err); + }; + this.boundOnTimelineReset = async (room, timelineSet, + resetAllTimelines) => await this.onTimelineReset(room); } async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); - return indexManager.initEventIndex(); + await indexManager.initEventIndex(); + + this.registerListeners(); + } + + registerListeners() { + const client = MatrixClientPeg.get(); + + client.on('sync', this.boundOnSync); + client.on('Room.timeline', this.boundOnRoomTimeline); + client.on('Event.decrypted', this.boundOnEventDecrypted); + client.on('Room.timelineReset', this.boundOnTimelineReset); + } + + removeListeners() { + const client = MatrixClientPeg.get(); + if (client === null) return; + + client.removeListener('sync', this.boundOnSync); + client.removeListener('Room.timeline', this.boundOnRoomTimeline); + client.removeListener('Event.decrypted', this.boundOnEventDecrypted); + client.removeListener('Room.timelineReset', this.boundOnTimelineReset); } async onSync(state, prevState, data) { @@ -343,7 +377,9 @@ export default class EventIndex { console.log("EventIndex: Stopping crawler function"); } - async onLimitedTimeline(room) { + async onTimelineReset(room) { + if (room === null) return; + const indexManager = PlatformPeg.get().getEventIndexingManager(); if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; @@ -377,6 +413,7 @@ export default class EventIndex { async close() { const indexManager = PlatformPeg.get().getEventIndexingManager(); + this.removeListeners(); this.stopCrawler(); return indexManager.closeEventIndex(); } diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 266b8f2d53..74b7968c70 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -97,10 +97,9 @@ class EventIndexPeg { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (indexManager !== null) { - this.stop(); + this.unset(); console.log("EventIndex: Deleting event index."); await indexManager.deleteEventIndex(); - this.index = null; } } } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b45884e64f..da67416400 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -31,7 +31,6 @@ import Analytics from "../../Analytics"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; import MatrixClientPeg from "../../MatrixClientPeg"; import PlatformPeg from "../../PlatformPeg"; -import EventIndexPeg from "../../EventIndexPeg"; import SdkConfig from "../../SdkConfig"; import * as RoomListSorter from "../../RoomListSorter"; import dis from "../../dispatcher"; @@ -1288,31 +1287,6 @@ export default createReactClass({ return self._loggedInView.child.canResetTimelineInRoom(roomId); }); - cli.on('sync', async (state, prevState, data) => { - const eventIndex = EventIndexPeg.get(); - if (eventIndex === null) return; - await eventIndex.onSync(state, prevState, data); - }); - - cli.on("Room.timeline", async (ev, room, toStartOfTimeline, removed, data) => { - const eventIndex = EventIndexPeg.get(); - if (eventIndex === null) return; - await eventIndex.onRoomTimeline(ev, room, toStartOfTimeline, removed, data); - }); - - cli.on("Event.decrypted", async (ev, err) => { - const eventIndex = EventIndexPeg.get(); - if (eventIndex === null) return; - await eventIndex.onEventDecrypted(ev, err); - }); - - cli.on("Room.timelineReset", async (room, timelineSet, resetAllTimelines) => { - const eventIndex = EventIndexPeg.get(); - if (eventIndex === null) return; - if (room === null) return; - await eventIndex.onLimitedTimeline(room); - }); - cli.on('sync', function(state, prevState, data) { // LifecycleStore and others cannot directly subscribe to matrix client for // events because flux only allows store state changes during flux dispatches. From 979803797fb58bb421c1e1a98cf90f263ae3af91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 19 Nov 2019 11:05:37 +0100 Subject: [PATCH 0685/2372] Lifecycle: Make the clear storage method async. --- src/Lifecycle.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 1d38934ade..1b69ca6ade 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -607,20 +607,20 @@ async function startMatrixClient(startSyncing=true) { * Stops a running client and all related services, and clears persistent * storage. Used after a session has been logged out. */ -export function onLoggedOut() { +export async function onLoggedOut() { _isLoggingOut = false; // Ensure that we dispatch a view change **before** stopping the client so // so that React components unmount first. This avoids React soft crashes // that can occur when components try to use a null client. dis.dispatch({action: 'on_logged_out'}); stopMatrixClient(); - _clearStorage().done(); + await _clearStorage(); } /** * @returns {Promise} promise which resolves once the stores have been cleared */ -function _clearStorage() { +async function _clearStorage() { Analytics.logout(); if (window.localStorage) { @@ -633,12 +633,8 @@ function _clearStorage() { baseUrl: "", }); - const clear = async () => { - await EventIndexPeg.deleteEventIndex(); - await cli.clearStores(); - }; - - return clear(); + await EventIndexPeg.deleteEventIndex(); + await cli.clearStores(); } /** @@ -662,7 +658,7 @@ export function stopMatrixClient(unsetClient=true) { if (unsetClient) { MatrixClientPeg.unset(); - EventIndexPeg.unset().done(); + EventIndexPeg.unset(); } } } From f776bdcc8b4306557df2bda71bce6ec097694abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 19 Nov 2019 12:23:49 +0100 Subject: [PATCH 0686/2372] EventIndex: Hide the feature behind a labs flag. --- src/EventIndexPeg.js | 5 +++++ src/i18n/strings/en_EN.json | 3 ++- src/settings/Settings.js | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/EventIndexPeg.js b/src/EventIndexPeg.js index 74b7968c70..eb4caa2ca4 100644 --- a/src/EventIndexPeg.js +++ b/src/EventIndexPeg.js @@ -21,6 +21,7 @@ limitations under the License. import PlatformPeg from "./PlatformPeg"; import EventIndex from "./EventIndex"; +import SettingsStore from './settings/SettingsStore'; class EventIndexPeg { constructor() { @@ -34,6 +35,10 @@ class EventIndexPeg { * EventIndex was successfully initialized, false otherwise. */ async init() { + if (!SettingsStore.isFeatureEnabled("feature_event_indexing")) { + return false; + } + const indexManager = PlatformPeg.get().getEventIndexingManager(); if (!indexManager || await indexManager.supportsEventIndexing() !== true) { console.log("EventIndex: Platform doesn't support event indexing,", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f524a22d4b..69c3f07f3f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1829,5 +1829,6 @@ "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", - "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", + "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)" } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 3c33ae57fe..8abd845f0c 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -120,6 +120,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_event_indexing": { + isFeature: true, + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + displayName: _td("Enable local event indexing and E2EE search (requires restart)"), + default: false, + }, "useCiderComposer": { displayName: _td("Use the new, faster, composer for writing messages"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, From e9df973c8273f7a0958a69e257cd2d9204ce8404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 19 Nov 2019 12:52:12 +0100 Subject: [PATCH 0687/2372] EventIndex: Move the event indexing files into a separate folder. --- src/BasePlatform.js | 2 +- src/Lifecycle.js | 2 +- src/Searching.js | 2 +- src/{ => indexing}/BaseEventIndexManager.js | 0 src/{ => indexing}/EventIndex.js | 4 ++-- src/{ => indexing}/EventIndexPeg.js | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) rename src/{ => indexing}/BaseEventIndexManager.js (100%) rename src/{ => indexing}/EventIndex.js (99%) rename src/{ => indexing}/EventIndexPeg.js (95%) diff --git a/src/BasePlatform.js b/src/BasePlatform.js index f6301fd173..14e34a1f40 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -19,7 +19,7 @@ limitations under the License. */ import dis from './dispatcher'; -import BaseEventIndexManager from './BaseEventIndexManager'; +import BaseEventIndexManager from './indexing/BaseEventIndexManager'; /** * Base class for classes that provide platform-specific functionality diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 1b69ca6ade..65fa0b29ce 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -20,7 +20,7 @@ import Promise from 'bluebird'; import Matrix from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; -import EventIndexPeg from './EventIndexPeg'; +import EventIndexPeg from './indexing/EventIndexPeg'; import createMatrixClient from './utils/createMatrixClient'; import Analytics from './Analytics'; import Notifier from './Notifier'; diff --git a/src/Searching.js b/src/Searching.js index ca3e7f041f..f8976c92e4 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import EventIndexPeg from "./EventIndexPeg"; +import EventIndexPeg from "./indexing/EventIndexPeg"; import MatrixClientPeg from "./MatrixClientPeg"; function serverSideSearch(term, roomId = undefined) { diff --git a/src/BaseEventIndexManager.js b/src/indexing/BaseEventIndexManager.js similarity index 100% rename from src/BaseEventIndexManager.js rename to src/indexing/BaseEventIndexManager.js diff --git a/src/EventIndex.js b/src/indexing/EventIndex.js similarity index 99% rename from src/EventIndex.js rename to src/indexing/EventIndex.js index b6784cd331..df81667c6e 100644 --- a/src/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import PlatformPeg from "./PlatformPeg"; -import MatrixClientPeg from "./MatrixClientPeg"; +import PlatformPeg from "../PlatformPeg"; +import MatrixClientPeg from "../MatrixClientPeg"; /** * Event indexing class that wraps the platform specific event indexing. diff --git a/src/EventIndexPeg.js b/src/indexing/EventIndexPeg.js similarity index 95% rename from src/EventIndexPeg.js rename to src/indexing/EventIndexPeg.js index eb4caa2ca4..c0bdd74ff4 100644 --- a/src/EventIndexPeg.js +++ b/src/indexing/EventIndexPeg.js @@ -19,9 +19,9 @@ limitations under the License. * platform supports event indexing. */ -import PlatformPeg from "./PlatformPeg"; -import EventIndex from "./EventIndex"; -import SettingsStore from './settings/SettingsStore'; +import PlatformPeg from "../PlatformPeg"; +import EventIndex from "../indexing/EventIndex"; +import SettingsStore from '../settings/SettingsStore'; class EventIndexPeg { constructor() { From 43884923e839fc900ab51fd8aef5a7ed903c6372 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 19 Nov 2019 14:07:14 +0100 Subject: [PATCH 0688/2372] merge the feature_user_info_panel flag into feature_dm_verification --- src/components/structures/RightPanel.js | 4 ++-- src/i18n/strings/en_EN.json | 3 +-- src/settings/Settings.js | 9 ++------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 48d272f6c9..895f6ae57e 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -185,7 +185,7 @@ export default class RightPanel extends React.Component { } else if (this.state.phase === RightPanel.Phase.GroupRoomList) { panel = ; } else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) { - if (SettingsStore.isFeatureEnabled("feature_user_info_panel")) { + if (SettingsStore.isFeatureEnabled("feature_dm_verification")) { const onClose = () => { dis.dispatch({ action: "view_user", @@ -204,7 +204,7 @@ export default class RightPanel extends React.Component { } else if (this.state.phase === RightPanel.Phase.Room3pidMemberInfo) { panel = ; } else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) { - if (SettingsStore.isFeatureEnabled("feature_user_info_panel")) { + if (SettingsStore.isFeatureEnabled("feature_dm_verification")) { const onClose = () => { dis.dispatch({ action: "view_user", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9f13d133c4..473efdfb76 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -340,9 +340,8 @@ "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", - "Use the new, consistent UserInfo panel for Room Members and Group Members": "Use the new, consistent UserInfo panel for Room Members and Group Members", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", - "Send verification requests in direct message": "Send verification requests in direct message", + "Send verification requests in direct message, including a new verification UX in the member panel.": "Send verification requests in direct message, including a new verification UX in the member panel.", "Enable cross-signing to verify per-user instead of per-device": "Enable cross-signing to verify per-user instead of per-device", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 89bca043bd..718a0daec3 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -120,12 +120,6 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, - "feature_user_info_panel": { - isFeature: true, - displayName: _td("Use the new, consistent UserInfo panel for Room Members and Group Members"), - supportedLevels: LEVELS_FEATURE, - default: false, - }, "feature_mjolnir": { isFeature: true, displayName: _td("Try out new ways to ignore people (experimental)"), @@ -142,7 +136,8 @@ export const SETTINGS = { }, "feature_dm_verification": { isFeature: true, - displayName: _td("Send verification requests in direct message"), + displayName: _td("Send verification requests in direct message," + + " including a new verification UX in the member panel."), supportedLevels: LEVELS_FEATURE, default: false, }, From 27d1e4fbbedb6ccf1bccdf17bd1cee74dee4c224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 19 Nov 2019 14:17:51 +0100 Subject: [PATCH 0689/2372] Fix the translations en_EN file by regenerating it. --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 69c3f07f3f..6f116cbac2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -334,6 +334,7 @@ "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", + "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", @@ -1829,6 +1830,5 @@ "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", - "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", - "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" } From da2665f4a331d2c91ad07ff4e9ee59132cea3575 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Tue, 19 Nov 2019 06:47:53 +0000 Subject: [PATCH 0690/2372] Translated using Weblate (Albanian) Currently translated at 99.7% (1913 of 1918 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 2bf5732131..4d0ad6582b 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2292,5 +2292,17 @@ "%(role)s in %(roomName)s": "%(role)s në %(roomName)s", "Messages in this room are end-to-end encrypted.": "Mesazhet në këtë dhomë janë të fshehtëzuara skaj-më-skaj.", "Security": "Siguri", - "Verify": "Verifikoje" + "Verify": "Verifikoje", + "Any of the following data may be shared:": "Mund të ndahen me të tjerët cilado prej të dhënave vijuese:", + "Your display name": "Emri juaj në ekran", + "Your avatar URL": "URL-ja e avatarit tuaj", + "Your user ID": "ID-ja juaj e përdoruesit", + "Your theme": "Tema juaj", + "Riot URL": "URL Riot-i", + "Room ID": "ID dhome", + "Widget ID": "ID widget-i", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Përdorimi i këtij widget-i mund të sjellë ndarje të dhënash me %(widgetDomain)s & Përgjegjësin tuaj të Integrimeve.", + "Using this widget may share data with %(widgetDomain)s.": "Përdorimi i këtij widget-i mund të sjellë ndarje të dhënash me %(widgetDomain)s.", + "Widget added by": "Widget i shtuar nga", + "This widget may use cookies.": "Ky widget mund të përdorë cookies." } From 0c0437ebf501c4da485460343dff6a6d25ad0540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Tue, 19 Nov 2019 08:04:08 +0000 Subject: [PATCH 0691/2372] Translated using Weblate (French) Currently translated at 100.0% (1918 of 1918 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index eef9438761..824da9d3ff 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2337,5 +2337,18 @@ "%(role)s in %(roomName)s": "%(role)s dans %(roomName)s", "Messages in this room are end-to-end encrypted.": "Les messages dans ce salon sont chiffrés de bout en bout.", "Security": "Sécurité", - "Verify": "Vérifier" + "Verify": "Vérifier", + "Enable cross-signing to verify per-user instead of per-device": "Activer la signature croisée pour vérifier par utilisateur et non par appareil", + "Any of the following data may be shared:": "Les données suivants peuvent être partagées :", + "Your display name": "Votre nom d’affichage", + "Your avatar URL": "L’URL de votre avatar", + "Your user ID": "Votre identifiant utilisateur", + "Your theme": "Votre thème", + "Riot URL": "URL de Riot", + "Room ID": "Identifiant du salon", + "Widget ID": "Identifiant du widget", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "L’utilisation de ce widget pourrait partager des données avec %(widgetDomain)s et votre gestionnaire d’intégrations.", + "Using this widget may share data with %(widgetDomain)s.": "L’utilisation de ce widget pourrait partager des données avec %(widgetDomain)s.", + "Widget added by": "Widget ajouté par", + "This widget may use cookies.": "Ce widget pourrait utiliser des cookies." } From dbee3a1215c2c3022ecc3ed87c9efeabf507dbae Mon Sep 17 00:00:00 2001 From: random Date: Tue, 19 Nov 2019 09:21:14 +0000 Subject: [PATCH 0692/2372] Translated using Weblate (Italian) Currently translated at 99.9% (1916 of 1918 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 10227d447a..efab4595f6 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2282,5 +2282,18 @@ "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", "Messages in this room are end-to-end encrypted.": "I messaggi in questa stanza sono cifrati end-to-end.", "Security": "Sicurezza", - "Verify": "Verifica" + "Verify": "Verifica", + "Enable cross-signing to verify per-user instead of per-device": "Attiva la firma incrociata per verificare per-utente invece che per-dispositivo", + "Any of the following data may be shared:": "Possono essere condivisi tutti i seguenti dati:", + "Your display name": "Il tuo nome visualizzato", + "Your avatar URL": "L'URL del tuo avatar", + "Your user ID": "Il tuo ID utente", + "Your theme": "Il tuo tema", + "Riot URL": "URL di Riot", + "Room ID": "ID stanza", + "Widget ID": "ID widget", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Usando questo widget i dati possono essere condivisi con %(widgetDomain)s e il tuo Gestore di Integrazione.", + "Using this widget may share data with %(widgetDomain)s.": "Usando questo widget i dati possono essere condivisi con %(widgetDomain)s.", + "Widget added by": "Widget aggiunto da", + "This widget may use cookies.": "Questo widget può usare cookie." } From afeab31ce6a3cd784606e4caa4624f30832dd3a8 Mon Sep 17 00:00:00 2001 From: fenuks Date: Tue, 19 Nov 2019 00:34:13 +0000 Subject: [PATCH 0693/2372] Translated using Weblate (Polish) Currently translated at 73.8% (1415 of 1918 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pl/ --- src/i18n/strings/pl.json | 46 +++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 31f82bc2dd..4054c48f97 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -320,7 +320,7 @@ "Mobile phone number (optional)": "Numer telefonu komórkowego (opcjonalne)", "Moderator": "Moderator", "%(serverName)s Matrix ID": "%(serverName)s Matrix ID", - "Name": "Imię", + "Name": "Nazwa", "Never send encrypted messages to unverified devices from this device": "Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych urządzeń z tego urządzenia", "Never send encrypted messages to unverified devices in this room from this device": "Nigdy nie wysyłaj niezaszyfrowanych wiadomości do niezweryfikowanych urządzeń z tego urządzenia", "New address (e.g. #foo:%(localDomain)s)": "Nowy adres (np. #foo:%(localDomain)s)", @@ -972,7 +972,7 @@ "Disinvite this user?": "Anulować zaproszenie tego użytkownika?", "Unignore": "Przestań ignorować", "Jump to read receipt": "Przeskocz do potwierdzenia odczytu", - "Share Link to User": "Udostępnij link do użytkownika", + "Share Link to User": "Udostępnij odnośnik do użytkownika", "At this time it is not possible to reply with a file so this will be sent without being a reply.": "W tej chwili nie można odpowiedzieć plikiem, więc zostanie wysłany nie będąc odpowiedzią.", "Unable to reply": "Nie udało się odpowiedzieć", "At this time it is not possible to reply with an emote.": "W tej chwili nie można odpowiedzieć emotikoną.", @@ -1556,7 +1556,7 @@ "Order rooms in the room list by most important first instead of most recent": "Kolejkuj pokoje na liście pokojów od najważniejszych niż od najnowszych", "Show hidden events in timeline": "Pokaż ukryte wydarzenia na linii czasowej", "Low bandwidth mode": "Tryb wolnej przepustowości", - "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Powzól na awaryjny serwer wspomagania połączeń turn.matrix.org, gdy Twój serwer domowy takiego nie oferuje (Twój adres IP będzie udostępniony podczas połączenia)", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Pozwól na awaryjny serwer wspomagania połączeń turn.matrix.org, gdy Twój serwer domowy takiego nie oferuje (Twój adres IP będzie udostępniony podczas połączenia)", "Messages containing my username": "Wiadomości zawierające moją nazwę użytkownika", "Encrypted messages in one-to-one chats": "Zaszyforwane wiadomości w rozmowach jeden-do-jednego", "Encrypted messages in group chats": "Zaszyfrowane wiadomości w rozmowach grupowych", @@ -1619,7 +1619,7 @@ "Disconnect Identity Server": "Odłącz Serwer Tożsamości", "Disconnect": "Odłącz", "Identity Server (%(server)s)": "Serwer tożsamości (%(server)s)", - "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Używasz aby odkrywać i być odkrywanym przez isteniejące kontakty, które znasz. Możesz zmienić serwer tożsamości poniżej.", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Używasz , aby odnajdywać i móc być odnajdywanym przez istniejące kontakty, które znasz. Możesz zmienić serwer tożsamości poniżej.", "Identity Server": "Serwer Tożsamości", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Nie używasz serwera tożsamości. Aby odkrywać i być odkrywanym przez istniejące kontakty które znasz, dodaj jeden poniżej.", "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Odłączenie się od serwera tożsamości oznacza, że nie będzie możliwości wykrycia przez innych użytkowników oraz nie będzie możliwości zaproszenia innych e-mailem lub za pomocą telefonu.", @@ -1653,5 +1653,41 @@ "You do not have the required permissions to use this command.": "Nie posiadasz wymaganych uprawnień do użycia tego polecenia.", "Changes the avatar of the current room": "Zmienia awatar dla obecnego pokoju", "Use an identity server": "Użyj serwera tożsamości", - "Show previews/thumbnails for images": "Pokaż podgląd/miniatury obrazów" + "Show previews/thumbnails for images": "Pokaż podgląd/miniatury obrazów", + "Trust": "Zaufaj", + "Custom (%(level)s)": "Własny (%(level)s)", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Użyj serwera tożsamości, by zaprosić z użyciem adresu e-mail. Kliknij dalej, żeby użyć domyślnego serwera tożsamości (%(defaultIdentityServerName)s), lub zmień w Ustawieniach.", + "Use an identity server to invite by email. Manage in Settings.": "Użyj serwera tożsamości, by zaprosić za pomocą adresu e-mail. Zarządzaj w ustawieniach.", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "Use the new, consistent UserInfo panel for Room Members and Group Members": "Użyj nowego, spójnego panelu informacji o użytkowniku dla członków pokoju i grup", + "Try out new ways to ignore people (experimental)": "Wypróbuj nowe sposoby na ignorowanie ludzi (eksperymentalne)", + "Send verification requests in direct message": "Wysyłaj prośby o weryfikację w bezpośredniej wiadomości", + "Use the new, faster, composer for writing messages": "Używaj nowego, szybszego kompozytora do pisania wiadomości", + "My Ban List": "Moja lista zablokowanych", + "This is your list of users/servers you have blocked - don't leave the room!": "To jest Twoja lista zablokowanych użytkowników/serwerów – nie opuszczaj tego pokoju!", + "Change identity server": "Zmień serwer tożsamości", + "Disconnect from the identity server and connect to instead?": "Rozłączyć się z serwerem tożsamości i połączyć się w jego miejsce z ?", + "Disconnect identity server": "Odłączanie serwera tożsamości", + "You should:": "Należy:", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "sprawdzić rozszerzenia przeglądarki, które mogą blokować serwer tożsamości (takie jak Privacy Badger)", + "contact the administrators of identity server ": "skontaktować się z administratorami serwera tożsamości ", + "wait and try again later": "zaczekaj i spróbuj ponownie później", + "Disconnect anyway": "Odłącz mimo to", + "You are still sharing your personal data on the identity server .": "W dalszym ciągu udostępniasz swoje dane osobowe na serwerze tożsamości .", + "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Zalecamy, by usunąć swój adres e-mail i numer telefonu z serwera tożsamości przed odłączeniem.", + "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "Jeżeli nie chcesz używać do odnajdywania i bycia odnajdywanym przez osoby, które znasz, wpisz inny serwer tożsamości poniżej.", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Używanie serwera tożsamości jest opcjonalne. Jeżeli postanowisz nie używać serwera tożsamości, pozostali użytkownicy nie będą w stanie Cię odnaleźć ani nie będziesz mógł zaprosić innych po adresie e-mail czy numerze telefonu.", + "Do not use an identity server": "Nie używaj serwera tożsamości", + "Clear cache and reload": "Wyczyść pamięć podręczną i przeładuj", + "Something went wrong. Please try again or view your console for hints.": "Coś poszło nie tak. Spróbuj ponownie lub sprawdź konsolę przeglądarki dla wskazówek.", + "Please verify the room ID or alias and try again.": "Zweryfikuj poprawność ID pokoju lub nazwy zastępczej i spróbuj ponownie.", + "Please try again or view your console for hints.": "Spróbuj ponownie lub sprawdź konsolę przeglądarki dla wskazówek.", + "Personal ban list": "Osobista lista zablokowanych", + "Server or user ID to ignore": "ID serwera lub użytkownika do zignorowania", + "eg: @bot:* or example.org": "np: @bot:* lub przykład.pl", + "Composer": "Kompozytor", + "Autocomplete delay (ms)": "Opóźnienie autouzupełniania (ms)", + "Explore": "Przeglądaj", + "Filter": "Filtruj", + "Add room": "Dodaj pokój" } From 0eedab4154c18a39d9ae193a9eaf0ed2a34a3077 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 19 Nov 2019 05:46:11 +0000 Subject: [PATCH 0694/2372] Translated using Weblate (Portuguese) Currently translated at 33.3% (638 of 1918 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pt/ --- src/i18n/strings/pt.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json index 7cc80cfc78..5a56e807e4 100644 --- a/src/i18n/strings/pt.json +++ b/src/i18n/strings/pt.json @@ -842,5 +842,11 @@ "Collapse panel": "Colapsar o painel", "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Com o seu navegador atual, a aparência e sensação de uso da aplicação podem estar completamente incorretas, e algumas das funcionalidades poderão não funcionar. Se quiser tentar de qualquer maneira pode continuar, mas está por sua conta com algum problema que possa encontrar!", "Checking for an update...": "A procurar uma atualização...", - "There are advanced notifications which are not shown here": "Existem notificações avançadas que não são exibidas aqui" + "There are advanced notifications which are not shown here": "Existem notificações avançadas que não são exibidas aqui", + "Add Email Address": "Adicione adresso de e-mail", + "Add Phone Number": "Adicione número de telefone", + "The platform you're on": "A plataforma em que se encontra", + "The version of Riot.im": "A versão do RIOT.im", + "Whether or not you're logged in (we don't record your username)": "Tenha ou não, iniciado sessão (não iremos guardar o seu username)", + "Your language of choice": "O seu idioma de escolha" } From de0287213e240aff60a3e9e4fff9421dab42715f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 19 Nov 2019 14:42:35 +0100 Subject: [PATCH 0695/2372] use general warning icon instead of e2e one for room status --- src/components/structures/RoomStatusBar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 21dd06767c..b0aa4cb59b 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -289,7 +289,7 @@ module.exports = createReactClass({ } return
        - +
        { title } @@ -306,7 +306,7 @@ module.exports = createReactClass({ if (this._shouldShowConnectionError()) { return (
        - /!\ + /!\
        { _t('Connectivity to the server has been lost.') } From 9dea84892720c760dfd1d241d633b87a2c87a6ab Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 19 Nov 2019 16:28:49 +0000 Subject: [PATCH 0696/2372] Use div around buttons to fix React warning --- res/css/views/settings/_KeyBackupPanel.scss | 4 ++++ src/components/views/settings/KeyBackupPanel.js | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/res/css/views/settings/_KeyBackupPanel.scss b/res/css/views/settings/_KeyBackupPanel.scss index 1bcc0ab10d..4c4190c604 100644 --- a/res/css/views/settings/_KeyBackupPanel.scss +++ b/res/css/views/settings/_KeyBackupPanel.scss @@ -30,3 +30,7 @@ limitations under the License. .mx_KeyBackupPanel_deviceName { font-style: italic; } + +.mx_KeyBackupPanel_buttonRow { + margin: 1em 0; +} diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 3d00695e73..67d2d32d50 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -288,14 +288,14 @@ export default class KeyBackupPanel extends React.PureComponent {
        {backupSigStatuses}
        {trustedLocally}
        -

        +

        {restoreButtonCaption}     { _t("Delete Backup") } -

        +
        ; } else { return
        From 80ee68a42f468e5754cad43797e37a0a8e668811 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 19 Nov 2019 22:36:55 +0000 Subject: [PATCH 0697/2372] Use a settings watcher to set the theme Rather than listening for account data updates manually --- src/components/structures/MatrixChat.js | 20 +++++++++---------- .../tabs/user/GeneralUserSettingsTab.js | 3 +++ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 620e73bf93..c6efb56a9d 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -274,6 +274,7 @@ export default createReactClass({ componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); + this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onThemeChanged); this.focusComposer = false; @@ -360,6 +361,7 @@ export default createReactClass({ componentWillUnmount: function() { Lifecycle.stopMatrixClient(); dis.unregister(this.dispatcherRef); + SettingsStore.unwatchSetting(this._themeWatchRef); window.removeEventListener("focus", this.onFocus); window.removeEventListener('resize', this.handleResize); this.state.resizeNotifier.removeListener("middlePanelResized", this._dispatchTimelineResize); @@ -382,6 +384,13 @@ export default createReactClass({ } }, + _onThemeChanged: function(settingName, roomId, atLevel, newValue) { + dis.dispatch({ + action: 'set_theme', + value: newValue, + }); + }, + startPageChangeTimer() { // Tor doesn't support performance if (!performance || !performance.mark) return null; @@ -1376,17 +1385,6 @@ export default createReactClass({ }, null, true); }); - cli.on("accountData", function(ev) { - if (ev.getType() === 'im.vector.web.settings') { - if (ev.getContent() && ev.getContent().theme) { - dis.dispatch({ - action: 'set_theme', - value: ev.getContent().theme, - }); - } - } - }); - const dft = new DecryptionFailureTracker((total, errorCode) => { Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total); }, (errorCode) => { diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 78961ad663..42324f1379 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -175,6 +175,9 @@ export default class GeneralUserSettingsTab extends React.Component { SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme); this.setState({theme: newTheme}); + // The settings watcher doesn't fire until the echo comes back from the + // server, so to make the theme change immediately we need to manually + // do the dispatch now dis.dispatch({action: 'set_theme', value: newTheme}); }; From a31d222570f7159d922ca0a94161574e92578678 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 19 Nov 2019 23:00:54 +0000 Subject: [PATCH 0698/2372] Add catch handler for theme setting --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 42324f1379..d400e7a839 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -173,7 +173,13 @@ export default class GeneralUserSettingsTab extends React.Component { const newTheme = e.target.value; if (this.state.theme === newTheme) return; - SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme); + // doing getValue in the .catch will still return the value we failed to set, + // so remember what the value was before we tried to set it so we can revert + const oldTheme = SettingsStore.getValue('theme'); + SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => { + dis.dispatch({action: 'set_theme', value: oldTheme}); + this.setState({theme: oldTheme}); + }); this.setState({theme: newTheme}); // The settings watcher doesn't fire until the echo comes back from the // server, so to make the theme change immediately we need to manually From ab8a9dd0e9d70ed2e340cb2594fb13ab62377161 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 20 Nov 2019 03:58:41 +0000 Subject: [PATCH 0699/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1917 of 1917 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 5c6e69c864..1dfdc34f1a 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2330,5 +2330,19 @@ "%(role)s in %(roomName)s": "%(role)s 在 %(roomName)s", "Messages in this room are end-to-end encrypted.": "在此聊天室中的訊息為端到端加密。", "Security": "安全", - "Verify": "驗證" + "Verify": "驗證", + "Send verification requests in direct message, including a new verification UX in the member panel.": "在直接訊息中傳送驗證請求,包含成員面板中新的驗證使用者體驗。", + "Enable cross-signing to verify per-user instead of per-device": "啟用交叉簽章以驗證每個使用者而非每個裝置", + "Any of the following data may be shared:": "可能會分享以下資料:", + "Your display name": "您的顯示名稱", + "Your avatar URL": "您的大頭貼 URL", + "Your user ID": "您的使用 ID", + "Your theme": "您的佈景主題", + "Riot URL": "Riot URL", + "Room ID": "聊天室 ID", + "Widget ID": "小工具 ID", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "使用這個小工具可能會與 %(widgetDomain)s 以及您的整合管理員分享資料 。", + "Using this widget may share data with %(widgetDomain)s.": "使用這個小工具可能會與 %(widgetDomain)s 分享資料 。", + "Widget added by": "小工具新增由", + "This widget may use cookies.": "這個小工具可能會使用 cookies。" } From df868a6b0971be5b62c28563e9cb40308f3623ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Wed, 20 Nov 2019 08:43:16 +0000 Subject: [PATCH 0700/2372] Translated using Weblate (French) Currently translated at 100.0% (1917 of 1917 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 824da9d3ff..64272bb839 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2350,5 +2350,6 @@ "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "L’utilisation de ce widget pourrait partager des données avec %(widgetDomain)s et votre gestionnaire d’intégrations.", "Using this widget may share data with %(widgetDomain)s.": "L’utilisation de ce widget pourrait partager des données avec %(widgetDomain)s.", "Widget added by": "Widget ajouté par", - "This widget may use cookies.": "Ce widget pourrait utiliser des cookies." + "This widget may use cookies.": "Ce widget pourrait utiliser des cookies.", + "Send verification requests in direct message, including a new verification UX in the member panel.": "Envoyer les demandes de vérification en message direct, en incluant une nouvelle expérience de vérification dans le tableau des membres." } From 8df0aee12b15b963c0398049d54bf179bdb062c7 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 19 Nov 2019 18:58:32 +0000 Subject: [PATCH 0701/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1917 of 1917 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 003af8240c..892f21dbb1 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2337,5 +2337,6 @@ "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg %(widgetDomain)s domain-nel és az Integrációs Menedzserrel.", "Using this widget may share data with %(widgetDomain)s.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg %(widgetDomain)s domain-nel.", "Widget added by": "A kisalkalmazást hozzáadta", - "This widget may use cookies.": "Ez a kisalkalmazás sütiket használhat." + "This widget may use cookies.": "Ez a kisalkalmazás sütiket használhat.", + "Send verification requests in direct message, including a new verification UX in the member panel.": "Ellenőrzés küldése közvetlen üzenetben, beleértve az új ellenőrzési felhasználói élményt a résztvevői panelen." } From f25236c3fb902b109ac1c480058843d17e2536f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Wed, 20 Nov 2019 07:16:06 +0000 Subject: [PATCH 0702/2372] Translated using Weblate (Korean) Currently translated at 100.0% (1917 of 1917 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 8d34fab025..757edbfa4b 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2181,5 +2181,19 @@ "Messages in this room are end-to-end encrypted.": "이 방의 메시지는 종단간 암호화되었습니다.", "Security": "보안", "Verify": "확인", - "You have ignored this user, so their message is hidden. Show anyways.": "이 사용자를 무시했습니다. 사용자의 메시지는 숨겨집니다. 무시하고 보이기." + "You have ignored this user, so their message is hidden. Show anyways.": "이 사용자를 무시했습니다. 사용자의 메시지는 숨겨집니다. 무시하고 보이기.", + "Send verification requests in direct message, including a new verification UX in the member panel.": "다이렉트 메시지에서 구성원 패널에 새 확인 UX가 적용된 확인 요청을 보냅니다.", + "Enable cross-signing to verify per-user instead of per-device": "기기 당 확인이 아닌 사람 당 확인을 위한 교차 서명 켜기", + "Any of the following data may be shared:": "다음 데이터가 공유됩니다:", + "Your display name": "당신의 표시 이름", + "Your avatar URL": "당신의 아바타 URL", + "Your user ID": "당신의 사용자 ID", + "Your theme": "당신의 테마", + "Riot URL": "Riot URL", + "Room ID": "방 ID", + "Widget ID": "위젯 ID", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "이 위젯을 사용하면 %(widgetDomain)s & 통합 관리자와 데이터를 공유합니다.", + "Using this widget may share data with %(widgetDomain)s.": "이 위젯을 사용하면 %(widgetDomain)s와(과) 데이터를 공유합니다.", + "Widget added by": "위젯을 추가했습니다", + "This widget may use cookies.": "이 위젯은 쿠키를 사용합니다." } From 870def5d858b2a7431cdf55b0fd4099a645bfdc9 Mon Sep 17 00:00:00 2001 From: fenuks Date: Tue, 19 Nov 2019 19:22:27 +0000 Subject: [PATCH 0703/2372] Translated using Weblate (Polish) Currently translated at 76.0% (1456 of 1917 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pl/ --- src/i18n/strings/pl.json | 48 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 4054c48f97..f9c056b02b 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -1492,7 +1492,7 @@ "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Możesz się zarejestrować, lecz niektóre funkcje nie będą dostępne dopóki Serwer Tożsamości nie będzie znów online. Jeśli ciągle widzisz to ostrzeżenie, sprawdź swoją konfigurację lub skontaktuj się z administratorem serwera.", "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Możesz zresetować hasło, lecz niektóre funkcje nie będą dostępne dopóki Serwer Tożsamości nie będzie znów online. Jeśli ciągle widzisz to ostrzeżenie, sprawdź swoją konfigurację lub skontaktuj się z administratorem serwera.", "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Możesz się zalogować, lecz niektóre funkcje nie będą dostępne dopóki Serwer Tożsamości nie będzie znów online. Jeśli ciągle widzisz to ostrzeżenie, sprawdź swoją konfigurację lub skontaktuj się z administratorem serwera.", - "No homeserver URL provided": "Nie podano URL serwera głównego.", + "No homeserver URL provided": "Nie podano URL serwera głównego", "The server does not support the room version specified.": "Serwer nie wspiera tej wersji pokoju.", "Name or Matrix ID": "Imię lub identyfikator Matrix", "Email, name or Matrix ID": "E-mail, imię lub Matrix ID", @@ -1528,7 +1528,7 @@ "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s dezaktywował Flair dla %(groups)s w tym pokoju.", "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s aktywował Flair dla %(newGroups)s i dezaktywował Flair dla %(oldGroups)s w tym pokoju.", "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s odwołał zaproszednie dla %(targetDisplayName)s aby dołączył do pokoju.", - "%(names)s and %(count)s others are typing …|one": "%(names)s i jedna osoba pisze.", + "%(names)s and %(count)s others are typing …|one": "%(names)s i jedna osoba pisze…", "Cannot reach homeserver": "Błąd połączenia z serwerem domowym", "Ensure you have a stable internet connection, or get in touch with the server admin": "Upewnij się, że posiadasz stabilne połączenie internetowe lub skontaktuj się z administratorem serwera", "Your Riot is misconfigured": "Twój Riot jest źle skonfigurowany", @@ -1689,5 +1689,47 @@ "Autocomplete delay (ms)": "Opóźnienie autouzupełniania (ms)", "Explore": "Przeglądaj", "Filter": "Filtruj", - "Add room": "Dodaj pokój" + "Add room": "Dodaj pokój", + "A device's public name is visible to people you communicate with": "Publiczna nazwa urządzenia jest widoczna dla ludzi, z którymi się komunikujesz", + "Request media permissions": "Zapytaj o uprawnienia", + "Voice & Video": "Głos & Wideo", + "this room": "ten pokój", + "View older messages in %(roomName)s.": "Wyświetl starsze wiadomości w %(roomName)s.", + "Room information": "Informacje o pokoju", + "Internal room ID:": "Wewnętrzne ID pokoju:", + "Uploaded sound": "Przesłano dźwięk", + "Change history visibility": "Zmień widoczność historii", + "Upgrade the room": "Zaktualizuj pokój", + "Enable room encryption": "Włącz szyfrowanie pokoju", + "Select the roles required to change various parts of the room": "Wybierz role wymagane do zmieniania różnych części pokoju", + "Enable encryption?": "Włączyć szyfrowanie?", + "Your email address hasn't been verified yet": "Twój adres e-mail nie został jeszcze zweryfikowany", + "Verification code": "Kod weryfikacyjny", + "Remove %(email)s?": "Usunąć %(email)s?", + "Remove %(phone)s?": "Usunąć %(phone)s?", + "Some devices in this encrypted room are not trusted": "Niektóre urządzenia w tym zaszyfrowanym pokoju nie są zaufane", + "Loading …": "Ładowanie…", + "Loading room preview": "Wczytywanie podglądu pokoju", + "Try to join anyway": "Spróbuj dołączyć mimo tego", + "You can still join it because this is a public room.": "Możesz mimo to dołączyć, gdyż pokój jest publiczny.", + "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "To zaproszenie do %(roomName)s zostało wysłane na adres %(email)s, który nie jest przypisany do Twojego konta", + "Link this email with your account in Settings to receive invites directly in Riot.": "Połącz ten adres e-mail z Twoim kontem w Ustawieniach, aby otrzymywać zaproszenia bezpośrednio w Riot.", + "This invite to %(roomName)s was sent to %(email)s": "To zaproszenie do %(roomName)s zostało wysłane do %(email)s", + "Use an identity server in Settings to receive invites directly in Riot.": "Użyj serwera tożsamości w Ustawieniach, aby otrzymywać zaproszenia bezpośrednio w Riot.", + "Do you want to chat with %(user)s?": "Czy chcesz rozmawiać z %(user)s?", + "Do you want to join %(roomName)s?": "Czy chcesz dołączyć do %(roomName)s?", + " invited you": " zaprosił(a) CIę", + "You're previewing %(roomName)s. Want to join it?": "Przeglądasz %(roomName)s. Czy chcesz dołączyć do pokoju?", + "Not now": "Nie teraz", + "Don't ask me again": "Nie pytaj ponownie", + "%(count)s unread messages including mentions.|other": "%(count)s nieprzeczytanych wiadomości, wliczając wzmianki.", + "%(count)s unread messages including mentions.|one": "1 nieprzeczytana wzmianka.", + "%(count)s unread messages.|other": "%(count)s nieprzeczytanych wiadomości.", + "%(count)s unread messages.|one": "1 nieprzeczytana wiadomość.", + "Unread mentions.": "Nieprzeczytane wzmianki.", + "Unread messages.": "Nieprzeczytane wiadomości.", + "Join": "Dołącz", + "%(creator)s created and configured the room.": "%(creator)s stworzył(a) i skonfigurował(a) pokój.", + "Preview": "Przejrzyj", + "View": "Wyświetl" } From d277a1946ba6270e30a61b3ec3566e898df0c256 Mon Sep 17 00:00:00 2001 From: Karol Kosek Date: Tue, 19 Nov 2019 19:43:44 +0000 Subject: [PATCH 0704/2372] Translated using Weblate (Polish) Currently translated at 76.0% (1456 of 1917 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pl/ --- src/i18n/strings/pl.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index f9c056b02b..a0ce517404 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -1731,5 +1731,6 @@ "Join": "Dołącz", "%(creator)s created and configured the room.": "%(creator)s stworzył(a) i skonfigurował(a) pokój.", "Preview": "Przejrzyj", - "View": "Wyświetl" + "View": "Wyświetl", + "Missing media permissions, click the button below to request.": "Brakuje uprawnień do mediów, kliknij przycisk poniżej, aby o nie zapytać." } From 8f796617257758f3b91752e8eef5f167de5c6578 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 20 Nov 2019 10:30:34 +0000 Subject: [PATCH 0705/2372] Update code style for our 90 char life We've been using 90 chars for JS code for quite a while now, but for some reason, the code style guide hasn't admitted that, so this adjusts it to match ESLint settings. --- code_style.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_style.md b/code_style.md index e7844b939c..4b2338064c 100644 --- a/code_style.md +++ b/code_style.md @@ -22,7 +22,7 @@ number throgh from the original code to the final application. General Style ------------- - 4 spaces to indent, for consistency with Matrix Python. -- 120 columns per line, but try to keep JavaScript code around the 80 column mark. +- 120 columns per line, but try to keep JavaScript code around the 90 column mark. Inline JSX in particular can be nicer with more columns per line. - No trailing whitespace at end of lines. - Don't indent empty lines. From 2f5b0a9652629cb75bdf39926ff2f045511286ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 20 Nov 2019 12:30:03 +0100 Subject: [PATCH 0706/2372] EventIndex: Use property initializer style for the bound callbacks. --- src/indexing/EventIndex.js | 37 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index df81667c6e..e6a1d4007b 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -31,19 +31,6 @@ export default class EventIndex { this._eventsPerCrawl = 100; this._crawler = null; this.liveEventsForIndex = new Set(); - - this.boundOnSync = async (state, prevState, data) => { - await this.onSync(state, prevState, data); - }; - this.boundOnRoomTimeline = async ( ev, room, toStartOfTimeline, removed, - data) => { - await this.onRoomTimeline(ev, room, toStartOfTimeline, removed, data); - }; - this.boundOnEventDecrypted = async (ev, err) => { - await this.onEventDecrypted(ev, err); - }; - this.boundOnTimelineReset = async (room, timelineSet, - resetAllTimelines) => await this.onTimelineReset(room); } async init() { @@ -56,23 +43,23 @@ export default class EventIndex { registerListeners() { const client = MatrixClientPeg.get(); - client.on('sync', this.boundOnSync); - client.on('Room.timeline', this.boundOnRoomTimeline); - client.on('Event.decrypted', this.boundOnEventDecrypted); - client.on('Room.timelineReset', this.boundOnTimelineReset); + client.on('sync', this.onSync); + client.on('Room.timeline', this.onRoomTimeline); + client.on('Event.decrypted', this.onEventDecrypted); + client.on('Room.timelineReset', this.onTimelineReset); } removeListeners() { const client = MatrixClientPeg.get(); if (client === null) return; - client.removeListener('sync', this.boundOnSync); - client.removeListener('Room.timeline', this.boundOnRoomTimeline); - client.removeListener('Event.decrypted', this.boundOnEventDecrypted); - client.removeListener('Room.timelineReset', this.boundOnTimelineReset); + client.removeListener('sync', this.onSync); + client.removeListener('Room.timeline', this.onRoomTimeline); + client.removeListener('Event.decrypted', this.onEventDecrypted); + client.removeListener('Room.timelineReset', this.onTimelineReset); } - async onSync(state, prevState, data) { + onSync = async (state, prevState, data) => { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (prevState === null && state === "PREPARED") { @@ -146,7 +133,7 @@ export default class EventIndex { } } - async onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { + onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => { // We only index encrypted rooms locally. if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; @@ -169,7 +156,7 @@ export default class EventIndex { } } - async onEventDecrypted(ev, err) { + onEventDecrypted = async (ev, err) => { const eventId = ev.getId(); // If the event isn't in our live event set, ignore it. @@ -377,7 +364,7 @@ export default class EventIndex { console.log("EventIndex: Stopping crawler function"); } - async onTimelineReset(room) { + onTimelineReset = async (room, timelineSet, resetAllTimelines) => { if (room === null) return; const indexManager = PlatformPeg.get().getEventIndexingManager(); From 0631faf902c5870b263d8f2745745c6ae2281a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 20 Nov 2019 12:31:07 +0100 Subject: [PATCH 0707/2372] Settings: Fix the supportedLevels for event indexing feature. --- src/settings/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 8abd845f0c..2cf9509aca 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -122,7 +122,7 @@ export const SETTINGS = { }, "feature_event_indexing": { isFeature: true, - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + supportedLevels: LEVELS_FEATURE, displayName: _td("Enable local event indexing and E2EE search (requires restart)"), default: false, }, From 4bd46f9d694f03aeaad667e35ab64083f7d4479f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 20 Nov 2019 12:47:20 +0100 Subject: [PATCH 0708/2372] EventIndex: Silence the linter complaining about missing docs. --- src/indexing/EventIndex.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index e6a1d4007b..6bad992017 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -17,7 +17,7 @@ limitations under the License. import PlatformPeg from "../PlatformPeg"; import MatrixClientPeg from "../MatrixClientPeg"; -/** +/* * Event indexing class that wraps the platform specific event indexing. */ export default class EventIndex { From 5a700b518a5063a1484ee339daf2a4deb611d485 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Nov 2019 13:41:06 +0000 Subject: [PATCH 0709/2372] Get theme automatically from system setting Uses CSS `prefers-color-scheme` to get the user's preferred colour scheme. Also bundles up some theme logic into its own class. --- src/components/structures/MatrixChat.js | 17 ++--- .../tabs/user/GeneralUserSettingsTab.js | 28 ++++++-- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 5 ++ src/theme.js | 67 ++++++++++++++++++- 5 files changed, 100 insertions(+), 18 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index c6efb56a9d..661a0c7077 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -59,7 +59,7 @@ import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; import DMRoomMap from '../../utils/DMRoomMap'; import { countRoomsWithNotif } from '../../RoomNotifs'; -import { setTheme } from "../../theme"; +import { ThemeWatcher } from "../../theme"; import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { defer } from "../../utils/promise"; @@ -274,7 +274,8 @@ export default createReactClass({ componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); - this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onThemeChanged); + this._themeWatcher = new ThemeWatcher(); + this._themeWatcher.start(); this.focusComposer = false; @@ -361,7 +362,7 @@ export default createReactClass({ componentWillUnmount: function() { Lifecycle.stopMatrixClient(); dis.unregister(this.dispatcherRef); - SettingsStore.unwatchSetting(this._themeWatchRef); + this._themeWatcher.stop(); window.removeEventListener("focus", this.onFocus); window.removeEventListener('resize', this.handleResize); this.state.resizeNotifier.removeListener("middlePanelResized", this._dispatchTimelineResize); @@ -384,13 +385,6 @@ export default createReactClass({ } }, - _onThemeChanged: function(settingName, roomId, atLevel, newValue) { - dis.dispatch({ - action: 'set_theme', - value: newValue, - }); - }, - startPageChangeTimer() { // Tor doesn't support performance if (!performance || !performance.mark) return null; @@ -672,9 +666,6 @@ export default createReactClass({ }); break; } - case 'set_theme': - setTheme(payload.value); - break; case 'on_logging_in': // We are now logging in, so set the state to reflect that // NB. This does not touch 'ready' since if our dispatches diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index d400e7a839..50f37cea1f 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -27,7 +27,7 @@ import LanguageDropdown from "../../../elements/LanguageDropdown"; import AccessibleButton from "../../../elements/AccessibleButton"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import PropTypes from "prop-types"; -import {enumerateThemes} from "../../../../../theme"; +import {enumerateThemes, ThemeWatcher} from "../../../../../theme"; import PlatformPeg from "../../../../../PlatformPeg"; import MatrixClientPeg from "../../../../../MatrixClientPeg"; import sdk from "../../../../.."; @@ -50,6 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component { this.state = { language: languageHandler.getCurrentLanguage(), theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"), + useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"), haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), serverSupportsSeparateAddAndBind: null, idServerHasUnsignedTerms: false, @@ -177,16 +178,22 @@ export default class GeneralUserSettingsTab extends React.Component { // so remember what the value was before we tried to set it so we can revert const oldTheme = SettingsStore.getValue('theme'); SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => { - dis.dispatch({action: 'set_theme', value: oldTheme}); + dis.dispatch({action: 'recheck_theme'}); this.setState({theme: oldTheme}); }); this.setState({theme: newTheme}); // The settings watcher doesn't fire until the echo comes back from the // server, so to make the theme change immediately we need to manually // do the dispatch now - dis.dispatch({action: 'set_theme', value: newTheme}); + dis.dispatch({action: 'recheck_theme'}); }; + _onUseSystemThemeChanged = (checked) => { + this.setState({useSystemTheme: checked}); + dis.dispatch({action: 'recheck_theme'}); + } + + _onPasswordChangeError = (err) => { // TODO: Figure out a design that doesn't involve replacing the current dialog let errMsg = err.error || ""; @@ -297,11 +304,24 @@ export default class GeneralUserSettingsTab extends React.Component { _renderThemeSection() { const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); + + const themeWatcher = new ThemeWatcher(); + let systemThemeSection; + if (themeWatcher.isSystemThemeSupported()) { + systemThemeSection =
        + +
        ; + } return (
        {_t("Theme")} + {systemThemeSection} + value={this.state.theme} onChange={this._onThemeChange} + disabled={this.state.useSystemTheme} + > {Object.entries(enumerateThemes()).map(([theme, text]) => { return ; })} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 473efdfb76..5dfcd038ec 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -363,6 +363,7 @@ "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", + "Match system theme": "Match system theme", "Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls", "Send analytics data": "Send analytics data", "Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 718a0daec3..8a3bc3ecbc 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -275,6 +275,11 @@ export const SETTINGS = { supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: [], }, + "use_system_theme": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: true, + displayName: _td("Match system theme"), + }, "webRtcAllowPeerToPeer": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, displayName: _td('Allow Peer-to-Peer for 1:1 calls'), diff --git a/src/theme.js b/src/theme.js index 8a15c606d7..8996fe28fd 100644 --- a/src/theme.js +++ b/src/theme.js @@ -19,8 +19,72 @@ import {_t} from "./languageHandler"; export const DEFAULT_THEME = "light"; import Tinter from "./Tinter"; +import dis from "./dispatcher"; import SettingsStore from "./settings/SettingsStore"; +export class ThemeWatcher { + static _instance = null; + + constructor() { + this._themeWatchRef = null; + this._systemThemeWatchRef = null; + this._dispatcherRef = null; + + // we have both here as each may either match or not match, so by having both + // we can get the tristate of dark/light/unsupported + this._preferDark = global.matchMedia("(prefers-color-scheme: dark)"); + this._preferLight = global.matchMedia("(prefers-color-scheme: light)"); + + this._currentTheme = this.getEffectiveTheme(); + } + + start() { + this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onChange); + this._systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this._onChange); + this._preferDark.addEventListener('change', this._onChange); + this._preferLight.addEventListener('change', this._onChange); + this._dispatcherRef = dis.register(this._onAction); + } + + stop() { + this._preferDark.removeEventListener('change', this._onChange); + this._preferLight.removeEventListener('change', this._onChange); + SettingsStore.unwatchSetting(this._systemThemeWatchRef); + SettingsStore.unwatchSetting(this._themeWatchRef); + dis.unregister(this._dispatcherRef); + } + + _onChange = () => { + this.recheck(); + } + + _onAction = (payload) => { + if (payload.action === 'recheck_theme') { + this.recheck(); + } + } + + recheck() { + const oldTheme = this._currentTheme; + this._currentTheme = this.getEffectiveTheme(); + if (oldTheme !== this._currentTheme) { + setTheme(this._currentTheme); + } + } + + getEffectiveTheme() { + if (SettingsStore.getValue('use_system_theme')) { + if (this._preferDark.matches) return 'dark'; + if (this._preferLight.matches) return 'light'; + } + return SettingsStore.getValue('theme'); + } + + isSystemThemeSupported() { + return this._preferDark || this._preferLight; + } +} + export function enumerateThemes() { const BUILTIN_THEMES = { "light": _t("Light theme"), @@ -83,7 +147,8 @@ export function getBaseTheme(theme) { */ export function setTheme(theme) { if (!theme) { - theme = SettingsStore.getValue("theme"); + const themeWatcher = new ThemeWatcher(); + theme = themeWatcher.getEffectiveTheme(); } let stylesheetName = theme; if (theme.startsWith("custom-")) { From 71f5c8b2b045a85beb563be5ef5483a6c0137723 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Nov 2019 13:47:54 +0000 Subject: [PATCH 0710/2372] Lint --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 50f37cea1f..dbe0a9a301 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -188,7 +188,7 @@ export default class GeneralUserSettingsTab extends React.Component { dis.dispatch({action: 'recheck_theme'}); }; - _onUseSystemThemeChanged = (checked) => { + _onUseSystemThemeChanged = (checked) => { this.setState({useSystemTheme: checked}); dis.dispatch({action: 'recheck_theme'}); } From a7444152213fc18e070e9e3ffca92f349c51fb47 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Nov 2019 15:34:32 +0000 Subject: [PATCH 0711/2372] Add hack to work around mystery settings bug --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 5 ++++- src/theme.js | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index dbe0a9a301..b518f7c81b 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -185,7 +185,10 @@ export default class GeneralUserSettingsTab extends React.Component { // The settings watcher doesn't fire until the echo comes back from the // server, so to make the theme change immediately we need to manually // do the dispatch now - dis.dispatch({action: 'recheck_theme'}); + // XXX: The local echoed value appears to be unreliable, in particular + // when settings custom themes(!) so adding forceTheme to override + // the value from settings. + dis.dispatch({action: 'recheck_theme', forceTheme: newTheme}); }; _onUseSystemThemeChanged = (checked) => { diff --git a/src/theme.js b/src/theme.js index 8996fe28fd..5e390bf2c8 100644 --- a/src/theme.js +++ b/src/theme.js @@ -60,13 +60,15 @@ export class ThemeWatcher { _onAction = (payload) => { if (payload.action === 'recheck_theme') { - this.recheck(); + // XXX forceTheme + this.recheck(payload.forceTheme); } } - recheck() { + // XXX: forceTheme param aded here as local echo appears to be unreliable + recheck(forceTheme) { const oldTheme = this._currentTheme; - this._currentTheme = this.getEffectiveTheme(); + this._currentTheme = forceTheme === undefined ? this.getEffectiveTheme() : forceTheme; if (oldTheme !== this._currentTheme) { setTheme(this._currentTheme); } From 518130c912dfd8cf196be96698fc4d342b79841e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Nov 2019 15:37:48 +0000 Subject: [PATCH 0712/2372] add bug link --- src/theme.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/theme.js b/src/theme.js index 5e390bf2c8..43bc813d34 100644 --- a/src/theme.js +++ b/src/theme.js @@ -66,6 +66,7 @@ export class ThemeWatcher { } // XXX: forceTheme param aded here as local echo appears to be unreliable + // https://github.com/vector-im/riot-web/issues/11443 recheck(forceTheme) { const oldTheme = this._currentTheme; this._currentTheme = forceTheme === undefined ? this.getEffectiveTheme() : forceTheme; From b69cee0c6756796ae6db514a2a328fd7b012ff02 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Nov 2019 15:45:32 +0000 Subject: [PATCH 0713/2372] Remove getBaseTheme This was only used by vector/index.js, in the code removed by https://github.com/vector-im/riot-web/pull/11445 React SDK does a very similar thing in setTheme but also gets the rest of the custom theme name. Requires https://github.com/vector-im/riot-web/pull/11445 --- src/theme.js | 79 ++++++++++++++++++++++------------------------------ 1 file changed, 34 insertions(+), 45 deletions(-) diff --git a/src/theme.js b/src/theme.js index 43bc813d34..634e5cce7f 100644 --- a/src/theme.js +++ b/src/theme.js @@ -127,28 +127,14 @@ function getCustomTheme(themeName) { return customTheme; } -/** - * Gets the underlying theme name for the given theme. This is usually the theme or - * CSS resource that the theme relies upon to load. - * @param {string} theme The theme name to get the base of. - * @returns {string} The base theme (typically "light" or "dark"). - */ -export function getBaseTheme(theme) { - if (!theme) return "light"; - if (theme.startsWith("custom-")) { - const customTheme = getCustomTheme(theme.substr(7)); - return customTheme.is_dark ? "dark-custom" : "light-custom"; - } - - return theme; // it's probably a base theme -} - /** * Called whenever someone changes the theme + * Async function that returns once the theme has been set + * (ie. the CSS has been loaded) * * @param {string} theme new theme */ -export function setTheme(theme) { +export async function setTheme(theme) { if (!theme) { const themeWatcher = new ThemeWatcher(); theme = themeWatcher.getEffectiveTheme(); @@ -190,38 +176,41 @@ export function setTheme(theme) { styleElements[stylesheetName].disabled = false; - const switchTheme = function() { - // we re-enable our theme here just in case we raced with another - // theme set request as per https://github.com/vector-im/riot-web/issues/5601. - // We could alternatively lock or similar to stop the race, but - // this is probably good enough for now. - styleElements[stylesheetName].disabled = false; - Object.values(styleElements).forEach((a) => { - if (a == styleElements[stylesheetName]) return; - a.disabled = true; - }); - Tinter.setTheme(theme); - }; + return new Promise((resolve) => { + const switchTheme = function() { + // we re-enable our theme here just in case we raced with another + // theme set request as per https://github.com/vector-im/riot-web/issues/5601. + // We could alternatively lock or similar to stop the race, but + // this is probably good enough for now. + styleElements[stylesheetName].disabled = false; + Object.values(styleElements).forEach((a) => { + if (a == styleElements[stylesheetName]) return; + a.disabled = true; + }); + Tinter.setTheme(theme); + resolve(); + }; - // turns out that Firefox preloads the CSS for link elements with - // the disabled attribute, but Chrome doesn't. + // turns out that Firefox preloads the CSS for link elements with + // the disabled attribute, but Chrome doesn't. - let cssLoaded = false; + let cssLoaded = false; - styleElements[stylesheetName].onload = () => { - switchTheme(); - }; + styleElements[stylesheetName].onload = () => { + switchTheme(); + }; - for (let i = 0; i < document.styleSheets.length; i++) { - const ss = document.styleSheets[i]; - if (ss && ss.href === styleElements[stylesheetName].href) { - cssLoaded = true; - break; + for (let i = 0; i < document.styleSheets.length; i++) { + const ss = document.styleSheets[i]; + if (ss && ss.href === styleElements[stylesheetName].href) { + cssLoaded = true; + break; + } } - } - if (cssLoaded) { - styleElements[stylesheetName].onload = undefined; - switchTheme(); - } + if (cssLoaded) { + styleElements[stylesheetName].onload = undefined; + switchTheme(); + } + }); } From e36f4375b0bdece26b729679c61e530ca17a26bf Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Nov 2019 16:12:14 +0000 Subject: [PATCH 0714/2372] Bugfix & clearer setting name --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 2 +- src/theme.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5dfcd038ec..c62b39cda0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -363,7 +363,7 @@ "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", - "Match system theme": "Match system theme", + "Match system dark mode setting": "Match system dark mode setting", "Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls", "Send analytics data": "Send analytics data", "Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 8a3bc3ecbc..59e60353b8 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -278,7 +278,7 @@ export const SETTINGS = { "use_system_theme": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: true, - displayName: _td("Match system theme"), + displayName: _td("Match system dark mode setting"), }, "webRtcAllowPeerToPeer": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, diff --git a/src/theme.js b/src/theme.js index 43bc813d34..fa7e3f783b 100644 --- a/src/theme.js +++ b/src/theme.js @@ -84,7 +84,7 @@ export class ThemeWatcher { } isSystemThemeSupported() { - return this._preferDark || this._preferLight; + return this._preferDark.matches || this._preferLight.matches; } } From 758dd4127fe434cbcb0d1fd88d4361877f4aa458 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 20 Nov 2019 16:36:27 +0000 Subject: [PATCH 0715/2372] upgrade nunito from 3.500 to 3.504 fixes https://github.com/vector-im/riot-web/issues/8092 by way of https://github.com/google/fonts/issues/632 --- res/fonts/Nunito/Nunito-Bold.ttf | Bin 168112 -> 176492 bytes res/fonts/Nunito/Nunito-Regular.ttf | Bin 165596 -> 172236 bytes res/fonts/Nunito/Nunito-SemiBold.ttf | Bin 166620 -> 175064 bytes .../Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf | Bin 47628 -> 0 bytes .../Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf | Bin 47796 -> 0 bytes res/fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf | Bin 46796 -> 0 bytes res/fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf | Bin 47220 -> 0 bytes res/fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf | Bin 46556 -> 0 bytes res/fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf | Bin 48080 -> 0 bytes 9 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 res/fonts/Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf delete mode 100644 res/fonts/Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf delete mode 100644 res/fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf delete mode 100644 res/fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf delete mode 100644 res/fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf delete mode 100644 res/fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf diff --git a/res/fonts/Nunito/Nunito-Bold.ttf b/res/fonts/Nunito/Nunito-Bold.ttf index c70de76bbd1bf407351094e9c0b092a65082d394..c8fabf7d920a82c54d8bac9cc3ec330e91401709 100644 GIT binary patch delta 68198 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 delta 60528 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 N$WwtOPO44r{{fVkFJ%A# diff --git a/res/fonts/Nunito/Nunito-Regular.ttf b/res/fonts/Nunito/Nunito-Regular.ttf index 064e805431b203c336af6b5c8de7b8708f7304bf..86ce522f609fee54bdbb16dc9337b712611da618 100644 GIT binary patch delta 63829 zcmcG%31C#!**<>GoqaNyWM-1dGRb82Y?B1C0U-%P62h7g5P={ z(T`Cs0#dY!NUa?}#ZvdGwU#QiR;g91t+lQ7Yt>q{TH*h^=gv$f8`yrof2o-}_q_Mq zv%KeBpL6ETn~f)*Hr{FvA9$8Ark~g{q4oG*e)M-HIX`7gvQF%ne!)FW`uCXR)-!gg ze&PieHvOdJu!c!H(iyWJoqj<@HgZxR~X9!y*aa|`-(NVpU%RP^Fv(ddsbbu?EaJHuNbTDV*JUE zmM`sIV*BTqpW_9nkku~74c$Ur6TUCOci-|$*R8+#uQ$v@W6{8L&#KE7ci;OL+k1=! z>rtKW((d(Zw6-)E^f!RswYvM#rQ4sqD2?$?I~dai)?9YQI@`zpv$>w}uhui>UAt!O z(lzSmc4snH@)RC;jdA={U90`~swbXYm{$K!mZthM?y%Rt{eCKa{^rj+9+cNtOQT&x zskX2ynUP(_FhXoLyMx_~7pIsrnVP90|K2<`vxVn?5Y55;KS@vH!^Ko`v|73=$Ia$Q zAE6(ANczOJ`Zx0G8foG2pVdhFB-I2qM_w~pS|az3mh|$gqowh@UUrU=I^~KnlK%MB zW2ADGX2jXE(uCuO>!lxQc!9j6NjjyLUT68oM>k6gId747w@Bj(CA?dO|1$8G`k2Ag z#^q?0gfBX@p0QL`!Nx?kY@LyotM;lZ)K%&l^%!-Xx;gUB)_ZwfWXrZ0X%4kdJwjcr zuEqVa>Xyhm+xAP+%aKjj&oqDiFUHQa<3CK!nF}K`9CeY58|JkbzpwpDybyQsx?Fy@ zM6AbDvsANHbH&vX6&j*iiT@6(9#9>nd$>NTdPwz%>KWBZ)vJ+r({kslp2y8EFy^M% ze@1mk-K1_&Pf_=3$~D!RI!%kFRWnJ`u9>Bot(mKtrq(yY;J()4O}Y4&OE z);yqjkgC@_pk2V27EGmGu3d!}WQ@?xrvmLfZI^bDc8L~k>ieG{rU&q!@;~j|$ieGM zj@|Q}DpkHyDv;cgOY%rwDIgU}ekmx8lq#hy(pG85v7(#bk>o?`rS`Db?RA$$RYBL1 zW2%fy*3=xeHM_dHnvO^r%}q!As^+NFJZrrxYVzWuxqC@e-Ld|tBuUL(O;PvKJU87v zYR=?Mc`jUfn~vJJ4Q1Xa>zKQ=^QfH@Wm!}mjH*hbnay+QeNlUJbBrL(;acK~KGhLb z2j(3u;;FLw8mTNTwRytg33yjg$6WUj+zeeKHF!2gKWny1jn;{pQ^O{Z@&@hx2phXoa-i}tx5RkOw0z1>kubC+v-mn&*P56Ys};M7@j zkEoZlbo!#HOTFvMqPF1F@Qk@rFHr6}+$gY#f{fr1W^KN3?h&iCIm)}6qL$K5bRsJG zn~tQ>pLG0*ay!OFnL&1hNfTzxjat1;t_e{yW-!%@S;K5*;7%7_b38knGYhk^ zrluz9Z3fu_EV{qv+CwN3fpSi&!YWj2pk7(#K zFL*>N%JYLqbadei9?{chLGXxyK1=1dHcELsD1V8Ma9I!op^SYr(1lUjWw9?^vK^=Hl=>WApxzTPk+J$#{&}jPfqS5r}L!;@_k4DpHU;$c97lmjweHNk7 z^jVBX(`N}9O`qjKSG`ym6+sMKc9#q5ly}jrb`wjN6WfglMk`9A72xiX;O>cFx)^(F zH%Vr$T!*{7-J`)RWzov`^c*p3ClEtKN0uDX@XQHwM|6sTuS)9ta5U9HSB+>^4Qf;B znXniv+@CL^yfkWV7Wd`*Hc8I(YMx0Au0^NN&R8?EyTGR1qsyYBg5_EDWzo_7N>fE? zls7L1#TeNYmVDvSoM&dByLDKl_GlLPOYO)1CweD(yN{SyQ#7Tybm{hTugg`x9Z!wz zPv$BYgs!L-3>TwzMZ0K?hGxt?rgmvuj$`V8CbzSRgov>jYeGEXZS9I`G4%3J!cvY+ z-Q2YVTn!0~g`jTkc0@Ju-_g3DvAGq~gXhFUU~YP*|G;#iUS8^rYUOdgQmC;$-ar@T zP7{bV&|8nTHzbOpMywr|tJOP^9;NYWRLbPXd!-)JxFkY(#I;f&90G@zQ&U=_S7zOJf{n_(qhmd44IVKLYpo2V5R z9k}3hF(Y`Ci#s!MA?{p2G!s!Yi)iTLLZYFI*+fGZbArcUyENl!F0P;iaMc++1_O|; z=7n*o+`NbgIo+I3gyQOAA{18(h)!HBq?b+rSr@&OF1qQZbg_tDN*9ZXW)d!z5Di@{ zB^tU|Ml^KMBN`TN!R2z%GP+tJT1HnZMZ@Uo645ZaS|u7rSC@*0(ba0vFuJ-7!(5-3 z!!_btG=zf7mCNzCTub9lWYH#kyCQfb%yCUb>mcEANkmr)BKk%|SK;Y~M739oZ{msd z$|XHzLED(9FT6>76%~e+OQP>pE{XnHJld#KxLLU* zg6ot^BG>}jafu4Iif^L6ZOSE)U$0ye`3-n-oKoL+f=5c|nkcp_w~6A$;L#Le2t~Dy zqiXmguqmNxWWG(R46|_+ZVf9r!gNg`(==G()%t)gTc>X~wbwRAd; z6c^IH6Z&4p)u9^}&l034q5p+U?NX~$t1Z&xYBXl~>1~od@=$J2_OF-na!L8 z{Nqi$ozLUT_*Hx}-@*6rgZwCehQGw$;P3NK_!-G4+2J3QOLfw#s`cv87_H~kuV{Rl z4VrD5o!V)-B3-pEq}!m|7S_G3`#|@J?u_23x9b<^SL(0QZ#I-0?M9EW+}LQGVw`PU zWL$0BVBBWhY20r-Y<$G{jPbPbZz*$AmZUsyDl%1@Fde43rX{8|rdLx}rLIril6q_E z$<))S@1=f}`fsz|>@=5{Yt2pOcJn;*GIP}YnE6BV7im+{W~XgQ3xAOIS-O;NPIsjT z(?_Sbr0=m9Eq2Rl%LdCf%TCLF%VEnS)}VE?wZ+)X~3te@E= zo7v{G)!ABYGi?iOuiDumWWHl> zu|Jfh&svw&n{`vx?r_#US<$S=vR=p@kzJoXGkZbyx|~qXw4Av)OLEra^ya#9H|Cyn zG&$NGrycJ(K63m!uQsnKuRU*G-m<*4d7JXK=k3aSC-1|&FY?v->G^s2MfuhFq5RqT zkLEv{e=7fd=V<31=RxOD=M&Bs3tR9`<$k=KCJ;Tm4RdNg!A_zi@fsy28(jRu`L#A1Z#T_+;_vk`X2KC6h{Kl^!fT zTKYukOQmm=zF+!D>6xH09JB{L!SY~Tur)X{xIDNi_*t1$Hn*&+Y;D=As4b(Geex_+q4bWctXwkwqgLM@|_zd*q^#t4D4a zxozYJBR{K@D$6VDDtA{Nu6(4*Ts5U?c2%_Mv8t0*r>owp`lxzA^*hxc)^yZ_=hrN+ zSy$6rb5qUkT2pOyt*>@O?b_N+wcBfV)!touwDyVG7i(Xu{eA7bn|NH*9Fw7H-(tu)pEK#%YbWHGVO!D-;Zk4owNo z4lN435PCKAPUyqX7vt6A)5qtHFB)Gx{_yxm#y>OurSWf!e}DWZWy0GNKA7-jtG3nJ>TE4(t!-^;ZEu~|x~z3=>!#K( zCaNdCG4cIL>nClQbnB#jlhu7RAfb==f(Dm=qFWB!bj zGXpdC%)I*o{RJy8SbM>y3%1RYW^JGK$%X9~J}`UI?2g$7=h)}$p4&3_Oy|aV>GKX= zH2R`L^ULSodU5T=e_l|zpl-nf3r!2RFMPjiTi2hv=Xal6v~1DpMH?1LsgB ztbS(otE=C??1sxeTQhCV+%+pNufP28<&Uk^uidcr&9(1cQF}$>6|Gn7x?=woht~zy zRj=E(?wu=rS6+4H$*W9P&A#gS@YQu!AHMqN`rP%K*Kc3HYyJLfg4Z0~VBB!ex1?{~ z@U2_F_0q=rjZb~McT?V`UE!keGrco=pSU)3?f%Uzn-6dPQn~wR7IigAsM)rrXkQn|9mI+upl<*6ok&3hcUR*E6zH-Xg!YJA3zn-AC^z zx?}Af@9dehXY-!-_s-mV&)x_2KD76-z0dAFx%bt*Z|;3>?}vLo-PgEpEOJgGeyI=S-XRVN=g`OYtlzo`AiRlnH(i&tJU zzBKKn9WPs7ZhU#q%fCP6I<@lDzEjWtQvJ*JUq1HBFJGy9W!Wn`Upf7&j9=CLYSFK@ z{py)tee!DH)kUu!c=f|yFUrY>+e{IWaFTM8V>z>zVzP{!4=<9Eu)}O9E zz3B9+(^sADJ-z+(ZKwC2K6Lu%>Bmk#fBMwvH%`BI`ftCf`_1~_oP5Lg#)H2t`R#_^ zzVK$#n-9IEf2-@Q58ht(_Cvp8zuWw~FCyu;2EtRBJv%egzLs$x*GXL0qveu@t6*XE zFtwi>O=@EgGo^5Y-el-u`T)12>McEmJj0xtF(;j;GILt0xre1Ot1Zpi!)!&lQnuZe z-NQ0jR!(Nta+Xuf^PD+(JWXC7(RoVFJ~%MeVYWYA;j7d6-*tn ztN~?1KhoKGJfqNK^=b@`$dsMAWBhKFbRsJ~$2jrK^^=S_>Dg-T)MlI0vbEfKCYv{W zx6qK5k$v~w*_mm2y|mGco`1dNMc%`X?b~^lhwlufjNn?e-7Kl3cvJm|6l+vW)629> zQfZ~-Os%eOL%Tz5Orzo5aB0T!c-x01Zs4~^vc-V$fiy&C`Sf|AM|d3Kjyy`TE_;jW@EY~n=sjBt`BHIQrE4; z#kDoX)x}j6WnSvM!Qd$MWoBr-p29-64j+M<>QOv^zY)jo0veP0-DS#{tlB+-v&jD4 z#o=on4q`evx?PAlDTE88)oZj^-i+(?U}&{kwGix7+h*W89qXpokD_Llo`(|4myK#; zk`$8KJ403^-%409EAqL0wH{lw(U4#2v!kt%X?Fy;J95PxLGRZWa_`g$R!cg>N967t zi#a7l+Gw|?Tdbd2)6=bIAL2_Q&)zYzY#QS3YK^4sL9_TmESuUkE!SvBU{KTY^YeZA zUY{4;)`1&5d#)_)#gIyB)h4utb%RPQJ54t|)u2Ikh!wH`JrU3uX&AXKff$-*9%Ev|jodo6HVBtmhg|!YvL(JLyB<|ucjaH16FoLavo&~FEv?|QGTBA~m{1l=o*zPPlsV-$rw{u6T8W+GH+}pU#r9 z)cVeACqzEllZEsh*W~+4RX&y1<1aUS{5x7hs%~+* z&77Hi<>~bFAxCzE^o2=p(5vKT1MJ=$2k(&+tLn>CBCk0gCYjEs}bZH z#oN=;7)#4c%dlCPnVD*|hOE-cK#c{>6V2Cpy*^92+iR&>ot0(#$^Ab%ap))f)S18S zium{WW~$f+(yORb!hcLUh6HdH7C?PyjD|~EPMUv(SoM0mF}CbuODj8@vFyU^fZtn? zpO=frGJRew<($a(_PqqAe@ps9WYe8F{vNE!SgTQyW+AP!QDU9Bn9~gjP(uszu{-zC z%9?|fm5(+wvjY!9u_RVj6;jwKCSwXJ>swL!g-oxnZA;}Q6I-YwQ8Zvph1h^Kl&C~m zOW?VrI_lz;^jl5nBwEune%#nGqibut3Y+-MvGs(p?ORS>PvnU^bG(uvBg1+?g9TOE zINImZ8%#!x$^kb&JuStf)~9mA{>aM*?BNy*gj;Z^#WmS(l`gkJ*ri+lDTJMvA2Bwn z?JS2~%uYS*<_1lCku^aMb1X1!;VKhXtpFROrJK`wn8wHrI*nlkOHDN`#Nw)Lv-0$G z^}-acR)uReeX>AgYBddT9O(DS*unHMFfikk5{lV59z!jy&><|*t`9FZjV+Y4K}0gF%vtvR2b4j z6~KZ)>&&RWfH`e`ugy_D#y0^QIjJ@PG0pBOh zCb}1lB214sqm-5eTxvsZ9G7d&b?S6li3S#xk`^NEqMd}U}mBZXHz}!ex zajF8$q8MRG-)ZC@@%#N=s_@oBH|C&1&ZUL)1ZqU(R+jGds}0$a>ieg*DuXR`YqWrJ zpxhBEP@`7mq1at=PWmW1LlziHewQ=bu2iK%RnOi3D|GUX!!FAt>3QjS=3zZ}lZsy; zOW%!`}e&`-}Y5;Nr-L?{8WaGr#S44%M(;4ZP1m zp50KTF_s+ivDkv~FgFIsPYZ_GGc6hNsR`T}8U4VeZLznSVJ61Mx6hAEzAN66d_!S) z;sY#4RIV}P$WNz8SrNkz>M?h%KRB==R(V#eE0|$;e3-&+JTz8#L*%U{6~o6%Do!rx z_h777e&+KRR+AWrOn-2#Ws>So z$wC@FE?OKRmr^WAwIp%`v-c-~gm5~Y#m=GDh z9I4T$y1|MVme>k``RpqSWcY1f@@6kPcIT*RUaq!qjWm_1B(x05aa&xc_kVRT7_16b z7JEy*LNZzYQ5aeI&@*^x*~9eeBT;+gvWGJ#)s)h^SOdoJLl`IdVAGmVWn5arN{Sme zI&iaQAzT8*!U_1G;qsXS8o*l(e=PFLhYR#JuSc7iVXumO@vu95-J|#4f83hxiK%%{ zx>fr2&h0nuMBQ`Xeb2?)tk99`6>*uX^u zQ`Q->*=&s2a%|aInRuy%?6T}qdw)ZcbTg}|_QIIF)ShMg-^;$WarJX{8^8Xp#~;^Z z*`#$=`>Mr@SO43V#aG^Z6vrNC&rzc}Vu_A=29@%re(>tw?0mxA#6(~3eOU_pccaK6n_D8qO zh%HZLs1Qnm4W)^>yb@<%fy6itE~M2TO7nV(3q2*?k{YNKogw$wUmqR8%X`s(Ox>oW zR&bu|bJnnGZ=lQrpVLulmv27Bv*k}lb6aHHi34E7%pW(01ru@>goQj7zULfv=wWq| zg**oKvEG!T?}Z-;?;^?dG?|Ru@H(+{3uo5_nEs;YRGunapx z8ST?1PaHq4azwzUSjtx6c_w?E@=d4UQ&Z=PvYb+;G`4>AMDjj|F_!hAD}0gg319y8 zomIoQqUxYKa{mc`WbKc4g=6ygL5^0DYQieMwrdmg+nHK)NGNF&}ee6jLI=oLiR0^kNO>{A0G zJ@K^}cTO#kDUsqQd&vJ&gS%%y=+&|fA!8}zsu3(sF;fb!xD(DFF|mH3fs^~!Ka5g7 z(B+GTO_HDJ$FVSD@V06+Fbk0qzt2e1rAQA8JpXvCWC7%$2ny6zxupg;9j{xorZi`8 zTz?PGY=Vbp;s)dER?tXXdfIBSSie4;ol*`*&sQ-ZiZUa`D$OQ}$iXKUkQ9zz{Z|0FvUTS_M4F443;W4!O4V)!PLbap~Z?ePYPuN|c< z;{H72*mr(*P?|ao?jQvgdtunN1fk7e{1a_uQ$4O-(CDG8( zFsWgpzqk-?dx_tYVknSz7-2Z?K6xrW42@(gZDj^MMG$B#$f<>Q2L)(UBb=mzs?w#b zK&ZgjZRSSfxVAKea1?DA(!{0Oe_l&_Uo{BiIzs<>4Sf}$9g&)s3MDA;1uv!WQCNG* z)S3}d(d!KQUL%)GTxXDUE7DTnl_GiBTHW`rvFS{i8l#hR!75UA}f3Ji? zPEH@f=ukl_)wnj8MzED*)cN_00Sr>;b+bI?@cRQWWg=gmx;8j4WIc{VbYj53nL$?S z^98cVe2Col%X>+bmY*HRU*P{>ZM^(pL(EX1B-#0c)Tn*vAhi;v_y!e(6E!sSm(Slc zPCjfCzFC}nYEsBOxvFHzPwNCD*T$wzYHbQ-AxuM&@~n8I90`}CPnT zPP6Y^*2>%!V^>ZHOwE%o`8m&VW~N%~QedbMUrXxN9{HVb^X#xBX6onCXK=hO<`;#q zL>SLO2tA%EY=Uw(aAu&uxzS`yHIemTOyx$1mjMxU^mXV6x-xS^)f7H&RSo@v@yZkq zRn>WG8W`4}j-G`FbZtO1Khe|vQT^nO#(hI%3Md6gas7+a=g*xzvt!ESrdV7*f7rOb zF=Oydhq*^@G;7ycv(oJuH{bHjXWg2XX40jnNVyfB$unk5_KeI)$DaHWaF=Gk*j~zg^*%g$HgowNHi5WkkJO! z@%&>T=A)$$*| z;CwYha$OiIpED;Xhp{;q&zV23GiP?rg)=**w@sedSU(oLF`@!Cm%G4;xygbjk^%qC z3Zq%cLfWe;N7cr%m0GP=$yfGyFFX)=`i%@svW~waf0Qm|7=E1YZJd%w`l|F@t9PEx zu9jMAMi_Ld>6zol8&fQ)I{k>6Gi6qDPNq*Yv=&R*a!JH}+IDR3=|z0nqs+sR@L^un z3;ZxdyH52KKA&OwBPt$!CQ1Q}vu70!Rt=_ovL9_^@*8=rU8#|V zF*tIJjfsICVghYoiI86dZwhgBoE&CCAeCCRJTsE_WUl;VD%V6FeRB>;FE`k{BoX8w zAqs)2Rx4su42*zV2mwH0BwSxkA#&uM%i>zbjj*kbtMqD&IU$DdUVS$OF9yE%JcObJ z=b=&BLP3pcW~vfAgy;Z*8fZ|iyn7|ji@f{p7I^r}e!r-W-k%(drRb1yPjTxLUzJd6 zaz0D?!+kDV!!4lksvhSk6YJfWwB92>`$G|Lj(qutYbg<_M#e+aD}v>ul|(iG%NPNQ zMHud}Dx`nPgYJS%o7u!tn9-+0*6hf8yZNNZls^Vc+c0S3c)K4OJUg=Sj}5qe;*Uig zU_$a~EQ(u24iy$2X}s8a8&DudaKJq=Vu@Ut^G{ik$?tbzfDXSuq9sP7%HfqfCvyB*^?%nODf@fr zNzC?AWwxc!kzN0*Bm*fAOVXK=fta5ml7V(58R+_38J~73>R5w1Xx)Ebtos-6NqSyA zK+k`z>Z5&P0IgyZMDF= zAgx$XKv2*UM5;tuapgy26(dDyJT+PoT1w*w8Yy23Bc(s>ld+D*8vpUfxg+5aX$Ujf z3%gLm*(Q=`B;_G7?hrTAxtt?Y0^*6 z+x$&~XlcHLJtpA<6z2^p z-zX);v?BXI&6nT+KK|)X5d9*YA^<6p6n9}UMzG*SWU;^ZnTUSnMo#{7IVq5#0%zxY zeC21Gs|UT6cjJY^ZiHpj|E=(O6E3g3%7g^mq|ay7$0R}u9uu=l45&m@61Ri|NPH73 z{~>=VDXY_;Z{?px7JN~GzdOX=C%*6_qSrqmh_d@ZJQKT5qYL~jw?`vg8|N~QSH0~?mXi;KcZoWp~m6u?gbqw~RSnW`wu(EwuV zQWS(&ia>ZF6D2^ZF&rk^LLp2sw*9gzQ^NvE{~uH^Wccqtcz!^pa#?7?z{U@es-)oG z5M7B!Pbg6dx7*q_by91n(Hn!vT1>)O=<}H(mVZ@xcywXMxx#&usv?vB6>v;6?38TaL#@NOyBheikk5b<9PqQ+3qF-|Igu;3Dd7#V_0WZKS2q3C(C`^K|S zMOjh6T|m5(U8;tUNKRtpqOTs#+lnxOW*hk>9FZ2z$FO!A*@(rEOp)Nf*Oek_&kWmi zFi;V2Yb(rrVd#F8WIMN)O*%@uyPNRSGAy6|O@djsG$QvUD4s+?4W;q2*YcJ8Ro z$#HZ{Yqr>ImS(%nYQHSqW=p@Utjr;0>r)K97OTaqRfW%FM=tw!K^Scshc@*B2K}p0 zN~=>!F-}CRA|6mJXF9D8aAZwNig7KV+Td}XqFRX{xkd}OMXMbn?BcjXgbAYTV@2XA zjZq-HhkcAzj_|qa`#pld8Q@Q(Cn%Y^2`@%sWWakd`SI6+MPo?r#)i7l81_ODp{Dq> zTEMr}nAaNlW4)9enf`B0m~xgC5)zi&GfXPoSX?y30D3X989=KR4}tQYV{2>t%3@Bl zUT(|Gv|VmZGh5D%8VRc~-(rCfFIAC8fA;K|vwV6bhTB7KKUCi-ez{UWtf8t_?!!HJ zo%_$8Am>?n^z4afP2{DoTNBn63@y?Mcsh{|iLDIVz#_%k8hE$Q+UoPdj$MAHO?ay0 zBxEs-q}hs8_Ry~xvRJ%o$P(ps25%-AlBC#?@rxO;HHg4qw*>*P0Ux(K+`#j=iUd7vMak(iDBMY@G*3c54@2N03v!q;JdC9` zI2Nwg30RSsQJc(54fA*Y< za-;no@qLqb$-p&~fZeK0{G%|!7X%a`9TJN?k<3i?%?dCSy5Y#gvN|V)vb^l2h#8jF zme!1{z#g<>C?A`MM9Fuj@>|3FJ>k4olgz=v29rd1aSmk8-|0S^Mfcfaw2H!xjc3W( zCHJRU(8Jlqr^K9G<}T3tfr)4ig^XOsrzTPyu{1t9i?yO-OCg<}A<~AwC zA-`tkH#)@3Q$0}@-P^lcMc8Uk*xN}?u`B=1e0KCD5^7wR~>-E*vR0k9w9sqdPS?qSp znR}mDd-do`CcVdl7fhe|$V2kC(|HzTG?Q;j=f#ti>M6tGNn}_oV9VHhslY`z!2$O` zj0KT`Y6L1!t;?3R7QQ6BhPtWLNmimqm@*`20mvmb9-@?>twWW%2(dI&0V*Y1jRI3y z%#J{$2CGoPB+7rW@YbrT&#eqB0}U%%gO`yZRV^Yia@ zlLq|y0rAytyYfn#UFDEZS-C4$GN5%T)o=LQ3Il?x@>Ls{M>5DrpA^IwcP`iiRuxyV z9xds*f;QwPuz)-Xi2HC9E<&~sSBS?kKvp!Ng*E%jJT|Yj%;!zf!AcO2C9QnO#-AIP zjQ3E?Ohk6zfA(OMM0~b@91taH>(Y?}GMbGFczo4>C6btWAcNnV6b=KTU)V@}jfd=y z-xTdl`YxLIh(F+Wsdc&X?U{TzUnHN&Ofu>nTw?@+SEE8) zpx+&xDjpmtg0Mjv2ClanaT8>8usvx7g;WdLOhP~c|D@44NYVx4q`E<*5M^;;>L84Z zM+XR_Q!KfboaRvDm{FD3s8R@9I+k=v#p2r+kaFd}*m>0yo3U_uVO?g5&16&C7mVds z*i@B9huN<37wNTD`D`RP>4zU~}MxXmm(Oh~)f8j8#@BtOCK*$0&o#1e*-LEturE4w-cUNDht&gZL~!X{95`C<793MY2fC~Q_Kzq^>*6IF}w;>K3?1?0W=big=(i_KE`9TqGLt z#$u?|zc_hI7|K{h`%h3iay-&PLb^Mo_KXaiEbGX~&B?+6kQM;{b=mRllCah@6;vvA zppkwh^|~sPR`Bu5*VnC@^jjX7dDnvvX4)jkf4lAPE0#7b9n*5b#GB9jhHMEn{yZf; z4PoJ8wKbtCx+S(I!`_dnCa_k2-R{aMaORP2_f~sJx4&D!b8F)7{yg(hOJ=6!U7P)T z;{7jh+XZ*+-HZ3nvZbflGHriVS}>~>Es#gJc;0*gD^_H&0j{jntyjZ@q>ej6Zt%vy za%`MLDXdks+5-Sh$l>wO2ziP2-PX3^y;- z$2%|gx_Q3(pH$slZte$A3gS`q8N{O(c(`9yb%neyq>9vmbnGyMV5APTS?;sX7g>|pRmIo4>-GW0A zG)~sN6|5X+Vc<4}?lpV)?Rc1k5ygkjr?|^lFd&|Wv6C;q;pN}s|B^TPxHoQzI8cQ+ zvw=iJd|GJ0zyL9ydsq?|jp?wZ#6j2sfiA&X_ag&OJgtl(hD>((`KE9W3^fT-lSDFk z5u7sD$wJ?a<}n@2q@f9gCO8j`qHF9f7X(zPs|2zbFnXvuugxeyP!$AtFJQ`63fMzz z60Ri|kuhb93W*9#Ol}+E)SNqn?KMmxy(W~7JWIgqW{`vp*bR&&d?vuXhyXy;SILEi zykOx&8oTYQi;iVxrKcF>;&MaEijKvXz?!_#W|uZkYS}>|8z;KG4AD`+Dnk{1&b6rk zOauAOv?$c_6%-E;CT*_pv*5sbF}VJsbRFjSzCwOmWe>O+AUUm$HV47D#5#xz)Ifl- zKv^JIS`0VGMk#A z|4HlRQzhIPUV#Q^Aqllw7#qnN(~;QxFn~lT&S6UxBa7C7k6~Lag-mH3DEkWBF&zWt zLTC!Iye=)}^W&@2NooYKQv#32IFW3kxVnoi(4nbZ398W01c_>}OwDK>^sL8M<1Z%l zJt@eqrAAQl&5tIqZQvpBok_3i5w=c&*lN$ap{&6bVr2j*q8^qG%ZHPij%gO!N#D-e zTAD)R>g&i!@%r61zZsqi&i)WPwE{Krq%!`SFhWH)c?gH^#eF<^;{GHy;0`ymwAfAAziG|H3Wp>h4+MK0>)1+CI+RkYnrLjq8*N7_}2OB#$j#&akdWiX*fg_fp3e#i75xRVK@qUy|OBXqo96Z!Np&o)e4?e#C4XC*P?+qb_s{w zkn|;?y`hRnI)|L<|Bf9>Sqa&|HmoUPhe@@(Aad$^2Kl4KTrKac<_n;CDY&lq1!8|t zvcAXIU_0R;64Lid=N&YBLy=G|p@KxI5akKc6@L$+I2K_TP$0(ZtFrmY-s`R5*G%7r zW@#3}0;13tf;dUb0ZWFK!+`SsOM#>;Y_=NFbE*_Q-xyCT6f>o!tY6%h|d=o``^NFcjML6JG-QKR@T;`~*DWR6y?5=SjiZalHo6TCp_ zaJ&*yM#O+)BK#XX0f`oWf+X5dPt29Ng?BsXB|}g{<0dvjCpC)#-ip-*_%YDO#a=s& zW8;euD4AYvp#zYNvS|#TU$hMfhdxb-U?&VY5vQc?F_#yzNT@n_BdSH-5n0a|ycQYD}<^kZnf(N8NM2by7JT~^1xBBgCMpye1=xwCZlU#IxRF~+0lFHuM}DmrF7c##z5-B^n6AO`V=Clc)#?J#7LG+5 zZ$5VV0Nd)rDktmy_x1dSI0w2Cl3cHYF^>tBBuzJ3l}FmGPu8BBPLXzWBHOv?VlofP zqe9U!0H7nmwj%y+@kZh+>NoruTM;(sXNX2;H}dsi^pv7qli|*}7|!y`MJ6GI?9Uj7 z#Lx00Gl*i1$vqfcCRD`W+d}Ln;uydJleq=A-lLV9#&LUvgp+n`_J?XG7SJcfU*=4| zavMVFw#W9Qrkr_(?n&3s-_Ns`UH%g}+Q@Sj#nibgq0RwLBVcEIuKL6`oXVIsA3)My zg3$=k1}+M>DAQUxj&GbeG1S7iUeePpW}MyvfNZaUHvcmu2!)MKC?m0qA;sWGiLh-= zb>#@Li@*cRz>hd^zWk4Iyh3M%ONel{TpWUj{p8#g%dXJzd*wMH?hc>1dHbc9yR5V0 zRX%JiZ)I)l;?O*Xw0er(0J94-C2g49}jrG(RPnp-_CUcKzy>0}-Dr?30S_V2=i~b2u(z zLC>tc%Vy5XwPe{Xj-F+=Oqw0JNVgJ$B;Ot4jeT)8c&Qv-s^ZC#iJl1L(+?Q+FB4wO z;M>A@JWf_zF~!;H#`9|${s}K|1RE2o1(Rr%6@khIYr#01fq6!-im}*mpINAvkKBM1 zovw-Jr`S{@HJK)Jrpi|+mpAdO)P@NZYL29tt!X&fZblQ&lULzSxQBd8AuWU%tnDT( z?8J#s3dE@ZJS@(fNRSCTyN?XIe;`Hwiek#8kOjy<$;=>E2(qubb`w2Z@KnBLXs1wLQw=y38&kSjjPKxQ2iG4dxWnYX=yQ_tlmL-j%QH_w! zJiCn&K?vy|q^ri{FS8k=MpdJQErYpK@f9(y zfrg^hqC`qX%mnL1m`0Da@XNxpP)#MfMH!X!Vn-%c7luPeV?)`I{f-$w%>l}v*-z;A=A;S|0Gi6{6PgmVCkk>aAW8gX)%{L=}%Jc-W&^4SU8 zVM*e$RT5H@Uqu!?kX4dp`}%&QVU;sQW#*wA9ZV)B%@kQPb1Hjs8hbIPu1{4Qh|folR% z924^mqnx8yErM9pp28vw_O3R5z(;3`q7xL5Mkkcb8BWpn0P{n{`LxJGJ=@0jkJ*}t zcEKdX!B(`Ma=&TNt3rOUoiB{rO@-88jdm@u2^1Rz7~tuMlWb#QFf^zyWB!fEMIOB< zF%m=8oE__{l3zCl-2NIbFniG0Pfh2yFQk}*ghPy=U$Cc|3jwHw<4p>|@s>s&CM5eDf_ zI6Q>88@i(Kxoh(DuPM|%s5O9&sWq5rI;%mosjqR!)(qJ;ET3422?(CfPFKi|y~UGS zTS5(EYH@g>z~%WwAS-FhI1k7VX7FmSGY$MkWU)|u#-0suv1bz0I+l4g*@QlupD(&1(_M?R-Y63 z6kC?ZmX%?(WS-fUY2h6YhyE%5?LzL*N~INV&S!GkY@Q#Mwu*i}$q&KwFaRU}=-IQh zX*0eJRr)c;LZNv)P6KSJph2hXXTOxb3%Fzp+Y(Btut7tnB6<{;a(#%L6(W7@9t7xV zAC5!_>fs6%Nj(IjN1nb4)=1hF+$ERK;gR!WdXO%UV|oy^!Xm+@0(fH8RoK}IjXRg$ z*$>j=7EzKuHzt*`w8~A<;H2-OjkIabO~{aU=kn$Le+WBnGWNoLCRl8HZ0H~A$6~8H zd1)AytBp36*O$l8K0a)aR1>y`qFnw2pa<|A`~<9eDS7`&tQ;uMIIu@Civ1{~#T~~0 z#UG)-dJ=J{IEl_lV4?nmWd6eD5D*}_b{==Du)A8`ACR);we$EyNs}%AcpkrW2?P!$ ztmxNsk+W0Max_9LDZVm9p$Jb5abLtILIx4WFN9KtWu28J->inQx#mjzcH)tX_@o82 zUU$USYb1FvOL3Fg&VI??<$q#894KwGkf5Vnd!A^Qc=dpL^x8*k{(3J>-kkXyJFpTs zqojj22X+hsU{I;3ey9SaYpGy51ZVw$fS*jaqKo;Kxji&o?sI~eVx$3i|kEbYcUmFg^ijXtq2J|tZ#{Q^A-AVwKG@i73c12-cD zF=}PUhPw@0qzOSmR-p?9uL!j`Lgyy*#0dd!1IXLOSYcU1VOg-$6>#BfC>lk(&1<)% z0t!Y^$#MzadF`m!evC{U(+Ab1qa2+?045+E>R^bHw1_W{L6o}0njZq9l)o5}k$V;aoitmZlfZMZij!*; zVcY~d$-bCx9e26%YBCbKXljJ}gx?2=LC4VTSLBXXN&1>ov1wc?mCxfs7$B6h@}>V*-dIPI7l; z6A%ffxW|A31TT`pS6s|Df+Q%h@FAdVId zfjCMo?;40Wa=WRA?qW9(M>dzOAU7LGW1LP*h@(fBa*r}-eTrpQv8RJEf0>^E6O^y2 z_Kr0`dhJ@q{l$s4_^oz-DIi$vtSw7vQHGr#n>;a9CuO&cA6+NEw+zuj1)C(mc(4x$ zo3v;sY*I2o7tCr$5;jRRCg>+@lF}Fjnzix>!DkFenbMuObB zoXfB@@XIS2t-2Q)0J(p-PRYk=qGoJNApY*FB!OTO5&ygu{CY^_X8FMtyudJt9R?6z z;upwoui$n*E#iJRBMkc;K|gUU!a%*#cjN&b<2i^>sG#%EDB6g!=2fgx~74dk9pZV3UZKzY#X6AED4IgRx2dXrN}~YcD~l$RY2&gl{B+*hcXNAhfYM z>GX^k0ttQ$X))9ZI8~9)!yxq^Y=vEKVhWRW=DF!aPY08f45$n8 z8hg|UOI}@B9xN{Od7ODbvZfn!c6s5Y{9qkUbpc?BlpIM-jswccaG`~w#rd9EtwW3B z&yyCTN9;gEUL#K)x0(kG2>~UqUd^*LM_a~??v!s_&FkZF`XL}mc`;m*=)*b6$sizP z{b&TLU^oZ-H8={zh&AE9j32M1h#AMx*pOTzVkNbg_`QH5ms*9|%(5Z4J*t)>rBIzJ zu_dffl`f~P;da;4{3nLA%oAU>)s85x#pyY=6)>T+Hl50W`amC+A? z5qpZT7^)O|o0MRqk}DX7N+b*jm=hX?AQ7Kb6eFc*Yy2hI6e5jY&Ua8FD99NAhj4az z0+u8o-ViLhH^aKy;_Lz?wro%+*>Shz0%V!~-%m>!%N*gSq*B(u6158CB574O;upy-(m9BPG(ShVBFR%kD#=zv;b-MnuH+S0Az~Agab5*= zWT1ne@zYg69VrP~oPa%0xty0&H0ZokO0&|EAxMNk+I2NQ7;jV@Jw(QbS|E@V4-JpT zQfRf2IfsF?BDI707n7&fp5X3qYX<3iJ}4uL}v*4H*JIh|E%Lw*dzQ6AGP<=+YqFPl0LBYWmTVOVdXxv{J)Z z(noUBm<`bR17voMp!7yvnD6M1&!I&rq!DT>Pr~PX80I@?2(L~--YB8nxNoN* zZ`8Wkm_d>-lm`-1Pu^3UNXhF7k1YIM^u^|gR?JwhoC5p+oVapX^Vn%f^qk)?#5_`0mU3N4A zQ+jkWU)>cG?gjD2D~7R3C14oI(%m}@MSq^A9529M=x)5fz1Q*U!q+MedIYRJ7tk46@bcWZ1s7JoMeG4y1 zpjUpph5N$mCxk-q)f~D_Yc^RjGA%xT_tjVTWaeBrr%Git*-Uu_eAV=h$%rc$kfdtI zIVkn4nf*gCA;vSU38`x&`!q%kGZLy;%w!6Va)yPY)2t+8LQlYA^vP%xCw3q*NKiNQ z)2V6?Lljd~k<0LV zN?al#r5vvXbPXNuj+E3*Te#CD0SN>wjZ~^LrCUeVkM`!<@)OV**RreSC${jMQGcXC zmL`NQr$P0lW!EM^GGogtyqZlb2aY~(?U+1ey6oD@>*Fg0d+cE~;%77nJR(Fv+|-My zyuk#0#^T_z_`~S42%46-C}?UYxDe1JabEyoD*3lt`M2;(H3B_CD4IU>NbX?y{vQf| zz-{XNNpPE&xACnCil(oJeejqeP&9%}^s|pF)DJ~7L`5;v9T_TtRS_Es6ci2mT_gYM zdVWP(tiyzzp$^L*ujBUrgK{S)UO|L^xg+~66mJ#9XZibJ zWO2xn8l{)s2-p-od^+~<{^W;6dP$^?#0zeS(R`$Y?v)n^nmjb(hl1uiF`Dc9(?GeX z0I7Kz;r&V$3N;iH(sCL@=tN>8p?wA-VBnI)^F4sCqQg+~0Rd+bp`0-{@teX2(QG9( z^8)Xenh{M>(l^vKh34@%O;WSz2;#3rvm>!)f4_hCMKBvQki2OKdgrr-P#vVPS|h1@ zvismObVNoBJ_Ci91fOxqyKd&=!~nn3?+vP?R=ClMABk2RiqTxzfAEmwOT(EDpGwqY zgxWF7FYw(8P5kiI1P%3Ap?N$`6Ha=WQqd;~qNn3TNi=21d?~ZJFV>*V{TqbYoP2g8 z-+|f0FDi!$@f{Pn6w{<2g_5lLT+DR#%?<`3_!}L(TkNtoHXr?Xe+|r`*CF zNdO|@?+*eH;df4urOr+Oh;F-u@9PI3!a5!VAQJZyf;RymT6QbnI~YK;oPzuAp#Vfl zC1l?82M|sAAAZOA0YpN|D6uLfor$;%olxMjmr$hAwUggliPHdhUwA?!#xt>C;6Nwe zC&ZKh2(vKX5Xi-s7V^Kq(99EXdz?S+&cnH5H%L^m?mo|C)ZG0bqKLAoALeLXSDI|MK zI1Pjl#RCaM#$=c>C2L|JIUc}?7kP61?R?|^!S#WF9}DfLz=F=)&i8Z))L7r3_rVm< zA`A>10pf~20x>JYl+ny2e$Wy>p#`W?uKdL=95;gHl80lxeh8^Dmq14sCV`G9+A=g& zXeg)<_$UTCa=>eVVM|ePAbVhv{D+{UzMB4DfsT@EguaS5O@NL%+9ywJX=)rh3L8I% z0UgPc*YR3cciXuDNAnxyy1Cr%=xEayB8EKpr=8U$^6_=t3&hcX{~IFc$Iyr)#T+{S zZ-}_t@XHj+Pi4FMW0sUz`X)8REBNm~PgDASwmZ2u6rS%^Bmix181gqnVz4IywI>}Y z`2Xi`h~SrFq!ZFlkvPj_1))3~G=m>#1X~x8uo9`RV%DBnSf$eEDDg5k+03K3I5Zw7 zfvMcmiLCUmkECa*xl@~CPR-GB=b1vtf?dI2mA19xk$o~jHP7&J^G8%D8R+=z$71u6X zc;O`!vSi(evanL>dk_dP#bFy%Uffqc0s>@EdE$Acs8FriI;d`90dmygwIZX0T}5c@ z5AFb<9Rq}u@jjQvLEQ)0Hl>Lk@}HqJ==XRjIJyazF3eJby`tTmmkJ?7-en%)TrkEO z-a7QdZ`hzTJ^I;@&_4+*d*dBhPpTbWAe*SadF;#{vdB*qgGkqKP@-ame zP@>62TnfMP0o8=TrE_4EpKQf1Tp|rgKiMk3wU;mR^`ajt@|Ecou`N}JA9oFSOR9*n zXCL1*X1J^(#cFlk0OQmP=B4I_SUbI>${WDXJ33|OoxD~80DbnIyi~ImKjl~>Z@Ci^ zYtaSu^AJaXek{e_qOnMSIbdsD_Am|>7QaYOBWuTj6zBqNv8LUQcn7C5k7NGmP^IEH z>!Awpm@>%u_*D&-8vweGe)3Tsxu55&+*KBN2H@klXRi48EnAvr9S(lUSeJn#6!~wg z_KY**`9<=z`+0UF4$JI!FeLdxDjeGJ|F!q+fl*c0-e>PKbLNqmNixqoA&+^GHzAV{ zNC+Xk0)$6U9sxyys7NhROKEE!n_Fs)x4JdaYJ!Ej~g4QBkS2)}m5tEw$8g zskK&-qV;-_(&YZuK4+d8Nbvi<|GvP?th3*HpS9QCd#$zCA~vp2Y=^0Ipou;V=p6*K z{zkA!U>yQB0DpHk+c}9|5p-S*KrBdiq0dZAX0dprIuO}qYsZC|9b^IfQ%2wESSCB(mwdFbzJ zNR$xj4GAS7NzYFM0bwubtM{_*5Dh2kqR=qGY8ykobhFn`;nd7tt2sYlQ&OfukvU#B`;4u|XZT-ZCg z-IL>XvK?kC&^f}!;T>HwIGq=Q;)SId?NjUNTEVc`jiXAUV`wg{v-`2}3;4tp48N{6 zTc(+@RhRsb?YIcG)0GTenE^~zVb!J1U#nSlDGveL(INi>?Dk?>8#Q;>s9%UM@vWge z7}-F1pZd24*z}=zPu7GIr&jFb#|G*`{!o>;&#`>Bn2Q(0`;?a`Kf;S$;LApVg@U^p$w~te$R_oM7k3l z^_70MP~G|y*0f+je8r#!C6WF(`4eN8DMAb$L`3?MKq~_Jm%E3J;th>z{T}9u{CU&% zVtaNt*Xhg+yX+1yoO4{xPaefEVg}U=o@a~M15g3{3Vcx01d5GH>MB7y2`Kcy;Ztn2 zNF9|6r!LnP7d^Qc7GnQH+)<8shq0l<^wa&A0sq{Scq@U}3WGLcFCbqZUuohqPP~s1 zq!U$oMvn%ifqDKwIhN>REbORPxMJ6gq6p);K3t7@|6#UiGH#AY$fgthY@FJ}fhd=26k z^Jfw7kHzctXz@YBFBjzpV)2weB0M5d1pydHSRx7t#uB6yFd7A1DGCV1;^pK9PzM%^ z_%X5glm?a|ewl~|8fhIwyq`uwYvKPNDH;eILN*Nk9UFvK!HOL-=aC9vD|sns6|0^w z!;43WZADT%_3RD8U1^@WW*@6lckY964jseuY0odQ&(ZCES?rzH&@$Lc1~&63j%^zn zrHNhzXI|VDq}}Su{cP7%Y`35~h4Q4WXv;=OqF|_=RE`_Q`CN2OhKRCk^EmP>?{kkM z%Vjvlp)lnx{MRU}fy_Dalj`IH%XWz#jaFLYYLZ zbV<$twdB>u*`gj8KZ9}@?l9skp;I@fIPj5KG%ZFPKhgt{95ol@<zLHi z46CsIQW#f&9h*`o%<(gmW;7B@y-@R0v!7xUJmseGG*63&&|ql8PqGD zVl_GYYVWCK$NuWH+j4%8W%uQ(-+hXO7X9_FG$UHT*8h=o1BPh2_U`EBfWYx26iI}| z9d*T%pwZT(x!^qnX)IJhaMfCdDhM`v#Js}WG#HGy>2EZ`-H48OV8+l+O}C;k^u5ELdh@1#~b))1Bw34q(g zIxe;sPy{Gn?ch#U;P&Mka^*bsfWv;o>dhNfaHNn9PA!ACNk0O=DGN6w>e{P;onRvv znIHRQnM3j!!*iy!cC>9&5^|q>hTV{jbB0D;H-<`yvR?Tt>r&q@=efqLFcE>LpJi?7 zq)L!6Y`0y9S(7+`;1;l^cS&rOro;iHMexb$q?&M}KMZ$fqhaY>6%}22CSufmTRErhH`Exd3eY}8Yo9vP{5?}l| zo0uB%iy}V*kWqyO{m$l zAj@NS8ag@*PMalH=^n7zv%c=iv6;kAo}?9FW6mTM8wFqO7j9@M6P{-&GFGdX;T*AfMbio3oo#_!>k+Qe#yE# z$dA>N5PTzY<7QEXtoYREFWD3ll#CucB|X7kl-x8I90u?>V(V=zhg))uK@KR5jd#hU zJ)tkXLQ}cgE<>9-%aU!l(IBtqS&BQy#ul+|*nG1f!eju-Eze;`PS8UW2azaA{rVm;@+QkP&*#f6F#QDQ?;#_F?fD-B^Ye3p& zPA{nNgV9)CSpp9?>WU-mRki*lR)IOY{3TXc+67AK!dm0T)e1%OkG2>MCZ)b6B+D>O zj<`bgs+X7#sULocc_Uq54&zQEoe=VHo4^chJfyn@Y(3(*a5x7QMpkHn_9GtT(1e@u ziP*Vh*oS*c#9 zR;AVx7_dPfsao(COWd@I7g=*d_?B$qT7OIg7 z2YrERzDEfbo;m1D|+ zux1SwCCog=&A-*z5}u?T3=}-edNp1puq*CS+MdCq^d)5z?M=&3I&4kq`@d%JMIgLM zzdVJ-Y#Vts+z5HiX)S*Ua--suPuixC<@x;*WD=Avt0ro$o|tOo`>@RF*| z|1H}EW-PEw=1LzhRp)g;Y6GKwxG^(?dbLUu!Hch~u7aC$)&Dwscn-2@_hhYPz)9qT zM;E&wMzFo5I1`)xb)Xu6&TLjpEQ>`*`I_eDm_QT_S2>9BJ^?pZ$ZsOt{BZ3cLzAK#18R|t zmJyfc=#?Cf$#ZieTG6D(JhWswBF(tek~6fEPEgt+IHa`5r|vds`G4we$9o+a((U$n zm(XJgVs33Dv8%%?$}o6Pw}9?yf&E6L4X_ z79X^*h!VaIoo>Gr` z`~(=#z|-?5*s~EbDFo#rI!QxCA|)Z-En!Lk^C`=WBLpVY1rm#Pj36C)i7|ti0WR#J zVlGuTsI4cN*bym%#xNIazEtoY+ zu%wWRzs%|A*CtK!hwJ^N;c%5dOBt%=BApnIh7>%Bhe{-xvt+30HO3%cGQ=?>I z_=qPumai7Q#r`dVD+@a1Yl8N3^5GaZ@4;^XsY_2HPQ(|C)sDel!7K4Q%1cHS_;5s| zGY<~4IEm8EyA|uwd2+$=oRfhzOkJaFz{WjTSR4*Cm8Rzj=o}nDHb}=d!=CaVxIS*sr)eLfjY* z`98eD8zEJvJ3(SQAQn7`A2|52LlbEn+IDwQQBF=#s3=%gl2e#d03SHyvDT|TeGWH@ zz3;N1!Iw9>L~VbUO-$uJD0%8P-esM^9@3M+`ie~o_A>_B-x>t_6jmUxO@P$bQdlLa zuV89MUw&Oteebc3q=PxSbK`q#l1E$v;=LGgJ{h9cYQLWrY(O*0s~3krx0SP&@imyELI+k7s~FB&l{zj_Mx=NqdYWzf#urSb7G1KOlw9^}Ok z3-`BML(HhyFhPk$Xa$)fQK3;;k7VFYV=%F&#hzvj=DUm&#S;3(J%997mga-9(dt@> zEDG4pnR6^p&4Rn3dTO?9nBp-f$Yc*mud&$zQj`%<5~dQN76L|7nfk{2tbsdn)!fr8 zDCgSDYU^p1-+_kUeslxJtqfRS2>;Z9H$y@{GHgZWo9O*8j4-t5MXPaqit!nG_7;SQ z$8zUsw!CJfSt^O!t``5Dt*FqL2tWk3727l-u(Vs#DQtdzelXuZzADeCGi+4Nb?6}G zgn>?mCgmz@8pkX1N=hWDq^6`QB+Qfu1hoP|ozee1?^BCDU{&TPV&%>MAYR_*K47KR z1Tv|+5Y-Hh04zcpq&{@n2#?30tUwd8ri%x;M=$~)){u?FmGDdDPX8FE;E{ds0ozox zO$`4M8h&U$F^Se7c*F~rlakCgK4i_2bNq5x>IuRg>qP0Y_66BsG&GMNTbB<#9zz)x z4_cNC#4Tc5NdgHAe%NP#0zj5(#D&2g{1`ddY)0N)Uyq~7#QF&pA(R)WF0VE#!tos$ zO%?upiD9ENv7lopv?SEFe=t*ixdgia!b%I{;zmq}c^f)%Z67h9QR-F)|G^44UUlI|Z2Kr&VaE(44xC^bf(-z$SSk!R zh6x>h{t;W{znfpRSo0uOqr3DhOhdK@;iNV0g&Yxo`)s z@3Mx(Pm0Cs%7_`Vg3dTl;h31dA<=4hC(?xG!t-p%qdxs@wn#1ggn6663kI_q$c9A* zxQ4j}B%`-RxJD;@yfWbey|TChCMKZA)({K;*VG$70UMPWv}?zpv$&C}nAhSsYh|(p zw^BfCuu(zK&$DWR`YOSyJ*nGk`m{GB=&1Oai{6?qt)@66f zSy@tVt|i9=ZUC*>T8|q?l+i>PN`k6ttP|nSf)n^9TWbVnmQ^tlwlI|SC z%fnEHg0WlK2$5NQq_vqv(;c|c)Kpp7G`Xp*vavE;Sp{>smV!GY{JD}o)GrjJo3wl2 zE#f9Mrk^X&l^avmXkdMK!Qii9V{VjtkrSQQ^-3TDYN)TR4h5hMssNS$ z$@r;o9|Wd~*(9WWu*yn`QyQO?#!xm{t_RZxO1eGxkr^)S;;CCLltDI~%@!~d(eECsfZ!?w$z>!TrsS-o zqScp;FaV_#sP7s1gF#YtqwdF*Khgbk-A@d}6AEAH(vB;d)QqIuB(tkE@kxI4m?T{> z6#{TN#Tl>AQ2>)rF?smEoA}k(b$|(hyXJW1bbbVb3`Cb~P=N!)dr*Rbj6rY%){%yK zSPicz1B1(i##Wm71I6MJy+*@?tuk!VX^##)$y`YjgTS%YH_4nPv*8#wk@ zEZWJ}Ld;uI7p9{Mec_-|)dV+7V}B1b@q=b=RqwIz<(-}F(<_R>Uk9~=_#L|;S~2>N z*9g8RL=xC9Dc}_(8jMYvXmE8E77eIAJ43FZ8BOlA^5RA(TrB9e`V}~KYA9thetK2! z-Y@QWtm|K2HJZk3n%BLlX=d9UUp!~6-7>X((>RNfol?JT-x-%gX+VpsmR`eg?%N;-LJ8b$Xx_#FFIYT!l~QYxjR;&}(ZCO^O3YeQpc zMbS<=BqkM-@*UZ{N_{Gu=aKXnJ7$qL7!)4r-_GVseRKfj(pDfVr&Y)!x&1<4eNhfy z>WAzfxzGtvI65(jm4b=wXP)GPei4baf1SfuXQT!hhHiN-zbeuL77*GgP7I)&Sk#Dg z0;R~%PMJQjG0!pKQ<93Cm^fCDlOa$;a6~}S1r^W}qa(3mV`=q`UO}} zZxPx9Rv|Oj-dW!$p09r3kfW0nJr)oIRvdBLZ60F{BdZ3p zKaU5uu@;PQqg9^8U$i+~xw+rrMvK*UN0$Czuw*I99q9bP5k77@Or-n{zEEo)RaQ@H zFUM*Dbqh2}hU_l!A`vKeW0@B1{)(Gl-Lf7yfXzw|p@OuEwk-=`yB@t}gdJ%ynr0~E z-vKZz^3;3}pC6(50x1ai3If%TgNs88(OrpmyUTMhf5jlky4Mmg>`WoGrB0Up&fM8k zE7^Nvnu0Dbl#<98HPr6L4^AKa-rxgJ9SMrwK%WHontAT2fA;Vxp<$=5Xn;Yz?I2Wh z_t<%^`gt#}Yk}oSylbuVtd}~Cw+^FgFd8*9Cf@lVL@0UcAHDoCqPZl(#g!sOHZFl+ z7N;D#t$|WeO0x1Ap)V1B@FbB8TOV3(R@Wm z9&v`n5!TzFKJ4RHmd#EdczSW1j#k)RmQ_(>YxDT>i_L3FsLku29pMKoHNBggQKEHgJ_l4UdlJ>4P7x0#hoI&XLyi&mHCxdqx+fx(O^}@SlUeyqa z`-;cm%)Q!Vw zr83<5pgSjp>g{8BKrI->lU0PMfKhxx%v3R9IH>>(p?DfLLpCRT=3F8@?AaG|RSBEc zqek;D&jR_NRWKMNOUXmth(`Zn@y0d|YhxFMUJ1H#p^Fno$AI`DU2!B$7_R$yX5bQ~+17)xW{%b$a zn=3BN4&Nm*LYc8Uaz%1?Y6JX!Hcfpkz;9{CB0#p+vq9fM{pD+bn9^usI{N@++DK`} z{q?;R2*Iu(@2;+HuW$-lc7aSfIR}+Sj-y`Yj?fk9@`2-5d1bfFl@(ktFO=o7x#4Z+ z$`+Z28_XuV(><%x#^$p0*2Y$uNB(q!sQG1AdC5u&_wq<0h1?&bGj7eqX=H%pr za)M=L)xi=vGjZ(w68e8?Q7tb}4~6)GnIOV+e;Ml-SQfnBVBUn0>_NtRB zxvy^Em=li4u7swX$#mn)1zw{gE7u;FIyGR=$#NJx-C=&pjRdPK7IT(i{z3J-WBAw= zec%Q&3H}#Qvrzbi0Fz`V!bp%oq1_{$ToFsVIV6QJ@Iq;^{FauMHkXbsE2|4tmXdDD zf7J1I_4*3_xjV4pnDy$LaeqoG zd2_~O3H;NBN8sNUTy@1x8>&=L*Ms&ddWQ2|5+359N2rLSTnY}-KFX?%Sy1h%;U4wB z8~BR-`E&UbjD^+T&f~Q#yx%aNhd3UqyZM!PytSL(0eba?b=QxrA2&{|Si!diwWo$A z?RV;UrK|VqFKoC*>AJf2rW@Dku^sB4R`3=*XSi{H=qjEI?yV@Ou}Pi2l26s1#??3P zzjGyb%CTA+)V){pNA#zL@#^PR^TOEERQ21d`DFbm+_e8MtCLC{ugcdZN}aI(vTOO> z4s#Y&*)VqM{(o-ZAMTIbRthX;k_{h;y#=h97j0$tD(pNPlz9J}Y&lk}{${xi zj{0tjen|-gz68q*NZPRi-sR}`9_@GPS&2_KkOttK@SMZ_hn250%N}~1{nqS{q6^J+ zS$*^b_3(YtvFL5)!pI5!Hb2Eb($~Vbdqs{0v<{5*=u(89qz_`L;yuo<0@gd`zN)G%vw)Zhva7m|IzQTcd@aPUKTxa!;n}rB=OV0 zAYXv~U4cXP8fl%hLAn{JzfIaM-68Fi?w0P09&!{$Uvk*wKMejfIyT!KZOc}M`{eM4 z2Qjjf7Eo2w#!VmSzbpHPC4AMs((Y?)3RkRJ$7uIwyVFj$Yr$brpNj>JI+Lx zmQJ z|5$&B$-&Cb{$&9eLGeZR)OZaQMHS@s|X>91nX-AmtD~&xx@3D^XrV*ijKn(CY-zpK_eOse#H#g6Pkf7=8g!m!f?Y z1=<*#))FDZM;C?8#bQkYNDQ=b0Loel1)6O5I0;I6anwmMrlvf^j-pt7{KOc<-)p8p zHTg}Irm>WT{!Fc=CLQV^$kb_?XS!U(5VR~65>G3|_q9lS13~m>x=H&54kSR+mtuiJ zup}N#TjLC+)XRnrj)1$0dN1@xwPrpCM_ z!l?hIcSYbC)5oUs1VJ}E4k1>Ai4?o&oBmA4%w`=D<6(+7Mhi7Nv|p43$n3_a#9TmO z=3qQVK&ao$MRc`!97UN{itqTK)9{;{Da_o4&omJmA9N8uo4)DKyg>T}w3r|h`f0u* z7M3)=Vx(zQ&5>0!tRiu|cji^GEao*L1_NwfcOi(v%^T>O{>(Rv?><1=1eDZhG)TNU z8ej8vij^b`f`(FwA*5DosFDba9;^;AU;oMK&olO==;GSX$baO!qtDeg+IM5^{TB>^ z;2R-I-L~i*bv0~v^qD%}B*ftkH>LQ1zL`hBe-bbk@F@Y$6W;+ehp_;A59v3s_o4-3 z2gK}jzUVyaJWh?x2%WrW?EhKb$Z!=4nv=0v7q9pobmp|^5tNrOamT3C9zJQU-1f!eq#@fj;13TqNjYdkgqbs(%$#LjbF?ph{R2p54XsWlgb zmR&Sf%X{sxrD*9=YKsi1n2H%mbt%QTZiqKfK;kLA6p%!hn3t~2fCSxhMF_lAB+mrx zh$qYxdlzEwbzxS!Fza-P!uF(v?R6b=JtOKM==o#_`0pALI3(;yau&qCdf@_xdt>(QrNEI`8H#f}W2 zs>26jNaW;iC631CX#Ij zbVnK#&*VIZ*qszBpu1Bc_kBZSFCON85IG(}DG^D4`=~$xJsF1*VTa>klqEUG3wkk` z!zflOLw`XQXmg(S7pc>i5G*y zP&BGP3kXHy+dDKY5t|GR+n6Ghl(Eqf(GwBvDMO+$qT*UW;W#vLc*q^c9#1>+&JZ~W z>KX>3rPZ_WlCmHNy*tlRDq27*E(qZzYUwNAwhY9+QIDC2T0r*1^_z|nUI*zc&Ns;g!&uPy`qB?@keL4uY(=R!1WM~#q%q2I*tHc_uLEAlyx+e^^0I3&U%-aRSNTEaxwUc??0SrTE-AnbWj-H|v%!}_WSBj{KhN)Mw~k{ni# zl;S;Ufp6mJEs;4rA*FlQ`?2?Y^pUnu^x-yR^sct`pv>O$BKf;eU@#Npa|{jh#$yvu zw0-h8U|uY7Dx?(^-7%T=2fh-D^t=NomcyoL^2eEK}RJhyblm60dz-(utaPUG+eWMJ5w7SN$rUqeK!)_M-_W+ z!{=LZ2n&iHc0tJZAo4y!>3sWQr3mQBcvvz-`-7zD!z0L&lEZr|a$p|_6eZ~83qrf% z>GpcB^}R;vd~f(pM0{_@K;NkgLLd50TqG97j3_o!DUp$8PNbt^M$+g+jtme48f(#A z7Cl@&8XT8br_PdHH-aN_ngXTMg41#uoR->|*RUI--}vqux-`@oz_dAM>B^$N1Cy2tUSu&;P_v z@c-l|`3KR>UBw|cH1id1;>~;t{|p>gZ{gqM-{arsPx3?j=lps8Oa2P~HUCZjTV21X ziIhn*qxdlBErsUx zal?^#ATu08qGJ?^4<%!Uckp>WDTJ!pi_fW~G;iSf-gp{jcq_VU)17Qt^z5eby<-vA zgb#j^Vzel844%vI!LJD8h5~f1UwVq&&-S2AwxlO!u#&4r+?2w%JRrW$iti8c&E*fH zJHG0PoPql6CjM1^mVbg&r(yVr$?xFOC6iA=bDqh^r4oFffcYaPA4LesOVMiigx=~B z5rW>!L~*E9TF4JYdM%_>r1=&?V&hQ?_(`NTqdI61JPu*+dU*~4*Fn7#E`y>IH%};B zM+qQZRA2?+Jd|#yvVY-w8JfN6Kj*%B zAnWaGv#$3fu6TnnW}h+b*y&Gvx$hmucpYORXU4p_C#-p^MKGp}VeA~&j1x|p@?T%S zGKaA>U5pj&ntMWH^I2crdLd(%9m4ZVm!Gq2&7_Wx=Q6f&5o0;Km!H4R7d|Eb?@WCE zUnt+TVomQk$NH{Y&%^~g7|T7UciDMskYB(O;`^Uc2YS!GaK!_!w|W@6zJT$2r>*Q+ zc6xs0x=l>9o`~}HmB_Gl+os_6HTWG`dCs~EuDfva8vNdcdID#!UcT(&HSd4J#54@Z z9y({)1#7H@c~3FXMIG?1T6Rv)70;b&V`6qOW46ee)#t4{c#ZGgY9>yMGZx&nW^K3 ziuvMG9yY$GsZ1POz*NRMsu9-SKHB(*qIFn0pQVWXG}_*d z@uGM|{6hRj98!ehQXq3H>($%Uht>V+LG?H4`|9VGkmY2{X3I^MCz+kk zU_m~URq|OZ#yjvgn>Ft%jf%^K!>-I{c4Yyx@7qx$7I6KBm>9F~j+k)r<0r*sr^D<- zwva7lE7;j=ExUkiV4K($c0Id^ZD)709qbYIINQ&jV=uE;+3W00_6~cWeaJp#U$R47 za68ZAE?&xgJi_brPwIqU|F%x#%sqYD(}zD80PRL3~^c)>K6zHYK;b=9l%dwS{g_nD2=FxK~3-`Dzq$zqoF z3zubIR$P3{9WDZy`N*u9d#8vfSDI^2+*dnQcsVcDH*^Y*T2oy!NnbQg{3~JKX5VJH z1bB*`lsZtz4G%Zz&=kY`Rm{isn=3@ZdmbJjDT0Pcs`!;KUO0CuSYmli{q~g|bltya0@tsT=PguLm z(&JO5r%31}fIFR1a{SgaW#$^|`PTKR)MYZY8GzSO%IwUIR*(E8=g5*8aYnap!4vme z@5E<1^)NkS{Il-vmr5YpnrfJeEXHK6vaGXQB!44ieUehvXYlu;%u6ka^%dm(LY7kM zH=T+CsKk+bDX3{@Etj+RfAyO4(-G=Gab@C6rp& zKP7)Hm7gmBdp4!yueHfk!j?+fE=XqDHl$LUB={EFO}6c}>uq=2W}=!MGVc+j9+#>8 zNIf?o^)m8ax4lZaNWE!%BlV8_O`UmP=F#|VAIi*6ZC~0B$xM+>q14DDxb3nODSMvX zZlsO*u%$XcS;5bp<}Jk*SPMqeF7(?3iEsR$00&f=r20=_#aM8>o`b(^oA6F)iM=yF;AE`FVPz zOPr;AC?3>r>=JFc4}ztC%l}HB$apUJdarvE6@LZ&~G={fQkj8aZ3{&FalkjbJ{W;!K{hb=YW zicw9-S&7eD{Ed<+%4?UY38~U_Y6|jZ%F;Q`6P*ja3G~59$)&Oj!=)CESZX}G;)t~j z(MDoW!&J@DG}phYXQw(SQ}WlqTGnEy7s!$gNNtj-1SPkmG6$xvPXP`EcN0qQc5cV# zj#P^B9vP7LxO2brIoSrKUd~Jb|DA&}2j#t*`3Op1AHKpt&-QB$rElWz9a$fx67S38 zhe&=ZQ(vZ1G+oa*54nVsQZJ`cGS6l2S2`fimFNB3<)SCjaIRA1`KS$7B$;v+uS7K= znOTR-X4wUa!2?nfgNMs=jlr{%Wa$uau4%4$u9Nk7Cx{Zg`vj3i9~X|p6DQLXu66i3 zO{OjyA}?Kr zTl5V|w^6A~-QGXtx_5Bt$a$`vcw!f*48AD2S2j$kr&6hO-t(zEJksxpgZMRk9+euk zyoS^p^4YhL`itvbd{Qd$Q7UO1t3Jsz&qw zLJC#m(=#|PU7DFn*D_4195oHq>4cFfi`@YLRm+AOkZMh(#vM83N@0&Xj%TOKCn(i5 za!PVp_wnE55vq{Z&V5QMwPe^7&7y>JpYGi6K2vsPjr)A}dNY+c-+fssb6{#S00-vL z!{04$#99*WqpgstN%s1?b|Kxyb(oJE<$x)5i{!HIJCVF!WCN}o%&5GFP(ZD```U13HX8g6ZaRgIZA!=pQSwPJ3pHkqB&X3ckYbGn(VZZ zlD`Jj;>q!T?kS=YnJSj_&r^w?A(>f=)TmUd{Xa}iKrJ&pQ}8(_m7=^8Wgev#rb-hA z;8M>D&)KrMwMbo%N~QCTHnm~Evzz+WJgAn$$i?WZ`3O>Gw^Dgqkh)$rN2!}+YJ0yt zqjW9uW_s@S?C?-(VJby=i5;Ft`V||1>Ac5Lx?et$&U+4dFU#g$O{HjBUzd55dNY+8 zm`6*Jk=HB%)0yv}`27KS=?XveeCau)FI^<+^o5JW8qj6WAqo5;KJAq9<_*sCq)O8% zebpj}3b!s21r>P8s72N!>ytIf`t)lSiAue0F(d_VDYAW3-rU6BmvWT>C`3OE8M#QdySV;PyUCgt*KfdsSQ=l=dzK{(a7hrk>@btUi`^gWa_C@ zUOJUXCymteM|@WOc4`Dvvw`irPA~X8> zGsH>!SpD@g#F`b#vC4F1hSCX7%JIqx%8AO!$|*{ZvRSzbt%Zd}ScOfD5yyzL#5p1% zHi>J6CVnJt6L*Lm;z99{__5e6o)XW9XT?k6W$`ods`wxAOYtl5rud!sqj*=mC;lWp z+qdIP@zeT56^pZ(Y#v*}Rydpc*7$*@c74_bG``g<%<;d4x%iXJt2A>CGNHb!{vwsf)wcojHq8e9D#rng zs~_R-m1I_GD!KY;#$)dQ=Kd56S3l6F-2yyYqQ7;E7@feVRE$_PMn{cv6*H+~R_U9X zLPshwj6LjW7z`i5FLL)Ke{nUB*#T>ppMpM>L-O-WdFhJ!EAQ+oM!F}7^2gb-NU9Daxi zK9zrq&xPpr5`E2fF_~BDyXfl+{nPE@4*s@&{cYlf{0S^3045lp#!nPnkKYE%Yt?qq zwr}U{;ype$7n4S~yaE4+0(>HCo{B!4z)xT*U&Gfhi&CQiy_BiSR92uotsG#5$_vV? z%%gm${EhjPKBbQZRGRFdwu5&b8|O|;la5*$|EoBFFJnK^p2DASp(XmSoaRsdwsP_l zJJ?rLF8@=s1Fo>Pz*FTCeF}O5hpeKgibb(1HpQ-8;MtM@oRgKZAREOlW}Ded?C+eT zhQrvaR-Vtxcnz<@zCM=E<}>&#zM7xQH}LcLMf^IxhwtZm`IE|CWjFk~S1OMv)07|c z2jSa&RC!GKO!-o|kyWr;l-re?;q%p%tCjnd`;~i?yTxJU4mqEPc*4Q2QD!T%l)(Q@(-qF-Hv%U#4Z6|tVb%BHEJgp9haT2y46y(QEgMlX?u(A(-u4DWf!O(wG2M$ zc6Ge=l=F6Nk?VNnzv9 z0lpn>(>pD9!(n>AWrt;_wXaBLB1ANnh66t+#6@djdn^(Vh!DEy{5&;T9g@6~&@N zl!`J@E-FQp@C%;^h@gmws0fQX(IlG12Joj##HHX-my6BfDzQaeEv^&SiyOpN@T?ob zwQdr+*tX|j$-Ba??4%`TOk3I-Nz7uNVz=7^0vzHt!EHTOE-YM8h0?>+Fwe?SvwB!g zHh0)_96ih);f1;O!k#F1<>k6gD&V;+FF!Z0hvl;(XMRx+b5@s%Vvn=92e{@faeG&= zk{VuKSyJ8u!dOvOS<%DFYI(4#EZ7s^Rm|`5Rr!0c4VmvG=JNr$ke;oTv@H?YW!i;p zyH~G{XO(hQ$>ZP_9i6+f=baLFTeyn-sqz&}Q5rf0kfWJ(m38jQ9avZ^3mZ|`{~;qE z4=q>_&n+n_DJv<928x1Kd$D#yd#Q*j+K<~y%OASq_NXJ@Rebl|#qNB&U98W;Sijx$ z3hzC9^PczH`*`BMcy=SVs-8Tdh-B{~KtzkeEE{ax3L0n?E10S_bfT-xoy=n4-C)o4 zJlWkLp=$}cu^)7ND8Ps+A^X*f5#={DG>mE(RUZv_!alpbRJ&wsqsSK8?y=R%b9daX z{c&tf;_%JrCadWCx7d$uR0)<*&z_Fw)KwHJRyA|7!QyP(0s_|y>V;XiV%BLq{bzk7 zD4PDxT}@bTVNsc47Bg_S!n((w3dInvZnce846&C%@laaReqDgkb>wnqyUn}vp~n)nq2vr{)R z1$_ir?{#n+#%;s6EtY}fJ|bviA~pIW!V(f=on_+|tIcY$VHyT@B4ZjnZEZC*ZKK;p z*R z7*>~K;R`z7)LxrdsT|(Uw2vk>)psDk zO0@{J2hH=v*frIiR&KEnji4@6R8)j2f}voft<{Fr@0j$xx(%2+p(=8VBwBGg!vMd*nLJ@B_lH;?Y?V-x!(L*V)u3xM!oODMmD6u^@C%FSke(8x#mTcVh0 zk4~93M^R)?syi)Q2)kBTJhcGk;I%uZ zdW7Ap?VI}A>>&TW__OT$iz-6zfUEN%yHr5F@v%m%lVAolhY&(fS9hXL%VL_88aXLG zR_Vuzv|&8Q& z%shwl7A?0UHn$~abO-f}Lp^P5QhYqRW{23jfzZbWOiLpev)ROAEFF->ZcmcQ8rii_ z*ypfUXjgVTeOeC?AvN);+N>riY02P$f=@uw0J08KhKGp(pp#{+l%@a-Am+go6l>3P zPHhJQLaGy1Ny$N!ED{rmegm;oRiYS(8y;_D^#Q`GYEaufqr1Xvs7}Yp$ZK4l!xl8Sfw)@bcSh#n8#h{(&qlHO#AihDGANsh=Cl3O|fLDf@ND+ zFA3*7Tl9c*SZg~KAZBxl-y8|D5s>jC0iim~8=^8@ZLKZMtci^ZIB92LDqvtLgNAr={8;Ybf@?^W|pGFH=D9&X$Va4v&UZ49iua~f%*nYXK4%DP znYps+%nYMS!U^n7)M&vHesb~&Si&ju?>xh-*K2ki3k@*{D@k;}KGi@*G32R?;&?_$ zzXwf%5N4rpw3P^g)_Ka>k`CosIsfOOcA9^K-aeyzti5*1%&5U@AT=wzoA>~bxu5{? z$7Kbj9v4L8ya@3DRX^}DFWYm^?0#1(c1ZD7#L7vDCzBgoZ4ZV|H>wB3!MV0;sv zmAk8d({*)qEp^Q`!B~*m)g1RnwIxfQN2C63YI=5eY_{YHf=vYeegvLS0n{89ADyJ# zRt(sr?hcz!!Kds7pQ@~cWhGJ>_E+JNGBAZ;D?}abx$Y|a=)z#Y>UMdwzjph}o_z4Z zeMJR2mmRf<@LqIeNkj3E& zNx)eUcewdm0$-K5IVi21wm~L^}rsK?whsiK`8DWqiB9d?@adfys z+S2j-V4x-%s14S(foIt4#e05x+9=+z0V70(>oa=6d8X{v##)1s`T)spF^~T7^}JY{ zefl0?74Ml^N>~Mnr4EwDcqz<1j0J}<$NyV?Ng=TPh%^avUg1v4K5ToL$93Kv=GB zB=H82ro@l+2om=Jvc@1^7V7>}xogiK&WZ6ErZ9RuTDuIiyot5PTax_GW(BQ5Ekn^l zGzlSv_k}^Ga_3sv#b>%>P-XRXMVm5QH*MhkxJ&G(8IkXNys?435&NF14OppZ& zo`AYWGRmcEdRxs(h!;{V#02CcMn+18TCgt?jYR!&sO{Rh`^&X4=f2T^hmS_I3Dt&3 zw9&?`T_4Zvw4C&`xF|a<^U|G8f}qpY)jo83&zEc4c_oSN)t$6jlCm0d2bQcg+)A8# z)zcMP@4B17y$`P&liZNiq_9Cdf#8DG64qWIoTZycev_$hl5CdVl1B$GH|2<+IZ~H3 z>(&l@uULRf{{DMUiH;5iTL^8dtwHiuUZ!XciES2}-Oh|T) zExQ-0PgZvx&&q1*%;z~d1_5lgBschXuVqfU8tA}f@$X(kx&rh=yX~T=0L^~+qHI1I zJJ49ZJnpdBZ4Uc}EG{s&4q@xf&xS~8bib*yAXnJ!tUEd3_Vqqtuh>(&-pZSyWRVmP zGXu!4qBM1a*;!Jc6%~wCR98fUeps%{!r_P=sI+rOskZ*&-a+bq6Va>GjA7r_u~;Y+ z@k(_+f5Uz4AT|wsllejZ4;J7=0Et69AUOx+|F`(k%qN`|`6z#mKcaay&PuK#u~P`j zNhUR*x26y=0+2+_5y}mj*VTX6kuWqMjQhQfTcDFBcZ|U|dNF-ceyhdAnQQ}R8Ji=-_{IndZ{qMP&?h%@FuGC(nAQ)J8pm15Bmp$z#* zd*gC9wwd;~%XcKZ63wy+7@uG-gyDvCbs=q{03N6iaH6ao{)9F#CPu5dG&JMwJ%k4knaTRwgKUG!m(n1OyRo@ z>@vlghQ6=)pOwL|&-R1W7F=6C74n3k(!ptGvDq!K^A>@*#v|NH=mqPDv2!z1HmxKCA+Wte&RD05OBL< zC@=C&eTSIiG0vatJ0!=#`}+F+jb(iiQvZntt1Kc0&cNjvi<#C!6Z;01NMswBDRFBb z$rO-dN@7J&wA_ZJP4oY+TYKq;3kli$;K}4BHH1GxWZ=K58mKJiL|Q6S$|DS+CNLR) z6^w=OGxbzb+JI;bGe`{%^BERc$T!hQ7y<}cVWpu42G~8fUmxbw2`@U)N`xQgiw!vr ziTQ#sKP$@yo@cW)b>{NyY{_NN9a9E3Tm?0FwmJDMpqSN?ZS5VplFTRLjmASY%7!7I zg0iFV<>?+|b!lg9EeCPduG;!PDIFU%VsAqF>n5{8(*WDQcc@}X=!PsYNE{j6GAsoL zwAH*re=N*h3E7gp+rjPjfi3x=bJ)0ISFs!x%OF76<`H0w)?sUb8=ys^4Wz{^dwFWP zv{$r3y|)G0#m6^phIVoOk4_s${pcsk;c+9+kcv`22b-;mZhF8+8+RIDg38kzCM~(i zvobch_Pd*^9R|QNJWCV$rDS?kVcD>3kP0#B{!-h=9)^gz6cb*rbU`+(^H;f@c{won zWrb`olKn*A#yhm*w?*zf?X#=ax$dpxP{O(^}%~r_@S0RjMsT12RqRC@(LsEU&aC4Ds5lt-JLyjPI*kD@!`~RpoMdaD^`JD2q*_oBsI=zxNprH{AS5~sS16HdZ*{nujGVC|N#$&|Lh|*% z$^c)F_S@|X&oWoQLu3`Aw9TrJFs(vA%8;XnDwS(8RDrQ&NPWYWA>|6X5Y)FA)1JA} ztNrxGJgxrDEbYHuLx}{-m;0_*CBwHPnYcxqT!q0rv9vSA1_UiA<(O$d# zH)+}g0!PT6Y7khRmSDUjbq_3-jAY0XLsFr3LCMi>Iobbm?_2}>>z~B4Soaa1u&tusCithqRKO1$ep}Ig49+D*CAijz3s{{D{w@MoJ znE|kdjG*0c_gRUKvUrvUvrq|3q999RJvhbyOJY;nl86Z|i~Ew6M35D-HWJC`2LelC zRTcSIYoXL6y$>!cAsoDJ?d?Fp7Eeoa`{?#PKe#8r6GZ5YQPF4<8L>pfrO3lR>c0 znlq4s#nO>j{w>7yAnZ!e*WF}+fCy|MpQSXNa0Hi_T6>dQ$~8-Y0Mdk68+HP3O0}l@ z9&GFwY`N?|)IpZZgBhdw=@CbBacVS4SXx&p$BF)OT;_TvVUug09=XCAw0RGdbaiAS zyntL!8=xSnR-4-6$bl+l2*`yo_8LEG) z&~`}K`Ql_BUyTT1 zFfWpBj$!e}`s#?kibmmysW27MmeuC(dLptVto=q7HOx?jbu*l#u19Q`SCM}DSB&c=ntK72>p%Fc1 zQ(k5fmW{A}4&B_4#j?pEON?e6K{xSIO!}S~``WpWXvl7fSqBj|O4Rddd`rxdG(@}Y z$?}E{AP6p$v2LIxkaRm~in3byQs6OmaV=_v35vWoe|_?0DSjID{?x1&MvV-N)qehy zVrK`LBjrXw5JNOdW0#@(;xSy%f5cXtb$N;%+Z_?Db z<-fj{(G2xdHnXSbsY!OGMt87V{_W3i+Oo0?d83w>d%(A3u*r#DlA8fI)+q`>z`OE`i9IY6PEui3} zev?G%hbE0!2XamaWRMrFTP5#mX(8Gjfz1Z1=dhJ&yAS*}DN~LjuiHl4Jp{J}Q(-5~ z=EA0kUgmJvx^r~8ua*sCug-;XE+sJW1 zWW`jMV*}L1D$IjzI54?qMuaf-jwcp_y`VkT*3`&qSq+GgQ!3rs%P(GEEXGP1y_Kj6 zU&wDWgI4~nt$N9uAPJr%xs<-u4~rnlODwA7I`^TLFl_%*(z z+1PT>AH!%pN9l?nPA}3F85OI+vmuy8pj~FcF5z*@psa}Y#Y-DIJ3Du|fsbAwfHXPG z0pd#u7Rb>_0T?dqZe)>ao&=#Fs-heiEpxm%o}vP&@>#Ro+MO>?fM~D1{c?B|s2lu9 z=o5wclmf#kc8d&_WlC3R3H(TvCH_)BycHgVx&S}5rk|DuMmM*&wML{r(&OYC3q78~ z!}mS2_WNVbn)&|Q^G=w1{9bL z7FBH??VF#To>)agGpeLb5s*$S+NOv~AXFcsl;S&wD)o_@Yp4QLO7Vi2HQWg0fn~;G z^O`-OE&jjxYu*YAhOIAqFH1wuiH9 zCECe9i%bm{`mv_Adbisd9vgK#`2|kTc&FP_SmgQkB~KCm_VBM`O}BfTH#j{DzPe|b z6L8-?EPr~O-}|1^^Jg(ed+ukxQi!(1{(grW#mI*_AM~OE;mnA-(!TjwRgwUd0wutM zVcqMo65Ffl%tFi$=Ydpc&u|bv!l>a2@i-=^2tAm_riSYS&R|h}D41=lfXL8Bj&*I@ zD=$sX^okhb3DjGN)IFFaX|Jjx5z4S)`D`RYjbWoBflw=4wnWIb|Mz_vrU3*rNrOtd z>tGqlOxfFv-?D>y!;!F0wH0fZzq*ny(>{6C2S>i5V^OEF2Mt{(e|(+DvX^p8R)E8Q z1AVApj!j7_Mb+SR9G)16)ldwxX;u})c-rk%ojH<#!Wn3>lo2JYOM==4fwmd26q->+ zZlHh-`?Inbm&6rMEuJ!Y;+XcP#u^v{eU*fHfj}e|n8)@lJ*4!2lWt#uj-Tnr?J_R+i75F zYC8=qklQIv5ou75LqL|W%?4+@_UT`sCI3&tMK0-5N$$%aQijKeXvyyUpSxz8+Z@J_ z416uh*y2czfbbKN_5e`0*XPS75KgP|2sp@GY{AOLhENEqM{C3A#?hhrP+c?(tv3M590}%v z;+*z+*cCM+T|5XTZ8yobcI)fD;CaWMJ?YX_g(cpiN%6#~i#AMZ0rEIoCT&=BY9cm-{n zDns-z#TlYHqAPj7PWYwoAK|x%50J)-O0&KyikOs*KJx3{T8YYN2Q`A(h{Y7NTPC{P z2=*;4c9pryaAc*Z9(K{44j9 zg>HA@drr?ivI7TSnsdTkx4NC0`dhbF{M+(HRtvZG7^Kqy#1q|i6*3`>t1Rxv8V@XA zY@<;_`c!cxBnAs7ssJu#-?*0tXsch;zxH*iu?|gkWJj5Sznr_z^CaN|^|_sHtynNc2D! z5}-^(`AC}Ql-;#tblF>o+oQLIU6C#+Z;r|D?8!|2}n(+gp&GrPnk#vd@^e{45A;SAzqugyI;)1c$2&Y*^)u@7!3{1LR~jyVXXaryBuCuEYrC#wtMg8w+ub zwTGkOwrD7rW#eVq3GY6&+yPZ0DMgU@E`_{w!>KIUOzM}c^ojD)kg&O9PD$RH5$foi zBu2N>2?!h1KSkP*qXnvV?t3MQxWTSn_g=Z&`0rJ&pcrQ>NR`zJ{+-F1%2EMn13)B) z8n)DsS_tJs3{m{tK&UNTL+mj0=Svp=!O2z+J%h7NKnUPaj$P@=Mi7my3LfE{9NrBsh&7j}4rjAE zEFAU&CoU)ng?9MQrMyBbdH;QwPEat*aJV-!oGil83z!%Y=>%6atbtG*LFlYjdpGb5 z%WV3deVyoUr8JLJp^M+)N!T@vC*?zv`?p5}9llWmDRWHi!3hFcp}%`Q#O59UU8^1X zf9og!l_m^(n(DFQh?G^K=vNDoD>^v>k-?lSOe!X8C>Ud=puT`0Y6t$}q=@Rm$UyvzHhzPGk|MH^@u6CnJK%u8as_^d+yUv5>Y0{jX%%Dm2gk?o9Th6zfIQ zn8=ojKrK}r*oEOBsC8q(hJ&DCVDBUwuv#So8f4TfwF%k31@SDL8Kce-F+KT1ZT^r8 z`&}1b9=sqw2}z4iKg>vad^GoKug-R8Xa4Opa6_^#8-lV76U}77m=cv@cL#tF3pU1F zMzb~)2-Om=N|j3Dnj${Qc3`_Gl)Rcu55Z82GfaeQ(MK1~-GnABiy@GZnFpqe44lWl zAeCI?J)hCW`OT zew46MIYxB+F_YO?HYOZ^UhMZovTfxsGDt__=w`1+tNP?XvJbex04KE-dGd<9U#X~Y zDTK++fWk?G8)f4_Q@1vDhZ-pJ}l)-PD-KrkVm1dE(u$)EI($MTSN{ijyjSTY0@dbHa< z?ObjuaGn$=l%d!I_~cNfhE6~9W9V;In6`b`M_a4&v#UZKFl^wYJ6lejqpdCpD!K^4 z754lvDgN5OKD#9WSCACNKoJZ)qVlN5a*Rlh>WQ>6Nld8*AH(6>$i!3_Q5j3$`bi5*yQaM+^2O{J<;vMWZw2UsU+ zKs+dd+(OvoQtJN4<|MxPQk2|Ir%te(LRh-dr*h(BX*xSHoI&csz^#$tOo9jIQE%`V z1Q^1<)SGNuwn$h+qlT8@tj1X!SJ)2g4QivlEIG~OlfD$6L@Wr!O(o|gO(~%!nQU?ZNPnt9-Jy`$aB2!4`f`0&S9})3 zZOC>yR$;~b!&eR2MSxYpPbKA6I$k7q2uO#aJvGAQ+1S?D*3!hVOK_?IxEIU;?ydT& z(dLXmHL}{Yd%ud>UQkatW5szk?N?v4Ft-FKV@4PfFW5psbM9)KF+hu1i@=U8KnLE0dz^;A|?8f8hHM?re|_PcFQpnV zHr|dovSyKxZerj7!ke6BbR0`LKH~JdBOnN|5M*H+9nb~=96eh9)WHJ{e@a;atFu(6 z=kRNx*~et(t8nJq?AqJklxtsqQ3X99J9%(Qs*QLS0hXz9QugD~5Q5GR4>2^{7vHx1zM~{XXO4I(mGNHio*uXX! zn?4J3VmIalVJYShH5HZu4nsD>d|I3kVrb3ea4U znR)QG4X0tSeKTRu0`ve#w-u!COFAOYu!O_FKBAuH6rFguAuBaSQSI8pWmy^cwdb%O z+6L`6@qKf8K_B2F z0$HHL6sGio{g|?;l*Q}eQ$TbquKWSU(qS;;2906hQv+*j4MTF31M@_p`j%Nd{G&R4 zqhK3;^gIujKsg|VhC(T663Hrset}xw#CZvK>!qB36WN3;!yqZG#+hunl9hvH_(R}% z`Z&R>6)EP z$FnKG8~QYCTT<9SS(n}Amd00 z#nuf5ko;wsGDGooI3p!MHY;StxPz~+9wf>^hP9WDnuF^vG_(w<-lZ#2hLO#ZW>i}iE! zxSM$3Hi_D*QMz@y)n z&s(L}n*0r!7?Sc(8wLHn)+u1Dc{Vzn^jhnI0zNizoA?}J$EBD9I;(NXzyl6!7NVc7 z5~slHx5BWu;8jEbPInbXAAHKp}$mcx~-{dYj&*^Qw ze%-N7O!wi-JkE7RZg!m`CI%G!4~+9>3Wv$K&}IU za!>a4Q50Abrd0e_3h3!15aPW6q}LYl@+2KdJF$DEU&ud#54=Bw+;<~9j-?*P2i^u* zRDY?6KX=R#o#1dqCAk?iUaS$~2qD?Fw%~9Kc&-;zX7*u9@P!hJ5jGW@xmYQnlNC1!3hr~2_s%+#CRJVr6XPA7Ev2@UX+m%&pW z!f^_f{Nkh!hf~gQxVCY9o4VXEdXP;G;ZjMbxFN}eQAR`{=|4|CG5|PUlK}@+Q0eUt zNHPofhIF6n-?+J-d-ZY;UnZlW!bb}(v8^1_E#c^M0V5g~JEUlEe2Nz0{*AV%{ z2p7a1B*=%NL}EL=eC-n2&}81H4XtHPTMU`Z+4l{94_A-E&u4U|kVqdn`O0NSWLpFB zsGYse@Pr^OQ&}-TYv>y{295-@0AUTu`#6kJS^ZE2##o57!;%w@M8d>qKPu*%a4jn4 z$3LQ{-z-fL8tESG{}?8U%+;y8XCV9>Ea4YQUp}Z2`CigEn{wNmB@%H5W*Q7G#~@xA zzI^?GQr<1kvnQ!sQgulmT{lW(e+On7_I;|>JkL%sK6IX4?=9oU2>wfbXBjU`tTk3I z41P{hAQ{bIz2*K+X=yi3p!pzJe~HI%o?R_$8hWGuU7C1{o6`@=H%5z>sSf4gk&r!;TEG6Bd7vvmsw=sD^?)G@gLNG zR>h~Cob>*qVWJF5`APN2a{w0V9s;VgVmH5D~-G<*^f7-`ODwAP%?)~2Nd4+{} zcR9UFsN%%hmg(`a`WHUlD&6~7VcN^my}xt__kL#il7a61fZvG`A_}}FP~-GDt4fQF zal*Z?Z}Rg%$_eh)AN6yO<+)L_jv1rB;^z~Lwn6ApA{&Jl*l5*cbU_G&k)jL8W0na* zj@dF9c_t9Bv zfW?#Rf9X)J|4e`-&;`meT>sQ?tR`F+sWTfkT>tvaAdeud!4Mt05hPRO`HVigPCkA0 z+|@4pDB=%%f6`d}h9K`4y#dBa+(&4`eS|8)fmCVgmuDA%dgf_s&|#1ssMiz=fbeM2 zjEX{BhkAvZ3@ujN%9K2Ks)D$yigD`*#Ss|zOUkHGNh+d%rMHIzK{F< z&HgQ(f{Zi`u!?bv5b^Vm_jRzHGMe>7nUB+h_~v~dz&4lz!$S+(`ot(O>ZB7leKwN% z01+Q%8hOxiAS3DrIBzhdWJ>*j2E!QqiOwRlux9C0g58kJGjxutMPC!)D|*ur1cc&8 z6>88AP_mi9p$7e6z;*PAQ7HN)`ur$gPm;1z1{Ne^2FSV~tk5f z83^M<(wV)wwVDU*QIZEi!u6(V?ys=8oi~Ppr@K7)S-DgFb+)XI=9zOmMTbv!y1(A5 zuc_uGNzv5@8y(J0AUvlB&0*>hK_O#Fq|lgVq$9)0@s12)Fj<^#qY%v2X5C&xFICmO zHT?bwcALfCBl#LpQ;yrqNQj{gW?f8CGiBDUej7I6m+OX*84&R%8?UI(q==+^?e?zC zFDlAkOHoo+Q|fAiJSJMEO&+69t>qJvcGw}p5J*FzSOz(&5z1*uB%Y6f`2iAjB;1BW z)>+sgzpLf@%3yd#lmf(BkV&FG0_Pt>2<}7dfxa`wOUqnpNkK{W{Y!n*$2mQ|s+i|u zYe`;Sv5o(iUH@YYcfucv@yw_L*!+(abs#~cFfvrsL9zm{)`3w6`XzO|B?pt$TIg~W zwnEj?pRMB!Ni3-(-bVND5>86f2I_+1nlum@x26pPmGaidhXF`dV@h`9%4rMN7L!@) z?0SA3b%G32h+xNT!ir#qBJ@fD$v-F*!PEo*oe_^P957skAff5_hW>EUUTIGTF6fsw zAl9Kmzpa74nF?K?p0x}LU64J68Kz$V1Mt8!PRNLf!C?#3^&l@ZjBprV zg?{IYc*k?YR9>J@XyRuk5upOzlcpt9EhCFatnfq3%2a$t0FnfHgmQy8XG?p&vzknT z#wCY(xEXdEQ|k&E!3PdvSw`@I;XcBdlYtE5h@i4VM}#tYMrI@e0uc}wah#V!oNwVb z;0XJH|8*v02!f%1;Th8hoD47^p~MJMNNZ@u^$Wv6P#vid1iI=$qL2(hNHszZxnu-_ z1vbEBukLB(7tcXdM&{BF#*G00gK=YE1cFOhTZMFp(Dfa} zFJGV_Ocl`wM7(G?hU_Pu_1Mg~Td1a%$Y9cW5aq30LD>425Cph2w$hSC8Wzkq&Taut%^rc=?IsfED0@aD=$>C_=$7bQC@D z5UeIeI786iYfoEL5-7}7L)vSv?-7EF?~rKv|A zRKS?~Q@mk;Oo}&bxj|aUq5&xz4)tmZVZd2WecB|xZ7C=o#gtVg`)!EFiXsoUWYYTJ zFcAF_oIK_5N@AqRp-!00Hzuw$Ob|F=XUAq)gDo<^UA{7T0TRYd!Nw$-!IW4QZyMgk zKFl)<;>Ge}n#P*O`WT(p2n6gQ#F+))X4m6!UYqo@>*vOK$aax9HXfgC({GFOvZ_GG z+ODgScmVEXeNiPI?7I3WyA zeU$cWADF&gHjQ6yINH-=OncA`xvZZXU4o71Xdj{?Q)0x)LFvz?I91cpj*++N8`~hQ zjy{&3y1*Q_z2CSIZfFEU{vVi;cJ3j>LeiMG;#y(T(~YeRif%FNB3M7|4R6>ihVO`S_Qv@4U&>f{D8NLqSf0z(4~-dLng!rqD1A*;F>M6h6ZE6Je`D@lhK$_5#1bU03ApIWx&+K z4Pz|kcC$m5Wp+saq>~r>mt#$gNP|oPgiTBMlG^498%YLcu`5qWXo$@m&ZjFg@lZePOhjvOZ36 zLyH~+Anp6Zlo>XD2SqnWaQdQD-+Vk@|9=qNLG{OjHyS|)9VhTxmdY5E^mL~KG0-DS z3B*^zhctj(k73GaO;WF9zy;9=gecahoybp5V4s%b#*jy)Gy+-)X@qW7F`URlT$4w+f2q~zJhuv5n9HGYjNVHHJ0l>sb_(=wA!DhRnElRf6(1ZOlERV}8j=VhV zS48c8Z^5^Z6?j!%3G-2IiIrD=dyxPB?j?9B|FXrvDB}s-e;o~t zt!cGom*~HGf?stk>Yvp&ga1GN8N@qSHM3w_&X;l!J*wiOcEnO4L>VYEA4nvxNKx5b zG!AJHwpax5qN@B;`N{;{%lH*u(e_uwcW&+6l?72aLb56L0loZ!ED-B~^OvD3=ox07 zEe7MJjzP#H2d$6%!|0F&3~6Y%kt(dptkIN#>R6pKgb|n8vrF~tMf~b8DSZtYw`}3E zw1M{_*xmYFXT#9@#v(qpn$$SDe+}9ot}leL2*nZek2qrDig+6pE;Xk&BIrLqo%{8Z z7xQC7)OmRwB(;MnOM^!kZc?pnA@FAx^9^H%GwzdxB=bsfw1gWgsHK)*B!pYP_4AkT zcHwsEk1XLa<=lzw`a4T_FtP0TiKn`-U@@5P$HR1Ah7O)j?*&kj#(OV35jd{{bH0j` zcZ05!8?sp5CpTn?aZ{mFUgKF=UU>`}EQLWAm&M|B7obP5?HU)$A;_8{R5@Mt@Ea?%b|F-OY%H^!U^U3Ra_w zYYD$G#k4EdlNhMrxGGs&qy+&N90H|ikuhQTghwURCf}-p%jWdhQod~>ttcGNDsU3+ z7{r8;rAMmd{Vz?I=KK0UU3G-^8D4@wm!{vej9*hMU)X_`l8qqLC>+6CJM?>(@r9{3 z4rNqJQfjhOp-?Cmg2)@K#bt{ndh9g58C^1+JMbt&SvMlAlNY%s2ZUQq`gbXeKA}tc zr>F62PBN`+POLsC1||qTL*l?`r@){hX)zy^D^34Tgu`^SUH|2Bew=(U1?(hJ+M+8) z@Z7+Q=@4gSTui5*emXy<-=--yH_a0C$Z-57Hw|bqHqDCkexXLp{bE3Z#6&~2twg=& z_wdV(ykEfR%;m>{vncjUE8UV0D{0XRK92UwoE5x5S$gIK{n8aY+OcTv=mqd?3f9^; zL)7q(LF;|2E#6!ucMQ-1<}2DSAbS+nfFKij_Cvl#!dK}7csXwBhJsbB|9u6oQh?Wb zVK4V`O|R|crSlKJhCy@I(|4>W16gb(JZ?HFJeC%Fi82@nDxrHu!N|g1ici(`UVi5Y zcx*%_$fse@pbG&)jiH9xYBHAvqrhVu;jv5KypmsB0o*fRL4!7B1SviZjD~|Za0XvA z7^CU@ds^Ox2r??%7!5^ZGPw1jGvp>47Nh&Ek-QnDDLksKmOQKx^p0G+mz>GB4TaHC zD;XN2WzisvPCkHJ=F=EmtY3Z>e{Z}D12SHguQN#v0eQhL6 zf>IgIU%F62F0YJbIBIA^Sf(?PEdj6*GsvBk-YCW4!wR@@72kaF$tjLOeaR&JDUv29 zY{(KhWhsu3VhEUsQ&;m!Pn(UOSy&Ko7UVnc%`b91mE+EL2YU4%uI8QuPM)y(zC-+E z+?rm`et?V9h~g(_q(9*&xdK}g*J&piK>ryzN@zJFWM_j(F)N4-hIye4HfjyuLI_9` zfEXTwU!Z#s1eD$s-$=b;LoUNnz@a%k|6I_AVm*2;zix65T1K8}kyqccio=Bdzxr=h@vwf$I=CcT^zG~L>fRRpDdX#HnY>wZR6{Z@kJGd%`x?@>-#(S-|H}1aFJopmv%LIM1TaY zhriFaA1@z(t%*~pCtnu{q0N|LP#mu_x!PI6omcGm7@yfrRXk(CJ5$TPt;|yMUAbg$ z=WIOtC3}!hVcD!w|Kw8cvC)0ic&VGd?*iVg|MOCwZ=;uFQI2vUpS6sHK-@)`ylFCh zJtnj-3}HaAycToF65~3|L0kaM{P;qCVg;RwNB8FAOIFIIa^z$P(*sE5Mf{}kxcUN@ z72v!=`tIf;(tL1pX0k-e*aI^Gu0L`SFIVy2FOUAip97t4`8m(gKf8#}PFfk#UB+FY zBaJnqc)(aQO3bn5VaD@H@`_zW^38el-m&&bREd!IF0Wm%M)aqDz>lkib7}BY)+>fe zRgzQeEW~nU;R0Q^K4v|ipXkKoJQ2@1rd`+_Wd(3wq~3R%lO^nV1-Xii*=;NGap;d? z{7CDkcQ`c2+kny+GDnUATHen*0C4tj^@6ZgGB$V4>@L>HI;KyXJaOFE)@Gcy!9g{c z4+`xSF>WK>1(Yd>yCn;=u>szc4d4Kf5a4SBU80*I@%A89wcse9#gcyg3*IzE;)pU5w0z6U(Ey@aOrA^1 zarqDfab=+tisk zIh^3vo^d*K*nmRD1_hr7X@=M_kPfyZKcD&H6?j=Ch!Jf;qv~3DLf0e%v7$($Ah8C^ zMu#L%&l5$w)P;BU7AnR3fxO&TvpmJcZkzgQnf~b|+#BtHjsspI;G#%&)wB*&J$k?0mA!!2HA5x~QLsXyHTfxIT3$AL}`Z`U#T@%E?HR%d%UE6l7nsdz*6m% zIxKEPm5(&Ej>ALr$VD6ZmP)Ek0vSirutkwk#!6aR+i*9%9@qrFNC+df+5s@FE%fds z{iaQPej&)X@k%PRQ%8hcKfH<0MYhRP(BnGEQ*Y6%Rd2v39-iGT2@k zuHaR}J-+-3K5n?OJtLGUSMtkG!DZKkF<2q_hFfeoTF1Gy5n@WC$w)Cei80mKPkJ)e)?{d?K*hbn2 z>rf$L*lUz*xBkbg_%UwSJbis1vHkGAPF2ogc>VlyEUsU*g;(O;O9O)xOc^)B;77({ z+JZ10hJtwcGT{_1BDn86J&YS0LXpzC7(KlIYJQJ$SG-DJdJV5k^08(d%pfv>E6UAS zS*!Ij+!am22lj>#@>UtpGms8CDJu!o(LDtImVOBF-7}6YtqH-H7OeGU*%3VT>^1y# z{rYQpEn>6&>snss?|?w=gyiq?6bUN}RXHrG&DvBS#kCbUZQHr4On>%T2=N_y{&i4W zIuO%?(=Vhd6eIl9j%W|SEtiqK#KKri9-<}#X9(q>hBjP>OL4zOX(wGCDsKlN*wkD2 zMye|xbydlyt+oV{FBeCf^C|dMfk+`cFxMQ!NcGVDu}9mdGQj%HHjO5Yk>Kb$`c2J3 zc~<7)%mZyO1vikw;0v>_eDk>+GYRJ@IDNqnc?I6&q+jqu{#p_V5PD-3ush>cA-wWl z0UN_cS`GZ8x4Ea3&oqCG4u)rBtL_IbS?9=*Wu^hpV5_;~$F{fDRtF;VuH<66Ht6Tc z4mReb-!+xTLC3SRmSHJ@VX1dcm(4$!}R_@=kE;T{>*r-boxxT zbblHpbdNQ;KM`k&D#{U$AznDbKs|8R$BK&WrFOD}Bdy{uExj zkk7JMe%1zDpl;_m_6>zND#3HIu%jFf>6xtVEWrC7aqn{;MLot4?M$!Pz%A?M+hk}C zj>CjH6pQ1iYs>3qd3kvKVZx}IqTvSDoQs;Lf7hCa=qIU72KO6OTjD<+HbsyA$HPvI zIx%S2@j2gN#0(x*VKJ%HE|*PB^ZsbJ~=)32o!YjlsJu z^rd%j8<1h&8F%m*JkbkLlB^>+@TBGO9N2erfC$;yw#C@|Qr1o@tPL;~Vow5pkNlKW zT#x)fZohW)33u^ILexcf@dF8Xi?PY1`b=+S8=^yDY^ZEIR0!;-VSr^njsnhj-4U$k zZ7rh^uE^?O;JTaR4W;EVXDYIE$%EXkzj8NUJpm%Jz@1k%%m|J|^<){Zu*1t6;f{p; zg8ae)!J>*F-Dwh=7;0?_`CD6Kq3rAm{d@QD{jG4^;ks>urkSskY`_`r{xnTWs|Gb| z#p?)(mX+x1@8wq~5C&^fziON@%T$bU-zkFAK<);DNMUo>QbPsyNTW$8FX*c*^+H)B zwGWCclu$CQ`wiQHzbK+#aUWlkq-%|!hA?uFz=KeI+)!qLOOir`acwMV%!rvA>LtqG z=`m^T8Sq5g5nIAOAiMY^HX%Z!JV-C=(s$m^_n&Q&Jt<23ykz~ozGu}2l0#u4} zBghmj&0;fW9DB^<3F9OOqLm}w_=^Yl+sVoD;VMzAJ4HhP1F(JRh2^*chk^fOF5}2> zaxF&&F&4CdZeRC@;LRpp?IR>=d64hSP~o9q{o_I2+}euBT7=WlJ(%#&0Hb)gWk*I5tqE8C!0VJgvY zR1evVB0S_vC{)Yb(o%|xC=Hi}e3dyRIbNsnwg?9-8I~XOOA{?I`AW|1@{=NNLf;4n!BErDAhmeJ z3XAd3Pb}{L2wfs*Ky$ojXvFG)*>p<-qV{3k!_rn45owX}3evcT5UPm2^by{oTJ3g+ z{-Z~D#G)wq4x9eWBfKQeaU`xq_@EeiS?d#d&~?a1MK_*gqgJWlH<4fiGdq4}m;x3& zUW%6;*wO{ z@x&FJ{-b+np4rOc zAW>l3lg|xjPUhnaC!Imj#oSS&7#lTq)R>wmsteZzV|Ijxc_O1*^__dTKM}=A;(&3U z-(wu;=bn~h3b`{nYV@4VC(NHdJ~uZfzfd&Bn&w?OZ_czD7@PR1qo*Eo=A>xwoiiu< zs}6tAGSTYJb`&>9S5NM|Y;N70yrS&vVr6{v%!yr>bw%0=3ZVJTQ2+DI9fMSU*&jYa^^Yk=TJ&!ZlKQ8Im(DQWDv!T+Yxv4B zs=C2-gTs56Z>1-Wr00Gs3D@88SwYpPnhX{*exp2~I*js`c>mTzLNXvQhF}|rid6}C ziU|q?rDII-lnMYa!2JDr)~I;6H^&v!6D={oQ*YjHp#EhB{O8_4!*L)=EN#GUS55>n zc*x`zB`S1uV5=jOF$*Cd;iU;^2^AP6erFcRLR|GO;V<#y_k$C^mBTo^l;V7~U!_66 zbbmJQ8;1x4xS`&U4v`~n=BmHG6To7^Q;bL~ZwMeFH4!N8uW^t{D^Mtyg@H1S1RH+( zd4H`_zgI>H%_taK_e?%t%7b6z8G+H-`@i~NX75XfVbJGqL8|xqVe*#uwcnDk2gJI- zR4Wzh4*r%rXDha?1?oWJJ#vIh7Q)I_u&s|+yzza+m;w_XIIR=V z8E4l_82k|Ed;chFND$n0aNTPIPNcF$#L>2WxFZs7AFILD21UDNCplk zwuq_bdL2F3Es{P??;a4r&Avz~8nNPyeE?7@gP0JvtIm`2A)NFCxB%eaQL|5X@m@cv4)X6Z{8}*;qvVzHjt|KsGZlLRr$$uf zkYVy0INaA99E8oA5?3k)dw=>NsROS8DFcoqV#w*{5E3xZO9BobA|7@gO5l$PvqG$Q za77E}okmEoB&e))fc#Swf9ldtxp=2GVBIO_ZUQp`Wf%M*M0jN<$TS=FG}NvfDi7>w zWHMf@=a5mPqBg8_A3}ey3$dknKK3Pge|3VCxM$`TS)(B_2XUjWWAOtbPi;R%kYvK$ zJkEV%qqJyDO~IX^ro6?jJ<=Xyq83`(*dU8c0iQji7xIP#8;rPXE;J-$VscvgL@2z2 zl2cPtaQ!Ek6^R&06G`E(7kXC?5}o32BDteAFSj{aCi4Dlkhn(nYw;k|IPxu{C`WH9 z6w1M_!fg_k4-qekjN}h@d}9FhJu^_#C_k}zc!r#Zej+!Pp<-GY6~R2bUUbT%GTyBx zNgQqpxLyW!_ayN_aR)|WJxy)G#TK~J(!1>xnO*V!TnP<59hBRo-s}8`*bOi!q1y@U zaj6AP@k5N%U-^hsdS5#OyB8=TcuSc7{)kM<#6%r2vhj(Td@^ikGpx;wy?as@q6yVD ziPcy|*djBvM4d}*UI6& z4_z-t_dX7~_ZfYIs_1pG#eUyBIOcZM;cu+gAu zYu{;7|4@qYkRHwc1nxrTK?a1L=l2ryd>Li-3Du_2v%SXQ$xjVBLDI&8@<*hw;h+xf zK`|i`p^61cDTaaR7G=2D5!hj_ErtOj9b={&6q}HChBxW2*eRu)Ax}PZXAjD>{|qV2 zYNb*>ez(JGkZCpHSd;b&yw;-QaMWTno$l_6D-Og2S@|)X_|N2dcQ}M*y!iy}Mu3tg zxE`X?0lue)F()K+1a+u-oGQmhA}q0Y&xmy@Ww0oLC?!g9B7M<`91x9*>fU1+uqcoGE+j6^5)|T`3H9{7^m_&O zUc2{CjKaI+42c|_jvxMxO!*%3IQ|@YA{$=Hz-IZIM1=?}9ZNXiAx!JHQ%0L)fu;n8 z_vgr*2CUYU<2Hiy3q+4Y@rq3eJxPF4QOa2JG>V{f6@b{0p2)=|31H;0c?F|_HI4S( zFmuA5dOPs?kPkr9Dp;{7+Mq0{W-u ztLI7OqwpLzTDl&Uw&3?9?}v9Bglxb6Jt;PR2cgB$25@B7WNt zjOYh0kjN^^WQ|HuNK*)|Eef%Hi}L;@ELg1#tdpo@Vu_$s6Rg$+E|bWlP#^$@9%t`> zqWl~xra7deAa_hlXg$;C+MEdpOQH9eFSlob^9^IWj->U!w*509pg9Bou&IJ>icqX#L;N-=lvdb00OKZCA(?_XGUPh@tSv zA|!hh`vOFmz*?DY>;YzvNv2)1TtTiFFs!NE}z zGOgB3S_@_^Yb1~1>hS<(JYTksu0fy~V`lm=hodzay`6s|Qz&m2DDrPvzWNh!J@6jk zt^JI|K-pw1P`YKeb)@q)RDW%W^d;k3_5~OA?YIKOJmC2+E;Zt7k z7bK|)EEFr*ueiLq7# zU8rCCisX71{Tq39a?2P#RStd@-X0(h0RKOaw^M{7y?CFEDIh}5J#^p2QM!I?2W5rRVI2{6{lyUy+ERhKF=$411!ch!lN9Z`Q zzvE2a|GbH9#!s%11%l7T#Gc&@Tde%u2Ha4*H%`R`;Zd`S6cKB00eYzru+0*+!G`s%?o#gBC> z;DJ-8eoUm2$;6kH3Lfipfg@Na@M*LG!9jIU3Mxg*yu~+3Y}GKqTMK_TXJLTzZ@7Q;3&b$zrpc+SxQ!u4RAiPlk6o2NIyA4E|IHnLC>;kHXK^Y zWHyJrC}{@uc)>!j_{8?sVam29bOm291? zUAA3zSKcAtDc>tUAipWUtB@-~6h?(Zk*_FGR4N)2O^UUOO^S1hD@v_0TDe@=tlX>8 zs3KKysuWeBs#I00YE&Ipr>Jw)o$B3cPJLKCpgya<98eapIABFUOF&GtTlb-lVl-8tPA-F4mHgKL8ugIj~Q1n&v%4(<&e3_cfpCHQ7Y zN67WioX~ee`$A8Jo)7)Z9eN|o8kQF33Y!%+KWuT>im;Zjwy?8dm&2}w-5Ez+oJNj| z8<#S!XxzSWJ>!m#yAW;!Me9jS?o zjEsv+iOh|h8+kMGZj?MKB+3$%9F-GQ6jc^g8`T)q64e&9EoxWP+fnXAQTU zjk+DpMsJDk8s9N~r#@d_qTg!}8?=UKgVm5_C^VEB&KhSJubDbbJ575{hs?N90yoB` znG4OO<|=ced9`_ixx>8Eyw`lde9Sy#zF_{`d^1KG6CJZHW>?G|3uDn*nk;u?<=8J7 zV;!+sv4wHkxE6Q3IzBw!9-k4PA72u`DZVp)cRUw=IDR1hQvB8Uzgxvttu@+ewYFQg zTlZMcSTEU}wpq6Mwr<;X+u!YT?bY_>_GbGgduM_;VPnG9ggXwQL*t0VmDDMYT*teP zG#)8aaEd%7B*mCQk8>2Jl%`ar45XY* zxtel2Rhb%=YEDgpb=tJl^3>aDIcdAn4x}AR8%n#Jb}j8rdR6+;^i}EW(%aLwr|(Jc zPVY@0%*b_T%*ZIusLxoQ(VVd{V{69i83!`;nfA<#%vqTmGS6pzmU$!d>nv&3{H)Hb z-C11L;jDqIvsst3u4UcH7G~#Xmt zn)~_`>6F?jOQ)=wvd(38ZF607-N_T?9nMe6cjh9LF9tC1t|-h3#Kh7Sx~v4VL{V^Ta`?uvNEi4Y2~WQb(QUv z+bj1}c30khf_&T{xS1MdO<0Y-rmN;w zZBA`T?VEK>owP2z&R#cE@2u~r-&xNsTDR!NVs`PY#eGYRORP(pm$WX~wB+KF&z9V1 zXlz*3aOp|olZ&71S*l(-YiZ|G(x;lAy7ttqWtGd0FFU>L;<7KEUiI|Xjb)8Do|*N` z*=H^;4_m(BS^2YrD;BS~{M?ErrMszXrF3Q6$}d*6ue$ZT^ZC`!54~W0Ve1PwUR?EJ z%ZoQxH?Q8fdduprAL)LywOQHR-u(HR7uK|{*|uiq8g5O`n!z;}*IaKYYiVd%-Lk3W zQp*iD>(;p~?hJR4dy{+TTH)GdYcIVt>!m#}z5UXGmwMODU)R6hu|946w)H#L?^)ly zzIVO*>(-*yeJ`^wZ+dxPgL*^thM^y4{do6A`NrIhU9YgO%zb6sD;L|6+IF|y{7Lms z1~##q7H>MU*}U1lIc2kRbN=QTo9Aw>++4qT+2*Fry_>&opVmI~Q_D|xZ;9U0u;s$5 zrLWe$y6e^Mj>ryshqGfwM`g#dj^>U{9bFy!Iu3W7>bTf(&E4^}N9l?5OD=K z>s!ON=4@TMbztk)Kdb-QscpJ#7ra{Uo1NOu=+2_ft(|?H*M6@3dE?Iqw+pxDY;WAY zb9?{x>pMbrM&ORu|Ky>tp|3t9Gh6ZRQ|$!ZCBB*OaEB*k6-Mbzx&b~i{I$qv-C~PoBh9>|I4#`oqM}~wQQem-@bik z-m<<`_SW{dj=imZyW#DYcZ}~WerGSoaAjOKH{dh-%6#X$bGz4eU)i6(zh(caceU?s zeD{lA?>*3U;PS!99%WB^&*|S({^rJ^_V-fWYkqInd!HSidwA>N>%T2?_a^l&?cLV9 ztM|$g<&m@_wMTXxIoBuebM>|M^&c%dy6WheW8ufjkL@~k=GfQoXT9J0{>|e_$Cn@P zIzI4$`h%hmmVeOs!Ql_?^vCsA_ILK5`_TAd(T6=B-WsqDtQa_SLUNR8eBZMWpMl8?!mVQdj^jUo*FziIQ#P8)xleXcTY-BYEMRnx;eCL=+ei@AGe*>o!)Z#(kIqW_MI_eGsy@8nUyfSeg;BHmMLQzn4e%ntx!D; zwJ14Qa*G@$B+09~O&lcYpz}6~W${{nhdhDD@;k(K@UM5s5~WHxweFdxpRTE?t6S9A z`{YfRL^>0&kz z2Q5m+#Chu_Y@SLvXH3+=cOfo0O{+lbt#Ce4(`pxIYL>(+vu{+gr72giT*B<;^1bZ90U`QL`(#~^Km*Dxp7U?c! zN|`(+k39-zuODto7?{nRz#aAdpAA}@`4JzetHk1o;{rt&zWD&dHN#L)cSfo*H^*`E%UqMh{Dqvl@OrF zE2t}|spsNK$_+lm^+ioG$@A(srnH=6c-6FlljlJ%6Ab_^dU{QdhN z>kIUpFs23ti}Fg!Ig!c5iA}BugQ0>W)d`%?ZZen*`g)(J)}^N@oVcJo0w2O>=`Sa$ zE~w)KRs#|}9nr}XMRrb-XX^$E=2g2AI5A#LE)$w$a@BBb?P8A9ppTMc@C|IxORCz@lQ&NtA$yLFr>02q@)Obta0#J;GubaL{ z$ker&YPf*BYJFR^o(n+j6F7~1`rLA#us*-S%&8WcUP#~q?bBzMr%s>akBu-OA&5`V z+I@^B@9}bp8W$H}t3U%dHpb;s(I2&Ep*|`{Z8eyXtf${X7+s3iQXu^2V=7SP zsi^2w#MYvf7@XYd3fh0eUFI3m2fKspjE`Xp=9Y6BlS^N~DKSC{6Gp|QuSW5|)@li4 zX)ae=wNERyaj)1SjA*Y8bqum4aKUyTq0bO>7=4D?eFFLnv-^bfInM4A(Py~bC#KH` zyH7%&k#?VyKBJ^|pNt0LJwGzw&KystA<}KLW4Qgj$yut=ZzjEiBP%FMC2-MphEv&o zK$XX%%F+0e9#y7K1FB4)MpT(TO{g+`nyXM}8i+xi>C=Kb(`PK|OrLS6GJV?ZdMA%_ z33haqwpyQu@u;TbRD&mHr-^f$B$gPy=8jSPTKsVH!9 zB!o+}`a~qSpggI9@9PO;TKp)OWV=3c@H-lcm_CO^lN z;Y%jLw0bFM0&3--L7_mhnv4W4)$Ry&CU9wE)08xgBYAa5Pxthi%5rkK`=-ij)0nPawrow7Yh(7kzn8yXNdDZnMPLNkq58P(+Y9cs+XS3OCm*y2> z6p)M0f;mWY+&=}XQ7W;SIHBiLGfs+))J~5P5?O|8XL6$A*`rCE6ca?RFEmZ1IcZNh z{b`;BF-w$_#=v=oVl3_)V6lVNqmT>CD=&#Ctti(!D;z!t(E)iT-LbV2&wvF2gn!z&(;{3qemu znl!^s$DEsvzL;t6#?(Pz76OC@itXKmk10WbkC{zj3XoJvVQ63ug`t7D6b1n|_qcsO zCgBvsl_5m<&^-HouwOK;93g*P1%)FtZa#(MLklPzAF8BqeCP@K(L8`v(T~!=Li$k} zsHPvKff@=^h(N8I0?|+%1)_m^3Pb~o_=>p@TFh6BhL-RZqoD@AVl?z5Uoje5%2$kr zp5iM;L(BMz(a_WA<*di|1xw7m8F8(&^4g1M9Y-+$R3P2o2nD`IJ*f9wy^ z0yp}D6yO#6erg)%XHPu8v?jY}Pnlp4F;*WVapfw@*g|QV)FKVTeL=%f<;*N`j@Tg{ z57R+JDP6t6eSv+dK3ShQ7tFm3ae=O0CU^9Yj}tKYJ~P=k8*IYHa-X0+-)Et3hooyE zbj{sZ2mS>hIJswqlBt}TCe9GYiNi%ArL~9Ldzlk%_OVRKGwf~YhB>|{?475Og6AjKkxh2lDTuw z@1A>>-}$ZIbLQlo`gh;a-)4C{ZY_eJ>bUwZkPb&o#2VLTeEMLpgXS1j!M z>tAQL3qq(4)%h>)TDMAV%6tIyH-g^1vg`82n;&W2EwHn@1VOWQ)fHE*G5zEA&2@sP z&JhIP>Q$>3uTs8vmqieYkK%#11%|(h>(%+}=S2%L>;A8hsW^>0!aLV}lun<&KYi<7 zsc^J7+E0{9qp)7k3s(ymAz`*~x9|hJI4#2>C=(30}8Vrvhqzfa2G0{)A&d9VYeaaEaN@cZjjIvhQ6m8q~FsqGzx@|_L zL+MwRE31?>xIb3e9Bupl{i66<^vw5XW__me z95=tjq#EM?^vbZZQQ54Vtn5{lsj5`9s%F&$RjX>MYL;rYszWtbHBU8P)v4-Mtx|1L z^{RHN?o&OedR(=as#iU(o-YV$aIJc&dIer!E?3W^0`**Vr@C9cNR2l2{f`sV1NbNZ zPu&sC-co!ZYkQ?4&n4!I9?>m&MV}ZH3&emJ5=V*^;udkMc)bJe4;M{ci(<;@>-LMH*won= z^DK6H=U8KTNzQ7#+0F$q9kT%>Y(?V4D9nNs4R|H35BSzb->)YH2L? z><1I_D$+tRp~=^r{BDpQ+a%Vb<7YOBRgsJkulqo{K~S`yzYd>gPBA7eGqg_>TVjj4 znoDC@A-vMEe!k+1X7o*3}p@ zmdrsXVq&0iUnc#@!k-wkVq6q~=^gv1r8kB9+YItN{u zhz?BbG+|6J0GPdS=NuZ`WBID42xC#zyL`8Sg^bKC`wYc5{it3s$kBKbDfxm@TxA(Ps_@mp*et`xNwP5A9RZ zrz5maMW4>lJ~c1T3+>a;g)6jAOP~3neLDIqk>2kWoh&4Mg^x&S2m>LHeJt3CQTDmZ zVr(@rVrk4B5@MMp!}WqXa8T|5wLZ1c9yHpGcX`oh`t+gE^yx>V=`(;v(`Rr#T1^)P zXf=HnqS5qOghtb6F&a&uWg&MRUl=1o7`U8HH`XcZq*?7EmM$ZZc%O<{S~|+-@PV&P>HKgsRUvmZ zZ&o#Glj~_&2o~N{1dzsJe zuG@~M#`Y(3mvKUOObv#MQ@dlGv_``-Iu0n^D!1c+GN`i8X(S<{Z^D}3PxvNu#?%;k z>6bT%xh7>(=OS=5Brq0&vZ>1vQ%V1W)`j#<6EHn^jz0wErf2#OOegAPCBB$iYS+2E?bYt#R!FU6Gb!hvzWKm3ywc~bA@J*ygX}s#?GU=HuVvnIAg-|NrDh4BAaCjLt zWkSr>)G^&Lb54i5Zcf?0a%Ki&jqg`F(=okYX=A_A#IvdO4Sb?06ss*sC5R-RO1!Z- z6dPNz9dkkhzkPHnZGTb>SXrzbZEWFvqPY#qJ@10nYm}#t=GX@|D)Wtw_h>?BpC0mz zSb`p@2>g2!JrT2U^ajy6+{D_XSGI|ch&*}|vCeBt5-SkqrSV%Oj zxL8Cqbg`Id=wb=c&_xe#Sfm-3OL@!aY8h`CT`lJgqpQn!!{}-SZx~%&&KpKoD|y4{ z>Iw{VU2+ar@o%v(3a*qd$K!G}jXROW8u9I_(7p)6H4&|WgvTWjUCoK;8xgI=)8mrW zUcq`5gyMaIrZH@2gC$<24Qg^UDt-Lcbg@;eQ|TsWPPJw~N;3llG7l*eE)4 za|HgM_%fLM^tmt3O-x)#^&3T76H8*o4{tW`D{7cz_zg+?0&X~9b(7X33iH|VPC+8&k*hK4JyQX@gv18%IV5^ z%0DYVQ8lXWQaz-KsZVJA+H!53cCB`EMEjohBkd>JuXJi%wys0BNViJ2QNJkdjN?k+Y~a5HZ_~3o93C8n%*;gWcti>)*LpsnLEsj z%&W}3=3C5nnIAI8%ty>8%%{ztTLg>2vc+@p(fW-| zYct!5ZDHGN+oL&#oQ*lJ<_2>sa_e&^M{;N9cIU3l-H`i1?#Fh~o?-VpW;*6OmOIuu zHam7WE1b7EPvybxm+EtMXpt@jjkJAcer-B9(Nti7xOdn?fJp{iv0Tg z$@#PMyYtuOf1LlNTj{pD=ewVDA9cUte%Jk>XM$&^=Rwb2&(ogco)ex^o->{=yb;k` z=WX@Q@^*Sxczb*B+u+;g+u^(4x7YWy@3`-T@09P1?+d@^&+yy* zL4Sq+1OLZ?j{=_sj|E>ZC@$Dia9hEBg)bCai@n8V#j%o5$;+Wtp^c#%LwAHe4t-gw zEX^uARQ5vIt7WIk&Xj#ILL8AX!X6nB98oc%e#GPvvqyA~SU2LsoGt&zq+uxsybY~wt92*2i2cdpRJi)v!mwznlDEwN0~>tM}9<_UP;pnQ- zca4rbG&(l=$mo|wzcu=UF`Z*pj9E8k%b4S1PK-G<=FFHcYSp#bwXWLY+M3$N+K$?l zwHs=;)$SOp9cv!z9vd1vdTjI9>0{@OT{?Et*gNXXb=&H8)ZJhAWZlbkAB@{E?&JE_ z`UmU3Xb>AR8te_hhKh#zhRF^48=h@=tKox&FUA*+uNohYjBgv?F@9;Iwz0eMWYgT{ zaC2Mp{O0A&Yn$I|{;2u0=CdvO7Hf;QrL3j4WkSo-Eyr6r*(Jh{?^Z0&rZ@$ zvQF|&s+qKZ(zBCZp7i#l$VZbtn{;-vezJA4cXHX}`pJ_g&z{^pdFA8{lebOYG5P+< zdnO;6{KAx(Q|7l-wT0W>ZTqn8)3$HgyW8(;f2jSl_OGTkPn|yX@U+5d<wNCqZ z`uyonPCq*Rl^OTU%$RAPId^9F%;hsz&pbBsm6>nNJb6**qR|(Spbn_1?ve z7jKJP{L<{~*_&sdykzPnvoCqN!`b2QDDN2E@#LJ^IS7Ppl!ja&d$z{x;nZGyLWYewNPBRX5oj67A<;varfe5OC~RQx@S$##-1CO zic8lmJ+o}{a_jP^mmgn#V)=W^KU)6D@~dQ7=w*9i5mqkur_W257g<(a` z3jd1o728(qSaJ69HJ5+6a_P!LE1z9?{EFZeCssAC+Oz7=s%KXnU-jzhoU3ZDYP{;m z8uOa@YgVm!>1x;2!K+7KJ^AW0YdhBNUHkOfSFYK3&6#VySy#Jm@A|CuIqR3NU%h_A z`j^+ASbu6m=Z2*lUcc6SZO65HHVPZV8@F%#DstV*>%O?|?4~7~UfuNerax~w6ImDe zs&~=#&g(a97B*`)Pv89P4XrnPvSrbhm$r6n{cPLjZJ&I9)%Rb$amS5c+*E(l#+zQ< zUbub7_D^nJbMwZVKe=V@4>EpW{ekxf_x#|STi4wB@eiAS`05Ye`{Bnw{QNfcZ5g+@ zZ>zYi`L;)Id-}FlZaZ~*`R$Fj&$>ObM%+fUv8>5h&auSTawU%10?hxrcY z9sWCx-Wj^{jyvzW^P8Q@od#*vT^sIt@~*G$?zsEedou2se9x`-d~om7d!M||ecz4u z9r=;-M_YdM_We2c&%ZzRJ$JaFuRR~~rl zfs+q>_`t^x);zf8!54ng8u`in4>=#&`OpUs&wTimhfhBI@x$NjHtcrp4(+bn-L`w) z?iIT??!IOBeY+puePs75yHD=^c=tDt7#^vAWa=aHA6fax=|{63t$DQj(W8%k{?mCs zePNGw&+CtAAKUe~{&DN$jgN1C{Ltfn{+ae?OMiB3uei5v?~1+m?LD^l<0s5d)IG82 zi4{+*d7}4;$SqGCed4nx&c^gHYpgl8Hnu1B!9Huh|F-P=eE+Qn)*blr$rldJI=KH( z{h>oowLbOI;mL>J`gz6E%3tVzarDTnBd|0J9^L2ea-Ke|Nf!h|M|W0_ZD=#xA(m>e{la{=^qZCRG+Lk zIrrrDlSfW|_m=4AHDzS`(K?>p2|3tbIN#b{U7fBYkqX&ZNbPC!J1>S zSXT?o&om;_^r)GrVu;%+dIV*F=?zMKk6=h+I;}z1BWQ!nn65SU6fko}y7`hUmM&yu zrf2jBnL@THGrLDH723rdt0|{Pun4wXi*2cpTg05MTxXBq67n3bydJ?(%zXI{UyqmN z3m&sO-_wIQx8S}+aJz*CxDcjAt8bq#s`H|E-o8JwB&^S4N<|h-ez9#zENyy67y)DW z3`(|CP$=r!MR*NiVJf16=|h%{L)p-e%$f6)xxi=gsdSDKzs0Qfd4mN79*vqVs;fq^ znwrXru+_!_9)x352Qs2ej2sH5jbv)2Ekjg@iEg+AwIC|g;!;7WtZPSK!tE;bg>}Kt z8OstK8bmd2Ae9N-9z@l@u2GI~05NNoLQwS%eygBTC0xWy@|2VmW6rfY zNA#|{oY6)pTU2vH^XKJQ!4^YrLHYDDKskTs4~uu zUO}l)ZWR<-rdBD`%aFrhf(E@+sRs7;LX>ismr$QCL`ocwIpuFwsWobq2Av%%~rGVrk$CY)~t*Saal$N-Fct>oPL~5 zh%ULOva}7sd8JBJ_Ml;G0hUosyPBy~Bvq(!d3kw(Jb%E4#jQdAkKeOC)Qb@mm5NPh zkI)4wz7ADg^wc;NQbcx%XuaOJuJ*Z_$Mq@-`X`Ga`kuZyM9$4TJ&N!-aS@KEcXjk5*b`e zct9EF{^x9#mcp7SK`B*es6AeZl=Fb z5m5L&fe~q%Y(th(o0Vlq*QV*3bo1`*S^1MoiV8A8mF~~Gb?dfUT|P?riZW*??u9a( z%_4^m=0Id5m}4TCfgv5P6}AdGy+N-t^rkaIn%r4_N=k0G_h2Vif zJ?`Ja@U#mzPEcz^wPur`QHdJ4#@YP`v0s&(pK`LYvSw#pJac;6;F;UD2O7BCY$~a#bWFS+O$mF0+ZR8AeTS-N*+E`Un$$O{v`TWK# zkIz_nxx^(U(qgkqla2QYuD>r)kzb#M5e@$I^Ep26An8tA~-3Iw=Q? z!B*LFb8Tl|V8#6NZB~C#k=Ys=LO@P(*u+b14(MgeDN@dV!)#;nB4R*s3)tflw)&up zX;mZ_Y5pd_aA1&$Dy3GVTCHPRjZ(WxU|CF&#uUqd1d-hfgCT7JBK5p<6~Ns%hFkzAhI;E2ObFwUIkQ9IO)+|kaQeK z!xyANg=mt)UQrp&Y)A-$#F-$fIi?u2NKcEYY7m?2o?ra8UKjHERSR z`t`$Iwd9$lwh2fs*#2~nzr63O{1!cm(*;97p|eGI?mo_{qnAGN$T)~F^1Y2iTJ68Q!A9ThfWBt{to<`&lX9X>tw)0s$XYyXL1iQ?-nVoqg423t7GZRU6x5j)wM( zYM19k?8bUPKmzY}ggr`BEkBvK%{_~*B+Z<##bgo$Q-LYqcI8?HUaOYZ`Z|W>`p4YH zR`CzwuLQ5qgT5%(MWQeI^kWrs&;{~oT>|1t3Z_gBN&`$4@NOrFx$o$PDa=F#>xD)Z znUcJ+x_%_`(g+HHKw+SYmPX6t>m#V0+%GF?$2)~y6=GL|;zC1}#<_jS$KtEdBe*fJ z0lcb2uv&~fMpSK7Ko}G#;@_m{ZA>4X__O8h@rGo;rA$m!UtlWrE`M?A4S9JH*Es|` z-lx*#O2AX)MIC!q-?phYF+!pU0h6W}rxSWS6@dKXOyu``*MSSwjsK-W65>{c_po{S)w zmrCi%N15Zm(pVbdO zfdm?N+UaF{puiPWEQ&QG>G(qGPXfUN*S;{Xpa@0|2_~8=GY{jz)u$+)&01Me1AG3! zZ}yMj>g2rxP$$68k~&ey>SW}TuDM-!L0vmUtXk29MH}YJO(Z9bJoIp3g2l*z8bpis zCP?RmGyQ)52!B~g&>Wy(lQa6$Cm$Hm2L47*VDW6ji#T!ro`gc7%1}j-uf)gY>jVF{ zAiC?|F^t{fLnVAHOIoW-=sk!@35tKf9?lbjLUp(zp=s0@zql^aU}>oqXcI&vD%+|_i`BR?AHHUxt=wYk5NH*J`qQ#XrZP~J&-Zm5AzHYoN zTgVmG$;-_SD;yzNB|J08p^?io#eR4VWBx;6BjhOrW~6EFEx@Y zkW*sq@3)XGBUD%U;Jd7JI&%K8YU9RL&p2|~hCBA}S2=UUPMc%p;zg_ep5tVzukSs3 zE;{0{JJN%gBm}hz5{lL+mPnss%2PuWC{+sOU?Q#+&QmU{3A4=x#hq_+!S=EWmVnQ% zCEJS|+&*fqH~Q+~Ili{O$xmnoELOgZ{ly43As!u_{PXKWZG4Ck=YhPKl{IQgYFx|mhkr4{uDY)=~)0$Y%w0^5}GD#lU+TUA1(FBtOjz}B>1 z{2JVqd!#ACxr+(K@U|u(#B~w7=Abeq#8rpbg<7vuBZ#cglfMlICMCkv=g3n?5Iq_y z!gannA_$WWss!R{kl!|Lh=!!PaTlzFKOlc~A{QMz_vi4Ua}-O>S*z z3YoofAR;@BJh;@50}DoH{G!YwP8fH^q+vr^<5&5j+kWA5-aNN@=-`#AC_nndFZ|JK zj@+40n?cAls9X_|P{>&&vOX%6rVExa76%1I$#d@0V&8?D3+)RMTI716e$oN~e^r$) z7|`lm(Ou8%b3^?LD(C=OqRK9kV|B3U)oPiggsIUhpEZe4{@b3tu}zNhDxs3XiiF*r zO^YNR|D}>~C40RlP8Gvaps^ZA z!(T9bvHU71O2}%2H4t&}AON4o1%uCLNyK5eJrONEx{%@>B39sBNZfMaXJLI1LS0Ai zh7mwo9)p{*?i8B_G&v=tOl)32)P0L%c=B+mOc_yv8B)Wu!_<%QqfWu$2zq=42(0&Y z%80O;96lpciL@}O;OeR%6b_X1s8JB*J}+)pdc^U$1B1TR?v;|5p){)8o< zQHwl#m!v~&DOLa-A?o0(e36nG7z{0PC<3t`V2-t zea#nr_GP_#S{WO?X;t*MF9%f3B{kz5(d%BfwF~FY3A}IOmvEXsLGKlcqwJ?b-~B%% z@4uLOU*S#MhaDcLkIwr|zC2PXrDCF#MoLvYub}yZ?Uj{%`u-kHzmt6pELq08Cm1 z!7hT8>xId}Z3m6ORS+vv9APBlItrMdy+}W6bqXzo+KB}tHuC)`dUlh zg0)3wyk5|^X!9|R$z}U`a^io*=U0ItioNdQer_RR*m|bbaX*)m=rsvhmBDlxtwyKq z)icq+G&)hUEHe#33wcx;+OyI{tybttNUm`Bd=DbOk$8^FK60M32G$Y1^bMbtY)Uzm z1r=P7lu$e0n1b1ktcrg4##nW*g$t*^i2_Z$pV8?jLKO%{>+HKH z*@vs=-%YX)QxA7TD9PUH5+*i{_Xmr3GRc-mvIAt2l4Ym4D&|3zno9Oec4oG{**C0T zGIyC|-@@`EMynVcB4yN)v1Liyny}Sm%`@6`x-8?onKS2PXKE>*bL|6K=V*67_E3~ z((<^qOYt8BmJ=bB0YWftMxTd>Tb1W2hnX#nOk-9N((~-UN}`3on~%It!oHP#;$;xT z+#`|0l-wo8a}^a8<0|S(0;Pc5R&P|!Zu3PP7w#T^7>OL^c@Hjd@Y1AtiZ zwqKBxhwd&&MaZr>VVkw2BqO7wrlfl0h>YTlqF|sX;L#(rmHEy?iMAE!HKIz%fJ_vC z5+rDpR?BM?pzNzTI`&<=sLPXb-)42u=({_^gjtvtmplJr zr=fzJf=_4&*C7spl#UYS8P0G3%8ci>xHSc-0uQDO4`C*vSN`6lr-+=aX`@e^v_>EL z{VfEVBtdps`~y6ki-fu26aXzTGj1fq6Nu2MMwXstNJ}^19fR`WdWL*>hLDz?k+xLi z9%43f;rcEJH#ERt;PVQ>(Jv`ZVvW}t`H!lFeCt;&$+v0!8ap7}t{B+bNL~APXs!+Q zP9LV;c#?cz#owvkyy+2Cj8OyB;Ja6U!3KwC4^!pf3KVW=UNOmET6I_yy>#Sd(By5IvX*$ zsdm!=0?3RNva%h}d9VAgB+}qNoHdq_<5vG*+rS`h5_hy+fc>a3K+5k41rZo;13T^JNqfaeeYj#qtE_%W)bUXLjKy?;4` z5b4YhyKCvSsW~(XsLS`{=wI?H1uwxt_?`e#eDdMd?2YK6zm9a%+HXdQPw@-`T#>K+ z%#^hsJ^0r`))f8duj`jAIcTS(V?1Qy&B^M40?$t-(t#07t#d1UjvZGnUAhTJ>L|8~iLf{6L#LwhuIrlGw{&fX75#$Z?y zdHud*%z&HmWK2dzh9kocGm)adhtF(AKga&vWo%`82#mny;wgX;gwLX@|6Xxfd|;t7 z5C9e^0E|Qy5}xrD*=IElN5L6%xI)gZhYACQZUWV0e!McVGlA=vV*K8lVMEYde8E!Q>3aok&+a%JL3u`LvX^T?uAc zg=a{f#JD_p37=$3C|NAkmO#k5kM`LCw7`XcHu~*9mUOi_09r+kh7c@pPW4zsyK6focyMeX=*cR=k5OY3^110755ST+KFDZ`1#3qz*fG8NE_YiGJ zlnDeNh!diB2|Bbtj&CW3$wQO(`sWW01#gLWnKvdu*+Kq(bDvC z6Hy#~S`+mqSRgr+Be<25Ja}b8rV|y15){;#VO2d9V0$TPWgsoI-ls!5FqO!8f z+|Zt`7gGYDj&RK&as!ldf*N7O#AW{n=Y;bsqFevz6ydY~#u(cmGSFu{E-{MD)*z*2lM$hzLK8BC$S2^!?FYbe;2|bKMW6l7TXxs zdtgul0Kt>&1h_mOPNff~g7(C*E--VGFqKHc3j22}!FVStp*qOX{*G9ezLtdB5p7T; z)+%Zl8tl>^6cCS*QV=rUq5f_8x3Dgw$Q~WC9`4v?O_>C+i%-V7P~Hhxm%a^La9F?n zdSpI3+%;oZpL9w^wDOyvy{%m}R6w~|>yBRfO+mEq+xy@Kk}AWp;scWT&85#UU|g-bmjN;d;bY3acx%b!lSnaBuDZC^i!%n@Uf?Wz~t;QKVox#o!hvp7ru3Q@n__0<{RujpM@&G}b}N z-_TYdCEpjR3RYM9ywbTf%o;IUZ8j^x5=V%D>9FFVHc0Ac4IX_fO-rUK-B2`mXJkik z!`}27-PU8tHkm&g5rS4Q8O;`m{bB|AP2ZmVnkMSb_n3+O>&{P@gH=m<92-V~ z1j2BeVC9nLgZL7 zvU(X=G-e>O2tcr^Kow!@Khm)SidNAj-KS@H5loLjX~utvS|08%6gC~ofqlor48)eX zeO8$#CG+h~23v+9aFa7IOc@>qRnwKO6*U@9JApXl6uvhJ-}v{Qa}!>p-y?qI zcX*fddL}DoInvje>>m+`?!;aMb(dZ%s?{{zSTo6FmK*94A<|*!xaK-P2@iUPod$%o z45JJ+Wu>&M&S|$Obj8$Q5IEBE;E0kU zJnZ-SAb0d>4yoS6HjEJkGwPj!dee{@C<+H5DS(}z_$J~MU3l|!aVqbS&0x*W;^8iJ znyn=J&Mwg=m6(}Nnr>!+%jH89@a_e8#Lih>fjzVeg+nnlH&+mHJ-P1uJRDwU#V%c~ zCO5IAIJu`(#da)`du#;&tEoz}C5o?N|t%!)}LGH?5&DUUuZ{mIO13cN`A z(#(n`$*+XQ6urs0G?H@eD{zz9G2kAEv7X4I;QIxXnjC4lg)NG#qMpl-(&F%PIB$Hv z5b^pDrIa+oexX67Zi*=mRe(xK|5EG|=*569pu&*IyA`B%E1R%z;glGqwgM!m7oaZ~ zQoL7CE`VTh$kz&rn=2G9dWVZH1;xcfl4FNpI2-5Y7Y8x@0(Rp0Y63cqL;BXrf*R(5 zQrCE-VjFY$*z%v`*>>3Mb3VCuPLAE4^X-dve9g1fWtuUWSN>2cz(Mc zy8#*VLO{vddpHU=iLlDyO7T2~6`dusCQ#}%`Las`zBCP)G+h;}N?Mu2?j7Hjf`OAw zd4vODSJOsM4hG66T_$G++#2#3#|WbfeEuqIfY533O03c^Dq)92a@ooh)r7OYssj*8V?QDBQtN01l#{}V(DX~&aecHG(QLptQvLa#+RPvjK z#~UmlD1TcEF2ODhKv|>-F4;aq-QiRAFEMJxFklzH)K=xk~;Z z3EHCb5>mFvL9UutGgZ3U!8|NidahWsO9wl#pX65#=A5RqX6Q}of~ix3>DEk((%Rj? z*4mVnYI}w;SK+nm^fu?&8?&5No5Kl9oo!09+iZ?83dQJ3gDT5xNNa7iSu)df6*b?^ zFd6k}QiYS%CX6v!O|&JjhctP9J9p0HC7M`-151;Oa9{ynggmNPBoz9~yx5|uabR*Y zs-ZLf+sPtkdF9cY9KuvnV62}et2d85&>oi$h##l$Y<7xlpb|dm18>SK8ejRQKs8pr zw9>_H=!cNz4A{p=+`!`Zc$Ix$d1VKSgKhMcfE6;8(D-(pWnzHJuiqeuG zq>bQ(Fc10+kT)%E7BDH~4P;gvCXo>I(sgd;F3o)N+I8KTnK^m3ig4t6^ETBN+MG^X zVg06g-;0DRYu70lC!25QED&I}EIQ8k-F`}rwk7A6-cZ02We5ts(p?!(s|ykW7El4lNasu>_av%_r9wc+S|BX%NcauWI- zXGg@htHf11Y4#O%TKBQl8|qe0KFK^&?s(`Sdyd#e^Zn=LOPZF9zGzDL`m>*tjSZbA zzAQcu!RQuhs^KMz2u9O+O;9g!y@)C4Z)&)6^Ic9dHGEY*m>LobFncwb74+^e96z-< z9F|iy`~9oek6k(WWtP!)$BrF%|5%&JWV7d-TE3)tal8f6^Z;|t<2#FFQLe+LBC%Vm zgo#TX#~x_#-oWxsU>!sU3~;5=OS;$L_0kA=i@b#e0kg;KcI3+Aq=(7zQh>=ePv5}Y z$p_|nu@K`yV{-O*+Z{Ja89_F3B=B0&?Y)B?Furd@#HLJehP1GN*@JO| z&w@Rfc~+={1@w34na!(SusO~?;IxUaOlqC7UHVA@Ya0chB_3kOzDqUsU6RD@;v5OT zB~@dW38g+SjNJffY$;gCZpXurV?1(qA$4CWgs|_3o~DOCDP%uk|B`kTF<-*%aUc}{ z9KDK*oWwNH_+Y~02F$P|ZW_X2ONnE!85mu{+6@G}e&S(y(15{~s*2eroM8bj^8dJQl7a;+|@e^?>7XW|>+;+nQm=Vyz@(ur$AvS=ET}AaEvmE@lPN z1EuWZgxbnO6$saWBWbS#->qwJ*J)H(TzrDgQ_e>XR_>@8%4YZq0@c`&p$Dr@FJn)Y zYvKAN3~h36`V5EUH>%*EBiAQt;L1s(M=<9&rgxh&b2b0x$IekxM&-Iprt12<=lGG8 z+Owy{+jQpK%&c$!$gXXk5mGChrpz3rw!Zc3Sfj~g6qiW1jbN_GGQ3Q^fHuS1DInLU zg&i0w^cF7K01~c@hb@&2G+GOO!HWPT5H++Gm0rr~NFEZ}GdMBTt;n~O({$b|Ry5u+4Q24OK-cYP1h zQVR}CgvE$@DG%UPvZ+#01=eQ1)KbAdrq+-(SRvqGNSt}o3mgocxJA%az+oU~)oOmo z9C#^yGzokF0WtwE9Q+X0;zh=+i`#`RgjGHdrSen#wA6rh2^8G zW;aIKuE6?Q^4VxI*e3Os>K=mykxu$=cSNakuh?Fkyal4U!m9JOEsg|?!@{c zp-u{bB%hpL8rX*6DCqU_91cf8BN=ujU!YcV&XdKrEg`>YUFp3h7(unf;~Qe zr71v$;C;1h{pAZF`s|6JAwfN0XzaKL87{{UQ;BIlt>Ow{N9KWNXSgl5b zFid=GzT*RkPB_9<@ybhwsvW~0ubj*A#5JNC`|6mLxulos*l!oc1!DlIHMm&>3?#b` zCk@5LAlNc~m=>3X-a&NG2}(YmtP9E~AAvvaA;LbzCd#5>c!<)MsdF7h$#m~R-aiz$qsP_LYf>Y^bh2NE3UAxlwuqaI;mheP_Np0!cydE_#wfB8WK zd@1#5XX_B9a?oUm0$C>m-HJ6AaMPlDtbyG`s1DL*In*vW3?{pjN&X63FmeLc^Wqc| zQUv5K<3L|zY5Wv_k**7~J0k$ca{Cd2RW_!$*6QH(<Y=gaF}{7Y&T>Z1|6nO zRuv6LYWorgt@oAjHVwYb+d<(LKZRR><{SIgp| zszJ{Zwaf|Uw6wHLX_+!<;`oNTTI!Yw-ScF?Box#>j}=FNBo5V8XNvsfslFTBV;cp8 z!gVbyHv;qw4TGZA<2*ZAd3E=q8JSr|tEq6jBCFnOw>Xp*2*^XE%8`c+B8J`lUWbG0Q;PS{}-*ioP=M%WubYpttUO!&JK#mYIjd%fm}GBWZ}_1kbH!43A&5wq0OXSQ&}$B_ViSC zt_3M>Zcu}EfYFwV=^>*H56K5r!|W!ig#G6doMf{3iQCUkW7k#nB5XsOQWw;)i!G4tR@oY%qqglv~E~O1i`Ft}Nyd#CG`?fGZG?qL@6! zMF~q$sg#EkBBwTJ0+iG)wa#MqjMx zK;5RbPiP+BFm`mMFJK~Qsy`ieVJh&Oa2TWVwygrbbm*V#=$$2fUCDy>Raa;Oiopof zM)f!;RK@)G>52dFH(J01@q=IZiVz8nNzS{>1Q$3yMu@}@q3NpyqY($>%eBztf7jn= z8NQZbj*k%{Fjtkmkc-HwDU!d@GF&}aAVd6a4b{;95WvLII>Xc>&oLzO1i{(TnDQGf zG|BQ=2z?O$|G$2tC2?*OV4fllLX#YGn2iDhgt2g|tVRKi^V?i~;>Aads>E3zF;eCHg zuJwu>XVvze@wPmN^{nKuuD0gpS}(rXnk%{r-ndwh-kHbpYQ?R*`_Hl6=gwk#zKHGl zXX%)N#3=>h3m77q=9vVI$Z4dqOPMWUUJS&(c`k^ZOHlB%;&~p&7!3)X~#hd?aY7l}%E_W;mQun8EeBbCU8zP^E_Nng%q%le_{JVT-A2zcX^T-cC&1CqK!xk>?NA2e1%56ha z&j~xFO`XWPMt!0mLB+xwSsT zec4v$0SA8pEH?;c>;d72QcG`lv7RPc(YNxUKFaPJFw{)Q!Y-lzW~T+W0JIcI!3A)> zoQdX`0(CwPrt46S`kLE!G;xaL8+cQ0CJ8;9ERM)F0S%vC%w|W#yKqmTILH1- zpRk3);tk?&(U~Bki}AzS0r`V3sZB%bg#lW`n5P}*W>83pOn*0gqIg{A{Dj^F;heCm zpup`0(4*8^P#P+62i?Jdp9aus@>xyk7{DJdVflU@zeDYH(Ps zj(WPPcUZSbe_6r;+1))8CSWq>pM8j5oPAJ`Y(1=aadH(PY=9pZStd6gwhiTZ<1=D` zC>T)4Q8`20mxDmWNglyX@db9Bo4b6DU7Py`1&TPGI{;w54GC__6wSQ*KWD*E}?V^f- zAhlFJ=e){ju^4yRoZmY#s-t4_kH*(kHc7KCV^tiWhj}KgFf2eXwR{jj&*PzPdWt*% z=;7Dm^X)m<6M!E;A%Nb8mocv{iBXd>S1_ycgT}f_sb~coC%+ntDQ=|Dq2pqghUU#d zHlQDpnBakw4@o7VJQQI3GE?GLQn)Qx8lbQOnHp8Tu{sE>7gn%f%B%meg4tDKP6OVZ zbvdiY@AVNzk3-O);rnCs#FQgfPFt7bJOzLJ{aXA~yX0NTCXGkPD{+dF68K~#Pl>ZG z=n2>l@$fYyIT#1ALBR0>)*#2gIu?`{DrI#Me1}~ARL3pGDs*fV9BJnhw zAkuxjW&&@>=k)azk_}Ho2LhfIEe!e~R)zd=GVJfw)y%6=_`LKBBho)t;{eb_D!X+{ zLwAlnJ0r8oH$r1*t!SNUx1C*MwX=t2mExp&OwlaSk0}~0oCv3x5qV>6)lgV*uufq3 z=|FqYnAAw=AmZhHvkdBF_;gAvfZh+H?nj)gIt)Kg6tKvLYv*SNZqM4UrCwV%lF(mriJQi9Hsis-(-ILj(4`s6YfpNE~xv#r+b z<+L~Y29wolx?v%S(cAXc*H%oEnyzM}&Vx&IUI3SvC?_L^T!>oimk~w<2$vY(xWu=v zX1|z9VdMl@)BuqLag8o=IobD>c4Zwb4&cNv98gEc{a4sbqiSkK(T@<)d9n(A3UPrV zT{^WEdE#f-Gx#BG*V&)GH*Jb&G36W7CFN&7FlHN#EK2M53mD1w!V2{Z*T~lKP+&vE zO;e4*VMzv~75#BQMxq9Y$iZ+$>DG^#Tl(RhOf_;Vrr^aclg(!8BC%LRS4GTW{feEo z8jaTL%|@g7A*;n=9Z`4)+}ngDAUg2@K*?sknmK0YSWBJ^Kre zBf&3ntJKQgL86?Xw}8-4G8~~+k7}yyN){olg$=nI*ugUF=)!U4DlwUsCmY7YF&}6T zdQ%~clJAJ<+xVOpgp1C9+~l zI_ z<4@@ivcW~YtRV&Yhn?R8w*)|U_Jj4sNo3?70aXSg|59oR=aV_fE8xO2j{HN&D*he} z(h$hMFMHXV=EWFNo&<%*m>h{gh(4Os)KM4==(9*KUeA_sKwl#OeK`QyRQa*xo9B+> zze6wR6sbVIxIf(oumeBPnOMUk`8=u6KaS+X=5G_Tp!koFZjx3Vrz8&o;uBXl4@cq$cFexL(N2&%iB zl5h8KW!wGu4KLVDahzOSUUCtZ3X*Hv#%`UF0(_H$&xw5pslYc`2qug(tlI=p$_B9n z?oGOFJh-^!`)p~51nM9}TWV{Dg0oqqBRxzj>27565^^ICKyo?KEm zdQ#ciHw_1ufyFc`{N{$iz)}5ZS$w;&g9`vf7o-K>SRQE<20Wga&Ki$sqJtE+3fOm=2ii~Vq z(HN)GYd1TSfG+pqb<)Z0%s%E%G*Dtg?FJebS4LW)Jq5ir!l%A=>7XHG6_c7sf^`pAZ$=wDIX2u9?U zgQobN9c(?V;=Qewp85gnzWnk-8PGyF!9q^C`?3DeM-A^I6RwP#a}bm%W*m-ZO(nxn z$VOj(Dp{^U8t|7FB)~*gY4=80(xysrR9|!*C-@CgvzXQB!A1A2|Hpy!lDK( z5I{v9MYI%=r%Ze*qP3PX)$6ksky=Z&DjF2Eh-=kSOVO&O)`jw@^;wFRs--T;|NA@l z&XUO_#QHveG;@FVoZngRS$^C39THr|30XRtFc?ePJOD{!L2xcC2*GzKX3*zSJGQcj zcH0vaAf?d7YeL}ngjJPxr_@tgxbQi&Q%LkV6j1OXEtFKik;_RD$}A#{aIMY%j5ODw zt|kbS|BBWNhk0_${dNVk0Dlo_#!;GxBmy`UbXX*MlHEC|ATsg#g}~XO;c!V)FC+sb*ndqGD!YI8}{qfwUOtG{}Ce>hOXO7$1fmA!& zOEvtlp79BhvECc1?H1-uYlcVEm9XZF*^M>8g`~g4? zo^&m>J&8TiK5;(l@0PYr2x*&G58nK|2kV5Q}uO8J^mqvj0B4xt&4*-7KAgMLbd#d?8>jfabb_Zb{?~E zbE;qdkX^qN+x7sVPu#c&l|m=H;u$?tt?n%%wv3 zM%<4jdjZ+T&HH+`ap>FsL)*ASZenPNL82M6{X4dC>2muzwsGn5`&zbfLu9DgP0c=Y z1h#SIWMYw3v|`SXZc!IbQu`LOK>oZrmQWf4xhaOA8d`$wj>Z^SO@5s#*3cf^Mh>5E zfOQ+uox|G5(KXMyja(uZETaQ*`6@PYiCj|alGa9!=0#c?xuNp;sy1>%h>- z^6Wk`V!#(D>F?kb{2A=}Rqf^QdPPNoi&!GcrOiXASgVD`ki1ni*BQ-TwPri(u7GDd zU{<(lz_v;-Ru)e>2QSPlioSA}uj?N%!+BeO9A$U?Xl zW7Q+17?s-5Pm=mnQM0Q_<13;J@^#o#r0(P>O{!i_{_9~zKqYQzUuZ0lqQ5;ElED^aXUe(qdtqf$|W52 zeYAs(FDD@wby^Rm4H7~s!-OzMyG0lGFhWsOhZ{uH&b!#MKo9y$COH6=fyU4`)i8rn zSx!y=!y^ok|oe*(^Wuf z;ZN9B5A=m}Pcn*uA6oo6q`WdPFCMl1erV|r|Ab8rqZHvu38z|h3sv&ftaA{kVj$r0 z1aK<1Jm~RzN-Hbzy}at2d)N)A9|jQ$fc748bX=}T-#K>cdaRKscVPX>Na2q9IeyBv zF4T!yJL*6SM-Qqf6y*tnx*Drip^#GXm5d(xu+~ zU~<#V4~qxPhr*=!053|&1Pe$I7K=m~eX@&P7>LgY%3Yri8fHiehWwQRGxD>M%m*y? z`dGjr9rA+_4upg*tKnXDe@#vM=uu*%WA33^B-)p{X{{0`j6hMKZbgzdU2%LUU-jI_ ziVHAMx!dW?nNZ+#7W94M@#eCs++t_pgua{9x%V+Ie2hxy?8W`lcnKJ%T4{Rwlo~Q< zJnhI8I!3YDpti^)Y2d&Q9r;9AHO;OQ^AdF`7%LG8Z{#5<|8yy7qTQ z_3@vxOSG|zGKF3qzc6dY*o6=sV|P)SVGEasv0)3wx2(+T)r_jW0k6NZ3c@F^dhmXB z%W%VX!1owf$3U4I8pC{b#{=vK>4q&T3PvTq@FWjV5jAMo#uC7Jl0Iy2c#!>Tq7XJ} zBUUI-ooG47k(8uxO}U=B#KYiQTjJl?%^E`xKXL3mkaS#^Sl_i#N=uu+w3f`*0_v8> zaN4{3Ay!$6%|I+!C}_Z-#v+eFP3Y?V53$RMZ=XW>X={L~3)%S4iLej}4k3Qoz7re8y$`di=gxgZn;u$)QdV@aPKq8( z7a_V3I<0~8=jvWIYD`@{TjX@w3tV=)>xX%+jJ-B*UVg#bYrmhDlapKFfZmP;T0Z0Bsi{b=ae&w+vhQeWH0cC=iF3ak(LQBoBb)86H_$JPNk zGe0-WW&A+a9{f2|t{Wc7;ID(8{B3pjlg#PrqLyK|M8TZ^$?3KXr`vygk~O5pWlOdo z#fA2>GrGHh+(z57i~~Hf+%B9x6I<#gB0}tbNzvk(kF+sJh|!*xx=N#%SN5|7MesjQ z-PMJM$dpM`OFPIjs>%L?tWmw6FC_dlg zp$ZV;R$Q2waNKP{86Q2xE{O@@5-n&MzUfKPDNRp`(VK3wNs`TL^EmP(y0=dO57H0} z$w6ZLg!PsZRNr|T-uIyoAKbIJg-`UIH`4>B>Gb)KfqM2cY+>XKX}0u>_KY@2qzZDt z_7O8i=N&?sR&KWCWXUGUY+9L{t-v)S78H_*(Urh0!Zf=*E-o7px^VVB5VL*goYCc_ zvt~)stl6_>cbqAmA>kIti4&R{!?if3UFLT}z?NqjrLK6EHNu>#4oW{w>uC}X#cyi# zo76qevMQI|X~_S65w~aMI9YyOj>D1jNY&PA^^<3rKd-=k&8?Z)jvU2sjWgS3fBWsX z)vD*9^`z0BG){{gc1 zo-o0v4UO8&aa9$;0G!Rz@GxU|cGUtCdn^I zj60oU9+g>~G}z|6z*c61$rmg$<=I)d@-d9bTJ?}3O?9c0evPv{ z(_dtxQdbwT@_^I$&lg$4XkxZVi5s`uYY-X*!BbBVMD;jnY`8v97RJqJ>N_v8=xEIh zgwrLZx-FF8Ace4Ff@mOAoZ)69q9og2VmB5Ohc1w099$O{S|XrGDyR?F(-juef5jF9 zM`sL^aS9}FxK3Pi@$9eI!Ym8`ad}vE8MQocfX$~MacNAo=!|O*u!U;H-&nRu?+o?B z1FU&qNZYSdL%ugC0@+OQgy1JFK>*wgfZ#IkwT#c z>sdBWw#V4kW_0CvY;(WN%F4Y4PQ=ZbSNCL^vmLH$P=Dl7FU^yF&z=QSQQTetS2wT~ zMkp3!Dhqo6*5gWp*y$R7!*2F^+g)}I2~pj^X#s~3pkDbH_lAun{6=<2dK4lGJBh)v z638?ffsIo2)g68|pFRkGXqO*km8ahaW;?0<(sh*0dSv319Obg&R(-!^?IesG)qh-i zk{{5eZ#c|^k_V-zL<(}Z8*a4AIyFMuDOO>BbrxH_nq^sYjB5XS+1`?v5Gc|1CATKnW8CvTJSr1h&L#_7R8ph6_p7z2~~%5O38UO2?QHvtSRqeY`9ekHS_ zTq|J+@h*rZi6p_i!y?iRF$I>WF_i)E^|(sRVi~n}>nk`n2V*wPy@Ct3NXd#6EVUR7 zCZ%p{1z2yB3_mGa1WS$Rf)Be~q7gvpgWZx%@&)490&PChc;WGdyr~G|B7VK|9vBtyk0nAY@;i1IY_tImOEqy7T&=;I9rR&vXN+|U-KO(!T zluYbjDJe=$Tax<8>kRi$6sg`f*umL7HU=vvmX!%SLM?qHf z?a>~XH2QPRet2Z8qS~DK2X+VKwOqzA4W$Q_(OE_KAj8F#sMnx*+afi?^=@T#mDy6L z`rc#@)*_j9Ow-B~T>eJsaQjb|n1dIiM=jk@^N0;YbP7l*mHm-947gXUA$t2;1#0+@ z?3uxO?NuazXi2&2IHA2xiWVJ^Hb!eV^x7~G_m>F~^Z9QvwHnUVj3mQ18ZL&~Ad`XJ z8G32%*|PT2WC^h6{w% z)_2)=Vv{Y9nGxfzXhATwh*OmcZtZ@n@@Ofp{wU9OOljeHYew( zT8}c&JfrrGKFa2^$i=vfmH6CDs2yzZz|43iW^+b{X$c0tpuG}|#u#LWaYwy-+Ejwy zIDLYw0p+Nf$5=7-Wc4xjOayMnFtmv65!c$MsZbd{cZL}xC2BMTvZ%)h@X$&uFc4DK z*VRBgB~{m{tN+Z*23*u=S9e^8t@-}rxWVG~KeJ0(Kw)vV87@N@EI5>ltd{u3fz^l0 z=g{g2vQ*}0Ve+&}69eIKfs$~zDv+VTIrV$&i7+%FGF&B!NvkMAiH9iwDltB3kuoe{ z#igyi#9xOCLe;vzupMh)8mJ>R2r+Sjp@9Q_j2cYtR20F*3>mAjE-u0325f#`alRX@ z1ChbRh)jB)T^*aWV=>k7F=*v5;{Q!pVevm4;Rqiq3N{WC2Ami*vYe1Ne5GW98Ba;k za@vygtpO{0kk%=A^mPojG~7!lyZXG|l%k~An_ z%;yi-FfM$5WiP_tPX<(r9Z+CTkZyjY8Dk`-SoGl%IE*z&Fv$ZP>`%!F3*4VdSmuq! z#P}H=PYj}=QJ~2YdC`|>Y8W>bwWBJQ`Dox?bDZ6imYW3m-}X0F8+ldx7BJKWTMR<( zVq#3dQ0i1jOz)>J^02@s4xdB7IshLXL|c%y{*E9o5)g{K77m>mNpm@5hJ#BKg=y9d zg@rjeg`vV=sV}D>Cm(kgL9+-QMkg;&n@_N!tb&5FvI1rd1PD-9oM2@^5XI6>{rlO= z{G^mCdD@*eTwtj2ZbCvyOuFJx??1t22D?Z%uaU<@XR&T@074cBUjv{D{KPL;N$Q=K z1ufi>#kM-ZNyI!sSJA8QPL;-13jLKTH z5Moq(P|&HMQ&wVmlR?U#Lr>T>ph2>97G2&*7rp|SZ9WaI=F3G0SxaebmC|~ut5y6a zn47=!ceZ6TDnSM>85j-JQw;5eTS!rvB%ZiyPi2U>-m6Zs^G2S8XWqD7wwCw- zROa}6lH?ohs|pDx6XL^L@!|8-zI9Lx7(QX^yAmHn6NpQbq}R9``*}#3{E)I@Dr|<~ zK!O`2qIiumGh!*nCm|2Lc#*pVQweFeY$OI3THw4ud8rV`{O%LBxp1@S#dFZ9B!!EzXR=eTh&SZ*FS9%ca0*;V_i&5F26Fsx?$lleO&IK`!-6}WOzoHYnB zSKZ{iOmnEFan6?c3uZQFW@qN)@zGT^bFZJvwXJL^D`mM&D<}Gj`aY_e zV02m)r$xQupRB+UYQCVgW??bw?z>?81#Q)5jq2O!Re$*60tr040_lcEwA#UQkWi-9*VF0gkW;<$PDt>wzGSW@$QdCO6{Hnr5qK~fV)Wf; zh!IPSkA^E}rK0kpkiUe?4(fts7-H&qU*dFhbw7307+eP)+J>B+P zPo>FY{Z8ln*0yiV3x2udXZN$#4fE$;(b^GN@5(8`9{c6g3(uQ(-qLdxE~8Rv z`#+R72yIjW=2D&1#thoAQ7n*>*{GOTfzp5rCJQ{Vi*+3oLF?DBRwLpKzlF|N#OSlIWMSCmCcLuk0_yu63T=6V0Ve&$6+I{i+(%CPf7%SzAYISjO{HB%zq8B-d0IGybir4)9{LEM_=*KLgW8mc;fVUx#IZ^YvD%)o!)t<+}57 zO;``UV0=pCL5<$Y2r;WXDvukj;&M&A8T2=Z^oSrGhtt<4W;XR?WzcHOa7WK7$&?9~ zU=~frhjD{(VYP}yN^Y}a+pIr*8apL27{FvwN-T6psAkA;f&jv=T;_jKa}C@N@f^Hn zf((GRCEjh?e56GJAG)ETva+GAp|!HUG7KXZzZvw&N&|n<*MmHTj-Y`Inqnd;%vyU{ zgPm;q&Vq+l9EVL2|G^&*)drQ*RpHP|G=yu$ln3E+g=Bw!G4hYYJy5X0C92RA!5=f( zd|WmaOt8j^OlbX)rA~E{S<7R&nZHeIrxjnQ+yW}h3}Y88R}jPXD5M^}mhL_> zn$IC`I7Zy4VKf`h!dfcSgeiz-3xsfJcb8Q_I2;Xh8R)0UIU^}+^?(JRPRUpQV&V5z zldTRlpA^W#ln2dEwLQ}nM|tX{yWmpgu$8yjTZ9Bh)9H}Llr+JW8GK>@ji=!f(@8@$ zD%j2fTfWmC)2*uC$>2+xKwqKi5=7wSp%&s?+H1+{5CqsloNJ&X``mz?2v-PTYbY(R zC^MnnrJ4NBB5=SOHNl~_N0}g$iB9$tq1*s(A_eNdGWpCg;D^8&Nfsu7AF^09Hrzr9 za9#=&reIt0>|MEo0E!f`Fnurw56RkUc^$ z4iZZN>6NH32s%;W>Z(8~ZJ&7|d(ez3H`#bmeID+L&>j9OFwLneVbdR3(!J;Mtq*s6 zd6m&re&w9bD;s9CZoB+5>zHe&v|m|kF|y<84K_YGw)IwEC5M*<`0=HEfOi|LAV}eF zMPp|{Ll&kSu^C|kpWvzd7@+ABw5BbhJtUDV)k3nO^!1{N54#g}OExbDf$~r`e?Av9 zhs3X=nDy|pB4e6v&f&|4(S$B7fo1_DNjI2UtemV<=-Lwo0;FrdI+vecklu_!=seZk z#oVE0=JCLI%ssXq$8w>TBW+HNrz01x$ z=mlVCX+t3&^}+!=U*aYvj!V}O6GsX(qB29m1lGa3E1)b!D%2k{mG~U7ibJ!aA(oZ~ zp)l22d&I}R>i;cJbn+z~T|y!WVH|0m;6LB0r8q2Zo?b754c0Yu`(5T)=A#$+nDa)AY-eU-*kzxekZp`&qfR z8*B+s{gs;+7E^D^63E_oZ(@GSou$E`_>!`Re`6+AZ4w}PP)W|#;;VrAhGQ};+RFe* z^9tJG@fEbg;Fy^4n7u4O-*1dr=3e8twHU33LNM5T-;0W4==As^aK?O*L~sxQcvh#ka_}bgXg}~n0QYFA&~H> zEAqLx>X7;s9>vJu69?)rvEe5J`zlRz!Na42aitHoL81*y1tM6qo}n?@%M(ZtrEnR! zf)h%UX;-c`T*(n8dFZC$IDLo4-NWRAgAmo)lQR(Kyc;!?IA#MpcZyF) zG5CYA;q%@a}?3NUlw8zTvf_Pwc-PdE6~s<p5R?(Pm+)$|RIZw7XW1O62$*|14 z)Sd0gA~@4BL%tv<+g)&hNwH>H7up1@z}&*R5LM`V5%z0Y@=e&NeC_XAwDT}U5K#;qsk#DKN{kpVG!COMH{-k7}Zym^P1V>Ox)>P zNRIe~my*=h$)gLMj>7s0l~oh!CG})E{|ReVZ>!)p1czCzy)aAl2jPntYv3JALCG$y zb_s=~tJq z@gVQ$md^5WNE+M9TbmlfwNL~@1Wkt<1DWWcXGZh=CD=-+YrvL5$VfTzYe5h<$aAok z27N}L?HI$?kB81p4p3JTzBpS(wZ^x&AchU?9k6w)@WiZF9~#45)r$*q3o^>M4NK|7 zyKFW`&U}Z{?wX(Lv}I&0n9on-lmgl1 zlO~nNMxp!*eH2==3^RMxM{57?M`1}xNmEH9jl#+ja;(iCv1)R&OP!G~2+ zcMdrw)uJ%>@`5V$b0-g~i^KeuB7GP_nSikfrV!b1qIoa`(TO?r{6EKln-a60kwi&b z3J8V99l9-bx6#I3PeHEBHd(RZ>YD{O4jOmzgV-5M2B98TK8jg zW3bWW#PD0I53qq02sw{V#*#OsMbk%OC0sa&F8gU^2PA4tGBG-z$&}3+rztuPGIEU{ zrJ+c&xz0vzkM~qx4aV-yM!tOSujcaC7z?Z0=kXdA-uv5myqV+Gx{!C{b@f8NJJW3X z_L_@p#?{uU70dYMp!QbRpgpHFDjnU+zrF4PrDJ*bWtXhcV<)TcF5?sQl;Qflq2)Xm zB36-6eS%*C&x;uQ}hRK|03)`u% zFId0Cd*5N_VT0;zl3U^F>9XhsrL1hLL{=PZ@&Vj2hv$?`?KyRY#HXz#Q@S&Ebd2eJ zR=J*8^2O`iGnYo^na5@vr#I#>@tr!C}ZZR2y0iQw{SBOATubR~l|K+-A7P z@N>h%hQ|#14bK}67!DcUFuY|rYIxsp!tk--GssF6W2Q0B=rIP3VdG@uT;pP!akX)y z@#n^OlpNes*xs9G`60K2VVDz!gd*JgXhs){c4vE|X8c`}4dHEv^TFD$l-5Y=q>UKqo28qiZPIPhcIocuep^BGH?~aq zkNuBFYqA~D)@wcY{*w})xQ~jOx_$bp-dnR@H(MT{*U4ihAM9P{c}lkU#Ou*% z=H8Y1=~vN zdz(xD#w_b7_6*w@yLxK^AtrCD+|#?L41Nppq7RIA87j6^Y>(a)JQbm_V(h^mDQLw% z<2K_>I&>RB#_h(tjrUTBfOZdr#M`6dcONjGC5ZkRU(%ifpo8(S*K`P1iM>gn@d*Cj z6VP!$r;I1@+oxFZK@Eya!A(R;Da9(-u9YzY0zImQY?>*2p=r17H9 z6gjK`0N)V-#{hj05Bo@jeQNr`%n34^6A<1+n83J|e(9gtVYX?I)*q&LWwcOpzV-wr zAhQqS&s=Q|QlPmu9;HKOGeTR{z;}p2MdV0)x3tfRUju?w}#&7pLreqn$W@KjYB~cZtkI9`e)uO zes2P_O+dE++TKoJ;@gQ1zMI16UuqXpt+kPo=+@{H)ghMGd$jsnEPCFU3*|rXUD4;q z)MxF2uttHK0{kLG8T;#r6mmB^j$exiw{QwsMZeN10p|<&n1Bre{)d3iMGm$aUo>3v zh3K!m19^w?#I#Ne9UV6I>!ppHu3)k5q^;KQ1!JMfJ3*bEN6^Ot`izbq(LcL_zf6K6 z0?dmCCLj?#WUPjV<;5Pb7vmSphP_5W6xJZ1mUwIenvAfSB6f~_fqjt}uXX~LiYg2R ziK%iLybDj8OPVqwg9@f>MpE3ss;-V#UqIq5HJ8iK_6;HiYn}aC0YwPBF&>x-ZH*(Q z!^1qb@3h~6-(7Kt!X8Ko+hc#i{9z- zi2MkO924LN06!AYr*Y^DKuF_y z8=xMM=(aRriP$(4Jyh?Hep#PgED~v%VD;CtBS>TyJyAa@`n?8A#IYSgcMIfu0qstK z5=kFL>^>1o(6cGfOKD<<4?BnyuZgMcAn;9*AwfstP$KNTc-U)kNQ*r#vO!={E{QOL zG`y35PKmUAI2LJj8t@J%;>?MGTA8sDEyV6<}_&|N!cJXWR2fK z{2^=nHm3k~G9;ii62U3S9cpjj0-l}PblB5Q)S3<=#F$NPc(3FqwsZ-?_9=g&1L0`TE@U@G+3u(6a)YKnAW z!{ri@oavIJ3!`lGT92^cG`Y|M=iE;X7SQv8v~eCl;33fqZvc8L4joMc4IitecptfZ z>^$N8%o&F;t@N-YNWuJfWfGz*&*gCyQ*ir2acJqk30;U>E_1EM?}j);VVgu4LD$A&BRb-ZuC1SrBABiN1uul>81r_J!G025(b(=*M2(rbY)G&OK zB)qX2Vm#38Kyd)U@H*`?pnfh`|#H)ps8uX60vb8y0vvw z^slW%V#gAsKq9;75uiQUW{u1R*4YB%d_c;B+>D-*lk)iLxfSRhIbpFyWQK}_qqo{5iM}{fapgBVj?U7 z?Q=g%YpDArH$g}LP3T}Emq@y7VhgoGDesbH6sA(vg&w=+SQi z>4+%!JwV6f(8&=%r;wtL@H~cCE(Ce3v9M%FY*I3NyUc;HANE#mKSjf(fZtPfF$|F3=Z{fT6&-i`(e*Q3jioeVc^FQ!+`4Rpn zew6<``awrg$N_VDg`0R2pTz%zf0JL!zsrBb@8tXVe*R1T0{<2NEq{gou6OS27e_}* zrRmZfXcAUHi?CkWByEwlLW8hF+9mCl9+RGyUXl(;hovLZ`_f0!DJXHQ%mHU=0anez ztc6Ww9c%$RA8kV`4NXvl(en^&jY+#~V?JO_7jH0B10IGgL`p(0fagg`q2bD5c^=lX z;H~;|s{Wj%KWFOC=_&D(_4juDIY)mk(4X`4=iHQds4J-|JTgnG9iPb;>CiGRG!uxt zQb*dNKR0Xj=a1>{2ec=St7+}uu0LPXpRelALwdiwfxdZ1gca){&z}o(?6(Xr#{-$+ zFpv&Y-S9_f%z&X|cp)hSafUtkgQPBw^CsSR#&MY8SoGkPx3ERg6<0MzaIu%60e^S` zW0c6V9Pg$0!&8KD1L@Ctr6<`>*aN6jX3`roSn0An9H7Fl+$Vlv_y+hSemVW}4g3ne zk^d+En12F&-#-!h5$tfUK#gKfd%Q2u7EmCr=r8rY)Z zs+H(Ho5o|h9oghX?|inYjpaw1BW>)X=*GyM>~J){=Q&f8REUASAi8T)gGbsU;ix9# z{huOnp*XBaMiZ>J;p+cklT2(1n}V9m$5pmWeo}r0?S4*vSu#P3_*W@c?w9)|J1*xk V!#yynga0;3C)ipvN2|W{{{UIbmn;AP delta 59523 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 diff --git a/res/fonts/Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf b/res/fonts/Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf deleted file mode 100644 index 4387fb67c41bde8b9d73ff8d830eac5718de3324..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47628 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൏#;KetQ>Yi-#CE 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 K=ik%v1OE@M+QcOQ diff --git a/res/fonts/Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf b/res/fonts/Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf deleted file mode 100644 index 68fb3ff5cbe2150feff0cd5439ce42b470392c4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47796 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 Xpr=1K{=)dG@s~)R-0l1udfxGW+TqtT diff --git a/res/fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf b/res/fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf deleted file mode 100644 index c40e599260f0b1e1b926cca0885fd7959a22f813..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46796 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�%M1!f7PsX2f$<7;yR<4Pp4&R5~$mrS~ z6^+5ls)X5VldZn8m92?f%|+-{6tlDrz3O0>p6U>yO8i2*j8o5EXr*gue>XS}G6E7&DL7jt?b9%N!1y)z#XVs+)+D=@_cX zB&zEa2$Zjh<<=ytzuj8XxvJb}m2F;2A`)HYvD-b-D1pLflPv&6bG*e4e60i|&O@#M zNy3dqNl2^qN<>HUvdF*ED%JWjny42O2(o*6qx!J+KKV{tKlD{-8_=W=w+4A-Y{sD znX0z>SK$Fg>}2A zgvJEI!!2IsDUq?o+5Ta zwc75AR#nGh)m72Lmj(t02heZgiQ9@;=3j(msw6JOE>$F#(pFNl3(nqdIV_``U&1mN z%0llbk{0P3om%!4S;$2!n_CXSy4P*$PXfW>Ks5fzGZ2ih?#F~+glO;w#CU*jm!5=X z{BEFJIkTK((xbdQ-wNLcWF{}?W73xv$xPK&B#B8)_CWjei3KqwG81u)ze8s7ZOaHv z^sNSOpA>z&RqGpH=vx#$Ux#%T9kf$`BEHU$TSa+A3}mlnpal$LrBX;JNu^r;|JzcC z|Mo43A&j^ZV>(azGG^m_+K9E;Ky4Pa+oUH@dz)6P!2`7^)b0?_x?QU+xi^XJKqke# z@7HQe?ybhXyTrX8&}wxmr14~e^fpm@hgMtiWEDrytM_QNB~N~nIaHsFImEs1UFP05 zfd(QR$L3$;J<@+7KY=oV#BLB`DPJlgGJ#b4flM5-C#GnvEk90a(Wb&R9GTb$GcTR5}aCp`!et7# z&Od?Nu@|x1TrlSo!&rx6hQ-$WSSmrYR9qXRF+Ho~cZMv#$6uRQFMZB{O~}G?RaU*r z=WycIUiMw~03U!|QI!khSwgEvKubIvaW7O@6FPXvIusX+c|?m42!>>zm;g^h^_4&@b-{j(6OT((-oD3`5KVw20+My0MuX_?W7 zR4DLw0t5tx>cZOz7z7l8?EHJ!1MGaL2v||&0LD?&iI8#7iXC>%g&im05RbH!B=Kjh>gsUjGxKt_wx8dOL25F&t22E*ZCq1WkrR2!Cpy(232meHPQ z0l$luSRxj<3Xm&x-EA${7b%I&CKFK^i@3N+osT$zLW)@GEWIQzHq50?)wF1(K!^}prqW9)F~&`} zlYc+E$KjQo7eufUz$x>}4@bDi1|D2F=#*vWhU`JAy||icu{+j>tGO&L`la3oZfvCg`_hMeFOXvJnw3^3}xaXfcZr zIGUFR>xPthR%a5w>Hl~saempEt&2tb+p}X$9-H0euDPUp@^H4HzR|#)Hi)YbQOiSa zXa7`N{fcCLGsPyHK36KcZMc1XTk~kM&HiX46pBVep$MoYg!KWR6OyNea=?nQ6zCJ! zFHmgxu^W&Qi57+#0#cblq2LyKbA4?pQ5F=lVm^b4N%~=68di#tARC0N5*1G|vhS%K zK6E$~udZ*x7_4?@C|;qC5L;%ofxpJsX>?tOzhA|fQ@O}@cCkpV(kkKDJ%c(|5#V~|q z_O*_Vw2_GTEHQWo*-UTMe6}VN~ ztIoptHjsNj_rH2NNxyFf(iqLsuN+RFTW6iC!tHGS8^~azeYsL6jr&%sniQ4|VgCid zk#S^Sq0-hy`p-!tTdk0(7U{qqN-q8&7O1!i8`@nqgr00RIK4QVVc%rmL3_yAdsDBso-rXn}CvW49h0}& z+S(IIu{Wn)FiQdqtYyk;g7W}jLcwPhMhd?@_$f9Z!Kt;Ck3k0>YHq7elg}+(-PSxr z@wLYK`l_n>`o?1(m)qUm{A_Isdq!ZbC6|)5&o;Na-7Zh|*O?A15zIW7rG7K?yF=@D zIr4`2IwlU})N#&DHn-!Q2f-yhkSEwz zIu|e#s>32tcD}%5WQ0uu2TPH$`F)xxb?`h#!0ypaOz1rJfFu3PGv5fh?I!b6-~H}W z7Nf(3{dWj!huHVDr|a=>B39a^P@rf=*rfr5TJaLi6eDJ_D`@xVw`|dS>_Nx5=YGaz z*W17Ho$q`FCs**Bzxc&B1#YpIbid&V5T-_5XQ5byx=N{4_c5Z@H@3e7YIV_}J`edy z|IqU^>R)wpq<1$ zwHY_5^PNhsG?_Nq{0><^FrbH_>oLCUusdw;^1AJ2^Sf@qCa-R*1y8~I**EYWKg|mE zbygTO%qCnB6~St%6BFN|Onj#{?erp(>5vz17#uXn4!>>OY#!g{wp+|Myvu6Bog47@o7>1^cm&f66N2ba`Xi4#eR-n!hWyP zR|Ob6z{ZjS`$=|b%8R0f8cpx6s0x;o+_I5vCvB?=mQ|-})^}NjahG27WPlSwM$ z(RjONH!dd|$!^3}!>ZjVJw=jGJiEL`qIZeD|Nmg0a<*gsM*b&hFY6Z3_5fU+ZY%-O zV}-D!gKc4e(n*_+i|vOMPd6ML`blIW(_HI?h1|`$5~=b`J@Iqlye8#O1!}t@ibxJY z4EX~=;Zy)-kdRum6##!83&w-}QLo$WojiDOlFq{ols$aLb^x?A+~;)rbsK1p(sAeY z`}e=gHBQ%Krs$b_1CVXCjm0pwZ(~)}YW`!z2j#Ej&MIIk*JB}3Mppm=+ z)SCn?UDfMMdRn@w)sAA_E2R)CZHs$Hz1g!|cviJ&Z!|IEK|B?&oW#kbgD*n7e_S2p zn>~h-$=+(02q@7-uT%yiis-F~kn6_7xg_x`+NK;^^dJ|4`ldAm^V8Z9$nY;0Xm4k% zeRca_UoWkbi#NvO4YW{~1VN`@Z$+j9yjyVn5%LkkBdau>raw@T`?1@#z(tC$1X}kw z&2`!Ol+|UkSkvw6*LS3?R;$ZW;}2$QtlVR@m}^_c*L9|Cw%-rNW2+k*2jlUE<;n*G~6ec-XdH5V^i4iFE>g z+weeNB3tJ4rZx<8Om-O8yZt`TdP7Yd*++g`A~w9Dt|jc26Kng3pIVS>vQcL->rjtclao%?y&G>ymES9>DAakGid&9SA zn=#wZen#7jIc46 zyV#r*;VYn%8y~_xkxgn`M6(Un;ST zKpeQ*fGkoqcK6IA66$u?PeDe<1XbV z_TcUM)5bD4zB_c<;Bk5^C-bDAcHvJ-P*0Y1<=R0tq#D89i_bSOJrXNOBQhCxferhO zW044Bk!++sohZ^0sXt0EVu5y)*<47(Qk+EEZh)hLu&~_j6uHSyfsQ<<_-6%a5P|TI z^E8#+Otf_~G;YrcCsJc>{*5;4=va>Z5W$T-@CvUHeziUats+1cGB$A0W3N^Y8-im& zHMCisL=GW*3}q5-A+aD+XCPhxhq9cgkp(Xx&s#z<42ra+O3grPiy$yk;P3&Le_Byj zT_@Ltoi3BfY4G(XBRv@uU>jDa+2khKsRk;xl6D2j#<4*bZ3_>_3nl0#;EB?pMpg_sHGjojcpI@M!A+bW z@H+DVmRoi{?ZigQwEAyEtp2M(oDA7SdYuc?s3~2^~~w~Q-U82D{k(o72jyHa9zWGr;-Z13L~ zl=;ip7iC(;Hu6{;Sau(!b4xqKejI-o?Atk`b53CKYyT*@A4jeqkN({5}uXu`0S+j6bT z+#!5W>Njze7cIUEz)G~W)znl}xZQ0l+xmODYFcYr8Zs5>ic~B@d%)MFB2Kvd^a^t+ zeu_XU;h)+n3dzF|BuO^CqPAZXPRql#0$jT+H;{JO=7&o%uwurB{Ec+YdK0WJ+?g>A1rt!OhIg27ZW{?AXxJq8On7Ff?DQ zRdG&MTWN~O^m{8eLYpeRG1vYUUWR-0dhAU_H-bmtg_x%D^`OM{+t*J|Y{uwD;Wwzo z8u%18S*Sc?bAF`N#?^SE!Zk{wiE8egFj3%kP)t3+QUpga#wBgqL`y#Pg@Z?*xcutV zyLxxDw)g3!veA}-&FwS&jl(u>mShLwjJ5hmq@t&0?4OSwxc8iG_Z+=zD`L5Bd-nG2 z`n9Rf#-@>ESM6QrJ#qZHr_Mk3 z{FccbpSI)-`!PH zRq26^qJy^v--v>n2Hu(?)+?!q#9csL9UXOb9V)Pwu>gp5N&8_&^jkEw6;q{_y z!yv^EpedM)rl9zCTTlUz+|h|U2`&%2+q3!W6uI=8mI_-elZ>FsyJJC6Cu(@Gf{hG~V1053$53b#h-J;}c z&N)w-YN@O4O4anlQw?(`Gnubt);D$Kz)`c{s4u~LUcu^e*uoWw>Efb5R zvBM!@Eem^-9bQUhrwy@zGZ>uWyDzR@qzi8H^=@x&-m&6GE}zfo^j_w5K8DXsJl(Rr zr+a&g&-upJoha9Z-+0dHBN+{EH+t|@wgQ-luR#s>N0I$N%cJxL`lcK$y1^Hsu7n*S z5E#UxW=SGk)7spSNhk5C9)Ob!=%N{@C@t_i2=iGiNvD}mix*qYLGqp%gM`Frn5J34 z67`p=!}7U1_OFt$HHtkL>1*iht!UYU z9q+7au7Eo<-cgMVARTLgr8Ngj%LP?rkR1|w0G1jV%|?UyIPw&XW|N3mL!k!uSN#^m zXLKgZK^xQsuA9UOJL$vn(DCs|gm&183`Pd}db>JWn&68`xU1b&p@4{SI@Ac=(l2u? z@#2U%heBS2bBV7}vA{6@ZTFpX&VBp#-HX3_C*QMa(|abT=lVZAd&M2}cXxL4dnYGv-MsnM$;tO_o;|j2@3E{^wj;IKGd?;t z?vc$%F|#2~re6f5RcxIGr+#j*P{a&oDCifv=pseV!~rN|cwlGU{Qwib0A(={pa5(E zDDp(;*!aKz!D(RKz}hv#-LNtM3`wtgWd#=N##&a|E;$$j$93 z1=?*ki^Jo)!t1(8EO46j_*SiGS!vb>yfcwRvie4%yClDluMrp)W?cfm08;qFl%+L< zKgAU`z@KIIXCl2)R=I$d)uyp~;$-g`NT&yCYv^xHs--2BZfQyL`%(k7wF4>TZ#$F) zQW)Aux7#_7tReIVSx{m#Rb@kVCAPWzm!B@i-cxR@*?KJK(KXW6VsCucgXOp-PZHmy zDMs8O3bp(x1R_NWRM=VW_u`dyeEdhG9jQB~5F>+DQu0fC8Fu*+FXcz@$v88;weS~D zaPG&Yzc88s_HHR4y$x1aRKPZXn2r*CmaZk~aV(vP(=O*iIam16!Y=KKob^fpuft<{ z@Ij+&_vs%u2C=og@$pj@r^n52F1*d{fxXMIses#2z{fxe&q}-IU&fAkloQt?-kByZ z0U}j6!SaJkhH^s3MY>hV@?(|qOLeW%gto$bMFqYFS`oODz6y$KX&1gDT3+rClq+XH zB~Z?{N!#Z?jP>BQMY#Y5LanOR@Q@vAW;I z%EV}(hcC+cq=#tV3D6i);TyhyJ18GBI`Lsh>63`&`h$0w%oc}{`ZyuQSfzMkwRoaW zi><1O5K6|c=$xXUl5IqajOiA$)oDB?2i*bRyQLWX4yL<;_!y!GDX4{CO7Zzuv5S~r zn`xggchOmXEkXWRDkXN>Sn^y*9c9|nrWcVHYuZVeyvyXZ%a)BBr8vHsX}vKNy3vYk zSoby8Acx5y)y;ofdP&Uh-=^2I%yKEzOKJ5Bz4V>f-a7g~EFDr^cO88bwywKb`yMPH zSoo&2ftjUmz-MwhK~VYJYZ!bJ+yYs0Xd>)UNFCT>BzB5#@*7=CXOR5rV?g(h&yi0^4|tc`piW zsScue;Ti01@dw)C;t$dhw-YHLE+=FTC|FlM|FZ7wVuqoOy;bUc%ap4lipAs#$`un$ zqxrV57unid zrN3Z~>3JU5&O`+eE5{Vtl9tfbtwxK(w3j~Z5_+5T7dwRW`5r_W+}i|Q{bl|YJV)eM zoQ0-Jo>xLouR83qZJNRs(>B@WuuV)zf05xM-061j#826F&N+n3x&Sllj_s%MNw~*K5
      • <=_r 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 diff --git a/res/fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf b/res/fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf deleted file mode 100644 index 0c4fd17dfafb7e3bfba40095065f75dd60902230..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47220 zcmce<2Y@6+y+2;nlXLFLvy*3cW;W+=dvhCZ?{4>U+J(zGz;VCq)-CgyqZ~lI(nsLUM z3xBlCIW{u7RN5`w%hy83zFJM=#lXw6|;3Md)u4?eAu0wr+ZYAC?|r z8s=a=7Gx>bz&cqkTf~;JU`ZYx?Cd4op;~~H7cAB-EnOh3nm2mk}zIGpu&Qxc+ zL7a2GROd`7)m2;Ix>LQbwlUC+-R=Bv<##`PdvD+E{r&xuxA(`Qm*3vkH`$NVR4g_X zz5Vvvqf?jPJ~(*$`GZmEkymcgT-F~=M5Bp~TVi!}bsJ;x*r2~Z=4tabjP2>#GuF^> zel#DA<^dcQoPJq)O!_Jdu_)VDGDo>qE5^%~HIpST5P~x;-^VnX$hZUqiL;6D zc-c0uP8M}h)Xlj?DaI{RQxAG`DNjmg2o};luRfK`=5h&xo-R5&x_MVudzxU|DAJPYG-Z~u*8R}fFIs*??Vmxzui_sHu*zdT*?g)8|R_UPA?RK7d*Q0!# zO;0PM(cHz7=r&M+y&&n>Z26rtKl}DK1S+mE4Rq7V*A(I+5;9c{$y(H#F*GbG#brB z)0q@z)PUJ$)p2yXFvIE;XWH9na#KvqF?f#DH2nIb(>xFk2cEm|s{YQ(SMGbq^6~q) zW7+bFFMWxhH#v0v-mqWV>JRVS+_TP`Ioi{8=6`zn2l}4|17&^FFGyEQkFy->Dz$@m zpp)8bm{uccHCF@143dIH#?f8fcIs(VM{GXFvZ-_`o25=_1^x*ZAiA5(WwWWIiiX_^ zViK?5@jKS6xnu9p7eN8QvSvVj9kkJvxv*xq#YPtP^$97fB7Ntc@J@*CxZ+1gaMxC?8^M7MDdixul^KF z%UL6u`$yl~UP!BArVoWvd}72n=&-FsOVd zg-Hh!@>e-el0-vycawy|b7O{d!0V|$JfO2%>I$X)T+HpV$3mfq!){J$jW%wax_0y& z2y+)9MY>*-(WDcZW{bgXb!v@Hr`2Y(n1)ST?%KZq9)a?RCd;5nI~Yx|)Br3Hgy$XC zaY^D^fl}PZC!7w(yl#ij=|e+nr{3Tzv}J23FqP_#yPS!X>q1c6qxXLHv-dvwd9JOz za#A6(hd%bPhc|KH;+bbiM&qCKCA`xQq8TXl;!S$a4HDlk_@9x_FhG?B1Oki&a)E3n zRTqtfaGNik0#oy=%z#9$OUPX?1i=;D)EA$^98$y%^wgt3xKj=WZ`dB%$R`1j|w*WC7u`OAMwE!u-CUKa0Bo3JxL7o_YGc| z8~+xJ%duA6XIn% z<_%0wt5ujDBgWmrnv&^sHd}8rgb1TZXApWEk#7y9W^0&ucV`vfq$~WMNd1wn!|z?U z?mhdLUXk~D?IydWbxp@5$FI6#&5G;1e!urYe!1*#S=#N7 zM*Wovm;6x$kJ!T)hmDOYMqw=!4QUsVJkE2-Ht^ud?NrXyX=FYTh4Knq&qr1DS zYX+9UDT6m&sB3PDqU%#rbp~0^X(Xw$mw#74u6f%|oupFWQM@D0x*x2Q4H^aoX}oNL z47AaE@osy>jT`=6`HjF)j5&xNx&;(yS^)}a+bk5^%$dp% z6jc64vsPt&7ly?fTLiPaDE?4k!_PP2wOU>cA^PE-9w;PZPtpoE`Ag0e60s3#Wl zJQk3B!H8Vv`STw=F+mM?`a>Z<|8C``REl3*Y4?Z1vb4`1O;@`4<7stX--(fh*~Zg; zlcY>5scLrUmDPDAGf1&TYfHsIY73MB5WR$12>sV7qEPgB?!5cCpyAhrBXY=IIUbhf z5MA*%1M(^jdnH{ZTqR-enV0x){ZWOMq<3n_nn~h48upT8!dVi!@#EmM4!oQ6g&B2N z2eHk!lfD3IcEb7L3_C-z7Y*%M(idiRVp56qDG&!#c10=X(c9Ooe#f4@?^wP19eWR5 ze%axJmtA(yIQhXN7u~&i^W8^|+&#JBZ8zL->#aB3@HUcmq@O=7=rb+)_JZUWQcx); zI#dlpoT~gz^K@5p*vE zx?;Bp$qqZT8w#7ShyMQA&)h2|{K1yWBfKEE!Vmdks6c0a@%k&SJ#pgNhwk~&tujN&UyhE%>*8AX&vq19=$d*OHi$JMdDKrg{&XNBjhG!oMERq7S- z3cUmhSe-u>4ddm$Oe$@h)u?G4vzmaf-CZ@T`R}iM-35=DTDpVFpm@IJgjJPs_9h2 zIR}!cl8Q`a1{qqT>dgTPNv}6WJaJE7eEFU#57~2mAzoQ^*+GSoT)cjPY)2mHX07caYN+vZT-yT9_%s__-8 zKhy4T2ZP^9N1qeG1il>DvwpwbkdA+lcP%QF`d=UkoCp6~3Yc~X|Aa;h)1F;}3Uv7D z8qjiH>!0Xg?Wt@rDf|;MbXnmbt6HbIbDE|8m?YTspWo5|ABEjJb;+7dYbG~buxR-B z%CX~lug`AtHm&VlxoKj<#jQ0<0g=d9HpyBfXc`ut%aNkWZR=q%4NR+syO1blEqNpj&FGO=tDoI8!^Q7A0 zp7lfUkabAIRhUtVMjBgV1I#G&Irv&|J zAmB6_yc%zqmA$pvsX)!D28|c};_XB%TeqCQjn22f)%f;X`0HqLD{t7jXU|T4f<$lS z#+z>kM;7lCa;*)|CvPuo<(YDlYfAZxbIM7sDdm5kQ%-VCDgOtR1KQ^4m!wa_H|}Tc zr)4;`SXH^Q(g0x>3E2%zd}h4FDjvw?;u>NN0-<3}5o^eC6@fe+X;;h}*HwONh>=&j z@;hC^TW5RzdFiWRkHLE84)SM9hpYx~D01e%qzv^VJ^iA#5&dXjeGk^h9nk8k9U(1; zdQvqMgmzS8IZQnW%*`c;K`>RH)hPYZRL@GkWK!9v1L32BKJ0Xc3=e%)de9m4T1;o|aJk(sNqgoMaXw_Sc!N&onLkJlm)qky z^G7L&5s=^fGGIrFtuC38L8n1GtI*BW%T>8kQz+EAn%_d3JDtv_YqfU_hZt~2w?{Jr z=<0wy(q%D^&+*frIlP{j^|$|JjpEn0zipR2G24Ir&w$Glb#qtc&+e$l>3}lkw7F4I zd70}_;<8u%Qu#GTpvMTdVgy09{K0_3P%{T}W?$|8Y?)%J%|Vc<2kPrX=%CQ-ak zVx6!Lzt5jz_2A5(c)-jJ`YK-6llK^oqifJgZ;x^xpDEg&ep0M0ciAp2K-kKlkx`9EgLpPf_w=uG*a z=9JI2|I3{6=VsdfZBF@jX3C$Va$p_atEtC~_OT^wC3{Q>+nDRQMrY$TG%+^Xzofr3xCnjkO{7y9Om;f!nsrnY6F=uW&K;w3&6YH|e_D8(umpV4I^t|DsKvPHKqIk$+cc;g99?!=% zbu8M_p00e?^5#@$)Z#Pzderc;w5YrC{iU8&8`E9+t7KQqX*avhVF*t%Z7%=#)kNFgO^N*X-)|3h*p=$mH6mdeI0>(@=4w|@P3 z;JkToiEjg^@~;wIs+)~2#f4ir&l=L?q$HP}Jfjc$6(zaWGb!dtSR)3E=R1e{7abYP zu8hUqc9Yv4EW}2}SB%7(0v6e1cRSp%H4TQrOU5UTm1K_`b@@#ur)$f)byIGK!R&Y0 z{Z_c0smFt&$IB39GX3jOCJaW6GR=+2D4vej7-gD$$ox>%zY=Adeaif<{7X@$b3Nu? zjWV6%IWtkFWo&GCNjlpgqD=m3l!@Ha|18Y(Pf>(_4$~hRNu4v&bl$;pV$Rh9HC(ck zT_DypYQKKpg7`!d61XoJReC*nI-rp2tkny@Ue$h?N42URLRRfL8VOLUpby2rowmM~9yeBQWtY)V+RB^0Xwrut4<;z#8GtZv_ z*NZ@JT3i}1Ao5S~^lOmVAR+p{*N71cPXK5FsvLRIFjg^Jsc14@N6b2r%4AJsaD(j; zcO$h<2&s88DghbAzvoV;)@P61*nfCr*Cpx7*UdL&x?-BJ>8Yg#c!(DFLh2xULw?g` zVA$6juD{Fclx&KGfgVCU`YEA@kY7T>{>nct^bn<-^a`c?**WE;hbZNLno~a8{x4LH zNWvLJ9*#Z^^BD}%^@L4yRP*}FhH)uvM=xxSvU`u6%Z z=)K6($U+^Oqxa5uL!jt%{xcTv*YFj`E?AcF*lm&YVBw^1Q*UWcchBxp&qnZqt5+-^ z8XBKi!k-Pxm3>DqY4Al|vcu)MX=Sl>;K;INM;5gd^Kz(i;iif4&6_4BHjzEN6xi?z zctV05eIN#7xN0@{1?}Uvb~^=K>*nRFEm$qs;DR*@1Dp+eq(#_cChDQ6ICHfhAE=AJ z0>?$TwPVqWs>T!C(8Zs7tn!K+3dvkE&@)kbhAySUr0%?wFM@vh5J7tg7JiZ;YZ3JG zXO08{Wu)w0{_NbiAdQRjS=T1HZ^c%8^`-#=B}= z_#v;)=|L2!*uuXq#@)YRlb)i9H1cPGS#dU{L=$J?X)Z-cCiS>#fGGOdyb6I&^U4G! z&5t+c=Aw>kCamG-&xYI{$BPkJ7W4BPmlHnF`yPeL8L|KTVd}4RQ1trv4;>LiChz`a z6;sgj|8FA5HJBnDqIaLU14$+VUYZbiQzyIlbXyS+tdgQkoEa9oL9ZdZOG_R#fI;QX z3)Kq1EmWbwHdhR+Sk1S*5Sefz)u|*#qUTz*j!Yfe^zCY*IK7c@?Gy`&0(R-{lL3d=14W z^4Tanl`MKvB{b|g{vghZ{$h8MC$ew6) zS842m{{9QbO1q-b9g7xiZO>G`V?LP{u9x2|HJp(KyDQ&Z3X=sM3snj3Y-{aO(6y_r zwR^t9<^2lsZkQtaSvox?WdP%O9v6y%Bi%}ps&*St5hgb0L9QtAui4{5tW60ws<{C- z?e6Z{T_TT*j93Yn9jveXvgJz@k4x#;#5nA%;xByQ1=0%gyEKr)Woq5}EyCea?K5Nv z^%=Iut9pt4*Zn@rQ*_g=o}OL8Q-pr2o}&D+s;3D3p15R@+HXzWtf%O_wd=NujuWpM zlZ>;lR?TH$A=kiKSWSSnh@bvHG;jZi^c$^+4yQ*%kD}g6Vpw^N~d*(EC^oc@~ z;xuY0Hpt=1X6o)%c#W#M?B)D0 z0%<}FqQL;W?`&Dv-HimhaBT4+MaS@ly%Q~V{(MoEC5?Wf>wg0H$KM6HC$|)v$hzXJ z9v0q1fNkfi#8A=OY7ABJ!a-oCv&EF+g_||4$vq0`r5f8pGLwveyr!72Kw~A0nmV}z zUKbb+vED*Um#?FJa%J0OPyIwR;jx+Aj&O4--`>1rcp`D(`G&kD;Bd-Lr#D&W4R|b` zB|U}FEEY0Eow5;mgz36So!y@9Y*?~d$m|!TZP2%e*bRz33AIw<&|eU6khI%@G9tPM zhbW~H&IVP_YNkdxMKcj$ebp99xRp{!huD&qCWOV~=^Uj+XjNE{k|L|%QDV%b$SPc` zYF@&{QuSA2cq*Xy@wIgXDwAx9w55wJZ5>U$_32bJ+L&Lmj0}-$F5R%bJ{=A=w>S3Z z4{S8ZUI8z&!RblX#S$)Oq$8i{OornRSe z6V~{Pe4F$}SmSE8BJ3w*_voPVNifDG3C6fc>Is(?tRY-2nP5YwGQd!UmFshaA8?bC z`-Mzr8sv;=O#}7waZg<=ymrk%5%x2UNwKMO^^v6>m)9pr`pp}k$+>;|$)&X4=k|D? zZ7x!Ll(P|F@3XL@NJlfnX0EEK9;j!;gUN-mW3FC*R+XZoEpP*Dtg4lmo}U~ijN_^n z>Qa`~(BSxw!8v>(|Jt_uNLg!X>mQ4d;_t5<^nqh!^TkKGmc~&z)88|(_!&72IW9&a zG$D$gk(cE@p$RGFq}eFt&(0|)O-L#K)130z_J5&rK*=`!3V)gZl?7NEc##rirc6o^ z%@o9q)OI!8EqhhN#mSHGDxE_eSk^-N}D#54p{z z%3-I=<>c=)THK*Xd_ilzts$RXaHjoH;$EA>Q)cPrMd=u2$@_2$7$E}M&s zc(^9dX?8zC~V7*!77|xL`9nKJ`(T z3%@?}As23NeDqUd_Q%BRhgq}~3b_=pQ{Y-mHT05vZq7t2i46!8chAPH)w!-bt@qp9 zQQhbxnC+NtX134VXLkhLM(geTPNUhQOgQk`HjOt6ygsht^_>E*m2$#grTkAE<>+Y+ ze}2xNf~2d7Y5V8Lw2^Ppk?9aIZ6RIMsJ7~z6S*yjYxBqae!22-IppB-%jB8k6TYyc z@-)}2B3E3{<8kxlbOoE-U3n0_T{^u&s+;kSq(Ft_t4Xo**|SRT31-ODfzML1LB?2E zH@jVtGI5jeS7M1BayH0-(r-5#CB2^dF4m%{_Hj)+`RsNg_wLo3C>eMWD_ld?ZfjRl zV-D$t5rm&TF7(HowjjZjDV}Zt8|ceb@gmZF$Z}QzMZ9>ig!KiADv{u)lF4W!m5he{ zq&TMil}o`Hu`3kN8CLIf=i zoioYS2qG_c)}9!he;h{(S3{Ywvj zjn5^=GfZm6vMt%>LIcK=PNpDk%%)($Ef!(Wd?~(IRfIy7s?9}!p-E9_h|>1omjZ5= z-_jH<_K$D#1j6a8(`a*;ZNC1PF>UV81-;T&2A90qhkw>!v^(8YkJMz3!DF>J4Ho;~ zlQE0ii=|Bx^8&i#fUX4@B;(`d6h`i^dXh(g)WQP-JYP(5!wZZ@k%sS{X(Co%3M}wmcu;pA$hY14 zCU7OlSS%+ctEr{bXr-*$63zH<6t6ee%S}OOo1_PJwH=A8S@11_1967d8_7)0Wng4BxKIZY;cG9vcCR;o`M~bWItM?;Gt{r3$M501eM3WM?-GL{MC}#g(;2oig8|C88lHga zm`$cTkeZ2DUPN8cMs@?iu1)C9)b%RwQu0WvWH37p{@tMyN0waCfW=1+Phow_E#nj2 z9$EI}makmgyJ3UJfiX2)Hn{t;{w_Ybd1%WaBxzy+k}aONvcEs*at0Umcl0g6y3&9% zli#&rV9E=yvkZ9E5HNt&(qHrdEv2vOnKIldB7soLD3wWA4YLmXg=&Q>f1wJ}a;8cU zIS~jr7NoJNNwqWCw4JJ`QxPc9?vjRDg}<}%J2_zEi=9zltW>$~C%UVJ{Mo^2j}w2Pc~tG z+Q1izz`P_@mXDT(VqCARdqWsmi*;{WM3J?KBC9FZk#UN#3U??JGc1;7sist8Lk_nm zkO(Fst88Xiz&`@_Y=#N3+OvE!x9;g4I5>RW%|lmj?7c)C*M^DpTjbq?h4Jyh#j979 z_!oNj4GkUazyBkb-E&@bL`&ZD7OB0za`WmXrPZrTFkvxA9T-g-b5vbjC2gf>2qKrl z^uk|SU6mD%0Kgs*afOz-K#ul=Wb<1p4?82CIwNnad?pZR;kQeV1UzokZC#{2&4#qoap z{mI>S#yph^%;t$ixu?*%vnq(e@1N>)LpgNA+q>;7MktF0ue?q z--DQU>G1_-TR{WNIp&+@okkObg|>~~Dt5$~dji>INasqQ3F!h3pSjiQ z_f@`5G=P;jd@JCQRn}Ii(oR{^0)Whyb-7f&=G7$XAQI+?x|(z_Lp$GE_@!HRUGMkT zg;T8tyV2${m_y4_rHNQ}_wDGB51i=MP#&(-I;}onwK()%|B{aMV0It+Msu|Wed}VE zKhPz_oU$&laF(<)fbkf>c&buQ#Xxl@i}|wg9Ib^@#R75A(WVMV(kg23R27c1vT@G9 zYh+`6{o3W(6+ws7ZgKhZq2&{4%1IB7$0O-hBY=SP?`6s1@pRp{J91q^exJo>4Or~1 z-X0&;JM;y@1Pi;z;`Nu>lC4%?tsjuM2$>uatl3%fK%FWV$)2Al-~<~H*t<}LKwk*H zSX9-W(n9cwQC1g{K*Lou7-B5G_EdW(KhNj$gt0hXzRN9J9<&GDq3{z&Z*x1Ht_Ck$ zqYZu-@7P2m;QPbBVb&VZ*Yo*DAtA1MpeO`ib=Ky}KiXi#fUQVo<>1^|dyTEdtT{o# zO{_852^5!=*yh}+gIJka!J`UGp)1m=?-Yqj=lJAkil)qFv>RG->4t`cCmJ;x4R){J z`=w&>uDT}fT()RvWx#9k*aLc9EETWInCu#j$N34LoKEeNF;7XAyTlM_IrczU-Fb!P zDFYTGq$D5&g;ukFMq19uNP(@dp`949uMU3Fx_n(j23g@On#`p!Y?ZaDx|{HX3Ixt& zNkxEog#|Mcz~P?p4W(7*?OK^w5;hrJHk+d*S>Gx-J7bP)HlC=@C9ucJU}@$414k~A zeFhjeep7sONDh7{mq=jip#(V*SRK4I+c4W+wpy{!vABZr03@u4V?l-#?&AvGXuO1x1g%do)H%VWfBSBTmk>n>e*#)iCE0$5!NKgcL{sjrjcbz3d(X%w(y;bz= z)@siX=c)FL`n^Va3^EM+L?}Y+Ea^2PwMaqPT{YQc-9)V{5=s-$WUit1|Dr77zkf}_ z2qX4mOczOyVlHm0j<`A(sBJ*)`O+h(eS5XG3J=sKXXf$tYHiKEb=dl4o4EHK)!LeS z!?<^+fb=`7wHg)DcrroyJW+c`wYKKTDu$p}w^VCup8P6vs6H8Uhl|ih#=9fF@R5|IAoerDyrVyRZ zTuD?gG5u%FEqGdzy&I27u@7VUCB-prm0E?bkLo|LK>clMeQc(l{J^vKi+T`AkniC4 z!v1b&=aUT)SOZxE?xet#9mXZ`Zs{9RtJ`$)gu@9>)2+D$%3z|eoh4d3#S zeuEzxZF`&9>+tLQk4QZ-_5ldR!-7?KyvyW&NC_HOt*Kl z|754JL7RoeO4!8%a6w3hacr#y2*CzHg*mN(udGX9o#M$`GMR8VoeuwJIuglbA`w!{ z*;+QqKf^x*+&RaSXr}|^N%SZ}B6^rU^`i}~&FkA+*0;5;Z=Gyj+uF3Qt$A%*%i0!V zFx|)mc^r=@GTTpnMj-+bsh|{avV>-1whLB^SnYx}N{n_PE2-8gDJ?L1ERGRS z;1^fHP&rw>BFIkP$WF7XnTH{2VFNVz3I!!M<0Sge83EV>vt57%(5Mrx*@k%Q;Iw3sIX|gmP5lR#! zt`6?7o5wUM#6mPNEE=uO{^#8e{;^0Z6{$=*U0<$_NsMJo#oQokFSYm_Na*J)7Q)@2 zWUJEyw zBJ%=Puc^6+TfL^1O86={0oTIB>WS5>R-&UTmX0i5)Yse5hTXz4*fceraAR>EH>i`5 znsHWl*Ip(CGPZXMNrl9Ms+nsr)LtscYQUZRM}^H!uh+RZCV6a59}nMFM;Bfm?xEt* z!WKFmilWfv<)Pc?0?k70RQy2UZAJe5CwvaO=Ps+m>(p!CTs>2nsJH_aF1_YDI-|jL zqT-1s1b+Z$6O>!C2b5sEHS`JsRG2aC8l&t1HPZwp2<-t?Wze*zQk4#?7&1d^N_>)k z3OJC3tM+{gC(WW8aGq$cfsFyRPD|ohQub;=N{kL3GMx^YJ*rXkT3TbT$8hPjG{j@k ze{T+ES*|VD+KBL^h)vLPBOz+4o*LEu?NDY?NDNFp&3-Lye$|z)kJt4y=a)BnZ4Qqo ze`#;u`R&0>yrEGixoqG+f(L!x>*`c6+v`o?u!>Qt? z7!8H#Xc4JEZyMcq!7?N0_Q8vS>AHNAPI6oAE`K6iSJ%_hu)NV{#V8vt>8qjoA8uZn z>S>5%2a7h~T*Kl*q^>oQDJ0@*+yWZe0RMM>IrwXmQMRuJf4_kp&k!L_ZPxjCT`(Bs9({!Wd*TZ^V<;90nRQ=CDAF4;P{2^^ z4j2^PLWpa)7o^>@tdJ{ktFy+=!T(58m@gZvI3AfJBWH#2^JhuRTJ0%N^4V!aF*q+V z+UqdFW4Qz=QQ)t{L{xr@*J_4jn=7PwTBn@a9kLs1m7j2m^O+QGXVZ@$Q;v3H%bwEj z8Bz79jRe`PLXTQ0DlHu(=1=L_h(fB>*oQ33ZvG)R{#LducH8W3WWc#;RyMQ8*bjLx zu;?5bRh9H>b+aqgaQ$QP5O5_Niqn(Q*w6YXVh61ZyUI2oZA-ZGxvsjWBw3rYyUJqu z5IWq-nmanv8L_LZR!DaO3_LQ56;1R>h*0In6#k&yXDBu;!}T#42)LJZ^v1H}bj!wi zJC?aol9PRnb#;wCIk(U2@p$_>e^8&1xa@YxT*}n{ptFxkef}R5dku!Z;t%}rY7=}X z5OscpHB;|HSo}x3&p;cNbRxQ@-3n%fI0(eqR!k@|SJ3-r*3{95{;PZVp=U^pG{8Sv zl?+(OquKEIXxlr#4_}$Aod!t$2B)i<`aJoV5 zaPbGlK7*kbCv{K_Y4R#vp!PdT8X&3G6o?fa)nMaLjRtZb1=tj-v!j#pR)7|@nsUG?#t4uTy$g6=-MP;P=t{6^OB40Ah18W<;o{_RV1y>}VGmGYUdt zahGcjFepbPAyDNK*C|CxEZ zYMBC2i*tw;Z>h2;dV}GTInO?H=;O+hzxgfo$&dZwmtPhb#-7nU#q*%xDy5x+dKF5m zrCiOsiE>{#@Jub`GWHCA?tk~ZDkqrpyhZF+{3vgKt^KQ_%*H^#|3g3h`4w&6N;|fa zU0!{f#2yuPco$p8-^afMsqi(-n+AU$5#^mIf43-qT$Ddf<(O%-XFo@|kE8`w0Fbyq zdDV183%)v^rCIMLPFfXehN=l&GMiW11CD@h@nW6q4A?PLo9jBi&tbHlc%v0DAer8R zx3eFMw_r|?LZz^3hLph>>nShX5?u5en-GQVKz^!!)jgA8&8;^ zhxxCwI~~sJ014P=`~Y;YB>T1);kOwwBdMH!haV8xM&A_W-<(rU^7C7w{99CxzM?%l zjrJba{D4PvRZ+0WMXbb1;aV=u{F326{&Ysz>~6tXg~X}Ke#0H{R4A72v)XJPSbow< zGW8k@n-*DJHZuKY-uD%}k7huD4ZVqkHXe?*R;@~U{(Q+f*Q%7hq6$ijxoTF@8~%T= zUV$*vZ^p{bJ;12Z(ooa~C#b^!KR1?oOB&c8Iw+<(Y^@K)REKL6Uy;P+(zYlZ#X z#SlknDCUXN3&*wr){_W?b#@AWZ?~$i_~aR`lcf+tRpmlFC@u(+h--^J*ba^rZ}MFFswA0H54*`!IQHR#)$8{nKWXjVYOrYy1rYLRk*T~w3` zUV9r=Okt4@J=Kq>6g{1?|_9;!clEQP2wS(b5XwYOTmKto{&rY<@H) zPmYF>hXUa)uHQd%Ct;TGv~bPpa4AhRk9L@r`OUyU$RWctt0{%e+1=f}w0kt?N!O)Q z&E!r42c?Dg;0uZ#fh-q2gMhk_d+1zNJC_Oj* zW~oWj%SPBxX>l~pDkNa6R!0Fe%9p`j1y_TO66R9ahDn>zfDl$T*dWWe06b2D%ds2;pYe46VJ` zvXttU>T(GuZAq7im%1}eLAyIMUZU|EkzcU}R^$ZRT-t!i2Nr9uv2z=@a+~!U?6Ge` zL0FC)oJd=*Z9@)_Wnp1{r?oWY#fv|DCE{ zM%YrlR?tM@ngSFv9syD-s*;a@NEjt8@_32|+xOkz9fv2?G0gceoyWa5>}x-q2gF>i z`lZ|&NoL+BuAPocGI1-XMDEdy5p2KW{*Vd9*4to{Mvfua|5a9hv~O1 z8nsaZ*+=T|eG@0xVLFw@ z{*H-MyF~>=HA_&5b3@x!&|w}Un_V}NkkZK4TaN6(jv*S#(1qIuj#Ap2$!ThwsBh`L zgG%qjQ91wp30uaNG$LzPC%;lvs)hMLszTWZIp|Q(I9k+ z9XJup6MJzY)t2@EtaTW?dOP7Nc=aZVlfc>2)!K|LngEz|CQgu1x7wST`PuQSe7}k; zGhnL{7xMp&wAI=dj}si;^B(aRYr3NErmJ_0zgIqDi@JS|Z+%PIg6*#~$w(1&c&?({ zxvOS=KI_CnZBOMTPP6`2_~pML{POzy&78L^U`PJ!pV^T;tU-!PE@Vwm7AyG~Hq$~Q zh1c!{&X6jh6<&F4QH~ucQnTjGY%<`?*cEROcK0{SvhyCF)9Jg%IdjE_W(#CTwTdXb z2m30<`4DtX{r%W6vqkm%S9RW1cm#hhJ*cwh1;l!MbkKck*^p%5FR&bom!i3-$0YXr z1KPl)M44)J$Pp{1vGH}FkWrNqC742UqpWTqP0=sHC_;NT@Ru!Cv(0I{^G>VNX)#;x zy31lQJDk=#@3gpF;QM!7Yr!uw)mzMVC+cl>YKMB|H)*qjEQyS;Agt&hvZza?MGPP5 zfq=y46wqJgYXw@JPG}V(ty`;G9SV^{Hk1ja<1uT{>h~zA%H;a4O`}&lP*un%MwH@P z(O*5J%fF7nu9ww+pZ=U*eK1b@g zh|(gfMW9SK_WBas#jlgBUazZimCfvuef)Uk%|5@&WaBsR2`nXv8a|=JcP`8yy4&n? z`E;KU7&F@o`CsZqih7}L$PO`#*LP<~4S)im(@AmJWwuqWa-lQ)2Aj!+EU-88;}_xW zx=$FeM|;GGuDE>WyFWytwFiH)f_fTRPpK1BgAG#1t%;9OFf9@X_EUbpek*h!pMD}9 zXDr?rFXYl7H-(mHr$5E%zQB4k0i({8;s_Bbl}KJCTDXU%P$4XF#qSfOK?K6z%hR+% z@%8lM^;D}|{j56c=-3kW1IZ6m%&{TiJL-n^iyeQkUoAG#({L=ih5~^_ZAeq)I?ZPC zHPT0QG&%|$VXqc|$uOG<8bFM7JI=+Hq z(t8IobpyFn+LZ~}-9|G?eEk_XUtEqjmc=X^L_qiPj{Bq)ZDT%b0_1nNF?!jKq z=~9ByXr${=%J@dI&mh$}b_8U!6LbeQ?!+v^cT|NNCYUsI{1Re!i#aGX+gvki6s{<2RjaVS52}?EMeI;9fxD1x~)_$Gmta|K=rqq|ts;b@! zg%!>Qyf246CouIBH?rXE*`>bEBs)yEBp#cS|luXg=y_wUR~ygPpi-}KiBQF zLRgxhh1iudi@L_KIZPPIWo?Lc9~Y5#+6lUyEoNUEFOx=&^g$r9-gv;mv8kfQq(dqY zNdi*HBjYv%XV`Wtj3&~mpzjm)H~-UH4BN5cq5;EJZY#AfaEI`*%nX~~(VKYL7Q`0epEmq4}Ys-04 z&;bjF@(ldO%3D6QWy{)iTehq#=JUm3Lqn0j2S3(sML|QpK!q-B=`Ioxa5{4(cPmM( zhcBcg)}^CJVy)bH;o*#N~1+!Ryrg_I8Db zkVi!|m?dsV^No`snLUJhZKMOIPQhPU_k|vc7Z2VAF_2 z(n~(4%fqwUXkDl?J$}apdp~gghWB4^%O(dDZD-+alhR14z1TREY^%TX&?B$E@oR_o z-LrH1>-(D;Hg8)udc{~inX)_G*jD%_F=HZH*g3K0&FiP`x#aK%Hji6<4!5nLl?Qvb zv~~9MOto|mkQUGcyX8IbAJTUm7qhX_NHau#Z%=(Z7Ihhn@HHTb8T(vl$mKAD^s^gd zg1EZ6^7*dCU4!|~d`G^J#<%rmz9o_qfD__NfK#zT41P7l1BZlPd}PnB0Lb0djXTKw zAiXDqT|6e6j+KTF^{-mnP`_`@#Cc1`-WNCUu*)5kOx?RGZ)lAr`yxkz<&4z z#F7HBw49v#iaH}On-IHdIAaBLQ+_+ou+}Vd}vl6lqR&^k$2PR*l>gaW{1wX!d%VIuCqgB$#a4u%keW?0qJ^ z%hTGJZowYQ8wN6?!DMmWu6i47S70`{+^vnNR=GX9p|9N^jwd_Aok3S5p1>Jc+6Ig3 zj1q^#C)5rIjR1>n^hUkTcnu=+dZR(aXrWqD`Z#?Ja=+j=8iQ%S1xxL?W;0G$p8*?0 z!&k@Sv{O%fX?%3?qTcQf?C+LMJL{aWfGh%fHZ>kKp9#!!wx~EwP!sl56_BcpEi_&O z#CgYuwr%^+o;`Qt@9s@Etyy!^rcG~Mv*xXvHXXfq>*_9(!D%~~{%rZ?pG-aNLtyUiVTtV*S_>(@(P*(Ttn!0yccTCm+|!%ma(6%PAV zav&h{wLbswqSlg0>+>E8$K&D4i4IeU1g{X77G^yH&l(}CstZd9k7m}F0FM^f?}_wH z_dMZI<^ROmZW+ntMj9H1bGhM$baQh$-O`fg|DGLg$PZ_;!}*5cY=3Ju+e!y2tIon@_EHe!(M(Q?0bP;ia%$5I(vjK_n4L_GNJcsvlqwX``F48&rAV5}~n zobmMOg0x}!9_UV%IT7%4WD?TAYjwc;)rj@%B67wnr=`^N_1Nvp!GdBmA_#aL26QYd z;wJQv>NgftZ&dJA zt)g3ss!GNZ=`@D7SS&8X1>TS+6nK-Af@{Heb2#Mod#^wbe=en=Q~U$)R_9qUeSrb| zI)eGJobp|WInNbxs@mV2;g4Rs&v>4h9B$_Gj6S=^Ix!)oy>^S`gbhyU6Bdiz>pp%Q z**lWdF#SF0IWfQgoLyPDB?S7OT_d zX->GE4vX@`-JGB?JuV$$7O4y-@EUVB6iQJvyi}_43j}|kr3mn|(jgC0?(k_%d9nCTTR^kY1R+9$#W`u-fckSy0Sb)bdm$;PQI5 zn;aISL9-kmpb2>Pn#^W9LTp4WFEHcGljc&2G>=MdF94$q*VA{C2f3Bj@3Qw3{b0p7LPhHC6bXuSN?do8{JvPm4|Rq3ws6|*pqtmI#(h#W z#8M#o;_^9Kmg;94Q5L5LPo))IXFpW+c)$hbp-t2YG{mn|O;32u{i;?CaADrNyW8;v z-~V3dvfg`c&kY=*-g7lkC{8oZ!-VFGGe*S@^#CP zEx)$>+48b=vGoHsZd+^HYI~RMUi*Z7(!R%j)PA-7&GvUV_BcN8^t%RJpL6%R-{yX= z`(y4;yT9le^nBIZ;vMi_;l0j#tM@MNN4@uZzuhSsD{ozZ)4@P{EOCtX#@|nn2qxxtldMf(4 z=;P5RVsDN8xXxX-vF;;rPyBfN*NGL0YZ6Z+N0PTBzmW>1-k*9teQEj+nXb%7v+-Tr$_1E9F+_w&gC&U7fo-cYp57x$oDntiP=O&icn1k_{UhKATVG z4;0!8hl*11g5vSwhl+n_^fwMS9%%e(Q>tl4)16I^H@(mtZ(i1XQS%ke*EQeT{H5mS zT3CyIYO}T7+#YMct|Qpd-*HXHogKgJT->>#^Ie@k>+0-!d)Jq{ zd%Iui`F-yNy`StG==*;E#r+TWKRFN^m>PKZz#{`sFLEzhzUZb!&krUBw+x;b{P5rp z7bh1N7Jq#4{fj@l__4*`TKwb1za#yp&q>#!O^U74ENAPb-{BgE247@6*P->{d2dBK9Mnd- z57(EnyhNV``UzWk*6}%J)~;n~=@yI$E-Z1ZXBlY+?(-wsaE?Q>26d~E=kRNmrvuk{ zsTYUq?1OKbJ|nG1c?mY|v)IApS#ieo^fNe~Qp%$>u}zQcO!a#)?s8ej3Fr5SX9Dw_24hD9RCfTHHQ0^;Q$=) zyN|ELPD9(7SDIukIF91z!_kZ*gJTpS|2CXEaO80mvGOT~V**E7z4qhWfFp^c4o4UV zwRx@W<~)l%FnyWklWamgV*JZk9aTU59QzaNmaK@);ViCcAC~I%BvxeNnty5f4DzhJ z{C+mTeusm~a4+Swf}d{YN73h_xE^O0;9$tF+=rt;)FRq^k}cwNAk&3^i*<4*&NiIi zkLRdI^_}9`L^G2xOeWa|9620D9BCXD9B;+ZqaJH<9#oHN8J(Aidd#Ed9@eJ$AZwD| zk7EPkrx}*jG@}d$<~@aeY^DS4M;NR86mqA4^J$#d;QVu(LDiM7;XsZX)mOfu);&q( z82{D4P$Sb#|7!Yqco8Yj7koqWx61n&({)u&PhX}J8J8%Z{r3vg-gdl`l2lLP%-Fbc z=G)LOWC##DnfXVT#EZngReLvYU|(b`=~LdJUCcdk#_I@|5q1yz3Hua# zl-^B&z6+MT>i_diWSpbg07&NRnOECI!YZ35h#;97c>Y*Dk z!d{GTfPJ2QoPB})k^KjwXzNlo#+Jh~z6Nx%4r4lxZ9xEQ8+<}L*+F)gT>x$2BJB5c z8M~ZafxYRjVxMKtu+Kqv_zpY8zRP~dzQ=w9J>qw)!k%I$*jLzpBXj%rn3Sj4FW3j! z-Rx!dEB0Gtrct zFTiQ6(9f}-!F%0>*iZ+2*!`@L9brT8qn3c#OV}_QWy|1Goj}xW6(SfL*(RiCtzbLw zanxOGH{yqT*nhEo>=?V4UBWJ9SF%Iwb=U#>0Q)lg68k#V0sLo~72{>LadLDj9v^>* z+1HMj_3O`@EO&&<^;0|c#ZPXWEK8Z44;#_#-Mdq}!ihwgO_kYjYUDo5@$inJ#xgIK z<2&{>mZf5RPrUraHDzt~y!-08d3bd9XjwlxnJ8;AQ|r#3Or#RwlaulCnl&gYO@-s- z9=ht8nu?!NTJPLbu1AsjD_(A)`W9;W#Wj<0^ycKwc-g#WatBJ{RA;8EF1qU45#E91 zvv8RgrlwM5wq|ng)Kp_xQ;d(s%i7FN^h`IrX0ohH4VCq&A&g_H%y%@FwZ#-}a%ue} z>cTWM_5F=yU9oH!F5CyY6_C>7XKE+~h^K~jmZe?$%6vC^QP$-f%Z6f{x@sBT{g9UJ zLKAehv}1~z>=+SUH5TtPSlICBP(CpOdsA`lT$q)o@B+FpjB)RXkDg5Jq<6@v?Pz1&s?AsiCQ|jef1eFB^U}mK}JEQ-CB62<%3;%l6?N@sm5^Wjlb@ zSauf2H%y+=?ira%mu-7f#~aJ8;`rLh@%2h!IDvAHD0df6F~{)6$x{x;aGCEMDmw~< zv%ut`Q&#$8!=EyjF%z21n#ogy$N<*RNz5<3HlIl0w(2#of$HfNkOI|CVI*Vd{}@W= zPSrUk@D!4BQvmUBnGN2DAg;g$PZ69%8r?8icBF>lqh$+_&zu6n4aIlh^$)mQ9PD9e z=;V%5ZhfKrmO?lQFnKXHPoc5wE1u$XmH}rv`-`VEbPg0xY3Up+p3>1dR6M1pbGUfQ zK<7yDl#$NS;wclI8;XE}0{60h2Oyk^HK@GG|w}72}a#|z%G15~lIr?K2wZ1N8@mb?C0Q(*9JH1EcoFgX)%ijNWH1D4BAo*YY!fzwPv zM1Zq`9dvQ-^8%zjU{P|}iDp`G&5USPHVqf{o@`3R;{zx0yxzG@;!Vo4%39onmhtiq zVj88jllMz;O+0)*KC%{?8X`t&1W(3$@#fT0kT9loEj|E? z6E|l>C-4_Si&*9CyMP*fRXD`a16@{yLuvpZ>Yu498^JB&@ujITdMVA`K($T`Mujlj zFxeCzfXJoZOG+UgIy(jW41O&~)r@pkChu&F+ks8-#7v>Tjpaqvw+ZO0Q?-LsfcZnN zPVfKi>|CI$s?I!q&dGh>OnSzR4EZ74FO6o<#Kq&N%?b!zRn?6?>#)uI_zX-3MFGr#|L&b_$- zX~#Kx-E+RZ_qX5QYk&LRbFW2EG-?*x2dn1a-Z2rCnVY+;aciTSg+Z?ymAiOi$Ee(S zqjC#nOY1wgL`%cqocdCPLZL!~tqp@$)<4OBL|;$B(iiF*3)(o?NEheZkVU?QH4#}( zg|8anvtSr)$Q@LOXx-@i|4zgU-;JdHKXj`ZmWY}=bfM8bnNF|i+R;I}kw05=1;0zT64J&w}=Ish!?7U3))WwCGsED?@?rNR;L z(S~ivy%wNbf%t|U&}|Ldu*L=54iuy<6P~NI<-#-QkA!E?9l|r{PPI87w7b-%fVv3-1g*Ay3F@|f30h8_^RRp_F8Q4S~B_q@H)WzH0eScoJZ{As8K-S^ar4D))|g6gfl?ltBbX+x0sdK z5P&Lie*g-7BlTPzRQW&v3gN*36v8LKom;H$A&Xgk4+o&o9|=IAKT65DL4BJ7Pzaw4 zKp{K^?lr~w9=Dj)_o)CB`V#>t^iNarnxMYV1fUQ$2cQr>+psOk5mjicw$=|O@`Gj5 zIyM&6v`)p@4E<;^mZAF;+AP@q+_%+m6)EWNt7jmSQ{SWNimD1 zT6bmkB=NB(jOJ^2>I({u6_d&!czSwnbOquE(|Uhpv_E{W_cu`47D3c$d24FN|Dw z_AT!qXPD2qr~K1F94y0gziD{Iba#u-Kk%K8=k0YTjH>TBckC!Uht1+jc_<=Dy7(zR z0nGani3PBSY5O-JT*mHJ-hSanN^qj=h>wNoG1AY5aK>qNC$an0{`>)Uw%zP>SF_8_ zvESFOzZqyLDS4hZvxDyiV*uZC&avd%oa&m$jdTUq&8|51GH_-|()i#=9cF&DD5reubYA zhF}Ep%dBAq);eFu_tDM>7UFNYo{?#mK8{D{MtnMpoS!mM{5Ecc+5fN`WxhXRo}P`Y z$piRu9>Ndu*UrQ2zn0*gTxy<^&$1%7<2l=eH|J5jDa(1={~dUEb~<s8N1l|5_I?t zcRJiT72nfl=hK{!J;}?Y?#6@F;$Gs;a4&T)bL-s(_j0$<{eU|YpJ0De@McJtW#^8MX?E4uQ%1Nr{^3Y(VsElUHmmbUOYGqPZPf1j0U zy5fUDVr%f6HFJ)|XU&|QT-D#XA>Y-v_MW88vY;?8Xm{4kmgGQp&kDl{bDGPwG}O}A z)wwR8DOM3gi}hGEL~RQ4iZxjj;eg462f30be?Jil3lEx&z&9>@qhIen3@>?b(__F# z;BevUlR^fzJ6>D+qNPrzr*oheIUi9S)TsOsm~T)giIH)pNpXFA@sv*^Ri{$#fBG*G*ZGEB zq#SfA{g?f37J5zY3ID9WpLP!Od7AHE?x(!wiZ{ox65m1&>)atm&>v;!#1uWjGeP^h zT@x^m(WA zLvnW!JY?;&w6mMwa$d2Q`#$8chyHsWoEHdX=%Sxe>McT&oCI)<>m^Q<@2JjIKTnA! z^_V{0g>HM0u-W-4=L9>^Yd|T!wj8TRck%G8;M_*4YG01-Ryv#-{RVkX^vAJBIQf{Ok^668iEi=F zKwl^QL3m)FznzpJV&5V6pIO1vylMNCe;lel3&)*z1!=}R;`?74Lcrh1B>x?#_;;fV zonH&>p9(nK|26HLq)jeT-b4yWGGvJjJ2j+;PbC>nieJ@g@hay(ZifBesy+Xmunqb_ znX}-es7KdiyW1`7D-%*k^AFJe38)Poq6OdTmqdG$9&sW`Eh*YKE_~`eN!TCs1iK_! z+8gmTm}f%CBd$~#W<%-Rift+-xJKrMKK!2RTxoUz)H?r|Ni&-#wdudj=al~%;f(l` z-VGy(hWyuU&WBa6rH%A$29ofk|2P<&zVe0&$3FypoI)PCeA|N;n2i zDWQ|Xx=@ql_BTL!5W>p%CStnUftZ#wXQ_{Vtr z`5|KCs9o>V@c7G;rA0X< zSGazfC-jlkj}8iIxAoU}&ss~ARcXsBsezOO%mG@43#w)g#T}LYOK{#1p8KFAk9j@J z%x~wp4GP<0&+S-_Q6MxIm|CeV+)tn}USdu(LS5)+;=fX@(08B(NcLa1T=*tCzLFvy(kCJ+WQKCoJPipxj$P9y&^nJ2n%I>e zW6$htglJytMu<&dXmQ840@?P@sAq>y~Cj%SgrE!QiL*Vk6)b7Uewx)S$lD7FJbK^ z*_AE85?aXqa*}*4%)l*#N$e?k6O8TKa_qWRlhRGdv4g!AxQCEq7rT}o_YrDsZ~i`Z zs~ecB`w8RlH#`WeS-%KN`VnfDC0%K@OUi7QjM*-e&336`7d^QAQzJ@N9lPvrp1y>!cjdn~NqyJXrvtjiRXmV)l!P(D0IIH-gI3t%0& zQ$^cg7VPMENO`v_ElxI@TD(`hO0Dc-B9v`(U(gZ745glX8JD~WpY1nUTpn^S*7#pB0j+`^!z;t zmX!r{ru0fv>t!Z40_j!(HIIWkBsV;h8xph%cG6grC$%O|5++Y-@rJD-T~Z}sQl-|U zN(vuYFX@skX_GBklP%>YTPjVql$&g+GT9P$HW9{}gh?7-*BW0}7++Vw%U?EL-c{Ct zoRDUaF7n_E58f6m**c2vN+-}b=q&Re6$L>lNoeesz~!dBOb?adH-q;-##!y?U%1`w z6QPzS)31^S)zlDZG3l@Z#iyXNpp7FuUzWvTZI|NE5cG&GP3ylw28ptVjhkdE4_n;q zfRY{=7s_@8_jTjW^JqM5Nux}mnNJ!|oP{%o;A-jUbG!$owU362qbVde0AR)N-{&Xj zU93|)>XvO=1m02jqQ#zjcW)^}x?(9p>X`zF)OK4PRrfPgJO(y56 zvE8DKMjn^I?_$7(Z|C!Uh+B!|z(T=3kFt`Y7d#`j6F5!(WO3QOP%~0;n3kC}lNMUL zUmzg^Yb97Mtv5a(xafMI8{wCXYP2qe+085($vCew0~1vY`Yh<$YNZ*%E%wTC24soUY$KY+%l0KU7I>kld$DxMO2s-;h7Z?H!EU{SrKuwA~I%0cxFW;&5DSd6;WYU#QDCD3bP_2W<^BJ ziinsM5j86!ZdOFXtcbWt@MMY2p#PC-l$^n5-OM~oS8O*ulNG;1HLsz~D|iMw9(<6L z_Rs~g>2$gzc`Vzl!elUS%IDjkrE)i8vmSdFiL2R7gmP4NSu$Kl&1l+YwA^Mi9m-3O zn$**F=*Tte<~wLHerE{vRBNvQm0#|^fFw9*I_|XpB{Wo)Tm^YBWzC7VM(dWW>6UDX zZpoT%$=bMTM~zF?Z`8C(pikm9u5zQJl+jVS>56h2SGmzkm5r;$C}oU|YpjiHjEyU0 zbdfN+NE<~|7%fzUv>gfGGnqRlV(vS&7w@(npfCrSJ-M-n^wu1 zR>_*~=6m7HhV;oxUT&06IA`-KpRndRiQb>WOvGE3FUUD_ahvn!Kl2xJzvHHs)qB?E6lMstDv2!%Y5q|vwL8R z$!-uw1eBn=tT*2KWA`{KPzQH@yBLmQ1s+C@{?=`E*MwLJ@IM5cVRfdki&W`HsZ%?< z2z$Z;voVv7 diff --git a/res/fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf b/res/fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf deleted file mode 100644 index 339d59ac003965bb53004c6d81a9e67061571ea1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46556 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 T-*^7R`BNlMr=)*L%g_D~j~wAG diff --git a/res/fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf b/res/fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf deleted file mode 100644 index b5fcd891af7039f7b3fba5075e85cfed13cf88db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48080 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 From c568c15186ab475d7a310ea5735ec31076f46486 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 20 Nov 2019 17:35:10 +0000 Subject: [PATCH 0716/2372] In-memory keys need an object --- src/MatrixClientPeg.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index ef0130ec15..30983c452a 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -223,7 +223,7 @@ class MatrixClientPeg { if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { // TODO: Cross-signing keys are temporarily in memory only. A // separate task in the cross-signing project will build from here. - const keys = []; + const keys = {}; opts.cryptoCallbacks = { getCrossSigningKey: k => keys[k], saveCrossSigningKeys: newKeys => Object.assign(keys, newKeys), From e6dea37693d1db03d680f469d683aa7c30084016 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 20 Nov 2019 17:56:44 +0000 Subject: [PATCH 0717/2372] Add hidden button for bootstrapping SSSS This adds an testing button to the key backup panel which bootstraps the Secure Secret Storage system (and also cross-signing keys). Fixes https://github.com/vector-im/riot-web/issues/11212 --- .../views/settings/KeyBackupPanel.js | 46 +++++++++++++++++-- src/i18n/strings/en_EN.json | 2 + 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 67d2d32d50..f4740ea649 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -20,6 +20,7 @@ import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; +import SettingsStore from '../../../../lib/settings/SettingsStore'; export default class KeyBackupPanel extends React.PureComponent { constructor(props) { @@ -124,6 +125,27 @@ export default class KeyBackupPanel extends React.PureComponent { ); } + _bootstrapSecureSecretStorage = async () => { + try { + const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + await MatrixClientPeg.get().bootstrapSecretStorage({ + doInteractiveAuthFlow: async (makeRequest) => { + const { finished } = Modal.createTrackedDialog( + 'Cross-signing keys dialog', '', InteractiveAuthDialog, + { + title: _t("Send cross-signing keys to homeserver"), + matrixClient: MatrixClientPeg.get(), + makeRequest, + }, + ); + await finished; + }, + }); + } catch (e) { + console.error(e); + } + } + _deleteBackup = () => { const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, { @@ -298,6 +320,21 @@ export default class KeyBackupPanel extends React.PureComponent {
      • ; } else { + // This is a temporary button for testing SSSS. Initialising SSSS + // depends on cross-signing and is part of the same project, so we + // only show this mode when the cross-signing feature is enabled. + // TODO: Clean this up when removing the feature flag. + let bootstrapSecureSecretStorage; + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { + bootstrapSecureSecretStorage = ( +
        + + {_t("Bootstrap Secure Secret Storage (MSC1946)")} + +
        + ); + } + return

        {_t( @@ -307,9 +344,12 @@ export default class KeyBackupPanel extends React.PureComponent {

        {encryptedMessageAreEncrypted}

        {_t("Back up your keys before signing out to avoid losing them.")}

        - - { _t("Start using Key Backup") } - +
        + + {_t("Start using Key Backup")} + +
        + {bootstrapSecureSecretStorage}
        ; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c42a137800..e60007be5e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -511,6 +511,7 @@ "Connecting to integrations server...": "Connecting to integrations server...", "Cannot connect to integrations server": "Cannot connect to integrations server", "The integrations server is offline or it cannot reach your homeserver.": "The integrations server is offline or it cannot reach your homeserver.", + "Send cross-signing keys to homeserver": "Send cross-signing keys to homeserver", "Delete Backup": "Delete Backup", "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", @@ -533,6 +534,7 @@ "This backup is trusted because it has been restored on this device": "This backup is trusted because it has been restored on this device", "Backup version: ": "Backup version: ", "Algorithm: ": "Algorithm: ", + "Bootstrap Secure Secret Storage (MSC1946)": "Bootstrap Secure Secret Storage (MSC1946)", "Your keys are not being backed up from this device.": "Your keys are not being backed up from this device.", "Back up your keys before signing out to avoid losing them.": "Back up your keys before signing out to avoid losing them.", "Start using Key Backup": "Start using Key Backup", From 93e4de986128267b96cf2d307b890c248ed9003b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 11:00:39 -0700 Subject: [PATCH 0718/2372] Fix positioning and sizing of e2e icon in the composer This also removes the special case class for the composer because the input is now aligned regardless of icon. --- res/css/views/rooms/_MessageComposer.scss | 8 ++++---- src/components/views/rooms/MessageComposer.js | 6 +----- src/components/views/rooms/SlateMessageComposer.js | 6 +----- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 14562fe7ed..a0c8048475 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -23,10 +23,6 @@ limitations under the License. padding-left: 84px; } -.mx_MessageComposer_wrapper.mx_MessageComposer_hasE2EIcon { - padding-left: 109px; -} - .mx_MessageComposer_replaced_wrapper { margin-left: auto; margin-right: auto; @@ -78,6 +74,10 @@ limitations under the License. .mx_MessageComposer_e2eIcon.mx_E2EIcon { position: absolute; left: 60px; + width: 16px; + height: 16px; + margin-right: 0; // Counteract the E2EIcon class + margin-left: 3px; // Counteract the E2EIcon class &::after { background-color: $composer-e2e-icon-color; diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 632ca53f82..b41b970fc6 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -353,13 +353,9 @@ export default class MessageComposer extends React.Component { ); } - const wrapperClasses = classNames({ - mx_MessageComposer_wrapper: true, - mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus, - }); return (
        -
        +
        { controls }
        diff --git a/src/components/views/rooms/SlateMessageComposer.js b/src/components/views/rooms/SlateMessageComposer.js index 4bb2f29e61..eb41f6729b 100644 --- a/src/components/views/rooms/SlateMessageComposer.js +++ b/src/components/views/rooms/SlateMessageComposer.js @@ -460,13 +460,9 @@ export default class SlateMessageComposer extends React.Component { const showFormatBar = this.state.showFormatting && this.state.inputState.isRichTextEnabled; - const wrapperClasses = classNames({ - mx_MessageComposer_wrapper: true, - mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus, - }); return (
        -
        +
        { controls }
        From f6394b1251cef54042914c421dc15306bc0d2980 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 11:03:53 -0700 Subject: [PATCH 0719/2372] Remove stray colour correction on composer e2e icon We want it to match the room header --- res/css/views/rooms/_MessageComposer.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index a0c8048475..036756e2eb 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -78,10 +78,6 @@ limitations under the License. height: 16px; margin-right: 0; // Counteract the E2EIcon class margin-left: 3px; // Counteract the E2EIcon class - - &::after { - background-color: $composer-e2e-icon-color; - } } .mx_MessageComposer_noperm_error { From 8abc0953d518d7f7c47a1a1b74f92cdaf4a06567 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 11:08:53 -0700 Subject: [PATCH 0720/2372] Remove the import my IDE should have removed for me --- src/components/views/rooms/MessageComposer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index b41b970fc6..128f9be964 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -25,7 +25,6 @@ import RoomViewStore from '../../../stores/RoomViewStore'; import Stickerpicker from './Stickerpicker'; import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks'; import ContentMessages from '../../../ContentMessages'; -import classNames from 'classnames'; import E2EIcon from './E2EIcon'; function ComposerAvatar(props) { From 966f84115dc16415269e5285d104f90e3596a988 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 20 Nov 2019 18:26:29 +0000 Subject: [PATCH 0721/2372] js-sdk rc.1 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index eb234e0573..57e8dd77e3 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "lolex": "4.2", - "matrix-js-sdk": "2.4.3", + "matrix-js-sdk": "2.4.4-rc.1", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index 3e43c29ef6..eb72b11793 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5197,10 +5197,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc" integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw== -matrix-js-sdk@2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.3.tgz#23b78cc707a02eb0ce7eecb3aa50129e46dd5b6e" - integrity sha512-8qTqILd/NmTWF24tpaxmDIzkTk/bZhPD5N8h69PlvJ5Y6kMFctpRj+Tud5zZjl5/yhO07+g+JCyDzg+AagiM/A== +matrix-js-sdk@2.4.4-rc.1: + version "2.4.4-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.4-rc.1.tgz#5fd33fd11be9eea23cd0d0b8eb79da7a4b6253bf" + integrity sha512-Kn94zZMXh2EmihYL3lWNp2lpT7RtqcaUxjkP7H9Mr113swSOXtKr8RWMrvopAIguC1pcLzL+lCk+N8rrML2A4Q== dependencies: another-json "^0.2.0" babel-runtime "^6.26.0" From 318a720e75e9e70831aa7f9cd775cb902274b657 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 20 Nov 2019 18:29:16 +0000 Subject: [PATCH 0722/2372] Prepare changelog for v1.7.3-rc.1 --- CHANGELOG.md | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c46530fad..2dad0accd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,97 @@ +Changes in [1.7.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.3-rc.1) (2019-11-20) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.2...v1.7.3-rc.1) + + * Fix positioning, size, and colour of the composer e2e icon + [\#3641](https://github.com/matrix-org/matrix-react-sdk/pull/3641) + * upgrade nunito from 3.500 to 3.504 + [\#3639](https://github.com/matrix-org/matrix-react-sdk/pull/3639) + * Wire up the widget permission prompt to the cross-platform setting + [\#3630](https://github.com/matrix-org/matrix-react-sdk/pull/3630) + * Get theme automatically from system setting + [\#3637](https://github.com/matrix-org/matrix-react-sdk/pull/3637) + * Update code style for our 90 char life + [\#3636](https://github.com/matrix-org/matrix-react-sdk/pull/3636) + * use general warning icon instead of e2e one for room status + [\#3633](https://github.com/matrix-org/matrix-react-sdk/pull/3633) + * Add support for platform specific event indexing and search + [\#3550](https://github.com/matrix-org/matrix-react-sdk/pull/3550) + * Update from Weblate + [\#3635](https://github.com/matrix-org/matrix-react-sdk/pull/3635) + * Use a settings watcher to set the theme + [\#3634](https://github.com/matrix-org/matrix-react-sdk/pull/3634) + * Merge the `feature_user_info_panel` flag into `feature_dm_verification` + [\#3632](https://github.com/matrix-org/matrix-react-sdk/pull/3632) + * Fix some styling regressions in member panel + [\#3631](https://github.com/matrix-org/matrix-react-sdk/pull/3631) + * Add a bit more safety around breadcrumbs + [\#3629](https://github.com/matrix-org/matrix-react-sdk/pull/3629) + * Ensure widgets always have a sender associated with them + [\#3628](https://github.com/matrix-org/matrix-react-sdk/pull/3628) + * re-add missing case of codepath + [\#3627](https://github.com/matrix-org/matrix-react-sdk/pull/3627) + * Implement the bulk of the new widget permission prompt design + [\#3622](https://github.com/matrix-org/matrix-react-sdk/pull/3622) + * Relax identity server discovery error handling + [\#3588](https://github.com/matrix-org/matrix-react-sdk/pull/3588) + * Add cross-signing feature flag + [\#3626](https://github.com/matrix-org/matrix-react-sdk/pull/3626) + * Attempt number two at ripping out Bluebird from rageshake.js + [\#3624](https://github.com/matrix-org/matrix-react-sdk/pull/3624) + * Update from Weblate + [\#3625](https://github.com/matrix-org/matrix-react-sdk/pull/3625) + * Remove Bluebird: phase 2.1 + [\#3618](https://github.com/matrix-org/matrix-react-sdk/pull/3618) + * Add better error handling to Synapse user deactivation + [\#3619](https://github.com/matrix-org/matrix-react-sdk/pull/3619) + * New design for member panel + [\#3620](https://github.com/matrix-org/matrix-react-sdk/pull/3620) + * Show server details on login for unreachable homeserver + [\#3617](https://github.com/matrix-org/matrix-react-sdk/pull/3617) + * Add a function to get the "base" theme for a theme + [\#3615](https://github.com/matrix-org/matrix-react-sdk/pull/3615) + * Remove Bluebird: phase 2 + [\#3616](https://github.com/matrix-org/matrix-react-sdk/pull/3616) + * Remove Bluebird: phase 1 + [\#3612](https://github.com/matrix-org/matrix-react-sdk/pull/3612) + * Move notification count to in front of the room name in the page title + [\#3613](https://github.com/matrix-org/matrix-react-sdk/pull/3613) + * Add some logging/recovery for lost rooms + [\#3614](https://github.com/matrix-org/matrix-react-sdk/pull/3614) + * Add Mjolnir ban list support + [\#3585](https://github.com/matrix-org/matrix-react-sdk/pull/3585) + * Improve room switching performance with alias cache + [\#3610](https://github.com/matrix-org/matrix-react-sdk/pull/3610) + * Fix draw order when hovering composer format buttons + [\#3609](https://github.com/matrix-org/matrix-react-sdk/pull/3609) + * Use a ternary operator instead of relying on AND semantics in + EditHistoryDialog + [\#3606](https://github.com/matrix-org/matrix-react-sdk/pull/3606) + * Update from Weblate + [\#3608](https://github.com/matrix-org/matrix-react-sdk/pull/3608) + * Fix HTML fallback in replies + [\#3607](https://github.com/matrix-org/matrix-react-sdk/pull/3607) + * Fix rounded corners for the formatting toolbar + [\#3605](https://github.com/matrix-org/matrix-react-sdk/pull/3605) + * Check for a message type before assuming it is a room message + [\#3604](https://github.com/matrix-org/matrix-react-sdk/pull/3604) + * Remove lint comments about no-descending-specificity + [\#3603](https://github.com/matrix-org/matrix-react-sdk/pull/3603) + * Show verification requests in the timeline + [\#3601](https://github.com/matrix-org/matrix-react-sdk/pull/3601) + * Match identity server registration to the IS r0.3.0 spec + [\#3602](https://github.com/matrix-org/matrix-react-sdk/pull/3602) + * Restore thumbs after variation selector removal + [\#3600](https://github.com/matrix-org/matrix-react-sdk/pull/3600) + * Fix breadcrumbs so the bar is a toolbar and the buttons are buttons. + [\#3599](https://github.com/matrix-org/matrix-react-sdk/pull/3599) + * Now that part of spacing is padding, make it smaller when collapsed + [\#3597](https://github.com/matrix-org/matrix-react-sdk/pull/3597) + * Remove variation selectors from quick reactions + [\#3598](https://github.com/matrix-org/matrix-react-sdk/pull/3598) + * Fix linkify imports + [\#3595](https://github.com/matrix-org/matrix-react-sdk/pull/3595) + Changes in [1.7.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.2) (2019-11-06) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.1...v1.7.2) From ef475bbdadd9c4ae1ab9057b6b18602cf39af137 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 20 Nov 2019 18:29:16 +0000 Subject: [PATCH 0723/2372] v1.7.3-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 57e8dd77e3..e5d2d7635c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.2", + "version": "1.7.3-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From fccf9f138e53fd8286cd42066233a16eff814d00 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 20 Nov 2019 22:32:11 +0000 Subject: [PATCH 0724/2372] Add eslint-plugin-jest because we inherit js-sdk's eslintrc and it wants Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 1 + yarn.lock | 72 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index eb234e0573..fe399e4c49 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "eslint": "^5.12.0", "eslint-config-google": "^0.7.1", "eslint-plugin-babel": "^5.2.1", + "eslint-plugin-jest": "^23.0.4", "eslint-plugin-flowtype": "^2.30.0", "eslint-plugin-react": "^7.7.0", "eslint-plugin-react-hooks": "^2.0.1", diff --git a/yarn.lock b/yarn.lock index 3e43c29ef6..8bfaaa74c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -244,6 +244,11 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/json-schema@^7.0.3": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" + integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -293,6 +298,28 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/experimental-utils@^2.5.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.8.0.tgz#208b4164d175587e9b03ce6fea97d55f19c30ca9" + integrity sha512-jZ05E4SxCbbXseQGXOKf3ESKcsGxT8Ucpkp1jiVp55MGhOvZB2twmWKf894PAuVQTCgbPbJz9ZbRDqtUWzP8xA== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.8.0" + eslint-scope "^5.0.0" + +"@typescript-eslint/typescript-estree@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.8.0.tgz#fcc3fe6532840085d29b75432c8a59895876aeca" + integrity sha512-ksvjBDTdbAQ04cR5JyFSDX113k66FxH1tAXmi+dj6hufsl/G0eMc/f1GgLjEVPkYClDbRKv+rnBFuE5EusomUw== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash.unescape "4.0.1" + semver "^6.3.0" + tsutils "^3.17.1" + "@webassemblyjs/ast@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" @@ -2891,6 +2918,13 @@ eslint-plugin-flowtype@^2.30.0: dependencies: lodash "^4.17.10" +eslint-plugin-jest@^23.0.4: + version "23.0.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.0.4.tgz#1ab81ffe3b16c5168efa72cbd4db14d335092aa0" + integrity sha512-OaP8hhT8chJNodUPvLJ6vl8gnalcsU/Ww1t9oR3HnGdEWjm/DdCCUXLOral+IPGAeWu/EwgVQCK/QtxALpH1Yw== + dependencies: + "@typescript-eslint/experimental-utils" "^2.5.0" + eslint-plugin-react-hooks@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.0.1.tgz#e898ec26a0a335af6f7b0ad1f0bedda7143ed756" @@ -2924,6 +2958,14 @@ eslint-scope@^4.0.3: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-scope@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" + integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + eslint-utils@^1.3.1: version "1.4.2" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab" @@ -2931,7 +2973,7 @@ eslint-utils@^1.3.1: dependencies: eslint-visitor-keys "^1.0.0" -eslint-visitor-keys@^1.0.0: +eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== @@ -3714,6 +3756,18 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-modules@2.0.0, global-modules@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" @@ -5038,6 +5092,11 @@ lodash.mergewith@^4.6.1: resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== +lodash.unescape@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" + integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= + lodash@^4.1.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.2.1: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" @@ -7149,7 +7208,7 @@ selection-is-backward@^1.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@6.3.0: +semver@6.3.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -8086,11 +8145,18 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.4.tgz#3b52b1f13924f460c3fbfd0df69b587dbcbc762e" integrity sha512-tdzBRDGWcI1OpPVmChbdSKhvSVurznZ8X36AYURAcl+0o2ldlCY2XPzyXNNxwJwwyIU+rIglTCG4kxtNKBQH7Q== -tslib@^1.9.0: +tslib@^1.8.1, tslib@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== +tsutils@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + dependencies: + tslib "^1.8.1" + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" From 62a2c7a51a58bbfb992565868da873ccbc65d682 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 16:26:06 -0700 Subject: [PATCH 0725/2372] Re-add encryption warning to widget permission prompt --- src/components/views/elements/AppPermission.js | 5 ++++- src/components/views/elements/AppTile.js | 2 ++ src/i18n/strings/en_EN.json | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js index 422427d4c4..c514dbc950 100644 --- a/src/components/views/elements/AppPermission.js +++ b/src/components/views/elements/AppPermission.js @@ -30,6 +30,7 @@ export default class AppPermission extends React.Component { creatorUserId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired, onPermissionGranted: PropTypes.func.isRequired, + isRoomEncrypted: PropTypes.bool, }; static defaultProps = { @@ -114,6 +115,8 @@ export default class AppPermission extends React.Component { : _t("Using this widget may share data with %(widgetDomain)s.", {widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip}); + const encryptionWarning = this.props.isRoomEncrypted ? _t("Widgets are not encrypted.") : null; + return (
        @@ -128,7 +131,7 @@ export default class AppPermission extends React.Component { {warning}
        - {_t("This widget may use cookies.")} + {_t("This widget may use cookies.")} {encryptionWarning}
        diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index db5978c792..4b0079098a 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -585,12 +585,14 @@ export default class AppTile extends React.Component {
        ); if (!this.state.hasPermissionToLoad) { + const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); appTileBody = (
        diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7709a4a398..8b71a1e182 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1195,6 +1195,7 @@ "Widget ID": "Widget ID", "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Using this widget may share data with %(widgetDomain)s & your Integration Manager.", "Using this widget may share data with %(widgetDomain)s.": "Using this widget may share data with %(widgetDomain)s.", + "Widgets are not encrypted.": "Widgets are not encrypted.", "Widget added by": "Widget added by", "This widget may use cookies.": "This widget may use cookies.", "Delete Widget": "Delete Widget", From a40194194d10756414703abad5fc45cd5d180188 Mon Sep 17 00:00:00 2001 From: bkil Date: Thu, 21 Nov 2019 01:50:18 +0100 Subject: [PATCH 0726/2372] ReactionsRowButtonTooltip: fix null dereference if emoji owner left room Signed-off-by: bkil --- src/components/views/messages/ReactionsRowButtonTooltip.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.js b/src/components/views/messages/ReactionsRowButtonTooltip.js index b70724d516..d7e1ef3488 100644 --- a/src/components/views/messages/ReactionsRowButtonTooltip.js +++ b/src/components/views/messages/ReactionsRowButtonTooltip.js @@ -43,7 +43,8 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent { if (room) { const senders = []; for (const reactionEvent of reactionEvents) { - const { name } = room.getMember(reactionEvent.getSender()); + const member = room.getMember(reactionEvent.getSender()); + const name = member ? member.name : reactionEvent.getSender(); senders.push(name); } const shortName = unicodeToShortcode(content); From fd12eb28e76eab962d7748a63ce00907106c67c5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 19:17:42 -0700 Subject: [PATCH 0727/2372] Move many widget options to a context menu Part of https://github.com/vector-im/riot-web/issues/11262 --- res/css/_components.scss | 1 + .../context_menus/_WidgetContextMenu.scss | 36 +++++ res/css/views/rooms/_AppsDrawer.scss | 32 +---- res/img/feather-customised/widget/bin.svg | 65 --------- res/img/feather-customised/widget/camera.svg | 6 - res/img/feather-customised/widget/edit.svg | 6 - res/img/feather-customised/widget/refresh.svg | 6 - .../feather-customised/widget/x-circle.svg | 6 - .../views/context_menus/WidgetContextMenu.js | 134 ++++++++++++++++++ src/components/views/elements/AppTile.js | 105 +++++++------- src/i18n/strings/en_EN.json | 8 +- 11 files changed, 234 insertions(+), 171 deletions(-) create mode 100644 res/css/views/context_menus/_WidgetContextMenu.scss delete mode 100644 res/img/feather-customised/widget/bin.svg delete mode 100644 res/img/feather-customised/widget/camera.svg delete mode 100644 res/img/feather-customised/widget/edit.svg delete mode 100644 res/img/feather-customised/widget/refresh.svg delete mode 100644 res/img/feather-customised/widget/x-circle.svg create mode 100644 src/components/views/context_menus/WidgetContextMenu.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 40a2c576d0..45c0443cfb 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -48,6 +48,7 @@ @import "./views/context_menus/_StatusMessageContextMenu.scss"; @import "./views/context_menus/_TagTileContextMenu.scss"; @import "./views/context_menus/_TopLeftMenu.scss"; +@import "./views/context_menus/_WidgetContextMenu.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; @import "./views/dialogs/_ChangelogDialog.scss"; diff --git a/res/css/views/context_menus/_WidgetContextMenu.scss b/res/css/views/context_menus/_WidgetContextMenu.scss new file mode 100644 index 0000000000..314c3be7bb --- /dev/null +++ b/res/css/views/context_menus/_WidgetContextMenu.scss @@ -0,0 +1,36 @@ +/* +Copyright 2019 The Matrix.org Foundaction C.I.C. + +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. +*/ + +.mx_WidgetContextMenu { + padding: 6px; + + .mx_WidgetContextMenu_option { + padding: 3px 6px 3px 6px; + cursor: pointer; + white-space: nowrap; + } + + .mx_WidgetContextMenu_separator { + margin-top: 0; + margin-bottom: 0; + border-bottom-style: none; + border-left-style: none; + border-right-style: none; + border-top-style: solid; + border-top-width: 1px; + border-color: $menu-border-color; + } +} diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index 6f5e3abade..a3fe573ad0 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -153,40 +153,12 @@ $AppsDrawerBodyHeight: 273px; background-color: $accent-color; } -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_reload { - mask-image: url('$(res)/img/feather-customised/widget/refresh.svg'); - mask-size: 100%; -} - .mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_popout { mask-image: url('$(res)/img/feather-customised/widget/external-link.svg'); } -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_snapshot { - mask-image: url('$(res)/img/feather-customised/widget/camera.svg'); -} - -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_edit { - mask-image: url('$(res)/img/feather-customised/widget/edit.svg'); -} - -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_delete { - mask-image: url('$(res)/img/feather-customised/widget/bin.svg'); - background-color: $warning-color; -} - -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_cancel { - mask-image: url('$(res)/img/feather-customised/widget/x-circle.svg'); -} - -/* delete ? */ -.mx_AppTileMenuBarWidget { - cursor: pointer; - width: 10px; - height: 10px; - padding: 1px; - transition-duration: 500ms; - border: 1px solid transparent; +.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_menu { + mask-image: url('$(res)/img/icon_context.svg'); } .mx_AppTileMenuBarWidgetDelete { diff --git a/res/img/feather-customised/widget/bin.svg b/res/img/feather-customised/widget/bin.svg deleted file mode 100644 index 7616d8931b..0000000000 --- a/res/img/feather-customised/widget/bin.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/res/img/feather-customised/widget/camera.svg b/res/img/feather-customised/widget/camera.svg deleted file mode 100644 index 5502493068..0000000000 --- a/res/img/feather-customised/widget/camera.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/widget/edit.svg b/res/img/feather-customised/widget/edit.svg deleted file mode 100644 index 749e83f982..0000000000 --- a/res/img/feather-customised/widget/edit.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/widget/refresh.svg b/res/img/feather-customised/widget/refresh.svg deleted file mode 100644 index 0994bbdd52..0000000000 --- a/res/img/feather-customised/widget/refresh.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/widget/x-circle.svg b/res/img/feather-customised/widget/x-circle.svg deleted file mode 100644 index 951407b39c..0000000000 --- a/res/img/feather-customised/widget/x-circle.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/components/views/context_menus/WidgetContextMenu.js b/src/components/views/context_menus/WidgetContextMenu.js new file mode 100644 index 0000000000..43e7e172cc --- /dev/null +++ b/src/components/views/context_menus/WidgetContextMenu.js @@ -0,0 +1,134 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; +import {_t} from '../../../languageHandler'; + +export default class WidgetContextMenu extends React.Component { + static propTypes = { + onFinished: PropTypes.func, + + // Callback for when the revoke button is clicked. Required. + onRevokeClicked: PropTypes.func.isRequired, + + // Callback for when the snapshot button is clicked. Button not shown + // without a callback. + onSnapshotClicked: PropTypes.func, + + // Callback for when the reload button is clicked. Button not shown + // without a callback. + onReloadClicked: PropTypes.func, + + // Callback for when the edit button is clicked. Button not shown + // without a callback. + onEditClicked: PropTypes.func, + + // Callback for when the delete button is clicked. Button not shown + // without a callback. + onDeleteClicked: PropTypes.func, + }; + + proxyClick(fn) { + fn(); + if (this.props.onFinished) this.props.onFinished(); + } + + // XXX: It's annoying that our context menus require us to hit onFinished() to close :( + + onEditClicked = () => { + this.proxyClick(this.props.onEditClicked); + }; + + onReloadClicked = () => { + this.proxyClick(this.props.onReloadClicked); + }; + + onSnapshotClicked = () => { + this.proxyClick(this.props.onSnapshotClicked); + }; + + onDeleteClicked = () => { + this.proxyClick(this.props.onDeleteClicked); + }; + + onRevokeClicked = () => { + this.proxyClick(this.props.onRevokeClicked); + }; + + render() { + const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); + + const options = []; + + if (this.props.onEditClicked) { + options.push( + + {_t("Edit")} + , + ); + } + + if (this.props.onReloadClicked) { + options.push( + + {_t("Reload")} + , + ); + } + + if (this.props.onSnapshotClicked) { + options.push( + + {_t("Take picture")} + , + ); + } + + if (this.props.onDeleteClicked) { + options.push( + + {_t("Remove for everyone")} + , + ); + } + + // Push this last so it appears last. It's always present. + options.push( + + {_t("Remove for me")} + , + ); + + // Put separators between the options + if (options.length > 1) { + const length = options.length; + for (let i = 0; i < length - 1; i++) { + const sep =
        ; + + // Insert backwards so the insertions don't affect our math on where to place them. + // We also use our cached length to avoid worrying about options.length changing + options.splice(length - 1 - i, 0, sep); + } + } + + return
        {options}
        ; + } +} diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index db5978c792..0010e8022e 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -35,6 +35,7 @@ import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import classNames from 'classnames'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import {createMenu} from "../../structures/ContextualMenu"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ENABLE_REACT_PERF = false; @@ -52,7 +53,7 @@ export default class AppTile extends React.Component { this._onLoaded = this._onLoaded.bind(this); this._onEditClick = this._onEditClick.bind(this); this._onDeleteClick = this._onDeleteClick.bind(this); - this._onCancelClick = this._onCancelClick.bind(this); + this._onRevokeClicked = this._onRevokeClicked.bind(this); this._onSnapshotClick = this._onSnapshotClick.bind(this); this.onClickMenuBar = this.onClickMenuBar.bind(this); this._onMinimiseClick = this._onMinimiseClick.bind(this); @@ -271,7 +272,7 @@ export default class AppTile extends React.Component { return WidgetUtils.canUserModifyWidgets(this.props.room.roomId); } - _onEditClick(e) { + _onEditClick() { console.log("Edit widget ID ", this.props.id); if (this.props.onEditClick) { this.props.onEditClick(); @@ -293,7 +294,7 @@ export default class AppTile extends React.Component { } } - _onSnapshotClick(e) { + _onSnapshotClick() { console.warn("Requesting widget snapshot"); ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot() .catch((err) => { @@ -360,13 +361,9 @@ export default class AppTile extends React.Component { } } - _onCancelClick() { - if (this.props.onDeleteClick) { - this.props.onDeleteClick(); - } else { - console.log("Revoke widget permissions - %s", this.props.id); - this._revokeWidgetPermission(); - } + _onRevokeClicked() { + console.log("Revoke widget permissions - %s", this.props.id); + this._revokeWidgetPermission(); } /** @@ -544,18 +541,59 @@ export default class AppTile extends React.Component { } } - _onPopoutWidgetClick(e) { + _onPopoutWidgetClick() { // Using Object.assign workaround as the following opens in a new window instead of a new tab. // window.open(this._getSafeUrl(), '_blank', 'noopener=yes'); Object.assign(document.createElement('a'), { target: '_blank', href: this._getSafeUrl(), rel: 'noopener'}).click(); } - _onReloadWidgetClick(e) { + _onReloadWidgetClick() { // Reload iframe in this way to avoid cross-origin restrictions this.refs.appFrame.src = this.refs.appFrame.src; } + _getMenuOptions(ev) { + // TODO: This block of code gets copy/pasted a lot. We should make that happen less. + const menuOptions = {}; + const buttonRect = ev.target.getBoundingClientRect(); + // The window X and Y offsets are to adjust position when zoomed in to page + const buttonLeft = buttonRect.left + window.pageXOffset; + const buttonTop = buttonRect.top + window.pageYOffset; + // Align the right edge of the menu to the left edge of the button + menuOptions.right = window.innerWidth - buttonLeft; + // Align the menu vertically on whichever side of the button has more + // space available. + if (buttonTop < window.innerHeight / 2) { + menuOptions.top = buttonTop; + } else { + menuOptions.bottom = window.innerHeight - buttonTop; + } + return menuOptions; + } + + _onContextMenuClick = (ev) => { + const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu'); + const menuOptions = { + ...this._getMenuOptions(ev), + + // A revoke handler is always required + onRevokeClicked: this._onRevokeClicked, + }; + + const canUserModify = this._canUserModify(); + const showEditButton = Boolean(this._scalarClient && canUserModify); + const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify; + const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show; + + if (showEditButton) menuOptions.onEditClicked = this._onEditClick; + if (showDeleteButton) menuOptions.onDeleteClicked = this._onDeleteClick; + if (showPictureSnapshotButton) menuOptions.onSnapshotClicked = this._onSnapshotClick; + if (this.props.showReload) menuOptions.onReloadClicked = this._onReloadWidgetClick; + + createMenu(WidgetContextMenu, menuOptions); + }; + render() { let appTileBody; @@ -565,7 +603,7 @@ export default class AppTile extends React.Component { } // Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin - // because that would allow the iframe to prgramatically remove the sandbox attribute, but + // because that would allow the iframe to programmatically remove the sandbox attribute, but // this would only be for content hosted on the same origin as the riot client: anything // hosted on the same origin as the client will get the same access as if you clicked // a link to it. @@ -643,13 +681,6 @@ export default class AppTile extends React.Component { } } - // editing is done in scalar - const canUserModify = this._canUserModify(); - const showEditButton = Boolean(this._scalarClient && canUserModify); - const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify; - const showCancelButton = (this.props.showCancel === undefined || this.props.showCancel) && !showDeleteButton; - // Picture snapshot - only show button when apps are maximised. - const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show; const showMinimiseButton = this.props.showMinimise && this.props.show; const showMaximiseButton = this.props.showMinimise && !this.props.show; @@ -688,41 +719,17 @@ export default class AppTile extends React.Component { { this.props.showTitle && this._getTileTitle() } - { /* Reload widget */ } - { this.props.showReload && } { /* Popout widget */ } { this.props.showPopout && } - { /* Snapshot widget */ } - { showPictureSnapshotButton && } - { /* Edit widget */ } - { showEditButton && } - { /* Delete widget */ } - { showDeleteButton && } - { /* Cancel widget */ } - { showCancelButton && }
        } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7709a4a398..dbe5cb3e08 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1204,10 +1204,8 @@ "An error ocurred whilst trying to remove the widget from the room": "An error ocurred whilst trying to remove the widget from the room", "Minimize apps": "Minimize apps", "Maximize apps": "Maximize apps", - "Reload widget": "Reload widget", "Popout widget": "Popout widget", - "Picture": "Picture", - "Revoke widget access": "Revoke widget access", + "More options": "More options", "Create new room": "Create new room", "Unblacklist": "Unblacklist", "Blacklist": "Blacklist", @@ -1564,6 +1562,10 @@ "Hide": "Hide", "Home": "Home", "Sign in": "Sign in", + "Reload": "Reload", + "Take picture": "Take picture", + "Remove for everyone": "Remove for everyone", + "Remove for me": "Remove for me", "powered by Matrix": "powered by Matrix", "This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.", "Custom Server Options": "Custom Server Options", From 66c51704cc7edb7f2bb758b5eba785f07681b200 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 19:21:10 -0700 Subject: [PATCH 0728/2372] Fix indentation --- .../context_menus/_WidgetContextMenu.scss | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/res/css/views/context_menus/_WidgetContextMenu.scss b/res/css/views/context_menus/_WidgetContextMenu.scss index 314c3be7bb..60b7b93f99 100644 --- a/res/css/views/context_menus/_WidgetContextMenu.scss +++ b/res/css/views/context_menus/_WidgetContextMenu.scss @@ -17,20 +17,20 @@ limitations under the License. .mx_WidgetContextMenu { padding: 6px; - .mx_WidgetContextMenu_option { - padding: 3px 6px 3px 6px; - cursor: pointer; - white-space: nowrap; - } + .mx_WidgetContextMenu_option { + padding: 3px 6px 3px 6px; + cursor: pointer; + white-space: nowrap; + } - .mx_WidgetContextMenu_separator { - margin-top: 0; - margin-bottom: 0; - border-bottom-style: none; - border-left-style: none; - border-right-style: none; - border-top-style: solid; - border-top-width: 1px; - border-color: $menu-border-color; - } + .mx_WidgetContextMenu_separator { + margin-top: 0; + margin-bottom: 0; + border-bottom-style: none; + border-left-style: none; + border-right-style: none; + border-top-style: solid; + border-top-width: 1px; + border-color: $menu-border-color; + } } From b0eb54541cbe18c67c91f9017ec609478bf53ce1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 19:50:13 -0700 Subject: [PATCH 0729/2372] Rip out options to change your integration manager We are not supporting this due to the complexity involved in switching integration managers. We still support custom ones under the hood, just not to the common user. A later sprint on integrations will consider re-adding the option alongside fixing the various bugs out there. --- .../settings/_SetIntegrationManager.scss | 8 - .../views/settings/SetIntegrationManager.js | 153 +----------------- 2 files changed, 2 insertions(+), 159 deletions(-) diff --git a/res/css/views/settings/_SetIntegrationManager.scss b/res/css/views/settings/_SetIntegrationManager.scss index 99537f9eb4..454fb95cf7 100644 --- a/res/css/views/settings/_SetIntegrationManager.scss +++ b/res/css/views/settings/_SetIntegrationManager.scss @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_SetIntegrationManager .mx_Field_input { - @mixin mx_Settings_fullWidthField; -} - .mx_SetIntegrationManager { margin-top: 10px; margin-bottom: 10px; @@ -31,7 +27,3 @@ limitations under the License. display: inline-block; padding-left: 5px; } - -.mx_SetIntegrationManager_tooltip { - @mixin mx_Settings_tooltip; -} diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js index b1268c8048..2482b3c846 100644 --- a/src/components/views/settings/SetIntegrationManager.js +++ b/src/components/views/settings/SetIntegrationManager.js @@ -16,13 +16,7 @@ limitations under the License. import React from 'react'; import {_t} from "../../../languageHandler"; -import sdk from '../../../index'; -import Field from "../elements/Field"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; -import MatrixClientPeg from "../../../MatrixClientPeg"; -import {SERVICE_TYPES} from "matrix-js-sdk"; -import {IntegrationManagerInstance} from "../../../integrations/IntegrationManagerInstance"; -import Modal from "../../../Modal"; export default class SetIntegrationManager extends React.Component { constructor() { @@ -32,136 +26,10 @@ export default class SetIntegrationManager extends React.Component { this.state = { currentManager, - url: "", // user-entered text - error: null, - busy: false, - checking: false, }; } - _onUrlChanged = (ev) => { - const u = ev.target.value; - this.setState({url: u}); - }; - - _getTooltip = () => { - if (this.state.checking) { - const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); - return
        - - { _t("Checking server") } -
        ; - } else if (this.state.error) { - return {this.state.error}; - } else { - return null; - } - }; - - _canChange = () => { - return !!this.state.url && !this.state.busy; - }; - - _continueTerms = async (manager) => { - try { - await IntegrationManagers.sharedInstance().overwriteManagerOnAccount(manager); - this.setState({ - busy: false, - error: null, - currentManager: IntegrationManagers.sharedInstance().getPrimaryManager(), - url: "", // clear input - }); - } catch (e) { - console.error(e); - this.setState({ - busy: false, - error: _t("Failed to update integration manager"), - }); - } - }; - - _setManager = async (ev) => { - // Don't reload the page when the user hits enter in the form. - ev.preventDefault(); - ev.stopPropagation(); - - this.setState({busy: true, checking: true, error: null}); - - let offline = false; - let manager: IntegrationManagerInstance; - try { - manager = await IntegrationManagers.sharedInstance().tryDiscoverManager(this.state.url); - offline = !manager; // no manager implies offline - } catch (e) { - console.error(e); - offline = true; // probably a connection error - } - if (offline) { - this.setState({ - busy: false, - checking: false, - error: _t("Integration manager offline or not accessible."), - }); - return; - } - - // Test the manager (causes terms of service prompt if agreement is needed) - // We also cancel the tooltip at this point so it doesn't collide with the dialog. - this.setState({checking: false}); - try { - const client = manager.getScalarClient(); - await client.connect(); - } catch (e) { - console.error(e); - this.setState({ - busy: false, - error: _t("Terms of service not accepted or the integration manager is invalid."), - }); - return; - } - - // Specifically request the terms of service to see if there are any. - // The above won't trigger a terms of service check if there are no terms to - // sign, so when there's no terms at all we need to ensure we tell the user. - let hasTerms = true; - try { - const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IM, manager.trimmedApiUrl); - hasTerms = terms && terms['policies'] && Object.keys(terms['policies']).length > 0; - } catch (e) { - // Assume errors mean there are no terms. This could be a 404, 500, etc - console.error(e); - hasTerms = false; - } - if (!hasTerms) { - this.setState({busy: false}); - const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); - Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { - title: _t("Integration manager has no terms of service"), - description: ( -
        - - {_t("The integration manager you have chosen does not have any terms of service.")} - - -  {_t("Only continue if you trust the owner of the server.")} - -
        - ), - button: _t("Continue"), - onFinished: async (confirmed) => { - if (!confirmed) return; - this._continueTerms(manager); - }, - }); - return; - } - - this._continueTerms(manager); - }; - render() { - const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); - const currentManager = this.state.currentManager; let managerName; let bodyText; @@ -181,7 +49,7 @@ export default class SetIntegrationManager extends React.Component { } return ( -
        +
        {_t("Integration Manager")} {managerName} @@ -189,24 +57,7 @@ export default class SetIntegrationManager extends React.Component { {bodyText} - - {_t("Change")} - +
        ); } } From 0a0e952691808ed17787bf2950825a9567cfd2d8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 19:53:52 -0700 Subject: [PATCH 0730/2372] Update integration manager copy --- .../views/settings/SetIntegrationManager.js | 15 +++++++++------ src/i18n/strings/en_EN.json | 13 ++++--------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js index 2482b3c846..11dadb4918 100644 --- a/src/components/views/settings/SetIntegrationManager.js +++ b/src/components/views/settings/SetIntegrationManager.js @@ -36,26 +36,29 @@ export default class SetIntegrationManager extends React.Component { if (currentManager) { managerName = `(${currentManager.name})`; bodyText = _t( - "You are currently using %(serverName)s to manage your bots, widgets, " + + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, " + "and sticker packs.", {serverName: currentManager.name}, { b: sub => {sub} }, ); } else { - bodyText = _t( - "Add which integration manager you want to manage your bots, widgets, " + - "and sticker packs.", - ); + bodyText = _t("Use an Integration Manager to manage bots, widgets, and sticker packs."); } return (
        - {_t("Integration Manager")} + {_t("Integrations")} {managerName}
        {bodyText} +
        +
        + {_t( + "Integration Managers receive configuration data, and can modify widgets, " + + "send room invites, and set power levels on your behalf.", + )}
        ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7709a4a398..7f1a5ab851 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -598,15 +598,10 @@ "Do not use an identity server": "Do not use an identity server", "Enter a new identity server": "Enter a new identity server", "Change": "Change", - "Failed to update integration manager": "Failed to update integration manager", - "Integration manager offline or not accessible.": "Integration manager offline or not accessible.", - "Terms of service not accepted or the integration manager is invalid.": "Terms of service not accepted or the integration manager is invalid.", - "Integration manager has no terms of service": "Integration manager has no terms of service", - "The integration manager you have chosen does not have any terms of service.": "The integration manager you have chosen does not have any terms of service.", - "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.", - "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Add which integration manager you want to manage your bots, widgets, and sticker packs.", - "Integration Manager": "Integration Manager", - "Enter a new integration manager": "Enter a new integration manager", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.", + "Integrations": "Integrations", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.", "Flair": "Flair", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", "Success": "Success", From 3391cc0d9017065c777c681381b5dcc0c8f9e9fc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 20:05:32 -0700 Subject: [PATCH 0731/2372] Add the toggle switch for provisioning --- .../views/settings/_SetIntegrationManager.scss | 8 ++++++++ .../views/settings/SetIntegrationManager.js | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/res/css/views/settings/_SetIntegrationManager.scss b/res/css/views/settings/_SetIntegrationManager.scss index 454fb95cf7..3e59ac73ac 100644 --- a/res/css/views/settings/_SetIntegrationManager.scss +++ b/res/css/views/settings/_SetIntegrationManager.scss @@ -27,3 +27,11 @@ limitations under the License. display: inline-block; padding-left: 5px; } + +.mx_SetIntegrationManager .mx_ToggleSwitch { + display: inline-block; + float: right; + top: 9px; + + @mixin mx_Settings_fullWidthField; +} diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js index 11dadb4918..26c45e3d2a 100644 --- a/src/components/views/settings/SetIntegrationManager.js +++ b/src/components/views/settings/SetIntegrationManager.js @@ -17,6 +17,8 @@ limitations under the License. import React from 'react'; import {_t} from "../../../languageHandler"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; +import sdk from '../../../index'; +import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; export default class SetIntegrationManager extends React.Component { constructor() { @@ -26,10 +28,24 @@ export default class SetIntegrationManager extends React.Component { this.state = { currentManager, + provisioningEnabled: SettingsStore.getValue("integrationProvisioning"), }; } + onProvisioningToggled = () => { + const current = this.state.provisioningEnabled; + SettingsStore.setValue("integrationProvisioning", null, SettingLevel.ACCOUNT, !current).catch(err => { + console.error("Error changing integration manager provisioning"); + console.error(err); + + this.setState({provisioningEnabled: current}); + }); + this.setState({provisioningEnabled: !current}); + }; + render() { + const ToggleSwitch = sdk.getComponent("views.elements.ToggleSwitch"); + const currentManager = this.state.currentManager; let managerName; let bodyText; @@ -50,6 +66,7 @@ export default class SetIntegrationManager extends React.Component {
        {_t("Integrations")} {managerName} +
        {bodyText} From 81c9bdd9f33580cc10ced8ac18c7cc31e171f9d8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 20:14:20 -0700 Subject: [PATCH 0732/2372] It's called an "Integration Manager" (singular) Fixes https://github.com/vector-im/riot-web/issues/11256 This was finally annoying me enough to fix it. --- res/css/_components.scss | 2 +- res/css/views/dialogs/_TermsDialog.scss | 4 ++-- ...sManager.scss => _IntegrationManager.scss} | 10 ++++----- src/CallHandler.js | 2 +- .../dialogs/TabbedIntegrationManagerDialog.js | 8 +++---- src/components/views/dialogs/TermsDialog.js | 2 +- src/components/views/rooms/Stickerpicker.js | 6 ++--- ...ationsManager.js => IntegrationManager.js} | 22 +++++++++---------- src/i18n/strings/en_EN.json | 16 +++++++------- .../IntegrationManagerInstance.js | 14 ++++++------ src/integrations/IntegrationManagers.js | 7 +++--- 11 files changed, 46 insertions(+), 47 deletions(-) rename res/css/views/settings/{_IntegrationsManager.scss => _IntegrationManager.scss} (83%) rename src/components/views/settings/{IntegrationsManager.js => IntegrationManager.js} (81%) diff --git a/res/css/_components.scss b/res/css/_components.scss index 40a2c576d0..dc360c5caa 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -172,7 +172,7 @@ @import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/settings/_DevicesPanel.scss"; @import "./views/settings/_EmailAddresses.scss"; -@import "./views/settings/_IntegrationsManager.scss"; +@import "./views/settings/_IntegrationManager.scss"; @import "./views/settings/_KeyBackupPanel.scss"; @import "./views/settings/_Notifications.scss"; @import "./views/settings/_PhoneNumbers.scss"; diff --git a/res/css/views/dialogs/_TermsDialog.scss b/res/css/views/dialogs/_TermsDialog.scss index aad679a5b3..beb507e778 100644 --- a/res/css/views/dialogs/_TermsDialog.scss +++ b/res/css/views/dialogs/_TermsDialog.scss @@ -16,10 +16,10 @@ limitations under the License. /* * To avoid visual glitching of two modals stacking briefly, we customise the - * terms dialog sizing when it will appear for the integrations manager so that + * terms dialog sizing when it will appear for the integration manager so that * it gets the same basic size as the IM's own modal. */ -.mx_TermsDialog_forIntegrationsManager .mx_Dialog { +.mx_TermsDialog_forIntegrationManager .mx_Dialog { width: 60%; height: 70%; box-sizing: border-box; diff --git a/res/css/views/settings/_IntegrationsManager.scss b/res/css/views/settings/_IntegrationManager.scss similarity index 83% rename from res/css/views/settings/_IntegrationsManager.scss rename to res/css/views/settings/_IntegrationManager.scss index 8b51eb272e..81b01ab8de 100644 --- a/res/css/views/settings/_IntegrationsManager.scss +++ b/res/css/views/settings/_IntegrationManager.scss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_IntegrationsManager .mx_Dialog { +.mx_IntegrationManager .mx_Dialog { width: 60%; height: 70%; overflow: hidden; @@ -23,22 +23,22 @@ limitations under the License. max-height: initial; } -.mx_IntegrationsManager iframe { +.mx_IntegrationManager iframe { background-color: #fff; border: 0px; width: 100%; height: 100%; } -.mx_IntegrationsManager_loading h3 { +.mx_IntegrationManager_loading h3 { text-align: center; } -.mx_IntegrationsManager_error { +.mx_IntegrationManager_error { text-align: center; padding-top: 20px; } -.mx_IntegrationsManager_error h3 { +.mx_IntegrationManager_error h3 { color: $warning-color; } diff --git a/src/CallHandler.js b/src/CallHandler.js index bcdf7853fd..625ca8c551 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -382,7 +382,7 @@ function _onAction(payload) { } async function _startCallApp(roomId, type) { - // check for a working integrations manager. Technically we could put + // check for a working integration manager. Technically we could put // the state event in anyway, but the resulting widget would then not // work for us. Better that the user knows before everyone else in the // room sees it. diff --git a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js index 5ef7aef9ab..e86a46fb36 100644 --- a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js +++ b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js @@ -82,10 +82,10 @@ export default class TabbedIntegrationManagerDialog extends React.Component { client.setTermsInteractionCallback((policyInfo, agreedUrls) => { // To avoid visual glitching of two modals stacking briefly, we customise the - // terms dialog sizing when it will appear for the integrations manager so that + // terms dialog sizing when it will appear for the integration manager so that // it gets the same basic size as the IM's own modal. return dialogTermsInteractionCallback( - policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationsManager', + policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationManager', ); }); @@ -139,7 +139,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component { } _renderTab() { - const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager"); let uiUrl = null; if (this.state.currentScalarClient) { uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom( @@ -148,7 +148,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component { this.props.integrationId, ); } - return {_t("Identity Server")}
        ({host})
        ; case Matrix.SERVICE_TYPES.IM: - return
        {_t("Integrations Manager")}
        ({host})
        ; + return
        {_t("Integration Manager")}
        ({host})
        ; } } diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 28e51ed12e..47239cf33f 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -74,10 +74,10 @@ export default class Stickerpicker extends React.Component { this.forceUpdate(); return this.scalarClient; }).catch((e) => { - this._imError(_td("Failed to connect to integrations server"), e); + this._imError(_td("Failed to connect to integration manager"), e); }); } else { - this._imError(_td("No integrations server is configured to manage stickers with")); + this._imError(_td("No integration manager is configured to manage stickers with")); } } @@ -346,7 +346,7 @@ export default class Stickerpicker extends React.Component { } /** - * Launch the integrations manager on the stickers integration page + * Launch the integration manager on the stickers integration page */ _launchManageIntegrations() { // TODO: Open the right integration manager for the widget diff --git a/src/components/views/settings/IntegrationsManager.js b/src/components/views/settings/IntegrationManager.js similarity index 81% rename from src/components/views/settings/IntegrationsManager.js rename to src/components/views/settings/IntegrationManager.js index d463b043d5..97c469e9aa 100644 --- a/src/components/views/settings/IntegrationsManager.js +++ b/src/components/views/settings/IntegrationManager.js @@ -21,12 +21,12 @@ import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher'; -export default class IntegrationsManager extends React.Component { +export default class IntegrationManager extends React.Component { static propTypes = { - // false to display an error saying that there is no integrations manager configured + // false to display an error saying that there is no integration manager configured configured: PropTypes.bool.isRequired, - // false to display an error saying that we couldn't connect to the integrations manager + // false to display an error saying that we couldn't connect to the integration manager connected: PropTypes.bool.isRequired, // true to display a loading spinner @@ -72,9 +72,9 @@ export default class IntegrationsManager extends React.Component { render() { if (!this.props.configured) { return ( -
        -

        {_t("No integrations server configured")}

        -

        {_t("This Riot instance does not have an integrations server configured.")}

        +
        +

        {_t("No integration manager configured")}

        +

        {_t("This Riot instance does not have an integration manager configured.")}

        ); } @@ -82,8 +82,8 @@ export default class IntegrationsManager extends React.Component { if (this.props.loading) { const Spinner = sdk.getComponent("elements.Spinner"); return ( -
        -

        {_t("Connecting to integrations server...")}

        +
        +

        {_t("Connecting to integration manager...")}

        ); @@ -91,9 +91,9 @@ export default class IntegrationsManager extends React.Component { if (!this.props.connected) { return ( -
        -

        {_t("Cannot connect to integrations server")}

        -

        {_t("The integrations server is offline or it cannot reach your homeserver.")}

        +
        +

        {_t("Cannot connect to integration manager")}

        +

        {_t("The integration manager is offline or it cannot reach your homeserver.")}

        ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7f1a5ab851..0735a8e4b3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -507,11 +507,11 @@ "Failed to set display name": "Failed to set display name", "Disable Notifications": "Disable Notifications", "Enable Notifications": "Enable Notifications", - "No integrations server configured": "No integrations server configured", - "This Riot instance does not have an integrations server configured.": "This Riot instance does not have an integrations server configured.", - "Connecting to integrations server...": "Connecting to integrations server...", - "Cannot connect to integrations server": "Cannot connect to integrations server", - "The integrations server is offline or it cannot reach your homeserver.": "The integrations server is offline or it cannot reach your homeserver.", + "No integration manager configured": "No integration manager configured", + "This Riot instance does not have an integration manager configured.": "This Riot instance does not have an integration manager configured.", + "Connecting to integration manager...": "Connecting to integration manager...", + "Cannot connect to integration manager": "Cannot connect to integration manager", + "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", "Delete Backup": "Delete Backup", "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", @@ -1019,8 +1019,8 @@ "numbered-list": "numbered-list", "Show Text Formatting Toolbar": "Show Text Formatting Toolbar", "Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar", - "Failed to connect to integrations server": "Failed to connect to integrations server", - "No integrations server is configured to manage stickers with": "No integrations server is configured to manage stickers with", + "Failed to connect to integration manager": "Failed to connect to integration manager", + "No integration manager is configured to manage stickers with": "No integration manager is configured to manage stickers with", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", "Stickerpack": "Stickerpack", @@ -1470,7 +1470,7 @@ "Missing session data": "Missing session data", "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.", "Your browser likely removed this data when running low on disk space.": "Your browser likely removed this data when running low on disk space.", - "Integrations Manager": "Integrations Manager", + "Integration Manager": "Integration Manager", "Find others by phone or email": "Find others by phone or email", "Be found by phone or email": "Be found by phone or email", "Use bots, bridges, widgets and sticker packs": "Use bots, bridges, widgets and sticker packs", diff --git a/src/integrations/IntegrationManagerInstance.js b/src/integrations/IntegrationManagerInstance.js index d36fa73d48..2b616c9fed 100644 --- a/src/integrations/IntegrationManagerInstance.js +++ b/src/integrations/IntegrationManagerInstance.js @@ -57,19 +57,19 @@ export class IntegrationManagerInstance { } async open(room: Room = null, screen: string = null, integrationId: string = null): void { - const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager"); const dialog = Modal.createTrackedDialog( - 'Integration Manager', '', IntegrationsManager, - {loading: true}, 'mx_IntegrationsManager', + 'Integration Manager', '', IntegrationManager, + {loading: true}, 'mx_IntegrationManager', ); const client = this.getScalarClient(); client.setTermsInteractionCallback((policyInfo, agreedUrls) => { // To avoid visual glitching of two modals stacking briefly, we customise the - // terms dialog sizing when it will appear for the integrations manager so that + // terms dialog sizing when it will appear for the integration manager so that // it gets the same basic size as the IM's own modal. return dialogTermsInteractionCallback( - policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationsManager', + policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationManager', ); }); @@ -94,8 +94,8 @@ export class IntegrationManagerInstance { // Close the old dialog and open a new one dialog.close(); Modal.createTrackedDialog( - 'Integration Manager', '', IntegrationsManager, - newProps, 'mx_IntegrationsManager', + 'Integration Manager', '', IntegrationManager, + newProps, 'mx_IntegrationManager', ); } } diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index a0fbff56fb..96fd18b5b8 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -172,11 +172,10 @@ export class IntegrationManagers { } openNoManagerDialog(): void { - // TODO: Is it Integrations (plural) or Integration (singular). Singular is easier spoken. - const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager"); Modal.createTrackedDialog( - "Integration Manager", "None", IntegrationsManager, - {configured: false}, 'mx_IntegrationsManager', + "Integration Manager", "None", IntegrationManager, + {configured: false}, 'mx_IntegrationManager', ); } From 94fed922cfe3c61ca3fd6169efdb1c4e54405778 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 20:40:39 -0700 Subject: [PATCH 0733/2372] Intercept cases of disabled/no integration managers We already intercepted most of the cases where no integration manager was present, though there was a bug in many components where openAll() would be called regardless of an integration manager being available. The integration manager being disabled by the user is handled in the IntegrationManager classes rather than on click because we have quite a few calls to these functions. The StickerPicker is an exception because it does slightly different behaviour. This also removes the old "no integration manager configured" state from the IntegrationManager component as it is now replaced by a dialog. --- .../dialogs/IntegrationsDisabledDialog.js | 57 +++++++++++++++++++ .../dialogs/IntegrationsImpossibleDialog.js | 55 ++++++++++++++++++ src/components/views/rooms/Stickerpicker.js | 7 ++- .../views/settings/IntegrationManager.js | 13 ----- src/i18n/strings/en_EN.json | 7 ++- .../IntegrationManagerInstance.js | 6 ++ src/integrations/IntegrationManagers.js | 24 ++++++-- 7 files changed, 147 insertions(+), 22 deletions(-) create mode 100644 src/components/views/dialogs/IntegrationsDisabledDialog.js create mode 100644 src/components/views/dialogs/IntegrationsImpossibleDialog.js diff --git a/src/components/views/dialogs/IntegrationsDisabledDialog.js b/src/components/views/dialogs/IntegrationsDisabledDialog.js new file mode 100644 index 0000000000..3ab1123f8b --- /dev/null +++ b/src/components/views/dialogs/IntegrationsDisabledDialog.js @@ -0,0 +1,57 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {_t} from "../../../languageHandler"; +import sdk from "../../../index"; +import dis from '../../../dispatcher'; + +export default class IntegrationsDisabledDialog extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + }; + + _onAcknowledgeClick = () => { + this.props.onFinished(); + }; + + _onOpenSettingsClick = () => { + this.props.onFinished(); + dis.dispatch({action: "view_user_settings"}); + }; + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + return ( + +
        +

        {_t("Enable 'Manage Integrations' in Settings to do this.")}

        +
        + +
        + ); + } +} diff --git a/src/components/views/dialogs/IntegrationsImpossibleDialog.js b/src/components/views/dialogs/IntegrationsImpossibleDialog.js new file mode 100644 index 0000000000..9927f627f1 --- /dev/null +++ b/src/components/views/dialogs/IntegrationsImpossibleDialog.js @@ -0,0 +1,55 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {_t} from "../../../languageHandler"; +import sdk from "../../../index"; + +export default class IntegrationsImpossibleDialog extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + }; + + _onAcknowledgeClick = () => { + this.props.onFinished(); + }; + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + return ( + +
        +

        + {_t( + "Your Riot doesn't allow you to use an Integration Manager to do this. " + + "Please contact an admin.", + )} +

        +
        + +
        + ); + } +} diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 47239cf33f..d35285463a 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -77,7 +77,7 @@ export default class Stickerpicker extends React.Component { this._imError(_td("Failed to connect to integration manager"), e); }); } else { - this._imError(_td("No integration manager is configured to manage stickers with")); + IntegrationManagers.sharedInstance().openNoManagerDialog(); } } @@ -293,6 +293,11 @@ export default class Stickerpicker extends React.Component { * @param {Event} e Event that triggered the function */ _onShowStickersClick(e) { + if (!SettingsStore.getValue("integrationProvisioning")) { + // Intercept this case and spawn a warning. + return IntegrationManagers.sharedInstance().showDisabledDialog(); + } + // XXX: Simplify by using a context menu that is positioned relative to the sticker picker button const buttonRect = e.target.getBoundingClientRect(); diff --git a/src/components/views/settings/IntegrationManager.js b/src/components/views/settings/IntegrationManager.js index 97c469e9aa..1ab17ca8a0 100644 --- a/src/components/views/settings/IntegrationManager.js +++ b/src/components/views/settings/IntegrationManager.js @@ -23,9 +23,6 @@ import dis from '../../../dispatcher'; export default class IntegrationManager extends React.Component { static propTypes = { - // false to display an error saying that there is no integration manager configured - configured: PropTypes.bool.isRequired, - // false to display an error saying that we couldn't connect to the integration manager connected: PropTypes.bool.isRequired, @@ -40,7 +37,6 @@ export default class IntegrationManager extends React.Component { }; static defaultProps = { - configured: true, connected: true, loading: false, }; @@ -70,15 +66,6 @@ export default class IntegrationManager extends React.Component { }; render() { - if (!this.props.configured) { - return ( -
        -

        {_t("No integration manager configured")}

        -

        {_t("This Riot instance does not have an integration manager configured.")}

        -
        - ); - } - if (this.props.loading) { const Spinner = sdk.getComponent("elements.Spinner"); return ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0735a8e4b3..375124b4dc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -507,8 +507,6 @@ "Failed to set display name": "Failed to set display name", "Disable Notifications": "Disable Notifications", "Enable Notifications": "Enable Notifications", - "No integration manager configured": "No integration manager configured", - "This Riot instance does not have an integration manager configured.": "This Riot instance does not have an integration manager configured.", "Connecting to integration manager...": "Connecting to integration manager...", "Cannot connect to integration manager": "Cannot connect to integration manager", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", @@ -1020,7 +1018,6 @@ "Show Text Formatting Toolbar": "Show Text Formatting Toolbar", "Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar", "Failed to connect to integration manager": "Failed to connect to integration manager", - "No integration manager is configured to manage stickers with": "No integration manager is configured to manage stickers with", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", "Stickerpack": "Stickerpack", @@ -1393,6 +1390,10 @@ "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.", "Waiting for partner to confirm...": "Waiting for partner to confirm...", "Incoming Verification Request": "Incoming Verification Request", + "Integrations are disabled": "Integrations are disabled", + "Enable 'Manage Integrations' in Settings to do this.": "Enable 'Manage Integrations' in Settings to do this.", + "Integrations not allowed": "Integrations not allowed", + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.", "You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.", "Your unverified device '%(displayName)s' is requesting encryption keys.": "Your unverified device '%(displayName)s' is requesting encryption keys.", "Start verification": "Start verification", diff --git a/src/integrations/IntegrationManagerInstance.js b/src/integrations/IntegrationManagerInstance.js index 2b616c9fed..4958209351 100644 --- a/src/integrations/IntegrationManagerInstance.js +++ b/src/integrations/IntegrationManagerInstance.js @@ -20,6 +20,8 @@ import {dialogTermsInteractionCallback, TermsNotSignedError} from "../Terms"; import type {Room} from "matrix-js-sdk"; import Modal from '../Modal'; import url from 'url'; +import SettingsStore from "../settings/SettingsStore"; +import {IntegrationManagers} from "./IntegrationManagers"; export const KIND_ACCOUNT = "account"; export const KIND_CONFIG = "config"; @@ -57,6 +59,10 @@ export class IntegrationManagerInstance { } async open(room: Room = null, screen: string = null, integrationId: string = null): void { + if (!SettingsStore.getValue("integrationProvisioning")) { + return IntegrationManagers.sharedInstance().showDisabledDialog(); + } + const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager"); const dialog = Modal.createTrackedDialog( 'Integration Manager', '', IntegrationManager, diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 96fd18b5b8..60ceb49dc0 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -22,6 +22,10 @@ import type {MatrixClient, MatrixEvent, Room} from "matrix-js-sdk"; import WidgetUtils from "../utils/WidgetUtils"; import MatrixClientPeg from "../MatrixClientPeg"; import {AutoDiscovery} from "matrix-js-sdk"; +import {_t} from "../languageHandler"; +import dis from "../dispatcher"; +import React from 'react'; +import SettingsStore from "../settings/SettingsStore"; const HS_MANAGERS_REFRESH_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours const KIND_PREFERENCE = [ @@ -172,14 +176,19 @@ export class IntegrationManagers { } openNoManagerDialog(): void { - const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager"); - Modal.createTrackedDialog( - "Integration Manager", "None", IntegrationManager, - {configured: false}, 'mx_IntegrationManager', - ); + const IntegrationsImpossibleDialog = sdk.getComponent("dialogs.IntegrationsImpossibleDialog"); + Modal.createTrackedDialog('Integrations impossible', '', IntegrationsImpossibleDialog); } openAll(room: Room = null, screen: string = null, integrationId: string = null): void { + if (!SettingsStore.getValue("integrationProvisioning")) { + return this.showDisabledDialog(); + } + + if (this._managers.length === 0) { + return this.openNoManagerDialog(); + } + const TabbedIntegrationManagerDialog = sdk.getComponent("views.dialogs.TabbedIntegrationManagerDialog"); Modal.createTrackedDialog( 'Tabbed Integration Manager', '', TabbedIntegrationManagerDialog, @@ -187,6 +196,11 @@ export class IntegrationManagers { ); } + showDisabledDialog(): void { + const IntegrationsDisabledDialog = sdk.getComponent("dialogs.IntegrationsDisabledDialog"); + Modal.createTrackedDialog('Integrations disabled', '', IntegrationsDisabledDialog); + } + async overwriteManagerOnAccount(manager: IntegrationManagerInstance) { // TODO: TravisR - We should be logging out of scalar clients. await WidgetUtils.removeIntegrationManagerWidgets(); From 560c0afae3d0ea6cf9b7a0b2df508b339c9734d4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 20:45:16 -0700 Subject: [PATCH 0734/2372] Appease the linter --- src/components/views/rooms/Stickerpicker.js | 1 + src/integrations/IntegrationManagers.js | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index d35285463a..879b7c7582 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -291,6 +291,7 @@ export default class Stickerpicker extends React.Component { * Show the sticker picker overlay * If no stickerpacks have been added, show a link to the integration manager add sticker packs page. * @param {Event} e Event that triggered the function + * @returns Nothing of use when the thing happens. */ _onShowStickersClick(e) { if (!SettingsStore.getValue("integrationProvisioning")) { diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 60ceb49dc0..6c4d2ae4d4 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -22,9 +22,6 @@ import type {MatrixClient, MatrixEvent, Room} from "matrix-js-sdk"; import WidgetUtils from "../utils/WidgetUtils"; import MatrixClientPeg from "../MatrixClientPeg"; import {AutoDiscovery} from "matrix-js-sdk"; -import {_t} from "../languageHandler"; -import dis from "../dispatcher"; -import React from 'react'; import SettingsStore from "../settings/SettingsStore"; const HS_MANAGERS_REFRESH_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours From a69d818a0de2a22a7267dee89e34b17a88d29ad2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 20:49:41 -0700 Subject: [PATCH 0735/2372] Our linter is seriously picky. --- src/components/views/rooms/Stickerpicker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 879b7c7582..25001a2b80 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -291,7 +291,7 @@ export default class Stickerpicker extends React.Component { * Show the sticker picker overlay * If no stickerpacks have been added, show a link to the integration manager add sticker packs page. * @param {Event} e Event that triggered the function - * @returns Nothing of use when the thing happens. + * @return Nothing of use when the thing happens. */ _onShowStickersClick(e) { if (!SettingsStore.getValue("integrationProvisioning")) { From 670c14b2e3f00588c99916043b4bca4225aae54b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 20 Nov 2019 20:54:21 -0700 Subject: [PATCH 0736/2372] Circumvent the linter --- src/components/views/rooms/Stickerpicker.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 25001a2b80..7eabf27528 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -287,11 +287,10 @@ export default class Stickerpicker extends React.Component { return stickersContent; } - /** + // Dev note: this isn't jsdoc because it's angry. + /* * Show the sticker picker overlay * If no stickerpacks have been added, show a link to the integration manager add sticker packs page. - * @param {Event} e Event that triggered the function - * @return Nothing of use when the thing happens. */ _onShowStickersClick(e) { if (!SettingsStore.getValue("integrationProvisioning")) { From 8c02893da7616a2ceadd976e7e98a93b20f5be1f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 08:58:13 +0000 Subject: [PATCH 0737/2372] Update CIDER docs now that it is used for main composer as well --- docs/ciderEditor.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/ciderEditor.md b/docs/ciderEditor.md index e67c74a95c..00033b5b8c 100644 --- a/docs/ciderEditor.md +++ b/docs/ciderEditor.md @@ -2,8 +2,7 @@ The CIDER editor is a custom editor written for Riot. Most of the code can be found in the `/editor/` directory of the `matrix-react-sdk` project. -It is used to power the composer to edit messages, -and will soon be used as the main composer to send messages as well. +It is used to power the composer main composer (both to send and edit messages), and might be used for other usecases where autocomplete is desired (invite box, ...). ## High-level overview. From 5670a524693c3cc267065dd782967078a27a74d6 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 21 Nov 2019 02:52:36 +0000 Subject: [PATCH 0738/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1919 of 1919 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 1dfdc34f1a..2d6d3f55bc 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2344,5 +2344,7 @@ "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "使用這個小工具可能會與 %(widgetDomain)s 以及您的整合管理員分享資料 。", "Using this widget may share data with %(widgetDomain)s.": "使用這個小工具可能會與 %(widgetDomain)s 分享資料 。", "Widget added by": "小工具新增由", - "This widget may use cookies.": "這個小工具可能會使用 cookies。" + "This widget may use cookies.": "這個小工具可能會使用 cookies。", + "Enable local event indexing and E2EE search (requires restart)": "啟用本機事件索引與端到端加密搜尋(需要重新啟動)", + "Match system dark mode setting": "與系統深色模式設定相符" } From 670d44ecd8bfdf41026cfbed45ca5d7f120f5137 Mon Sep 17 00:00:00 2001 From: Tuomas Hietala Date: Thu, 21 Nov 2019 11:36:43 +0000 Subject: [PATCH 0739/2372] Translated using Weblate (Finnish) Currently translated at 96.1% (1845 of 1919 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fi/ --- src/i18n/strings/fi.json | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 80fbb9b138..81a8563e5b 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -2137,5 +2137,25 @@ "%(count)s unread messages including mentions.|one": "Yksi lukematon maininta.", "%(count)s unread messages.|one": "Yksi lukematon viesti.", "Unread messages.": "Lukemattomat viestit.", - "Message Actions": "Viestitoiminnot" + "Message Actions": "Viestitoiminnot", + "Custom (%(level)s)": "Mukautettu (%(level)s)", + "Match system dark mode setting": "Sovita järjestelmän tumman tilan asetukseen", + "None": "Ei mitään", + "Unsubscribe": "Lopeta tilaus", + "View rules": "Näytä säännöt", + "Subscribe": "Tilaa", + "Direct message": "Yksityisviesti", + "%(role)s in %(roomName)s": "%(role)s huoneessa %(roomName)s", + "Security": "Tietoturva", + "Any of the following data may be shared:": "Seuraavat tiedot saatetaan jakaa:", + "Your display name": "Näyttönimesi", + "Your avatar URL": "Kuvasi URL-osoite", + "Your user ID": "Käyttäjätunnuksesi", + "Your theme": "Teemasi", + "Riot URL": "Riotin URL-osoite", + "Room ID": "Huoneen tunnus", + "Widget ID": "Sovelman tunnus", + "Using this widget may share data with %(widgetDomain)s.": "Tämän sovelman käyttäminen voi jakaa tietoja verkkotunnukselle %(widgetDomain)s.", + "Widget added by": "Sovelman lisäsi", + "This widget may use cookies.": "Tämä sovelma saattaa käyttää evästeitä." } From ad941b96e09c863dd5c2a8add15c455ea9ce9218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Thu, 21 Nov 2019 11:17:02 +0000 Subject: [PATCH 0740/2372] Translated using Weblate (French) Currently translated at 100.0% (1919 of 1919 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 64272bb839..e58cb187e8 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2351,5 +2351,7 @@ "Using this widget may share data with %(widgetDomain)s.": "L’utilisation de ce widget pourrait partager des données avec %(widgetDomain)s.", "Widget added by": "Widget ajouté par", "This widget may use cookies.": "Ce widget pourrait utiliser des cookies.", - "Send verification requests in direct message, including a new verification UX in the member panel.": "Envoyer les demandes de vérification en message direct, en incluant une nouvelle expérience de vérification dans le tableau des membres." + "Send verification requests in direct message, including a new verification UX in the member panel.": "Envoyer les demandes de vérification en message direct, en incluant une nouvelle expérience de vérification dans le tableau des membres.", + "Enable local event indexing and E2EE search (requires restart)": "Activer l’indexation des événements locaux et la recherche des données chiffrées de bout en bout (nécessite un redémarrage)", + "Match system dark mode setting": "S’adapter aux paramètres de mode sombre du système" } From d786017d08deef229710db2c2cf7a65a0400efd3 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Wed, 20 Nov 2019 19:02:23 +0000 Subject: [PATCH 0741/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1919 of 1919 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 892f21dbb1..e48161d798 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2338,5 +2338,7 @@ "Using this widget may share data with %(widgetDomain)s.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg %(widgetDomain)s domain-nel.", "Widget added by": "A kisalkalmazást hozzáadta", "This widget may use cookies.": "Ez a kisalkalmazás sütiket használhat.", - "Send verification requests in direct message, including a new verification UX in the member panel.": "Ellenőrzés küldése közvetlen üzenetben, beleértve az új ellenőrzési felhasználói élményt a résztvevői panelen." + "Send verification requests in direct message, including a new verification UX in the member panel.": "Ellenőrzés küldése közvetlen üzenetben, beleértve az új ellenőrzési felhasználói élményt a résztvevői panelen.", + "Enable local event indexing and E2EE search (requires restart)": "Helyi esemény indexálás és végponttól végpontig titkosított események keresésének engedélyezése (újraindítás szükséges)", + "Match system dark mode setting": "Rendszer sötét témájához alkalmazkodás" } From 87167f42f89cd8c2f3e87f8d18ad95d717ad678e Mon Sep 17 00:00:00 2001 From: random Date: Thu, 21 Nov 2019 09:34:39 +0000 Subject: [PATCH 0742/2372] Translated using Weblate (Italian) Currently translated at 99.9% (1917 of 1919 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index efab4595f6..9faa48328c 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2295,5 +2295,8 @@ "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Usando questo widget i dati possono essere condivisi con %(widgetDomain)s e il tuo Gestore di Integrazione.", "Using this widget may share data with %(widgetDomain)s.": "Usando questo widget i dati possono essere condivisi con %(widgetDomain)s.", "Widget added by": "Widget aggiunto da", - "This widget may use cookies.": "Questo widget può usare cookie." + "This widget may use cookies.": "Questo widget può usare cookie.", + "Send verification requests in direct message, including a new verification UX in the member panel.": "Invia le richieste di verifica via messaggio diretto, inclusa una nuova esperienza utente per la verifica nel pannello membri.", + "Enable local event indexing and E2EE search (requires restart)": "Attiva l'indicizzazione di eventi locali e la ricerca E2EE (richiede riavvio)", + "Match system dark mode setting": "Combacia la modalità scura di sistema" } From 598901b48339e5547b1396db16e81d8b2ab5e932 Mon Sep 17 00:00:00 2001 From: Edgars Voroboks Date: Wed, 20 Nov 2019 18:58:42 +0000 Subject: [PATCH 0743/2372] Translated using Weblate (Latvian) Currently translated at 47.8% (918 of 1919 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/lv/ --- src/i18n/strings/lv.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/lv.json b/src/i18n/strings/lv.json index 80e173dc3f..1c7d2b0311 100644 --- a/src/i18n/strings/lv.json +++ b/src/i18n/strings/lv.json @@ -1144,5 +1144,8 @@ "You do not have permission to start a conference call in this room": "Šajā istabā nav atļaujas sākt konferences zvanu", "Replying With Files": "Atbildot ar failiem", "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Šobrīd nav iespējams atbildēt ar failu. Vai vēlaties augšupielādēt šo failu, neatbildot?", - "Your Riot is misconfigured": "Jūsu Riot ir nepareizi konfigurēts" + "Your Riot is misconfigured": "Jūsu Riot ir nepareizi konfigurēts", + "Add Email Address": "Pievienot e-pasta adresi", + "Add Phone Number": "Pievienot tālruņa numuru", + "Call failed due to misconfigured server": "Zvans neizdevās nekorekti nokonfigurēta servera dēļ" } From 5c6ef10c6b6c1f063e9eeee653cc8419ff172200 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Nov 2019 15:55:30 +0000 Subject: [PATCH 0744/2372] Ignore media actions Hopefully the comment explains all Fixes https://github.com/vector-im/riot-web/issues/11118 --- src/CallHandler.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/CallHandler.js b/src/CallHandler.js index 625ca8c551..4ffc9fb7a2 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -495,6 +495,15 @@ async function _startCallApp(roomId, type) { // with the dispatcher once if (!global.mxCallHandler) { dis.register(_onAction); + // add empty handlers for media actions, otherwise the media keys + // end up causing the audio elements with our ring/ringback etc + // audio clips in to play. + navigator.mediaSession.setActionHandler('play', function() {}); + navigator.mediaSession.setActionHandler('pause', function() {}); + navigator.mediaSession.setActionHandler('seekbackward', function() {}); + navigator.mediaSession.setActionHandler('seekforward', function() {}); + navigator.mediaSession.setActionHandler('previoustrack', function() {}); + navigator.mediaSession.setActionHandler('nexttrack', function() {}); } const callHandler = { From f7f22444e8f0581ddb1e6520dcfd596a29e1b139 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 21 Nov 2019 09:03:07 -0700 Subject: [PATCH 0745/2372] Rename section heading for integrations in settings Misc design update --- src/components/views/settings/SetIntegrationManager.js | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js index 26c45e3d2a..e205f02e6c 100644 --- a/src/components/views/settings/SetIntegrationManager.js +++ b/src/components/views/settings/SetIntegrationManager.js @@ -64,7 +64,7 @@ export default class SetIntegrationManager extends React.Component { return (
        - {_t("Integrations")} + {_t("Manage integrations")} {managerName}
        diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a6173e15b7..618c9ad63a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -598,7 +598,7 @@ "Change": "Change", "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.", "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.", - "Integrations": "Integrations", + "Manage integrations": "Manage integrations", "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.", "Flair": "Flair", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", From f0fbb20ee50b8d822175207136d8ffa3f1ee107c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Nov 2019 16:11:42 +0000 Subject: [PATCH 0746/2372] Detect support for mediaSession Firefox doesn't support mediaSession so don't try setting handlers --- src/CallHandler.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 4ffc9fb7a2..9350fe4dd9 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -498,12 +498,14 @@ if (!global.mxCallHandler) { // add empty handlers for media actions, otherwise the media keys // end up causing the audio elements with our ring/ringback etc // audio clips in to play. - navigator.mediaSession.setActionHandler('play', function() {}); - navigator.mediaSession.setActionHandler('pause', function() {}); - navigator.mediaSession.setActionHandler('seekbackward', function() {}); - navigator.mediaSession.setActionHandler('seekforward', function() {}); - navigator.mediaSession.setActionHandler('previoustrack', function() {}); - navigator.mediaSession.setActionHandler('nexttrack', function() {}); + if (navigator.mediaSession) { + navigator.mediaSession.setActionHandler('play', function() {}); + navigator.mediaSession.setActionHandler('pause', function() {}); + navigator.mediaSession.setActionHandler('seekbackward', function() {}); + navigator.mediaSession.setActionHandler('seekforward', function() {}); + navigator.mediaSession.setActionHandler('previoustrack', function() {}); + navigator.mediaSession.setActionHandler('nexttrack', function() {}); + } } const callHandler = { From a55e5f77598f4ca7c433240b50229329f6a45e9c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 21 Nov 2019 09:12:07 -0700 Subject: [PATCH 0747/2372] Update copy for widgets not using message encryption Misc design update --- src/components/views/elements/AppPermission.js | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js index c514dbc950..8dc58643bd 100644 --- a/src/components/views/elements/AppPermission.js +++ b/src/components/views/elements/AppPermission.js @@ -115,7 +115,7 @@ export default class AppPermission extends React.Component { : _t("Using this widget may share data with %(widgetDomain)s.", {widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip}); - const encryptionWarning = this.props.isRoomEncrypted ? _t("Widgets are not encrypted.") : null; + const encryptionWarning = this.props.isRoomEncrypted ? _t("Widgets do not use message encryption.") : null; return (
        diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a6173e15b7..56ae95d568 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1187,7 +1187,7 @@ "Widget ID": "Widget ID", "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Using this widget may share data with %(widgetDomain)s & your Integration Manager.", "Using this widget may share data with %(widgetDomain)s.": "Using this widget may share data with %(widgetDomain)s.", - "Widgets are not encrypted.": "Widgets are not encrypted.", + "Widgets do not use message encryption.": "Widgets do not use message encryption.", "Widget added by": "Widget added by", "This widget may use cookies.": "This widget may use cookies.", "Delete Widget": "Delete Widget", From 08e08376a204e34926a525b34454427589878b25 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Thu, 21 Nov 2019 15:19:59 +0000 Subject: [PATCH 0748/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1922 of 1922 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index e48161d798..70a966ce3d 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2340,5 +2340,11 @@ "This widget may use cookies.": "Ez a kisalkalmazás sütiket használhat.", "Send verification requests in direct message, including a new verification UX in the member panel.": "Ellenőrzés küldése közvetlen üzenetben, beleértve az új ellenőrzési felhasználói élményt a résztvevői panelen.", "Enable local event indexing and E2EE search (requires restart)": "Helyi esemény indexálás és végponttól végpontig titkosított események keresésének engedélyezése (újraindítás szükséges)", - "Match system dark mode setting": "Rendszer sötét témájához alkalmazkodás" + "Match system dark mode setting": "Rendszer sötét témájához alkalmazkodás", + "Widgets are not encrypted.": "A kisalkalmazások nem titkosítottak.", + "More options": "További beállítások", + "Reload": "Újratölt", + "Take picture": "Fénykép készítés", + "Remove for everyone": "Visszavonás mindenkitől", + "Remove for me": "Visszavonás magamtól" } From 5d8476185f83e73ff06a59e89a71a4f53bf0a419 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Nov 2019 17:00:35 +0000 Subject: [PATCH 0749/2372] Catch exceptions when we can't play audio ...or try to: the chrome debugger still breakpoints, even when we catch the exception. Related to, but probably does not fix https://github.com/vector-im/riot-web/issues/7657 --- src/CallHandler.js | 17 +++++++++++++++-- src/Notifier.js | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 625ca8c551..c15fda1ef9 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -80,13 +80,26 @@ function play(audioId) { // which listens? const audio = document.getElementById(audioId); if (audio) { + const playAudio = async () => { + try { + // This still causes the chrome debugger to break on promise rejection if + // the promise is rejected, even though we're catching the exception. + await audio.play(); + } catch (e) { + // This is usually because the user hasn't interacted with the document, + // or chrome doesn't think so and is denying the request. Not sure what + // we can really do here... + // https://github.com/vector-im/riot-web/issues/7657 + console.log("Unable to play audio clip", e); + } + }; if (audioPromises[audioId]) { audioPromises[audioId] = audioPromises[audioId].then(()=>{ audio.load(); - return audio.play(); + return playAudio(); }); } else { - audioPromises[audioId] = audio.play(); + audioPromises[audioId] = playAudio(); } } } diff --git a/src/Notifier.js b/src/Notifier.js index edb9850dfe..dd691d8ca7 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -146,7 +146,7 @@ const Notifier = { } document.body.appendChild(audioElement); } - audioElement.play(); + await audioElement.play(); } catch (ex) { console.warn("Caught error when trying to fetch room notification sound:", ex); } From 3cddcad5de18f926dcd2b44e0b4dfe36968018d3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:12:32 +0100 Subject: [PATCH 0750/2372] use correct icons with borders --- res/img/e2e/verified.svg | 14 +++----------- res/img/e2e/warning.svg | 15 ++++----------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/res/img/e2e/verified.svg b/res/img/e2e/verified.svg index af6bb92297..464b443dcf 100644 --- a/res/img/e2e/verified.svg +++ b/res/img/e2e/verified.svg @@ -1,12 +1,4 @@ - - - + + + diff --git a/res/img/e2e/warning.svg b/res/img/e2e/warning.svg index 2501da6ab3..209ae0f71f 100644 --- a/res/img/e2e/warning.svg +++ b/res/img/e2e/warning.svg @@ -1,12 +1,5 @@ - - - + + + + From cb0e6ca5d280d16b951e843748936f337f05d802 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:13:01 +0100 Subject: [PATCH 0751/2372] dont make e2e icons themable, as they have multiple colors --- res/css/views/rooms/_E2EIcon.scss | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index 1ee5008888..cb99aa63f1 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -15,8 +15,8 @@ limitations under the License. */ .mx_E2EIcon { - width: 25px; - height: 25px; + width: 16px; + height: 16px; margin: 0 9px; position: relative; display: block; @@ -30,16 +30,14 @@ limitations under the License. bottom: 0; left: 0; right: 0; - mask-repeat: no-repeat; - mask-size: contain; + background-repeat: no-repeat; + background-size: contain; } .mx_E2EIcon_verified::after { - mask-image: url('$(res)/img/e2e/verified.svg'); - background-color: $accent-color; + background-image: url('$(res)/img/e2e/verified.svg'); } .mx_E2EIcon_warning::after { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $warning-color; + background-image: url('$(res)/img/e2e/warning.svg'); } From 854f27df3fbd0b8d65790a06c33d0ed301756a7f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:14:25 +0100 Subject: [PATCH 0752/2372] remove obsolete style from message composer for e2e icon as it's now the default size for the e2e iconn --- res/css/views/rooms/_MessageComposer.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 036756e2eb..12e45a07c9 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -74,8 +74,6 @@ limitations under the License. .mx_MessageComposer_e2eIcon.mx_E2EIcon { position: absolute; left: 60px; - width: 16px; - height: 16px; margin-right: 0; // Counteract the E2EIcon class margin-left: 3px; // Counteract the E2EIcon class } From f75e45a715db4314ffa23684412d24fbd86f02a4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:17:55 +0100 Subject: [PATCH 0753/2372] reduce margin on e2e icon in room header --- res/css/views/rooms/_RoomHeader.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 5da8ff76b9..f1e4456cc1 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -17,6 +17,10 @@ limitations under the License. .mx_RoomHeader { flex: 0 0 52px; border-bottom: 1px solid $primary-hairline-color; + + .mx_E2EIcon { + margin: 0 5px; + } } .mx_RoomHeader_wrapper { From 812b0785bf704bb65a6a6b19819860d42a928132 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 21 Nov 2019 10:22:31 -0700 Subject: [PATCH 0754/2372] Fix i18n post-merge --- src/i18n/strings/en_EN.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9feded09b6..367450656e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -507,15 +507,10 @@ "Failed to set display name": "Failed to set display name", "Disable Notifications": "Disable Notifications", "Enable Notifications": "Enable Notifications", - "No integrations server configured": "No integrations server configured", - "This Riot instance does not have an integrations server configured.": "This Riot instance does not have an integrations server configured.", - "Connecting to integrations server...": "Connecting to integrations server...", - "Cannot connect to integrations server": "Cannot connect to integrations server", - "The integrations server is offline or it cannot reach your homeserver.": "The integrations server is offline or it cannot reach your homeserver.", - "Send cross-signing keys to homeserver": "Send cross-signing keys to homeserver", "Connecting to integration manager...": "Connecting to integration manager...", "Cannot connect to integration manager": "Cannot connect to integration manager", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", + "Send cross-signing keys to homeserver": "Send cross-signing keys to homeserver", "Delete Backup": "Delete Backup", "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", From b239fde32dec454b29f9b11611e02862a79cd979 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Nov 2019 17:31:57 +0000 Subject: [PATCH 0755/2372] Workaround for soft-crash with calls on startup Fixes https://github.com/vector-im/riot-web/issues/11458 --- src/components/views/voip/CallView.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/views/voip/CallView.js b/src/components/views/voip/CallView.js index a4d7927ac3..cf1f505197 100644 --- a/src/components/views/voip/CallView.js +++ b/src/components/views/voip/CallView.js @@ -90,6 +90,13 @@ module.exports = createReactClass({ } } else { call = CallHandler.getAnyActiveCall(); + // Ignore calls if we can't get the room associated with them. + // I think the underlying problem is that the js-sdk sends events + // for calls before it has made the rooms available in the store, + // although this isn't confirmed. + if (MatrixClientPeg.get().getRoom(call.roomId) === null) { + call = null; + } this.setState({ call: call }); } From a02a285058fdc38bebcc198aaefe06d1f28cd586 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 22 Nov 2019 10:24:51 +0000 Subject: [PATCH 0756/2372] Show m.room.create event before the ELS on room upgrade Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MessagePanel.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index cf2a5b1738..39504666bf 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -411,6 +411,11 @@ module.exports = createReactClass({ readMarkerInSummary = true; } + // If this m.room.create event should be shown (room upgrade) then show it before the summary + if (this._shouldShowEvent(mxEv)) { + ret.push(...this._getTilesForEvent(prevEvent, mxEv, false)); + } + const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary for (;i + 1 < this.props.events.length; i++) { const collapsedMxEv = this.props.events[i + 1]; From 6d4abeef4515bab769d04359a2ded0ca70219d57 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Nov 2019 12:07:25 +0000 Subject: [PATCH 0757/2372] Convert MessagePanel to React class I was about to add the getDerivedStateFromProps function to change how read markers worked, but doing that in an old style class means the statics object, so let;s just convert the thing. --- src/components/structures/MessagePanel.js | 132 +++++++++++----------- 1 file changed, 65 insertions(+), 67 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index cf2a5b1738..3781dd0ce7 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -1,6 +1,7 @@ /* Copyright 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,7 +19,6 @@ limitations under the License. /* global Velocity */ import React from 'react'; -import createReactClass from 'create-react-class'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -37,10 +37,8 @@ const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() /* (almost) stateless UI component which builds the event tiles in the room timeline. */ -module.exports = createReactClass({ - displayName: 'MessagePanel', - - propTypes: { +export default class MessagePanel extends React.Component { + propTypes = { // true to give the component a 'display: none' style. hidden: PropTypes.bool, @@ -109,9 +107,9 @@ module.exports = createReactClass({ // whether to show reactions for an event showReactions: PropTypes.bool, - }, + }; - componentWillMount: function() { + componentDidMount() { // the event after which we put a visible unread marker on the last // render cycle; null if readMarkerVisible was false or the RM was // suppressed (eg because it was at the end of the timeline) @@ -168,37 +166,37 @@ module.exports = createReactClass({ SettingsStore.getValue("showHiddenEventsInTimeline"); this._isMounted = true; - }, + } - componentWillUnmount: function() { + componentWillUnmount() { this._isMounted = false; - }, + } /* get the DOM node representing the given event */ - getNodeForEventId: function(eventId) { + getNodeForEventId(eventId) { if (!this.eventNodes) { return undefined; } return this.eventNodes[eventId]; - }, + } /* return true if the content is fully scrolled down right now; else false. */ - isAtBottom: function() { + isAtBottom() { return this.refs.scrollPanel && this.refs.scrollPanel.isAtBottom(); - }, + } /* get the current scroll state. See ScrollPanel.getScrollState for * details. * * returns null if we are not mounted. */ - getScrollState: function() { + getScrollState() { if (!this.refs.scrollPanel) { return null; } return this.refs.scrollPanel.getScrollState(); - }, + } // returns one of: // @@ -206,7 +204,7 @@ module.exports = createReactClass({ // -1: read marker is above the window // 0: read marker is within the window // +1: read marker is below the window - getReadMarkerPosition: function() { + getReadMarkerPosition() { const readMarker = this.refs.readMarkerNode; const messageWrapper = this.refs.scrollPanel; @@ -226,45 +224,45 @@ module.exports = createReactClass({ } else { return 1; } - }, + } /* jump to the top of the content. */ - scrollToTop: function() { + scrollToTop() { if (this.refs.scrollPanel) { this.refs.scrollPanel.scrollToTop(); } - }, + } /* jump to the bottom of the content. */ - scrollToBottom: function() { + scrollToBottom() { if (this.refs.scrollPanel) { this.refs.scrollPanel.scrollToBottom(); } - }, + } /** * Page up/down. * * @param {number} mult: -1 to page up, +1 to page down */ - scrollRelative: function(mult) { + scrollRelative(mult) { if (this.refs.scrollPanel) { this.refs.scrollPanel.scrollRelative(mult); } - }, + } /** * Scroll up/down in response to a scroll key * * @param {KeyboardEvent} ev: the keyboard event to handle */ - handleScrollKey: function(ev) { + handleScrollKey(ev) { if (this.refs.scrollPanel) { this.refs.scrollPanel.handleScrollKey(ev); } - }, + } /* jump to the given event id. * @@ -276,33 +274,33 @@ module.exports = createReactClass({ * node (specifically, the bottom of it) will be positioned. If omitted, it * defaults to 0. */ - scrollToEvent: function(eventId, pixelOffset, offsetBase) { + scrollToEvent(eventId, pixelOffset, offsetBase) { if (this.refs.scrollPanel) { this.refs.scrollPanel.scrollToToken(eventId, pixelOffset, offsetBase); } - }, + } - scrollToEventIfNeeded: function(eventId) { + scrollToEventIfNeeded(eventId) { const node = this.eventNodes[eventId]; if (node) { node.scrollIntoView({block: "nearest", behavior: "instant"}); } - }, + } /* check the scroll state and send out pagination requests if necessary. */ - checkFillState: function() { + checkFillState() { if (this.refs.scrollPanel) { this.refs.scrollPanel.checkFillState(); } - }, + } - _isUnmounting: function() { + _isUnmounting() { return !this._isMounted; - }, + } // TODO: Implement granular (per-room) hide options - _shouldShowEvent: function(mxEv) { + _shouldShowEvent(mxEv) { if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) { return false; // ignored = no show (only happens if the ignore happens after an event was received) } @@ -320,9 +318,9 @@ module.exports = createReactClass({ if (this.props.highlightedEventId === mxEv.getId()) return true; return !shouldHideEvent(mxEv); - }, + } - _getEventTiles: function() { + _getEventTiles() { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); @@ -596,9 +594,9 @@ module.exports = createReactClass({ this.currentReadMarkerEventId = readMarkerVisible ? this.props.readMarkerEventId : null; return ret; - }, + } - _getTilesForEvent: function(prevEvent, mxEv, last) { + _getTilesForEvent(prevEvent, mxEv, last) { const EventTile = sdk.getComponent('rooms.EventTile'); const DateSeparator = sdk.getComponent('messages.DateSeparator'); const ret = []; @@ -691,20 +689,20 @@ module.exports = createReactClass({ ); return ret; - }, + } - _wantsDateSeparator: function(prevEvent, nextEventDate) { + _wantsDateSeparator(prevEvent, nextEventDate) { if (prevEvent == null) { // first event in the panel: depends if we could back-paginate from // here. return !this.props.suppressFirstDateSeparator; } return wantsDateSeparator(prevEvent.getDate(), nextEventDate); - }, + } // Get a list of read receipts that should be shown next to this event // Receipts are objects which have a 'userId', 'roomMember' and 'ts'. - _getReadReceiptsForEvent: function(event) { + _getReadReceiptsForEvent(event) { const myUserId = MatrixClientPeg.get().credentials.userId; // get list of read receipts, sorted most recent first @@ -728,12 +726,12 @@ module.exports = createReactClass({ }); }); return receipts; - }, + } // Get an object that maps from event ID to a list of read receipts that // should be shown next to that event. If a hidden event has read receipts, // they are folded into the receipts of the last shown event. - _getReadReceiptsByShownEvent: function() { + _getReadReceiptsByShownEvent() { const receiptsByEvent = {}; const receiptsByUserId = {}; @@ -786,9 +784,9 @@ module.exports = createReactClass({ } return receiptsByEvent; - }, + } - _getReadMarkerTile: function(visible) { + _getReadMarkerTile(visible) { let hr; if (visible) { hr =
        ); - }, + } - _startAnimation: function(ghostNode) { + _startAnimation = (ghostNode) => { if (this._readMarkerGhostNode) { Velocity.Utilities.removeData(this._readMarkerGhostNode); } @@ -816,9 +814,9 @@ module.exports = createReactClass({ {duration: 400, easing: 'easeInSine', delay: 1000}); } - }, + }; - _getReadMarkerGhostTile: function() { + _getReadMarkerGhostTile() { const hr =
        ); - }, + } - _collectEventNode: function(eventId, node) { + _collectEventNode = (eventId, node) => { this.eventNodes[eventId] = node; - }, + } // once dynamic content in the events load, make the scrollPanel check the // scroll offsets. - _onHeightChanged: function() { + _onHeightChanged = () => { const scrollPanel = this.refs.scrollPanel; if (scrollPanel) { scrollPanel.checkScroll(); } - }, + }; - _onTypingShown: function() { + _onTypingShown = () => { const scrollPanel = this.refs.scrollPanel; // this will make the timeline grow, so checkScroll scrollPanel.checkScroll(); if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) { scrollPanel.preventShrinking(); } - }, + }; - _onTypingHidden: function() { + _onTypingHidden = () => { const scrollPanel = this.refs.scrollPanel; if (scrollPanel) { // as hiding the typing notifications doesn't @@ -868,9 +866,9 @@ module.exports = createReactClass({ // reveal added padding to balance the notifs disappearing. scrollPanel.checkScroll(); } - }, + }; - updateTimelineMinHeight: function() { + updateTimelineMinHeight() { const scrollPanel = this.refs.scrollPanel; if (scrollPanel) { @@ -885,16 +883,16 @@ module.exports = createReactClass({ scrollPanel.preventShrinking(); } } - }, + } - onTimelineReset: function() { + onTimelineReset() { const scrollPanel = this.refs.scrollPanel; if (scrollPanel) { scrollPanel.clearPreventShrinking(); } - }, + } - render: function() { + render() { const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile"); const Spinner = sdk.getComponent("elements.Spinner"); @@ -941,5 +939,5 @@ module.exports = createReactClass({ { bottomSpinner } ); - }, -}); + } +} From fd5a5e13ee0c485716e7d758e2da63c9b434ee12 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Nov 2019 12:59:51 +0000 Subject: [PATCH 0758/2372] Make addEventListener conditional Safari doesn't support addEventListener --- src/theme.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/theme.js b/src/theme.js index 92bf03ef0a..9208ff2045 100644 --- a/src/theme.js +++ b/src/theme.js @@ -41,14 +41,18 @@ export class ThemeWatcher { start() { this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onChange); this._systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this._onChange); - this._preferDark.addEventListener('change', this._onChange); - this._preferLight.addEventListener('change', this._onChange); + if (this._preferDark.addEventListener) { + this._preferDark.addEventListener('change', this._onChange); + this._preferLight.addEventListener('change', this._onChange); + } this._dispatcherRef = dis.register(this._onAction); } stop() { - this._preferDark.removeEventListener('change', this._onChange); - this._preferLight.removeEventListener('change', this._onChange); + if (this._preferDark.addEventListener) { + this._preferDark.removeEventListener('change', this._onChange); + this._preferLight.removeEventListener('change', this._onChange); + } SettingsStore.unwatchSetting(this._systemThemeWatchRef); SettingsStore.unwatchSetting(this._themeWatchRef); dis.unregister(this._dispatcherRef); From 3f5a8faf376b33499258f071b1eefe69e8c2c5ee Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Nov 2019 13:01:56 +0000 Subject: [PATCH 0759/2372] PropTypes should be static --- res/css/structures/_RoomView.scss | 1 + src/components/structures/MessagePanel.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index 50d412ad58..8e47fe7509 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -221,6 +221,7 @@ hr.mx_RoomView_myReadMarker { position: relative; top: -1px; z-index: 1; + transition: width 1s easeInSine; } .mx_RoomView_callStatusBar .mx_UploadBar_uploadProgressInner { diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 3781dd0ce7..bd5630ab12 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -38,7 +38,7 @@ const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() /* (almost) stateless UI component which builds the event tiles in the room timeline. */ export default class MessagePanel extends React.Component { - propTypes = { + static propTypes = { // true to give the component a 'display: none' style. hidden: PropTypes.bool, From 0dbb639aea1ef008bcdede36899112f3b7bca1fa Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Nov 2019 13:06:35 +0000 Subject: [PATCH 0760/2372] Accidentally committed --- res/css/structures/_RoomView.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index 8e47fe7509..50d412ad58 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -221,7 +221,6 @@ hr.mx_RoomView_myReadMarker { position: relative; top: -1px; z-index: 1; - transition: width 1s easeInSine; } .mx_RoomView_callStatusBar .mx_UploadBar_uploadProgressInner { From 936c36dd586ebaacfe203f00759f7d01f1abe0dd Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Fri, 22 Nov 2019 03:23:27 +0000 Subject: [PATCH 0761/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1918 of 1918 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 2d6d3f55bc..2fd014554e 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2346,5 +2346,22 @@ "Widget added by": "小工具新增由", "This widget may use cookies.": "這個小工具可能會使用 cookies。", "Enable local event indexing and E2EE search (requires restart)": "啟用本機事件索引與端到端加密搜尋(需要重新啟動)", - "Match system dark mode setting": "與系統深色模式設定相符" + "Match system dark mode setting": "與系統深色模式設定相符", + "Connecting to integration manager...": "正在連線到整合管理員……", + "Cannot connect to integration manager": "無法連線到整合管理員", + "The integration manager is offline or it cannot reach your homeserver.": "整合管理員已離線或無法存取您的家伺服器。", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "使用整合管理員 (%(serverName)s) 以管理機器人、小工具與貼紙包。", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "使用整合管理員以管理機器人、小工具與貼紙包。", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "整合管理員接收設定資料,並可以修改小工具、傳送聊天室邀請並設定權限等級。", + "Failed to connect to integration manager": "連線到整合管理員失敗", + "Widgets do not use message encryption.": "小工具不使用訊息加密。", + "More options": "更多選項", + "Integrations are disabled": "整合已停用", + "Enable 'Manage Integrations' in Settings to do this.": "在設定中啟用「管理整合」以執行此動作。", + "Integrations not allowed": "不允許整合", + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "您的 Riot 不允許您使用整合管理員來執行此動作。請聯絡管理員。", + "Reload": "重新載入", + "Take picture": "拍照", + "Remove for everyone": "對所有人移除", + "Remove for me": "對我移除" } From 27c64db613281d6d44c35fca40402c53df8e39c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Fri, 22 Nov 2019 08:35:43 +0000 Subject: [PATCH 0762/2372] Translated using Weblate (French) Currently translated at 100.0% (1918 of 1918 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index e58cb187e8..0c13b3c722 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2353,5 +2353,22 @@ "This widget may use cookies.": "Ce widget pourrait utiliser des cookies.", "Send verification requests in direct message, including a new verification UX in the member panel.": "Envoyer les demandes de vérification en message direct, en incluant une nouvelle expérience de vérification dans le tableau des membres.", "Enable local event indexing and E2EE search (requires restart)": "Activer l’indexation des événements locaux et la recherche des données chiffrées de bout en bout (nécessite un redémarrage)", - "Match system dark mode setting": "S’adapter aux paramètres de mode sombre du système" + "Match system dark mode setting": "S’adapter aux paramètres de mode sombre du système", + "Connecting to integration manager...": "Connexion au gestionnaire d’intégrations…", + "Cannot connect to integration manager": "Impossible de se connecter au gestionnaire d’intégrations", + "The integration manager is offline or it cannot reach your homeserver.": "Le gestionnaire d’intégrations est hors ligne ou il ne peut pas joindre votre serveur d’accueil.", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Utilisez un gestionnaire d’intégrations (%(serverName)s) pour gérer les bots, les widgets et les packs de stickers.", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Utilisez un gestionnaire d’intégrations pour gérer les bots, les widgets et les packs de stickers.", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Les gestionnaires d’intégrations reçoivent les données de configuration et peuvent modifier les widgets, envoyer des invitations aux salons et définir les rangs à votre place.", + "Failed to connect to integration manager": "Échec de la connexion au gestionnaire d’intégrations", + "Widgets do not use message encryption.": "Les widgets n’utilisent pas le chiffrement des messages.", + "More options": "Plus d’options", + "Integrations are disabled": "Les intégrations sont désactivées", + "Enable 'Manage Integrations' in Settings to do this.": "Activez « Gérer les intégrations » dans les paramètres pour faire ça.", + "Integrations not allowed": "Les intégrations ne sont pas autorisées", + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Votre Riot ne vous autorise pas à utiliser un gestionnaire d’intégrations pour faire ça. Contactez un administrateur.", + "Reload": "Recharger", + "Take picture": "Prendre une photo", + "Remove for everyone": "Supprimer pour tout le monde", + "Remove for me": "Supprimer pour moi" } From fed1ed3b500d5e98bc04ab9ab52dd9f63949db06 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Thu, 21 Nov 2019 20:00:59 +0000 Subject: [PATCH 0763/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1918 of 1918 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 70a966ce3d..7ed4eb253c 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2346,5 +2346,17 @@ "Reload": "Újratölt", "Take picture": "Fénykép készítés", "Remove for everyone": "Visszavonás mindenkitől", - "Remove for me": "Visszavonás magamtól" + "Remove for me": "Visszavonás magamtól", + "Connecting to integration manager...": "Kapcsolódás az integrációs menedzserhez...", + "Cannot connect to integration manager": "A kapcsolódás az integrációs menedzserhez sikertelen", + "The integration manager is offline or it cannot reach your homeserver.": "Az integrációs menedzser nem működik vagy nem éri el a matrix szerveredet.", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Használj Integrációs Menedzsert (%(serverName)s) a botok, kisalkalmazások és matrica csomagok kezeléséhez.", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Használj Integrációs Menedzsert a botok, kisalkalmazások és matrica csomagok kezeléséhez.", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrációs Menedzser megkapja a konfigurációt, módosíthat kisalkalmazásokat, szobához meghívót küldhet és a hozzáférési szintet beállíthatja helyetted.", + "Failed to connect to integration manager": "Az integrációs menedzserhez nem sikerült csatlakozni", + "Widgets do not use message encryption.": "A kisalkalmazások nem használnak üzenet titkosítást.", + "Integrations are disabled": "Az integrációk le vannak tiltva", + "Enable 'Manage Integrations' in Settings to do this.": "Ehhez engedélyezd az „Integrációk Kezelésé”-t a Beállításokban.", + "Integrations not allowed": "Az integrációk nem engedélyezettek", + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "A Riotod nem használhat ehhez Integrációs Menedzsert. Kérlek vedd fel a kapcsolatot az adminisztrátorral." } From 25ba4c5f7124d40fd5d83cddba30b1479f64dc0c Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Nov 2019 13:11:36 +0000 Subject: [PATCH 0764/2372] Fix read markers init code needs to be a constructor or its run too late --- src/components/structures/MessagePanel.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index bd5630ab12..22be35db60 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -109,7 +109,8 @@ export default class MessagePanel extends React.Component { showReactions: PropTypes.bool, }; - componentDidMount() { + constructor() { + super(); // the event after which we put a visible unread marker on the last // render cycle; null if readMarkerVisible was false or the RM was // suppressed (eg because it was at the end of the timeline) @@ -165,6 +166,10 @@ export default class MessagePanel extends React.Component { this._showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline"); + this._isMounted = false; + } + + componentDidMount() { this._isMounted = true; } From d1501a16515c003a949cd819be4a1f34c567f8ea Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:17:55 +0100 Subject: [PATCH 0765/2372] reduce margin on e2e icon in room header --- res/css/views/rooms/_RoomHeader.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 5da8ff76b9..f1e4456cc1 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -17,6 +17,10 @@ limitations under the License. .mx_RoomHeader { flex: 0 0 52px; border-bottom: 1px solid $primary-hairline-color; + + .mx_E2EIcon { + margin: 0 5px; + } } .mx_RoomHeader_wrapper { From de645a32a8d3024956270094bc01e4b6277d7bf9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:14:25 +0100 Subject: [PATCH 0766/2372] remove obsolete style from message composer for e2e icon as it's now the default size for the e2e iconn --- res/css/views/rooms/_MessageComposer.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 036756e2eb..12e45a07c9 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -74,8 +74,6 @@ limitations under the License. .mx_MessageComposer_e2eIcon.mx_E2EIcon { position: absolute; left: 60px; - width: 16px; - height: 16px; margin-right: 0; // Counteract the E2EIcon class margin-left: 3px; // Counteract the E2EIcon class } From 9c234d93172c77b532b696ce18f71fb77e70db66 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:13:01 +0100 Subject: [PATCH 0767/2372] dont make e2e icons themable, as they have multiple colors --- res/css/views/rooms/_E2EIcon.scss | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index 1ee5008888..cb99aa63f1 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -15,8 +15,8 @@ limitations under the License. */ .mx_E2EIcon { - width: 25px; - height: 25px; + width: 16px; + height: 16px; margin: 0 9px; position: relative; display: block; @@ -30,16 +30,14 @@ limitations under the License. bottom: 0; left: 0; right: 0; - mask-repeat: no-repeat; - mask-size: contain; + background-repeat: no-repeat; + background-size: contain; } .mx_E2EIcon_verified::after { - mask-image: url('$(res)/img/e2e/verified.svg'); - background-color: $accent-color; + background-image: url('$(res)/img/e2e/verified.svg'); } .mx_E2EIcon_warning::after { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $warning-color; + background-image: url('$(res)/img/e2e/warning.svg'); } From 35877a06a387e5148cdcb1aa2158283dbdeb5371 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 21 Nov 2019 18:12:32 +0100 Subject: [PATCH 0768/2372] use correct icons with borders --- res/img/e2e/verified.svg | 14 +++----------- res/img/e2e/warning.svg | 15 ++++----------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/res/img/e2e/verified.svg b/res/img/e2e/verified.svg index af6bb92297..464b443dcf 100644 --- a/res/img/e2e/verified.svg +++ b/res/img/e2e/verified.svg @@ -1,12 +1,4 @@ - - - + + + diff --git a/res/img/e2e/warning.svg b/res/img/e2e/warning.svg index 2501da6ab3..209ae0f71f 100644 --- a/res/img/e2e/warning.svg +++ b/res/img/e2e/warning.svg @@ -1,12 +1,5 @@ - - - + + + + From 521cbbac74b392d61b1b85c31d6e0d06808929d3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Nov 2019 12:59:51 +0000 Subject: [PATCH 0769/2372] Make addEventListener conditional Safari doesn't support addEventListener --- src/theme.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/theme.js b/src/theme.js index fa7e3f783b..e89af55924 100644 --- a/src/theme.js +++ b/src/theme.js @@ -41,14 +41,18 @@ export class ThemeWatcher { start() { this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onChange); this._systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this._onChange); - this._preferDark.addEventListener('change', this._onChange); - this._preferLight.addEventListener('change', this._onChange); + if (this._preferDark.addEventListener) { + this._preferDark.addEventListener('change', this._onChange); + this._preferLight.addEventListener('change', this._onChange); + } this._dispatcherRef = dis.register(this._onAction); } stop() { - this._preferDark.removeEventListener('change', this._onChange); - this._preferLight.removeEventListener('change', this._onChange); + if (this._preferDark.addEventListener) { + this._preferDark.removeEventListener('change', this._onChange); + this._preferLight.removeEventListener('change', this._onChange); + } SettingsStore.unwatchSetting(this._systemThemeWatchRef); SettingsStore.unwatchSetting(this._themeWatchRef); dis.unregister(this._dispatcherRef); From 5ec4b6efcdf74197b8efd60e1014aa0d83d50d6b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 22 Nov 2019 10:24:51 +0000 Subject: [PATCH 0770/2372] Show m.room.create event before the ELS on room upgrade Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MessagePanel.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index cf2a5b1738..39504666bf 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -411,6 +411,11 @@ module.exports = createReactClass({ readMarkerInSummary = true; } + // If this m.room.create event should be shown (room upgrade) then show it before the summary + if (this._shouldShowEvent(mxEv)) { + ret.push(...this._getTilesForEvent(prevEvent, mxEv, false)); + } + const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary for (;i + 1 < this.props.events.length; i++) { const collapsedMxEv = this.props.events[i + 1]; From e86d2b616e647f85bbfa56cb79bc952ca932ad40 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 20 Nov 2019 16:18:28 +0100 Subject: [PATCH 0771/2372] add ToastContainer --- res/css/_components.scss | 1 + res/css/structures/_ToastContainer.scss | 105 ++++++++++++++++++++ src/components/structures/LoggedInView.js | 2 + src/components/structures/ToastContainer.js | 85 ++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 res/css/structures/_ToastContainer.scss create mode 100644 src/components/structures/ToastContainer.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 40a2c576d0..f7147b3b9f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -25,6 +25,7 @@ @import "./structures/_TabbedView.scss"; @import "./structures/_TagPanel.scss"; @import "./structures/_TagPanelButtons.scss"; +@import "./structures/_ToastContainer.scss"; @import "./structures/_TopLeftMenuButton.scss"; @import "./structures/_UploadBar.scss"; @import "./structures/_ViewSource.scss"; diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss new file mode 100644 index 0000000000..54132d19bf --- /dev/null +++ b/res/css/structures/_ToastContainer.scss @@ -0,0 +1,105 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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. +*/ + +.mx_ToastContainer { + position: absolute; + top: 0; + left: 70px; + z-index: 101; + padding: 4px; + display: grid; + grid-template-rows: 1fr 14px 6px; + + &.mx_ToastContainer_stacked::before { + content: ""; + margin: 0 4px; + grid-row: 2 / 4; + grid-column: 1; + background-color: white; + box-shadow: 0px 4px 12px $menu-box-shadow-color; + border-radius: 8px; + } + + .mx_Toast_toast { + grid-row: 1 / 3; + grid-column: 1; + color: $primary-fg-color; + background-color: $primary-bg-color; + box-shadow: 0px 4px 12px $menu-box-shadow-color; + border-radius: 8px; + overflow: hidden; + } + + .mx_Toast_toast { + display: grid; + grid-template-columns: 20px 1fr; + column-gap: 10px; + row-gap: 4px; + padding: 8px; + padding-right: 16px; + + &.mx_Toast_hasIcon { + &::after { + content: ""; + width: 20px; + height: 20px; + grid-column: 1; + grid-row: 1; + mask-size: 100%; + mask-repeat: no-repeat; + } + + &.mx_Toast_icon_verification::after { + mask-image: url("$(res)/img/e2e/normal.svg"); + background-color: $primary-fg-color; + } + + h2, .mx_Toast_body { + grid-column: 2; + } + } + + h2 { + grid-column: 1 / 3; + grid-row: 1; + margin: 0; + font-size: 15px; + font-weight: 600; + } + + .mx_Toast_body { + grid-column: 1 / 3; + grid-row: 2; + } + + .mx_Toast_buttons { + display: flex; + + > :not(:last-child) { + margin-right: 8px; + } + } + + .mx_Toast_description { + max-width: 400px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin: 4px 0 11px 0; + font-size: 12px; + } + } +} diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 889b0cdc8b..d071ba1d79 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -525,6 +525,7 @@ const LoggedInView = createReactClass({ const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage'); const GroupView = sdk.getComponent('structures.GroupView'); const MyGroups = sdk.getComponent('structures.MyGroups'); + const ToastContainer = sdk.getComponent('structures.ToastContainer'); const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); const CookieBar = sdk.getComponent('globals.CookieBar'); const NewVersionBar = sdk.getComponent('globals.NewVersionBar'); @@ -628,6 +629,7 @@ const LoggedInView = createReactClass({ return (
        { topBar } +
        { + if (payload.action === "show_toast") { + this._addToast(payload.toast); + } + }; + + _addToast(toast) { + this.setState({toasts: this.state.toasts.concat(toast)}); + } + + dismissTopToast = () => { + const [, ...remaining] = this.state.toasts; + this.setState({toasts: remaining}); + }; + + render() { + const totalCount = this.state.toasts.length; + if (totalCount === 0) { + return null; + } + const isStacked = totalCount > 1; + const topToast = this.state.toasts[0]; + const {title, icon, key, component, props} = topToast; + + const containerClasses = classNames("mx_ToastContainer", { + "mx_ToastContainer_stacked": isStacked, + }); + + const toastClasses = classNames("mx_Toast_toast", { + "mx_Toast_hasIcon": icon, + [`mx_Toast_icon_${icon}`]: icon, + }); + + const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null; + + const toastProps = Object.assign({}, props, { + dismiss: this.dismissTopToast, + key, + }); + + return ( +
        +
        +

        {title}{countIndicator}

        +
        {React.createElement(component, toastProps)}
        +
        +
        + ); + } +} From 66cc68bae4dcee465946089df4a5d973a8e02497 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 20 Nov 2019 16:18:59 +0100 Subject: [PATCH 0772/2372] add new-styled button might merge it later on with accessible button --- res/css/_components.scss | 1 + res/css/views/elements/_FormButton.scss | 31 +++++++++++++++++++++ src/components/views/elements/FormButton.js | 27 ++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 res/css/views/elements/_FormButton.scss create mode 100644 src/components/views/elements/FormButton.js diff --git a/res/css/_components.scss b/res/css/_components.scss index f7147b3b9f..8d2b1cc91a 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -91,6 +91,7 @@ @import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_EventListSummary.scss"; @import "./views/elements/_Field.scss"; +@import "./views/elements/_FormButton.scss"; @import "./views/elements/_IconButton.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InlineSpinner.scss"; diff --git a/res/css/views/elements/_FormButton.scss b/res/css/views/elements/_FormButton.scss new file mode 100644 index 0000000000..191dee5566 --- /dev/null +++ b/res/css/views/elements/_FormButton.scss @@ -0,0 +1,31 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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. +*/ + +.mx_FormButton { + line-height: 16px; + padding: 5px 15px; + font-size: 12px; + + &.mx_AccessibleButton_kind_primary { + color: $accent-color; + background-color: $accent-bg-color; + } + + &.mx_AccessibleButton_kind_danger { + color: $notice-primary-color; + background-color: $notice-primary-bg-color; + } +} diff --git a/src/components/views/elements/FormButton.js b/src/components/views/elements/FormButton.js new file mode 100644 index 0000000000..d667132c38 --- /dev/null +++ b/src/components/views/elements/FormButton.js @@ -0,0 +1,27 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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 AccessibleButton from "./AccessibleButton"; + +export default function FormButton(props) { + const {className, label, kind, ...restProps} = props; + const newClassName = (className || "") + " mx_FormButton"; + const allProps = Object.assign({}, restProps, {className: newClassName, kind: kind || "primary", children: [label]}); + return React.createElement(AccessibleButton, allProps); +} + +FormButton.propTypes = AccessibleButton.propTypes; From f1c62e7dab0ef27bb3a5fd9fa8a63833a8779148 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 20 Nov 2019 16:19:51 +0100 Subject: [PATCH 0773/2372] make colors slightly more opaque than in design as it is very light otherwise --- res/themes/light/css/_light.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index dcd7ce166e..0a3ef812b8 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -12,9 +12,9 @@ $monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emo // unified palette // try to use these colors when possible $accent-color: #03b381; -$accent-bg-color: rgba(115, 247, 91, 0.08); +$accent-bg-color: rgba(3, 179, 129, 0.16); $notice-primary-color: #ff4b55; -$notice-primary-bg-color: rgba(255, 75, 85, 0.08); +$notice-primary-bg-color: rgba(255, 75, 85, 0.16); $notice-secondary-color: #61708b; $header-panel-bg-color: #f3f8fd; From c705752317769c7bcd0ed00cef8beca7c15d82a9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 22 Nov 2019 16:00:39 +0100 Subject: [PATCH 0774/2372] add toast for verification requests this uses a verification requests as emitted by the js-sdk with the `crypto.verification.request` rather than a verifier as emitted by `crypto.verification.start` as this works for both to_device and timeline events with the changes made in the js-sdk pr. --- .../views/toasts/VerificationRequestToast.js | 123 ++++++++++++++++++ src/i18n/strings/en_EN.json | 3 + src/utils/KeyVerificationStateObserver.js | 14 +- 3 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 src/components/views/toasts/VerificationRequestToast.js diff --git a/src/components/views/toasts/VerificationRequestToast.js b/src/components/views/toasts/VerificationRequestToast.js new file mode 100644 index 0000000000..c6f7f3a363 --- /dev/null +++ b/src/components/views/toasts/VerificationRequestToast.js @@ -0,0 +1,123 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import sdk from "../../../index"; +import { _t } from '../../../languageHandler'; +import Modal from "../../../Modal"; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; +import KeyVerificationStateObserver, {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver"; +import dis from "../../../dispatcher"; + +export default class VerificationRequestToast extends React.PureComponent { + constructor(props) { + super(props); + const {event, timeout} = props.request; + // to_device requests don't have a timestamp, so consider them age=0 + const age = event.getTs() ? event.getLocalAge() : 0; + const remaining = Math.max(0, timeout - age); + const counter = Math.ceil(remaining / 1000); + this.state = {counter}; + if (this.props.requestObserver) { + this.props.requestObserver.setCallback(this._checkRequestIsPending); + } + } + + componentDidMount() { + if (this.props.requestObserver) { + this.props.requestObserver.attach(); + this._checkRequestIsPending(); + } + this._intervalHandle = setInterval(() => { + let {counter} = this.state; + counter -= 1; + if (counter <= 0) { + this.cancel(); + } else { + this.setState({counter}); + } + }, 1000); + } + + componentWillUnmount() { + clearInterval(this._intervalHandle); + if (this.props.requestObserver) { + this.props.requestObserver.detach(); + } + } + + _checkRequestIsPending = () => { + if (!this.props.requestObserver.pending) { + this.props.dismiss(); + } + } + + cancel = () => { + this.props.dismiss(); + try { + this.props.request.cancel(); + } catch (err) { + console.error("Error while cancelling verification request", err); + } + } + + accept = () => { + this.props.dismiss(); + const {event} = this.props.request; + // no room id for to_device requests + if (event.getRoomId()) { + dis.dispatch({ + action: 'view_room', + room_id: event.getRoomId(), + should_peek: false, + }); + } + + const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS); + const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); + Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {verifier}); + }; + + render() { + const FormButton = sdk.getComponent("elements.FormButton"); + const {event} = this.props.request; + const userId = event.getSender(); + let nameLabel = event.getRoomId() ? userLabelForEventRoom(userId, event) : userId; + // for legacy to_device verification requests + if (nameLabel === userId) { + const client = MatrixClientPeg.get(); + const user = client.getUser(event.getSender()); + if (user && user.displayName) { + nameLabel = _t("%(name)s (%(userId)s)", {name: user.displayName, userId}); + } + } + return (
        +
        {nameLabel}
        +
        + + +
        +
        ); + } +} + +VerificationRequestToast.propTypes = { + dismiss: PropTypes.func.isRequired, + request: PropTypes.object.isRequired, + requestObserver: PropTypes.instanceOf(KeyVerificationStateObserver), +}; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7709a4a398..177d999148 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -481,6 +481,7 @@ "Headphones": "Headphones", "Folder": "Folder", "Pin": "Pin", + "Decline (%(counter)s)": "Decline (%(counter)s)", "Accept to continue:": "Accept to continue:", "Failed to upload profile picture!": "Failed to upload profile picture!", "Upload new:": "Upload new:", @@ -1694,6 +1695,7 @@ "Review terms and conditions": "Review terms and conditions", "Old cryptography data detected": "Old cryptography data detected", "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", + "Verification Request": "Verification Request", "Logout": "Logout", "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "Your Communities": "Your Communities", @@ -1759,6 +1761,7 @@ "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Failed to load timeline position": "Failed to load timeline position", + " (1/%(totalCount)s)": " (1/%(totalCount)s)", "Guest": "Guest", "Your profile": "Your profile", "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", diff --git a/src/utils/KeyVerificationStateObserver.js b/src/utils/KeyVerificationStateObserver.js index 7de50ec4bf..2f7c0367ad 100644 --- a/src/utils/KeyVerificationStateObserver.js +++ b/src/utils/KeyVerificationStateObserver.js @@ -30,6 +30,18 @@ export default class KeyVerificationStateObserver { this._updateVerificationState(); } + get concluded() { + return this.accepted || this.done || this.cancelled; + } + + get pending() { + return !this.concluded; + } + + setCallback(callback) { + this._updateCallback = callback; + } + attach() { this._requestEvent.on("Event.relationsCreated", this._onRelationsCreated); for (const phaseName of SUB_EVENT_TYPES_OF_INTEREST) { @@ -83,7 +95,7 @@ export default class KeyVerificationStateObserver { _onRelationsUpdated = (event) => { this._updateVerificationState(); - this._updateCallback(); + this._updateCallback && this._updateCallback(); }; _updateVerificationState() { From 8cb362002bf00062b8a106cdc2a2cd14b001f93b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 22 Nov 2019 16:02:11 +0100 Subject: [PATCH 0775/2372] show a toast instead of dialog when feature flag is enabled --- src/components/structures/MatrixChat.js | 35 ++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 661a0c7077..9d0dd7da8f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -62,6 +62,7 @@ import { countRoomsWithNotif } from '../../RoomNotifs'; import { ThemeWatcher } from "../../theme"; import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { defer } from "../../utils/promise"; +import KeyVerificationStateObserver from '../../utils/KeyVerificationStateObserver'; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -1270,7 +1271,6 @@ export default createReactClass({ this.firstSyncComplete = false; this.firstSyncPromise = defer(); const cli = MatrixClientPeg.get(); - const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); // 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 @@ -1469,12 +1469,35 @@ export default createReactClass({ } }); - cli.on("crypto.verification.start", (verifier) => { - Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { - verifier, - }); - }); + if (SettingsStore.isFeatureEnabled("feature_dm_verification")) { + cli.on("crypto.verification.request", request => { + let requestObserver; + if (request.event.getRoomId()) { + requestObserver = new KeyVerificationStateObserver( + request.event, MatrixClientPeg.get()); + } + if (!requestObserver || requestObserver.pending) { + dis.dispatch({ + action: "show_toast", + toast: { + key: request.event.getId(), + title: _t("Verification Request"), + icon: "verification", + props: {request, requestObserver}, + component: sdk.getComponent("toasts.VerificationRequestToast"), + }, + }); + } + }); + } else { + cli.on("crypto.verification.start", (verifier) => { + const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog"); + Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { + verifier, + }); + }); + } // Fire the tinter right on startup to ensure the default theme is applied // A later sync can/will correct the tint to be the right value for the user const colorScheme = SettingsStore.getValue("roomColor"); From 0dfb0f54688e2bff8427dd38d7ce108666fc871a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 22 Nov 2019 16:25:30 +0100 Subject: [PATCH 0776/2372] fix lint --- src/components/views/elements/FormButton.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/FormButton.js b/src/components/views/elements/FormButton.js index d667132c38..f6b4c986f5 100644 --- a/src/components/views/elements/FormButton.js +++ b/src/components/views/elements/FormButton.js @@ -20,7 +20,8 @@ import AccessibleButton from "./AccessibleButton"; export default function FormButton(props) { const {className, label, kind, ...restProps} = props; const newClassName = (className || "") + " mx_FormButton"; - const allProps = Object.assign({}, restProps, {className: newClassName, kind: kind || "primary", children: [label]}); + const allProps = Object.assign({}, restProps, + {className: newClassName, kind: kind || "primary", children: [label]}); return React.createElement(AccessibleButton, allProps); } From 309633181d60b0732966e7c8fe4acd4255341af4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 22 Nov 2019 16:32:50 +0100 Subject: [PATCH 0777/2372] use FormButton in verification request tile too and dedupe styles --- res/css/structures/_ToastContainer.scss | 4 ---- res/css/views/elements/_FormButton.scss | 5 +++++ .../messages/_MKeyVerificationRequest.scss | 17 ----------------- .../views/messages/MKeyVerificationRequest.js | 6 +++--- 4 files changed, 8 insertions(+), 24 deletions(-) diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 54132d19bf..ca8477dcc5 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -87,10 +87,6 @@ limitations under the License. .mx_Toast_buttons { display: flex; - - > :not(:last-child) { - margin-right: 8px; - } } .mx_Toast_description { diff --git a/res/css/views/elements/_FormButton.scss b/res/css/views/elements/_FormButton.scss index 191dee5566..1483fe2091 100644 --- a/res/css/views/elements/_FormButton.scss +++ b/res/css/views/elements/_FormButton.scss @@ -18,6 +18,11 @@ limitations under the License. line-height: 16px; padding: 5px 15px; font-size: 12px; + height: min-content; + + &:not(:last-child) { + margin-right: 8px; + } &.mx_AccessibleButton_kind_primary { color: $accent-color; diff --git a/res/css/views/messages/_MKeyVerificationRequest.scss b/res/css/views/messages/_MKeyVerificationRequest.scss index 87a75dee82..ee20751083 100644 --- a/res/css/views/messages/_MKeyVerificationRequest.scss +++ b/res/css/views/messages/_MKeyVerificationRequest.scss @@ -65,23 +65,6 @@ limitations under the License. .mx_KeyVerification_buttons { align-items: center; display: flex; - - .mx_AccessibleButton_kind_decline { - color: $notice-primary-color; - background-color: $notice-primary-bg-color; - } - - .mx_AccessibleButton_kind_accept { - color: $accent-color; - background-color: $accent-bg-color; - } - - [role=button] { - margin: 10px; - padding: 7px 15px; - border-radius: 5px; - height: min-content; - } } .mx_KeyVerification_state { diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.js index 21d82309ed..b2a1724fc6 100644 --- a/src/components/views/messages/MKeyVerificationRequest.js +++ b/src/components/views/messages/MKeyVerificationRequest.js @@ -111,10 +111,10 @@ export default class MKeyVerificationRequest extends React.Component { userLabelForEventRoom(fromUserId, mxEvent)}
        ); const isResolved = !(this.state.accepted || this.state.cancelled || this.state.done); if (isResolved) { - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + const FormButton = sdk.getComponent("elements.FormButton"); stateNode = (
        - {_t("Decline")} - {_t("Accept")} + +
        ); } } else if (isOwn) { // request sent by us From 8ce1ed472654297091d8fc4b76c89ffe772572e7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 22 Nov 2019 16:36:22 +0100 Subject: [PATCH 0778/2372] moar lint --- res/css/structures/_ToastContainer.scss | 3 --- 1 file changed, 3 deletions(-) diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index ca8477dcc5..4c5e746e66 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -41,9 +41,6 @@ limitations under the License. box-shadow: 0px 4px 12px $menu-box-shadow-color; border-radius: 8px; overflow: hidden; - } - - .mx_Toast_toast { display: grid; grid-template-columns: 20px 1fr; column-gap: 10px; From 32b6fccbfcfe9f8197354e69634d48d2da557ce5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 22 Nov 2019 17:26:14 +0100 Subject: [PATCH 0779/2372] fix double date separator --- src/components/structures/MessagePanel.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 7eae3ff7a3..912b865b9f 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -416,7 +416,8 @@ export default class MessagePanel extends React.Component { // If this m.room.create event should be shown (room upgrade) then show it before the summary if (this._shouldShowEvent(mxEv)) { - ret.push(...this._getTilesForEvent(prevEvent, mxEv, false)); + // pass in the mxEv as prevEvent as well so no extra DateSeparator is rendered + ret.push(...this._getTilesForEvent(mxEv, mxEv, false)); } const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary From f1d096e7aa860bb673513f3d6fead7880343574c Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Fri, 22 Nov 2019 15:16:53 +0000 Subject: [PATCH 0780/2372] Translated using Weblate (Bulgarian) Currently translated at 95.9% (1840 of 1918 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/bg/ --- src/i18n/strings/bg.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index 3697cc635c..0ec1d91b9b 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -2260,5 +2260,7 @@ "You cancelled": "Отказахте потвърждаването", "%(name)s cancelled": "%(name)s отказа", "%(name)s wants to verify": "%(name)s иска да извърши потвърждение", - "You sent a verification request": "Изпратихте заявка за потвърждение" + "You sent a verification request": "Изпратихте заявка за потвърждение", + "Custom (%(level)s)": "Собствен (%(level)s)", + "Try out new ways to ignore people (experimental)": "Опитайте нови начини да игнорирате хора (експериментално)" } From 9bb98e2b9c64bf7127ffcd7143cc387a8d1ae965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20Luke=C5=A1?= Date: Fri, 22 Nov 2019 13:42:28 +0000 Subject: [PATCH 0781/2372] Translated using Weblate (Czech) Currently translated at 95.8% (1837 of 1918 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/cs/ --- src/i18n/strings/cs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index e4e01b0116..62e6147506 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -1269,7 +1269,7 @@ "Security & Privacy": "Bezpečnost & Soukromí", "Encryption": "Šifrování", "Once enabled, encryption cannot be disabled.": "Když se šifrování zapne, už nepůjde vypnout.", - "Encrypted": "Šifrování je zapnuté", + "Encrypted": "Šifrování", "General": "Obecné", "General failure": "Nějaká chyba", "This homeserver does not support login using email address.": "Tento homeserver neumožňuje přihlášní pomocí emailu.", From fab7dfd8e81f709d05e012284200d59a5c1369e5 Mon Sep 17 00:00:00 2001 From: random Date: Fri, 22 Nov 2019 14:01:15 +0000 Subject: [PATCH 0782/2372] Translated using Weblate (Italian) Currently translated at 99.9% (1916 of 1918 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 9faa48328c..eb5f5f76f2 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2298,5 +2298,22 @@ "This widget may use cookies.": "Questo widget può usare cookie.", "Send verification requests in direct message, including a new verification UX in the member panel.": "Invia le richieste di verifica via messaggio diretto, inclusa una nuova esperienza utente per la verifica nel pannello membri.", "Enable local event indexing and E2EE search (requires restart)": "Attiva l'indicizzazione di eventi locali e la ricerca E2EE (richiede riavvio)", - "Match system dark mode setting": "Combacia la modalità scura di sistema" + "Match system dark mode setting": "Combacia la modalità scura di sistema", + "Connecting to integration manager...": "Connessione al gestore di integrazioni...", + "Cannot connect to integration manager": "Impossibile connettere al gestore di integrazioni", + "The integration manager is offline or it cannot reach your homeserver.": "Il gestore di integrazioni è offline o non riesce a raggiungere il tuo homeserver.", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Usa un gestore di integrazioni (%(serverName)s) per gestire bot, widget e pacchetti di adesivi.", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Usa un gestore di integrazioni per gestire bot, widget e pacchetti di adesivi.", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "I gestori di integrazione ricevono dati di configurazione e possono modificare widget, inviare inviti alla stanza, assegnare permessi a tuo nome.", + "Failed to connect to integration manager": "Connessione al gestore di integrazioni fallita", + "Widgets do not use message encryption.": "I widget non usano la cifratura dei messaggi.", + "More options": "Altre opzioni", + "Integrations are disabled": "Le integrazioni sono disattivate", + "Enable 'Manage Integrations' in Settings to do this.": "Attiva 'Gestisci integrazioni' nelle impostazioni per continuare.", + "Integrations not allowed": "Integrazioni non permesse", + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Il tuo Riot non ti permette di usare il gestore di integrazioni per questa azione. Contatta un amministratore.", + "Reload": "Ricarica", + "Take picture": "Scatta foto", + "Remove for everyone": "Rimuovi per tutti", + "Remove for me": "Rimuovi per me" } From 2b7a7f88b8dd2dba96b58e18d81c53939707b781 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 22 Nov 2019 17:26:14 +0100 Subject: [PATCH 0783/2372] fix double date separator --- src/components/structures/MessagePanel.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 39504666bf..aad01a2bff 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -413,7 +413,8 @@ module.exports = createReactClass({ // If this m.room.create event should be shown (room upgrade) then show it before the summary if (this._shouldShowEvent(mxEv)) { - ret.push(...this._getTilesForEvent(prevEvent, mxEv, false)); + // pass in the mxEv as prevEvent as well so no extra DateSeparator is rendered + ret.push(...this._getTilesForEvent(mxEv, mxEv, false)); } const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary From aae315038309df0533986e3f7a186c236a6a7998 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Nov 2019 16:50:32 +0000 Subject: [PATCH 0784/2372] Null check on thumbnail_file --- src/components/views/messages/MVideoBody.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index 43e4f2dd75..44954344ff 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -88,7 +88,7 @@ module.exports = createReactClass({ const content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { let thumbnailPromise = Promise.resolve(null); - if (content.info.thumbnail_file) { + if (content.info && content.info.thumbnail_file) { thumbnailPromise = decryptFile( content.info.thumbnail_file, ).then(function(blob) { From f23e5942e6e80465a8031aacd15878fc0360563d Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 22 Nov 2019 17:18:26 +0000 Subject: [PATCH 0785/2372] Prepare changelog for v1.7.3-rc.2 --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dad0accd2..a3f72685db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +Changes in [1.7.3-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.3-rc.2) (2019-11-22) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.3-rc.1...v1.7.3-rc.2) + + * Fix double date separator for room upgrade tiles + [\#3663](https://github.com/matrix-org/matrix-react-sdk/pull/3663) + * Show m.room.create event before the ELS on room upgrade + [\#3660](https://github.com/matrix-org/matrix-react-sdk/pull/3660) + * Make addEventListener conditional + [\#3659](https://github.com/matrix-org/matrix-react-sdk/pull/3659) + * Fix e2e icons + [\#3658](https://github.com/matrix-org/matrix-react-sdk/pull/3658) + Changes in [1.7.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.3-rc.1) (2019-11-20) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.2...v1.7.3-rc.1) From 730967fd3f381ed26db5f09a3cbaf9824fe14191 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 22 Nov 2019 17:18:27 +0000 Subject: [PATCH 0786/2372] v1.7.3-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5d2d7635c..099fc30b33 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.3-rc.1", + "version": "1.7.3-rc.2", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 8254261f261006740cc53ff6ea0602f14aac4562 Mon Sep 17 00:00:00 2001 From: Keunes Date: Fri, 22 Nov 2019 18:48:44 +0000 Subject: [PATCH 0787/2372] Translated using Weblate (Dutch) Currently translated at 93.4% (1794 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/nl/ --- src/i18n/strings/nl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 825f3c6a48..7ec8197eaf 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -1193,7 +1193,7 @@ "Encrypted, not sent": "Versleuteld, niet verstuurd", "Demote yourself?": "Uzelf degraderen?", "Demote": "Degraderen", - "Share Link to User": "Koppeling met gebruiker delen", + "Share Link to User": "Link naar gebruiker delen", "deleted": "verwijderd", "underlined": "onderstreept", "inline-code": "code", From d867f41f569e01db0fbeba732ff97ba354fc3f64 Mon Sep 17 00:00:00 2001 From: Nathan Follens Date: Fri, 22 Nov 2019 18:48:50 +0000 Subject: [PATCH 0788/2372] Translated using Weblate (Dutch) Currently translated at 93.4% (1794 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/nl/ --- src/i18n/strings/nl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 7ec8197eaf..347afc3583 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -1193,7 +1193,7 @@ "Encrypted, not sent": "Versleuteld, niet verstuurd", "Demote yourself?": "Uzelf degraderen?", "Demote": "Degraderen", - "Share Link to User": "Link naar gebruiker delen", + "Share Link to User": "Koppeling naar gebruiker delen", "deleted": "verwijderd", "underlined": "onderstreept", "inline-code": "code", From 95e02899b45735da73f299043e341f08287fc30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Sat, 23 Nov 2019 10:08:24 +0000 Subject: [PATCH 0789/2372] Translated using Weblate (French) Currently translated at 100.0% (1921 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 0c13b3c722..097dd0824f 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2370,5 +2370,9 @@ "Reload": "Recharger", "Take picture": "Prendre une photo", "Remove for everyone": "Supprimer pour tout le monde", - "Remove for me": "Supprimer pour moi" + "Remove for me": "Supprimer pour moi", + "Decline (%(counter)s)": "Refuser (%(counter)s)", + "Manage integrations": "Gérer les intégrations", + "Verification Request": "Demande de vérification", + " (1/%(totalCount)s)": " (1/%(totalCount)s)" } From 1c02fa573f315ba5dbb479b440c7506f606d7b92 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Fri, 22 Nov 2019 18:52:54 +0000 Subject: [PATCH 0790/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1921 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 7ed4eb253c..9e41b7381e 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2358,5 +2358,9 @@ "Integrations are disabled": "Az integrációk le vannak tiltva", "Enable 'Manage Integrations' in Settings to do this.": "Ehhez engedélyezd az „Integrációk Kezelésé”-t a Beállításokban.", "Integrations not allowed": "Az integrációk nem engedélyezettek", - "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "A Riotod nem használhat ehhez Integrációs Menedzsert. Kérlek vedd fel a kapcsolatot az adminisztrátorral." + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "A Riotod nem használhat ehhez Integrációs Menedzsert. Kérlek vedd fel a kapcsolatot az adminisztrátorral.", + "Decline (%(counter)s)": "Elutasítás (%(counter)s)", + "Manage integrations": "Integrációk kezelése", + "Verification Request": "Ellenőrzési kérés", + " (1/%(totalCount)s)": " (1/%(totalCount)s)" } From 00fd329bfeeb3dab87e2ccbc2d08a64ebed35c85 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 23 Nov 2019 18:56:46 +0000 Subject: [PATCH 0791/2372] Translated using Weblate (Italian) Currently translated at 100.0% (1921 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index eb5f5f76f2..21c03a7802 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -2315,5 +2315,11 @@ "Reload": "Ricarica", "Take picture": "Scatta foto", "Remove for everyone": "Rimuovi per tutti", - "Remove for me": "Rimuovi per me" + "Remove for me": "Rimuovi per me", + "Trust": "Fidati", + "Decline (%(counter)s)": "Rifiuta (%(counter)s)", + "Manage integrations": "Gestisci integrazioni", + "Ignored/Blocked": "Ignorati/Bloccati", + "Verification Request": "Richiesta verifica", + " (1/%(totalCount)s)": " (1/%(totalCount)s)" } From 1607bc329bc819866e22f1d68814f897d3d7a336 Mon Sep 17 00:00:00 2001 From: dreboy30 Date: Sat, 23 Nov 2019 15:16:23 +0000 Subject: [PATCH 0792/2372] Translated using Weblate (Portuguese) Currently translated at 34.5% (663 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pt/ --- src/i18n/strings/pt.json | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json index 5a56e807e4..7aa8851daa 100644 --- a/src/i18n/strings/pt.json +++ b/src/i18n/strings/pt.json @@ -350,7 +350,7 @@ "No devices with registered encryption keys": "Não há dispositivos com chaves de criptografia registradas", "No more results": "Não há mais resultados", "No results": "Sem resultados", - "OK": "Ok", + "OK": "OK", "Revoke Moderator": "Retirar status de moderador", "Search": "Pesquisar", "Search failed": "Busca falhou", @@ -847,6 +847,29 @@ "Add Phone Number": "Adicione número de telefone", "The platform you're on": "A plataforma em que se encontra", "The version of Riot.im": "A versão do RIOT.im", - "Whether or not you're logged in (we don't record your username)": "Tenha ou não, iniciado sessão (não iremos guardar o seu username)", - "Your language of choice": "O seu idioma de escolha" + "Whether or not you're logged in (we don't record your username)": "Tenha ou não, iniciado sessão (não iremos guardar o seu nome de utilizador)", + "Your language of choice": "O seu idioma que escolheu", + "Which officially provided instance you are using, if any": "Qual instância oficial está utilizando, se for o caso", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Se está a usar o modo de texto enriquecido do editor de texto enriquecido", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Se usa a funcionalidade 'breadcrumbs' (avatares em cima da lista de salas", + "Your homeserver's URL": "O URL do seu servidor de início", + "Your identity server's URL": "O URL do seu servidor de identidade", + "e.g. %(exampleValue)s": "ex. %(exampleValue)s", + "Every page you use in the app": "Todas as páginas que usa na aplicação", + "e.g. ": "ex. ", + "Your User Agent": "O seu Agente de Utilizador", + "Your device resolution": "A resolução do seu dispositivo", + "The information being sent to us to help make Riot.im better includes:": "As informações que estão sendo enviadas para ajudar a melhorar o Riot.im incluem:", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Quando esta página contém informação de que permitam a sua identificação, como uma sala, ID de utilizador ou de grupo, estes dados são removidos antes de serem enviados ao servidor.", + "Call Failed": "A chamada falhou", + "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Há dispositivos desconhecidos nesta sala: se continuar sem os verificar, será possível que alguém espie a sua chamada.", + "Review Devices": "Rever dispositivos", + "Call Anyway": "Ligar na mesma", + "Answer Anyway": "Responder na mesma", + "Call": "Ligar", + "Answer": "Responder", + "Call failed due to misconfigured server": "Chamada falhada devido a um erro de configuração do servidor", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Peça ao administrador do seu servidor inicial (%(homeserverDomain)s) de configurar um servidor TURN para que as chamadas funcionem fiavelmente.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativamente, pode tentar usar o servidor público em turn.matrix.org, mas não será tão fiável e partilhará o seu IP com esse servidor. Também pode gerir isso nas definições.", + "Try using turn.matrix.org": "Tente utilizar turn.matrix.org" } From 4fb6e4b9833e4cbb69dd773515ff41d17eb13f89 Mon Sep 17 00:00:00 2001 From: dreboy30 Date: Sat, 23 Nov 2019 15:31:58 +0000 Subject: [PATCH 0793/2372] Translated using Weblate (Portuguese (Brazil)) Currently translated at 68.8% (1321 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pt_BR/ --- src/i18n/strings/pt_BR.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index 072215663d..415d2fdd4b 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -652,7 +652,7 @@ "Whether or not you're using the Richtext mode of the Rich Text Editor": "Se você está usando o editor de texto visual", "Your homeserver's URL": "A URL do seu Servidor de Base (homeserver)", "Your identity server's URL": "A URL do seu servidor de identidade", - "The information being sent to us to help make Riot.im better includes:": "As informações que estão sendo usadas para ajudar a melhorar o Riot.im incluem:", + "The information being sent to us to help make Riot.im better includes:": "As informações que estão sendo enviadas para ajudar a melhorar o Riot.im incluem:", "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Quando esta página tem informação de identificação, como uma sala, ID de usuária/o ou de grupo, estes dados são removidos antes de serem enviados ao servidor.", "Call Failed": "A chamada falhou", "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Há dispositivos desconhecidos nesta sala: se você continuar sem verificá-los, será possível alguém espiar sua chamada.", From 11fec80370fa7cd8e081acbd19e631a88cfff131 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 24 Nov 2019 20:52:22 -0700 Subject: [PATCH 0794/2372] Hide tooltips with CSS when they aren't visible Fixes https://github.com/vector-im/riot-web/issues/11456 --- src/components/views/elements/Tooltip.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/Tooltip.js b/src/components/views/elements/Tooltip.js index bb5f9f0604..8ff3ce9bdb 100644 --- a/src/components/views/elements/Tooltip.js +++ b/src/components/views/elements/Tooltip.js @@ -100,7 +100,9 @@ module.exports = createReactClass({ const parent = ReactDOM.findDOMNode(this).parentNode; let style = {}; style = this._updatePosition(style); - style.display = "block"; + // Hide the entire container when not visible. This prevents flashing of the tooltip + // if it is not meant to be visible on first mount. + style.display = this.props.visible ? "block" : "none"; const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName, { "mx_Tooltip_visible": this.props.visible, From 56a7de5157dd70d281b80cdf59523844971bb7c8 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Mon, 25 Nov 2019 08:31:58 +0000 Subject: [PATCH 0795/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1921 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 2fd014554e..a5f8e5e04b 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2363,5 +2363,9 @@ "Reload": "重新載入", "Take picture": "拍照", "Remove for everyone": "對所有人移除", - "Remove for me": "對我移除" + "Remove for me": "對我移除", + "Decline (%(counter)s)": "拒絕 (%(counter)s)", + "Manage integrations": "管理整合", + "Verification Request": "驗證請求", + " (1/%(totalCount)s)": " (1/%(totalCount)s)" } From 422ab81185433c3d61cd261760dfcaee192b72e2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 25 Nov 2019 12:31:57 +0100 Subject: [PATCH 0796/2372] a11y adjustments for toasts --- src/components/structures/ToastContainer.js | 2 +- src/components/views/toasts/VerificationRequestToast.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/ToastContainer.js b/src/components/structures/ToastContainer.js index b8ced1e9de..83bbdac1a1 100644 --- a/src/components/structures/ToastContainer.js +++ b/src/components/structures/ToastContainer.js @@ -74,7 +74,7 @@ export default class ToastContainer extends React.Component { }); return ( -
        +

        {title}{countIndicator}

        {React.createElement(component, toastProps)}
        diff --git a/src/components/views/toasts/VerificationRequestToast.js b/src/components/views/toasts/VerificationRequestToast.js index c6f7f3a363..89af91c41f 100644 --- a/src/components/views/toasts/VerificationRequestToast.js +++ b/src/components/views/toasts/VerificationRequestToast.js @@ -108,7 +108,7 @@ export default class VerificationRequestToast extends React.PureComponent { } return (
        {nameLabel}
        -
        +
        From 694f2cb1dc676c06fd2961e566eeaf144c5cf603 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 25 Nov 2019 13:20:20 +0100 Subject: [PATCH 0797/2372] make sure the toast container is always in the document --- src/components/structures/ToastContainer.js | 43 ++++++++++----------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/components/structures/ToastContainer.js b/src/components/structures/ToastContainer.js index 83bbdac1a1..a8dca35747 100644 --- a/src/components/structures/ToastContainer.js +++ b/src/components/structures/ToastContainer.js @@ -50,35 +50,34 @@ export default class ToastContainer extends React.Component { render() { const totalCount = this.state.toasts.length; - if (totalCount === 0) { - return null; - } const isStacked = totalCount > 1; - const topToast = this.state.toasts[0]; - const {title, icon, key, component, props} = topToast; + let toast; + if (totalCount !== 0) { + const topToast = this.state.toasts[0]; + const {title, icon, key, component, props} = topToast; + const toastClasses = classNames("mx_Toast_toast", { + "mx_Toast_hasIcon": icon, + [`mx_Toast_icon_${icon}`]: icon, + }); + const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null; + + const toastProps = Object.assign({}, props, { + dismiss: this.dismissTopToast, + key, + }); + toast = (
        +

        {title}{countIndicator}

        +
        {React.createElement(component, toastProps)}
        +
        ); + } const containerClasses = classNames("mx_ToastContainer", { "mx_ToastContainer_stacked": isStacked, }); - const toastClasses = classNames("mx_Toast_toast", { - "mx_Toast_hasIcon": icon, - [`mx_Toast_icon_${icon}`]: icon, - }); - - const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null; - - const toastProps = Object.assign({}, props, { - dismiss: this.dismissTopToast, - key, - }); - return ( -
        -
        -

        {title}{countIndicator}

        -
        {React.createElement(component, toastProps)}
        -
        +
        + {toast}
        ); } From 942db34e9263c6e9b6f7236cfdb704f223a169a7 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 25 Nov 2019 13:27:15 +0000 Subject: [PATCH 0798/2372] released js-sdk --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 099fc30b33..a92222579c 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "lolex": "4.2", - "matrix-js-sdk": "2.4.4-rc.1", + "matrix-js-sdk": "2.4.4", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index eb72b11793..44d9548b54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5197,10 +5197,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc" integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw== -matrix-js-sdk@2.4.4-rc.1: - version "2.4.4-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.4-rc.1.tgz#5fd33fd11be9eea23cd0d0b8eb79da7a4b6253bf" - integrity sha512-Kn94zZMXh2EmihYL3lWNp2lpT7RtqcaUxjkP7H9Mr113swSOXtKr8RWMrvopAIguC1pcLzL+lCk+N8rrML2A4Q== +matrix-js-sdk@2.4.4: + version "2.4.4" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.4.tgz#d5e2d6fbe938c4275a1423a5f09330d33517ce3f" + integrity sha512-wSaRFvhWvwEzVaEkyBGo5ReumvaM5OrC1MJ6SVlyoLwH/WRPEXcUlu+rUNw5TFVEAH4TAVHXf/SVRBiR0j5nSQ== dependencies: another-json "^0.2.0" babel-runtime "^6.26.0" From 92a50c73274160cc3476dfcfba1a2fb3c0421f43 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 25 Nov 2019 13:30:40 +0000 Subject: [PATCH 0799/2372] Prepare changelog for v1.7.3 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3f72685db..bc2341863a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [1.7.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.3) (2019-11-25) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.3-rc.2...v1.7.3) + + * No changes since rc.2 + Changes in [1.7.3-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.3-rc.2) (2019-11-22) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.3-rc.1...v1.7.3-rc.2) From f62cd367450ce47329828ca44b50550c774c61c6 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 25 Nov 2019 13:30:40 +0000 Subject: [PATCH 0800/2372] v1.7.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a92222579c..1ecfe63c23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.3-rc.2", + "version": "1.7.3", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 54294e4927b0392d6eb6a14a9282128405955eb1 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 25 Nov 2019 14:03:40 +0000 Subject: [PATCH 0801/2372] Clarify that cross-signing is in development In an attempt to clarify the state of this highly anticipated feature, this updates the labs flag name to match. Part of https://github.com/vector-im/riot-web/issues/11492 --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ddc7c016cc..0a83237f45 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -342,7 +342,7 @@ "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Send verification requests in direct message, including a new verification UX in the member panel.": "Send verification requests in direct message, including a new verification UX in the member panel.", - "Enable cross-signing to verify per-user instead of per-device": "Enable cross-signing to verify per-user instead of per-device", + "Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)", "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 89693f7c50..5a3283c5f0 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -143,7 +143,7 @@ export const SETTINGS = { }, "feature_cross_signing": { isFeature: true, - displayName: _td("Enable cross-signing to verify per-user instead of per-device"), + displayName: _td("Enable cross-signing to verify per-user instead of per-device (in development)"), supportedLevels: LEVELS_FEATURE, default: false, controller: new ReloadOnChangeController(), From b55a1a107788f027ca8cc85afc81edac79fe37e1 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 25 Nov 2019 14:39:04 +0000 Subject: [PATCH 0802/2372] Appease linter --- .../views/dialogs/keybackup/CreateKeyBackupDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index c43fdb0626..ba75032ea4 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -45,7 +45,7 @@ function selectText(target) { selection.addRange(range); } -/** +/* * Walks the user through the process of creating an e2e key backup * on the server. */ From 21a15fdcb4ec1098df2a62ea3fabea5812c7dbd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 21 Nov 2019 10:38:21 +0100 Subject: [PATCH 0803/2372] EventIndex: Move the checkpoint loading logic into the init method. The checkpoints don't seem to be loaded anymore in the onSync method, the reason why this has stopped working is left unexplored since loading the checkpoints makes most sense during the initialization step anyways. --- src/indexing/EventIndex.js | 13 +++++-------- src/indexing/EventIndexPeg.js | 2 -- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 6bad992017..cf7e2d8da2 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -35,7 +35,12 @@ export default class EventIndex { async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); + await indexManager.initEventIndex(); + console.log("EventIndex: Successfully initialized the event index"); + + this.crawlerCheckpoints = await indexManager.loadCheckpoints(); + console.log("EventIndex: Loaded checkpoints", this.crawlerCheckpoints); this.registerListeners(); } @@ -62,14 +67,6 @@ export default class EventIndex { onSync = async (state, prevState, data) => { const indexManager = PlatformPeg.get().getEventIndexingManager(); - if (prevState === null && state === "PREPARED") { - // Load our stored checkpoints, if any. - this.crawlerCheckpoints = await indexManager.loadCheckpoints(); - console.log("EventIndex: Loaded checkpoints", - this.crawlerCheckpoints); - return; - } - if (prevState === "PREPARED" && state === "SYNCING") { const addInitialCheckpoints = async () => { const client = MatrixClientPeg.get(); diff --git a/src/indexing/EventIndexPeg.js b/src/indexing/EventIndexPeg.js index c0bdd74ff4..75f0fa66ba 100644 --- a/src/indexing/EventIndexPeg.js +++ b/src/indexing/EventIndexPeg.js @@ -55,8 +55,6 @@ class EventIndexPeg { return false; } - console.log("EventIndex: Successfully initialized the event index"); - this.index = index; return true; From 9df227dbd08b130e93ed636ba2421022bc7cdd27 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 25 Nov 2019 16:21:09 -0700 Subject: [PATCH 0804/2372] Update breadcrumbs when we do eventually see upgraded rooms Fixes https://github.com/vector-im/riot-web/issues/11469 --- src/components/views/rooms/RoomBreadcrumbs.js | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js index 6529b5b1da..a80602368f 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.js +++ b/src/components/views/rooms/RoomBreadcrumbs.js @@ -31,6 +31,9 @@ import {_t} from "../../../languageHandler"; const MAX_ROOMS = 20; const MIN_ROOMS_BEFORE_ENABLED = 10; +// The threshold time in milliseconds to wait for an autojoined room to show up. +const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90 seconds + export default class RoomBreadcrumbs extends React.Component { constructor(props) { super(props); @@ -38,6 +41,10 @@ export default class RoomBreadcrumbs extends React.Component { this.onAction = this.onAction.bind(this); this._dispatcherRef = null; + + // The room IDs we're waiting to come down the Room handler and when we + // started waiting for them. Used to track a room over an upgrade/autojoin. + this._waitingRoomQueue = [/* { roomId, addedTs } */]; } componentWillMount() { @@ -54,7 +61,7 @@ export default class RoomBreadcrumbs extends React.Component { MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted); - MatrixClientPeg.get().on("Room", this.onRoomMembershipChanged); + MatrixClientPeg.get().on("Room", this.onRoom); } componentWillUnmount() { @@ -68,7 +75,7 @@ export default class RoomBreadcrumbs extends React.Component { client.removeListener("Room.receipt", this.onRoomReceipt); client.removeListener("Room.timeline", this.onRoomTimeline); client.removeListener("Event.decrypted", this.onEventDecrypted); - client.removeListener("Room", this.onRoomMembershipChanged); + client.removeListener("Room", this.onRoom); } } @@ -87,6 +94,12 @@ export default class RoomBreadcrumbs extends React.Component { onAction(payload) { switch (payload.action) { case 'view_room': + if (payload.auto_join && !MatrixClientPeg.get().getRoom(payload.room_id)) { + // Queue the room instead of pushing it immediately - we're probably just waiting + // for a join to complete (ie: joining the upgraded room). + this._waitingRoomQueue.push({roomId: payload.room_id, addedTs: (new Date).getTime()}); + break; + } this._appendRoomId(payload.room_id); break; @@ -153,7 +166,20 @@ export default class RoomBreadcrumbs extends React.Component { if (!this.state.enabled && this._shouldEnable()) { this.setState({enabled: true}); } - } + }; + + onRoom = (room) => { + // Always check for membership changes when we see new rooms + this.onRoomMembershipChanged(); + + const waitingRoom = this._waitingRoomQueue.find(r => r.roomId === room.roomId); + if (!waitingRoom) return; + this._waitingRoomQueue.splice(this._waitingRoomQueue.indexOf(waitingRoom), 1); + + const now = (new Date()).getTime(); + if ((now - waitingRoom.addedTs) > AUTOJOIN_WAIT_THRESHOLD_MS) return; // Too long ago. + this._appendRoomId(room.roomId); // add the room we've been waiting for + }; _shouldEnable() { const client = MatrixClientPeg.get(); From 1ff39f252406a531a6e3f918e1a03c9001cb52be Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 25 Nov 2019 16:51:48 -0700 Subject: [PATCH 0805/2372] Make the communities button behave more like a toggle Fixes https://github.com/vector-im/riot-web/issues/10771 Clicking the button should toggle between your last page (room in our case) and the communities stuff. --- src/components/structures/MatrixChat.js | 16 ++++++++++++++++ src/components/views/elements/GroupsButton.js | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index f5b64fe2ed..eb0481e1cd 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -627,6 +627,22 @@ export default createReactClass({ case 'view_invite': showRoomInviteDialog(payload.roomId); break; + case 'view_last_screen': + // This function does what we want, despite the name. The idea is that it shows + // the last room we were looking at or some reasonable default/guess. We don't + // have to worry about email invites or similar being re-triggered because the + // function will have cleared that state and not execute that path. + this._showScreenAfterLogin(); + break; + case 'toggle_my_groups': + // We just dispatch the page change rather than have to worry about + // what the logic is for each of these branches. + if (this.state.page_type === PageTypes.MyGroups) { + dis.dispatch({action: 'view_last_screen'}); + } else { + dis.dispatch({action: 'view_my_groups'}); + } + break; case 'notifier_enabled': { this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()}); } diff --git a/src/components/views/elements/GroupsButton.js b/src/components/views/elements/GroupsButton.js index 3932c827c5..7b15e96424 100644 --- a/src/components/views/elements/GroupsButton.js +++ b/src/components/views/elements/GroupsButton.js @@ -22,7 +22,7 @@ import { _t } from '../../../languageHandler'; const GroupsButton = function(props) { const ActionButton = sdk.getComponent('elements.ActionButton'); return ( - Date: Tue, 26 Nov 2019 01:14:03 +0000 Subject: [PATCH 0806/2372] console.log doesn't take %s substitutions --- src/CallHandler.js | 4 ++-- src/Presence.js | 2 +- src/components/structures/MatrixChat.js | 2 +- src/components/structures/RoomView.js | 4 ++-- src/components/views/auth/CaptchaForm.js | 2 +- src/components/views/elements/AppTile.js | 2 +- src/components/views/messages/TextualBody.js | 4 ++-- src/components/views/rooms/EventTile.js | 2 +- src/components/views/rooms/MemberInfo.js | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index bcdf7853fd..427be14097 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -322,7 +322,7 @@ function _onAction(payload) { }); return; } else if (members.length === 2) { - console.log("Place %s call in %s", payload.type, payload.room_id); + console.info("Place %s call in %s", payload.type, payload.room_id); const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id); placeCall(call); } else { // > 2 @@ -337,7 +337,7 @@ function _onAction(payload) { } break; case 'place_conference_call': - console.log("Place conference call in %s", payload.room_id); + console.info("Place conference call in %s", payload.room_id); _startCallApp(payload.room_id, payload.type); break; case 'incoming_call': diff --git a/src/Presence.js b/src/Presence.js index ca3db9b762..8ef988f171 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -96,7 +96,7 @@ class Presence { try { await MatrixClientPeg.get().setPresence(this.state); - console.log("Presence: %s", newState); + console.info("Presence: %s", newState); } catch (err) { console.error("Failed to set presence: %s", err); this.state = oldState; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 661a0c7077..81098df319 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1315,7 +1315,7 @@ export default createReactClass({ if (state === "SYNCING" && prevState === "SYNCING") { return; } - console.log("MatrixClient sync state => %s", state); + console.info("MatrixClient sync state => %s", state); if (state !== "PREPARED") { return; } self.firstSyncComplete = true; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 6dee60bec7..db7ae33c8a 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -358,7 +358,7 @@ module.exports = createReactClass({ if (this.props.autoJoin) { this.onJoinButtonClicked(); } else if (!room && shouldPeek) { - console.log("Attempting to peek into room %s", roomId); + console.info("Attempting to peek into room %s", roomId); this.setState({ peekLoading: true, isPeeking: true, // this will change to false if peeking fails @@ -1897,7 +1897,7 @@ module.exports = createReactClass({ highlightedEventId = this.state.initialEventId; } - // console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); + // console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); const messagePanel = ( = %s", them.powerLevel, me.powerLevel); + //console.info("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); return can; } const editPowerLevel = ( From 4cec7c41b109714eae3f5b44535b0130ad2f0fd4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 26 Nov 2019 09:52:03 -0700 Subject: [PATCH 0807/2372] Fix override behaviour of system vs defined themes Fixes https://github.com/vector-im/riot-web/issues/11509 --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 2 +- src/theme.js | 23 ++++++++++++++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0a83237f45..9136f432dd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -364,7 +364,7 @@ "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", - "Match system dark mode setting": "Match system dark mode setting", + "Match system theme": "Match system theme", "Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls", "Send analytics data": "Send analytics data", "Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 5a3283c5f0..b02ab82400 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -284,7 +284,7 @@ export const SETTINGS = { "use_system_theme": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: true, - displayName: _td("Match system dark mode setting"), + displayName: _td("Match system theme"), }, "webRtcAllowPeerToPeer": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, diff --git a/src/theme.js b/src/theme.js index 9208ff2045..045e573361 100644 --- a/src/theme.js +++ b/src/theme.js @@ -20,7 +20,7 @@ import {_t} from "./languageHandler"; export const DEFAULT_THEME = "light"; import Tinter from "./Tinter"; import dis from "./dispatcher"; -import SettingsStore from "./settings/SettingsStore"; +import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; export class ThemeWatcher { static _instance = null; @@ -60,14 +60,14 @@ export class ThemeWatcher { _onChange = () => { this.recheck(); - } + }; _onAction = (payload) => { if (payload.action === 'recheck_theme') { // XXX forceTheme this.recheck(payload.forceTheme); } - } + }; // XXX: forceTheme param aded here as local echo appears to be unreliable // https://github.com/vector-im/riot-web/issues/11443 @@ -80,6 +80,23 @@ export class ThemeWatcher { } getEffectiveTheme() { + // If the user has specifically enabled the system matching option (excluding default), + // then use that over anything else. We pick the lowest possible level for the setting + // to ensure the ordering otherwise works. + const systemThemeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme", null, false, true); + if (systemThemeExplicit) { + if (this._preferDark.matches) return 'dark'; + if (this._preferLight.matches) return 'light'; + } + + // If the user has specifically enabled the theme (without the system matching option being + // enabled specifically and excluding the default), use that theme. We pick the lowest possible + // level for the setting to ensure the ordering otherwise works. + const themeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "theme", null, false, true); + if (themeExplicit) return themeExplicit; + + // If the user hasn't really made a preference in either direction, assume the defaults of the + // settings and use those. if (SettingsStore.getValue('use_system_theme')) { if (this._preferDark.matches) return 'dark'; if (this._preferLight.matches) return 'light'; From d50d8877e0d92a843ef0b2d54318438259b86f44 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 26 Nov 2019 09:56:04 -0700 Subject: [PATCH 0808/2372] Appease the linter --- src/theme.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/theme.js b/src/theme.js index 045e573361..3f50f8ba88 100644 --- a/src/theme.js +++ b/src/theme.js @@ -83,7 +83,8 @@ export class ThemeWatcher { // If the user has specifically enabled the system matching option (excluding default), // then use that over anything else. We pick the lowest possible level for the setting // to ensure the ordering otherwise works. - const systemThemeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme", null, false, true); + const systemThemeExplicit = SettingsStore.getValueAt( + SettingLevel.DEVICE, "use_system_theme", null, false, true); if (systemThemeExplicit) { if (this._preferDark.matches) return 'dark'; if (this._preferLight.matches) return 'light'; @@ -92,7 +93,8 @@ export class ThemeWatcher { // If the user has specifically enabled the theme (without the system matching option being // enabled specifically and excluding the default), use that theme. We pick the lowest possible // level for the setting to ensure the ordering otherwise works. - const themeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "theme", null, false, true); + const themeExplicit = SettingsStore.getValueAt( + SettingLevel.DEVICE, "theme", null, false, true); if (themeExplicit) return themeExplicit; // If the user hasn't really made a preference in either direction, assume the defaults of the From c40a3312646a6bfc6f911b713da9a846a9ecb8bf Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Mon, 25 Nov 2019 17:04:05 +0000 Subject: [PATCH 0809/2372] Translated using Weblate (Albanian) Currently translated at 99.6% (1914 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 4d0ad6582b..92dee371e1 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -2304,5 +2304,28 @@ "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Përdorimi i këtij widget-i mund të sjellë ndarje të dhënash me %(widgetDomain)s & Përgjegjësin tuaj të Integrimeve.", "Using this widget may share data with %(widgetDomain)s.": "Përdorimi i këtij widget-i mund të sjellë ndarje të dhënash me %(widgetDomain)s.", "Widget added by": "Widget i shtuar nga", - "This widget may use cookies.": "Ky widget mund të përdorë cookies." + "This widget may use cookies.": "Ky widget mund të përdorë cookies.", + "Send verification requests in direct message, including a new verification UX in the member panel.": "Dërgo kërkesa verifikimi në mesazhe të drejtpërdrejtë, përfshi një UX të ri verifikimesh te paneli i anëtarit.", + "Match system dark mode setting": "Përputhje me rregullimin e sistemit për mënyrë të errët", + "Decline (%(counter)s)": "Hidhe poshtë (%(counter)s)", + "Connecting to integration manager...": "Po lidhet me përgjegjës integrimesh…", + "Cannot connect to integration manager": "S’lidhet dot te përgjegjës integrimesh", + "The integration manager is offline or it cannot reach your homeserver.": "Përgjegjësi i integrimeve s’është në linjë ose s’kap dot shërbyesin tuaj Home.", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Përdorni një Përgjegjës Integrimesh (%(serverName)s) që të administroni robotë, widget-e dhe paketa ngjitësish.", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Përdorni një Përgjegjës Integrimesh që të administroni robotë, widget-e dhe paketa ngjitësish.", + "Manage integrations": "Administroni integrime", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Përgjegjësit e Integrimeve marrin të dhëna formësimi, dhe mund të ndryshojnë widget-e, të dërgojnë ftesa dhome, dhe të caktojnë shkallë pushteti në emër tuajin.", + "Failed to connect to integration manager": "S’u arrit të lidhet te përgjegjës integrimesh", + "Widgets do not use message encryption.": "Widget-et s’përdorin fshehtëzim mesazhesh.", + "More options": "Më tepër mundësi", + "Integrations are disabled": "Integrimet janë të çaktivizuara", + "Enable 'Manage Integrations' in Settings to do this.": "Që të bëhet kjo, aktivizoni “Administroni Integrime”, te Rregullimet.", + "Integrations not allowed": "Integrimet s’lejohen", + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Riot-i juah nuk ju lejon të përdorni një Përgjegjës Integrimesh për të bërë këtë. Ju lutemi, lidhuni me përgjegjësin.", + "Reload": "Ringarkoje", + "Take picture": "Bëni një foto", + "Remove for everyone": "Hiqe për këdo", + "Remove for me": "Hiqe për mua", + "Verification Request": "Kërkesë Verifikimi", + " (1/%(totalCount)s)": " (1/%(totalCount)s)" } From 5bfcf00f5b48a09193688c1581507c940e372cd2 Mon Sep 17 00:00:00 2001 From: tleydxdy Date: Tue, 26 Nov 2019 15:13:13 +0000 Subject: [PATCH 0810/2372] Translated using Weblate (Chinese (Simplified)) Currently translated at 77.5% (1488 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hans/ --- src/i18n/strings/zh_Hans.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 694306bf90..8ae217ea66 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -1785,5 +1785,13 @@ "Riot failed to get the public room list.": "Riot 无法获取公开聊天室列表。", "The homeserver may be unavailable or overloaded.": "主服务器似乎不可用或过载。", "You have %(count)s unread notifications in a prior version of this room.|other": "您在此聊天室的先前版本中有 %(count)s 条未读通知。", - "You have %(count)s unread notifications in a prior version of this room.|one": "您在此聊天室的先前版本中有 %(count)s 条未读通知。" + "You have %(count)s unread notifications in a prior version of this room.|one": "您在此聊天室的先前版本中有 %(count)s 条未读通知。", + "Add Email Address": "添加 Email 地址", + "Add Phone Number": "添加电话号码", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "是否使用“面包屑”功能(最近访问的房间的图标在房间列表上方显示)", + "Call failed due to misconfigured server": "因为服务器配置错误通话失败", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "请联系您主服务器(%(homeserverDomain)s)的管理员设置 TURN 服务器来确保通话运作稳定。", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "您也可以尝试使用turn.matrix.org公共服务器,但通话质量稍差,并且其将会得知您的 IP。您可以在设置中更改此选项。", + "Try using turn.matrix.org": "尝试使用 turn.matrix.org", + "Your Riot is misconfigured": "您的 Riot 配置有错误" } From 0e8a912dd1440919598f6aa9b756e8987766df58 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Tue, 26 Nov 2019 07:28:08 +0000 Subject: [PATCH 0811/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1921 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index a5f8e5e04b..d962f8a546 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2367,5 +2367,6 @@ "Decline (%(counter)s)": "拒絕 (%(counter)s)", "Manage integrations": "管理整合", "Verification Request": "驗證請求", - " (1/%(totalCount)s)": " (1/%(totalCount)s)" + " (1/%(totalCount)s)": " (1/%(totalCount)s)", + "Enable cross-signing to verify per-user instead of per-device (in development)": "啟用交叉簽章以驗證每個使者而非每個裝置(開發中)" } From a36de8ed9264a8bb18b02dc5f20d7d88184d32d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Tue, 26 Nov 2019 08:02:45 +0000 Subject: [PATCH 0812/2372] Translated using Weblate (French) Currently translated at 100.0% (1921 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 097dd0824f..397b7c6f14 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2374,5 +2374,6 @@ "Decline (%(counter)s)": "Refuser (%(counter)s)", "Manage integrations": "Gérer les intégrations", "Verification Request": "Demande de vérification", - " (1/%(totalCount)s)": " (1/%(totalCount)s)" + " (1/%(totalCount)s)": " (1/%(totalCount)s)", + "Enable cross-signing to verify per-user instead of per-device (in development)": "Activer la signature croisée pour vérifier par utilisateur plutôt que par appareil (en développement)" } From daccdbc0a959cd79e81277be23ab19d94271cb3e Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 26 Nov 2019 12:54:18 +0000 Subject: [PATCH 0813/2372] Translated using Weblate (Hungarian) Currently translated at 100.0% (1921 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 9e41b7381e..17c0abde87 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2362,5 +2362,6 @@ "Decline (%(counter)s)": "Elutasítás (%(counter)s)", "Manage integrations": "Integrációk kezelése", "Verification Request": "Ellenőrzési kérés", - " (1/%(totalCount)s)": " (1/%(totalCount)s)" + " (1/%(totalCount)s)": " (1/%(totalCount)s)", + "Enable cross-signing to verify per-user instead of per-device (in development)": "Kereszt-aláírás engedélyezése a felhasználó alapú azonosításhoz az eszköz alapú helyett (fejlesztés alatt)" } From 6d0cd2aa9c30c938c5a75f510e3ec038bb36d897 Mon Sep 17 00:00:00 2001 From: take100yen Date: Tue, 26 Nov 2019 11:03:44 +0000 Subject: [PATCH 0814/2372] Translated using Weblate (Japanese) Currently translated at 62.0% (1191 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ja/ --- src/i18n/strings/ja.json | 46 +++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 54e2c29a21..8a6d1c7e24 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -56,9 +56,9 @@ "Operation failed": "操作に失敗しました", "Custom Server Options": "カスタムサーバのオプション", "Dismiss": "やめる", - "powered by Matrix": "Matrixによって動作しています", + "powered by Matrix": "powered by Matrix", "Error": "エラー", - "Remove": "取り除く", + "Remove": "削除", "Submit debug logs": "デバッグログを送信する", "Edit": "編集", "Continue": "続ける", @@ -254,7 +254,7 @@ "You are already in a call.": "すでに通話中です。", "VoIP is unsupported": "VoIPはサポートされていません", "You cannot place VoIP calls in this browser.": "このブラウザにはVoIP通話はできません。", - "You cannot place a call with yourself.": "自分で電話をかけることはできません。", + "You cannot place a call with yourself.": "自分自身に電話をかけることはできません。", "Could not connect to the integration server": "統合サーバーに接続できません", "A conference call could not be started because the intgrations server is not available": "統合サーバーが使用できないため、会議通話を開始できませんでした", "Call in Progress": "発信中", @@ -345,7 +345,7 @@ "Sets the room topic": "部屋のトピックを設定", "Invites user with given id to current room": "指定されたIDを持つユーザーを現在のルームに招待する", "Joins room with given alias": "指定された別名で部屋に参加する", - "Leave room": "部屋を出る", + "Leave room": "部屋を退出", "Unrecognised room alias:": "認識されない部屋の別名:", "Kicks user with given id": "与えられたIDを持つユーザーを追放する", "Bans user with given id": "指定されたIDでユーザーをブロックする", @@ -714,7 +714,7 @@ "'%(alias)s' is not a valid format for an alias": "'%(alias)s' はエイリアスの有効な形式ではありません", "Invalid address format": "無効なアドレス形式", "'%(alias)s' is not a valid format for an address": "'%(alias)s' は有効なアドレス形式ではありません", - "not specified": "指定されていない", + "not specified": "指定なし", "not set": "設定されていない", "Remote addresses for this room:": "この部屋のリモートアドレス:", "Addresses": "アドレス", @@ -780,7 +780,7 @@ "Mobile phone number": "携帯電話番号", "Forgot your password?": "パスワードを忘れましたか?", "%(serverName)s Matrix ID": "%(serverName)s Matrix ID", - "Sign in with": "にログインする", + "Sign in with": "ログインに使用するユーザー情報", "Email address": "メールアドレス", "Sign in": "サインイン", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "メールアドレスを指定しないと、パスワードをリセットできません。間違いありませんか?", @@ -1302,7 +1302,7 @@ "Whether or not you're logged in (we don't record your username)": "ログインしているか否か(私たちはあなたのユーザー名を記録しません)", "Upgrade": "アップグレード", "Sets the room name": "部屋名を設定する", - "Change room name": "部屋名を変える", + "Change room name": "部屋名の変更", "Room Name": "部屋名", "Add Email Address": "メールアドレスの追加", "Add Phone Number": "電話番号の追加", @@ -1422,5 +1422,35 @@ "Filter": "フィルター", "Find a room… (e.g. %(exampleRoom)s)": "部屋を探す… (例: %(exampleRoom)s)", "If you can't find the room you're looking for, ask for an invite or Create a new room.": "もしお探しの部屋が見つからない場合、招待してもらうか部屋を作成しましょう。", - "Enable room encryption": "部屋の暗号化を有効にする" + "Enable room encryption": "部屋の暗号化を有効化", + "Change": "変更", + "Change room avatar": "部屋アバターの変更", + "Change main address for the room": "部屋のメインアドレスの変更", + "Change history visibility": "履歴を表示できるかの変更", + "Change permissions": "権限の変更", + "Change topic": "トピックの変更", + "Upgrade the room": "部屋のアップグレード", + "Modify widgets": "ウィジェットの変更", + "Error changing power level requirement": "必要な権限レベルの変更中にエラーが発生しました", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "部屋で必要な権限レベルの変更中にエラーが発生しました。あなたが十分な権限を確認したうえで再度お試しください。", + "Default role": "既定の役割", + "Send messages": "メッセージの送信", + "Invite users": "ユーザーの招待", + "Change settings": "設定の変更", + "Kick users": "ユーザーの追放", + "Ban users": "ユーザーのブロック", + "Remove messages": "メッセージの削除", + "Notify everyone": "全員に通知", + "Select the roles required to change various parts of the room": "部屋の様々な部分の変更に必要な役割を選択", + "Upload room avatar": "部屋アバターをアップロード", + "Room Topic": "部屋のトピック", + "reacted with %(shortName)s": "%(shortName)s とリアクションしました", + "Next": "次へ", + "The username field must not be blank.": "ユーザー名フィールドは空白であってはいけません。", + "Username": "ユーザー名", + "Not sure of your password?
        Set a new one": "パスワードに覚えがありませんか? 新しいものを設定しましょう", + "Other servers": "他のサーバー", + "Sign in to your Matrix account on %(serverName)s": "%(serverName)s上のMatrixアカウントにサインインします", + "Sign in to your Matrix account on ": "上のMatrixアカウントにサインインします", + "Create account": "アカウントを作成" } From 3679dd73d4477251903e6b440f14d625253b7078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=ED=83=9C=EC=84=AD?= Date: Tue, 26 Nov 2019 03:07:47 +0000 Subject: [PATCH 0815/2372] Translated using Weblate (Korean) Currently translated at 99.1% (1904 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 757edbfa4b..da89c70421 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -2195,5 +2195,12 @@ "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "이 위젯을 사용하면 %(widgetDomain)s & 통합 관리자와 데이터를 공유합니다.", "Using this widget may share data with %(widgetDomain)s.": "이 위젯을 사용하면 %(widgetDomain)s와(과) 데이터를 공유합니다.", "Widget added by": "위젯을 추가했습니다", - "This widget may use cookies.": "이 위젯은 쿠키를 사용합니다." + "This widget may use cookies.": "이 위젯은 쿠키를 사용합니다.", + "Enable cross-signing to verify per-user instead of per-device (in development)": "기기 당 확인이 아닌 사용자 당 확인을 위한 교차 서명 켜기 (개발 중)", + "Enable local event indexing and E2EE search (requires restart)": "로컬 이벤트 인덱싱 및 종단간 암호화 켜기 (다시 시작해야 합니다)", + "Match system dark mode setting": "시스템의 다크 모드 설정에 맞춤", + "Decline (%(counter)s)": "거절 (%(counter)s)", + "Connecting to integration manager...": "통합 관리자로 연결 중...", + "Cannot connect to integration manager": "통합 관리자에 연결할 수 없음", + "The integration manager is offline or it cannot reach your homeserver.": "통합 관리자가 오프라인이거나 당신의 홈서버에서 접근할 수 없습니다." } From 088fb35a2fc39ab9a5fde57219d8c635158ebba6 Mon Sep 17 00:00:00 2001 From: fenuks Date: Mon, 25 Nov 2019 22:59:10 +0000 Subject: [PATCH 0816/2372] Translated using Weblate (Polish) Currently translated at 75.8% (1456 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pl/ --- src/i18n/strings/pl.json | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index a0ce517404..624b5fcc77 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -1622,7 +1622,7 @@ "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Używasz , aby odnajdywać i móc być odnajdywanym przez istniejące kontakty, które znasz. Możesz zmienić serwer tożsamości poniżej.", "Identity Server": "Serwer Tożsamości", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Nie używasz serwera tożsamości. Aby odkrywać i być odkrywanym przez istniejące kontakty które znasz, dodaj jeden poniżej.", - "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Odłączenie się od serwera tożsamości oznacza, że nie będzie możliwości wykrycia przez innych użytkowników oraz nie będzie możliwości zaproszenia innych e-mailem lub za pomocą telefonu.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Odłączenie się od serwera tożsamości oznacza, że inni nie będą mogli Cię odnaleźć ani Ty nie będziesz w stanie zaprosić nikogo za pomocą e-maila czy telefonu.", "Enter a new identity server": "Wprowadź nowy serwer tożsamości", "Change": "Zmień", "Failed to update integration manager": "Błąd przy aktualizacji menedżera integracji", @@ -1732,5 +1732,21 @@ "%(creator)s created and configured the room.": "%(creator)s stworzył(a) i skonfigurował(a) pokój.", "Preview": "Przejrzyj", "View": "Wyświetl", - "Missing media permissions, click the button below to request.": "Brakuje uprawnień do mediów, kliknij przycisk poniżej, aby o nie zapytać." + "Missing media permissions, click the button below to request.": "Brakuje uprawnień do mediów, kliknij przycisk poniżej, aby o nie zapytać.", + "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)snie wykonało zmian %(count)s razy", + "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)snie wykonało zmian", + "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)snie wykonał(a) zmian %(count)s razy", + "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)snie wykonał(a) zmian", + "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Nie zdołano wczytać zdarzenia, na które odpowiedziano, może ono nie istnieć lub nie masz uprawnienia, by je zobaczyć.", + "Room alias": "Nazwa zastępcza pokoju", + "e.g. my-room": "np. mój-pokój", + "Some characters not allowed": "Niektóre znaki niedozwolone", + "Please provide a room alias": "Proszę podać nazwę zastępczą pokoju", + "This alias is available to use": "Ta nazwa zastępcza jest dostępna do użycia", + "This alias is already in use": "Ta nazwa zastępcza jest już w użyciu", + "Report bugs & give feedback": "Zgłoś błędy & sugestie", + "Filter rooms…": "Filtruj pokoje…", + "Find a room…": "Znajdź pokój…", + "Find a room… (e.g. %(exampleRoom)s)": "Znajdź pokój… (np. %(exampleRoom)s)", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Jeżeli nie możesz znaleźć szukanego pokoju, poproś o zaproszenie albo Stwórz nowy pokój." } From ff2ac63530646ca1d3c22b322153589fff9fb59a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 26 Nov 2019 09:52:03 -0700 Subject: [PATCH 0817/2372] Fix override behaviour of system vs defined themes Fixes https://github.com/vector-im/riot-web/issues/11509 --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 2 +- src/theme.js | 23 ++++++++++++++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7709a4a398..f31f086d26 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -364,7 +364,7 @@ "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", - "Match system dark mode setting": "Match system dark mode setting", + "Match system theme": "Match system theme", "Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls", "Send analytics data": "Send analytics data", "Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 89693f7c50..54b8715b6e 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -284,7 +284,7 @@ export const SETTINGS = { "use_system_theme": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: true, - displayName: _td("Match system dark mode setting"), + displayName: _td("Match system theme"), }, "webRtcAllowPeerToPeer": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, diff --git a/src/theme.js b/src/theme.js index e89af55924..2973d2d3fd 100644 --- a/src/theme.js +++ b/src/theme.js @@ -20,7 +20,7 @@ import {_t} from "./languageHandler"; export const DEFAULT_THEME = "light"; import Tinter from "./Tinter"; import dis from "./dispatcher"; -import SettingsStore from "./settings/SettingsStore"; +import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; export class ThemeWatcher { static _instance = null; @@ -60,14 +60,14 @@ export class ThemeWatcher { _onChange = () => { this.recheck(); - } + }; _onAction = (payload) => { if (payload.action === 'recheck_theme') { // XXX forceTheme this.recheck(payload.forceTheme); } - } + }; // XXX: forceTheme param aded here as local echo appears to be unreliable // https://github.com/vector-im/riot-web/issues/11443 @@ -80,6 +80,23 @@ export class ThemeWatcher { } getEffectiveTheme() { + // If the user has specifically enabled the system matching option (excluding default), + // then use that over anything else. We pick the lowest possible level for the setting + // to ensure the ordering otherwise works. + const systemThemeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme", null, false, true); + if (systemThemeExplicit) { + if (this._preferDark.matches) return 'dark'; + if (this._preferLight.matches) return 'light'; + } + + // If the user has specifically enabled the theme (without the system matching option being + // enabled specifically and excluding the default), use that theme. We pick the lowest possible + // level for the setting to ensure the ordering otherwise works. + const themeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "theme", null, false, true); + if (themeExplicit) return themeExplicit; + + // If the user hasn't really made a preference in either direction, assume the defaults of the + // settings and use those. if (SettingsStore.getValue('use_system_theme')) { if (this._preferDark.matches) return 'dark'; if (this._preferLight.matches) return 'light'; From 810fff64bc65ecfe146442c050f7c514bd20d563 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 26 Nov 2019 09:56:04 -0700 Subject: [PATCH 0818/2372] Appease the linter --- src/theme.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/theme.js b/src/theme.js index 2973d2d3fd..77cf6e9593 100644 --- a/src/theme.js +++ b/src/theme.js @@ -83,7 +83,8 @@ export class ThemeWatcher { // If the user has specifically enabled the system matching option (excluding default), // then use that over anything else. We pick the lowest possible level for the setting // to ensure the ordering otherwise works. - const systemThemeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme", null, false, true); + const systemThemeExplicit = SettingsStore.getValueAt( + SettingLevel.DEVICE, "use_system_theme", null, false, true); if (systemThemeExplicit) { if (this._preferDark.matches) return 'dark'; if (this._preferLight.matches) return 'light'; @@ -92,7 +93,8 @@ export class ThemeWatcher { // If the user has specifically enabled the theme (without the system matching option being // enabled specifically and excluding the default), use that theme. We pick the lowest possible // level for the setting to ensure the ordering otherwise works. - const themeExplicit = SettingsStore.getValueAt(SettingLevel.DEVICE, "theme", null, false, true); + const themeExplicit = SettingsStore.getValueAt( + SettingLevel.DEVICE, "theme", null, false, true); if (themeExplicit) return themeExplicit; // If the user hasn't really made a preference in either direction, assume the defaults of the From a2e3f6496312d9def35d529e7ede9b5bfcc7622f Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 26 Nov 2019 19:06:02 +0000 Subject: [PATCH 0819/2372] Change read markers to use CSS transitions Removes one of the two places we use Velocity, so we're one step closer to getting rid of it for good. Should therefore fix the fact that Velocity is leaking data entries and therefore
        elements. Hopefully also makes the logic in getEventTiles incrementally simpler, if still somwewhat byzantine. --- res/css/structures/_RoomView.scss | 3 + src/components/structures/MessagePanel.js | 228 ++++++++---------- .../structures/MessagePanel-test.js | 118 +++++---- 3 files changed, 176 insertions(+), 173 deletions(-) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index 50d412ad58..5e826306c6 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -221,6 +221,9 @@ hr.mx_RoomView_myReadMarker { position: relative; top: -1px; z-index: 1; + transition: width 400ms easeInSine 1s, opacity 400ms easeInSine 1s; + width: 99%; + opacity: 1; } .mx_RoomView_callStatusBar .mx_UploadBar_uploadProgressInner { diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 912b865b9f..d1cc1b7caf 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -16,8 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/* global Velocity */ - import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; @@ -111,14 +109,12 @@ export default class MessagePanel extends React.Component { constructor() { super(); - // the event after which we put a visible unread marker on the last - // render cycle; null if readMarkerVisible was false or the RM was - // suppressed (eg because it was at the end of the timeline) - this.currentReadMarkerEventId = null; - // the event after which we are showing a disappearing read marker - // animation - this.currentGhostEventId = null; + this.state = { + // previous positions the read marker has been in, so we can + // display 'ghost' read markers that are animating away + ghostReadMarkers: [], + }; // opaque readreceipt info for each userId; used by ReadReceiptMarker // to manage its animations @@ -157,10 +153,6 @@ export default class MessagePanel extends React.Component { // displayed event in the current render cycle. this._readReceiptsByUserId = {}; - // Remember the read marker ghost node so we can do the cleanup that - // Velocity requires - this._readMarkerGhostNode = null; - // Cache hidden events setting on mount since Settings is expensive to // query, and we check this in a hot code path. this._showHiddenEventsInTimeline = @@ -177,6 +169,16 @@ export default class MessagePanel extends React.Component { this._isMounted = false; } + componentDidUpdate(prevProps, prevState) { + if (prevProps.readMarkerVisible && this.props.readMarkerEventId !== prevProps.readMarkerEventId) { + const ghostReadMarkers = this.state.ghostReadMarkers; + ghostReadMarkers.push(prevProps.readMarkerEventId); + this.setState({ + ghostReadMarkers, + }); + } + } + /* get the DOM node representing the given event */ getNodeForEventId(eventId) { if (!this.eventNodes) { @@ -325,6 +327,78 @@ export default class MessagePanel extends React.Component { return !shouldHideEvent(mxEv); } + _readMarkerForEvent(eventId, isLastEvent) { + const visible = !isLastEvent && this.props.readMarkerVisible; + + if (this.props.readMarkerEventId === eventId) { + let hr; + // if the read marker comes at the end of the timeline (except + // for local echoes, which are excluded from RMs, because they + // don't have useful event ids), we don't want to show it, but + // we still want to create the
      • for it so that the + // algorithms which depend on its position on the screen aren't + // confused. + if (visible) { + hr =
        ; + } + + return ( +
      • + { hr } +
      • + ); + } else if (this.state.ghostReadMarkers.includes(eventId)) { + // We render 'ghost' read markers in the DOM while they + // transition away. This allows the actual read marker + // to be in the right place straight away without having + // to wait for the transition to finish. + // There are probably much simpler ways to do this transition, + // possibly using react-transition-group which handles keeping + // elements in the DOM whilst they transition out, although our + // case is a little more complex because only some of the items + // transition (ie. the read markers do but the event tiles do not) + // and TransitionGroup requires that all its children are Transitions. + const hr =
        ; + + // give it a key which depends on the event id. That will ensure that + // we get a new DOM node (restarting the animation) when the ghost + // moves to a different event. + return ( +
      • + { hr } +
      • + ); + } + + return null; + } + + _collectGhostReadMarker = (node) => { + if (node) { + // now the element has appeared, change the style which will trigger the CSS transition + requestAnimationFrame(() => { + node.style.width = '10%'; + node.style.opacity = '0'; + }); + } + }; + + _onGhostTransitionEnd = (ev) => { + // we can now clean up the ghost element + const finishedEventId = ev.target.dataset.eventid; + this.setState({ + ghostReadMarkers: this.state.ghostReadMarkers.filter(eid => eid !== finishedEventId), + }); + }; + _getEventTiles() { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); @@ -332,7 +406,6 @@ export default class MessagePanel extends React.Component { this.eventNodes = {}; - let visible = false; let i; // first figure out which is the last event in the list which we're @@ -367,16 +440,6 @@ export default class MessagePanel extends React.Component { let prevEvent = null; // the last event we showed - // assume there is no read marker until proven otherwise - let readMarkerVisible = false; - - // if the readmarker has moved, cancel any active ghost. - if (this.currentReadMarkerEventId && this.props.readMarkerEventId && - this.props.readMarkerVisible && - this.currentReadMarkerEventId !== this.props.readMarkerEventId) { - this.currentGhostEventId = null; - } - this._readReceiptsByEvent = {}; if (this.props.showReadReceipts) { this._readReceiptsByEvent = this._getReadReceiptsByShownEvent(); @@ -401,7 +464,7 @@ export default class MessagePanel extends React.Component { return false; }; if (mxEv.getType() === "m.room.create") { - let readMarkerInSummary = false; + let summaryReadMarker = null; const ts1 = mxEv.getTs(); if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) { @@ -410,9 +473,7 @@ export default class MessagePanel extends React.Component { } // If RM event is the first in the summary, append the RM after the summary - if (mxEv.getId() === this.props.readMarkerEventId) { - readMarkerInSummary = true; - } + summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId()); // If this m.room.create event should be shown (room upgrade) then show it before the summary if (this._shouldShowEvent(mxEv)) { @@ -427,9 +488,7 @@ export default class MessagePanel extends React.Component { // Ignore redacted/hidden member events if (!this._shouldShowEvent(collapsedMxEv)) { // If this hidden event is the RM and in or at end of a summary put RM after the summary. - if (collapsedMxEv.getId() === this.props.readMarkerEventId) { - readMarkerInSummary = true; - } + summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId()); continue; } @@ -438,9 +497,7 @@ export default class MessagePanel extends React.Component { } // If RM event is in the summary, mark it as such and the RM will be appended after the summary. - if (collapsedMxEv.getId() === this.props.readMarkerEventId) { - readMarkerInSummary = true; - } + summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId()); summarisedEvents.push(collapsedMxEv); } @@ -468,8 +525,8 @@ export default class MessagePanel extends React.Component { { eventTiles } ); - if (readMarkerInSummary) { - ret.push(this._getReadMarkerTile(visible)); + if (summaryReadMarker) { + ret.push(summaryReadMarker); } prevEvent = mxEv; @@ -480,7 +537,7 @@ export default class MessagePanel extends React.Component { // Wrap consecutive member events in a ListSummary, ignore if redacted if (isMembershipChange(mxEv) && wantTile) { - let readMarkerInMels = false; + let summaryReadMarker = null; const ts1 = mxEv.getTs(); // Ensure that the key of the MemberEventListSummary does not change with new // member events. This will prevent it from being re-created unnecessarily, and @@ -498,9 +555,7 @@ export default class MessagePanel extends React.Component { } // If RM event is the first in the MELS, append the RM after MELS - if (mxEv.getId() === this.props.readMarkerEventId) { - readMarkerInMels = true; - } + summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId()); const summarisedEvents = [mxEv]; for (;i + 1 < this.props.events.length; i++) { @@ -509,9 +564,7 @@ export default class MessagePanel extends React.Component { // Ignore redacted/hidden member events if (!this._shouldShowEvent(collapsedMxEv)) { // If this hidden event is the RM and in or at end of a MELS put RM after MELS. - if (collapsedMxEv.getId() === this.props.readMarkerEventId) { - readMarkerInMels = true; - } + summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId()); continue; } @@ -521,9 +574,7 @@ export default class MessagePanel extends React.Component { } // If RM event is in MELS mark it as such and the RM will be appended after MELS. - if (collapsedMxEv.getId() === this.props.readMarkerEventId) { - readMarkerInMels = true; - } + summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId()); summarisedEvents.push(collapsedMxEv); } @@ -554,8 +605,8 @@ export default class MessagePanel extends React.Component { { eventTiles } ); - if (readMarkerInMels) { - ret.push(this._getReadMarkerTile(visible)); + if (summaryReadMarker) { + ret.push(summaryReadMarker); } prevEvent = mxEv; @@ -570,40 +621,10 @@ export default class MessagePanel extends React.Component { prevEvent = mxEv; } - let isVisibleReadMarker = false; - - if (eventId === this.props.readMarkerEventId) { - visible = this.props.readMarkerVisible; - - // if the read marker comes at the end of the timeline (except - // for local echoes, which are excluded from RMs, because they - // don't have useful event ids), we don't want to show it, but - // we still want to create the
      • for it so that the - // algorithms which depend on its position on the screen aren't - // confused. - if (i >= lastShownNonLocalEchoIndex) { - visible = false; - } - ret.push(this._getReadMarkerTile(visible)); - readMarkerVisible = visible; - isVisibleReadMarker = visible; - } - - // XXX: there should be no need for a ghost tile - we should just use a - // a dispatch (user_activity_end) to start the RM animation. - if (eventId === this.currentGhostEventId) { - // if we're showing an animation, continue to show it. - ret.push(this._getReadMarkerGhostTile()); - } else if (!isVisibleReadMarker && - eventId === this.currentReadMarkerEventId) { - // there is currently a read-up-to marker at this point, but no - // more. Show an animation of it disappearing. - ret.push(this._getReadMarkerGhostTile()); - this.currentGhostEventId = eventId; - } + const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex); + if (readMarker) ret.push(readMarker); } - this.currentReadMarkerEventId = readMarkerVisible ? this.props.readMarkerEventId : null; return ret; } @@ -797,53 +818,6 @@ export default class MessagePanel extends React.Component { return receiptsByEvent; } - _getReadMarkerTile(visible) { - let hr; - if (visible) { - hr =
        ; - } - - return ( -
      • - { hr } -
      • - ); - } - - _startAnimation = (ghostNode) => { - if (this._readMarkerGhostNode) { - Velocity.Utilities.removeData(this._readMarkerGhostNode); - } - this._readMarkerGhostNode = ghostNode; - - if (ghostNode) { - // eslint-disable-next-line new-cap - Velocity(ghostNode, {opacity: '0', width: '10%'}, - {duration: 400, easing: 'easeInSine', - delay: 1000}); - } - }; - - _getReadMarkerGhostTile() { - const hr =
        ; - - // give it a key which depends on the event id. That will ensure that - // we get a new DOM node (restarting the animation) when the ghost - // moves to a different event. - return ( -
      • - { hr } -
      • - ); - } - _collectEventNode = (eventId, node) => { this.eventNodes[eventId] = node; } diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index f58f1b925c..7c52512bc2 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -81,6 +82,7 @@ describe('MessagePanel', function() { // HACK: We assume all settings want to be disabled SettingsStore.getValue = sinon.stub().returns(false); + SettingsStore.getValue.withArgs('showDisplaynameChanges').returns(true); // This option clobbers the duration of all animations to be 1ms // which makes unit testing a lot simpler (the animation doesn't @@ -109,6 +111,44 @@ describe('MessagePanel', function() { return events; } + + // make a collection of events with some member events that should be collapsed + // with a MemberEventListSummary + function mkMelsEvents() { + const events = []; + const ts0 = Date.now(); + + let i = 0; + events.push(test_utils.mkMessage({ + event: true, room: "!room:id", user: "@user:id", + ts: ts0 + ++i*1000, + })); + + for (i = 0; i < 10; i++) { + events.push(test_utils.mkMembership({ + event: true, room: "!room:id", user: "@user:id", + target: { + userId: "@user:id", + name: "Bob", + getAvatarUrl: () => { + return "avatar.jpeg"; + }, + }, + ts: ts0 + i*1000, + mship: 'join', + prevMship: 'join', + name: 'A user', + })); + } + + events.push(test_utils.mkMessage({ + event: true, room: "!room:id", user: "@user:id", + ts: ts0 + ++i*1000, + })); + + return events; + } + it('should show the events', function() { const res = TestUtils.renderIntoDocument( , @@ -120,6 +160,23 @@ describe('MessagePanel', function() { expect(tiles.length).toEqual(10); }); + it('should collapse adjacent member events', function() { + const res = TestUtils.renderIntoDocument( + , + ); + + // just check we have the right number of tiles for now + const tiles = TestUtils.scryRenderedComponentsWithType( + res, sdk.getComponent('rooms.EventTile'), + ); + expect(tiles.length).toEqual(2); + + const summaryTiles = TestUtils.scryRenderedComponentsWithType( + res, sdk.getComponent('elements.MemberEventListSummary'), + ); + expect(summaryTiles.length).toEqual(1); + }); + it('should show the read-marker in the right place', function() { const res = TestUtils.renderIntoDocument( , + ); + + const summary = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_EventListSummary'); + + // find the
      • which wraps the read marker + const rm = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_RoomView_myReadMarker_container'); + + expect(rm.previousSibling).toEqual(summary); + }); + it('shows a ghost read-marker when the read-marker moves', function(done) { // fake the clock so that we can test the velocity animation. clock.install(); @@ -191,50 +263,4 @@ describe('MessagePanel', function() { }, 100); }, 100); }); - - it('shows only one ghost when the RM moves twice', function() { - const parentDiv = document.createElement('div'); - - // first render with the RM in one place - let mp = ReactDOM.render( - , parentDiv); - - const tiles = TestUtils.scryRenderedComponentsWithType( - mp, sdk.getComponent('rooms.EventTile')); - const tileContainers = tiles.map(function(t) { - return ReactDOM.findDOMNode(t).parentNode; - }); - - // now move the RM - mp = ReactDOM.render( - , parentDiv); - - // now there should be two RM containers - let found = TestUtils.scryRenderedDOMComponentsWithClass(mp, 'mx_RoomView_myReadMarker_container'); - expect(found.length).toEqual(2); - - // the first should be the ghost - expect(tileContainers.indexOf(found[0].previousSibling)).toEqual(4); - - // the second should be the real RM - expect(tileContainers.indexOf(found[1].previousSibling)).toEqual(6); - - // and move the RM again - mp = ReactDOM.render( - , parentDiv); - - // still two RM containers - found = TestUtils.scryRenderedDOMComponentsWithClass(mp, 'mx_RoomView_myReadMarker_container'); - expect(found.length).toEqual(2); - - // they should have moved - expect(tileContainers.indexOf(found[0].previousSibling)).toEqual(6); - expect(tileContainers.indexOf(found[1].previousSibling)).toEqual(8); - }); }); From c2c8b1b6e0d8f0e07e502e8a58f877427603c89b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 25 Nov 2019 14:03:40 +0000 Subject: [PATCH 0820/2372] Clarify that cross-signing is in development In an attempt to clarify the state of this highly anticipated feature, this updates the labs flag name to match. Part of https://github.com/vector-im/riot-web/issues/11492 --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f31f086d26..ad877f11e7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -342,7 +342,7 @@ "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Send verification requests in direct message, including a new verification UX in the member panel.": "Send verification requests in direct message, including a new verification UX in the member panel.", - "Enable cross-signing to verify per-user instead of per-device": "Enable cross-signing to verify per-user instead of per-device", + "Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)", "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 54b8715b6e..b02ab82400 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -143,7 +143,7 @@ export const SETTINGS = { }, "feature_cross_signing": { isFeature: true, - displayName: _td("Enable cross-signing to verify per-user instead of per-device"), + displayName: _td("Enable cross-signing to verify per-user instead of per-device (in development)"), supportedLevels: LEVELS_FEATURE, default: false, controller: new ReloadOnChangeController(), From 7905e6cc3b9933153e5dc73c2d1b9d24c51308a3 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Tue, 26 Nov 2019 14:31:11 -0600 Subject: [PATCH 0821/2372] Add a link to the labs feature documentation Signed-off-by: Aaron Raimist --- .../views/settings/tabs/user/LabsUserSettingsTab.js | 9 +++++++++ src/i18n/strings/en_EN.json | 1 + 2 files changed, 10 insertions(+) diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index 07a2bf722a..71a3e5f1d2 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -49,6 +49,15 @@ export default class LabsUserSettingsTab extends React.Component { return (
        {_t("Labs")}
        +
        + { + _t('These are experimental features. For more information on what ' + + 'these options do see the documentation.', {}, { + 'a': (sub) => {sub}, + }) + } +
        {flags} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9136f432dd..9522655698 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -641,6 +641,7 @@ "Access Token:": "Access Token:", "click to reveal": "click to reveal", "Labs": "Labs", + "These are experimental features. For more information on what these options do see the documentation.": "These are experimental features. For more information on what these options do see the documentation.", "Ignored/Blocked": "Ignored/Blocked", "Error adding ignored user/server": "Error adding ignored user/server", "Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.", From 98c47265c9d7c9f16a27946fee8dab946158fce4 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Tue, 26 Nov 2019 15:00:56 -0600 Subject: [PATCH 0822/2372] Fix indentation Signed-off-by: Aaron Raimist --- .../views/settings/tabs/user/LabsUserSettingsTab.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index 71a3e5f1d2..4232cc90fb 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -53,8 +53,10 @@ export default class LabsUserSettingsTab extends React.Component { { _t('These are experimental features. For more information on what ' + 'these options do see the documentation.', {}, { - 'a': (sub) => {sub}, + 'a': (sub) => { + return {sub}; + }, }) }
        From bb7cc20b1a6cf181ea209fd382dd5a62a506a89c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 27 Nov 2019 00:45:46 +0000 Subject: [PATCH 0823/2372] fix font smoothing to match figma as per https://github.com/vector-im/riot-web/issues/11425 with apologies to https://usabilitypost.com/2012/11/05/stop-fixing-font-smoothing/ :/ --- res/css/_common.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/res/css/_common.scss b/res/css/_common.scss index 5987275f7f..51d985efb7 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -30,6 +30,11 @@ body { color: $primary-fg-color; border: 0px; margin: 0px; + + // needed to match the designs correctly on macOS + // see https://github.com/vector-im/riot-web/issues/11425 + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } pre, code { From c3dd491eacc2b24ddfd7e26131e6222dfecfc220 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 27 Nov 2019 06:41:15 +0000 Subject: [PATCH 0824/2372] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1921 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index d962f8a546..52ca7879d4 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2368,5 +2368,6 @@ "Manage integrations": "管理整合", "Verification Request": "驗證請求", " (1/%(totalCount)s)": " (1/%(totalCount)s)", - "Enable cross-signing to verify per-user instead of per-device (in development)": "啟用交叉簽章以驗證每個使者而非每個裝置(開發中)" + "Enable cross-signing to verify per-user instead of per-device (in development)": "啟用交叉簽章以驗證每個使者而非每個裝置(開發中)", + "Match system theme": "符合系統佈景主題" } From b54ec3d708ed0bd03ddc270cf7b16f955a156ff6 Mon Sep 17 00:00:00 2001 From: Samu Voutilainen Date: Wed, 27 Nov 2019 06:28:33 +0000 Subject: [PATCH 0825/2372] Translated using Weblate (Finnish) Currently translated at 97.5% (1873 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fi/ --- src/i18n/strings/fi.json | 49 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 81a8563e5b..fbd5125bf9 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -830,11 +830,11 @@ "%(duration)sd": "%(duration)s pv", "Online for %(duration)s": "Läsnä %(duration)s", "Idle for %(duration)s": "Toimettomana %(duration)s", - "Offline for %(duration)s": "Poissa %(duration)s", + "Offline for %(duration)s": "Poissa verkosta %(duration)s", "Unknown for %(duration)s": "Tuntematon %(duration)s", "Online": "Online", "Idle": "Toimeton", - "Offline": "Offline", + "Offline": "Poissa verkosta", "Unknown": "Tuntematon", "To change the room's avatar, you must be a": "Vaihtaaksesi huoneen kuvan, sinun tulee olla", "To change the room's name, you must be a": "Vaihtaaksesi huoneen nimen, sinun tulee olla", @@ -1778,7 +1778,7 @@ "this room": "tämä huone", "View older messages in %(roomName)s.": "Näytä vanhemmat viestit huoneessa %(roomName)s.", "Joining room …": "Liitytään huoneeseen …", - "Loading …": "Latataan …", + "Loading …": "Ladataan …", "Join the conversation with an account": "Liity keskusteluun tilin avulla", "Sign Up": "Rekisteröidy", "Sign In": "Kirjaudu", @@ -2157,5 +2157,46 @@ "Widget ID": "Sovelman tunnus", "Using this widget may share data with %(widgetDomain)s.": "Tämän sovelman käyttäminen voi jakaa tietoja verkkotunnukselle %(widgetDomain)s.", "Widget added by": "Sovelman lisäsi", - "This widget may use cookies.": "Tämä sovelma saattaa käyttää evästeitä." + "This widget may use cookies.": "Tämä sovelma saattaa käyttää evästeitä.", + "Trust": "Luota", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Voit käyttää identiteettipalvelinta lähettääksesi sähköpostikutsuja. Klikkaa Jatka käyttääksesi oletuspalvelinta (%(defaultIdentityServerName)s) tai syötä eri palvelin asetuksissa.", + "Use an identity server to invite by email. Manage in Settings.": "Voit käyttää identiteettipalvelinta sähköpostikutsujen lähettämiseen.", + "Multiple integration managers": "Useita integraatiolähteitä", + "Try out new ways to ignore people (experimental)": "Kokeile uusia tapoja käyttäjien huomiotta jättämiseen (kokeellinen)", + "Send verification requests in direct message, including a new verification UX in the member panel.": "Lähetä varmennuspyynnöt suoralla viestillä. Tämä sisältää uuden varmennuskäyttöliittymän jäsenpaneelissa.", + "Enable cross-signing to verify per-user instead of per-device (in development)": "Ota käyttöön ristivarmennus, jolla varmennat käyttäjän, jokaisen käyttäjän laitteen sijaan (kehitysversio)", + "Enable local event indexing and E2EE search (requires restart)": "Ota käyttöön paikallinen tapahtumaindeksointi ja paikallinen haku salatuista viesteistä (vaatii uudelleenlatauksen)", + "Use the new, faster, composer for writing messages": "Käytä uutta, nopeampaa kirjoitinta viestien kirjoittamiseen", + "Match system theme": "Käytä järjestelmän teemaa", + "Decline (%(counter)s)": "Hylkää (%(counter)s)", + "Connecting to integration manager...": "Yhdistetään integraatioiden lähteeseen...", + "Cannot connect to integration manager": "Integraatioiden lähteeseen yhdistäminen epäonnistui", + "The integration manager is offline or it cannot reach your homeserver.": "Integraatioiden lähde on poissa verkosta, tai siihen ei voida yhdistää kotipalvelimeltasi.", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Käytä integraatioiden lähdettä (%(serverName)s) bottien, sovelmien ja tarrapakettien hallintaan.", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Käytä integraatioiden lähdettä bottien, sovelmien ja tarrapakettien hallintaan.", + "Manage integrations": "Hallitse integraatioita", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integraatioiden lähteet vastaanottavat asetusdataa ja voivat muokata sovelmia, lähettää kutsuja huoneeseen ja asettaa oikeustasoja puolestasi.", + "Discovery": "Käyttäjien etsintä", + "Ignored/Blocked": "Jätetty huomiotta/estetty", + "Error adding ignored user/server": "Ongelma huomiotta jätetyn käyttäjän/palvelimen lisäämisessä", + "Error subscribing to list": "Virhe listalle liityttäessä", + "Error removing ignored user/server": "Ongelma huomiotta jätetyn käyttäjän/palvelimen poistamisessa", + "Error unsubscribing from list": "Virhe listalta poistuttaessa", + "Ban list rules - %(roomName)s": "Estolistan säännöt - %(roomName)s", + "Server rules": "Palvelinehdot", + "User rules": "Käyttäjäehdot", + "You have not ignored anyone.": "Et ole jättänyt ketään huomiotta.", + "You are currently ignoring:": "Jätät tällä hetkellä huomiotta:", + "You are not subscribed to any lists": "Et ole liittynyt yhteenkään listaan", + "You are currently subscribed to:": "Olet tällä hetkellä liittynyt:", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Lisää käyttäjät ja palvelimet, jotka haluat jättää huomiotta. Voit käyttää asteriskia(*) täsmätäksesi mihin tahansa merkkeihin. Esimerkiksi, @bot:* jättäisi huomiotta kaikki käyttäjät, joiden nimi alkaa kirjaimilla ”bot”.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Käyttäjien huomiotta jättäminen tapahtuu estolistojen kautta, joissa on tieto siitä, kenet pitää estää. Estolistalle liittyminen tarkoittaa, että ne käyttäjät/palvelimet, jotka tämä lista estää, eivät näy sinulle.", + "Personal ban list": "Henkilökohtainen estolista", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Henkilökohtainen estolistasi sisältää kaikki käyttäjät/palvelimet, joilta et henkilökohtaisesti halua nähdä viestejä. Sen jälkeen, kun olet estänyt ensimmäisen käyttäjän/palvelimen, huonelistaan ilmestyy uusi huone nimeltä ”Tekemäni estot” (englanniksi ”My Ban List”). Pysy tässä huoneessa, jotta estolistasi pysyy voimassa.", + "Server or user ID to ignore": "Huomiotta jätettävä palvelin tai käyttäjätunnus", + "Subscribed lists": "Tilatut listat", + "Subscribing to a ban list will cause you to join it!": "Estolistan käyttäminen saa sinut liittymään listalle!", + "If this isn't what you want, please use a different tool to ignore users.": "Jos tämä ei ole mitä haluat, käytä eri työkalua käyttäjien huomiotta jättämiseen.", + "Room ID or alias of ban list": "Huoneen tunnus tai estolistan alias", + "Integration Manager": "Integraatioiden lähde" } From d9e322bbcaf2f4bf78b608266c35c6c5292bb4ef Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 27 Nov 2019 10:32:21 +0000 Subject: [PATCH 0826/2372] Upgrade to JS SDK 2.4.5 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1ecfe63c23..6cdd42c9da 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "lolex": "4.2", - "matrix-js-sdk": "2.4.4", + "matrix-js-sdk": "2.4.5", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index 44d9548b54..e43f12760b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5197,10 +5197,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc" integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw== -matrix-js-sdk@2.4.4: - version "2.4.4" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.4.tgz#d5e2d6fbe938c4275a1423a5f09330d33517ce3f" - integrity sha512-wSaRFvhWvwEzVaEkyBGo5ReumvaM5OrC1MJ6SVlyoLwH/WRPEXcUlu+rUNw5TFVEAH4TAVHXf/SVRBiR0j5nSQ== +matrix-js-sdk@2.4.5: + version "2.4.5" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.5.tgz#0a02f0a3e18c59a393b34b8d6ebc54226cce6465" + integrity sha512-Mh0fPoiqyXRksFNYS4/2s20xAklmYVIgSms3qFvLhno32LN43NizUoAMBYYGtyjt8BQi+U77lbNL0s5f2V7gPQ== dependencies: another-json "^0.2.0" babel-runtime "^6.26.0" From 25b5921ddfeeb4dbbfd4387b8f8482fcbdda82fe Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 27 Nov 2019 10:38:35 +0000 Subject: [PATCH 0827/2372] Prepare changelog for v1.7.4 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2341863a..8fe6f80e43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +Changes in [1.7.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.4) (2019-11-27) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.3...v1.7.4) + +* Upgrade to JS SDK 2.5.4 to relax identity server discovery and E2EE debugging +* Fix override behaviour of system vs defined theme +* Clarify that cross-signing is in development + Changes in [1.7.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.3) (2019-11-25) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.3-rc.2...v1.7.3) From 1a98c0d04e74684dc47ee1c7d917ae56a5a9ba91 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 27 Nov 2019 10:38:35 +0000 Subject: [PATCH 0828/2372] v1.7.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6cdd42c9da..7b75390293 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.3", + "version": "1.7.4", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From c9b972116ad28a4a569c9cd4010774a985763539 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 27 Nov 2019 11:47:57 +0000 Subject: [PATCH 0829/2372] Remove broken velocity-ui animation The deactivate account dialog tried to shake the password box on an incorrect password but it didn't work because we don't import velocity-ui anywhere. Looks like we forgot to import it in DeactivateAccountDialog.js and were relying on it being imported elsewhere (so probably broke when BottomLeftMenu got removed). Either way, we no longer use the field-shake animation anywhere else, so just remove it. --- src/components/views/dialogs/DeactivateAccountDialog.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index 8e5db11cf0..703ecaf092 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,15 +22,12 @@ import sdk from '../../../index'; import Analytics from '../../../Analytics'; import MatrixClientPeg from '../../../MatrixClientPeg'; import * as Lifecycle from '../../../Lifecycle'; -import Velocity from 'velocity-animate'; import { _t } from '../../../languageHandler'; export default class DeactivateAccountDialog extends React.Component { constructor(props, context) { super(props, context); - this._passwordField = null; - this._onOk = this._onOk.bind(this); this._onCancel = this._onCancel.bind(this); this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this); @@ -78,7 +76,6 @@ export default class DeactivateAccountDialog extends React.Component { // https://matrix.org/jira/browse/SYN-744 if (err.httpStatus === 401 || err.httpStatus === 403) { errStr = _t('Incorrect password'); - Velocity(this._passwordField, "callout.shake", 300); } this.setState({ busy: false, @@ -181,7 +178,6 @@ export default class DeactivateAccountDialog extends React.Component { label={_t('Password')} onChange={this._onPasswordFieldChange} value={this.state.password} - ref={(e) => {this._passwordField = e;}} className={passwordBoxClass} />
        From 54d6b6aa73656d9c00cc047ace964bc73ce011e7 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 27 Nov 2019 13:31:44 +0000 Subject: [PATCH 0830/2372] Flip JS SDK back to develop --- package.json | 4 ++-- yarn.lock | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 92bf2e452d..5b82d9b111 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "lolex": "4.2", - "matrix-js-sdk": "2.4.5", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", @@ -133,8 +133,8 @@ "eslint": "^5.12.0", "eslint-config-google": "^0.7.1", "eslint-plugin-babel": "^5.2.1", - "eslint-plugin-jest": "^23.0.4", "eslint-plugin-flowtype": "^2.30.0", + "eslint-plugin-jest": "^23.0.4", "eslint-plugin-react": "^7.7.0", "eslint-plugin-react-hooks": "^2.0.1", "estree-walker": "^0.5.0", diff --git a/yarn.lock b/yarn.lock index 62b45fd715..073fc95b37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5246,10 +5246,9 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc" integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw== -matrix-js-sdk@2.4.5: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "2.4.5" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-2.4.5.tgz#0a02f0a3e18c59a393b34b8d6ebc54226cce6465" - integrity sha512-Mh0fPoiqyXRksFNYS4/2s20xAklmYVIgSms3qFvLhno32LN43NizUoAMBYYGtyjt8BQi+U77lbNL0s5f2V7gPQ== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/6ea8003df23d55e2b84911c3204005c42a9ffa9c" dependencies: another-json "^0.2.0" babel-runtime "^6.26.0" From b52b8b484896f0f91577bcbfca0a42a0d5ac5ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Wed, 27 Nov 2019 10:56:06 +0000 Subject: [PATCH 0831/2372] Translated using Weblate (French) Currently translated at 100.0% (1921 of 1921 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 397b7c6f14..4b4dbfddbc 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2375,5 +2375,6 @@ "Manage integrations": "Gérer les intégrations", "Verification Request": "Demande de vérification", " (1/%(totalCount)s)": " (1/%(totalCount)s)", - "Enable cross-signing to verify per-user instead of per-device (in development)": "Activer la signature croisée pour vérifier par utilisateur plutôt que par appareil (en développement)" + "Enable cross-signing to verify per-user instead of per-device (in development)": "Activer la signature croisée pour vérifier par utilisateur plutôt que par appareil (en développement)", + "Match system theme": "S’adapter au thème du système" } From d6821ecb990c7667e96fab58661fbc3cb89e76bd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 27 Nov 2019 10:44:36 -0700 Subject: [PATCH 0832/2372] Fix multi-invite error dialog messaging Fixes https://github.com/vector-im/riot-web/issues/11515 --- src/RoomInvite.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 64aab36128..babed0e6b8 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -202,11 +202,14 @@ function _showAnyInviteErrors(addrs, room, inviter) { } } + // React 16 doesn't let us use `errorList.join(
        )` anymore, so this is our solution + let description =
        {errorList.map(e =>
        {e}
        )}
        ; + if (errorList.length > 0) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), - description: errorList.join(
        ), + description, }); } } From 275bd33a6c59f25106063e3967c93b942c111872 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 27 Nov 2019 10:48:05 -0700 Subject: [PATCH 0833/2372] Move the description into the relevant branch --- src/RoomInvite.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index babed0e6b8..c72ca4d662 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -202,10 +202,10 @@ function _showAnyInviteErrors(addrs, room, inviter) { } } - // React 16 doesn't let us use `errorList.join(
        )` anymore, so this is our solution - let description =
        {errorList.map(e =>
        {e}
        )}
        ; - if (errorList.length > 0) { + // React 16 doesn't let us use `errorList.join(
        )` anymore, so this is our solution + let description =
        {errorList.map(e =>
        {e}
        )}
        ; + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), From 673e6c31625ded3f9215ec4cfbdefa09b1c97295 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 27 Nov 2019 12:26:43 -0700 Subject: [PATCH 0834/2372] Don't assume that diffs will have an appropriate child node Fixes https://github.com/vector-im/riot-web/issues/11497 This is a regression from react-sdk v1.5.0 where the diff feature was added in the first place. It only affects lists. --- src/utils/MessageDiffUtils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/MessageDiffUtils.js b/src/utils/MessageDiffUtils.js index 78f3faa0c5..de0d8fdc89 100644 --- a/src/utils/MessageDiffUtils.js +++ b/src/utils/MessageDiffUtils.js @@ -77,6 +77,8 @@ function findRefNodes(root, route, isAddition) { const end = isAddition ? route.length - 1 : route.length; for (let i = 0; i < end; ++i) { refParentNode = refNode; + // Lists don't have appropriate child nodes we can use. + if (!refNode.childNodes[route[i]]) continue; refNode = refNode.childNodes[route[i]]; } return {refNode, refParentNode}; From 7b013ecc697ca8fe6aebfd96bab3ff583e2357d0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 27 Nov 2019 12:54:31 -0700 Subject: [PATCH 0835/2372] Fix persisted widgets getting stuck at loading screens The widget itself is rendered underneath the loading screen, so we just have to disable the loading state. This commit also removes the "is" attribute because React 16 includes unknown attributes: https://reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html Fixes https://github.com/vector-im/riot-web/issues/11536 --- src/components/views/elements/AppTile.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 4cfce0c5dd..9a29843d3b 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -36,6 +36,7 @@ import classNames from 'classnames'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import {createMenu} from "../../structures/ContextualMenu"; +import PersistedElement from "./PersistedElement"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ENABLE_REACT_PERF = false; @@ -247,7 +248,8 @@ export default class AppTile extends React.Component { this.setScalarToken(); } } else if (nextProps.show && !this.props.show) { - if (this.props.waitForIframeLoad) { + // We assume that persisted widgets are loaded and don't need a spinner. + if (this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey)) { this.setState({ loading: true, }); @@ -652,12 +654,7 @@ export default class AppTile extends React.Component { appTileBody = (
        { this.state.loading && loadingElement } - { /* - The "is" attribute in the following iframe tag is needed in order to enable rendering of the - "allow" attribute, which is unknown to react 15. - */ }