forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {createContext, useCallback, useContext} from 'react'
2import {type GestureResponderEvent, Keyboard, View} from 'react-native'
3import {msg} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
5import {useNavigation} from '@react-navigation/native'
6
7import {HITSLOP_30} from '#/lib/constants'
8import {type NavigationProp} from '#/lib/routes/types'
9import {useSetDrawerOpen} from '#/state/shell'
10import {
11 atoms as a,
12 platform,
13 type TextStyleProp,
14 useBreakpoints,
15 useGutters,
16 useLayoutBreakpoints,
17 useTheme,
18 web,
19} from '#/alf'
20import {Button, ButtonIcon, type ButtonProps} from '#/components/Button'
21import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
22import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
23import {
24 BUTTON_VISUAL_ALIGNMENT_OFFSET,
25 CENTER_COLUMN_OFFSET,
26 HEADER_SLOT_SIZE,
27 SCROLLBAR_OFFSET,
28} from '#/components/Layout/const'
29import {ScrollbarOffsetContext} from '#/components/Layout/context'
30import {Text} from '#/components/Typography'
31import {IS_IOS} from '#/env'
32
33export function Outer({
34 children,
35 noBottomBorder,
36 headerRef,
37 sticky = true,
38}: {
39 children: React.ReactNode
40 noBottomBorder?: boolean
41 headerRef?: React.RefObject<View | null>
42 sticky?: boolean
43}) {
44 const t = useTheme()
45 const gutters = useGutters([0, 'base'])
46 const {gtMobile} = useBreakpoints()
47 const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
48 const {centerColumnOffset} = useLayoutBreakpoints()
49
50 return (
51 <View
52 ref={headerRef}
53 style={[
54 a.w_full,
55 !noBottomBorder && a.border_b,
56 a.flex_row,
57 a.align_center,
58 a.gap_sm,
59 sticky && web([a.sticky, {top: 0}, a.z_10, t.atoms.bg]),
60 gutters,
61 platform({
62 native: [a.pb_xs, {minHeight: 48}],
63 web: [
64 a.py_xs,
65 {
66 minHeight: 52,
67 paddingTop: 'env(safe-area-inset-top)',
68 },
69 ],
70 }),
71 t.atoms.border_contrast_low,
72 gtMobile && [a.mx_auto, {maxWidth: 600}],
73 !isWithinOffsetView && {
74 transform: [
75 {translateX: centerColumnOffset ? CENTER_COLUMN_OFFSET : 0},
76 {translateX: web(SCROLLBAR_OFFSET) ?? 0},
77 ],
78 },
79 ]}>
80 {children}
81 </View>
82 )
83}
84
85const AlignmentContext = createContext<'platform' | 'left'>('platform')
86AlignmentContext.displayName = 'AlignmentContext'
87
88export function Content({
89 children,
90 align = 'platform',
91}: {
92 children?: React.ReactNode
93 align?: 'platform' | 'left'
94}) {
95 return (
96 <View
97 style={[
98 a.flex_1,
99 a.justify_center,
100 IS_IOS && align === 'platform' && a.align_center,
101 {minHeight: HEADER_SLOT_SIZE},
102 ]}>
103 <AlignmentContext.Provider value={align}>
104 {children}
105 </AlignmentContext.Provider>
106 </View>
107 )
108}
109
110export function Slot({children}: {children?: React.ReactNode}) {
111 return <View style={[a.z_50, {width: HEADER_SLOT_SIZE}]}>{children}</View>
112}
113
114export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
115 const {_} = useLingui()
116 const navigation = useNavigation<NavigationProp>()
117
118 const onPressBack = useCallback(
119 (evt: GestureResponderEvent) => {
120 onPress?.(evt)
121 if (evt.defaultPrevented) return
122 if (navigation.canGoBack()) {
123 navigation.goBack()
124 } else {
125 navigation.navigate('Home')
126 }
127 },
128 [onPress, navigation],
129 )
130
131 return (
132 <Slot>
133 <Button
134 label={_(msg`Go back`)}
135 size="small"
136 variant="ghost"
137 color="secondary"
138 shape="round"
139 onPress={onPressBack}
140 hitSlop={HITSLOP_30}
141 style={[
142 {marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
143 a.bg_transparent,
144 style,
145 ]}
146 {...props}>
147 <ButtonIcon icon={ArrowLeft} size="lg" />
148 </Button>
149 </Slot>
150 )
151}
152
153export function MenuButton() {
154 const {_} = useLingui()
155 const setDrawerOpen = useSetDrawerOpen()
156 const {gtMobile} = useBreakpoints()
157
158 const onPress = useCallback(() => {
159 Keyboard.dismiss()
160 setDrawerOpen(true)
161 }, [setDrawerOpen])
162
163 return gtMobile ? null : (
164 <Slot>
165 <Button
166 label={_(msg`Open drawer menu`)}
167 size="small"
168 variant="ghost"
169 color="secondary"
170 shape="square"
171 onPress={onPress}
172 hitSlop={HITSLOP_30}
173 style={[
174 {marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
175 a.bg_transparent,
176 ]}>
177 <ButtonIcon icon={Menu} size="lg" />
178 </Button>
179 </Slot>
180 )
181}
182
183export function TitleText({
184 children,
185 style,
186}: {children: React.ReactNode} & TextStyleProp) {
187 const {gtMobile} = useBreakpoints()
188 const align = useContext(AlignmentContext)
189 return (
190 <Text
191 style={[
192 a.text_lg,
193 a.font_semi_bold,
194 a.leading_tight,
195 IS_IOS && align === 'platform' && a.text_center,
196 gtMobile && a.text_xl,
197 style,
198 ]}
199 numberOfLines={2}
200 emoji>
201 {children}
202 </Text>
203 )
204}
205
206export function SubtitleText({children}: {children: React.ReactNode}) {
207 const t = useTheme()
208 const align = useContext(AlignmentContext)
209 return (
210 <Text
211 style={[
212 a.text_sm,
213 a.leading_snug,
214 IS_IOS && align === 'platform' && a.text_center,
215 t.atoms.text_contrast_medium,
216 ]}
217 numberOfLines={2}>
218 {children}
219 </Text>
220 )
221}