From af5a866596e71359a810d5d9bea85500f504ca31 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Feb 2016 21:29:56 +0200 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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 04/12] 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 05/12] 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 06/12] 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 07/12] 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 08/12] 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 09/12] 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 34d0fc890a54f7e85aa29247a136820bb8a85287 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 16 Feb 2016 17:39:32 +0000 Subject: [PATCH 10/12] 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 11/12] 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 12/12] 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 (