From 8c73056693caec6f9e97705e3dbc6205332b50b9 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Wed, 21 Aug 2019 16:06:32 +0300 Subject: [PATCH 01/22] Tweak rageshake logging messages Signed-off-by: Jason Robinson --- src/rageshake/rageshake.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index 6366392c04..87c98d105a 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -74,13 +74,17 @@ class ConsoleLogger { // Convert objects and errors to helpful things args = args.map((arg) => { + let msg = ''; if (arg instanceof Error) { - return arg.message + (arg.stack ? `\n${arg.stack}` : ''); + msg = arg.message + (arg.stack ? `\n${arg.stack}` : ''); } else if (typeof(arg) === 'object') { - return JSON.stringify(arg); + msg = JSON.stringify(arg); } else { - return arg; + msg = arg; } + // Do some cleanup + msg = msg.replace(/token=[a-zA-Z0-9-]+/gm, 'token=xxxxx'); + return msg; }); // Some browsers support string formatting which we're not doing here From e7097d58ecae06d2e9370c03b2654425e01bad47 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 22 Aug 2019 14:44:09 +0100 Subject: [PATCH 02/22] Add IS access token callback This passes a callback to the JS SDK which it can use to get IS access tokens whenever needed for either talking to the IS directly or passing along to the HS. Fixes https://github.com/vector-im/riot-web/issues/10525 --- src/MatrixClientPeg.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 813f0ed87e..94bf6e30d9 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -32,6 +32,7 @@ import Modal from './Modal'; import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; +import IdentityAuthClient from './IdentityAuthClient'; interface MatrixClientCreds { homeserverUrl: string, @@ -219,6 +220,9 @@ class MatrixClientPeg { fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), verificationMethods: [verificationMethods.SAS], unstableClientRelationAggregation: true, + getIdentityAccessToken: () => { + return new IdentityAuthClient().getAccessToken(); + }, }; this.matrixClient = createMatrixClient(opts); From b5daba90261df41b8d39d98966a48c0a113b416e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Aug 2019 18:17:08 +0100 Subject: [PATCH 03/22] Iterate over all instances of variable/tag for _t substitutions Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/languageHandler.js | 78 +++++++++++++++----------- test/i18n-test/languageHandler-test.js | 11 ++++ 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index 474cd2b3cd..9e354cee9e 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -179,12 +179,12 @@ export function replaceByRegexes(text, mapping) { for (const regexpString in mapping) { // TODO: Cache regexps - const regexp = new RegExp(regexpString); + const regexp = new RegExp(regexpString, "g"); // Loop over what output we have so far and perform replacements // We look for matches: if we find one, we get three parts: everything before the match, the replaced part, // and everything after the match. Insert all three into the output. We need to do this because we can insert objects. - // Otherwise there would be no need for the splitting and we could do simple replcement. + // Otherwise there would be no need for the splitting and we could do simple replacement. let matchFoundSomewhere = false; // If we don't find a match anywhere we want to log it for (const outputIndex in output) { const inputText = output[outputIndex]; @@ -192,44 +192,58 @@ export function replaceByRegexes(text, mapping) { continue; } - const match = inputText.match(regexp); - if (!match) { - continue; - } + // process every match in the string + // starting with the first + let match = regexp.exec(inputText); + + if (!match) continue; matchFoundSomewhere = true; - const capturedGroups = match.slice(2); - - // The textual part before the match + // The textual part before the first match const head = inputText.substr(0, match.index); - // The textual part after the match - const tail = inputText.substr(match.index + match[0].length); + const parts = []; + // keep track of prevMatch + let prevMatch; + while (match) { + // store prevMatch + prevMatch = match; + const capturedGroups = match.slice(2); - let replaced; - // If substitution is a function, call it - if (mapping[regexpString] instanceof Function) { - replaced = mapping[regexpString].apply(null, capturedGroups); - } else { - replaced = mapping[regexpString]; + let replaced; + // If substitution is a function, call it + if (mapping[regexpString] instanceof Function) { + replaced = mapping[regexpString].apply(null, capturedGroups); + } else { + replaced = mapping[regexpString]; + } + + if (typeof replaced === 'object') { + shouldWrapInSpan = true; + } + + // Here we also need to check that it actually is a string before comparing against one + // The head and tail are always strings + if (typeof replaced !== 'string' || replaced !== '') { + parts.push(replaced); + } + + // try the next match + match = regexp.exec(inputText); + + // add the text between prevMatch and this one + // or the end of the string if prevMatch is the last match + if (match) { + const startIndex = prevMatch.index + prevMatch[0].length; + parts.push(inputText.substr(startIndex, match.index - startIndex)); + } else { + parts.push(inputText.substr(prevMatch.index + prevMatch[0].length)); + } } - if (typeof replaced === 'object') { - shouldWrapInSpan = true; - } - - output.splice(outputIndex, 1); // Remove old element - // Insert in reverse order as splice does insert-before and this way we get the final order correct - if (tail !== '') { - output.splice(outputIndex, 0, tail); - } - - // Here we also need to check that it actually is a string before comparing against one - // The head and tail are always strings - if (typeof replaced !== 'string' || replaced !== '') { - output.splice(outputIndex, 0, replaced); - } + // remove the old element at the same time + output.splice(outputIndex, 1, ...parts); if (head !== '') { // Don't push empty nodes, they are of no use output.splice(outputIndex, 0, head); diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index ce9f8e1684..07e3f2cb8b 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -70,4 +70,15 @@ describe('languageHandler', function() { const text = '%(var1)s %(var2)s'; expect(languageHandler._t(text, { var2: 'val2', var1: 'val1' })).toBe('val1 val2'); }); + + it('multiple replacements of the same variable', function() { + const text = '%(var1)s %(var1)s'; + expect(languageHandler._t(text, { var1: 'val1' })).toBe('val1 val1'); + }); + + it('multiple replacements of the same tag', function() { + const text = 'Click here to join the discussion! or here'; + expect(languageHandler._t(text, {}, { 'a': (sub) => `x${sub}x` })) + .toBe('xClick herex to join the discussion! xor herex'); + }); }); From 310457059b32b0503c8575d8a6093599000e4ade Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Aug 2019 18:31:02 +0100 Subject: [PATCH 04/22] [i18n] only append tail if it is actually needed Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/languageHandler.js | 12 ++++++++++-- test/i18n-test/languageHandler-test.js | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index 9e354cee9e..e5656e5f69 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -177,6 +177,10 @@ export function replaceByRegexes(text, mapping) { // If we insert any components we need to wrap the output in a span. React doesn't like just an array of components. let shouldWrapInSpan = false; + if (text === "You are now ignoring %(userId)s") { + debugger; + } + for (const regexpString in mapping) { // TODO: Cache regexps const regexp = new RegExp(regexpString, "g"); @@ -233,11 +237,15 @@ export function replaceByRegexes(text, mapping) { // add the text between prevMatch and this one // or the end of the string if prevMatch is the last match + let tail; if (match) { const startIndex = prevMatch.index + prevMatch[0].length; - parts.push(inputText.substr(startIndex, match.index - startIndex)); + tail = inputText.substr(startIndex, match.index - startIndex); } else { - parts.push(inputText.substr(prevMatch.index + prevMatch[0].length)); + tail = inputText.substr(prevMatch.index + prevMatch[0].length); + } + if (tail) { + parts.push(tail); } } diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js index 07e3f2cb8b..0d96bc15ab 100644 --- a/test/i18n-test/languageHandler-test.js +++ b/test/i18n-test/languageHandler-test.js @@ -73,12 +73,12 @@ describe('languageHandler', function() { it('multiple replacements of the same variable', function() { const text = '%(var1)s %(var1)s'; - expect(languageHandler._t(text, { var1: 'val1' })).toBe('val1 val1'); + expect(languageHandler.substitute(text, { var1: 'val1' })).toBe('val1 val1'); }); it('multiple replacements of the same tag', function() { const text = 'Click here to join the discussion! or here'; - expect(languageHandler._t(text, {}, { 'a': (sub) => `x${sub}x` })) + expect(languageHandler.substitute(text, {}, { 'a': (sub) => `x${sub}x` })) .toBe('xClick herex to join the discussion! xor herex'); }); }); From 7d511fbbc5d0513f8575464cd58d4e0c65bea9f4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Aug 2019 18:34:26 +0100 Subject: [PATCH 05/22] remove leftover debugger =) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/languageHandler.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/languageHandler.js b/src/languageHandler.js index e5656e5f69..179bb2d1d0 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -177,10 +177,6 @@ export function replaceByRegexes(text, mapping) { // If we insert any components we need to wrap the output in a span. React doesn't like just an array of components. let shouldWrapInSpan = false; - if (text === "You are now ignoring %(userId)s") { - debugger; - } - for (const regexpString in mapping) { // TODO: Cache regexps const regexp = new RegExp(regexpString, "g"); From f505aa0c8330557256693d9c01166f32347c74b3 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Fri, 23 Aug 2019 10:12:46 +0300 Subject: [PATCH 06/22] Ensure logging tweak doesn't fail on undefined Run the replace on the log line string instead of the separate parts since we can ensure the line is a string. Signed-off-by: Jason Robinson --- src/rageshake/rageshake.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index 87c98d105a..1acce0600c 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -74,17 +74,13 @@ class ConsoleLogger { // Convert objects and errors to helpful things args = args.map((arg) => { - let msg = ''; if (arg instanceof Error) { - msg = arg.message + (arg.stack ? `\n${arg.stack}` : ''); + return arg.message + (arg.stack ? `\n${arg.stack}` : ''); } else if (typeof(arg) === 'object') { - msg = JSON.stringify(arg); + return JSON.stringify(arg); } else { - msg = arg; + return arg; } - // Do some cleanup - msg = msg.replace(/token=[a-zA-Z0-9-]+/gm, 'token=xxxxx'); - return msg; }); // Some browsers support string formatting which we're not doing here @@ -92,7 +88,9 @@ class ConsoleLogger { // run. // Example line: // 2017-01-18T11:23:53.214Z W Failed to set badge count - const line = `${ts} ${level} ${args.join(' ')}\n`; + let line = `${ts} ${level} ${args.join(' ')}\n`; + // Do some cleanup + line = line.replace(/token=[a-zA-Z0-9-]+/gm, 'token=xxxxx'); // Using + really is the quickest way in JS // http://jsperf.com/concat-vs-plus-vs-join this.logs += line; From 84e3d339ac0a67035c0b0330f7470bbec136c52a Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 23 Aug 2019 11:17:51 +0100 Subject: [PATCH 07/22] Change to provider object --- src/MatrixClientPeg.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 94bf6e30d9..27c4f40669 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -220,9 +220,7 @@ class MatrixClientPeg { fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), verificationMethods: [verificationMethods.SAS], unstableClientRelationAggregation: true, - getIdentityAccessToken: () => { - return new IdentityAuthClient().getAccessToken(); - }, + identityServer: new IdentityAuthClient(), }; this.matrixClient = createMatrixClient(opts); From c44ae2df4d47b6fb5787cdc50ca01dbf7e12aa1d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 23 Aug 2019 11:58:04 -0600 Subject: [PATCH 08/22] Treat 404 errors on IS as having no terms Fixes https://github.com/vector-im/riot-web/issues/10634 --- src/components/views/settings/SetIdServer.js | 46 ++++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index e7aa22527d..ddd246d928 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -153,31 +153,17 @@ export default class SetIdServer extends React.Component { // Double check that the identity server even has terms of service. const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl); if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) { - const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); - Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { - title: _t("Identity server has no terms of service"), - description: ( -
- - {_t("The identity server 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._saveIdServer(fullUrl); - }, - }); + this._showNoTermsWarning(fullUrl); return; } this._saveIdServer(fullUrl); } catch (e) { console.error(e); + if (e.cors === "rejected" || e.httpStatus === 404) { + this._showNoTermsWarning(fullUrl); + return; + } errStr = _t("Terms of service not accepted or the identity server is invalid."); } } @@ -190,6 +176,28 @@ export default class SetIdServer extends React.Component { }); }; + _showNoTermsWarning(fullUrl) { + const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); + Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { + title: _t("Identity server has no terms of service"), + description: ( +
+ + {_t("The identity server 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._saveIdServer(fullUrl); + }, + }); + }; + _onDisconnectClicked = async () => { this.setState({disconnectBusy: true}); try { From e1552b61fcece1c776f8ce0bc2e96bca1b2b9a3d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 23 Aug 2019 12:01:13 -0600 Subject: [PATCH 09/22] fix i18n --- 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 83a9602a51..a0c98a0335 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -549,10 +549,10 @@ "Not a valid Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)", "Could not connect to Identity Server": "Could not connect to Identity Server", "Checking server": "Checking server", + "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.", - "Terms of service not accepted or the identity server is invalid.": "Terms of service not accepted or the identity server is invalid.", "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.": "You are currently sharing email addresses or phone numbers on the identity server . You will need to reconnect to to stop sharing them.", "Disconnect from the identity server ?": "Disconnect from the identity server ?", "Disconnect Identity Server": "Disconnect Identity Server", From e8b0c411578d84363b105e7f76e7e4e9b0be7237 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 23 Aug 2019 12:01:47 -0600 Subject: [PATCH 10/22] minus ; --- src/components/views/settings/SetIdServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index ddd246d928..ba405a5652 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -196,7 +196,7 @@ export default class SetIdServer extends React.Component { this._saveIdServer(fullUrl); }, }); - }; + } _onDisconnectClicked = async () => { this.setState({disconnectBusy: true}); From accb0abe2dbef874aa6e5c80777e75d3db60d828 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 24 Aug 2019 11:47:07 +0100 Subject: [PATCH 11/22] Switch from react-addons-test-utils to react-dom/test-utils. React 16 :D Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 1 - test/components/structures/MessagePanel-test.js | 2 +- test/components/views/dialogs/InteractiveAuthDialog-test.js | 2 +- .../components/views/elements/MemberEventListSummary-test.js | 2 +- test/components/views/rooms/MemberList-test.js | 2 +- test/components/views/rooms/MessageComposerInput-test.js | 5 ++--- test/components/views/rooms/RoomList-test.js | 2 +- yarn.lock | 5 ----- 8 files changed, 7 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index ffd701a233..b4e1af9f0a 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,6 @@ "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", "mocha": "^5.0.5", - "react-addons-test-utils": "^15.4.0", "require-json": "0.0.1", "rimraf": "^2.4.3", "sinon": "^5.0.7", diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 138681457c..58b1590cf1 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -19,7 +19,7 @@ import SettingsStore from "../../../src/settings/SettingsStore"; const React = require('react'); const ReactDOM = require("react-dom"); import PropTypes from "prop-types"; -const TestUtils = require('react-addons-test-utils'); +const TestUtils = require('react-dom/test-utils'); const expect = require('expect'); import sinon from 'sinon'; import { EventEmitter } from "events"; diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index 95f76dfd3e..b14ea7c242 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -18,7 +18,7 @@ import expect from 'expect'; import Promise from 'bluebird'; import React from 'react'; import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-addons-test-utils'; +import ReactTestUtils from 'react-dom/test-utils'; import sinon from 'sinon'; import MatrixReactTestUtils from 'matrix-react-test-utils'; diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index f3749b850f..d1e112735d 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -1,6 +1,6 @@ import expect from 'expect'; import React from 'react'; -import ReactTestUtils from 'react-addons-test-utils'; +import ReactTestUtils from 'react-dom/test-utils'; import sdk from 'matrix-react-sdk'; import * as languageHandler from '../../../../src/languageHandler'; import * as testUtils from '../../../test-utils'; diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.js index b9d96635a2..9a1439c2f7 100644 --- a/test/components/views/rooms/MemberList-test.js +++ b/test/components/views/rooms/MemberList-test.js @@ -1,5 +1,5 @@ import React from 'react'; -import ReactTestUtils from 'react-addons-test-utils'; +import ReactTestUtils from 'react-dom/test-utils'; import ReactDOM from 'react-dom'; import expect from 'expect'; import lolex from 'lolex'; diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index ed07c0f233..1105a4af17 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -1,5 +1,5 @@ import React from 'react'; -import ReactTestUtils from 'react-addons-test-utils'; +import ReactTestUtils from 'react-dom/test-utils'; import ReactDOM from 'react-dom'; import expect from 'expect'; import sinon from 'sinon'; @@ -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 RoomMember from 'matrix-js-sdk'; function addTextToDraft(text) { const components = document.getElementsByClassName('public-DraftEditor-content'); @@ -301,4 +300,4 @@ xdescribe('MessageComposerInput', () => { expect(spy.args[0][1].body).toEqual('[Click here](https://some.lovely.url)'); expect(spy.args[0][1].formatted_body).toEqual('Click here'); }); -}); \ No newline at end of file +}); diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index 754367cd23..68168fcf29 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -1,5 +1,5 @@ import React from 'react'; -import ReactTestUtils from 'react-addons-test-utils'; +import ReactTestUtils from 'react-dom/test-utils'; import ReactDOM from 'react-dom'; import expect from 'expect'; import lolex from 'lolex'; diff --git a/yarn.lock b/yarn.lock index b9341b2a0e..c664d0b7dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6349,11 +6349,6 @@ react-addons-css-transition-group@15.3.2: resolved "https://registry.yarnpkg.com/react-addons-css-transition-group/-/react-addons-css-transition-group-15.3.2.tgz#d8fa52bec9bb61bdfde8b9e4652b80297cbff667" integrity sha1-2PpSvsm7Yb396LnkZSuAKXy/9mc= -react-addons-test-utils@^15.4.0: - version "15.6.2" - resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz#c12b6efdc2247c10da7b8770d185080a7b047156" - integrity sha1-wStu/cIkfBDae4dw0YUICnsEcVY= - react-beautiful-dnd@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz#3b0a49bf6be75af351176c904f012611dd292b81" From 360cef66c1b3e61978bc8f7fe5397a8419fe2d2a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 24 Aug 2019 11:53:28 +0100 Subject: [PATCH 12/22] Migrate away from React.createClass for async-components. React 16 :D Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/async-components/views/dialogs/EncryptedEventDialog.js | 3 ++- src/async-components/views/dialogs/ExportE2eKeysDialog.js | 3 ++- src/async-components/views/dialogs/ImportE2eKeysDialog.js | 3 ++- .../views/dialogs/keybackup/CreateKeyBackupDialog.js | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js index 5db8b2365f..145203136a 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -15,12 +15,13 @@ limitations under the License. */ const React = require("react"); +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; const sdk = require('../../../index'); const MatrixClientPeg = require("../../../MatrixClientPeg"); -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: 'EncryptedEventDialog', propTypes: { diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 529780c121..0fd412935a 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -17,6 +17,7 @@ limitations under the License. import FileSaver from 'file-saver'; import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; import { MatrixClient } from 'matrix-js-sdk'; @@ -26,7 +27,7 @@ import sdk from '../../../index'; const PHASE_EDIT = 1; const PHASE_EXPORTING = 2; -export default React.createClass({ +export default createReactClass({ displayName: 'ExportE2eKeysDialog', propTypes: { diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index 5181b6da2f..17f3bba117 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -16,6 +16,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { MatrixClient } from 'matrix-js-sdk'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; @@ -37,7 +38,7 @@ function readFileAsArrayBuffer(file) { const PHASE_EDIT = 1; const PHASE_IMPORTING = 2; -export default React.createClass({ +export default createReactClass({ displayName: 'ImportE2eKeysDialog', propTypes: { diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 9ceff69467..e36763591e 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -15,6 +15,7 @@ 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'; @@ -48,7 +49,7 @@ function selectText(target) { * Walks the user through the process of creating an e2e key backup * on the server. */ -export default React.createClass({ +export default createReactClass({ getInitialState: function() { return { phase: PHASE_PASSPHRASE, From d94e2179bfcf919f7b557a9ad1e4e78074cedac9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 24 Aug 2019 11:59:46 +0100 Subject: [PATCH 13/22] Migrate away from React.createClass for views/dialogs. React 16 :D Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/AddressPickerDialog.js | 3 ++- src/components/views/dialogs/AskInviteAnywayDialog.js | 3 ++- src/components/views/dialogs/BaseDialog.js | 3 ++- src/components/views/dialogs/ConfirmRedactDialog.js | 3 ++- src/components/views/dialogs/ConfirmUserActionDialog.js | 3 ++- src/components/views/dialogs/CreateGroupDialog.js | 3 ++- src/components/views/dialogs/CreateRoomDialog.js | 3 ++- src/components/views/dialogs/ErrorDialog.js | 3 ++- src/components/views/dialogs/InfoDialog.js | 3 ++- src/components/views/dialogs/InteractiveAuthDialog.js | 3 ++- src/components/views/dialogs/KeyShareDialog.js | 3 ++- src/components/views/dialogs/QuestionDialog.js | 3 ++- src/components/views/dialogs/RoomUpgradeDialog.js | 3 ++- src/components/views/dialogs/SessionRestoreErrorDialog.js | 3 ++- src/components/views/dialogs/SetEmailDialog.js | 3 ++- src/components/views/dialogs/SetMxIdDialog.js | 3 ++- src/components/views/dialogs/SetPasswordDialog.js | 3 ++- src/components/views/dialogs/TextInputDialog.js | 3 ++- src/components/views/dialogs/UnknownDeviceDialog.js | 5 ++--- .../views/dialogs/keybackup/RestoreKeyBackupDialog.js | 3 ++- 20 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 6cb5a278fd..ac2181f1f2 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -19,6 +19,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import { _t, _td } from '../../../languageHandler'; import sdk from '../../../index'; @@ -39,7 +40,7 @@ const addressTypeName = { }; -module.exports = React.createClass({ +module.exports = createReactClass({ displayName: "AddressPickerDialog", propTypes: { diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.js b/src/components/views/dialogs/AskInviteAnywayDialog.js index d4b073eb01..3d10752ff8 100644 --- a/src/components/views/dialogs/AskInviteAnywayDialog.js +++ b/src/components/views/dialogs/AskInviteAnywayDialog.js @@ -16,12 +16,13 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {SettingLevel} from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore"; -export default React.createClass({ +export default createReactClass({ propTypes: { unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ] onInviteAnyways: PropTypes.func.isRequired, diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index ee838b9825..65b89d1631 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import FocusTrap from 'focus-trap-react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -32,7 +33,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; * Includes a div for the title, and a keypress handler which cancels the * dialog on escape. */ -export default React.createClass({ +export default createReactClass({ displayName: 'BaseDialog', propTypes: { diff --git a/src/components/views/dialogs/ConfirmRedactDialog.js b/src/components/views/dialogs/ConfirmRedactDialog.js index a967b5df9a..c606706ed2 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.js +++ b/src/components/views/dialogs/ConfirmRedactDialog.js @@ -15,13 +15,14 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; /* * A dialog for confirming a redaction. */ -export default React.createClass({ +export default createReactClass({ displayName: 'ConfirmRedactDialog', render: function() { diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index 4848e468e9..4d33b2b500 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; @@ -29,7 +30,7 @@ import { GroupMemberType } from '../../../groups'; * to make it obvious what is going to happen. * Also tweaks the style for 'dangerous' actions (albeit only with colour) */ -export default React.createClass({ +export default createReactClass({ displayName: 'ConfirmUserActionDialog', propTypes: { // matrix-js-sdk (room) member object. Supply either this or 'groupMember' diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 882d323449..11f4c21366 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -15,13 +15,14 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import { _t } from '../../../languageHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; -export default React.createClass({ +export default createReactClass({ displayName: 'CreateGroupDialog', propTypes: { onFinished: PropTypes.func.isRequired, diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index 3212e53c05..e1da9f841d 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -15,12 +15,13 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import { _t } from '../../../languageHandler'; -export default React.createClass({ +export default createReactClass({ displayName: 'CreateRoomDialog', propTypes: { onFinished: PropTypes.func.isRequired, diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js index a055f07629..f6db0a14a5 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.js @@ -26,11 +26,12 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; -export default React.createClass({ +export default createReactClass({ displayName: 'ErrorDialog', propTypes: { title: PropTypes.string, diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.js index d01b737309..c54da480e6 100644 --- a/src/components/views/dialogs/InfoDialog.js +++ b/src/components/views/dialogs/InfoDialog.js @@ -17,12 +17,13 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import classNames from "classnames"; -export default React.createClass({ +export default createReactClass({ displayName: 'InfoDialog', propTypes: { className: PropTypes.string, diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index b068428bed..0b658bad81 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; @@ -23,7 +24,7 @@ import { _t } from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; -export default React.createClass({ +export default createReactClass({ displayName: 'InteractiveAuthDialog', propTypes: { diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index b9b64a69d2..a10c25a0fb 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -16,6 +16,7 @@ limitations under the License. import Modal from '../../../Modal'; import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; @@ -29,7 +30,7 @@ import { _t, _td } from '../../../languageHandler'; * should not, and `undefined` if the dialog is cancelled. (In other words: * truthy: do the key share. falsy: don't share the keys). */ -export default React.createClass({ +export default createReactClass({ propTypes: { matrixClient: PropTypes.object.isRequired, userId: PropTypes.string.isRequired, diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js index 4d0defadc2..4d2a699898 100644 --- a/src/components/views/dialogs/QuestionDialog.js +++ b/src/components/views/dialogs/QuestionDialog.js @@ -16,11 +16,12 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; -export default React.createClass({ +export default createReactClass({ displayName: 'QuestionDialog', propTypes: { title: PropTypes.string, diff --git a/src/components/views/dialogs/RoomUpgradeDialog.js b/src/components/views/dialogs/RoomUpgradeDialog.js index 45c242fea5..6900ac6fe8 100644 --- a/src/components/views/dialogs/RoomUpgradeDialog.js +++ b/src/components/views/dialogs/RoomUpgradeDialog.js @@ -15,13 +15,14 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; -export default React.createClass({ +export default createReactClass({ displayName: 'RoomUpgradeDialog', propTypes: { diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js index f7e117b31b..b9f6e77222 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.js +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; @@ -23,7 +24,7 @@ import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; -export default React.createClass({ +export default createReactClass({ displayName: 'SessionRestoreErrorDialog', propTypes: { diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.js index e643ddbc34..88baa5fd3e 100644 --- a/src/components/views/dialogs/SetEmailDialog.js +++ b/src/components/views/dialogs/SetEmailDialog.js @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import Email from '../../../email'; @@ -29,7 +30,7 @@ import Modal from '../../../Modal'; * * On success, `onFinished(true)` is called. */ -export default React.createClass({ +export default createReactClass({ displayName: 'SetEmailDialog', propTypes: { onFinished: PropTypes.func.isRequired, diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index dfaff52278..3bc6f5597e 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -17,6 +17,7 @@ limitations under the License. import Promise from 'bluebird'; import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; @@ -34,7 +35,7 @@ const USERNAME_CHECK_DEBOUNCE_MS = 250; * * On success, `onFinished(true, newDisplayName)` is called. */ -export default React.createClass({ +export default createReactClass({ displayName: 'SetMxIdDialog', propTypes: { onFinished: PropTypes.func.isRequired, diff --git a/src/components/views/dialogs/SetPasswordDialog.js b/src/components/views/dialogs/SetPasswordDialog.js index 0ec933b59f..0fe65aaca3 100644 --- a/src/components/views/dialogs/SetPasswordDialog.js +++ b/src/components/views/dialogs/SetPasswordDialog.js @@ -17,6 +17,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -62,7 +63,7 @@ const WarmFuzzy = function(props) { * * On success, `onFinished()` when finished */ -export default React.createClass({ +export default createReactClass({ displayName: 'SetPasswordDialog', propTypes: { onFinished: PropTypes.func.isRequired, diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.js index f28b16ef6f..3ce32ef4ec 100644 --- a/src/components/views/dialogs/TextInputDialog.js +++ b/src/components/views/dialogs/TextInputDialog.js @@ -15,10 +15,11 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; -export default React.createClass({ +export default createReactClass({ displayName: 'TextInputDialog', propTypes: { title: PropTypes.string, diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index 09b967c72f..e7522e971d 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -16,11 +16,10 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import GeminiScrollbar from 'react-gemini-scrollbar'; -import Resend from '../../../Resend'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import { markAllDevicesKnown } from '../../../cryptodevices'; @@ -67,7 +66,7 @@ UnknownDeviceList.propTypes = { }; -export default React.createClass({ +export default createReactClass({ displayName: 'UnknownDeviceDialog', propTypes: { diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 0f390a02c9..172a3ed9ea 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import createReactClass from 'create-react-class'; import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; import Modal from '../../../../Modal'; @@ -29,7 +30,7 @@ const RESTORE_TYPE_RECOVERYKEY = 1; /** * Dialog for restoring e2e keys from a backup and the user's recovery key */ -export default React.createClass({ +export default createReactClass({ getInitialState: function() { return { backupInfo: null, From 10291bafe0a72a3ed492324b5b32b2e12d6ab029 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 23 Aug 2019 17:37:58 +0100 Subject: [PATCH 14/22] add support for selecting ranges in the editor model, and replacing them this to support finding emoticons and replacing them with an emoji --- src/editor/model.js | 39 +++++++++++------ src/editor/position.js | 38 +++++++++++++++++ src/editor/range.js | 53 +++++++++++++++++++++++ test/editor/range-test.js | 88 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+), 12 deletions(-) create mode 100644 src/editor/position.js create mode 100644 src/editor/range.js create mode 100644 test/editor/range-test.js diff --git a/src/editor/model.js b/src/editor/model.js index 2f1e5218d8..c5b80247f6 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -16,6 +16,8 @@ limitations under the License. */ import {diffAtCaret, diffDeletion} from "./diff"; +import DocumentPosition from "./position"; +import Range from "./range"; export default class EditorModel { constructor(parts, partCreator, updateCallback = null) { @@ -197,7 +199,7 @@ export default class EditorModel { this._updateCallback(pos); } - _mergeAdjacentParts(docPos) { + _mergeAdjacentParts() { let prevPart; for (let i = 0; i < this._parts.length; ++i) { let part = this._parts[i]; @@ -339,19 +341,32 @@ export default class EditorModel { return new DocumentPosition(index, totalOffset - currentOffset); } -} -class DocumentPosition { - constructor(index, offset) { - this._index = index; - this._offset = offset; + startRange(position) { + return new Range(this, position); } - get index() { - return this._index; - } - - get offset() { - return this._offset; + // called from Range.replace + replaceRange(startPosition, endPosition, parts) { + const newStartPartIndex = this._splitAt(startPosition); + const idxDiff = newStartPartIndex - startPosition.index; + // if both position are in the same part, and we split it at start position, + // the offset of the end position needs to be decreased by the offset of the start position + const removedOffset = startPosition.index === endPosition.index ? startPosition.offset : 0; + const adjustedEndPosition = new DocumentPosition( + endPosition.index + idxDiff, + endPosition.offset - removedOffset, + ); + const newEndPartIndex = this._splitAt(adjustedEndPosition); + for (let i = newEndPartIndex - 1; i >= newStartPartIndex; --i) { + this._removePart(i); + } + let insertIdx = newStartPartIndex; + for (const part of parts) { + this._insertPart(insertIdx, part); + insertIdx += 1; + } + this._mergeAdjacentParts(); + this._updateCallback(); } } diff --git a/src/editor/position.js b/src/editor/position.js new file mode 100644 index 0000000000..c771482012 --- /dev/null +++ b/src/editor/position.js @@ -0,0 +1,38 @@ +/* +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. +*/ + +export default class DocumentPosition { + constructor(index, offset) { + this._index = index; + this._offset = offset; + } + + get index() { + return this._index; + } + + get offset() { + return this._offset; + } + + compare(otherPos) { + if (this._index === otherPos._index) { + return this._offset - otherPos._offset; + } else { + return this._index - otherPos._index; + } + } +} diff --git a/src/editor/range.js b/src/editor/range.js new file mode 100644 index 0000000000..e2ecc5d12b --- /dev/null +++ b/src/editor/range.js @@ -0,0 +1,53 @@ +/* +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. +*/ + +export default class Range { + constructor(model, startPosition, endPosition = startPosition) { + this._model = model; + this._start = startPosition; + this._end = endPosition; + } + + moveStart(delta) { + this._start = this._start.forwardsWhile(this._model, () => { + delta -= 1; + return delta >= 0; + }); + } + + expandBackwardsWhile(predicate) { + this._start = this._start.backwardsWhile(this._model, predicate); + } + + get text() { + let text = ""; + this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { + const t = part.text.substring(startIdx, endIdx); + text = text + t; + }); + return text; + } + + replace(parts) { + const newLength = parts.reduce((sum, part) => sum + part.text.length, 0); + let oldLength = 0; + this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { + oldLength += endIdx - startIdx; + }); + this._model.replaceRange(this._start, this._end, parts); + return newLength - oldLength; + } +} diff --git a/test/editor/range-test.js b/test/editor/range-test.js new file mode 100644 index 0000000000..5a95da952d --- /dev/null +++ b/test/editor/range-test.js @@ -0,0 +1,88 @@ +/* +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 expect from 'expect'; +import EditorModel from "../../src/editor/model"; +import {createPartCreator} from "./mock"; + +function createRenderer() { + const render = (c) => { + render.caret = c; + render.count += 1; + }; + render.count = 0; + render.caret = null; + return render; +} + +const pillChannel = "#riot-dev:matrix.org"; + +describe('editor/range', function() { + it('range on empty model', function() { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([], pc, renderer); + const range = model.startRange(model.positionForOffset(0, true)); // after "world" + let called = false; + range.expandBackwardsWhile(chr => { + called = true; + return true; + }); + expect(called).toBe(false); + expect(range.text).toBe(""); + }); + it('range replace within a part', function() { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello world!!!!")], pc, renderer); + const range = model.startRange(model.positionForOffset(11)); // after "world" + range.expandBackwardsWhile((index, offset) => model.parts[index].text[offset] !== " "); + expect(range.text).toBe("world"); + range.replace([pc.roomPill(pillChannel)]); + console.log({parts: JSON.stringify(model.serializeParts())}); + expect(model.parts[0].type).toBe("plain"); + expect(model.parts[0].text).toBe("hello "); + expect(model.parts[1].type).toBe("room-pill"); + expect(model.parts[1].text).toBe(pillChannel); + expect(model.parts[2].type).toBe("plain"); + expect(model.parts[2].text).toBe("!!!!"); + expect(model.parts.length).toBe(3); + expect(renderer.count).toBe(1); + }); + it('range replace across parts', function() { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("try to re"), + pc.plain("pla"), + pc.plain("ce "), + pc.plain("me"), + ], pc, renderer); + const range = model.startRange(model.positionForOffset(14)); // after "replace" + range.expandBackwardsWhile((index, offset) => model.parts[index].text[offset] !== " "); + expect(range.text).toBe("replace"); + console.log("range.text", {text: range.text}); + range.replace([pc.roomPill(pillChannel)]); + expect(model.parts[0].type).toBe("plain"); + expect(model.parts[0].text).toBe("try to "); + expect(model.parts[1].type).toBe("room-pill"); + expect(model.parts[1].text).toBe(pillChannel); + expect(model.parts[2].type).toBe("plain"); + expect(model.parts[2].text).toBe(" me"); + expect(model.parts.length).toBe(3); + expect(renderer.count).toBe(1); + }); +}); From 0e65f71a375fbdee100828dcd84cd6c22a448796 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 26 Aug 2019 16:00:56 +0200 Subject: [PATCH 15/22] support incrementing/decrementing doc positions with predicate --- src/editor/position.js | 69 +++++++++++++++++++++++++++++++ test/editor/position-test.js | 80 ++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 test/editor/position-test.js diff --git a/src/editor/position.js b/src/editor/position.js index c771482012..5dcb31fe65 100644 --- a/src/editor/position.js +++ b/src/editor/position.js @@ -35,4 +35,73 @@ export default class DocumentPosition { return this._index - otherPos._index; } } + + iteratePartsBetween(other, model, callback) { + if (this.index === -1 || other.index === -1) { + return; + } + const [startPos, endPos] = this.compare(other) < 0 ? [this, other] : [other, this]; + if (startPos.index === endPos.index) { + callback(model.parts[this.index], startPos.offset, endPos.offset); + } else { + const firstPart = model.parts[startPos.index]; + callback(firstPart, startPos.offset, firstPart.text.length); + for (let i = startPos.index + 1; i < endPos.index; ++i) { + const part = model.parts[i]; + callback(part, 0, part.text.length); + } + const lastPart = model.parts[endPos.index]; + callback(lastPart, 0, endPos.offset); + } + } + + forwardsWhile(model, predicate) { + if (this.index === -1) { + return this; + } + + let {index, offset} = this; + const {parts} = model; + while (index < parts.length) { + const part = parts[index]; + while (offset < part.text.length) { + if (!predicate(index, offset, part)) { + return new DocumentPosition(index, offset); + } + offset += 1; + } + // end reached + if (index === (parts.length - 1)) { + return new DocumentPosition(index, offset); + } else { + index += 1; + offset = 0; + } + } + } + + backwardsWhile(model, predicate) { + if (this.index === -1) { + return this; + } + + let {index, offset} = this; + const parts = model.parts; + while (index >= 0) { + const part = parts[index]; + while (offset > 0) { + if (!predicate(index, offset - 1, part)) { + return new DocumentPosition(index, offset); + } + offset -= 1; + } + // start reached + if (index === 0) { + return new DocumentPosition(index, offset); + } else { + index -= 1; + offset = parts[index].text.length; + } + } + } } diff --git a/test/editor/position-test.js b/test/editor/position-test.js new file mode 100644 index 0000000000..7ac4284c60 --- /dev/null +++ b/test/editor/position-test.js @@ -0,0 +1,80 @@ +/* +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 expect from 'expect'; +import EditorModel from "../../src/editor/model"; +import {createPartCreator} from "./mock"; + +function createRenderer() { + const render = (c) => { + render.caret = c; + render.count += 1; + }; + render.count = 0; + render.caret = null; + return render; +} + +describe('editor/position', function() { + it('move first position backward in empty model', function() { + const model = new EditorModel([], createPartCreator(), createRenderer()); + const pos = model.positionForOffset(0, true); + const pos2 = pos.backwardsWhile(model, () => true); + expect(pos).toBe(pos2); + }); + it('move first position forwards in empty model', function() { + const model = new EditorModel([], createPartCreator(), createRenderer()); + const pos = model.positionForOffset(0, true); + const pos2 = pos.forwardsWhile(() => true); + expect(pos).toBe(pos2); + }); + it('move forwards within one part', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello")], pc, createRenderer()); + const pos = model.positionForOffset(1); + let n = 3; + const pos2 = pos.forwardsWhile(model, () => { n -= 1; return n >= 0; }); + expect(pos2.index).toBe(0); + expect(pos2.offset).toBe(4); + }); + it('move forwards crossing to other part', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello"), pc.plain(" world")], pc, createRenderer()); + const pos = model.positionForOffset(4); + let n = 3; + const pos2 = pos.forwardsWhile(model, () => { n -= 1; return n >= 0; }); + expect(pos2.index).toBe(1); + expect(pos2.offset).toBe(2); + }); + it('move backwards within one part', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello")], pc, createRenderer()); + const pos = model.positionForOffset(4); + let n = 3; + const pos2 = pos.backwardsWhile(model, () => { n -= 1; return n >= 0; }); + expect(pos2.index).toBe(0); + expect(pos2.offset).toBe(1); + }); + it('move backwards crossing to other part', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello"), pc.plain(" world")], pc, createRenderer()); + const pos = model.positionForOffset(7); + let n = 3; + const pos2 = pos.backwardsWhile(model, () => { n -= 1; return n >= 0; }); + expect(pos2.index).toBe(0); + expect(pos2.offset).toBe(4); + }); +}); From f8f0e77bdefbc8cbc2de0def3d78c5de0eec0123 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 26 Aug 2019 16:09:46 +0200 Subject: [PATCH 16/22] add transform step during editor model update --- src/editor/model.js | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/editor/model.js b/src/editor/model.js index c5b80247f6..5584627688 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -19,6 +19,15 @@ import {diffAtCaret, diffDeletion} from "./diff"; import DocumentPosition from "./position"; import Range from "./range"; + /** + * @callback TransformCallback + * @param {DocumentPosition?} caretPosition the position where the caret should be position + * @param {string?} inputType the inputType of the DOM input event + * @param {object?} diff an object with `removed` and `added` strings + * @return {Number?} addedLen how many characters were added/removed (-) before the caret during the transformation step. + This is used to adjust the caret position. + */ + export default class EditorModel { constructor(parts, partCreator, updateCallback = null) { this._parts = parts; @@ -26,7 +35,19 @@ export default class EditorModel { this._activePartIdx = null; this._autoComplete = null; this._autoCompletePartIdx = null; + this._transformCallback = null; this.setUpdateCallback(updateCallback); + this._updateInProgress = false; + } + + /** Set a callback for the transformation step. + * While processing an update, right before calling the update callback, + * a transform callback can be called, which serves to do modifications + * on the model that can span multiple parts. Also see `startRange()`. + * @param {TransformCallback} transformCallback + */ + setTransformCallback(transformCallback) { + this._transformCallback = transformCallback; } setUpdateCallback(updateCallback) { @@ -133,6 +154,7 @@ export default class EditorModel { } update(newValue, inputType, caret) { + this._updateInProgress = true; const diff = this._diff(newValue, inputType, caret); const position = this.positionForOffset(diff.at, caret.atNodeEnd); let removedOffsetDecrease = 0; @@ -147,11 +169,23 @@ export default class EditorModel { } this._mergeAdjacentParts(); const caretOffset = diff.at - removedOffsetDecrease + addedLen; - const newPosition = this.positionForOffset(caretOffset, true); + let newPosition = this.positionForOffset(caretOffset, true); this._setActivePart(newPosition, canOpenAutoComplete); + if (this._transformCallback) { + const transformAddedLen = this._transform(newPosition, inputType, diff); + if (transformAddedLen !== 0) { + newPosition = this.positionForOffset(caretOffset + transformAddedLen, true); + } + } + this._updateInProgress = false; this._updateCallback(newPosition, inputType, diff); } + _transform(newPosition, inputType, diff) { + const result = this._transformCallback(newPosition, inputType, diff); + return Number.isFinite(result) ? result : 0; + } + _setActivePart(pos, canOpenAutoComplete) { const {index} = pos; const part = this._parts[index]; @@ -367,6 +401,8 @@ export default class EditorModel { insertIdx += 1; } this._mergeAdjacentParts(); - this._updateCallback(); + if (!this._updateInProgress) { + this._updateCallback(); + } } } From 4fd4ad41c127061c7b5440f72dc3fcd3eae721ee Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 26 Aug 2019 16:10:02 +0200 Subject: [PATCH 17/22] improve editor model documentation --- src/editor/model.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/editor/model.js b/src/editor/model.js index 5584627688..689b657f05 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -19,6 +19,13 @@ import {diffAtCaret, diffDeletion} from "./diff"; import DocumentPosition from "./position"; import Range from "./range"; +/** + * @callback ModelCallback + * @param {DocumentPosition?} caretPosition the position where the caret should be position + * @param {string?} inputType the inputType of the DOM input event + * @param {object?} diff an object with `removed` and `added` strings + */ + /** * @callback TransformCallback * @param {DocumentPosition?} caretPosition the position where the caret should be position @@ -50,6 +57,9 @@ export default class EditorModel { this._transformCallback = transformCallback; } + /** Set a callback for rerendering the model after it has been updated. + * @param {ModelCallback} updateCallback + */ setUpdateCallback(updateCallback) { this._updateCallback = updateCallback; } @@ -376,6 +386,11 @@ export default class EditorModel { return new DocumentPosition(index, totalOffset - currentOffset); } + /** + * Starts a range, which can span across multiple parts, to find and replace text. + * @param {DocumentPosition} position where to start the range + * @return {Range} + */ startRange(position) { return new Range(this, position); } From 0273795f5db86c416a2e40978c5b853a3fae26c8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 26 Aug 2019 16:10:26 +0200 Subject: [PATCH 18/22] add transform step to composer to auto-replace emoticons with emoji --- .../views/rooms/BasicMessageComposer.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index e4179d9c3b..780f39f9e7 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -25,6 +25,11 @@ import {autoCompleteCreator} from '../../../editor/parts'; import {renderModel} from '../../../editor/render'; import {Room} from 'matrix-js-sdk'; import TypingStore from "../../../stores/TypingStore"; +import EMOJIBASE from 'emojibase-data/en/compact.json'; +import SettingsStore from "../../../settings/SettingsStore"; +import EMOTICON_REGEX from 'emojibase-regex/emoticon'; + +const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); const IS_MAC = navigator.platform.indexOf("Mac") !== -1; @@ -70,6 +75,28 @@ export default class BasicMessageEditor extends React.Component { this._modifiedFlag = false; } + _replaceEmoticon = (caret, inputType, diff) => { + const {model} = this.props; + const range = model.startRange(caret); + // expand range max 8 characters backwards from caret + let n = 8; + range.expandBackwardsWhile((index, offset) => { + const part = model.parts[index]; + n -= 1; + return n >= 0 && (part.type === "plain" || part.type === "pill-candidate"); + }); + const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text); + if (emoticonMatch) { + const query = emoticonMatch[1].toLowerCase().replace("-", ""); + const data = EMOJIBASE.find(e => e.emoticon ? e.emoticon.toLowerCase() === query : false); + if (data) { + // + 1 because index is reported without preceding space + range.moveStart(emoticonMatch.index + 1); + return range.replace([this.props.model.partCreator.plain(data.unicode + " ")]); + } + } + } + _updateEditorState = (caret, inputType, diff) => { renderModel(this._editorRef, this.props.model); if (caret) { @@ -262,6 +289,9 @@ export default class BasicMessageEditor extends React.Component { componentDidMount() { const model = this.props.model; model.setUpdateCallback(this._updateEditorState); + if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) { + model.setTransformCallback(this._replaceEmoticon); + } const partCreator = model.partCreator; // TODO: does this allow us to get rid of EditorStateTransfer? // not really, but we could not serialize the parts, and just change the autoCompleter From abbc8ffef06dfde98102bc3a02024d5065c03e62 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 26 Aug 2019 11:25:50 -0600 Subject: [PATCH 19/22] Adjust copy and include identity server changing when terms are pending Fixes https://github.com/vector-im/riot-web/issues/10636 Fixes https://github.com/vector-im/riot-web/issues/10635 --- src/components/views/settings/SetIdServer.js | 33 ++++++++++++++++--- .../tabs/user/GeneralUserSettingsTab.js | 21 ++++++++---- src/i18n/strings/en_EN.json | 3 ++ 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index ba405a5652..55dc3b6e94 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -16,6 +16,7 @@ limitations under the License. import url from 'url'; import React from 'react'; +import PropTypes from 'prop-types'; import {_t} from "../../../languageHandler"; import sdk from '../../../index'; import MatrixClientPeg from "../../../MatrixClientPeg"; @@ -55,6 +56,12 @@ async function checkIdentityServerUrl(u) { } export default class SetIdServer extends React.Component { + static propTypes = { + // Whether or not the ID server is missing terms. This affects the text + // shown to the user. + missingTerms: PropTypes.bool, + }; + constructor() { super(); @@ -274,6 +281,13 @@ export default class SetIdServer extends React.Component { {}, { server: sub => {abbreviateUrl(idServerUrl)} }, ); + if (this.props.missingTerms) { + bodyText = _t( + "If you don't want to use to discover and be discoverable by existing " + + "contacts you know, enter another identity server below.", + {}, {server: sub => {abbreviateUrl(idServerUrl)}}, + ); + } } else { sectionTitle = _t("Identity Server"); bodyText = _t( @@ -286,16 +300,25 @@ export default class SetIdServer extends React.Component { let discoSection; if (idServerUrl) { let discoButtonContent = _t("Disconnect"); + let discoBodyText = _t( + "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.", + ); + if (this.props.missingTerms) { + discoBodyText = _t( + "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.", + ); + discoButtonContent = _t("Do not use an identity server"); + } if (this.state.disconnectBusy) { const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); discoButtonContent = ; } discoSection =
- {_t( - "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.", - )} + {discoBodyText} {discoButtonContent} diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 3d78afd222..9c37730fc5 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -249,6 +249,8 @@ export default class GeneralUserSettingsTab extends React.Component { } _renderDiscoverySection() { + const SetIdServer = sdk.getComponent("views.settings.SetIdServer"); + if (this.state.requiredPolicyInfo.hasTerms) { const InlineTermsAgreement = sdk.getComponent("views.terms.InlineTermsAgreement"); const intro = @@ -258,17 +260,22 @@ export default class GeneralUserSettingsTab extends React.Component { {serverName: this.state.idServerName}, )} ; - return ; + return ( +
+ + { /* has its own heading as it includes the current ID server */ } + +
+ ); } const EmailAddresses = sdk.getComponent("views.settings.discovery.EmailAddresses"); const PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers"); - const SetIdServer = sdk.getComponent("views.settings.SetIdServer"); const threepidSection = this.state.haveIdServer ?
{_t("Email addresses")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a0c98a0335..1c3c75ebd5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -559,9 +559,12 @@ "Disconnect": "Disconnect", "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.", + "If you don't want to use to discover and be discoverable by existing contacts you know, enter another 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.", "Identity Server": "Identity Server", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one 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.": "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.": "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.", + "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", From 5c28b57681e0ff75194a15cd180000f3886e481c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 09:49:22 +0200 Subject: [PATCH 20/22] always recalculate position after doing transform step as the amount of characters might not have changed, parts may still have been merged, removed or added which requires a new position. --- src/editor/model.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/editor/model.js b/src/editor/model.js index 689b657f05..d0f1be7158 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -183,9 +183,7 @@ export default class EditorModel { this._setActivePart(newPosition, canOpenAutoComplete); if (this._transformCallback) { const transformAddedLen = this._transform(newPosition, inputType, diff); - if (transformAddedLen !== 0) { - newPosition = this.positionForOffset(caretOffset + transformAddedLen, true); - } + newPosition = this.positionForOffset(caretOffset + transformAddedLen, true); } this._updateInProgress = false; this._updateCallback(newPosition, inputType, diff); From 56606a46f4b120065957ca020122e7da6edc5bc2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 09:50:36 +0200 Subject: [PATCH 21/22] don't assume preceding space for emoticon at start of document also add more inline comments to explain what is going on --- src/components/views/rooms/BasicMessageComposer.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 780f39f9e7..662167b714 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -78,7 +78,8 @@ export default class BasicMessageEditor extends React.Component { _replaceEmoticon = (caret, inputType, diff) => { const {model} = this.props; const range = model.startRange(caret); - // expand range max 8 characters backwards from caret + // expand range max 8 characters backwards from caret, + // as a space to look for an emoticon let n = 8; range.expandBackwardsWhile((index, offset) => { const part = model.parts[index]; @@ -90,8 +91,14 @@ export default class BasicMessageEditor extends React.Component { const query = emoticonMatch[1].toLowerCase().replace("-", ""); const data = EMOJIBASE.find(e => e.emoticon ? e.emoticon.toLowerCase() === query : false); if (data) { - // + 1 because index is reported without preceding space - range.moveStart(emoticonMatch.index + 1); + const hasPrecedingSpace = emoticonMatch[0][0] === " "; + // we need the range to only comprise of the emoticon + // because we'll replace the whole range with an emoji, + // so move the start forward to the start of the emoticon. + // Take + 1 because index is reported without the possible preceding space. + range.moveStart(emoticonMatch.index + (hasPrecedingSpace ? 1 : 0)); + // this returns the amount of added/removed characters during the replace + // so the caret position can be adjusted. return range.replace([this.props.model.partCreator.plain(data.unicode + " ")]); } } From f10e1d76549b1cf7972ec951741521088739ebd3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 09:54:13 +0200 Subject: [PATCH 22/22] fix jsdoc comments --- src/editor/model.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/editor/model.js b/src/editor/model.js index d0f1be7158..9d129afa69 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -32,7 +32,7 @@ import Range from "./range"; * @param {string?} inputType the inputType of the DOM input event * @param {object?} diff an object with `removed` and `added` strings * @return {Number?} addedLen how many characters were added/removed (-) before the caret during the transformation step. - This is used to adjust the caret position. + * This is used to adjust the caret position. */ export default class EditorModel { @@ -47,7 +47,8 @@ export default class EditorModel { this._updateInProgress = false; } - /** Set a callback for the transformation step. + /** + * Set a callback for the transformation step. * While processing an update, right before calling the update callback, * a transform callback can be called, which serves to do modifications * on the model that can span multiple parts. Also see `startRange()`. @@ -57,7 +58,8 @@ export default class EditorModel { this._transformCallback = transformCallback; } - /** Set a callback for rerendering the model after it has been updated. + /** + * Set a callback for rerendering the model after it has been updated. * @param {ModelCallback} updateCallback */ setUpdateCallback(updateCallback) {