diff --git a/web/modals/threads/gallery/thread-settings-media-gallery.css b/web/modals/threads/gallery/thread-settings-media-gallery.css new file mode 100644 --- /dev/null +++ b/web/modals/threads/gallery/thread-settings-media-gallery.css @@ -0,0 +1,24 @@ +div.container { + display: flex; + flex-wrap: wrap; + overflow-y: scroll; + justify-content: flex-start; + max-height: 700px; + padding: 10px; + margin-top: 10px; +} + +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; +} diff --git a/web/modals/threads/gallery/thread-settings-media-gallery.react.js b/web/modals/threads/gallery/thread-settings-media-gallery.react.js new file mode 100644 --- /dev/null +++ b/web/modals/threads/gallery/thread-settings-media-gallery.react.js @@ -0,0 +1,128 @@ +// @flow + +import * as React from 'react'; + +import { fetchThreadMedia } from 'lib/actions/thread-actions.js'; +import type { ThreadInfo } from 'lib/types/thread-types.js'; +import { useServerCall } from 'lib/utils/action-utils.js'; + +import css from './thread-settings-media-gallery.css'; +import Tabs from '../../../components/tabs.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 { onClose, parentThreadInfo, limit, activeTab } = props; + const { id: threadID } = parentThreadInfo; + const modalName = 'Media'; + + const callFetchThreadMedia = useServerCall(fetchThreadMedia); + const [mediaInfos, setMediaInfos] = React.useState([]); + const [adjustedOffset, setAdjustedOffset] = React.useState(0); + const [tab, setTab] = React.useState(activeTab); + + React.useEffect(() => { + const fetchData = async () => { + const result = await callFetchThreadMedia({ + threadID, + limit, + offset: 0, + currentMediaIDs: [], + }); + setMediaInfos(result.media); + setAdjustedOffset(result.adjustedOffset); + }; + fetchData(); + }, [callFetchThreadMedia, threadID, limit]); + + const filteredMediaInfos = React.useMemo(() => { + if (tab === 'All') { + return mediaInfos; + } else if (tab === 'Images') { + return mediaInfos.filter(mediaInfo => mediaInfo.type === 'photo'); + } else if (tab === 'Videos') { + return mediaInfos.filter(mediaInfo => mediaInfo.type === 'video'); + } + return mediaInfos; + }, [tab, mediaInfos]); + + const mediaCoverPhotos = React.useMemo( + () => filteredMediaInfos.map(media => media.thumbnailURI || media.uri), + [filteredMediaInfos], + ); + + const mediaGalleryItems = React.useMemo( + () => + filteredMediaInfos.map((media, i) => ( +
+ +
+ )), + [filteredMediaInfos, mediaCoverPhotos], + ); + + 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 mediaIDs = mediaInfos.map(mediaInfo => String(mediaInfo.id)); + const thumbnailIDs = mediaInfos.map( + mediaInfo => String(mediaInfo.thumbnailID) || '', + ); + const currentMediaIDs = [...mediaIDs, ...thumbnailIDs]; + + const result = await callFetchThreadMedia({ + threadID, + limit, + offset: adjustedOffset, + currentMediaIDs, + }); + setMediaInfos([...mediaInfos, ...result.media]); + setAdjustedOffset(result.adjustedOffset); + }, + [callFetchThreadMedia, threadID, limit, adjustedOffset, mediaInfos], + ); + + return ( + + + +
+ {mediaGalleryItems} +
+
+ +
+ {mediaGalleryItems} +
+
+ +
+ {mediaGalleryItems} +
+
+
+
+ ); +} + +export default ThreadSettingsMediaGalleryModal;