From d36b91fe67225a9d3c79c8eeb3c80f6a72e9f73f Mon Sep 17 00:00:00 2001
From: dan <dan.abramov@gmail.com>
Date: Fri, 9 Feb 2024 05:00:50 +0000
Subject: Fix flashes and jumps when opening profile (#2815)

* Don't reset the tree when profile loads fully

* Give avatars a background color like placeholders

* Prevent jumps due to rich text resolving

* Rm log

* Rm unused
---
 src/view/screens/Profile.tsx | 68 +++++++++++++++++++++++++++++++++++++-------
 1 file changed, 57 insertions(+), 11 deletions(-)

(limited to 'src/view/screens/Profile.tsx')

diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 9ca1b8c05..64e067593 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -1,7 +1,12 @@
 import React, {useMemo} from 'react'
 import {StyleSheet, View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
-import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
+import {
+  AppBskyActorDefs,
+  moderateProfile,
+  ModerationOpts,
+  RichText as RichTextAPI,
+} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
@@ -11,7 +16,7 @@ import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
 import {Feed} from 'view/com/posts/Feed'
 import {ProfileLists} from '../com/lists/ProfileLists'
 import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
-import {ProfileHeader} from '../com/profile/ProfileHeader'
+import {ProfileHeader, ProfileHeaderLoading} from '../com/profile/ProfileHeader'
 import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
 import {ErrorScreen} from '../com/util/error/ErrorScreen'
 import {EmptyState} from '../com/util/EmptyState'
@@ -28,7 +33,7 @@ import {
 import {useResolveDidQuery} from '#/state/queries/resolve-uri'
 import {useProfileQuery} from '#/state/queries/profile'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
-import {useSession} from '#/state/session'
+import {useSession, getAgent} from '#/state/session'
 import {useModerationOpts} from '#/state/queries/preferences'
 import {useProfileExtraInfoQuery} from '#/state/queries/profile-extra-info'
 import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
@@ -87,14 +92,10 @@ export function ProfileScreen({route}: Props) {
   }, [profile?.viewer?.blockedBy, resolvedDid])
 
   // Most pushes will happen here, since we will have only placeholder data
-  if (isLoadingDid || isLoadingProfile || isPlaceholderProfile) {
+  if (isLoadingDid || isLoadingProfile) {
     return (
       <CenteredView>
-        <ProfileHeader
-          profile={profile ?? null}
-          moderationOpts={moderationOpts ?? null}
-          isProfilePreview={true}
-        />
+        <ProfileHeaderLoading />
       </CenteredView>
     )
   }
@@ -114,6 +115,7 @@ export function ProfileScreen({route}: Props) {
       <ProfileScreenLoaded
         profile={profile}
         moderationOpts={moderationOpts}
+        isPlaceholderProfile={isPlaceholderProfile}
         hideBackButton={!!route.params.hideBackButton}
       />
     )
@@ -132,12 +134,14 @@ export function ProfileScreen({route}: Props) {
 
 function ProfileScreenLoaded({
   profile: profileUnshadowed,
+  isPlaceholderProfile,
   moderationOpts,
   hideBackButton,
 }: {
   profile: AppBskyActorDefs.ProfileViewDetailed
   moderationOpts: ModerationOpts
   hideBackButton: boolean
+  isPlaceholderProfile: boolean
 }) {
   const profile = useProfileShadow(profileUnshadowed)
   const {hasSession, currentAccount} = useSession()
@@ -157,6 +161,10 @@ function ProfileScreenLoaded({
 
   useSetTitle(combinedDisplayName(profile))
 
+  const description = profile.description ?? ''
+  const hasDescription = description !== ''
+  const [descriptionRT, isResolvingDescriptionRT] = useRichText(description)
+  const showPlaceholder = isPlaceholderProfile || isResolvingDescriptionRT
   const moderation = useMemo(
     () => moderateProfile(profile, moderationOpts),
     [profile, moderationOpts],
@@ -270,11 +278,20 @@ function ProfileScreenLoaded({
     return (
       <ProfileHeader
         profile={profile}
+        descriptionRT={hasDescription ? descriptionRT : null}
         moderationOpts={moderationOpts}
         hideBackButton={hideBackButton}
+        isPlaceholderProfile={showPlaceholder}
       />
     )
-  }, [profile, moderationOpts, hideBackButton])
+  }, [
+    profile,
+    descriptionRT,
+    hasDescription,
+    moderationOpts,
+    hideBackButton,
+    showPlaceholder,
+  ])
 
   return (
     <ScreenHider
@@ -284,7 +301,7 @@ function ProfileScreenLoaded({
       moderation={moderation.account}>
       <PagerWithHeader
         testID="profilePager"
-        isHeaderReady={true}
+        isHeaderReady={!showPlaceholder}
         items={sectionTitles}
         onPageSelected={onPageSelected}
         onCurrentPageSelected={onCurrentPageSelected}
@@ -441,6 +458,35 @@ function ProfileEndOfFeed() {
   )
 }
 
+function useRichText(text: string): [RichTextAPI, boolean] {
+  const [prevText, setPrevText] = React.useState(text)
+  const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
+  const [resolvedRT, setResolvedRT] = React.useState<RichTextAPI | null>(null)
+  if (text !== prevText) {
+    setPrevText(text)
+    setRawRT(new RichTextAPI({text}))
+    setResolvedRT(null)
+    // This will queue an immediate re-render
+  }
+  React.useEffect(() => {
+    let ignore = false
+    async function resolveRTFacets() {
+      // new each time
+      const resolvedRT = new RichTextAPI({text})
+      await resolvedRT.detectFacets(getAgent())
+      if (!ignore) {
+        setResolvedRT(resolvedRT)
+      }
+    }
+    resolveRTFacets()
+    return () => {
+      ignore = true
+    }
+  }, [text])
+  const isResolving = resolvedRT === null
+  return [resolvedRT ?? rawRT, isResolving]
+}
+
 const styles = StyleSheet.create({
   container: {
     flexDirection: 'column',
-- 
cgit 1.4.1

