From 1c8586e80294ba1e281b13dee5074356873235f2 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Thu, 4 Jan 2018 22:21:38 +0000 Subject: [PATCH] Add sticker message rendering. --- src/components/views/messages/MStickerBody.js | 253 ++++++++++++++++++ src/components/views/messages/MessageEvent.js | 13 +- 2 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 src/components/views/messages/MStickerBody.js diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js new file mode 100644 index 0000000000..8424bbca22 --- /dev/null +++ b/src/components/views/messages/MStickerBody.js @@ -0,0 +1,253 @@ +/* +Copyright 2017 New Vector 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'; + +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, readBlobAsDataUri } from '../../../utils/DecryptFile'; +import Promise from 'bluebird'; +import { _t } from '../../../languageHandler'; +import SettingsStore from "../../../settings/SettingsStore"; + +module.exports = React.createClass({ + displayName: 'MStickerBody', + + propTypes: { + /* the MatrixEvent to show */ + mxEvent: React.PropTypes.object.isRequired, + + /* called when the image has loaded */ + onWidgetLoad: React.PropTypes.func.isRequired, + }, + + getInitialState: function() { + return { + decryptedUrl: null, + decryptedThumbnailUrl: null, + decryptedBlob: null, + error: null, + }; + }, + + + onClick: function onClick(ev) { + if (ev.button == 0 && !ev.metaKey) { + ev.preventDefault(); + const content = this.props.mxEvent.getContent(); + const httpUrl = this._getContentUrl(); + const ImageView = sdk.getComponent("elements.ImageView"); + const params = { + src: httpUrl, + name: content.body && content.body.length > 0 ? content.body : _t('Attachment'), + mxEvent: this.props.mxEvent, + }; + + if (content.info) { + params.width = content.info.w; + params.height = content.info.h; + params.fileSize = content.info.size; + } + + Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); + } + }, + + _isGif: function() { + const content = this.props.mxEvent.getContent(); + return ( + content && + content.info && + content.info.mimetype === "image/gif" + ); + }, + + onImageEnter: function(e) { + if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { + return; + } + const imgElement = e.target; + imgElement.src = this._getContentUrl(); + }, + + onImageLeave: function(e) { + if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { + return; + } + const imgElement = e.target; + 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() { + const content = this.props.mxEvent.getContent(); + if (content.file !== undefined) { + // Don't use the thumbnail for clients wishing to autoplay gifs. + if (this.state.decryptedThumbnailUrl) { + return this.state.decryptedThumbnailUrl; + } + 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) { + let thumbnailPromise = Promise.resolve(null); + if (content.info.thumbnail_file) { + thumbnailPromise = decryptFile( + content.info.thumbnail_file, + ).then(function(blob) { + return readBlobAsDataUri(blob); + }); + } + let decryptedBlob; + thumbnailPromise.then((thumbnailUrl) => { + return decryptFile(content.file).then(function(blob) { + decryptedBlob = blob; + return readBlobAsDataUri(blob); + }).then((contentUrl) => { + this.setState({ + decryptedUrl: contentUrl, + decryptedThumbnailUrl: thumbnailUrl, + decryptedBlob: decryptedBlob, + }); + this.props.onWidgetLoad(); + }); + }).catch((err) => { + console.warn("Unable to decrypt attachment: ", err); + // Set a placeholder image when we can't decrypt the image. + this.setState({ + error: err, + }); + }).done(); + } + }, + + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); + }, + + onAction: function(payload) { + if (payload.action === "timeline_resize") { + this.fixupHeight(); + } + }, + + fixupHeight: function() { + if (!this.refs.image) { + console.warn("Refusing to fix up height on MStickerBody with no image element"); + return; + } + + 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); + let thumbHeight = null; + if (content.info) { + thumbHeight = ImageUtils.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight); + } + this.refs.image.style.height = thumbHeight + "px"; + // console.log("Image height now", thumbHeight); + }, + + render: function() { + const TintableSvg = sdk.getComponent("elements.TintableSvg"); + const content = this.props.mxEvent.getContent(); + + if (this.state.error !== null) { + return ( + + + { _t("Error decrypting image") } + + ); + } + + 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 ( + +
+ {content.body} +
+
+ ); + } + + const contentUrl = this._getContentUrl(); + let thumbUrl; + if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) { + thumbUrl = contentUrl; + } else { + thumbUrl = this._getThumbUrl(); + } + + if (thumbUrl) { + return ( + + + {content.body} + + + ); + } else if (content.body) { + return ( + + { _t("Image '%(Body)s' cannot be displayed.", {Body: content.body}) } + + ); + } else { + return ( + + { _t("This image cannot be displayed.") } + + ); + } + }, +}); diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index deda1d8d20..7dd5661bb8 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -54,6 +54,7 @@ module.exports = React.createClass({ 'm.notice': sdk.getComponent('messages.TextualBody'), 'm.emote': sdk.getComponent('messages.TextualBody'), 'm.image': sdk.getComponent('messages.MImageBody'), + 'm.sticker': sdk.getComponent('messages.MStickerBody'), 'm.file': sdk.getComponent('messages.MFileBody'), 'm.audio': sdk.getComponent('messages.MAudioBody'), 'm.video': sdk.getComponent('messages.MVideoBody'), @@ -69,10 +70,12 @@ module.exports = React.createClass({ BodyType = bodyTypes['m.file']; } - return ; + return ; }, });