diff --git a/web/modals/threads/gallery/thread-settings-media-gallery.css b/web/modals/threads/gallery/thread-settings-media-gallery.css index b566e0625..b83ab901a 100644 --- a/web/modals/threads/gallery/thread-settings-media-gallery.css +++ b/web/modals/threads/gallery/thread-settings-media-gallery.css @@ -1,24 +1,31 @@ div.container { display: flex; flex-wrap: wrap; overflow-y: scroll; justify-content: flex-start; - max-height: 700px; - padding: 10px; - margin-top: 10px; + height: 80vh; + padding-top: 16px; } div.mediaContainer { flex: 0 1 31%; width: 150px; height: 200px; margin: 5px; } img.media, video.media { width: 100%; height: 100%; object-fit: cover; cursor: pointer; } + +.noMedia { + margin-top: 16px; + color: var(--text-background-tertiary-default); + display: flex; + flex: 1; + justify-content: center; +} 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 f6d19be52..d5e2565a0 100644 --- a/web/modals/threads/gallery/thread-settings-media-gallery.react.js +++ b/web/modals/threads/gallery/thread-settings-media-gallery.react.js @@ -1,211 +1,218 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useFetchThreadMedia } from 'lib/actions/thread-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { encryptedMediaBlobURI, encryptedVideoThumbnailBlobURI, } from 'lib/media/media-utils.js'; import type { Media } from 'lib/types/media-types.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import GalleryItem from './thread-settings-media-gallery-item.react.js'; import css from './thread-settings-media-gallery.css'; import Tabs, { type TabData } from '../../../components/tabs.react.js'; import MultimediaModal from '../../../media/multimedia-modal.react.js'; import Modal from '../../modal.react.js'; type MediaGalleryTab = 'All' | 'Images' | 'Videos'; const tabsData: $ReadOnlyArray> = [ { id: 'All', header: 'All', }, { id: 'Images', header: 'Images', }, { id: 'Videos', header: '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 = useFetchThreadMedia(); const [mediaInfos, setMediaInfos] = React.useState<$ReadOnlyArray>([]); const [tab, setTab] = React.useState(activeTab); const tabs = React.useMemo( () => , [tab], ); React.useEffect(() => { const fetchData = async () => { const result = await callFetchThreadMedia({ threadID, limit, offset: 0, }); setMediaInfos(result.media); }; void 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 { encryptionKey, thumbnailEncryptionKey } = media; const thumbnailBlobURI = media.type === 'encrypted_video' ? encryptedVideoThumbnailBlobURI(media) : null; mediaInfo = { ...mediaInfo, type: media.type, blobURI: encryptedMediaBlobURI(media), encryptionKey, thumbnailBlobURI, thumbnailEncryptionKey, }; } pushModal(); }, [pushModal], ); const mediaGalleryItems = React.useMemo(() => { let filteredMediaInfos = mediaInfos; if (tab === 'Images') { filteredMediaInfos = mediaInfos.filter( mediaInfo => mediaInfo.type === 'photo' || mediaInfo.type === 'encrypted_photo', ); } else if (tab === 'Videos') { filteredMediaInfos = mediaInfos.filter( mediaInfo => mediaInfo.type === 'video' || mediaInfo.type === 'encrypted_video', ); } + if (filteredMediaInfos.length === 0) { + return ( +
+ No {tab === 'All' ? 'media' : tab.toLowerCase()} in this chat. +
+ ); + } + 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', blobURI: encryptedMediaBlobURI(media), encryptionKey: media.encryptionKey, thumbHash: media.thumbHash, }; } else { imageSource = { kind: 'encrypted', blobURI: encryptedVideoThumbnailBlobURI(media), encryptionKey: media.thumbnailEncryptionKey, thumbHash: media.thumbnailThumbHash, }; } return ( onClick(media)} imageSource={imageSource} imageCSSClass={css.media} imageContainerCSSClass={css.mediaContainer} /> ); }); }, [tab, mediaInfos, onClick]); const handleScroll = React.useCallback( async (event: SyntheticEvent) => { const container = event.target; invariant(container instanceof HTMLDivElement, 'target not div'); // 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], ); const tabContent = React.useMemo( () => (
{mediaGalleryItems}
), [handleScroll, mediaGalleryItems], ); const threadSettingsMediaGalleryModal = React.useMemo( () => ( - - {tabs} + {tabContent} ), [onClose, tabContent, tabs], ); return threadSettingsMediaGalleryModal; } export default ThreadSettingsMediaGalleryModal;