An ATproto social media client -- with an independent Appview.
1import React from 'react'
2import {type GestureResponderEvent, View} from 'react-native'
3import {msg} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
5
6import {
7 atoms as a,
8 useBreakpoints,
9 useTheme,
10 type ViewStyleProp,
11 web,
12} from '#/alf'
13import {Button, type ButtonColor, ButtonText} from '#/components/Button'
14import * as Dialog from '#/components/Dialog'
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 = React.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 nativeOptions?: Omit<BottomSheetViewProps, 'children'>
41}>) {
42 const titleId = React.useId()
43 const descriptionId = React.useId()
44
45 const context = React.useMemo(
46 () => ({titleId, descriptionId}),
47 [titleId, descriptionId],
48 )
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: 400})}>
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} = React.useContext(Context)
74 return (
75 <Text
76 nativeID={titleId}
77 style={[
78 a.flex_1,
79 a.text_2xl,
80 a.font_bold,
81 a.pb_sm,
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} = React.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}: React.PropsWithChildren<{}>) {
107 const {gtMobile} = useBreakpoints()
108
109 return (
110 <View
111 style={[
112 a.w_full,
113 a.gap_md,
114 a.justify_end,
115 gtMobile
116 ? [a.flex_row, a.flex_row_reverse, a.justify_start]
117 : [a.flex_col],
118 ]}>
119 {children}
120 </View>
121 )
122}
123
124export function Cancel({
125 cta,
126}: {
127 /**
128 * Optional i18n string. If undefined, it will default to "Cancel".
129 */
130 cta?: string
131}) {
132 const {_} = useLingui()
133 const {gtMobile} = useBreakpoints()
134 const {close} = Dialog.useDialogContext()
135 const onPress = React.useCallback(() => {
136 close()
137 }, [close])
138
139 return (
140 <Button
141 variant="solid"
142 color="secondary"
143 size={gtMobile ? 'small' : 'large'}
144 label={cta || _(msg`Cancel`)}
145 onPress={onPress}>
146 <ButtonText>{cta || _(msg`Cancel`)}</ButtonText>
147 </Button>
148 )
149}
150
151export function Action({
152 onPress,
153 color = 'primary',
154 cta,
155 testID,
156}: {
157 /**
158 * Callback to run when the action is pressed. The method is called _after_
159 * the dialog closes.
160 *
161 * Note: The dialog will close automatically when the action is pressed, you
162 * should NOT close the dialog as a side effect of this method.
163 */
164 onPress: (e: GestureResponderEvent) => void
165 color?: ButtonColor
166 /**
167 * Optional i18n string. If undefined, it will default to "Confirm".
168 */
169 cta?: string
170 testID?: string
171}) {
172 const {_} = useLingui()
173 const {gtMobile} = useBreakpoints()
174 const {close} = Dialog.useDialogContext()
175 const handleOnPress = React.useCallback(
176 (e: GestureResponderEvent) => {
177 close(() => onPress?.(e))
178 },
179 [close, onPress],
180 )
181
182 return (
183 <Button
184 variant="solid"
185 color={color}
186 size={gtMobile ? 'small' : 'large'}
187 label={cta || _(msg`Confirm`)}
188 onPress={handleOnPress}
189 testID={testID}>
190 <ButtonText>{cta || _(msg`Confirm`)}</ButtonText>
191 </Button>
192 )
193}
194
195export function Basic({
196 control,
197 title,
198 description,
199 cancelButtonCta,
200 confirmButtonCta,
201 onConfirm,
202 confirmButtonColor,
203 showCancel = true,
204}: React.PropsWithChildren<{
205 control: Dialog.DialogOuterProps['control']
206 title: string
207 description?: string
208 cancelButtonCta?: string
209 confirmButtonCta?: string
210 /**
211 * Callback to run when the Confirm button is pressed. The method is called
212 * _after_ the dialog closes.
213 *
214 * Note: The dialog will close automatically when the action is pressed, you
215 * should NOT close the dialog as a side effect of this method.
216 */
217 onConfirm: (e: GestureResponderEvent) => void
218 confirmButtonColor?: ButtonColor
219 showCancel?: boolean
220}>) {
221 return (
222 <Outer control={control} testID="confirmModal">
223 <TitleText>{title}</TitleText>
224 {description && <DescriptionText>{description}</DescriptionText>}
225 <Actions>
226 <Action
227 cta={confirmButtonCta}
228 onPress={onConfirm}
229 color={confirmButtonColor}
230 testID="confirmBtn"
231 />
232 {showCancel && <Cancel cta={cancelButtonCta} />}
233 </Actions>
234 </Outer>
235 )
236}