about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/alf/breakpoints.ts15
-rw-r--r--src/components/Dialog/context.ts1
-rw-r--r--src/components/Dialog/index.tsx1
-rw-r--r--src/components/Dialog/index.web.tsx1
-rw-r--r--src/components/Dialog/types.ts2
-rw-r--r--src/components/Layout/Header/index.tsx10
-rw-r--r--src/components/Layout/index.tsx43
-rw-r--r--src/screens/Deactivated.tsx1
-rw-r--r--src/screens/Profile/Sections/Labels.tsx10
-rw-r--r--src/view/com/auth/SplashScreen.web.tsx8
-rw-r--r--src/view/com/posts/PostFeed.tsx9
-rw-r--r--src/view/com/util/Views.web.tsx33
-rw-r--r--src/view/com/util/load-latest/LoadLatestBtn.tsx64
-rw-r--r--src/view/screens/Home.tsx6
-rw-r--r--src/view/shell/createNativeStackNavigatorWithAuth.tsx7
-rw-r--r--src/view/shell/desktop/LeftNav.tsx86
-rw-r--r--src/view/shell/desktop/RightNav.tsx36
17 files changed, 211 insertions, 122 deletions
diff --git a/src/alf/breakpoints.ts b/src/alf/breakpoints.ts
index 934585644..f30a4b489 100644
--- a/src/alf/breakpoints.ts
+++ b/src/alf/breakpoints.ts
@@ -26,3 +26,18 @@ export function useBreakpoints(): Record<Breakpoint, boolean> & {
     }
   }, [gtPhone, gtMobile, gtTablet])
 }
+
+/**
+ * Fine-tuned breakpoints for the shell layout
+ */
+export function useLayoutBreakpoints() {
+  const rightNavVisible = useMediaQuery({minWidth: 1075})
+  const centerColumnOffset = useMediaQuery({minWidth: 1075, maxWidth: 1300})
+  const leftNavMinimal = useMediaQuery({maxWidth: 1300})
+
+  return {
+    rightNavVisible,
+    centerColumnOffset,
+    leftNavMinimal,
+  }
+}
diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts
index b479bc7f0..331ff3f33 100644
--- a/src/components/Dialog/context.ts
+++ b/src/components/Dialog/context.ts
@@ -14,6 +14,7 @@ export const Context = React.createContext<DialogContextProps>({
   nativeSnapPoint: BottomSheetSnapPoint.Hidden,
   disableDrag: false,
   setDisableDrag: () => {},
+  isWithinDialog: false,
 })
 
 export function useDialogContext() {
diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx
index e70e4aef4..463cadf3c 100644
--- a/src/components/Dialog/index.tsx
+++ b/src/components/Dialog/index.tsx
@@ -154,6 +154,7 @@ export function Outer({
       nativeSnapPoint: snapPoint,
       disableDrag,
       setDisableDrag,
+      isWithinDialog: true,
     }),
     [close, snapPoint, disableDrag, setDisableDrag],
   )
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index a27222229..153954691 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -97,6 +97,7 @@ export function Outer({
       nativeSnapPoint: 0,
       disableDrag: false,
       setDisableDrag: () => {},
+      isWithinDialog: true,
     }),
     [close],
   )
diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts
index b87bfe2b7..32886f3ce 100644
--- a/src/components/Dialog/types.ts
+++ b/src/components/Dialog/types.ts
@@ -44,6 +44,8 @@ export type DialogContextProps = {
   nativeSnapPoint: BottomSheetSnapPoint
   disableDrag: boolean
   setDisableDrag: React.Dispatch<React.SetStateAction<boolean>>
+  // in the event that the hook is used outside of a dialog
+  isWithinDialog: boolean
 }
 
 export type DialogControlOpenOptions = {
diff --git a/src/components/Layout/Header/index.tsx b/src/components/Layout/Header/index.tsx
index 3af0215c5..8ef114b44 100644
--- a/src/components/Layout/Header/index.tsx
+++ b/src/components/Layout/Header/index.tsx
@@ -14,6 +14,7 @@ import {
   TextStyleProp,
   useBreakpoints,
   useGutters,
+  useLayoutBreakpoints,
   useTheme,
   web,
 } from '#/alf'
@@ -23,6 +24,7 @@ import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
 import {
   BUTTON_VISUAL_ALIGNMENT_OFFSET,
   HEADER_SLOT_SIZE,
+  SCROLLBAR_OFFSET,
 } from '#/components/Layout/const'
 import {ScrollbarOffsetContext} from '#/components/Layout/context'
 import {Text} from '#/components/Typography'
@@ -42,6 +44,7 @@ export function Outer({
   const gutters = useGutters([0, 'base'])
   const {gtMobile} = useBreakpoints()
   const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
+  const {centerColumnOffset} = useLayoutBreakpoints()
 
   return (
     <View
@@ -60,7 +63,12 @@ export function Outer({
         }),
         t.atoms.border_contrast_low,
         gtMobile && [a.mx_auto, {maxWidth: 600}],
-        !isWithinOffsetView && a.scrollbar_offset,
+        !isWithinOffsetView && {
+          transform: [
+            {translateX: centerColumnOffset ? -150 : 0},
+            {translateX: web(SCROLLBAR_OFFSET) ?? 0},
+          ],
+        },
       ]}>
       {children}
     </View>
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx
index 489ebb225..623478a6a 100644
--- a/src/components/Layout/index.tsx
+++ b/src/components/Layout/index.tsx
@@ -13,7 +13,15 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
 
 import {isWeb} from '#/platform/detection'
 import {useShellLayout} from '#/state/shell/shell-layout'
-import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
+import {
+  atoms as a,
+  useBreakpoints,
+  useLayoutBreakpoints,
+  useTheme,
+  web,
+} from '#/alf'
+import {useDialogContext} from '#/components/Dialog'
+import {SCROLLBAR_OFFSET} from '#/components/Layout/const'
 import {ScrollbarOffsetContext} from '#/components/Layout/context'
 
 export * from '#/components/Layout/const'
@@ -47,6 +55,7 @@ export const Screen = React.memo(function Screen({
 export type ContentProps = AnimatedScrollViewProps & {
   style?: StyleProp<ViewStyle>
   contentContainerStyle?: StyleProp<ViewStyle>
+  ignoreTabletLayoutOffset?: boolean
 }
 
 /**
@@ -56,6 +65,7 @@ export const Content = React.memo(function Content({
   children,
   style,
   contentContainerStyle,
+  ignoreTabletLayoutOffset,
   ...props
 }: ContentProps) {
   const t = useTheme()
@@ -84,8 +94,10 @@ export const Content = React.memo(function Content({
       ]}
       {...props}>
       {isWeb ? (
-        // @ts-ignore web only -esb
-        <Center>{children}</Center>
+        <Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}>
+          {/* @ts-expect-error web only -esb */}
+          {children}
+        </Center>
       ) : (
         children
       )}
@@ -138,10 +150,13 @@ export const KeyboardAwareContent = React.memo(function LayoutScrollView({
 export const Center = React.memo(function LayoutContent({
   children,
   style,
+  ignoreTabletLayoutOffset,
   ...props
-}: ViewProps) {
+}: ViewProps & {ignoreTabletLayoutOffset?: boolean}) {
   const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
   const {gtMobile} = useBreakpoints()
+  const {centerColumnOffset} = useLayoutBreakpoints()
+  const {isWithinDialog} = useDialogContext()
   const ctx = useMemo(() => ({isWithinOffsetView: true}), [])
   return (
     <View
@@ -151,8 +166,20 @@ export const Center = React.memo(function LayoutContent({
         gtMobile && {
           maxWidth: 600,
         },
+        !isWithinOffsetView && {
+          transform: [
+            {
+              translateX:
+                centerColumnOffset &&
+                !ignoreTabletLayoutOffset &&
+                !isWithinDialog
+                  ? -150
+                  : 0,
+            },
+            {translateX: web(SCROLLBAR_OFFSET) ?? 0},
+          ],
+        },
         style,
-        !isWithinOffsetView && a.scrollbar_offset,
       ]}
       {...props}>
       <ScrollbarOffsetContext.Provider value={ctx}>
@@ -168,6 +195,7 @@ export const Center = React.memo(function LayoutContent({
 const WebCenterBorders = React.memo(function LayoutContent() {
   const t = useTheme()
   const {gtMobile} = useBreakpoints()
+  const {centerColumnOffset} = useLayoutBreakpoints()
   return gtMobile ? (
     <View
       style={[
@@ -180,9 +208,8 @@ const WebCenterBorders = React.memo(function LayoutContent() {
           width: 602,
           left: '50%',
           transform: [
-            {
-              translateX: '-50%',
-            },
+            {translateX: '-50%'},
+            {translateX: centerColumnOffset ? -150 : 0},
             ...a.scrollbar_offset.transform,
           ],
         }),
diff --git a/src/screens/Deactivated.tsx b/src/screens/Deactivated.tsx
index de03a8d68..602012f08 100644
--- a/src/screens/Deactivated.tsx
+++ b/src/screens/Deactivated.tsx
@@ -106,6 +106,7 @@ export function Deactivated() {
   return (
     <View style={[a.util_screen_outer, a.flex_1]}>
       <Layout.Content
+        ignoreTabletLayoutOffset
         contentContainerStyle={[
           a.px_2xl,
           {
diff --git a/src/screens/Profile/Sections/Labels.tsx b/src/screens/Profile/Sections/Labels.tsx
index 6c76d7b15..770464a71 100644
--- a/src/screens/Profile/Sections/Labels.tsx
+++ b/src/screens/Profile/Sections/Labels.tsx
@@ -15,7 +15,6 @@ import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
 import {useScrollHandlers} from '#/lib/ScrollContext'
 import {isNative} from '#/platform/detection'
 import {ListRef} from '#/view/com/util/List'
-import {ScrollView} from '#/view/com/util/Views'
 import {atoms as a, useTheme} from '#/alf'
 import {Divider} from '#/components/Divider'
 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
@@ -148,8 +147,8 @@ export function ProfileLabelsSectionInner({
   }, [labelerInfo, labelValues])
 
   return (
-    <ScrollView
-      // @ts-ignore TODO fix this
+    <Layout.Content
+      // @ts-expect-error TODO fix this
       ref={scrollElRef}
       scrollEventThrottle={1}
       contentContainerStyle={{
@@ -228,9 +227,8 @@ export function ProfileLabelsSectionInner({
             })}
           </View>
         )}
-
-        <View style={{height: 400}} />
+        <View style={{height: 100}} />
       </View>
-    </ScrollView>
+    </Layout.Content>
   )
 }
diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx
index eded80358..21b289e2c 100644
--- a/src/view/com/auth/SplashScreen.web.tsx
+++ b/src/view/com/auth/SplashScreen.web.tsx
@@ -16,9 +16,9 @@ import {
 import {atoms as a, useTheme} from '#/alf'
 import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
 import {Button, ButtonText} from '#/components/Button'
+import * as Layout from '#/components/Layout'
 import {InlineLinkText} from '#/components/Link'
 import {Text} from '#/components/Typography'
-import {CenteredView} from '../util/Views'
 
 export const SplashScreen = ({
   onDismiss,
@@ -70,13 +70,13 @@ export const SplashScreen = ({
         </Pressable>
       )}
 
-      <CenteredView style={[a.h_full, a.flex_1]}>
+      <Layout.Center style={[a.h_full, a.flex_1]} ignoreTabletLayoutOffset>
         <View
           testID="noSessionView"
           style={[
             a.h_full,
             a.justify_center,
-            // @ts-ignore web only
+            // @ts-expect-error web only
             {paddingBottom: '20vh'},
             isMobileWeb && a.pb_5xl,
             t.atoms.border_contrast_medium,
@@ -135,7 +135,7 @@ export const SplashScreen = ({
           </ErrorBoundary>
         </View>
         <Footer />
-      </CenteredView>
+      </Layout.Center>
       <AppClipOverlay
         visible={showClipOverlay}
         setIsVisible={setShowClipOverlay}
diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx
index c50d7f979..8ad97e2c1 100644
--- a/src/view/com/posts/PostFeed.tsx
+++ b/src/view/com/posts/PostFeed.tsx
@@ -40,7 +40,7 @@ import {List, ListRef} from '#/view/com/util/List'
 import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
 import {VideoFeedSourceContext} from '#/screens/VideoFeed/types'
-import {useBreakpoints} from '#/alf'
+import {useBreakpoints, useLayoutBreakpoints} from '#/alf'
 import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials'
 import {
   PostFeedVideoGridRow,
@@ -197,7 +197,8 @@ let PostFeed = ({
   const checkForNewRef = React.useRef<(() => void) | null>(null)
   const lastFetchRef = React.useRef<number>(Date.now())
   const [feedType, feedUriOrActorDid, feedTab] = feed.split('|')
-  const {gtMobile, gtTablet} = useBreakpoints()
+  const {gtMobile} = useBreakpoints()
+  const {rightNavVisible} = useLayoutBreakpoints()
   const areVideoFeedsEnabled = isNative
 
   const feedCacheKey = feedParams?.feedCacheKey
@@ -396,7 +397,7 @@ let PostFeed = ({
                         key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
                       })
                     }
-                    if (!gtTablet && !trendingDisabled) {
+                    if (!rightNavVisible && !trendingDisabled) {
                       arr.push({
                         type: 'interstitialTrending',
                         key:
@@ -512,7 +513,7 @@ let PostFeed = ({
     showProgressIntersitial,
     trendingDisabled,
     trendingVideoDisabled,
-    gtTablet,
+    rightNavVisible,
     gtMobile,
     isVideoFeed,
     areVideoFeedsEnabled,
diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx
index e64b0ce9a..94cadb13e 100644
--- a/src/view/com/util/Views.web.tsx
+++ b/src/view/com/util/Views.web.tsx
@@ -26,6 +26,8 @@ import Animated from 'react-native-reanimated'
 import {usePalette} from '#/lib/hooks/usePalette'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {addStyle} from '#/lib/styles'
+import {useLayoutBreakpoints} from '#/alf'
+import {useDialogContext} from '#/components/Dialog'
 
 interface AddedProps {
   desktopFixedHeight?: boolean | number
@@ -46,9 +48,14 @@ export const CenteredView = React.forwardRef(function CenteredView(
 ) {
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
+  const {centerColumnOffset} = useLayoutBreakpoints()
+  const {isWithinDialog} = useDialogContext()
   if (!isMobile) {
     style = addStyle(style, styles.container)
   }
+  if (centerColumnOffset && !isWithinDialog) {
+    style = addStyle(style, styles.containerOffset)
+  }
   if (topBorder) {
     style = addStyle(style, {
       borderTopWidth: 1,
@@ -71,12 +78,17 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
   ref: React.Ref<FlatList<ItemT>>,
 ) {
   const {isMobile} = useWebMediaQueries()
+  const {centerColumnOffset} = useLayoutBreakpoints()
+  const {isWithinDialog} = useDialogContext()
   if (!isMobile) {
     contentContainerStyle = addStyle(
       contentContainerStyle,
       styles.containerScroll,
     )
   }
+  if (centerColumnOffset && !isWithinDialog) {
+    style = addStyle(style, styles.containerOffset)
+  }
   if (contentOffset && contentOffset?.y !== 0) {
     // NOTE
     // we use paddingTop & contentOffset to space around the floating header
@@ -92,7 +104,7 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
   }
   if (desktopFixedHeight) {
     if (typeof desktopFixedHeight === 'number') {
-      // @ts-ignore Web only -prf
+      // @ts-expect-error Web only -prf
       style = addStyle(style, {
         height: `calc(100vh - ${desktopFixedHeight}px)`,
       })
@@ -108,9 +120,9 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
       // around this, we set data-stable-gutters which can then be
       // styled in our external CSS.
       // -prf
-      // @ts-ignore web only -prf
+      // @ts-expect-error web only -prf
       props.dataSet = props.dataSet || {}
-      // @ts-ignore web only -prf
+      // @ts-expect-error web only -prf
       props.dataSet.stableGutters = '1'
     }
   }
@@ -133,16 +145,22 @@ export const ScrollView = React.forwardRef(function ScrollViewImpl(
   ref: React.Ref<Animated.ScrollView>,
 ) {
   const {isMobile} = useWebMediaQueries()
+  const {centerColumnOffset} = useLayoutBreakpoints()
   if (!isMobile) {
     contentContainerStyle = addStyle(
       contentContainerStyle,
       styles.containerScroll,
     )
   }
+  if (centerColumnOffset) {
+    contentContainerStyle = addStyle(
+      contentContainerStyle,
+      styles.containerOffset,
+    )
+  }
   return (
     <Animated.ScrollView
       contentContainerStyle={[styles.contentContainer, contentContainerStyle]}
-      // @ts-ignore something is wrong with the reanimated types -prf
       ref={ref}
       {...props}
     />
@@ -151,7 +169,7 @@ export const ScrollView = React.forwardRef(function ScrollViewImpl(
 
 const styles = StyleSheet.create({
   contentContainer: {
-    // @ts-ignore web only
+    // @ts-expect-error web only
     minHeight: '100vh',
   },
   container: {
@@ -160,6 +178,9 @@ const styles = StyleSheet.create({
     marginLeft: 'auto',
     marginRight: 'auto',
   },
+  containerOffset: {
+    transform: [{translateX: -150}],
+  },
   containerScroll: {
     width: '100%',
     maxWidth: 600,
@@ -167,7 +188,7 @@ const styles = StyleSheet.create({
     marginRight: 'auto',
   },
   fixedHeight: {
-    // @ts-ignore web only
+    // @ts-expect-error web only
     height: '100vh',
   },
 })
diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx
index b502f0b68..89e5784b7 100644
--- a/src/view/com/util/load-latest/LoadLatestBtn.tsx
+++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx
@@ -1,10 +1,11 @@
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {StyleSheet, 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 {PressableScale} from '#/lib/custom-animations/PressableScale'
 import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform'
 import {usePalette} from '#/lib/hooks/usePalette'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
@@ -13,9 +14,7 @@ import {useGate} from '#/lib/statsig/statsig'
 import {colors} from '#/lib/styles'
 import {isWeb} from '#/platform/detection'
 import {useSession} from '#/state/session'
-
-const AnimatedTouchableOpacity =
-  Animated.createAnimatedComponent(TouchableOpacity)
+import {useLayoutBreakpoints} from '#/alf'
 
 export function LoadLatestBtn({
   onPress,
@@ -29,6 +28,7 @@ export function LoadLatestBtn({
   const pal = usePalette('default')
   const {hasSession} = useSession()
   const {isDesktop, isTablet, isMobile, isTabletOrMobile} = useWebMediaQueries()
+  const {centerColumnOffset} = useLayoutBreakpoints()
   const fabMinimalShellTransform = useMinimalShellFabTransform()
   const insets = useSafeAreaInsets()
 
@@ -49,33 +49,37 @@ export function LoadLatestBtn({
     : {bottom: clamp(insets.bottom, 15, 60) + 15}
 
   return (
-    <AnimatedTouchableOpacity
-      style={[
-        styles.loadLatest,
-        isDesktop &&
-          (isTallViewport
-            ? styles.loadLatestOutOfLine
-            : styles.loadLatestInline),
-        isTablet && styles.loadLatestInline,
-        pal.borderDark,
-        pal.view,
-        bottomPosition,
-        showBottomBar && fabMinimalShellTransform,
-      ]}
-      onPress={onPress}
-      hitSlop={HITSLOP_20}
-      accessibilityRole="button"
-      accessibilityLabel={label}
-      accessibilityHint="">
-      <FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} />
-      {showIndicator && <View style={[styles.indicator, pal.borderDark]} />}
-    </AnimatedTouchableOpacity>
+    <Animated.View style={[showBottomBar && fabMinimalShellTransform]}>
+      <PressableScale
+        style={[
+          styles.loadLatest,
+          isDesktop &&
+            (isTallViewport
+              ? styles.loadLatestOutOfLine
+              : styles.loadLatestInline),
+          isTablet &&
+            (centerColumnOffset
+              ? styles.loadLatestInlineOffset
+              : styles.loadLatestInline),
+          pal.borderDark,
+          pal.view,
+          bottomPosition,
+        ]}
+        onPress={onPress}
+        hitSlop={HITSLOP_20}
+        accessibilityLabel={label}
+        accessibilityHint=""
+        targetScale={0.9}>
+        <FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} />
+        {showIndicator && <View style={[styles.indicator, pal.borderDark]} />}
+      </PressableScale>
+    </Animated.View>
   )
 }
 
 const styles = StyleSheet.create({
   loadLatest: {
-    // @ts-ignore 'fixed' is web only -prf
+    zIndex: 20,
     position: isWeb ? 'fixed' : 'absolute',
     left: 18,
     borderWidth: StyleSheet.hairlineWidth,
@@ -87,11 +91,15 @@ const styles = StyleSheet.create({
     justifyContent: 'center',
   },
   loadLatestInline: {
-    // @ts-ignore web only
+    // @ts-expect-error web only
     left: 'calc(50vw - 282px)',
   },
+  loadLatestInlineOffset: {
+    // @ts-expect-error web only
+    left: 'calc(50vw - 432px)',
+  },
   loadLatestOutOfLine: {
-    // @ts-ignore web only
+    // @ts-expect-error web only
     left: 'calc(50vw - 382px)',
   },
   indicator: {
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index ef672f7fb..a6e2595ee 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -81,8 +81,10 @@ export function HomeScreen(props: Props) {
     )
   } else {
     return (
-      <Layout.Screen style={styles.loading}>
-        <ActivityIndicator size="large" />
+      <Layout.Screen>
+        <Layout.Center style={styles.loading}>
+          <ActivityIndicator size="large" />
+        </Layout.Center>
       </Layout.Screen>
     )
   }
diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx
index 35a46b427..018ff3d97 100644
--- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx
+++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx
@@ -150,11 +150,10 @@ function NativeStackNavigator({
           descriptors={newDescriptors}
         />
       </View>
-      {isWeb && showBottomBar && <BottomBarWeb />}
-      {isWeb && !showBottomBar && (
+      {isWeb && (
         <>
-          <DesktopLeftNav />
-          <DesktopRightNav routeName={activeRoute.name} />
+          {showBottomBar ? <BottomBarWeb /> : <DesktopLeftNav />}
+          {!isMobile && <DesktopRightNav routeName={activeRoute.name} />}
         </>
       )}
     </NavigationContent>
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index 62fbf5cae..59055c6dc 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -1,7 +1,6 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
 import {AppBskyActorDefs} from '@atproto/api'
-import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
 import {msg, plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {
@@ -33,7 +32,7 @@ import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 import {PressableWithHover} from '#/view/com/util/PressableWithHover'
 import {UserAvatar} from '#/view/com/util/UserAvatar'
 import {NavSignupCard} from '#/view/shell/NavSignupCard'
-import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf'
+import {atoms as a, tokens, useLayoutBreakpoints, useTheme} from '#/alf'
 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import {DialogControlProps} from '#/components/Dialog'
 import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft'
@@ -86,7 +85,7 @@ function ProfileCard() {
   })
   const profiles = data?.profiles
   const signOutPromptControl = Prompt.usePromptControl()
-  const {gtTablet} = useBreakpoints()
+  const {leftNavMinimal} = useLayoutBreakpoints()
   const {_} = useLingui()
   const t = useTheme()
 
@@ -101,7 +100,7 @@ function ProfileCard() {
     }))
 
   return (
-    <View style={[a.my_md, gtTablet && [a.w_full, a.align_start]]}>
+    <View style={[a.my_md, !leftNavMinimal && [a.w_full, a.align_start]]}>
       {!isLoading && profile ? (
         <Menu.Root>
           <Menu.Trigger label={_(msg`Switch accounts`)}>
@@ -120,7 +119,7 @@ function ProfileCard() {
                     a.align_center,
                     a.flex_row,
                     {gap: 6},
-                    gtTablet && [a.pl_lg, a.pr_md],
+                    !leftNavMinimal && [a.pl_lg, a.pr_md],
                   ]}>
                   <View
                     style={[
@@ -133,8 +132,8 @@ function ProfileCard() {
                       a.z_10,
                       active && {
                         transform: [
-                          {scale: gtTablet ? 2 / 3 : 0.8},
-                          {translateX: gtTablet ? -22 : 0},
+                          {scale: !leftNavMinimal ? 2 / 3 : 0.8},
+                          {translateX: !leftNavMinimal ? -22 : 0},
                         ],
                       },
                     ]}>
@@ -144,7 +143,7 @@ function ProfileCard() {
                       type={profile?.associated?.labeler ? 'labeler' : 'user'}
                     />
                   </View>
-                  {gtTablet && (
+                  {!leftNavMinimal && (
                     <>
                       <View
                         style={[
@@ -197,7 +196,7 @@ function ProfileCard() {
         <LoadingPlaceholder
           width={size}
           height={size}
-          style={[{borderRadius: size}, gtTablet && a.ml_lg]}
+          style={[{borderRadius: size}, !leftNavMinimal && a.ml_lg]}
         />
       )}
       <Prompt.Basic
@@ -307,8 +306,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
   const t = useTheme()
   const {_} = useLingui()
   const {currentAccount} = useSession()
-  const {gtMobile, gtTablet} = useBreakpoints()
-  const isTablet = gtMobile && !gtTablet
+  const {leftNavMinimal} = useLayoutBreakpoints()
   const [pathName] = React.useMemo(() => router.matchPath(href), [href])
   const currentRouteInfo = useNavigationState(state => {
     if (!state) {
@@ -350,9 +348,8 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
         a.transition_color,
       ]}
       hoverStyle={t.atoms.bg_contrast_25}
-      // @ts-ignore the function signature differs on web -prf
+      // @ts-expect-error the function signature differs on web -prf
       onPress={onPressWrapped}
-      // @ts-ignore web only -prf
       href={href}
       dataSet={{noUnderline: 1}}
       role="link"
@@ -367,7 +364,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
             width: 24,
             height: 24,
           },
-          isTablet && {
+          leftNavMinimal && {
             width: 40,
             height: 40,
           },
@@ -407,7 +404,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
                   paddingVertical: 1,
                   minWidth: 16,
                 },
-                isTablet && [
+                leftNavMinimal && [
                   {
                     top: '10%',
                     left: count.length === 1 ? 20 : 16,
@@ -429,7 +426,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
                 right: -1,
                 top: -3,
               },
-              isTablet && {
+              leftNavMinimal && {
                 right: 6,
                 top: 4,
               },
@@ -437,7 +434,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
           />
         ) : null}
       </View>
-      {gtTablet && (
+      {!leftNavMinimal && (
         <Text style={[a.text_xl, isCurrent ? a.font_heavy : a.font_normal]}>
           {label}
         </Text>
@@ -451,7 +448,7 @@ function ComposeBtn() {
   const {getState} = useNavigation()
   const {openComposer} = useComposerControls()
   const {_} = useLingui()
-  const {isTablet} = useWebMediaQueries()
+  const {leftNavMinimal} = useLayoutBreakpoints()
   const [isFetchingHandle, setIsFetchingHandle] = React.useState(false)
   const fetchHandle = useFetchHandle()
 
@@ -491,9 +488,10 @@ function ComposeBtn() {
   const onPressCompose = async () =>
     openComposer({mention: await getProfileHandle()})
 
-  if (isTablet) {
+  if (leftNavMinimal) {
     return null
   }
+
   return (
     <View style={[a.flex_row, a.pl_md, a.pt_xl]}>
       <Button
@@ -541,7 +539,8 @@ export function DesktopLeftNav() {
   const {hasSession, currentAccount} = useSession()
   const pal = usePalette('default')
   const {_} = useLingui()
-  const {isDesktop, isTablet} = useWebMediaQueries()
+  const {isDesktop} = useWebMediaQueries()
+  const {leftNavMinimal, centerColumnOffset} = useLayoutBreakpoints()
   const numUnreadNotifications = useUnreadNotifications()
   const hasHomeBadge = useHomeBadge()
   const gate = useGate()
@@ -556,8 +555,14 @@ export function DesktopLeftNav() {
       style={[
         a.px_xl,
         styles.leftNav,
-        isTablet && styles.leftNavTablet,
-        pal.border,
+        leftNavMinimal && styles.leftNavMinimal,
+        {
+          transform: [
+            {translateX: centerColumnOffset ? -450 : -300},
+            {translateX: '-100%'},
+            ...a.scrollbar_offset.transform,
+          ],
+        },
       ]}>
       {hasSession ? (
         <ProfileCard />
@@ -630,14 +635,14 @@ export function DesktopLeftNav() {
             href="/feeds"
             icon={
               <Hashtag
-                style={pal.text as FontAwesomeIconStyle}
+                style={pal.text}
                 aria-hidden={true}
                 width={NAV_ICON_WIDTH}
               />
             }
             iconFilled={
               <HashtagFilled
-                style={pal.text as FontAwesomeIconStyle}
+                style={pal.text}
                 aria-hidden={true}
                 width={NAV_ICON_WIDTH}
               />
@@ -708,36 +713,25 @@ export function DesktopLeftNav() {
 
 const styles = StyleSheet.create({
   leftNav: {
-    // @ts-ignore web only
     position: 'fixed',
-    top: 10,
-    // @ts-ignore web only
+    top: 0,
+    paddingTop: 10,
+    paddingBottom: 10,
     left: '50%',
-    transform: [
-      {
-        translateX: -300,
-      },
-      {
-        translateX: '-100%',
-      },
-      ...a.scrollbar_offset.transform,
-    ],
     width: 240,
-    // @ts-ignore web only
-    maxHeight: 'calc(100vh - 10px)',
+    // @ts-expect-error web only
+    maxHeight: '100vh',
     overflowY: 'auto',
   },
-  leftNavTablet: {
-    top: 0,
-    left: 0,
-    right: 'auto',
-    borderRightWidth: 1,
-    height: '100%',
-    width: 76,
+  leftNavMinimal: {
+    paddingTop: 0,
+    paddingBottom: 0,
     paddingLeft: 0,
     paddingRight: 0,
+    height: '100%',
+    width: 86,
     alignItems: 'center',
-    transform: [],
+    overflowX: 'hidden',
   },
   backBtn: {
     position: 'absolute',
diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx
index 363294aa5..510d505cd 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -1,17 +1,23 @@
-import React from 'react'
+import {useEffect, useState} from 'react'
 import {View} from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/core'
 
 import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {useKawaiiMode} from '#/state/preferences/kawaii'
 import {useSession} from '#/state/session'
 import {DesktopFeeds} from '#/view/shell/desktop/Feeds'
 import {DesktopSearch} from '#/view/shell/desktop/Search'
 import {SidebarTrendingTopics} from '#/view/shell/desktop/SidebarTrendingTopics'
-import {atoms as a, useGutters, useTheme, web} from '#/alf'
+import {
+  atoms as a,
+  useGutters,
+  useLayoutBreakpoints,
+  useTheme,
+  web,
+} from '#/alf'
+import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
 import {Divider} from '#/components/Divider'
 import {InlineLinkText} from '#/components/Link'
 import {ProgressGuideList} from '#/components/ProgressGuide/List'
@@ -19,16 +25,15 @@ import {Text} from '#/components/Typography'
 
 function useWebQueryParams() {
   const navigation = useNavigation()
-  const [params, setParams] = React.useState<Record<string, string>>({})
+  const [params, setParams] = useState<Record<string, string>>({})
 
-  React.useEffect(() => {
+  useEffect(() => {
     return navigation.addListener('state', e => {
       try {
         const {state} = e.data
         const lastRoute = state.routes[state.routes.length - 1]
-        const {params} = lastRoute
-        setParams(params)
-      } catch (e) {}
+        setParams(lastRoute.params)
+      } catch (err) {}
     })
   }, [navigation, setParams])
 
@@ -45,9 +50,10 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
   const webqueryParams = useWebQueryParams()
   const searchQuery = webqueryParams?.q
   const showTrending = !isSearchScreen || (isSearchScreen && !!searchQuery)
+  const {rightNavVisible, centerColumnOffset, leftNavMinimal} =
+    useLayoutBreakpoints()
 
-  const {isTablet} = useWebMediaQueries()
-  if (isTablet) {
+  if (!rightNavVisible) {
     return null
   }
 
@@ -60,9 +66,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
           position: 'fixed',
           left: '50%',
           transform: [
-            {
-              translateX: 300,
-            },
+            {translateX: centerColumnOffset ? 150 : 300},
             ...a.scrollbar_offset.transform,
           ],
           width: 300 + gutters.paddingLeft,
@@ -125,6 +129,12 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
           </Trans>
         </Text>
       )}
+
+      {!hasSession && leftNavMinimal && (
+        <View style={[a.w_full, {height: 32}]}>
+          <AppLanguageDropdown style={{marginTop: 0}} />
+        </View>
+      )}
     </View>
   )
 }