From 424aae6b91283edd1a4e25142cec48dacf3fb6ac Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 12 Apr 2017 15:04:38 +0100 Subject: [PATCH 01/46] Prevent the ghost and real RM tile from both appearing --- src/components/structures/MessagePanel.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 0f8d35f525..6ee308a5a7 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -386,9 +386,7 @@ module.exports = React.createClass({ ret.push(this._getReadMarkerTile(visible)); readMarkerVisible = visible; isVisibleReadMarker = visible; - } - - if (eventId == this.currentGhostEventId) { + } else if (eventId == this.currentGhostEventId) { // if we're showing an animation, continue to show it. ret.push(this._getReadMarkerGhostTile()); } else if (!isVisibleReadMarker && From 1c25ed89b01345da3af185bb1900dd7943c388aa Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 12 Apr 2017 15:05:39 +0100 Subject: [PATCH 02/46] Initial implementation of using new RM API As detailed here https://docs.google.com/document/d/1UWqdS-e1sdwkLDUY0wA4gZyIkRp-ekjsLZ8k6g_Zvso/edit, the RM state is no longer kept locally, but rather server-side. The client now uses it's locally-calculated RM to update the server and receives server updates via the per-room account data. The sending of the RR has been bundled in to reduce traffic when sending both. In effect, whenever a RR is sent the RM is sent with it but using the new API. This uses a js-sdk change which has set to be finalised and so might change. --- src/components/structures/TimelinePanel.js | 55 +++++++++++++++------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 8cd820c284..4fbca4d40a 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -102,9 +102,6 @@ var TimelinePanel = React.createClass({ }, statics: { - // a map from room id to read marker event ID - roomReadMarkerMap: {}, - // a map from room id to read marker event timestamp roomReadMarkerTsMap: {}, }, @@ -121,10 +118,15 @@ var TimelinePanel = React.createClass({ getInitialState: function() { // XXX: we could track RM per TimelineSet rather than per Room. // but for now we just do it per room for simplicity. + let initialReadMarker = null; if (this.props.manageReadMarkers) { - var initialReadMarker = - TimelinePanel.roomReadMarkerMap[this.props.timelineSet.room.roomId] - || this._getCurrentReadReceipt(); + const readmarker = this.props.timelineSet.room.getAccountData('m.read_marker'); + if (readmarker){ + initialReadMarker = readmarker.getContent().marker; + } else { + initialReadMarker = this._getCurrentReadReceipt(); + } + console.info('Read marker initially', initialReadMarker); } return { @@ -180,6 +182,7 @@ var TimelinePanel = React.createClass({ MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction); MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated); + MatrixClientPeg.get().on("Room.accountData", this.onAccountData); this._initTimeline(this.props); }, @@ -466,6 +469,21 @@ var TimelinePanel = React.createClass({ this._reloadEvents(); }, + onAccountData: function(ev, room) { + if (this.unmounted) return; + + // ignore events for other rooms + if (room !== this.props.timelineSet.room) return; + + if (ev.getType() !== "m.read_marker") return; + + const markerEventId = ev.getContent().marker; + console.log('TimelinePanel: Read marker received from server', markerEventId); + + this.setState({ + readMarkerEventId: markerEventId, + }, this.props.onReadMarkerUpdated); + }, sendReadReceipt: function() { if (!this.refs.messagePanel) return; @@ -505,13 +523,23 @@ var TimelinePanel = React.createClass({ // we also remember the last read receipt we sent to avoid spamming the // same one at the server repeatedly - if (lastReadEventIndex > currentReadUpToEventIndex - && this.last_rr_sent_event_id != lastReadEvent.getId()) { + if ((lastReadEventIndex > currentReadUpToEventIndex && + this.last_rr_sent_event_id != lastReadEvent.getId()) || + this.last_rm_sent_event_id != this.state.readMarkerEventId) { + this.last_rr_sent_event_id = lastReadEvent.getId(); - MatrixClientPeg.get().sendReadReceipt(lastReadEvent).catch(() => { + this.last_rm_sent_event_id = this.state.readMarkerEventId; + + MatrixClientPeg.get().setRoomReadMarker( + this.props.timelineSet.room.roomId, + this.state.readMarkerEventId, + lastReadEvent + ).catch(() => { // it failed, so allow retries next time the user is active this.last_rr_sent_event_id = undefined; + this.last_rm_sent_event_id = undefined; }); + console.log('TimelinePanel: Read marker sent to the server ', this.state.readMarkerEventId, ); // do a quick-reset of our unreadNotificationCount to avoid having // to wait from the remote echo from the homeserver. @@ -956,16 +984,10 @@ var TimelinePanel = React.createClass({ _setReadMarker: function(eventId, eventTs, inhibitSetState) { var roomId = this.props.timelineSet.room.roomId; - if (TimelinePanel.roomReadMarkerMap[roomId] == eventId) { - // don't update the state (and cause a re-render) if there is - // no change to the RM. + if (eventId === this.state.readMarkerEventId) { return; } - // ideally we'd sync these via the server, but for now just stash them - // in a map. - TimelinePanel.roomReadMarkerMap[roomId] = eventId; - // in order to later figure out if the read marker is // above or below the visible timeline, we stash the timestamp. TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs; @@ -974,6 +996,7 @@ var TimelinePanel = React.createClass({ return; } + // Do the local echo of the RM // run the render cycle before calling the callback, so that // getReadMarkerPosition() returns the right thing. this.setState({ From 249e42747b2435df6d431ee0170dd167133b4479 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 12 Apr 2017 15:09:56 +0100 Subject: [PATCH 03/46] Fix bug where `roomId` was expected to be a property on timelineSet --- src/components/structures/TimelinePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 4fbca4d40a..18f52d1f07 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -734,7 +734,7 @@ var TimelinePanel = React.createClass({ // the messagePanel doesn't know where the read marker is. // if we know the timestamp of the read marker, make a guess based on that. - var rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.roomId]; + const rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.room.roomId]; if (rmTs && this.state.events.length > 0) { if (rmTs < this.state.events[0].getTs()) { return -1; From 9c9dc84f45e0b26df4c444babc42a614749c92c0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 12 Apr 2017 15:12:37 +0100 Subject: [PATCH 04/46] Remove redundant setting of readMarkerEventId --- src/components/structures/TimelinePanel.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 18f52d1f07..9277c3f2b7 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -417,9 +417,10 @@ var TimelinePanel = React.createClass({ } else if(lastEv && this.getReadMarkerPosition() === 0) { // we know we're stuckAtBottom, so we can advance the RM // immediately, to save a later render cycle + + // This call will setState with readMarkerEventId = lastEv.getId() this._setReadMarker(lastEv.getId(), lastEv.getTs(), true); updatedState.readMarkerVisible = false; - updatedState.readMarkerEventId = lastEv.getId(); callback = this.props.onReadMarkerUpdated; } } From a0c498e8ba786543fa8efadd8d563af939299b11 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 17 Apr 2017 14:37:24 +0100 Subject: [PATCH 05/46] Make Download behaviour consistent with that of E2E (iframed) download butttons (ACTUALLY DOWNLOAD) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/messages/MFileBody.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 86aee28269..029a8a9fe4 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -346,7 +346,7 @@ module.exports = React.createClass({ return (
- + { fileName }
@@ -360,7 +360,7 @@ module.exports = React.createClass({ return (
- + Download {text} From 6f0c3b1c03f92f49a1f90f6edcdcc73283479401 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 17 Apr 2017 14:50:34 +0100 Subject: [PATCH 06/46] Pass file name (as name) to the ImageView modal Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/messages/MImageBody.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index ab163297d7..0b4bc6ecb9 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -56,6 +56,7 @@ module.exports = React.createClass({ const ImageView = sdk.getComponent("elements.ImageView"); const params = { src: httpUrl, + name: content.body && content.body.length > 0 ? content.body : 'Attachment', mxEvent: this.props.mxEvent, }; From 28ed69b61761abe28b0454c09e58841f0f4f19a1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 18 Apr 2017 14:44:43 +0100 Subject: [PATCH 07/46] m.read_marker -> m.fully_read --- src/components/structures/TimelinePanel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 9277c3f2b7..162c474a25 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -120,7 +120,7 @@ var TimelinePanel = React.createClass({ // but for now we just do it per room for simplicity. let initialReadMarker = null; if (this.props.manageReadMarkers) { - const readmarker = this.props.timelineSet.room.getAccountData('m.read_marker'); + const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read'); if (readmarker){ initialReadMarker = readmarker.getContent().marker; } else { @@ -476,7 +476,7 @@ var TimelinePanel = React.createClass({ // ignore events for other rooms if (room !== this.props.timelineSet.room) return; - if (ev.getType() !== "m.read_marker") return; + if (ev.getType() !== "m.fully_read") return; const markerEventId = ev.getContent().marker; console.log('TimelinePanel: Read marker received from server', markerEventId); From d33afa99ab25f85b2932f7d9c9621347eabaa40c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 18 Apr 2017 15:13:05 +0100 Subject: [PATCH 08/46] marker -> event_id --- src/components/structures/TimelinePanel.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 162c474a25..74cf549c4d 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -122,7 +122,7 @@ var TimelinePanel = React.createClass({ if (this.props.manageReadMarkers) { const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read'); if (readmarker){ - initialReadMarker = readmarker.getContent().marker; + initialReadMarker = readmarker.getContent().event_id; } else { initialReadMarker = this._getCurrentReadReceipt(); } @@ -478,7 +478,7 @@ var TimelinePanel = React.createClass({ if (ev.getType() !== "m.fully_read") return; - const markerEventId = ev.getContent().marker; + const markerEventId = ev.getContent().event_id; console.log('TimelinePanel: Read marker received from server', markerEventId); this.setState({ @@ -1045,7 +1045,6 @@ var TimelinePanel = React.createClass({ // events when viewing historical messages, we get stuck in a loop // of paginating our way through the entire history of the room. var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS); - return (
; }, + _showSpoiler: function(event) { + const target = event.target; + const hidden = target.getAttribute('data-spoiler'); + + target.innerHTML = hidden; + + const range = document.createRange(); + range.selectNodeContents(target); + + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }, + nameForMedium: function(medium) { if (medium == 'msisdn') return 'Phone'; return medium[0].toUpperCase() + medium.slice(1); @@ -958,6 +972,9 @@ module.exports = React.createClass({
Logged in as {this._me}
+
+ Access Token: <click to reveal> +
Homeserver is { MatrixClientPeg.get().getHomeserverUrl() }
From 566a31524271cc50a76f7af25866468f0eb09911 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:08:04 +0100 Subject: [PATCH 12/46] Initial commit on riot-web#3524 (login UI update) --- src/components/views/elements/Dropdown.js | 21 ++++--- src/components/views/login/CountryDropdown.js | 2 +- src/components/views/login/PasswordLogin.js | 59 +++++++++++++------ 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index 3b34d3cac1..907d4b0905 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -249,7 +249,7 @@ export default class Dropdown extends React.Component { ); }); - if (!this.state.searchQuery) { + if (!this.state.searchQuery && this.props.searchEnabled) { options.push(
Type to search... @@ -267,16 +267,20 @@ export default class Dropdown extends React.Component { let menu; if (this.state.expanded) { - currentValue = ; + if (this.props.searchEnabled) { + currentValue = ; + } menu =
{this._getMenuOptions()}
; - } else { + } + + if (!currentValue) { const selectedChild = this.props.getShortOption ? this.props.getShortOption(this.props.value) : this.childrenByKey[this.props.value]; @@ -313,6 +317,7 @@ Dropdown.propTypes = { onOptionChange: React.PropTypes.func.isRequired, // Called when the value of the search field changes onSearchChange: React.PropTypes.func, + searchEnabled: React.PropTypes.boolean, // Function that, given the key of an option, returns // a node representing that option to be displayed in the // box itself as the currently-selected option (ie. as diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index 9729c9e23f..be1ed51b5e 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -111,7 +111,7 @@ export default class CountryDropdown extends React.Component { return {options} diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 61cb3da652..002de0c2ba 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -60,6 +60,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin', password: this.props.initialPassword, phoneCountry: this.props.initialPhoneCountry, phoneNumber: this.props.initialPhoneNumber, + loginType: "mxid", }; }, @@ -88,6 +89,10 @@ module.exports = React.createClass({displayName: 'PasswordLogin', this.props.onUsernameChanged(ev.target.value); }, + onLoginTypeChange: function(loginType) { + this.setState({loginType: loginType}); + }, + onPhoneCountryChanged: function(country) { this.setState({phoneCountry: country}); this.props.onPhoneCountryChanged(country); @@ -120,28 +125,46 @@ module.exports = React.createClass({displayName: 'PasswordLogin', }); const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); - return ( -
-
+ const Dropdown = sdk.getComponent('elements.Dropdown'); + + const loginType = { + 'email': - or -
- - + placeholder="Email or user name" autoFocus />, + 'mxid': + , + 'phone':
+ + +
+ }[this.state.loginType]; + + return ( +
+ +
+ + + Matrix ID + Email + Phone +
-
+ {loginType} {this._passwordField = e;}} type="password" name="password" value={this.state.password} onChange={this.onPasswordChanged} From 81bdfe2126fe9375b937aa1ec39f7acc61e221b2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:14:57 +0100 Subject: [PATCH 13/46] Update to match renamed API --- src/components/structures/TimelinePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 74cf549c4d..9dc1b2dead 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -531,7 +531,7 @@ var TimelinePanel = React.createClass({ this.last_rr_sent_event_id = lastReadEvent.getId(); this.last_rm_sent_event_id = this.state.readMarkerEventId; - MatrixClientPeg.get().setRoomReadMarker( + MatrixClientPeg.get().setRoomReadMarkers( this.props.timelineSet.room.roomId, this.state.readMarkerEventId, lastReadEvent From 28818b857acbd0505c66a418c7b56e6b95c395ad Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:17:44 +0100 Subject: [PATCH 14/46] Remove log --- src/components/structures/TimelinePanel.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 9dc1b2dead..34f492c585 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -126,7 +126,6 @@ var TimelinePanel = React.createClass({ } else { initialReadMarker = this._getCurrentReadReceipt(); } - console.info('Read marker initially', initialReadMarker); } return { From e32f153573cc36fac793c2253cbb7d3485858615 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:18:25 +0100 Subject: [PATCH 15/46] Remove Room.accountData listener on unmount --- src/components/structures/TimelinePanel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 34f492c585..a9c063b2fa 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -249,6 +249,7 @@ var TimelinePanel = React.createClass({ client.removeListener("Room.redaction", this.onRoomRedaction); client.removeListener("Room.receipt", this.onRoomReceipt); client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated); + client.removeListener("Room.accountData", this.onAccountData); } }, From 00cf5b59183252a2650de970566a97c83d2bc21d Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:20:24 +0100 Subject: [PATCH 16/46] Revert change --- src/components/structures/TimelinePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index a9c063b2fa..4657548a3c 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -418,9 +418,9 @@ var TimelinePanel = React.createClass({ // we know we're stuckAtBottom, so we can advance the RM // immediately, to save a later render cycle - // This call will setState with readMarkerEventId = lastEv.getId() this._setReadMarker(lastEv.getId(), lastEv.getTs(), true); updatedState.readMarkerVisible = false; + updatedState.readMarkerEventId = lastEv.getId(); callback = this.props.onReadMarkerUpdated; } } From a787ee848065adb134d7007ce3cbf15d4f14e35f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:20:53 +0100 Subject: [PATCH 17/46] Remove spammy log --- src/components/structures/TimelinePanel.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 4657548a3c..7d202b7a85 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -478,11 +478,8 @@ var TimelinePanel = React.createClass({ if (ev.getType() !== "m.fully_read") return; - const markerEventId = ev.getContent().event_id; - console.log('TimelinePanel: Read marker received from server', markerEventId); - this.setState({ - readMarkerEventId: markerEventId, + readMarkerEventId: ev.getContent().event_id, }, this.props.onReadMarkerUpdated); }, From 81bf2be13b96baff2796690d799f7444bb8262f6 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:27:43 +0100 Subject: [PATCH 18/46] Make note of inconsistant roomReadMarkerTsMap This will become redundant when there is server support for directionality of the RM --- src/components/structures/TimelinePanel.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 7d202b7a85..5a52d57f17 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -478,6 +478,9 @@ var TimelinePanel = React.createClass({ if (ev.getType() !== "m.fully_read") return; + // XXX: roomReadMarkerTsMap not updated here so it is now inconsistent. Replace + // this mechanism of determining where the RM is relative to the view-port with + // one supported by the server (the client needs more than an event ID). this.setState({ readMarkerEventId: ev.getContent().event_id, }, this.props.onReadMarkerUpdated); From edeaef8c2f163ca8053ab817a37e49c0f142cbec Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:28:38 +0100 Subject: [PATCH 19/46] Initialise last_rm_sent_event_id --- src/components/structures/TimelinePanel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 5a52d57f17..92aeb7cc66 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -174,6 +174,7 @@ var TimelinePanel = React.createClass({ debuglog("TimelinePanel: mounting"); this.last_rr_sent_event_id = undefined; + this.last_rm_sent_event_id = undefined; this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); From a4ba5f041c1f80d942984e6ed7fdc25ed022beea Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 19 Apr 2017 10:46:08 +0100 Subject: [PATCH 20/46] Remove log, reinstate comment --- src/components/structures/TimelinePanel.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 92aeb7cc66..787638f966 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -541,7 +541,6 @@ var TimelinePanel = React.createClass({ this.last_rr_sent_event_id = undefined; this.last_rm_sent_event_id = undefined; }); - console.log('TimelinePanel: Read marker sent to the server ', this.state.readMarkerEventId, ); // do a quick-reset of our unreadNotificationCount to avoid having // to wait from the remote echo from the homeserver. @@ -986,6 +985,8 @@ var TimelinePanel = React.createClass({ _setReadMarker: function(eventId, eventTs, inhibitSetState) { var roomId = this.props.timelineSet.room.roomId; + // don't update the state (and cause a re-render) if there is + // no change to the RM. if (eventId === this.state.readMarkerEventId) { return; } From 67089cb5279c80afabdce4cdb0707f0183a1185a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 20 Apr 2017 14:34:59 +0100 Subject: [PATCH 21/46] If new RR-RM API not implemented, fallback to RR-only API --- src/components/structures/TimelinePanel.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 787638f966..e8774cec62 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -536,9 +536,16 @@ var TimelinePanel = React.createClass({ this.props.timelineSet.room.roomId, this.state.readMarkerEventId, lastReadEvent - ).catch(() => { + ).catch((e) => { + // /read_markers API is not implemented on this HS, fallback to just RR + if (e.errcode === 'M_UNRECOGNIZED') { + return MatrixClientPeg.get().sendReadReceipt( + lastReadEvent + ).catch(() => { + this.last_rr_sent_event_id = undefined; + }); + } // it failed, so allow retries next time the user is active - this.last_rr_sent_event_id = undefined; this.last_rm_sent_event_id = undefined; }); From bbd1f3433683dd64119406898de205b6deaa4762 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 03:04:34 +0100 Subject: [PATCH 22/46] Prepend REACT_SDK_VERSION with a v to match riot-web version output Add simple helper to construct version/commit hash urls var -> let/const and prepend olmVersionString with v for same reason for both matrix-react-sdk and riot-web, if unknown/local don't do anything else try to create a link to the commit hash/tag name Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/UserSettings.js | 26 ++++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 892865fdf9..881817acab 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -31,10 +31,14 @@ var SdkConfig = require('../../SdkConfig'); import AccessibleButton from '../views/elements/AccessibleButton'; // if this looks like a release, use the 'version' from package.json; else use -// the git sha. -const REACT_SDK_VERSION = - 'dist' in package_json ? package_json.version : package_json.gitHead || ""; +// the git sha. Prepend version with v, to look like riot-web version +const REACT_SDK_VERSION = 'dist' in package_json ? `v${package_json.version}` : package_json.gitHead || ''; +// Simple method to help prettify GH Release Tags and Commit Hashes. +const GHVersionUrl = function(repo, token) { + const uriTail = (token.startsWith('v') && token.includes('.')) ? `releases/tag/${token}` : `commit/${token}`; + return `https://github.com/${repo}/${uriTail}`; +} // Enumerate some simple 'flip a bit' UI settings (if any). // 'id' gives the key name in the im.vector.web.settings account data event @@ -880,12 +884,12 @@ module.exports = React.createClass({
); } - var olmVersion = MatrixClientPeg.get().olmVersion; + const olmVersion = MatrixClientPeg.get().olmVersion; // If the olmVersion is not defined then either crypto is disabled, or // we are using a version old version of olm. We assume the former. - var olmVersionString = ""; + let olmVersionString = ""; if (olmVersion !== undefined) { - olmVersionString = olmVersion[0] + "." + olmVersion[1] + "." + olmVersion[2]; + olmVersionString = `v${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`; } return ( @@ -965,8 +969,14 @@ module.exports = React.createClass({ Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() }
- matrix-react-sdk version: {REACT_SDK_VERSION}
- riot-web version: {this.state.vectorVersion !== null ? this.state.vectorVersion : 'unknown'}
+ matrix-react-sdk version: {(REACT_SDK_VERSION !== '') + ? {REACT_SDK_VERSION} + : REACT_SDK_VERSION + }
+ riot-web version: {(this.state.vectorVersion !== null) + ? {this.state.vectorVersion} + : 'unknown' + }
olm version: {olmVersionString}
From 9cd7914ea51dbfb60f8b84a80cb800282476d3e4 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 21 Apr 2017 11:37:08 +0100 Subject: [PATCH 23/46] Finishing off the first iteration on login UI This makes the following changes: - Improve CountryDropdown by allowing all countries to be displayed at once and using PNGs for performance (trading of quality - the pngs are scaled down from 32px to 25px) - "I want to sign in with" dropdown to select login method - MXID login field that suffixes HS domain (whether custom or matrix.org) and prefixes "@" - Email field which is secretly the same as the username field but with a different placeholder - No more login flickering when changing ServerConfig (!) fixes https://github.com/vector-im/riot-web/issues/1517 This implements most of the design in https://github.com/vector-im/riot-web/issues/3524 but neglects the phone number login: ![login_with_msisdn](https://cloud.githubusercontent.com/assets/1922197/24864469/30a921fc-1dfc-11e7-95d1-76f619da1402.png) This will be updated in another PR to implement desired things: - Country code visible once a country has been selected (propbably but as a prefix to the phone number input box. - Use square flags - Move CountryDropdown above phone input and make it show the full country name when not expanded - Auto-select country based on IP --- src/HtmlUtils.js | 14 +- src/components/structures/login/Login.js | 85 +++---- src/components/views/elements/Dropdown.js | 13 +- src/components/views/login/CountryDropdown.js | 8 +- src/components/views/login/PasswordLogin.js | 210 +++++++++++------- src/components/views/login/ServerConfig.js | 28 ++- 6 files changed, 207 insertions(+), 151 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index a8e20f5ec1..96934d205e 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -25,6 +25,9 @@ import emojione from 'emojione'; import classNames from 'classnames'; emojione.imagePathSVG = 'emojione/svg/'; +// Store PNG path for displaying many flags at once (for increased performance over SVG) +emojione.imagePathPNG = 'emojione/png/'; +// Use SVGs for emojis emojione.imageType = 'svg'; const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi"); @@ -64,16 +67,23 @@ export function unicodeToImage(str) { * emoji. * * @param alt {string} String to use for the image alt text + * @param useSvg {boolean} Whether to use SVG image src. If False, PNG will be used. * @param unicode {integer} One or more integers representing unicode characters * @returns A img node with the corresponding emoji */ -export function charactersToImageNode(alt, ...unicode) { +export function charactersToImageNode(alt, useSvg, ...unicode) { const fileName = unicode.map((u) => { return u.toString(16); }).join('-'); - return {alt}; + const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG; + const fileType = useSvg ? 'svg' : 'png'; + return {alt}; } + export function stripParagraphs(html: string): string { const contentDiv = document.createElement('div'); contentDiv.innerHTML = html; diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 7e1a5f9d35..d9a7039686 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -17,13 +17,11 @@ limitations under the License. 'use strict'; -var React = require('react'); -var ReactDOM = require('react-dom'); -var sdk = require('../../../index'); -var Login = require("../../../Login"); -var PasswordLogin = require("../../views/login/PasswordLogin"); -var CasLogin = require("../../views/login/CasLogin"); -var ServerConfig = require("../../views/login/ServerConfig"); +import React from 'react'; +import ReactDOM from 'react-dom'; +import url from 'url'; +import sdk from '../../../index'; +import Login from '../../../Login'; /** * A wire component which glues together login UI components and Login logic @@ -67,6 +65,7 @@ module.exports = React.createClass({ username: "", phoneCountry: null, phoneNumber: "", + currentFlow: "m.login.password", }; }, @@ -129,23 +128,19 @@ module.exports = React.createClass({ this.setState({ phoneNumber: phoneNumber }); }, - onHsUrlChanged: function(newHsUrl) { + onServerConfigChange: function(config) { var self = this; - this.setState({ - enteredHomeserverUrl: newHsUrl, + let newState = { errorText: null, // reset err messages - }, function() { - self._initLoginLogic(newHsUrl); - }); - }, - - onIsUrlChanged: function(newIsUrl) { - var self = this; - this.setState({ - enteredIdentityServerUrl: newIsUrl, - errorText: null, // reset err messages - }, function() { - self._initLoginLogic(null, newIsUrl); + }; + if (config.hsUrl !== undefined) { + newState.enteredHomeserverUrl = config.hsUrl; + } + if (config.isUrl !== undefined) { + newState.enteredIdentityServerUrl = config.isUrl; + } + this.setState(newState, function() { + self._initLoginLogic(config.hsUrl || null, config.isUrl); }); }, @@ -161,25 +156,28 @@ module.exports = React.createClass({ }); this._loginLogic = loginLogic; - loginLogic.getFlows().then(function(flows) { - // old behaviour was to always use the first flow without presenting - // options. This works in most cases (we don't have a UI for multiple - // logins so let's skip that for now). - loginLogic.chooseFlow(0); - }, function(err) { - self._setStateFromError(err, false); - }).finally(function() { - self.setState({ - busy: false - }); - }); - this.setState({ enteredHomeserverUrl: hsUrl, enteredIdentityServerUrl: isUrl, busy: true, loginIncorrect: false, }); + + loginLogic.getFlows().then(function(flows) { + // old behaviour was to always use the first flow without presenting + // options. This works in most cases (we don't have a UI for multiple + // logins so let's skip that for now). + loginLogic.chooseFlow(0); + self.setState({ + currentFlow: self._getCurrentFlowStep(), + }); + }, function(err) { + self._setStateFromError(err, false); + }).finally(function() { + self.setState({ + busy: false, + }); + }); }, _getCurrentFlowStep: function() { @@ -231,6 +229,7 @@ module.exports = React.createClass({ componentForStep: function(step) { switch (step) { case 'm.login.password': + const PasswordLogin = sdk.getComponent('login.PasswordLogin'); return ( ); case 'm.login.cas': + const CasLogin = sdk.getComponent('login.CasLogin'); return ( ); @@ -262,10 +263,11 @@ module.exports = React.createClass({ }, render: function() { - var Loader = sdk.getComponent("elements.Spinner"); - var LoginHeader = sdk.getComponent("login.LoginHeader"); - var LoginFooter = sdk.getComponent("login.LoginFooter"); - var loader = this.state.busy ?
: null; + const Loader = sdk.getComponent("elements.Spinner"); + const LoginHeader = sdk.getComponent("login.LoginHeader"); + const LoginFooter = sdk.getComponent("login.LoginFooter"); + const ServerConfig = sdk.getComponent("login.ServerConfig"); + const loader = this.state.busy ?
: null; var loginAsGuestJsx; if (this.props.enableGuest) { @@ -291,15 +293,14 @@ module.exports = React.createClass({

Sign in { loader }

- { this.componentForStep(this._getCurrentFlowStep()) } + { this.componentForStep(this.state.currentFlow) }
{ this.state.errorText } diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index 907d4b0905..a9ecf5b669 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -248,13 +248,10 @@ export default class Dropdown extends React.Component { ); }); - - if (!this.state.searchQuery && this.props.searchEnabled) { - options.push( -
- Type to search... -
- ); + if (options.length === 0) { + return [
+ No results +
]; } return options; } @@ -317,7 +314,7 @@ Dropdown.propTypes = { onOptionChange: React.PropTypes.func.isRequired, // Called when the value of the search field changes onSearchChange: React.PropTypes.func, - searchEnabled: React.PropTypes.boolean, + searchEnabled: React.PropTypes.bool, // Function that, given the key of an option, returns // a node representing that option to be displayed in the // box itself as the currently-selected option (ie. as diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index be1ed51b5e..7f6b21650d 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -33,8 +33,6 @@ function countryMatchesSearchQuery(query, country) { return false; } -const MAX_DISPLAYED_ROWS = 2; - export default class CountryDropdown extends React.Component { constructor(props) { super(props); @@ -64,7 +62,7 @@ export default class CountryDropdown extends React.Component { // Unicode Regional Indicator Symbol letter 'A' const RIS_A = 0x1F1E6; const ASCII_A = 65; - return charactersToImageNode(iso2, + return charactersToImageNode(iso2, true, RIS_A + (iso2.charCodeAt(0) - ASCII_A), RIS_A + (iso2.charCodeAt(1) - ASCII_A), ); @@ -93,10 +91,6 @@ export default class CountryDropdown extends React.Component { displayedCountries = COUNTRIES; } - if (displayedCountries.length > MAX_DISPLAYED_ROWS) { - displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS); - } - const options = displayedCountries.map((country) => { return
{this._flagImgForIso2(country.iso2)} diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 002de0c2ba..fc063efbe9 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -25,56 +25,49 @@ import {field_input_incorrect} from '../../../UiEffects'; /** * A pure UI component which displays a username/password form. */ -module.exports = React.createClass({displayName: 'PasswordLogin', - propTypes: { - onSubmit: React.PropTypes.func.isRequired, // fn(username, password) - onForgotPasswordClick: React.PropTypes.func, // fn() - initialUsername: React.PropTypes.string, - initialPhoneCountry: React.PropTypes.string, - initialPhoneNumber: React.PropTypes.string, - initialPassword: React.PropTypes.string, - onUsernameChanged: React.PropTypes.func, - onPhoneCountryChanged: React.PropTypes.func, - onPhoneNumberChanged: React.PropTypes.func, - onPasswordChanged: React.PropTypes.func, - loginIncorrect: React.PropTypes.bool, - }, +class PasswordLogin extends React.Component { + static defaultProps = { + onUsernameChanged: function() {}, + onPasswordChanged: function() {}, + onPhoneCountryChanged: function() {}, + onPhoneNumberChanged: function() {}, + initialUsername: "", + initialPhoneCountry: "", + initialPhoneNumber: "", + initialPassword: "", + loginIncorrect: false, + hsDomain: "", + } - getDefaultProps: function() { - return { - onUsernameChanged: function() {}, - onPasswordChanged: function() {}, - onPhoneCountryChanged: function() {}, - onPhoneNumberChanged: function() {}, - initialUsername: "", - initialPhoneCountry: "", - initialPhoneNumber: "", - initialPassword: "", - loginIncorrect: false, - }; - }, - - getInitialState: function() { - return { + constructor(props) { + super(props); + this.state = { username: this.props.initialUsername, password: this.props.initialPassword, phoneCountry: this.props.initialPhoneCountry, phoneNumber: this.props.initialPhoneNumber, - loginType: "mxid", + loginType: PasswordLogin.LOGIN_FIELD_MXID, }; - }, - componentWillMount: function() { + this.onSubmitForm = this.onSubmitForm.bind(this); + this.onUsernameChanged = this.onUsernameChanged.bind(this); + this.onLoginTypeChange = this.onLoginTypeChange.bind(this); + this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this); + this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this); + this.onPasswordChanged = this.onPasswordChanged.bind(this); + } + + componentWillMount() { this._passwordField = null; - }, + } - componentWillReceiveProps: function(nextProps) { + componentWillReceiveProps(nextProps) { if (!this.props.loginIncorrect && nextProps.loginIncorrect) { field_input_incorrect(this._passwordField); } - }, + } - onSubmitForm: function(ev) { + onSubmitForm(ev) { ev.preventDefault(); this.props.onSubmit( this.state.username, @@ -82,33 +75,87 @@ module.exports = React.createClass({displayName: 'PasswordLogin', this.state.phoneNumber, this.state.password, ); - }, + } - onUsernameChanged: function(ev) { + onUsernameChanged(ev) { this.setState({username: ev.target.value}); this.props.onUsernameChanged(ev.target.value); - }, + } - onLoginTypeChange: function(loginType) { - this.setState({loginType: loginType}); - }, + onLoginTypeChange(loginType) { + this.setState({ + loginType: loginType, + username: "" // Reset because email and username use the same state + }); + } - onPhoneCountryChanged: function(country) { + onPhoneCountryChanged(country) { this.setState({phoneCountry: country}); this.props.onPhoneCountryChanged(country); - }, + } - onPhoneNumberChanged: function(ev) { + onPhoneNumberChanged(ev) { this.setState({phoneNumber: ev.target.value}); this.props.onPhoneNumberChanged(ev.target.value); - }, + } - onPasswordChanged: function(ev) { + onPasswordChanged(ev) { this.setState({password: ev.target.value}); this.props.onPasswordChanged(ev.target.value); - }, + } - render: function() { + renderLoginField(loginType) { + switch(loginType) { + case PasswordLogin.LOGIN_FIELD_EMAIL: + return ; + case PasswordLogin.LOGIN_FIELD_MXID: + return
+
@
+ +
:{this.props.hsDomain}
+
; + case PasswordLogin.LOGIN_FIELD_PHONE: + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + return
+ + +
; + } + } + + render() { var forgotPasswordJsx; if (this.props.onForgotPasswordClick) { @@ -124,47 +171,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin', error: this.props.loginIncorrect, }); - const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); const Dropdown = sdk.getComponent('elements.Dropdown'); - const loginType = { - 'email': - , - 'mxid': - , - 'phone':
- - -
- }[this.state.loginType]; + const loginField = this.renderLoginField(this.state.loginType); return (
- - Matrix ID - Email - Phone + + Matrix ID + Email Address + Phone
- {loginType} + {loginField} {this._passwordField = e;}} type="password" name="password" value={this.state.password} onChange={this.onPasswordChanged} @@ -176,4 +201,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
); } -}); +} + +PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email"; +PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid"; +PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone"; + +PasswordLogin.propTypes = { + onSubmit: React.PropTypes.func.isRequired, // fn(username, password) + onForgotPasswordClick: React.PropTypes.func, // fn() + initialUsername: React.PropTypes.string, + initialPhoneCountry: React.PropTypes.string, + initialPhoneNumber: React.PropTypes.string, + initialPassword: React.PropTypes.string, + onUsernameChanged: React.PropTypes.func, + onPhoneCountryChanged: React.PropTypes.func, + onPhoneNumberChanged: React.PropTypes.func, + onPasswordChanged: React.PropTypes.func, + loginIncorrect: React.PropTypes.bool, + hsDomain: React.PropTypes.string, +}; + +module.exports = PasswordLogin; diff --git a/src/components/views/login/ServerConfig.js b/src/components/views/login/ServerConfig.js index 4e6ed12f9e..2853945425 100644 --- a/src/components/views/login/ServerConfig.js +++ b/src/components/views/login/ServerConfig.js @@ -27,8 +27,7 @@ module.exports = React.createClass({ displayName: 'ServerConfig', propTypes: { - onHsUrlChanged: React.PropTypes.func, - onIsUrlChanged: React.PropTypes.func, + onServerConfigChange: React.PropTypes.func, // default URLs are defined in config.json (or the hardcoded defaults) // they are used if the user has not overridden them with a custom URL. @@ -50,8 +49,7 @@ module.exports = React.createClass({ getDefaultProps: function() { return { - onHsUrlChanged: function() {}, - onIsUrlChanged: function() {}, + onServerConfigChange: function() {}, customHsUrl: "", customIsUrl: "", withToggleButton: false, @@ -75,7 +73,10 @@ module.exports = React.createClass({ this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() { var hsUrl = this.state.hs_url.trim().replace(/\/$/, ""); if (hsUrl === "") hsUrl = this.props.defaultHsUrl; - this.props.onHsUrlChanged(hsUrl); + this.props.onServerConfigChange({ + hsUrl : this.state.hs_url, + isUrl : this.state.is_url, + }); }); }); }, @@ -85,7 +86,10 @@ module.exports = React.createClass({ this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() { var isUrl = this.state.is_url.trim().replace(/\/$/, ""); if (isUrl === "") isUrl = this.props.defaultIsUrl; - this.props.onIsUrlChanged(isUrl); + this.props.onServerConfigChange({ + hsUrl : this.state.hs_url, + isUrl : this.state.is_url, + }); }); }); }, @@ -102,12 +106,16 @@ module.exports = React.createClass({ configVisible: visible }); if (!visible) { - this.props.onHsUrlChanged(this.props.defaultHsUrl); - this.props.onIsUrlChanged(this.props.defaultIsUrl); + this.props.onServerConfigChange({ + hsUrl : this.props.defaultHsUrl, + isUrl : this.props.defaultIsUrl, + }); } else { - this.props.onHsUrlChanged(this.state.hs_url); - this.props.onIsUrlChanged(this.state.is_url); + this.props.onServerConfigChange({ + hsUrl : this.state.hs_url, + isUrl : this.state.is_url, + }); } }, From 2b9cb999baebc04bc8d62f1159714278dd67711f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 21 Apr 2017 11:50:19 +0100 Subject: [PATCH 24/46] autoFocus PasswordLogin --- src/components/views/login/PasswordLogin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index fc063efbe9..ffb86636ca 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -150,6 +150,7 @@ class PasswordLogin extends React.Component { onChange={this.onPhoneNumberChanged} placeholder="Mobile phone number" value={this.state.phoneNumber} + autoFocus />
; } From 9c4c706120497ad677cff3f1820c31cb628516e1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 21 Apr 2017 16:09:11 +0100 Subject: [PATCH 25/46] Remove :server.name for custom servers Custom servers may not be configured such that their domain name === domain part. --- src/components/structures/login/Login.js | 8 +++++++- src/components/views/login/PasswordLogin.js | 15 +++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index d9a7039686..315a0ea242 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -230,6 +230,12 @@ module.exports = React.createClass({ switch (step) { case 'm.login.password': const PasswordLogin = sdk.getComponent('login.PasswordLogin'); + // HSs that are not matrix.org may not be configured to have their + // domain name === domain part. + let hsDomain = url.parse(this.state.enteredHomeserverUrl).hostname; + if (hsDomain !== 'matrix.org') { + hsDomain = null; + } return ( ); case 'm.login.cas': diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index ffb86636ca..568461817c 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -118,10 +118,21 @@ class PasswordLogin extends React.Component { autoFocus />; case PasswordLogin.LOGIN_FIELD_MXID: + const mxidInputClasses = classNames({ + "mx_Login_field": true, + "mx_Login_username": true, + "mx_Login_field_has_suffix": Boolean(this.props.hsDomain), + }); + let suffix = null; + if (this.props.hsDomain) { + suffix =
+ :{this.props.hsDomain} +
; + } return
@
-
:{this.props.hsDomain}
+ {suffix}
; case PasswordLogin.LOGIN_FIELD_PHONE: const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); From 29c2bd3d18beb22f619f297127ddf28fd3e1e9ab Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 21 Apr 2017 16:46:36 +0100 Subject: [PATCH 26/46] reset last_rr_sent on error Indicate that setting the RR was a failure and that hitting the API should be retried (in the case where the errcode !== "M_UNRECOGNISED") --- src/components/structures/TimelinePanel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index e8774cec62..872d30ac8c 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -546,6 +546,7 @@ var TimelinePanel = React.createClass({ }); } // it failed, so allow retries next time the user is active + this.last_rr_sent_event_id = undefined; this.last_rm_sent_event_id = undefined; }); From fdc26a490ad8b9525fbc0633d192472a4af431d4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 18:45:28 +0100 Subject: [PATCH 27/46] On return to RoomView from auxPanel, send focus back to Composer Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index b09b101b8a..9d5d50e9b1 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1181,6 +1181,7 @@ module.exports = React.createClass({ console.log("updateTint from onCancelClick"); this.updateTint(); this.setState({editingRoomSettings: false}); + dis.dispatch({action: 'focus_composer'}); }, onLeaveClick: function() { From 8e9f52e2172e8118ca5767c15729dbf08c16ce4a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Apr 2017 19:46:19 +0100 Subject: [PATCH 28/46] Disable Scalar Integrations if urls passed to it are falsey Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomSettings.js | 76 ++++++++++++---------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 2c7e1d7140..2c29dd433c 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -129,14 +129,17 @@ module.exports = React.createClass({ console.error("Failed to get room visibility: " + err); }); - this.scalarClient = new ScalarAuthClient(); - this.scalarClient.connect().done(() => { - this.forceUpdate(); - }, (err) => { - this.setState({ - scalar_error: err + this.scalarClient = null; + if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { + this.scalarClient = new ScalarAuthClient(); + this.scalarClient.connect().done(() => { + this.forceUpdate(); + }, (err) => { + this.setState({ + scalar_error: err + }); }); - }); + } dis.dispatch({ action: 'ui_opacity', @@ -490,7 +493,7 @@ module.exports = React.createClass({ ev.preventDefault(); var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); Modal.createDialog(IntegrationsManager, { - src: this.scalarClient.hasCredentials() ? + src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ? this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) : null, onFinished: ()=>{ @@ -765,36 +768,39 @@ module.exports = React.createClass({
; } - var integrationsButton; - var integrationsError; - if (this.state.showIntegrationsError && this.state.scalar_error) { - console.error(this.state.scalar_error); - integrationsError = ( - - Could not connect to the integration server - - ); - } + let integrationsButton; + let integrationsError; - if (this.scalarClient.hasCredentials()) { - integrationsButton = ( + if (this.scalarClient !== null) { + if (this.state.showIntegrationsError && this.state.scalar_error) { + console.error(this.state.scalar_error); + integrationsError = ( + + Could not connect to the integration server + + ); + } + + if (this.scalarClient.hasCredentials()) { + integrationsButton = (
- Manage Integrations -
- ); - } else if (this.state.scalar_error) { - integrationsButton = ( + Manage Integrations +
+ ); + } else if (this.state.scalar_error) { + integrationsButton = (
- Integrations Error - { integrationsError } -
- ); - } else { - integrationsButton = ( -
- Manage Integrations -
- ); + Integrations Error + { integrationsError } +
+ ); + } else { + integrationsButton = ( +
+ Manage Integrations +
+ ); + } } return ( From 2d39b5955616a74d148cb4d797fb8446d4c7b7ad Mon Sep 17 00:00:00 2001 From: turt2live Date: Fri, 21 Apr 2017 13:41:37 -0600 Subject: [PATCH 29/46] Change presence status labels to be more clear. As per vector-im/riot-web#3626 the current labels are unclear. Changing the verbage should make it more clear. Signed-off-by: Travis Ralston --- src/components/views/rooms/PresenceLabel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/PresenceLabel.js b/src/components/views/rooms/PresenceLabel.js index 2ece4c771e..52d831fcf6 100644 --- a/src/components/views/rooms/PresenceLabel.js +++ b/src/components/views/rooms/PresenceLabel.js @@ -75,7 +75,7 @@ module.exports = React.createClass({ render: function() { if (this.props.activeAgo >= 0) { - var ago = this.props.currentlyActive ? "now" : (this.getDuration(this.props.activeAgo) + " ago"); + var ago = this.props.currentlyActive ? "" : "for " + (this.getDuration(this.props.activeAgo)); // var ago = this.getDuration(this.props.activeAgo) + " ago"; // if (this.props.currentlyActive) ago += " (now?)"; return ( From e4c4adc5177fc9f33bcd39070ba79cab38cf3055 Mon Sep 17 00:00:00 2001 From: turt2live Date: Fri, 21 Apr 2017 14:28:28 -0600 Subject: [PATCH 30/46] Add option to hide other people's read receipts. Addresses vector-im/riot-web#2526 Signed-off-by: Travis Ralston --- src/components/structures/UserSettings.js | 4 ++++ src/components/views/rooms/EventTile.js | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 892865fdf9..b2ee29a1da 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -44,6 +44,10 @@ const SETTINGS_LABELS = [ id: 'autoplayGifsAndVideos', label: 'Autoplay GIFs and videos', }, + { + id: 'hideReadReceipts', + label: 'Hide read receipts' + }, /* { id: 'alwaysShowTimestamps', diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 9df0499eb2..e4234fc0bc 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -23,6 +23,7 @@ var Modal = require('../../../Modal'); var sdk = require('../../../index'); var TextForEvent = require('../../../TextForEvent'); import WithMatrixClient from '../../../wrappers/WithMatrixClient'; +import * as UserSettingsStore from "../../../UserSettingsStore"; var ContextualMenu = require('../../structures/ContextualMenu'); import dis from '../../../dispatcher'; @@ -284,6 +285,11 @@ module.exports = WithMatrixClient(React.createClass({ }, getReadAvatars: function() { + // return early if the user doesn't want any read receipts + if (UserSettingsStore.getSyncedSetting('hideReadReceipts', false)) { + return (); + } + const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); const avatars = []; const receiptOffset = 15; From 64e416e11745a9fc5513926f3738d0cac728ac1f Mon Sep 17 00:00:00 2001 From: turt2live Date: Fri, 21 Apr 2017 14:50:26 -0600 Subject: [PATCH 31/46] Add option to not send typing notifications Addresses vector-im/riot-web#3220 Fix applies to both the RTE and plain editor. Signed-off-by: Travis Ralston --- src/components/structures/UserSettings.js | 4 ++++ src/components/views/rooms/MessageComposerInput.js | 1 + src/components/views/rooms/MessageComposerInputOld.js | 2 ++ 3 files changed, 7 insertions(+) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 892865fdf9..619d8f32c8 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -44,6 +44,10 @@ const SETTINGS_LABELS = [ id: 'autoplayGifsAndVideos', label: 'Autoplay GIFs and videos', }, + { + id: 'dontSendTypingNotifications', + label: "Don't send typing notifications", + }, /* { id: 'alwaysShowTimestamps', diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 51c9ba881b..a7c20b02b5 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -355,6 +355,7 @@ export default class MessageComposerInput extends React.Component { } sendTyping(isTyping) { + if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return; MatrixClientPeg.get().sendTyping( this.props.room.roomId, this.isTyping, TYPING_SERVER_TIMEOUT diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index f0b650eb04..f5366c36ad 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -20,6 +20,7 @@ var SlashCommands = require("../../../SlashCommands"); var Modal = require("../../../Modal"); var MemberEntry = require("../../../TabCompleteEntries").MemberEntry; var sdk = require('../../../index'); +import UserSettingsStore from "../../../UserSettingsStore"; var dis = require("../../../dispatcher"); var KeyCode = require("../../../KeyCode"); @@ -420,6 +421,7 @@ export default React.createClass({ }, sendTyping: function(isTyping) { + if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return; MatrixClientPeg.get().sendTyping( this.props.room.roomId, this.isTyping, TYPING_SERVER_TIMEOUT From ec6a1c4c750f959017cdf823402a6c9d86b16fe2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 01:16:16 +0100 Subject: [PATCH 32/46] recalculate roomlist when your invites change --- src/components/views/rooms/RoomList.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 394de8876b..f36078e47d 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -265,9 +265,16 @@ module.exports = React.createClass({ }, onRoomStateMember: function(ev, state, member) { - constantTimeDispatcher.dispatch( - "RoomTile.refresh", member.roomId, {} - ); + if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId && + ev.getPrevContent() && ev.getPrevContent().membership === "invite") + { + this._delayedRefreshRoomList(); + } + else { + constantTimeDispatcher.dispatch( + "RoomTile.refresh", member.roomId, {} + ); + } }, onRoomMemberName: function(ev, member) { From 1faecfd0f7b5cd48bea0166cf73d273fd201d38f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 01:29:48 +0100 Subject: [PATCH 33/46] fix sticky headers on resize --- src/components/views/rooms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index f36078e47d..5372135f95 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -606,7 +606,7 @@ module.exports = React.createClass({ return ( + autoshow={true} onScroll={ self._whenScrolling } onResize={ self._whenScrolling } ref="gemscroll">
Date: Sat, 22 Apr 2017 04:57:27 +0100 Subject: [PATCH 34/46] Remember element that was in focus before rendering dialog restore focus to that element when we unmount also remove some whitespace because ESLint is a big bad bully... Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/BaseDialog.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 0b2ca5225d..d0f34c5fbd 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -47,6 +47,16 @@ export default React.createClass({ children: React.PropTypes.node, }, + componentWillMount: function() { + this.priorActiveElement = document.activeElement; + }, + + componentWillUnmount: function() { + if (this.priorActiveElement !== null) { + this.priorActiveElement.focus(); + } + }, + _onKeyDown: function(e) { if (e.keyCode === KeyCode.ESCAPE) { e.stopPropagation(); @@ -67,7 +77,7 @@ export default React.createClass({ render: function() { const TintableSvg = sdk.getComponent("elements.TintableSvg"); - + return (
Date: Sat, 22 Apr 2017 14:52:20 +0100 Subject: [PATCH 35/46] Specify cross platform regexes and add olm to noParse Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- karma.conf.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index 6d3047bb3b..3495a981be 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -135,17 +135,24 @@ module.exports = function (config) { }, ], noParse: [ + // for cross platform compatibility use [\\\/] as the path separator + // this ensures that the regex trips on both Windows and *nix + // don't parse the languages within highlight.js. They // cause stack overflows // (https://github.com/webpack/webpack/issues/1721), and // there is no need for webpack to parse them - they can // just be included as-is. - /highlight\.js\/lib\/languages/, + /highlight\.js[\\\/]lib[\\\/]languages/, + + // olm takes ages for webpack to process, and it's already heavily + // optimised, so there is little to gain by us uglifying it. + /olm[\\\/](javascript[\\\/])?olm\.js$/, // also disable parsing for sinon, because it // tries to do voodoo with 'require' which upsets // webpack (https://github.com/webpack/webpack/issues/304) - /sinon\/pkg\/sinon\.js$/, + /sinon[\\\/]pkg[\\\/]sinon\.js$/, ], }, resolve: { From 33e841a786b507a705cc54b735c29ec9427c2ae8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 22 Apr 2017 15:40:29 +0100 Subject: [PATCH 36/46] move user settings outward and use built in read receipts disabling Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 3 ++- src/components/views/rooms/EventTile.js | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9d5d50e9b1..ea8b6e2ae0 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -26,6 +26,7 @@ var q = require("q"); var classNames = require("classnames"); var Matrix = require("matrix-js-sdk"); +var UserSettingsStore = require('../../UserSettingsStore'); var MatrixClientPeg = require("../../MatrixClientPeg"); var ContentMessages = require("../../ContentMessages"); var Modal = require("../../Modal"); @@ -1727,7 +1728,7 @@ module.exports = React.createClass({ var messagePanel = (
, button: "Sign out", extraButtons: [ - From 0e5006b0415b4b42789661eace8aab26b2f0dc48 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 17:28:28 +0100 Subject: [PATCH 38/46] typo --- src/components/views/rooms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 5372135f95..3810f7d4d6 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -483,7 +483,7 @@ module.exports = React.createClass({ // Use the offset of the top of the scroll area from the window // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset; - // Use the offset of the top of the componet from the window + // Use the offset of the top of the component from the window // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height; From 34c1a8f3cf7965ac77819b41b61c0702bb28ede4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 17:28:48 +0100 Subject: [PATCH 39/46] make autofocus explicit on errordialog as it autoFocus attr seems unreliable --- src/components/views/dialogs/ErrorDialog.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js index 937595dfa8..ef6fdbbead 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.js @@ -50,6 +50,12 @@ export default React.createClass({ }; }, + componentDidMount: function() { + if (this.props.focus) { + this.refs.button.focus(); + } + }, + render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( @@ -59,7 +65,7 @@ export default React.createClass({ {this.props.description}
-
From 6a63c7e50c46ffbe16a4d1df500bac8565b03b90 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 22 Apr 2017 21:06:38 +0100 Subject: [PATCH 40/46] fix deep-linking to riot.im/app --- src/linkify-matrix.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index c8e20316a9..d9b0b78982 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -122,7 +122,7 @@ var escapeRegExp = function(string) { // anyone else really should be using matrix.to. matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:" + escapeRegExp(window.location.host + window.location.pathname) + "|" - + "(?:www\\.)?(?:riot|vector)\\.im/(?:beta|staging|develop)/" + + "(?:www\\.)?(?:riot|vector)\\.im/(?:app|beta|staging|develop)/" + ")(#.*)"; matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)"; From fa033e6116e65b5b1e40042eecf537cc2d7a70a0 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 23 Apr 2017 00:49:14 +0100 Subject: [PATCH 41/46] limit our keyboard shortcut modifiers correctly fixes https://github.com/vector-im/riot-web/issues/3614 --- src/components/structures/LoggedInView.js | 11 +++++++---- src/components/structures/ScrollPanel.js | 12 ++++++++---- src/components/structures/TimelinePanel.js | 4 +++- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index ef9d8d112a..318a5d7805 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -117,9 +117,10 @@ export default React.createClass({ } break; + case KeyCode.UP: case KeyCode.DOWN: - if (ev.altKey) { + if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) { var action = ev.keyCode == KeyCode.UP ? 'view_prev_room' : 'view_next_room'; dis.dispatch({action: action}); @@ -129,13 +130,15 @@ export default React.createClass({ case KeyCode.PAGE_UP: case KeyCode.PAGE_DOWN: - this._onScrollKeyPressed(ev); - handled = true; + if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { + this._onScrollKeyPressed(ev); + handled = true; + } break; case KeyCode.HOME: case KeyCode.END: - if (ev.ctrlKey) { + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { this._onScrollKeyPressed(ev); handled = true; } diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 83bec03e9e..d43e22e2f1 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -483,21 +483,25 @@ module.exports = React.createClass({ handleScrollKey: function(ev) { switch (ev.keyCode) { case KeyCode.PAGE_UP: - this.scrollRelative(-1); + if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { + this.scrollRelative(-1); + } break; case KeyCode.PAGE_DOWN: - this.scrollRelative(1); + if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { + this.scrollRelative(1); + } break; case KeyCode.HOME: - if (ev.ctrlKey) { + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { this.scrollToTop(); } break; case KeyCode.END: - if (ev.ctrlKey) { + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { this.scrollToBottom(); } break; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 7325cea2da..8babdaae4a 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -766,7 +766,9 @@ var TimelinePanel = React.createClass({ // jump to the live timeline on ctrl-end, rather than the end of the // timeline window. - if (ev.ctrlKey && ev.keyCode == KeyCode.END) { + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && + ev.keyCode == KeyCode.END) + { this.jumpToLiveTimeline(); } else { this.refs.messagePanel.handleScrollKey(ev); From 7854cac61d823f8c98e3a30caec5276e90a98823 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 23 Apr 2017 01:00:44 +0100 Subject: [PATCH 42/46] hook up keyb shortcuts for roomdir --- src/components/structures/LoggedInView.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 318a5d7805..4c012b42a8 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -156,6 +156,9 @@ export default React.createClass({ if (this.refs.roomView) { this.refs.roomView.handleScrollKey(ev); } + else if (this.refs.roomDirectory) { + this.refs.roomDirectory.handleScrollKey(ev); + } }, render: function() { @@ -216,6 +219,7 @@ export default React.createClass({ case PageTypes.RoomDirectory: page_element = ; From db996f678c9d0f31c30e44b221250431af89d89c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 23 Apr 2017 01:32:51 +0100 Subject: [PATCH 43/46] show better errors when slash commands fail --- src/components/views/rooms/MessageComposerInput.js | 2 +- src/components/views/rooms/MessageComposerInputOld.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index a7c20b02b5..417d003226 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -510,7 +510,7 @@ export default class MessageComposerInput extends React.Component { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Server error", - description: "Server unavailable, overloaded, or something else went wrong.", + description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."), }); }); } diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index f5366c36ad..378644478c 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -312,7 +312,7 @@ export default React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Server error", - description: "Server unavailable, overloaded, or something else went wrong.", + description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."), }); }); } From a2be764681240644ba5cbfaa7965adc308094dbf Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 23 Apr 2017 01:48:27 +0100 Subject: [PATCH 44/46] display err.message to user if available in error msgs --- src/CallHandler.js | 2 +- src/components/structures/MatrixChat.js | 2 +- src/components/structures/RoomView.js | 4 ++-- src/components/structures/UserSettings.js | 18 +++++++++--------- .../views/dialogs/ChatInviteDialog.js | 12 ++++++------ src/components/views/rooms/MemberInfo.js | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 42cc681d08..5199ef0a67 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -313,7 +313,7 @@ function _onAction(payload) { console.error("Conference call failed: " + err); Modal.createDialog(ErrorDialog, { title: "Failed to set up conference call", - description: "Conference call failed.", + description: "Conference call failed. " + ((err && err.message) ? err.message : ""), }); }); } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b449ff3094..9b8aa3426a 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -413,7 +413,7 @@ module.exports = React.createClass({ console.error("Failed to leave room " + payload.room_id + " " + err); Modal.createDialog(ErrorDialog, { title: "Failed to leave room", - description: "Server may be unavailable, overloaded, or you hit a bug." + description: (err && err.message ? err.message : "Server may be unavailable, overloaded, or you hit a bug."), }); }); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index ea8b6e2ae0..c158b87ff3 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -947,7 +947,7 @@ module.exports = React.createClass({ console.error("Failed to upload file " + file + " " + error); Modal.createDialog(ErrorDialog, { title: "Failed to upload file", - description: "Server may be unavailable, overloaded, or the file too big", + description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or the file too big"), }); }); }, @@ -1034,7 +1034,7 @@ module.exports = React.createClass({ console.error("Search failed: " + error); Modal.createDialog(ErrorDialog, { title: "Search failed", - description: "Server may be unavailable, overloaded, or search timed out :(" + description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or search timed out :("), }); }).finally(function() { self.setState({ diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 3e636c3eb1..ba5d5780b4 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -223,7 +223,7 @@ module.exports = React.createClass({ console.error("Failed to load user settings: " + error); Modal.createDialog(ErrorDialog, { title: "Can't load user settings", - description: "Server may be unavailable or overloaded", + description: ((error && error.message) ? error.message : "Server may be unavailable or overloaded"), }); }); }, @@ -264,8 +264,8 @@ module.exports = React.createClass({ console.error("Failed to set avatar: " + err); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to set avatar." + title: "Failed to set avatar", + description: ((err && err.message) ? err.message : "Operation failed"), }); }); }, @@ -366,8 +366,8 @@ module.exports = React.createClass({ this.setState({email_add_pending: false}); console.error("Unable to add email address " + email_address + " " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Unable to add email address" + title: "Unable to add email address", + description: ((err && err.message) ? err.message : "Operation failed"), }); }); ReactDOM.findDOMNode(this.refs.add_email_input).blur(); @@ -391,8 +391,8 @@ module.exports = React.createClass({ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Unable to remove contact information: " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Unable to remove contact information", + title: "Unable to remove contact information", + description: ((err && err.message) ? err.message : "Operation failed"), }); }).done(); } @@ -432,8 +432,8 @@ module.exports = React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Unable to verify email address: " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Unable to verify email address", + title: "Unable to verify email address", + description: ((err && err.message) ? err.message : "Operation failed"), }); } }); diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 16f756a773..7ba503099a 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -308,8 +308,8 @@ module.exports = React.createClass({ console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to invite", + title: "Failed to invite", + description: ((err && err.message) ? err.message : "Operation failed"), }); return null; }) @@ -321,8 +321,8 @@ module.exports = React.createClass({ console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to invite user", + title: "Failed to invite user", + description: ((err && err.message) ? err.message : "Operation failed"), }); return null; }) @@ -342,8 +342,8 @@ module.exports = React.createClass({ console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to invite", + title: "Failed to invite", + description: ((err && err.message) ? err.message : "Operation failed"), }); return null; }) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 1459ad3eb7..1a9a8d5e0f 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -241,8 +241,8 @@ module.exports = WithMatrixClient(React.createClass({ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Kick error: " + err); Modal.createDialog(ErrorDialog, { - title: "Error", - description: "Failed to kick user", + title: "Failed to kick", + description: ((err && err.message) ? err.message : "Operation failed"), }); } ).finally(()=>{ From 6f461f0ebbb084a73f545b8008b34d96c351312b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 24 Apr 2017 01:09:54 +0100 Subject: [PATCH 45/46] add in scrollto button --- src/components/views/rooms/TopUnreadMessagesBar.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/TopUnreadMessagesBar.js b/src/components/views/rooms/TopUnreadMessagesBar.js index 5bef8c0b0a..72b489a406 100644 --- a/src/components/views/rooms/TopUnreadMessagesBar.js +++ b/src/components/views/rooms/TopUnreadMessagesBar.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. @@ -32,7 +33,10 @@ module.exports = React.createClass({
- Jump to first unread message. Mark all read + Scroll to unread messages + Jump to first unread message.
Date: Mon, 24 Apr 2017 12:53:53 +0100 Subject: [PATCH 46/46] fix scroll behaviour on macs with no gemini --- src/components/views/rooms/RoomList.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3810f7d4d6..96ff65498f 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -456,11 +456,10 @@ module.exports = React.createClass({ var panel = ReactDOM.findDOMNode(this); if (!panel) return null; - if (panel.classList.contains('gm-prevented')) { - return panel; - } else { - return panel.children[2]; // XXX: Fragile! - } + // empirically, if we have gm-prevented for some reason, the scroll node + // is still the 3rd child (i.e. the view child). This looks to be due + // to vdh's improved resize updater logic...? + return panel.children[2]; // XXX: Fragile! }, _whenScrolling: function(e) { @@ -506,7 +505,7 @@ module.exports = React.createClass({ // Use the offset of the top of the scroll area from the window // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset; - // Use the offset of the top of the componet from the window + // Use the offset of the top of the component from the window // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;