Bluesky app fork with some witchin' additions 馃挮
at main 221 lines 5.5 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: [ 64 a.py_xs, 65 { 66 minHeight: 52, 67 paddingTop: 'env(safe-area-inset-top)', 68 }, 69 ], 70 }), 71 t.atoms.border_contrast_low, 72 gtMobile && [a.mx_auto, {maxWidth: 600}], 73 !isWithinOffsetView && { 74 transform: [ 75 {translateX: centerColumnOffset ? CENTER_COLUMN_OFFSET : 0}, 76 {translateX: web(SCROLLBAR_OFFSET) ?? 0}, 77 ], 78 }, 79 ]}> 80 {children} 81 </View> 82 ) 83} 84 85const AlignmentContext = createContext<'platform' | 'left'>('platform') 86AlignmentContext.displayName = 'AlignmentContext' 87 88export function Content({ 89 children, 90 align = 'platform', 91}: { 92 children?: React.ReactNode 93 align?: 'platform' | 'left' 94}) { 95 return ( 96 <View 97 style={[ 98 a.flex_1, 99 a.justify_center, 100 IS_IOS && align === 'platform' && a.align_center, 101 {minHeight: HEADER_SLOT_SIZE}, 102 ]}> 103 <AlignmentContext.Provider value={align}> 104 {children} 105 </AlignmentContext.Provider> 106 </View> 107 ) 108} 109 110export function Slot({children}: {children?: React.ReactNode}) { 111 return <View style={[a.z_50, {width: HEADER_SLOT_SIZE}]}>{children}</View> 112} 113 114export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) { 115 const {_} = useLingui() 116 const navigation = useNavigation<NavigationProp>() 117 118 const onPressBack = useCallback( 119 (evt: GestureResponderEvent) => { 120 onPress?.(evt) 121 if (evt.defaultPrevented) return 122 if (navigation.canGoBack()) { 123 navigation.goBack() 124 } else { 125 navigation.navigate('Home') 126 } 127 }, 128 [onPress, navigation], 129 ) 130 131 return ( 132 <Slot> 133 <Button 134 label={_(msg`Go back`)} 135 size="small" 136 variant="ghost" 137 color="secondary" 138 shape="round" 139 onPress={onPressBack} 140 hitSlop={HITSLOP_30} 141 style={[ 142 {marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, 143 a.bg_transparent, 144 style, 145 ]} 146 {...props}> 147 <ButtonIcon icon={ArrowLeft} size="lg" /> 148 </Button> 149 </Slot> 150 ) 151} 152 153export function MenuButton() { 154 const {_} = useLingui() 155 const setDrawerOpen = useSetDrawerOpen() 156 const {gtMobile} = useBreakpoints() 157 158 const onPress = useCallback(() => { 159 Keyboard.dismiss() 160 setDrawerOpen(true) 161 }, [setDrawerOpen]) 162 163 return gtMobile ? null : ( 164 <Slot> 165 <Button 166 label={_(msg`Open drawer menu`)} 167 size="small" 168 variant="ghost" 169 color="secondary" 170 shape="square" 171 onPress={onPress} 172 hitSlop={HITSLOP_30} 173 style={[ 174 {marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, 175 a.bg_transparent, 176 ]}> 177 <ButtonIcon icon={Menu} size="lg" /> 178 </Button> 179 </Slot> 180 ) 181} 182 183export function TitleText({ 184 children, 185 style, 186}: {children: React.ReactNode} & TextStyleProp) { 187 const {gtMobile} = useBreakpoints() 188 const align = useContext(AlignmentContext) 189 return ( 190 <Text 191 style={[ 192 a.text_lg, 193 a.font_semi_bold, 194 a.leading_tight, 195 IS_IOS && align === 'platform' && a.text_center, 196 gtMobile && a.text_xl, 197 style, 198 ]} 199 numberOfLines={2} 200 emoji> 201 {children} 202 </Text> 203 ) 204} 205 206export function SubtitleText({children}: {children: React.ReactNode}) { 207 const t = useTheme() 208 const align = useContext(AlignmentContext) 209 return ( 210 <Text 211 style={[ 212 a.text_sm, 213 a.leading_snug, 214 IS_IOS && align === 'platform' && a.text_center, 215 t.atoms.text_contrast_medium, 216 ]} 217 numberOfLines={2}> 218 {children} 219 </Text> 220 ) 221}