diff --git a/native/chat/fullscreen-thread-media-gallery.react.js b/native/chat/fullscreen-thread-media-gallery.react.js index 6ff00b4d5..13cd30553 100644 --- a/native/chat/fullscreen-thread-media-gallery.react.js +++ b/native/chat/fullscreen-thread-media-gallery.react.js @@ -1,27 +1,179 @@ // @flow import * as React from 'react'; -import { Text } from 'react-native'; +import { Text, View, TouchableOpacity } from 'react-native'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import type { ChatNavigationProp } from './chat.react.js'; +import ThreadSettingsMediaGallery from './settings/thread-settings-media-gallery.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; +import { useStyles } from '../themes/colors.js'; +import type { VerticalBounds } from '../types/layout-types.js'; export type FullScreenThreadMediaGalleryParams = { +threadInfo: ThreadInfo, }; +const Tabs = { + All: 'ALL', + Images: 'IMAGES', + Videos: 'VIDEOS', +}; + +type FilterBarProps = { + +setActiveTab: (tab: string) => void, + +activeTab: string, +}; + +function FilterBar(props: FilterBarProps): React.Node { + const styles = useStyles(unboundStyles); + const { setActiveTab, activeTab } = props; + + const allTabsOnPress = React.useCallback( + () => setActiveTab(Tabs.All), + [setActiveTab], + ); + + const imagesTabOnPress = React.useCallback( + () => setActiveTab(Tabs.Images), + [setActiveTab], + ); + + const videosTabOnPress = React.useCallback( + () => setActiveTab(Tabs.Videos), + [setActiveTab], + ); + + const tabStyles = (currentTab: string) => + currentTab === activeTab ? styles.tabActiveItem : styles.tabItem; + + return ( + + + + {Tabs.All} + + + {Tabs.Images} + + + {Tabs.Videos} + + + + ); +} + type FullScreenThreadMediaGalleryProps = { +navigation: ChatNavigationProp<'FullScreenThreadMediaGallery'>, +route: NavigationRoute<'FullScreenThreadMediaGallery'>, }; function FullScreenThreadMediaGallery( props: FullScreenThreadMediaGalleryProps, ): React.Node { - const { id } = props.route.params.threadInfo; - return {id}; + const { threadInfo } = props.route.params; + const { id } = threadInfo; + const styles = useStyles(unboundStyles); + + const [activeTab, setActiveTab] = React.useState(Tabs.All); + const flatListContainerRef = React.useRef>(); + const [verticalBounds, setVerticalBounds] = + React.useState(null); + + const onFlatListContainerLayout = React.useCallback(() => { + if (!flatListContainerRef.current) { + return; + } + + flatListContainerRef.current.measure( + (x, y, width, height, pageX, pageY) => { + if ( + height === null || + height === undefined || + pageY === null || + pageY === undefined + ) { + return; + } + setVerticalBounds({ height, y: pageY }); + }, + ); + }, [flatListContainerRef]); + + return ( + + + + + ); } -export default FullScreenThreadMediaGallery; +const unboundStyles = { + container: { + marginBottom: 120, + }, + filterBar: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + marginTop: 20, + marginBottom: 40, + }, + tabNavigator: { + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-start', + position: 'absolute', + width: '90%', + padding: 0, + }, + tabActiveItem: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'floatingButtonBackground', + flex: 1, + height: 30, + borderRadius: 8, + }, + tabItem: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'listInputBackground', + flex: 1, + height: 30, + }, + tabText: { + color: 'floatingButtonLabel', + }, +}; + +const MemoizedFullScreenMediaGallery: React.ComponentType = + React.memo(FullScreenThreadMediaGallery); + +export default MemoizedFullScreenMediaGallery; diff --git a/native/chat/settings/thread-settings-media-gallery.react.js b/native/chat/settings/thread-settings-media-gallery.react.js index 0cb3f985e..2c12f2659 100644 --- a/native/chat/settings/thread-settings-media-gallery.react.js +++ b/native/chat/settings/thread-settings-media-gallery.react.js @@ -1,194 +1,220 @@ // @flow import { useNavigation, useRoute } from '@react-navigation/native'; import * as React from 'react'; import { View, useWindowDimensions } from 'react-native'; import type { ViewStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet'; import { FlatList } from 'react-native-gesture-handler'; import { fetchThreadMedia } from 'lib/actions/thread-actions.js'; import type { MediaInfo, Media } from 'lib/types/media-types'; import { useServerCall } from 'lib/utils/action-utils.js'; import GestureTouchableOpacity from '../../components/gesture-touchable-opacity.react.js'; import Multimedia from '../../media/multimedia.react.js'; import { ImageModalRouteName, VideoPlaybackModalRouteName, } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; import type { LayoutCoordinates, VerticalBounds, } from '../../types/layout-types.js'; const galleryItemGap = 8; const numColumns = 3; type ThreadSettingsMediaGalleryProps = { +threadID: string, +limit: number, +verticalBounds: ?VerticalBounds, + +offset?: number, + +activeTab?: string, }; function ThreadSettingsMediaGallery( props: ThreadSettingsMediaGalleryProps, ): React.Node { const styles = useStyles(unboundStyles); const { width } = useWindowDimensions(); // Explanation of galleryItemWidth: // The FlatList has a horizontal padding of 16px on each side, // and so the width of the actual FlatList is `width - 32px`. // With three columns, there will be two gaps in between the items, // so the width of each item (with the gaps) will be // (width - 32px - (numColumns-1) * galleryItemGap) / numColumns. // E.g. 16px, media, galleryItemGap, media, galleryItemGap, media, 16px const galleryItemWidth = (width - 32 - (numColumns - 1) * galleryItemGap) / numColumns; - - const { threadID, limit, verticalBounds } = props; + const { threadID, limit, verticalBounds, offset, activeTab } = props; const [mediaInfos, setMediaInfos] = React.useState([]); const callFetchThreadMedia = useServerCall(fetchThreadMedia); React.useEffect(() => { const fetchData = async () => { const result = await callFetchThreadMedia({ threadID, limit, offset: 0, }); setMediaInfos(result.media); }; fetchData(); }, [callFetchThreadMedia, threadID, limit]); const memoizedStyles = React.useMemo(() => { return { mediaContainer: { marginTop: galleryItemGap, width: galleryItemWidth, ...styles.mediaContainer, }, mediaContainerWithMargin: { marginTop: galleryItemGap, marginLeft: galleryItemGap, width: galleryItemWidth, ...styles.mediaContainer, }, media: { width: galleryItemWidth, ...styles.media, }, }; }, [galleryItemWidth, styles.media, styles.mediaContainer]); + const filteredMediaInfos = React.useMemo(() => { + if (activeTab === 'ALL') { + return mediaInfos; + } else if (activeTab === 'IMAGES') { + return mediaInfos.filter(mediaInfo => mediaInfo.type === 'photo'); + } else if (activeTab === 'VIDEOS') { + return mediaInfos.filter(mediaInfo => mediaInfo.type === 'video'); + } + return mediaInfos; + }, [activeTab, mediaInfos]); + const renderItem = React.useCallback( ({ item, index }) => ( ), [threadID, verticalBounds, memoizedStyles], ); + const onEndReached = React.useCallback(async () => { + // As the FlatList fetches more media, we set the offset to be the length + // of mediaInfos. This will ensure that the next set of media is retrieved + // from the starting point. + const result = await callFetchThreadMedia({ + threadID, + limit, + offset: mediaInfos.length, + }); + setMediaInfos([...mediaInfos, ...result.media]); + }, [callFetchThreadMedia, mediaInfos, threadID, limit]); + return ( ); } type MediaGalleryItemProps = { +item: Media, +index: number, +memoizedStyles: { +mediaContainer: ViewStyleProp, +mediaContainerWithMargin: ViewStyleProp, +media: ViewStyleProp, }, +threadID: string, +verticalBounds: ?VerticalBounds, }; function MediaGalleryItem(props: MediaGalleryItemProps): React.Node { const navigation = useNavigation(); const route = useRoute(); const ref = React.useRef(null); const onLayout = React.useCallback(() => {}, []); const { threadID, verticalBounds, memoizedStyles, item, index } = props; const mediaInfo: MediaInfo = React.useMemo( () => ({ ...(item: Media), index, }), [item, index], ); const navigateToMedia = React.useCallback(() => { ref.current?.measure((x, y, width, height, pageX, pageY) => { const initialCoordinates: LayoutCoordinates = { x: pageX, y: pageY, width, height, }; navigation.navigate<'VideoPlaybackModal' | 'ImageModal'>({ name: mediaInfo.type === 'video' ? VideoPlaybackModalRouteName : ImageModalRouteName, key: `multimedia|${threadID}|${mediaInfo.id}`, params: { presentedFrom: route.key, mediaInfo, item, initialCoordinates, verticalBounds, }, }); }); }, [navigation, route, threadID, mediaInfo, item, verticalBounds]); const containerStyle = index % numColumns === 0 ? memoizedStyles.mediaContainer : memoizedStyles.mediaContainerWithMargin; return ( ); } const unboundStyles = { flatListContainer: { paddingHorizontal: 16, }, mediaContainer: { height: 180, justifyContent: 'center', alignItems: 'center', }, media: { height: 180, }, }; export default ThreadSettingsMediaGallery;