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