Bluesky app fork with some witchin' additions 💫

Compare changes

Choose any two refs to compare.

Changed files
+152 -116
src
+5 -5
src/Splash.web.tsx
··· 9 9 10 10 import {atoms as a} from '#/alf' 11 11 12 - const size = 100 13 - const ratio = 57 / 64 12 + const size = 125 13 + const ratio = 512 / 512 14 14 15 15 export function Splash() { 16 16 return ( 17 17 <View style={[a.fixed, a.inset_0, a.align_center, a.justify_center]}> 18 18 <Svg 19 19 fill="none" 20 - viewBox="0 0 64 57" 20 + viewBox="0 0 512 512" 21 21 style={[a.relative, {width: size, height: size * ratio, top: -50}]}> 22 22 <Path 23 - fill="#006AFF" 24 - d="M13.873 3.805C21.21 9.332 29.103 20.537 32 26.55v15.882c0-.338-.13.044-.41.867-1.512 4.456-7.418 21.847-20.923 7.944-7.111-7.32-3.819-14.64 9.125-16.85-7.405 1.264-15.73-.825-18.014-9.015C1.12 23.022 0 8.51 0 6.55 0-3.268 8.579-.182 13.873 3.805ZM50.127 3.805C42.79 9.332 34.897 20.537 32 26.55v15.882c0-.338.13.044.41.867 1.512 4.456 7.418 21.847 20.923 7.944 7.111-7.32 3.819-14.64-9.125-16.85 7.405 1.264 15.73-.825 18.014-9.015C62.88 23.022 64 8.51 64 6.55c0-9.818-8.578-6.732-13.873-2.745Z" 23 + fill="#ED5345" 24 + d="M374.473 57.7173C367.666 50.7995 357.119 49.1209 348.441 53.1659C347.173 53.7567 342.223 56.0864 334.796 59.8613C326.32 64.1696 314.568 70.3869 301.394 78.0596C275.444 93.1728 242.399 114.83 218.408 139.477C185.983 172.786 158.719 225.503 140.029 267.661C130.506 289.144 122.878 308.661 117.629 322.81C116.301 326.389 115.124 329.63 114.104 332.478C87.1783 336.42 64.534 341.641 47.5078 348.101C37.6493 351.84 28.3222 356.491 21.0573 362.538C13.8818 368.511 6.00003 378.262 6.00003 391.822C6.00014 403.222 11.8738 411.777 17.4566 417.235C23.0009 422.655 29.9593 426.793 36.871 430.062C50.8097 436.653 69.5275 441.988 90.8362 446.249C133.828 454.846 192.21 460 256.001 460C319.79 460 378.172 454.846 421.164 446.249C442.472 441.988 461.19 436.653 475.129 430.062C482.041 426.793 488.999 422.655 494.543 417.235C500.039 411.862 505.817 403.489 505.996 392.353L506 391.822L505.995 391.188C505.754 377.959 498.012 368.417 490.945 362.534C483.679 356.485 474.35 351.835 464.491 348.095C446.749 341.366 422.906 335.982 394.476 331.987C393.6 330.57 392.633 328.995 391.595 327.273C386.477 318.777 379.633 306.842 372.737 293.115C358.503 264.781 345.757 232.098 344.756 206.636C343.87 184.121 351.638 154.087 360.819 127.789C365.27 115.041 369.795 103.877 373.207 95.9072C374.909 91.9309 376.325 88.7712 377.302 86.6328C377.79 85.5645 378.167 84.7524 378.416 84.2224C378.54 83.9579 378.632 83.7635 378.69 83.643C378.718 83.5829 378.739 83.5411 378.75 83.5181C378.753 83.5108 378.756 83.5049 378.757 83.5015C382.909 74.8634 381.196 64.5488 374.473 57.7173Z" 25 25 /> 26 26 </Svg> 27 27 </View>
+57 -94
src/components/dialogs/ServerInput.tsx
··· 12 12 import {Admonition} from '#/components/Admonition' 13 13 import {Button, ButtonText} from '#/components/Button' 14 14 import * as Dialog from '#/components/Dialog' 15 - import * as SegmentedControl from '#/components/forms/SegmentedControl' 16 15 import * as TextField from '#/components/forms/TextField' 17 16 import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' 18 17 import {InlineLinkText} from '#/components/Link' 19 18 import {Text} from '#/components/Typography' 20 - 21 - type SegmentedControlOptions = typeof BSKY_SERVICE | 'custom' 22 19 23 20 export function ServerInputDialog({ 24 21 control, ··· 31 28 const formRef = useRef<DialogInnerRef>(null) 32 29 33 30 // persist these options between dialog open/close 34 - const [fixedOption, setFixedOption] = 35 - useState<SegmentedControlOptions>(BSKY_SERVICE) 36 31 const [previousCustomAddress, setPreviousCustomAddress] = useState('') 37 32 38 33 const onClose = useCallback(() => { ··· 44 39 } 45 40 } 46 41 logger.metric('signin:hostingProviderPressed', { 47 - hostingProviderDidChange: fixedOption !== BSKY_SERVICE, 42 + hostingProviderDidChange: false, // stubbed for PDS auto-resolution 48 43 }) 49 - }, [onSelect, fixedOption]) 44 + }, [onSelect]) 50 45 51 46 return ( 52 47 <Dialog.Outer ··· 59 54 <Dialog.Handle /> 60 55 <DialogInner 61 56 formRef={formRef} 62 - fixedOption={fixedOption} 63 - setFixedOption={setFixedOption} 64 57 initialCustomAddress={previousCustomAddress} 65 58 /> 66 59 </Dialog.Outer> ··· 71 64 72 65 function DialogInner({ 73 66 formRef, 74 - fixedOption, 75 - setFixedOption, 76 67 initialCustomAddress, 77 68 }: { 78 69 formRef: React.Ref<DialogInnerRef> 79 - fixedOption: SegmentedControlOptions 80 - setFixedOption: (opt: SegmentedControlOptions) => void 81 70 initialCustomAddress: string 82 71 }) { 83 72 const control = Dialog.useDialogContext() ··· 94 83 formRef, 95 84 () => ({ 96 85 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 86 + let url = customAddress.trim().toLowerCase() 87 + if (!url) { 88 + return null 105 89 } 106 90 if (!url.startsWith('http://') && !url.startsWith('https://')) { 107 91 if (url === 'localhost' || url.startsWith('localhost:')) { ··· 111 95 } 112 96 } 113 97 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 - } 98 + if (!pdsAddressHistory.includes(url)) { 99 + const newHistory = [url, ...pdsAddressHistory.slice(0, 4)] 100 + setPdsAddressHistory(newHistory) 101 + persisted.write('pdsAddressHistory', newHistory) 120 102 } 121 103 122 104 return url 123 105 }, 124 106 }), 125 - [customAddress, fixedOption, pdsAddressHistory], 107 + [customAddress, pdsAddressHistory], 126 108 ) 127 109 128 110 const isFirstTimeUser = accounts.length === 0 ··· 136 118 <Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}> 137 119 <Trans>Choose your account provider</Trans> 138 120 </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 121 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> 122 + {isFirstTimeUser && ( 123 + <Admonition type="tip"> 124 + <Trans> 125 + Bluesky is an open network where you can choose your own provider. 126 + If you're new here, we recommend sticking with the default Bluesky 127 + Social option. 128 + </Trans> 129 + </Admonition> 172 130 )} 173 131 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 - )} 132 + <View 133 + style={[ 134 + a.border, 135 + t.atoms.border_contrast_low, 136 + a.rounded_sm, 137 + a.px_md, 138 + a.py_md, 139 + ]}> 140 + <TextField.LabelText nativeID="address-input-label"> 141 + <Trans>Server address</Trans> 142 + </TextField.LabelText> 143 + <TextField.Root> 144 + <TextField.Icon icon={Globe} /> 145 + <Dialog.Input 146 + testID="customServerTextInput" 147 + value={customAddress} 148 + onChangeText={setCustomAddress} 149 + label="my-server.com" 150 + accessibilityLabelledBy="address-input-label" 151 + autoCapitalize="none" 152 + keyboardType="url" 153 + /> 154 + </TextField.Root> 155 + {pdsAddressHistory.length > 0 && ( 156 + <View style={[a.flex_row, a.flex_wrap, a.mt_xs]}> 157 + {pdsAddressHistory.map(uri => ( 158 + <Button 159 + key={uri} 160 + variant="ghost" 161 + color="primary" 162 + label={uri} 163 + style={[a.px_sm, a.py_xs, a.rounded_sm, a.gap_sm]} 164 + onPress={() => setCustomAddress(uri)}> 165 + <ButtonText>{uri}</ButtonText> 166 + </Button> 167 + ))} 168 + </View> 169 + )} 170 + </View> 208 171 209 172 <View style={[a.py_xs]}> 210 173 <Text
+7 -7
src/components/forms/HostingProvider.tsx
··· 18 18 onOpenDialog, 19 19 minimal, 20 20 }: { 21 - serviceUrl: string 21 + serviceUrl?: string | undefined 22 22 onSelectServiceUrl: (provider: string) => void 23 23 onOpenDialog?: () => void 24 24 minimal?: boolean ··· 26 26 const serverInputControl = useDialogControl() 27 27 const t = useTheme() 28 28 const {_} = useLingui() 29 + const serviceProviderLabel = 30 + serviceUrl === undefined ? _(msg`Automatic`) : toNiceDomain(serviceUrl) 29 31 30 32 const onPressSelectService = React.useCallback(() => { 31 33 Keyboard.dismiss() ··· 45 47 <Trans>You are creating an account on</Trans> 46 48 </Text> 47 49 <Button 48 - label={toNiceDomain(serviceUrl)} 50 + label={serviceProviderLabel} 49 51 accessibilityHint={_(msg`Changes hosting provider`)} 50 52 onPress={onPressSelectService} 51 53 variant="ghost" ··· 56 58 {marginHorizontal: tokens.space.xs * -1}, 57 59 {paddingVertical: 0}, 58 60 ]}> 59 - <ButtonText style={[a.text_sm]}> 60 - {toNiceDomain(serviceUrl)} 61 - </ButtonText> 61 + <ButtonText style={[a.text_sm]}>{serviceProviderLabel}</ButtonText> 62 62 <ButtonIcon icon={PencilIcon} /> 63 63 </Button> 64 64 </View> 65 65 ) : ( 66 66 <Button 67 67 testID="selectServiceButton" 68 - label={toNiceDomain(serviceUrl)} 68 + label={serviceProviderLabel} 69 69 accessibilityHint={_(msg`Changes hosting provider`)} 70 70 variant="solid" 71 71 color="secondary" ··· 94 94 } 95 95 /> 96 96 </View> 97 - <Text style={[a.text_md]}>{toNiceDomain(serviceUrl)}</Text> 97 + <Text style={[a.text_md]}>{serviceProviderLabel}</Text> 98 98 <View 99 99 style={[ 100 100 a.rounded_sm,
+27 -2
src/screens/Login/LoginForm.tsx
··· 48 48 onPressForgotPassword, 49 49 onAttemptSuccess, 50 50 onAttemptFailed, 51 + debouncedResolveService, 52 + isResolvingService, 51 53 }: { 52 54 error: string 53 - serviceUrl: string 55 + serviceUrl?: string | undefined 54 56 serviceDescription: ServiceDescription | undefined 55 57 initialHandle: string 56 58 setError: (v: string) => void ··· 60 62 onPressForgotPassword: () => void 61 63 onAttemptSuccess: () => void 62 64 onAttemptFailed: () => void 65 + debouncedResolveService: (identifier: string) => void 66 + isResolvingService: boolean 63 67 }) => { 64 68 const t = useTheme() 65 69 const [isProcessing, setIsProcessing] = useState<boolean>(false) ··· 97 101 98 102 if (!password) { 99 103 setError(_(msg`Please enter your password`)) 104 + return 105 + } 106 + 107 + if (!serviceUrl) { 108 + setError(_(msg`Please enter hosting provider URL`)) 100 109 return 101 110 } 102 111 ··· 182 191 <View> 183 192 <TextField.LabelText> 184 193 <Trans>Hosting provider</Trans> 194 + {isResolvingService && ( 195 + <ActivityIndicator 196 + size={12} 197 + color={t.palette.contrast_500} 198 + style={a.ml_sm} 199 + /> 200 + )} 185 201 </TextField.LabelText> 186 202 <HostingProvider 187 203 serviceUrl={serviceUrl} ··· 209 225 defaultValue={initialHandle || ''} 210 226 onChangeText={v => { 211 227 identifierValueRef.current = v 228 + // Trigger PDS auto-resolution for handles/DIDs 229 + const id = v.trim() 230 + if (!id) return 231 + if ( 232 + id.startsWith('did:') || 233 + (id.includes('.') && !id.includes('@')) 234 + ) { 235 + debouncedResolveService(id) 236 + } 212 237 }} 213 238 onSubmitEditing={() => { 214 239 passwordRef.current?.focus() ··· 333 358 <Trans>Retry</Trans> 334 359 </ButtonText> 335 360 </Button> 336 - ) : !serviceDescription ? ( 361 + ) : !serviceDescription && serviceUrl !== undefined ? ( 337 362 <> 338 363 <ActivityIndicator color={t.palette.primary_500} /> 339 364 <Text style={[t.atoms.text_contrast_high, a.pl_md]}>
+56 -8
src/screens/Login/index.tsx
··· 1 - import {useEffect, useRef, useState} from 'react' 1 + import {useCallback,useEffect, useMemo, useRef, useState} from 'react' 2 2 import {KeyboardAvoidingView} from 'react-native' 3 3 import Animated, {FadeIn, LayoutAnimationConfig} from 'react-native-reanimated' 4 + import {type Did} from '@atproto/api' 4 5 import {msg} from '@lingui/macro' 5 6 import {useLingui} from '@lingui/react' 7 + import debounce from 'lodash.debounce' 6 8 7 9 import {DEFAULT_SERVICE} from '#/lib/constants' 8 10 import {logEvent} from '#/lib/statsig/statsig' 9 11 import {logger} from '#/logger' 12 + import {resolvePdsServiceUrl} from '#/state/queries/resolve-identity' 10 13 import {useServiceQuery} from '#/state/queries/service' 11 - import {type SessionAccount, useSession} from '#/state/session' 14 + import {type SessionAccount, useAgent, useSession} from '#/state/session' 12 15 import {useLoggedOutView} from '#/state/shell/logged-out' 13 16 import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' 14 17 import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm' ··· 40 43 const failedAttemptCountRef = useRef(0) 41 44 const startTimeRef = useRef(Date.now()) 42 45 46 + const agent = useAgent() 43 47 const {accounts} = useSession() 44 48 const {requestedAccountSwitchTo} = useLoggedOutView() 45 49 const requestedAccount = accounts.find( 46 50 acc => acc.did === requestedAccountSwitchTo, 47 51 ) 48 52 49 - const [error, setError] = useState('') 50 - const [serviceUrl, setServiceUrl] = useState( 51 - requestedAccount?.service || DEFAULT_SERVICE, 53 + const [isResolvingService, setIsResolvingService] = useState(false) 54 + const [error, setError] = useState<string>('') 55 + const [serviceUrl, setServiceUrl] = useState<string | undefined>( 56 + requestedAccount?.service, 52 57 ) 53 58 const [initialHandle, setInitialHandle] = useState( 54 59 requestedAccount?.handle || '', ··· 68 73 data: serviceDescription, 69 74 error: serviceError, 70 75 refetch: refetchService, 71 - } = useServiceQuery(serviceUrl) 76 + } = useServiceQuery(serviceUrl ?? '') 72 77 73 78 const onSelectAccount = (account?: SessionAccount) => { 74 79 if (account?.service) { ··· 102 107 } 103 108 }, [serviceError, serviceUrl, _]) 104 109 110 + const resolveIdentity = useCallback( 111 + async (identifier: string) => { 112 + setIsResolvingService(true) 113 + 114 + try { 115 + const getDid = async () => { 116 + if (identifier.startsWith('did:')) return identifier 117 + else 118 + return ( 119 + await agent.resolveHandle({ 120 + handle: identifier, 121 + }) 122 + ).data.did 123 + } 124 + 125 + const did = (await getDid()) as Did 126 + const pdsUrl = await resolvePdsServiceUrl(did) 127 + 128 + if (!pdsUrl) { 129 + throw new Error(`No PDS service found in DID document for ${did}`) 130 + } 131 + 132 + if (pdsUrl.endsWith('.bsky.network')) { 133 + setServiceUrl('https://bsky.social') 134 + } else { 135 + setServiceUrl(pdsUrl) 136 + } 137 + } catch (err) { 138 + logger.error(`Service auto-resolution failed: ${err}`) 139 + } finally { 140 + setIsResolvingService(false) 141 + } 142 + }, 143 + [agent], 144 + ) 145 + 146 + const debouncedResolveService = useMemo( 147 + () => debounce(resolveIdentity, 800), 148 + [resolveIdentity], 149 + ) 150 + 105 151 const onPressForgotPassword = () => { 106 152 gotoForm(Forms.ForgotPassword) 107 153 logEvent('signin:forgotPasswordPressed', {}) ··· 150 196 } 151 197 onPressForgotPassword={onPressForgotPassword} 152 198 onPressRetryConnect={refetchService} 199 + debouncedResolveService={debouncedResolveService} 200 + isResolvingService={isResolvingService} 153 201 /> 154 202 ) 155 203 break ··· 169 217 content = ( 170 218 <ForgotPasswordForm 171 219 error={error} 172 - serviceUrl={serviceUrl} 220 + serviceUrl={serviceUrl ?? DEFAULT_SERVICE} 173 221 serviceDescription={serviceDescription} 174 222 setError={setError} 175 223 setServiceUrl={setServiceUrl} ··· 184 232 content = ( 185 233 <SetNewPasswordForm 186 234 error={error} 187 - serviceUrl={serviceUrl} 235 + serviceUrl={serviceUrl ?? DEFAULT_SERVICE} 188 236 setError={setError} 189 237 onPressBack={() => gotoForm(Forms.ForgotPassword)} 190 238 onPasswordSet={() => gotoForm(Forms.PasswordUpdated)}