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