Bluesky app fork with some witchin' additions 馃挮
at main 279 lines 8.9 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 {useAppPasswordCreateMutation} from '#/state/queries/app-passwords' 17import {atoms as a, native, useTheme} from '#/alf' 18import {Admonition} from '#/components/Admonition' 19import {Button, ButtonIcon, ButtonText} from '#/components/Button' 20import * as Dialog from '#/components/Dialog' 21import * as TextInput from '#/components/forms/TextField' 22import * as Toggle from '#/components/forms/Toggle' 23import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 24import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4' 25import {Text} from '#/components/Typography' 26import {IS_WEB} from '#/env' 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={IS_WEB ? 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 color="secondary"> 199 <ButtonText>{data.password}</ButtonText> 200 <ButtonIcon icon={CopyIcon} /> 201 </CopyButton> 202 <Text 203 style={[ 204 a.text_md, 205 a.leading_snug, 206 t.atoms.text_contrast_medium, 207 ]}> 208 <Trans> 209 For security reasons, you won't be able to view this again. If 210 you lose this app password, you'll need to generate a new one. 211 </Trans> 212 </Text> 213 <Button 214 label={_(msg`Done`)} 215 size="large" 216 variant="outline" 217 color="primary" 218 style={[a.flex_1]} 219 onPress={() => control.close()}> 220 <ButtonText> 221 <Trans>Done</Trans> 222 </ButtonText> 223 </Button> 224 </Animated.View> 225 )} 226 </LayoutAnimationConfig> 227 </View> 228 <Dialog.Close /> 229 </Dialog.ScrollableInner> 230 ) 231} 232 233class DisplayableError extends Error { 234 constructor(message: string) { 235 super(message) 236 this.name = 'DisplayableError' 237 } 238} 239 240function useRandomName() { 241 return useState( 242 () => shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)], 243 )[0] 244} 245 246const shadesOfBlue: string[] = [ 247 'AliceBlue', 248 'Aqua', 249 'Aquamarine', 250 'Azure', 251 'BabyBlue', 252 'Blue', 253 'BlueViolet', 254 'CadetBlue', 255 'CornflowerBlue', 256 'Cyan', 257 'DarkBlue', 258 'DarkCyan', 259 'DarkSlateBlue', 260 'DeepSkyBlue', 261 'DodgerBlue', 262 'ElectricBlue', 263 'LightBlue', 264 'LightCyan', 265 'LightSkyBlue', 266 'LightSteelBlue', 267 'MediumAquaMarine', 268 'MediumBlue', 269 'MediumSlateBlue', 270 'MidnightBlue', 271 'Navy', 272 'PowderBlue', 273 'RoyalBlue', 274 'SkyBlue', 275 'SlateBlue', 276 'SteelBlue', 277 'Teal', 278 'Turquoise', 279]