mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at remove-hackfix 280 lines 9.0 kB view raw
1import {useEffect, useMemo, useState} from 'react' 2import {useWindowDimensions, View} from 'react-native' 3import Animated, { 4 FadeIn, 5 FadeOut, 6 LayoutAnimationConfig, 7 LinearTransition, 8 SlideInRight, 9 SlideOutLeft, 10} from 'react-native-reanimated' 11import {type ComAtprotoServerCreateAppPassword} from '@atproto/api' 12import {msg, Trans} from '@lingui/macro' 13import {useLingui} from '@lingui/react' 14import {useMutation} from '@tanstack/react-query' 15 16import {isWeb} from '#/platform/detection' 17import {useAppPasswordCreateMutation} from '#/state/queries/app-passwords' 18import {atoms as a, native, useTheme} from '#/alf' 19import {Admonition} from '#/components/Admonition' 20import {Button, ButtonIcon, ButtonText} from '#/components/Button' 21import * as Dialog from '#/components/Dialog' 22import * as TextInput from '#/components/forms/TextField' 23import * as Toggle from '#/components/forms/Toggle' 24import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 25import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4' 26import {Text} from '#/components/Typography' 27import {CopyButton} from './CopyButton' 28 29export function AddAppPasswordDialog({ 30 control, 31 passwords, 32}: { 33 control: Dialog.DialogControlProps 34 passwords: string[] 35}) { 36 const {height} = useWindowDimensions() 37 return ( 38 <Dialog.Outer control={control} nativeOptions={{minHeight: height}}> 39 <Dialog.Handle /> 40 <CreateDialogInner passwords={passwords} /> 41 </Dialog.Outer> 42 ) 43} 44 45function CreateDialogInner({passwords}: {passwords: string[]}) { 46 const control = Dialog.useDialogContext() 47 const t = useTheme() 48 const {_} = useLingui() 49 const autogeneratedName = useRandomName() 50 const [name, setName] = useState('') 51 const [privileged, setPrivileged] = useState(false) 52 const { 53 mutateAsync: actuallyCreateAppPassword, 54 error: apiError, 55 data, 56 } = useAppPasswordCreateMutation() 57 58 const regexFailError = useMemo( 59 () => 60 new DisplayableError( 61 _( 62 msg`App password names can only contain letters, numbers, spaces, dashes, and underscores`, 63 ), 64 ), 65 [_], 66 ) 67 68 const { 69 mutate: createAppPassword, 70 error: validationError, 71 isPending, 72 } = useMutation< 73 ComAtprotoServerCreateAppPassword.AppPassword, 74 Error | DisplayableError 75 >({ 76 mutationFn: async () => { 77 const chosenName = name.trim() || autogeneratedName 78 if (chosenName.length < 4) { 79 throw new DisplayableError( 80 _(msg`App password names must be at least 4 characters long`), 81 ) 82 } 83 if (passwords.find(p => p === chosenName)) { 84 throw new DisplayableError(_(msg`App password name must be unique`)) 85 } 86 return await actuallyCreateAppPassword({name: chosenName, privileged}) 87 }, 88 }) 89 90 const [hasBeenCopied, setHasBeenCopied] = useState(false) 91 useEffect(() => { 92 if (hasBeenCopied) { 93 const timeout = setTimeout(() => setHasBeenCopied(false), 100) 94 return () => clearTimeout(timeout) 95 } 96 }, [hasBeenCopied]) 97 98 const error = 99 validationError || (!name.match(/^[a-zA-Z0-9-_ ]*$/) && regexFailError) 100 101 return ( 102 <Dialog.ScrollableInner label={_(msg`Add app password`)}> 103 <View style={[native(a.pt_md)]}> 104 <LayoutAnimationConfig skipEntering skipExiting> 105 {!data ? ( 106 <Animated.View 107 style={[a.gap_lg]} 108 exiting={native(SlideOutLeft)} 109 key={0}> 110 <Text style={[a.text_2xl, a.font_semi_bold]}> 111 <Trans>Add App Password</Trans> 112 </Text> 113 <Text style={[a.text_md, a.leading_snug]}> 114 <Trans> 115 Please enter a unique name for this app password or use our 116 randomly generated one. 117 </Trans> 118 </Text> 119 <View> 120 <TextInput.Root isInvalid={!!error}> 121 <Dialog.Input 122 label={_(msg`App Password`)} 123 placeholder={autogeneratedName} 124 onChangeText={setName} 125 returnKeyType="done" 126 onSubmitEditing={() => createAppPassword()} 127 blurOnSubmit 128 autoCorrect={false} 129 autoComplete="off" 130 autoCapitalize="none" 131 autoFocus 132 /> 133 </TextInput.Root> 134 </View> 135 {error instanceof DisplayableError && ( 136 <Animated.View entering={FadeIn} exiting={FadeOut}> 137 <Admonition type="error">{error.message}</Admonition> 138 </Animated.View> 139 )} 140 <Animated.View 141 style={[a.gap_lg]} 142 layout={native(LinearTransition)}> 143 <Toggle.Item 144 name="privileged" 145 type="checkbox" 146 label={_(msg`Allow access to your direct messages`)} 147 value={privileged} 148 onChange={setPrivileged} 149 style={[a.flex_1]}> 150 <Toggle.Checkbox /> 151 <Toggle.LabelText 152 style={[a.font_normal, a.text_md, a.leading_snug]}> 153 <Trans>Allow access to your direct messages</Trans> 154 </Toggle.LabelText> 155 </Toggle.Item> 156 <Button 157 label={_(msg`Next`)} 158 size="large" 159 variant="solid" 160 color="primary" 161 style={[a.flex_1]} 162 onPress={() => createAppPassword()} 163 disabled={isPending}> 164 <ButtonText> 165 <Trans>Next</Trans> 166 </ButtonText> 167 <ButtonIcon icon={ChevronRight} /> 168 </Button> 169 {!!apiError || 170 (error && !(error instanceof DisplayableError) && ( 171 <Animated.View entering={FadeIn} exiting={FadeOut}> 172 <Admonition type="error"> 173 <Trans> 174 Failed to create app password. Please try again. 175 </Trans> 176 </Admonition> 177 </Animated.View> 178 ))} 179 </Animated.View> 180 </Animated.View> 181 ) : ( 182 <Animated.View 183 style={[a.gap_lg]} 184 entering={isWeb ? FadeIn.delay(200) : SlideInRight} 185 key={1}> 186 <Text style={[a.text_2xl, a.font_semi_bold]}> 187 <Trans>Here is your app password!</Trans> 188 </Text> 189 <Text style={[a.text_md, a.leading_snug]}> 190 <Trans> 191 Use this to sign in to the other app along with your handle. 192 </Trans> 193 </Text> 194 <CopyButton 195 value={data.password} 196 label={_(msg`Copy App Password`)} 197 size="large" 198 variant="solid" 199 color="secondary"> 200 <ButtonText>{data.password}</ButtonText> 201 <ButtonIcon icon={CopyIcon} /> 202 </CopyButton> 203 <Text 204 style={[ 205 a.text_md, 206 a.leading_snug, 207 t.atoms.text_contrast_medium, 208 ]}> 209 <Trans> 210 For security reasons, you won't be able to view this again. If 211 you lose this app password, you'll need to generate a new one. 212 </Trans> 213 </Text> 214 <Button 215 label={_(msg`Done`)} 216 size="large" 217 variant="outline" 218 color="primary" 219 style={[a.flex_1]} 220 onPress={() => control.close()}> 221 <ButtonText> 222 <Trans>Done</Trans> 223 </ButtonText> 224 </Button> 225 </Animated.View> 226 )} 227 </LayoutAnimationConfig> 228 </View> 229 <Dialog.Close /> 230 </Dialog.ScrollableInner> 231 ) 232} 233 234class DisplayableError extends Error { 235 constructor(message: string) { 236 super(message) 237 this.name = 'DisplayableError' 238 } 239} 240 241function useRandomName() { 242 return useState( 243 () => shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)], 244 )[0] 245} 246 247const shadesOfBlue: string[] = [ 248 'AliceBlue', 249 'Aqua', 250 'Aquamarine', 251 'Azure', 252 'BabyBlue', 253 'Blue', 254 'BlueViolet', 255 'CadetBlue', 256 'CornflowerBlue', 257 'Cyan', 258 'DarkBlue', 259 'DarkCyan', 260 'DarkSlateBlue', 261 'DeepSkyBlue', 262 'DodgerBlue', 263 'ElectricBlue', 264 'LightBlue', 265 'LightCyan', 266 'LightSkyBlue', 267 'LightSteelBlue', 268 'MediumAquaMarine', 269 'MediumBlue', 270 'MediumSlateBlue', 271 'MidnightBlue', 272 'Navy', 273 'PowderBlue', 274 'RoyalBlue', 275 'SkyBlue', 276 'SlateBlue', 277 'SteelBlue', 278 'Teal', 279 'Turquoise', 280]