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