diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 1c809f0743..4c763c5991 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -20,5 +20,29 @@ limitations under the License. } .mx_MImageBody_thumbnail { - max-width: 100%; -} \ No newline at end of file + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; +} + +.mx_MImageBody_thumbnail_container { + // Prevent the padding-bottom (added inline in MImageBody.js) from + // affecting elements below the container. + overflow: hidden; + + // Make sure the _thumbnail is positioned relative to the _container + position: relative; +} + +.mx_MImageBody_thumbnail_spinner { + position: absolute; + left: 50%; + top: 50%; +} + +// Inner img and TintableSvg should be centered around 0, 0 +.mx_MImageBody_thumbnail_spinner > * { + transform: translate(-50%, -50%); +} diff --git a/res/css/views/messages/_MStickerBody.scss b/res/css/views/messages/_MStickerBody.scss index 3e6bbe5aa4..e4977bcc34 100644 --- a/res/css/views/messages/_MStickerBody.scss +++ b/res/css/views/messages/_MStickerBody.scss @@ -14,33 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MStickerBody { - display: block; - margin-right: 34px; - min-height: 110px; - padding: 20px 0; +.mx_MStickerBody_wrapper { + padding: 20px 0px; } -.mx_MStickerBody_image_container { - display: inline-block; - position: relative; -} - -.mx_MStickerBody_image { - max-width: 100%; - opacity: 0; -} - -.mx_MStickerBody_image_visible { - opacity: 1; -} - -.mx_MStickerBody_placeholder { - position: absolute; - opacity: 1; -} - -.mx_MStickerBody_placeholder_invisible { - transition: 500ms; - opacity: 0; +.mx_MStickerBody_tooltip { + position: absolute; + top: 50%; } diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 4dfd89ec0a..c42840b03a 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -22,10 +22,8 @@ import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; import MFileBody from './MFileBody'; -import ImageUtils from '../../../ImageUtils'; import Modal from '../../../Modal'; import sdk from '../../../index'; -import dis from '../../../dispatcher'; import { decryptFile } from '../../../utils/DecryptFile'; import Promise from 'bluebird'; import { _t } from '../../../languageHandler'; @@ -52,14 +50,12 @@ export default class extends React.Component { constructor(props) { super(props); - this.onAction = this.onAction.bind(this); this.onImageError = this.onImageError.bind(this); this.onImageLoad = this.onImageLoad.bind(this); this.onImageEnter = this.onImageEnter.bind(this); this.onImageLeave = this.onImageLeave.bind(this); this.onClientSync = this.onClientSync.bind(this); this.onClick = this.onClick.bind(this); - this.fixupHeight = this.fixupHeight.bind(this); this._isGif = this._isGif.bind(this); this.state = { @@ -68,6 +64,8 @@ export default class extends React.Component { decryptedBlob: null, error: null, imgError: false, + imgLoaded: false, + hover: false, }; } @@ -122,6 +120,8 @@ export default class extends React.Component { } onImageEnter(e) { + this.setState({ hover: true }); + if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { return; } @@ -130,6 +130,8 @@ export default class extends React.Component { } onImageLeave(e) { + this.setState({ hover: false }); + if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { return; } @@ -145,6 +147,7 @@ export default class extends React.Component { onImageLoad() { this.props.onWidgetLoad(); + this.setState({ imgLoaded: true }); } _getContentUrl() { @@ -179,7 +182,6 @@ export default class extends React.Component { } componentDidMount() { - this.dispatcherRef = dis.register(this.onAction); const content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { let thumbnailPromise = Promise.resolve(null); @@ -210,7 +212,6 @@ export default class extends React.Component { }); }).done(); } - this.fixupHeight(); this._afterComponentDidMount(); } @@ -221,7 +222,6 @@ export default class extends React.Component { componentWillUnmount() { this.unmounted = true; - dis.unregister(this.dispatcherRef); this.context.matrixClient.removeListener('sync', this.onClientSync); this._afterComponentWillUnmount(); @@ -238,60 +238,87 @@ export default class extends React.Component { _afterComponentWillUnmount() { } - onAction(payload) { - if (payload.action === "timeline_resize") { - this.fixupHeight(); - } - } - - fixupHeight() { - if (!this.refs.image) { - console.warn(`Refusing to fix up height on ${this.displayName} with no image element`); - return; - } - - const content = this.props.mxEvent.getContent(); - const timelineWidth = this.refs.body.offsetWidth; - const maxHeight = this.props.maxImageHeight || 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 - - // FIXME: this will break on clientside generated thumbnails (as per e2e rooms) - // which may well be much smaller than the 800x600 bounding box. - - // FIXME: It will also break really badly for images with broken or missing thumbnails - - // FIXME: Because we don't know what size of thumbnail the server's actually going to send - // us, we can't even really layout the page nicely for it. Instead we have to assume - // it'll target 800x600 and we'll downsize if needed to make things fit. - - // 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); - } - _messageContent(contentUrl, thumbUrl, content) { + // The maximum height of the thumbnail as it is rendered as an + const maxHeight = Math.min(this.props.maxImageHeight || 600, content.info.h); + // The maximum width of the thumbnail, as dictated by its natural + // maximum height. + const maxWidth = content.info.w * maxHeight / content.info.h; + + let img = null; + let placeholder = null; + + // e2e image hasn't been decrypted yet + if (content.file !== undefined && this.state.decryptedUrl === null) { + placeholder = {content.body}; + } else if (!this.state.imgLoaded) { + // Deliberately, getSpinner is left unimplemented here, MStickerBody overides + placeholder = this.getPlaceholder(); + } + + const showPlaceholder = Boolean(placeholder); + + if (thumbUrl && !this.state.imgError) { + // Restrict the width of the thumbnail here, otherwise it will fill the container + // which has the same width as the timeline + // mx_MImageBody_thumbnail resizes img to exactly container size + img = {content.body}; + } + const thumbnail = ( - - {content.body} - +
+ { /* Calculate aspect ratio, using %padding will size _container correctly */ } +
+ + { showPlaceholder && +
+
+ { placeholder } +
+
+ } + +
+ { img } +
+ + { this.state.hover && this.getTooltip() } +
); - return ( - - { thumbUrl && !this.state.imgError ? thumbnail : '' } - - - ); + return this.wrapImage(contentUrl, thumbnail); + } + + // Overidden by MStickerBody + wrapImage(contentUrl, children) { + return + {children} + ; + } + + // Overidden by MStickerBody + getPlaceholder() { + // MImageBody doesn't show a placeholder whilst the image loads, (but it could do) + return null; + } + + // Overidden by MStickerBody + getTooltip() { + return null; + } + + // Overidden by MStickerBody + getFileBody() { + return ; } render() { @@ -306,25 +333,6 @@ export default class extends React.Component { ); } - 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")) { @@ -333,6 +341,12 @@ export default class extends React.Component { thumbUrl = this._getThumbUrl(); } - return this._messageContent(contentUrl, thumbUrl, content); + const thumbnail = this._messageContent(contentUrl, thumbUrl, content); + const fileBody = this.getFileBody(); + + return + { thumbnail } + { fileBody } + ; } } diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js index aaad5ba75e..d9ed668e42 100644 --- a/src/components/views/messages/MStickerBody.js +++ b/src/components/views/messages/MStickerBody.js @@ -18,143 +18,39 @@ limitations under the License. import MImageBody from './MImageBody'; import sdk from '../../../index'; -import TintableSVG from '../elements/TintableSvg'; export default class MStickerBody extends MImageBody { - displayName: 'MStickerBody' - - constructor(props) { - super(props); - - this._onMouseEnter = this._onMouseEnter.bind(this); - this._onMouseLeave = this._onMouseLeave.bind(this); - this._onImageLoad = this._onImageLoad.bind(this); - } - - _onMouseEnter() { - this.setState({showTooltip: true}); - } - - _onMouseLeave() { - this.setState({showTooltip: false}); - } - - _onImageLoad() { - this.setState({ - placeholderClasses: 'mx_MStickerBody_placeholder_invisible', - }); - const hidePlaceholderTimer = setTimeout(() => { - this.setState({ - placeholderVisible: false, - imageClasses: 'mx_MStickerBody_image_visible', - }); - }, 500); - this.setState({hidePlaceholderTimer}); - if (this.props.onWidgetLoad) { - this.props.onWidgetLoad(); - } - } - - _afterComponentDidMount() { - if (this.refs.image.complete) { - // Image already loaded - this.setState({ - placeholderVisible: false, - placeholderClasses: '.mx_MStickerBody_placeholder_invisible', - imageClasses: 'mx_MStickerBody_image_visible', - }); - } else { - // Image not already loaded - this.setState({ - placeholderVisible: true, - placeholderClasses: '', - imageClasses: '', - }); - } - } - - _afterComponentWillUnmount() { - if (this.state.hidePlaceholderTimer) { - clearTimeout(this.state.hidePlaceholderTimer); - this.setState({hidePlaceholderTimer: null}); - } - } - - _messageContent(contentUrl, thumbUrl, content) { - let tooltip; - const tooltipBody = ( - this.props.mxEvent && - this.props.mxEvent.getContent() && - this.props.mxEvent.getContent().body) ? - this.props.mxEvent.getContent().body : null; - if (this.state.showTooltip && tooltipBody) { - const RoomTooltip = sdk.getComponent('rooms.RoomTooltip'); - tooltip = ; - } - - const gutterSize = 0; - let placeholderSize = 75; - let placeholderFixupHeight = '100px'; - let placeholderTop = 0; - let placeholderLeft = 0; - - if (content.info) { - placeholderTop = Math.floor((content.info.h/2) - (placeholderSize/2)) + 'px'; - placeholderLeft = Math.floor((content.info.w/2) - (placeholderSize/2) + gutterSize) + 'px'; - placeholderFixupHeight = content.info.h + 'px'; - } - - // The pixel size of sticker images is generally larger than their intended display - // size so they render at native reolution on HiDPI displays. We therefore need to - // explicity set the size so they render at the intended size. - // XXX: This will be clobberred when we run fixupHeight(), but we need to do it - // here otherwise the stickers are momentarily displayed at the pixel size. - const imageStyle = { - height: content.info.h, - // leave the browser the calculate the width automatically - }; - - placeholderSize = placeholderSize + 'px'; - - // Body 'ref' required by MImageBody - return ( - -
- { this.state.placeholderVisible && -
- -
} - {content.body} - { tooltip } -
-
- ); - } - // Empty to prevent default behaviour of MImageBody onClick() { } + + // MStickerBody doesn't need a wrapping ``, but it does need extra padding + // which is added by mx_MStickerBody_wrapper + wrapImage(contentUrl, children) { + return
{ children }
; + } + + // Placeholder to show in place of the sticker image if + // img onLoad hasn't fired yet. + getPlaceholder() { + const TintableSVG = sdk.getComponent('elements.TintableSvg'); + return ; + } + + // Tooltip to show on mouse over + getTooltip() { + const content = this.props.mxEvent && this.props.mxEvent.getContent(); + + if (!content || !content.body || !content.info || !content.info.w) return null; + + const RoomTooltip = sdk.getComponent('rooms.RoomTooltip'); + return
+ +
; + } + + // Don't show "Download this_file.png ..." + getFileBody() { + return null; + } } diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index 5365daee03..37fc94d1ed 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -147,12 +147,7 @@ module.exports = React.createClass({ // For now add an img tag with a spinner. return ( -
+
{content.body}