mirror of
https://github.com/mastodon/mastodon.git
synced 2025-11-27 18:10:58 +00:00
Refactor: Media Modal (#36673)
This commit is contained in:
parent
13457111d5
commit
90d4b3b943
|
|
@ -10,7 +10,7 @@ import ModalRoot from 'mastodon/components/modal_root';
|
||||||
import { Poll } from 'mastodon/components/poll';
|
import { Poll } from 'mastodon/components/poll';
|
||||||
import { Audio } from 'mastodon/features/audio';
|
import { Audio } from 'mastodon/features/audio';
|
||||||
import Card from 'mastodon/features/status/components/card';
|
import Card from 'mastodon/features/status/components/card';
|
||||||
import MediaModal from 'mastodon/features/ui/components/media_modal';
|
import { MediaModal } from 'mastodon/features/ui/components/media_modal';
|
||||||
import { Video } from 'mastodon/features/video';
|
import { Video } from 'mastodon/features/video';
|
||||||
import { IntlProvider } from 'mastodon/locales';
|
import { IntlProvider } from 'mastodon/locales';
|
||||||
import { createPollFromServerJSON } from 'mastodon/models/poll';
|
import { createPollFromServerJSON } from 'mastodon/models/poll';
|
||||||
|
|
|
||||||
|
|
@ -1,296 +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 'mastodon/blurhash';
|
|
||||||
import { GIFV } from 'mastodon/components/gifv';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
|
||||||
import { Footer } from 'mastodon/features/picture_in_picture/components/footer';
|
|
||||||
import { Video } from 'mastodon/features/video';
|
|
||||||
import { disableSwiping } from 'mastodon/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'])}
|
|
||||||
aspectRatio={`${image.getIn(['meta', 'original', 'width'])} / ${image.getIn(['meta', 'original', 'height'])}`}
|
|
||||||
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);
|
|
||||||
363
app/javascript/mastodon/features/ui/components/media_modal.tsx
Normal file
363
app/javascript/mastodon/features/ui/components/media_modal.tsx
Normal file
|
|
@ -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 '@/mastodon/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 'mastodon/blurhash';
|
||||||
|
import { getAverageFromBlurhash } from 'mastodon/blurhash';
|
||||||
|
import { GIFV } from 'mastodon/components/gifv';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
import { Footer } from 'mastodon/features/picture_in_picture/components/footer';
|
||||||
|
import { Video } from 'mastodon/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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -43,7 +43,7 @@ import {
|
||||||
QuietPostQuoteInfoModal,
|
QuietPostQuoteInfoModal,
|
||||||
} from './confirmation_modals';
|
} from './confirmation_modals';
|
||||||
import { ImageModal } from './image_modal';
|
import { ImageModal } from './image_modal';
|
||||||
import MediaModal from './media_modal';
|
import { MediaModal } from './media_modal';
|
||||||
import { ModalPlaceholder } from './modal_placeholder';
|
import { ModalPlaceholder } from './modal_placeholder';
|
||||||
import VideoModal from './video_modal';
|
import VideoModal from './video_modal';
|
||||||
import { VisibilityModal } from './visibility_modal';
|
import { VisibilityModal } from './visibility_modal';
|
||||||
|
|
|
||||||
|
|
@ -2685,6 +2685,7 @@ a.account__display-name {
|
||||||
outline-offset: -1px;
|
outline-offset: -1px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--zoomed-in {
|
&--zoomed-in {
|
||||||
|
|
@ -6096,6 +6097,7 @@ a.status-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
touch-action: pan-y;
|
||||||
|
|
||||||
&__buttons {
|
&__buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -6131,11 +6133,17 @@ a.status-card {
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-modal__closer {
|
.media-modal__closer {
|
||||||
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-modal__navigation {
|
.media-modal__navigation {
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,6 @@
|
||||||
"react-router-dom": "^5.3.4",
|
"react-router-dom": "^5.3.4",
|
||||||
"react-select": "^5.7.3",
|
"react-select": "^5.7.3",
|
||||||
"react-sparklines": "^1.7.0",
|
"react-sparklines": "^1.7.0",
|
||||||
"react-swipeable-views": "^0.14.0",
|
|
||||||
"react-textarea-autosize": "^8.4.1",
|
"react-textarea-autosize": "^8.4.1",
|
||||||
"react-toggle": "^4.1.3",
|
"react-toggle": "^4.1.3",
|
||||||
"redux-immutable": "^4.0.0",
|
"redux-immutable": "^4.0.0",
|
||||||
|
|
@ -158,7 +157,6 @@
|
||||||
"@types/react-router": "^5.1.20",
|
"@types/react-router": "^5.1.20",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/react-sparklines": "^1.7.2",
|
"@types/react-sparklines": "^1.7.2",
|
||||||
"@types/react-swipeable-views": "^0.13.1",
|
|
||||||
"@types/react-test-renderer": "^18.0.0",
|
"@types/react-test-renderer": "^18.0.0",
|
||||||
"@types/react-toggle": "^4.0.3",
|
"@types/react-toggle": "^4.0.3",
|
||||||
"@types/redux-immutable": "^4.0.3",
|
"@types/redux-immutable": "^4.0.3",
|
||||||
|
|
|
||||||
103
yarn.lock
103
yarn.lock
|
|
@ -1174,16 +1174,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@babel/runtime@npm:7.0.0":
|
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
|
||||||
version: 7.0.0
|
|
||||||
resolution: "@babel/runtime@npm:7.0.0"
|
|
||||||
dependencies:
|
|
||||||
regenerator-runtime: "npm:^0.12.0"
|
|
||||||
checksum: 10c0/fbbdf86380a1cfa6ce32a743549f4e4c8b8eb06a18be5054441cc0f66e75a747ae43b042d8989f4657027e1be3b9a82069865ccc5080838f004abd1161093742
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
|
|
||||||
version: 7.27.0
|
version: 7.27.0
|
||||||
resolution: "@babel/runtime@npm:7.27.0"
|
resolution: "@babel/runtime@npm:7.27.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -2827,7 +2818,6 @@ __metadata:
|
||||||
"@types/react-router": "npm:^5.1.20"
|
"@types/react-router": "npm:^5.1.20"
|
||||||
"@types/react-router-dom": "npm:^5.3.3"
|
"@types/react-router-dom": "npm:^5.3.3"
|
||||||
"@types/react-sparklines": "npm:^1.7.2"
|
"@types/react-sparklines": "npm:^1.7.2"
|
||||||
"@types/react-swipeable-views": "npm:^0.13.1"
|
|
||||||
"@types/react-test-renderer": "npm:^18.0.0"
|
"@types/react-test-renderer": "npm:^18.0.0"
|
||||||
"@types/react-toggle": "npm:^4.0.3"
|
"@types/react-toggle": "npm:^4.0.3"
|
||||||
"@types/redux-immutable": "npm:^4.0.3"
|
"@types/redux-immutable": "npm:^4.0.3"
|
||||||
|
|
@ -2905,7 +2895,6 @@ __metadata:
|
||||||
react-router-dom: "npm:^5.3.4"
|
react-router-dom: "npm:^5.3.4"
|
||||||
react-select: "npm:^5.7.3"
|
react-select: "npm:^5.7.3"
|
||||||
react-sparklines: "npm:^1.7.0"
|
react-sparklines: "npm:^1.7.0"
|
||||||
react-swipeable-views: "npm:^0.14.0"
|
|
||||||
react-test-renderer: "npm:^18.2.0"
|
react-test-renderer: "npm:^18.2.0"
|
||||||
react-textarea-autosize: "npm:^8.4.1"
|
react-textarea-autosize: "npm:^8.4.1"
|
||||||
react-toggle: "npm:^4.1.3"
|
react-toggle: "npm:^4.1.3"
|
||||||
|
|
@ -4484,15 +4473,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/react-swipeable-views@npm:^0.13.1":
|
|
||||||
version: 0.13.6
|
|
||||||
resolution: "@types/react-swipeable-views@npm:0.13.6"
|
|
||||||
dependencies:
|
|
||||||
"@types/react": "npm:*"
|
|
||||||
checksum: 10c0/a26879146748417234bb7f44c5a71e6bab2b76c0b34c72f0493c18403487a5d77021510e8665bd8bd22786904fbbd90d6db55c8dd2bf983c32421139de851c94
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@types/react-test-renderer@npm:^18.0.0":
|
"@types/react-test-renderer@npm:^18.0.0":
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
resolution: "@types/react-test-renderer@npm:18.3.1"
|
resolution: "@types/react-test-renderer@npm:18.3.1"
|
||||||
|
|
@ -9221,13 +9201,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"keycode@npm:^2.1.7":
|
|
||||||
version: 2.2.1
|
|
||||||
resolution: "keycode@npm:2.2.1"
|
|
||||||
checksum: 10c0/e38ecbdc62f57e18ef9f7ad88aefded84e05b89115a40eea3081a7002d7c055765ddb5f5c3e2dd36ac2b2ab3901053c8286c9db082fd807b3abeb7e44034d96a
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"keyv@npm:^4.5.4":
|
"keyv@npm:^4.5.4":
|
||||||
version: 4.5.4
|
version: 4.5.4
|
||||||
resolution: "keyv@npm:4.5.4"
|
resolution: "keyv@npm:4.5.4"
|
||||||
|
|
@ -11267,7 +11240,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"prop-types@npm:^15.5.10, prop-types@npm:^15.5.4, prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
|
"prop-types@npm:^15.5.10, prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
|
||||||
version: 15.8.1
|
version: 15.8.1
|
||||||
resolution: "prop-types@npm:15.8.1"
|
resolution: "prop-types@npm:15.8.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -11427,19 +11400,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-event-listener@npm:^0.6.0":
|
|
||||||
version: 0.6.6
|
|
||||||
resolution: "react-event-listener@npm:0.6.6"
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime": "npm:^7.2.0"
|
|
||||||
prop-types: "npm:^15.6.0"
|
|
||||||
warning: "npm:^4.0.1"
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.3.0
|
|
||||||
checksum: 10c0/07ca093d74b7963cb6048953517f881a0fdee9b485d31dabd49814cda51543eee20e714dd423e25946984b0ac26dbcb80aaf1211b0170e9b0cfa92fa85b0984e
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-fast-compare@npm:^3.1.1":
|
"react-fast-compare@npm:^3.1.1":
|
||||||
version: 3.2.2
|
version: 3.2.2
|
||||||
resolution: "react-fast-compare@npm:3.2.2"
|
resolution: "react-fast-compare@npm:3.2.2"
|
||||||
|
|
@ -11682,49 +11642,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-swipeable-views-core@npm:^0.14.1":
|
|
||||||
version: 0.14.1
|
|
||||||
resolution: "react-swipeable-views-core@npm:0.14.1"
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime": "npm:7.0.0"
|
|
||||||
warning: "npm:^4.0.1"
|
|
||||||
peerDependencies:
|
|
||||||
react: ^15.3.0 || ^16.0.0 || ^17.0.0
|
|
||||||
checksum: 10c0/4da08493dad34f8498b66c596c1f40cd9a62e4968e71d00c79a26bf45e25ba3a3323e690641121cc8e7fd9c695e7da37fe781fa3f69e7d2b60fff3bfe4621426
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-swipeable-views-utils@npm:^0.14.1":
|
|
||||||
version: 0.14.1
|
|
||||||
resolution: "react-swipeable-views-utils@npm:0.14.1"
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime": "npm:7.0.0"
|
|
||||||
keycode: "npm:^2.1.7"
|
|
||||||
prop-types: "npm:^15.6.0"
|
|
||||||
react-event-listener: "npm:^0.6.0"
|
|
||||||
react-swipeable-views-core: "npm:^0.14.1"
|
|
||||||
shallow-equal: "npm:^1.2.1"
|
|
||||||
peerDependencies:
|
|
||||||
react: ^15.3.0 || ^16.0.0 || ^17.0.0
|
|
||||||
checksum: 10c0/f9a9930e7df9dab9cd0f5344c7cf0423cc8aff4728436eb7dc13588186fbd1a557c8250b6329ec09242f3f6d2700d33e1155ab191ce8de48aa860fad6fb1814b
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-swipeable-views@npm:^0.14.0":
|
|
||||||
version: 0.14.1
|
|
||||||
resolution: "react-swipeable-views@npm:0.14.1"
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime": "npm:7.0.0"
|
|
||||||
prop-types: "npm:^15.5.4"
|
|
||||||
react-swipeable-views-core: "npm:^0.14.1"
|
|
||||||
react-swipeable-views-utils: "npm:^0.14.1"
|
|
||||||
warning: "npm:^4.0.1"
|
|
||||||
peerDependencies:
|
|
||||||
react: ^15.3.0 || ^16.0.0 || ^17.0.0
|
|
||||||
checksum: 10c0/7ee6d19cc33172e0846835eafd4b24ece0f26aed5cc9bbe80d3ec7c8d22f327443df8b2d50ddb4b837e1d6fbb40744abc41f62cdcab8e941e0663a952d627a15
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-test-renderer@npm:^18.2.0":
|
"react-test-renderer@npm:^18.2.0":
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
resolution: "react-test-renderer@npm:18.3.1"
|
resolution: "react-test-renderer@npm:18.3.1"
|
||||||
|
|
@ -11923,13 +11840,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"regenerator-runtime@npm:^0.12.0":
|
|
||||||
version: 0.12.1
|
|
||||||
resolution: "regenerator-runtime@npm:0.12.1"
|
|
||||||
checksum: 10c0/dbefbb38f5d6d55261aec1182d7c97ac93f2eec6ef9c756d9656eb5152f5cb3f8d873df020ddb4a3a8126c2ac1a3c2e534413cffe18d8f89b1c2a480a0a38601
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"regenerator-runtime@npm:^0.13.3":
|
"regenerator-runtime@npm:^0.13.3":
|
||||||
version: 0.13.11
|
version: 0.13.11
|
||||||
resolution: "regenerator-runtime@npm:0.13.11"
|
resolution: "regenerator-runtime@npm:0.13.11"
|
||||||
|
|
@ -12521,13 +12431,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"shallow-equal@npm:^1.2.1":
|
|
||||||
version: 1.2.1
|
|
||||||
resolution: "shallow-equal@npm:1.2.1"
|
|
||||||
checksum: 10c0/51e03abadd97c9ebe590547d92db9148446962a3f23a3a0fb1ba2fccab80af881eef0ff1f8ccefd3f066c0bc5a4c8ca53706194813b95c8835fa66448a843a26
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"shebang-command@npm:^2.0.0":
|
"shebang-command@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "shebang-command@npm:2.0.0"
|
resolution: "shebang-command@npm:2.0.0"
|
||||||
|
|
@ -14377,7 +14280,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"warning@npm:^4.0.1, warning@npm:^4.0.3":
|
"warning@npm:^4.0.3":
|
||||||
version: 4.0.3
|
version: 4.0.3
|
||||||
resolution: "warning@npm:4.0.3"
|
resolution: "warning@npm:4.0.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user