-
+
Download {text}
diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index 526fc6a3a5..4a5dbab51e 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -16,14 +16,14 @@ limitations under the License.
'use strict';
-var React = require('react');
-var filesize = require('filesize');
-
-var MatrixClientPeg = require('../../../MatrixClientPeg');
-var ImageUtils = require('../../../ImageUtils');
-var Modal = require('../../../Modal');
-var sdk = require('../../../index');
-var dis = require("../../../dispatcher");
+import React from 'react';
+import MFileBody from './MFileBody';
+import MatrixClientPeg from '../../../MatrixClientPeg';
+import ImageUtils from '../../../ImageUtils';
+import Modal from '../../../Modal';
+import sdk from '../../../index';
+import dis from '../../../dispatcher';
+import {decryptFile} from '../../../utils/DecryptFile';
module.exports = React.createClass({
displayName: 'MImageBody',
@@ -33,13 +33,20 @@ module.exports = React.createClass({
mxEvent: React.PropTypes.object.isRequired,
},
+ getInitialState: function() {
+ return {
+ decryptedUrl: null,
+ };
+ },
+
+
onClick: function onClick(ev) {
if (ev.button == 0 && !ev.metaKey) {
ev.preventDefault();
- var content = this.props.mxEvent.getContent();
- var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(content.url);
- var ImageView = sdk.getComponent("elements.ImageView");
- var params = {
+ const content = this.props.mxEvent.getContent();
+ const httpUrl = this._getContentUrl();
+ const ImageView = sdk.getComponent("elements.ImageView");
+ const params = {
src: httpUrl,
mxEvent: this.props.mxEvent
};
@@ -55,7 +62,7 @@ module.exports = React.createClass({
},
_isGif: function() {
- var content = this.props.mxEvent.getContent();
+ const content = this.props.mxEvent.getContent();
return (content && content.info && content.info.mimetype === "image/gif");
},
@@ -64,9 +71,7 @@ module.exports = React.createClass({
return;
}
var imgElement = e.target;
- imgElement.src = MatrixClientPeg.get().mxcUrlToHttp(
- this.props.mxEvent.getContent().url
- );
+ imgElement.src = this._getContentUrl();
},
onImageLeave: function(e) {
@@ -77,14 +82,40 @@ module.exports = React.createClass({
imgElement.src = this._getThumbUrl();
},
+ _getContentUrl: function() {
+ const content = this.props.mxEvent.getContent();
+ if (content.file !== undefined) {
+ return this.state.decryptedUrl;
+ } else {
+ return MatrixClientPeg.get().mxcUrlToHttp(content.url);
+ }
+ },
+
_getThumbUrl: function() {
- var content = this.props.mxEvent.getContent();
- return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600);
+ const content = this.props.mxEvent.getContent();
+ if (content.file !== undefined) {
+ // TODO: Decrypt and use the thumbnail file if one is present.
+ return this.state.decryptedUrl;
+ } else {
+ return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600);
+ }
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this.fixupHeight();
+ const content = this.props.mxEvent.getContent();
+ if (content.file !== undefined && this.state.decryptedUrl === null) {
+ decryptFile(content.file).done((url) => {
+ this.setState({
+ decryptedUrl: url,
+ });
+ }, (err) => {
+ console.warn("Unable to decrypt attachment: ", err)
+ // Set a placeholder image when we can't decrypt the image.
+ this.refs.image.src = "img/warning.svg";
+ });
+ }
},
componentWillUnmount: function() {
@@ -103,14 +134,13 @@ module.exports = React.createClass({
return;
}
- var content = this.props.mxEvent.getContent();
-
- var thumbHeight = null;
- var timelineWidth = this.refs.body.offsetWidth;
- var maxHeight = 600; // let images take up as much width as they can so long as the height doesn't exceed 600px.
+ const content = this.props.mxEvent.getContent();
+ const timelineWidth = this.refs.body.offsetWidth;
+ const maxHeight = 600; // let images take up as much width as they can so long as the height doesn't exceed 600px.
// the alternative here would be 600*timelineWidth/800; to scale them down to fit inside a 4:3 bounding box
//console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth);
+ var thumbHeight = null;
if (content.info) {
thumbHeight = ImageUtils.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight);
}
@@ -119,45 +149,35 @@ module.exports = React.createClass({
},
render: function() {
- var TintableSvg = sdk.getComponent("elements.TintableSvg");
- var content = this.props.mxEvent.getContent();
- var cli = MatrixClientPeg.get();
+ const TintableSvg = sdk.getComponent("elements.TintableSvg");
+ const content = this.props.mxEvent.getContent();
- var download;
- if (this.props.tileShape === "file_grid") {
- download = (
-
-
- {content.body}
-
-
- { content.info && content.info.size ? filesize(content.info.size) : "" }
-
-
- );
- }
- else {
- download = (
-
+ if (content.file !== undefined && this.state.decryptedUrl === null) {
+
+ // Need to decrypt the attachment
+ // The attachment is decrypted in componentDidMount.
+ // For now add an img tag with a spinner.
+ return (
+
+
+
);
}
- var thumbUrl = this._getThumbUrl();
+ const contentUrl = this._getContentUrl();
+ const thumbUrl = this._getThumbUrl();
+
if (thumbUrl) {
return (
-
+
- { download }
+
);
} else if (content.body) {
diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js
index d29a3ea53e..a32348ea1a 100644
--- a/src/components/views/messages/MVideoBody.js
+++ b/src/components/views/messages/MVideoBody.js
@@ -16,16 +16,24 @@ limitations under the License.
'use strict';
-var React = require('react');
-var filesize = require('filesize');
-
-var MatrixClientPeg = require('../../../MatrixClientPeg');
-var Modal = require('../../../Modal');
-var sdk = require('../../../index');
+import React from 'react';
+import MFileBody from './MFileBody';
+import MatrixClientPeg from '../../../MatrixClientPeg';
+import Model from '../../../Modal';
+import sdk from '../../../index';
+import { decryptFile } from '../../../utils/DecryptFile';
+import q from 'q';
module.exports = React.createClass({
displayName: 'MVideoBody',
+ getInitialState: function() {
+ return {
+ decryptedUrl: null,
+ decryptedThumbnailUrl: null,
+ };
+ },
+
thumbScale: function(fullWidth, fullHeight, thumbWidth, thumbHeight) {
if (!fullWidth || !fullHeight) {
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
@@ -48,59 +56,92 @@ module.exports = React.createClass({
}
},
+ _getContentUrl: function() {
+ const content = this.props.mxEvent.getContent();
+ if (content.file !== undefined) {
+ return this.state.decryptedUrl;
+ } else {
+ return MatrixClientPeg.get().mxcUrlToHttp(content.url);
+ }
+ },
+
+ _getThumbUrl: function() {
+ const content = this.props.mxEvent.getContent();
+ if (content.file !== undefined) {
+ return this.state.decryptedThumbnailUrl;
+ } else if (content.info.thumbnail_url) {
+ return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url);
+ } else {
+ return null;
+ }
+ },
+
+ componentDidMount: function() {
+ const content = this.props.mxEvent.getContent();
+ if (content.file !== undefined && this.state.decryptedUrl === null) {
+ var thumbnailPromise = q(null);
+ if (content.info.thumbnail_file) {
+ thumbnailPromise = decryptFile(
+ content.info.thumbnail_file
+ );
+ }
+ thumbnailPromise.then((thumbnailUrl) => {
+ decryptFile(content.file).then((contentUrl) => {
+ this.setState({
+ decryptedUrl: contentUrl,
+ decryptedThumbnailUrl: thumbnailUrl,
+ });
+ });
+ }).catch((err) => {
+ console.warn("Unable to decrypt attachment: ", err)
+ // Set a placeholder image when we can't decrypt the image.
+ this.refs.image.src = "img/warning.svg";
+ }).done();
+ }
+ },
+
render: function() {
- var content = this.props.mxEvent.getContent();
- var cli = MatrixClientPeg.get();
+ const content = this.props.mxEvent.getContent();
+
+ if (content.file !== undefined && this.state.decryptedUrl === null) {
+ // Need to decrypt the attachment
+ // The attachment is decrypted in componentDidMount.
+ // For now add an img tag with a spinner.
+ return (
+
+
+
+ );
+ }
+
+ const contentUrl = this._getContentUrl();
+ const thumbUrl = this._getThumbUrl();
var height = null;
var width = null;
var poster = null;
var preload = "metadata";
if (content.info) {
- var scale = this.thumbScale(content.info.w, content.info.h, 480, 360);
+ const scale = this.thumbScale(content.info.w, content.info.h, 480, 360);
if (scale) {
width = Math.floor(content.info.w * scale);
height = Math.floor(content.info.h * scale);
}
- if (content.info.thumbnail_url) {
- poster = cli.mxcUrlToHttp(content.info.thumbnail_url);
+ if (thumbUrl) {
+ poster = thumbUrl;
preload = "none";
}
}
- var download;
- if (this.props.tileShape === "file_grid") {
- download = (
-
-
- {content.body}
-
-
- { content.info && content.info.size ? filesize(content.info.size) : "" }
-
-
- );
- }
- else {
- var TintableSvg = sdk.getComponent("elements.TintableSvg");
- download = (
-
- );
- }
-
return (
-
- { download }
+
);
},
diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.js
new file mode 100644
index 0000000000..38eab1d073
--- /dev/null
+++ b/src/utils/DecryptFile.js
@@ -0,0 +1,67 @@
+/*
+Copyright 2016 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.
+*/
+
+// Pull in the encryption lib so that we can decrypt attachments.
+import encrypt from 'browser-encrypt-attachment';
+// Pull in a fetch polyfill so we can download encrypted attachments.
+import 'isomorphic-fetch';
+// Grab the client so that we can turn mxc:// URLs into https:// URLS.
+import MatrixClientPeg from '../MatrixClientPeg';
+import q from 'q';
+
+
+/**
+ * Read blob as a data:// URI.
+ * @return {Promise} A promise that resolves with the data:// URI.
+ */
+function readBlobAsDataUri(file) {
+ var deferred = q.defer();
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ deferred.resolve(e.target.result);
+ };
+ reader.onerror = function(e) {
+ deferred.reject(e);
+ };
+ reader.readAsDataURL(file);
+ return deferred.promise;
+}
+
+
+/**
+ * Decrypt a file attached to a matrix event.
+ * @param file {Object} The json taken from the matrix event.
+ * This passed to [link]{@link https://github.com/matrix-org/browser-encrypt-attachments}
+ * as the encryption info object, so will also have the those keys in addition to
+ * the keys below.
+ * @param file.url {string} An mxc:// URL for the encrypted file.
+ * @param file.mimetype {string} The MIME-type of the plaintext file.
+ */
+export function decryptFile(file) {
+ const url = MatrixClientPeg.get().mxcUrlToHttp(file.url);
+ // Download the encrypted file as an array buffer.
+ return q(fetch(url)).then(function(response) {
+ return response.arrayBuffer();
+ }).then(function(responseData) {
+ // Decrypt the array buffer using the information taken from
+ // the event content.
+ return encrypt.decryptAttachment(responseData, file);
+ }).then(function(dataArray) {
+ // Turn the array into a Blob and give it the correct MIME-type.
+ var blob = new Blob([dataArray], {type: file.mimetype});
+ return readBlobAsDataUri(blob);
+ });
+}