Merge commit 'e3feae3' into rav/roomview_works

This commit is contained in:
Richard van der Hoff 2016-02-23 12:01:53 +00:00
commit 7f4f30d7d5
13 changed files with 137 additions and 65 deletions

View file

@ -92,6 +92,7 @@ class ContentMessages {
this.inprogress.push(upload); this.inprogress.push(upload);
dis.dispatch({action: 'upload_started'}); dis.dispatch({action: 'upload_started'});
var error;
var self = this; var self = this;
return def.promise.then(function() { return def.promise.then(function() {
upload.promise = matrixClient.uploadContent(file); upload.promise = matrixClient.uploadContent(file);
@ -103,11 +104,10 @@ class ContentMessages {
dis.dispatch({action: 'upload_progress', upload: upload}); dis.dispatch({action: 'upload_progress', upload: upload});
} }
}).then(function(url) { }).then(function(url) {
dis.dispatch({action: 'upload_finished', upload: upload});
content.url = url; content.url = url;
return matrixClient.sendMessage(roomId, content); return matrixClient.sendMessage(roomId, content);
}, function(err) { }, function(err) {
dis.dispatch({action: 'upload_failed', upload: upload}); error = err;
if (!upload.canceled) { if (!upload.canceled) {
var desc = "The file '"+upload.fileName+"' failed to upload."; var desc = "The file '"+upload.fileName+"' failed to upload.";
if (err.http_status == 413) { if (err.http_status == 413) {
@ -128,6 +128,12 @@ class ContentMessages {
break; break;
} }
} }
if (error) {
dis.dispatch({action: 'upload_failed', upload: upload});
}
else {
dis.dispatch({action: 'upload_finished', upload: upload});
}
}); });
} }

View file

