From a6659bfc3dc58f764c22c80e9aee45f979579dfe Mon Sep 17 00:00:00 2001 From: ghostcoder42 Date: Tue, 8 Sep 2026 10:03:49 +0800 Subject: [PATCH] fix(layout): single safe-area owner for the tab area MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tab screens lost a strip roughly the tab bar's height between content and the bar on devices where the bottom inset is real (e.g. a Pixel C on Android 8.1 with its 3-button navigation bar): the tab bar already positioned itself above the system navigation bar, and the screens' SafeAreaView padded the same bottom inset again as content padding. On gesture-navigation devices the inset is near zero, which is why it went unnoticed there. Rather than splitting the inset policy across five screen files (opting each out of the bottom edge) and react-navigation's internal bar behavior, the fix consolidates it in one place: a wrapper in the tab layout consumes all four safe-area edges, the navigator gets an explicit safeAreaInsets.bottom = 0 (a public option, not a patch — and one that fails as visible layout breakage if library defaults ever shift under an upgrade, never as a silent change), and the tab screens become plain Views with no inset logic at all. The strip under the system navigation bar is painted by the container in the bar's color, and the vendored bar's inset channel — which reads 0 on some older devices — is no longer relied upon. Verified pixel-identical on device (content, bar and label positions unchanged). A future floating tab bar over scrolling content now only edits the tab layout file; full-screen stack screens (post, author, model, tag, category) keep handling their own edges. --- src/app/(app)/_layout.tsx | 126 +++++++++++++++++++----------------- src/app/(app)/following.tsx | 10 ++- src/app/(app)/index.tsx | 10 +-- src/app/(app)/library.tsx | 10 ++- src/app/(app)/search.tsx | 10 ++- src/app/(app)/settings.tsx | 6 +- 6 files changed, 97 insertions(+), 75 deletions(-) diff --git a/src/app/(app)/_layout.tsx b/src/app/(app)/_layout.tsx index c3004f3..3c84056 100644 --- a/src/app/(app)/_layout.tsx +++ b/src/app/(app)/_layout.tsx @@ -4,7 +4,7 @@ import { Tabs } from 'expo-router/js-tabs'; import { useColorScheme } from 'nativewind'; import * as React from 'react'; -import { colors } from '@/components/ui'; +import { SafeAreaView, colors } from '@/components/ui'; import { Users as FollowingIcon, Home as HomeIcon, @@ -38,64 +38,74 @@ export default function TabLayout(): React.ReactElement { return () => clearTimeout(timer); }, []); + // Single owner of the tab area's system-chrome insets: this wrapper consumes + // all four edges, the tab bar gets an explicit zero bottom inset below, and + // the tab screens are plain views. Any change here (e.g. a floating tab bar + // over scrolling content) only edits this file — and a regression shows up + // as obvious layout breakage, not a silent shift inside library defaults. + const shellBg = isDark ? 'bg-neutral-900' : 'bg-white'; + return ( - - , - tabBarButtonTestID: 'home-tab', - }} - /> - , - tabBarButtonTestID: 'search-tab', - // Hide from the tab bar if the user disabled it in Settings. - href: tabs.search ? undefined : null, - }} - /> - , - tabBarButtonTestID: 'following-tab', - href: tabs.following ? undefined : null, - }} - /> - , - tabBarButtonTestID: 'library-tab', - href: tabs.library ? undefined : null, - }} - /> - , - tabBarButtonTestID: 'settings-tab', + + - + > + , + tabBarButtonTestID: 'home-tab', + }} + /> + , + tabBarButtonTestID: 'search-tab', + // Hide from the tab bar if the user disabled it in Settings. + href: tabs.search ? undefined : null, + }} + /> + , + tabBarButtonTestID: 'following-tab', + href: tabs.following ? undefined : null, + }} + /> + , + tabBarButtonTestID: 'library-tab', + href: tabs.library ? undefined : null, + }} + /> + , + tabBarButtonTestID: 'settings-tab', + }} + /> + + ); } diff --git a/src/app/(app)/following.tsx b/src/app/(app)/following.tsx index d04d868..3610cd4 100644 --- a/src/app/(app)/following.tsx +++ b/src/app/(app)/following.tsx @@ -3,14 +3,18 @@ import { Link } from 'expo-router'; import type * as React from 'react'; import { Text, TouchableOpacity, View } from 'react-native'; -import { FocusAwareStatusBar, SafeAreaView } from '@/components/ui'; +import { FocusAwareStatusBar } from '@/components/ui'; import { useFollowingStore } from '@/lib/stores/following-store'; export default function FollowingScreen(): React.ReactElement { const { following, unfollow } = useFollowingStore(); return ( - + {following.length === 0 ? ( @@ -48,6 +52,6 @@ export default function FollowingScreen(): React.ReactElement { keyExtractor={(item) => item.slug} /> )} - + ); } diff --git a/src/app/(app)/index.tsx b/src/app/(app)/index.tsx index 891259b..a2f9889 100644 --- a/src/app/(app)/index.tsx +++ b/src/app/(app)/index.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { ActivityIndicator, TouchableOpacity, View } from 'react-native'; import { useVideos } from '@/api/video-queries'; -import { SafeAreaView, Text } from '@/components/ui'; +import { Text } from '@/components/ui'; import { VideoTile } from '@/components/video-tile'; import { flattenUniquePages } from '@/lib/flatten-pages'; import { useColumns } from '@/lib/hooks/use-columns'; @@ -46,7 +46,7 @@ export default function Home(): React.ReactElement { if (isError && videos.length === 0) { return ( - + Error loading videos refetch()} @@ -54,12 +54,12 @@ export default function Home(): React.ReactElement { > Retry - + ); } return ( - + - + ); } diff --git a/src/app/(app)/library.tsx b/src/app/(app)/library.tsx index 50f2720..b985dc9 100644 --- a/src/app/(app)/library.tsx +++ b/src/app/(app)/library.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { Alert, ScrollView, Text, TouchableOpacity, View, useWindowDimensions } from 'react-native'; import { Swipeable } from 'react-native-gesture-handler'; -import { FocusAwareStatusBar, SafeAreaView } from '@/components/ui'; +import { FocusAwareStatusBar } from '@/components/ui'; import { Trash } from '@/components/ui/icons'; import { VideoTile } from '@/components/video-tile'; import type { DownloadMetadata } from '@/lib/download'; @@ -372,7 +372,11 @@ export default function Library(): React.ReactElement { }; return ( - + {TABS.map((tab, index) => ( @@ -404,6 +408,6 @@ export default function Library(): React.ReactElement { ))} - + ); } diff --git a/src/app/(app)/search.tsx b/src/app/(app)/search.tsx index 00a1472..572835b 100644 --- a/src/app/(app)/search.tsx +++ b/src/app/(app)/search.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { ActivityIndicator, ScrollView, TextInput, TouchableOpacity, View } from 'react-native'; import { useSearch } from '@/api/search'; -import { FocusAwareStatusBar, SafeAreaView, Text } from '@/components/ui'; +import { FocusAwareStatusBar, Text } from '@/components/ui'; import { VideoTile } from '@/components/video-tile'; import { useTranslate } from '@/lib'; import { flattenUniquePages } from '@/lib/flatten-pages'; @@ -95,7 +95,11 @@ export default function SearchScreen() { const showResults = submittedQuery.length > 0; return ( - + )} - + ); } diff --git a/src/app/(app)/settings.tsx b/src/app/(app)/settings.tsx index a7d7af7..f066943 100644 --- a/src/app/(app)/settings.tsx +++ b/src/app/(app)/settings.tsx @@ -9,7 +9,7 @@ import { AppIconItem } from '@/components/settings/app-icon-item'; import { ItemsContainer } from '@/components/settings/items-container'; import { LanguageItem } from '@/components/settings/language-item'; import { ThemeItem } from '@/components/settings/theme-item'; -import { FocusAwareStatusBar, SafeAreaView, ScrollView, Text, View, colors } from '@/components/ui'; +import { FocusAwareStatusBar, ScrollView, Text, View, colors } from '@/components/ui'; import { Trash } from '@/components/ui/icons'; import { UpdateDialog } from '@/components/update-dialog'; import { LOCK_TIMEOUT_OPTIONS, useSecuritySettings } from '@/lib/hooks/use-security-settings'; @@ -141,7 +141,7 @@ export default function Settings() { return ( <> - + {/* Preferences */} @@ -367,7 +367,7 @@ export default function Settings() { - +