diff --git a/web/media/multimedia-modal.react.js b/web/media/multimedia-modal.react.js index e2ea7e830..0eb3b273a 100644 --- a/web/media/multimedia-modal.react.js +++ b/web/media/multimedia-modal.react.js @@ -1,204 +1,205 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { XCircle as XCircleIcon } from 'react-feather'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { fetchableMediaURI } from 'lib/media/media-utils.js'; import type { EncryptedMediaType, MediaType, Dimensions, } from 'lib/types/media-types.js'; import EncryptedMultimedia from './encrypted-multimedia.react.js'; import LoadableVideo from './loadable-video.react.js'; import { usePlaceholder } from './media-utils.js'; import css from './media.css'; type MediaInfo = | { +type: MediaType, +uri: string, +dimensions: ?Dimensions, +thumbHash: ?string, +thumbnailURI: ?string, } | { +type: EncryptedMediaType, +holder: string, +encryptionKey: string, +dimensions: ?Dimensions, +thumbHash: ?string, +thumbnailHolder: ?string, +thumbnailEncryptionKey: ?string, }; type BaseProps = { +media: MediaInfo, }; type Props = { ...BaseProps, +popModal: (modal: ?React.Node) => void, +placeholderImage: ?string, }; type State = { +dimensions: ?Dimensions, }; class MultimediaModal extends React.PureComponent { overlay: ?HTMLDivElement; constructor(props: Props) { super(props); this.state = { dimensions: null }; } componentDidMount() { invariant(this.overlay, 'overlay ref unset'); this.overlay.focus(); this.calculateMediaDimensions(); window.addEventListener('resize', this.calculateMediaDimensions); } componentWillUnmount() { window.removeEventListener('resize', this.calculateMediaDimensions); } render(): React.Node { let mediaModalItem; const { media, placeholderImage } = this.props; const style = { backgroundImage: placeholderImage ? `url(${placeholderImage})` : undefined, }; if (media.type === 'photo') { const uri = fetchableMediaURI(media.uri); mediaModalItem = ; } else if (media.type === 'video') { const uri = fetchableMediaURI(media.uri); const { thumbnailURI } = media; invariant(thumbnailURI, 'video missing thumbnail'); mediaModalItem = ( ); } else { invariant( media.type === 'encrypted_photo' || media.type === 'encrypted_video', 'invalid media type', ); const { type, holder, encryptionKey, thumbnailHolder, thumbnailEncryptionKey, } = media; const dimensions = this.state.dimensions ?? media.dimensions; const elementStyle = dimensions ? { width: `${dimensions.width}px`, height: `${dimensions.height}px`, } : undefined; mediaModalItem = ( ); } return (
-
{mediaModalItem}
+
+ {mediaModalItem} +
); } overlayRef: (overlay: ?HTMLDivElement) => void = overlay => { this.overlay = overlay; }; onBackgroundClick: (event: SyntheticEvent) => void = event => { if (event.target === this.overlay) { this.props.popModal(); } }; onKeyDown: (event: SyntheticKeyboardEvent) => void = event => { if (event.key === 'Escape') { this.props.popModal(); } }; calculateMediaDimensions: () => void = () => { if (!this.overlay || !this.props.media.dimensions) { return; } const containerWidth = this.overlay.clientWidth; const containerHeight = this.overlay.clientHeight; const containerAspectRatio = containerWidth / containerHeight; const { width: mediaWidth, height: mediaHeight } = this.props.media.dimensions; const mediaAspectRatio = mediaWidth / mediaHeight; let newWidth, newHeight; if (containerAspectRatio > mediaAspectRatio) { newWidth = Math.min(mediaWidth, containerHeight * mediaAspectRatio); newHeight = newWidth / mediaAspectRatio; } else { newHeight = Math.min(mediaHeight, containerWidth / mediaAspectRatio); newWidth = newHeight * mediaAspectRatio; } this.setState({ dimensions: { width: newWidth, height: newHeight, }, }); }; } function ConnectedMultiMediaModal(props: BaseProps): React.Node { const modalContext = useModalContext(); const { thumbHash, encryptionKey, thumbnailEncryptionKey } = props.media; const thumbHashEncryptionKey = thumbnailEncryptionKey ?? encryptionKey; const placeholderImage = usePlaceholder(thumbHash, thumbHashEncryptionKey); return ( ); } export default ConnectedMultiMediaModal; diff --git a/web/modals/threads/gallery/thread-settings-media-gallery.react.js b/web/modals/threads/gallery/thread-settings-media-gallery.react.js index 49e8d7d24..f7fe4641e 100644 --- a/web/modals/threads/gallery/thread-settings-media-gallery.react.js +++ b/web/modals/threads/gallery/thread-settings-media-gallery.react.js @@ -1,195 +1,188 @@ // @flow import * as React from 'react'; import { fetchThreadMedia } from 'lib/actions/thread-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import type { Media } from 'lib/types/media-types.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { useServerCall } from 'lib/utils/action-utils.js'; import GalleryItem from './thread-settings-media-gallery-item.react.js'; import css from './thread-settings-media-gallery.css'; import Tabs from '../../../components/tabs.react.js'; import MultimediaModal from '../../../media/multimedia-modal.react.js'; import Modal from '../../modal.react.js'; type MediaGalleryTab = 'All' | 'Images' | 'Videos'; type ThreadSettingsMediaGalleryModalProps = { +onClose: () => void, +parentThreadInfo: ThreadInfo, +limit: number, +activeTab: MediaGalleryTab, }; function ThreadSettingsMediaGalleryModal( props: ThreadSettingsMediaGalleryModalProps, ): React.Node { const { pushModal } = useModalContext(); const { onClose, parentThreadInfo, limit, activeTab } = props; const { id: threadID } = parentThreadInfo; const modalName = 'Media'; const callFetchThreadMedia = useServerCall(fetchThreadMedia); const [mediaInfos, setMediaInfos] = React.useState([]); const [tab, setTab] = React.useState(activeTab); React.useEffect(() => { const fetchData = async () => { const result = await callFetchThreadMedia({ threadID, limit, offset: 0, }); setMediaInfos(result.media); }; fetchData(); }, [callFetchThreadMedia, threadID, limit]); const onClick = React.useCallback( (media: Media) => { const thumbHash = media.thumbnailThumbHash ?? media.thumbHash; let mediaInfo = { thumbHash, dimensions: media.dimensions, }; if (media.type === 'photo' || media.type === 'video') { const { uri, thumbnailURI } = media; mediaInfo = { ...mediaInfo, type: media.type, uri, thumbnailURI, }; } else { const { holder, encryptionKey, thumbnailHolder, thumbnailEncryptionKey, } = media; mediaInfo = { ...mediaInfo, type: media.type, holder, encryptionKey, thumbnailHolder, thumbnailEncryptionKey, }; } pushModal(); }, [pushModal], ); - const filteredMediaInfos = React.useMemo(() => { + const mediaGalleryItems = React.useMemo(() => { + let filteredMediaInfos = mediaInfos; if (tab === 'Images') { - return mediaInfos.filter( + filteredMediaInfos = mediaInfos.filter( mediaInfo => mediaInfo.type === 'photo' || mediaInfo.type === 'encrypted_photo', ); } else if (tab === 'Videos') { - return mediaInfos.filter( + filteredMediaInfos = mediaInfos.filter( mediaInfo => mediaInfo.type === 'video' || mediaInfo.type === 'encrypted_video', ); } - return mediaInfos; - }, [tab, mediaInfos]); - - const mediaCoverPhotos = React.useMemo( - () => - filteredMediaInfos.map(media => { - if (media.type === 'photo') { - return { - kind: 'plain', - uri: media.uri, - thumbHash: media.thumbHash, - }; - } else if (media.type === 'video') { - return { - kind: 'plain', - uri: media.thumbnailURI, - thumbHash: media.thumbnailThumbHash, - }; - } else if (media.type === 'encrypted_photo') { - return { - kind: 'encrypted', - holder: media.holder, - encryptionKey: media.encryptionKey, - thumbHash: media.thumbHash, - }; - } else { - return { - kind: 'encrypted', - holder: media.thumbnailHolder, - encryptionKey: media.thumbnailEncryptionKey, - thumbHash: media.thumbnailThumbHash, - }; - } - }), - [filteredMediaInfos], - ); - const mediaGalleryItems = React.useMemo( - () => - filteredMediaInfos.map((media, i) => ( + return filteredMediaInfos.map((media, i) => { + let imageSource; + if (media.type === 'photo') { + imageSource = { + kind: 'plain', + uri: media.uri, + thumbHash: media.thumbHash, + }; + } else if (media.type === 'video') { + imageSource = { + kind: 'plain', + uri: media.thumbnailURI, + thumbHash: media.thumbnailThumbHash, + }; + } else if (media.type === 'encrypted_photo') { + imageSource = { + kind: 'encrypted', + holder: media.holder, + encryptionKey: media.encryptionKey, + thumbHash: media.thumbHash, + }; + } else { + imageSource = { + kind: 'encrypted', + holder: media.thumbnailHolder, + encryptionKey: media.thumbnailEncryptionKey, + thumbHash: media.thumbnailThumbHash, + }; + } + + return ( onClick(media)} - imageSource={mediaCoverPhotos[i]} + imageSource={imageSource} imageCSSClass={css.media} imageContainerCSSClass={css.mediaContainer} /> - )), - [filteredMediaInfos, onClick, mediaCoverPhotos], - ); + ); + }); + }, [tab, mediaInfos, onClick]); const handleScroll = React.useCallback( async event => { const container = event.target; // Load more data when the user is within 1000 pixels of the end const buffer = 1000; if ( container.scrollHeight - container.scrollTop > container.clientHeight + buffer ) { return; } const result = await callFetchThreadMedia({ threadID, limit, offset: mediaInfos.length, }); setMediaInfos([...mediaInfos, ...result.media]); }, [callFetchThreadMedia, threadID, limit, mediaInfos], ); return (
{mediaGalleryItems}
{mediaGalleryItems}
{mediaGalleryItems}
); } export default ThreadSettingsMediaGalleryModal;