Precache basic profile from posts for instant future navigations (#2795)

* skeleton for caching

* modify some existing logic

* refactor uri resolution query

* add precache feed posts

* adjustments

* remove prefetch on hover (maybe revert, just example)

* fix

* change arg name to match what we want

* optional infinite stale time

* use `ProfileViewDetailed`

* Revert "remove prefetch on hover (maybe revert, just example)"

This reverts commit 08609deb0defa7cea040438bc37dd3488ddc56f4.

* add warning comment back for stale time

* remove comment

* store profile with both the handle and did for query key

* remove extra block from revert

* clarify argument name

* remove QT cache

* structure queries the same (put `enabled` at bottom)

* use both `ProfileViewDetailed` and `ProfileView` for the query return type

* placeholder profile header

* remove logs

* remove a few other things we don't need

* add placeholder

* refactor

* refactor

* we don't need this height adjustment now

* use gray banner while loading

* set background color of image to the loading placeholder color

* reorg imports

* add border to header on loading

* Fix style

* Rm radius

* oops

* Undo edit

* Back out type changes

* Tighten some types and moderate shadow

* Move precaching fns to profile where the cache is

* Rename functions to match what they do now

* Remove anys

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
zio/stable
Hailey 2024-02-08 17:38:16 -08:00 committed by GitHub
parent d9b62955b5
commit de28626001
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 170 additions and 85 deletions

View File

@ -12,7 +12,7 @@ import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import chunk from 'lodash.chunk' import chunk from 'lodash.chunk'
import {QueryClient} from '@tanstack/react-query' import {QueryClient} from '@tanstack/react-query'
import {getAgent} from '../../session' import {getAgent} from '../../session'
import {precacheProfile as precacheResolvedUri} from '../resolve-uri' import {precacheProfile} from '../profile'
import {NotificationType, FeedNotification, FeedPage} from './types' import {NotificationType, FeedNotification, FeedPage} from './types'
const GROUPABLE_REASONS = ['like', 'repost', 'follow'] const GROUPABLE_REASONS = ['like', 'repost', 'follow']
@ -59,7 +59,7 @@ export async function fetchPage({
if (notif.subjectUri) { if (notif.subjectUri) {
notif.subject = subjects.get(notif.subjectUri) notif.subject = subjects.get(notif.subjectUri)
if (notif.subject) { if (notif.subject) {
precacheResolvedUri(queryClient, notif.subject.author) // precache the handle->did resolution precacheProfile(queryClient, notif.subject.author)
} }
} }
} }

View File

@ -21,7 +21,7 @@ import {MergeFeedAPI} from 'lib/api/feed/merge'
import {HomeFeedAPI} from '#/lib/api/feed/home' import {HomeFeedAPI} from '#/lib/api/feed/home'
import {logger} from '#/logger' import {logger} from '#/logger'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {precacheFeedPosts as precacheResolvedUris} from './resolve-uri' import {precacheFeedPostProfiles} from './profile'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const' import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
import {getModerationOpts} from '#/state/queries/preferences/moderation' import {getModerationOpts} from '#/state/queries/preferences/moderation'
@ -138,7 +138,7 @@ export function usePostFeedQuery(
} }
const res = await api.fetch({cursor, limit: PAGE_SIZE}) const res = await api.fetch({cursor, limit: PAGE_SIZE})
precacheResolvedUris(queryClient, res.feed) // precache the handle->did resolution precacheFeedPostProfiles(queryClient, res.feed)
/* /*
* If this is a public view, we need to check if posts fail moderation. * If this is a public view, we need to check if posts fail moderation.

View File

@ -10,7 +10,7 @@ import {getAgent} from '#/state/session'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {findPostInQueryData as findPostInFeedQueryData} from './post-feed' import {findPostInQueryData as findPostInFeedQueryData} from './post-feed'
import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed' import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed'
import {precacheThreadPosts as precacheResolvedUris} from './resolve-uri' import {precacheThreadPostProfiles} from './profile'
import {getEmbeddedPost} from './util' import {getEmbeddedPost} from './util'
export const RQKEY = (uri: string) => ['post-thread', uri] export const RQKEY = (uri: string) => ['post-thread', uri]
@ -71,7 +71,7 @@ export function usePostThreadQuery(uri: string | undefined) {
const res = await getAgent().getPostThread({uri: uri!}) const res = await getAgent().getPostThread({uri: uri!})
if (res.success) { if (res.success) {
const nodes = responseToThreadNodes(res.data.thread) const nodes = responseToThreadNodes(res.data.thread)
precacheResolvedUris(queryClient, nodes) // precache the handle->did resolution precacheThreadPostProfiles(queryClient, nodes)
return nodes return nodes
} }
return {type: 'unknown', uri: uri!} return {type: 'unknown', uri: uri!}

View File

@ -4,6 +4,9 @@ import {
AppBskyActorDefs, AppBskyActorDefs,
AppBskyActorProfile, AppBskyActorProfile,
AppBskyActorGetProfile, AppBskyActorGetProfile,
AppBskyFeedDefs,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
} from '@atproto/api' } from '@atproto/api'
import { import {
useQuery, useQuery,
@ -23,9 +26,14 @@ import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts' import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {track} from '#/lib/analytics/analytics' import {track} from '#/lib/analytics/analytics'
import {ThreadNode} from './post-thread'
export const RQKEY = (did: string) => ['profile', did] export const RQKEY = (did: string) => ['profile', did]
export const profilesQueryKey = (handles: string[]) => ['profiles', handles] export const profilesQueryKey = (handles: string[]) => ['profiles', handles]
export const profileBasicQueryKey = (didOrHandle: string) => [
'profileBasic',
didOrHandle,
]
export function useProfileQuery({ export function useProfileQuery({
did, did,
@ -34,18 +42,26 @@ export function useProfileQuery({
did: string | undefined did: string | undefined
staleTime?: number staleTime?: number
}) { }) {
return useQuery({ const queryClient = useQueryClient()
return useQuery<AppBskyActorDefs.ProfileViewDetailed>({
// WARNING // WARNING
// this staleTime is load-bearing // this staleTime is load-bearing
// if you remove it, the UI infinite-loops // if you remove it, the UI infinite-loops
// -prf // -prf
staleTime, staleTime,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
queryKey: RQKEY(did || ''), queryKey: RQKEY(did ?? ''),
queryFn: async () => { queryFn: async () => {
const res = await getAgent().getProfile({actor: did || ''}) const res = await getAgent().getProfile({actor: did ?? ''})
return res.data return res.data
}, },
placeholderData: () => {
if (!did) return
return queryClient.getQueryData<AppBskyActorDefs.ProfileViewBasic>(
profileBasicQueryKey(did),
)
},
enabled: !!did, enabled: !!did,
}) })
} }
@ -405,6 +421,64 @@ function useProfileUnblockMutation() {
}) })
} }
export function precacheProfile(
queryClient: QueryClient,
profile: AppBskyActorDefs.ProfileViewBasic,
) {
queryClient.setQueryData(profileBasicQueryKey(profile.handle), profile)
queryClient.setQueryData(profileBasicQueryKey(profile.did), profile)
}
export function precacheFeedPostProfiles(
queryClient: QueryClient,
posts: AppBskyFeedDefs.FeedViewPost[],
) {
for (const post of posts) {
// Save the author of the post every time
precacheProfile(queryClient, post.post.author)
precachePostEmbedProfile(queryClient, post.post.embed)
// Cache parent author and embeds
const parent = post.reply?.parent
if (AppBskyFeedDefs.isPostView(parent)) {
precacheProfile(queryClient, parent.author)
precachePostEmbedProfile(queryClient, parent.embed)
}
}
}
function precachePostEmbedProfile(
queryClient: QueryClient,
embed: AppBskyFeedDefs.PostView['embed'],
) {
if (AppBskyEmbedRecord.isView(embed)) {
if (AppBskyEmbedRecord.isViewRecord(embed.record)) {
precacheProfile(queryClient, embed.record.author)
}
} else if (AppBskyEmbedRecordWithMedia.isView(embed)) {
if (AppBskyEmbedRecord.isViewRecord(embed.record.record)) {
precacheProfile(queryClient, embed.record.record.author)
}
}
}
export function precacheThreadPostProfiles(
queryClient: QueryClient,
node: ThreadNode,
) {
if (node.type === 'post') {
precacheProfile(queryClient, node.post.author)
if (node.parent) {
precacheThreadPostProfiles(queryClient, node.parent)
}
if (node.replies?.length) {
for (const reply of node.replies) {
precacheThreadPostProfiles(queryClient, reply)
}
}
}
}
async function whenAppViewReady( async function whenAppViewReady(
actor: string, actor: string,
fn: (res: AppBskyActorGetProfile.Response) => boolean, fn: (res: AppBskyActorGetProfile.Response) => boolean,

View File

@ -1,9 +1,9 @@
import {QueryClient, useQuery, UseQueryResult} from '@tanstack/react-query' import {useQuery, useQueryClient, UseQueryResult} from '@tanstack/react-query'
import {AtUri, AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api' import {AtUri, AppBskyActorDefs} from '@atproto/api'
import {profileBasicQueryKey as RQKEY_PROFILE_BASIC} from './profile'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {ThreadNode} from './post-thread'
export const RQKEY = (didOrHandle: string) => ['resolved-did', didOrHandle] export const RQKEY = (didOrHandle: string) => ['resolved-did', didOrHandle]
@ -22,55 +22,29 @@ export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult {
} }
export function useResolveDidQuery(didOrHandle: string | undefined) { export function useResolveDidQuery(didOrHandle: string | undefined) {
const queryClient = useQueryClient()
return useQuery<string, Error>({ return useQuery<string, Error>({
staleTime: STALE.HOURS.ONE, staleTime: STALE.HOURS.ONE,
queryKey: RQKEY(didOrHandle || ''), queryKey: RQKEY(didOrHandle ?? ''),
async queryFn() { queryFn: async () => {
if (!didOrHandle) { if (!didOrHandle) return ''
return '' // Just return the did if it's already one
} if (didOrHandle.startsWith('did:')) return didOrHandle
if (!didOrHandle.startsWith('did:')) {
const res = await getAgent().resolveHandle({handle: didOrHandle}) const res = await getAgent().resolveHandle({handle: didOrHandle})
didOrHandle = res.data.did return res.data.did
} },
return didOrHandle initialData: () => {
// Return undefined if no did or handle
if (!didOrHandle) return
const profile =
queryClient.getQueryData<AppBskyActorDefs.ProfileViewBasic>(
RQKEY_PROFILE_BASIC(didOrHandle),
)
return profile?.did
}, },
enabled: !!didOrHandle, enabled: !!didOrHandle,
}) })
} }
export function precacheProfile(
queryClient: QueryClient,
profile:
| AppBskyActorDefs.ProfileView
| AppBskyActorDefs.ProfileViewBasic
| AppBskyActorDefs.ProfileViewDetailed,
) {
queryClient.setQueryData(RQKEY(profile.handle), profile.did)
}
export function precacheFeedPosts(
queryClient: QueryClient,
posts: AppBskyFeedDefs.FeedViewPost[],
) {
for (const post of posts) {
precacheProfile(queryClient, post.post.author)
}
}
export function precacheThreadPosts(
queryClient: QueryClient,
node: ThreadNode,
) {
if (node.type === 'post') {
precacheProfile(queryClient, node.post.author)
if (node.parent) {
precacheThreadPosts(queryClient, node.parent)
}
if (node.replies?.length) {
for (const reply of node.replies) {
precacheThreadPosts(queryClient, reply)
}
}
}
}

View File

@ -1,4 +1,4 @@
import React, {memo} from 'react' import React, {memo, useMemo} from 'react'
import { import {
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
@ -10,7 +10,8 @@ import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import { import {
AppBskyActorDefs, AppBskyActorDefs,
ProfileModeration, ModerationOpts,
moderateProfile,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
@ -42,12 +43,11 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {BACK_HITSLOP} from 'lib/constants' import {BACK_HITSLOP} from 'lib/constants'
import {isInvalidHandle} from 'lib/strings/handles' import {isInvalidHandle, sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {pluralize} from 'lib/strings/helpers' import {pluralize} from 'lib/strings/helpers'
import {toShareUrl} from 'lib/strings/url-helpers' import {toShareUrl} from 'lib/strings/url-helpers'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {shareUrl} from 'lib/sharing' import {shareUrl} from 'lib/sharing'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
import {logger} from '#/logger' import {logger} from '#/logger'
@ -55,17 +55,19 @@ import {useSession, getAgent} from '#/state/session'
import {Shadow} from '#/state/cache/types' import {Shadow} from '#/state/cache/types'
import {useRequireAuth} from '#/state/session' import {useRequireAuth} from '#/state/session'
import {LabelInfo} from '../util/moderation/LabelInfo' import {LabelInfo} from '../util/moderation/LabelInfo'
import {useProfileShadow} from 'state/cache/profile-shadow'
interface Props { interface Props {
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> | null profile: AppBskyActorDefs.ProfileView | null
moderation: ProfileModeration | null placeholderData?: AppBskyActorDefs.ProfileView | null
moderationOpts: ModerationOpts | null
hideBackButton?: boolean hideBackButton?: boolean
isProfilePreview?: boolean isProfilePreview?: boolean
} }
export function ProfileHeader({ export function ProfileHeader({
profile, profile,
moderation, moderationOpts,
hideBackButton = false, hideBackButton = false,
isProfilePreview, isProfilePreview,
}: Props) { }: Props) {
@ -73,10 +75,14 @@ export function ProfileHeader({
// loading // loading
// = // =
if (!profile || !moderation) { if (!profile || !moderationOpts) {
return ( return (
<View style={pal.view}> <View style={pal.view}>
<LoadingPlaceholder width="100%" height={153} /> <LoadingPlaceholder
width="100%"
height={150}
style={{borderRadius: 0}}
/>
<View <View
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}> style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
<LoadingPlaceholder width={80} height={80} style={styles.br40} /> <LoadingPlaceholder width={80} height={80} style={styles.br40} />
@ -95,7 +101,7 @@ export function ProfileHeader({
return ( return (
<ProfileHeaderLoaded <ProfileHeaderLoaded
profile={profile} profile={profile}
moderation={moderation} moderationOpts={moderationOpts}
hideBackButton={hideBackButton} hideBackButton={hideBackButton}
isProfilePreview={isProfilePreview} isProfilePreview={isProfilePreview}
/> />
@ -103,18 +109,20 @@ export function ProfileHeader({
} }
interface LoadedProps { interface LoadedProps {
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> profile: AppBskyActorDefs.ProfileViewDetailed
moderation: ProfileModeration moderationOpts: ModerationOpts
hideBackButton?: boolean hideBackButton?: boolean
isProfilePreview?: boolean isProfilePreview?: boolean
} }
let ProfileHeaderLoaded = ({ let ProfileHeaderLoaded = ({
profile, profile: profileUnshadowed,
moderation, moderationOpts,
hideBackButton = false, hideBackButton = false,
isProfilePreview, isProfilePreview,
}: LoadedProps): React.ReactNode => { }: LoadedProps): React.ReactNode => {
const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
useProfileShadow(profileUnshadowed)
const pal = usePalette('default') const pal = usePalette('default')
const palInverted = usePalette('inverted') const palInverted = usePalette('inverted')
const {currentAccount, hasSession} = useSession() const {currentAccount, hasSession} = useSession()
@ -131,6 +139,10 @@ let ProfileHeaderLoaded = ({
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
const queryClient = useQueryClient() const queryClient = useQueryClient()
const moderation = useMemo(
() => moderateProfile(profile, moderationOpts),
[profile, moderationOpts],
)
/* /*
* BEGIN handle bio facet resolution * BEGIN handle bio facet resolution
@ -442,9 +454,22 @@ let ProfileHeaderLoaded = ({
const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower') const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
return ( return (
<View style={pal.view} pointerEvents="box-none"> <View
style={[
pal.view,
isProfilePreview && isDesktop && styles.loadingBorderStyle,
]}
pointerEvents="box-none">
<View pointerEvents="none"> <View pointerEvents="none">
{isProfilePreview ? (
<LoadingPlaceholder
width="100%"
height={150}
style={{borderRadius: 0}}
/>
) : (
<UserBanner banner={profile.banner} moderation={moderation.avatar} /> <UserBanner banner={profile.banner} moderation={moderation.avatar} />
)}
</View> </View>
<View style={styles.content} pointerEvents="box-none"> <View style={styles.content} pointerEvents="box-none">
<View style={[styles.buttonsLine]} pointerEvents="box-none"> <View style={[styles.buttonsLine]} pointerEvents="box-none">
@ -478,7 +503,7 @@ let ProfileHeaderLoaded = ({
) )
) : !profile.viewer?.blockedBy ? ( ) : !profile.viewer?.blockedBy ? (
<> <>
{!isProfilePreview && hasSession && ( {hasSession && (
<TouchableOpacity <TouchableOpacity
testID="suggestedFollowsBtn" testID="suggestedFollowsBtn"
onPress={() => setShowSuggestedFollows(!showSuggestedFollows)} onPress={() => setShowSuggestedFollows(!showSuggestedFollows)}
@ -597,7 +622,7 @@ let ProfileHeaderLoaded = ({
{invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`} {invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`}
</ThemedText> </ThemedText>
</View> </View>
{!blockHide && ( {!isProfilePreview && !blockHide && (
<> <>
<View style={styles.metricsLine} pointerEvents="box-none"> <View style={styles.metricsLine} pointerEvents="box-none">
<Link <Link
@ -665,7 +690,7 @@ let ProfileHeaderLoaded = ({
)} )}
</View> </View>
{!isProfilePreview && showSuggestedFollows && ( {showSuggestedFollows && (
<ProfileHeaderSuggestedFollows <ProfileHeaderSuggestedFollows
actorDid={profile.did} actorDid={profile.did}
requestDismiss={() => { requestDismiss={() => {
@ -820,4 +845,9 @@ const styles = StyleSheet.create({
br40: {borderRadius: 40}, br40: {borderRadius: 40},
br50: {borderRadius: 50}, br50: {borderRadius: 50},
loadingBorderStyle: {
borderLeftWidth: 1,
borderRightWidth: 1,
},
}) })

View File

@ -3,7 +3,10 @@ import {StyleSheet, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {ModerationUI} from '@atproto/api' import {ModerationUI} from '@atproto/api'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {colors} from 'lib/styles' import {colors} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
import { import {
usePhotoLibraryPermission, usePhotoLibraryPermission,
@ -13,8 +16,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {isWeb, isAndroid} from 'platform/detection' import {isWeb, isAndroid} from 'platform/detection'
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
import {NativeDropdown, DropdownItem} from './forms/NativeDropdown' import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
export function UserBanner({ export function UserBanner({
banner, banner,
@ -26,6 +27,7 @@ export function UserBanner({
onSelectNewBanner?: (img: RNImage | null) => void onSelectNewBanner?: (img: RNImage | null) => void
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const theme = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {requestCameraAccessIfNeeded} = useCameraPermission() const {requestCameraAccessIfNeeded} = useCameraPermission()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
@ -142,7 +144,10 @@ export function UserBanner({
!((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( !((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
<Image <Image
testID="userBannerImage" testID="userBannerImage"
style={styles.bannerImage} style={[
styles.bannerImage,
{backgroundColor: theme.palette.default.backgroundLight},
]}
resizeMode="cover" resizeMode="cover"
source={{uri: banner}} source={{uri: banner}}
blurRadius={moderation?.blur ? 100 : 0} blurRadius={moderation?.blur ? 100 : 0}

View File

@ -66,6 +66,7 @@ export function ProfileScreen({route}: Props) {
error: profileError, error: profileError,
refetch: refetchProfile, refetch: refetchProfile,
isLoading: isLoadingProfile, isLoading: isLoadingProfile,
isPlaceholderData: isPlaceholderProfile,
} = useProfileQuery({ } = useProfileQuery({
did: resolvedDid, did: resolvedDid,
}) })
@ -85,12 +86,13 @@ export function ProfileScreen({route}: Props) {
} }
}, [profile?.viewer?.blockedBy, resolvedDid]) }, [profile?.viewer?.blockedBy, resolvedDid])
if (isLoadingDid || isLoadingProfile || !moderationOpts) { // Most pushes will happen here, since we will have only placeholder data
if (isLoadingDid || isLoadingProfile || isPlaceholderProfile) {
return ( return (
<CenteredView> <CenteredView>
<ProfileHeader <ProfileHeader
profile={null} profile={profile ?? null}
moderation={null} moderationOpts={moderationOpts ?? null}
isProfilePreview={true} isProfilePreview={true}
/> />
</CenteredView> </CenteredView>
@ -268,11 +270,11 @@ function ProfileScreenLoaded({
return ( return (
<ProfileHeader <ProfileHeader
profile={profile} profile={profile}
moderation={moderation} moderationOpts={moderationOpts}
hideBackButton={hideBackButton} hideBackButton={hideBackButton}
/> />
) )
}, [profile, moderation, hideBackButton]) }, [profile, moderationOpts, hideBackButton])
return ( return (
<ScreenHider <ScreenHider