diff --git a/native/chat/fullscreen-thread-media-gallery.react.js b/native/chat/fullscreen-thread-media-gallery.react.js
--- a/native/chat/fullscreen-thread-media-gallery.react.js
+++ b/native/chat/fullscreen-thread-media-gallery.react.js
@@ -1,17 +1,82 @@
// @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'>,
@@ -20,8 +85,95 @@
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
--- a/native/chat/settings/thread-settings-media-gallery.react.js
+++ b/native/chat/settings/thread-settings-media-gallery.react.js
@@ -29,6 +29,8 @@
+threadID: string,
+limit: number,
+verticalBounds: ?VerticalBounds,
+ +offset?: number,
+ +activeTab?: string,
};
function ThreadSettingsMediaGallery(
@@ -46,9 +48,9 @@
// 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 [adjustedOffset, setAdjustedOffset] = React.useState(offset || 0);
const callFetchThreadMedia = useServerCall(fetchThreadMedia);
React.useEffect(() => {
@@ -60,6 +62,7 @@
currentMediaIDs: [],
});
setMediaInfos(result.media);
+ setAdjustedOffset(result.adjustedOffset);
};
fetchData();
}, [callFetchThreadMedia, threadID, limit]);
@@ -84,6 +87,17 @@
};
}, [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 }) => {
return (
@@ -99,12 +113,37 @@
[threadID, verticalBounds, memoizedStyles],
);
+ const onEndReached = React.useCallback(async () => {
+ // We need to provide the existing media and thumbnail IDs with the request
+ // in order to ensure that we don't get duplicate media. Coerce the IDs to
+ // strings because the server returns them as either strings or numbers.
+ const mediaIDs = mediaInfos.map(mediaInfo => String(mediaInfo.id));
+ const thumbnailIDs = mediaInfos.map(
+ mediaInfo => String(mediaInfo.thumbnailID) || '',
+ );
+ const currentMediaIDs = [...mediaIDs, ...thumbnailIDs];
+
+ // 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: adjustedOffset,
+ currentMediaIDs,
+ });
+ setMediaInfos([...mediaInfos, ...result.media]);
+ setAdjustedOffset(result.adjustedOffset);
+ }, [callFetchThreadMedia, mediaInfos, threadID, limit, adjustedOffset]);
+
return (
);