Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor sidebar (#6971)

* Refactor RightNav

(cherry picked from commit 96bb02acfd2d7452df18a0e7410e6a7169a583ed)

* Better gutter handling

* Clean up styles

* Memoize breakpoints

* Format

* Comment

* Loosen spacing, handle overflow, smaller text to match prod

* Fix circular imports on native

* Return 0 instead of undefined for easier calculations

* Re-assign

* Fix

* Port over fix from subs/base

* Space out right nav feeds, widen sidebar to match prod

* Fix lost padding on home header

* Fix perf by not actually linking to new URL

* Remove underline on focus

* Foramt

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>

authored by

Eric Bailey
Dan Abramov
and committed by
GitHub
e052f5e1 f34e8d8c

+228 -226
+28
src/alf/breakpoints.ts
··· 1 + import {useMemo} from 'react' 2 + import {useMediaQuery} from 'react-responsive' 3 + 4 + export type Breakpoint = 'gtPhone' | 'gtMobile' | 'gtTablet' 5 + 6 + export function useBreakpoints(): Record<Breakpoint, boolean> & { 7 + activeBreakpoint: Breakpoint | undefined 8 + } { 9 + const gtPhone = useMediaQuery({minWidth: 500}) 10 + const gtMobile = useMediaQuery({minWidth: 800}) 11 + const gtTablet = useMediaQuery({minWidth: 1300}) 12 + return useMemo(() => { 13 + let active: Breakpoint | undefined 14 + if (gtTablet) { 15 + active = 'gtTablet' 16 + } else if (gtMobile) { 17 + active = 'gtMobile' 18 + } else if (gtPhone) { 19 + active = 'gtPhone' 20 + } 21 + return { 22 + activeBreakpoint: active, 23 + gtPhone, 24 + gtMobile, 25 + gtTablet, 26 + } 27 + }, [gtPhone, gtMobile, gtTablet]) 28 + }
+2 -13
src/alf/index.tsx
··· 1 1 import React from 'react' 2 - import {useMediaQuery} from 'react-responsive' 3 2 4 3 import { 5 4 computeFontScaleMultiplier, ··· 14 13 import {Device} from '#/storage' 15 14 16 15 export {atoms} from '#/alf/atoms' 16 + export * from '#/alf/breakpoints' 17 17 export * from '#/alf/fonts' 18 18 export * as tokens from '#/alf/tokens' 19 19 export * from '#/alf/types' 20 20 export * from '#/alf/util/flatten' 21 21 export * from '#/alf/util/platform' 22 22 export * from '#/alf/util/themeSelector' 23 - export * from '#/alf/util/useGutterStyles' 23 + export * from '#/alf/util/useGutters' 24 24 25 25 export type Alf = { 26 26 themeName: ThemeName ··· 142 142 return theme ? alf.themes[theme] : alf.theme 143 143 }, [theme, alf]) 144 144 } 145 - 146 - export function useBreakpoints() { 147 - const gtPhone = useMediaQuery({minWidth: 500}) 148 - const gtMobile = useMediaQuery({minWidth: 800}) 149 - const gtTablet = useMediaQuery({minWidth: 1300}) 150 - return { 151 - gtPhone, 152 - gtMobile, 153 - gtTablet, 154 - } 155 - }
-21
src/alf/util/useGutterStyles.ts
··· 1 - import React from 'react' 2 - 3 - import {atoms as a, useBreakpoints, ViewStyleProp} from '#/alf' 4 - 5 - export function useGutterStyles({ 6 - top, 7 - bottom, 8 - }: { 9 - top?: boolean 10 - bottom?: boolean 11 - } = {}) { 12 - const {gtMobile} = useBreakpoints() 13 - return React.useMemo<ViewStyleProp['style']>(() => { 14 - return [ 15 - a.px_lg, 16 - top && a.pt_md, 17 - bottom && a.pb_md, 18 - gtMobile && [a.px_xl, top && a.pt_lg, bottom && a.pb_lg], 19 - ] 20 - }, [gtMobile, top, bottom]) 21 - }
+66
src/alf/util/useGutters.ts
··· 1 + import React from 'react' 2 + 3 + import {Breakpoint, useBreakpoints} from '#/alf/breakpoints' 4 + import * as tokens from '#/alf/tokens' 5 + 6 + type Gutter = 'compact' | 'base' | 'wide' | 0 7 + 8 + const gutters: Record< 9 + Exclude<Gutter, 0>, 10 + Record<Breakpoint | 'default', number> 11 + > = { 12 + compact: { 13 + default: tokens.space.sm, 14 + gtPhone: tokens.space.sm, 15 + gtMobile: tokens.space.md, 16 + gtTablet: tokens.space.md, 17 + }, 18 + base: { 19 + default: tokens.space.lg, 20 + gtPhone: tokens.space.lg, 21 + gtMobile: tokens.space.xl, 22 + gtTablet: tokens.space.xl, 23 + }, 24 + wide: { 25 + default: tokens.space.xl, 26 + gtPhone: tokens.space.xl, 27 + gtMobile: tokens.space._3xl, 28 + gtTablet: tokens.space._3xl, 29 + }, 30 + } 31 + 32 + type Gutters = { 33 + paddingTop: number 34 + paddingRight: number 35 + paddingBottom: number 36 + paddingLeft: number 37 + } 38 + 39 + export function useGutters([all]: [Gutter]): Gutters 40 + export function useGutters([vertical, horizontal]: [Gutter, Gutter]): Gutters 41 + export function useGutters([top, right, bottom, left]: [ 42 + Gutter, 43 + Gutter, 44 + Gutter, 45 + Gutter, 46 + ]): Gutters 47 + export function useGutters([top, right, bottom, left]: Gutter[]) { 48 + const {activeBreakpoint} = useBreakpoints() 49 + if (right === undefined) { 50 + right = bottom = left = top 51 + } else if (bottom === undefined) { 52 + bottom = top 53 + left = right 54 + } 55 + return React.useMemo(() => { 56 + return { 57 + paddingTop: top === 0 ? 0 : gutters[top][activeBreakpoint || 'default'], 58 + paddingRight: 59 + right === 0 ? 0 : gutters[right][activeBreakpoint || 'default'], 60 + paddingBottom: 61 + bottom === 0 ? 0 : gutters[bottom][activeBreakpoint || 'default'], 62 + paddingLeft: 63 + left === 0 ? 0 : gutters[left][activeBreakpoint || 'default'], 64 + } 65 + }, [activeBreakpoint, top, right, bottom, left]) 66 + }
+3 -3
src/components/Layout/Header/index.tsx
··· 13 13 platform, 14 14 TextStyleProp, 15 15 useBreakpoints, 16 - useGutterStyles, 16 + useGutters, 17 17 useTheme, 18 18 } from '#/alf' 19 19 import {Button, ButtonIcon, ButtonProps} from '#/components/Button' ··· 34 34 noBottomBorder?: boolean 35 35 }) { 36 36 const t = useTheme() 37 - const gutter = useGutterStyles() 37 + const gutters = useGutters([0, 'base']) 38 38 const {gtMobile} = useBreakpoints() 39 39 const {isWithinOffsetView} = useContext(ScrollbarOffsetContext) 40 40 ··· 46 46 a.flex_row, 47 47 a.align_center, 48 48 a.gap_sm, 49 - gutter, 49 + gutters, 50 50 platform({ 51 51 native: [a.pb_sm, a.pt_xs], 52 52 web: [a.py_sm],
+4 -5
src/components/Link.tsx
··· 237 237 } 238 238 239 239 export type InlineLinkProps = React.PropsWithChildren< 240 - BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'> 240 + BaseLinkProps & 241 + TextStyleProp & 242 + Pick<TextProps, 'selectable' | 'numberOfLines'> 241 243 > & 242 244 Pick<ButtonProps, 'label'> & { 243 245 disableUnderline?: boolean ··· 273 275 onIn: onHoverIn, 274 276 onOut: onHoverOut, 275 277 } = useInteractionState() 276 - const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 277 278 const flattenedStyle = flatten(style) || {} 278 279 279 280 return ( ··· 284 285 {...rest} 285 286 style={[ 286 287 {color: t.palette.primary_500}, 287 - (hovered || focused) && 288 + hovered && 288 289 !disableUnderline && { 289 290 ...web({ 290 291 outline: 0, ··· 298 299 role="link" 299 300 onPress={download ? undefined : onPress} 300 301 onLongPress={onLongPress} 301 - onFocus={onFocus} 302 - onBlur={onBlur} 303 302 onMouseEnter={onHoverIn} 304 303 onMouseLeave={onHoverOut} 305 304 accessibilityRole="link"
+3 -3
src/view/com/home/HomeHeaderLayout.web.tsx
··· 10 10 import {useShellLayout} from '#/state/shell/shell-layout' 11 11 import {HomeHeaderLayoutMobile} from '#/view/com/home/HomeHeaderLayoutMobile' 12 12 import {Logo} from '#/view/icons/Logo' 13 - import {atoms as a, useBreakpoints, useGutterStyles, useTheme} from '#/alf' 13 + import {atoms as a, useBreakpoints, useGutters, useTheme} from '#/alf' 14 14 import {ButtonIcon} from '#/components/Button' 15 15 import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag' 16 16 import * as Layout from '#/components/Layout' ··· 41 41 const {hasSession} = useSession() 42 42 const {_} = useLingui() 43 43 const kawaii = useKawaiiMode() 44 - const gutter = useGutterStyles() 44 + const gutters = useGutters([0, 'base']) 45 45 46 46 return ( 47 47 <> 48 48 {hasSession && ( 49 49 <Layout.Center> 50 50 <View 51 - style={[a.flex_row, a.align_center, a.pt_md, gutter, t.atoms.bg]}> 51 + style={[a.flex_row, a.align_center, gutters, a.pt_md, t.atoms.bg]}> 52 52 <View style={{width: 34}} /> 53 53 <View style={[a.flex_1, a.align_center, a.justify_center]}> 54 54 <Logo width={kawaii ? 60 : 28} />
+43 -59
src/view/shell/desktop/Feeds.tsx
··· 1 - import {StyleSheet, View} from 'react-native' 1 + import {View} from 'react-native' 2 2 import {msg} from '@lingui/macro' 3 3 import {useLingui} from '@lingui/react' 4 4 import {useNavigation, useNavigationState} from '@react-navigation/native' 5 5 6 - import {usePalette} from '#/lib/hooks/usePalette' 7 6 import {getCurrentRoute} from '#/lib/routes/helpers' 8 7 import {NavigationProp} from '#/lib/routes/types' 9 8 import {emitSoftReset} from '#/state/events' 10 9 import {usePinnedFeedsInfos} from '#/state/queries/feed' 11 10 import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed' 12 - import {TextLink} from '#/view/com/util/Link' 11 + import {atoms as a, useTheme, web} from '#/alf' 12 + import {createStaticClick, InlineLinkText} from '#/components/Link' 13 13 14 14 export function DesktopFeeds() { 15 - const pal = usePalette('default') 15 + const t = useTheme() 16 16 const {_} = useLingui() 17 17 const {data: pinnedFeedInfos} = usePinnedFeedsInfos() 18 18 const selectedFeed = useSelectedFeed() ··· 24 24 } 25 25 return getCurrentRoute(state) 26 26 }) 27 + 27 28 if (!pinnedFeedInfos) { 28 29 return null 29 30 } 31 + 30 32 return ( 31 - <View style={[styles.container, pal.view]}> 33 + <View 34 + style={[ 35 + a.flex_1, 36 + web({ 37 + gap: 10, 38 + /* 39 + * Small padding prevents overflow prior to actually overflowing the 40 + * height of the screen with lots of feeds. 41 + */ 42 + paddingVertical: 2, 43 + overflowY: 'auto', 44 + }), 45 + ]}> 32 46 {pinnedFeedInfos.map(feedInfo => { 33 47 const feed = feedInfo.feedDescriptor 48 + const current = route.name === 'Home' && feed === selectedFeed 49 + 34 50 return ( 35 - <FeedItem 36 - key={feed} 37 - href={'/?' + new URLSearchParams([['feed', feed]])} 38 - title={feedInfo.displayName} 39 - current={route.name === 'Home' && feed === selectedFeed} 40 - onPress={() => { 51 + <InlineLinkText 52 + key={feedInfo.uri} 53 + label={feedInfo.displayName} 54 + {...createStaticClick(() => { 41 55 setSelectedFeed(feed) 42 56 navigation.navigate('Home') 43 57 if (route.name === 'Home' && feed === selectedFeed) { 44 58 emitSoftReset() 45 59 } 46 - }} 47 - /> 60 + })} 61 + style={[ 62 + a.text_md, 63 + a.leading_snug, 64 + current 65 + ? [a.font_heavy, t.atoms.text] 66 + : [t.atoms.text_contrast_medium], 67 + ]} 68 + numberOfLines={1}> 69 + {feedInfo.displayName} 70 + </InlineLinkText> 48 71 ) 49 72 })} 50 - <View style={{paddingTop: 8, paddingBottom: 6}}> 51 - <TextLink 52 - type="lg" 53 - href="/feeds" 54 - text={_(msg`More feeds`)} 55 - style={[pal.link]} 56 - /> 57 - </View> 58 - </View> 59 - ) 60 - } 61 73 62 - function FeedItem({ 63 - title, 64 - href, 65 - current, 66 - onPress, 67 - }: { 68 - title: string 69 - href: string 70 - current: boolean 71 - onPress: () => void 72 - }) { 73 - const pal = usePalette('default') 74 - return ( 75 - <View style={{paddingVertical: 6}}> 76 - <TextLink 77 - type="xl" 78 - href={href} 79 - text={title} 80 - onPress={onPress} 81 - style={[ 82 - current ? pal.text : pal.textLight, 83 - {letterSpacing: 0.15, fontWeight: current ? '600' : '400'}, 84 - ]} 85 - /> 74 + <InlineLinkText 75 + to="/feeds" 76 + label={_(msg`More feeds`)} 77 + style={[a.text_md, a.leading_snug]} 78 + numberOfLines={1}> 79 + {_(msg`More feeds`)} 80 + </InlineLinkText> 86 81 </View> 87 82 ) 88 83 } 89 - 90 - const styles = StyleSheet.create({ 91 - container: { 92 - flex: 1, 93 - // @ts-ignore web only -prf 94 - overflowY: 'auto', 95 - width: 300, 96 - paddingHorizontal: 12, 97 - paddingVertical: 18, 98 - }, 99 - })
+77 -120
src/view/shell/desktop/RightNav.tsx
··· 1 - import {StyleSheet, View} from 'react-native' 1 + import {View} from 'react-native' 2 2 import {msg, Trans} from '@lingui/macro' 3 3 import {useLingui} from '@lingui/react' 4 4 5 5 import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants' 6 - import {usePalette} from '#/lib/hooks/usePalette' 7 6 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 8 - import {s} from '#/lib/styles' 9 7 import {useKawaiiMode} from '#/state/preferences/kawaii' 10 8 import {useSession} from '#/state/session' 11 - import {TextLink} from '#/view/com/util/Link' 12 - import {Text} from '#/view/com/util/text/Text' 13 - import {atoms as a} from '#/alf' 9 + import {DesktopFeeds} from '#/view/shell/desktop/Feeds' 10 + import {DesktopSearch} from '#/view/shell/desktop/Search' 11 + import {atoms as a, useGutters, useTheme, web} from '#/alf' 12 + import {InlineLinkText} from '#/components/Link' 14 13 import {ProgressGuideList} from '#/components/ProgressGuide/List' 15 - import {DesktopFeeds} from './Feeds' 16 - import {DesktopSearch} from './Search' 14 + import {Text} from '#/components/Typography' 17 15 18 16 export function DesktopRightNav({routeName}: {routeName: string}) { 19 - const pal = usePalette('default') 17 + const t = useTheme() 20 18 const {_} = useLingui() 21 19 const {hasSession, currentAccount} = useSession() 22 - 23 20 const kawaii = useKawaiiMode() 21 + const gutters = useGutters(['base', 0, 'base', 'wide']) 24 22 25 23 const {isTablet} = useWebMediaQueries() 26 24 if (isTablet) { ··· 28 26 } 29 27 30 28 return ( 31 - <View style={[a.px_xl, styles.rightNav]}> 32 - <View style={{paddingVertical: 20}}> 33 - {routeName === 'Search' ? ( 34 - <View style={{marginBottom: 18}}> 29 + <View 30 + style={[ 31 + gutters, 32 + web({ 33 + position: 'fixed', 34 + left: '50%', 35 + transform: [ 36 + { 37 + translateX: 300, 38 + }, 39 + ...a.scrollbar_offset.transform, 40 + ], 41 + width: 300 + gutters.paddingLeft, 42 + maxHeight: '100%', 43 + overflowY: 'auto', 44 + }), 45 + ]}> 46 + {routeName !== 'Search' && ( 47 + <View style={[a.pb_lg]}> 48 + <DesktopSearch /> 49 + </View> 50 + )} 51 + {hasSession && ( 52 + <> 53 + <ProgressGuideList style={[a.pb_xl]} /> 54 + <View 55 + style={[a.pb_lg, a.mb_lg, a.border_b, t.atoms.border_contrast_low]}> 35 56 <DesktopFeeds /> 36 57 </View> 37 - ) : ( 58 + </> 59 + )} 60 + 61 + <Text style={[a.leading_snug, t.atoms.text_contrast_low]}> 62 + {hasSession && ( 38 63 <> 39 - <DesktopSearch /> 40 - 41 - {hasSession && ( 42 - <> 43 - <ProgressGuideList style={[{marginTop: 22, marginBottom: 8}]} /> 44 - <View style={[pal.border, styles.desktopFeedsContainer]}> 45 - <DesktopFeeds /> 46 - </View> 47 - </> 48 - )} 64 + <InlineLinkText 65 + to={FEEDBACK_FORM_URL({ 66 + email: currentAccount?.email, 67 + handle: currentAccount?.handle, 68 + })} 69 + label={_(msg`Feedback`)}> 70 + {_(msg`Feedback`)} 71 + </InlineLinkText> 72 + {' • '} 49 73 </> 50 74 )} 75 + <InlineLinkText 76 + to="https://bsky.social/about/support/privacy-policy" 77 + label={_(msg`Privacy`)}> 78 + {_(msg`Privacy`)} 79 + </InlineLinkText> 80 + {' • '} 81 + <InlineLinkText 82 + to="https://bsky.social/about/support/tos" 83 + label={_(msg`Terms`)}> 84 + {_(msg`Terms`)} 85 + </InlineLinkText> 86 + {' • '} 87 + <InlineLinkText label={_(msg`Help`)} to={HELP_DESK_URL}> 88 + {_(msg`Help`)} 89 + </InlineLinkText> 90 + </Text> 51 91 52 - <View 53 - style={[ 54 - styles.message, 55 - { 56 - paddingTop: hasSession ? 0 : 18, 57 - }, 58 - ]}> 59 - <View style={[{flexWrap: 'wrap'}, s.flexRow, a.gap_xs]}> 60 - {hasSession && ( 61 - <> 62 - <TextLink 63 - type="md" 64 - style={pal.link} 65 - href={FEEDBACK_FORM_URL({ 66 - email: currentAccount?.email, 67 - handle: currentAccount?.handle, 68 - })} 69 - text={_(msg`Feedback`)} 70 - /> 71 - <Text type="md" style={pal.textLight}> 72 - &middot; 73 - </Text> 74 - </> 75 - )} 76 - <TextLink 77 - type="md" 78 - style={pal.link} 79 - href="https://bsky.social/about/support/privacy-policy" 80 - text={_(msg`Privacy`)} 81 - /> 82 - <Text type="md" style={pal.textLight}> 83 - &middot; 84 - </Text> 85 - <TextLink 86 - type="md" 87 - style={pal.link} 88 - href="https://bsky.social/about/support/tos" 89 - text={_(msg`Terms`)} 90 - /> 91 - <Text type="md" style={pal.textLight}> 92 - &middot; 93 - </Text> 94 - <TextLink 95 - type="md" 96 - style={pal.link} 97 - href={HELP_DESK_URL} 98 - text={_(msg`Help`)} 99 - /> 100 - </View> 101 - {kawaii && ( 102 - <Text type="md" style={[pal.textLight, {marginTop: 12}]}> 103 - <Trans> 104 - Logo by{' '} 105 - <TextLink 106 - type="md" 107 - href="/profile/sawaratsuki.bsky.social" 108 - text="@sawaratsuki.bsky.social" 109 - style={pal.link} 110 - /> 111 - </Trans> 112 - </Text> 113 - )} 114 - </View> 115 - </View> 92 + {kawaii && ( 93 + <Text style={[t.atoms.text_contrast_medium, {marginTop: 12}]}> 94 + <Trans> 95 + Logo by{' '} 96 + <InlineLinkText 97 + label={_(msg`Logo by @sawaratsuki.bsky.social`)} 98 + to="/profile/sawaratsuki.bsky.social"> 99 + @sawaratsuki.bsky.social 100 + </InlineLinkText> 101 + </Trans> 102 + </Text> 103 + )} 116 104 </View> 117 105 ) 118 106 } 119 - 120 - const styles = StyleSheet.create({ 121 - rightNav: { 122 - // @ts-ignore web only 123 - position: 'fixed', 124 - // @ts-ignore web only 125 - left: '50%', 126 - transform: [ 127 - { 128 - translateX: 300, 129 - }, 130 - ...a.scrollbar_offset.transform, 131 - ], 132 - maxHeight: '100%', 133 - overflowY: 'auto', 134 - }, 135 - 136 - message: { 137 - paddingVertical: 18, 138 - paddingHorizontal: 12, 139 - }, 140 - messageLine: { 141 - marginBottom: 10, 142 - }, 143 - desktopFeedsContainer: { 144 - borderTopWidth: StyleSheet.hairlineWidth, 145 - borderBottomWidth: StyleSheet.hairlineWidth, 146 - marginTop: 18, 147 - marginBottom: 18, 148 - }, 149 - })
+2 -2
src/view/shell/desktop/Search.tsx
··· 225 225 const styles = StyleSheet.create({ 226 226 container: { 227 227 position: 'relative', 228 - width: 300, 228 + width: '100%', 229 229 }, 230 230 resultsContainer: { 231 231 marginTop: 10, 232 232 flexDirection: 'column', 233 - width: 300, 233 + width: '100%', 234 234 borderWidth: 1, 235 235 borderRadius: 6, 236 236 },