Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at feat/tealfm 273 lines 8.4 kB view raw
1import {useCallback, useEffect, useMemo, useRef, useState} from 'react' 2import {KeyboardAvoidingView} from 'react-native' 3import Animated, {FadeIn, LayoutAnimationConfig} from 'react-native-reanimated' 4import {type Did} from '@atproto/api' 5import {msg} from '@lingui/macro' 6import {useLingui} from '@lingui/react' 7import debounce from 'lodash.debounce' 8 9import {DEFAULT_SERVICE} from '#/lib/constants' 10import {logger} from '#/logger' 11import {resolvePdsServiceUrl} from '#/state/queries/resolve-identity' 12import {useServiceQuery} from '#/state/queries/service' 13import {type SessionAccount, useAgent, useSession} from '#/state/session' 14import {useLoggedOutView} from '#/state/shell/logged-out' 15import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' 16import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm' 17import {LoginForm} from '#/screens/Login/LoginForm' 18import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm' 19import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm' 20import {atoms as a, native} from '#/alf' 21import {ScreenTransition} from '#/components/ScreenTransition' 22import {useAnalytics} from '#/analytics' 23import {ChooseAccountForm} from './ChooseAccountForm' 24 25enum Forms { 26 Login, 27 ChooseAccount, 28 ForgotPassword, 29 SetNewPassword, 30 PasswordUpdated, 31} 32 33const OrderedForms = [ 34 Forms.ChooseAccount, 35 Forms.Login, 36 Forms.ForgotPassword, 37 Forms.SetNewPassword, 38 Forms.PasswordUpdated, 39] as const 40 41export const Login = ({onPressBack}: {onPressBack: () => void}) => { 42 const {_} = useLingui() 43 const failedAttemptCountRef = useRef(0) 44 const startTimeRef = useRef(Date.now()) 45 46 const agent = useAgent() 47 const {accounts} = useSession() 48 const {requestedAccountSwitchTo} = useLoggedOutView() 49 const requestedAccount = accounts.find( 50 acc => acc.did === requestedAccountSwitchTo, 51 ) 52 53 const [isResolvingService, setIsResolvingService] = useState(false) 54 const [error, setError] = useState<string>('') 55 const [serviceUrl, setServiceUrl] = useState<string | undefined>( 56 requestedAccount?.service, 57 ) 58 const [initialHandle, setInitialHandle] = useState( 59 requestedAccount?.handle || '', 60 ) 61 const [currentForm, setCurrentForm] = useState<Forms>( 62 requestedAccount 63 ? Forms.Login 64 : accounts.length 65 ? Forms.ChooseAccount 66 : Forms.Login, 67 ) 68 const [screenTransitionDirection, setScreenTransitionDirection] = useState< 69 'Forward' | 'Backward' 70 >('Forward') 71 72 const ax = useAnalytics() 73 const { 74 data: serviceDescription, 75 error: serviceError, 76 refetch: refetchService, 77 } = useServiceQuery(serviceUrl ?? '') 78 79 const onSelectAccount = (account?: SessionAccount) => { 80 if (account?.service) { 81 setServiceUrl(account.service) 82 } 83 setInitialHandle(account?.handle || '') 84 gotoForm(Forms.Login) 85 } 86 87 const gotoForm = (form: Forms) => { 88 setError('') 89 const index = OrderedForms.indexOf(currentForm) 90 const nextIndex = OrderedForms.indexOf(form) 91 setScreenTransitionDirection(index < nextIndex ? 'Forward' : 'Backward') 92 setCurrentForm(form) 93 } 94 95 useEffect(() => { 96 if (serviceError) { 97 setError( 98 _( 99 msg`Unable to contact your service. Please check your Internet connection.`, 100 ), 101 ) 102 logger.warn(`Failed to fetch service description for ${serviceUrl}`, { 103 error: String(serviceError), 104 }) 105 ax.metric('signin:hostingProviderFailedResolution', {}) 106 } else { 107 setError('') 108 } 109 }, [serviceError, serviceUrl, _]) 110 111 const resolveIdentity = useCallback( 112 async (identifier: string) => { 113 setIsResolvingService(true) 114 115 try { 116 const getDid = async () => { 117 if (identifier.startsWith('did:')) return identifier 118 else 119 return ( 120 await agent.resolveHandle({ 121 handle: identifier, 122 }) 123 ).data.did 124 } 125 126 const did = (await getDid()) as Did 127 const pdsUrl = await resolvePdsServiceUrl(did) 128 129 if (!pdsUrl) { 130 throw new Error(`No PDS service found in DID document for ${did}`) 131 } 132 133 if (pdsUrl.endsWith('.bsky.network')) { 134 setServiceUrl('https://bsky.social') 135 } else { 136 setServiceUrl(pdsUrl) 137 } 138 } catch (err) { 139 logger.error( 140 `Service auto-resolution failed: ${err instanceof Error ? err.message : String(err)}`, 141 ) 142 } finally { 143 setIsResolvingService(false) 144 } 145 }, 146 [agent], 147 ) 148 149 const debouncedResolveService = useMemo( 150 () => debounce(resolveIdentity, 400), 151 [resolveIdentity], 152 ) 153 154 const onPressForgotPassword = () => { 155 gotoForm(Forms.ForgotPassword) 156 ax.metric('signin:forgotPasswordPressed', {}) 157 } 158 159 const handlePressBack = () => { 160 onPressBack() 161 setScreenTransitionDirection('Backward') 162 ax.metric('signin:backPressed', { 163 failedAttemptsCount: failedAttemptCountRef.current, 164 }) 165 } 166 167 const onAttemptSuccess = () => { 168 ax.metric('signin:success', { 169 isUsingCustomProvider: serviceUrl !== DEFAULT_SERVICE, 170 timeTakenSeconds: Math.round((Date.now() - startTimeRef.current) / 1000), 171 failedAttemptsCount: failedAttemptCountRef.current, 172 }) 173 } 174 175 const onAttemptFailed = () => { 176 failedAttemptCountRef.current += 1 177 } 178 179 let content = null 180 let title = '' 181 let description = '' 182 183 switch (currentForm) { 184 case Forms.Login: 185 title = _(msg`Sign in`) 186 description = _(msg`Enter your username and password`) 187 content = ( 188 <LoginForm 189 error={error} 190 serviceUrl={serviceUrl} 191 serviceDescription={serviceDescription} 192 initialHandle={initialHandle} 193 setError={setError} 194 onAttemptFailed={onAttemptFailed} 195 onAttemptSuccess={onAttemptSuccess} 196 setServiceUrl={setServiceUrl} 197 onPressBack={() => 198 accounts.length ? gotoForm(Forms.ChooseAccount) : handlePressBack() 199 } 200 onPressForgotPassword={onPressForgotPassword} 201 onPressRetryConnect={refetchService} 202 debouncedResolveService={debouncedResolveService} 203 isResolvingService={isResolvingService} 204 /> 205 ) 206 break 207 case Forms.ChooseAccount: 208 title = _(msg`Sign in`) 209 description = _(msg`Select from an existing account`) 210 content = ( 211 <ChooseAccountForm 212 onSelectAccount={onSelectAccount} 213 onPressBack={handlePressBack} 214 /> 215 ) 216 break 217 case Forms.ForgotPassword: 218 title = _(msg`Forgot Password`) 219 description = _(msg`Let's get your password reset!`) 220 content = ( 221 <ForgotPasswordForm 222 error={error} 223 serviceUrl={serviceUrl ?? DEFAULT_SERVICE} 224 serviceDescription={serviceDescription} 225 setError={setError} 226 setServiceUrl={setServiceUrl} 227 onPressBack={() => gotoForm(Forms.Login)} 228 onEmailSent={() => gotoForm(Forms.SetNewPassword)} 229 /> 230 ) 231 break 232 case Forms.SetNewPassword: 233 title = _(msg`Forgot Password`) 234 description = _(msg`Let's get your password reset!`) 235 content = ( 236 <SetNewPasswordForm 237 error={error} 238 serviceUrl={serviceUrl ?? DEFAULT_SERVICE} 239 setError={setError} 240 onPressBack={() => gotoForm(Forms.ForgotPassword)} 241 onPasswordSet={() => gotoForm(Forms.PasswordUpdated)} 242 /> 243 ) 244 break 245 case Forms.PasswordUpdated: 246 title = _(msg`Password updated`) 247 description = _(msg`You can now sign in with your new password.`) 248 content = ( 249 <PasswordUpdatedForm onPressNext={() => gotoForm(Forms.Login)} /> 250 ) 251 break 252 } 253 254 return ( 255 <Animated.View style={a.flex_1} entering={native(FadeIn.duration(90))}> 256 <KeyboardAvoidingView testID="signIn" behavior="padding" style={a.flex_1}> 257 <LoggedOutLayout 258 leadin="" 259 title={title} 260 description={description} 261 scrollable> 262 <LayoutAnimationConfig skipEntering> 263 <ScreenTransition 264 key={currentForm} 265 direction={screenTransitionDirection}> 266 {content} 267 </ScreenTransition> 268 </LayoutAnimationConfig> 269 </LoggedOutLayout> 270 </KeyboardAvoidingView> 271 </Animated.View> 272 ) 273}