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