diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 5e01c321473..fafd34f8ca4 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -296,6 +296,25 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { overflow-y: hidden; } +/* Spoiler stuff */ +.mx_EventTile_spoiler { + cursor: pointer; +} + +.mx_EventTile_spoiler_reason { + color: $event-timestamp-color; + font-size: 11px; +} + +.mx_EventTile_spoiler_content { + filter: blur(5px) saturate(0.1) sepia(1); + transition-duration: 0.5s; +} + +.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content { + filter: none; +} + .mx_EventTile_e2eIcon { display: block; position: absolute; diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index aeaf95ddb77..6ede36ee81d 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -256,7 +256,7 @@ const sanitizeHtmlParams = { allowedAttributes: { // custom ones first: font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix - span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix + span: ['data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix img: ['src', 'width', 'height', 'alt', 'title'], ol: ['start'], diff --git a/src/components/views/elements/Spoiler.js b/src/components/views/elements/Spoiler.js new file mode 100644 index 00000000000..b75967b2259 --- /dev/null +++ b/src/components/views/elements/Spoiler.js @@ -0,0 +1,51 @@ +/* + Copyright 2019 Sorunome + + 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. + */ + +import React from 'react'; + +export default class Spoiler extends React.Component { + constructor(props) { + super(props); + this.state = { + visible: false, + }; + } + + toggleVisible(e) { + if (!this.state.visible) { + // we are un-blurring, we don't want this click to propagate to potential child pills + e.preventDefault(); + e.stopPropagation(); + } + this.setState({ visible: !this.state.visible }); + } + + render() { + const reason = this.props.reason ? ( + {"(" + this.props.reason + ")"} + ) : null; + // react doesn't allow appending a DOM node as child. + // as such, we pass the this.props.contentHtml instead and then set the raw + // HTML content. This is secure as the contents have already been parsed previously + return ( + + { reason } +   + + + ); + } +} diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 492c95ba1b2..95b733c5f3e 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -95,6 +95,8 @@ module.exports = React.createClass({ }, _applyFormatting() { + this.activateSpoilers(this.refs.content.children); + // pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer // are still sent as plaintext URLs. If these are ever pillified in the composer, // we should be pillify them here by doing the linkifying BEFORE the pillifying. @@ -183,6 +185,34 @@ module.exports = React.createClass({ } }, + activateSpoilers: function(nodes) { + let node = nodes[0]; + while (node) { + if (node.tagName === "SPAN" && typeof node.getAttribute("data-mx-spoiler") === "string") { + const spoilerContainer = document.createElement('span'); + + const reason = node.getAttribute("data-mx-spoiler"); + const Spoiler = sdk.getComponent('elements.Spoiler'); + node.removeAttribute("data-mx-spoiler"); // we don't want to recurse + const spoiler = ; + + ReactDOM.render(spoiler, spoilerContainer); + node.parentNode.replaceChild(spoilerContainer, node); + + node = spoilerContainer; + } + + if (node.childNodes && node.childNodes.length) { + this.activateSpoilers(node.childNodes); + } + + node = node.nextSibling; + } + }, + findLinks: function(nodes) { let links = [];