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: [a.py_xs, {minHeight: 52}],
64 }),
65 t.atoms.border_contrast_low,
66 gtMobile && [a.mx_auto, {maxWidth: 600}],
67 !isWithinOffsetView && {
68 transform: [
69 {translateX: centerColumnOffset ? CENTER_COLUMN_OFFSET : 0},
70 {translateX: web(SCROLLBAR_OFFSET) ?? 0},
71 ],
72 },
73 ]}>
74 {children}
75 </View>
76 )
77}
78
79const AlignmentContext = createContext<'platform' | 'left'>('platform')
80AlignmentContext.displayName = 'AlignmentContext'
81
82export function Content({
83 children,
84 align = 'platform',
85}: {
86 children?: React.ReactNode
87 align?: 'platform' | 'left'
88}) {
89 return (
90 <View
91 style={[
92 a.flex_1,
93 a.justify_center,
94 IS_IOS && align === 'platform' && a.align_center,
95 {minHeight: HEADER_SLOT_SIZE},
96 ]}>
97 <AlignmentContext.Provider value={align}>
98 {children}
99 </AlignmentContext.Provider>
100 </View>
101 )
102}
103
104export function Slot({children}: {children?: React.ReactNode}) {
105 return <View style={[a.z_50, {width: HEADER_SLOT_SIZE}]}>{children}</View>
106}
107
108export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
109 const {_} = useLingui()
110 const navigation = useNavigation<NavigationProp>()
111
112 const onPressBack = useCallback(
113 (evt: GestureResponderEvent) => {
114 onPress?.(evt)
115 if (evt.defaultPrevented) return
116 if (navigation.canGoBack()) {
117 navigation.goBack()
118 } else {
119 navigation.navigate('Home')
120 }
121 },
122 [onPress, navigation],
123 )
124
125 return (
126 <Slot>
127 <Button
128 label={_(msg`Go back`)}
129 size="small"
130 variant="ghost"
131 color="secondary"
132 shape="round"
133 onPress={onPressBack}
134 hitSlop={HITSLOP_30}
135 style={[
136 {marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
137 a.bg_transparent,
138 style,
139 ]}
140 {...props}>
141 <ButtonIcon icon={ArrowLeft} size="lg" />
142 </Button>
143 </Slot>
144 )
145}
146
147export function MenuButton() {
148 const {_} = useLingui()
149 const setDrawerOpen = useSetDrawerOpen()
150 const {gtMobile} = useBreakpoints()
151
152 const onPress = useCallback(() => {
153 Keyboard.dismiss()
154 setDrawerOpen(true)
155 }, [setDrawerOpen])
156
157 return gtMobile ? null : (
158 <Slot>
159 <Button
160 label={_(msg`Open drawer menu`)}
161 size="small"
162 variant="ghost"
163 color="secondary"
164 shape="square"
165 onPress={onPress}
166 hitSlop={HITSLOP_30}
167 style={[
168 {marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
169 a.bg_transparent,
170 ]}>
171 <ButtonIcon icon={Menu} size="lg" />
172 </Button>
173 </Slot>
174 )
175}
176
177export function TitleText({
178 children,
179 style,
180}: {children: React.ReactNode} & TextStyleProp) {
181 const {gtMobile} = useBreakpoints()
182 const align = useContext(AlignmentContext)
183 return (
184 <Text
185 style={[
186 a.text_lg,
187 a.font_semi_bold,
188 a.leading_tight,
189 IS_IOS && align === 'platform' && a.text_center,
190 gtMobile && a.text_xl,
191 style,
192 ]}
193 numberOfLines={2}
194 emoji>
195 {children}
196 </Text>
197 )
198}
199
200export function SubtitleText({children}: {children: React.ReactNode}) {
201 const t = useTheme()
202 const align = useContext(AlignmentContext)
203 return (
204 <Text
205 style={[
206 a.text_sm,
207 a.leading_snug,
208 IS_IOS && align === 'platform' && a.text_center,
209 t.atoms.text_contrast_medium,
210 ]}
211 numberOfLines={2}>
212 {children}
213 </Text>
214 )
215}