diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 0ab4bb613f..985a64d791 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import {JSX} from 'react/jsx-runtime' +import {View} from 'react-native' import {i18n, MessageDescriptor} from '@lingui/core' import {msg} from '@lingui/macro' import { @@ -19,7 +20,7 @@ import {timeout} from '#/lib/async/timeout' import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration' import {buildStateObject} from '#/lib/routes/helpers' -import { +import type { AllNavigatorParams, BottomTabNavigatorParams, FlatNavigatorParams, @@ -28,11 +29,12 @@ import { MyProfileTabNavigatorParams, NotificationsTabNavigatorParams, SearchTabNavigatorParams, + SideTabNavigatorParams, } from '#/lib/routes/types' import {RouteParams, State} from '#/lib/routes/types' import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig' 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 {useUnreadNotifications} from '#/state/queries/notifications/unread' import {useSession} from '#/state/session' @@ -78,27 +80,27 @@ import {PostQuotesScreen} from '#/screens/Post/PostQuotes' import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy' import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers' 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 {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 {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' +import {SettingsScreen} from '#/screens/Settings/Settings' +import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' import { StarterPackScreen, StarterPackScreenShort, } from '#/screens/StarterPack/StarterPackScreen' import {Wizard} from '#/screens/StarterPack/Wizard' -import {useTheme} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {router} from '#/routes' 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() @@ -110,15 +112,23 @@ const MyProfileTab = createNativeStackNavigatorWithAuth() const MessagesTab = createNativeStackNavigatorWithAuth() +const ListsTab = createNativeStackNavigatorWithAuth() +const ModerationTab = createNativeStackNavigatorWithAuth() +const SettingsTab = createNativeStackNavigatorWithAuth() const Flat = createNativeStackNavigatorWithAuth() -const Tab = createBottomTabNavigator() + +const MobileTab = createBottomTabNavigator() +const TabletTab = createBottomTabNavigator() /** * 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) => - bskyTitle(i18n._(page), unreadCountLabel) + bskyTitle(i18n._(page), opts?.unreadCountLabel) return ( <> @@ -127,16 +137,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { getComponent={() => NotFoundScreen} options={{title: title(msg`Not Found`)}} /> - - ModerationScreen} - options={{title: title(msg`Moderation`), requireAuth: true}} - /> ModerationModlistsScreen} @@ -152,11 +152,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { getComponent={() => ModerationBlockedAccounts} options={{title: title(msg`Blocked Accounts`), requireAuth: true}} /> - SettingsScreen} - options={{title: title(msg`Settings`), requireAuth: true}} - /> LanguageSettingsScreen} @@ -166,7 +161,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { name="Profile" getComponent={() => ProfileScreen} options={({route}) => ({ - title: bskyTitle(`@${route.params.name}`, unreadCountLabel), + title: bskyTitle(`@${route.params.name}`, opts?.unreadCountLabel), })} /> Wizard} options={{title: title(msg`Edit your starter pack`), requireAuth: true}} /> + {!isNativeTablet && ( + <> + ModerationScreen} + options={{title: title(msg`Moderation`), requireAuth: true}} + /> + + SettingsScreen} + options={{title: title(msg`Settings`), requireAuth: true}} + /> + + )} ) } /** * 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() { + return isNativeTablet ? : +} + +function TabsNavigatorMobile() { const tabBar = React.useCallback( (props: JSX.IntrinsicAttributes & BottomTabBarProps) => ( @@ -424,26 +442,91 @@ function TabsNavigator() { ) return ( - - HomeTabNavigator} /> - SearchTabNavigator} /> - HomeTabNavigator} /> + SearchTabNavigator} + /> + FeedsTabNavigator} + /> + NotificationsTabNavigator} /> - MyProfileTabNavigator} /> - MessagesTabNavigator} /> - + + ) +} + +/** + * 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 ( + + + + HomeTabNavigator} + /> + SearchTabNavigator} + /> + NotificationsTabNavigator} + /> + MessagesTabNavigator} + /> + FeedsTabNavigator} + /> + ListsTabNavigator} + /> + ModerationTabNavigator} + /> + MyProfileTabNavigator} + /> + SettingsTabNavigator} + /> + + ) } @@ -485,6 +568,24 @@ function SearchTabNavigator() { ) } +function FeedsTabNavigator() { + const t = useTheme() + return ( + + FeedsScreen} /> + {commonScreens(FeedsTab as typeof HomeTab)} + + ) +} + function NotificationsTabNavigator() { const t = useTheme() return ( @@ -558,6 +659,72 @@ function MessagesTabNavigator() { ) } +function ListsTabNavigator() { + const t = useTheme() + return ( + + ListsScreen} + options={{requireAuth: true}} + /> + {commonScreens(ListsTab as typeof HomeTab)} + + ) +} + +function ModerationTabNavigator() { + const t = useTheme() + return ( + + ModerationScreen} + options={{requireAuth: true}} + /> + {commonScreens(ModerationTab as typeof HomeTab)} + + ) +} + +function SettingsTabNavigator() { + const t = useTheme() + return ( + + SettingsScreen} + options={{requireAuth: true}} + /> + {commonScreens(SettingsTab as typeof HomeTab)} + + ) +} + /** * The FlatNavigator is used by Web to represent the routes * in a single ("flat") stack. @@ -604,7 +771,7 @@ const FlatNavigator = () => { getComponent={() => HomeScreen} options={{title: title(msg`Home`)}} /> - {commonScreens(Flat as typeof HomeTab, numUnread)} + {commonScreens(Flat as typeof HomeTab, {unreadCountLabel: numUnread})} ) } @@ -651,6 +818,9 @@ const LINKING = { if (name === 'Search') { return buildStateObject('SearchTab', 'Search', params) } + if (name === 'Feeds') { + return buildStateObject('FeedsTab', 'Feeds', params) + } if (name === 'Notifications') { return buildStateObject('NotificationsTab', 'Notifications', params) } @@ -660,6 +830,17 @@ const LINKING = { if (name === 'Messages') { 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 return buildStateObject('HomeTab', name, params, [ { @@ -855,5 +1036,6 @@ export { reset, resetToTab, RoutesContainer, + TabNavigatorTablet, TabsNavigator, } diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 7b33c6e256..899893b564 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -3,6 +3,7 @@ import {GestureResponderEvent, View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {isNativeTablet} from '#/platform/detection' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonColor, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -52,7 +53,9 @@ export function Outer({ accessibilityLabelledBy={titleId} accessibilityDescribedBy={descriptionId} style={[ - gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, + gtMobile && !isNativeTablet + ? {width: 'auto', maxWidth: 400, minWidth: 200} + : a.w_full, ]}> {children} @@ -97,7 +100,7 @@ export function Actions({children}: React.PropsWithChildren<{}>) { a.w_full, a.gap_md, a.justify_end, - gtMobile + gtMobile && !isNativeTablet ? [a.flex_row, a.flex_row_reverse, a.justify_start] : [a.flex_col], ]}> diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 41a47b4bc9..ac9f5014ae 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -65,6 +65,18 @@ export type BottomTabNavigatorParams = CommonNavigatorParams & { 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 & { Home: undefined } diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx index f5397d7172..4b8f472faf 100644 --- a/src/view/com/home/HomeHeaderLayoutMobile.tsx +++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx @@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react' import {HITSLOP_10} from '#/lib/constants' import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform' import {usePalette} from '#/lib/hooks/usePalette' -import {isWeb} from '#/platform/detection' +import {isNativeTablet, isWeb} from '#/platform/detection' import {useSession} from '#/state/session' import {useSetDrawerOpen} from '#/state/shell/drawer-open' import {useShellLayout} from '#/state/shell/shell-layout' @@ -46,19 +46,25 @@ export function HomeHeaderLayoutMobile({ headerHeight.value = e.nativeEvent.layout.height }}> - - - - - + {!isNativeTablet ? ( + + + + + + ) : ( + // subtract width of LeftNav + + )} + diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx index 5208224c50..ffcc76f9d9 100644 --- a/src/view/com/profile/ProfileSubpageHeader.tsx +++ b/src/view/com/profile/ProfileSubpageHeader.tsx @@ -11,7 +11,7 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {makeProfileLink} from '#/lib/routes/links' import {NavigationProp} from '#/lib/routes/types' import {sanitizeHandle} from '#/lib/strings/handles' -import {isNative} from '#/platform/detection' +import {isNative, isNativeTablet} from '#/platform/detection' import {emitSoftReset} from '#/state/events' import {useLightboxControls} from '#/state/lightbox' import {useSetDrawerOpen} from '#/state/shell' @@ -89,9 +89,11 @@ export function ProfileSubpageHeader({ } }, [openLightbox, avatar]) + const isMobileLayout = isMobile || isNativeTablet + return ( - {isMobile && ( + {isMobileLayout && ( @@ -133,7 +135,7 @@ export function ProfileSubpageHeader({ gap: 10, paddingTop: 14, paddingBottom: 6, - paddingHorizontal: isMobile ? 12 : 14, + paddingHorizontal: isMobileLayout ? 12 : 14, }}> )} - {!isMobile && ( + {!isMobileLayout && ( - ) : ( + ) : !isNativeTablet ? ( - )} + ) : null} ) : null} {children} diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx index 0d7820dbd9..55f3e6475e 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx @@ -1,18 +1,16 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' import Animated from 'react-native-reanimated' -import {useSafeAreaInsets} from 'react-native-safe-area-context' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useMediaQuery} from 'react-responsive' import {HITSLOP_20} from '#/lib/constants' import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform' -import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {clamp} from '#/lib/numbers' import {colors} from '#/lib/styles' -import {isNativeTablet, isWeb} from '#/platform/detection' +import {isNativeTablet} from '#/platform/detection' import {useSession} from '#/state/session' +import {useTheme} from '#/alf' const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity) @@ -26,24 +24,17 @@ export function LoadLatestBtn({ label: string showIndicator: boolean }) { - const pal = usePalette('default') + const t = useTheme() const {hasSession} = useSession() const {isDesktop, isTablet, isMobile, isTabletOrMobile} = useWebMediaQueries() const fabMinimalShellTransform = useMinimalShellFabTransform() - const insets = useSafeAreaInsets() // move button inline if it starts overlapping the left nav 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 // it on both tablet and mobile since we are showing the bottom bar (see createNativeStackNavigatorWithAuth) - const showBottomBar = hasSession - ? isMobile || isNativeTablet - : isTabletOrMobile - - const bottomPosition = isTablet - ? {bottom: 50} - : {bottom: clamp(insets.bottom, 15, 60) + 15} + const showBottomBar = hasSession ? isMobile : isTabletOrMobile return ( - - {showIndicator && } + + {showIndicator && ( + + )} ) } diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx index 9bcb91b7aa..29cd7bc693 100644 --- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx +++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx @@ -36,7 +36,7 @@ import {Onboarding} from '#/screens/Onboarding' import {SignupQueued} from '#/screens/SignupQueued' import {atoms as a} from '#/alf' import {BottomBarWeb} from './bottom-bar/BottomBarWeb' -import {DesktopLeftNav} from './desktop/LeftNav' +import {LeftNav} from './desktop/LeftNav' import {DesktopRightNav} from './desktop/RightNav' type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & { @@ -149,7 +149,7 @@ function NativeStackNavigator({ {isWeb && showBottomBar && } {isWeb && !showBottomBar && ( <> - + )} diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index acbd5076ae..556814378d 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -350,7 +350,7 @@ function ChatNavItem() { ) } -export function DesktopLeftNav() { +export function LeftNav() { const {hasSession, currentAccount} = useSession() const pal = usePalette('default') const {_} = useLingui() @@ -519,17 +519,19 @@ export function DesktopLeftNav() { } const styles = StyleSheet.create({ - leftNav: { - // @ts-ignore web only - position: 'fixed', - top: 10, - // @ts-ignore web only - left: 'calc(50vw - 300px - 220px - 20px)', - width: 220, - // @ts-ignore web only - maxHeight: 'calc(100vh - 10px)', - overflowY: 'auto', - }, + // @ts-ignore web only + leftNav: isWeb + ? { + position: 'fixed', + top: 10, + left: 'calc(50vw - 300px - 220px - 20px)', + width: 220, + maxHeight: 'calc(100vh - 10px)', + overflowY: 'auto', + } + : { + paddingTop: 12, + }, leftNavTablet: { top: 0, left: 0, @@ -556,4 +558,41 @@ const styles = StyleSheet.create({ width: 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, + }, }) diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index 5f75c220cc..2a8715e924 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -119,15 +119,18 @@ export function DesktopRightNav({routeName}: {routeName: string}) { } const styles = StyleSheet.create({ - rightNav: { - // @ts-ignore web only - position: 'fixed', - // @ts-ignore web only - left: 'calc(50vw + 300px + 20px)', - width: 300, - maxHeight: '100%', - overflowY: 'auto', - }, + // @ts-ignore web only + rightNav: isWeb + ? { + position: 'fixed', + left: 'calc(50vw + 300px + 20px)', + width: 300, + maxHeight: '100%', + overflowY: 'auto', + } + : { + width: 300, + }, message: { paddingVertical: 18, diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 6dc4f95a52..76a5e1e72f 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -1,5 +1,5 @@ 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 Animated from 'react-native-reanimated' 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 {isStateAtTabRoot} from '#/lib/routes/helpers' import {useTheme} from '#/lib/ThemeContext' -import {isAndroid, isIOS} from '#/platform/detection' +import {isAndroid, isIOS, isNativeTablet} from '#/platform/detection' import {useDialogStateControlContext} from '#/state/dialogs' import {useSession} from '#/state/session' import { @@ -26,6 +26,7 @@ import {useCloseAnyActiveElement} from '#/state/util' import {Lightbox} from '#/view/com/lightbox/Lightbox' import {ModalsContainer} from '#/view/com/modals/Modal' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' +import {useTheme as useAlfTheme} from '#/alf' import {atoms as a, select, useTheme as useNewTheme} from '#/alf' import {MutedWordsDialog} from '#/components/dialogs/MutedWords' import {SigninDialog} from '#/components/dialogs/Signin' @@ -98,25 +99,29 @@ function ShellInner() { - + {!isNativeTablet ? ( + + + + ) : ( - + )} @@ -132,8 +137,8 @@ function ShellInner() { export const Shell: React.FC = function ShellImpl() { const {fullyExpandedCount} = useDialogStateControlContext() - const pal = usePalette('default') const theme = useTheme() + const t = useAlfTheme useIntentHandler() React.useEffect(() => { @@ -146,7 +151,7 @@ export const Shell: React.FC = function ShellImpl() { } }, [theme]) return ( - + 0) @@ -161,9 +166,3 @@ export const Shell: React.FC = function ShellImpl() { ) } - -const styles = StyleSheet.create({ - outerContainer: { - height: '100%', - }, -})