Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

[Perf - part 1] Hoist service config query (#8812)

authored by samuel.fm and committed by

GitHub 7a334362 b2c56cbd

+119 -88
+15 -12
src/App.native.tsx
··· 29 import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance' 30 import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' 31 import {Provider as DialogStateProvider} from '#/state/dialogs' 32 import {listenSessionDropped} from '#/state/events' 33 import { 34 beginResolveGeolocation, ··· 155 <MutedThreadsProvider> 156 <ProgressGuideProvider> 157 <ServiceAccountManager> 158 - <HideBottomBarBorderProvider> 159 - <GestureHandlerRootView 160 - style={s.h100pct}> 161 - <GlobalGestureEventsProvider> 162 - <IntentDialogProvider> 163 - <TestCtrls /> 164 - <Shell /> 165 - <NuxDialogs /> 166 - </IntentDialogProvider> 167 - </GlobalGestureEventsProvider> 168 - </GestureHandlerRootView> 169 - </HideBottomBarBorderProvider> 170 </ServiceAccountManager> 171 </ProgressGuideProvider> 172 </MutedThreadsProvider>
··· 29 import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance' 30 import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' 31 import {Provider as DialogStateProvider} from '#/state/dialogs' 32 + import {Provider as EmailVerificationProvider} from '#/state/email-verification' 33 import {listenSessionDropped} from '#/state/events' 34 import { 35 beginResolveGeolocation, ··· 156 <MutedThreadsProvider> 157 <ProgressGuideProvider> 158 <ServiceAccountManager> 159 + <EmailVerificationProvider> 160 + <HideBottomBarBorderProvider> 161 + <GestureHandlerRootView 162 + style={s.h100pct}> 163 + <GlobalGestureEventsProvider> 164 + <IntentDialogProvider> 165 + <TestCtrls /> 166 + <Shell /> 167 + <NuxDialogs /> 168 + </IntentDialogProvider> 169 + </GlobalGestureEventsProvider> 170 + </GestureHandlerRootView> 171 + </HideBottomBarBorderProvider> 172 + </EmailVerificationProvider> 173 </ServiceAccountManager> 174 </ProgressGuideProvider> 175 </MutedThreadsProvider>
+9 -6
src/App.web.tsx
··· 18 import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance' 19 import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' 20 import {Provider as DialogStateProvider} from '#/state/dialogs' 21 import {listenSessionDropped} from '#/state/events' 22 import { 23 beginResolveGeolocation, ··· 136 <SafeAreaProvider> 137 <ProgressGuideProvider> 138 <ServiceConfigProvider> 139 - <HideBottomBarBorderProvider> 140 - <IntentDialogProvider> 141 - <Shell /> 142 - <NuxDialogs /> 143 - </IntentDialogProvider> 144 - </HideBottomBarBorderProvider> 145 </ServiceConfigProvider> 146 </ProgressGuideProvider> 147 </SafeAreaProvider>
··· 18 import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance' 19 import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' 20 import {Provider as DialogStateProvider} from '#/state/dialogs' 21 + import {Provider as EmailVerificationProvider} from '#/state/email-verification' 22 import {listenSessionDropped} from '#/state/events' 23 import { 24 beginResolveGeolocation, ··· 137 <SafeAreaProvider> 138 <ProgressGuideProvider> 139 <ServiceConfigProvider> 140 + <EmailVerificationProvider> 141 + <HideBottomBarBorderProvider> 142 + <IntentDialogProvider> 143 + <Shell /> 144 + <NuxDialogs /> 145 + </IntentDialogProvider> 146 + </HideBottomBarBorderProvider> 147 + </EmailVerificationProvider> 148 </ServiceConfigProvider> 149 </ProgressGuideProvider> 150 </SafeAreaProvider>
+5 -1
src/components/dialogs/nuxs/index.tsx
··· 3 4 import {useGate} from '#/lib/statsig/statsig' 5 import {logger} from '#/logger' 6 import {Nux, useNuxs, useResetNuxs, useSaveNux} from '#/state/queries/nuxs' 7 import { 8 usePreferencesQuery, ··· 56 export function NuxDialogs() { 57 const {currentAccount} = useSession() 58 const {data: preferences} = usePreferencesQuery() 59 - const {data: profile} = useProfileQuery({did: currentAccount?.did}) 60 const onboardingActive = useOnboardingState().isActive 61 62 const isLoading =
··· 3 4 import {useGate} from '#/lib/statsig/statsig' 5 import {logger} from '#/logger' 6 + import {STALE} from '#/state/queries' 7 import {Nux, useNuxs, useResetNuxs, useSaveNux} from '#/state/queries/nuxs' 8 import { 9 usePreferencesQuery, ··· 57 export function NuxDialogs() { 58 const {currentAccount} = useSession() 59 const {data: preferences} = usePreferencesQuery() 60 + const {data: profile} = useProfileQuery({ 61 + did: currentAccount?.did, 62 + staleTime: STALE.INFINITY, // createdAt isn't gonna change 63 + }) 64 const onboardingActive = useOnboardingState().isActive 65 66 const isLoading =
-36
src/lib/hooks/useEmail.ts
··· 1 - import {STALE} from '#/state/queries' 2 - import {useServiceConfigQuery} from '#/state/queries/email-verification-required' 3 - import {useProfileQuery} from '#/state/queries/profile' 4 - import {useSession} from '#/state/session' 5 - import {BSKY_SERVICE} from '../constants' 6 - import {getHostnameFromUrl} from '../strings/url-helpers' 7 - 8 - export function useEmail() { 9 - const {currentAccount} = useSession() 10 - 11 - const {data: serviceConfig} = useServiceConfigQuery() 12 - const {data: profile} = useProfileQuery({ 13 - did: currentAccount?.did, 14 - staleTime: STALE.INFINITY, 15 - }) 16 - 17 - const checkEmailConfirmed = !!serviceConfig?.checkEmailConfirmed 18 - 19 - // Date set for 11 AM PST on the 18th of November 20 - const isNewEnough = 21 - !!profile?.createdAt && 22 - Date.parse(profile.createdAt) >= Date.parse('2024-11-18T19:00:00.000Z') 23 - 24 - const isSelfHost = 25 - currentAccount && 26 - getHostnameFromUrl(currentAccount.service) !== 27 - getHostnameFromUrl(BSKY_SERVICE) 28 - 29 - const needsEmailVerification = 30 - !isSelfHost && 31 - checkEmailConfirmed && 32 - !currentAccount?.emailConfirmed && 33 - isNewEnough 34 - 35 - return {needsEmailVerification} 36 - }
···
+2 -2
src/lib/hooks/useOpenComposer.tsx
··· 2 import {Trans} from '@lingui/macro' 3 4 import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' 5 - import {useOpenComposer as rootUseOpenComposer} from '#/state/shell/composer' 6 7 export function useOpenComposer() { 8 - const {openComposer} = rootUseOpenComposer() 9 const requireEmailVerification = useRequireEmailVerification() 10 return useMemo(() => { 11 return {
··· 2 import {Trans} from '@lingui/macro' 3 4 import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' 5 + import {useOpenComposer as useRootOpenComposer} from '#/state/shell/composer' 6 7 export function useOpenComposer() { 8 + const {openComposer} = useRootOpenComposer() 9 const requireEmailVerification = useRequireEmailVerification() 10 return useMemo(() => { 11 return {
+1 -1
src/lib/hooks/useRequireEmailVerification.tsx
··· 1 import {useCallback} from 'react' 2 import {Keyboard} from 'react-native' 3 4 - import {useEmail} from '#/lib/hooks/useEmail' 5 import {useRequireAuth, useSession} from '#/state/session' 6 import {useCloseAllActiveElements} from '#/state/util' 7 import {
··· 1 import {useCallback} from 'react' 2 import {Keyboard} from 'react-native' 3 4 + import {useEmail} from '#/state/email-verification' 5 import {useRequireAuth, useSession} from '#/state/session' 6 import {useCloseAllActiveElements} from '#/state/util' 7 import {
+1 -1
src/screens/Messages/Conversation.tsx
··· 15 } from '@react-navigation/native' 16 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 17 18 - import {useEmail} from '#/lib/hooks/useEmail' 19 import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' 20 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 21 import { ··· 24 } from '#/lib/routes/types' 25 import {isWeb} from '#/platform/detection' 26 import {type Shadow, useMaybeProfileShadow} from '#/state/cache/profile-shadow' 27 import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo' 28 import {ConvoStatus} from '#/state/messages/convo/types' 29 import {useCurrentConvoId} from '#/state/messages/current-convo-id'
··· 15 } from '@react-navigation/native' 16 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 17 18 import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' 19 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 20 import { ··· 23 } from '#/lib/routes/types' 24 import {isWeb} from '#/platform/detection' 25 import {type Shadow, useMaybeProfileShadow} from '#/state/cache/profile-shadow' 26 + import {useEmail} from '#/state/email-verification' 27 import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo' 28 import {ConvoStatus} from '#/state/messages/convo/types' 29 import {useCurrentConvoId} from '#/state/messages/current-convo-id'
+1 -1
src/screens/Messages/components/MessageInput.tsx
··· 18 19 import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' 20 import {useHaptics} from '#/lib/haptics' 21 - import {useEmail} from '#/lib/hooks/useEmail' 22 import {isIOS, isWeb} from '#/platform/detection' 23 import { 24 useMessageDraft, 25 useSaveMessageDraft,
··· 18 19 import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' 20 import {useHaptics} from '#/lib/haptics' 21 import {isIOS, isWeb} from '#/platform/detection' 22 + import {useEmail} from '#/state/email-verification' 23 import { 24 useMessageDraft, 25 useSaveMessageDraft,
+1 -1
src/screens/Messages/components/RequestButtons.tsx
··· 5 import {StackActions, useNavigation} from '@react-navigation/native' 6 import {useQueryClient} from '@tanstack/react-query' 7 8 - import {useEmail} from '#/lib/hooks/useEmail' 9 import {type NavigationProp} from '#/lib/routes/types' 10 import {useProfileShadow} from '#/state/cache/profile-shadow' 11 import {useAcceptConversation} from '#/state/queries/messages/accept-conversation' 12 import {precacheConvoQuery} from '#/state/queries/messages/conversation' 13 import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
··· 5 import {StackActions, useNavigation} from '@react-navigation/native' 6 import {useQueryClient} from '@tanstack/react-query' 7 8 import {type NavigationProp} from '#/lib/routes/types' 9 import {useProfileShadow} from '#/state/cache/profile-shadow' 10 + import {useEmail} from '#/state/email-verification' 11 import {useAcceptConversation} from '#/state/queries/messages/accept-conversation' 12 import {precacheConvoQuery} from '#/state/queries/messages/conversation' 13 import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
+64
src/state/email-verification.tsx
···
··· 1 + import {createContext, useContext, useMemo} from 'react' 2 + 3 + import {BSKY_SERVICE} from '#/lib/constants' 4 + import {getHostnameFromUrl} from '#/lib/strings/url-helpers' 5 + import {STALE} from '#/state/queries' 6 + import {useProfileQuery} from '#/state/queries/profile' 7 + import {useCheckEmailConfirmed} from '#/state/service-config' 8 + import {useSession} from '#/state/session' 9 + 10 + type EmailVerificationContext = { 11 + needsEmailVerification: boolean 12 + } 13 + 14 + const EmailVerificationContext = createContext<EmailVerificationContext | null>( 15 + null, 16 + ) 17 + EmailVerificationContext.displayName = 'EmailVerificationContext' 18 + 19 + export function Provider({children}: {children: React.ReactNode}) { 20 + const {currentAccount} = useSession() 21 + 22 + const {data: profile} = useProfileQuery({ 23 + did: currentAccount?.did, 24 + staleTime: STALE.INFINITY, 25 + }) 26 + 27 + const checkEmailConfirmed = useCheckEmailConfirmed() 28 + 29 + // Date set for 11 AM PST on the 18th of November 30 + const isNewEnough = 31 + !!profile?.createdAt && 32 + Date.parse(profile.createdAt) >= Date.parse('2024-11-18T19:00:00.000Z') 33 + 34 + const isSelfHost = 35 + currentAccount && 36 + getHostnameFromUrl(currentAccount.service) !== 37 + getHostnameFromUrl(BSKY_SERVICE) 38 + 39 + const needsEmailVerification = 40 + !isSelfHost && 41 + checkEmailConfirmed && 42 + !currentAccount?.emailConfirmed && 43 + isNewEnough 44 + 45 + const value = useMemo( 46 + () => ({needsEmailVerification}), 47 + [needsEmailVerification], 48 + ) 49 + 50 + return ( 51 + <EmailVerificationContext.Provider value={value}> 52 + {children} 53 + </EmailVerificationContext.Provider> 54 + ) 55 + } 56 + Provider.displayName = 'EmailVerificationProvider' 57 + 58 + export function useEmail() { 59 + const ctx = useContext(EmailVerificationContext) 60 + if (!ctx) { 61 + throw new Error('useEmail must be used within a EmailVerificationProvider') 62 + } 63 + return ctx 64 + }
-25
src/state/queries/email-verification-required.ts
··· 1 - import {useQuery} from '@tanstack/react-query' 2 - 3 - interface ServiceConfig { 4 - checkEmailConfirmed: boolean 5 - } 6 - 7 - export function useServiceConfigQuery() { 8 - return useQuery({ 9 - queryKey: ['service-config'], 10 - queryFn: async () => { 11 - const res = await fetch( 12 - 'https://api.bsky.app/xrpc/app.bsky.unspecced.getConfig', 13 - ) 14 - if (!res.ok) { 15 - return { 16 - checkEmailConfirmed: false, 17 - } 18 - } 19 - 20 - const json = await res.json() 21 - return json as ServiceConfig 22 - }, 23 - staleTime: 5 * 60 * 1000, 24 - }) 25 - }
···
+20 -2
src/state/service-config.tsx
··· 21 const LiveNowContext = createContext<LiveNowContext | null>(null) 22 LiveNowContext.displayName = 'LiveNowContext' 23 24 export function Provider({children}: {children: React.ReactNode}) { 25 const langPrefs = useLanguagePrefs() 26 const {data: config, isLoading: isInitialLoad} = useServiceConfigQuery() ··· 61 62 const liveNow = useMemo<LiveNowContext>(() => config?.liveNow ?? [], [config]) 63 64 return ( 65 <TrendingContext.Provider value={trending}> 66 <LiveNowContext.Provider value={liveNow}> 67 - {children} 68 </LiveNowContext.Provider> 69 </TrendingContext.Provider> 70 ) ··· 78 const ctx = useContext(LiveNowContext) 79 if (!ctx) { 80 throw new Error( 81 - 'useLiveNowConfig must be used within a LiveNowConfigProvider', 82 ) 83 } 84 return ctx ··· 88 const config = useLiveNowConfig() 89 return !!config.find(cfg => cfg.did === did) 90 }
··· 21 const LiveNowContext = createContext<LiveNowContext | null>(null) 22 LiveNowContext.displayName = 'LiveNowContext' 23 24 + const CheckEmailConfirmedContext = createContext<boolean | null>(null) 25 + 26 export function Provider({children}: {children: React.ReactNode}) { 27 const langPrefs = useLanguagePrefs() 28 const {data: config, isLoading: isInitialLoad} = useServiceConfigQuery() ··· 63 64 const liveNow = useMemo<LiveNowContext>(() => config?.liveNow ?? [], [config]) 65 66 + // probably true, so default to true when loading 67 + // if the call fails, the query will set it to false for us 68 + const checkEmailConfirmed = config?.checkEmailConfirmed ?? true 69 + 70 return ( 71 <TrendingContext.Provider value={trending}> 72 <LiveNowContext.Provider value={liveNow}> 73 + <CheckEmailConfirmedContext.Provider value={checkEmailConfirmed}> 74 + {children} 75 + </CheckEmailConfirmedContext.Provider> 76 </LiveNowContext.Provider> 77 </TrendingContext.Provider> 78 ) ··· 86 const ctx = useContext(LiveNowContext) 87 if (!ctx) { 88 throw new Error( 89 + 'useLiveNowConfig must be used within a ServiceConfigManager', 90 ) 91 } 92 return ctx ··· 96 const config = useLiveNowConfig() 97 return !!config.find(cfg => cfg.did === did) 98 } 99 + 100 + export function useCheckEmailConfirmed() { 101 + const ctx = useContext(CheckEmailConfirmedContext) 102 + if (ctx === null) { 103 + throw new Error( 104 + 'useCheckEmailConfirmed must be used within a ServiceConfigManager', 105 + ) 106 + } 107 + return ctx 108 + }