@ -17,7 +17,6 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); var React = require('react');
var ReactDOMServer = require('react-dom/server')
var sanitizeHtml = require('sanitize-html'); var sanitizeHtml = require('sanitize-html');
var highlight = require('highlight.js'); var highlight = require('highlight.js');
@ -50,14 +49,23 @@ var sanitizeHtmlParams = {
}, },
}; };
class Highlighter { class BaseHighlighter {
constructor(html, highlightClass, onHighlightClick) { constructor(highlightClass, highlightLink) {
this.html = html;
this.highlightClass = highlightClass; this.highlightClass = highlightClass;
this.onHighlightClick = onHighlightClick; this.highlightLink = highlightLink;
this._key = 0;
} }
/**
* 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) { applyHighlights(safeSnippet, safeHighlights) {
var lastOffset = 0; var lastOffset = 0;
var offset; var offset;
@ -71,10 +79,12 @@ class Highlighter {
nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights)); nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
} }
// do highlight // do highlight. use the original string rather than safeHighlight
nodes.push(this._createSpan(safeHighlight, true)); // 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 // handle postamble
@ -92,31 +102,64 @@ class Highlighter {
} }
else { else {
// no more highlights to be found, just return the unhighlighted string // 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 = "<span class=\""+this.highlightClass+"\">"
+ snippet + "</span>";
if (this.highlightLink) {
span = "<a href=\""+encodeURI(this.highlightLink)+"\">"
+span+"</a>";
}
return span;
}
}
class TextHighlighter extends BaseHighlighter {
constructor(highlightClass, highlightLink) {
super(highlightClass, highlightLink);
this._key = 0;
}
/* create a <span> node to hold the given content /* create a <span> 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 * highlight: true to highlight as a search match
*
* returns a React node
*/ */
_createSpan(spanBody, highlight) { _processSnippet(snippet, highlight) {
var spanProps = { var spanProps = {
key: this._key++, key: this._key++,
}; };
if (highlight) { if (highlight) {
spanProps.onClick = this.onHighlightClick;
spanProps.className = this.highlightClass; spanProps.className = this.highlightClass;
} }
if (this.html) { var node = <span {...spanProps}>{ snippet }</span>;
return (<span {...spanProps} dangerouslySetInnerHTML={{ __html: spanBody }} />);
} if (highlight && this.highlightLink) {
else { node = <a href={this.highlightLink}>{node}</a>
return (<span {...spanProps}>{ spanBody }</span>);
} }
return node;
} }
} }
@ -128,8 +171,7 @@ module.exports = {
* *
* highlights: optional list of words to highlight, ordered by longest word first * highlights: optional list of words to highlight, ordered by longest word first
* *
* opts.onHighlightClick: optional callback function to be called when a * opts.highlightLink: optional href to add to highlights
* highlighted word is clicked
*/ */
bodyToHtml: function(content, highlights, opts) { bodyToHtml: function(content, highlights, opts) {
opts = 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 // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
try { try {
if (highlights && highlights.length > 0) { 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) { var safeHighlights = highlights.map(function(highlight) {
return sanitizeHtml(highlight, sanitizeHtmlParams); return sanitizeHtml(highlight, sanitizeHtmlParams);
}); });
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure. // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
sanitizeHtmlParams.textFilter = function(safeText) { sanitizeHtmlParams.textFilter = function(safeText) {
return highlighter.applyHighlights(safeText, safeHighlights).map(function(span) { return highlighter.applyHighlights(safeText, safeHighlights).join('');
// 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('');
}; };
} }
safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams); safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
@ -167,7 +204,7 @@ module.exports = {
} else { } else {
safeBody = content.body; safeBody = content.body;
if (highlights && highlights.length > 0) { 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); return highlighter.applyHighlights(safeBody, highlights);
} }
else { else {

View file

@ -182,6 +182,9 @@ var Notifier = {
if (state === "PREPARED" || state === "SYNCING") { if (state === "PREPARED" || state === "SYNCING") {
this.isPrepared = true; this.isPrepared = true;
} }
else if (state === "STOPPED" || state === "ERROR") {
this.isPrepared = false;
}
}, },
onRoomTimeline: function(ev, room, toStartOfTimeline) { onRoomTimeline: function(ev, room, toStartOfTimeline) {

View file

@ -64,7 +64,10 @@ module.exports = React.createClass({
}, },
componentWillUnmount: function() { 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) { onSyncStateChange: function(state, prevState) {

View file

@ -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() { getSearchResultTiles: function() {
var EventTile = sdk.getComponent('rooms.EventTile'); var EventTile = sdk.getComponent('rooms.EventTile');
var SearchResultTile = sdk.getComponent('rooms.SearchResultTile'); var SearchResultTile = sdk.getComponent('rooms.SearchResultTile');
@ -762,10 +753,12 @@ module.exports = React.createClass({
} }
} }
var resultLink = "#/room/"+this.props.roomId+"/"+mxEv.getId();
ret.push(<SearchResultTile key={mxEv.getId()} ret.push(<SearchResultTile key={mxEv.getId()}
searchResult={result} searchResult={result}
searchHighlights={this.state.searchHighlights} searchHighlights={this.state.searchHighlights}
onSelect={this._onSearchResultSelected.bind(this, result)}/>); resultLink={resultLink}/>);
} }
return ret; return ret;
}, },

View file

@ -115,6 +115,9 @@ module.exports = React.createClass({
onProcessingRegistration: function(promise) { onProcessingRegistration: function(promise) {
var self = this; var self = this;
promise.done(function(response) { promise.done(function(response) {
self.setState({
busy: false
});
if (!response || !response.access_token) { if (!response || !response.access_token) {
console.warn( console.warn(
"FIXME: Register fulfilled without a final response, " + "FIXME: Register fulfilled without a final response, " +
@ -126,7 +129,7 @@ module.exports = React.createClass({
if (!response || !response.user_id || !response.access_token) { if (!response || !response.user_id || !response.access_token) {
console.error("Final response is missing keys."); console.error("Final response is missing keys.");
self.setState({ self.setState({
errorText: "There was a problem processing the response." errorText: "Registration failed on server"
}); });
return; return;
} }
@ -136,9 +139,6 @@ module.exports = React.createClass({
identityServerUrl: self.registerLogic.getIdentityServerUrl(), identityServerUrl: self.registerLogic.getIdentityServerUrl(),
accessToken: response.access_token accessToken: response.access_token
}); });
self.setState({
busy: false
});
}, function(err) { }, function(err) {
if (err.message) { if (err.message) {
self.setState({ self.setState({

View file

@ -31,14 +31,22 @@ module.exports = React.createClass({
} }
}, },
onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
e.stopPropagation();
e.preventDefault();
this.cancelPrompt();
}
},
render: function() { render: function() {
return ( return (
<div> <div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
Sign out? Sign out?
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons" onKeyDown={ this.onKeyDown }>
<button onClick={this.logOut}>Sign Out</button> <button autoFocus onClick={this.logOut}>Sign Out</button>
<button onClick={this.cancelPrompt}>Cancel</button> <button onClick={this.cancelPrompt}>Cancel</button>
</div> </div>
</div> </div>

View file

@ -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() { render: function() {
var UnknownMessageTile = sdk.getComponent('messages.UnknownBody'); var UnknownMessageTile = sdk.getComponent('messages.UnknownBody');
@ -48,6 +60,6 @@ module.exports = React.createClass({
} }
return <TileType mxEvent={this.props.mxEvent} highlights={this.props.highlights} return <TileType mxEvent={this.props.mxEvent} highlights={this.props.highlights}
onHighlightClick={this.props.onHighlightClick} />; highlightLink={this.props.highlightLink} />;
}, },
}); });

View file

@ -28,6 +28,17 @@ linkifyMatrix(linkify);
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'TextualBody', 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() { componentDidMount: function() {
linkifyElement(this.refs.content, linkifyMatrix.options); linkifyElement(this.refs.content, linkifyMatrix.options);
@ -46,14 +57,15 @@ module.exports = React.createClass({
shouldComponentUpdate: function(nextProps) { shouldComponentUpdate: function(nextProps) {
// exploit that events are immutable :) // exploit that events are immutable :)
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || 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() { render: function() {
var mxEvent = this.props.mxEvent; var mxEvent = this.props.mxEvent;
var content = mxEvent.getContent(); var content = mxEvent.getContent();
var body = HtmlUtils.bodyToHtml(content, this.props.highlights, var body = HtmlUtils.bodyToHtml(content, this.props.highlights,
{onHighlightClick: this.props.onHighlightClick}); {highlightLink: this.props.highlightLink});
switch (content.msgtype) { switch (content.msgtype) {
case "m.emote": case "m.emote":

View file

@ -96,8 +96,8 @@ module.exports = React.createClass({
/* a list of words to highlight */ /* a list of words to highlight */
highlights: React.PropTypes.array, highlights: React.PropTypes.array,
/* a function to be called when the highlight is clicked */ /* link URL for the highlights */
onHighlightClick: React.PropTypes.func, highlightLink: React.PropTypes.string,
/* is this the focussed event */ /* is this the focussed event */
isSelectedEvent: React.PropTypes.bool, isSelectedEvent: React.PropTypes.bool,
@ -313,8 +313,8 @@ module.exports = React.createClass({
{ avatar } { avatar }
{ sender } { sender }
<div className="mx_EventTile_line"> <div className="mx_EventTile_line">
<EventTileType mxEvent={this.props.mxEvent} highlights={this.props.highlights} <EventTileType mxEvent={this.props.mxEvent} highlights={this.props.highlights}
onHighlightClick={this.props.onHighlightClick} /> highlightLink={this.props.highlightLink}/>
</div> </div>
</div> </div>
); );

View file

@ -327,7 +327,7 @@ module.exports = React.createClass({
var memberList = self.state.members.filter(function(userId) { var memberList = self.state.members.filter(function(userId) {
var m = self.memberDict[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 false;
} }
return m.membership == membership; return m.membership == membership;

View file

@ -29,8 +29,8 @@ module.exports = React.createClass({
// a list of strings to be highlighted in the results // a list of strings to be highlighted in the results
searchHighlights: React.PropTypes.array, searchHighlights: React.PropTypes.array,
// callback to be called when the user selects this result // href for the highlights in this result
onSelect: React.PropTypes.func, resultLink: React.PropTypes.string,
}, },
render: function() { render: function() {
@ -53,7 +53,7 @@ module.exports = React.createClass({
} }
if (EventTile.haveTileForEvent(ev)) { if (EventTile.haveTileForEvent(ev)) {
ret.push(<EventTile key={eventId+"+"+j} mxEvent={ev} contextual={contextual} highlights={highlights} ret.push(<EventTile key={eventId+"+"+j} mxEvent={ev} contextual={contextual} highlights={highlights}
onHighlightClick={this.props.onSelect}/>) highlightLink={this.props.resultLink}/>);
} }
} }
return ( return (

View file

@ -110,19 +110,17 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
var avatarImg; var avatarImg;
// Having just set an avatar we just display that since it will take a little // Having just set an avatar we just display that since it will take a little
// time to propagate through to the RoomAvatar. // time to propagate through to the RoomAvatar.
if (this.props.room && !this.avatarSet) { if (this.props.room && !this.avatarSet) {
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
avatarImg = <RoomAvatar room={this.props.room} width={ this.props.width } height={ this.props.height } resizeMethod='crop' />; avatarImg = <RoomAvatar room={this.props.room} width={ this.props.width } height={ this.props.height } resizeMethod='crop' />;
} else { } else {
var style = { var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
width: this.props.width, // XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ?
height: this.props.height, avatarImg = <BaseAvatar width={this.props.width} height={this.props.height} resizeMethod='crop'
objectFit: 'cover', name='?' idName={ MatrixClientPeg.get().getUserIdLocalpart() } url={this.state.avatarUrl} />
};
avatarImg = <img className="mx_BaseAvatar_image" src={this.state.avatarUrl} style={style} />;
} }
var uploadSection; var uploadSection;