From 832da3aa8ec22b1bd8c5b7d230996ac8a9ec5c25 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 28 Nov 2015 12:44:10 +0000 Subject: [PATCH 01/13] support del tags for markdown --- src/HtmlUtils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 824f59ab20..9afaf8adfe 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' From 9befe243b5000c6809fa1251bf7db971848a0139 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 28 Nov 2015 21:12:02 +0000 Subject: [PATCH 02/13] combine M{Text,Notice,Emote}Message into a generic TextualMessage component --- .../views/messages/MEmoteMessage.js | 44 -------------- .../views/messages/MNoticeMessage.js | 59 ------------------- src/components/views/messages/Message.js | 6 +- .../{MTextMessage.js => TextualMessage.js} | 36 ++++++++--- 4 files changed, 30 insertions(+), 115 deletions(-) delete mode 100644 src/components/views/messages/MEmoteMessage.js delete mode 100644 src/components/views/messages/MNoticeMessage.js rename src/components/views/messages/{MTextMessage.js => TextualMessage.js} (57%) 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/Message.js b/src/components/views/messages/Message.js index fa74a8e137..d983d3da06 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') diff --git a/src/components/views/messages/MTextMessage.js b/src/components/views/messages/TextualMessage.js similarity index 57% rename from src/components/views/messages/MTextMessage.js rename to src/components/views/messages/TextualMessage.js index d3b337cbc1..7393e2e149 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,18 +26,18 @@ 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) { @@ -46,14 +47,31 @@ module.exports = React.createClass({ }, render: function() { - var content = this.props.mxEvent.getContent(); + var mxEvent = this.props.mxEvent; + var content = mxEvent.getContent(); var body = HtmlUtils.bodyToHtml(content, this.props.searchTerm); - return ( - - { body } - - ); + switch (content.msgtype) { + case "m.emote": + var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); + return ( + + * {name} {content.body} + + ); + case "m.notice": + return ( + + { body } + + ); + default: // including "m.text" + return ( + + { body } + + ); + } }, }); From 52376091cb19db9f85290e7b3114ff93c4ad7cf1 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 28 Nov 2015 22:20:14 +0000 Subject: [PATCH 03/13] resize the composer when you change rooms --- src/components/views/messages/MessageComposer.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/messages/MessageComposer.js b/src/components/views/messages/MessageComposer.js index 869e9f7614..cad584b981 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(); } } }; From f028b343b00df053826d661b128d775243272524 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 28 Nov 2015 22:34:45 +0000 Subject: [PATCH 04/13] HTML and Markdown emotes. --- .../views/messages/MessageComposer.js | 26 +++++++++---------- .../views/messages/TextualMessage.js | 2 +- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/components/views/messages/MessageComposer.js b/src/components/views/messages/MessageComposer.js index cad584b981..313dad3666 100644 --- a/src/components/views/messages/MessageComposer.js +++ b/src/components/views/messages/MessageComposer.js @@ -311,23 +311,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/TextualMessage.js b/src/components/views/messages/TextualMessage.js index 7393e2e149..58d3520719 100644 --- a/src/components/views/messages/TextualMessage.js +++ b/src/components/views/messages/TextualMessage.js @@ -56,7 +56,7 @@ module.exports = React.createClass({ var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); return ( - * {name} {content.body} + * { name } { body } ); case "m.notice": From 3cc1b1ad821d1130fca965a030638895be9449d7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 29 Nov 2015 03:21:21 +0000 Subject: [PATCH 05/13] fix unwanted vertical scrollbar issue and typo --- src/components/views/messages/MessageComposer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/MessageComposer.js b/src/components/views/messages/MessageComposer.js index 313dad3666..eefec48cff 100644 --- a/src/components/views/messages/MessageComposer.js +++ b/src/components/views/messages/MessageComposer.js @@ -239,7 +239,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(); @@ -317,7 +317,7 @@ module.exports = React.createClass({ } var htmlText; - if (this.markdownEnabled) && (htmlText = mdownToHtml(contentText)) !== contentText) { + 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); From 0b483c4707d4501fb196a5f0d79401b157b68f48 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 29 Nov 2015 03:22:01 +0000 Subject: [PATCH 06/13] rename searchTerms to highlights, and support highlighting multiple search terms --- src/HtmlUtils.js | 97 +++++++++++++------ src/components/views/messages/Event.js | 2 +- src/components/views/messages/Message.js | 2 +- .../views/messages/TextualMessage.js | 4 +- 4 files changed, 74 insertions(+), 31 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 9afaf8adfe..a7be229476 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -49,39 +49,82 @@ 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 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; + var safeHighlight = html ? sanitizeHtml(highlights[0], sanitizeHtmlParams) : highlights[0]; + while ((offset = safeSnippet.indexOf(safeHighlight, lastOffset)) >= 0) { + // handle preamble + if (offset > lastOffset) { + if (highlights[1]) { + // recurse into the preamble to check for the next highlights + var subnodes = this._applyHighlights( safeSnippet.substring(lastOffset, offset), highlights.slice(1), html, k ); + nodes = nodes.concat(subnodes); + k += subnodes.length; } - bodyList.push(); + else { + if (html) { + nodes.push(); + } + else { + nodes.push({ safeSnippet.substring(lastOffset, offset) }); + } + } + } + + // 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; + nodes.push({ safeHighlight }); + } + + lastOffset = offset + safeHighlight.length; + } + + // handle postamble + if (lastOffset != safeSnippet.length) { + if (highlights[1]) { + var subnodes = this._applyHighlights( safeSnippet.substring(lastOffset), highlights.slice(1), html, k ) + nodes = nodes.concat( subnodes ); + k += subnodes.length; + } + else { + if (html) { + nodes.push(); } - bodyList.push({ originalBody.substring(lastOffset) }); + else { + nodes.push({ safeSnippet.substring(lastOffset) }); + } + } + } + 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/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/Message.js b/src/components/views/messages/Message.js index d983d3da06..2318ca4a66 100644 --- a/src/components/views/messages/Message.js +++ b/src/components/views/messages/Message.js @@ -47,6 +47,6 @@ module.exports = React.createClass({ TileType = tileTypes[msgtype]; } - return ; + return ; }, }); diff --git a/src/components/views/messages/TextualMessage.js b/src/components/views/messages/TextualMessage.js index 58d3520719..f90b5ec738 100644 --- a/src/components/views/messages/TextualMessage.js +++ b/src/components/views/messages/TextualMessage.js @@ -43,13 +43,13 @@ module.exports = React.createClass({ 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 mxEvent = this.props.mxEvent; var content = mxEvent.getContent(); - var body = HtmlUtils.bodyToHtml(content, this.props.searchTerm); + var body = HtmlUtils.bodyToHtml(content, this.props.highlights); switch (content.msgtype) { case "m.emote": From d6b188751a3cdd028eb33ba4e594bb5fbb310b02 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 29 Nov 2015 03:25:55 +0000 Subject: [PATCH 07/13] s/autoplay/autoPlay/ --- src/components/views/messages/MVideoMessage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MVideoMessage.js b/src/components/views/messages/MVideoMessage.js index 5771ed2172..8780473d16 100644 --- a/src/components/views/messages/MVideoMessage.js +++ b/src/components/views/messages/MVideoMessage.js @@ -74,7 +74,7 @@ module.exports = React.createClass({ return ( From c96fa7f15d35807a4475b19d4b7d542aab3dbde0 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 29 Nov 2015 12:34:01 +0000 Subject: [PATCH 08/13] fix autoplay on chrome and remove loop for m.video --- src/components/views/messages/MVideoMessage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MVideoMessage.js b/src/components/views/messages/MVideoMessage.js index 8780473d16..6d5eb0e5ec 100644 --- a/src/components/views/messages/MVideoMessage.js +++ b/src/components/views/messages/MVideoMessage.js @@ -74,7 +74,7 @@ module.exports = React.createClass({ return ( From ca6cdfafd3dde7f9fd50a3629feb63a99f4e3ed7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 29 Nov 2015 13:00:37 +0000 Subject: [PATCH 09/13] clean up spurious whitespace --- src/components/views/messages/MVideoMessage.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/views/messages/MVideoMessage.js b/src/components/views/messages/MVideoMessage.js index 6d5eb0e5ec..a9139e2b3f 100644 --- a/src/components/views/messages/MVideoMessage.js +++ b/src/components/views/messages/MVideoMessage.js @@ -69,8 +69,6 @@ module.exports = React.createClass({ } } - - return (