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}); + } }); } 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/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) { 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) { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index be3ac1adfe..d3aabf366f 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -693,15 +693,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'); @@ -762,10 +753,12 @@ module.exports = React.createClass({ } } + var resultLink = "#/room/"+this.props.roomId+"/"+mxEv.getId(); + ret.push(); + resultLink={resultLink}/>); } return ret; }, diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 5666318368..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, " + @@ -126,7 +129,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; } @@ -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({ diff --git a/src/components/views/dialogs/LogoutPrompt.js b/src/components/views/dialogs/LogoutPrompt.js index 824924e999..67fedfe840 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?
-
- +
+
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/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; 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 ( 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;