From 1069546b50df87727c9c95602922d7e90f7a0b56 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 30 Sep 2022 18:05:23 -0500 Subject: [PATCH] MediaGallery: convert to TSX+FC (first pass) --- app/soapbox/components/media_gallery.js | 653 ----------------------- app/soapbox/components/media_gallery.tsx | 648 ++++++++++++++++++++++ app/soapbox/normalizers/attachment.ts | 2 +- 3 files changed, 649 insertions(+), 654 deletions(-) delete mode 100644 app/soapbox/components/media_gallery.js create mode 100644 app/soapbox/components/media_gallery.tsx diff --git a/app/soapbox/components/media_gallery.js b/app/soapbox/components/media_gallery.js deleted file mode 100644 index 7bbbbd5b7..000000000 --- a/app/soapbox/components/media_gallery.js +++ /dev/null @@ -1,653 +0,0 @@ -import classNames from 'clsx'; -import { Map as ImmutableMap, is } from 'immutable'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import { getSettings } from 'soapbox/actions/settings'; -import Blurhash from 'soapbox/components/blurhash'; -import Icon from 'soapbox/components/icon'; -import StillImage from 'soapbox/components/still_image'; -import { MIMETYPE_ICONS } from 'soapbox/features/compose/components/upload'; -import { truncateFilename } from 'soapbox/utils/media'; - -import { isIOS } from '../is_mobile'; -import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media_aspect_ratio'; - -import { Button, Text } from './ui'; - -const ATTACHMENT_LIMIT = 4; -const MAX_FILENAME_LENGTH = 45; - -const messages = defineMessages({ - toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Hide' }, -}); - -const mapStateToItemProps = state => ({ - autoPlayGif: getSettings(state).get('autoPlayGif'), -}); - -const withinLimits = aspectRatio => { - return aspectRatio >= minimumAspectRatio && aspectRatio <= maximumAspectRatio; -}; - -const shouldLetterbox = attachment => { - const aspectRatio = attachment.getIn(['meta', 'original', 'aspect']); - if (!aspectRatio) return true; - - return !withinLimits(aspectRatio); -}; - -@connect(mapStateToItemProps) -class Item extends React.PureComponent { - - static propTypes = { - attachment: ImmutablePropTypes.record.isRequired, - standalone: PropTypes.bool, - index: PropTypes.number.isRequired, - size: PropTypes.number.isRequired, - onClick: PropTypes.func.isRequired, - displayWidth: PropTypes.number, - visible: PropTypes.bool.isRequired, - dimensions: PropTypes.object, - autoPlayGif: PropTypes.bool, - last: PropTypes.bool, - total: PropTypes.number, - }; - - static defaultProps = { - standalone: false, - index: 0, - size: 1, - }; - - state = { - loaded: false, - }; - - handleMouseEnter = (e) => { - if (this.hoverToPlay()) { - e.target.play(); - } - } - - handleMouseLeave = (e) => { - if (this.hoverToPlay()) { - e.target.pause(); - e.target.currentTime = 0; - } - } - - hoverToPlay() { - const { attachment, autoPlayGif } = this.props; - return !autoPlayGif && attachment.get('type') === 'gifv'; - } - - handleClick = (e) => { - const { index, onClick } = this.props; - - if (isIOS() && !e.target.autoPlay) { - e.target.autoPlay = true; - e.preventDefault(); - } else { - if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { - if (this.hoverToPlay()) { - e.target.pause(); - e.target.currentTime = 0; - } - e.preventDefault(); - onClick(index); - } - } - - e.stopPropagation(); - } - - handleImageLoad = () => { - this.setState({ loaded: true }); - } - - handleVideoHover = ({ target: video }) => { - video.playbackRate = 3.0; - video.play(); - } - - handleVideoLeave = ({ target: video }) => { - video.pause(); - video.currentTime = 0; - } - - render() { - const { attachment, standalone, visible, dimensions, autoPlayGif, last, total } = this.props; - - let width = 100; - let height = '100%'; - let top = 'auto'; - let left = 'auto'; - let bottom = 'auto'; - let right = 'auto'; - let float = 'left'; - let position = 'relative'; - - if (dimensions) { - width = dimensions.w; - height = dimensions.h; - top = dimensions.t || 'auto'; - right = dimensions.r || 'auto'; - bottom = dimensions.b || 'auto'; - left = dimensions.l || 'auto'; - float = dimensions.float || 'left'; - position = dimensions.pos || 'relative'; - } - - let thumbnail = ''; - - if (attachment.get('type') === 'unknown') { - const filename = truncateFilename(attachment.get('remote_url'), MAX_FILENAME_LENGTH); - const attachmentIcon = ( - - ); - - return ( -
- - - {attachmentIcon} - {filename} - -
- ); - } else if (attachment.get('type') === 'image') { - const originalUrl = attachment.get('url'); - const letterboxed = shouldLetterbox(attachment); - - thumbnail = ( - - - - ); - } else if (attachment.get('type') === 'gifv') { - const conditionalAttributes = {}; - if (isIOS()) { - conditionalAttributes.playsInline = '1'; - } - if (autoPlayGif) { - conditionalAttributes.autoPlay = '1'; - } - - thumbnail = ( -
-
- ); - } else if (attachment.get('type') === 'audio') { - const ext = attachment.get('url').split('.').pop().toUpperCase(); - thumbnail = ( - - - {ext} - - ); - } else if (attachment.get('type') === 'video') { - const ext = attachment.get('url').split('.').pop().toUpperCase(); - thumbnail = ( - - - {ext} - - ); - } - - return ( -
- {last && total > ATTACHMENT_LIMIT && ( -
- +{total - ATTACHMENT_LIMIT + 1} -
- )} - - {visible && thumbnail} -
- ); - } - -} - -const mapStateToMediaGalleryProps = state => ({ - displayMedia: getSettings(state).get('displayMedia'), -}); - -export default @connect(mapStateToMediaGalleryProps) -@injectIntl -class MediaGallery extends React.PureComponent { - - static propTypes = { - sensitive: PropTypes.bool, - standalone: PropTypes.bool, - media: ImmutablePropTypes.list.isRequired, - size: PropTypes.object, - height: PropTypes.number.isRequired, - onOpenMedia: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - defaultWidth: PropTypes.number, - cacheWidth: PropTypes.func, - visible: PropTypes.bool, - onToggleVisibility: PropTypes.func, - displayMedia: PropTypes.string, - compact: PropTypes.bool, - }; - - static defaultProps = { - standalone: false, - }; - - state = { - visible: this.props.visible !== undefined ? this.props.visible : (this.props.displayMedia !== 'hide_all' && !this.props.sensitive || this.props.displayMedia === 'show_all'), - width: this.props.defaultWidth, - }; - - componentDidUpdate(prevProps) { - const { media, visible, sensitive } = this.props; - if (!is(media, prevProps.media) && visible === undefined) { - this.setState({ visible: prevProps.displayMedia !== 'hide_all' && !sensitive || prevProps.displayMedia === 'show_all' }); - } else if (!is(visible, prevProps.visible) && visible !== undefined) { - this.setState({ visible }); - } - } - - handleOpen = (e) => { - e.stopPropagation(); - - if (this.props.onToggleVisibility) { - this.props.onToggleVisibility(); - } else { - this.setState({ visible: !this.state.visible }); - } - } - - handleClick = (index) => { - this.props.onOpenMedia(this.props.media, index); - } - - handleRef = (node) => { - if (node) { - // offsetWidth triggers a layout, so only calculate when we need to - if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth); - - this.setState({ - width: node.offsetWidth, - }); - } - } - - getSizeDataSingle = () => { - const { media, defaultWidth } = this.props; - const width = this.state.width || defaultWidth; - const aspectRatio = media.getIn([0, 'meta', 'original', 'aspect']); - - const getHeight = () => { - if (!aspectRatio) return width * 9 / 16; - if (isPanoramic(aspectRatio)) return Math.floor(width / maximumAspectRatio); - if (isPortrait(aspectRatio)) return Math.floor(width / minimumAspectRatio); - return Math.floor(width / aspectRatio); - }; - - return ImmutableMap({ - style: { height: getHeight() }, - itemsDimensions: [], - size: 1, - width, - }); - } - - getSizeDataMultiple = size => { - const { media, defaultWidth } = this.props; - const width = this.state.width || defaultWidth; - const panoSize = Math.floor(width / maximumAspectRatio); - const panoSize_px = `${Math.floor(width / maximumAspectRatio)}px`; - - const style = {}; - let itemsDimensions = []; - - const ratios = Array(size).fill().map((_, i) => - media.getIn([i, 'meta', 'original', 'aspect']), - ); - - const [ar1, ar2, ar3, ar4] = ratios; - - if (size === 2) { - if (isPortrait(ar1) && isPortrait(ar2)) { - style.height = width - (width / maximumAspectRatio); - } else if (isPanoramic(ar1) && isPanoramic(ar2)) { - style.height = panoSize * 2; - } else if ( - (isPanoramic(ar1) && isPortrait(ar2)) || - (isPortrait(ar1) && isPanoramic(ar2)) || - (isPanoramic(ar1) && isNonConformingRatio(ar2)) || - (isNonConformingRatio(ar1) && isPanoramic(ar2)) - ) { - style.height = (width * 0.6) + (width / maximumAspectRatio); - } else { - style.height = width / 2; - } - - // - - if (isPortrait(ar1) && isPortrait(ar2)) { - itemsDimensions = [ - { w: 50, h: '100%', r: '2px' }, - { w: 50, h: '100%', l: '2px' }, - ]; - } else if (isPanoramic(ar1) && isPanoramic(ar2)) { - itemsDimensions = [ - { w: 100, h: panoSize_px, b: '2px' }, - { w: 100, h: panoSize_px, t: '2px' }, - ]; - } else if ( - (isPanoramic(ar1) && isPortrait(ar2)) || - (isPanoramic(ar1) && isNonConformingRatio(ar2)) - ) { - itemsDimensions = [ - { w: 100, h: `${(width / maximumAspectRatio)}px`, b: '2px' }, - { w: 100, h: `${(width * 0.6)}px`, t: '2px' }, - ]; - } else if ( - (isPortrait(ar1) && isPanoramic(ar2)) || - (isNonConformingRatio(ar1) && isPanoramic(ar2)) - ) { - itemsDimensions = [ - { w: 100, h: `${(width * 0.6)}px`, b: '2px' }, - { w: 100, h: `${(width / maximumAspectRatio)}px`, t: '2px' }, - ]; - } else { - itemsDimensions = [ - { w: 50, h: '100%', r: '2px' }, - { w: 50, h: '100%', l: '2px' }, - ]; - } - } else if (size === 3) { - if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) { - style.height = panoSize * 3; - } else if (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3)) { - style.height = Math.floor(width / minimumAspectRatio); - } else { - style.height = width; - } - - // - - if (isPanoramic(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) { - itemsDimensions = [ - { w: 100, h: '50%', b: '2px' }, - { w: 50, h: '50%', t: '2px', r: '2px' }, - { w: 50, h: '50%', t: '2px', l: '2px' }, - ]; - } else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) { - itemsDimensions = [ - { w: 100, h: panoSize_px, b: '4px' }, - { w: 100, h: panoSize_px }, - { w: 100, h: panoSize_px, t: '4px' }, - ]; - } else if (isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) { - itemsDimensions = [ - { w: 50, h: '100%', r: '2px' }, - { w: 50, h: '50%', b: '2px', l: '2px' }, - { w: 50, h: '50%', t: '2px', l: '2px' }, - ]; - } else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3)) { - itemsDimensions = [ - { w: 50, h: '50%', b: '2px', r: '2px' }, - { w: 50, h: '50%', l: '-2px', b: '-2px', pos: 'absolute', float: 'none' }, - { w: 50, h: '100%', r: '-2px', t: '0px', b: '0px', pos: 'absolute', float: 'none' }, - ]; - } else if ( - (isNonConformingRatio(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3)) || - (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3)) - ) { - itemsDimensions = [ - { w: 50, h: '50%', b: '2px', r: '2px' }, - { w: 50, h: '100%', l: '2px', float: 'right' }, - { w: 50, h: '50%', t: '2px', r: '2px' }, - ]; - } else if ( - (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3)) || - (isPanoramic(ar1) && isPanoramic(ar2) && isPortrait(ar3)) - ) { - itemsDimensions = [ - { w: 50, h: panoSize_px, b: '2px', r: '2px' }, - { w: 50, h: panoSize_px, b: '2px', l: '2px' }, - { w: 100, h: `${width - panoSize}px`, t: '2px' }, - ]; - } else if ( - (isNonConformingRatio(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) || - (isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) - ) { - itemsDimensions = [ - { w: 100, h: `${width - panoSize}px`, b: '2px' }, - { w: 50, h: panoSize_px, t: '2px', r: '2px' }, - { w: 50, h: panoSize_px, t: '2px', l: '2px' }, - ]; - } else { - itemsDimensions = [ - { w: 50, h: '50%', b: '2px', r: '2px' }, - { w: 50, h: '50%', b: '2px', l: '2px' }, - { w: 100, h: '50%', t: '2px' }, - ]; - } - } else if (size >= 4) { - if ( - (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3) && isPortrait(ar4)) || - (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3) && isNonConformingRatio(ar4)) || - (isPortrait(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3) && isPortrait(ar4)) || - (isPortrait(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3) && isPortrait(ar4)) || - (isNonConformingRatio(ar1) && isPortrait(ar2) && isPortrait(ar3) && isPortrait(ar4)) - ) { - style.height = Math.floor(width / minimumAspectRatio); - } else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) { - style.height = panoSize * 2; - } else if ( - (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) || - (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) - ) { - style.height = panoSize + (width / 2); - } else { - style.height = width; - } - - // - - if (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) { - itemsDimensions = [ - { w: 50, h: panoSize_px, b: '2px', r: '2px' }, - { w: 50, h: panoSize_px, b: '2px', l: '2px' }, - { w: 50, h: `${(width / 2)}px`, t: '2px', r: '2px' }, - { w: 50, h: `${(width / 2)}px`, t: '2px', l: '2px' }, - ]; - } else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) { - itemsDimensions = [ - { w: 50, h: `${(width / 2)}px`, b: '2px', r: '2px' }, - { w: 50, h: `${(width / 2)}px`, b: '2px', l: '2px' }, - { w: 50, h: panoSize_px, t: '2px', r: '2px' }, - { w: 50, h: panoSize_px, t: '2px', l: '2px' }, - ]; - } else if ( - (isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) || - (isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) - ) { - itemsDimensions = [ - { w: 67, h: '100%', r: '2px' }, - { w: 33, h: '33%', b: '4px', l: '2px' }, - { w: 33, h: '33%', l: '2px' }, - { w: 33, h: '33%', t: '4px', l: '2px' }, - ]; - } else { - itemsDimensions = [ - { w: 50, h: '50%', b: '2px', r: '2px' }, - { w: 50, h: '50%', b: '2px', l: '2px' }, - { w: 50, h: '50%', t: '2px', r: '2px' }, - { w: 50, h: '50%', t: '2px', l: '2px' }, - ]; - } - } - - return ImmutableMap({ - style, - itemsDimensions, - size: size, - width, - }); - - } - - getSizeData = size => { - const { height, defaultWidth } = this.props; - const width = this.state.width || defaultWidth; - - if (width) { - if (size === 1) return this.getSizeDataSingle(); - if (size > 1) return this.getSizeDataMultiple(size); - } - - // Default - return ImmutableMap({ - style: { height }, - itemsDimensions: [], - size, - width, - }); - } - - render() { - const { media, intl, sensitive, compact } = this.props; - const { visible } = this.state; - const sizeData = this.getSizeData(media.size); - - const children = media.take(ATTACHMENT_LIMIT).map((attachment, i) => ( - - )); - - let warning; - - if (sensitive) { - warning = ; - } else { - warning = ; - } - - return ( -
-
- {sensitive && ( - (visible || compact) ? ( - -
-
- ) - )} - - - {children} - - ); - } - -} diff --git a/app/soapbox/components/media_gallery.tsx b/app/soapbox/components/media_gallery.tsx new file mode 100644 index 000000000..0744361a4 --- /dev/null +++ b/app/soapbox/components/media_gallery.tsx @@ -0,0 +1,648 @@ +import classNames from 'clsx'; +import React, { useState, useRef, useEffect } from 'react'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; + +import Blurhash from 'soapbox/components/blurhash'; +import Icon from 'soapbox/components/icon'; +import StillImage from 'soapbox/components/still_image'; +import { MIMETYPE_ICONS } from 'soapbox/features/compose/components/upload'; +import { useSettings } from 'soapbox/hooks'; +import { Attachment } from 'soapbox/types/entities'; +import { truncateFilename } from 'soapbox/utils/media'; + +import { isIOS } from '../is_mobile'; +import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media_aspect_ratio'; + +import { Button, Text } from './ui'; + +import type { Property } from 'csstype'; +import type { List as ImmutableList } from 'immutable'; + +const ATTACHMENT_LIMIT = 4; +const MAX_FILENAME_LENGTH = 45; + +interface Dimensions { + w: Property.Width | number, + h: Property.Height | number, + t?: Property.Top, + r?: Property.Right, + b?: Property.Bottom, + l?: Property.Left, + float?: Property.Float, + pos?: Property.Position, +} + +interface SizeData { + style: React.CSSProperties, + itemsDimensions: Dimensions[], + size: number, + width: number, +} + +const messages = defineMessages({ + toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Hide' }, +}); + +const withinLimits = (aspectRatio: number) => { + return aspectRatio >= minimumAspectRatio && aspectRatio <= maximumAspectRatio; +}; + +const shouldLetterbox = (attachment: Attachment): boolean => { + const aspectRatio = attachment.getIn(['meta', 'original', 'aspect']) as number | undefined; + if (!aspectRatio) return true; + + return !withinLimits(aspectRatio); +}; + +interface IItem { + attachment: Attachment, + standalone?: boolean, + index: number, + size: number, + onClick: (index: number) => void, + displayWidth?: number, + visible: boolean, + dimensions: Dimensions, + last?: boolean, + total: number, +} + +const Item: React.FC = ({ + attachment, + index, + onClick, + standalone = false, + visible, + dimensions, + last, + total, +}) => { + const settings = useSettings(); + const autoPlayGif = settings.get('autoPlayGif') === true; + + const [loaded, setLoaded] = useState(false); + + const handleMouseEnter: React.MouseEventHandler = ({ currentTarget: video }) => { + if (hoverToPlay()) { + video.play(); + } + }; + + const handleMouseLeave: React.MouseEventHandler = ({ currentTarget: video }) => { + if (hoverToPlay()) { + video.pause(); + video.currentTime = 0; + } + }; + + const hoverToPlay = () => { + return !autoPlayGif && attachment.type === 'gifv'; + }; + + // FIXME: wtf? + const handleClick: React.MouseEventHandler = (e: any) => { + if (isIOS() && !e.target.autoPlay) { + e.target.autoPlay = true; + e.preventDefault(); + } else { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + if (hoverToPlay()) { + e.target.pause(); + e.target.currentTime = 0; + } + e.preventDefault(); + onClick(index); + } + } + + e.stopPropagation(); + }; + + const handleImageLoad = () => { + setLoaded(true); + }; + + const handleVideoHover: React.MouseEventHandler = ({ currentTarget: video }) => { + video.playbackRate = 3.0; + video.play(); + }; + + const handleVideoLeave: React.MouseEventHandler = ({ currentTarget: video }) => { + video.pause(); + video.currentTime = 0; + }; + + let width: Dimensions['w'] = 100; + let height: Dimensions['h'] = '100%'; + let top: Dimensions['t'] = 'auto'; + let left: Dimensions['l'] = 'auto'; + let bottom: Dimensions['b'] = 'auto'; + let right: Dimensions['r'] = 'auto'; + let float: Dimensions['float'] = 'left'; + let position: Dimensions['pos'] = 'relative'; + + if (dimensions) { + width = dimensions.w; + height = dimensions.h; + top = dimensions.t || 'auto'; + right = dimensions.r || 'auto'; + bottom = dimensions.b || 'auto'; + left = dimensions.l || 'auto'; + float = dimensions.float || 'left'; + position = dimensions.pos || 'relative'; + } + + let thumbnail: React.ReactNode = ''; + + if (attachment.type === 'unknown') { + const filename = truncateFilename(attachment.url, MAX_FILENAME_LENGTH); + const attachmentIcon = ( + + ); + + return ( + + ); + } else if (attachment.type === 'image') { + const letterboxed = shouldLetterbox(attachment); + + thumbnail = ( + + + + ); + } else if (attachment.type === 'gifv') { + const conditionalAttributes: React.VideoHTMLAttributes = {}; + if (isIOS()) { + conditionalAttributes.playsInline = true; + } + if (autoPlayGif) { + conditionalAttributes.autoPlay = true; + } + + thumbnail = ( +
+
+ ); + } else if (attachment.type === 'audio') { + const ext = attachment.url.split('.').pop()?.toUpperCase(); + thumbnail = ( + + + {ext} + + ); + } else if (attachment.type === 'video') { + const ext = attachment.url.split('.').pop()?.toUpperCase(); + thumbnail = ( + + + {ext} + + ); + } + + return ( +
+ {last && total > ATTACHMENT_LIMIT && ( +
+ +{total - ATTACHMENT_LIMIT + 1} +
+ )} + + {visible && thumbnail} +
+ ); +}; + +interface IMediaGallery { + sensitive?: boolean, + media: ImmutableList, + size: number, + height: number, + onOpenMedia: (media: ImmutableList, index: number) => void, + defaultWidth: number, + cacheWidth: (width: number) => void, + visible?: boolean, + onToggleVisibility?: () => void, + displayMedia: string, + compact: boolean, +} + +const MediaGallery: React.FC = (props) => { + const { + media, + sensitive = false, + defaultWidth, + onToggleVisibility, + onOpenMedia, + cacheWidth, + compact, + height, + } = props; + + const intl = useIntl(); + + const settings = useSettings(); + const displayMedia = settings.get('displayMedia') as string | undefined; + + const [visible, setVisible] = useState(props.visible !== undefined ? props.visible : (displayMedia !== 'hide_all' && !sensitive || displayMedia === 'show_all')); + const [width, setWidth] = useState(defaultWidth); + + const node = useRef(null); + + // const componentDidUpdate = (prevProps) => { + // const { visible } = props; + // if (!is(media, prevProps.media) && visible === undefined) { + // this.setState({ visible: prevProps.displayMedia !== 'hide_all' && !sensitive || prevProps.displayMedia === 'show_all' }); + // } else if (!is(visible, prevProps.visible) && visible !== undefined) { + // setVisible(visible); + // } + // }; + + const handleOpen: React.MouseEventHandler = (e) => { + e.stopPropagation(); + + if (onToggleVisibility) { + onToggleVisibility(); + } else { + setVisible(!visible); + } + }; + + const handleClick = (index: number) => { + onOpenMedia(media, index); + }; + + useEffect(() => { + if (node.current) { + const { offsetWidth } = node.current; + + if (cacheWidth) { + cacheWidth(offsetWidth); + } + + setWidth(offsetWidth); + } + }, [node.current]); + + const getSizeDataSingle = (): SizeData => { + const w = width || defaultWidth; + const aspectRatio = media.getIn([0, 'meta', 'original', 'aspect']) as number | undefined; + + const getHeight = () => { + if (!aspectRatio) return w * 9 / 16; + if (isPanoramic(aspectRatio)) return Math.floor(w / maximumAspectRatio); + if (isPortrait(aspectRatio)) return Math.floor(w / minimumAspectRatio); + return Math.floor(w / aspectRatio); + }; + + return { + style: { height: getHeight() }, + itemsDimensions: [], + size: 1, + width, + }; + }; + + const getSizeDataMultiple = (size: number): SizeData => { + const w = width || defaultWidth; + const panoSize = Math.floor(w / maximumAspectRatio); + const panoSize_px = `${Math.floor(w / maximumAspectRatio)}px`; + + const style: React.CSSProperties = {}; + let itemsDimensions: Dimensions[] = []; + + const ratios = Array(size).fill(null).map((_, i) => + media.getIn([i, 'meta', 'original', 'aspect']) as number, + ); + + const [ar1, ar2, ar3, ar4] = ratios; + + if (size === 2) { + if (isPortrait(ar1) && isPortrait(ar2)) { + style.height = w - (w / maximumAspectRatio); + } else if (isPanoramic(ar1) && isPanoramic(ar2)) { + style.height = panoSize * 2; + } else if ( + (isPanoramic(ar1) && isPortrait(ar2)) || + (isPortrait(ar1) && isPanoramic(ar2)) || + (isPanoramic(ar1) && isNonConformingRatio(ar2)) || + (isNonConformingRatio(ar1) && isPanoramic(ar2)) + ) { + style.height = (w * 0.6) + (w / maximumAspectRatio); + } else { + style.height = w / 2; + } + + if (isPortrait(ar1) && isPortrait(ar2)) { + itemsDimensions = [ + { w: 50, h: '100%', r: '2px' }, + { w: 50, h: '100%', l: '2px' }, + ]; + } else if (isPanoramic(ar1) && isPanoramic(ar2)) { + itemsDimensions = [ + { w: 100, h: panoSize_px, b: '2px' }, + { w: 100, h: panoSize_px, t: '2px' }, + ]; + } else if ( + (isPanoramic(ar1) && isPortrait(ar2)) || + (isPanoramic(ar1) && isNonConformingRatio(ar2)) + ) { + itemsDimensions = [ + { w: 100, h: `${(w / maximumAspectRatio)}px`, b: '2px' }, + { w: 100, h: `${(w * 0.6)}px`, t: '2px' }, + ]; + } else if ( + (isPortrait(ar1) && isPanoramic(ar2)) || + (isNonConformingRatio(ar1) && isPanoramic(ar2)) + ) { + itemsDimensions = [ + { w: 100, h: `${(w * 0.6)}px`, b: '2px' }, + { w: 100, h: `${(w / maximumAspectRatio)}px`, t: '2px' }, + ]; + } else { + itemsDimensions = [ + { w: 50, h: '100%', r: '2px' }, + { w: 50, h: '100%', l: '2px' }, + ]; + } + } else if (size === 3) { + if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) { + style.height = panoSize * 3; + } else if (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3)) { + style.height = Math.floor(w / minimumAspectRatio); + } else { + style.height = w; + } + + if (isPanoramic(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) { + itemsDimensions = [ + { w: 100, h: '50%', b: '2px' }, + { w: 50, h: '50%', t: '2px', r: '2px' }, + { w: 50, h: '50%', t: '2px', l: '2px' }, + ]; + } else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) { + itemsDimensions = [ + { w: 100, h: panoSize_px, b: '4px' }, + { w: 100, h: panoSize_px }, + { w: 100, h: panoSize_px, t: '4px' }, + ]; + } else if (isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) { + itemsDimensions = [ + { w: 50, h: '100%', r: '2px' }, + { w: 50, h: '50%', b: '2px', l: '2px' }, + { w: 50, h: '50%', t: '2px', l: '2px' }, + ]; + } else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3)) { + itemsDimensions = [ + { w: 50, h: '50%', b: '2px', r: '2px' }, + { w: 50, h: '50%', l: '-2px', b: '-2px', pos: 'absolute', float: 'none' }, + { w: 50, h: '100%', r: '-2px', t: '0px', b: '0px', pos: 'absolute', float: 'none' }, + ]; + } else if ( + (isNonConformingRatio(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3)) || + (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3)) + ) { + itemsDimensions = [ + { w: 50, h: '50%', b: '2px', r: '2px' }, + { w: 50, h: '100%', l: '2px', float: 'right' }, + { w: 50, h: '50%', t: '2px', r: '2px' }, + ]; + } else if ( + (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3)) || + (isPanoramic(ar1) && isPanoramic(ar2) && isPortrait(ar3)) + ) { + itemsDimensions = [ + { w: 50, h: panoSize_px, b: '2px', r: '2px' }, + { w: 50, h: panoSize_px, b: '2px', l: '2px' }, + { w: 100, h: `${w - panoSize}px`, t: '2px' }, + ]; + } else if ( + (isNonConformingRatio(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) || + (isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) + ) { + itemsDimensions = [ + { w: 100, h: `${w - panoSize}px`, b: '2px' }, + { w: 50, h: panoSize_px, t: '2px', r: '2px' }, + { w: 50, h: panoSize_px, t: '2px', l: '2px' }, + ]; + } else { + itemsDimensions = [ + { w: 50, h: '50%', b: '2px', r: '2px' }, + { w: 50, h: '50%', b: '2px', l: '2px' }, + { w: 100, h: '50%', t: '2px' }, + ]; + } + } else if (size >= 4) { + if ( + (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3) && isPortrait(ar4)) || + (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3) && isNonConformingRatio(ar4)) || + (isPortrait(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3) && isPortrait(ar4)) || + (isPortrait(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3) && isPortrait(ar4)) || + (isNonConformingRatio(ar1) && isPortrait(ar2) && isPortrait(ar3) && isPortrait(ar4)) + ) { + style.height = Math.floor(w / minimumAspectRatio); + } else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) { + style.height = panoSize * 2; + } else if ( + (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) || + (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) + ) { + style.height = panoSize + (w / 2); + } else { + style.height = w; + } + + if (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) { + itemsDimensions = [ + { w: 50, h: panoSize_px, b: '2px', r: '2px' }, + { w: 50, h: panoSize_px, b: '2px', l: '2px' }, + { w: 50, h: `${(w / 2)}px`, t: '2px', r: '2px' }, + { w: 50, h: `${(w / 2)}px`, t: '2px', l: '2px' }, + ]; + } else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) { + itemsDimensions = [ + { w: 50, h: `${(w / 2)}px`, b: '2px', r: '2px' }, + { w: 50, h: `${(w / 2)}px`, b: '2px', l: '2px' }, + { w: 50, h: panoSize_px, t: '2px', r: '2px' }, + { w: 50, h: panoSize_px, t: '2px', l: '2px' }, + ]; + } else if ( + (isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) || + (isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) + ) { + itemsDimensions = [ + { w: 67, h: '100%', r: '2px' }, + { w: 33, h: '33%', b: '4px', l: '2px' }, + { w: 33, h: '33%', l: '2px' }, + { w: 33, h: '33%', t: '4px', l: '2px' }, + ]; + } else { + itemsDimensions = [ + { w: 50, h: '50%', b: '2px', r: '2px' }, + { w: 50, h: '50%', b: '2px', l: '2px' }, + { w: 50, h: '50%', t: '2px', r: '2px' }, + { w: 50, h: '50%', t: '2px', l: '2px' }, + ]; + } + } + + return { + style, + itemsDimensions, + size, + width: w, + }; + }; + + const getSizeData = (size: number): Readonly => { + const w = width || defaultWidth; + + if (w) { + if (size === 1) return getSizeDataSingle(); + if (size > 1) return getSizeDataMultiple(size); + } + + return { + style: { height }, + itemsDimensions: [], + size, + width: w, + }; + }; + + const sizeData: SizeData = getSizeData(media.size); + + const children = media.take(ATTACHMENT_LIMIT).map((attachment, i) => ( + + )); + + let warning; + + if (sensitive) { + warning = ; + } else { + warning = ; + } + + return ( +
+
+ {sensitive && ( + (visible || compact) ? ( + +
+
+ ) + )} + + + {children} + + ); +}; + +export default MediaGallery; diff --git a/app/soapbox/normalizers/attachment.ts b/app/soapbox/normalizers/attachment.ts index 23cce3686..f5e00135e 100644 --- a/app/soapbox/normalizers/attachment.ts +++ b/app/soapbox/normalizers/attachment.ts @@ -20,7 +20,7 @@ export const AttachmentRecord = ImmutableRecord({ meta: ImmutableMap(), pleroma: ImmutableMap(), preview_url: '', - remote_url: null, + remote_url: null as string | null, type: 'unknown', url: '',