Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor ChangeHandle modal (#1929)

* Refactor ChangeHandle to use new methods

* Better telemetry

* Remove unused logic

* Remove caching

* Add error message

* Persist service changes, don't fall back on change handle

authored by

Eric Bailey and committed by
GitHub
a652b52b e6efeea7

+154 -101
+34 -1
src/state/queries/handle.ts
··· 1 1 import React from 'react' 2 - import {useQueryClient} from '@tanstack/react-query' 2 + import {useQueryClient, useMutation} from '@tanstack/react-query' 3 3 4 4 import {useSession} from '#/state/session' 5 5 6 6 const fetchHandleQueryKey = (handleOrDid: string) => ['handle', handleOrDid] 7 + const fetchDidQueryKey = (handleOrDid: string) => ['did', handleOrDid] 7 8 8 9 export function useFetchHandle() { 9 10 const {agent} = useSession() ··· 23 24 [agent, queryClient], 24 25 ) 25 26 } 27 + 28 + export function useUpdateHandleMutation() { 29 + const {agent} = useSession() 30 + 31 + return useMutation({ 32 + mutationFn: async ({handle}: {handle: string}) => { 33 + await agent.updateHandle({handle}) 34 + }, 35 + }) 36 + } 37 + 38 + export function useFetchDid() { 39 + const {agent} = useSession() 40 + const queryClient = useQueryClient() 41 + 42 + return React.useCallback( 43 + async (handleOrDid: string) => { 44 + return queryClient.fetchQuery({ 45 + queryKey: fetchDidQueryKey(handleOrDid), 46 + queryFn: async () => { 47 + let identifier = handleOrDid 48 + if (!identifier.startsWith('did:')) { 49 + const res = await agent.resolveHandle({handle: identifier}) 50 + identifier = res.data.did 51 + } 52 + return identifier 53 + }, 54 + }) 55 + }, 56 + [agent, queryClient], 57 + ) 58 + }
+16
src/state/queries/service.ts
··· 1 + import {useQuery} from '@tanstack/react-query' 2 + 3 + import {useSession} from '#/state/session' 4 + 5 + export const RQKEY = (serviceUrl: string) => ['service', serviceUrl] 6 + 7 + export function useServiceQuery() { 8 + const {agent} = useSession() 9 + return useQuery({ 10 + queryKey: RQKEY(agent.service.toString()), 11 + queryFn: async () => { 12 + const res = await agent.com.atproto.server.describeServer() 13 + return res.data 14 + }, 15 + }) 16 + }
+27 -12
src/state/session/index.tsx
··· 14 14 agent: BskyAgent 15 15 isInitialLoad: boolean 16 16 isSwitchingAccounts: boolean 17 - accounts: persisted.PersistedAccount[] 18 - currentAccount: persisted.PersistedAccount | undefined 17 + accounts: SessionAccount[] 18 + currentAccount: SessionAccount | undefined 19 19 } 20 20 export type StateContext = SessionState & { 21 21 hasSession: boolean ··· 70 70 }) 71 71 72 72 function createPersistSessionHandler( 73 - account: persisted.PersistedAccount, 73 + account: SessionAccount, 74 74 persistSessionCallback: (props: { 75 75 expired: boolean 76 - refreshedAccount: persisted.PersistedAccount 76 + refreshedAccount: SessionAccount 77 77 }) => void, 78 78 ): AtpPersistSessionHandler { 79 79 return function persistSession(event, session) { 80 80 const expired = !(event === 'create' || event === 'update') 81 - const refreshedAccount = { 81 + const refreshedAccount: SessionAccount = { 82 82 service: account.service, 83 83 did: session?.did || account.did, 84 84 handle: session?.handle || account.handle, ··· 128 128 ) 129 129 130 130 const upsertAccount = React.useCallback( 131 - (account: persisted.PersistedAccount, expired = false) => { 131 + (account: SessionAccount, expired = false) => { 132 132 setStateAndPersist(s => { 133 133 return { 134 134 ...s, ··· 164 164 throw new Error(`session: createAccount failed to establish a session`) 165 165 } 166 166 167 - const account: persisted.PersistedAccount = { 168 - service, 167 + const account: SessionAccount = { 168 + service: agent.service.toString(), 169 169 did: agent.session.did, 170 170 handle: agent.session.handle, 171 171 email: agent.session.email!, // TODO this is always defined? ··· 215 215 throw new Error(`session: login failed to establish a session`) 216 216 } 217 217 218 - const account: persisted.PersistedAccount = { 219 - service, 218 + const account: SessionAccount = { 219 + service: agent.service.toString(), 220 220 did: agent.session.did, 221 221 handle: agent.session.handle, 222 222 email: agent.session.email!, // TODO this is always defined? ··· 293 293 }), 294 294 ) 295 295 296 + if (!agent.session) { 297 + throw new Error(`session: initSession failed to establish a session`) 298 + } 299 + 300 + // ensure changes in handle/email etc are captured on reload 301 + const freshAccount: SessionAccount = { 302 + service: agent.service.toString(), 303 + did: agent.session.did, 304 + handle: agent.session.handle, 305 + email: agent.session.email!, // TODO this is always defined? 306 + emailConfirmed: agent.session.emailConfirmed || false, 307 + refreshJwt: agent.session.refreshJwt, 308 + accessJwt: agent.session.accessJwt, 309 + } 310 + 296 311 setState(s => ({...s, agent})) 297 - upsertAccount(account) 298 - emitSessionLoaded(account, agent) 312 + upsertAccount(freshAccount) 313 + emitSessionLoaded(freshAccount, agent) 299 314 }, 300 315 [upsertAccount], 301 316 )
+77 -88
src/view/com/modals/ChangeHandle.tsx
··· 1 1 import React, {useState} from 'react' 2 2 import Clipboard from '@react-native-clipboard/clipboard' 3 + import {ComAtprotoServerDescribeServer} from '@atproto/api' 3 4 import * as Toast from '../util/Toast' 4 5 import { 5 6 ActivityIndicator, ··· 13 14 import {Button} from '../util/forms/Button' 14 15 import {SelectableBtn} from '../util/forms/SelectableBtn' 15 16 import {ErrorMessage} from '../util/error/ErrorMessage' 16 - import {useStores} from 'state/index' 17 - import {ServiceDescription} from 'state/models/session' 18 17 import {s} from 'lib/styles' 19 18 import {createFullHandle, makeValidHandle} from 'lib/strings/handles' 20 19 import {usePalette} from 'lib/hooks/usePalette' ··· 25 24 import {Trans, msg} from '@lingui/macro' 26 25 import {useLingui} from '@lingui/react' 27 26 import {useModalControls} from '#/state/modals' 27 + import {useServiceQuery} from '#/state/queries/service' 28 + import {useUpdateHandleMutation, useFetchDid} from '#/state/queries/handle' 29 + import {useSession, useSessionApi, SessionAccount} from '#/state/session' 28 30 29 31 export const snapPoints = ['100%'] 30 32 31 - export function Component({onChanged}: {onChanged: () => void}) { 32 - const store = useStores() 33 - const [error, setError] = useState<string>('') 33 + export type Props = {onChanged: () => void} 34 + 35 + export function Component(props: Props) { 36 + const {currentAccount} = useSession() 37 + const { 38 + isLoading, 39 + data: serviceInfo, 40 + error: serviceInfoError, 41 + } = useServiceQuery() 42 + 43 + return isLoading || !currentAccount ? ( 44 + <View style={{padding: 18}}> 45 + <ActivityIndicator /> 46 + </View> 47 + ) : serviceInfoError || !serviceInfo ? ( 48 + <ErrorMessage message={cleanError(serviceInfoError)} /> 49 + ) : ( 50 + <Inner 51 + {...props} 52 + currentAccount={currentAccount} 53 + serviceInfo={serviceInfo} 54 + /> 55 + ) 56 + } 57 + 58 + export function Inner({ 59 + currentAccount, 60 + serviceInfo, 61 + onChanged, 62 + }: Props & { 63 + currentAccount: SessionAccount 64 + serviceInfo: ComAtprotoServerDescribeServer.OutputSchema 65 + }) { 66 + const {_} = useLingui() 34 67 const pal = usePalette('default') 35 68 const {track} = useAnalytics() 36 - const {_} = useLingui() 69 + const {updateCurrentAccount} = useSessionApi() 37 70 const {closeModal} = useModalControls() 71 + const {mutateAsync: updateHandle, isPending: isUpdateHandlePending} = 72 + useUpdateHandleMutation() 38 73 39 - const [isProcessing, setProcessing] = useState<boolean>(false) 40 - const [retryDescribeTrigger, setRetryDescribeTrigger] = React.useState<any>( 41 - {}, 42 - ) 43 - const [serviceDescription, setServiceDescription] = React.useState< 44 - ServiceDescription | undefined 45 - >(undefined) 46 - const [userDomain, setUserDomain] = React.useState<string>('') 74 + const [error, setError] = useState<string>('') 75 + 47 76 const [isCustom, setCustom] = React.useState<boolean>(false) 48 77 const [handle, setHandle] = React.useState<string>('') 49 78 const [canSave, setCanSave] = React.useState<boolean>(false) 50 79 51 - // init 52 - // = 53 - React.useEffect(() => { 54 - let aborted = false 55 - setError('') 56 - setServiceDescription(undefined) 57 - setProcessing(true) 58 - 59 - // load the service description so we can properly provision handles 60 - store.session.describeService(String(store.agent.service)).then( 61 - desc => { 62 - if (aborted) { 63 - return 64 - } 65 - setServiceDescription(desc) 66 - setUserDomain(desc.availableUserDomains[0]) 67 - setProcessing(false) 68 - }, 69 - err => { 70 - if (aborted) { 71 - return 72 - } 73 - setProcessing(false) 74 - logger.warn( 75 - `Failed to fetch service description for ${String( 76 - store.agent.service, 77 - )}`, 78 - {error: err}, 79 - ) 80 - setError( 81 - 'Unable to contact your service. Please check your Internet connection.', 82 - ) 83 - }, 84 - ) 85 - return () => { 86 - aborted = true 87 - } 88 - }, [store.agent.service, store.session, retryDescribeTrigger]) 80 + const userDomain = serviceInfo.availableUserDomains?.[0] 89 81 90 82 // events 91 83 // = 92 84 const onPressCancel = React.useCallback(() => { 93 85 closeModal() 94 86 }, [closeModal]) 95 - const onPressRetryConnect = React.useCallback( 96 - () => setRetryDescribeTrigger({}), 97 - [setRetryDescribeTrigger], 98 - ) 99 87 const onToggleCustom = React.useCallback(() => { 100 88 // toggle between a provided domain vs a custom one 101 89 setHandle('') ··· 106 94 ) 107 95 }, [setCustom, isCustom, track]) 108 96 const onPressSave = React.useCallback(async () => { 109 - setError('') 110 - setProcessing(true) 97 + if (!userDomain) { 98 + logger.error(`ChangeHandle: userDomain is undefined`, { 99 + service: serviceInfo, 100 + }) 101 + setError(`The service you've selected has no domains configured.`) 102 + return 103 + } 104 + 111 105 try { 112 106 track('EditHandle:SetNewHandle') 113 107 const newHandle = isCustom ? handle : createFullHandle(handle, userDomain) 114 108 logger.debug(`Updating handle to ${newHandle}`) 115 - await store.agent.updateHandle({ 109 + await updateHandle({ 110 + handle: newHandle, 111 + }) 112 + updateCurrentAccount({ 116 113 handle: newHandle, 117 114 }) 118 115 closeModal() ··· 121 118 setError(cleanError(err)) 122 119 logger.error('Failed to update handle', {handle, error: err}) 123 120 } finally { 124 - setProcessing(false) 125 121 } 126 122 }, [ 127 123 setError, 128 - setProcessing, 129 124 handle, 130 125 userDomain, 131 - store, 132 126 isCustom, 133 127 onChanged, 134 128 track, 135 129 closeModal, 130 + updateCurrentAccount, 131 + updateHandle, 132 + serviceInfo, 136 133 ]) 137 134 138 135 // rendering ··· 159 156 <Trans>Change Handle</Trans> 160 157 </Text> 161 158 <View style={styles.titleRight}> 162 - {isProcessing ? ( 159 + {isUpdateHandlePending ? ( 163 160 <ActivityIndicator /> 164 - ) : error && !serviceDescription ? ( 165 - <TouchableOpacity 166 - testID="retryConnectButton" 167 - onPress={onPressRetryConnect} 168 - accessibilityRole="button" 169 - accessibilityLabel={_(msg`Retry change handle`)} 170 - accessibilityHint={`Retries handle change to ${handle}`}> 171 - <Text type="xl-bold" style={[pal.link, s.pr5]}> 172 - Retry 173 - </Text> 174 - </TouchableOpacity> 175 161 ) : canSave ? ( 176 162 <TouchableOpacity 177 163 onPress={onPressSave} ··· 194 180 195 181 {isCustom ? ( 196 182 <CustomHandleForm 183 + currentAccount={currentAccount} 197 184 handle={handle} 198 - isProcessing={isProcessing} 185 + isProcessing={isUpdateHandlePending} 199 186 canSave={canSave} 200 187 onToggleCustom={onToggleCustom} 201 188 setHandle={setHandle} ··· 206 193 <ProvidedHandleForm 207 194 handle={handle} 208 195 userDomain={userDomain} 209 - isProcessing={isProcessing} 196 + isProcessing={isUpdateHandlePending} 210 197 onToggleCustom={onToggleCustom} 211 198 setHandle={setHandle} 212 199 setCanSave={setCanSave} ··· 297 284 * The form for using a custom domain 298 285 */ 299 286 function CustomHandleForm({ 287 + currentAccount, 300 288 handle, 301 289 canSave, 302 290 isProcessing, ··· 305 293 onPressSave, 306 294 setCanSave, 307 295 }: { 296 + currentAccount: SessionAccount 308 297 handle: string 309 298 canSave: boolean 310 299 isProcessing: boolean ··· 313 302 onPressSave: () => void 314 303 setCanSave: (v: boolean) => void 315 304 }) { 316 - const store = useStores() 317 305 const pal = usePalette('default') 318 306 const palSecondary = usePalette('secondary') 319 307 const palError = usePalette('error') ··· 322 310 const [isVerifying, setIsVerifying] = React.useState(false) 323 311 const [error, setError] = React.useState<string>('') 324 312 const [isDNSForm, setDNSForm] = React.useState<boolean>(true) 313 + const fetchDid = useFetchDid() 325 314 // events 326 315 // = 327 316 const onPressCopy = React.useCallback(() => { 328 - Clipboard.setString(isDNSForm ? `did=${store.me.did}` : store.me.did) 317 + Clipboard.setString( 318 + isDNSForm ? `did=${currentAccount.did}` : currentAccount.did, 319 + ) 329 320 Toast.show('Copied to clipboard') 330 - }, [store.me.did, isDNSForm]) 321 + }, [currentAccount, isDNSForm]) 331 322 const onChangeHandle = React.useCallback( 332 323 (v: string) => { 333 324 setHandle(v) ··· 342 333 try { 343 334 setIsVerifying(true) 344 335 setError('') 345 - const res = await store.agent.com.atproto.identity.resolveHandle({ 346 - handle, 347 - }) 348 - if (res.data.did === store.me.did) { 336 + const did = await fetchDid(handle) 337 + if (did === currentAccount.did) { 349 338 setCanSave(true) 350 339 } else { 351 - setError(`Incorrect DID returned (got ${res.data.did})`) 340 + setError(`Incorrect DID returned (got ${did})`) 352 341 } 353 342 } catch (err: any) { 354 343 setError(cleanError(err)) ··· 358 347 } 359 348 }, [ 360 349 handle, 361 - store.me.did, 350 + currentAccount, 362 351 setIsVerifying, 363 352 setCanSave, 364 353 setError, 365 354 canSave, 366 355 onPressSave, 367 - store.agent, 356 + fetchDid, 368 357 ]) 369 358 370 359 // rendering ··· 442 431 </Text> 443 432 <View style={[styles.dnsValue]}> 444 433 <Text type="mono" style={[styles.monoText, pal.text]}> 445 - did={store.me.did} 434 + did={currentAccount.did} 446 435 </Text> 447 436 </View> 448 437 </View> ··· 472 461 <View style={[styles.valueContainer, pal.btn]}> 473 462 <View style={[styles.dnsValue]}> 474 463 <Text type="mono" style={[styles.monoText, pal.text]}> 475 - {store.me.did} 464 + {currentAccount.did} 476 465 </Text> 477 466 </View> 478 467 </View>