Merge branch 'media-modal-tsx' into 'develop'

MediaModal: convert to TSX

See merge request soapbox-pub/soapbox!1829
environments/review-develop-3zknud/deployments/1146
Alex Gleason 2022-10-13 18:26:22 +00:00
commit eb6de469f3
6 zmienionych plików z 371 dodań i 347 usunięć

Wyświetl plik

@ -1,6 +1,6 @@
'use strict';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { List as ImmutableList } from 'immutable';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Link, useHistory } from 'react-router-dom';
@ -23,6 +23,7 @@ import MovedNote from 'soapbox/features/account_timeline/components/moved_note';
import ActionButton from 'soapbox/features/ui/components/action-button';
import SubscriptionButton from 'soapbox/features/ui/components/subscription-button';
import { useAppDispatch, useFeatures, useOwnAccount } from 'soapbox/hooks';
import { normalizeAttachment } from 'soapbox/normalizers';
import { Account } from 'soapbox/types/entities';
import { isRemote } from 'soapbox/utils/accounts';
@ -207,12 +208,9 @@ const Header: React.FC<IHeader> = ({ account }) => {
};
const onAvatarClick = () => {
const avatar_url = account.avatar;
const avatar = ImmutableMap({
const avatar = normalizeAttachment({
type: 'image',
preview_url: avatar_url,
url: avatar_url,
description: '',
url: account.avatar,
});
dispatch(openModal('MEDIA', { media: ImmutableList.of(avatar), index: 0 }));
};
@ -225,12 +223,9 @@ const Header: React.FC<IHeader> = ({ account }) => {
};
const onHeaderClick = () => {
const header_url = account.header;
const header = ImmutableMap({
const header = normalizeAttachment({
type: 'image',
preview_url: header_url,
url: header_url,
description: '',
url: account.header,
});
dispatch(openModal('MEDIA', { media: ImmutableList.of(header), index: 0 }));
};

Wyświetl plik

@ -1,19 +1,20 @@
import classNames from 'clsx';
import PropTypes from 'prop-types';
import React from 'react';
import ZoomableImage from './zoomable_image';
import ZoomableImage from './zoomable-image';
export default class ImageLoader extends React.PureComponent {
type EventRemover = () => void;
static propTypes = {
alt: PropTypes.string,
src: PropTypes.string.isRequired,
previewSrc: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
onClick: PropTypes.func,
}
interface IImageLoader {
alt?: string,
src: string,
previewSrc?: string,
width?: number,
height?: number,
onClick?: React.MouseEventHandler,
}
class ImageLoader extends React.PureComponent<IImageLoader> {
static defaultProps = {
alt: '',
@ -27,8 +28,9 @@ export default class ImageLoader extends React.PureComponent {
width: null,
}
removers = [];
canvas = null;
removers: EventRemover[] = [];
canvas: HTMLCanvasElement | null = null;
_canvasContext: CanvasRenderingContext2D | null = null;
get canvasContext() {
if (!this.canvas) {
@ -42,7 +44,7 @@ export default class ImageLoader extends React.PureComponent {
this.loadImage(this.props);
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: IImageLoader) {
if (prevProps.src !== this.props.src) {
this.loadImage(this.props);
}
@ -52,7 +54,7 @@ export default class ImageLoader extends React.PureComponent {
this.removeEventListeners();
}
loadImage(props) {
loadImage(props: IImageLoader) {
this.removeEventListeners();
this.setState({ loading: true, error: false });
Promise.all([
@ -66,7 +68,7 @@ export default class ImageLoader extends React.PureComponent {
.catch(() => this.setState({ loading: false, error: true }));
}
loadPreviewCanvas = ({ previewSrc, width, height }) => new Promise((resolve, reject) => {
loadPreviewCanvas = ({ previewSrc, width, height }: IImageLoader) => new Promise<void>((resolve, reject) => {
const image = new Image();
const removeEventListeners = () => {
image.removeEventListener('error', handleError);
@ -78,21 +80,23 @@ export default class ImageLoader extends React.PureComponent {
};
const handleLoad = () => {
removeEventListeners();
this.canvasContext.drawImage(image, 0, 0, width, height);
this.canvasContext?.drawImage(image, 0, 0, width || 0, height || 0);
resolve();
};
image.addEventListener('error', handleError);
image.addEventListener('load', handleLoad);
image.src = previewSrc;
image.src = previewSrc || '';
this.removers.push(removeEventListeners);
})
clearPreviewCanvas() {
const { width, height } = this.canvas;
this.canvasContext.clearRect(0, 0, width, height);
if (this.canvas && this.canvasContext) {
const { width, height } = this.canvas;
this.canvasContext.clearRect(0, 0, width, height);
}
}
loadOriginalImage = ({ src }) => new Promise((resolve, reject) => {
loadOriginalImage = ({ src }: IImageLoader) => new Promise<void>((resolve, reject) => {
const image = new Image();
const removeEventListeners = () => {
image.removeEventListener('error', handleError);
@ -122,7 +126,7 @@ export default class ImageLoader extends React.PureComponent {
return typeof width === 'number' && typeof height === 'number';
}
setCanvasRef = c => {
setCanvasRef = (c: HTMLCanvasElement) => {
this.canvas = c;
if (c) this.setState({ width: c.offsetWidth });
}
@ -157,3 +161,5 @@ export default class ImageLoader extends React.PureComponent {
}
}
export default ImageLoader;

Wyświetl plik

@ -0,0 +1,300 @@
import classNames from 'clsx';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { useHistory } from 'react-router-dom';
import ReactSwipeableViews from 'react-swipeable-views';
import ExtendedVideoPlayer from 'soapbox/components/extended_video_player';
import Icon from 'soapbox/components/icon';
import IconButton from 'soapbox/components/icon_button';
import Audio from 'soapbox/features/audio';
import Video from 'soapbox/features/video';
import ImageLoader from './image-loader';
import type { List as ImmutableList } from 'immutable';
import type { Account, Attachment, Status } from 'soapbox/types/entities';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' },
});
interface IMediaModal {
media: ImmutableList<Attachment>,
status: Status,
account: Account,
index: number,
time?: number,
onClose: () => void,
}
const MediaModal: React.FC<IMediaModal> = (props) => {
const {
media,
status,
account,
onClose,
time = 0,
} = props;
const intl = useIntl();
const history = useHistory();
const [index, setIndex] = useState<number | null>(null);
const [navigationHidden, setNavigationHidden] = useState(false);
const handleSwipe = (index: number) => {
setIndex(index % media.size);
};
const handleNextClick = () => {
setIndex((getIndex() + 1) % media.size);
};
const handlePrevClick = () => {
setIndex((media.size + getIndex() - 1) % media.size);
};
const handleChangeIndex: React.MouseEventHandler<HTMLButtonElement> = (e) => {
const index = Number(e.currentTarget.getAttribute('data-index'));
setIndex(index % media.size);
};
const handleKeyDown = (e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowLeft':
handlePrevClick();
e.preventDefault();
e.stopPropagation();
break;
case 'ArrowRight':
handleNextClick();
e.preventDefault();
e.stopPropagation();
break;
}
};
useEffect(() => {
window.addEventListener('keydown', handleKeyDown, false);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);
const getIndex = () => {
return index !== null ? index : props.index;
};
const toggleNavigation = () => {
setNavigationHidden(!navigationHidden);
};
const handleStatusClick: React.MouseEventHandler = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
history.push(`/@${account.acct}/posts/${status.id}`);
onClose();
}
};
const handleCloserClick: React.MouseEventHandler = ({ currentTarget }) => {
const whitelist = ['zoomable-image'];
const activeSlide = document.querySelector('.media-modal .react-swipeable-view-container > div[aria-hidden="false"]');
const isClickOutside = currentTarget === activeSlide || !activeSlide?.contains(currentTarget);
const isWhitelisted = whitelist.some(w => currentTarget.classList.contains(w));
if (isClickOutside || isWhitelisted) {
onClose();
}
};
let pagination: React.ReactNode[] = [];
const leftNav = media.size > 1 && (
<button
tabIndex={0}
className='media-modal__nav media-modal__nav--left'
onClick={handlePrevClick}
aria-label={intl.formatMessage(messages.previous)}
>
<Icon src={require('@tabler/icons/arrow-left.svg')} />
</button>
);
const rightNav = media.size > 1 && (
<button
tabIndex={0}
className='media-modal__nav media-modal__nav--right'
onClick={handleNextClick}
aria-label={intl.formatMessage(messages.next)}
>
<Icon src={require('@tabler/icons/arrow-right.svg')} />
</button>
);
if (media.size > 1) {
pagination = media.toArray().map((item, i) => {
const classes = ['media-modal__button'];
if (i === getIndex()) {
classes.push('media-modal__button--active');
}
return (
<li className='media-modal__page-dot' key={i}>
<button
tabIndex={0}
className={classes.join(' ')}
onClick={handleChangeIndex}
data-index={i}
>
{i + 1}
</button>
</li>
);
});
}
const isMultiMedia = media.map((image) => {
if (image.type !== 'image') {
return true;
}
return false;
}).toArray();
const content = media.map(attachment => {
const width = (attachment.meta.getIn(['original', 'width']) || undefined) as number | undefined;
const height = (attachment.meta.getIn(['original', 'height']) || undefined) as number | undefined;
const link = (status && account && (
<a href={status.url} onClick={handleStatusClick}>
<FormattedMessage id='lightbox.view_context' defaultMessage='View context' />
</a>
));
if (attachment.type === 'image') {
return (
<ImageLoader
previewSrc={attachment.preview_url}
src={attachment.url}
width={width}
height={height}
alt={attachment.description}
key={attachment.url}
onClick={toggleNavigation}
/>
);
} else if (attachment.type === 'video') {
return (
<Video
preview={attachment.preview_url}
blurhash={attachment.blurhash}
src={attachment.url}
width={width}
height={height}
startTime={time}
onCloseVideo={onClose}
detailed
link={link}
alt={attachment.description}
key={attachment.url}
/>
);
} else if (attachment.type === 'audio') {
return (
<Audio
src={attachment.url}
alt={attachment.description}
poster={attachment.preview_url !== attachment.url ? attachment.preview_url : (status.getIn(['account', 'avatar_static'])) as string | undefined}
backgroundColor={attachment.meta.getIn(['colors', 'background']) as string | undefined}
foregroundColor={attachment.meta.getIn(['colors', 'foreground']) as string | undefined}
accentColor={attachment.meta.getIn(['colors', 'accent']) as string | undefined}
duration={attachment.meta.getIn(['original', 'duration'], 0) as number | undefined}
key={attachment.url}
/>
);
} else if (attachment.type === 'gifv') {
return (
<ExtendedVideoPlayer
src={attachment.url}
muted
controls={false}
width={width}
link={link}
height={height}
key={attachment.preview_url}
alt={attachment.description}
onClick={toggleNavigation}
/>
);
}
return null;
}).toArray();
// you can't use 100vh, because the viewport height is taller
// than the visible part of the document in some mobile
// browsers when it's address bar is visible.
// https://developers.google.com/web/updates/2016/12/url-bar-resizing
const swipeableViewsStyle: React.CSSProperties = {
width: '100%',
height: '100%',
};
const containerStyle: React.CSSProperties = {
alignItems: 'center', // center vertically
};
const navigationClassName = classNames('media-modal__navigation', {
'media-modal__navigation--hidden': navigationHidden,
});
return (
<div className='modal-root__modal media-modal'>
<div
className='media-modal__closer'
role='presentation'
onClick={handleCloserClick}
>
<ReactSwipeableViews
style={swipeableViewsStyle}
containerStyle={containerStyle}
onChangeIndex={handleSwipe}
index={getIndex()}
>
{content}
</ReactSwipeableViews>
</div>
<div className={navigationClassName}>
<IconButton
className='media-modal__close'
title={intl.formatMessage(messages.close)}
src={require('@tabler/icons/x.svg')}
onClick={onClose}
/>
{leftNav}
{rightNav}
{(status && !isMultiMedia[getIndex()]) && (
<div className={classNames('media-modal__meta', { 'media-modal__meta--shifted': media.size > 1 })}>
<a href={status.url} onClick={handleStatusClick}>
<FormattedMessage id='lightbox.view_context' defaultMessage='View context' />
</a>
</div>
)}
<ul className='media-modal__pagination'>
{pagination}
</ul>
</div>
</div>
);
};
export default MediaModal;

Wyświetl plik

@ -1,274 +0,0 @@
import classNames from 'clsx';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { withRouter } from 'react-router-dom';
import ReactSwipeableViews from 'react-swipeable-views';
import ExtendedVideoPlayer from 'soapbox/components/extended_video_player';
import Icon from 'soapbox/components/icon';
import IconButton from 'soapbox/components/icon_button';
import Audio from 'soapbox/features/audio';
import Video from 'soapbox/features/video';
import ImageLoader from './image_loader';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' },
});
export default @injectIntl @withRouter
class MediaModal extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.list.isRequired,
status: ImmutablePropTypes.record,
account: ImmutablePropTypes.record,
index: PropTypes.number.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
history: PropTypes.object,
};
state = {
index: null,
navigationHidden: false,
};
handleSwipe = (index) => {
this.setState({ index: index % this.props.media.size });
}
handleNextClick = () => {
this.setState({ index: (this.getIndex() + 1) % this.props.media.size });
}
handlePrevClick = () => {
this.setState({ index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size });
}
handleChangeIndex = (e) => {
const index = Number(e.currentTarget.getAttribute('data-index'));
this.setState({ index: index % this.props.media.size });
}
handleKeyDown = (e) => {
switch (e.key) {
case 'ArrowLeft':
this.handlePrevClick();
e.preventDefault();
e.stopPropagation();
break;
case 'ArrowRight':
this.handleNextClick();
e.preventDefault();
e.stopPropagation();
break;
}
}
componentDidMount() {
window.addEventListener('keydown', this.handleKeyDown, false);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.handleKeyDown);
}
getIndex() {
return this.state.index !== null ? this.state.index : this.props.index;
}
toggleNavigation = () => {
this.setState(prevState => ({
navigationHidden: !prevState.navigationHidden,
}));
};
handleStatusClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
const { status, account } = this.props;
const acct = account.get('acct');
const statusId = status.get('id');
this.props.history.push(`/@${acct}/posts/${statusId}`);
this.props.onClose(null, true);
}
}
handleCloserClick = ({ target }) => {
const whitelist = ['zoomable-image'];
const activeSlide = document.querySelector('.media-modal .react-swipeable-view-container > div[aria-hidden="false"]');
const isClickOutside = target === activeSlide || !activeSlide.contains(target);
const isWhitelisted = whitelist.some(w => target.classList.contains(w));
if (isClickOutside || isWhitelisted) {
this.props.onClose();
}
}
render() {
const { media, status, account, intl, onClose } = this.props;
const { navigationHidden } = this.state;
const index = this.getIndex();
let pagination = [];
const leftNav = media.size > 1 && (
<button tabIndex='0' className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}>
<Icon src={require('@tabler/icons/arrow-left.svg')} />
</button>
);
const rightNav = media.size > 1 && (
<button tabIndex='0' className='media-modal__nav media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}>
<Icon src={require('@tabler/icons/arrow-right.svg')} />
</button>
);
if (media.size > 1) {
pagination = media.map((item, i) => {
const classes = ['media-modal__button'];
if (i === index) {
classes.push('media-modal__button--active');
}
return (<li className='media-modal__page-dot' key={i}><button tabIndex='0' className={classes.join(' ')} onClick={this.handleChangeIndex} data-index={i}>{i + 1}</button></li>);
});
}
const isMultiMedia = media.map((image) => {
if (image.get('type') !== 'image') {
return true;
}
return false;
}).toArray();
const content = media.map(attachment => {
const width = attachment.getIn(['meta', 'original', 'width']) || null;
const height = attachment.getIn(['meta', 'original', 'height']) || null;
const link = (status && account && <a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>);
if (attachment.get('type') === 'image') {
return (
<ImageLoader
previewSrc={attachment.get('preview_url')}
src={attachment.get('url')}
width={width}
height={height}
alt={attachment.get('description')}
key={attachment.get('url')}
onClick={this.toggleNavigation}
/>
);
} else if (attachment.get('type') === 'video') {
const { time } = this.props;
return (
<Video
preview={attachment.get('preview_url')}
blurhash={attachment.get('blurhash')}
src={attachment.get('url')}
width={attachment.get('width')}
height={attachment.get('height')}
startTime={time || 0}
onCloseVideo={onClose}
detailed
link={link}
alt={attachment.get('description')}
key={attachment.get('url')}
/>
);
} else if (attachment.get('type') === 'audio') {
return (
<Audio
src={attachment.get('url')}
alt={attachment.get('description')}
poster={attachment.get('preview_url') !== attachment.get('url') ? attachment.get('preview_url') : (status && status.getIn(['account', 'avatar_static']))}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
key={attachment.get('url')}
/>
);
} else if (attachment.get('type') === 'gifv') {
return (
<ExtendedVideoPlayer
src={attachment.get('url')}
muted
controls={false}
width={width}
link={link}
height={height}
key={attachment.get('preview_url')}
alt={attachment.get('description')}
onClick={this.toggleNavigation}
/>
);
}
return null;
}).toArray();
// you can't use 100vh, because the viewport height is taller
// than the visible part of the document in some mobile
// browsers when it's address bar is visible.
// https://developers.google.com/web/updates/2016/12/url-bar-resizing
const swipeableViewsStyle = {
width: '100%',
height: '100%',
};
const containerStyle = {
alignItems: 'center', // center vertically
};
const navigationClassName = classNames('media-modal__navigation', {
'media-modal__navigation--hidden': navigationHidden,
});
return (
<div className='modal-root__modal media-modal'>
<div
className='media-modal__closer'
role='presentation'
onClick={this.handleCloserClick}
>
<ReactSwipeableViews
style={swipeableViewsStyle}
containerStyle={containerStyle}
onChangeIndex={this.handleSwipe}
onSwitching={this.handleSwitching}
index={index}
>
{content}
</ReactSwipeableViews>
</div>
<div className={navigationClassName}>
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} src={require('@tabler/icons/x.svg')} onClick={onClose} />
{leftNav}
{rightNav}
{(status && !isMultiMedia[index]) && (
<div className={classNames('media-modal__meta', { 'media-modal__meta--shifted': media.size > 1 })}>
<a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
</div>
)}
<ul className='media-modal__pagination'>
{pagination}
</ul>
</div>
</div>
);
}
}

Wyświetl plik

@ -1,28 +1,27 @@
import PropTypes from 'prop-types';
import React from 'react';
const MIN_SCALE = 1;
const MAX_SCALE = 4;
const getMidpoint = (p1, p2) => ({
type Point = { x: number, y: number };
const getMidpoint = (p1: React.Touch, p2: React.Touch): Point => ({
x: (p1.clientX + p2.clientX) / 2,
y: (p1.clientY + p2.clientY) / 2,
});
const getDistance = (p1, p2) =>
const getDistance = (p1: React.Touch, p2: React.Touch): number =>
Math.sqrt(Math.pow(p1.clientX - p2.clientX, 2) + Math.pow(p1.clientY - p2.clientY, 2));
const clamp = (min, max, value) => Math.min(max, Math.max(min, value));
const clamp = (min: number, max: number, value: number): number => Math.min(max, Math.max(min, value));
export default class ZoomableImage extends React.PureComponent {
interface IZoomableImage {
alt?: string,
src: string,
onClick?: React.MouseEventHandler,
}
static propTypes = {
alt: PropTypes.string,
src: PropTypes.string.isRequired,
width: PropTypes.number,
height: PropTypes.number,
onClick: PropTypes.func,
}
class ZoomableImage extends React.PureComponent<IZoomableImage> {
static defaultProps = {
alt: '',
@ -34,39 +33,32 @@ export default class ZoomableImage extends React.PureComponent {
scale: MIN_SCALE,
}
removers = [];
container = null;
image = null;
lastTouchEndTime = 0;
container: HTMLDivElement | null = null;
image: HTMLImageElement | null = null;
lastDistance = 0;
componentDidMount() {
let handler = this.handleTouchStart;
this.container.addEventListener('touchstart', handler);
this.removers.push(() => this.container.removeEventListener('touchstart', handler));
handler = this.handleTouchMove;
this.container?.addEventListener('touchstart', this.handleTouchStart);
// on Chrome 56+, touch event listeners will default to passive
// https://www.chromestatus.com/features/5093566007214080
this.container.addEventListener('touchmove', handler, { passive: false });
this.removers.push(() => this.container.removeEventListener('touchend', handler));
this.container?.addEventListener('touchmove', this.handleTouchMove, { passive: false });
}
componentWillUnmount() {
this.removeEventListeners();
this.container?.removeEventListener('touchstart', this.handleTouchStart);
this.container?.removeEventListener('touchend', this.handleTouchMove);
}
removeEventListeners() {
this.removers.forEach(listeners => listeners());
this.removers = [];
}
handleTouchStart = e => {
handleTouchStart = (e: TouchEvent) => {
if (e.touches.length !== 2) return;
const [p1, p2] = Array.from(e.touches);
this.lastDistance = getDistance(...e.touches);
this.lastDistance = getDistance(p1, p2);
}
handleTouchMove = e => {
handleTouchMove = (e: TouchEvent) => {
if (!this.container) return;
const { scrollTop, scrollHeight, clientHeight } = this.container;
if (e.touches.length === 1 && scrollTop !== scrollHeight - clientHeight) {
// prevent propagating event to MediaModal
@ -78,17 +70,19 @@ export default class ZoomableImage extends React.PureComponent {
e.preventDefault();
e.stopPropagation();
const distance = getDistance(...e.touches);
const midpoint = getMidpoint(...e.touches);
const [p1, p2] = Array.from(e.touches);
const distance = getDistance(p1, p2);
const midpoint = getMidpoint(p1, p2);
const scale = clamp(MIN_SCALE, MAX_SCALE, this.state.scale * distance / this.lastDistance);
this.zoom(scale, midpoint);
this.lastMidpoint = midpoint;
this.lastDistance = distance;
}
zoom(nextScale, midpoint) {
zoom(nextScale: number, midpoint: Point) {
if (!this.container) return;
const { scale } = this.state;
const { scrollLeft, scrollTop } = this.container;
@ -102,23 +96,24 @@ export default class ZoomableImage extends React.PureComponent {
const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y;
this.setState({ scale: nextScale }, () => {
if (!this.container) return;
this.container.scrollLeft = nextScrollLeft;
this.container.scrollTop = nextScrollTop;
});
}
handleClick = e => {
handleClick: React.MouseEventHandler = e => {
// don't propagate event to MediaModal
e.stopPropagation();
const handler = this.props.onClick;
if (handler) handler();
if (handler) handler(e);
}
setContainerRef = c => {
setContainerRef = (c: HTMLDivElement) => {
this.container = c;
}
setImageRef = c => {
setImageRef = (c: HTMLImageElement) => {
this.image = c;
}
@ -150,3 +145,5 @@ export default class ZoomableImage extends React.PureComponent {
}
}
export default ZoomableImage;

Wyświetl plik

@ -123,7 +123,7 @@ export function Audio() {
}
export function MediaModal() {
return import(/* webpackChunkName: "features/ui" */'../components/media_modal');
return import(/* webpackChunkName: "features/ui" */'../components/media-modal');
}
export function VideoModal() {