diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 824f59ab20..7a3cdd277b 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -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 or whatever will break the HTML badly. - bodyList.push(); - bodyList.push(); - lastOffset = offset + safeSearchTerm.length; - } - bodyList.push(); + // do highlight + if (html) { + nodes.push(); } else { - while ((offset = originalBody.indexOf(searchTerm, lastOffset)) >= 0) { - bodyList.push({ originalBody.substring(lastOffset, offset) }); - bodyList.push({ searchTerm }); - lastOffset = offset + searchTerm.length; - } - bodyList.push({ originalBody.substring(lastOffset) }); + nodes.push({ safeHighlight }); + } + + 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(); + } + else { + nodes.push({ safeSnippet.substring(lastOffset, offset) }); + } + } + 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; } diff --git a/src/UserActivity.js b/src/UserActivity.js index cee1b4efe2..b283b9a58e 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.js @@ -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; diff --git a/src/components/views/messages/Event.js b/src/components/views/messages/Event.js index 2fb2917541..6b1acc3690 100644 --- a/src/components/views/messages/Event.js +++ b/src/components/views/messages/Event.js @@ -268,7 +268,7 @@ module.exports = React.createClass({ { avatar } { sender }
- +
); diff --git a/src/components/views/messages/MEmoteMessage.js b/src/components/views/messages/MEmoteMessage.js deleted file mode 100644 index 26e29363cd..0000000000 --- a/src/components/views/messages/MEmoteMessage.js +++ /dev/null @@ -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 ( - - * {name} {content.body} - - ); - }, -}); - diff --git a/src/components/views/messages/MNoticeMessage.js b/src/components/views/messages/MNoticeMessage.js deleted file mode 100644 index 3a89d1ff6a..0000000000 --- a/src/components/views/messages/MNoticeMessage.js +++ /dev/null @@ -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 ( - - { body } - - ); - }, -}); - diff --git a/src/components/views/messages/MVideoMessage.js b/src/components/views/messages/MVideoMessage.js index 5771ed2172..a9139e2b3f 100644 --- a/src/components/views/messages/MVideoMessage.js +++ b/src/components/views/messages/MVideoMessage.js @@ -69,12 +69,10 @@ module.exports = React.createClass({ } } - - return ( diff --git a/src/components/views/messages/Message.js b/src/components/views/messages/Message.js index fa74a8e137..2318ca4a66 100644 --- a/src/components/views/messages/Message.js +++ b/src/components/views/messages/Message.js @@ -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 ; + return ; }, }); diff --git a/src/components/views/messages/MessageComposer.js b/src/components/views/messages/MessageComposer.js index 869e9f7614..932fa8c5a7 100644 --- a/src/components/views/messages/MessageComposer.js +++ b/src/components/views/messages/MessageComposer.js @@ -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() { diff --git a/src/components/views/messages/MTextMessage.js b/src/components/views/messages/TextualMessage.js similarity index 52% rename from src/components/views/messages/MTextMessage.js rename to src/components/views/messages/TextualMessage.js index d3b337cbc1..f90b5ec738 100644 --- a/src/components/views/messages/MTextMessage.js +++ b/src/components/views/messages/TextualMessage.js @@ -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 ( - - { body } - - ); + switch (content.msgtype) { + case "m.emote": + var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); + return ( + + * { name } { body } + + ); + case "m.notice": + return ( + + { body } + + ); + default: // including "m.text" + return ( + + { body } + + ); + } }, }); diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 5adde400a6..3d3d23e1e8 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -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 ? : 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 = -
+
{ name }
}