From e747beac58620d65782f09e1c52d1f31939c99b4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 29 Jan 2016 11:19:27 +0000 Subject: [PATCH 01/15] Enable clicking on search results to switch to the result in context. --- src/components/structures/RoomView.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index c46149bfeb..a49287039d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -205,6 +205,8 @@ module.exports = React.createClass({ this.setState({ events: [], + searchResults: null, // we may have arrived here by clicking on a + // search result. Hide the results. timelineLoading: true, }); @@ -853,6 +855,14 @@ module.exports = React.createClass({ }); }, + _onSearchResultSelected: function(result) { + var event = result.context.getEvent(); + dis.dispatch({ + action: 'view_room', + room_id: event.getRoomId(), + event_id: event.getId(), + }); + }, getSearchResultTiles: function() { var EventTile = sdk.getComponent('rooms.EventTile'); @@ -916,7 +926,8 @@ module.exports = React.createClass({ ret.push(); + searchHighlights={this.state.searchHighlights} + onSelect={this._onSearchResultSelected.bind(this, result)}/>); } return ret; }, From d7576d223dc87155e2df29767444f7cf5fe6bbe4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 3 Feb 2016 15:34:20 +0000 Subject: [PATCH 02/15] Don't try to use local echoes as scroll tokens Local echoes don't have a (usable) event id, so don't use them when remembering the scroll state of a room. --- src/components/structures/RoomView.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index c46149bfeb..09b6baf2be 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -995,8 +995,18 @@ module.exports = React.createClass({ var eventId = mxEv.getId(); var highlight = (eventId == this.props.highlightedEventId); + + var scrollToken = eventId; + // we can't use local echoes as scroll tokens, because their event + // IDs change. + if (mxEv.status) { + scrollToken = undefined; + } + ret.push( -
  • +
  • From cebc2f5306fd20a9cde5fc9c66a5a4def71de65a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Feb 2016 16:23:57 +0000 Subject: [PATCH 03/15] Put the room preview bar back for rooms that aren't peekable (since we always tried to peek, it would fail which would reject the promise and cause loadingTimeline to stay true forever). --- src/components/structures/RoomView.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 6dbff018ef..4373569fd4 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -163,10 +163,7 @@ module.exports = React.createClass({ console.log("Attempting to peek into room %s", this.props.roomId); - roomProm = MatrixClientPeg.get().peekInRoom(this.props.roomId).catch((err) => { - console.error("Failed to peek into room: %s", err); - throw err; - }).then((room) => { + roomProm = MatrixClientPeg.get().peekInRoom(this.props.roomId).then((room) => { this.setState({ room: room }); @@ -180,6 +177,11 @@ module.exports = React.createClass({ roomProm.then((room) => { this._calculatePeekRules(room); return this._initTimeline(this.props); + }).catch(() => { + // This is fine: the room just isn't peekable (we assume). + this.setState({ + timelineLoading: false, + }); }).done(); }, From 164c9b90314c246cf393d2386839933d8c6b5e24 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Feb 2016 16:53:48 +0000 Subject: [PATCH 04/15] Check error to see if it's actually a failure to peek --- src/components/structures/RoomView.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4373569fd4..0d00b9afe5 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -177,11 +177,15 @@ module.exports = React.createClass({ roomProm.then((room) => { this._calculatePeekRules(room); return this._initTimeline(this.props); - }).catch(() => { - // This is fine: the room just isn't peekable (we assume). - this.setState({ - timelineLoading: false, - }); + }).catch((err) => { + if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") { + // This is fine: the room just isn't peekable (we assume). + this.setState({ + timelineLoading: false, + }); + } else { + throw err; + } }).done(); }, From 5c430395ea8cffc0bd4820dd580f412c55cba6ff Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 Feb 2016 10:50:21 +0000 Subject: [PATCH 05/15] init timeline when we get the room, as otherwise we never load it after joining --- 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 0d00b9afe5..fa183cd00e 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -420,6 +420,7 @@ module.exports = React.createClass({ this.setState({ room: room }); + this._initTimeline(this.props); } }, From c9a3ad31ab4e8565d964034ae593ea27aa0c6fa2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 Feb 2016 11:17:39 +0000 Subject: [PATCH 06/15] Comment error handling --- src/components/structures/RoomView.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index fa183cd00e..9df007e490 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -178,6 +178,9 @@ module.exports = React.createClass({ this._calculatePeekRules(room); return this._initTimeline(this.props); }).catch((err) => { + // This won't necessarily be a MatrixError, but we duck-type + // here and say if it's got an 'errcode' key with the right value, + // it means we can't peek. if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") { // This is fine: the room just isn't peekable (we assume). this.setState({ From 3c2c2b051b062ad027a0bb8446e18b90bbc7f188 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 Feb 2016 11:29:00 +0000 Subject: [PATCH 07/15] Listen for Room and use this to init the timeline, not Room.name --- src/components/structures/RoomView.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9df007e490..f77e2e226d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -122,6 +122,7 @@ module.exports = React.createClass({ componentWillMount: function() { this.last_rr_sent_event_id = undefined; 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); @@ -418,12 +419,20 @@ module.exports = React.createClass({ } }, + onRoom: function(room) { + if (room.roomId == this.props.roomId) { + this.setState({ + room: room + }); + this._initTimeline(this.props).done(); + } + }, + onRoomName: function(room) { if (room.roomId == this.props.roomId) { this.setState({ room: room }); - this._initTimeline(this.props); } }, From 47fef0896f552e14dca146c6e0c21da459cc9abb Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 Feb 2016 11:49:10 +0000 Subject: [PATCH 08/15] Prompt for display name before joining your first room (if you haven't set one). Fixes https://github.com/vector-im/vector-web/issues/758 --- src/component-index.js | 1 + src/components/structures/RoomView.js | 39 +++++++++- .../views/dialogs/SetDisplayNameDialog.js | 77 +++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/components/views/dialogs/SetDisplayNameDialog.js diff --git a/src/component-index.js b/src/component-index.js index 50803c045e..780bfa6bd7 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -41,6 +41,7 @@ module.exports.components['views.create_room.RoomAlias'] = require('./components module.exports.components['views.dialogs.ErrorDialog'] = require('./components/views/dialogs/ErrorDialog'); module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt'); module.exports.components['views.dialogs.QuestionDialog'] = require('./components/views/dialogs/QuestionDialog'); +module.exports.components['views.dialogs.SetDisplayNameDialog'] = require('./components/views/dialogs/SetDisplayNameDialog'); module.exports.components['views.dialogs.TextInputDialog'] = require('./components/views/dialogs/TextInputDialog'); module.exports.components['views.elements.EditableText'] = require('./components/views/elements/EditableText'); module.exports.components['views.elements.PowerSelector'] = require('./components/views/elements/PowerSelector'); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index f77e2e226d..8c97b49cce 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -698,7 +698,44 @@ module.exports = React.createClass({ onJoinButtonClicked: function(ev) { var self = this; - MatrixClientPeg.get().joinRoom(this.props.roomId).done(function() { + + var cli = MatrixClientPeg.get(); + var join_defer; + var join_promise; + // if this is the first room we're joining, checkthe user has a display name + // and if they don't, prompt them to set one. + // NB. This unfortunately does not re-use the ChangeDisplayName component because + // it doesn't behave quite as desired here (we want an input field here rather than + // content-editable, and we want a default). + if (MatrixClientPeg.get().getRooms().length == 0) { + join_promise = cli.getProfileInfo(cli.credentials.userId).then((result) => { + if (!result.displayname) { + var SetDisplayNameDialog = sdk.getComponent('views.dialogs.SetDisplayNameDialog'); + var dialog_defer = q.defer(); + var dialog_ref; + var modal; + var dialog_instance = { + dialog_ref = r; + }} onFinished={() => { + var new_displayname = dialog_ref.getValue() || dialog_ref.getDefaultValue(); + cli.setDisplayName(new_displayname).done(() => { + dialog_defer.resolve(); + }); + modal.close(); + }} /> + modal = Modal.createDialogWithElement(dialog_instance); + return dialog_defer.promise; + } + }); + } else { + join_defer = q.defer(); + join_promise = join_defer.promise; + join_defer.resolve(); + } + + join_promise.then(() => { + return MatrixClientPeg.get().joinRoom(this.props.roomId) + }).done(function() { // It is possible that there is no Room yet if state hasn't come down // from /sync - joinRoom will resolve when the HTTP request to join succeeds, // NOT when it comes down /sync. If there is no room, we'll keep the diff --git a/src/components/views/dialogs/SetDisplayNameDialog.js b/src/components/views/dialogs/SetDisplayNameDialog.js new file mode 100644 index 0000000000..43d5908327 --- /dev/null +++ b/src/components/views/dialogs/SetDisplayNameDialog.js @@ -0,0 +1,77 @@ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +var React = require("react"); +var sdk = require("../../../index.js"); +var MatrixClientPeg = require("../../../MatrixClientPeg"); + +module.exports = React.createClass({ + displayName: 'SetDisplayNameDialog', + propTypes: { + onFinished: React.PropTypes.func.isRequired, + currentDisplayName: React.PropTypes.string, + }, + + getInitialState: function() { + return { + value: this.props.currentDisplayName || this.getDefaultDisplayName(), + } + }, + + getDefaultDisplayName: function() { + return "Guest "+MatrixClientPeg.get().getUserIdLocalpart(); + }, + + getValue: function() { + return this.state.value; + }, + + onValueChange: function(ev) { + this.setState({ + value: ev.target.value + }); + }, + + onFormSubmit: function(ev) { + ev.preventDefault(); + this.props.onFinished(); + return false; + }, + + render: function() { + return ( +
    +
    + Set a Display Name +
    +
    + Your display name is how you'll appear to others when you speak in rooms. What would you like it to be? +
    +
    +
    + +
    +
    + +
    +
    +
    + ); + } +}); From 574560cc05db46b3dc08417411585c131c24a507 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 Feb 2016 13:06:59 +0000 Subject: [PATCH 09/15] remove listener on unmount --- 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 f77e2e226d..161818f8f1 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -274,6 +274,7 @@ module.exports = React.createClass({ } dis.unregister(this.dispatcherRef); 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); From 891f4761a0b7d9f534548a8a8e52dc7f763ff486 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 Feb 2016 13:21:42 +0000 Subject: [PATCH 10/15] Add comments and only set a room / init the timeline if we don't already have a room --- src/components/structures/RoomView.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 161818f8f1..4553e9053e 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -421,7 +421,12 @@ module.exports = React.createClass({ }, onRoom: function(room) { - if (room.roomId == this.props.roomId) { + // This event is fired when the room is 'stored' by the JS SDK, which + // means it's now a fully-fledged room object ready to be used, so + // set it in our state and start using it (ie. init the timeline) + // This will happen if we start off viewing a room we're not joined, + // then join it whilst RoomView is looking at that room. + if (room.roomId == this.props.roomId && !this.state.room) { this.setState({ room: room }); @@ -705,6 +710,10 @@ module.exports = React.createClass({ // NOT when it comes down /sync. If there is no room, we'll keep the // joining flag set until we see it. Likewise, if our state is not // "join" we'll keep this flag set until it comes down /sync. + + // We'll need to initialise the timeline when joining, but due to + // the above, we can't do it here: we do it in onRoom instead, + // once we have a useable room object. var room = MatrixClientPeg.get().getRoom(self.props.roomId); var me = MatrixClientPeg.get().credentials.userId; self.setState({ From b0da54533d040eeef3366854d8815a9d63067b24 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 Feb 2016 13:25:45 +0000 Subject: [PATCH 11/15] Don't do this check - it's not valid since we set the room in onRoomName --- src/components/structures/RoomView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4553e9053e..ca564d3e11 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -426,7 +426,7 @@ module.exports = React.createClass({ // set it in our state and start using it (ie. init the timeline) // This will happen if we start off viewing a room we're not joined, // then join it whilst RoomView is looking at that room. - if (room.roomId == this.props.roomId && !this.state.room) { + if (room.roomId == this.props.roomId) { this.setState({ room: room }); From f3fa5d6a2a12d98e9d758b4ddee944e4310abd4e Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 4 Feb 2016 13:30:25 +0000 Subject: [PATCH 12/15] Set the sync limit back to sane levels --- src/components/structures/MatrixChat.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index a46efc8025..3fbcf80805 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -586,8 +586,7 @@ module.exports = React.createClass({ Presence.start(); cli.startClient({ pendingEventOrdering: "end", - // deliberately huge limit for now to avoid hitting gappy /sync's until gappy /sync performance improves - initialSyncLimit: 250, + initialSyncLimit: 20, }); }, From 8f5c73988649d8251dba5cde3c8d8150f1b10b4c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 4 Feb 2016 14:05:16 +0000 Subject: [PATCH 13/15] Clean up scrollToken assignment --- src/components/structures/RoomView.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 09b6baf2be..6e9835834a 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -996,12 +996,9 @@ module.exports = React.createClass({ var eventId = mxEv.getId(); var highlight = (eventId == this.props.highlightedEventId); - var scrollToken = eventId; - // we can't use local echoes as scroll tokens, because their event - // IDs change. - if (mxEv.status) { - scrollToken = undefined; - } + // we can't use local echoes as scroll tokens, because their event IDs change. + // Local echos have a send "status". + var scrollToken = mxEv.status ? undefined : eventId; ret.push(
  • Date: Thu, 4 Feb 2016 14:38:05 +0000 Subject: [PATCH 14/15] typo --- src/components/structures/RoomView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 8c97b49cce..644927e3bd 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -702,7 +702,7 @@ module.exports = React.createClass({ var cli = MatrixClientPeg.get(); var join_defer; var join_promise; - // if this is the first room we're joining, checkthe user has a display name + // if this is the first room we're joining, check the user has a display name // and if they don't, prompt them to set one. // NB. This unfortunately does not re-use the ChangeDisplayName component because // it doesn't behave quite as desired here (we want an input field here rather than From 167da10b8bf4ae1366c519c01625aa262d7cb92f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 Feb 2016 15:07:30 +0000 Subject: [PATCH 15/15] address PR comments --- src/components/structures/RoomView.js | 14 ++++---------- .../views/dialogs/SetDisplayNameDialog.js | 6 +----- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 644927e3bd..836ea1ea2f 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -700,15 +700,14 @@ module.exports = React.createClass({ var self = this; var cli = MatrixClientPeg.get(); - var join_defer; - var join_promise; + var display_name_promise = q(); // if this is the first room we're joining, check the user has a display name // and if they don't, prompt them to set one. // NB. This unfortunately does not re-use the ChangeDisplayName component because // it doesn't behave quite as desired here (we want an input field here rather than // content-editable, and we want a default). if (MatrixClientPeg.get().getRooms().length == 0) { - join_promise = cli.getProfileInfo(cli.credentials.userId).then((result) => { + display_name_promise = cli.getProfileInfo(cli.credentials.userId).then((result) => { if (!result.displayname) { var SetDisplayNameDialog = sdk.getComponent('views.dialogs.SetDisplayNameDialog'); var dialog_defer = q.defer(); @@ -717,8 +716,7 @@ module.exports = React.createClass({ var dialog_instance = { dialog_ref = r; }} onFinished={() => { - var new_displayname = dialog_ref.getValue() || dialog_ref.getDefaultValue(); - cli.setDisplayName(new_displayname).done(() => { + cli.setDisplayName(dialog_ref.getValue()).done(() => { dialog_defer.resolve(); }); modal.close(); @@ -727,13 +725,9 @@ module.exports = React.createClass({ return dialog_defer.promise; } }); - } else { - join_defer = q.defer(); - join_promise = join_defer.promise; - join_defer.resolve(); } - join_promise.then(() => { + display_name_promise.then(() => { return MatrixClientPeg.get().joinRoom(this.props.roomId) }).done(function() { // It is possible that there is no Room yet if state hasn't come down diff --git a/src/components/views/dialogs/SetDisplayNameDialog.js b/src/components/views/dialogs/SetDisplayNameDialog.js index 43d5908327..d1287e2570 100644 --- a/src/components/views/dialogs/SetDisplayNameDialog.js +++ b/src/components/views/dialogs/SetDisplayNameDialog.js @@ -27,14 +27,10 @@ module.exports = React.createClass({ getInitialState: function() { return { - value: this.props.currentDisplayName || this.getDefaultDisplayName(), + value: this.props.currentDisplayName || "Guest "+MatrixClientPeg.get().getUserIdLocalpart(), } }, - getDefaultDisplayName: function() { - return "Guest "+MatrixClientPeg.get().getUserIdLocalpart(); - }, - getValue: function() { return this.state.value; },