Bluesky app fork with some witchin' additions 💫

Refactor feed header components (#2964)

* Move home-related files to view/com/home

* Add HomeHeader in front of FeedTabBar

* Move isDekstop check outside FeedsTabBar

* Remove PWI logic from tabbar

* Separate platform-specific layout from shared logic

authored by danabra.mov and committed by

GitHub 1ccb3be9 93b5eff4

+243 -312
+71
src/view/com/home/HomeHeader.tsx
··· 1 + import React from 'react' 2 + import {RenderTabBarFnProps} from 'view/com/pager/Pager' 3 + import {HomeHeaderLayout} from './HomeHeaderLayout' 4 + import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 5 + import {usePinnedFeedsInfos} from '#/state/queries/feed' 6 + import {useNavigation} from '@react-navigation/native' 7 + import {NavigationProp} from 'lib/routes/types' 8 + import {isWeb} from 'platform/detection' 9 + import {TabBar} from '../pager/TabBar' 10 + import {usePalette} from '#/lib/hooks/usePalette' 11 + 12 + export function HomeHeader( 13 + props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, 14 + ) { 15 + const {isDesktop} = useWebMediaQueries() 16 + if (isDesktop) { 17 + return null 18 + } 19 + return <HomeHeaderInner {...props} /> 20 + } 21 + 22 + export function HomeHeaderInner( 23 + props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, 24 + ) { 25 + const navigation = useNavigation<NavigationProp>() 26 + const {feeds, hasPinnedCustom} = usePinnedFeedsInfos() 27 + const pal = usePalette('default') 28 + 29 + const items = React.useMemo(() => { 30 + const pinnedNames = feeds.map(f => f.displayName) 31 + 32 + if (!hasPinnedCustom) { 33 + return pinnedNames.concat('Feeds ✨') 34 + } 35 + return pinnedNames 36 + }, [hasPinnedCustom, feeds]) 37 + 38 + const onPressFeedsLink = React.useCallback(() => { 39 + if (isWeb) { 40 + navigation.navigate('Feeds') 41 + } else { 42 + navigation.navigate('FeedsTab') 43 + navigation.popToTop() 44 + } 45 + }, [navigation]) 46 + 47 + const onSelect = React.useCallback( 48 + (index: number) => { 49 + if (!hasPinnedCustom && index === items.length - 1) { 50 + onPressFeedsLink() 51 + } else if (props.onSelect) { 52 + props.onSelect(index) 53 + } 54 + }, 55 + [items.length, onPressFeedsLink, props, hasPinnedCustom], 56 + ) 57 + 58 + return ( 59 + <HomeHeaderLayout> 60 + <TabBar 61 + key={items.join(',')} 62 + onPressSelected={props.onPressSelected} 63 + selectedPage={props.selectedPage} 64 + onSelect={onSelect} 65 + testID={props.testID} 66 + items={items} 67 + indicatorColor={pal.colors.link} 68 + /> 69 + </HomeHeaderLayout> 70 + ) 71 + }
+1
src/view/com/home/HomeHeaderLayout.tsx
··· 1 + export {HomeHeaderLayoutMobile as HomeHeaderLayout} from './HomeHeaderLayoutMobile'
+50
src/view/com/home/HomeHeaderLayout.web.tsx
··· 1 + import React from 'react' 2 + import {StyleSheet} from 'react-native' 3 + import Animated from 'react-native-reanimated' 4 + import {usePalette} from 'lib/hooks/usePalette' 5 + import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 6 + import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile' 7 + import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 8 + import {useShellLayout} from '#/state/shell/shell-layout' 9 + 10 + export function HomeHeaderLayout({children}: {children: React.ReactNode}) { 11 + const {isMobile} = useWebMediaQueries() 12 + if (isMobile) { 13 + return <HomeHeaderLayoutMobile>{children}</HomeHeaderLayoutMobile> 14 + } else { 15 + return <HomeHeaderLayoutTablet>{children}</HomeHeaderLayoutTablet> 16 + } 17 + } 18 + 19 + function HomeHeaderLayoutTablet({children}: {children: React.ReactNode}) { 20 + const pal = usePalette('default') 21 + const {headerMinimalShellTransform} = useMinimalShellMode() 22 + const {headerHeight} = useShellLayout() 23 + 24 + return ( 25 + // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf 26 + <Animated.View 27 + style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]} 28 + onLayout={e => { 29 + headerHeight.value = e.nativeEvent.layout.height 30 + }}> 31 + {children} 32 + </Animated.View> 33 + ) 34 + } 35 + 36 + const styles = StyleSheet.create({ 37 + tabBar: { 38 + // @ts-ignore Web only 39 + position: 'sticky', 40 + zIndex: 1, 41 + // @ts-ignore Web only -prf 42 + left: 'calc(50% - 300px)', 43 + width: 600, 44 + top: 0, 45 + flexDirection: 'row', 46 + alignItems: 'center', 47 + borderLeftWidth: 1, 48 + borderRightWidth: 1, 49 + }, 50 + })
+119
src/view/com/home/HomeHeaderLayoutMobile.tsx
··· 1 + import React from 'react' 2 + import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 + import {usePalette} from 'lib/hooks/usePalette' 4 + import {Link} from '../util/Link' 5 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 6 + import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' 7 + import {HITSLOP_10} from 'lib/constants' 8 + import Animated from 'react-native-reanimated' 9 + import {msg} from '@lingui/macro' 10 + import {useLingui} from '@lingui/react' 11 + import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 12 + import {useSetDrawerOpen} from '#/state/shell/drawer-open' 13 + import {useShellLayout} from '#/state/shell/shell-layout' 14 + import {isWeb} from 'platform/detection' 15 + import {Logo} from '#/view/icons/Logo' 16 + 17 + import {IS_DEV} from '#/env' 18 + import {atoms} from '#/alf' 19 + import {Link as Link2} from '#/components/Link' 20 + import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette' 21 + 22 + export function HomeHeaderLayoutMobile({ 23 + children, 24 + }: { 25 + children: React.ReactNode 26 + }) { 27 + const pal = usePalette('default') 28 + const {_} = useLingui() 29 + const setDrawerOpen = useSetDrawerOpen() 30 + const {headerHeight} = useShellLayout() 31 + const {headerMinimalShellTransform} = useMinimalShellMode() 32 + 33 + const onPressAvi = React.useCallback(() => { 34 + setDrawerOpen(true) 35 + }, [setDrawerOpen]) 36 + 37 + return ( 38 + <Animated.View 39 + style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]} 40 + onLayout={e => { 41 + headerHeight.value = e.nativeEvent.layout.height 42 + }}> 43 + <View style={[pal.view, styles.topBar]}> 44 + <View style={[pal.view, {width: 100}]}> 45 + <TouchableOpacity 46 + testID="viewHeaderDrawerBtn" 47 + onPress={onPressAvi} 48 + accessibilityRole="button" 49 + accessibilityLabel={_(msg`Open navigation`)} 50 + accessibilityHint={_( 51 + msg`Access profile and other navigation links`, 52 + )} 53 + hitSlop={HITSLOP_10}> 54 + <FontAwesomeIcon 55 + icon="bars" 56 + size={18} 57 + color={pal.colors.textLight} 58 + /> 59 + </TouchableOpacity> 60 + </View> 61 + <View> 62 + <Logo width={30} /> 63 + </View> 64 + <View 65 + style={[ 66 + atoms.flex_row, 67 + atoms.justify_end, 68 + atoms.align_center, 69 + atoms.gap_md, 70 + pal.view, 71 + {width: 100}, 72 + ]}> 73 + {IS_DEV && ( 74 + <Link2 to="/sys/debug"> 75 + <ColorPalette size="md" /> 76 + </Link2> 77 + )} 78 + <Link 79 + testID="viewHeaderHomeFeedPrefsBtn" 80 + href="/settings/home-feed" 81 + hitSlop={HITSLOP_10} 82 + accessibilityRole="button" 83 + accessibilityLabel={_(msg`Home Feed Preferences`)} 84 + accessibilityHint=""> 85 + <FontAwesomeIcon 86 + icon="sliders" 87 + style={pal.textLight as FontAwesomeIconStyle} 88 + /> 89 + </Link> 90 + </View> 91 + </View> 92 + {children} 93 + </Animated.View> 94 + ) 95 + } 96 + 97 + const styles = StyleSheet.create({ 98 + tabBar: { 99 + // @ts-ignore web-only 100 + position: isWeb ? 'fixed' : 'absolute', 101 + zIndex: 1, 102 + left: 0, 103 + right: 0, 104 + top: 0, 105 + flexDirection: 'column', 106 + borderBottomWidth: 1, 107 + }, 108 + topBar: { 109 + flexDirection: 'row', 110 + justifyContent: 'space-between', 111 + alignItems: 'center', 112 + paddingHorizontal: 18, 113 + paddingVertical: 8, 114 + width: '100%', 115 + }, 116 + title: { 117 + fontSize: 21, 118 + }, 119 + })
-1
src/view/com/pager/FeedsTabBar.tsx
··· 1 - export * from './FeedsTabBarMobile'
-138
src/view/com/pager/FeedsTabBar.web.tsx
··· 1 - import React from 'react' 2 - import {View, StyleSheet} from 'react-native' 3 - import Animated from 'react-native-reanimated' 4 - import {TabBar} from 'view/com/pager/TabBar' 5 - import {RenderTabBarFnProps} from 'view/com/pager/Pager' 6 - import {usePalette} from 'lib/hooks/usePalette' 7 - import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 8 - import {FeedsTabBar as FeedsTabBarMobile} from './FeedsTabBarMobile' 9 - import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 10 - import {useShellLayout} from '#/state/shell/shell-layout' 11 - import {usePinnedFeedsInfos} from '#/state/queries/feed' 12 - import {useSession} from '#/state/session' 13 - import {TextLink} from '#/view/com/util/Link' 14 - import {CenteredView} from '../util/Views' 15 - import {isWeb} from 'platform/detection' 16 - import {useNavigation} from '@react-navigation/native' 17 - import {NavigationProp} from 'lib/routes/types' 18 - 19 - export function FeedsTabBar( 20 - props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, 21 - ) { 22 - const {isMobile, isTablet} = useWebMediaQueries() 23 - const {hasSession} = useSession() 24 - 25 - if (isMobile) { 26 - return <FeedsTabBarMobile {...props} /> 27 - } else if (isTablet) { 28 - if (hasSession) { 29 - return <FeedsTabBarTablet {...props} /> 30 - } else { 31 - return <FeedsTabBarPublic /> 32 - } 33 - } else { 34 - return null 35 - } 36 - } 37 - 38 - function FeedsTabBarPublic() { 39 - const pal = usePalette('default') 40 - 41 - return ( 42 - <CenteredView sideBorders> 43 - <View 44 - style={[ 45 - pal.view, 46 - { 47 - flexDirection: 'row', 48 - alignItems: 'center', 49 - justifyContent: 'space-between', 50 - paddingHorizontal: 18, 51 - paddingVertical: 12, 52 - }, 53 - ]}> 54 - <TextLink 55 - type="title-lg" 56 - href="/" 57 - style={[pal.text, {fontWeight: 'bold'}]} 58 - text="Bluesky " 59 - /> 60 - </View> 61 - </CenteredView> 62 - ) 63 - } 64 - 65 - function FeedsTabBarTablet( 66 - props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, 67 - ) { 68 - const {feeds, hasPinnedCustom} = usePinnedFeedsInfos() 69 - const pal = usePalette('default') 70 - const {hasSession} = useSession() 71 - const navigation = useNavigation<NavigationProp>() 72 - const {headerMinimalShellTransform} = useMinimalShellMode() 73 - const {headerHeight} = useShellLayout() 74 - 75 - const items = React.useMemo(() => { 76 - if (!hasSession) return [] 77 - 78 - const pinnedNames = feeds.map(f => f.displayName) 79 - 80 - if (!hasPinnedCustom) { 81 - return pinnedNames.concat('Feeds ✨') 82 - } 83 - return pinnedNames 84 - }, [hasSession, hasPinnedCustom, feeds]) 85 - 86 - const onPressDiscoverFeeds = React.useCallback(() => { 87 - if (isWeb) { 88 - navigation.navigate('Feeds') 89 - } else { 90 - navigation.navigate('FeedsTab') 91 - navigation.popToTop() 92 - } 93 - }, [navigation]) 94 - 95 - const onSelect = React.useCallback( 96 - (index: number) => { 97 - if (hasSession && !hasPinnedCustom && index === items.length - 1) { 98 - onPressDiscoverFeeds() 99 - } else if (props.onSelect) { 100 - props.onSelect(index) 101 - } 102 - }, 103 - [items.length, onPressDiscoverFeeds, props, hasSession, hasPinnedCustom], 104 - ) 105 - 106 - return ( 107 - // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf 108 - <Animated.View 109 - style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]} 110 - onLayout={e => { 111 - headerHeight.value = e.nativeEvent.layout.height 112 - }}> 113 - <TabBar 114 - key={items.join(',')} 115 - {...props} 116 - onSelect={onSelect} 117 - items={items} 118 - indicatorColor={pal.colors.link} 119 - /> 120 - </Animated.View> 121 - ) 122 - } 123 - 124 - const styles = StyleSheet.create({ 125 - tabBar: { 126 - // @ts-ignore Web only 127 - position: 'sticky', 128 - zIndex: 1, 129 - // @ts-ignore Web only -prf 130 - left: 'calc(50% - 300px)', 131 - width: 600, 132 - top: 0, 133 - flexDirection: 'row', 134 - alignItems: 'center', 135 - borderLeftWidth: 1, 136 - borderRightWidth: 1, 137 - }, 138 - })
-171
src/view/com/pager/FeedsTabBarMobile.tsx
··· 1 - import React from 'react' 2 - import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 - import {TabBar} from 'view/com/pager/TabBar' 4 - import {RenderTabBarFnProps} from 'view/com/pager/Pager' 5 - import {usePalette} from 'lib/hooks/usePalette' 6 - import {Link} from '../util/Link' 7 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 8 - import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' 9 - import {HITSLOP_10} from 'lib/constants' 10 - import Animated from 'react-native-reanimated' 11 - import {msg} from '@lingui/macro' 12 - import {useLingui} from '@lingui/react' 13 - import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 14 - import {useSetDrawerOpen} from '#/state/shell/drawer-open' 15 - import {useShellLayout} from '#/state/shell/shell-layout' 16 - import {useSession} from '#/state/session' 17 - import {usePinnedFeedsInfos} from '#/state/queries/feed' 18 - import {isWeb} from 'platform/detection' 19 - import {useNavigation} from '@react-navigation/native' 20 - import {NavigationProp} from 'lib/routes/types' 21 - import {Logo} from '#/view/icons/Logo' 22 - 23 - import {IS_DEV} from '#/env' 24 - import {atoms} from '#/alf' 25 - import {Link as Link2} from '#/components/Link' 26 - import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette' 27 - 28 - export function FeedsTabBar( 29 - props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, 30 - ) { 31 - const pal = usePalette('default') 32 - const {hasSession} = useSession() 33 - const {_} = useLingui() 34 - const setDrawerOpen = useSetDrawerOpen() 35 - const navigation = useNavigation<NavigationProp>() 36 - const {feeds, hasPinnedCustom} = usePinnedFeedsInfos() 37 - const {headerHeight} = useShellLayout() 38 - const {headerMinimalShellTransform} = useMinimalShellMode() 39 - 40 - const items = React.useMemo(() => { 41 - if (!hasSession) return [] 42 - 43 - const pinnedNames = feeds.map(f => f.displayName) 44 - 45 - if (!hasPinnedCustom) { 46 - return pinnedNames.concat('Feeds ✨') 47 - } 48 - return pinnedNames 49 - }, [hasSession, hasPinnedCustom, feeds]) 50 - 51 - const onPressFeedsLink = React.useCallback(() => { 52 - if (isWeb) { 53 - navigation.navigate('Feeds') 54 - } else { 55 - navigation.navigate('FeedsTab') 56 - navigation.popToTop() 57 - } 58 - }, [navigation]) 59 - 60 - const onSelect = React.useCallback( 61 - (index: number) => { 62 - if (hasSession && !hasPinnedCustom && index === items.length - 1) { 63 - onPressFeedsLink() 64 - } else if (props.onSelect) { 65 - props.onSelect(index) 66 - } 67 - }, 68 - [items.length, onPressFeedsLink, props, hasSession, hasPinnedCustom], 69 - ) 70 - 71 - const onPressAvi = React.useCallback(() => { 72 - setDrawerOpen(true) 73 - }, [setDrawerOpen]) 74 - 75 - return ( 76 - <Animated.View 77 - style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]} 78 - onLayout={e => { 79 - headerHeight.value = e.nativeEvent.layout.height 80 - }}> 81 - <View style={[pal.view, styles.topBar]}> 82 - <View style={[pal.view, {width: 100}]}> 83 - <TouchableOpacity 84 - testID="viewHeaderDrawerBtn" 85 - onPress={onPressAvi} 86 - accessibilityRole="button" 87 - accessibilityLabel={_(msg`Open navigation`)} 88 - accessibilityHint={_( 89 - msg`Access profile and other navigation links`, 90 - )} 91 - hitSlop={HITSLOP_10}> 92 - <FontAwesomeIcon 93 - icon="bars" 94 - size={18} 95 - color={pal.colors.textLight} 96 - /> 97 - </TouchableOpacity> 98 - </View> 99 - <View> 100 - <Logo width={30} /> 101 - </View> 102 - <View 103 - style={[ 104 - atoms.flex_row, 105 - atoms.justify_end, 106 - atoms.align_center, 107 - atoms.gap_md, 108 - pal.view, 109 - {width: 100}, 110 - ]}> 111 - {IS_DEV && ( 112 - <Link2 to="/sys/debug"> 113 - <ColorPalette size="md" /> 114 - </Link2> 115 - )} 116 - 117 - {hasSession && ( 118 - <Link 119 - testID="viewHeaderHomeFeedPrefsBtn" 120 - href="/settings/home-feed" 121 - hitSlop={HITSLOP_10} 122 - accessibilityRole="button" 123 - accessibilityLabel={_(msg`Home Feed Preferences`)} 124 - accessibilityHint=""> 125 - <FontAwesomeIcon 126 - icon="sliders" 127 - style={pal.textLight as FontAwesomeIconStyle} 128 - /> 129 - </Link> 130 - )} 131 - </View> 132 - </View> 133 - 134 - {items.length > 0 && ( 135 - <TabBar 136 - key={items.join(',')} 137 - onPressSelected={props.onPressSelected} 138 - selectedPage={props.selectedPage} 139 - onSelect={onSelect} 140 - testID={props.testID} 141 - items={items} 142 - indicatorColor={pal.colors.link} 143 - /> 144 - )} 145 - </Animated.View> 146 - ) 147 - } 148 - 149 - const styles = StyleSheet.create({ 150 - tabBar: { 151 - // @ts-ignore web-only 152 - position: isWeb ? 'fixed' : 'absolute', 153 - zIndex: 1, 154 - left: 0, 155 - right: 0, 156 - top: 0, 157 - flexDirection: 'column', 158 - borderBottomWidth: 1, 159 - }, 160 - topBar: { 161 - flexDirection: 'row', 162 - justifyContent: 'space-between', 163 - alignItems: 'center', 164 - paddingHorizontal: 18, 165 - paddingVertical: 8, 166 - width: '100%', 167 - }, 168 - title: { 169 - fontSize: 21, 170 - }, 171 - })
+2 -2
src/view/screens/Home.tsx
··· 6 6 import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' 7 7 import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' 8 8 import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' 9 - import {FeedsTabBar} from '../com/pager/FeedsTabBar' 9 + import {HomeHeader} from '../com/home/HomeHeader' 10 10 import {Pager, RenderTabBarFnProps, PagerRef} from 'view/com/pager/Pager' 11 11 import {FeedPage} from 'view/com/feeds/FeedPage' 12 12 import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA' ··· 118 118 const renderTabBar = React.useCallback( 119 119 (props: RenderTabBarFnProps) => { 120 120 return ( 121 - <FeedsTabBar 121 + <HomeHeader 122 122 key="FEEDS_TAB_BAR" 123 123 selectedPage={props.selectedPage} 124 124 onSelect={props.onSelect}