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>
This commit is contained in:
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

@ -1,4 +1,4 @@
import React, {memo} from 'react'
import React, {memo, useMemo} from 'react'
import {
StyleSheet,
TouchableOpacity,
@ -10,7 +10,8 @@ import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {
AppBskyActorDefs,
ProfileModeration,
ModerationOpts,
moderateProfile,
RichText as RichTextAPI,
} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
@ -42,12 +43,11 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics/analytics'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
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 {pluralize} from 'lib/strings/helpers'
import {toShareUrl} from 'lib/strings/url-helpers'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {shareUrl} from 'lib/sharing'
import {s, colors} from 'lib/styles'
import {logger} from '#/logger'
@ -55,17 +55,19 @@ import {useSession, getAgent} from '#/state/session'
import {Shadow} from '#/state/cache/types'
import {useRequireAuth} from '#/state/session'
import {LabelInfo} from '../util/moderation/LabelInfo'
import {useProfileShadow} from 'state/cache/profile-shadow'
interface Props {
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> | null
moderation: ProfileModeration | null
profile: AppBskyActorDefs.ProfileView | null
placeholderData?: AppBskyActorDefs.ProfileView | null
moderationOpts: ModerationOpts | null
hideBackButton?: boolean
isProfilePreview?: boolean
}
export function ProfileHeader({
profile,
moderation,
moderationOpts,
hideBackButton = false,
isProfilePreview,
}: Props) {
@ -73,10 +75,14 @@ export function ProfileHeader({
// loading
// =
if (!profile || !moderation) {
if (!profile || !moderationOpts) {
return (
<View style={pal.view}>
<LoadingPlaceholder width="100%" height={153} />
<LoadingPlaceholder
width="100%"
height={150}
style={{borderRadius: 0}}
/>
<View
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
<LoadingPlaceholder width={80} height={80} style={styles.br40} />
@ -95,7 +101,7 @@ export function ProfileHeader({
return (
<ProfileHeaderLoaded
profile={profile}
moderation={moderation}
moderationOpts={moderationOpts}
hideBackButton={hideBackButton}
isProfilePreview={isProfilePreview}
/>
@ -103,18 +109,20 @@ export function ProfileHeader({
}
interface LoadedProps {
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
moderation: ProfileModeration
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
hideBackButton?: boolean
isProfilePreview?: boolean
}
let ProfileHeaderLoaded = ({
profile,
moderation,
profile: profileUnshadowed,
moderationOpts,
hideBackButton = false,
isProfilePreview,
}: LoadedProps): React.ReactNode => {
const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
useProfileShadow(profileUnshadowed)
const pal = usePalette('default')
const palInverted = usePalette('inverted')
const {currentAccount, hasSession} = useSession()
@ -131,6 +139,10 @@ let ProfileHeaderLoaded = ({
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
const queryClient = useQueryClient()
const moderation = useMemo(
() => moderateProfile(profile, moderationOpts),
[profile, moderationOpts],
)
/*
* BEGIN handle bio facet resolution
@ -442,9 +454,22 @@ let ProfileHeaderLoaded = ({
const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
return (
<View style={pal.view} pointerEvents="box-none">
<View
style={[
pal.view,
isProfilePreview && isDesktop && styles.loadingBorderStyle,
]}
pointerEvents="box-none">
<View pointerEvents="none">
<UserBanner banner={profile.banner} moderation={moderation.avatar} />
{isProfilePreview ? (
<LoadingPlaceholder
width="100%"
height={150}
style={{borderRadius: 0}}
/>
) : (
<UserBanner banner={profile.banner} moderation={moderation.avatar} />
)}
</View>
<View style={styles.content} pointerEvents="box-none">
<View style={[styles.buttonsLine]} pointerEvents="box-none">
@ -478,7 +503,7 @@ let ProfileHeaderLoaded = ({
)
) : !profile.viewer?.blockedBy ? (
<>
{!isProfilePreview && hasSession && (
{hasSession && (
<TouchableOpacity
testID="suggestedFollowsBtn"
onPress={() => setShowSuggestedFollows(!showSuggestedFollows)}
@ -597,7 +622,7 @@ let ProfileHeaderLoaded = ({
{invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`}
</ThemedText>
</View>
{!blockHide && (
{!isProfilePreview && !blockHide && (
<>
<View style={styles.metricsLine} pointerEvents="box-none">
<Link
@ -665,7 +690,7 @@ let ProfileHeaderLoaded = ({
)}
</View>
{!isProfilePreview && showSuggestedFollows && (
{showSuggestedFollows && (
<ProfileHeaderSuggestedFollows
actorDid={profile.did}
requestDismiss={() => {
@ -820,4 +845,9 @@ const styles = StyleSheet.create({
br40: {borderRadius: 40},
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 {ModerationUI} from '@atproto/api'
import {Image} from 'expo-image'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {colors} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
import {
usePhotoLibraryPermission,
@ -13,8 +16,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {isWeb, isAndroid} from 'platform/detection'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
export function UserBanner({
banner,
@ -26,6 +27,7 @@ export function UserBanner({
onSelectNewBanner?: (img: RNImage | null) => void
}) {
const pal = usePalette('default')
const theme = useTheme()
const {_} = useLingui()
const {requestCameraAccessIfNeeded} = useCameraPermission()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
@ -142,7 +144,10 @@ export function UserBanner({
!((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
<Image
testID="userBannerImage"
style={styles.bannerImage}
style={[
styles.bannerImage,
{backgroundColor: theme.palette.default.backgroundLight},
]}
resizeMode="cover"
source={{uri: banner}}
blurRadius={moderation?.blur ? 100 : 0}

View file

@ -66,6 +66,7 @@ export function ProfileScreen({route}: Props) {
error: profileError,
refetch: refetchProfile,
isLoading: isLoadingProfile,
isPlaceholderData: isPlaceholderProfile,
} = useProfileQuery({
did: resolvedDid,
})
@ -85,12 +86,13 @@ export function ProfileScreen({route}: Props) {
}
}, [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 (
<CenteredView>
<ProfileHeader
profile={null}
moderation={null}
profile={profile ?? null}
moderationOpts={moderationOpts ?? null}
isProfilePreview={true}
/>
</CenteredView>
@ -268,11 +270,11 @@ function ProfileScreenLoaded({
return (
<ProfileHeader
profile={profile}
moderation={moderation}
moderationOpts={moderationOpts}
hideBackButton={hideBackButton}
/>
)
}, [profile, moderation, hideBackButton])
}, [profile, moderationOpts, hideBackButton])
return (
<ScreenHider