Convert MImageBody to TS

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
pull/21833/head
Šimon Brandner 2021-07-02 13:46:42 +02:00
parent 38710eab88
commit 869f31deef
No known key found for this signature in database
GPG Key ID: 9760693FDD98A790
1 changed files with 92 additions and 86 deletions

View File

@ -17,8 +17,6 @@ limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import MFileBody from './MFileBody'; import MFileBody from './MFileBody';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
@ -31,36 +29,48 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromContent } from "../../../customisations/Media"; import { mediaFromContent } from "../../../customisations/Media";
import BlurhashPlaceholder from "../elements/BlurhashPlaceholder"; import BlurhashPlaceholder from "../elements/BlurhashPlaceholder";
import { BLURHASH_FIELD } from "../../../ContentMessages"; import { BLURHASH_FIELD } from "../../../ContentMessages";
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent';
import { IProps as ImageViewIProps } from "../elements/ImageView";
export interface IProps {
/* the MatrixEvent to show */
mxEvent: MatrixEvent,
/* called when the image has loaded */
onHeightChanged(): void,
/* the maximum image height to use */
maxImageHeight?: number,
/* the permalinkCreator */
permalinkCreator?: RoomPermalinkCreator,
}
interface IState {
decryptedUrl?: string,
decryptedThumbnailUrl?: string,
decryptedBlob?: Blob,
error,
imgError: boolean,
imgLoaded: boolean,
loadedImageDimensions?: {
naturalWidth: number;
naturalHeight: number;
},
hover: boolean,
showImage: boolean,
}
@replaceableComponent("views.messages.MImageBody") @replaceableComponent("views.messages.MImageBody")
export default class MImageBody extends React.Component { export default class MImageBody extends React.Component<IProps, IState> {
static propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
/* called when the image has loaded */
onHeightChanged: PropTypes.func.isRequired,
/* the maximum image height to use */
maxImageHeight: PropTypes.number,
/* the permalinkCreator */
permalinkCreator: PropTypes.object,
};
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
private unmounted = true;
private image = createRef<HTMLImageElement>();
constructor(props) { constructor(props: IProps) {
super(props); super(props);
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._isGif = this._isGif.bind(this);
this.state = { this.state = {
decryptedUrl: null, decryptedUrl: null,
decryptedThumbnailUrl: null, decryptedThumbnailUrl: null,
@ -72,12 +82,10 @@ export default class MImageBody extends React.Component {
hover: false, hover: false,
showImage: SettingsStore.getValue("showImages"), showImage: SettingsStore.getValue("showImages"),
}; };
this._image = createRef();
} }
// FIXME: factor this out and apply it to MVideoBody and MAudioBody too! // FIXME: factor this out and apply it to MVideoBody and MAudioBody too!
onClientSync(syncState, prevState) { private onClientSync = (syncState, prevState): void => {
if (this.unmounted) return; if (this.unmounted) return;
// Consider the client reconnected if there is no error with syncing. // Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP. // This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
@ -88,15 +96,15 @@ export default class MImageBody extends React.Component {
imgError: false, imgError: false,
}); });
} }
} };
showImage() { protected showImage(): void {
localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true"); localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true");
this.setState({ showImage: true }); this.setState({ showImage: true });
this._downloadImage(); this.downloadImage();
} }
onClick(ev) { protected onClick = (ev: React.MouseEvent): void => {
if (ev.button === 0 && !ev.metaKey) { if (ev.button === 0 && !ev.metaKey) {
ev.preventDefault(); ev.preventDefault();
if (!this.state.showImage) { if (!this.state.showImage) {
@ -104,12 +112,12 @@ export default class MImageBody extends React.Component {
return; return;
} }
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent() as IMediaEventContent;
const httpUrl = this._getContentUrl(); const httpUrl = this.getContentUrl();
const ImageView = sdk.getComponent("elements.ImageView"); const ImageView = sdk.getComponent("elements.ImageView");
const params = { const params: ImageViewIProps = {
src: httpUrl, src: httpUrl,
name: content.body && content.body.length > 0 ? content.body : _t('Attachment'), name: content.body?.length > 0 ? content.body : _t('Attachment'),
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
permalinkCreator: this.props.permalinkCreator, permalinkCreator: this.props.permalinkCreator,
}; };
@ -122,58 +130,54 @@ export default class MImageBody extends React.Component {
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true); Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
} }
} };
_isGif() { private isGif = (): boolean => {
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent();
return ( return content?.info?.mimetype === "image/gif";
content && };
content.info &&
content.info.mimetype === "image/gif"
);
}
onImageEnter(e) { private onImageEnter = (e: React.MouseEvent): void => {
this.setState({ hover: true }); this.setState({ hover: true });
if (!this.state.showImage || !this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return; return;
} }
const imgElement = e.target; const imgElement = e.target as HTMLImageElement;
imgElement.src = this._getContentUrl(); imgElement.src = this.getContentUrl();
} };
onImageLeave(e) { private onImageLeave = (e: React.MouseEvent): void => {
this.setState({ hover: false }); this.setState({ hover: false });
if (!this.state.showImage || !this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return; return;
} }
const imgElement = e.target; const imgElement = e.target as HTMLImageElement;
imgElement.src = this._getThumbUrl(); imgElement.src = this.getThumbUrl();
} };
onImageError() { private onImageError = (): void => {
this.setState({ this.setState({
imgError: true, imgError: true,
}); });
} };
onImageLoad() { private onImageLoad = (): void => {
this.props.onHeightChanged(); this.props.onHeightChanged();
let loadedImageDimensions; let loadedImageDimensions;
if (this._image.current) { if (this.image.current) {
const { naturalWidth, naturalHeight } = this._image.current; const { naturalWidth, naturalHeight } = this.image.current;
// this is only used as a fallback in case content.info.w/h is missing // this is only used as a fallback in case content.info.w/h is missing
loadedImageDimensions = { naturalWidth, naturalHeight }; loadedImageDimensions = { naturalWidth, naturalHeight };
} }
this.setState({ imgLoaded: true, loadedImageDimensions }); this.setState({ imgLoaded: true, loadedImageDimensions });
} };
_getContentUrl() { protected getContentUrl(): string {
const media = mediaFromContent(this.props.mxEvent.getContent()); const media = mediaFromContent(this.props.mxEvent.getContent());
if (media.isEncrypted) { if (media.isEncrypted) {
return this.state.decryptedUrl; return this.state.decryptedUrl;
@ -182,7 +186,7 @@ export default class MImageBody extends React.Component {
} }
} }
_getThumbUrl() { protected getThumbUrl(): string {
// FIXME: we let images grow as wide as you like, rather than capped to 800x600. // FIXME: we let images grow as wide as you like, rather than capped to 800x600.
// So either we need to support custom timeline widths here, or reimpose the cap, otherwise the // So either we need to support custom timeline widths here, or reimpose the cap, otherwise the
// thumbnail resolution will be unnecessarily reduced. // thumbnail resolution will be unnecessarily reduced.
@ -190,7 +194,7 @@ export default class MImageBody extends React.Component {
const thumbWidth = 800; const thumbWidth = 800;
const thumbHeight = 600; const thumbHeight = 600;
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent() as IMediaEventContent;
const media = mediaFromContent(content); const media = mediaFromContent(content);
if (media.isEncrypted) { if (media.isEncrypted) {
@ -218,7 +222,7 @@ export default class MImageBody extends React.Component {
// - If there's no sizing info in the event, default to thumbnail // - If there's no sizing info in the event, default to thumbnail
const info = content.info; const info = content.info;
if ( if (
this._isGif() || this.isGif() ||
window.devicePixelRatio === 1.0 || window.devicePixelRatio === 1.0 ||
(!info || !info.w || !info.h || !info.size) (!info || !info.w || !info.h || !info.size)
) { ) {
@ -253,7 +257,7 @@ export default class MImageBody extends React.Component {
} }
} }
_downloadImage() { private downloadImage(): void {
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) { if (content.file !== undefined && this.state.decryptedUrl === null) {
let thumbnailPromise = Promise.resolve(null); let thumbnailPromise = Promise.resolve(null);
@ -297,7 +301,7 @@ export default class MImageBody extends React.Component {
if (showImage) { if (showImage) {
// Don't download anything becaue we don't want to display anything. // Don't download anything becaue we don't want to display anything.
this._downloadImage(); this.downloadImage();
this.setState({ showImage: true }); this.setState({ showImage: true });
} }
@ -327,7 +331,7 @@ export default class MImageBody extends React.Component {
_afterComponentWillUnmount() { _afterComponentWillUnmount() {
} }
_messageContent(contentUrl, thumbUrl, content) { protected messageContent(contentUrl: string, thumbUrl: string, content: IMediaEventContent): JSX.Element {
let infoWidth; let infoWidth;
let infoHeight; let infoHeight;
@ -348,7 +352,7 @@ export default class MImageBody extends React.Component {
imageElement = <HiddenImagePlaceholder />; imageElement = <HiddenImagePlaceholder />;
} else { } else {
imageElement = ( imageElement = (
<img style={{ display: 'none' }} src={thumbUrl} ref={this._image} <img style={{ display: 'none' }} src={thumbUrl} ref={this.image}
alt={content.body} alt={content.body}
onError={this.onImageError} onError={this.onImageError}
onLoad={this.onImageLoad} onLoad={this.onImageLoad}
@ -382,7 +386,7 @@ export default class MImageBody extends React.Component {
// which has the same width as the timeline // which has the same width as the timeline
// mx_MImageBody_thumbnail resizes img to exactly container size // mx_MImageBody_thumbnail resizes img to exactly container size
img = ( img = (
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref={this._image} <img className="mx_MImageBody_thumbnail" src={thumbUrl} ref={this.image}
style={{ maxWidth: maxWidth + "px" }} style={{ maxWidth: maxWidth + "px" }}
alt={content.body} alt={content.body}
onError={this.onImageError} onError={this.onImageError}
@ -393,11 +397,11 @@ export default class MImageBody extends React.Component {
} }
if (!this.state.showImage) { if (!this.state.showImage) {
img = <HiddenImagePlaceholder style={{ maxWidth: maxWidth + "px" }} />; img = <HiddenImagePlaceholder maxWidth={maxWidth} />;
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder. showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
} }
if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) { if (this.isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>; gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
} }
@ -427,14 +431,14 @@ export default class MImageBody extends React.Component {
} }
// Overidden by MStickerBody // Overidden by MStickerBody
wrapImage(contentUrl, children) { protected wrapImage(contentUrl: string, children: JSX.Element): JSX.Element {
return <a href={contentUrl} onClick={this.onClick}> return <a href={contentUrl} onClick={this.onClick}>
{children} {children}
</a>; </a>;
} }
// Overidden by MStickerBody // Overidden by MStickerBody
getPlaceholder(width, height) { protected getPlaceholder() {
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD]; const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
if (blurhash) return <BlurhashPlaceholder blurhash={blurhash} width={width} height={height} />; if (blurhash) return <BlurhashPlaceholder blurhash={blurhash} width={width} height={height} />;
return <div className="mx_MImageBody_thumbnail_spinner"> return <div className="mx_MImageBody_thumbnail_spinner">
@ -443,17 +447,17 @@ export default class MImageBody extends React.Component {
} }
// Overidden by MStickerBody // Overidden by MStickerBody
getTooltip() { protected getTooltip() {
return null; return null;
} }
// Overidden by MStickerBody // Overidden by MStickerBody
getFileBody() { protected getFileBody(): JSX.Element {
return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />; return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />;
} }
render() { render() {
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent() as IMediaEventContent;
if (this.state.error !== null) { if (this.state.error !== null) {
return ( return (
@ -464,15 +468,15 @@ export default class MImageBody extends React.Component {
); );
} }
const contentUrl = this._getContentUrl(); const contentUrl = this.getContentUrl();
let thumbUrl; let thumbUrl;
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) { if (this.isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
thumbUrl = contentUrl; thumbUrl = contentUrl;
} else { } else {
thumbUrl = this._getThumbUrl(); thumbUrl = this.getThumbUrl();
} }
const thumbnail = this._messageContent(contentUrl, thumbUrl, content); const thumbnail = this.messageContent(contentUrl, thumbUrl, content);
const fileBody = this.getFileBody(); const fileBody = this.getFileBody();
return <span className="mx_MImageBody"> return <span className="mx_MImageBody">
@ -482,16 +486,18 @@ export default class MImageBody extends React.Component {
} }
} }
export class HiddenImagePlaceholder extends React.PureComponent { interface PlaceholderIProps {
static propTypes = { hover?: boolean;
hover: PropTypes.bool, maxWidth?: number;
}; }
export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProps> {
render() { render() {
const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null;
let className = 'mx_HiddenImagePlaceholder'; let className = 'mx_HiddenImagePlaceholder';
if (this.props.hover) className += ' mx_HiddenImagePlaceholder_hover'; if (this.props.hover) className += ' mx_HiddenImagePlaceholder_hover';
return ( return (
<div className={className}> <div className={className} style={{ maxWidth: maxWidth }}>
<div className='mx_HiddenImagePlaceholder_button'> <div className='mx_HiddenImagePlaceholder_button'>
<span className='mx_HiddenImagePlaceholder_eye' /> <span className='mx_HiddenImagePlaceholder_eye' />
<span>{_t("Show image")}</span> <span>{_t("Show image")}</span>