forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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})