From a967ddd1cb4ef0afd95189fa19cc1a6bc90e26e4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 13 Nov 2016 14:10:46 +0000 Subject: [PATCH 001/189] quick and dirty support for custom welcome pages, with an example for geektime techfest --- .DS_Store | Bin 6148 -> 6148 bytes src/PageTypes.js | 1 + src/components/structures/LoggedInView.js | 10 ++++++++++ src/components/structures/MatrixChat.js | 22 +++++++++++++++++++-- src/components/views/avatars/BaseAvatar.js | 2 +- 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.DS_Store b/.DS_Store index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..d2a053831af5212a0eeadcbe98a90fb570842166 100644 GIT binary patch delta 128 zcmZoMXfc=|&e%3FQEZ}~q6iZM0|O%ig8)Nua#DVN4v@#dJTXy29VE`oki(G4kd&5! zB*#$9P{fc76Jp$$7|K4`K!jy8HwO;~W822W@640=MHEFr+7*B}0f>RdGaLY7hRu#5 G` if (!this.props.collapse_rhs) right_panel = break; + + case PageTypes.HomePage: + page_element = + if (!this.props.collapse_rhs) right_panel = + break; + case PageTypes.UserView: page_element = null; // deliberately null for now right_panel = diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 945088106ba..9a3ff8f95c5 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -410,6 +410,10 @@ module.exports = React.createClass({ this._setPage(PageTypes.RoomDirectory); this.notifyNewScreen('directory'); break; + case 'view_home_page': + this._setPage(PageTypes.HomePage); + this.notifyNewScreen('home'); + break; case 'view_create_chat': this._createChat(); break; @@ -629,7 +633,12 @@ module.exports = React.createClass({ )[0].roomId; self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView}); } else { - self.setState({ready: true, page_type: PageTypes.RoomDirectory}); + if (self.props.config.home_page) { + self.setState({ready: true, page_type: PageTypes.HomePage}); + } + else { + self.setState({ready: true, page_type: PageTypes.RoomDirectory}); + } } } else { self.setState({ready: true, page_type: PageTypes.RoomView}); @@ -649,7 +658,12 @@ module.exports = React.createClass({ } else { // There is no information on presentedId // so point user to fallback like /directory - self.notifyNewScreen('directory'); + if (self.props.config.home_page) { + self.notifyNewScreen('home'); + } + else { + self.notifyNewScreen('directory'); + } } dis.dispatch({action: 'focus_composer'}); @@ -703,6 +717,10 @@ module.exports = React.createClass({ dis.dispatch({ action: 'view_user_settings', }); + } else if (screen == 'home') { + dis.dispatch({ + action: 'view_home_page', + }); } else if (screen == 'directory') { dis.dispatch({ action: 'view_room_directory', diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 47f0a768912..4025859478e 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -138,7 +138,7 @@ module.exports = React.createClass({ const { name, idName, title, url, urls, width, height, resizeMethod, - defaultToInitialLetter, + defaultToInitialLetter, viewUserOnClick, ...otherProps } = this.props; From 69f6393ed9fbb19c7460ac9fac3a61af851e3794 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 13 Nov 2016 14:13:21 +0000 Subject: [PATCH 002/189] try to make joining rooms more obvious --- src/components/views/rooms/RoomPreviewBar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index baeae4807d4..7c03ca89a40 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -146,7 +146,7 @@ module.exports = React.createClass({
You are trying to access { name }.
- Would you like to join in order to participate in the discussion? + Click here to join the discussion!
); From 8a5678efdd74f1997d65533352ec800d0482cc67 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 19 Nov 2016 01:20:09 +0200 Subject: [PATCH 003/189] boldify the preview bar click --- src/components/views/rooms/RoomPreviewBar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 7c03ca89a40..d41fed04b4f 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -146,7 +146,7 @@ module.exports = React.createClass({
You are trying to access { name }.
- Click here to join the discussion! + Click here to join the discussion!
); From ef88e02931f8de61960e362a636e47bc7fb102fc Mon Sep 17 00:00:00 2001 From: Sijmen Schoon Date: Sun, 8 Jan 2017 02:20:59 +0100 Subject: [PATCH 004/189] Add support for pasting into the text box Only supports the new rich-text-supporting text editor --- src/ContentMessages.js | 4 ++-- src/components/views/rooms/MessageComposer.js | 10 ++++++---- src/components/views/rooms/MessageComposerInput.js | 8 ++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index c169ce64b56..765c7ed9760 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -276,7 +276,7 @@ class ContentMessages { sendContentToRoom(file, roomId, matrixClient) { const content = { - body: file.name, + body: file.name || 'Attachment', info: { size: file.size, } @@ -316,7 +316,7 @@ class ContentMessages { } const upload = { - fileName: file.name, + fileName: file.name || 'Attachment', roomId: roomId, total: 0, loaded: 0, diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index ee9c49d52a7..6810e75f534 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -91,8 +91,9 @@ export default class MessageComposer extends React.Component { this.refs.uploadInput.click(); } - onUploadFileSelected(ev) { - let files = ev.target.files; + onUploadFileSelected(files, isPasted) { + if (!isPasted) + files = files.target.files; let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); let TintableSvg = sdk.getComponent("elements.TintableSvg"); @@ -100,7 +101,7 @@ export default class MessageComposer extends React.Component { let fileList = []; for (let i=0; i - {files[i].name} + {files[i].name || 'Attachment'} ); } @@ -171,7 +172,7 @@ export default class MessageComposer extends React.Component { } onUpArrow() { - return this.refs.autocomplete.onUpArrow(); + return this.refs.autocomplete.onUpArrow(); } onDownArrow() { @@ -293,6 +294,7 @@ export default class MessageComposer extends React.Component { tryComplete={this._tryComplete} onUpArrow={this.onUpArrow} onDownArrow={this.onDownArrow} + onUploadFileSelected={this.onUploadFileSelected} tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete onContentChanged={this.onInputContentChanged} onInputStateChanged={this.onInputStateChanged} />, diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 37d937d6f52..f0658ab543c 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -83,6 +83,7 @@ export default class MessageComposerInput extends React.Component { this.onAction = this.onAction.bind(this); this.handleReturn = this.handleReturn.bind(this); this.handleKeyCommand = this.handleKeyCommand.bind(this); + this.handlePastedFiles = this.handlePastedFiles.bind(this); this.onEditorContentChanged = this.onEditorContentChanged.bind(this); this.setEditorState = this.setEditorState.bind(this); this.onUpArrow = this.onUpArrow.bind(this); @@ -473,6 +474,10 @@ export default class MessageComposerInput extends React.Component { return false; } + handlePastedFiles(files) { + this.props.onUploadFileSelected(files, true); + } + handleReturn(ev) { if (ev.shiftKey) { this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); @@ -728,6 +733,7 @@ export default class MessageComposerInput extends React.Component { keyBindingFn={MessageComposerInput.getKeyBinding} handleKeyCommand={this.handleKeyCommand} handleReturn={this.handleReturn} + handlePastedFiles={this.handlePastedFiles} stripPastedStyles={!this.state.isRichtextEnabled} onTab={this.onTab} onUpArrow={this.onUpArrow} @@ -757,6 +763,8 @@ MessageComposerInput.propTypes = { onDownArrow: React.PropTypes.func, + onUploadFileSelected: React.PropTypes.func, + // attempts to confirm currently selected completion, returns whether actually confirmed tryComplete: React.PropTypes.func, From 8e3f2eb858b16d81fbe4f69896cf4b831ce3873f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 11 Jan 2017 16:35:37 +0000 Subject: [PATCH 005/189] Allow [bf]g colors for style attrib Instead of dropping the style attribute on `` tags entirely, sanitise aggressively and only keep `background-color` and `color` keys, and also sanitise the values to prevent `url(XXXXXX)` and `expression(XXXXXX)` type XSS attacks. --- src/HtmlUtils.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index fc1630b6fb7..ae594de9603 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -28,6 +28,11 @@ emojione.imagePathSVG = 'emojione/svg/'; emojione.imageType = 'svg'; const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi"); +const COLOR_REGEX = /^[#a-z0-9]+$/; +const ALLOWED_CSS = { + "background-color": COLOR_REGEX, + "color": COLOR_REGEX, +}; /* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js * because we want to include emoji shortnames in title text @@ -91,7 +96,7 @@ var sanitizeHtmlParams = { ], allowedAttributes: { // custom ones first: - font: [ 'color' ], // custom to matrix + font: [ 'color' , 'style' ], // custom to matrix a: [ 'href', 'name', 'target', 'rel' ], // remote target: custom to matrix // We don't currently allow img itself by default, but this // would make sense if we did @@ -136,6 +141,31 @@ var sanitizeHtmlParams = { attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ return { tagName: tagName, attribs : attribs }; }, + '*': function(tagName, attribs) { + // Only allow certain CSS attributes to avoid XSS attacks + // Sanitizing values to avoid `url(...)` and `expression(...)` attacks + if (!attribs.style) { + return { tagName: tagName, attribs : attribs }; + } + + const pairs = attribs.style.split(';'); + let sanitisedStyle = ""; + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i].split(':'); + if (!Object.keys(ALLOWED_CSS).includes(pair[0]) || !ALLOWED_CSS[pair[0]].test(pair[1])) { + continue; + } + sanitisedStyle += pair[0] + ":" + pair[1] + ";"; + } + + if (sanitisedStyle) { + attribs.style = sanitisedStyle; + } else { + delete attribs.style; + } + + return { tagName: tagName, attribs : attribs }; + }, }, }; From 32185befc009a32534e111fd549a50b873ad3ff4 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 11 Jan 2017 16:41:05 +0000 Subject: [PATCH 006/189] Only transform --- src/HtmlUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index ae594de9603..a8fb763a8d4 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -141,7 +141,7 @@ var sanitizeHtmlParams = { attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ return { tagName: tagName, attribs : attribs }; }, - '*': function(tagName, attribs) { + 'font': function(tagName, attribs) { // Only allow certain CSS attributes to avoid XSS attacks // Sanitizing values to avoid `url(...)` and `expression(...)` attacks if (!attribs.style) { From 8c941b11cde686ab065d130db0438e9f89da63c2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 2 Feb 2017 11:22:40 +0000 Subject: [PATCH 007/189] Use `mx_team_token` instead of config --- src/components/structures/LoggedInView.js | 2 +- src/components/structures/MatrixChat.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index fb553535aa6..63c4e9e0dc1 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -196,7 +196,7 @@ export default React.createClass({ case PageTypes.HomePage: page_element = if (!this.props.collapse_rhs) right_panel = break; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index f3fc5bf260e..3cc34ba69cc 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -694,7 +694,7 @@ module.exports = React.createClass({ )[0].roomId; self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView}); } else { - if (self.props.config.home_page) { + if (window.localStorage.getItem('mx_team_token')) { self.setState({ready: true, page_type: PageTypes.HomePage}); } else { From 7ece1ef8e7ccb99082a7d2499001453cdb9527fe Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 2 Feb 2017 11:42:34 +0000 Subject: [PATCH 008/189] rm .DS_Store --- .DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index d2a053831af5212a0eeadcbe98a90fb570842166..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKL5c!F3`|xJ!d^V?vA?iCXvBSiA6Qg`1wl}}&uQPaPur>_3oC2vn~5BQb&|B|F9DIf)&N&%lPSIY%nse0?^<=AT*{1eWcA2<#BpkRr142*V+2e#w4 cNXop%J Date: Thu, 2 Feb 2017 11:57:56 +0000 Subject: [PATCH 009/189] Pass RTS URL through --- src/components/structures/LoggedInView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 63c4e9e0dc1..feecc8ca4c8 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -196,6 +196,7 @@ export default React.createClass({ case PageTypes.HomePage: page_element = if (!this.props.collapse_rhs) right_panel = From b9bc8eefd06dfbd49186f8672e0d47db96b511f9 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 2 Feb 2017 13:56:54 +0000 Subject: [PATCH 010/189] Typo --- src/components/structures/LoggedInView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index feecc8ca4c8..7dd37683b76 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -196,7 +196,7 @@ export default React.createClass({ case PageTypes.HomePage: page_element = if (!this.props.collapse_rhs) right_panel = From 300cd962e572bb0ab3314295721f4d2e5d2d16c7 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 2 Feb 2017 14:27:27 +0000 Subject: [PATCH 011/189] Add glue code to make react-sdk use indexedDB --- src/MatrixClientPeg.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 9c0daf47263..36521204c53 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -16,6 +16,7 @@ limitations under the License. 'use strict'; +import q from "q"; import Matrix from 'matrix-js-sdk'; import utils from 'matrix-js-sdk/lib/utils'; import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline'; @@ -71,7 +72,17 @@ class MatrixClientPeg { const opts = utils.deepCopy(this.opts); // the react sdk doesn't work without this, so don't allow opts.pendingEventOrdering = "detached"; - this.get().startClient(opts); + + let promise = q(); + if (this.matrixClient.store instanceof Matrix.IndexedDBStore) { + // load from storage before starting up. + console.log("Loading history from IndexedDB."); + promise = this.matrixClient.store.startup(); + } + + promise.finally(() => { + this.get().startClient(opts); + }); } getCredentials(): MatrixClientCreds { @@ -111,6 +122,13 @@ class MatrixClientPeg { if (localStorage) { opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); } + if (window.indexedDB && localStorage) { + opts.store = new Matrix.IndexedDBStore( + new Matrix.IndexedDBStoreBackend(window.indexedDB), { + localStorage: localStorage, + } + ); + } this.matrixClient = Matrix.createClient(opts); From efae5f6bf1166324e56cc03115679922b883ee83 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Feb 2017 11:48:24 +0000 Subject: [PATCH 012/189] Use localStorage team token with fall-back on query parameter --- src/components/structures/LoggedInView.js | 4 +++- src/components/structures/MatrixChat.js | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 7dd37683b76..ba63794f60e 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -42,6 +42,8 @@ export default React.createClass({ onRoomCreated: React.PropTypes.func, onUserSettingsClose: React.PropTypes.func, + teamToken: React.PropTypes.string, + // and lots and lots of other stuff. }, @@ -197,7 +199,7 @@ export default React.createClass({ page_element = if (!this.props.collapse_rhs) right_panel = break; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 3cc34ba69cc..b986cd88f06 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -190,6 +190,11 @@ module.exports = React.createClass({ if (this.props.config.sync_timeline_limit) { MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; } + + // Use the locally-stored team token first, then as a fall-back, check to see if + // a referral link was used, which will contain a query parameter `team_token`. + this._teamToken = window.localStorage.getItem('mx_team_token') || + startingFragmentQueryParams.team_token; }, componentDidMount: function() { @@ -694,7 +699,7 @@ module.exports = React.createClass({ )[0].roomId; self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView}); } else { - if (window.localStorage.getItem('mx_team_token')) { + if (this._teamToken) { self.setState({ready: true, page_type: PageTypes.HomePage}); } else { @@ -1051,6 +1056,7 @@ module.exports = React.createClass({ onRoomIdResolved={this.onRoomIdResolved} onRoomCreated={this.onRoomCreated} onUserSettingsClose={this.onUserSettingsClose} + teamToken={this._teamToken} {...this.props} {...this.state} /> From 173e80a5de0bd14a687677e05e1a81bd26e79e99 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Feb 2017 14:34:24 +0000 Subject: [PATCH 013/189] Get team_token from the RTS on login Use the /login endpoint of the RTS to get the team token when the user has successfully logged in. --- src/Lifecycle.js | 16 ++++++++++++++++ src/RtsClient.js | 10 ++++++++++ src/components/structures/MatrixChat.js | 2 ++ 3 files changed, 28 insertions(+) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 493bbf12aa5..43a3029fc6d 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -228,6 +228,11 @@ function _restoreFromLocalStorage() { return false; } } +const RtsClient = require("./RtsClient"); +let rtsClient = null; +export function initRtsClient(url) { + rtsClient = new RtsClient(url); +} /** * Transitions to a logged-in state using the given credentials @@ -261,6 +266,17 @@ export function setLoggedIn(credentials) { } catch (e) { console.warn("Error using local storage: can't persist session!", e); } + + if (rtsClient) { + rtsClient.login(credentials.userId).then((teamToken) => { + localStorage.setItem("mx_team_token", teamToken); + }, (err) =>{ + console.error( + "Failed to get team token on login, not persisting to localStorage", + err + ); + }); + } } else { console.warn("No local storage available: can't persist session!"); } diff --git a/src/RtsClient.js b/src/RtsClient.js index ae62fb8b22f..b8b51791baf 100644 --- a/src/RtsClient.js +++ b/src/RtsClient.js @@ -77,4 +77,14 @@ export default class RtsClient { } ); } + + login(userId) { + return request(this._url + '/login', + { + qs: { + user_id: userId, + }, + } + ); + } } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 6a84fb940f8..3e57684ac28 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -210,6 +210,8 @@ module.exports = React.createClass({ window.addEventListener('resize', this.handleResize); this.handleResize(); + Lifecycle.initRtsClient(this.props.config.teamServerConfig.teamServerURL); + // the extra q() ensures that synchronous exceptions hit the same codepath as // asynchronous ones. q().then(() => { From fd0a66ab9927f4d2a05d6478b436774d2d5784d1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 3 Feb 2017 14:37:58 +0000 Subject: [PATCH 014/189] Unpin js-sdk --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 815d692eaff..d374243b6b2 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "0.7.5-rc.2", + "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.4.0", From 4f3549cc37b6e1ff3af1c679e2b94d665e490056 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Feb 2017 14:42:22 +0000 Subject: [PATCH 015/189] Fix: actually get the team token --- src/Lifecycle.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 43a3029fc6d..475cffcff85 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -268,8 +268,8 @@ export function setLoggedIn(credentials) { } if (rtsClient) { - rtsClient.login(credentials.userId).then((teamToken) => { - localStorage.setItem("mx_team_token", teamToken); + rtsClient.login(credentials.userId).then((body) => { + localStorage.setItem("mx_team_token", body.team_token); }, (err) =>{ console.error( "Failed to get team token on login, not persisting to localStorage", From 77f76db68bf047a1bbaf043ddbb622e74e9ae407 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 6 Feb 2017 09:50:14 +0000 Subject: [PATCH 016/189] UserSettings: s/vector-web/riot-web/ vector is dead, long live riot --- src/components/structures/UserSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index ff19e7c239e..88857475855 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -906,7 +906,7 @@ module.exports = React.createClass({
matrix-react-sdk version: {REACT_SDK_VERSION}
- vector-web version: {this.state.vectorVersion !== null ? this.state.vectorVersion : 'unknown'}
+ riot-web version: {this.state.vectorVersion !== null ? this.state.vectorVersion : 'unknown'}
olm version: {olmVersionString}
From 484d9d708e52496dc41218b3c54b0662c03b1b33 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 6 Feb 2017 16:01:25 +0000 Subject: [PATCH 017/189] Logging to try to track down riot-web#3148 Since I can't figure out how this is happening, add a shedload of logging to try to pin it down. --- src/Modal.js | 6 ++++++ src/Resend.js | 8 ++++++++ .../views/dialogs/UnknownDeviceDialog.js | 18 ++++++++++++++++-- .../views/rooms/MessageComposerInputOld.js | 8 ++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/Modal.js b/src/Modal.js index b6cc46ed452..7be37da92ee 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -43,7 +43,13 @@ const AsyncWrapper = React.createClass({ componentWillMount: function() { this._unmounted = false; + // XXX: temporary logging to try to diagnose + // https://github.com/vector-im/riot-web/issues/3148 + console.log('Starting load of AsyncWrapper for modal'); this.props.loader((e) => { + // XXX: temporary logging to try to diagnose + // https://github.com/vector-im/riot-web/issues/3148 + console.log('AsyncWrapper load completed with '+e.displayName); if (this._unmounted) { return; } diff --git a/src/Resend.js b/src/Resend.js index ad0f58eb9b1..e2f0c5a1eeb 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -29,11 +29,19 @@ module.exports = { event: event }); }, function(err) { + // XXX: temporary logging to try to diagnose + // https://github.com/vector-im/riot-web/issues/3148 + console.log('Resend got send failure: ' + err.name + '('+err+')'); if (err.name === "UnknownDeviceError") { var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); Modal.createDialog(UnknownDeviceDialog, { devices: err.devices, room: MatrixClientPeg.get().getRoom(event.getRoomId()), + onFinished: (r) => { + // XXX: temporary logging to try to diagnose + // https://github.com/vector-im/riot-web/issues/3148 + console.log('UnknownDeviceDialog closed with '+r); + }, }, "mx_Dialog_unknownDevice"); } diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index 409852a2ccd..3bebb8fdda2 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -103,6 +103,10 @@ export default React.createClass({ MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true); }); }); + + // XXX: temporary logging to try to diagnose + // https://github.com/vector-im/riot-web/issues/3148 + console.log('Opening UnknownDeviceDialog'); }, render: function() { @@ -137,7 +141,12 @@ export default React.createClass({ const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( { + // XXX: temporary logging to try to diagnose + // https://github.com/vector-im/riot-web/issues/3148 + console.log("UnknownDeviceDialog closed by escape"); + this.props.onFinished(); + }} title='Room contains unknown devices' > @@ -152,7 +161,12 @@ export default React.createClass({
diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index b91e5c43913..63821b7d969 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -30,11 +30,19 @@ var TYPING_SERVER_TIMEOUT = 30000; var MARKDOWN_ENABLED = true; export function onSendMessageFailed(err, room) { + // XXX: temporary logging to try to diagnose + // https://github.com/vector-im/riot-web/issues/3148 + console.log('MessageComposer got send failure: ' + err.name + '('+err+')'); if (err.name === "UnknownDeviceError") { const UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); Modal.createDialog(UnknownDeviceDialog, { devices: err.devices, room: room, + onFinished: (r) => { + // XXX: temporary logging to try to diagnose + // https://github.com/vector-im/riot-web/issues/3148 + console.log('UnknownDeviceDialog closed with '+r); + }, }, "mx_Dialog_unknownDevice"); } dis.dispatch({ From af19ea8bb789bd4877f0e40125b577d7cb6ac747 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 7 Feb 2017 12:01:44 +0000 Subject: [PATCH 018/189] Document login API --- src/RtsClient.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/RtsClient.js b/src/RtsClient.js index b8b51791baf..5cf2e811adb 100644 --- a/src/RtsClient.js +++ b/src/RtsClient.js @@ -78,6 +78,13 @@ export default class RtsClient { ); } + /** + * Signal to the RTS that a login has occurred and that a user requires their team's + * token. + * @param {string} userId the user ID of the user who is a member of a team. + * @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon + * success. + */ login(userId) { return request(this._url + '/login', { From c93b6c3c3461e8ac2bda3bf27adef9196cb8034f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 7 Feb 2017 13:15:40 +0000 Subject: [PATCH 019/189] Style, fixes --- src/components/structures/MatrixChat.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b986cd88f06..eea237c0140 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -194,7 +194,7 @@ module.exports = React.createClass({ // Use the locally-stored team token first, then as a fall-back, check to see if // a referral link was used, which will contain a query parameter `team_token`. this._teamToken = window.localStorage.getItem('mx_team_token') || - startingFragmentQueryParams.team_token; + this.props.startingFragmentQueryParams.team_token; }, componentDidMount: function() { @@ -699,10 +699,9 @@ module.exports = React.createClass({ )[0].roomId; self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView}); } else { - if (this._teamToken) { + if (self._teamToken) { self.setState({ready: true, page_type: PageTypes.HomePage}); - } - else { + } else { self.setState({ready: true, page_type: PageTypes.RoomDirectory}); } } @@ -726,8 +725,7 @@ module.exports = React.createClass({ // so point user to fallback like /directory if (self.props.config.home_page) { self.notifyNewScreen('home'); - } - else { + } else { self.notifyNewScreen('directory'); } } From d5f6ecdc496fda661b9afb34e86d2a41d3e0e26f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 7 Feb 2017 13:23:58 +0000 Subject: [PATCH 020/189] Use teamToken, not config when doing screen fallback --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index eea237c0140..db0b0314533 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -723,7 +723,7 @@ module.exports = React.createClass({ } else { // There is no information on presentedId // so point user to fallback like /directory - if (self.props.config.home_page) { + if (self._teamToken) { self.notifyNewScreen('home'); } else { self.notifyNewScreen('directory'); From 3f9f59bb73b743680ef99110ea9beb5e618f940e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 7 Feb 2017 15:23:23 +0000 Subject: [PATCH 021/189] Import RtsClient at top --- src/Lifecycle.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 475cffcff85..5aad0fae365 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -23,6 +23,7 @@ import UserActivity from './UserActivity'; import Presence from './Presence'; import dis from './dispatcher'; import DMRoomMap from './utils/DMRoomMap'; +import RtsClient from './RtsClient'; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -228,7 +229,7 @@ function _restoreFromLocalStorage() { return false; } } -const RtsClient = require("./RtsClient"); + let rtsClient = null; export function initRtsClient(url) { rtsClient = new RtsClient(url); From 3d30db81e0d7571ae5208063320456563fb02f27 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 7 Feb 2017 15:24:57 +0000 Subject: [PATCH 022/189] Only init RTS if configured correctly --- src/components/structures/MatrixChat.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 3e57684ac28..73c8ed21d33 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -210,7 +210,11 @@ module.exports = React.createClass({ window.addEventListener('resize', this.handleResize); this.handleResize(); - Lifecycle.initRtsClient(this.props.config.teamServerConfig.teamServerURL); + if (this.props.config.teamServerConfig && + this.props.config.teamServerConfig.teamServerURL + ) { + Lifecycle.initRtsClient(this.props.config.teamServerConfig.teamServerURL); + } // the extra q() ensures that synchronous exceptions hit the same codepath as // asynchronous ones. From 8fea4c27cb6c8c1793ced6dc4e99659a7466aed4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 7 Feb 2017 22:00:56 +0000 Subject: [PATCH 023/189] fix NPE --- src/components/views/elements/MemberEventListSummary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 61fa0e076f4..1a73b5a50e8 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -385,7 +385,7 @@ module.exports = React.createClass({ } userEvents[userId].push({ mxEvent: e, - displayName: e.target.name || userId, + displayName: (e.target ? e.target.name : null) || userId, index: index, }); }); From 260cb624385a21d056ab609d9ff30a866df3e369 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 8 Feb 2017 16:49:33 +0000 Subject: [PATCH 024/189] Do not set team_token if not returned by RTS on login --- src/Lifecycle.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 5aad0fae365..e899ec6ad85 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -270,7 +270,9 @@ export function setLoggedIn(credentials) { if (rtsClient) { rtsClient.login(credentials.userId).then((body) => { - localStorage.setItem("mx_team_token", body.team_token); + if (body.team_token) { + localStorage.setItem("mx_team_token", body.team_token); + } }, (err) =>{ console.error( "Failed to get team token on login, not persisting to localStorage", From c2d5b72d68a09f7071601cf0d1b4fa76ffa9485e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 8 Feb 2017 17:58:40 +0000 Subject: [PATCH 025/189] Set referrer qp in nextLink This is so that when the verification link is clicked from an email, the referrer is set on the new instance of riot when /rts/register is hit --- src/Signup.js | 4 ++++ src/SignupStages.js | 5 +++++ src/components/structures/login/Registration.js | 3 +++ 3 files changed, 12 insertions(+) diff --git a/src/Signup.js b/src/Signup.js index d3643bd7498..022a93524c2 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -91,6 +91,10 @@ class Register extends Signup { this.params.idSid = idSid; } + setReferrer(referrer) { + this.params.referrer = referrer; + } + setGuestAccessToken(token) { this.guestAccessToken = token; } diff --git a/src/SignupStages.js b/src/SignupStages.js index 6bdc331566c..cdb9d5989b7 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -136,6 +136,11 @@ class EmailIdentityStage extends Stage { "&session_id=" + encodeURIComponent(this.signupInstance.getServerData().session); + // Add the user ID of the referring user, if set + if (this.signupInstance.params.referrer) { + nextLink += "&referrer=" + encodeURIComponent(this.signupInstance.params.referrer); + } + var self = this; return this.client.requestRegisterEmailToken( this.signupInstance.email, diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 0fc0cac5273..efe7dae723a 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -98,6 +98,9 @@ module.exports = React.createClass({ this.registerLogic.setRegistrationUrl(this.props.registrationUrl); this.registerLogic.setIdSid(this.props.idSid); this.registerLogic.setGuestAccessToken(this.props.guestAccessToken); + if (this.props.referrer) { + this.registerLogic.setReferrer(this.props.referrer); + } this.registerLogic.recheckState(); if ( From 231997dd63da33b231307e8ea5ff6db5b8375d80 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 9 Feb 2017 01:18:09 +0000 Subject: [PATCH 026/189] unbreak /markdown off --- src/HtmlUtils.js | 2 +- src/components/views/rooms/MessageComposerInputOld.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index c7b13bc0713..b9d0ce67e88 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -290,7 +290,7 @@ export function bodyToHtml(content, highlights, opts) { } EMOJI_REGEX.lastIndex = 0; - let contentBodyTrimmed = content.body.trim(); + let contentBodyTrimmed = content.body !== undefined ? content.body.trim() : ''; let match = EMOJI_REGEX.exec(contentBodyTrimmed); let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length; diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index 63821b7d969..020c2238cb3 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -352,7 +352,7 @@ export default React.createClass({ MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText); } else { - const contentText = mdown.toPlaintext(); + if (mdown) contentText = mdown.toPlaintext(); sendMessagePromise = isEmote ? MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) : MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText); From ea50acfc8748835a0c3e5d22cfa3d78f4ddffc7f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 9 Feb 2017 01:26:53 +0000 Subject: [PATCH 027/189] fix dark theme for uploadbar --- src/components/structures/UploadBar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js index e91e558cb2d..8266a11bc8c 100644 --- a/src/components/structures/UploadBar.js +++ b/src/components/structures/UploadBar.js @@ -90,8 +90,8 @@ module.exports = React.createClass({displayName: 'UploadBar',
- - +
From ad2710ec5d94aa0ad98437d088b0de8f7e9fe51b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 9 Feb 2017 02:00:58 +0000 Subject: [PATCH 028/189] fix CSS for import/export buttons --- src/components/structures/UserSettings.js | 28 +++++++++++------------ 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 88857475855..fdade60dfda 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -552,21 +552,20 @@ module.exports = React.createClass({ const deviceId = client.deviceId; const identityKey = client.getDeviceEd25519Key() || ""; - let exportButton = null, - importButton = null; + let importExportButtons = null; if (client.isCryptoEnabled) { - exportButton = ( - - Export E2E room keys - - ); - importButton = ( - - Import E2E room keys - + importExportButtons = ( +
+ + Export E2E room keys + + + Import E2E room keys + +
); } return ( @@ -577,8 +576,7 @@ module.exports = React.createClass({
  • {deviceId}
  • {identityKey}
  • - {exportButton} - {importButton} + { importExportButtons }
    { CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) } From 747c0c44a68be5d40dfcad34b26af15e6d6092b0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Feb 2017 09:24:46 +0000 Subject: [PATCH 029/189] Use new method of getting team icon This was necessary because the team token may not be known when registering, but domain is. Storing the icon under the "common" directory is the chosen solution to this. --- src/components/structures/login/Registration.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index efe7dae723a..78e65519ff4 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -426,7 +426,12 @@ module.exports = React.createClass({ return (
    - + {this._getRegisterContentJsx()}
    From 40658494b4b90bfe9779fa1e849b592c89210da8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Feb 2017 09:29:55 +0000 Subject: [PATCH 030/189] Consider emails ending in matrix.org as a uni email For the purposes of testing (and having a team page) --- src/components/views/login/RegistrationForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 1cb82538120..1a448fa84ed 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -146,7 +146,7 @@ module.exports = React.createClass({ }, _isUniEmail: function(email) { - return email.endsWith('.ac.uk') || email.endsWith('.edu'); + return email.endsWith('.ac.uk') || email.endsWith('.edu') || email.endsWith('matrix.org'); }, validateField: function(field_id) { From cc69e982a75985964afdccb3e9fcc264e221ff5e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Feb 2017 10:01:51 +0000 Subject: [PATCH 031/189] Use single state to set both avatars and typing notif --- src/WhoIsTyping.js | 3 +-- src/components/structures/RoomStatusBar.js | 21 +++++++++------------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js index ecd7c495f93..bff536a61eb 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.js @@ -48,8 +48,7 @@ module.exports = { return whoIsTyping; }, - whoIsTypingString: function(room, limit) { - const whoIsTyping = this.usersTypingApartFromMe(room); + whoIsTypingString: function(whoIsTyping, limit) { const othersCount = limit === undefined ? 0 : Math.max(whoIsTyping.length - limit, 0); if (whoIsTyping.length == 0) { diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 3fd0a3b751e..d0f4fbfa0f0 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -89,10 +89,7 @@ module.exports = React.createClass({ getInitialState: function() { return { syncState: MatrixClientPeg.get().getSyncState(), - whoisTypingString: WhoIsTyping.whoIsTypingString( - this.props.room, - this.props.whoIsTypingLimit - ), + usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room), }; }, @@ -141,10 +138,7 @@ module.exports = React.createClass({ onRoomMemberTyping: function(ev, member) { this.setState({ - whoisTypingString: WhoIsTyping.whoIsTypingString( - this.props.room, - this.props.whoIsTypingLimit - ), + usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room), }); }, @@ -153,7 +147,7 @@ module.exports = React.createClass({ // indicate other sizes. _getSize: function(state, props) { if (state.syncState === "ERROR" || - state.whoisTypingString || + (state.usersTyping.length > 0) || props.numUnreadMessages || !props.atEndOfLiveTimeline || props.hasActiveCall) { @@ -220,7 +214,7 @@ module.exports = React.createClass({ }, _renderTypingIndicatorAvatars: function(limit) { - let users = WhoIsTyping.usersTypingApartFromMe(this.props.room); + let users = this.state.usersTyping; let othersCount = Math.max(users.length - limit, 0); users = users.slice(0, limit); @@ -324,7 +318,10 @@ module.exports = React.createClass({ ); } - var typingString = this.state.whoisTypingString; + const typingString = WhoIsTyping.whoIsTypingString( + this.state.usersTyping, + this.props.whoIsTypingLimit + ); if (typingString) { return (
    @@ -347,7 +344,7 @@ module.exports = React.createClass({ render: function() { var content = this._getContent(); - var indicator = this._getIndicator(this.state.whoisTypingString !== null); + var indicator = this._getIndicator(this.state.usersTyping.length > 0); return (
    From 553054409fdd930656da2a755c7d3d0dff8a8d6a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Feb 2017 10:01:59 +0000 Subject: [PATCH 032/189] Use (props,state) ordering of arguments There was a bug here that meant that sometimes arguments were given in the wrong order; presumably leading to the status bar not appearing for calls etc. --- src/components/structures/RoomStatusBar.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index d0f4fbfa0f0..adbfb25337b 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -103,7 +103,7 @@ module.exports = React.createClass({ this.props.onResize(); } - const size = this._getSize(this.state, this.props); + const size = this._getSize(this.props, this.state); if (size > 0) { this.props.onVisible(); } else { @@ -145,7 +145,7 @@ module.exports = React.createClass({ // We don't need the actual height - just whether it is likely to have // changed - so we use '0' to indicate normal size, and other values to // indicate other sizes. - _getSize: function(state, props) { + _getSize: function(props, state) { if (state.syncState === "ERROR" || (state.usersTyping.length > 0) || props.numUnreadMessages || @@ -163,7 +163,8 @@ module.exports = React.createClass({ // determine if we need to call onResize _checkForResize: function(prevProps, prevState) { // figure out the old height and the new height of the status bar. - return this._getSize(prevProps, prevState) !== this._getSize(this.props, this.state); + return this._getSize(prevProps, prevState) + !== this._getSize(this.props, this.state); }, // return suitable content for the image on the left of the status bar. From 103710728f74f3c74e5ead0a65eaf59084788f5a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Feb 2017 10:30:06 +0000 Subject: [PATCH 033/189] Do not show "+1 other" Instead show a user name or avatar. --- src/WhoIsTyping.js | 8 +++++--- src/components/structures/RoomStatusBar.js | 9 ++++++--- src/components/structures/RoomView.js | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js index bff536a61eb..4502b0ccd99 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.js @@ -49,8 +49,10 @@ module.exports = { }, whoIsTypingString: function(whoIsTyping, limit) { - const othersCount = limit === undefined ? - 0 : Math.max(whoIsTyping.length - limit, 0); + let othersCount = 0; + if (whoIsTyping.length > limit) { + othersCount = whoIsTyping.length - limit + 1; + } if (whoIsTyping.length == 0) { return ''; } else if (whoIsTyping.length == 1) { @@ -61,7 +63,7 @@ module.exports = { }); if (othersCount) { const other = ' other' + (othersCount > 1 ? 's' : ''); - return names.slice(0, limit).join(', ') + ' and ' + + return names.slice(0, limit - 1).join(', ') + ' and ' + othersCount + other + ' are typing'; } else { const lastPerson = names.pop(); diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index adbfb25337b..288ca0b974d 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -82,7 +82,7 @@ module.exports = React.createClass({ getDefaultProps: function() { return { - whoIsTypingLimit: 2, + whoIsTypingLimit: 3, }; }, @@ -217,8 +217,11 @@ module.exports = React.createClass({ _renderTypingIndicatorAvatars: function(limit) { let users = this.state.usersTyping; - let othersCount = Math.max(users.length - limit, 0); - users = users.slice(0, limit); + let othersCount = 0; + if (users.length > limit) { + othersCount = users.length - limit + 1; + users = users.slice(0, limit - 1); + } let avatars = users.map((u, index) => { let showInitial = othersCount === 0 && index === users.length - 1; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index a7d52019c47..432dc5b724a 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1537,7 +1537,7 @@ module.exports = React.createClass({ onResize={this.onChildResize} onVisible={this.onStatusBarVisible} onHidden={this.onStatusBarHidden} - whoIsTypingLimit={2} + whoIsTypingLimit={3} />; } From 69add8fd642a584281aecf0c775702144d2073fb Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Feb 2017 13:16:46 +0000 Subject: [PATCH 034/189] Actually use the RTS URL --- src/components/structures/login/Registration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 78e65519ff4..7887f3e7deb 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -428,7 +428,7 @@ module.exports = React.createClass({
    From 8d3876c7d04dea503e4c08f97e475069527eba5b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Feb 2017 15:14:16 +0000 Subject: [PATCH 035/189] MELS: either expanded or summary, not both Fixes vector-im/riot-web#3097 --- .../views/elements/MemberEventListSummary.js | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 1a73b5a50e8..1eb7b928d1b 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -108,7 +108,7 @@ module.exports = React.createClass({ } return ( - + {summaries.join(", ")} ); @@ -264,7 +264,7 @@ module.exports = React.createClass({ ); }); return ( - + {avatars} ); @@ -397,31 +397,28 @@ module.exports = React.createClass({ (seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2] ); - const avatars = this._renderAvatars(avatarMembers); - const summary = this._renderSummary(aggregate.names, orderedTransitionSequences); + let summaryContainer = null; + if (!expanded) { + summaryContainer = ( +
    +
    + {this._renderAvatars(avatarMembers)} + {this._renderSummary(aggregate.names, orderedTransitionSequences)} +
    +
    + ); + } const toggleButton = ( - +
    {expanded ? 'collapse' : 'expand'} - - ); - - const summaryContainer = ( -
    -
    - - {avatars} - - - {summary} -   - {toggleButton} -
    ); return (
    + {toggleButton} {summaryContainer} + {expanded ?
     
    : null} {expandedEvents}
    ); From 45f5b8b3a9261f63c3c7ac00a518d7694abfb62e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 10 Feb 2017 09:57:28 +0000 Subject: [PATCH 036/189] Thread teamToken through to LeftPanel for "Home" button This means the riot-web will use the same teamToken used by sdk components. This includes cases where only the fragment query parameter has been provided. Fixes matrix-org/riot-web#3185 --- src/components/structures/LoggedInView.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index ba63794f60e..961277a4a1e 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -232,7 +232,12 @@ export default React.createClass({
    {topBar}
    - +
    {page_element}
    From bab6a0b84a621210ce5e0a8f2dafb069d7f51e85 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 10 Feb 2017 11:31:04 +0000 Subject: [PATCH 037/189] Persist query parameter team token across refreshes --- src/components/structures/MatrixChat.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ab73ba366bd..c3467355bda 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -191,10 +191,18 @@ module.exports = React.createClass({ MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; } + // Persist the team token across refreshes + if (this.props.startingFragmentQueryParams.team_token) { + window.sessionStorage.setItem( + 'mx_team_token', + this.props.startingFragmentQueryParams.team_token, + ); + } + // Use the locally-stored team token first, then as a fall-back, check to see if // a referral link was used, which will contain a query parameter `team_token`. this._teamToken = window.localStorage.getItem('mx_team_token') || - this.props.startingFragmentQueryParams.team_token; + window.sessionStorage.getItem('mx_team_token'); }, componentDidMount: function() { From ec730056d85d2d1e9ec04dff4cbf705f5ffa2e5f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 10 Feb 2017 11:39:22 +0000 Subject: [PATCH 038/189] Alter comment --- src/components/structures/MatrixChat.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index c3467355bda..8fdcf15e1b6 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -191,7 +191,8 @@ module.exports = React.createClass({ MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; } - // Persist the team token across refreshes + // Persist the team token across refreshes using sessionStorage. A new window or + // tab will not persist sessionStorage, but refreshes will. if (this.props.startingFragmentQueryParams.team_token) { window.sessionStorage.setItem( 'mx_team_token', From 407bcf1bb932ca53a753ae9d88a21bd44f020710 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 10 Feb 2017 14:22:54 +0000 Subject: [PATCH 039/189] Delete database on logout. DI a SyncAccumulator. Log uncaught errors --- src/Lifecycle.js | 11 +++++++++++ src/MatrixClientPeg.js | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 493bbf12aa5..739e8e38325 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -329,10 +329,21 @@ export function startMatrixClient() { */ export function onLoggedOut() { _clearLocalStorage(); + _clearIndexedDB(); stopMatrixClient(); dis.dispatch({action: 'on_logged_out'}); } +function _clearIndexedDB() { + // remove indexeddb instances + if (!window.indexedDB) { + return; + } + console.log("Clearing indexeddb"); + window.indexedDB.deleteDatabase("matrix-js-sdk"); + window.indexedDB.deleteDatabase("logs"); +} + function _clearLocalStorage() { if (!window.localStorage) { return; diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 36521204c53..42834618aec 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -79,6 +79,7 @@ class MatrixClientPeg { console.log("Loading history from IndexedDB."); promise = this.matrixClient.store.startup(); } + promise.catch((err) => { console.error(err); }); promise.finally(() => { this.get().startClient(opts); @@ -124,7 +125,8 @@ class MatrixClientPeg { } if (window.indexedDB && localStorage) { opts.store = new Matrix.IndexedDBStore( - new Matrix.IndexedDBStoreBackend(window.indexedDB), { + new Matrix.IndexedDBStoreBackend(window.indexedDB), + new Matrix.SyncAccumulator(), { localStorage: localStorage, } ); From bdd031eac2b4ea70d43c03c0c11b26a4760405ea Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 10 Feb 2017 15:09:45 +0000 Subject: [PATCH 040/189] Enable branded URLs again by parsing the path client-side Use the first path segment to key off config.teamTokenMap, which contains a mapping to teamTokens. The client then behaves as before, keeping the path in the address bar constant with no redirects required. --- src/components/structures/MatrixChat.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8fdcf15e1b6..75ae51e9e58 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -191,6 +191,17 @@ module.exports = React.createClass({ MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; } + // To enable things like riot.im/geektime in a nicer way than rewriting the URL + // and appending a team token query parameter, use the first path segment to + // indicate a team, with "public" team tokens stored in the config teamTokenMap. + let routedTeamToken = null; + if (this.props.config.teamTokenMap) { + const teamName = window.location.pathname.split('/')[1]; + if (this.props.config.teamTokenMap.hasOwnProperty(teamName)) { + routedTeamToken = this.props.config.teamTokenMap[teamName]; + } + } + // Persist the team token across refreshes using sessionStorage. A new window or // tab will not persist sessionStorage, but refreshes will. if (this.props.startingFragmentQueryParams.team_token) { @@ -202,8 +213,13 @@ module.exports = React.createClass({ // Use the locally-stored team token first, then as a fall-back, check to see if // a referral link was used, which will contain a query parameter `team_token`. - this._teamToken = window.localStorage.getItem('mx_team_token') || + this._teamToken = routedTeamToken || + window.localStorage.getItem('mx_team_token') || window.sessionStorage.getItem('mx_team_token'); + + if (this._teamToken) { + console.info(`Team token set to ${this._teamToken}`); + } }, componentDidMount: function() { From c46f28213995ef6a10f0c3715bef7b5b43ecc595 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 10 Feb 2017 16:19:39 +0000 Subject: [PATCH 041/189] Comments --- src/MatrixClientPeg.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 42834618aec..0267b36464e 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -79,8 +79,11 @@ class MatrixClientPeg { console.log("Loading history from IndexedDB."); promise = this.matrixClient.store.startup(); } + // log any errors when starting up the database promise.catch((err) => { console.error(err); }); + // regardless of errors, start the client. If we did error out, we'll + // just end up doing a full initial /sync. promise.finally(() => { this.get().startClient(opts); }); From 29f5e88f6aae7bb8a4d827f9a607b5b713a48bce Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 10 Feb 2017 16:50:25 +0000 Subject: [PATCH 042/189] Instead of sending userId, userEmail, send sid, client_secret This has the benefit of being possible from the _second_ riot instance, which may not actually have the email of the user registering. With these parameters, the RTS can get the email and user ID itself. (see https://github.com/matrix-org/riot-team-server/pull/15) --- src/RtsClient.js | 10 +++++----- src/components/structures/login/Registration.js | 11 +++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/RtsClient.js b/src/RtsClient.js index 5cf2e811adb..8c3ce54b375 100644 --- a/src/RtsClient.js +++ b/src/RtsClient.js @@ -50,18 +50,18 @@ export default class RtsClient { * Track a referral with the Riot Team Server. This should be called once a referred * user has been successfully registered. * @param {string} referrer the user ID of one who referred the user to Riot. - * @param {string} userId the user ID of the user being referred. - * @param {string} userEmail the email address linked to `userId`. + * @param {string} sid the sign-up identity server session ID . + * @param {string} clientSecret the sign-up client secret. * @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon * success. */ - trackReferral(referrer, userId, userEmail) { + trackReferral(referrer, sid, clientSecret) { return request(this._url + '/register', { body: { referrer: referrer, - user_id: userId, - user_email: userEmail, + session_id: sid, + client_secret: clientSecret, }, method: 'POST', } diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index efe7dae723a..ddd67921a83 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -213,16 +213,19 @@ module.exports = React.createClass({ accessToken: response.access_token }); + // Done regardless of `teamSelected`. People registering with non-team emails + // will just nop. The point of this being we might not have the email address + // that the user registered with at this stage (depending on whether this + // is the client they initiated registration). if ( self._rtsClient && - self.props.referrer && - self.state.teamSelected + self.props.referrer ) { // Track referral, get team_token in order to retrieve team config self._rtsClient.trackReferral( self.props.referrer, - response.user_id, - self.state.formVals.email + self.registerLogic.params.idSid, + self.registerLogic.params.clientSecret ).then((data) => { const teamToken = data.team_token; // Store for use /w welcome pages From 4ac769168aa2a9035e83e6616bd747ba7ebed41c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 10 Feb 2017 17:17:58 +0000 Subject: [PATCH 043/189] View /home on registered /w team --- src/components/structures/login/Registration.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index efe7dae723a..bd4dad5d5f2 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -227,6 +227,9 @@ module.exports = React.createClass({ const teamToken = data.team_token; // Store for use /w welcome pages window.localStorage.setItem('mx_team_token', teamToken); + // Set the team token and view homepage + window.matrixChat._teamToken = teamToken; + window.mxDispatcher.dispatch({action: 'view_home_page'}); self._rtsClient.getTeam(teamToken).then((team) => { console.log( From 75deb5584495f07247772eb65b42ef68b983b4eb Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 13 Feb 2017 11:48:03 +0000 Subject: [PATCH 044/189] Null check on teamName --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 75ae51e9e58..8d08c19001c 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -197,7 +197,7 @@ module.exports = React.createClass({ let routedTeamToken = null; if (this.props.config.teamTokenMap) { const teamName = window.location.pathname.split('/')[1]; - if (this.props.config.teamTokenMap.hasOwnProperty(teamName)) { + if (teamName && this.props.config.teamTokenMap.hasOwnProperty(teamName)) { routedTeamToken = this.props.config.teamTokenMap[teamName]; } } From 16e3365240b5d4c737d8626ceece0a918cefd5e9 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 13 Feb 2017 14:36:03 +0000 Subject: [PATCH 045/189] Use a callback prop instead of `window.` --- src/components/structures/MatrixChat.js | 6 ++++++ src/components/structures/login/Registration.js | 5 ++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8fdcf15e1b6..a2a7e346692 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -975,6 +975,11 @@ module.exports = React.createClass({ this._setPage(PageTypes.UserSettings); }, + onTeamMemberRegistered: function(teamToken) { + this._teamToken = teamToken; + this._setPage(PageTypes.HomePage); + }, + onFinishPostRegistration: function() { // Don't confuse this with "PageType" which is the middle window to show this.setState({ @@ -1103,6 +1108,7 @@ module.exports = React.createClass({ customIsUrl={this.getCurrentIsUrl()} registrationUrl={this.props.registrationUrl} defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} + onTeamMemberRegistered={this.onTeamMemberRegistered} onLoggedIn={this.onRegistered} onLoginClick={this.onLoginClick} onRegisterClick={this.onRegisterClick} diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index bd4dad5d5f2..8af208de10e 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -58,6 +58,7 @@ module.exports = React.createClass({ teamServerURL: React.PropTypes.string.isRequired, }), teamSelected: React.PropTypes.object, + onTeamMemberRegistered: React.PropTypes.func.isRequired, defaultDeviceDisplayName: React.PropTypes.string, @@ -227,9 +228,7 @@ module.exports = React.createClass({ const teamToken = data.team_token; // Store for use /w welcome pages window.localStorage.setItem('mx_team_token', teamToken); - // Set the team token and view homepage - window.matrixChat._teamToken = teamToken; - window.mxDispatcher.dispatch({action: 'view_home_page'}); + self.props.onTeamMemberRegistered(teamToken); self._rtsClient.getTeam(teamToken).then((team) => { console.log( From 79d9deb339bdcde60e235b084e51a542ba3a1557 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 13 Feb 2017 16:03:21 +0000 Subject: [PATCH 046/189] Split out InterActiveAuthDialog Into a component that does Interactive Auth and a dialog that wraps it, so we can do interactive auth not necessarily in a dialog. As a side effect: * Put the buttons for each auth stage in the stage itself. Some stages don't have submit buttons (and it's very possible other stages may have other buttons entirely, like 'resend') so it makes more sense for the buttons to live in the stage components themselves. Plus it saves the slightly evil calling-functions-on-react-children thing we were doing (and indeed extending that to show the submit button at all). * Give all BaseDialogs a cross in the top right to cancel. They were all dismissable by clicking outside or pressing esc, so this adds a more visually obvious way of dismissing them. Plus, it means our InteractiveAuthDialog can have a way of canceling the whole operation separate from buttons for the individual stages. --- src/component-index.js | 2 + src/components/structures/InteractiveAuth.js | 153 +++++++++++++++++ src/components/views/dialogs/BaseDialog.js | 15 ++ .../views/dialogs/InteractiveAuthDialog.js | 154 +----------------- .../login/InteractiveAuthEntryComponents.js | 42 ++--- 5 files changed, 201 insertions(+), 165 deletions(-) create mode 100644 src/components/structures/InteractiveAuth.js diff --git a/src/component-index.js b/src/component-index.js index 5b28be0627c..b357472ad76 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -31,6 +31,8 @@ import structures$CreateRoom from './components/structures/CreateRoom'; structures$CreateRoom && (module.exports.components['structures.CreateRoom'] = structures$CreateRoom); import structures$FilePanel from './components/structures/FilePanel'; structures$FilePanel && (module.exports.components['structures.FilePanel'] = structures$FilePanel); +import structures$InteractiveAuth from './components/structures/InteractiveAuth'; +structures$InteractiveAuth && (module.exports.components['structures.InteractiveAuth'] = structures$InteractiveAuth); import structures$LoggedInView from './components/structures/LoggedInView'; structures$LoggedInView && (module.exports.components['structures.LoggedInView'] = structures$LoggedInView); import structures$MatrixChat from './components/structures/MatrixChat'; diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js new file mode 100644 index 00000000000..205d2ca1b85 --- /dev/null +++ b/src/components/structures/InteractiveAuth.js @@ -0,0 +1,153 @@ +/* +Copyright 2017 Vector Creations Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Matrix from 'matrix-js-sdk'; +const InteractiveAuth = Matrix.InteractiveAuth; + +import React from 'react'; + +import sdk from '../../index'; + +import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents'; + +export default React.createClass({ + displayName: 'InteractiveAuth', + + propTypes: { + // response from initial request. If not supplied, will do a request on + // mount. + authData: React.PropTypes.shape({ + flows: React.PropTypes.array, + params: React.PropTypes.object, + session: React.PropTypes.string, + }), + + // callback + makeRequest: React.PropTypes.func.isRequired, + + onFinished: React.PropTypes.func.isRequired, + }, + + getInitialState: function() { + return { + authStage: null, + busy: false, + errorText: null, + stageErrorText: null, + submitButtonEnabled: false, + }; + }, + + componentWillMount: function() { + this._unmounted = false; + this._authLogic = new InteractiveAuth({ + authData: this.props.authData, + doRequest: this._requestCallback, + startAuthStage: this._startAuthStage, + }); + + this._authLogic.attemptAuth().then((result) => { + this.props.onFinished(true, result); + }).catch((error) => { + console.error("Error during user-interactive auth:", error); + if (this._unmounted) { + return; + } + + const msg = error.message || error.toString(); + this.setState({ + errorText: msg + }); + }).done(); + }, + + componentWillUnmount: function() { + this._unmounted = true; + }, + + _startAuthStage: function(stageType, error) { + this.setState({ + authStage: stageType, + errorText: error ? error.error : null, + }, this._setFocus); + }, + + _requestCallback: function(auth) { + this.setState({ + busy: true, + errorText: null, + stageErrorText: null, + }); + return this.props.makeRequest(auth).finally(() => { + if (this._unmounted) { + return; + } + this.setState({ + busy: false, + }); + }); + }, + + _setFocus: function() { + if (this.refs.stageComponent && this.refs.stageComponent.focus) { + this.refs.stageComponent.focus(); + } + }, + + _onCancel: function() { + this.props.onFinished(false); + }, + + _submitAuthDict: function(authData) { + this._authLogic.submitAuthDict(authData); + }, + + _renderCurrentStage: function() { + const stage = this.state.authStage; + var StageComponent = getEntryComponentForLoginType(stage); + return ( + + ); + }, + + render: function() { + const Loader = sdk.getComponent("elements.Spinner"); + + let error = null; + if (this.state.errorText) { + error = ( +
    + {this.state.errorText} +
    + ); + } + + return ( +
    +
    + {this._renderCurrentStage()} + {error} +
    +
    + ); + }, +}); diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 2b3980c5364..01a16e86acf 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import * as KeyCode from '../../../KeyCode'; +import AccessibleButton from '../elements/AccessibleButton'; /** * Basic container for modal dialogs. @@ -59,9 +60,23 @@ export default React.createClass({ } }, + _onCancelClick: function(e) { + e.stopPropagation(); + e.preventDefault(); + this.props.onFinished(); + }, + render: function() { return (
    + + Cancel +
    { this.props.title }
    diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index a4abbb17d94..1c9c8720707 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -15,13 +15,12 @@ limitations under the License. */ import Matrix from 'matrix-js-sdk'; -const InteractiveAuth = Matrix.InteractiveAuth; import React from 'react'; import sdk from '../../../index'; -import {getEntryComponentForLoginType} from '../login/InteractiveAuthEntryComponents'; +import AccessibleButton from '../elements/AccessibleButton'; export default React.createClass({ displayName: 'InteractiveAuthDialog', @@ -41,168 +40,33 @@ export default React.createClass({ onFinished: React.PropTypes.func.isRequired, title: React.PropTypes.string, - submitButtonLabel: React.PropTypes.string, }, getDefaultProps: function() { return { title: "Authentication", - submitButtonLabel: "Submit", }; }, - getInitialState: function() { - return { - authStage: null, - busy: false, - errorText: null, - stageErrorText: null, - submitButtonEnabled: false, - }; - }, - - componentWillMount: function() { - this._unmounted = false; - this._authLogic = new InteractiveAuth({ - authData: this.props.authData, - doRequest: this._requestCallback, - startAuthStage: this._startAuthStage, - }); - - this._authLogic.attemptAuth().then((result) => { - this.props.onFinished(true, result); - }).catch((error) => { - console.error("Error during user-interactive auth:", error); - if (this._unmounted) { - return; - } - - const msg = error.message || error.toString(); - this.setState({ - errorText: msg - }); - }).done(); - }, - - componentWillUnmount: function() { - this._unmounted = true; - }, - - _startAuthStage: function(stageType, error) { - this.setState({ - authStage: stageType, - errorText: error ? error.error : null, - }, this._setFocus); - }, - - _requestCallback: function(auth) { - this.setState({ - busy: true, - errorText: null, - stageErrorText: null, - }); - return this.props.makeRequest(auth).finally(() => { - if (this._unmounted) { - return; - } - this.setState({ - busy: false, - }); - }); - }, - - _onEnterPressed: function(e) { - if (this.state.submitButtonEnabled && !this.state.busy) { - this._onSubmit(); - } - }, - - _onSubmit: function() { - if (this.refs.stageComponent && this.refs.stageComponent.onSubmitClick) { - this.refs.stageComponent.onSubmitClick(); - } - }, - - _setFocus: function() { - if (this.refs.stageComponent && this.refs.stageComponent.focus) { - this.refs.stageComponent.focus(); - } - }, - - _onCancel: function() { + _onCancelClick: function() { this.props.onFinished(false); }, - _setSubmitButtonEnabled: function(enabled) { - this.setState({ - submitButtonEnabled: enabled, - }); - }, - - _submitAuthDict: function(authData) { - this._authLogic.submitAuthDict(authData); - }, - - _renderCurrentStage: function() { - const stage = this.state.authStage; - var StageComponent = getEntryComponentForLoginType(stage); - return ( - - ); - }, - render: function() { - const Loader = sdk.getComponent("elements.Spinner"); + const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth"); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - let error = null; - if (this.state.errorText) { - error = ( -
    - {this.state.errorText} -
    - ); - } - - const submitLabel = this.state.busy ? : this.props.submitButtonLabel; - const submitEnabled = this.state.submitButtonEnabled && !this.state.busy; - - const submitButton = ( - - ); - - const cancelButton = ( - - ); - return ( -
    -

    This operation requires additional authentication.

    - {this._renderCurrentStage()} - {error} -
    -
    - {submitButton} - {cancelButton} +
    +
    ); diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index ec184ca09f9..a8c58ea76fc 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -32,7 +32,6 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; * stageParams: params from the server for the stage being attempted * errorText: error message from a previous attempt to authenticate * submitAuthDict: a function which will be called with the new auth dict - * setSubmitButtonEnabled: a function which will enable/disable the 'submit' button * * Each component may also provide the following functions (beyond the standard React ones): * onSubmitClick: handle a 'submit' button click @@ -48,12 +47,13 @@ export const PasswordAuthEntry = React.createClass({ propTypes: { submitAuthDict: React.PropTypes.func.isRequired, - setSubmitButtonEnabled: React.PropTypes.func.isRequired, errorText: React.PropTypes.string, }, - componentWillMount: function() { - this.props.setSubmitButtonEnabled(false); + getInitialState: function() { + return { + enableSubmit: false, + }; }, focus: function() { @@ -62,7 +62,7 @@ export const PasswordAuthEntry = React.createClass({ } }, - onSubmitClick: function() { + _onSubmit: function() { this.props.submitAuthDict({ type: PasswordAuthEntry.LOGIN_TYPE, user: MatrixClientPeg.get().credentials.userId, @@ -72,7 +72,9 @@ export const PasswordAuthEntry = React.createClass({ _onPasswordFieldChange: function(ev) { // enable the submit button iff the password is non-empty - this.props.setSubmitButtonEnabled(Boolean(ev.target.value)); + this.setState({ + enableSubmit: Boolean(this.refs.passwordField.value), + }); }, render: function() { @@ -86,12 +88,20 @@ export const PasswordAuthEntry = React.createClass({

    To continue, please enter your password.

    Password:

    - +
    + +
    + +
    +
    {this.props.errorText}
    @@ -110,14 +120,9 @@ export const RecaptchaAuthEntry = React.createClass({ propTypes: { submitAuthDict: React.PropTypes.func.isRequired, stageParams: React.PropTypes.object.isRequired, - setSubmitButtonEnabled: React.PropTypes.func.isRequired, errorText: React.PropTypes.string, }, - componentWillMount: function() { - this.props.setSubmitButtonEnabled(false); - }, - _onCaptchaResponse: function(response) { this.props.submitAuthDict({ type: RecaptchaAuthEntry.LOGIN_TYPE, @@ -148,7 +153,6 @@ export const FallbackAuthEntry = React.createClass({ authSessionId: React.PropTypes.string.isRequired, loginType: React.PropTypes.string.isRequired, submitAuthDict: React.PropTypes.func.isRequired, - setSubmitButtonEnabled: React.PropTypes.func.isRequired, errorText: React.PropTypes.string, }, @@ -156,7 +160,6 @@ export const FallbackAuthEntry = React.createClass({ // we have to make the user click a button, as browsers will block // the popup if we open it immediately. this._popupWindow = null; - this.props.setSubmitButtonEnabled(true); window.addEventListener("message", this._onReceiveMessage); }, @@ -173,7 +176,6 @@ export const FallbackAuthEntry = React.createClass({ this.props.authSessionId ); this._popupWindow = window.open(url); - this.props.setSubmitButtonEnabled(false); }, _onReceiveMessage: function(event) { From 77b226631a13c6c893c08943a781a2f0a1c0a874 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 13 Feb 2017 16:15:00 +0000 Subject: [PATCH 047/189] Copyright --- src/components/views/dialogs/InteractiveAuthDialog.js | 1 + src/components/views/login/InteractiveAuthEntryComponents.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index 1c9c8720707..ecca00358f5 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index a8c58ea76fc..e4c48b1b1be 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 8fc31045079c3a5c5df130500c508e2404cffc68 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 13 Feb 2017 18:52:33 +0000 Subject: [PATCH 048/189] Replace submit button with a spinner when busy and update test accordingly --- src/components/structures/InteractiveAuth.js | 1 + .../login/InteractiveAuthEntryComponents.js | 30 ++++++++++++++----- .../dialogs/InteractiveAuthDialog-test.js | 30 ++++++++++++------- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 205d2ca1b85..c29b0fe16af 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -125,6 +125,7 @@ export default React.createClass({ stageParams={this._authLogic.getStageParams(stage)} submitAuthDict={this._submitAuthDict} errorText={this.state.stageErrorText} + busy={this.state.busy} /> ); }, diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index e4c48b1b1be..4759a30c889 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -49,11 +49,14 @@ export const PasswordAuthEntry = React.createClass({ propTypes: { submitAuthDict: React.PropTypes.func.isRequired, errorText: React.PropTypes.string, + // is the auth logic currently waiting for something to + // happen? + busy: React.PropTypes.bool, }, getInitialState: function() { return { - enableSubmit: false, + passwordValid: false, }; }, @@ -63,7 +66,10 @@ export const PasswordAuthEntry = React.createClass({ } }, - _onSubmit: function() { + _onSubmit: function(e) { + e.preventDefault(); + if (this.props.busy) return; + this.props.submitAuthDict({ type: PasswordAuthEntry.LOGIN_TYPE, user: MatrixClientPeg.get().credentials.userId, @@ -74,7 +80,7 @@ export const PasswordAuthEntry = React.createClass({ _onPasswordFieldChange: function(ev) { // enable the submit button iff the password is non-empty this.setState({ - enableSubmit: Boolean(this.refs.passwordField.value), + passwordValid: Boolean(this.refs.passwordField.value), }); }, @@ -85,6 +91,19 @@ export const PasswordAuthEntry = React.createClass({ passwordBoxClass = 'error'; } + let submitButtonOrSpinner; + if (this.props.busy) { + const Loader = sdk.getComponent("elements.Spinner"); + submitButtonOrSpinner = ; + } else { + submitButtonOrSpinner = ( + + ); + } + return (

    To continue, please enter your password.

    @@ -97,10 +116,7 @@ export const PasswordAuthEntry = React.createClass({ type="password" />
    - + {submitButtonOrSpinner}
    diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index 35daace0f80..80f027ab44e 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -67,16 +67,24 @@ describe('InteractiveAuthDialog', function () { onFinished={onFinished} />, parentDiv); - // at this point there should be a password box - const passwordNode = ReactTestUtils.findRenderedDOMComponentWithTag( + // at this point there should be a password box and a submit button + const formNode = ReactTestUtils.findRenderedDOMComponentWithTag(dlg, "form"); + const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag( dlg, "input" ); - expect(passwordNode.type).toEqual("password"); + let passwordNode; + let submitNode; + for (const node of inputNodes) { + if (node.type == 'password') { + passwordNode = node; + } else if (node.type == 'submit') { + submitNode = node; + } + } + expect(passwordNode).toExist(); + expect(submitNode).toExist(); // submit should be disabled - const submitNode = ReactTestUtils.findRenderedDOMComponentWithClass( - dlg, "mx_Dialog_primary" - ); expect(submitNode.disabled).toBe(true); // put something in the password box, and hit enter; that should @@ -84,9 +92,7 @@ describe('InteractiveAuthDialog', function () { passwordNode.value = "s3kr3t"; ReactTestUtils.Simulate.change(passwordNode); expect(submitNode.disabled).toBe(false); - ReactTestUtils.Simulate.keyDown(passwordNode, { - key: "Enter", keyCode: 13, which: 13, - }); + ReactTestUtils.Simulate.submit(formNode, {}); expect(doRequest.callCount).toEqual(1); expect(doRequest.calledWithExactly({ @@ -96,8 +102,10 @@ describe('InteractiveAuthDialog', function () { user: "@user:id", })).toBe(true); - // the submit button should now be disabled (and be a spinner) - expect(submitNode.disabled).toBe(true); + // there should now be a spinner + ReactTestUtils.findRenderedComponentWithType( + dlg, sdk.getComponent('elements.Spinner'), + ); // let the request complete q.delay(1).then(() => { From 36d126f3a9f9aa07be401e0265f0c1d3cf0d7b54 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 13 Feb 2017 19:09:43 +0000 Subject: [PATCH 049/189] PR feedback --- src/components/structures/InteractiveAuth.js | 10 ++++------ src/components/views/dialogs/BaseDialog.js | 2 -- src/components/views/dialogs/InteractiveAuthDialog.js | 4 ---- .../views/login/InteractiveAuthEntryComponents.js | 6 +++--- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index c29b0fe16af..70b3c2e3061 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -38,6 +38,10 @@ export default React.createClass({ // callback makeRequest: React.PropTypes.func.isRequired, + // callback called when the auth process has finished + // @param {bool} status True if the operation requiring + // auth was completed sucessfully, false if canceled. + // @param result The result of the authenticated call onFinished: React.PropTypes.func.isRequired, }, @@ -107,10 +111,6 @@ export default React.createClass({ } }, - _onCancel: function() { - this.props.onFinished(false); - }, - _submitAuthDict: function(authData) { this._authLogic.submitAuthDict(authData); }, @@ -131,8 +131,6 @@ export default React.createClass({ }, render: function() { - const Loader = sdk.getComponent("elements.Spinner"); - let error = null; if (this.state.errorText) { error = ( diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 01a16e86acf..e83403ef7c9 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -61,8 +61,6 @@ export default React.createClass({ }, _onCancelClick: function(e) { - e.stopPropagation(); - e.preventDefault(); this.props.onFinished(); }, diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index ecca00358f5..66b662b23db 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -49,10 +49,6 @@ export default React.createClass({ }; }, - _onCancelClick: function() { - this.props.onFinished(false); - }, - render: function() { const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth"); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index 4759a30c889..62f59472ecf 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -21,7 +21,7 @@ import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; /* This file contains a collection of components which are used by the - * InteractiveAuthDialog to prompt the user to enter the information needed + * InteractiveAuth to prompt the user to enter the information needed * for an auth stage. (The intention is that they could also be used for other * components, such as the registration flow). * @@ -187,7 +187,7 @@ export const FallbackAuthEntry = React.createClass({ } }, - onSubmitClick: function() { + _onShowFallbackClick: function() { var url = MatrixClientPeg.get().getFallbackAuthUrl( this.props.loginType, this.props.authSessionId @@ -207,7 +207,7 @@ export const FallbackAuthEntry = React.createClass({ render: function() { return (
    - Click "Submit" to authenticate + Start authentication
    {this.props.errorText}
    From ba3e62e3950c8b9929f33d26b0166a8c0afdf488 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 10:31:56 +0000 Subject: [PATCH 050/189] Remove old docs --- src/components/views/login/InteractiveAuthEntryComponents.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index 62f59472ecf..bde0da30209 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -35,7 +35,6 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; * submitAuthDict: a function which will be called with the new auth dict * * Each component may also provide the following functions (beyond the standard React ones): - * onSubmitClick: handle a 'submit' button click * focus: set the input focus appropriately in the form. */ From 43a740df150ce70478e05c001e6f077bef575ec9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 10:34:43 +0000 Subject: [PATCH 051/189] Add busy param to docs --- src/components/views/login/InteractiveAuthEntryComponents.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index bde0da30209..e18e60d7bce 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -33,6 +33,8 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; * stageParams: params from the server for the stage being attempted * errorText: error message from a previous attempt to authenticate * submitAuthDict: a function which will be called with the new auth dict + * busy: a boolean indicating whether the auth logic is doing something + * the user needs to wait for. * * Each component may also provide the following functions (beyond the standard React ones): * focus: set the input focus appropriately in the form. From 6996291f0c210691853c24f57f4007167658c746 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 14 Feb 2017 11:00:40 +0000 Subject: [PATCH 052/189] Store retrieved sid in the signupInstance of EmailIdentityStage When registeration is complete, the RTS needs the sid, which was previously only sent to the HS. This update will also store it in the signupInstance so that it can be sent to the RTS. --- src/SignupStages.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SignupStages.js b/src/SignupStages.js index cdb9d5989b7..1441682c856 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -149,6 +149,7 @@ class EmailIdentityStage extends Stage { nextLink ).then(function(response) { self.sid = response.sid; + self.signupInstance.setIdSid(self.sid); return self._completeVerify(); }).then(function(request) { request.poll_for_success = true; From 1b8e93d4f2daf217762f9c476a0dc553fdd49760 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 14 Feb 2017 12:56:29 +0000 Subject: [PATCH 053/189] Treat the literal team token string "undefined" as undefined Some users appear to have gotten team tokens into their local storage. This fix will treat the literal string "undefined" as undefined. --- src/components/structures/MatrixChat.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 87cd2645db1..a467771a581 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -217,6 +217,12 @@ module.exports = React.createClass({ window.localStorage.getItem('mx_team_token') || window.sessionStorage.getItem('mx_team_token'); + // Some users have ended up with "undefined" as their local storage team token, + // treat that as undefined. + if (this._teamToken === "undefined") { + this._teamToken = undefined; + } + if (this._teamToken) { console.info(`Team token set to ${this._teamToken}`); } From 8001c0b16b7e4b2fb1dcd9677b2c1f2caab65ea5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 13:40:19 +0000 Subject: [PATCH 054/189] Add confirmation dialog to kick/ban buttons Add a specific dialog used for confirming member actions. Also remove onFinished from MemberInfo which did absolutely nothing. --- src/component-index.js | 2 + .../views/dialogs/ConfirmUserActionDialog.js | 83 ++++++++++++ src/components/views/rooms/MemberInfo.js | 124 +++++++++--------- 3 files changed, 145 insertions(+), 64 deletions(-) create mode 100644 src/components/views/dialogs/ConfirmUserActionDialog.js diff --git a/src/component-index.js b/src/component-index.js index b357472ad76..62d26d4ce75 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -77,6 +77,8 @@ import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog'; views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog); import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog'; views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog); +import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog'; +views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog); import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog'; views$dialogs$DeactivateAccountDialog && (module.exports.components['views.dialogs.DeactivateAccountDialog'] = views$dialogs$DeactivateAccountDialog); import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog'; diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js new file mode 100644 index 00000000000..8dc96bb5f6c --- /dev/null +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -0,0 +1,83 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import sdk from '../../../index'; +import classnames from 'classnames'; + +/* + * A dialog for confirming an operation on another user. + * Takes a user ID and a verb, displays the target user prominently + * such that it should be easy to confirm that tne operation is being + * performed on the right person, and displays the operation prominently + * to make it obvious what is going to happen. + * Also tweaks the style for 'dangerous' actions (albeit only with colour) + */ +export default React.createClass({ + displayName: 'ConfirmUserActionDialog', + propTypes: { + member: React.PropTypes.object.isRequired, // member object + action: React.PropTypes.string.isRequired, // eg. 'Ban' + danger: React.PropTypes.bool, + onFinished: React.PropTypes.func.isRequired, + }, + + defaultProps: { + danger: false, + }, + + onOk: function() { + this.props.onFinished(true); + }, + + onCancel: function() { + this.props.onFinished(false); + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); + + const title = this.props.action + " this person?"; + const confirmButtonClass = classnames({ + 'mx_Dialog_primary': true, + 'danger': this.props.danger, + }); + return ( + +
    +
    + +
    +
    {this.props.member.name}
    +
    {this.props.member.userId}
    +
    +
    + + + +
    +
    + ); + }, +}); diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index d33b8f35247..5f0578df603 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -25,16 +25,16 @@ limitations under the License. * 'muted': boolean, * 'isTargetMod': boolean */ -var React = require('react'); -var classNames = require('classnames'); -var dis = require("../../../dispatcher"); -var Modal = require("../../../Modal"); -var sdk = require('../../../index'); -var createRoom = require('../../../createRoom'); -var DMRoomMap = require('../../../utils/DMRoomMap'); -var Unread = require('../../../Unread'); -var Receipt = require('../../../utils/Receipt'); -var WithMatrixClient = require('../../../wrappers/WithMatrixClient'); +import React from 'react'; +import classNames from 'classnames'; +import dis from '../../../dispatcher'; +import Modal from '../../../Modal'; +import sdk from '../../../index'; +import createRoom from '../../../createRoom'; +import DMRoomMap from '../../../utils/DMRoomMap'; +import Unread from '../../../Unread'; +import Receipt from '../../../utils/Receipt'; +import WithMatrixClient from '../../../wrappers/WithMatrixClient'; import AccessibleButton from '../elements/AccessibleButton'; module.exports = WithMatrixClient(React.createClass({ @@ -43,13 +43,6 @@ module.exports = WithMatrixClient(React.createClass({ propTypes: { matrixClient: React.PropTypes.object.isRequired, member: React.PropTypes.object.isRequired, - onFinished: React.PropTypes.func, - }, - - getDefaultProps: function() { - return { - onFinished: function() {} - }; }, getInitialState: function() { @@ -224,46 +217,64 @@ module.exports = WithMatrixClient(React.createClass({ }, onKick: function() { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - var roomId = this.props.member.roomId; - var target = this.props.member.userId; - this.setState({ updating: this.state.updating + 1 }); - this.props.matrixClient.kick(roomId, target).then(function() { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Kick success"); - }, function(err) { - Modal.createDialog(ErrorDialog, { - title: "Kick error", - description: err.message + const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); + Modal.createDialog(ConfirmUserActionDialog, { + member: this.props.member, + action: 'Kick', + danger: true, + onFinished: (proceed) => { + if (!proceed) return; + + this.setState({ updating: this.state.updating + 1 }); + this.props.matrixClient.kick( + this.props.member.roomId, this.props.member.userId, + ).then(function() { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Kick success"); + }, function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Kick error", + description: err.message + }); + } + ).finally(()=>{ + this.setState({ updating: this.state.updating - 1 }); }); } - ).finally(()=>{ - this.setState({ updating: this.state.updating - 1 }); }); - this.props.onFinished(); }, onBan: function() { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - var roomId = this.props.member.roomId; - var target = this.props.member.userId; - this.setState({ updating: this.state.updating + 1 }); - this.props.matrixClient.ban(roomId, target).then( - function() { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Ban success"); - }, function(err) { - Modal.createDialog(ErrorDialog, { - title: "Ban error", - description: err.message + const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); + Modal.createDialog(ConfirmUserActionDialog, { + member: this.props.member, + action: 'Ban', + danger: true, + onFinished: (proceed) => { + if (!proceed) return; + + this.setState({ updating: this.state.updating + 1 }); + this.props.matrixClient.ban( + this.props.member.roomId, this.props.member.userId, + ).then( + function() { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Ban success"); + }, function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Ban error", + description: err.message, + }); + } + ).finally(()=>{ + this.setState({ updating: this.state.updating - 1 }); }); - } - ).finally(()=>{ - this.setState({ updating: this.state.updating - 1 }); + }, }); - this.props.onFinished(); }, onMuteToggle: function() { @@ -272,14 +283,12 @@ module.exports = WithMatrixClient(React.createClass({ var target = this.props.member.userId; var room = this.props.matrixClient.getRoom(roomId); if (!room) { - this.props.onFinished(); return; } var powerLevelEvent = room.currentState.getStateEvents( "m.room.power_levels", "" ); if (!powerLevelEvent) { - this.props.onFinished(); return; } var isMuted = this.state.muted; @@ -314,7 +323,6 @@ module.exports = WithMatrixClient(React.createClass({ this.setState({ updating: this.state.updating - 1 }); }); } - this.props.onFinished(); }, onModToggle: function() { @@ -323,19 +331,16 @@ module.exports = WithMatrixClient(React.createClass({ var target = this.props.member.userId; var room = this.props.matrixClient.getRoom(roomId); if (!room) { - this.props.onFinished(); return; } var powerLevelEvent = room.currentState.getStateEvents( "m.room.power_levels", "" ); if (!powerLevelEvent) { - this.props.onFinished(); return; } var me = room.getMember(this.props.matrixClient.credentials.userId); if (!me) { - this.props.onFinished(); return; } var defaultLevel = powerLevelEvent.getContent().users_default; @@ -366,7 +371,6 @@ module.exports = WithMatrixClient(React.createClass({ ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); }); - this.props.onFinished(); }, _applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) { @@ -386,7 +390,6 @@ module.exports = WithMatrixClient(React.createClass({ ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); }).done(); - this.props.onFinished(); }, onPowerChange: function(powerLevel) { @@ -396,14 +399,12 @@ module.exports = WithMatrixClient(React.createClass({ var room = this.props.matrixClient.getRoom(roomId); var self = this; if (!room) { - this.props.onFinished(); return; } var powerLevelEvent = room.currentState.getStateEvents( "m.room.power_levels", "" ); if (!powerLevelEvent) { - this.props.onFinished(); return; } if (powerLevelEvent.getContent().users) { @@ -422,9 +423,6 @@ module.exports = WithMatrixClient(React.createClass({ if (confirmed) { self._applyPowerChange(roomId, target, powerLevel, powerLevelEvent); } - else { - self.props.onFinished(); - } }, }); } @@ -440,7 +438,6 @@ module.exports = WithMatrixClient(React.createClass({ onNewDMClick: function() { this.setState({ updating: this.state.updating + 1 }); createRoom({dmUserId: this.props.member.userId}).finally(() => { - this.props.onFinished(); this.setState({ updating: this.state.updating - 1 }); }).done(); }, @@ -450,7 +447,6 @@ module.exports = WithMatrixClient(React.createClass({ action: 'leave_room', room_id: this.props.member.roomId, }); - this.props.onFinished(); }, _calculateOpsPermissions: function(member) { From 689972f0235c7df163cddcb2d15062964b99ba75 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 13:57:22 +0000 Subject: [PATCH 055/189] Copyright --- src/components/views/rooms/MemberInfo.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 5f0578df603..de1218444cc 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From ff61b76bf7b1788a1b54962c9d5fdac87ba3333e Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 13:58:29 +0000 Subject: [PATCH 056/189] Fix imports --- src/components/views/rooms/MemberInfo.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index de1218444cc..7ee5af7af51 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -34,7 +34,7 @@ import sdk from '../../../index'; import createRoom from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; import Unread from '../../../Unread'; -import Receipt from '../../../utils/Receipt'; +import findReadReceiptFromUserId from '../../../utils/Receipt'; import WithMatrixClient from '../../../wrappers/WithMatrixClient'; import AccessibleButton from '../elements/AccessibleButton'; @@ -158,7 +158,7 @@ module.exports = WithMatrixClient(React.createClass({ onRoomReceipt: function(receiptEvent, room) { // because if we read a notification, it will affect notification count // only bother updating if there's a receipt from us - if (Receipt.findReadReceiptFromUserId(receiptEvent, this.props.matrixClient.credentials.userId)) { + if (findReadReceiptFromUserId(receiptEvent, this.props.matrixClient.credentials.userId)) { this.forceUpdate(); } }, From 5e232d8500e4b842adc3c54766c7abd09fd7dc2a Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 14:33:21 +0000 Subject: [PATCH 057/189] Argh, ES6 import syntax --- src/components/views/rooms/MemberInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 7ee5af7af51..fc2df0a5ccd 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -34,7 +34,7 @@ import sdk from '../../../index'; import createRoom from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; import Unread from '../../../Unread'; -import findReadReceiptFromUserId from '../../../utils/Receipt'; +import { findReadReceiptFromUserId } from '../../../utils/Receipt'; import WithMatrixClient from '../../../wrappers/WithMatrixClient'; import AccessibleButton from '../elements/AccessibleButton'; From 0303a42fc7b9cb0982a077ab091252c6988305a1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 14 Feb 2017 15:35:14 +0000 Subject: [PATCH 058/189] Fix typo with Scalar popup --- src/components/views/messages/TextualBody.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index fd26ae58da7..a625e63062f 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -222,7 +222,8 @@ module.exports = React.createClass({ title: "Add an Integration", description:
    - You are about to taken to a third-party site so you can authenticate your account for use with {integrationsUrl}.
    + You are about to be taken to a third-party site so you can + authenticate your account for use with {integrationsUrl}.
    Do you wish to continue?
    , button: "Continue", From a1c990a2ea0644c11dc0e49547d9759408d92c5e Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 16:03:30 +0000 Subject: [PATCH 059/189] Make ban either ban or unban depending on whether the user is banned already Mostly gives some feedback that the ban has actually taken effect. --- src/components/views/rooms/MemberInfo.js | 42 +++++++++++++++--------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index fc2df0a5ccd..babffb9a023 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -251,15 +251,23 @@ module.exports = WithMatrixClient(React.createClass({ const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); Modal.createDialog(ConfirmUserActionDialog, { member: this.props.member, - action: 'Ban', - danger: true, + action: this.props.member.membership == 'ban' ? 'Unban' : 'Ban', + danger: this.props.member.membership != 'ban', onFinished: (proceed) => { if (!proceed) return; this.setState({ updating: this.state.updating + 1 }); - this.props.matrixClient.ban( - this.props.member.roomId, this.props.member.userId, - ).then( + let promise; + if (this.props.member.membership == 'ban') { + promise = this.props.matrixClient.unban( + this.props.member.roomId, this.props.member.userId, + ); + } else { + promise = this.props.matrixClient.ban( + this.props.member.roomId, this.props.member.userId, + ); + } + promise.then( function() { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! @@ -451,26 +459,26 @@ module.exports = WithMatrixClient(React.createClass({ }, _calculateOpsPermissions: function(member) { - var defaultPerms = { + const defaultPerms = { can: {}, muted: false, modifyLevel: false }; - var room = this.props.matrixClient.getRoom(member.roomId); + const room = this.props.matrixClient.getRoom(member.roomId); if (!room) { return defaultPerms; } - var powerLevels = room.currentState.getStateEvents( + const powerLevels = room.currentState.getStateEvents( "m.room.power_levels", "" ); if (!powerLevels) { return defaultPerms; } - var me = room.getMember(this.props.matrixClient.credentials.userId); + const me = room.getMember(this.props.matrixClient.credentials.userId); if (!me) { return defaultPerms; } - var them = member; + const them = member; return { can: this._calculateCanPermissions( me, them, powerLevels.getContent() @@ -481,22 +489,22 @@ module.exports = WithMatrixClient(React.createClass({ }, _calculateCanPermissions: function(me, them, powerLevels) { - var can = { + const can = { kick: false, ban: false, mute: false, modifyLevel: false }; - var canAffectUser = them.powerLevel < me.powerLevel; + const canAffectUser = them.powerLevel < me.powerLevel; if (!canAffectUser) { //console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); return can; } - var editPowerLevel = ( + const editPowerLevel = ( (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || powerLevels.state_default ); - var levelToSend = ( + const levelToSend = ( (powerLevels.events ? powerLevels.events["m.room.message"] : null) || powerLevels.events_default ); @@ -643,10 +651,14 @@ module.exports = WithMatrixClient(React.createClass({ ); } if (this.state.can.ban) { + let label = 'Ban'; + if (this.props.member.membership == 'ban') { + label = 'Unban'; + } banButton = ( - Ban + {label} ); } From ec0ce76d87e019fa31f49c97ab2f9fe3e9ad53fd Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 16:09:02 +0000 Subject: [PATCH 060/189] Clarify docs --- src/components/views/dialogs/ConfirmUserActionDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index 8dc96bb5f6c..fbe719710b8 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -21,7 +21,7 @@ import classnames from 'classnames'; /* * A dialog for confirming an operation on another user. * Takes a user ID and a verb, displays the target user prominently - * such that it should be easy to confirm that tne operation is being + * such that it should be easy to confirm that the operation is being * performed on the right person, and displays the operation prominently * to make it obvious what is going to happen. * Also tweaks the style for 'dangerous' actions (albeit only with colour) @@ -29,7 +29,7 @@ import classnames from 'classnames'; export default React.createClass({ displayName: 'ConfirmUserActionDialog', propTypes: { - member: React.PropTypes.object.isRequired, // member object + member: React.PropTypes.object.isRequired, // matrix-js-sdk member object action: React.PropTypes.string.isRequired, // eg. 'Ban' danger: React.PropTypes.bool, onFinished: React.PropTypes.func.isRequired, From 6663f5bff00b6f8a66a0430f0dca296c49098889 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 16:12:04 +0000 Subject: [PATCH 061/189] Remove commented stuff That I've now broken such that it wouldnt work if it were uncommented --- src/components/structures/MatrixChat.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 87cd2645db1..e5d0b560fae 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -904,14 +904,6 @@ module.exports = React.createClass({ onUserClick: function(event, userId) { event.preventDefault(); - // var MemberInfo = sdk.getComponent('rooms.MemberInfo'); - // var member = new Matrix.RoomMember(null, userId); - // ContextualMenu.createMenu(MemberInfo, { - // member: member, - // right: window.innerWidth - event.pageX, - // top: event.pageY - // }); - var member = new Matrix.RoomMember(null, userId); if (!member) { return; } dis.dispatch({ From f38b2dee78b03a7bb93993adfb3681df99d79ca8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 17:06:16 +0000 Subject: [PATCH 062/189] Convert some missed buttons to AccessibleButton In RoomSettings --- src/components/views/rooms/RoomSettings.js | 31 +++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index a23368f5e82..4d1285678bd 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -14,17 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -var q = require("q"); -var React = require('react'); -var MatrixClientPeg = require('../../../MatrixClientPeg'); -var SdkConfig = require('../../../SdkConfig'); -var sdk = require('../../../index'); -var Modal = require('../../../Modal'); -var ObjectUtils = require("../../../ObjectUtils"); -var dis = require("../../../dispatcher"); -var ScalarAuthClient = require("../../../ScalarAuthClient"); -var ScalarMessaging = require('../../../ScalarMessaging'); -var UserSettingsStore = require('../../../UserSettingsStore'); +import q from 'q'; +import React from 'react'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import SdkConfig from '../../../SdkConfig'; +import sdk from '../../../index'; +import Modal from '../../../Modal'; +import ObjectUtils from '../../../ObjectUtils'; +import dis from '../../../dispatcher'; +import ScalarAuthClient from '../../../ScalarAuthClient'; +import ScalarMessaging from '../../../ScalarMessaging'; +import UserSettingsStore from '../../../UserSettingsStore'; +import AccessibleButton from '../elements/AccessibleButton'; // parse a string as an integer; if the input is undefined, or cannot be parsed @@ -635,16 +636,16 @@ module.exports = React.createClass({ if (myMember) { if (myMember.membership === "join") { leaveButton = ( -
    + Leave room -
    + ); } else if (myMember.membership === "leave") { leaveButton = ( -
    + Forget room -
    + ); } } From 6fc70415cb4defa3c9cd5090d0993c470694fa84 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 17:29:40 +0000 Subject: [PATCH 063/189] s/onBan/onBanOrUnban/ --- src/components/views/rooms/MemberInfo.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index babffb9a023..699ee8a3a22 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -247,7 +247,7 @@ module.exports = WithMatrixClient(React.createClass({ }); }, - onBan: function() { + onBanOrUnban: function() { const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); Modal.createDialog(ConfirmUserActionDialog, { member: this.props.member, @@ -657,7 +657,7 @@ module.exports = WithMatrixClient(React.createClass({ } banButton = ( + onClick={this.onBanOrUnban}> {label} ); From 87516fb950159589ea725a9ae4ab6ad3ae0848d5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 17:54:57 +0000 Subject: [PATCH 064/189] Add a button to un-ban users in RoomSettings https://github.com/vector-im/riot-web/issues/3091 --- src/components/views/rooms/RoomSettings.js | 57 ++++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 4d1285678bd..8b156f2ccce 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -35,6 +35,47 @@ function parseIntWithDefault(val, def) { return isNaN(res) ? def : res; } +const BannedUser = React.createClass({ + propTypes: { + member: React.PropTypes.string.isRequired, + }, + + _onUnbanClick: function() { + const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); + Modal.createDialog(ConfirmUserActionDialog, { + member: this.props.member, + action: 'Unban', + danger: false, + onFinished: (proceed) => { + if (!proceed) return; + + MatrixClientPeg.get().unban( + this.props.member.roomId, this.props.member.userId, + ).catch((err) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Failed to unban", + description: err.message, + }); + }).done(); + }, + }); + }, + + render: function() { + return ( +
  • + + Unban + + {this.props.member.userId} +
  • + ); + } +}); + module.exports = React.createClass({ displayName: 'RoomSettings', @@ -74,6 +115,9 @@ module.exports = React.createClass({ componentWillMount: function() { ScalarMessaging.startListening(); + + MatrixClientPeg.get().on("RoomMember.membership", this._onRoomMemberMembership); + MatrixClientPeg.get().getRoomDirectoryVisibility( this.props.room.roomId ).done((result) => { @@ -102,6 +146,8 @@ module.exports = React.createClass({ componentWillUnmount: function() { ScalarMessaging.stopListening(); + MatrixClientPeg.get().removeListener("RoomMember.membership", this._onRoomMemberMembership); + dis.dispatch({ action: 'ui_opacity', sideOpacity: 1.0, @@ -501,6 +547,11 @@ module.exports = React.createClass({ }); }, + _onRoomMemberMembership: function() { + // Update, since our banned user list may have changed + this.forceUpdate(); + }, + _renderEncryptionSection: function() { var cli = MatrixClientPeg.get(); var roomState = this.props.room.currentState; @@ -611,11 +662,9 @@ module.exports = React.createClass({

    Banned users

      - {banned.map(function(member, i) { + {banned.map(function(member) { return ( -
    • - {member.userId} -
    • + ); })}
    From 431e7a875db2d35f04162c616ee086201f360aba Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 18:10:40 +0000 Subject: [PATCH 065/189] Copyright --- src/components/views/rooms/RoomSettings.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 8b156f2ccce..7a9cb2224c0 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 8067bb627f9d5e3f84dbef2637206dc8052bf3a4 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 15 Feb 2017 16:29:08 +0000 Subject: [PATCH 066/189] Display avatar initials in typing notifications It seems they don't overlap hawkwawdly anymore, so this displays them always. Fixes https://github.com/vector-im/riot-web/issues/3084 --- src/components/structures/RoomStatusBar.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 288ca0b974d..ca50e1071a4 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -223,8 +223,7 @@ module.exports = React.createClass({ users = users.slice(0, limit - 1); } - let avatars = users.map((u, index) => { - let showInitial = othersCount === 0 && index === users.length - 1; + const avatars = users.map((u) => { return ( ); }); From c082827fc7e3e11e4790c0306933c751c7a09c65 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 15 Feb 2017 17:12:51 +0000 Subject: [PATCH 067/189] Fix docs & use WithMatrixClient --- src/components/views/rooms/RoomSettings.js | 56 ++++++++++++---------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 7a9cb2224c0..783d3438902 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -17,7 +17,6 @@ limitations under the License. import q from 'q'; import React from 'react'; -import MatrixClientPeg from '../../../MatrixClientPeg'; import SdkConfig from '../../../SdkConfig'; import sdk from '../../../index'; import Modal from '../../../Modal'; @@ -27,6 +26,7 @@ import ScalarAuthClient from '../../../ScalarAuthClient'; import ScalarMessaging from '../../../ScalarMessaging'; import UserSettingsStore from '../../../UserSettingsStore'; import AccessibleButton from '../elements/AccessibleButton'; +import WithMatrixClient from '../../../wrappers/WithMatrixClient'; // parse a string as an integer; if the input is undefined, or cannot be parsed @@ -36,9 +36,12 @@ function parseIntWithDefault(val, def) { return isNaN(res) ? def : res; } -const BannedUser = React.createClass({ +const BannedUser = WithMatrixClient(React.createClass({ propTypes: { - member: React.PropTypes.string.isRequired, + /* MatrixClient instance */ + matrixClient: React.PropTypes.object.isRequired, + + member: React.PropTypes.object.isRequired, // js-sdk member object }, _onUnbanClick: function() { @@ -50,7 +53,7 @@ const BannedUser = React.createClass({ onFinished: (proceed) => { if (!proceed) return; - MatrixClientPeg.get().unban( + this.props.matrixClient.unban( this.props.member.roomId, this.props.member.userId, ).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -75,12 +78,15 @@ const BannedUser = React.createClass({ ); } -}); +})); -module.exports = React.createClass({ +module.exports = WithMatrixClient(React.createClass({ displayName: 'RoomSettings', propTypes: { + /* MatrixClient instance */ + matrixClient: React.PropTypes.object.isRequired, + room: React.PropTypes.object.isRequired, onSaveClick: React.PropTypes.func, onCancelClick: React.PropTypes.func, @@ -117,9 +123,9 @@ module.exports = React.createClass({ componentWillMount: function() { ScalarMessaging.startListening(); - MatrixClientPeg.get().on("RoomMember.membership", this._onRoomMemberMembership); + this.props.matrixClient.on("RoomMember.membership", this._onRoomMemberMembership); - MatrixClientPeg.get().getRoomDirectoryVisibility( + this.props.matrixClient.getRoomDirectoryVisibility( this.props.room.roomId ).done((result) => { this.setState({ isRoomPublished: result.visibility === "public" }); @@ -147,7 +153,7 @@ module.exports = React.createClass({ componentWillUnmount: function() { ScalarMessaging.stopListening(); - MatrixClientPeg.get().removeListener("RoomMember.membership", this._onRoomMemberMembership); + this.props.matrixClient.removeListener("RoomMember.membership", this._onRoomMemberMembership); dis.dispatch({ action: 'ui_opacity', @@ -195,14 +201,14 @@ module.exports = React.createClass({ // name and topic if (this._hasDiff(this.state.name, originalState.name)) { - promises.push(MatrixClientPeg.get().setRoomName(roomId, this.state.name)); + promises.push(this.props.matrixClient.setRoomName(roomId, this.state.name)); } if (this._hasDiff(this.state.topic, originalState.topic)) { - promises.push(MatrixClientPeg.get().setRoomTopic(roomId, this.state.topic)); + promises.push(this.props.matrixClient.setRoomTopic(roomId, this.state.topic)); } if (this.state.history_visibility !== originalState.history_visibility) { - promises.push(MatrixClientPeg.get().sendStateEvent( + promises.push(this.props.matrixClient.sendStateEvent( roomId, "m.room.history_visibility", { history_visibility: this.state.history_visibility }, "" @@ -210,14 +216,14 @@ module.exports = React.createClass({ } if (this.state.isRoomPublished !== originalState.isRoomPublished) { - promises.push(MatrixClientPeg.get().setRoomDirectoryVisibility( + promises.push(this.props.matrixClient.setRoomDirectoryVisibility( roomId, this.state.isRoomPublished ? "public" : "private" )); } if (this.state.join_rule !== originalState.join_rule) { - promises.push(MatrixClientPeg.get().sendStateEvent( + promises.push(this.props.matrixClient.sendStateEvent( roomId, "m.room.join_rules", { join_rule: this.state.join_rule }, "" @@ -225,7 +231,7 @@ module.exports = React.createClass({ } if (this.state.guest_access !== originalState.guest_access) { - promises.push(MatrixClientPeg.get().sendStateEvent( + promises.push(this.props.matrixClient.sendStateEvent( roomId, "m.room.guest_access", { guest_access: this.state.guest_access }, "" @@ -236,7 +242,7 @@ module.exports = React.createClass({ // power levels var powerLevels = this._getPowerLevels(); if (powerLevels) { - promises.push(MatrixClientPeg.get().sendStateEvent( + promises.push(this.props.matrixClient.sendStateEvent( roomId, "m.room.power_levels", powerLevels, "" )); } @@ -249,12 +255,12 @@ module.exports = React.createClass({ switch (diff.place) { case "add": promises.push( - MatrixClientPeg.get().setRoomTag(roomId, diff.key, {}) + this.props.matrixClient.setRoomTag(roomId, diff.key, {}) ); break; case "del": promises.push( - MatrixClientPeg.get().deleteRoomTag(roomId, diff.key) + this.props.matrixClient.deleteRoomTag(roomId, diff.key) ); break; default: @@ -311,7 +317,7 @@ module.exports = React.createClass({ if (!encrypt) { return q(); } var roomId = this.props.room.roomId; - return MatrixClientPeg.get().sendStateEvent( + return this.props.matrixClient.sendStateEvent( roomId, "m.room.encryption", { algorithm: "m.megolm.v1.aes-sha2" } ); @@ -476,7 +482,7 @@ module.exports = React.createClass({ }, mayChangeRoomAccess: function() { - var cli = MatrixClientPeg.get(); + var cli = this.props.matrixClient; var roomState = this.props.room.currentState; return (roomState.mayClientSendStateEvent("m.room.join_rules", cli) && roomState.mayClientSendStateEvent("m.room.guest_access", cli)); @@ -513,7 +519,7 @@ module.exports = React.createClass({ onForgetClick() { // FIXME: duplicated with RoomTagContextualMenu (and dead code in RoomView) - MatrixClientPeg.get().forget(this.props.room.roomId).done(function() { + this.props.matrixClient.forget(this.props.room.roomId).done(function() { dis.dispatch({ action: 'view_next_room' }); }, function(err) { var errCode = err.errcode || "unknown error code"; @@ -554,7 +560,7 @@ module.exports = React.createClass({ }, _renderEncryptionSection: function() { - var cli = MatrixClientPeg.get(); + var cli = this.props.matrixClient; var roomState = this.props.room.currentState; var isEncrypted = cli.isRoomEncrypted(this.props.room.roomId); var isGlobalBlacklistUnverified = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevices; @@ -608,7 +614,7 @@ module.exports = React.createClass({ var PowerSelector = sdk.getComponent('elements.PowerSelector'); var Loader = sdk.getComponent("elements.Spinner"); - var cli = MatrixClientPeg.get(); + var cli = this.props.matrixClient; var roomState = this.props.room.currentState; var user_id = cli.credentials.userId; @@ -833,7 +839,7 @@ module.exports = React.createClass({ - List this room in { MatrixClientPeg.get().getDomain() }'s room directory? + List this room in { this.props.matrixClient.getDomain() }'s room directory?
    @@ -943,4 +949,4 @@ module.exports = React.createClass({
    ); } -}); +})); From a5a056292dcb2ca2abd661153318e80d05a269a9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 15 Feb 2017 18:58:59 +0000 Subject: [PATCH 068/189] Revert c082827fc7e3e11e4790c0306933c751c7a09c65 Revert the WithMatrixClient change: RoomView calls methods on the RoomSettings component and this breaks when RoomSettings is wrapped in a WithMatrixClient. --- src/components/views/rooms/RoomSettings.js | 56 ++++++++++------------ 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 783d3438902..7a9cb2224c0 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -17,6 +17,7 @@ limitations under the License. import q from 'q'; import React from 'react'; +import MatrixClientPeg from '../../../MatrixClientPeg'; import SdkConfig from '../../../SdkConfig'; import sdk from '../../../index'; import Modal from '../../../Modal'; @@ -26,7 +27,6 @@ import ScalarAuthClient from '../../../ScalarAuthClient'; import ScalarMessaging from '../../../ScalarMessaging'; import UserSettingsStore from '../../../UserSettingsStore'; import AccessibleButton from '../elements/AccessibleButton'; -import WithMatrixClient from '../../../wrappers/WithMatrixClient'; // parse a string as an integer; if the input is undefined, or cannot be parsed @@ -36,12 +36,9 @@ function parseIntWithDefault(val, def) { return isNaN(res) ? def : res; } -const BannedUser = WithMatrixClient(React.createClass({ +const BannedUser = React.createClass({ propTypes: { - /* MatrixClient instance */ - matrixClient: React.PropTypes.object.isRequired, - - member: React.PropTypes.object.isRequired, // js-sdk member object + member: React.PropTypes.string.isRequired, }, _onUnbanClick: function() { @@ -53,7 +50,7 @@ const BannedUser = WithMatrixClient(React.createClass({ onFinished: (proceed) => { if (!proceed) return; - this.props.matrixClient.unban( + MatrixClientPeg.get().unban( this.props.member.roomId, this.props.member.userId, ).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -78,15 +75,12 @@ const BannedUser = WithMatrixClient(React.createClass({ ); } -})); +}); -module.exports = WithMatrixClient(React.createClass({ +module.exports = React.createClass({ displayName: 'RoomSettings', propTypes: { - /* MatrixClient instance */ - matrixClient: React.PropTypes.object.isRequired, - room: React.PropTypes.object.isRequired, onSaveClick: React.PropTypes.func, onCancelClick: React.PropTypes.func, @@ -123,9 +117,9 @@ module.exports = WithMatrixClient(React.createClass({ componentWillMount: function() { ScalarMessaging.startListening(); - this.props.matrixClient.on("RoomMember.membership", this._onRoomMemberMembership); + MatrixClientPeg.get().on("RoomMember.membership", this._onRoomMemberMembership); - this.props.matrixClient.getRoomDirectoryVisibility( + MatrixClientPeg.get().getRoomDirectoryVisibility( this.props.room.roomId ).done((result) => { this.setState({ isRoomPublished: result.visibility === "public" }); @@ -153,7 +147,7 @@ module.exports = WithMatrixClient(React.createClass({ componentWillUnmount: function() { ScalarMessaging.stopListening(); - this.props.matrixClient.removeListener("RoomMember.membership", this._onRoomMemberMembership); + MatrixClientPeg.get().removeListener("RoomMember.membership", this._onRoomMemberMembership); dis.dispatch({ action: 'ui_opacity', @@ -201,14 +195,14 @@ module.exports = WithMatrixClient(React.createClass({ // name and topic if (this._hasDiff(this.state.name, originalState.name)) { - promises.push(this.props.matrixClient.setRoomName(roomId, this.state.name)); + promises.push(MatrixClientPeg.get().setRoomName(roomId, this.state.name)); } if (this._hasDiff(this.state.topic, originalState.topic)) { - promises.push(this.props.matrixClient.setRoomTopic(roomId, this.state.topic)); + promises.push(MatrixClientPeg.get().setRoomTopic(roomId, this.state.topic)); } if (this.state.history_visibility !== originalState.history_visibility) { - promises.push(this.props.matrixClient.sendStateEvent( + promises.push(MatrixClientPeg.get().sendStateEvent( roomId, "m.room.history_visibility", { history_visibility: this.state.history_visibility }, "" @@ -216,14 +210,14 @@ module.exports = WithMatrixClient(React.createClass({ } if (this.state.isRoomPublished !== originalState.isRoomPublished) { - promises.push(this.props.matrixClient.setRoomDirectoryVisibility( + promises.push(MatrixClientPeg.get().setRoomDirectoryVisibility( roomId, this.state.isRoomPublished ? "public" : "private" )); } if (this.state.join_rule !== originalState.join_rule) { - promises.push(this.props.matrixClient.sendStateEvent( + promises.push(MatrixClientPeg.get().sendStateEvent( roomId, "m.room.join_rules", { join_rule: this.state.join_rule }, "" @@ -231,7 +225,7 @@ module.exports = WithMatrixClient(React.createClass({ } if (this.state.guest_access !== originalState.guest_access) { - promises.push(this.props.matrixClient.sendStateEvent( + promises.push(MatrixClientPeg.get().sendStateEvent( roomId, "m.room.guest_access", { guest_access: this.state.guest_access }, "" @@ -242,7 +236,7 @@ module.exports = WithMatrixClient(React.createClass({ // power levels var powerLevels = this._getPowerLevels(); if (powerLevels) { - promises.push(this.props.matrixClient.sendStateEvent( + promises.push(MatrixClientPeg.get().sendStateEvent( roomId, "m.room.power_levels", powerLevels, "" )); } @@ -255,12 +249,12 @@ module.exports = WithMatrixClient(React.createClass({ switch (diff.place) { case "add": promises.push( - this.props.matrixClient.setRoomTag(roomId, diff.key, {}) + MatrixClientPeg.get().setRoomTag(roomId, diff.key, {}) ); break; case "del": promises.push( - this.props.matrixClient.deleteRoomTag(roomId, diff.key) + MatrixClientPeg.get().deleteRoomTag(roomId, diff.key) ); break; default: @@ -317,7 +311,7 @@ module.exports = WithMatrixClient(React.createClass({ if (!encrypt) { return q(); } var roomId = this.props.room.roomId; - return this.props.matrixClient.sendStateEvent( + return MatrixClientPeg.get().sendStateEvent( roomId, "m.room.encryption", { algorithm: "m.megolm.v1.aes-sha2" } ); @@ -482,7 +476,7 @@ module.exports = WithMatrixClient(React.createClass({ }, mayChangeRoomAccess: function() { - var cli = this.props.matrixClient; + var cli = MatrixClientPeg.get(); var roomState = this.props.room.currentState; return (roomState.mayClientSendStateEvent("m.room.join_rules", cli) && roomState.mayClientSendStateEvent("m.room.guest_access", cli)); @@ -519,7 +513,7 @@ module.exports = WithMatrixClient(React.createClass({ onForgetClick() { // FIXME: duplicated with RoomTagContextualMenu (and dead code in RoomView) - this.props.matrixClient.forget(this.props.room.roomId).done(function() { + MatrixClientPeg.get().forget(this.props.room.roomId).done(function() { dis.dispatch({ action: 'view_next_room' }); }, function(err) { var errCode = err.errcode || "unknown error code"; @@ -560,7 +554,7 @@ module.exports = WithMatrixClient(React.createClass({ }, _renderEncryptionSection: function() { - var cli = this.props.matrixClient; + var cli = MatrixClientPeg.get(); var roomState = this.props.room.currentState; var isEncrypted = cli.isRoomEncrypted(this.props.room.roomId); var isGlobalBlacklistUnverified = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevices; @@ -614,7 +608,7 @@ module.exports = WithMatrixClient(React.createClass({ var PowerSelector = sdk.getComponent('elements.PowerSelector'); var Loader = sdk.getComponent("elements.Spinner"); - var cli = this.props.matrixClient; + var cli = MatrixClientPeg.get(); var roomState = this.props.room.currentState; var user_id = cli.credentials.userId; @@ -839,7 +833,7 @@ module.exports = WithMatrixClient(React.createClass({ - List this room in { this.props.matrixClient.getDomain() }'s room directory? + List this room in { MatrixClientPeg.get().getDomain() }'s room directory?
    @@ -949,4 +943,4 @@ module.exports = WithMatrixClient(React.createClass({
    ); } -})); +}); From 8698d40d3ce3ef665c704f6db14399fa635cc0de Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 15 Feb 2017 19:01:00 +0000 Subject: [PATCH 069/189] Fix docs & add MatrixClient check Addresses PR feedback without breaking RoomSettings --- src/components/views/rooms/RoomSettings.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 7a9cb2224c0..3247f5a90bd 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -38,7 +38,7 @@ function parseIntWithDefault(val, def) { const BannedUser = React.createClass({ propTypes: { - member: React.PropTypes.string.isRequired, + member: React.PropTypes.object.isRequired, // js-sdk RoomMember }, _onUnbanClick: function() { @@ -147,7 +147,10 @@ module.exports = React.createClass({ componentWillUnmount: function() { ScalarMessaging.stopListening(); - MatrixClientPeg.get().removeListener("RoomMember.membership", this._onRoomMemberMembership); + const cli = MatrixClientPeg.get(); + if (cli) { + cli.removeListener("RoomMember.membership", this._onRoomMemberMembership); + } dis.dispatch({ action: 'ui_opacity', From bdb8f9d05292300501ae5516bde780be17ed00b5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 15 Feb 2017 18:44:15 +0000 Subject: [PATCH 070/189] Don't force-logout the user if reading localstorage fails Give them a modal dialog to give them a chance to abort. --- src/Lifecycle.js | 82 +++++++++++++------ src/component-index.js | 2 + .../dialogs/SessionRestoreErrorDialog.js | 74 +++++++++++++++++ 3 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 src/components/views/dialogs/SessionRestoreErrorDialog.js diff --git a/src/Lifecycle.js b/src/Lifecycle.js index e899ec6ad85..e891ab59849 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,6 +25,8 @@ import Presence from './Presence'; import dis from './dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import RtsClient from './RtsClient'; +import Modal from './Modal'; +import sdk from './index'; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -109,16 +112,17 @@ export function loadSession(opts) { return q(); } - if (_restoreFromLocalStorage()) { - return q(); - } + return _restoreFromLocalStorage().then((success) => { + if (success) { + return; + } - if (enableGuest) { - return _registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName); - } + if (enableGuest) { + return _registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName); + } - // fall back to login screen - return q(); + // fall back to login screen + }); } function _loginWithToken(queryParams, defaultDeviceDisplayName) { @@ -178,10 +182,11 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { }); } -// returns true if a session is found in localstorage +// returns a promise which resolves to true if a session is found in +// localstorage function _restoreFromLocalStorage() { if (!localStorage) { - return false; + return q(false); } const hs_url = localStorage.getItem("mx_hs_url"); const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; @@ -208,26 +213,53 @@ function _restoreFromLocalStorage() { identityServerUrl: is_url, guest: is_guest, }); - return true; + return q(true); } catch (e) { - console.log("Unable to restore session", e); - - var msg = e.message; - if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") { - msg = "You need to log back in to generate end-to-end encryption keys " - + "for this device and submit the public key to your homeserver. " - + "This is a once off; sorry for the inconvenience."; - } - - // don't leak things into the new session - _clearLocalStorage(); - - throw new Error("Unable to restore previous session: " + msg); + return _handleRestoreFailure(e); } } else { console.log("No previous session found."); - return false; + return q(false); + } +} + +function _handleRestoreFailure(e) { + console.log("Unable to restore session", e); + + let msg = e.message; + if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") { + msg = "You need to log back in to generate end-to-end encryption keys " + + "for this device and submit the public key to your homeserver. " + + "This is a once off; sorry for the inconvenience."; + + _clearLocalStorage(); + + return q.reject(new Error( + "Unable to restore previous session: " + msg, + )); } + + const def = q.defer(); + const SessionRestoreErrorDialog = + sdk.getComponent('views.dialogs.SessionRestoreErrorDialog'); + + Modal.createDialog(SessionRestoreErrorDialog, { + error: msg, + onFinished: (success) => { + def.resolve(success); + }, + }); + + return def.promise.then((success) => { + if (success) { + // user clicked continue. + _clearLocalStorage(); + return false; + } + + // try, try again + return _restoreFromLocalStorage(); + }); } let rtsClient = null; diff --git a/src/component-index.js b/src/component-index.js index 62d26d4ce75..c705150e12c 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -89,6 +89,8 @@ import views$dialogs$NeedToRegisterDialog from './components/views/dialogs/NeedT views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog); import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDialog'; views$dialogs$QuestionDialog && (module.exports.components['views.dialogs.QuestionDialog'] = views$dialogs$QuestionDialog); +import views$dialogs$SessionRestoreErrorDialog from './components/views/dialogs/SessionRestoreErrorDialog'; +views$dialogs$SessionRestoreErrorDialog && (module.exports.components['views.dialogs.SessionRestoreErrorDialog'] = views$dialogs$SessionRestoreErrorDialog); import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDisplayNameDialog'; views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog); import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog'; diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js new file mode 100644 index 00000000000..358bbf1fec6 --- /dev/null +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -0,0 +1,74 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import sdk from '../../../index'; +import SdkConfig from '../../../SdkConfig'; +import Modal from '../../../Modal'; + + +export default React.createClass({ + displayName: 'SessionRestoreErrorDialog', + + propTypes: { + error: React.PropTypes.string.isRequired, + onFinished: React.PropTypes.func.isRequired, + }, + + _sendBugReport: function() { + const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); + Modal.createDialog(BugReportDialog, {}); + }, + + _continueClicked: function() { + this.props.onFinished(true); + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + let bugreport; + + if (SdkConfig.get().bug_report_endpoint_url) { + bugreport = ( +

    Otherwise, + click here to send a bug report. +

    + ); + } + + return ( + +
    +

    We encountered an error trying to restore your previous session. If + you continue, you will need to log in again, and encrypted chat + history will be unreadable.

    + +

    If you have previously used a more recent version of Riot, your session + may be incompatible with this version. Close this window and return + to the more recent version.

    + + {bugreport} +
    +
    + +
    +
    + ); + }, +}); From ebe7ec4000eca85b74ebd5d30dc5692dbd95c7ac Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Wed, 15 Feb 2017 09:16:17 +0530 Subject: [PATCH 071/189] Rename RTE labs option to "New Composer & Autocomplete" As per confusion around https://riot.im/develop/#/room/!DdJkzRliezrwpNebLk:matrix.org/$1487091505948TmtGn:t2l.io --- src/UserSettingsStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index d7d3e7bc7a2..66a872958c8 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -26,7 +26,7 @@ var Notifier = require("./Notifier"); module.exports = { LABS_FEATURES: [ { - name: 'Rich Text Editor', + name: 'New Composer & Autocomplete', id: 'rich_text_editor', default: false, }, From 74487c655d2d7b37d578b6e02c40d92916bc03e2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Feb 2017 09:22:44 +0000 Subject: [PATCH 072/189] If a referrer hasn't been specified, use empty string This is interpretted by the RTS as a non-referred team member who still needs the team token to access their welcome page etc. --- src/components/structures/login/Registration.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index fe9fd30062c..db1147a5d2c 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -218,13 +218,11 @@ module.exports = React.createClass({ // will just nop. The point of this being we might not have the email address // that the user registered with at this stage (depending on whether this // is the client they initiated registration). - if ( - self._rtsClient && - self.props.referrer - ) { - // Track referral, get team_token in order to retrieve team config + if (self._rtsClient) { + // Track referral if self.props.referrer set, get team_token in order to + // retrieve team config and see welcome page etc. self._rtsClient.trackReferral( - self.props.referrer, + self.props.referrer || '', // Default to empty string = not referred self.registerLogic.params.idSid, self.registerLogic.params.clientSecret ).then((data) => { From 0e66b370d46e3194f86d844c360fac2203adf31e Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Thu, 16 Feb 2017 16:49:00 +0530 Subject: [PATCH 073/189] fix eslint's no-invalid-this rule for class properties --- .eslintrc.js | 6 ++++++ package.json | 1 + 2 files changed, 7 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 34d3af270c9..6cd0e1015ec 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,6 +13,7 @@ module.exports = { plugins: [ "react", "flowtype", + "babel" ], env: { es6: true, @@ -23,6 +24,11 @@ module.exports = { } }, rules: { + // eslint's built in no-invalid-this rule breaks with class properties + "no-invalid-this": "off", + // so we replace it with a version that is class property aware + "babel/no-invalid-this": "error", + /** react **/ // This just uses the react plugin to help eslint known when // variables have been used in JSX diff --git a/package.json b/package.json index 6e7013fb936..a07e2236aa8 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "babel-preset-react": "^6.11.1", "eslint": "^3.13.1", "eslint-config-google": "^0.7.1", + "eslint-plugin-babel": "^4.0.1", "eslint-plugin-flowtype": "^2.30.0", "eslint-plugin-react": "^6.9.0", "expect": "^1.16.0", From f07da44aa55e6076ea28d57b761e627929425c3d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 16 Feb 2017 12:42:45 +0000 Subject: [PATCH 074/189] Don't handle logs db: It needs to close its connections first --- src/Lifecycle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 9aded551937..87a2878e371 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -360,7 +360,7 @@ function _clearIndexedDB() { } console.log("Clearing indexeddb"); window.indexedDB.deleteDatabase("matrix-js-sdk"); - window.indexedDB.deleteDatabase("logs"); + // TODO: Remove logs db as well. } function _clearLocalStorage() { From 9d2bb70823945391406bd65f3b8e34eef9a55662 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Feb 2017 17:03:22 +0000 Subject: [PATCH 075/189] If the home page is somehow accessed, goto directory For example, if someone ends up on /home somehow, just redirect to the directory instead of displaying a very awkward "File not found" plain text in the home page iFrame. --- src/components/structures/MatrixChat.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 72680a3eac9..3265249105e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -464,6 +464,10 @@ module.exports = React.createClass({ this.notifyNewScreen('directory'); break; case 'view_home_page': + if (!this._teamToken) { + dis.dispatch({action: 'view_room_directory'}); + return; + } this._setPage(PageTypes.HomePage); this.notifyNewScreen('home'); break; From 406c34b71576050fb9beffaee12088c1e55965af Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Feb 2017 18:00:52 +0000 Subject: [PATCH 076/189] Make UserSettings use the right teamToken This threads the correct teamToken through to UserSettings for generating the referral section. --- src/components/structures/LoggedInView.js | 1 + src/components/structures/UserSettings.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 961277a4a1e..aa9470f1265 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -175,6 +175,7 @@ export default React.createClass({ collapsedRhs={this.props.collapse_rhs} enableLabs={this.props.config.enableLabs} referralBaseUrl={this.props.config.referralBaseUrl} + teamToken={this.props.teamToken} />; if (!this.props.collapse_rhs) right_panel = ; break; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index fdade60dfda..f8240e247b1 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -109,6 +109,10 @@ module.exports = React.createClass({ // true if RightPanel is collapsed collapsedRhs: React.PropTypes.bool, + + // Team token for the referral link. If falsy, the referral section will + // not appear + teamToken: React.PropTypes.string, }, getDefaultProps: function() { @@ -462,7 +466,7 @@ module.exports = React.createClass({ }, _renderReferral: function() { - const teamToken = window.localStorage.getItem('mx_team_token'); + const teamToken = this.props.teamToken; if (!teamToken) { return null; } From cca266c62cea505d8e80e5e32cac07bfccc1caf3 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 17 Feb 2017 10:43:55 +0000 Subject: [PATCH 077/189] Review comments --- src/Lifecycle.js | 12 +----------- src/MatrixClientPeg.js | 9 ++------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index d5683b35a05..295ffe44f44 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -380,21 +380,10 @@ export function startMatrixClient() { */ export function onLoggedOut() { _clearLocalStorage(); - _clearIndexedDB(); stopMatrixClient(); dis.dispatch({action: 'on_logged_out'}); } -function _clearIndexedDB() { - // remove indexeddb instances - if (!window.indexedDB) { - return; - } - console.log("Clearing indexeddb"); - window.indexedDB.deleteDatabase("matrix-js-sdk"); - // TODO: Remove logs db as well. -} - function _clearLocalStorage() { if (!window.localStorage) { return; @@ -423,6 +412,7 @@ export function stopMatrixClient() { if (cli) { cli.stopClient(); cli.removeAllListeners(); + cli.store.deleteAllData(); MatrixClientPeg.unset(); } } diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 0267b36464e..3759aa72c16 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -73,13 +73,8 @@ class MatrixClientPeg { // the react sdk doesn't work without this, so don't allow opts.pendingEventOrdering = "detached"; - let promise = q(); - if (this.matrixClient.store instanceof Matrix.IndexedDBStore) { - // load from storage before starting up. - console.log("Loading history from IndexedDB."); - promise = this.matrixClient.store.startup(); - } - // log any errors when starting up the database + let promise = this.matrixClient.store.startup(); + // log any errors when starting up the database (if one exists) promise.catch((err) => { console.error(err); }); // regardless of errors, start the client. If we did error out, we'll From b2344ceb748c006573b9cfffc6bcf73b4308aa57 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Feb 2017 11:44:56 +0000 Subject: [PATCH 078/189] Update the room view on room name change --- src/components/structures/RoomView.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 432dc5b724a..662d322fb48 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -155,6 +156,7 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room", this.onRoom); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); + MatrixClientPeg.get().on("Room.name", this.onRoomName); MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().on("accountData", this.onAccountData); @@ -342,6 +344,7 @@ module.exports = React.createClass({ if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room", this.onRoom); MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); + MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().removeListener("accountData", this.onAccountData); @@ -479,6 +482,12 @@ module.exports = React.createClass({ } }, + onRoomName: function(room) { + if (this.state.room && room.roomId == this.state.room.roomId) { + this.forceUpdate(); + } + }, + // called when state.room is first initialised (either at initial load, // after a successful peek, or after we join the room). _onRoomLoaded: function(room) { From 0a31efa0b7cb845a1940b7a4ae36a674fa89417a Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 17 Feb 2017 15:16:28 +0000 Subject: [PATCH 079/189] Add a 'Clear Cache' button This deletes the IndexedDB database and reloads the page. This solely exists as a get-out clause for users in case the indexedDB instance gets corrupted. Hopefully we won't ever need to point users to it. --- src/components/structures/UserSettings.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index f8240e247b1..b928243d406 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -418,6 +418,14 @@ module.exports = React.createClass({ Modal.createDialog(BugReportDialog, {}); }, + _onClearCacheClicked: function() { + MatrixClientPeg.get().store.deleteAllData().then(() => { + // forceReload=false since we don't really need new HTML/JS files + // we just need to restart the JS runtime. + window.location.reload(false); + }); + }, + _onInviteStateChange: function(event, member, oldMembership) { if (member.userId === this._me && oldMembership === "invite") { this.forceUpdate(); @@ -690,6 +698,18 @@ module.exports = React.createClass({
    ; }, + _renderClearCache: function() { + return
    +

    Clear Cache

    +
    + + Clear Cache (triggers page refresh) + +
    +
    ; + }, + _renderBulkOptions: function() { let invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => { return r.hasMembershipState(this._me, "invite"); @@ -913,6 +933,8 @@ module.exports = React.createClass({
    + {this._renderClearCache()} + {this._renderDeactivateAccount()} From 8d6c3cd3f45909baad3fa1e29a4a2ab71c5f33d3 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 17 Feb 2017 15:37:49 +0000 Subject: [PATCH 080/189] Review comments --- 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 b928243d406..faa8a568945 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -419,7 +419,7 @@ module.exports = React.createClass({ }, _onClearCacheClicked: function() { - MatrixClientPeg.get().store.deleteAllData().then(() => { + MatrixClientPeg.get().store.deleteAllData().done(() => { // forceReload=false since we don't really need new HTML/JS files // we just need to restart the JS runtime. window.location.reload(false); @@ -704,7 +704,7 @@ module.exports = React.createClass({
    - Clear Cache (triggers page refresh) + Clear Cache and Reload
    ; From de4773ba935bfc06391fb819e39680a41de9d31b Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Feb 2017 15:50:30 +0000 Subject: [PATCH 081/189] Show when you've been kicked or banned Update the room state when you've been kicked or banned, and show a message in the preview bar, including the reason. --- src/components/structures/RoomView.js | 11 +++++ src/components/views/rooms/RoomPreviewBar.js | 50 +++++++++++++++++--- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 662d322fb48..5bf192dfc6c 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -159,6 +159,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().on("Room.name", this.onRoomName); MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); + MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership); MatrixClientPeg.get().on("accountData", this.onAccountData); this.tabComplete = new TabComplete({ @@ -347,6 +348,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); + MatrixClientPeg.get().removeListener("RoomMember.membership", this.onRoomMemberMembership); MatrixClientPeg.get().removeListener("accountData", this.onAccountData); } @@ -612,6 +614,12 @@ module.exports = React.createClass({ this._updateRoomMembers(); }, + onRoomMemberMembership: function(ev, member, oldMembership) { + if (member.userId == MatrixClientPeg.get().credentials.userId) { + this.forceUpdate(); + } + }, + // rate limited because a power level change will emit an event for every // member in the room. _updateRoomMembers: new rate_limited_func(function() { @@ -1456,6 +1464,7 @@ module.exports = React.createClass({ />
    { name } : fallback; + }, + render: function() { var joinBlock, previewBlock; @@ -89,6 +96,16 @@ module.exports = React.createClass({
    ); } + const myMember = this.props.room ? this.props.room.currentState.members[ + MatrixClientPeg.get().credentials.userId + ] : null; + const kicked = ( + myMember && + myMember.membership == 'leave' && + myMember.events.member.getSender() != MatrixClientPeg.get().credentials.userId + ); + const banned = myMember && myMember.membership == 'ban'; + if (this.props.inviterName) { var emailMatchBlock; if (this.props.invitedEmail) { @@ -122,8 +139,31 @@ module.exports = React.createClass({
    ); - } - else if (this.props.error) { + } else if (kicked || banned) { + const verb = kicked ? 'kicked' : 'banned'; + const roomName = this._roomNameElement('this room'); + const kicker = MatrixClientPeg.get().getUser( + myMember.events.member.getSender() + ); + let reason; + if (myMember.events.member.getContent().reason) { + reason =
    Reason: {myMember.events.member.getContent().reason}
    + } + let rejoinBlock; + if (!banned) { + rejoinBlock = Rejoin; + } + joinBlock = ( +
    +
    + You have been {verb} from {roomName} by {kicker.displayName}.
    + {reason} + Forget
    + {rejoinBlock} +
    +
    + ); + } else if (this.props.error) { var name = this.props.roomAlias || "This room"; var error; if (this.props.error.errcode == 'M_NOT_FOUND') { @@ -138,10 +178,8 @@ module.exports = React.createClass({
    ); - } - else { - var name = this.props.room ? this.props.room.name : (this.props.room_alias || ""); - name = name ? { name } : "a room"; + } else { + const name = this._roomNameElement(); joinBlock = (
    From 9f9de46b10cefa674f4677eb9e239f47acac42ac Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Feb 2017 16:09:25 +0000 Subject: [PATCH 082/189] Add onForgetClick proptype --- src/components/views/rooms/RoomPreviewBar.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 61515e07ca4..b6d3cf2f50d 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -27,6 +27,7 @@ module.exports = React.createClass({ propTypes: { onJoinClick: React.PropTypes.func, onRejectClick: React.PropTypes.func, + onForgetClick: React.PropTypes.func, // if inviterName is specified, the preview bar will shown an invite to the room. // You should also specify onRejectClick if specifiying inviterName From a3aea6ba2d75bc4e3d8468036fc25fdc36320ea4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Feb 2017 16:11:20 +0000 Subject: [PATCH 083/189] Swap rejoin / forget --- src/components/views/rooms/RoomPreviewBar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index b6d3cf2f50d..4eaa1cea717 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -152,15 +152,15 @@ module.exports = React.createClass({ } let rejoinBlock; if (!banned) { - rejoinBlock = Rejoin; + rejoinBlock = ; } joinBlock = (
    You have been {verb} from {roomName} by {kicker.displayName}.
    {reason} - Forget
    {rejoinBlock} + Forget
    ); From 1f5fdf794583d7474d8bafa0ec1e90b0288cafff Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Feb 2017 16:14:51 +0000 Subject: [PATCH 084/189] Get a member object, not the user object --- src/components/views/rooms/RoomPreviewBar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 4eaa1cea717..3476c47f096 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -143,7 +143,7 @@ module.exports = React.createClass({ } else if (kicked || banned) { const verb = kicked ? 'kicked' : 'banned'; const roomName = this._roomNameElement('this room'); - const kicker = MatrixClientPeg.get().getUser( + const kicker = this.props.room.currentState.getMember( myMember.events.member.getSender() ); let reason; @@ -157,7 +157,7 @@ module.exports = React.createClass({ joinBlock = (
    - You have been {verb} from {roomName} by {kicker.displayName}.
    + You have been {verb} from {roomName} by {kicker.name}.
    {reason} {rejoinBlock} Forget From b18473ccb28b7da707580b6ae43f8e38be7aa540 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Feb 2017 16:35:18 +0000 Subject: [PATCH 085/189] Handle there being no member event when banned Here, and also in MemberEventListSummary where this also broke. --- src/components/views/elements/MemberEventListSummary.js | 2 +- src/components/views/rooms/RoomPreviewBar.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 1a73b5a50e8..510d8617303 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -381,7 +381,7 @@ module.exports = React.createClass({ // Initialise a user's events if (!userEvents[userId]) { userEvents[userId] = []; - avatarMembers.push(e.target); + if (e.target) avatarMembers.push(e.target); } userEvents[userId].push({ mxEvent: e, diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 3476c47f096..43c3b052955 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -143,9 +143,11 @@ module.exports = React.createClass({ } else if (kicked || banned) { const verb = kicked ? 'kicked' : 'banned'; const roomName = this._roomNameElement('this room'); - const kicker = this.props.room.currentState.getMember( + const kickerMember = this.props.room.currentState.getMember( myMember.events.member.getSender() ); + const kickerName = kickerMember ? + kickerMember.name : myMember.events.member.getSender(); let reason; if (myMember.events.member.getContent().reason) { reason =
    Reason: {myMember.events.member.getContent().reason}
    @@ -157,7 +159,7 @@ module.exports = React.createClass({ joinBlock = (
    - You have been {verb} from {roomName} by {kicker.name}.
    + You have been {verb} from {roomName} by {kickerName}.
    {reason} {rejoinBlock} Forget From db4b9691ccce922023509466f61d50be251f6403 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Feb 2017 17:27:46 +0000 Subject: [PATCH 086/189] Support reasons for kick / ban Don't ban me for fun, girl Let me be the one, girl Ban me for a reason Let the reason be love. --- .../views/dialogs/ConfirmUserActionDialog.js | 43 +++++++++++++++++-- src/components/views/rooms/MemberInfo.js | 8 +++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index fbe719710b8..4bd9cb669ce 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -31,22 +31,40 @@ export default React.createClass({ propTypes: { member: React.PropTypes.object.isRequired, // matrix-js-sdk member object action: React.PropTypes.string.isRequired, // eg. 'Ban' + + // Whether to display a text field for a reason + // If true, the second argument to onFinished will + // be the string entered. + askReason: React.PropTypes.bool, danger: React.PropTypes.bool, onFinished: React.PropTypes.func.isRequired, }, defaultProps: { danger: false, + askReason: false, + }, + + componentWillMount: function() { + this._reasonField = null; }, onOk: function() { - this.props.onFinished(true); + let reason; + if (this._reasonField) { + reason = this._reasonField.value; + } + this.props.onFinished(true, reason); }, onCancel: function() { this.props.onFinished(false); }, + _collectReasonField: function(e) { + this._reasonField = e; + }, + render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); @@ -56,8 +74,24 @@ export default React.createClass({ 'mx_Dialog_primary': true, 'danger': this.props.danger, }); + + let reasonBox; + if (this.props.askReason) { + reasonBox = ( +
    +
    + +
    +
    + ); + } + return ( - @@ -68,8 +102,11 @@ export default React.createClass({
    {this.props.member.name}
    {this.props.member.userId}
    + {reasonBox}
    - diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 699ee8a3a22..0c54565b9d6 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -222,13 +222,15 @@ module.exports = WithMatrixClient(React.createClass({ Modal.createDialog(ConfirmUserActionDialog, { member: this.props.member, action: 'Kick', + askReason: true, danger: true, - onFinished: (proceed) => { + onFinished: (proceed, reason) => { if (!proceed) return; this.setState({ updating: this.state.updating + 1 }); this.props.matrixClient.kick( this.props.member.roomId, this.props.member.userId, + reason || undefined ).then(function() { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! @@ -252,8 +254,9 @@ module.exports = WithMatrixClient(React.createClass({ Modal.createDialog(ConfirmUserActionDialog, { member: this.props.member, action: this.props.member.membership == 'ban' ? 'Unban' : 'Ban', + askReason: this.props.member.membership != 'ban', danger: this.props.member.membership != 'ban', - onFinished: (proceed) => { + onFinished: (proceed, reason) => { if (!proceed) return; this.setState({ updating: this.state.updating + 1 }); @@ -265,6 +268,7 @@ module.exports = WithMatrixClient(React.createClass({ } else { promise = this.props.matrixClient.ban( this.props.member.roomId, this.props.member.userId, + reason || undefined ); } promise.then( From 4f4e9a6c3af648658229dc249c6af6a73ff5eafc Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Feb 2017 18:06:00 +0000 Subject: [PATCH 087/189] Fix block quotes all being on a single line Fixes https://github.com/vector-im/riot-web/issues/3154 --- src/Markdown.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Markdown.js b/src/Markdown.js index d6dc979a5a8..4a46ce4f249 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -92,7 +92,16 @@ export default class Markdown { } toHTML() { - const renderer = new commonmark.HtmlRenderer({safe: false}); + const renderer = new commonmark.HtmlRenderer({ + safe: false, + + // Set soft breaks to hard HTML breaks: commonmark + // puts softbreaks in for multiple lines in a blockquote, + // so if these are just newline characters then the + // block quote ends up all on one line + // (https://github.com/vector-im/riot-web/issues/3154) + softbreak: '
    ', + }); const real_paragraph = renderer.paragraph; renderer.paragraph = function(node, entering) { From 1819ff0939487205840ea677f8b7e944f8fe02f0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Feb 2017 19:26:21 +0000 Subject: [PATCH 088/189] Un-hardcode home in jenkins.sh --- jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins.sh b/jenkins.sh index c1fba19e945..0ddf2f1a89e 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -3,7 +3,7 @@ set -e export KARMAFLAGS="--no-colors" -export NVM_DIR="/home/jenkins/.nvm" +export NVM_DIR="$HOME/jenkins/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" nvm use 4 From e79d38467b96270fd846169d6b86986ec3519986 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Feb 2017 19:27:50 +0000 Subject: [PATCH 089/189] Oops, un-hardcode correctly --- jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins.sh b/jenkins.sh index 0ddf2f1a89e..6a77911c272 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -3,7 +3,7 @@ set -e export KARMAFLAGS="--no-colors" -export NVM_DIR="$HOME/jenkins/.nvm" +export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" nvm use 4 From 5ac76acc053c1397b09d8d4eaafb6c2bdaab1c8b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 19 Feb 2017 02:07:00 +0200 Subject: [PATCH 090/189] limit avatar uploads to images --- src/components/views/settings/ChangeAvatar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index de30b51f1b1..230787d2412 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -128,7 +128,7 @@ module.exports = React.createClass({ uploadSection = (
    Upload new: - + {this.state.errorText}
    ); From 8990e770b742b05ac3026bc9f8be36edd9a5b515 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 20 Feb 2017 01:43:55 +0200 Subject: [PATCH 091/189] fix colouring in voip dark theme --- src/components/structures/RoomStatusBar.js | 3 ++- src/components/structures/RoomView.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index ca50e1071a4..95631e304df 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -194,8 +194,9 @@ module.exports = React.createClass({ } if (this.props.hasActiveCall) { + var TintableSvg = sdk.getComponent("elements.TintableSvg"); return ( - + ); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 5bf192dfc6c..acdea38c69a 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1639,14 +1639,14 @@ module.exports = React.createClass({ videoMuteButton =
    - {call.isLocalVideoMuted()
    ; } voiceMuteButton =
    - {call.isMicrophoneMuted()
    ; From 6fead66f8920d1b9c76431bd9f46e8dc3099f655 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 20 Feb 2017 10:59:11 +0000 Subject: [PATCH 092/189] MELS: check scroll on load + use mels-1,-2,... key To fix https://github.com/vector-im/riot-web/issues/2916, force the checking of scroll position by calling _onWidgetLoad (might need renaming...) when a MELS is expanded/contracted. Also use an keying scheme for MELS that doesn't depend on the events contained, but rather a simple incrementing index based on the order of the MELS as it appears amongst all MELS. --- src/components/structures/MessagePanel.js | 13 ++++++++----- .../views/elements/MemberEventListSummary.js | 3 +++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index dcebe38fa48..6b61759344e 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -237,6 +237,7 @@ module.exports = React.createClass({ this.eventNodes = {}; var i; + let melsIndex = 0; // first figure out which is the last event in the list which we're // actually going to show; this allows us to behave slightly @@ -302,10 +303,10 @@ module.exports = React.createClass({ // instead will allow new props to be provided. In turn, the shouldComponentUpdate // method on MELS can be used to prevent unnecessary renderings. // - // Whilst back-paginating with a MELS at the top of the panel, prevEvent will be null, - // so use the key "membereventlistsummary-initial". Otherwise, use the ID of the first - // membership event, which will not change during forward pagination. - const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial"); + // melsIndex is deliberately unrelated to the contained events so that pagination + // will not cause it to be recreated. + const key = "membereventlistsummary-" + melsIndex; + melsIndex++; if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) { let dateSeparator =
  • ; @@ -349,7 +350,9 @@ module.exports = React.createClass({ + data-scroll-token={eventId} + onToggle={this._onWidgetLoad} // Update scroll state + > {eventTiles} ); diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 510d8617303..a3c31ae65b6 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -30,6 +30,8 @@ module.exports = React.createClass({ avatarsMaxLength: React.PropTypes.number, // The minimum number of events needed to trigger summarisation threshold: React.PropTypes.number, + // Called when the MELS expansion is toggled + onToggle: React.PropTypes.func, }, getInitialState: function() { @@ -63,6 +65,7 @@ module.exports = React.createClass({ this.setState({ expanded: !this.state.expanded, }); + this.props.onToggle(); }, /** From 9eef3c53a340c9af3a1582283a4f35daeb8a3034 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 20 Feb 2017 16:53:26 +0000 Subject: [PATCH 093/189] Allow setting the default HS from the query parameter Fixes https://github.com/vector-im/riot-web/issues/3207 --- src/components/structures/MatrixChat.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 3265249105e..e885614ffeb 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -226,6 +226,15 @@ module.exports = React.createClass({ if (this._teamToken) { console.info(`Team token set to ${this._teamToken}`); } + + // Set a default HS with query param `hs_url` + const paramHs = this.props.startingFragmentQueryParams.hs_url; + if (paramHs) { + console.log('Setting register_hs_url ', paramHs); + this.setState({ + register_hs_url: paramHs, + }); + } }, componentDidMount: function() { From 6af0b9618a6cb82a84a86a331321942a337bc736 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 21 Feb 2017 00:19:49 +0000 Subject: [PATCH 094/189] first cut of improving UX for deleting devices. This adds a 5 minute auth cache to speed up the process of deleting old devices. It has the following nastinesses (mainly due to being written on a flight whilst juggling kids): * the auth cache is done as context attached to MatrixChat. one could argue that it should be per-client instead, but we don't yet have multiple clients. * the auth cache is only maintained currently in DevicesPanelEntry (i.e. set & invalidated). One could argue that it might be better maintained in InteractiveAuth.js or a dedicated cache object abstraction, but given the only use I can think of is when managing devices, perhaps this is good enough for now. --- src/components/structures/MatrixChat.js | 5 ++++ .../views/settings/DevicesPanelEntry.js | 24 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 3265249105e..8cc966607ca 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -68,6 +68,7 @@ module.exports = React.createClass({ childContextTypes: { appConfig: React.PropTypes.object, + authCache: React.PropTypes.object, }, AuxPanel: { @@ -77,6 +78,10 @@ module.exports = React.createClass({ getChildContext: function() { return { appConfig: this.props.config, + authCache: { + auth: {}, + lastUpdate: 0, + }, }; }, diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js index 4fa7d961ac0..e889f88222b 100644 --- a/src/components/views/settings/DevicesPanelEntry.js +++ b/src/components/views/settings/DevicesPanelEntry.js @@ -19,6 +19,11 @@ import React from 'react'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; +import DateUtils from '../../../DateUtils'; +import utils from 'matrix-js-sdk/lib/utils'; + + +const AUTH_CACHE_AGE = 5 * 60 * 1000; // 5 minutes export default class DevicesPanelEntry extends React.Component { constructor(props, context) { @@ -30,7 +35,6 @@ export default class DevicesPanelEntry extends React.Component { }; this._unmounted = false; - this._onDeleteClick = this._onDeleteClick.bind(this); this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this); this._makeDeleteRequest = this._makeDeleteRequest.bind(this); @@ -53,8 +57,12 @@ export default class DevicesPanelEntry extends React.Component { _onDeleteClick() { this.setState({deleting: true}); - // try without interactive auth to start off - this._makeDeleteRequest(null).catch((error) => { + if (this.context.authCache.lastUpdate < Date.now() - AUTH_CACHE_AGE) { + this.context.authCache.auth = null; + } + + // try with auth cache (which is null, so no interactive auth, to start off) + this._makeDeleteRequest(this.context.authCache.auth).catch((error) => { if (this._unmounted) { return; } if (error.httpStatus !== 401 || !error.data || !error.data.flows) { // doesn't look like an interactive-auth failure @@ -83,6 +91,9 @@ export default class DevicesPanelEntry extends React.Component { } _makeDeleteRequest(auth) { + this.context.authCache.auth = auth; + this.context.authCache.lastUpdate = Date.now(); + const device = this.props.device; return MatrixClientPeg.get().deleteDevice(device.device_id, auth).then( () => { @@ -110,8 +121,7 @@ export default class DevicesPanelEntry extends React.Component { let lastSeen = ""; if (device.last_seen_ts) { - // todo: format the timestamp as "5 minutes ago" or whatever. - const lastSeenDate = new Date(device.last_seen_ts); + const lastSeenDate = DateUtils.formatDate(new Date(device.last_seen_ts)); lastSeen = device.last_seen_ip + " @ " + lastSeenDate.toLocaleString(); } @@ -160,6 +170,10 @@ DevicesPanelEntry.propTypes = { onDeleted: React.PropTypes.func, }; +DevicesPanelEntry.contextTypes = { + authCache: React.PropTypes.object, +}; + DevicesPanelEntry.defaultProps = { onDeleted: function() {}, }; From aabf9255d2c84ff19609a7aa15be1b0e203d0895 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 21 Feb 2017 01:03:22 +0000 Subject: [PATCH 095/189] anchor the authcache on LoggedInView to prevent it persisting over logouts --- src/components/structures/LoggedInView.js | 5 +++++ src/components/structures/MatrixChat.js | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index aa9470f1265..c2243820cde 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -49,11 +49,16 @@ export default React.createClass({ childContextTypes: { matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient), + authCache: React.PropTypes.object, }, getChildContext: function() { return { matrixClient: this._matrixClient, + authCache: { + auth: {}, + lastUpdate: 0, + }, }; }, diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8cc966607ca..3265249105e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -68,7 +68,6 @@ module.exports = React.createClass({ childContextTypes: { appConfig: React.PropTypes.object, - authCache: React.PropTypes.object, }, AuxPanel: { @@ -78,10 +77,6 @@ module.exports = React.createClass({ getChildContext: function() { return { appConfig: this.props.config, - authCache: { - auth: {}, - lastUpdate: 0, - }, }; }, From 629f8caad7015bbfc18f0448fab04c1adbac8501 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 21 Feb 2017 09:50:10 +0000 Subject: [PATCH 096/189] oops, remove unneeded import --- src/components/views/settings/DevicesPanelEntry.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js index e889f88222b..60501e326f2 100644 --- a/src/components/views/settings/DevicesPanelEntry.js +++ b/src/components/views/settings/DevicesPanelEntry.js @@ -20,8 +20,6 @@ import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import DateUtils from '../../../DateUtils'; -import utils from 'matrix-js-sdk/lib/utils'; - const AUTH_CACHE_AGE = 5 * 60 * 1000; // 5 minutes From 05d242cb5cd6a4686aff84442ecbcf95252377ec Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 21 Feb 2017 14:49:30 +0000 Subject: [PATCH 097/189] allow @local:domain style mxids --- src/Invite.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Invite.js b/src/Invite.js index d1f03fe211e..0e8aca2cb55 100644 --- a/src/Invite.js +++ b/src/Invite.js @@ -19,8 +19,7 @@ import MultiInviter from './utils/MultiInviter'; const emailRegex = /^\S+@\S+\.\S+$/; -// We allow localhost for mxids to avoid confusion -const mxidRegex = /^@\S+:(?:\S+\.\S+|localhost)$/ +const mxidRegex = /^@\S+:\S+$/ export function getAddressType(inputText) { const isEmailAddress = emailRegex.test(inputText); From 581c8c138ea24bb483fdb4cdf5fe93cf37771e11 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 21 Feb 2017 15:01:18 +0000 Subject: [PATCH 098/189] Do not push a dummy element with a scroll token for invisible events If an event does not `wantTile`, do not add a dummy element with a scroll token, as this can be unperformant with 1000s of events. --- src/components/structures/MessagePanel.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index dcebe38fa48..821cbc57448 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -362,10 +362,6 @@ module.exports = React.createClass({ // replacing all of the DOM elements every time we paginate. ret.push(...this._getTilesForEvent(prevEvent, mxEv, last)); prevEvent = mxEv; - } else if (!mxEv.status) { - // if we aren't showing the event, put in a dummy scroll token anyway, so - // that we can scroll to the right place. - ret.push(
  • ); } var isVisibleReadMarker = false; From fd146a732b3c3611929c202767c8187ddea10825 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 21 Feb 2017 15:33:44 +0000 Subject: [PATCH 099/189] Clarify non-e2e vs. e2e /w composers placeholder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For E2E rooms, display "Send an encrypted message…" otherwise display "Send a plaintext message…" as the placeholder for the input box in [old] message composer. --- src/components/views/rooms/MessageComposer.js | 8 ++++++-- src/components/views/rooms/MessageComposerInput.js | 2 +- src/components/views/rooms/MessageComposerInputOld.js | 5 ++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 113224666d5..719187740b2 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -223,8 +223,8 @@ export default class MessageComposer extends React.Component { ); let e2eImg, e2eTitle, e2eClass; - - if (MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId)) { + const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); + if (roomIsEncrypted) { // FIXME: show a /!\ if there are untrusted devices in the room... e2eImg = 'img/e2e-verified.svg'; e2eTitle = 'Encrypted room'; @@ -286,12 +286,16 @@ export default class MessageComposer extends React.Component { key="controls_formatting" /> ); + const placeholderText = roomIsEncrypted ? + "Send an encrypted message…" : "Send a plaintext message…"; + controls.push( this.messageComposerInput = c} key="controls_input" onResize={this.props.onResize} room={this.props.room} + placeholder={placeholderText} tryComplete={this._tryComplete} onUpArrow={this.onUpArrow} onDownArrow={this.onDownArrow} diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 9aab1745119..61dd1e1b1cc 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -721,7 +721,7 @@ export default class MessageComposerInput extends React.Component { title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`} src={`img/button-md-${!this.state.isRichtextEnabled}.png`} /> -