An ATproto social media client -- with an independent Appview.
at main 5.6 kB view raw
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}