From 76f4f88fcd527ad2393f7f6cb249ad2592b250b6 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 26 Jul 2017 11:28:43 +0100 Subject: [PATCH 01/83] App tile permissions -- broken --- .../views/elements/AppPermission.js | 43 +++++++++++++++++ src/components/views/elements/AppTile.js | 47 +++++++++++++------ 2 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 src/components/views/elements/AppPermission.js diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js new file mode 100644 index 0000000000..a6ecd7b5f7 --- /dev/null +++ b/src/components/views/elements/AppPermission.js @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { URL, URLSearchParams } from 'url'; + +export default class AppPermission extends React.Component { + constructor(props) { + super(props); + + this.state = { + curl: this.getCurl(), + }; + } + + getCurl() { + let wurl = URL.parse(this.props.url); + console.log('wurl', wurl); + if(wurl.searchParams.get('url')) { + let curl = wurl.searchParams.get('url'); + console.log('curl', curl); + } + } + + render() { + return ( +
+ Load widget with URL : {this.state.cUrl} + +
+ ); + } +} + +AppPermission.propTypes = { + url: PropTypes.string.isRequired, + onPermissionGranted: PropTypes.func.isRequired, +}; +AppPermission.defaultPropTypes = { + onPermissionGranted: function() {}, +}; diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 9573b9fd9f..994b613b41 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -24,6 +24,7 @@ import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; +import AppPermission from './AppPermission'; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const betaHelpMsg = 'This feature is currently experimental and is intended for beta testing only'; @@ -46,9 +47,12 @@ export default React.createClass({ }, getInitialState: function() { + const widgetPermissionId = [this.props.room.roomId, encodeURIComponent(this.props.url)].join('_'); return { loading: false, widgetUrl: this.props.url, + widgetPermissionId: widgetPermissionId, + hasPermissionToLoad: localStorage.getItem(widgetPermissionId), error: null, deleting: false, }; @@ -116,6 +120,11 @@ export default React.createClass({ }); }, + _grantWidgetPermission() { + console.warn('Granting permission to load widget - ', this.state.widgetUrl); + localStorage.setItem(this.state.widgetPermissionId, true); + }, + formatAppTileName: function() { let appTileName = "No name"; if(this.props.name && this.props.name.trim()) { @@ -133,30 +142,40 @@ export default React.createClass({ return
; } + // 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 + // 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. + const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+ + "allow-same-origin allow-scripts"; + const parsedWidgetUrl = url.parse(this.state.widgetUrl); + let safeWidgetUrl = ''; + if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) { + safeWidgetUrl = url.format(parsedWidgetUrl); + } + if (this.state.loading) { appTileBody = (
Loading...
); - } else { - // 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 - // 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. - const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+ - "allow-same-origin allow-scripts"; - const parsedWidgetUrl = url.parse(this.state.widgetUrl); - let safeWidgetUrl = ''; - if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) { - safeWidgetUrl = url.format(parsedWidgetUrl); - } + } else if (this.state.hasPermissionToLoad === true) { appTileBody = (
-
); + } else { + appTileBody = ( + + ); } // editing is done in scalar From 9f52c13bea3f706ba004a61b673427d2bc292156 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 26 Jul 2017 16:47:58 +0100 Subject: [PATCH 02/83] Grant permission to load app widget. --- .../views/elements/AppPermission.js | 29 +++++++++++++------ src/components/views/elements/AppTile.js | 1 + 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js index a6ecd7b5f7..ad697e2df0 100644 --- a/src/components/views/elements/AppPermission.js +++ b/src/components/views/elements/AppPermission.js @@ -1,30 +1,41 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { URL, URLSearchParams } from 'url'; +import url from 'url'; export default class AppPermission extends React.Component { constructor(props) { super(props); + const curl = this.getCurl(); this.state = { - curl: this.getCurl(), + curl: curl, }; + console.log('curl', curl); } getCurl() { - let wurl = URL.parse(this.props.url); - console.log('wurl', wurl); - if(wurl.searchParams.get('url')) { - let curl = wurl.searchParams.get('url'); - console.log('curl', curl); + const wurl = url.parse(this.props.url); + let curl; + + const searchParams = new URLSearchParams(wurl.search); + if(searchParams && searchParams.get('url')) { + curl = searchParams.get('url'); } + curl = curl || wurl; + return curl; } render() { return ( -
- Load widget with URL : {this.state.cUrl} +
+
+ Warning +
+
+ Do you want to load widget from URL?: {this.state.curl} +
); } From f2058e0a6cb053ab6b3a5d350aef149370fde042 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Thu, 27 Jul 2017 16:41:20 +0100 Subject: [PATCH 03/83] Add message spinner component. --- .../views/elements/MessageSpinner.js | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/components/views/elements/MessageSpinner.js diff --git a/src/components/views/elements/MessageSpinner.js b/src/components/views/elements/MessageSpinner.js new file mode 100644 index 0000000000..93995a83dc --- /dev/null +++ b/src/components/views/elements/MessageSpinner.js @@ -0,0 +1,36 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +const React = require('react'); + +module.exports = React.createClass({ + displayName: 'MessageSpinner', + + render: function() { + const w = this.props.w || 32; + const h = this.props.h || 32; + const imgClass = this.props.imgClassName || ""; + const msg = this.props.msg || "Loading..."; + return ( +
+
{msg}
  + +
+ ); + }, +}); From 8e4f1f09899ded2d7ae0c0b4971c0cc104a88d27 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Thu, 27 Jul 2017 16:41:52 +0100 Subject: [PATCH 04/83] Add message spinner. --- src/components/views/elements/AppTile.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 522db7399e..36954ff25f 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -25,6 +25,7 @@ import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import AppPermission from './AppPermission'; +import MessageSpinner from './MessageSpinner'; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const betaHelpMsg = 'This feature is currently experimental and is intended for beta testing only'; @@ -38,6 +39,7 @@ export default React.createClass({ name: React.PropTypes.string.isRequired, room: React.PropTypes.object.isRequired, type: React.PropTypes.string.isRequired, + fullWidth: React.PropTypes.bool, }, getDefaultProps: function() { @@ -48,11 +50,12 @@ export default React.createClass({ getInitialState: function() { const widgetPermissionId = [this.props.room.roomId, encodeURIComponent(this.props.url)].join('_'); + const hasPermissionToLoad = localStorage.getItem(widgetPermissionId); return { loading: false, widgetUrl: this.props.url, widgetPermissionId: widgetPermissionId, - hasPermissionToLoad: localStorage.getItem(widgetPermissionId), + hasPermissionToLoad: Boolean(hasPermissionToLoad === 'true'), error: null, deleting: false, }; @@ -123,6 +126,7 @@ export default React.createClass({ _grantWidgetPermission() { console.warn('Granting permission to load widget - ', this.state.widgetUrl); localStorage.setItem(this.state.widgetPermissionId, true); + this.setState({hasPermissionToLoad: true}); }, formatAppTileName: function() { @@ -157,9 +161,11 @@ export default React.createClass({ if (this.state.loading) { appTileBody = ( -
Loading...
+
+ +
); - } else if (this.state.hasPermissionToLoad === true) { + } else if (this.state.hasPermissionToLoad == true) { appTileBody = (
); + } else if (this.isMixedContent() == true) { + appTileBody = ( +
+ +
+ ); } else { appTileBody = (
diff --git a/src/components/views/elements/AppWarning.js b/src/components/views/elements/AppWarning.js new file mode 100644 index 0000000000..527ee087d6 --- /dev/null +++ b/src/components/views/elements/AppWarning.js @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { _t } from '../../../languageHandler'; + +function AppWarning(props) { + return ( +
+
+ {_t('Warning!')}/ +
+
+ {props.errorMsg} +
+
+ ); +} + +AppWarning.propTypes = { + errorMsg: PropTypes.string, +}; +AppWarning.defaultProps = { + errorMsg: _t('Error'), +}; + +export default AppWarning; From ff955425493988b457584a36a1275437efced63f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 1 Aug 2017 17:36:41 +0100 Subject: [PATCH 43/83] Adjust emoji sorting such that exact matches/prefixes appear first fixes https://github.com/vector-im/riot-web/issues/4704 --- src/autocomplete/EmojiProvider.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index d58587d423..d0257de3e8 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -71,6 +71,15 @@ const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sor let instance = null; +function score(query, space) { + const index = space.indexOf(query); + if (index === -1) { + return Infinity; + } else { + return index; + } +} + export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); @@ -104,8 +113,20 @@ export default class EmojiProvider extends AutocompleteProvider { // Do second match with shouldMatchWordsOnly in order to match against 'name' completions = completions.concat(this.nameMatcher.match(matchedString)); - // Reinstate original order - completions = _sortBy(_uniq(completions), '_orderBy'); + + const sorters = []; + // First, sort by score (Infinity if query not in shortname) + sorters.push((c) => score(query, c.shortname)); + // If the query is not empty, sort by length of shortname. Example: + // query = ":bookmark" + // completions = [":bookmark:", ":bookmark_tabs:", ...] + if (query.length > 1) { + sorters.push((c) => c.shortname.length); + } + // Finally, sort by original ordering + sorters.push((c) => c._orderBy); + completions = _sortBy(_uniq(completions), sorters); + completions = completions.map((result) => { const {shortname} = result; const unicode = shortnameToUnicode(shortname); From f57b0d4cc7521f06b09406aaaadfbb08a8085db7 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 1 Aug 2017 17:43:38 +0100 Subject: [PATCH 44/83] Fix invalid translation --- src/components/views/elements/AppWarning.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AppWarning.js b/src/components/views/elements/AppWarning.js index 527ee087d6..372535e069 100644 --- a/src/components/views/elements/AppWarning.js +++ b/src/components/views/elements/AppWarning.js @@ -19,7 +19,7 @@ AppWarning.propTypes = { errorMsg: PropTypes.string, }; AppWarning.defaultProps = { - errorMsg: _t('Error'), + errorMsg: 'Error', }; export default AppWarning; From d29610bdd21aef970d62e493fe5e079d2914f2f4 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 1 Aug 2017 17:45:06 +0100 Subject: [PATCH 45/83] Fix boolean comparison. --- src/components/views/elements/AppTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 22c1300c64..1119cf0387 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -219,7 +219,7 @@ export default React.createClass({ >
); - } else if (this.isMixedContent() == true) { + } else if (this.isMixedContent()) { appTileBody = (
Date: Tue, 1 Aug 2017 17:48:02 +0100 Subject: [PATCH 46/83] Fix comparison and handle case where app has permission to load but content is mixed protocol. --- src/components/views/elements/AppTile.js | 38 +++++++++++++----------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 1119cf0387..2aebc217f5 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -209,24 +209,26 @@ export default React.createClass({
); } else if (this.state.hasPermissionToLoad == true) { - appTileBody = ( -
- -
- ); - } else if (this.isMixedContent()) { - appTileBody = ( -
- -
- ); + if (this.isMixedContent()) { + appTileBody = ( +
+ +
+ ); + } else { + appTileBody = ( +
+ +
+ ); + } } else { appTileBody = (
From 2ab6bc84a79135d23006506642dc5f3c8fdfcc93 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 1 Aug 2017 17:49:41 +0100 Subject: [PATCH 47/83] Improve clarity --- src/components/views/elements/AppTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 2aebc217f5..ce64d2ff22 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -75,7 +75,7 @@ export default React.createClass({ const parentContentProtocol = window.location.protocol; const u = url.parse(this.props.url); const childContentProtocol = u.protocol; - if (parentContentProtocol === 'https:' && childContentProtocol !== parentContentProtocol) { + if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') { console.warn("Refusing to load mixed-content app:", parentContentProtocol, childContentProtocol, window.location, this.props.url); return true; } From 48faf72fdc992d0e39219caf6b96dc1658a52a59 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 1 Aug 2017 21:00:18 +0100 Subject: [PATCH 48/83] Disable eslint rule --- src/components/views/elements/AppWarning.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/AppWarning.js b/src/components/views/elements/AppWarning.js index 372535e069..944f1422e6 100644 --- a/src/components/views/elements/AppWarning.js +++ b/src/components/views/elements/AppWarning.js @@ -1,8 +1,8 @@ -import React from 'react'; +import React from 'react'; // eslint-disable-line no-unused-vars import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; -function AppWarning(props) { +const AppWarning = (props) => { return (
@@ -13,7 +13,7 @@ function AppWarning(props) {
); -} +}; AppWarning.propTypes = { errorMsg: PropTypes.string, From 8053d2933aff9c0016274626f7c359e6366d15ce Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 2 Aug 2017 09:35:07 +0100 Subject: [PATCH 49/83] Order room completions more intuitively by index of the query in displayedAlias and then length of displayedAlias. (So that aliases where the query appears earlier in the string appear first and if the query is in the same index for two aliases, the shorter one appears first). --- src/autocomplete/RoomProvider.js | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 09d2d79833..44018fc7b8 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -23,35 +23,51 @@ import FuzzyMatcher from './FuzzyMatcher'; import {PillCompletion} from './Components'; import {getDisplayAliasForRoom} from '../Rooms'; import sdk from '../index'; +import _sortBy from 'lodash/sortBy'; const ROOM_REGEX = /(?=#)(\S*)/g; let instance = null; +function score(query, space) { + const index = space.indexOf(query); + if (index === -1) { + return Infinity; + } else { + return index; + } +} + export default class RoomProvider extends AutocompleteProvider { constructor() { super(ROOM_REGEX); this.matcher = new FuzzyMatcher([], { - keys: ['name', 'roomId', 'aliases'], + keys: ['displayedAlias', 'name', 'roomId'], }); } async getCompletions(query: string, selection: {start: number, end: number}, force = false) { const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); - let client = MatrixClientPeg.get(); + const client = MatrixClientPeg.get(); let completions = []; const {command, range} = this.getCurrentCommand(query, selection, force); if (command) { // the only reason we need to do this is because Fuse only matches on properties - this.matcher.setObjects(client.getRooms().filter(room => !!room && !!getDisplayAliasForRoom(room)).map(room => { + this.matcher.setObjects(client.getRooms().filter( + (room) => !!room && !!getDisplayAliasForRoom(room), + ).map((room) => { return { room: room, name: room.name, - aliases: room.getAliases(), + displayedAlias: getDisplayAliasForRoom(room), }; })); - completions = this.matcher.match(command[0]).map(room => { + completions = this.matcher.match(command[0]); + completions = _sortBy(completions, [ + (c) => score(query, c.displayedAlias), + (c) => c.displayedAlias.length, + ]).map((room) => { const displayAlias = getDisplayAliasForRoom(room.room) || room.roomId; return { completion: displayAlias, @@ -62,7 +78,9 @@ export default class RoomProvider extends AutocompleteProvider { ), range, }; - }).filter(completion => !!completion.completion && completion.completion.length > 0).slice(0, 4); + }) + .filter((completion) => !!completion.completion && completion.completion.length > 0) + .slice(0, 4); } return completions; } From dbade448c1dca9f5883473c31fbf1bd61d532ea2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 2 Aug 2017 09:40:00 +0100 Subject: [PATCH 50/83] Don't try to match query against roomId We only care about aliases. --- src/autocomplete/RoomProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 44018fc7b8..5a8fd0a0b5 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -42,7 +42,7 @@ export default class RoomProvider extends AutocompleteProvider { constructor() { super(ROOM_REGEX); this.matcher = new FuzzyMatcher([], { - keys: ['displayedAlias', 'name', 'roomId'], + keys: ['displayedAlias', 'name'], }); } From 72c1cf9288554c37bfcaba9291df9ea87be1bb45 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 2 Aug 2017 10:09:00 +0100 Subject: [PATCH 51/83] When sorting completions, use matched string, not entire query Otherwise the results vary depending on where you start autocompleting in your message. We only care about the matched string. --- src/autocomplete/EmojiProvider.js | 10 +++++----- src/autocomplete/RoomProvider.js | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index d0257de3e8..16e0347a5b 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -115,12 +115,12 @@ export default class EmojiProvider extends AutocompleteProvider { completions = completions.concat(this.nameMatcher.match(matchedString)); const sorters = []; - // First, sort by score (Infinity if query not in shortname) - sorters.push((c) => score(query, c.shortname)); - // If the query is not empty, sort by length of shortname. Example: - // query = ":bookmark" + // First, sort by score (Infinity if matchedString not in shortname) + sorters.push((c) => score(matchedString, c.shortname)); + // If the matchedString is not empty, sort by length of shortname. Example: + // matchedString = ":bookmark" // completions = [":bookmark:", ":bookmark_tabs:", ...] - if (query.length > 1) { + if (matchedString.length > 1) { sorters.push((c) => c.shortname.length); } // Finally, sort by original ordering diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 5a8fd0a0b5..3749e7e693 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -63,9 +63,10 @@ export default class RoomProvider extends AutocompleteProvider { displayedAlias: getDisplayAliasForRoom(room), }; })); - completions = this.matcher.match(command[0]); + const matchedString = command[0]; + completions = this.matcher.match(matchedString); completions = _sortBy(completions, [ - (c) => score(query, c.displayedAlias), + (c) => score(matchedString, c.displayedAlias), (c) => c.displayedAlias.length, ]).map((room) => { const displayAlias = getDisplayAliasForRoom(room.room) || room.roomId; From 2c86086444f2b9977198ba20b20e0f2dae403120 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 2 Aug 2017 10:51:34 +0100 Subject: [PATCH 52/83] Account for `\n` after each block when converting from text offsets to selection state. fixes vector-im/riot-web#4728 --- src/RichText.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/RichText.js b/src/RichText.js index c060565e2f..ff223525d4 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -201,10 +201,8 @@ export function selectionStateToTextOffsets(selectionState: SelectionState, export function textOffsetsToSelectionState({start, end}: SelectionRange, contentBlocks: Array): SelectionState { let selectionState = SelectionState.createEmpty(); - - for (let block of contentBlocks) { - let blockLength = block.getLength(); - + for (const block of contentBlocks) { + const blockLength = block.getLength(); if (start !== -1 && start < blockLength) { selectionState = selectionState.merge({ anchorKey: block.getKey(), @@ -212,9 +210,8 @@ export function textOffsetsToSelectionState({start, end}: SelectionRange, }); start = -1; } else { - start -= blockLength; + start -= blockLength + 1; } - if (end !== -1 && end <= blockLength) { selectionState = selectionState.merge({ focusKey: block.getKey(), @@ -222,10 +219,9 @@ export function textOffsetsToSelectionState({start, end}: SelectionRange, }); end = -1; } else { - end -= blockLength; + end -= blockLength + 1; } } - return selectionState; } From 1512aff326d41157c8861dd782163fd958569e39 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 2 Aug 2017 11:06:02 +0100 Subject: [PATCH 53/83] Add comments --- src/RichText.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/RichText.js b/src/RichText.js index ff223525d4..225a1c212a 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -210,7 +210,7 @@ export function textOffsetsToSelectionState({start, end}: SelectionRange, }); start = -1; } else { - start -= blockLength + 1; + start -= blockLength + 1; // +1 to account for newline between blocks } if (end !== -1 && end <= blockLength) { selectionState = selectionState.merge({ @@ -219,7 +219,7 @@ export function textOffsetsToSelectionState({start, end}: SelectionRange, }); end = -1; } else { - end -= blockLength + 1; + end -= blockLength + 1; // +1 to account for newline between blocks } } return selectionState; From c914f1607beca31a7bd0504c52744388e0904753 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 2 Aug 2017 14:35:14 +0100 Subject: [PATCH 54/83] scalar-develop is a scalar URL --- src/components/views/elements/AppPermission.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js index dbdf74dbbc..083a7cd9c7 100644 --- a/src/components/views/elements/AppPermission.js +++ b/src/components/views/elements/AppPermission.js @@ -37,6 +37,7 @@ export default class AppPermission extends React.Component { if(wurl && wurl.hostname && ( wurl.hostname === 'scalar.vector.im' || wurl.hostname === 'scalar-staging.riot.im' || + wurl.hostname === 'scalar-develop.riot.im' || wurl.hostname === 'demo.riot.im' || wurl.hostname === 'localhost' )) { From 7599bde1f6a48ab51fad8f7aeead81c7de74c7f0 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 2 Aug 2017 17:05:46 +0100 Subject: [PATCH 55/83] Fix logging line length. --- src/components/views/elements/AppTile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index ce64d2ff22..2fcacaaee2 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -76,7 +76,8 @@ export default React.createClass({ const u = url.parse(this.props.url); const childContentProtocol = u.protocol; if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') { - console.warn("Refusing to load mixed-content app:", parentContentProtocol, childContentProtocol, window.location, this.props.url); + console.warn("Refusing to load mixed-content app:", + parentContentProtocol, childContentProtocol, window.location, this.props.url); return true; } return false; From 4f0cf7d6ecc39799ba8c0f6ea3b1c3bd173c68cb Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 3 Aug 2017 11:16:32 +0100 Subject: [PATCH 56/83] Update npm dep of draft-js to 0.11.0-alpha --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a3bab88d45..496d8a7de6 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "classnames": "^2.1.2", "commonmark": "^0.27.0", "counterpart": "^0.18.0", - "draft-js": "^0.10.1", + "draft-js": "^0.11.0-alpha", "draft-js-export-html": "^0.5.0", "draft-js-export-markdown": "^0.2.0", "emojione": "2.2.7", From 124795006ce071e1e3faa6a622c9adfc7ab1f6a2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 3 Aug 2017 11:18:56 +0100 Subject: [PATCH 57/83] Reflect API change for creating an Entity --- src/RichText.js | 5 ++++- .../views/rooms/MessageComposerInput.js | 15 +++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/RichText.js b/src/RichText.js index 225a1c212a..6f7d143e04 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -248,7 +248,10 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor .set('anchorOffset', start) .set('focusOffset', end); const emojiText = plainText.substring(start, end); - const entityKey = Entity.create('emoji', 'IMMUTABLE', { emojiUnicode: emojiText }); + newContentState = newContentState.createEntity( + 'emoji', 'IMMUTABLE', { emojiUnicode: emojiText } + ); + const entityKey = newContentState.getLastCreatedEntityKey(); newContentState = Modifier.replaceText( newContentState, selection, diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 743caf3a76..222a473a23 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -936,32 +936,27 @@ export default class MessageComposerInput extends React.Component { } const {range = null, completion = '', href = null, suffix = ''} = displayedCompletion; + let contentState = activeEditorState.getCurrentContent(); let entityKey; - let mdCompletion; if (href) { - entityKey = Entity.create('LINK', 'IMMUTABLE', { + contentState = contentState.createEntity('LINK', 'IMMUTABLE', { url: href, isCompletion: true, }); + entityKey = contentState.getLastCreatedEntityKey(); } let selection; if (range) { selection = RichText.textOffsetsToSelectionState( - range, activeEditorState.getCurrentContent().getBlocksAsArray(), + range, contentState.getBlocksAsArray(), ); } else { selection = activeEditorState.getSelection(); } - let contentState = Modifier.replaceText( - activeEditorState.getCurrentContent(), - selection, - mdCompletion || completion, - null, - entityKey, - ); + contentState = Modifier.replaceText(contentState, selection, completion, null, entityKey); // Move the selection to the end of the block const afterSelection = contentState.getSelectionAfter(); From fb5dc295aa1dd109c09b119bf2c38ea47e3169d9 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 3 Aug 2017 11:29:26 +0100 Subject: [PATCH 58/83] Reflect API change for getting an Entity --- src/RichText.js | 2 +- .../views/rooms/MessageComposerInput.js | 36 ++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/RichText.js b/src/RichText.js index 6f7d143e04..5f37d89c6f 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -238,7 +238,7 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor const existingEntityKey = block.getEntityAt(start); if (existingEntityKey) { // avoid manipulation in case the emoji already has an entity - const entity = Entity.get(existingEntityKey); + const entity = newContentState.getEntity(existingEntityKey); if (entity && entity.get('type') === 'emoji') { return; } diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 222a473a23..669156ba7a 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -165,17 +165,20 @@ export default class MessageComposerInput extends React.Component { this.client = MatrixClientPeg.get(); } - findLinkEntities(contentBlock, callback) { - contentBlock.findEntityRanges( - (character) => { - const entityKey = character.getEntity(); - return ( - entityKey !== null && - Entity.get(entityKey).getType() === 'LINK' - ); - }, callback, - ); + getLinkFindingStrategy(contentState: ContentState) { + return (contentBlock, callback) => { + contentBlock.findEntityRanges( + (character) => { + const entityKey = character.getEntity(); + return ( + entityKey !== null && + contentState.getEntity(entityKey).getType() === 'LINK' + ); + }, callback, + ); + }; } + /* * "Does the right thing" to create an EditorState, based on: * - whether we've got rich text mode enabled @@ -185,10 +188,10 @@ export default class MessageComposerInput extends React.Component { const decorators = richText ? RichText.getScopedRTDecorators(this.props) : RichText.getScopedMDDecorators(this.props); decorators.push({ - strategy: this.findLinkEntities.bind(this), + strategy: this.getLinkFindingStrategy(contentState), component: (entityProps) => { const Pill = sdk.getComponent('elements.Pill'); - const {url} = Entity.get(entityProps.entityKey).getData(); + const {url} = contentState.getEntity(entityProps.entityKey).getData(); if (Pill.isPillUrl(url)) { return ; } @@ -713,7 +716,7 @@ export default class MessageComposerInput extends React.Component { const hasLink = blocks.some((block) => { return block.getCharacterList().filter((c) => { const entityKey = c.getEntity(); - return entityKey && Entity.get(entityKey).getType() === 'LINK'; + return entityKey && contentState.getEntity(entityKey).getType() === 'LINK'; }).size > 0; }); shouldSendHTML = hasLink; @@ -724,6 +727,7 @@ export default class MessageComposerInput extends React.Component { ); } } else { + const findLinkEntities = this.getLinkFindingStrategy(contentState); // Use the original contentState because `contentText` has had mentions // stripped and these need to end up in contentHTML. @@ -734,8 +738,8 @@ export default class MessageComposerInput extends React.Component { const pt = contentState.getBlocksAsArray().map((block) => { let blockText = block.getText(); let offset = 0; - this.findLinkEntities(block, (start, end) => { - const entity = Entity.get(block.getEntityAt(start)); + findLinkEntities(block, (start, end) => { + const entity = contentState.getEntity(block.getEntityAt(start)); if (entity.getType() !== 'LINK') { return; } @@ -1042,7 +1046,7 @@ export default class MessageComposerInput extends React.Component { offset -= sum; const entityKey = block.getEntityAt(offset); - const entity = entityKey ? Entity.get(entityKey) : null; + const entity = entityKey ? contentState.getEntity(entityKey) : null; if (entity && entity.getData().isCompletion) { // This is a completed mention, so do not insert MD link, just text return text; From 1d1cd5f691f57c388fb2ba2c0c8fc6c317a707e1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 3 Aug 2017 11:36:07 +0100 Subject: [PATCH 59/83] Reflect API change for decorator strategy --- .../views/rooms/MessageComposerInput.js | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 669156ba7a..18b2424106 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -165,18 +165,16 @@ export default class MessageComposerInput extends React.Component { this.client = MatrixClientPeg.get(); } - getLinkFindingStrategy(contentState: ContentState) { - return (contentBlock, callback) => { - contentBlock.findEntityRanges( - (character) => { - const entityKey = character.getEntity(); - return ( - entityKey !== null && - contentState.getEntity(entityKey).getType() === 'LINK' - ); - }, callback, - ); - }; + findLinkEntities(contentBlock: ContentBlock, callback, contentState: ContentState) { + contentBlock.findEntityRanges( + (character) => { + const entityKey = character.getEntity(); + return ( + entityKey !== null && + contentState.getEntity(entityKey).getType() === 'LINK' + ); + }, callback, + ); } /* @@ -188,7 +186,7 @@ export default class MessageComposerInput extends React.Component { const decorators = richText ? RichText.getScopedRTDecorators(this.props) : RichText.getScopedMDDecorators(this.props); decorators.push({ - strategy: this.getLinkFindingStrategy(contentState), + strategy: this.findLinkEntities.bind(this), component: (entityProps) => { const Pill = sdk.getComponent('elements.Pill'); const {url} = contentState.getEntity(entityProps.entityKey).getData(); @@ -727,7 +725,6 @@ export default class MessageComposerInput extends React.Component { ); } } else { - const findLinkEntities = this.getLinkFindingStrategy(contentState); // Use the original contentState because `contentText` has had mentions // stripped and these need to end up in contentHTML. @@ -738,7 +735,7 @@ export default class MessageComposerInput extends React.Component { const pt = contentState.getBlocksAsArray().map((block) => { let blockText = block.getText(); let offset = 0; - findLinkEntities(block, (start, end) => { + this.findLinkEntities(block, (start, end) => { const entity = contentState.getEntity(block.getEntityAt(start)); if (entity.getType() !== 'LINK') { return; From 901cbf495dfce1edeed791edf1606a374e83ee5b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 3 Aug 2017 12:02:29 +0100 Subject: [PATCH 60/83] Update decorator strategy API in accordance with recent changes to 0.11.0 See https://github.com/facebook/draft-js/commit/590cdc6c54b409be750f5ad88206873218c6c289, which is a change to the API not mentioned in the migration to v0.10 notes https://draftjs.org/docs/v0-10-api-migration.html --- src/RichText.js | 6 +++--- src/components/views/rooms/MessageComposerInput.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/RichText.js b/src/RichText.js index 5f37d89c6f..6a183b31be 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -90,7 +90,7 @@ function findWithRegex(regex, contentBlock: ContentBlock, callback: (start: numb // Workaround for https://github.com/facebook/draft-js/issues/414 let emojiDecorator = { - strategy: (contentBlock, callback) => { + strategy: (contentState, contentBlock, callback) => { findWithRegex(EMOJI_REGEX, contentBlock, callback); }, component: (props) => { @@ -119,7 +119,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator { export function getScopedMDDecorators(scope: any): CompositeDecorator { let markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map( (style) => ({ - strategy: (contentBlock, callback) => { + strategy: (contentState, contentBlock, callback) => { return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback); }, component: (props) => ( @@ -130,7 +130,7 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator { })); markdownDecorators.push({ - strategy: (contentBlock, callback) => { + strategy: (contentState, contentBlock, callback) => { return findWithRegex(MARKDOWN_REGEX.LINK, contentBlock, callback); }, component: (props) => ( diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 18b2424106..938caa0969 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -165,7 +165,7 @@ export default class MessageComposerInput extends React.Component { this.client = MatrixClientPeg.get(); } - findLinkEntities(contentBlock: ContentBlock, callback, contentState: ContentState) { + findLinkEntities(contentState: ContentState, contentBlock: ContentBlock, callback) { contentBlock.findEntityRanges( (character) => { const entityKey = character.getEntity(); @@ -189,7 +189,7 @@ export default class MessageComposerInput extends React.Component { strategy: this.findLinkEntities.bind(this), component: (entityProps) => { const Pill = sdk.getComponent('elements.Pill'); - const {url} = contentState.getEntity(entityProps.entityKey).getData(); + const {url} = entityProps.contentState.getEntity(entityProps.entityKey).getData(); if (Pill.isPillUrl(url)) { return ; } From a27eefd89338cf1546f859bdae434afc4052d30e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 3 Aug 2017 15:20:44 +0100 Subject: [PATCH 61/83] Fix a couple of more errors due to API changes --- src/RichText.js | 3 ++- src/components/views/rooms/MessageComposerInput.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/RichText.js b/src/RichText.js index 6a183b31be..9876fcc93f 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -51,7 +51,8 @@ export const contentStateToHTML = (contentState: ContentState) => { }; export function htmlToContentState(html: string): ContentState { - return ContentState.createFromBlockArray(convertFromHTML(html)); + const blockArray = convertFromHTML(html).contentBlocks; + return ContentState.createFromBlockArray(blockArray); } function unicodeToEmojiUri(str) { diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 938caa0969..c16348300f 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -735,7 +735,7 @@ export default class MessageComposerInput extends React.Component { const pt = contentState.getBlocksAsArray().map((block) => { let blockText = block.getText(); let offset = 0; - this.findLinkEntities(block, (start, end) => { + this.findLinkEntities(contentState, block, (start, end) => { const entity = contentState.getEntity(block.getEntityAt(start)); if (entity.getType() !== 'LINK') { return; From ee18ddb7003e9004ea5ad18759e65a5dfb4db6b6 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 3 Aug 2017 18:21:08 +0100 Subject: [PATCH 62/83] MD-escape URLs/alises/user IDs prior to parsing markdown So that MD characters in them do not result in formatting being applied. Fixes https://github.com/vector-im/riot-web/issues/3428 Fixes https://github.com/vector-im/riot-web/issues/4674 --- src/Markdown.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Markdown.js b/src/Markdown.js index 5730e42a09..6e735c6f0e 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -55,6 +55,25 @@ function is_multi_line(node) { return par.firstChild != par.lastChild; } +import linkifyMatrix from './linkify-matrix'; +import * as linkify from 'linkifyjs'; +linkifyMatrix(linkify); + +// Thieved from draft-js-export-markdown +function escapeMarkdown(s) { + return s.replace(/[*_`]/g, '\\$&'); +} + +// Replace URLs, room aliases and user IDs with md-escaped URLs +function linkifyMarkdown(s) { + const links = linkify.find(s); + links.forEach((l) => { + // This may replace several instances of `l.value` at once, but that's OK + s = s.replace(l.value, escapeMarkdown(l.value)); + }); + return s; +} + /** * Class that wraps commonmark, adding the ability to see whether * a given message actually uses any markdown syntax or whether @@ -62,7 +81,7 @@ function is_multi_line(node) { */ export default class Markdown { constructor(input) { - this.input = input; + this.input = linkifyMarkdown(input); const parser = new commonmark.Parser(); this.parsed = parser.parse(this.input); From fb8d6c962d5c0a00afad3d3bc1a4a5fed7a35227 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 4 Aug 2017 11:28:42 +0100 Subject: [PATCH 63/83] Use npm 6 --- jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins.sh b/jenkins.sh index a0e8d2e893..0979edfa13 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -4,7 +4,7 @@ set -e export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" -nvm use 4 +nvm use 6 set -x From 91c96c1c27684200d6ee5ed31fa93ca8039b450e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 4 Aug 2017 15:08:03 +0100 Subject: [PATCH 64/83] Update draft-js-export-* deps to be compatible with draft-js >0.10.0 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 496d8a7de6..661db4b6bc 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,8 @@ "commonmark": "^0.27.0", "counterpart": "^0.18.0", "draft-js": "^0.11.0-alpha", - "draft-js-export-html": "^0.5.0", - "draft-js-export-markdown": "^0.2.0", + "draft-js-export-html": "^0.6.0", + "draft-js-export-markdown": "^0.3.0", "emojione": "2.2.7", "file-saver": "^1.3.3", "filesize": "3.5.6", From d9d8f2055ff34b2c5e2ca7167c9728ecd3e0bb19 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 7 Aug 2017 16:23:37 +0100 Subject: [PATCH 65/83] Allow default for ctrl+shift+b, ctrl+shift+u in RTE fixes vector-im/riot-web#4750 --- src/components/views/rooms/MessageComposerInput.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index c16348300f..68df8fce57 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -104,7 +104,11 @@ export default class MessageComposerInput extends React.Component { } // Allow opening of dev tools. getDefaultKeyBinding would be 'italic' for KEY_I - if (e.keyCode === KeyCode.KEY_I && e.shiftKey && e.ctrlKey) { + // Likewise protect bold and underline (in case some browsers use these as + // shortcuts for things). + if ([KeyCode.KEY_B, KeyCode.KEY_I, KeyCode.KEY_U].includes(e.keyCode) && + e.shiftKey && e.ctrlKey + ) { // When null is returned, draft-js will NOT preventDefault, allowing dev tools // to be toggled when the editor is focussed return null; From 641fda01622754f31a7b49629a617440317c86c1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 7 Aug 2017 16:29:22 +0100 Subject: [PATCH 66/83] Adjust comment --- src/components/views/rooms/MessageComposerInput.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 68df8fce57..f2c6c3a054 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -109,8 +109,7 @@ export default class MessageComposerInput extends React.Component { if ([KeyCode.KEY_B, KeyCode.KEY_I, KeyCode.KEY_U].includes(e.keyCode) && e.shiftKey && e.ctrlKey ) { - // When null is returned, draft-js will NOT preventDefault, allowing dev tools - // to be toggled when the editor is focussed + // When null is returned, draft-js will NOT preventDefault return null; } From 7018deee44da83472be19145cc8f8612ae5d8d32 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 7 Aug 2017 17:16:42 +0100 Subject: [PATCH 67/83] Fix ctrl+a, backspace toggling block format Now it will delete the selected range (and not toggle the block format). Fixes vector-im/riot-web#4753 --- src/components/views/rooms/MessageComposerInput.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index f2c6c3a054..d619de96d3 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -519,7 +519,8 @@ export default class MessageComposerInput extends React.Component { newState = RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH'); } else if (shouldToggleBlockFormat) { const currentStartOffset = this.state.editorState.getSelection().getStartOffset(); - if (currentStartOffset === 0) { + const currentEndOffset = this.state.editorState.getSelection().getEndOffset(); + if (currentStartOffset === 0 && currentEndOffset === 0) { // Toggle current block type (setting it to 'unstyled') newState = RichUtils.toggleBlockType(this.state.editorState, currentBlockType); } From 1743c047bd66366779b80defdb3f2f7534ae7233 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 8 Aug 2017 10:28:11 +0100 Subject: [PATCH 68/83] Use the rawDisplayName for the user provider completion to make sure that the length of text in the decoration (See ) is equal to the length of text in the completion (underlying text range that the Entity covers). --- src/autocomplete/UserProvider.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 9c93cf537f..499ddb51ce 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -54,7 +54,9 @@ export default class UserProvider extends AutocompleteProvider { completions = this.matcher.match(command[0]).map((user) => { const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done return { - completion: displayName, + // Length of completion should equal length of text in decorator. draft-js + // relies on the length of the entity === length of the text in the decoration. + completion: user.rawDisplayName, suffix: range.start === 0 ? ': ' : ' ', href: 'https://matrix.to/#/' + user.userId, component: ( From b08d32371d07efc572610bdbdc4dabfc6bc5aa6e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 8 Aug 2017 11:13:29 +0100 Subject: [PATCH 69/83] Add optional setting for hiding avatars in s As part of https://github.com/vector-im/riot-web/issues/4640#issuecomment-316659445 --- src/components/structures/UserSettings.js | 4 ++++ src/components/views/elements/Pill.js | 10 ++++++++-- src/components/views/messages/TextualBody.js | 8 +++++++- src/components/views/rooms/MessageComposerInput.js | 8 +++++++- src/i18n/strings/en_EN.json | 3 ++- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 1e0fcff445..72a287d584 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -101,6 +101,10 @@ const SETTINGS_LABELS = [ id: 'MessageComposerInput.autoReplaceEmoji', label: 'Automatically replace plain text Emoji', }, + { + id :'Pill.shouldHidePillAvatar', + label: 'Hide avatars in user and room mentions', + } /* { id: 'useFixedWidthFont', diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index 8d19eb5999..b5fa163608 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -47,6 +47,8 @@ const Pill = React.createClass({ inMessage: PropTypes.bool, // The room in which this pill is being rendered room: PropTypes.instanceOf(Room), + // Whether to include an avatar in the pill + shouldShowPillAvatar: PropTypes.bool, }, getInitialState() { @@ -155,7 +157,9 @@ const Pill = React.createClass({ if (member) { userId = member.userId; linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done - avatar = ; + if (this.props.shouldShowPillAvatar) { + avatar = ; + } pillClass = 'mx_UserPill'; } } @@ -164,7 +168,9 @@ const Pill = React.createClass({ const room = this.state.room; if (room) { linkText = (room ? getDisplayAliasForRoom(room) : null) || resource; - avatar = ; + if (this.props.shouldShowPillAvatar) { + avatar = ; + } pillClass = 'mx_RoomPill'; } } diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 6d4d01a196..27dba76146 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -170,6 +170,7 @@ module.exports = React.createClass({ }, pillifyLinks: function(nodes) { + const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false); for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.tagName === "A" && node.getAttribute("href")) { @@ -181,7 +182,12 @@ module.exports = React.createClass({ const pillContainer = document.createElement('span'); const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); - const pill = ; + const pill = ; ReactDOM.render(pill, pillContainer); node.parentNode.replaceChild(pillContainer, node); diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index d619de96d3..81704d5aba 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -188,13 +188,19 @@ export default class MessageComposerInput extends React.Component { createEditorState(richText: boolean, contentState: ?ContentState): EditorState { const decorators = richText ? RichText.getScopedRTDecorators(this.props) : RichText.getScopedMDDecorators(this.props); + const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false); decorators.push({ strategy: this.findLinkEntities.bind(this), component: (entityProps) => { const Pill = sdk.getComponent('elements.Pill'); const {url} = entityProps.contentState.getEntity(entityProps.entityKey).getData(); if (Pill.isPillUrl(url)) { - return ; + return ; } return ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0402c242aa..0d5b7d9d96 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -966,5 +966,6 @@ "Edit Group": "Edit Group", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Failed to upload image": "Failed to upload image", - "Failed to update group": "Failed to update group" + "Failed to update group": "Failed to update group", + "Hide avatars in user and room mentions": "Hide avatars in user and room mentions" } From 91a1cc443142e31fa53c4684e1a38a22e4615609 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 8 Aug 2017 13:36:43 +0100 Subject: [PATCH 70/83] Mandate ctrl/meta ONLY for a subset of key bindings Because by default dratf-js doesn't check that other modifiers are _not_ pressed. --- .../views/rooms/MessageComposerInput.js | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index d619de96d3..1de2214574 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -97,23 +97,39 @@ export default class MessageComposerInput extends React.Component { onInputStateChanged: React.PropTypes.func, }; - static getKeyBinding(e: SyntheticKeyboardEvent): string { - // C-m => Toggles between rich text and markdown modes - if (e.keyCode === KeyCode.KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) { - return 'toggle-mode'; + static getKeyBinding(ev: SyntheticKeyboardEvent): string { + const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + let ctrlCmdOnly; + if (isMac) { + ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey; + } else { + ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey; } - // Allow opening of dev tools. getDefaultKeyBinding would be 'italic' for KEY_I - // Likewise protect bold and underline (in case some browsers use these as - // shortcuts for things). - if ([KeyCode.KEY_B, KeyCode.KEY_I, KeyCode.KEY_U].includes(e.keyCode) && - e.shiftKey && e.ctrlKey - ) { - // When null is returned, draft-js will NOT preventDefault - return null; + // Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and + // importantly NOT having alt, shift, meta/ctrl* pressed. draft-js does not + // handle this in `getDefaultKeyBinding` so we do it ourselves here. + // + // * if macOS, read second option + const ctrlCmdCommand = { + // C-m => Toggles between rich text and markdown modes + [KeyCode.KEY_M]: 'toggle-mode', + [KeyCode.KEY_B]: 'bold', + [KeyCode.KEY_I]: 'italic', + [KeyCode.KEY_U]: 'underline', + [KeyCode.KEY_J]: 'code', + [KeyCode.KEY_O]: 'split-block', + }[ev.keyCode]; + + if (ctrlCmdCommand) { + if (!ctrlCmdOnly) { + return null; + } + return ctrlCmdCommand; } - return getDefaultKeyBinding(e); + // Handle keys such as return, left and right arrows etc. + return getDefaultKeyBinding(ev); } static getBlockStyle(block: ContentBlock): ?string { From bef67262905bee335e90f60627bda0b1ebf9ecf1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 8 Aug 2017 13:42:51 +0100 Subject: [PATCH 71/83] Lint --- src/components/structures/UserSettings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 72a287d584..916e50d86b 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -102,9 +102,9 @@ const SETTINGS_LABELS = [ label: 'Automatically replace plain text Emoji', }, { - id :'Pill.shouldHidePillAvatar', + id: 'Pill.shouldHidePillAvatar', label: 'Hide avatars in user and room mentions', - } + }, /* { id: 'useFixedWidthFont', From 503fa6a7b3f6c201c805eca3da16f7535364312b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 8 Aug 2017 14:59:56 +0100 Subject: [PATCH 72/83] Always use message `body` when quoting (not formatted_body) This is because draft-js has regressed with a bug that causes some entities to not exist within a given ContentState - see vector-im/riot-web#4756 --- src/components/views/rooms/MessageComposerInput.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 283a0e9330..4e6b57b7f4 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -279,10 +279,13 @@ export default class MessageComposerInput extends React.Component { } break; case 'quote': { - let {body, formatted_body} = payload.event.getContent(); - formatted_body = formatted_body || escape(body); - if (formatted_body) { - let content = RichText.htmlToContentState(`
${formatted_body}
`); + let {body} = payload.event.getContent(); + /// XXX: Not doing rich-text quoting from formatted-body because draft-js + /// has regressed such that when links are quoted, errors are thrown. See + /// https://github.com/vector-im/riot-web/issues/4756. + body = escape(body); + if (body) { + let content = RichText.htmlToContentState(`
${body}
`); if (!this.state.isRichtextEnabled) { content = ContentState.createFromText(RichText.stateToMarkdown(content)); } From a72f38799f82cabe1bc9d0fc1b9d610311490707 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 8 Aug 2017 15:58:15 +0100 Subject: [PATCH 73/83] Disable autocompletions for users and rooms when entering a command This only affects commands that take a room alias or user ID as an argument. (Leaving commands such as `/me` unaffected) --- src/autocomplete/RoomProvider.js | 6 ++++++ src/autocomplete/UserProvider.js | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 3749e7e693..1770089eb2 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -49,6 +49,12 @@ export default class RoomProvider extends AutocompleteProvider { async getCompletions(query: string, selection: {start: number, end: number}, force = false) { const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); + // Disable autocompletions when composing commands because of various issues + // (see https://github.com/vector-im/riot-web/issues/4762) + if (/^(\/join|\/leave)/.test(query)) { + return []; + } + const client = MatrixClientPeg.get(); let completions = []; const {command, range} = this.getCurrentCommand(query, selection, force); diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 499ddb51ce..5db0369150 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -48,6 +48,12 @@ export default class UserProvider extends AutocompleteProvider { async getCompletions(query: string, selection: {start: number, end: number}, force = false) { const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); + // Disable autocompletions when composing commands because of various issues + // (see https://github.com/vector-im/riot-web/issues/4762) + if (/^(\/ban|\/unban|\/op|\/deop|\/invite|\/kick|\/verify)/.test(query)) { + return []; + } + let completions = []; let {command, range} = this.getCurrentCommand(query, selection, force); if (command) { From cb8a66b5a119d46e93265b0f73031e10d7559fa0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 8 Aug 2017 17:25:11 +0100 Subject: [PATCH 74/83] When `hide`ing autocomplete, also remove completion state --- src/components/views/rooms/Autocomplete.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 1ea2eada7c..cdd57801a5 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -172,7 +172,7 @@ export default class Autocomplete extends React.Component { } hide() { - this.setState({hide: true, selectionOffset: 0}); + this.setState({hide: true, selectionOffset: 0, completions: [], completionList: []}); } forceComplete() { From da85cb9f454227d9d165c1e6660463cafaa59457 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 8 Aug 2017 12:34:40 +0100 Subject: [PATCH 75/83] Show unencrypted messages as unencrypted Previously, we were special-casing outgoing messages such that they were shown as encrypted even when encryption had failed for some reason. There's no need for this: outgoing messages have a working isEncrypted() method which we can use to show whether the event has been encrypted yet. Arguably we could do better than an open padlock for events in the 'encrypting' send state, but I'm not really sure what. --- src/components/views/rooms/EventTile.js | 94 +++++++++++++++++++------ 1 file changed, 71 insertions(+), 23 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index b3831a7d0d..815f0a3c6a 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -155,7 +155,9 @@ module.exports = withMatrixClient(React.createClass({ }, componentWillReceiveProps: function(nextProps) { - if (nextProps.mxEvent !== this.props.mxEvent) { + // re-check the sender verification as outgoing events progress through + // the send process. + if (nextProps.eventSendStatus !== this.props.eventSendStatus) { this._verifyEvent(nextProps.mxEvent); } }, @@ -386,6 +388,36 @@ module.exports = withMatrixClient(React.createClass({ }); }, + _renderE2EPadlock: function() { + const ev = this.props.mxEvent; + const props = {onClick: this.onCryptoClicked}; + + + if (ev.getContent().msgtype === 'm.bad.encrypted') { + return ; + } else if (ev.isEncrypted()) { + if (this.state.verified) { + return ; + } else { + return ; + } + } else { + // XXX: if the event is being encrypted (ie eventSendStatus === + // encrypting), it might be nice to show something other than the + // open padlock? + + // if the event is not encrypted, but it's an e2e room, show the + // open padlock + const e2eEnabled = this.props.matrixClient.isRoomEncrypted(ev.getRoomId()); + if (e2eEnabled) { + return ; + } + } + + // no padlock needed + return null; + }, + render: function() { var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp'); var SenderProfile = sdk.getComponent('messages.SenderProfile'); @@ -407,7 +439,6 @@ module.exports = withMatrixClient(React.createClass({ throw new Error("Event type not supported"); } - var e2eEnabled = this.props.matrixClient.isRoomEncrypted(this.props.mxEvent.getRoomId()); var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1); const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted; @@ -485,26 +516,7 @@ module.exports = withMatrixClient(React.createClass({ const editButton = ( ); - let e2e; - // cosmetic padlocks: - if ((e2eEnabled && this.props.eventSendStatus) || this.props.mxEvent.getType() === 'm.room.encryption') { - e2e = {_t("Encrypted; - } - // real padlocks - else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) { - if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') { - e2e = {_t("Undecryptable")}; - } - else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) { - e2e = {_t("Encrypted; - } - else { - e2e = {_t("Encrypted; - } - } - else if (e2eEnabled) { - e2e = {_t("Unencrypted; - } + const timestamp = this.props.mxEvent.getTs() ? : null; @@ -572,7 +584,7 @@ module.exports = withMatrixClient(React.createClass({ { timestamp } - { e2e } + { this._renderE2EPadlock() } + ); +} + +function E2ePadlockVerified(props) { + return ( + + ); +} + +function E2ePadlockUnverified(props) { + return ( + + ); +} + +function E2ePadlockUnencrypted(props) { + return ( + + ); +} + +function E2ePadlock(props) { + return ; +} From 38114711fdde1fa5c019c34f3a9c93d3b4e64108 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 8 Aug 2017 22:19:38 +0100 Subject: [PATCH 76/83] Make MatrixChat do fewer render cycles during mount This is mostly with the intent of making the login tests more reliable, but it seems generally worthwhile: * keep screenAfterLogin in the object props rather than `state` so that we can clear it without triggering a rerender * also move our record of the window width to the object props, and call `handleResize` from componentWillMount rather than componentDidMount so that we don't trigger a rerender by updating `state.width` * Remove update of unused `loading` state --- src/components/structures/MatrixChat.js | 48 ++++++++++++------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b90cb53435..cb6419c9e8 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -131,9 +131,6 @@ module.exports = React.createClass({ // the master view we are showing. view: VIEWS.LOADING, - // a thing to call showScreen with once login completes. - screenAfterLogin: this.props.initialScreenAfterLogin, - // What the LoggedInView would be showing if visible page_type: null, @@ -147,8 +144,6 @@ module.exports = React.createClass({ collapse_lhs: false, collapse_rhs: false, - ready: false, - width: 10000, leftOpacity: 1.0, middleOpacity: 1.0, rightOpacity: 1.0, @@ -274,6 +269,15 @@ module.exports = React.createClass({ register_hs_url: paramHs, }); } + + // a thing to call showScreen with once login completes. this is kept + // outside this.state because updating it should never trigger a + // rerender. + this._screenAfterLogin = this.props.initialScreenAfterLogin; + + this._windowWidth = 10000; + this.handleResize(); + window.addEventListener('resize', this.handleResize); }, componentDidMount: function() { @@ -294,9 +298,6 @@ module.exports = React.createClass({ linkifyMatrix.onGroupClick = this.onGroupClick; } - window.addEventListener('resize', this.handleResize); - this.handleResize(); - const teamServerConfig = this.props.config.teamServerConfig || {}; Lifecycle.initRtsClient(teamServerConfig.teamServerURL); @@ -312,13 +313,12 @@ module.exports = React.createClass({ // if the user has followed a login or register link, don't reanimate // the old creds, but rather go straight to the relevant page - const firstScreen = this.state.screenAfterLogin ? - this.state.screenAfterLogin.screen : null; + const firstScreen = this._screenAfterLogin ? + this._screenAfterLogin.screen : null; if (firstScreen === 'login' || firstScreen === 'register' || firstScreen === 'forgot_password') { - this.setState({loading: false}); this._showScreenAfterLogin(); return; } @@ -367,9 +367,9 @@ module.exports = React.createClass({ } const newState = { viewUserId: null, - }; - Object.assign(newState, state); - this.setState(newState); + }; + Object.assign(newState, state); + this.setState(newState); }, onAction: function(payload) { @@ -992,14 +992,12 @@ module.exports = React.createClass({ _showScreenAfterLogin: function() { // If screenAfterLogin is set, use that, then null it so that a second login will // result in view_home_page, _user_settings or _room_directory - if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) { + if (this._screenAfterLogin && this._screenAfterLogin.screen) { this.showScreen( - this.state.screenAfterLogin.screen, - this.state.screenAfterLogin.params, + this._screenAfterLogin.screen, + this._screenAfterLogin.params, ); - // XXX: is this necessary? `showScreen` should do it for us. - this.notifyNewScreen(this.state.screenAfterLogin.screen); - this.setState({screenAfterLogin: null}); + this._screenAfterLogin = null; } else if (localStorage && localStorage.getItem('mx_last_room_id')) { // Before defaulting to directory, show the last viewed room dis.dispatch({ @@ -1276,20 +1274,20 @@ module.exports = React.createClass({ const hideRhsThreshold = 820; const showRhsThreshold = 820; - if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) { + if (this._windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) { dis.dispatch({ action: 'hide_left_panel' }); } - if (this.state.width <= showLhsThreshold && window.innerWidth > showLhsThreshold) { + if (this._windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) { dis.dispatch({ action: 'show_left_panel' }); } - if (this.state.width > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) { + if (this._windowWidth > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) { dis.dispatch({ action: 'hide_right_panel' }); } - if (this.state.width <= showRhsThreshold && window.innerWidth > showRhsThreshold) { + if (this._windowWidth <= showRhsThreshold && window.innerWidth > showRhsThreshold) { dis.dispatch({ action: 'show_right_panel' }); } - this.setState({width: window.innerWidth}); + this._windowWidth = window.innerWidth; }, onRoomCreated: function(roomId) { From 610b2a3a428df410d2549508b2a3f883526c4e3b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 9 Aug 2017 10:40:06 +0100 Subject: [PATCH 77/83] For mentions, always use rawDisplayName and remove (IRC) --- src/autocomplete/UserProvider.js | 2 +- src/components/views/rooms/MessageComposerInput.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 5db0369150..69b80dade4 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -62,7 +62,7 @@ export default class UserProvider extends AutocompleteProvider { return { // Length of completion should equal length of text in decorator. draft-js // relies on the length of the entity === length of the text in the decoration. - completion: user.rawDisplayName, + completion: user.rawDisplayName.replace(' (IRC)', ''), suffix: range.start === 0 ? ': ' : ' ', href: 'https://matrix.to/#/' + user.userId, component: ( diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 4e6b57b7f4..b2c1436365 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -269,7 +269,8 @@ export default class MessageComposerInput extends React.Component { // paths for inserting a user pill is not fun const selection = this.state.editorState.getSelection(); const member = this.props.room.getMember(payload.user_id); - const completion = member ? member.name.replace(' (IRC)', '') : payload.user_id; + const completion = member ? + member.rawDisplayName.replace(' (IRC)', '') : payload.user_id; this.setDisplayedCompletion({ completion, selection, From 579090a4e307cd38403c7fed163f73b615d34c62 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@googlemail.com> Date: Wed, 9 Aug 2017 16:37:38 +0100 Subject: [PATCH 78/83] add comment --- src/components/views/dialogs/SetMxIdDialog.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 9fb6b838b2..554a244358 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -108,6 +108,7 @@ export default React.createClass({ _doUsernameCheck: function() { // XXX: SPEC-1 // Check if username is valid + // Naive impl copied from https://github.com/matrix-org/matrix-react-sdk/blob/66c3a6d9ca695780eb6b662e242e88323053ff33/src/components/views/login/RegistrationForm.js#L190 if (encodeURIComponent(this.state.username) !== this.state.username) { this.setState({ usernameError: _t('User names may only contain letters, numbers, dots, hyphens and underscores.'), From 2d47d3d2c371d5d31d1239dc5a98c2a779a07c98 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 9 Aug 2017 17:36:35 +0100 Subject: [PATCH 79/83] Hide autocomplete when RTE selection state (cursor) changes --- src/components/views/rooms/MessageComposerInput.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index b2c1436365..ab6c20684b 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -457,6 +457,19 @@ export default class MessageComposerInput extends React.Component { state.editorState = RichText.attachImmutableEntitiesToEmoji( state.editorState); + // Hide the autocomplete if the cursor location changes but the plaintext + // content stays the same. We don't hide if the pt has changed because the + // autocomplete will probably have different completions to show. + if ( + !state.editorState.getSelection().equals( + this.state.editorState.getSelection() + ) + && state.editorState.getCurrentContent().getPlainText() === + this.state.editorState.getCurrentContent().getPlainText() + ) { + this.autocomplete.hide(); + } + if (state.editorState.getCurrentContent().hasText()) { this.onTypingActivity(); } else { From e121440d05088ed7267dd40bff5ac6083974e69b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 9 Aug 2017 18:39:06 +0100 Subject: [PATCH 80/83] Track whether the user has richtext mode enabled --- src/Analytics.js | 6 ++++++ src/components/views/rooms/MessageComposerInput.js | 3 +++ 2 files changed, 9 insertions(+) diff --git a/src/Analytics.js b/src/Analytics.js index 92691da1ea..0a31625ebc 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -31,6 +31,7 @@ const customVariables = { 'User Type': 3, 'Chosen Language': 4, 'Instance': 5, + 'RTE: Uses Richtext Mode': 6, }; @@ -145,6 +146,11 @@ class Analytics { if (this.disabled) return; this._setVisitVariable('User Type', guest ? 'Guest' : 'Logged In'); } + + setRichtextMode(state) { + if (this.disabled) return; + this._setVisitVariable('RTE: Uses Richtext Mode', state ? 'on' : 'off'); + } } if (!global.mxAnalytics) { diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index b2c1436365..aa4acd7655 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -31,6 +31,7 @@ import KeyCode from '../../../KeyCode'; import Modal from '../../../Modal'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; +import Analytics from '../../../Analytics'; import dis from '../../../dispatcher'; import UserSettingsStore from '../../../UserSettingsStore'; @@ -513,6 +514,8 @@ export default class MessageComposerInput extends React.Component { contentState = ContentState.createFromText(markdown); } + Analytics.setRichtextMode(enabled); + this.setState({ editorState: this.createEditorState(enabled, contentState), isRichtextEnabled: enabled, From fc6977e68d5be61a135a47d434cec36f2d61173c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 9 Aug 2017 19:00:38 +0100 Subject: [PATCH 81/83] Track RT mode once we've retrieved the setting from account data --- src/components/views/rooms/MessageComposerInput.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index aa4acd7655..856c7aaa88 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -161,6 +161,8 @@ export default class MessageComposerInput extends React.Component { const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false); + Analytics.setRichtextMode(isRichtextEnabled); + this.state = { // whether we're in rich text or markdown mode isRichtextEnabled, From 678c472b753940553d68445bd10ded0be27c9295 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 10 Aug 2017 10:14:14 +0100 Subject: [PATCH 82/83] Quote by taking the innerText of eventTiles because using `body` gives inconsistent results - sometimes it will contain markdown and sometimes not, and this may not correspond with the `formatted_body`. TODO: Do quoting proper - using `in_response_to`. --- src/components/views/messages/TextualBody.js | 15 +++++++++------ .../views/rooms/MessageComposerInput.js | 3 +-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 27dba76146..18265ce559 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -275,18 +275,21 @@ module.exports = React.createClass({ }, getEventTileOps: function() { - var self = this; return { - isWidgetHidden: function() { - return self.state.widgetHidden; + isWidgetHidden: () => { + return this.state.widgetHidden; }, - unhideWidget: function() { - self.setState({ widgetHidden: false }); + unhideWidget: () => { + this.setState({ widgetHidden: false }); if (global.localStorage) { - global.localStorage.removeItem("hide_preview_" + self.props.mxEvent.getId()); + global.localStorage.removeItem("hide_preview_" + this.props.mxEvent.getId()); } }, + + getInnerText: () => { + return this.refs.content.innerText; + } }; }, diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index b2c1436365..950ccfc21f 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -280,11 +280,10 @@ export default class MessageComposerInput extends React.Component { } break; case 'quote': { - let {body} = payload.event.getContent(); /// XXX: Not doing rich-text quoting from formatted-body because draft-js /// has regressed such that when links are quoted, errors are thrown. See /// https://github.com/vector-im/riot-web/issues/4756. - body = escape(body); + let body = escape(payload.text); if (body) { let content = RichText.htmlToContentState(`
${body}
`); if (!this.state.isRichtextEnabled) { From 60c1ba4f4d48c33eaf84a5b6ab66721903db87ea Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 10 Aug 2017 14:29:10 +0200 Subject: [PATCH 83/83] Add LanguageDropdown to LoginPage (#1284) --- src/components/structures/login/Login.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index a081d2a205..a6c0a70c66 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -19,8 +19,11 @@ limitations under the License. import React from 'react'; import { _t, _tJsx } from '../../../languageHandler'; +import * as languageHandler from '../../../languageHandler'; import sdk from '../../../index'; import Login from '../../../Login'; +import UserSettingsStore from '../../../UserSettingsStore'; +import PlatformPeg from '../../../PlatformPeg'; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/; @@ -306,6 +309,23 @@ module.exports = React.createClass({ } }, + _onLanguageChange: function(newLang) { + if(languageHandler.getCurrentLanguage() !== newLang) { + UserSettingsStore.setLocalSetting('language', newLang); + PlatformPeg.get().reload(); + } + }, + + _renderLanguageSetting: function() { + const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown'); + return
+ +
; + }, + render: function() { const Loader = sdk.getComponent("elements.Spinner"); const LoginHeader = sdk.getComponent("login.LoginHeader"); @@ -354,6 +374,7 @@ module.exports = React.createClass({ { loginAsGuestJsx } { returnToAppJsx } + { this._renderLanguageSetting() }