forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1/**
2 * In the Web build, we center all content so that it mirrors the
3 * mobile experience (a single narrow column). We then place a UI
4 * shell around the content if you're in desktop.
5 *
6 * Because scrolling is handled by components deep in the hierarchy,
7 * we can't just wrap the top-level element with a max width. The
8 * centering has to be done at the ScrollView.
9 *
10 * These components wrap the RN ScrollView-based components to provide
11 * consistent layout. It also provides <CenteredView> for views that
12 * need to match layout but which aren't scrolled.
13 */
14
15import React from 'react'
16import {
17 type FlatList,
18 type FlatListProps,
19 type ScrollViewProps,
20 StyleSheet,
21 View,
22 type ViewProps,
23} from 'react-native'
24import Animated from 'react-native-reanimated'
25
26import {usePalette} from '#/lib/hooks/usePalette'
27import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
28import {addStyle} from '#/lib/styles'
29import {useLayoutBreakpoints} from '#/alf'
30import {useDialogContext} from '#/components/Dialog'
31import {CENTER_COLUMN_OFFSET} from '#/components/Layout'
32
33interface AddedProps {
34 desktopFixedHeight?: boolean | number
35}
36
37/**
38 * @deprecated use `Layout` components
39 */
40export const CenteredView = React.forwardRef(function CenteredView(
41 {
42 style,
43 topBorder,
44 ...props
45 }: React.PropsWithChildren<
46 ViewProps & {sideBorders?: boolean; topBorder?: boolean}
47 >,
48 ref: React.Ref<View>,
49) {
50 const pal = usePalette('default')
51 const {isMobile} = useWebMediaQueries()
52 const {centerColumnOffset} = useLayoutBreakpoints()
53 const {isWithinDialog} = useDialogContext()
54 if (!isMobile) {
55 style = addStyle(style, styles.container)
56 }
57 if (centerColumnOffset && !isWithinDialog) {
58 style = addStyle(style, styles.containerOffset)
59 }
60 if (topBorder) {
61 style = addStyle(style, {
62 borderTopWidth: 1,
63 })
64 style = addStyle(style, pal.border)
65 }
66 return <View ref={ref} style={style} {...props} />
67})
68
69export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
70 {
71 contentContainerStyle,
72 style,
73 contentOffset,
74 desktopFixedHeight,
75 ...props
76 }: React.PropsWithChildren<
77 Omit<FlatListProps<ItemT>, 'CellRendererComponent'> & AddedProps
78 >,
79 ref: React.Ref<FlatList<ItemT>>,
80) {
81 const {isMobile} = useWebMediaQueries()
82 const {centerColumnOffset} = useLayoutBreakpoints()
83 const {isWithinDialog} = useDialogContext()
84 if (!isMobile) {
85 contentContainerStyle = addStyle(
86 contentContainerStyle,
87 styles.containerScroll,
88 )
89 }
90 if (centerColumnOffset && !isWithinDialog) {
91 style = addStyle(style, styles.containerOffset)
92 }
93 if (contentOffset && contentOffset?.y !== 0) {
94 // NOTE
95 // we use paddingTop & contentOffset to space around the floating header
96 // but reactnative web puts the paddingTop on the wrong element (style instead of the contentContainer)
97 // so we manually correct it here
98 // -prf
99 style = addStyle(style, {
100 paddingTop: 0,
101 })
102 contentContainerStyle = addStyle(contentContainerStyle, {
103 paddingTop: Math.abs(contentOffset.y),
104 })
105 }
106 if (desktopFixedHeight) {
107 if (typeof desktopFixedHeight === 'number') {
108 // @ts-expect-error Web only -prf
109 style = addStyle(style, {
110 height: `calc(100vh - ${desktopFixedHeight}px)`,
111 })
112 } else {
113 style = addStyle(style, styles.fixedHeight)
114 }
115 if (!isMobile) {
116 // NOTE
117 // react native web produces *three* wrapping divs
118 // the first two use the `style` prop and the innermost uses the
119 // `contentContainerStyle`. Unfortunately the stable-gutter style
120 // needs to be applied to only the "middle" of these. To hack
121 // around this, we set data-stable-gutters which can then be
122 // styled in our external CSS.
123 // -prf
124 // @ts-expect-error web only -prf
125 props.dataSet = props.dataSet || {}
126 // @ts-expect-error web only -prf
127 props.dataSet.stableGutters = '1'
128 }
129 }
130 return (
131 <Animated.FlatList
132 ref={ref}
133 contentContainerStyle={[styles.contentContainer, contentContainerStyle]}
134 style={style}
135 contentOffset={contentOffset}
136 {...props}
137 />
138 )
139})
140
141/**
142 * @deprecated use `Layout` components
143 */
144export const ScrollView = React.forwardRef(function ScrollViewImpl(
145 {contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>,
146 ref: React.Ref<Animated.ScrollView>,
147) {
148 const {isMobile} = useWebMediaQueries()
149 const {centerColumnOffset} = useLayoutBreakpoints()
150 if (!isMobile) {
151 contentContainerStyle = addStyle(
152 contentContainerStyle,
153 styles.containerScroll,
154 )
155 }
156 if (centerColumnOffset) {
157 contentContainerStyle = addStyle(
158 contentContainerStyle,
159 styles.containerOffset,
160 )
161 }
162 return (
163 <Animated.ScrollView
164 contentContainerStyle={[styles.contentContainer, contentContainerStyle]}
165 ref={ref}
166 {...props}
167 />
168 )
169})
170
171const styles = StyleSheet.create({
172 contentContainer: {
173 // @ts-expect-error web only
174 minHeight: '100vh',
175 },
176 container: {
177 width: '100%',
178 maxWidth: 600,
179 marginLeft: 'auto',
180 marginRight: 'auto',
181 },
182 containerOffset: {
183 transform: [{translateX: CENTER_COLUMN_OFFSET}],
184 },
185 containerScroll: {
186 width: '100%',
187 maxWidth: 600,
188 marginLeft: 'auto',
189 marginRight: 'auto',
190 },
191 fixedHeight: {
192 // @ts-expect-error web only
193 height: '100vh',
194 },
195})