mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-13 15:58:50 +00:00
[Glitch] Refactor: Media Modal
Port 90d4b3b943 to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
@@ -10,7 +10,7 @@ import ModalRoot from 'flavours/glitch/components/modal_root';
|
||||
import { Poll } from 'flavours/glitch/components/poll';
|
||||
import { Audio } from 'flavours/glitch/features/audio';
|
||||
import Card from 'flavours/glitch/features/status/components/card';
|
||||
import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
|
||||
import { MediaModal } from 'flavours/glitch/features/ui/components/media_modal';
|
||||
import { Video } from 'flavours/glitch/features/video';
|
||||
import { IntlProvider } from 'flavours/glitch/locales';
|
||||
import { createPollFromServerJSON } from 'flavours/glitch/models/poll';
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import ReactSwipeableViews from 'react-swipeable-views';
|
||||
|
||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import FitScreenIcon from '@/material-icons/400-24px/fit_screen.svg?react';
|
||||
import ActualSizeIcon from '@/svg-icons/actual_size.svg?react';
|
||||
import { getAverageFromBlurhash } from 'flavours/glitch/blurhash';
|
||||
import { GIFV } from 'flavours/glitch/components/gifv';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import { Footer } from 'flavours/glitch/features/picture_in_picture/components/footer';
|
||||
import { Video } from 'flavours/glitch/features/video';
|
||||
import { disableSwiping } from 'flavours/glitch/initial_state';
|
||||
|
||||
import { ZoomableImage } from './zoomable_image';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
|
||||
next: { id: 'lightbox.next', defaultMessage: 'Next' },
|
||||
zoomIn: { id: 'lightbox.zoom_in', defaultMessage: 'Zoom to actual size' },
|
||||
zoomOut: { id: 'lightbox.zoom_out', defaultMessage: 'Zoom to fit' },
|
||||
});
|
||||
|
||||
class MediaModal extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.list.isRequired,
|
||||
statusId: PropTypes.string,
|
||||
lang: PropTypes.string,
|
||||
index: PropTypes.number.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChangeBackgroundColor: PropTypes.func.isRequired,
|
||||
currentTime: PropTypes.number,
|
||||
autoPlay: PropTypes.bool,
|
||||
volume: PropTypes.number,
|
||||
};
|
||||
|
||||
state = {
|
||||
index: null,
|
||||
navigationHidden: false,
|
||||
zoomedIn: false,
|
||||
};
|
||||
|
||||
handleZoomClick = () => {
|
||||
this.setState(prevState => ({
|
||||
zoomedIn: !prevState.zoomedIn,
|
||||
}));
|
||||
};
|
||||
|
||||
handleZoomChange = (zoomedIn) => {
|
||||
this.setState({
|
||||
zoomedIn,
|
||||
});
|
||||
};
|
||||
|
||||
handleSwipe = (index) => {
|
||||
this.setState({
|
||||
index: index % this.props.media.size,
|
||||
zoomedIn: false,
|
||||
});
|
||||
};
|
||||
|
||||
handleTransitionEnd = () => {
|
||||
this.setState({
|
||||
zoomedIn: false,
|
||||
});
|
||||
};
|
||||
|
||||
handleNextClick = () => {
|
||||
this.setState({
|
||||
index: (this.getIndex() + 1) % this.props.media.size,
|
||||
zoomedIn: false,
|
||||
});
|
||||
};
|
||||
|
||||
handlePrevClick = () => {
|
||||
this.setState({
|
||||
index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size,
|
||||
zoomedIn: false,
|
||||
});
|
||||
};
|
||||
|
||||
handleChangeIndex = (e) => {
|
||||
const index = Number(e.currentTarget.getAttribute('data-index'));
|
||||
|
||||
this.setState({
|
||||
index: index % this.props.media.size,
|
||||
zoomedIn: false,
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
this._sendBackgroundColor();
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
if (prevState.index !== this.state.index) {
|
||||
this._sendBackgroundColor();
|
||||
}
|
||||
}
|
||||
|
||||
_sendBackgroundColor () {
|
||||
const { media, onChangeBackgroundColor } = this.props;
|
||||
const index = this.getIndex();
|
||||
const blurhash = media.getIn([index, 'blurhash']);
|
||||
|
||||
if (blurhash) {
|
||||
const backgroundColor = getAverageFromBlurhash(blurhash);
|
||||
onChangeBackgroundColor(backgroundColor);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
|
||||
this.props.onChangeBackgroundColor(null);
|
||||
}
|
||||
|
||||
getIndex () {
|
||||
return this.state.index !== null ? this.state.index : this.props.index;
|
||||
}
|
||||
|
||||
handleToggleNavigation = () => {
|
||||
this.setState(prevState => ({
|
||||
navigationHidden: !prevState.navigationHidden,
|
||||
}));
|
||||
};
|
||||
|
||||
setRef = c => {
|
||||
this.setState({
|
||||
viewportWidth: c?.clientWidth,
|
||||
viewportHeight: c?.clientHeight,
|
||||
});
|
||||
};
|
||||
|
||||
render () {
|
||||
const { media, statusId, lang, intl, onClose } = this.props;
|
||||
const { navigationHidden, zoomedIn, viewportWidth, viewportHeight } = this.state;
|
||||
|
||||
const index = this.getIndex();
|
||||
|
||||
const leftNav = media.size > 1 && <button tabIndex={0} className='media-modal__nav media-modal__nav--prev' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>;
|
||||
const rightNav = media.size > 1 && <button tabIndex={0} className='media-modal__nav media-modal__nav--next' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>;
|
||||
|
||||
const content = media.map((image, idx) => {
|
||||
const width = image.getIn(['meta', 'original', 'width']) || null;
|
||||
const height = image.getIn(['meta', 'original', 'height']) || null;
|
||||
const description = image.getIn(['translation', 'description']) || image.get('description');
|
||||
|
||||
if (image.get('type') === 'image') {
|
||||
return (
|
||||
<ZoomableImage
|
||||
src={image.get('url')}
|
||||
blurhash={image.get('blurhash')}
|
||||
width={width}
|
||||
height={height}
|
||||
alt={description}
|
||||
lang={lang}
|
||||
key={image.get('url')}
|
||||
onClick={this.handleToggleNavigation}
|
||||
onDoubleClick={this.handleZoomClick}
|
||||
onClose={onClose}
|
||||
onZoomChange={this.handleZoomChange}
|
||||
zoomedIn={zoomedIn && idx === index}
|
||||
/>
|
||||
);
|
||||
} else if (image.get('type') === 'video') {
|
||||
const { currentTime, autoPlay, volume } = this.props;
|
||||
|
||||
return (
|
||||
<Video
|
||||
preview={image.get('preview_url')}
|
||||
blurhash={image.get('blurhash')}
|
||||
src={image.get('url')}
|
||||
frameRate={image.getIn(['meta', 'original', 'frame_rate'])}
|
||||
startTime={currentTime || 0}
|
||||
startPlaying={autoPlay || false}
|
||||
startVolume={volume || 1}
|
||||
onCloseVideo={onClose}
|
||||
detailed
|
||||
alt={description}
|
||||
lang={lang}
|
||||
key={image.get('url')}
|
||||
/>
|
||||
);
|
||||
} else if (image.get('type') === 'gifv') {
|
||||
return (
|
||||
<GIFV
|
||||
src={image.get('url')}
|
||||
key={image.get('url')}
|
||||
alt={description}
|
||||
lang={lang}
|
||||
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,
|
||||
});
|
||||
|
||||
let pagination;
|
||||
|
||||
if (media.size > 1) {
|
||||
pagination = media.map((item, i) => (
|
||||
<button key={i} className={classNames('media-modal__page-dot', { active: i === index })} data-index={i} onClick={this.handleChangeIndex}>
|
||||
{i + 1}
|
||||
</button>
|
||||
));
|
||||
}
|
||||
|
||||
const currentMedia = media.get(index);
|
||||
const zoomable = currentMedia.get('type') === 'image' && (currentMedia.getIn(['meta', 'original', 'width']) > viewportWidth || currentMedia.getIn(['meta', 'original', 'height']) > viewportHeight);
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal media-modal' ref={this.setRef}>
|
||||
<div className='media-modal__closer' role='presentation' onClick={onClose}>
|
||||
<ReactSwipeableViews
|
||||
style={swipeableViewsStyle}
|
||||
containerStyle={containerStyle}
|
||||
onChangeIndex={this.handleSwipe}
|
||||
onTransitionEnd={this.handleTransitionEnd}
|
||||
index={index}
|
||||
disabled={disableSwiping || zoomedIn}
|
||||
>
|
||||
{content}
|
||||
</ReactSwipeableViews>
|
||||
</div>
|
||||
|
||||
<div className={navigationClassName}>
|
||||
<div className='media-modal__buttons'>
|
||||
{zoomable && <IconButton title={intl.formatMessage(zoomedIn ? messages.zoomOut : messages.zoomIn)} iconComponent={zoomedIn ? FitScreenIcon : ActualSizeIcon} onClick={this.handleZoomClick} />}
|
||||
<IconButton title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} />
|
||||
</div>
|
||||
|
||||
{leftNav}
|
||||
{rightNav}
|
||||
|
||||
<div className='media-modal__overlay'>
|
||||
{pagination && <ul className='media-modal__pagination'>{pagination}</ul>}
|
||||
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(MediaModal);
|
||||
@@ -0,0 +1,363 @@
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { RefCallback, FC } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
|
||||
import { animated, useSpring } from '@react-spring/web';
|
||||
import { useDrag } from '@use-gesture/react';
|
||||
|
||||
import type { MediaAttachment } from '@/flavours/glitch/models/status';
|
||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import FitScreenIcon from '@/material-icons/400-24px/fit_screen.svg?react';
|
||||
import ActualSizeIcon from '@/svg-icons/actual_size.svg?react';
|
||||
import type { RGB } from 'flavours/glitch/blurhash';
|
||||
import { getAverageFromBlurhash } from 'flavours/glitch/blurhash';
|
||||
import { GIFV } from 'flavours/glitch/components/gifv';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import { Footer } from 'flavours/glitch/features/picture_in_picture/components/footer';
|
||||
import { Video } from 'flavours/glitch/features/video';
|
||||
|
||||
import { ZoomableImage } from './zoomable_image';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
|
||||
next: { id: 'lightbox.next', defaultMessage: 'Next' },
|
||||
zoomIn: { id: 'lightbox.zoom_in', defaultMessage: 'Zoom to actual size' },
|
||||
zoomOut: { id: 'lightbox.zoom_out', defaultMessage: 'Zoom to fit' },
|
||||
});
|
||||
|
||||
interface MediaModalProps {
|
||||
media: ImmutableList<MediaAttachment>;
|
||||
statusId?: string;
|
||||
lang?: string;
|
||||
index: number;
|
||||
onClose: () => void;
|
||||
onChangeBackgroundColor: (color: RGB | null) => void;
|
||||
currentTime?: number;
|
||||
autoPlay?: boolean;
|
||||
volume?: number;
|
||||
}
|
||||
|
||||
export const MediaModal: FC<MediaModalProps> = forwardRef<
|
||||
HTMLDivElement,
|
||||
MediaModalProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
media,
|
||||
onClose,
|
||||
index: startIndex,
|
||||
lang,
|
||||
currentTime,
|
||||
autoPlay,
|
||||
volume,
|
||||
statusId,
|
||||
onChangeBackgroundColor,
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- _ref is required to keep the ref forwarding working
|
||||
_ref,
|
||||
) => {
|
||||
const [index, setIndex] = useState(startIndex);
|
||||
const currentMedia = media.get(index);
|
||||
|
||||
const handleChangeIndex = useCallback(
|
||||
(newIndex: number) => {
|
||||
if (newIndex < 0) {
|
||||
newIndex = media.size + newIndex;
|
||||
}
|
||||
setIndex(newIndex % media.size);
|
||||
setZoomedIn(false);
|
||||
},
|
||||
[media.size],
|
||||
);
|
||||
const handlePrevClick = useCallback(() => {
|
||||
handleChangeIndex(index - 1);
|
||||
}, [handleChangeIndex, index]);
|
||||
const handleNextClick = useCallback(() => {
|
||||
handleChangeIndex(index + 1);
|
||||
}, [handleChangeIndex, index]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
handlePrevClick();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
handleNextClick();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
[handleNextClick, handlePrevClick],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown, false);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [handleKeyDown]);
|
||||
|
||||
useEffect(() => {
|
||||
const blurhash = currentMedia?.get('blurhash') as string | undefined;
|
||||
if (blurhash) {
|
||||
const backgroundColor = getAverageFromBlurhash(blurhash);
|
||||
if (backgroundColor) {
|
||||
onChangeBackgroundColor(backgroundColor);
|
||||
}
|
||||
}
|
||||
}, [currentMedia, onChangeBackgroundColor]);
|
||||
|
||||
const [viewportDimensions, setViewportDimensions] = useState<{
|
||||
width: number;
|
||||
height: number;
|
||||
}>({ width: 0, height: 0 });
|
||||
const handleRef: RefCallback<HTMLDivElement> = useCallback((ele) => {
|
||||
if (ele?.clientWidth && ele.clientHeight) {
|
||||
setViewportDimensions({
|
||||
width: ele.clientWidth,
|
||||
height: ele.clientHeight,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [zoomedIn, setZoomedIn] = useState(false);
|
||||
const zoomable =
|
||||
currentMedia?.get('type') === 'image' &&
|
||||
((currentMedia.getIn(['meta', 'original', 'width']) as number) >
|
||||
viewportDimensions.width ||
|
||||
(currentMedia.getIn(['meta', 'original', 'height']) as number) >
|
||||
viewportDimensions.height);
|
||||
const handleZoomClick = useCallback(() => {
|
||||
setZoomedIn((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const wrapperStyles = useSpring({
|
||||
x: `-${index * 100}%`,
|
||||
});
|
||||
const bind = useDrag(
|
||||
({ swipe: [swipeX] }) => {
|
||||
if (swipeX === 0) return;
|
||||
handleChangeIndex(index + swipeX * -1); // Invert swipe as swiping left loads the next slide.
|
||||
},
|
||||
{ pointer: { capture: false } },
|
||||
);
|
||||
|
||||
const [navigationHidden, setNavigationHidden] = useState(false);
|
||||
const handleToggleNavigation = useCallback(() => {
|
||||
setNavigationHidden((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const content = useMemo(
|
||||
() =>
|
||||
media.map((item, idx) => {
|
||||
const url = item.get('url') as string;
|
||||
const blurhash = item.get('blurhash') as string;
|
||||
const width = item.getIn(['meta', 'original', 'width'], 0) as number;
|
||||
const height = item.getIn(
|
||||
['meta', 'original', 'height'],
|
||||
0,
|
||||
) as number;
|
||||
const description = item.getIn(
|
||||
['translation', 'description'],
|
||||
item.get('description'),
|
||||
) as string;
|
||||
if (item.get('type') === 'image') {
|
||||
return (
|
||||
<ZoomableImage
|
||||
src={url}
|
||||
blurhash={blurhash}
|
||||
width={width}
|
||||
height={height}
|
||||
alt={description}
|
||||
lang={lang}
|
||||
key={url}
|
||||
onClick={handleToggleNavigation}
|
||||
onDoubleClick={handleZoomClick}
|
||||
onClose={onClose}
|
||||
onZoomChange={setZoomedIn}
|
||||
zoomedIn={zoomedIn && idx === index}
|
||||
/>
|
||||
);
|
||||
} else if (item.get('type') === 'video') {
|
||||
return (
|
||||
<Video
|
||||
preview={item.get('preview_url') as string | undefined}
|
||||
blurhash={blurhash}
|
||||
src={url}
|
||||
frameRate={
|
||||
item.getIn(['meta', 'original', 'frame_rate']) as
|
||||
| string
|
||||
| undefined
|
||||
}
|
||||
aspectRatio={`${width} / ${height}`}
|
||||
startTime={currentTime ?? 0}
|
||||
startPlaying={autoPlay ?? false}
|
||||
startVolume={volume ?? 1}
|
||||
onCloseVideo={onClose}
|
||||
detailed
|
||||
alt={description}
|
||||
lang={lang}
|
||||
key={url}
|
||||
/>
|
||||
);
|
||||
} else if (item.get('type') === 'gifv') {
|
||||
return (
|
||||
<GIFV
|
||||
src={url}
|
||||
key={url}
|
||||
alt={description}
|
||||
lang={lang}
|
||||
onClick={handleToggleNavigation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
[
|
||||
autoPlay,
|
||||
currentTime,
|
||||
handleToggleNavigation,
|
||||
handleZoomClick,
|
||||
index,
|
||||
lang,
|
||||
media,
|
||||
onClose,
|
||||
volume,
|
||||
zoomedIn,
|
||||
],
|
||||
);
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
const leftNav = media.size > 1 && (
|
||||
<button
|
||||
tabIndex={0}
|
||||
className='media-modal__nav media-modal__nav--prev'
|
||||
onClick={handlePrevClick}
|
||||
aria-label={intl.formatMessage(messages.previous)}
|
||||
>
|
||||
<Icon id='chevron-left' icon={ChevronLeftIcon} />
|
||||
</button>
|
||||
);
|
||||
const rightNav = media.size > 1 && (
|
||||
<button
|
||||
tabIndex={0}
|
||||
className='media-modal__nav media-modal__nav--next'
|
||||
onClick={handleNextClick}
|
||||
aria-label={intl.formatMessage(messages.next)}
|
||||
>
|
||||
<Icon id='chevron-right' icon={ChevronRightIcon} />
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...bind()}
|
||||
className='modal-root__modal media-modal'
|
||||
ref={handleRef}
|
||||
>
|
||||
<animated.div
|
||||
style={wrapperStyles}
|
||||
className='media-modal__closer'
|
||||
role='presentation'
|
||||
onClick={onClose}
|
||||
>
|
||||
{content}
|
||||
</animated.div>
|
||||
|
||||
<div
|
||||
className={classNames('media-modal__navigation', {
|
||||
'media-modal__navigation--hidden': navigationHidden,
|
||||
})}
|
||||
>
|
||||
<div className='media-modal__buttons'>
|
||||
{zoomable && (
|
||||
<IconButton
|
||||
title={intl.formatMessage(
|
||||
zoomedIn ? messages.zoomOut : messages.zoomIn,
|
||||
)}
|
||||
icon=''
|
||||
iconComponent={zoomedIn ? FitScreenIcon : ActualSizeIcon}
|
||||
onClick={handleZoomClick}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
title={intl.formatMessage(messages.close)}
|
||||
icon='times'
|
||||
iconComponent={CloseIcon}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{leftNav}
|
||||
{rightNav}
|
||||
|
||||
<div className='media-modal__overlay'>
|
||||
<MediaPagination
|
||||
itemsCount={media.size}
|
||||
index={index}
|
||||
onChangeIndex={handleChangeIndex}
|
||||
/>
|
||||
{statusId && (
|
||||
<Footer statusId={statusId} withOpenButton onClose={onClose} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
MediaModal.displayName = 'MediaModal';
|
||||
|
||||
interface MediaPaginationProps {
|
||||
itemsCount: number;
|
||||
index: number;
|
||||
onChangeIndex: (newIndex: number) => void;
|
||||
}
|
||||
|
||||
const MediaPagination: FC<MediaPaginationProps> = ({
|
||||
itemsCount,
|
||||
index,
|
||||
onChangeIndex,
|
||||
}) => {
|
||||
const handleChangeIndex = useCallback(
|
||||
(curIndex: number) => {
|
||||
return () => {
|
||||
onChangeIndex(curIndex);
|
||||
};
|
||||
},
|
||||
[onChangeIndex],
|
||||
);
|
||||
|
||||
if (itemsCount <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className='media-modal__pagination'>
|
||||
{Array.from({ length: itemsCount }).map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className={classNames('media-modal__page-dot', {
|
||||
active: i === index,
|
||||
})}
|
||||
onClick={handleChangeIndex(i)}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
@@ -47,7 +47,7 @@ import DeprecatedSettingsModal from './deprecated_settings_modal';
|
||||
import DoodleModal from './doodle_modal';
|
||||
import { FavouriteModal } from './favourite_modal';
|
||||
import { ImageModal } from './image_modal';
|
||||
import MediaModal from './media_modal';
|
||||
import { MediaModal } from './media_modal';
|
||||
import { ModalPlaceholder } from './modal_placeholder';
|
||||
import VideoModal from './video_modal';
|
||||
import { VisibilityModal } from './visibility_modal';
|
||||
|
||||
@@ -2750,6 +2750,7 @@ a.account__display-name {
|
||||
outline-offset: -1px;
|
||||
border-radius: 8px;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&--zoomed-in {
|
||||
@@ -6333,6 +6334,7 @@ a.status-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
touch-action: pan-y;
|
||||
|
||||
&__buttons {
|
||||
position: absolute;
|
||||
@@ -6368,11 +6370,17 @@ a.status-card {
|
||||
}
|
||||
|
||||
.media-modal__closer {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
bottom: 0;
|
||||
|
||||
> div {
|
||||
flex-shrink: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.media-modal__navigation {
|
||||
|
||||
Reference in New Issue
Block a user