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 = [];