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 (
+    <View style={styles.filterBar}>
+      <View style={styles.tabNavigator}>
+        <TouchableOpacity
+          key={Tabs.All}
+          style={tabStyles(Tabs.All)}
+          onPress={allTabsOnPress}
+        >
+          <Text style={styles.tabText}>{Tabs.All}</Text>
+        </TouchableOpacity>
+        <TouchableOpacity
+          key={Tabs.Images}
+          style={tabStyles(Tabs.Images)}
+          onPress={imagesTabOnPress}
+        >
+          <Text style={styles.tabText}>{Tabs.Images}</Text>
+        </TouchableOpacity>
+        <TouchableOpacity
+          key={Tabs.Videos}
+          style={tabStyles(Tabs.Videos)}
+          onPress={videosTabOnPress}
+        >
+          <Text style={styles.tabText}>{Tabs.Videos}</Text>
+        </TouchableOpacity>
+      </View>
+    </View>
+  );
+}
+
 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 <Text>{id}</Text>;
+  const { threadInfo } = props.route.params;
+  const { id } = threadInfo;
+  const styles = useStyles(unboundStyles);
+
+  const [activeTab, setActiveTab] = React.useState(Tabs.All);
+  const flatListContainerRef = React.useRef<?React.ElementRef<typeof View>>();
+  const [verticalBounds, setVerticalBounds] =
+    React.useState<?VerticalBounds>(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 (
+    <View
+      style={styles.container}
+      ref={flatListContainerRef}
+      onLayout={onFlatListContainerLayout}
+    >
+      <FilterBar setActiveTab={setActiveTab} activeTab={activeTab} />
+      <ThreadSettingsMediaGallery
+        threadID={id}
+        verticalBounds={verticalBounds}
+        limit={21}
+        offset={0}
+        activeTab={activeTab}
+      />
+    </View>
+  );
 }
 
-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<FullScreenThreadMediaGalleryProps> =
+  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,8 +48,7 @@
   // 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);
 
@@ -83,6 +84,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 }) => (
       <MediaGalleryItem
@@ -96,12 +108,26 @@
     [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 (
     <View style={styles.flatListContainer}>
       <FlatList
-        data={mediaInfos}
+        data={filteredMediaInfos}
         numColumns={numColumns}
         renderItem={renderItem}
+        onEndReached={offset !== undefined ? onEndReached : null}
+        onEndReachedThreshold={1}
       />
     </View>
   );