Bluesky app fork with some witchin' additions 💫

Better tablet layout (#7656)

* better tablet layout

* adjust left nav spacing

* add right nav to pwi

* clearer logic

* fix a couple screens that don't need the tablet layout

* fix horiz scroll bar

* fix double trending

* fix ts-ignore

* fix labeller screen

* don't offset things within dialogs

* fix load latest button (and add scale animation)

* center loader on home screen

* adjust break points

* adjust left nav spacing

* fix load latest btn (again)

* add lang select to right nav if left nav is minimal

* fix double scrollbar on tiny screens

* fix scrollbar

* fix type errors

authored by samuel.fm and committed by

GitHub cc8369e8 04378386

+211 -122
+15
src/alf/breakpoints.ts
··· 26 26 } 27 27 }, [gtPhone, gtMobile, gtTablet]) 28 28 } 29 + 30 + /** 31 + * Fine-tuned breakpoints for the shell layout 32 + */ 33 + export function useLayoutBreakpoints() { 34 + const rightNavVisible = useMediaQuery({minWidth: 1075}) 35 + const centerColumnOffset = useMediaQuery({minWidth: 1075, maxWidth: 1300}) 36 + const leftNavMinimal = useMediaQuery({maxWidth: 1300}) 37 + 38 + return { 39 + rightNavVisible, 40 + centerColumnOffset, 41 + leftNavMinimal, 42 + } 43 + }
+1
src/components/Dialog/context.ts
··· 14 14 nativeSnapPoint: BottomSheetSnapPoint.Hidden, 15 15 disableDrag: false, 16 16 setDisableDrag: () => {}, 17 + isWithinDialog: false, 17 18 }) 18 19 19 20 export function useDialogContext() {
+1
src/components/Dialog/index.tsx
··· 154 154 nativeSnapPoint: snapPoint, 155 155 disableDrag, 156 156 setDisableDrag, 157 + isWithinDialog: true, 157 158 }), 158 159 [close, snapPoint, disableDrag, setDisableDrag], 159 160 )
+1
src/components/Dialog/index.web.tsx
··· 97 97 nativeSnapPoint: 0, 98 98 disableDrag: false, 99 99 setDisableDrag: () => {}, 100 + isWithinDialog: true, 100 101 }), 101 102 [close], 102 103 )
+2
src/components/Dialog/types.ts
··· 44 44 nativeSnapPoint: BottomSheetSnapPoint 45 45 disableDrag: boolean 46 46 setDisableDrag: React.Dispatch<React.SetStateAction<boolean>> 47 + // in the event that the hook is used outside of a dialog 48 + isWithinDialog: boolean 47 49 } 48 50 49 51 export type DialogControlOpenOptions = {
+9 -1
src/components/Layout/Header/index.tsx
··· 14 14 TextStyleProp, 15 15 useBreakpoints, 16 16 useGutters, 17 + useLayoutBreakpoints, 17 18 useTheme, 18 19 web, 19 20 } from '#/alf' ··· 23 24 import { 24 25 BUTTON_VISUAL_ALIGNMENT_OFFSET, 25 26 HEADER_SLOT_SIZE, 27 + SCROLLBAR_OFFSET, 26 28 } from '#/components/Layout/const' 27 29 import {ScrollbarOffsetContext} from '#/components/Layout/context' 28 30 import {Text} from '#/components/Typography' ··· 42 44 const gutters = useGutters([0, 'base']) 43 45 const {gtMobile} = useBreakpoints() 44 46 const {isWithinOffsetView} = useContext(ScrollbarOffsetContext) 47 + const {centerColumnOffset} = useLayoutBreakpoints() 45 48 46 49 return ( 47 50 <View ··· 60 63 }), 61 64 t.atoms.border_contrast_low, 62 65 gtMobile && [a.mx_auto, {maxWidth: 600}], 63 - !isWithinOffsetView && a.scrollbar_offset, 66 + !isWithinOffsetView && { 67 + transform: [ 68 + {translateX: centerColumnOffset ? -150 : 0}, 69 + {translateX: web(SCROLLBAR_OFFSET) ?? 0}, 70 + ], 71 + }, 64 72 ]}> 65 73 {children} 66 74 </View>
+35 -8
src/components/Layout/index.tsx
··· 13 13 14 14 import {isWeb} from '#/platform/detection' 15 15 import {useShellLayout} from '#/state/shell/shell-layout' 16 - import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 16 + import { 17 + atoms as a, 18 + useBreakpoints, 19 + useLayoutBreakpoints, 20 + useTheme, 21 + web, 22 + } from '#/alf' 23 + import {useDialogContext} from '#/components/Dialog' 24 + import {SCROLLBAR_OFFSET} from '#/components/Layout/const' 17 25 import {ScrollbarOffsetContext} from '#/components/Layout/context' 18 26 19 27 export * from '#/components/Layout/const' ··· 47 55 export type ContentProps = AnimatedScrollViewProps & { 48 56 style?: StyleProp<ViewStyle> 49 57 contentContainerStyle?: StyleProp<ViewStyle> 58 + ignoreTabletLayoutOffset?: boolean 50 59 } 51 60 52 61 /** ··· 56 65 children, 57 66 style, 58 67 contentContainerStyle, 68 + ignoreTabletLayoutOffset, 59 69 ...props 60 70 }: ContentProps) { 61 71 const t = useTheme() ··· 84 94 ]} 85 95 {...props}> 86 96 {isWeb ? ( 87 - // @ts-ignore web only -esb 88 - <Center>{children}</Center> 97 + <Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}> 98 + {/* @ts-expect-error web only -esb */} 99 + {children} 100 + </Center> 89 101 ) : ( 90 102 children 91 103 )} ··· 138 150 export const Center = React.memo(function LayoutContent({ 139 151 children, 140 152 style, 153 + ignoreTabletLayoutOffset, 141 154 ...props 142 - }: ViewProps) { 155 + }: ViewProps & {ignoreTabletLayoutOffset?: boolean}) { 143 156 const {isWithinOffsetView} = useContext(ScrollbarOffsetContext) 144 157 const {gtMobile} = useBreakpoints() 158 + const {centerColumnOffset} = useLayoutBreakpoints() 159 + const {isWithinDialog} = useDialogContext() 145 160 const ctx = useMemo(() => ({isWithinOffsetView: true}), []) 146 161 return ( 147 162 <View ··· 151 166 gtMobile && { 152 167 maxWidth: 600, 153 168 }, 169 + !isWithinOffsetView && { 170 + transform: [ 171 + { 172 + translateX: 173 + centerColumnOffset && 174 + !ignoreTabletLayoutOffset && 175 + !isWithinDialog 176 + ? -150 177 + : 0, 178 + }, 179 + {translateX: web(SCROLLBAR_OFFSET) ?? 0}, 180 + ], 181 + }, 154 182 style, 155 - !isWithinOffsetView && a.scrollbar_offset, 156 183 ]} 157 184 {...props}> 158 185 <ScrollbarOffsetContext.Provider value={ctx}> ··· 168 195 const WebCenterBorders = React.memo(function LayoutContent() { 169 196 const t = useTheme() 170 197 const {gtMobile} = useBreakpoints() 198 + const {centerColumnOffset} = useLayoutBreakpoints() 171 199 return gtMobile ? ( 172 200 <View 173 201 style={[ ··· 180 208 width: 602, 181 209 left: '50%', 182 210 transform: [ 183 - { 184 - translateX: '-50%', 185 - }, 211 + {translateX: '-50%'}, 212 + {translateX: centerColumnOffset ? -150 : 0}, 186 213 ...a.scrollbar_offset.transform, 187 214 ], 188 215 }),
+1
src/screens/Deactivated.tsx
··· 106 106 return ( 107 107 <View style={[a.util_screen_outer, a.flex_1]}> 108 108 <Layout.Content 109 + ignoreTabletLayoutOffset 109 110 contentContainerStyle={[ 110 111 a.px_2xl, 111 112 {
+4 -6
src/screens/Profile/Sections/Labels.tsx
··· 15 15 import {useScrollHandlers} from '#/lib/ScrollContext' 16 16 import {isNative} from '#/platform/detection' 17 17 import {ListRef} from '#/view/com/util/List' 18 - import {ScrollView} from '#/view/com/util/Views' 19 18 import {atoms as a, useTheme} from '#/alf' 20 19 import {Divider} from '#/components/Divider' 21 20 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' ··· 148 147 }, [labelerInfo, labelValues]) 149 148 150 149 return ( 151 - <ScrollView 152 - // @ts-ignore TODO fix this 150 + <Layout.Content 151 + // @ts-expect-error TODO fix this 153 152 ref={scrollElRef} 154 153 scrollEventThrottle={1} 155 154 contentContainerStyle={{ ··· 228 227 })} 229 228 </View> 230 229 )} 231 - 232 - <View style={{height: 400}} /> 230 + <View style={{height: 100}} /> 233 231 </View> 234 - </ScrollView> 232 + </Layout.Content> 235 233 ) 236 234 }
+4 -4
src/view/com/auth/SplashScreen.web.tsx
··· 16 16 import {atoms as a, useTheme} from '#/alf' 17 17 import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 18 18 import {Button, ButtonText} from '#/components/Button' 19 + import * as Layout from '#/components/Layout' 19 20 import {InlineLinkText} from '#/components/Link' 20 21 import {Text} from '#/components/Typography' 21 - import {CenteredView} from '../util/Views' 22 22 23 23 export const SplashScreen = ({ 24 24 onDismiss, ··· 70 70 </Pressable> 71 71 )} 72 72 73 - <CenteredView style={[a.h_full, a.flex_1]}> 73 + <Layout.Center style={[a.h_full, a.flex_1]} ignoreTabletLayoutOffset> 74 74 <View 75 75 testID="noSessionView" 76 76 style={[ 77 77 a.h_full, 78 78 a.justify_center, 79 - // @ts-ignore web only 79 + // @ts-expect-error web only 80 80 {paddingBottom: '20vh'}, 81 81 isMobileWeb && a.pb_5xl, 82 82 t.atoms.border_contrast_medium, ··· 135 135 </ErrorBoundary> 136 136 </View> 137 137 <Footer /> 138 - </CenteredView> 138 + </Layout.Center> 139 139 <AppClipOverlay 140 140 visible={showClipOverlay} 141 141 setIsVisible={setShowClipOverlay}
+5 -4
src/view/com/posts/PostFeed.tsx
··· 40 40 import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 41 41 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 42 42 import {VideoFeedSourceContext} from '#/screens/VideoFeed/types' 43 - import {useBreakpoints} from '#/alf' 43 + import {useBreakpoints, useLayoutBreakpoints} from '#/alf' 44 44 import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials' 45 45 import { 46 46 PostFeedVideoGridRow, ··· 197 197 const checkForNewRef = React.useRef<(() => void) | null>(null) 198 198 const lastFetchRef = React.useRef<number>(Date.now()) 199 199 const [feedType, feedUriOrActorDid, feedTab] = feed.split('|') 200 - const {gtMobile, gtTablet} = useBreakpoints() 200 + const {gtMobile} = useBreakpoints() 201 + const {rightNavVisible} = useLayoutBreakpoints() 201 202 const areVideoFeedsEnabled = isNative 202 203 203 204 const feedCacheKey = feedParams?.feedCacheKey ··· 396 397 key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt, 397 398 }) 398 399 } 399 - if (!gtTablet && !trendingDisabled) { 400 + if (!rightNavVisible && !trendingDisabled) { 400 401 arr.push({ 401 402 type: 'interstitialTrending', 402 403 key: ··· 512 513 showProgressIntersitial, 513 514 trendingDisabled, 514 515 trendingVideoDisabled, 515 - gtTablet, 516 + rightNavVisible, 516 517 gtMobile, 517 518 isVideoFeed, 518 519 areVideoFeedsEnabled,
+27 -6
src/view/com/util/Views.web.tsx
··· 26 26 import {usePalette} from '#/lib/hooks/usePalette' 27 27 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 28 28 import {addStyle} from '#/lib/styles' 29 + import {useLayoutBreakpoints} from '#/alf' 30 + import {useDialogContext} from '#/components/Dialog' 29 31 30 32 interface AddedProps { 31 33 desktopFixedHeight?: boolean | number ··· 46 48 ) { 47 49 const pal = usePalette('default') 48 50 const {isMobile} = useWebMediaQueries() 51 + const {centerColumnOffset} = useLayoutBreakpoints() 52 + const {isWithinDialog} = useDialogContext() 49 53 if (!isMobile) { 50 54 style = addStyle(style, styles.container) 51 55 } 56 + if (centerColumnOffset && !isWithinDialog) { 57 + style = addStyle(style, styles.containerOffset) 58 + } 52 59 if (topBorder) { 53 60 style = addStyle(style, { 54 61 borderTopWidth: 1, ··· 71 78 ref: React.Ref<FlatList<ItemT>>, 72 79 ) { 73 80 const {isMobile} = useWebMediaQueries() 81 + const {centerColumnOffset} = useLayoutBreakpoints() 82 + const {isWithinDialog} = useDialogContext() 74 83 if (!isMobile) { 75 84 contentContainerStyle = addStyle( 76 85 contentContainerStyle, 77 86 styles.containerScroll, 78 87 ) 79 88 } 89 + if (centerColumnOffset && !isWithinDialog) { 90 + style = addStyle(style, styles.containerOffset) 91 + } 80 92 if (contentOffset && contentOffset?.y !== 0) { 81 93 // NOTE 82 94 // we use paddingTop & contentOffset to space around the floating header ··· 92 104 } 93 105 if (desktopFixedHeight) { 94 106 if (typeof desktopFixedHeight === 'number') { 95 - // @ts-ignore Web only -prf 107 + // @ts-expect-error Web only -prf 96 108 style = addStyle(style, { 97 109 height: `calc(100vh - ${desktopFixedHeight}px)`, 98 110 }) ··· 108 120 // around this, we set data-stable-gutters which can then be 109 121 // styled in our external CSS. 110 122 // -prf 111 - // @ts-ignore web only -prf 123 + // @ts-expect-error web only -prf 112 124 props.dataSet = props.dataSet || {} 113 - // @ts-ignore web only -prf 125 + // @ts-expect-error web only -prf 114 126 props.dataSet.stableGutters = '1' 115 127 } 116 128 } ··· 133 145 ref: React.Ref<Animated.ScrollView>, 134 146 ) { 135 147 const {isMobile} = useWebMediaQueries() 148 + const {centerColumnOffset} = useLayoutBreakpoints() 136 149 if (!isMobile) { 137 150 contentContainerStyle = addStyle( 138 151 contentContainerStyle, 139 152 styles.containerScroll, 153 + ) 154 + } 155 + if (centerColumnOffset) { 156 + contentContainerStyle = addStyle( 157 + contentContainerStyle, 158 + styles.containerOffset, 140 159 ) 141 160 } 142 161 return ( 143 162 <Animated.ScrollView 144 163 contentContainerStyle={[styles.contentContainer, contentContainerStyle]} 145 - // @ts-ignore something is wrong with the reanimated types -prf 146 164 ref={ref} 147 165 {...props} 148 166 /> ··· 151 169 152 170 const styles = StyleSheet.create({ 153 171 contentContainer: { 154 - // @ts-ignore web only 172 + // @ts-expect-error web only 155 173 minHeight: '100vh', 156 174 }, 157 175 container: { ··· 160 178 marginLeft: 'auto', 161 179 marginRight: 'auto', 162 180 }, 181 + containerOffset: { 182 + transform: [{translateX: -150}], 183 + }, 163 184 containerScroll: { 164 185 width: '100%', 165 186 maxWidth: 600, ··· 167 188 marginRight: 'auto', 168 189 }, 169 190 fixedHeight: { 170 - // @ts-ignore web only 191 + // @ts-expect-error web only 171 192 height: '100vh', 172 193 }, 173 194 })
+36 -28
src/view/com/util/load-latest/LoadLatestBtn.tsx
··· 1 - import {StyleSheet, TouchableOpacity, View} from 'react-native' 1 + import {StyleSheet, View} from 'react-native' 2 2 import Animated from 'react-native-reanimated' 3 3 import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 5 import {useMediaQuery} from 'react-responsive' 6 6 7 7 import {HITSLOP_20} from '#/lib/constants' 8 + import {PressableScale} from '#/lib/custom-animations/PressableScale' 8 9 import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform' 9 10 import {usePalette} from '#/lib/hooks/usePalette' 10 11 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' ··· 13 14 import {colors} from '#/lib/styles' 14 15 import {isWeb} from '#/platform/detection' 15 16 import {useSession} from '#/state/session' 16 - 17 - const AnimatedTouchableOpacity = 18 - Animated.createAnimatedComponent(TouchableOpacity) 17 + import {useLayoutBreakpoints} from '#/alf' 19 18 20 19 export function LoadLatestBtn({ 21 20 onPress, ··· 29 28 const pal = usePalette('default') 30 29 const {hasSession} = useSession() 31 30 const {isDesktop, isTablet, isMobile, isTabletOrMobile} = useWebMediaQueries() 31 + const {centerColumnOffset} = useLayoutBreakpoints() 32 32 const fabMinimalShellTransform = useMinimalShellFabTransform() 33 33 const insets = useSafeAreaInsets() 34 34 ··· 49 49 : {bottom: clamp(insets.bottom, 15, 60) + 15} 50 50 51 51 return ( 52 - <AnimatedTouchableOpacity 53 - style={[ 54 - styles.loadLatest, 55 - isDesktop && 56 - (isTallViewport 57 - ? styles.loadLatestOutOfLine 58 - : styles.loadLatestInline), 59 - isTablet && styles.loadLatestInline, 60 - pal.borderDark, 61 - pal.view, 62 - bottomPosition, 63 - showBottomBar && fabMinimalShellTransform, 64 - ]} 65 - onPress={onPress} 66 - hitSlop={HITSLOP_20} 67 - accessibilityRole="button" 68 - accessibilityLabel={label} 69 - accessibilityHint=""> 70 - <FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} /> 71 - {showIndicator && <View style={[styles.indicator, pal.borderDark]} />} 72 - </AnimatedTouchableOpacity> 52 + <Animated.View style={[showBottomBar && fabMinimalShellTransform]}> 53 + <PressableScale 54 + style={[ 55 + styles.loadLatest, 56 + isDesktop && 57 + (isTallViewport 58 + ? styles.loadLatestOutOfLine 59 + : styles.loadLatestInline), 60 + isTablet && 61 + (centerColumnOffset 62 + ? styles.loadLatestInlineOffset 63 + : styles.loadLatestInline), 64 + pal.borderDark, 65 + pal.view, 66 + bottomPosition, 67 + ]} 68 + onPress={onPress} 69 + hitSlop={HITSLOP_20} 70 + accessibilityLabel={label} 71 + accessibilityHint="" 72 + targetScale={0.9}> 73 + <FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} /> 74 + {showIndicator && <View style={[styles.indicator, pal.borderDark]} />} 75 + </PressableScale> 76 + </Animated.View> 73 77 ) 74 78 } 75 79 76 80 const styles = StyleSheet.create({ 77 81 loadLatest: { 78 - // @ts-ignore 'fixed' is web only -prf 82 + zIndex: 20, 79 83 position: isWeb ? 'fixed' : 'absolute', 80 84 left: 18, 81 85 borderWidth: StyleSheet.hairlineWidth, ··· 87 91 justifyContent: 'center', 88 92 }, 89 93 loadLatestInline: { 90 - // @ts-ignore web only 94 + // @ts-expect-error web only 91 95 left: 'calc(50vw - 282px)', 92 96 }, 97 + loadLatestInlineOffset: { 98 + // @ts-expect-error web only 99 + left: 'calc(50vw - 432px)', 100 + }, 93 101 loadLatestOutOfLine: { 94 - // @ts-ignore web only 102 + // @ts-expect-error web only 95 103 left: 'calc(50vw - 382px)', 96 104 }, 97 105 indicator: {
+4 -2
src/view/screens/Home.tsx
··· 81 81 ) 82 82 } else { 83 83 return ( 84 - <Layout.Screen style={styles.loading}> 85 - <ActivityIndicator size="large" /> 84 + <Layout.Screen> 85 + <Layout.Center style={styles.loading}> 86 + <ActivityIndicator size="large" /> 87 + </Layout.Center> 86 88 </Layout.Screen> 87 89 ) 88 90 }
+3 -4
src/view/shell/createNativeStackNavigatorWithAuth.tsx
··· 150 150 descriptors={newDescriptors} 151 151 /> 152 152 </View> 153 - {isWeb && showBottomBar && <BottomBarWeb />} 154 - {isWeb && !showBottomBar && ( 153 + {isWeb && ( 155 154 <> 156 - <DesktopLeftNav /> 157 - <DesktopRightNav routeName={activeRoute.name} /> 155 + {showBottomBar ? <BottomBarWeb /> : <DesktopLeftNav />} 156 + {!isMobile && <DesktopRightNav routeName={activeRoute.name} />} 158 157 </> 159 158 )} 160 159 </NavigationContent>
+40 -46
src/view/shell/desktop/LeftNav.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, View} from 'react-native' 3 3 import {AppBskyActorDefs} from '@atproto/api' 4 - import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' 5 4 import {msg, plural, Trans} from '@lingui/macro' 6 5 import {useLingui} from '@lingui/react' 7 6 import { ··· 33 32 import {PressableWithHover} from '#/view/com/util/PressableWithHover' 34 33 import {UserAvatar} from '#/view/com/util/UserAvatar' 35 34 import {NavSignupCard} from '#/view/shell/NavSignupCard' 36 - import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf' 35 + import {atoms as a, tokens, useLayoutBreakpoints, useTheme} from '#/alf' 37 36 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 38 37 import {DialogControlProps} from '#/components/Dialog' 39 38 import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft' ··· 86 85 }) 87 86 const profiles = data?.profiles 88 87 const signOutPromptControl = Prompt.usePromptControl() 89 - const {gtTablet} = useBreakpoints() 88 + const {leftNavMinimal} = useLayoutBreakpoints() 90 89 const {_} = useLingui() 91 90 const t = useTheme() 92 91 ··· 101 100 })) 102 101 103 102 return ( 104 - <View style={[a.my_md, gtTablet && [a.w_full, a.align_start]]}> 103 + <View style={[a.my_md, !leftNavMinimal && [a.w_full, a.align_start]]}> 105 104 {!isLoading && profile ? ( 106 105 <Menu.Root> 107 106 <Menu.Trigger label={_(msg`Switch accounts`)}> ··· 120 119 a.align_center, 121 120 a.flex_row, 122 121 {gap: 6}, 123 - gtTablet && [a.pl_lg, a.pr_md], 122 + !leftNavMinimal && [a.pl_lg, a.pr_md], 124 123 ]}> 125 124 <View 126 125 style={[ ··· 133 132 a.z_10, 134 133 active && { 135 134 transform: [ 136 - {scale: gtTablet ? 2 / 3 : 0.8}, 137 - {translateX: gtTablet ? -22 : 0}, 135 + {scale: !leftNavMinimal ? 2 / 3 : 0.8}, 136 + {translateX: !leftNavMinimal ? -22 : 0}, 138 137 ], 139 138 }, 140 139 ]}> ··· 144 143 type={profile?.associated?.labeler ? 'labeler' : 'user'} 145 144 /> 146 145 </View> 147 - {gtTablet && ( 146 + {!leftNavMinimal && ( 148 147 <> 149 148 <View 150 149 style={[ ··· 197 196 <LoadingPlaceholder 198 197 width={size} 199 198 height={size} 200 - style={[{borderRadius: size}, gtTablet && a.ml_lg]} 199 + style={[{borderRadius: size}, !leftNavMinimal && a.ml_lg]} 201 200 /> 202 201 )} 203 202 <Prompt.Basic ··· 307 306 const t = useTheme() 308 307 const {_} = useLingui() 309 308 const {currentAccount} = useSession() 310 - const {gtMobile, gtTablet} = useBreakpoints() 311 - const isTablet = gtMobile && !gtTablet 309 + const {leftNavMinimal} = useLayoutBreakpoints() 312 310 const [pathName] = React.useMemo(() => router.matchPath(href), [href]) 313 311 const currentRouteInfo = useNavigationState(state => { 314 312 if (!state) { ··· 350 348 a.transition_color, 351 349 ]} 352 350 hoverStyle={t.atoms.bg_contrast_25} 353 - // @ts-ignore the function signature differs on web -prf 351 + // @ts-expect-error the function signature differs on web -prf 354 352 onPress={onPressWrapped} 355 - // @ts-ignore web only -prf 356 353 href={href} 357 354 dataSet={{noUnderline: 1}} 358 355 role="link" ··· 367 364 width: 24, 368 365 height: 24, 369 366 }, 370 - isTablet && { 367 + leftNavMinimal && { 371 368 width: 40, 372 369 height: 40, 373 370 }, ··· 407 404 paddingVertical: 1, 408 405 minWidth: 16, 409 406 }, 410 - isTablet && [ 407 + leftNavMinimal && [ 411 408 { 412 409 top: '10%', 413 410 left: count.length === 1 ? 20 : 16, ··· 429 426 right: -1, 430 427 top: -3, 431 428 }, 432 - isTablet && { 429 + leftNavMinimal && { 433 430 right: 6, 434 431 top: 4, 435 432 }, ··· 437 434 /> 438 435 ) : null} 439 436 </View> 440 - {gtTablet && ( 437 + {!leftNavMinimal && ( 441 438 <Text style={[a.text_xl, isCurrent ? a.font_heavy : a.font_normal]}> 442 439 {label} 443 440 </Text> ··· 451 448 const {getState} = useNavigation() 452 449 const {openComposer} = useComposerControls() 453 450 const {_} = useLingui() 454 - const {isTablet} = useWebMediaQueries() 451 + const {leftNavMinimal} = useLayoutBreakpoints() 455 452 const [isFetchingHandle, setIsFetchingHandle] = React.useState(false) 456 453 const fetchHandle = useFetchHandle() 457 454 ··· 491 488 const onPressCompose = async () => 492 489 openComposer({mention: await getProfileHandle()}) 493 490 494 - if (isTablet) { 491 + if (leftNavMinimal) { 495 492 return null 496 493 } 494 + 497 495 return ( 498 496 <View style={[a.flex_row, a.pl_md, a.pt_xl]}> 499 497 <Button ··· 541 539 const {hasSession, currentAccount} = useSession() 542 540 const pal = usePalette('default') 543 541 const {_} = useLingui() 544 - const {isDesktop, isTablet} = useWebMediaQueries() 542 + const {isDesktop} = useWebMediaQueries() 543 + const {leftNavMinimal, centerColumnOffset} = useLayoutBreakpoints() 545 544 const numUnreadNotifications = useUnreadNotifications() 546 545 const hasHomeBadge = useHomeBadge() 547 546 const gate = useGate() ··· 556 555 style={[ 557 556 a.px_xl, 558 557 styles.leftNav, 559 - isTablet && styles.leftNavTablet, 560 - pal.border, 558 + leftNavMinimal && styles.leftNavMinimal, 559 + { 560 + transform: [ 561 + {translateX: centerColumnOffset ? -450 : -300}, 562 + {translateX: '-100%'}, 563 + ...a.scrollbar_offset.transform, 564 + ], 565 + }, 561 566 ]}> 562 567 {hasSession ? ( 563 568 <ProfileCard /> ··· 630 635 href="/feeds" 631 636 icon={ 632 637 <Hashtag 633 - style={pal.text as FontAwesomeIconStyle} 638 + style={pal.text} 634 639 aria-hidden={true} 635 640 width={NAV_ICON_WIDTH} 636 641 /> 637 642 } 638 643 iconFilled={ 639 644 <HashtagFilled 640 - style={pal.text as FontAwesomeIconStyle} 645 + style={pal.text} 641 646 aria-hidden={true} 642 647 width={NAV_ICON_WIDTH} 643 648 /> ··· 708 713 709 714 const styles = StyleSheet.create({ 710 715 leftNav: { 711 - // @ts-ignore web only 712 716 position: 'fixed', 713 - top: 10, 714 - // @ts-ignore web only 717 + top: 0, 718 + paddingTop: 10, 719 + paddingBottom: 10, 715 720 left: '50%', 716 - transform: [ 717 - { 718 - translateX: -300, 719 - }, 720 - { 721 - translateX: '-100%', 722 - }, 723 - ...a.scrollbar_offset.transform, 724 - ], 725 721 width: 240, 726 - // @ts-ignore web only 727 - maxHeight: 'calc(100vh - 10px)', 722 + // @ts-expect-error web only 723 + maxHeight: '100vh', 728 724 overflowY: 'auto', 729 725 }, 730 - leftNavTablet: { 731 - top: 0, 732 - left: 0, 733 - right: 'auto', 734 - borderRightWidth: 1, 735 - height: '100%', 736 - width: 76, 726 + leftNavMinimal: { 727 + paddingTop: 0, 728 + paddingBottom: 0, 737 729 paddingLeft: 0, 738 730 paddingRight: 0, 731 + height: '100%', 732 + width: 86, 739 733 alignItems: 'center', 740 - transform: [], 734 + overflowX: 'hidden', 741 735 }, 742 736 backBtn: { 743 737 position: 'absolute',
+23 -13
src/view/shell/desktop/RightNav.tsx
··· 1 - import React from 'react' 1 + import {useEffect, useState} from 'react' 2 2 import {View} from 'react-native' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 import {useNavigation} from '@react-navigation/core' 6 6 7 7 import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants' 8 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 9 8 import {useKawaiiMode} from '#/state/preferences/kawaii' 10 9 import {useSession} from '#/state/session' 11 10 import {DesktopFeeds} from '#/view/shell/desktop/Feeds' 12 11 import {DesktopSearch} from '#/view/shell/desktop/Search' 13 12 import {SidebarTrendingTopics} from '#/view/shell/desktop/SidebarTrendingTopics' 14 - import {atoms as a, useGutters, useTheme, web} from '#/alf' 13 + import { 14 + atoms as a, 15 + useGutters, 16 + useLayoutBreakpoints, 17 + useTheme, 18 + web, 19 + } from '#/alf' 20 + import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 15 21 import {Divider} from '#/components/Divider' 16 22 import {InlineLinkText} from '#/components/Link' 17 23 import {ProgressGuideList} from '#/components/ProgressGuide/List' ··· 19 25 20 26 function useWebQueryParams() { 21 27 const navigation = useNavigation() 22 - const [params, setParams] = React.useState<Record<string, string>>({}) 28 + const [params, setParams] = useState<Record<string, string>>({}) 23 29 24 - React.useEffect(() => { 30 + useEffect(() => { 25 31 return navigation.addListener('state', e => { 26 32 try { 27 33 const {state} = e.data 28 34 const lastRoute = state.routes[state.routes.length - 1] 29 - const {params} = lastRoute 30 - setParams(params) 31 - } catch (e) {} 35 + setParams(lastRoute.params) 36 + } catch (err) {} 32 37 }) 33 38 }, [navigation, setParams]) 34 39 ··· 45 50 const webqueryParams = useWebQueryParams() 46 51 const searchQuery = webqueryParams?.q 47 52 const showTrending = !isSearchScreen || (isSearchScreen && !!searchQuery) 53 + const {rightNavVisible, centerColumnOffset, leftNavMinimal} = 54 + useLayoutBreakpoints() 48 55 49 - const {isTablet} = useWebMediaQueries() 50 - if (isTablet) { 56 + if (!rightNavVisible) { 51 57 return null 52 58 } 53 59 ··· 60 66 position: 'fixed', 61 67 left: '50%', 62 68 transform: [ 63 - { 64 - translateX: 300, 65 - }, 69 + {translateX: centerColumnOffset ? 150 : 300}, 66 70 ...a.scrollbar_offset.transform, 67 71 ], 68 72 width: 300 + gutters.paddingLeft, ··· 124 128 </InlineLinkText> 125 129 </Trans> 126 130 </Text> 131 + )} 132 + 133 + {!hasSession && leftNavMinimal && ( 134 + <View style={[a.w_full, {height: 32}]}> 135 + <AppLanguageDropdown style={{marginTop: 0}} /> 136 + </View> 127 137 )} 128 138 </View> 129 139 )