[iPad support] Custom tab navigator for native tablet, with side nav (#3385)

* fix fab position

* show left nav

* custom tab navigator for tablets

* re-disable right nav on tablet

* fix unread count not being rounded

* use mobile subpage header on native tablet

* fix feed being hidden under header

* rename misleading variable name

* remove drawer on native tablet, as on web tablet

* fix prompt styles on tablet

* import isWeb (ts-ignore 😡)

* disable emoji picker on tablet

* prettier format
This commit is contained in:
Samuel Newman
2024-04-10 14:59:54 +01:00
parent 4f226a23e0
commit 8fcd649544
11 changed files with 373 additions and 135 deletions
+224 -42
View File
@@ -1,5 +1,6 @@
import * as React from 'react' import * as React from 'react'
import {JSX} from 'react/jsx-runtime' import {JSX} from 'react/jsx-runtime'
import {View} from 'react-native'
import {i18n, MessageDescriptor} from '@lingui/core' import {i18n, MessageDescriptor} from '@lingui/core'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import { import {
@@ -19,7 +20,7 @@ import {timeout} from '#/lib/async/timeout'
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration' import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration'
import {buildStateObject} from '#/lib/routes/helpers' import {buildStateObject} from '#/lib/routes/helpers'
import { import type {
AllNavigatorParams, AllNavigatorParams,
BottomTabNavigatorParams, BottomTabNavigatorParams,
FlatNavigatorParams, FlatNavigatorParams,
@@ -28,11 +29,12 @@ import {
MyProfileTabNavigatorParams, MyProfileTabNavigatorParams,
NotificationsTabNavigatorParams, NotificationsTabNavigatorParams,
SearchTabNavigatorParams, SearchTabNavigatorParams,
SideTabNavigatorParams,
} from '#/lib/routes/types' } from '#/lib/routes/types'
import {RouteParams, State} from '#/lib/routes/types' import {RouteParams, State} from '#/lib/routes/types'
import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig' import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'
import {bskyTitle} from '#/lib/strings/headings' import {bskyTitle} from '#/lib/strings/headings'
import {isAndroid, isNative, isWeb} from '#/platform/detection' import {isAndroid, isNative, isNativeTablet, isWeb} from '#/platform/detection'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useUnreadNotifications} from '#/state/queries/notifications/unread' import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
@@ -78,27 +80,27 @@ import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy' import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers' import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy' import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
import {AboutSettingsScreen} from '#/screens/Settings/AboutSettings'
import {AccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings'
import {AccountSettingsScreen} from '#/screens/Settings/AccountSettings'
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings' import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords'
import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings'
import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences'
import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences'
import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings'
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings' import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings'
import {SettingsScreen} from '#/screens/Settings/Settings'
import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences'
import { import {
StarterPackScreen, StarterPackScreen,
StarterPackScreenShort, StarterPackScreenShort,
} from '#/screens/StarterPack/StarterPackScreen' } from '#/screens/StarterPack/StarterPackScreen'
import {Wizard} from '#/screens/StarterPack/Wizard' import {Wizard} from '#/screens/StarterPack/Wizard'
import {useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {router} from '#/routes' import {router} from '#/routes'
import {Referrer} from '../modules/expo-bluesky-swiss-army' import {Referrer} from '../modules/expo-bluesky-swiss-army'
import {AboutSettingsScreen} from './screens/Settings/AboutSettings'
import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings'
import {AccountSettingsScreen} from './screens/Settings/AccountSettings'
import {AppPasswordsScreen} from './screens/Settings/AppPasswords'
import {ContentAndMediaSettingsScreen} from './screens/Settings/ContentAndMediaSettings'
import {ExternalMediaPreferencesScreen} from './screens/Settings/ExternalMediaPreferences'
import {FollowingFeedPreferencesScreen} from './screens/Settings/FollowingFeedPreferences'
import {LanguageSettingsScreen} from './screens/Settings/LanguageSettings'
import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings'
import {SettingsScreen} from './screens/Settings/Settings'
import {ThreadPreferencesScreen} from './screens/Settings/ThreadPreferences'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>() const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@@ -110,15 +112,23 @@ const MyProfileTab =
createNativeStackNavigatorWithAuth<MyProfileTabNavigatorParams>() createNativeStackNavigatorWithAuth<MyProfileTabNavigatorParams>()
const MessagesTab = const MessagesTab =
createNativeStackNavigatorWithAuth<MessagesTabNavigatorParams>() createNativeStackNavigatorWithAuth<MessagesTabNavigatorParams>()
const ListsTab = createNativeStackNavigatorWithAuth<FlatNavigatorParams>()
const ModerationTab = createNativeStackNavigatorWithAuth<FlatNavigatorParams>()
const SettingsTab = createNativeStackNavigatorWithAuth<FlatNavigatorParams>()
const Flat = createNativeStackNavigatorWithAuth<FlatNavigatorParams>() const Flat = createNativeStackNavigatorWithAuth<FlatNavigatorParams>()
const Tab = createBottomTabNavigator<BottomTabNavigatorParams>()
const MobileTab = createBottomTabNavigator<BottomTabNavigatorParams>()
const TabletTab = createBottomTabNavigator<SideTabNavigatorParams>()
/** /**
* These "common screens" are reused across stacks. * These "common screens" are reused across stacks.
*/ */
function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { function commonScreens(
Stack: typeof HomeTab,
opts?: {unreadCountLabel?: string},
) {
const title = (page: MessageDescriptor) => const title = (page: MessageDescriptor) =>
bskyTitle(i18n._(page), unreadCountLabel) bskyTitle(i18n._(page), opts?.unreadCountLabel)
return ( return (
<> <>
@@ -127,16 +137,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => NotFoundScreen} getComponent={() => NotFoundScreen}
options={{title: title(msg`Not Found`)}} options={{title: title(msg`Not Found`)}}
/> />
<Stack.Screen
name="Lists"
component={ListsScreen}
options={{title: title(msg`Lists`), requireAuth: true}}
/>
<Stack.Screen
name="Moderation"
getComponent={() => ModerationScreen}
options={{title: title(msg`Moderation`), requireAuth: true}}
/>
<Stack.Screen <Stack.Screen
name="ModerationModlists" name="ModerationModlists"
getComponent={() => ModerationModlistsScreen} getComponent={() => ModerationModlistsScreen}
@@ -152,11 +152,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => ModerationBlockedAccounts} getComponent={() => ModerationBlockedAccounts}
options={{title: title(msg`Blocked Accounts`), requireAuth: true}} options={{title: title(msg`Blocked Accounts`), requireAuth: true}}
/> />
<Stack.Screen
name="Settings"
getComponent={() => SettingsScreen}
options={{title: title(msg`Settings`), requireAuth: true}}
/>
<Stack.Screen <Stack.Screen
name="LanguageSettings" name="LanguageSettings"
getComponent={() => LanguageSettingsScreen} getComponent={() => LanguageSettingsScreen}
@@ -166,7 +161,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
name="Profile" name="Profile"
getComponent={() => ProfileScreen} getComponent={() => ProfileScreen}
options={({route}) => ({ options={({route}) => ({
title: bskyTitle(`@${route.params.name}`, unreadCountLabel), title: bskyTitle(`@${route.params.name}`, opts?.unreadCountLabel),
})} })}
/> />
<Stack.Screen <Stack.Screen
@@ -407,15 +402,38 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => Wizard} getComponent={() => Wizard}
options={{title: title(msg`Edit your starter pack`), requireAuth: true}} options={{title: title(msg`Edit your starter pack`), requireAuth: true}}
/> />
{!isNativeTablet && (
<>
<Stack.Screen
name="Moderation"
getComponent={() => ModerationScreen}
options={{title: title(msg`Moderation`), requireAuth: true}}
/>
<Stack.Screen
name="Lists"
component={ListsScreen}
options={{title: title(msg`Lists`), requireAuth: true}}
/>
<Stack.Screen
name="Settings"
getComponent={() => SettingsScreen}
options={{title: title(msg`Settings`), requireAuth: true}}
/>
</>
)}
</> </>
) )
} }
/** /**
* The TabsNavigator is used by native mobile to represent the routes * The TabsNavigator is used by native mobile to represent the routes
* in 3 distinct tab-stacks with a different root screen on each. * in the distinct tab-stacks with a different root screen on each.
*/ */
function TabsNavigator() { function TabsNavigator() {
return isNativeTablet ? <TabNavigatorTablet /> : <TabsNavigatorMobile />
}
function TabsNavigatorMobile() {
const tabBar = React.useCallback( const tabBar = React.useCallback(
(props: JSX.IntrinsicAttributes & BottomTabBarProps) => ( (props: JSX.IntrinsicAttributes & BottomTabBarProps) => (
<BottomBar {...props} /> <BottomBar {...props} />
@@ -424,26 +442,91 @@ function TabsNavigator() {
) )
return ( return (
<Tab.Navigator <MobileTab.Navigator
initialRouteName="HomeTab" initialRouteName="HomeTab"
backBehavior="initialRoute" backBehavior="initialRoute"
screenOptions={{headerShown: false, lazy: true}} screenOptions={{headerShown: false, lazy: true}}
tabBar={tabBar}> tabBar={tabBar}>
<Tab.Screen name="HomeTab" getComponent={() => HomeTabNavigator} /> <MobileTab.Screen name="HomeTab" getComponent={() => HomeTabNavigator} />
<Tab.Screen name="SearchTab" getComponent={() => SearchTabNavigator} /> <MobileTab.Screen
<Tab.Screen name="SearchTab"
getComponent={() => SearchTabNavigator}
/>
<MobileTab.Screen
name="FeedsTab"
getComponent={() => FeedsTabNavigator}
/>
<MobileTab.Screen
name="NotificationsTab" name="NotificationsTab"
getComponent={() => NotificationsTabNavigator} getComponent={() => NotificationsTabNavigator}
/> />
<Tab.Screen <MobileTab.Screen
name="MyProfileTab" name="MyProfileTab"
getComponent={() => MyProfileTabNavigator} getComponent={() => MyProfileTabNavigator}
/> />
<Tab.Screen <MobileTab.Screen
name="MessagesTab" name="MessagesTab"
getComponent={() => MessagesTabNavigator} getComponent={() => MessagesTabNavigator}
/> />
</Tab.Navigator> </MobileTab.Navigator>
)
}
/**
* The TabNavigatorTablet is used by native tablet to represent the routes
* in all the extra tabs shown in the tablet left nav.
*/
function TabNavigatorTablet() {
// we can't style the wrapper component of the tab navigator
// to set it to flex_row, so put the left nav outside of the tab navigator
const tabBar = React.useCallback(() => null, [])
return (
<View style={[a.flex_row, a.flex_1]}>
<LeftNav />
<TabletTab.Navigator
initialRouteName="HomeTab"
backBehavior="initialRoute"
screenOptions={{headerShown: false, lazy: true}}
tabBar={tabBar}>
<TabletTab.Screen
name="HomeTab"
getComponent={() => HomeTabNavigator}
/>
<TabletTab.Screen
name="SearchTab"
getComponent={() => SearchTabNavigator}
/>
<TabletTab.Screen
name="NotificationsTab"
getComponent={() => NotificationsTabNavigator}
/>
<TabletTab.Screen
name="MessagesTab"
getComponent={() => MessagesTabNavigator}
/>
<TabletTab.Screen
name="FeedsTab"
getComponent={() => FeedsTabNavigator}
/>
<TabletTab.Screen
name="ListsTab"
getComponent={() => ListsTabNavigator}
/>
<TabletTab.Screen
name="ModerationTab"
getComponent={() => ModerationTabNavigator}
/>
<TabletTab.Screen
name="MyProfileTab"
getComponent={() => MyProfileTabNavigator}
/>
<TabletTab.Screen
name="SettingsTab"
getComponent={() => SettingsTabNavigator}
/>
</TabletTab.Navigator>
</View>
) )
} }
@@ -485,6 +568,24 @@ function SearchTabNavigator() {
) )
} }
function FeedsTabNavigator() {
const t = useTheme()
return (
<FeedsTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
contentStyle: t.atoms.bg,
}}>
<FeedsTab.Screen name="Feeds" getComponent={() => FeedsScreen} />
{commonScreens(FeedsTab as typeof HomeTab)}
</FeedsTab.Navigator>
)
}
function NotificationsTabNavigator() { function NotificationsTabNavigator() {
const t = useTheme() const t = useTheme()
return ( return (
@@ -558,6 +659,72 @@ function MessagesTabNavigator() {
) )
} }
function ListsTabNavigator() {
const t = useTheme()
return (
<ListsTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
contentStyle: t.atoms.bg,
}}>
<ListsTab.Screen
name="Lists"
getComponent={() => ListsScreen}
options={{requireAuth: true}}
/>
{commonScreens(ListsTab as typeof HomeTab)}
</ListsTab.Navigator>
)
}
function ModerationTabNavigator() {
const t = useTheme()
return (
<ModerationTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
contentStyle: t.atoms.bg,
}}>
<ModerationTab.Screen
name="Moderation"
getComponent={() => ModerationScreen}
options={{requireAuth: true}}
/>
{commonScreens(ModerationTab as typeof HomeTab)}
</ModerationTab.Navigator>
)
}
function SettingsTabNavigator() {
const t = useTheme()
return (
<SettingsTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
contentStyle: t.atoms.bg,
}}>
<SettingsTab.Screen
name="Settings"
getComponent={() => SettingsScreen}
options={{requireAuth: true}}
/>
{commonScreens(SettingsTab as typeof HomeTab)}
</SettingsTab.Navigator>
)
}
/** /**
* The FlatNavigator is used by Web to represent the routes * The FlatNavigator is used by Web to represent the routes
* in a single ("flat") stack. * in a single ("flat") stack.
@@ -604,7 +771,7 @@ const FlatNavigator = () => {
getComponent={() => HomeScreen} getComponent={() => HomeScreen}
options={{title: title(msg`Home`)}} options={{title: title(msg`Home`)}}
/> />
{commonScreens(Flat as typeof HomeTab, numUnread)} {commonScreens(Flat as typeof HomeTab, {unreadCountLabel: numUnread})}
</Flat.Navigator> </Flat.Navigator>
) )
} }
@@ -651,6 +818,9 @@ const LINKING = {
if (name === 'Search') { if (name === 'Search') {
return buildStateObject('SearchTab', 'Search', params) return buildStateObject('SearchTab', 'Search', params)
} }
if (name === 'Feeds') {
return buildStateObject('FeedsTab', 'Feeds', params)
}
if (name === 'Notifications') { if (name === 'Notifications') {
return buildStateObject('NotificationsTab', 'Notifications', params) return buildStateObject('NotificationsTab', 'Notifications', params)
} }
@@ -660,6 +830,17 @@ const LINKING = {
if (name === 'Messages') { if (name === 'Messages') {
return buildStateObject('MessagesTab', 'Messages', params) return buildStateObject('MessagesTab', 'Messages', params)
} }
if (isNativeTablet) {
if (name === 'Lists') {
return buildStateObject('ListsTab', 'Lists', params)
}
if (name === 'Moderation') {
return buildStateObject('ModerationTab', 'Moderation', params)
}
if (name === 'Settings') {
return buildStateObject('SettingsTab', 'Settings', params)
}
}
// if the path is something else, like a post, profile, or even settings, we need to initialize the home tab as pre-existing state otherwise the back button will not work // if the path is something else, like a post, profile, or even settings, we need to initialize the home tab as pre-existing state otherwise the back button will not work
return buildStateObject('HomeTab', name, params, [ return buildStateObject('HomeTab', name, params, [
{ {
@@ -855,5 +1036,6 @@ export {
reset, reset,
resetToTab, resetToTab,
RoutesContainer, RoutesContainer,
TabNavigatorTablet,
TabsNavigator, TabsNavigator,
} }
+5 -2
View File
@@ -3,6 +3,7 @@ import {GestureResponderEvent, View} from 'react-native'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {isNativeTablet} from '#/platform/detection'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonColor, ButtonText} from '#/components/Button' import {Button, ButtonColor, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
@@ -52,7 +53,9 @@ export function Outer({
accessibilityLabelledBy={titleId} accessibilityLabelledBy={titleId}
accessibilityDescribedBy={descriptionId} accessibilityDescribedBy={descriptionId}
style={[ style={[
gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, gtMobile && !isNativeTablet
? {width: 'auto', maxWidth: 400, minWidth: 200}
: a.w_full,
]}> ]}>
{children} {children}
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
@@ -97,7 +100,7 @@ export function Actions({children}: React.PropsWithChildren<{}>) {
a.w_full, a.w_full,
a.gap_md, a.gap_md,
a.justify_end, a.justify_end,
gtMobile gtMobile && !isNativeTablet
? [a.flex_row, a.flex_row_reverse, a.justify_start] ? [a.flex_row, a.flex_row_reverse, a.justify_start]
: [a.flex_col], : [a.flex_col],
]}> ]}>
+12
View File
@@ -65,6 +65,18 @@ export type BottomTabNavigatorParams = CommonNavigatorParams & {
MessagesTab: undefined MessagesTab: undefined
} }
export type SideTabNavigatorParams = CommonNavigatorParams & {
HomeTab: undefined
SearchTab: undefined
FeedsTab: undefined
NotificationsTab: undefined
MyProfileTab: undefined
MessagesTab: undefined
ListsTab: undefined
ModerationTab: undefined
SettingsTab: undefined
}
export type HomeTabNavigatorParams = CommonNavigatorParams & { export type HomeTabNavigatorParams = CommonNavigatorParams & {
Home: undefined Home: undefined
} }
+7 -1
View File
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
import {HITSLOP_10} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants'
import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform' import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {isWeb} from '#/platform/detection' import {isNativeTablet, isWeb} from '#/platform/detection'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useSetDrawerOpen} from '#/state/shell/drawer-open' import {useSetDrawerOpen} from '#/state/shell/drawer-open'
import {useShellLayout} from '#/state/shell/shell-layout' import {useShellLayout} from '#/state/shell/shell-layout'
@@ -46,6 +46,7 @@ export function HomeHeaderLayoutMobile({
headerHeight.value = e.nativeEvent.layout.height headerHeight.value = e.nativeEvent.layout.height
}}> }}>
<View style={[pal.view, styles.topBar]}> <View style={[pal.view, styles.topBar]}>
{!isNativeTablet ? (
<View style={[pal.view, {width: 100}]}> <View style={[pal.view, {width: 100}]}>
<TouchableOpacity <TouchableOpacity
testID="viewHeaderDrawerBtn" testID="viewHeaderDrawerBtn"
@@ -59,6 +60,11 @@ export function HomeHeaderLayoutMobile({
<Menu size="lg" fill={t.atoms.text_contrast_medium.color} /> <Menu size="lg" fill={t.atoms.text_contrast_medium.color} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
) : (
// subtract width of LeftNav
<View style={{width: 100 - 77}} />
)}
<View> <View>
<Logo width={30} /> <Logo width={30} />
</View> </View>
@@ -11,7 +11,7 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {NavigationProp} from '#/lib/routes/types' import {NavigationProp} from '#/lib/routes/types'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {isNative} from '#/platform/detection' import {isNative, isNativeTablet} from '#/platform/detection'
import {emitSoftReset} from '#/state/events' import {emitSoftReset} from '#/state/events'
import {useLightboxControls} from '#/state/lightbox' import {useLightboxControls} from '#/state/lightbox'
import {useSetDrawerOpen} from '#/state/shell' import {useSetDrawerOpen} from '#/state/shell'
@@ -89,9 +89,11 @@ export function ProfileSubpageHeader({
} }
}, [openLightbox, avatar]) }, [openLightbox, avatar])
const isMobileLayout = isMobile || isNativeTablet
return ( return (
<CenteredView style={pal.view}> <CenteredView style={pal.view}>
{isMobile && ( {isMobileLayout && (
<View <View
style={[ style={[
{ {
@@ -100,7 +102,7 @@ export function ProfileSubpageHeader({
borderBottomWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
paddingTop: isNative ? 0 : 8, paddingTop: isNative ? 0 : 8,
paddingBottom: 8, paddingBottom: 8,
paddingHorizontal: isMobile ? 12 : 14, paddingHorizontal: isMobileLayout ? 12 : 14,
}, },
pal.border, pal.border,
]}> ]}>
@@ -133,7 +135,7 @@ export function ProfileSubpageHeader({
gap: 10, gap: 10,
paddingTop: 14, paddingTop: 14,
paddingBottom: 6, paddingBottom: 6,
paddingHorizontal: isMobile ? 12 : 14, paddingHorizontal: isMobileLayout ? 12 : 14,
}}> }}>
<Pressable <Pressable
testID="headerAviButton" testID="headerAviButton"
@@ -188,7 +190,7 @@ export function ProfileSubpageHeader({
</Text> </Text>
)} )}
</View> </View>
{!isMobile && ( {!isMobileLayout && (
<View <View
style={{ style={{
flexDirection: 'row', flexDirection: 'row',
+3 -3
View File
@@ -12,7 +12,7 @@ import {useNavigation} from '@react-navigation/native'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {NavigationProp} from '#/lib/routes/types' import {NavigationProp} from '#/lib/routes/types'
import {isWeb} from '#/platform/detection' import {isNativeTablet, isWeb} from '#/platform/detection'
import {useSetDrawerOpen} from '#/state/shell' import {useSetDrawerOpen} from '#/state/shell'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {CenteredView} from './Views' import {CenteredView} from './Views'
@@ -70,9 +70,9 @@ export function SimpleViewHeader({
icon="angle-left" icon="angle-left"
style={[styles.backIcon, pal.text]} style={[styles.backIcon, pal.text]}
/> />
) : ( ) : !isNativeTablet ? (
<Menu size="lg" style={[{marginTop: 4}, pal.textLight]} /> <Menu size="lg" style={[{marginTop: 4}, pal.textLight]} />
)} ) : null}
</TouchableOpacity> </TouchableOpacity>
) : null} ) : null}
{children} {children}
+10 -18
View File
@@ -1,18 +1,16 @@
import React from 'react' import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native'
import Animated from 'react-native-reanimated' import Animated from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useMediaQuery} from 'react-responsive' import {useMediaQuery} from 'react-responsive'
import {HITSLOP_20} from '#/lib/constants' import {HITSLOP_20} from '#/lib/constants'
import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform' import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {clamp} from '#/lib/numbers'
import {colors} from '#/lib/styles' import {colors} from '#/lib/styles'
import {isNativeTablet, isWeb} from '#/platform/detection' import {isNativeTablet} from '#/platform/detection'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useTheme} from '#/alf'
const AnimatedTouchableOpacity = const AnimatedTouchableOpacity =
Animated.createAnimatedComponent(TouchableOpacity) Animated.createAnimatedComponent(TouchableOpacity)
@@ -26,24 +24,17 @@ export function LoadLatestBtn({
label: string label: string
showIndicator: boolean showIndicator: boolean
}) { }) {
const pal = usePalette('default') const t = useTheme()
const {hasSession} = useSession() const {hasSession} = useSession()
const {isDesktop, isTablet, isMobile, isTabletOrMobile} = useWebMediaQueries() const {isDesktop, isTablet, isMobile, isTabletOrMobile} = useWebMediaQueries()
const fabMinimalShellTransform = useMinimalShellFabTransform() const fabMinimalShellTransform = useMinimalShellFabTransform()
const insets = useSafeAreaInsets()
// move button inline if it starts overlapping the left nav // move button inline if it starts overlapping the left nav
const isTallViewport = useMediaQuery({minHeight: 700}) const isTallViewport = useMediaQuery({minHeight: 700})
// Adjust height of the fab if we have a session only on mobile web. If we don't have a session, we want to adjust // Adjust height of the fab if we have a session only on mobile web. If we don't have a session, we want to adjust
// it on both tablet and mobile since we are showing the bottom bar (see createNativeStackNavigatorWithAuth) // it on both tablet and mobile since we are showing the bottom bar (see createNativeStackNavigatorWithAuth)
const showBottomBar = hasSession const showBottomBar = hasSession ? isMobile : isTabletOrMobile
? isMobile || isNativeTablet
: isTabletOrMobile
const bottomPosition = isTablet
? {bottom: 50}
: {bottom: clamp(insets.bottom, 15, 60) + 15}
return ( return (
<AnimatedTouchableOpacity <AnimatedTouchableOpacity
@@ -54,9 +45,8 @@ export function LoadLatestBtn({
? styles.loadLatestOutOfLine ? styles.loadLatestOutOfLine
: styles.loadLatestInline), : styles.loadLatestInline),
isTablet && !isNativeTablet && styles.loadLatestInline, isTablet && !isNativeTablet && styles.loadLatestInline,
pal.borderDark, t.atoms.border_contrast_high,
pal.view, t.atoms.bg,
bottomPosition,
showBottomBar && fabMinimalShellTransform, showBottomBar && fabMinimalShellTransform,
]} ]}
onPress={onPress} onPress={onPress}
@@ -64,8 +54,10 @@ export function LoadLatestBtn({
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={label} accessibilityLabel={label}
accessibilityHint=""> accessibilityHint="">
<FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} /> <FontAwesomeIcon icon="angle-up" color={t.atoms.text.color} size={19} />
{showIndicator && <View style={[styles.indicator, pal.borderDark]} />} {showIndicator && (
<View style={[styles.indicator, t.atoms.border_contrast_high]} />
)}
</AnimatedTouchableOpacity> </AnimatedTouchableOpacity>
) )
} }
@@ -36,7 +36,7 @@ import {Onboarding} from '#/screens/Onboarding'
import {SignupQueued} from '#/screens/SignupQueued' import {SignupQueued} from '#/screens/SignupQueued'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {BottomBarWeb} from './bottom-bar/BottomBarWeb' import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
import {DesktopLeftNav} from './desktop/LeftNav' import {LeftNav} from './desktop/LeftNav'
import {DesktopRightNav} from './desktop/RightNav' import {DesktopRightNav} from './desktop/RightNav'
type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & { type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & {
@@ -149,7 +149,7 @@ function NativeStackNavigator({
{isWeb && showBottomBar && <BottomBarWeb />} {isWeb && showBottomBar && <BottomBarWeb />}
{isWeb && !showBottomBar && ( {isWeb && !showBottomBar && (
<> <>
<DesktopLeftNav /> <LeftNav />
<DesktopRightNav routeName={activeRoute.name} /> <DesktopRightNav routeName={activeRoute.name} />
</> </>
)} )}
+43 -4
View File
@@ -350,7 +350,7 @@ function ChatNavItem() {
) )
} }
export function DesktopLeftNav() { export function LeftNav() {
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
@@ -519,16 +519,18 @@ export function DesktopLeftNav() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
leftNav: {
// @ts-ignore web only // @ts-ignore web only
leftNav: isWeb
? {
position: 'fixed', position: 'fixed',
top: 10, top: 10,
// @ts-ignore web only
left: 'calc(50vw - 300px - 220px - 20px)', left: 'calc(50vw - 300px - 220px - 20px)',
width: 220, width: 220,
// @ts-ignore web only
maxHeight: 'calc(100vh - 10px)', maxHeight: 'calc(100vh - 10px)',
overflowY: 'auto', overflowY: 'auto',
}
: {
paddingTop: 12,
}, },
leftNavTablet: { leftNavTablet: {
top: 0, top: 0,
@@ -556,4 +558,41 @@ const styles = StyleSheet.create({
width: 30, width: 30,
height: 30, height: 30,
}, },
navItemWrapper: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
padding: 12,
borderRadius: 8,
gap: 10,
},
navItemIconWrapper: {
alignItems: 'center',
justifyContent: 'center',
width: 28,
height: 24,
marginTop: 2,
zIndex: 1,
},
navItemIconWrapperTablet: {
width: 40,
height: 40,
},
navItemCount: {
position: 'absolute',
top: 0,
left: 15,
backgroundColor: colors.blue3,
color: colors.white,
fontSize: 12,
fontWeight: 'bold',
paddingHorizontal: 4,
borderRadius: 6,
overflow: 'hidden',
},
navItemCountTablet: {
left: 18,
fontSize: 14,
},
}) })
+5 -2
View File
@@ -119,14 +119,17 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
rightNav: {
// @ts-ignore web only // @ts-ignore web only
rightNav: isWeb
? {
position: 'fixed', position: 'fixed',
// @ts-ignore web only
left: 'calc(50vw + 300px + 20px)', left: 'calc(50vw + 300px + 20px)',
width: 300, width: 300,
maxHeight: '100%', maxHeight: '100%',
overflowY: 'auto', overflowY: 'auto',
}
: {
width: 300,
}, },
message: { message: {
+9 -10
View File
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {BackHandler, StyleSheet, useWindowDimensions, View} from 'react-native' import {BackHandler, useWindowDimensions, View} from 'react-native'
import {Drawer} from 'react-native-drawer-layout' import {Drawer} from 'react-native-drawer-layout'
import Animated from 'react-native-reanimated' import Animated from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
@@ -14,7 +14,7 @@ import {usePalette} from '#/lib/hooks/usePalette'
import {useNotificationsRegistration} from '#/lib/notifications/notifications' import {useNotificationsRegistration} from '#/lib/notifications/notifications'
import {isStateAtTabRoot} from '#/lib/routes/helpers' import {isStateAtTabRoot} from '#/lib/routes/helpers'
import {useTheme} from '#/lib/ThemeContext' import {useTheme} from '#/lib/ThemeContext'
import {isAndroid, isIOS} from '#/platform/detection' import {isAndroid, isIOS, isNativeTablet} from '#/platform/detection'
import {useDialogStateControlContext} from '#/state/dialogs' import {useDialogStateControlContext} from '#/state/dialogs'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import { import {
@@ -26,6 +26,7 @@ import {useCloseAnyActiveElement} from '#/state/util'
import {Lightbox} from '#/view/com/lightbox/Lightbox' import {Lightbox} from '#/view/com/lightbox/Lightbox'
import {ModalsContainer} from '#/view/com/modals/Modal' import {ModalsContainer} from '#/view/com/modals/Modal'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {useTheme as useAlfTheme} from '#/alf'
import {atoms as a, select, useTheme as useNewTheme} from '#/alf' import {atoms as a, select, useTheme as useNewTheme} from '#/alf'
import {MutedWordsDialog} from '#/components/dialogs/MutedWords' import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
import {SigninDialog} from '#/components/dialogs/Signin' import {SigninDialog} from '#/components/dialogs/Signin'
@@ -98,6 +99,7 @@ function ShellInner() {
<Animated.View style={[a.h_full]}> <Animated.View style={[a.h_full]}>
<ErrorBoundary <ErrorBoundary
style={{paddingTop: insets.top, paddingBottom: insets.bottom}}> style={{paddingTop: insets.top, paddingBottom: insets.bottom}}>
{!isNativeTablet ? (
<Drawer <Drawer
renderDrawerContent={renderDrawerContent} renderDrawerContent={renderDrawerContent}
drawerStyle={{width: Math.min(400, winDim.width * 0.8)}} drawerStyle={{width: Math.min(400, winDim.width * 0.8)}}
@@ -117,6 +119,9 @@ function ShellInner() {
}}> }}>
<TabsNavigator /> <TabsNavigator />
</Drawer> </Drawer>
) : (
<TabsNavigator />
)}
</ErrorBoundary> </ErrorBoundary>
</Animated.View> </Animated.View>
<Composer winHeight={winDim.height} /> <Composer winHeight={winDim.height} />
@@ -132,8 +137,8 @@ function ShellInner() {
export const Shell: React.FC = function ShellImpl() { export const Shell: React.FC = function ShellImpl() {
const {fullyExpandedCount} = useDialogStateControlContext() const {fullyExpandedCount} = useDialogStateControlContext()
const pal = usePalette('default')
const theme = useTheme() const theme = useTheme()
const t = useAlfTheme
useIntentHandler() useIntentHandler()
React.useEffect(() => { React.useEffect(() => {
@@ -146,7 +151,7 @@ export const Shell: React.FC = function ShellImpl() {
} }
}, [theme]) }, [theme])
return ( return (
<View testID="mobileShellView" style={[styles.outerContainer, pal.view]}> <View testID="mobileShellView" style={[a.h_full, t.atoms.bg]}>
<StatusBar <StatusBar
style={ style={
theme.colorScheme === 'dark' || (isIOS && fullyExpandedCount > 0) theme.colorScheme === 'dark' || (isIOS && fullyExpandedCount > 0)
@@ -161,9 +166,3 @@ export const Shell: React.FC = function ShellImpl() {
</View> </View>
) )
} }
const styles = StyleSheet.create({
outerContainer: {
height: '100%',
},
})