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 = ; + } 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 = ; + } + const thumbnail = ( - - - +