Bluesky app fork with some witchin' additions 馃挮
at readme-update 215 lines 5.4 kB view raw
1import {createContext, useCallback, useContext} from 'react' 2import {type GestureResponderEvent, Keyboard, View} from 'react-native' 3import {msg} from '@lingui/macro' 4import {useLingui} from '@lingui/react' 5import {useNavigation} from '@react-navigation/native' 6 7import {HITSLOP_30} from '#/lib/constants' 8import {type NavigationProp} from '#/lib/routes/types' 9import {useSetDrawerOpen} from '#/state/shell' 10import { 11 atoms as a, 12 platform, 13 type TextStyleProp, 14 useBreakpoints, 15 useGutters, 16 useLayoutBreakpoints, 17 useTheme, 18 web, 19} from '#/alf' 20import {Button, ButtonIcon, type ButtonProps} from '#/components/Button' 21import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow' 22import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' 23import { 24 BUTTON_VISUAL_ALIGNMENT_OFFSET, 25 CENTER_COLUMN_OFFSET, 26 HEADER_SLOT_SIZE, 27 SCROLLBAR_OFFSET, 28} from '#/components/Layout/const' 29import {ScrollbarOffsetContext} from '#/components/Layout/context' 30import {Text} from '#/components/Typography' 31import {IS_IOS} from '#/env' 32 33export function Outer({ 34 children, 35 noBottomBorder, 36 headerRef, 37 sticky = true, 38}: { 39 children: React.ReactNode 40 noBottomBorder?: boolean 41 headerRef?: React.RefObject<View | null> 42 sticky?: boolean 43}) { 44 const t = useTheme() 45 const gutters = useGutters([0, 'base']) 46 const {gtMobile} = useBreakpoints() 47 const {isWithinOffsetView} = useContext(ScrollbarOffsetContext) 48 const {centerColumnOffset} = useLayoutBreakpoints() 49 50 return ( 51 <View 52 ref={headerRef} 53 style={[ 54 a.w_full, 55 !noBottomBorder && a.border_b, 56 a.flex_row, 57 a.align_center, 58 a.gap_sm, 59 sticky && web([a.sticky, {top: 0}, a.z_10, t.atoms.bg]), 60 gutters, 61 platform({ 62 native: [a.pb_xs, {minHeight: 48}], 63 web: [a.py_xs, {minHeight: 52}], 64 }), 65 t.atoms.border_contrast_low, 66 gtMobile && [a.mx_auto, {maxWidth: 600}], 67 !isWithinOffsetView && { 68 transform: [ 69 {translateX: centerColumnOffset ? CENTER_COLUMN_OFFSET : 0}, 70 {translateX: web(SCROLLBAR_OFFSET) ?? 0}, 71 ], 72 }, 73 ]}> 74 {children} 75 </View> 76 ) 77} 78 79const AlignmentContext = createContext<'platform' | 'left'>('platform') 80AlignmentContext.displayName = 'AlignmentContext' 81 82export function Content({ 83 children, 84 align = 'platform', 85}: { 86 children?: React.ReactNode 87 align?: 'platform' | 'left' 88}) { 89 return ( 90 <View 91 style={[ 92 a.flex_1, 93 a.justify_center, 94 IS_IOS && align === 'platform' && a.align_center, 95 {minHeight: HEADER_SLOT_SIZE}, 96 ]}> 97 <AlignmentContext.Provider value={align}> 98 {children} 99 </AlignmentContext.Provider> 100 </View> 101 ) 102} 103 104export function Slot({children}: {children?: React.ReactNode}) { 105 return <View style={[a.z_50, {width: HEADER_SLOT_SIZE}]}>{children}</View> 106} 107 108export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) { 109 const {_} = useLingui() 110 const navigation = useNavigation<NavigationProp>() 111 112 const onPressBack = useCallback( 113 (evt: GestureResponderEvent) => { 114 onPress?.(evt) 115 if (evt.defaultPrevented) return 116 if (navigation.canGoBack()) { 117 navigation.goBack() 118 } else { 119 navigation.navigate('Home') 120 } 121 }, 122 [onPress, navigation], 123 ) 124 125 return ( 126 <Slot> 127 <Button 128 label={_(msg`Go back`)} 129 size="small" 130 variant="ghost" 131 color="secondary" 132 shape="round" 133 onPress={onPressBack} 134 hitSlop={HITSLOP_30} 135 style={[ 136 {marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, 137 a.bg_transparent, 138 style, 139 ]} 140 {...props}> 141 <ButtonIcon icon={ArrowLeft} size="lg" /> 142 </Button> 143 </Slot> 144 ) 145} 146 147export function MenuButton() { 148 const {_} = useLingui() 149 const setDrawerOpen = useSetDrawerOpen() 150 const {gtMobile} = useBreakpoints() 151 152 const onPress = useCallback(() => { 153 Keyboard.dismiss() 154 setDrawerOpen(true) 155 }, [setDrawerOpen]) 156 157 return gtMobile ? null : ( 158 <Slot> 159 <Button 160 label={_(msg`Open drawer menu`)} 161 size="small" 162 variant="ghost" 163 color="secondary" 164 shape="square" 165 onPress={onPress} 166 hitSlop={HITSLOP_30} 167 style={[ 168 {marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, 169 a.bg_transparent, 170 ]}> 171 <ButtonIcon icon={Menu} size="lg" /> 172 </Button> 173 </Slot> 174 ) 175} 176 177export function TitleText({ 178 children, 179 style, 180}: {children: React.ReactNode} & TextStyleProp) { 181 const {gtMobile} = useBreakpoints() 182 const align = useContext(AlignmentContext) 183 return ( 184 <Text 185 style={[ 186 a.text_lg, 187 a.font_semi_bold, 188 a.leading_tight, 189 IS_IOS && align === 'platform' && a.text_center, 190 gtMobile && a.text_xl, 191 style, 192 ]} 193 numberOfLines={2} 194 emoji> 195 {children} 196 </Text> 197 ) 198} 199 200export function SubtitleText({children}: {children: React.ReactNode}) { 201 const t = useTheme() 202 const align = useContext(AlignmentContext) 203 return ( 204 <Text 205 style={[ 206 a.text_sm, 207 a.leading_snug, 208 IS_IOS && align === 'platform' && a.text_center, 209 t.atoms.text_contrast_medium, 210 ]} 211 numberOfLines={2}> 212 {children} 213 </Text> 214 ) 215}