forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {createContext, useCallback, useContext, useId, useMemo} from 'react'
2import {type GestureResponderEvent, View} from 'react-native'
3import {msg} from '@lingui/core/macro'
4import {useLingui} from '@lingui/react'
5
6import {atoms as a, useTheme, type ViewStyleProp, web} from '#/alf'
7import {
8 Button,
9 type ButtonColor,
10 ButtonIcon,
11 ButtonText,
12} from '#/components/Button'
13import * as Dialog from '#/components/Dialog'
14import {type Props as SVGIconProps} from '#/components/icons/common'
15import {Text} from '#/components/Typography'
16import {type BottomSheetViewProps} from '../../modules/bottom-sheet'
17
18export {
19 type DialogControlProps as PromptControlProps,
20 useDialogControl as usePromptControl,
21} from '#/components/Dialog'
22
23const Context = createContext<{
24 titleId: string
25 descriptionId: string
26}>({
27 titleId: '',
28 descriptionId: '',
29})
30Context.displayName = 'PromptContext'
31
32export function Outer({
33 children,
34 control,
35 testID,
36 nativeOptions,
37}: React.PropsWithChildren<{
38 control: Dialog.DialogControlProps
39 testID?: string
40 /**
41 * Native-specific options for the prompt. Extends `BottomSheetViewProps`
42 */
43 nativeOptions?: Omit<BottomSheetViewProps, 'children'>
44}>) {
45 const titleId = useId()
46 const descriptionId = useId()
47
48 const context = useMemo(
49 () => ({titleId, descriptionId}),
50 [titleId, descriptionId],
51 )
52
53 return (
54 <Dialog.Outer
55 control={control}
56 testID={testID}
57 webOptions={{alignCenter: true}}
58 nativeOptions={{preventExpansion: true, ...nativeOptions}}>
59 <Dialog.Handle />
60 <Context.Provider value={context}>
61 <Dialog.ScrollableInner
62 accessibilityLabelledBy={titleId}
63 accessibilityDescribedBy={descriptionId}
64 style={web([{maxWidth: 320, borderRadius: 36}])}>
65 {children}
66 </Dialog.ScrollableInner>
67 </Context.Provider>
68 </Dialog.Outer>
69 )
70}
71
72export function TitleText({
73 children,
74 style,
75}: React.PropsWithChildren<ViewStyleProp>) {
76 const {titleId} = useContext(Context)
77 return (
78 <Text
79 nativeID={titleId}
80 style={[
81 a.flex_1,
82 a.text_2xl,
83 a.font_semi_bold,
84 a.pb_xs,
85 a.leading_snug,
86 style,
87 ]}>
88 {children}
89 </Text>
90 )
91}
92
93export function DescriptionText({
94 children,
95 selectable,
96}: React.PropsWithChildren<{selectable?: boolean}>) {
97 const t = useTheme()
98 const {descriptionId} = useContext(Context)
99 return (
100 <Text
101 nativeID={descriptionId}
102 selectable={selectable}
103 style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high, a.pb_lg]}>
104 {children}
105 </Text>
106 )
107}
108
109export function Actions({children}: {children: React.ReactNode}) {
110 return <View style={[a.w_full, a.gap_sm, a.justify_end]}>{children}</View>
111}
112
113export function Content({children}: {children: React.ReactNode}) {
114 return <View style={[a.pb_sm]}>{children}</View>
115}
116
117export function Cancel({
118 cta,
119}: {
120 /**
121 * Optional i18n string. If undefined, it will default to "Cancel".
122 */
123 cta?: string
124}) {
125 const {_} = useLingui()
126 const {close} = Dialog.useDialogContext()
127 const onPress = useCallback(() => {
128 close()
129 }, [close])
130
131 return (
132 <Button
133 variant="solid"
134 color="secondary"
135 size="large"
136 label={cta || _(msg`Cancel`)}
137 onPress={onPress}>
138 <ButtonText>{cta || _(msg`Cancel`)}</ButtonText>
139 </Button>
140 )
141}
142
143export function Action({
144 onPress,
145 color = 'primary',
146 cta,
147 disabled = false,
148 icon,
149 shouldCloseOnPress = true,
150 testID,
151}: {
152 /**
153 * Callback to run when the action is pressed. The method is called _after_
154 * the dialog closes.
155 *
156 * Note: The dialog will close automatically when the action is pressed, you
157 * should NOT close the dialog as a side effect of this method.
158 */
159 onPress: (e: GestureResponderEvent) => void
160 color?: ButtonColor
161 /**
162 * Optional i18n string. If undefined, it will default to "Confirm".
163 */
164 cta?: string
165 /**
166 * If undefined, it will default to false.
167 */
168 disabled?: boolean
169 icon?: React.ComponentType<SVGIconProps>
170 /**
171 * Optionally close dialog automatically on press. If undefined, it will
172 * default to true.
173 */
174 shouldCloseOnPress?: boolean
175 testID?: string
176}) {
177 const {_} = useLingui()
178 const {close} = Dialog.useDialogContext()
179 const handleOnPress = useCallback(
180 (e: GestureResponderEvent) => {
181 if (shouldCloseOnPress) {
182 close(() => onPress?.(e))
183 } else {
184 onPress?.(e)
185 }
186 },
187 [close, onPress, shouldCloseOnPress],
188 )
189
190 return (
191 <Button
192 color={color}
193 disabled={disabled}
194 size="large"
195 label={cta || _(msg`Confirm`)}
196 onPress={handleOnPress}
197 testID={testID}>
198 <ButtonText>{cta || _(msg`Confirm`)}</ButtonText>
199 {icon && <ButtonIcon icon={icon} />}
200 </Button>
201 )
202}
203
204export function Basic({
205 control,
206 title,
207 description,
208 cancelButtonCta,
209 confirmButtonCta,
210 onConfirm,
211 confirmButtonColor,
212 showCancel = true,
213}: React.PropsWithChildren<{
214 control: Dialog.DialogOuterProps['control']
215 title: string
216 description?: string
217 cancelButtonCta?: string
218 confirmButtonCta?: string
219 /**
220 * Callback to run when the Confirm button is pressed. The method is called
221 * _after_ the dialog closes.
222 *
223 * Note: The dialog will close automatically when the action is pressed, you
224 * should NOT close the dialog as a side effect of this method.
225 */
226 onConfirm: (e: GestureResponderEvent) => void
227 confirmButtonColor?: ButtonColor
228 showCancel?: boolean
229}>) {
230 return (
231 <Outer control={control} testID="confirmModal">
232 <Content>
233 <TitleText>{title}</TitleText>
234 {description && <DescriptionText>{description}</DescriptionText>}
235 </Content>
236 <Actions>
237 <Action
238 cta={confirmButtonCta}
239 onPress={onConfirm}
240 color={confirmButtonColor}
241 testID="confirmBtn"
242 />
243 {showCancel && <Cancel cta={cancelButtonCta} />}
244 </Actions>
245 </Outer>
246 )
247}