From 30b4b91cf3a916e3ee455ad55f2f9d5a2352d532 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 9 Feb 2016 14:42:32 +0000 Subject: [PATCH 01/33] Show jump-to-bottom icon even when there is other stuff in the status bar Also includes a general simplification of the classes being used in the status bar. This should fix vector-im/vector-web#879. Obviously it needs corresponding changes in the CSS in vector-web. --- src/components/structures/RoomStatusBar.js | 123 ++++++++++++++------- 1 file changed, 83 insertions(+), 40 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 9ff3925b10..7a5f2ffdb2 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -76,7 +76,50 @@ module.exports = React.createClass({ }); }, - render: function() { + // return suitable content for the image on the left of the status bar. + // + // if wantPlaceholder is true, we include a "..." placeholder if + // there is nothing better to put in. + _getIndicator: function(wantPlaceholder) { + if (this.props.numUnreadMessages) { + return ( +
+ +
+ ); + } + + if (!this.props.atEndOfLiveTimeline) { + return ( +
+ Scroll to bottom of page +
+ ); + } + + if (this.props.hasActiveCall) { + return ( + + ); + } + + if (wantPlaceholder) { + return ( +
...
+ ); + } + + return null; + }, + + + // return suitable content for the main (text) part of the status bar. + _getContent: function() { var TabCompleteBar = sdk.getComponent('rooms.TabCompleteBar'); var TintableSvg = sdk.getComponent("elements.TintableSvg"); @@ -86,15 +129,13 @@ module.exports = React.createClass({ // a connection! if (this.state.syncState === "ERROR") { return ( -
+
/!\ -
-
- Connectivity to the server has been lost. -
-
- Sent messages will be stored until your connection has returned. -
+
+ Connectivity to the server has been lost. +
+
+ Sent messages will be stored until your connection has returned.
); @@ -102,11 +143,10 @@ module.exports = React.createClass({ if (this.props.tabCompleteEntries) { return ( -
-
...
-
+
+
-
+
Auto-complete
@@ -117,18 +157,16 @@ module.exports = React.createClass({ if (this.props.hasUnsentMessages) { return ( -
+
/!\ -
-
- Some of your messages have not been sent. -
-
- - Resend all now - or select individual messages to re-send. -
+
+ Some of your messages have not been sent. +
+
+ + Resend all now + or select individual messages to re-send.
); @@ -141,8 +179,8 @@ module.exports = React.createClass({ (this.props.numUnreadMessages > 1 ? "s" : ""); return ( -
- +
{unreadMsgs}
); @@ -151,30 +189,35 @@ module.exports = React.createClass({ var typingString = WhoIsTyping.whoIsTypingString(this.props.room); if (typingString) { return ( -
-
...
- {typingString} +
+ {typingString}
); } - if (!this.props.atEndOfLiveTimeline) { - return ( -
- Scroll to bottom of page -
- ); - } - if (this.props.hasActiveCall) { return ( -
- +
Active call
); } - return
; + return null; }, + + + render: function() { + var content = this._getContent(); + var indicator = this._getIndicator(content !== null); + + return ( +
+
+ {indicator} +
+ {content} +
+ ); + }, }); From e17d77778f0aa9e8e0a4cc5781654cb42145b32d Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Feb 2016 19:37:03 +0200 Subject: [PATCH 02/33] sanitize setting displayname prompt --- .../views/dialogs/SetDisplayNameDialog.js | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/views/dialogs/SetDisplayNameDialog.js b/src/components/views/dialogs/SetDisplayNameDialog.js index d1287e2570..624bb50a46 100644 --- a/src/components/views/dialogs/SetDisplayNameDialog.js +++ b/src/components/views/dialogs/SetDisplayNameDialog.js @@ -26,9 +26,20 @@ module.exports = React.createClass({ }, getInitialState: function() { - return { - value: this.props.currentDisplayName || "Guest "+MatrixClientPeg.get().getUserIdLocalpart(), + if (this.props.currentDisplayName) { + return { value: this.props.currentDisplayName }; } + + if (MatrixClientPeg.get().isGuest()) { + return { value : "Guest " + MatrixClientPeg.get().getUserIdLocalpart() }; + } + else { + return { value : MatrixClientPeg.get().getUserIdLocalpart() }; + } + }, + + componentDidMount: function() { + this.refs.input_value.select(); }, getValue: function() { @@ -54,11 +65,12 @@ module.exports = React.createClass({ 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? + Your display name is how you'll appear to others when you speak in rooms.
+ What would you like it to be?
- From 30e9c7608945aefeebf1ca42b69411978dd4295c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Feb 2016 20:44:13 +0200 Subject: [PATCH 03/33] login as guest button on the login page --- src/components/structures/MatrixChat.js | 6 ++++-- src/components/structures/login/Login.js | 11 ++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 5869c8ef33..0a9231247c 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -175,7 +175,7 @@ module.exports = React.createClass({ guest: true }); }, function(err) { - console.error(err.data); + console.error("Failed to register as guest: " + err + " " + err.data); self._setAutoRegisterAsGuest(false); }); }, @@ -970,7 +970,9 @@ module.exports = React.createClass({ onRegisterClick={this.onRegisterClick} homeserverUrl={this.props.config.default_hs_url} identityServerUrl={this.props.config.default_is_url} - onForgotPasswordClick={this.onForgotPasswordClick} /> + onForgotPasswordClick={this.onForgotPasswordClick} + onLoginAsGuestClick={this.props.enableGuest && this.props.config && this.props.config.default_hs_url ? this._registerAsGuest: undefined} + /> ); } } diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index b853b8fd95..356439b0cc 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -35,7 +35,8 @@ module.exports = React.createClass({displayName: 'Login', // login shouldn't know or care how registration is done. onRegisterClick: React.PropTypes.func.isRequired, // login shouldn't care how password recovery is done. - onForgotPasswordClick: React.PropTypes.func + onForgotPasswordClick: React.PropTypes.func, + onLoginAsGuestClick: React.PropTypes.func, }, getDefaultProps: function() { @@ -167,6 +168,13 @@ module.exports = React.createClass({displayName: 'Login', var LoginFooter = sdk.getComponent("login.LoginFooter"); var loader = this.state.busy ?
: null; + var loginAsGuestJsx; + if (this.props.onLoginAsGuestClick) { + loginAsGuestJsx = + + Login as guest + + } return (
@@ -188,6 +196,7 @@ module.exports = React.createClass({displayName: 'Login', Create a new account + { loginAsGuestJsx }
From b1a6575b1dc172e209ab21cdbf5c1b19d03c63a6 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Feb 2016 20:59:44 +0200 Subject: [PATCH 04/33] remove ugly join & reject error msgs in favour of modal dialogs --- src/components/structures/RoomView.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index d68ae35dc8..842f59700b 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1229,11 +1229,19 @@ module.exports = React.createClass({ self.setState({ rejecting: false }); - }, function(err) { - console.error("Failed to reject invite: %s", err); + }, function(error) { + console.error("Failed to reject invite: %s", error); + + var msg = error.message ? error.message : JSON.stringify(error); + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Failed to reject invite", + description: msg + }); + self.setState({ rejecting: false, - rejectError: err + rejectError: error }); }); }, @@ -1427,7 +1435,6 @@ module.exports = React.createClass({ ); } else { - var joinErrorText = this.state.joinError ? "Failed to join room!" : ""; return (
@@ -1436,7 +1443,6 @@ module.exports = React.createClass({ canJoin={ true } canPreview={ false } spinner={this.state.joining} /> -
{joinErrorText}
@@ -1462,10 +1468,6 @@ module.exports = React.createClass({ } else { var inviteEvent = myMember.events.member; var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender(); - // XXX: Leaving this intentionally basic for now because invites are about to change totally - // FIXME: This comment is now outdated - what do we need to fix? ^ - var joinErrorText = this.state.joinError ? "Failed to join room!" : ""; - var rejectErrorText = this.state.rejectError ? "Failed to reject invite!" : ""; // We deliberately don't try to peek into invites, even if we have permission to peek // as they could be a spam vector. @@ -1481,8 +1483,6 @@ module.exports = React.createClass({ canJoin={ true } canPreview={ false } spinner={this.state.joining} /> -
{joinErrorText}
-
{rejectErrorText}
From 014acbab1f6bf3553b80ad140924399bcf7b696d Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Feb 2016 21:16:04 +0200 Subject: [PATCH 05/33] restore drag & drop file upload, broken by @richvdh's new timeline stuff --- src/components/structures/RoomView.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 842f59700b..25c289ba96 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -552,14 +552,6 @@ module.exports = React.createClass({ window.addEventListener('resize', this.onResize); this.onResize(); - if (this.refs.roomView) { - var roomView = ReactDOM.findDOMNode(this.refs.roomView); - roomView.addEventListener('drop', this.onDrop); - roomView.addEventListener('dragover', this.onDragOver); - roomView.addEventListener('dragleave', this.onDragLeaveOrEnd); - roomView.addEventListener('dragend', this.onDragLeaveOrEnd); - } - this._updateTabCompleteList(); // XXX: EVIL HACK to autofocus inviting on empty rooms. @@ -597,6 +589,16 @@ module.exports = React.createClass({ // separate component to avoid this ridiculous dance. if (!this.refs.messagePanel) return; + if (this.refs.roomView) { + var roomView = ReactDOM.findDOMNode(this.refs.roomView); + if (!roomView.ondrop) { + roomView.addEventListener('drop', this.onDrop); + roomView.addEventListener('dragover', this.onDragOver); + roomView.addEventListener('dragleave', this.onDragLeaveOrEnd); + roomView.addEventListener('dragend', this.onDragLeaveOrEnd); + } + } + if (!this.refs.messagePanel.initialised) { this._initialiseMessagePanel(); } From af5a866596e71359a810d5d9bea85500f504ca31 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Feb 2016 21:29:56 +0200 Subject: [PATCH 06/33] clear upload bar correctly after upload completes by fixing a race and moving the upload_finished dispatch after clearing up the inprogress uploads data structure. I have zero idea how this ever worked... :/ --- src/ContentMessages.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 82c295756b..bbd714fa57 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -92,6 +92,7 @@ class ContentMessages { this.inprogress.push(upload); dis.dispatch({action: 'upload_started'}); + var error; var self = this; return def.promise.then(function() { upload.promise = matrixClient.uploadContent(file); @@ -103,11 +104,10 @@ class ContentMessages { dis.dispatch({action: 'upload_progress', upload: upload}); } }).then(function(url) { - dis.dispatch({action: 'upload_finished', upload: upload}); content.url = url; return matrixClient.sendMessage(roomId, content); }, function(err) { - dis.dispatch({action: 'upload_failed', upload: upload}); + error = err; if (!upload.canceled) { var desc = "The file '"+upload.fileName+"' failed to upload."; if (err.http_status == 413) { @@ -128,6 +128,12 @@ class ContentMessages { break; } } + if (error) { + dis.dispatch({action: 'upload_failed', upload: upload}); + } + else { + dis.dispatch({action: 'upload_finished', upload: upload}); + } }); } From 0d153df417a7c43c63a7696a11b4f6951f751841 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Feb 2016 21:58:37 +0200 Subject: [PATCH 07/33] improve registration fail error msg slightly --- src/components/structures/login/Registration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 5666318368..64df73962c 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -126,7 +126,7 @@ module.exports = React.createClass({ if (!response || !response.user_id || !response.access_token) { console.error("Final response is missing keys."); self.setState({ - errorText: "There was a problem processing the response." + errorText: "Registration failed on server" }); return; } From dfbc88d421f5c6f7d83432eade282bd74b58bb9f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Feb 2016 22:01:05 +0200 Subject: [PATCH 08/33] fix keyboard shortcuts on logout prompt --- src/components/views/dialogs/LogoutPrompt.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/LogoutPrompt.js b/src/components/views/dialogs/LogoutPrompt.js index 824924e999..06d3d4dec1 100644 --- a/src/components/views/dialogs/LogoutPrompt.js +++ b/src/components/views/dialogs/LogoutPrompt.js @@ -31,14 +31,22 @@ module.exports = React.createClass({ } }, + onKeyDown: function(e) { + if (e.keyCode === 27) { // escape + e.stopPropagation(); + e.preventDefault(); + this.cancelPrompt(); + } + }, + render: function() { return (
Sign out?
-
- +
+
From 576de32ce4b1ff07356298035d96d4a1f9650975 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Feb 2016 22:01:22 +0200 Subject: [PATCH 09/33] show vaguely accurate default avatar --- src/components/views/settings/ChangeAvatar.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 89303856b2..9b03aba1a3 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -110,19 +110,17 @@ module.exports = React.createClass({ }, render: function() { - var RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); var avatarImg; // Having just set an avatar we just display that since it will take a little // time to propagate through to the RoomAvatar. if (this.props.room && !this.avatarSet) { + var RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); avatarImg = ; } else { - var style = { - width: this.props.width, - height: this.props.height, - objectFit: 'cover', - }; - avatarImg = ; + var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + // XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ? + avatarImg = } var uploadSection; From 6b48b626e61e9aa0ec4669aae224014ef93b111a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Feb 2016 20:39:02 +0000 Subject: [PATCH 10/33] fix spinner of doom --- src/components/structures/login/Registration.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 64df73962c..9ec379a814 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -115,6 +115,9 @@ module.exports = React.createClass({ onProcessingRegistration: function(promise) { var self = this; promise.done(function(response) { + self.setState({ + busy: false + }); if (!response || !response.access_token) { console.warn( "FIXME: Register fulfilled without a final response, " + @@ -136,9 +139,6 @@ module.exports = React.createClass({ identityServerUrl: self.registerLogic.getIdentityServerUrl(), accessToken: response.access_token }); - self.setState({ - busy: false - }); }, function(err) { if (err.message) { self.setState({ From 61018f4f38bce689db01bf9a2528ea09523f8654 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Feb 2016 20:42:44 +0000 Subject: [PATCH 11/33] whitespace --- src/components/views/dialogs/LogoutPrompt.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/LogoutPrompt.js b/src/components/views/dialogs/LogoutPrompt.js index 06d3d4dec1..67fedfe840 100644 --- a/src/components/views/dialogs/LogoutPrompt.js +++ b/src/components/views/dialogs/LogoutPrompt.js @@ -45,7 +45,7 @@ module.exports = React.createClass({
Sign out?
-
+
From ca56b7ec2d39fdec96a582a2ce95278a6f495221 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Feb 2016 20:43:43 +0000 Subject: [PATCH 12/33] match partial names in memberlist --- src/components/views/rooms/MemberList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 4e8f38b035..eba09ca9da 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -327,7 +327,7 @@ module.exports = React.createClass({ var memberList = self.state.members.filter(function(userId) { var m = self.memberDict[userId]; - if (query && m.name.toLowerCase().indexOf(query) !== 0) { + if (query && m.name.toLowerCase().indexOf(query) === -1) { return false; } return m.membership == membership; From 4b8b2ade8b8a322fdb70ca26bb6a8326c1ec8df4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Feb 2016 21:50:39 +0000 Subject: [PATCH 13/33] fix login-on-guest-bar-NPE crash https://github.com/vector-im/vector-web/issues/930 --- src/components/structures/RoomStatusBar.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 9ff3925b10..a4ac219b95 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -64,7 +64,10 @@ module.exports = React.createClass({ }, componentWillUnmount: function() { - MatrixClientPeg.get().removeListener("sync", this.onSyncStateChange); + // we may have entirely lost our client as we're logging out before clicking login on the guest bar... + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("sync", this.onSyncStateChange); + } }, onSyncStateChange: function(state, prevState) { From 687eae7f438826e53479d9f50fb22376a1298acb Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Feb 2016 22:07:08 +0000 Subject: [PATCH 14/33] stop floods of notifs when doing a logout+login --- src/Notifier.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Notifier.js b/src/Notifier.js index e52fd252fe..b64a001a5f 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -182,6 +182,9 @@ var Notifier = { if (state === "PREPARED" || state === "SYNCING") { this.isPrepared = true; } + else if (state === "STOPPED" || state === "ERROR") { + this.isPrepared = false; + } }, onRoomTimeline: function(ev, room, toStartOfTimeline) { From eb91faf55401754c4b449b53e2e2c80e97495fb9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 16 Feb 2016 16:05:27 +0000 Subject: [PATCH 15/33] Handle redacted events matrix-js-sdk now retains redacted events. Filter them out of the timeline. Also put empty placeholders in the dom so that if we try to scroll to a redacted event, we don't end up blowing up. --- src/components/structures/RoomView.js | 38 +++++++++++++++++++------ src/components/views/rooms/EventTile.js | 1 + 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 25c289ba96..db18fe92c5 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -114,6 +114,7 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room", this.onRoom); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); + MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction); MatrixClientPeg.get().on("Room.name", this.onRoomName); MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); @@ -259,6 +260,7 @@ module.exports = React.createClass({ if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room", this.onRoom); MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); + MatrixClientPeg.get().removeListener("Room.redaction", this.onRoomRedaction); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); @@ -377,6 +379,17 @@ module.exports = React.createClass({ } }, + onRoomRedaction: function(ev, room) { + if (this.unmounted) return; + + // ignore events for other rooms + if (room.roomId != this.props.roomId) return; + + // we could skip an update if the event isn't in our timeline, + // but that's probably an early optimisation. + this.forceUpdate(); + }, + _calculatePeekRules: function(room) { var guestAccessEvent = room.currentState.getStateEvents("m.room.guest_access", ""); if (guestAccessEvent && guestAccessEvent.getContent().guest_access === "can_join") { @@ -969,17 +982,31 @@ module.exports = React.createClass({ var readMarkerIndex; for (var i = 0; i < this.state.events.length; i++) { var mxEv = this.state.events[i]; + var eventId = mxEv.getId(); + + // 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; + + var wantTile = true; if (!EventTile.haveTileForEvent(mxEv)) { - continue; + wantTile = false; } - if (this.props.ConferenceHandler && mxEv.getType() === "m.room.member") { + else if (this.props.ConferenceHandler && mxEv.getType() === "m.room.member") { if (this.props.ConferenceHandler.isConferenceUser(mxEv.getSender()) || this.props.ConferenceHandler.isConferenceUser(mxEv.getStateKey())) { - continue; // suppress conf user join/parts + wantTile = false; // suppress conf user join/parts } } + if (!wantTile) { + // if we aren't showing the event, put in a dummy scroll token anyway, so + // that we can scroll to the right place. + ret.push(
  • ); + continue; + } + // now we've decided whether or not to show this message, // add the read up to marker if appropriate // doing this here means we implicitly do not show the marker @@ -1028,13 +1055,8 @@ module.exports = React.createClass({ last = true; } - var eventId = mxEv.getId(); var highlight = (eventId == this.props.highlightedEventId); - // 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: Tue, 16 Feb 2016 17:39:32 +0000 Subject: [PATCH 16/33] disable scroll-to-token entirely temporarily - https://github.com/vector-im/vector-web/issues/946 --- src/components/structures/ScrollPanel.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 514937f877..fd8befea01 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -418,7 +418,9 @@ module.exports = React.createClass({ var scrollState = this.scrollState; var scrollNode = this._getScrollNode(); - if (scrollState.stuckAtBottom) { + // XXX: DISABLE SCROLL TO TOKEN ENTIRELY TEMPORARILY AS IT'S SCREWING + // UP MY DEMO - see https://github.com/vector-im/vector-web/issues/946 + if (true || scrollState.stuckAtBottom) { scrollNode.scrollTop = scrollNode.scrollHeight; debuglog("Scrolled to bottom; offset now", scrollNode.scrollTop); } else if (scrollState.trackedScrollToken) { From 38a2a61b38b804bdb59758bf256b53c533001dc7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 16 Feb 2016 19:39:22 +0000 Subject: [PATCH 17/33] back out hacky previous commit as #946 only happens when gemini is disabled --- src/components/structures/ScrollPanel.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index fd8befea01..514937f877 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -418,9 +418,7 @@ module.exports = React.createClass({ var scrollState = this.scrollState; var scrollNode = this._getScrollNode(); - // XXX: DISABLE SCROLL TO TOKEN ENTIRELY TEMPORARILY AS IT'S SCREWING - // UP MY DEMO - see https://github.com/vector-im/vector-web/issues/946 - if (true || scrollState.stuckAtBottom) { + if (scrollState.stuckAtBottom) { scrollNode.scrollTop = scrollNode.scrollHeight; debuglog("Scrolled to bottom; offset now", scrollNode.scrollTop); } else if (scrollState.trackedScrollToken) { From e3feae32e1ba8fdcfc48fa979006d7b7bbf9e73e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 17 Feb 2016 19:50:04 +0000 Subject: [PATCH 18/33] Fix search clickthrough for HTML events Switch to using a normal link for search result clickthrough. Apart from generally giving a better experience, this means that it also works on html messages. The problem there was that we were attaching onClick handlers to s which we were then flattening into HTML with ReactDOMServer (which meant the onClick handlers were never attached to React's list of listeners). To make this work without jumping through React hoops, the highlighter now returns either a list of strings or a list of nodes, depending on whether we are dealing with an HTML event or a text one. We therefore have a separate HtmlHighlighter and TextHighlighter. --- src/HtmlUtils.js | 93 +++++++++++++------ src/components/structures/RoomView.js | 13 +-- src/components/views/messages/MessageEvent.js | 14 ++- src/components/views/messages/TextualBody.js | 16 +++- src/components/views/rooms/EventTile.js | 8 +- .../views/rooms/SearchResultTile.js | 6 +- 6 files changed, 102 insertions(+), 48 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 0b7f17b2b2..fe97d7b84f 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -17,7 +17,6 @@ limitations under the License. 'use strict'; var React = require('react'); -var ReactDOMServer = require('react-dom/server') var sanitizeHtml = require('sanitize-html'); var highlight = require('highlight.js'); @@ -50,14 +49,23 @@ var sanitizeHtmlParams = { }, }; -class Highlighter { - constructor(html, highlightClass, onHighlightClick) { - this.html = html; +class BaseHighlighter { + constructor(highlightClass, highlightLink) { this.highlightClass = highlightClass; - this.onHighlightClick = onHighlightClick; - this._key = 0; + this.highlightLink = highlightLink; } + /** + * apply the highlights to a section of text + * + * @param {string} safeSnippet The snippet of text to apply the highlights + * to. + * @param {string[]} safeHighlights A list of substrings to highlight, + * sorted by descending length. + * + * returns a list of results (strings for HtmlHighligher, react nodes for + * TextHighlighter). + */ applyHighlights(safeSnippet, safeHighlights) { var lastOffset = 0; var offset; @@ -71,10 +79,12 @@ class Highlighter { nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights)); } - // do highlight - nodes.push(this._createSpan(safeHighlight, true)); + // do highlight. use the original string rather than safeHighlight + // to preserve the original casing. + var endOffset = offset + safeHighlight.length; + nodes.push(this._processSnippet(safeSnippet.substring(offset, endOffset), true)); - lastOffset = offset + safeHighlight.length; + lastOffset = endOffset; } // handle postamble @@ -92,31 +102,64 @@ class Highlighter { } else { // no more highlights to be found, just return the unhighlighted string - return [this._createSpan(safeSnippet, false)]; + return [this._processSnippet(safeSnippet, false)]; } } +} + +class HtmlHighlighter extends BaseHighlighter { + /* highlight the given snippet if required + * + * snippet: content of the span; must have been sanitised + * highlight: true to highlight as a search match + * + * returns an HTML string + */ + _processSnippet(snippet, highlight) { + if (!highlight) { + // nothing required here + return snippet; + } + + var span = "" + + snippet + ""; + + if (this.highlightLink) { + span = "" + +span+""; + } + return span; + } +} + +class TextHighlighter extends BaseHighlighter { + constructor(highlightClass, highlightLink) { + super(highlightClass, highlightLink); + this._key = 0; + } /* create a node to hold the given content * - * spanBody: content of the span. If html, must have been sanitised + * snippet: content of the span * highlight: true to highlight as a search match + * + * returns a React node */ - _createSpan(spanBody, highlight) { + _processSnippet(snippet, highlight) { var spanProps = { key: this._key++, }; if (highlight) { - spanProps.onClick = this.onHighlightClick; spanProps.className = this.highlightClass; } - if (this.html) { - return (); - } - else { - return ({ spanBody }); + var node = { snippet }; + + if (highlight && this.highlightLink) { + node = {node} } + return node; } } @@ -128,8 +171,7 @@ module.exports = { * * highlights: optional list of words to highlight, ordered by longest word first * - * opts.onHighlightClick: optional callback function to be called when a - * highlighted word is clicked + * opts.highlightLink: optional href to add to highlights */ bodyToHtml: function(content, highlights, opts) { opts = opts || {}; @@ -144,18 +186,13 @@ module.exports = { // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either try { if (highlights && highlights.length > 0) { - var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick); + var highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink); var safeHighlights = highlights.map(function(highlight) { return sanitizeHtml(highlight, sanitizeHtmlParams); }); // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure. sanitizeHtmlParams.textFilter = function(safeText) { - return highlighter.applyHighlights(safeText, safeHighlights).map(function(span) { - // XXX: rather clunky conversion from the react nodes returned by applyHighlights - // (which need to be nodes for the non-html highlighting case), to convert them - // back into raw HTML given that's what sanitize-html works in terms of. - return ReactDOMServer.renderToString(span); - }).join(''); + return highlighter.applyHighlights(safeText, safeHighlights).join(''); }; } safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams); @@ -167,7 +204,7 @@ module.exports = { } else { safeBody = content.body; if (highlights && highlights.length > 0) { - var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick); + var highlighter = new TextHighlighter("mx_EventTile_searchHighlight", opts.highlightLink); return highlighter.applyHighlights(safeBody, highlights); } else { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 25c289ba96..cd02d724b5 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -879,15 +879,6 @@ 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'); var SearchResultTile = sdk.getComponent('rooms.SearchResultTile'); @@ -948,10 +939,12 @@ module.exports = React.createClass({ } } + var resultLink = "#/room/"+this.props.roomId+"/"+mxEv.getId(); + ret.push(); + resultLink={resultLink}/>); } return ret; }, diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 2490d9be8b..9cc0e22c59 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -28,6 +28,18 @@ module.exports = React.createClass({ } }, + propTypes: { + /* the MatrixEvent to show */ + mxEvent: React.PropTypes.object.isRequired, + + /* a list of words to highlight */ + highlights: React.PropTypes.array, + + /* link URL for the highlights */ + highlightLink: React.PropTypes.string, + }, + + render: function() { var UnknownMessageTile = sdk.getComponent('messages.UnknownBody'); @@ -48,6 +60,6 @@ module.exports = React.createClass({ } return ; + highlightLink={this.props.highlightLink} />; }, }); diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index e3613ef9a3..92447dd1da 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -28,6 +28,17 @@ linkifyMatrix(linkify); module.exports = React.createClass({ displayName: 'TextualBody', + propTypes: { + /* the MatrixEvent to show */ + mxEvent: React.PropTypes.object.isRequired, + + /* a list of words to highlight */ + highlights: React.PropTypes.array, + + /* link URL for the highlights */ + highlightLink: React.PropTypes.string, + }, + componentDidMount: function() { linkifyElement(this.refs.content, linkifyMatrix.options); @@ -46,14 +57,15 @@ module.exports = React.createClass({ shouldComponentUpdate: function(nextProps) { // exploit that events are immutable :) return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || - nextProps.highlights !== this.props.highlights); + nextProps.highlights !== this.props.highlights || + nextProps.highlightLink !== this.props.highlightLink); }, render: function() { var mxEvent = this.props.mxEvent; var content = mxEvent.getContent(); var body = HtmlUtils.bodyToHtml(content, this.props.highlights, - {onHighlightClick: this.props.onHighlightClick}); + {highlightLink: this.props.highlightLink}); switch (content.msgtype) { case "m.emote": diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index f580686128..36ec85e91b 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -96,8 +96,8 @@ module.exports = React.createClass({ /* a list of words to highlight */ highlights: React.PropTypes.array, - /* a function to be called when the highlight is clicked */ - onHighlightClick: React.PropTypes.func, + /* link URL for the highlights */ + highlightLink: React.PropTypes.string, /* is this the focussed event */ isSelectedEvent: React.PropTypes.bool, @@ -313,8 +313,8 @@ module.exports = React.createClass({ { avatar } { sender }
    - +
  • ); diff --git a/src/components/views/rooms/SearchResultTile.js b/src/components/views/rooms/SearchResultTile.js index 9d3af16ee7..9c793e8705 100644 --- a/src/components/views/rooms/SearchResultTile.js +++ b/src/components/views/rooms/SearchResultTile.js @@ -29,8 +29,8 @@ module.exports = React.createClass({ // a list of strings to be highlighted in the results searchHighlights: React.PropTypes.array, - // callback to be called when the user selects this result - onSelect: React.PropTypes.func, + // href for the highlights in this result + resultLink: React.PropTypes.string, }, render: function() { @@ -53,7 +53,7 @@ module.exports = React.createClass({ } if (EventTile.haveTileForEvent(ev)) { ret.push() + highlightLink={this.props.resultLink}/>); } } return ( From 9a0ea56bba49fb6324a199b55c02cc11662e48f0 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 18 Feb 2016 00:07:35 +0000 Subject: [PATCH 19/33] don't suppress conf join/parts --- src/components/structures/RoomView.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 57d3a675d1..61e8710c0d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -989,7 +989,8 @@ module.exports = React.createClass({ else if (this.props.ConferenceHandler && mxEv.getType() === "m.room.member") { if (this.props.ConferenceHandler.isConferenceUser(mxEv.getSender()) || this.props.ConferenceHandler.isConferenceUser(mxEv.getStateKey())) { - wantTile = false; // suppress conf user join/parts + // don't suppress conf user join/parts entirely, as they're useful! + // wantTile = false; } } From 4d7eb5795cda8c4187d674a474113ac7abb9b34d Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 18 Feb 2016 00:32:46 +0000 Subject: [PATCH 20/33] tint border-left --- src/Tinter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tinter.js b/src/Tinter.js index b258930425..a83ccdce74 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -64,6 +64,7 @@ var cssAttrs = [ "borderColor", "borderTopColor", "borderBottomColor", + "borderLeftColor", ]; var svgAttrs = [ From aef04d682ba2da7abfb5cefd6e48c7ce5c88beb9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 18 Feb 2016 18:16:39 +0000 Subject: [PATCH 21/33] Make the links we emit for room URLs valid rather than relying on the onClick handler (ie. make them work if you c+p them) --- src/linkify-matrix.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index abb51e85d8..7fb043f800 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -114,6 +114,17 @@ matrixLinkify.options = { } }; } + }, + + formatHref: function (href, type) { + switch (type) { + case 'roomalias': + return '#/room/' + href; + case 'userid': + return '#'; + default: + return href; + } } }; From 12f5407392701d5db71d4628e92eb2de0b7a40eb Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 18 Feb 2016 18:16:48 +0000 Subject: [PATCH 22/33] remove ... in error view --- src/components/structures/RoomStatusBar.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index a8f0de3e92..75cb06daf6 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -111,6 +111,10 @@ module.exports = React.createClass({ ); } + if (this.state.syncState === "ERROR") { + return null; + } + if (wantPlaceholder) { return (
    ...
    From dc94df4b06fbc3f7cd661645a537614cf1844814 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 18 Feb 2016 19:09:58 +0000 Subject: [PATCH 23/33] accomodate 800px wide thumbs and support horizontal scaling --- src/components/views/messages/MImageBody.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 691380d678..b54a51c45b 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -94,7 +94,7 @@ module.exports = React.createClass({ _getThumbUrl: function() { var content = this.props.mxEvent.getContent(); - return MatrixClientPeg.get().mxcUrlToHttp(content.url, 480, 360); + return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600); }, render: function() { @@ -103,10 +103,10 @@ module.exports = React.createClass({ var cli = MatrixClientPeg.get(); var thumbHeight = null; - if (content.info) thumbHeight = this.thumbHeight(content.info.w, content.info.h, 480, 360); + if (content.info) thumbHeight = this.thumbHeight(content.info.w, content.info.h, 800, 600); var imgStyle = {}; - if (thumbHeight) imgStyle['height'] = thumbHeight; + if (thumbHeight) imgStyle['maxHeight'] = thumbHeight; var thumbUrl = this._getThumbUrl(); if (thumbUrl) { From 7f310ce3073f67c852edf0dfe072169e1ff116d2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 19 Feb 2016 01:18:37 +0000 Subject: [PATCH 24/33] fix nasty overscroll bug on video element caused by inline v. block --- src/components/views/voip/CallView.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/voip/CallView.js b/src/components/views/voip/CallView.js index ed44313b9e..5958c2b278 100644 --- a/src/components/views/voip/CallView.js +++ b/src/components/views/voip/CallView.js @@ -85,9 +85,9 @@ module.exports = React.createClass({ // if this call is a conf call, don't display local video as the // conference will have us in it this.getVideoView().getLocalVideoElement().style.display = ( - call.confUserId ? "none" : "initial" + call.confUserId ? "none" : "block" ); - this.getVideoView().getRemoteVideoElement().style.display = "initial"; + this.getVideoView().getRemoteVideoElement().style.display = "block"; } else { this.getVideoView().getLocalVideoElement().style.display = "none"; From d2a731e255d7450a8e5bb9f1b00314ea065e428f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 19 Feb 2016 01:18:53 +0000 Subject: [PATCH 25/33] only resize the video element if not fullscreen --- src/components/structures/RoomView.js | 18 ++++++++++++++---- src/components/views/voip/VideoView.js | 1 + 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 61e8710c0d..5ac8943b33 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1368,9 +1368,14 @@ module.exports = React.createClass({ if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50; if (this.refs.callView) { - var video = this.refs.callView.getVideoView().getRemoteVideoElement(); - - video.style.maxHeight = auxPanelMaxHeight + "px"; + var fullscreenElement = + (document.fullscreenElement || + document.mozFullScreenElement || + document.webkitFullscreenElement); + if (!fullscreenElement) { + var video = this.refs.callView.getVideoView().getRemoteVideoElement(); + video.style.maxHeight = auxPanelMaxHeight + "px"; + } } // we need to do this for general auxPanels too @@ -1414,6 +1419,11 @@ module.exports = React.createClass({ }); }, + onCallViewResize: function() { + this.onChildResize(); + this.onResize(); + }, + onChildResize: function() { // When the video or the message composer resizes, the scroll panel // also changes size. Work around GeminiScrollBar fail by telling it @@ -1733,7 +1743,7 @@ module.exports = React.createClass({
    { fileDropTarget } + onResize={this.onCallViewResize} /> { conferenceCallNotification } { aux }
    diff --git a/src/components/views/voip/VideoView.js b/src/components/views/voip/VideoView.js index 08e587f47f..6b4b546270 100644 --- a/src/components/views/voip/VideoView.js +++ b/src/components/views/voip/VideoView.js @@ -64,6 +64,7 @@ module.exports = React.createClass({ element.msRequestFullscreen ); requestMethod.call(element); + this.getRemoteVideoElement().style.maxHeight = "inherit"; } else { var exitMethod = ( From 957ef9cdc82432be40a88058a6ed9bd6b624ab8f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 19 Feb 2016 01:56:03 +0000 Subject: [PATCH 26/33] fix self-highlight --- src/components/views/rooms/EventTile.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index daff377ebd..0205062f84 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -111,6 +111,14 @@ module.exports = React.createClass({ shouldHighlight: function() { var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent); if (!actions || !actions.tweaks) { return false; } + + // don't show self-highlights from another of our clients + if (this.props.mxEvent.sender && + this.props.mxEvent.sender.userId === MatrixClientPeg.get().credentials.userId) + { + return false; + } + return actions.tweaks.highlight; }, From a44ef5bd48c9803d43a961dd538cfa712a0cdb2b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 19 Feb 2016 02:21:17 +0000 Subject: [PATCH 27/33] fix incomingCallBox vertical offset if MatrixToolbar is present --- src/components/views/rooms/RoomList.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 5c729c6047..07ed450ef2 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -291,6 +291,13 @@ module.exports = React.createClass({ } } + // slightly ugly hack to offset if there's a toolbar present. + // we really should be calculating our absolute offsets of top by recursing through the DOM + toolbar = document.getElementsByClassName("mx_MatrixToolbar")[0]; + if (toolbar) { + top += toolbar.offsetHeight; + } + incomingCallBox.style.top = top + "px"; incomingCallBox.style.left = scroll.offsetLeft + scroll.offsetWidth + "px"; } From b4fe9473d5692e57873e78ce2a7b5f66673d13e2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 19 Feb 2016 14:17:35 +0000 Subject: [PATCH 28/33] improve error messages when failing to talk to a HS --- src/components/structures/login/Login.js | 27 ++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 356439b0cc..ef6b095da0 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -129,11 +129,30 @@ module.exports = React.createClass({displayName: 'Login', if (!errCode && err.httpStatus) { errCode = "HTTP " + err.httpStatus; } - this.setState({ - errorText: ( - "Error: Problem communicating with the given homeserver " + + + var errorText = "Error: Problem communicating with the given homeserver " + (errCode ? "(" + errCode + ")" : "") - ) + + if (err.cors === 'rejected') { + if (window.location.protocol === 'https:' && + (this.state.enteredHomeserverUrl.startsWith("http:") || + !this.state.enteredHomeserverUrl.startsWith("http"))) + { + errorText = + Can't connect to homeserver via HTTP when using a vector served by HTTPS. + Either use HTTPS or enable unsafe scripts + ; + } + else { + errorText = + Can't connect to homeserver - please check your connectivity and ensure + your homeserver's SSL certificate is trusted. + ; + } + } + + this.setState({ + errorText: errorText }); }, From 71e2495e817c086aab60846679284f37fc7f9346 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 19 Feb 2016 14:45:02 +0000 Subject: [PATCH 29/33] Use new flag in js-sdk to look at the last read receipt the server actually has and ignore implicit ones, otherwise we can end up not sending an RR because we think there's already a more recent one, even though that one is implicit. --- 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 61e8710c0d..38a6abb5cf 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1111,7 +1111,7 @@ module.exports = React.createClass({ // the bottom of the room again, or something. if (!this.state.atEndOfLiveTimeline) return; - var currentReadUpToEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId); + var currentReadUpToEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId, true); var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId); // We want to avoid sending out read receipts when we are looking at From 26e66326a240b45cd57fd3a447a0bb3918cd1fc4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 22 Feb 2016 12:54:22 +0000 Subject: [PATCH 30/33] Link search results to the right room Fixes https://github.com/vector-im/vector-web/issues/980 --- src/components/structures/RoomView.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index d423f29bc6..07a8da5eac 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -927,6 +927,7 @@ module.exports = React.createClass({ var result = this.state.searchResults.results[i]; var mxEv = result.context.getEvent(); + var roomId = mxEv.getRoomId(); if (!EventTile.haveTileForEvent(mxEv)) { // XXX: can this ever happen? It will make the result count @@ -935,7 +936,6 @@ module.exports = React.createClass({ } if (this.state.searchScope === 'All') { - var roomId = mxEv.getRoomId(); if(roomId != lastRoomId) { var room = cli.getRoom(roomId); @@ -952,7 +952,7 @@ module.exports = React.createClass({ } } - var resultLink = "#/room/"+this.props.roomId+"/"+mxEv.getId(); + var resultLink = "#/room/"+roomId+"/"+mxEv.getId(); ret.push( Date: Mon, 22 Feb 2016 17:19:04 +0000 Subject: [PATCH 31/33] Update the scroll offset when images load In order to deal with image-loading reshaping the DOM, wire up ScrollPanel.checkScroll to the image load events. Fixes https://github.com/vector-im/vector-web/issues/984 --- src/components/structures/RoomView.js | 27 ++++++++++++++++--- src/components/structures/ScrollPanel.js | 14 +++++++--- src/components/views/messages/MImageBody.js | 11 +++++++- src/components/views/messages/MessageEvent.js | 6 ++++- src/components/views/rooms/EventTile.js | 6 ++++- .../views/rooms/SearchResultTile.js | 5 +++- 6 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 07a8da5eac..856ab955a4 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -921,6 +921,15 @@ module.exports = React.createClass({ } } + // once images in the search results load, make the scrollPanel check + // the scroll offsets. + var onImageLoad = () => { + var scrollPanel = this.refs.searchResultsPanel; + if (scrollPanel) { + scrollPanel.checkScroll(); + } + } + var lastRoomId; for (var i = this.state.searchResults.results.length - 1; i >= 0; i--) { @@ -957,19 +966,28 @@ module.exports = React.createClass({ ret.push(); + resultLink={resultLink} + onImageLoad={onImageLoad}/>); } return ret; }, getEventTiles: function() { var DateSeparator = sdk.getComponent('messages.DateSeparator'); + var EventTile = sdk.getComponent('rooms.EventTile'); + + // once images in the events load, make the scrollPanel check the + // scroll offsets. + var onImageLoad = () => { + var scrollPanel = this.refs.messagePanel; + if (scrollPanel) { + scrollPanel.checkScroll(); + } + } var ret = []; var count = 0; - var EventTile = sdk.getComponent('rooms.EventTile'); - var prevEvent = null; // the last event we showed var ghostIndex; var readMarkerIndex; @@ -1056,7 +1074,8 @@ module.exports = React.createClass({ ref={this._collectEventNode.bind(this, eventId)} data-scroll-token={scrollToken}> + last={last} isSelectedEvent={highlight} + onImageLoad={onImageLoad} /> ); diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 514937f877..044ef48687 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -124,10 +124,9 @@ module.exports = React.createClass({ // after adding event tiles, we may need to tweak the scroll (either to // keep at the bottom of the timeline, or to maintain the view after // adding events to the top). - this._restoreSavedScrollState(); - - // we also re-check the fill state, in case the paginate was inadequate - this.checkFillState(); + // + // This will also re-check the fill state, in case the paginate was inadequate + this.checkScroll(); }, componentWillUnmount: function() { @@ -178,6 +177,13 @@ module.exports = React.createClass({ this.checkFillState(); }, + // after an update to the contents of the panel, check that the scroll is + // where it ought to be, and set off pagination requests if necessary. + checkScroll: function() { + this._restoreSavedScrollState(); + this.checkFillState(); + }, + // return true if the content is fully scrolled down right now; else false. // // note that this is independent of the 'stuckAtBottom' state - it is simply diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index b54a51c45b..b60098295a 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -27,6 +27,14 @@ var dis = require("../../../dispatcher"); module.exports = React.createClass({ displayName: 'MImageBody', + propTypes: { + /* the MatrixEvent to show */ + mxEvent: React.PropTypes.object.isRequired, + + /* callback called when images in events are loaded */ + onImageLoad: React.PropTypes.func, + }, + thumbHeight: function(fullWidth, fullHeight, thumbWidth, thumbHeight) { if (!fullWidth || !fullHeight) { // Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even @@ -116,7 +124,8 @@ module.exports = React.createClass({ {content.body} + onMouseLeave={this.onImageLeave} + onLoad={this.props.onImageLoad} />
    diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 9cc0e22c59..34d6d53924 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -37,6 +37,9 @@ module.exports = React.createClass({ /* link URL for the highlights */ highlightLink: React.PropTypes.string, + + /* callback called when images in events are loaded */ + onImageLoad: React.PropTypes.func, }, @@ -60,6 +63,7 @@ module.exports = React.createClass({ } return ; + highlightLink={this.props.highlightLink} + onImageLoad={this.props.onImageLoad} />; }, }); diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 0205062f84..8ac7ab7996 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -102,6 +102,9 @@ module.exports = React.createClass({ /* is this the focussed event */ isSelectedEvent: React.PropTypes.bool, + + /* callback called when images in events are loaded */ + onImageLoad: React.PropTypes.func, }, getInitialState: function() { @@ -323,7 +326,8 @@ module.exports = React.createClass({ { sender }
    + highlightLink={this.props.highlightLink} + onImageLoad={this.props.onImageLoad} />
    ); diff --git a/src/components/views/rooms/SearchResultTile.js b/src/components/views/rooms/SearchResultTile.js index 9c793e8705..1fc0384433 100644 --- a/src/components/views/rooms/SearchResultTile.js +++ b/src/components/views/rooms/SearchResultTile.js @@ -31,6 +31,8 @@ module.exports = React.createClass({ // href for the highlights in this result resultLink: React.PropTypes.string, + + onImageLoad: React.PropTypes.func, }, render: function() { @@ -53,7 +55,8 @@ module.exports = React.createClass({ } if (EventTile.haveTileForEvent(ev)) { ret.push(); + highlightLink={this.props.resultLink} + onImageLoad={this.props.onImageLoad} />); } } return ( From 4158a007db5c744ebe01543a8ef39f0d9b3f43e7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 22 Feb 2016 17:44:34 +0000 Subject: [PATCH 32/33] Give elements in search results a key ... to make react shut up about them --- src/HtmlUtils.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index fe97d7b84f..6db2b08fd1 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -146,19 +146,17 @@ class TextHighlighter extends BaseHighlighter { * returns a React node */ _processSnippet(snippet, highlight) { - var spanProps = { - key: this._key++, - }; + var key = this._key++; - if (highlight) { - spanProps.className = this.highlightClass; - } - - var node = { snippet }; + var node = + + { snippet } + ; if (highlight && this.highlightLink) { - node = {node} + node = {node} } + return node; } } From d6732496818a134d2f732a6cb2b54f41942fffe0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 23 Feb 2016 11:06:16 +0000 Subject: [PATCH 33/33] Wire up StatusBar size changes to a geminipanel update When the statusbar changes size, we need to tell the gemini panel to update. This is slightly tortuous as figuring out the size of the statusbar isn't completely trivial. Fixes https://github.com/vector-im/vector-web/issues/945 and https://github.com/vector-im/vector-web/issues/986 --- src/components/structures/RoomStatusBar.js | 42 ++++++++++++++++++++++ src/components/structures/RoomView.js | 8 +++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 75cb06daf6..2e0897e3d0 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -51,6 +51,11 @@ module.exports = React.createClass({ // callback for when the user clicks on the 'scroll to bottom' button onScrollToBottomClick: React.PropTypes.func, + + // callback for when we do something that changes the size of the + // status bar. This is used to trigger a re-layout in the parent + // component. + onResize: React.PropTypes.func, }, getInitialState: function() { @@ -63,6 +68,12 @@ module.exports = React.createClass({ MatrixClientPeg.get().on("sync", this.onSyncStateChange); }, + componentDidUpdate: function(prevProps, prevState) { + if(this.props.onResize && this._checkForResize(prevProps, prevState)) { + this.props.onResize(); + } + }, + componentWillUnmount: function() { // we may have entirely lost our client as we're logging out before clicking login on the guest bar... if (MatrixClientPeg.get()) { @@ -79,6 +90,37 @@ module.exports = React.createClass({ }); }, + // determine if we need to call onResize + _checkForResize: function(prevProps, prevState) { + // figure out the old height and the new height of the status bar. We + // don't need the actual height - just whether it is likely to have + // changed - so we use '0' to indicate normal size, and other values to + // indicate other sizes. + var oldSize, newSize; + + if (prevState.syncState === "ERROR") { + oldSize = 1; + } else if (prevProps.tabCompleteEntries) { + oldSize = 0; + } else if (prevProps.hasUnsentMessages) { + oldSize = 2; + } else { + oldSize = 0; + } + + if (this.state.syncState === "ERROR") { + newSize = 1; + } else if (this.props.tabCompleteEntries) { + newSize = 0; + } else if (this.props.hasUnsentMessages) { + newSize = 2; + } else { + newSize = 0; + } + + return newSize != oldSize; + }, + // return suitable content for the image on the left of the status bar. // // if wantPlaceholder is true, we include a "..." placeholder if diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 856ab955a4..61248313db 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1444,9 +1444,10 @@ module.exports = React.createClass({ }, onChildResize: function() { - // When the video or the message composer resizes, the scroll panel - // also changes size. Work around GeminiScrollBar fail by telling it - // about it. This also ensures that the scroll offset is updated. + // When the video, status bar, or the message composer resizes, the + // scroll panel also changes size. Work around GeminiScrollBar fail by + // telling it about it. This also ensures that the scroll offset is + // updated. if (this.refs.messagePanel) { this.refs.messagePanel.forceUpdate(); } @@ -1577,6 +1578,7 @@ module.exports = React.createClass({ hasActiveCall={inCall} onResendAllClick={this.onResendAllClick} onScrollToBottomClick={this.jumpToLiveTimeline} + onResize={this.onChildResize} /> }