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