Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Weblate 2018-06-14 10:17:20 +00:00
commit 5c80c4fd2f
5 changed files with 153 additions and 246 deletions

View file

@ -20,5 +20,29 @@ limitations under the License.
}
.mx_MImageBody_thumbnail {
max-width: 100%;
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%);
}

View file

@ -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 {
.mx_MStickerBody_tooltip {
position: absolute;
opacity: 1;
}
.mx_MStickerBody_placeholder_invisible {
transition: 500ms;
opacity: 0;
top: 50%;
}

View file

@ -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) {
const thumbnail = (
<a href={contentUrl} onClick={this.onClick}>
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
// The maximum height of the thumbnail as it is rendered as an <img>
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 = <img src="img/spinner.gif" alt={content.body} width="32" height="32" />;
} 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 = <img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
style={{ "max-width": maxWidth + "px" }}
alt={content.body}
onError={this.onImageError}
onLoad={this.onImageLoad}
onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />
</a>
onMouseLeave={this.onImageLeave} />;
}
const thumbnail = (
<div className="mx_MImageBody_thumbnail_container" style={{ "max-height": maxHeight + "px" }} >
{ /* Calculate aspect ratio, using %padding will size _container correctly */ }
<div style={{ paddingBottom: (100 * content.info.h / content.info.w) + '%' }}></div>
{ showPlaceholder &&
<div className="mx_MImageBody_thumbnail" style={{
// Constrain width here so that spinner appears central to the loaded thumbnail
"max-width": content.info.w + "px",
}}>
<div className="mx_MImageBody_thumbnail_spinner">
{ placeholder }
</div>
</div>
}
<div style={{display: !showPlaceholder ? undefined : 'none'}}>
{ img }
</div>
{ this.state.hover && this.getTooltip() }
</div>
);
return (
<span className="mx_MImageBody" ref="body">
{ thumbUrl && !this.state.imgError ? thumbnail : '' }
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
</span>
);
return this.wrapImage(contentUrl, thumbnail);
}
// Overidden by MStickerBody
wrapImage(contentUrl, children) {
return <a href={contentUrl} onClick={this.onClick}>
{children}
</a>;
}
// 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 <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />;
}
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 (
<span className="mx_MImageBody" ref="body">
<div className="mx_MImageBody_thumbnail" ref="image" style={{
"display": "flex",
"alignItems": "center",
"width": "100%",
}}>
<img src="img/spinner.gif" alt={content.body} width="32" height="32" style={{
"margin": "auto",
}} />
</div>
</span>
);
}
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 <span className="mx_MImageBody" ref="body">
{ thumbnail }
{ fileBody }
</span>;
}
}

View file

@ -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 = <RoomTooltip
className='mx_RoleButton_tooltip'
label={tooltipBody} />;
}
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 (
<span className='mx_MStickerBody' ref='body'
style={{
height: placeholderFixupHeight,
}}>
<div className={'mx_MStickerBody_image_container'}>
{ this.state.placeholderVisible &&
<div
className={'mx_MStickerBody_placeholder ' + this.state.placeholderClasses}
style={{
top: placeholderTop,
left: placeholderLeft,
}}
>
<TintableSVG
src={'img/icons-show-stickers.svg'}
width={placeholderSize}
height={placeholderSize} />
</div> }
<img
className={'mx_MStickerBody_image ' + this.state.imageClasses}
src={contentUrl}
style={imageStyle}
ref='image'
alt={content.body}
onLoad={this._onImageLoad}
onMouseEnter={this._onMouseEnter}
onMouseLeave={this._onMouseLeave}
/>
{ tooltip }
</div>
</span>
);
}
// Empty to prevent default behaviour of MImageBody
onClick() {
}
// MStickerBody doesn't need a wrapping `<a href=...>`, but it does need extra padding
// which is added by mx_MStickerBody_wrapper
wrapImage(contentUrl, children) {
return <div className="mx_MStickerBody_wrapper"> { children } </div>;
}
// Placeholder to show in place of the sticker image if
// img onLoad hasn't fired yet.
getPlaceholder() {
const TintableSVG = sdk.getComponent('elements.TintableSvg');
return <TintableSVG src="img/icons-show-stickers.svg" width="75" height="75" />;
}
// 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 <div style={{left: content.info.w + 'px'}} className="mx_MStickerBody_tooltip">
<RoomTooltip label={content.body} />
</div>;
}
// Don't show "Download this_file.png ..."
getFileBody() {
return null;
}
}

View file

@ -147,12 +147,7 @@ module.exports = React.createClass({
// For now add an img tag with a spinner.
return (
<span className="mx_MVideoBody" ref="body">
<div className="mx_MImageBody_thumbnail" ref="image" style={{
"display": "flex",
"align-items": "center",
"justify-items": "center",
"width": "100%",
}}>
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner" ref="image">
<img src="img/spinner.gif" alt={content.body} width="16" height="16" />
</div>
</span>