Bluesky app fork with some witchin' additions 💫
at b4e1fdfccfda7150f5053dec5a94fe05bb7ed17e 249 lines 8.1 kB view raw
1import {useCallback, useImperativeHandle, useRef, useState} from 'react' 2import {View} from 'react-native' 3import {useWindowDimensions} from 'react-native' 4import {msg, Trans} from '@lingui/macro' 5import {useLingui} from '@lingui/react' 6 7import {BSKY_SERVICE} from '#/lib/constants' 8import {logger} from '#/logger' 9import * as persisted from '#/state/persisted' 10import {useSession} from '#/state/session' 11import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf' 12import {Admonition} from '#/components/Admonition' 13import {Button, ButtonText} from '#/components/Button' 14import * as Dialog from '#/components/Dialog' 15import * as SegmentedControl from '#/components/forms/SegmentedControl' 16import * as TextField from '#/components/forms/TextField' 17import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' 18import {InlineLinkText} from '#/components/Link' 19import {Text} from '#/components/Typography' 20 21type SegmentedControlOptions = typeof BSKY_SERVICE | 'custom' 22 23export function ServerInputDialog({ 24 control, 25 onSelect, 26}: { 27 control: Dialog.DialogOuterProps['control'] 28 onSelect: (url: string) => void 29}) { 30 const {height} = useWindowDimensions() 31 const formRef = useRef<DialogInnerRef>(null) 32 33 // persist these options between dialog open/close 34 const [fixedOption, setFixedOption] = 35 useState<SegmentedControlOptions>(BSKY_SERVICE) 36 const [previousCustomAddress, setPreviousCustomAddress] = useState('') 37 38 const onClose = useCallback(() => { 39 const result = formRef.current?.getFormState() 40 if (result) { 41 onSelect(result) 42 if (result !== BSKY_SERVICE) { 43 setPreviousCustomAddress(result) 44 } 45 } 46 logger.metric('signin:hostingProviderPressed', { 47 hostingProviderDidChange: fixedOption !== BSKY_SERVICE, 48 }) 49 }, [onSelect, fixedOption]) 50 51 return ( 52 <Dialog.Outer 53 control={control} 54 onClose={onClose} 55 nativeOptions={platform({ 56 android: {minHeight: height / 2}, 57 ios: {preventExpansion: true}, 58 })}> 59 <Dialog.Handle /> 60 <DialogInner 61 formRef={formRef} 62 fixedOption={fixedOption} 63 setFixedOption={setFixedOption} 64 initialCustomAddress={previousCustomAddress} 65 /> 66 </Dialog.Outer> 67 ) 68} 69 70type DialogInnerRef = {getFormState: () => string | null} 71 72function DialogInner({ 73 formRef, 74 fixedOption, 75 setFixedOption, 76 initialCustomAddress, 77}: { 78 formRef: React.Ref<DialogInnerRef> 79 fixedOption: SegmentedControlOptions 80 setFixedOption: (opt: SegmentedControlOptions) => void 81 initialCustomAddress: string 82}) { 83 const control = Dialog.useDialogContext() 84 const {_} = useLingui() 85 const t = useTheme() 86 const {accounts} = useSession() 87 const {gtMobile} = useBreakpoints() 88 const [customAddress, setCustomAddress] = useState(initialCustomAddress) 89 const [pdsAddressHistory, setPdsAddressHistory] = useState<string[]>( 90 persisted.get('pdsAddressHistory') || [], 91 ) 92 93 useImperativeHandle( 94 formRef, 95 () => ({ 96 getFormState: () => { 97 let url 98 if (fixedOption === 'custom') { 99 url = customAddress.trim().toLowerCase() 100 if (!url) { 101 return null 102 } 103 } else { 104 url = fixedOption 105 } 106 if (!url.startsWith('http://') && !url.startsWith('https://')) { 107 if (url === 'localhost' || url.startsWith('localhost:')) { 108 url = `http://${url}` 109 } else { 110 url = `https://${url}` 111 } 112 } 113 114 if (fixedOption === 'custom') { 115 if (!pdsAddressHistory.includes(url)) { 116 const newHistory = [url, ...pdsAddressHistory.slice(0, 4)] 117 setPdsAddressHistory(newHistory) 118 persisted.write('pdsAddressHistory', newHistory) 119 } 120 } 121 122 return url 123 }, 124 }), 125 [customAddress, fixedOption, pdsAddressHistory], 126 ) 127 128 const isFirstTimeUser = accounts.length === 0 129 130 return ( 131 <Dialog.ScrollableInner 132 accessibilityDescribedBy="dialog-description" 133 accessibilityLabelledBy="dialog-title" 134 style={web({maxWidth: 500})}> 135 <View style={[a.relative, a.gap_md, a.w_full]}> 136 <Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}> 137 <Trans>Choose your account provider</Trans> 138 </Text> 139 <SegmentedControl.Root 140 type="tabs" 141 label={_(msg`Account provider`)} 142 value={fixedOption} 143 onChange={setFixedOption}> 144 <SegmentedControl.Item 145 testID="bskyServiceSelectBtn" 146 value={BSKY_SERVICE} 147 label={_(msg`Bluesky`)}> 148 <SegmentedControl.ItemText> 149 {_(msg`Bluesky`)} 150 </SegmentedControl.ItemText> 151 </SegmentedControl.Item> 152 <SegmentedControl.Item 153 testID="customSelectBtn" 154 value="custom" 155 label={_(msg`Custom`)}> 156 <SegmentedControl.ItemText> 157 {_(msg`Custom`)} 158 </SegmentedControl.ItemText> 159 </SegmentedControl.Item> 160 </SegmentedControl.Root> 161 162 {fixedOption === BSKY_SERVICE && isFirstTimeUser && ( 163 <View role="tabpanel"> 164 <Admonition type="tip"> 165 <Trans> 166 Bluesky is an open network where you can choose your own 167 provider. If you're new here, we recommend sticking with the 168 default Bluesky Social option. 169 </Trans> 170 </Admonition> 171 </View> 172 )} 173 174 {fixedOption === 'custom' && ( 175 <View role="tabpanel"> 176 <TextField.LabelText nativeID="address-input-label"> 177 <Trans>Server address</Trans> 178 </TextField.LabelText> 179 <TextField.Root> 180 <TextField.Icon icon={Globe} /> 181 <Dialog.Input 182 testID="customServerTextInput" 183 value={customAddress} 184 onChangeText={setCustomAddress} 185 label="my-server.com" 186 accessibilityLabelledBy="address-input-label" 187 autoCapitalize="none" 188 keyboardType="url" 189 /> 190 </TextField.Root> 191 {pdsAddressHistory.length > 0 && ( 192 <View style={[a.flex_row, a.flex_wrap, a.mt_xs]}> 193 {pdsAddressHistory.map(uri => ( 194 <Button 195 key={uri} 196 variant="ghost" 197 color="primary" 198 label={uri} 199 style={[a.px_sm, a.py_xs, a.rounded_sm, a.gap_sm]} 200 onPress={() => setCustomAddress(uri)}> 201 <ButtonText>{uri}</ButtonText> 202 </Button> 203 ))} 204 </View> 205 )} 206 </View> 207 )} 208 209 <View style={[a.py_xs]}> 210 <Text 211 style={[t.atoms.text_contrast_medium, a.text_sm, a.leading_snug]}> 212 {isFirstTimeUser ? ( 213 <Trans> 214 If you're a developer, you can host your own server. 215 </Trans> 216 ) : ( 217 <Trans> 218 Bluesky is an open network where you can choose your hosting 219 provider. If you're a developer, you can host your own server. 220 </Trans> 221 )}{' '} 222 <InlineLinkText 223 label={_(msg`Learn more about self hosting your PDS.`)} 224 to="https://atproto.com/guides/self-hosting"> 225 <Trans>Learn more.</Trans> 226 </InlineLinkText> 227 </Text> 228 </View> 229 230 <View style={gtMobile && [a.flex_row, a.justify_end]}> 231 <Button 232 testID="doneBtn" 233 variant="solid" 234 color="primary" 235 size={platform({ 236 native: 'large', 237 web: 'small', 238 })} 239 onPress={() => control.close()} 240 label={_(msg`Done`)}> 241 <ButtonText> 242 <Trans>Done</Trans> 243 </ButtonText> 244 </Button> 245 </View> 246 </View> 247 </Dialog.ScrollableInner> 248 ) 249}