Merge branch 'develop' into kegan/controller-merging4

This commit is contained in:
Kegan Dougal 2015-11-30 10:56:39 +00:00
commit 78cfaeb5c3
10 changed files with 135 additions and 176 deletions

View File

@ -23,6 +23,7 @@ var highlight = require('highlight.js');
var sanitizeHtmlParams = {
allowedTags: [
'font', // custom to matrix. deliberately no h1/h2 to stop people shouting.
'del', // for markdown
'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre'
@ -48,39 +49,79 @@ var sanitizeHtmlParams = {
};
module.exports = {
bodyToHtml: function(content, searchTerm) {
var originalBody = content.body;
var body;
_applyHighlights: function(safeSnippet, highlights, html, k) {
var lastOffset = 0;
var offset;
var nodes = [];
if (searchTerm) {
var lastOffset = 0;
var bodyList = [];
var k = 0;
var offset;
// XXX: when highlighting HTML, synapse performs the search on the plaintext body,
// but we're attempting to apply the highlights here to the HTML body. This is
// never going to end well - we really should be hooking into the sanitzer HTML
// parser to only attempt to highlight text nodes to avoid corrupting tags.
// If and when this happens, we'll probably have to split his method in two between
// HTML and plain-text highlighting.
// XXX: rather than searching for the search term in the body,
// we should be looking at the match delimiters returned by the FTS engine
if (content.format === "org.matrix.custom.html") {
var safeHighlight = html ? sanitizeHtml(highlights[0], sanitizeHtmlParams) : highlights[0];
while ((offset = safeSnippet.indexOf(safeHighlight, lastOffset)) >= 0) {
// handle preamble
if (offset > lastOffset) {
nodes = nodes.concat(this._applySubHighlightsInRange(safeSnippet, lastOffset, offset, highlights, html, k));
k += nodes.length;
}
var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
var safeSearchTerm = sanitizeHtml(searchTerm, sanitizeHtmlParams);
while ((offset = safeBody.indexOf(safeSearchTerm, lastOffset)) >= 0) {
// FIXME: we need to apply the search highlighting to only the text elements of HTML, which means
// hooking into the sanitizer parser rather than treating it as a string. Otherwise
// the act of highlighting a <b/> or whatever will break the HTML badly.
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeBody.substring(lastOffset, offset) }} />);
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeSearchTerm }} className="mx_MessageTile_searchHighlight" />);
lastOffset = offset + safeSearchTerm.length;
}
bodyList.push(<span className="markdown-body" key={ k++ } dangerouslySetInnerHTML={{ __html: safeBody.substring(lastOffset) }} />);
// do highlight
if (html) {
nodes.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeHighlight }} className="mx_MessageTile_searchHighlight" />);
}
else {
while ((offset = originalBody.indexOf(searchTerm, lastOffset)) >= 0) {
bodyList.push(<span key={ k++ } >{ originalBody.substring(lastOffset, offset) }</span>);
bodyList.push(<span key={ k++ } className="mx_MessageTile_searchHighlight">{ searchTerm }</span>);
lastOffset = offset + searchTerm.length;
}
bodyList.push(<span key={ k++ }>{ originalBody.substring(lastOffset) }</span>);
nodes.push(<span key={ k++ } className="mx_MessageTile_searchHighlight">{ safeHighlight }</span>);
}
lastOffset = offset + safeHighlight.length;
}
// handle postamble
if (lastOffset != safeSnippet.length) {
nodes = nodes.concat(this._applySubHighlightsInRange(safeSnippet, lastOffset, undefined, highlights, html, k));
k += nodes.length;
}
return nodes;
},
_applySubHighlightsInRange: function(safeSnippet, lastOffset, offset, highlights, html, k) {
var nodes = [];
if (highlights[1]) {
// recurse into this range to check for the next set of highlight matches
var subnodes = this._applyHighlights( safeSnippet.substring(lastOffset, offset), highlights.slice(1), html, k );
nodes = nodes.concat(subnodes);
k += subnodes.length;
}
else {
// no more highlights to be found, just return the unhighlighted string
if (html) {
nodes.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeSnippet.substring(lastOffset, offset) }} />);
}
else {
nodes.push(<span key={ k++ }>{ safeSnippet.substring(lastOffset, offset) }</span>);
}
}
return nodes;
},
bodyToHtml: function(content, highlights) {
var originalBody = content.body;
var body;
var k = 0;
if (highlights && highlights.length > 0) {
var bodyList = [];
if (content.format === "org.matrix.custom.html") {
var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
bodyList = this._applyHighlights(safeBody, highlights, true, k);
}
else {
bodyList = this._applyHighlights(originalBody, highlights, true, k);
}
body = bodyList;
}

View File

@ -43,7 +43,18 @@ class UserActivity {
document.onkeypress = undefined;
}
_onUserActivity() {
_onUserActivity(event) {
if (event.screenX) {
if (event.screenX === this.lastScreenX &&
event.screenY === this.lastScreenY)
{
// mouse hasn't actually moved
return;
}
this.lastScreenX = event.screenX;
this.lastScreenY = event.screenY;
}
this.lastActivityAtTs = (new Date).getTime();
if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL) {
this.lastDispatchAtTs = this.lastActivityAtTs;

View File

@ -268,7 +268,7 @@ module.exports = React.createClass({
{ avatar }
{ sender }
<div className="mx_EventTile_line">
<EventTileType mxEvent={this.props.mxEvent} searchTerm={this.props.searchTerm} />
<EventTileType mxEvent={this.props.mxEvent} highlights={this.props.highlights} />
</div>
</div>
);

View File

@ -1,44 +0,0 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var React = require('react');
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix');
linkifyMatrix(linkify);
module.exports = React.createClass({
displayName: 'MEmoteMessage',
componentDidMount: function() {
linkifyElement(this.refs.content, linkifyMatrix.options);
},
render: function() {
var mxEvent = this.props.mxEvent;
var content = mxEvent.getContent();
var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
return (
<span ref="content" className="mx_MEmoteTile mx_MessageTile_content">
* {name} {content.body}
</span>
);
},
});

View File

@ -1,59 +0,0 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var React = require('react');
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix.js');
linkifyMatrix(linkify);
var HtmlUtils = require('../../../HtmlUtils');
module.exports = React.createClass({
displayName: 'MNoticeMessage',
componentDidMount: function() {
linkifyElement(this.refs.content, linkifyMatrix.options);
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
HtmlUtils.highlightDom(this.getDOMNode());
},
componentDidUpdate: function() {
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
HtmlUtils.highlightDom(this.getDOMNode());
},
shouldComponentUpdate: function(nextProps) {
// exploit that events are immutable :)
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
nextProps.searchTerm !== this.props.searchTerm);
},
// XXX: fix horrible duplication with MTextTile
render: function() {
var content = this.props.mxEvent.getContent();
var body = HtmlUtils.bodyToHtml(content, this.props.searchTerm);
return (
<span ref="content" className="mx_MNoticeTile mx_MessageTile_content">
{ body }
</span>
);
},
});

View File

@ -69,12 +69,10 @@ module.exports = React.createClass({
}
}
return (
<span className="mx_MVideoTile">
<video className="mx_MVideoTile" src={cli.mxcUrlToHttp(content.url)} alt={content.body}
controls preload={preload} autoplay="false" loop
controls preload={preload} autoPlay="0"
height={height} width={width} poster={poster}>
</video>
</span>

View File

@ -32,9 +32,9 @@ module.exports = React.createClass({
var UnknownMessageTile = sdk.getComponent('messages.UnknownMessage');
var tileTypes = {
'm.text': sdk.getComponent('messages.MTextMessage'),
'm.notice': sdk.getComponent('messages.MNoticeMessage'),
'm.emote': sdk.getComponent('messages.MEmoteMessage'),
'm.text': sdk.getComponent('messages.TextualMessage'),
'm.notice': sdk.getComponent('messages.TextualMessage'),
'm.emote': sdk.getComponent('messages.TextualMessage'),
'm.image': sdk.getComponent('messages.MImageMessage'),
'm.file': sdk.getComponent('messages.MFileMessage'),
'm.video': sdk.getComponent('messages.MVideoMessage')
@ -47,6 +47,6 @@ module.exports = React.createClass({
TileType = tileTypes[msgtype];
}
return <TileType mxEvent={this.props.mxEvent} searchTerm={this.props.searchTerm} />;
return <TileType mxEvent={this.props.mxEvent} highlights={this.props.highlights} />;
},
});

View File

@ -69,6 +69,7 @@ module.exports = React.createClass({
original: null,
index: 0
};
var self = this;
this.sentHistory = {
// The list of typed messages. Index 0 is more recent
data: [],
@ -138,6 +139,8 @@ module.exports = React.createClass({
// restore the original text the user was typing.
this.element.value = this.originalText;
}
self.resizeInput();
return true;
},
@ -153,6 +156,7 @@ module.exports = React.createClass({
var text = window.sessionStorage.getItem("input_" + this.roomId);
if (text) {
this.element.value = text;
self.resizeInput();
}
}
};
@ -164,6 +168,7 @@ module.exports = React.createClass({
this.refs.textarea,
this.props.room.roomId
);
this.resizeInput();
},
componentWillUnmount: function() {
@ -235,7 +240,7 @@ module.exports = React.createClass({
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
this.refs.textarea.style.height = "0px";
var newHeight = this.refs.textarea.scrollHeight < 100 ? this.refs.textarea.scrollHeight : 100;
this.refs.textarea.style.height = newHeight + "px";
this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
if (this.props.roomView) {
// kick gemini-scrollbar to re-layout
this.props.roomView.forceUpdate();
@ -307,23 +312,21 @@ module.exports = React.createClass({
var isEmote = /^\/me /i.test(contentText);
var sendMessagePromise;
if (isEmote) {
sendMessagePromise = MatrixClientPeg.get().sendEmoteMessage(
this.props.room.roomId, contentText.substring(4)
);
contentText = contentText.substring(4);
}
var htmlText;
if (this.markdownEnabled && (htmlText = mdownToHtml(contentText)) !== contentText) {
sendMessagePromise = isEmote ?
MatrixClientPeg.get().sendHtmlEmote(this.props.room.roomId, contentText, htmlText) :
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText);
}
else {
var htmlText = mdownToHtml(contentText);
if (this.markdownEnabled && htmlText !== contentText) {
sendMessagePromise = MatrixClientPeg.get().sendHtmlMessage(
this.props.room.roomId, contentText, htmlText
);
}
else {
sendMessagePromise = MatrixClientPeg.get().sendTextMessage(
this.props.room.roomId, contentText
);
}
sendMessagePromise = isEmote ?
MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) :
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
}
sendMessagePromise.then(function() {

View File

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
var React = require('react');
var ReactDOM = require('react-dom');
var HtmlUtils = require('../../../HtmlUtils');
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
@ -25,35 +26,52 @@ var linkifyMatrix = require('../../../linkify-matrix');
linkifyMatrix(linkify);
module.exports = React.createClass({
displayName: 'MTextMessage',
displayName: 'TextualMessage',
componentDidMount: function() {
linkifyElement(this.refs.content, linkifyMatrix.options);
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
HtmlUtils.highlightDom(this.getDOMNode());
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this));
},
componentDidUpdate: function() {
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
HtmlUtils.highlightDom(this.getDOMNode());
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this));
},
shouldComponentUpdate: function(nextProps) {
// exploit that events are immutable :)
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
nextProps.searchTerm !== this.props.searchTerm);
nextProps.highlights !== this.props.highlights);
},
render: function() {
var content = this.props.mxEvent.getContent();
var body = HtmlUtils.bodyToHtml(content, this.props.searchTerm);
var mxEvent = this.props.mxEvent;
var content = mxEvent.getContent();
var body = HtmlUtils.bodyToHtml(content, this.props.highlights);
return (
<span ref="content" className="mx_MTextTile mx_MessageTile_content">
{ body }
</span>
);
switch (content.msgtype) {
case "m.emote":
var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
return (
<span ref="content" className="mx_MEmoteTile mx_MessageTile_content">
* { name } { body }
</span>
);
case "m.notice":
return (
<span ref="content" className="mx_MNoticeTile mx_MessageTile_content">
{ body }
</span>
);
default: // including "m.text"
return (
<span ref="content" className="mx_MTextTile mx_MessageTile_content">
{ body }
</span>
);
}
},
});

View File

@ -23,10 +23,6 @@ var sdk = require('../../../index');
var dis = require('../../../dispatcher');
var Modal = require("../../../Modal");
// The Lato WOFF doesn't include sensible combining diacritics, so Chrome chokes
// on rendering them. Revert to Arial when this happens, which on OSX works at least.
var zalgo = /[\u0300-\u036f\u1ab0-\u1aff\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]/;
module.exports = React.createClass({
displayName: 'MemberTile',
@ -168,11 +164,6 @@ module.exports = React.createClass({
// if (isMyUser) name += " (me)"; // this does nothing other than introduce line wrapping and pain
//var leave = isMyUser ? <img className="mx_MemberTile_leave" src="img/delete.png" width="10" height="10" onClick={this.onLeaveClick}/> : null;
var nameClass = "mx_MemberTile_name";
if (zalgo.test(name)) {
nameClass += " mx_MemberTile_zalgo";
}
var nameEl;
if (this.state.hover) {
var presence;
@ -194,7 +185,7 @@ module.exports = React.createClass({
}
else {
nameEl =
<div className={nameClass}>
<div className="mx_MemberTile_name">
{ name }
</div>
}