Bluesky app fork with some witchin' additions 馃挮
at main 225 lines 5.8 kB view raw
1import {forwardRef, memo, useContext, useMemo} from 'react' 2import {StyleSheet, View, type ViewProps, type ViewStyle} from 'react-native' 3import {type StyleProp} from 'react-native' 4import { 5 KeyboardAwareScrollView, 6 type KeyboardAwareScrollViewProps, 7} from 'react-native-keyboard-controller' 8import Animated, { 9 type AnimatedScrollViewProps, 10 useAnimatedProps, 11} from 'react-native-reanimated' 12import {useSafeAreaInsets} from 'react-native-safe-area-context' 13 14import {useShellLayout} from '#/state/shell/shell-layout' 15import { 16 atoms as a, 17 useBreakpoints, 18 useLayoutBreakpoints, 19 useTheme, 20 web, 21} from '#/alf' 22import {useDialogContext} from '#/components/Dialog' 23import {CENTER_COLUMN_OFFSET, SCROLLBAR_OFFSET} from '#/components/Layout/const' 24import {ScrollbarOffsetContext} from '#/components/Layout/context' 25import {IS_WEB} from '#/env' 26 27export * from '#/components/Layout/const' 28export * as Header from '#/components/Layout/Header' 29 30export type ScreenProps = React.ComponentProps<typeof View> & { 31 style?: StyleProp<ViewStyle> 32 noInsetTop?: boolean 33} 34 35/** 36 * Outermost component of every screen 37 */ 38export const Screen = memo(function Screen({ 39 style, 40 noInsetTop, 41 ...props 42}: ScreenProps) { 43 const {top} = useSafeAreaInsets() 44 return ( 45 <> 46 {IS_WEB && <WebCenterBorders />} 47 <View 48 style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]} 49 {...props} 50 /> 51 </> 52 ) 53}) 54 55export type ContentProps = AnimatedScrollViewProps & { 56 style?: StyleProp<ViewStyle> 57 contentContainerStyle?: StyleProp<ViewStyle> 58 ignoreTabletLayoutOffset?: boolean 59} 60 61/** 62 * Default scroll view for simple pages 63 */ 64export const Content = memo( 65 forwardRef<Animated.ScrollView, ContentProps>(function Content( 66 { 67 children, 68 style, 69 contentContainerStyle, 70 ignoreTabletLayoutOffset, 71 ...props 72 }, 73 ref, 74 ) { 75 const t = useTheme() 76 const {footerHeight} = useShellLayout() 77 const animatedProps = useAnimatedProps(() => { 78 return { 79 scrollIndicatorInsets: { 80 bottom: footerHeight.get(), 81 top: 0, 82 right: 1, 83 }, 84 } satisfies AnimatedScrollViewProps 85 }) 86 87 return ( 88 <Animated.ScrollView 89 ref={ref} 90 id="content" 91 automaticallyAdjustsScrollIndicatorInsets={false} 92 indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'} 93 // sets the scroll inset to the height of the footer 94 animatedProps={animatedProps} 95 style={[scrollViewStyles.common, style]} 96 contentContainerStyle={[ 97 scrollViewStyles.contentContainer, 98 contentContainerStyle, 99 ]} 100 {...props}> 101 {IS_WEB ? ( 102 <Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}> 103 {/* @ts-expect-error web only -esb */} 104 {children} 105 </Center> 106 ) : ( 107 children 108 )} 109 </Animated.ScrollView> 110 ) 111 }), 112) 113 114const scrollViewStyles = StyleSheet.create({ 115 common: { 116 width: '100%', 117 }, 118 contentContainer: { 119 paddingBottom: 100, 120 }, 121}) 122 123export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & { 124 children: React.ReactNode 125 contentContainerStyle?: StyleProp<ViewStyle> 126} 127 128/** 129 * Default scroll view for simple pages. 130 * 131 * BE SURE TO TEST THIS WHEN USING, it's untested as of writing this comment. 132 */ 133export const KeyboardAwareContent = memo(function LayoutKeyboardAwareContent({ 134 children, 135 style, 136 contentContainerStyle, 137 ...props 138}: KeyboardAwareContentProps) { 139 return ( 140 <KeyboardAwareScrollView 141 style={[scrollViewStyles.common, style]} 142 contentContainerStyle={[ 143 scrollViewStyles.contentContainer, 144 contentContainerStyle, 145 ]} 146 keyboardShouldPersistTaps="handled" 147 {...props}> 148 {IS_WEB ? <Center>{children}</Center> : children} 149 </KeyboardAwareScrollView> 150 ) 151}) 152 153/** 154 * Utility component to center content within the screen 155 */ 156export const Center = memo(function LayoutCenter({ 157 children, 158 style, 159 ignoreTabletLayoutOffset, 160 ...props 161}: ViewProps & {ignoreTabletLayoutOffset?: boolean}) { 162 const {isWithinOffsetView} = useContext(ScrollbarOffsetContext) 163 const {gtMobile} = useBreakpoints() 164 const {centerColumnOffset} = useLayoutBreakpoints() 165 const {isWithinDialog} = useDialogContext() 166 const ctx = useMemo(() => ({isWithinOffsetView: true}), []) 167 return ( 168 <View 169 style={[ 170 a.w_full, 171 a.mx_auto, 172 gtMobile && { 173 maxWidth: 600, 174 }, 175 !isWithinOffsetView && { 176 transform: [ 177 { 178 translateX: 179 centerColumnOffset && 180 !ignoreTabletLayoutOffset && 181 !isWithinDialog 182 ? CENTER_COLUMN_OFFSET 183 : 0, 184 }, 185 {translateX: web(SCROLLBAR_OFFSET) ?? 0}, 186 ], 187 }, 188 style, 189 ]} 190 {...props}> 191 <ScrollbarOffsetContext.Provider value={ctx}> 192 {children} 193 </ScrollbarOffsetContext.Provider> 194 </View> 195 ) 196}) 197 198/** 199 * Only used within `Layout.Screen`, not for reuse 200 */ 201const WebCenterBorders = memo(function LayoutWebCenterBorders() { 202 const t = useTheme() 203 const {gtMobile} = useBreakpoints() 204 const {centerColumnOffset} = useLayoutBreakpoints() 205 return gtMobile ? ( 206 <View 207 style={[ 208 a.fixed, 209 a.inset_0, 210 a.border_l, 211 a.border_r, 212 t.atoms.border_contrast_low, 213 web({ 214 width: 602, 215 left: '50%', 216 transform: [ 217 {translateX: '-50%'}, 218 {translateX: centerColumnOffset ? CENTER_COLUMN_OFFSET : 0}, 219 ...a.scrollbar_offset.transform, 220 ], 221 }), 222 ]} 223 /> 224 ) : null 225})