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
This commit is contained in:
		
							parent
							
								
									0d00c7d851
								
							
						
					
					
						commit
						d36b91fe67
					
				
					 6 changed files with 141 additions and 135 deletions
				
			
		
							
								
								
									
										24
									
								
								src/state/cache/profile-shadow.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								src/state/cache/profile-shadow.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -22,15 +22,15 @@ export interface ProfileShadow { | |||
|   blockingUri: string | undefined | ||||
| } | ||||
| 
 | ||||
| type ProfileView = | ||||
|   | AppBskyActorDefs.ProfileView | ||||
|   | AppBskyActorDefs.ProfileViewBasic | ||||
|   | AppBskyActorDefs.ProfileViewDetailed | ||||
| 
 | ||||
| const shadows: WeakMap<ProfileView, Partial<ProfileShadow>> = new WeakMap() | ||||
| const shadows: WeakMap< | ||||
|   AppBskyActorDefs.ProfileView, | ||||
|   Partial<ProfileShadow> | ||||
| > = new WeakMap() | ||||
| const emitter = new EventEmitter() | ||||
| 
 | ||||
| export function useProfileShadow(profile: ProfileView): Shadow<ProfileView> { | ||||
| export function useProfileShadow< | ||||
|   TProfileView extends AppBskyActorDefs.ProfileView, | ||||
| >(profile: TProfileView): Shadow<TProfileView> { | ||||
|   const [shadow, setShadow] = useState(() => shadows.get(profile)) | ||||
|   const [prevPost, setPrevPost] = useState(profile) | ||||
|   if (profile !== prevPost) { | ||||
|  | @ -70,10 +70,10 @@ export function updateProfileShadow( | |||
|   }) | ||||
| } | ||||
| 
 | ||||
| function mergeShadow( | ||||
|   profile: ProfileView, | ||||
| function mergeShadow<TProfileView extends AppBskyActorDefs.ProfileView>( | ||||
|   profile: TProfileView, | ||||
|   shadow: Partial<ProfileShadow>, | ||||
| ): Shadow<ProfileView> { | ||||
| ): Shadow<TProfileView> { | ||||
|   return castAsShadow({ | ||||
|     ...profile, | ||||
|     viewer: { | ||||
|  | @ -89,7 +89,9 @@ function mergeShadow( | |||
|   }) | ||||
| } | ||||
| 
 | ||||
| function* findProfilesInCache(did: string): Generator<ProfileView, void> { | ||||
| function* findProfilesInCache( | ||||
|   did: string, | ||||
| ): Generator<AppBskyActorDefs.ProfileView, void> { | ||||
|   yield* findAllProfilesInListMembersQueryData(queryClient, did) | ||||
|   yield* findAllProfilesInMyBlockedAccountsQueryData(queryClient, did) | ||||
|   yield* findAllProfilesInMyMutedAccountsQueryData(queryClient, did) | ||||
|  |  | |||
|  | @ -61,25 +61,21 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( | |||
|     const headerHeight = headerOnlyHeight + tabBarHeight | ||||
| 
 | ||||
|     // capture the header bar sizing
 | ||||
|     const onTabBarLayout = React.useCallback( | ||||
|     const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => { | ||||
|       const height = evt.nativeEvent.layout.height | ||||
|       if (height > 0) { | ||||
|         // The rounding is necessary to prevent jumps on iOS
 | ||||
|         setTabBarHeight(Math.round(height)) | ||||
|       } | ||||
|     }) | ||||
|     const onHeaderOnlyLayout = useNonReactiveCallback( | ||||
|       (evt: LayoutChangeEvent) => { | ||||
|         const height = evt.nativeEvent.layout.height | ||||
|         if (height > 0) { | ||||
|           // The rounding is necessary to prevent jumps on iOS
 | ||||
|           setTabBarHeight(Math.round(height)) | ||||
|         } | ||||
|       }, | ||||
|       [setTabBarHeight], | ||||
|     ) | ||||
|     const onHeaderOnlyLayout = React.useCallback( | ||||
|       (evt: LayoutChangeEvent) => { | ||||
|         const height = evt.nativeEvent.layout.height | ||||
|         if (height > 0) { | ||||
|         if (height > 0 && isHeaderReady) { | ||||
|           // The rounding is necessary to prevent jumps on iOS
 | ||||
|           setHeaderOnlyHeight(Math.round(height)) | ||||
|         } | ||||
|       }, | ||||
|       [setHeaderOnlyHeight], | ||||
|     ) | ||||
| 
 | ||||
|     const renderTabBar = React.useCallback( | ||||
|  |  | |||
|  | @ -31,6 +31,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( | |||
|       children, | ||||
|       testID, | ||||
|       items, | ||||
|       isHeaderReady, | ||||
|       renderHeader, | ||||
|       initialPage, | ||||
|       onPageSelected, | ||||
|  | @ -46,6 +47,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( | |||
|           <PagerTabBar | ||||
|             items={items} | ||||
|             renderHeader={renderHeader} | ||||
|             isHeaderReady={isHeaderReady} | ||||
|             currentPage={currentPage} | ||||
|             onCurrentPageSelected={onCurrentPageSelected} | ||||
|             onSelect={props.onSelect} | ||||
|  | @ -54,7 +56,14 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( | |||
|           /> | ||||
|         ) | ||||
|       }, | ||||
|       [items, renderHeader, currentPage, onCurrentPageSelected, testID], | ||||
|       [ | ||||
|         items, | ||||
|         isHeaderReady, | ||||
|         renderHeader, | ||||
|         currentPage, | ||||
|         onCurrentPageSelected, | ||||
|         testID, | ||||
|       ], | ||||
|     ) | ||||
| 
 | ||||
|     const onPageSelectedInner = React.useCallback( | ||||
|  | @ -80,8 +89,14 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( | |||
|         {toArray(children) | ||||
|           .filter(Boolean) | ||||
|           .map((child, i) => { | ||||
|             const isReady = isHeaderReady | ||||
|             return ( | ||||
|               <View key={i} collapsable={false}> | ||||
|               <View | ||||
|                 key={i} | ||||
|                 collapsable={false} | ||||
|                 style={{ | ||||
|                   display: isReady ? undefined : 'none', | ||||
|                 }}> | ||||
|                 <PagerItem isFocused={i === currentPage} renderTab={child} /> | ||||
|               </View> | ||||
|             ) | ||||
|  | @ -94,6 +109,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( | |||
| let PagerTabBar = ({ | ||||
|   currentPage, | ||||
|   items, | ||||
|   isHeaderReady, | ||||
|   testID, | ||||
|   renderHeader, | ||||
|   onCurrentPageSelected, | ||||
|  | @ -104,6 +120,7 @@ let PagerTabBar = ({ | |||
|   items: string[] | ||||
|   testID?: string | ||||
|   renderHeader?: () => JSX.Element | ||||
|   isHeaderReady: boolean | ||||
|   onCurrentPageSelected?: (index: number) => void | ||||
|   onSelect?: (index: number) => void | ||||
|   tabBarAnchor?: JSX.Element | null | undefined | ||||
|  | @ -112,7 +129,12 @@ let PagerTabBar = ({ | |||
|   const {isMobile} = useWebMediaQueries() | ||||
|   return ( | ||||
|     <> | ||||
|       <View style={[!isMobile && styles.headerContainerDesktop, pal.border]}> | ||||
|       <View | ||||
|         style={[ | ||||
|           !isMobile && styles.headerContainerDesktop, | ||||
|           pal.border, | ||||
|           !isHeaderReady && styles.loadingHeader, | ||||
|         ]}> | ||||
|         {renderHeader?.()} | ||||
|       </View> | ||||
|       {tabBarAnchor} | ||||
|  | @ -123,6 +145,9 @@ let PagerTabBar = ({ | |||
|             ? styles.tabBarContainerMobile | ||||
|             : styles.tabBarContainerDesktop, | ||||
|           pal.border, | ||||
|           { | ||||
|             display: isHeaderReady ? undefined : 'none', | ||||
|           }, | ||||
|         ]}> | ||||
|         <TabBar | ||||
|           testID={testID} | ||||
|  | @ -183,6 +208,9 @@ const styles = StyleSheet.create({ | |||
|     paddingLeft: 14, | ||||
|     paddingRight: 14, | ||||
|   }, | ||||
|   loadingHeader: { | ||||
|     borderColor: 'transparent', | ||||
|   }, | ||||
| }) | ||||
| 
 | ||||
| function toArray<T>(v: T | T[]): T[] { | ||||
|  |  | |||
|  | @ -51,76 +51,47 @@ import {sanitizeDisplayName} from 'lib/strings/display-names' | |||
| import {shareUrl} from 'lib/sharing' | ||||
| import {s, colors} from 'lib/styles' | ||||
| import {logger} from '#/logger' | ||||
| import {useSession, getAgent} from '#/state/session' | ||||
| import {useSession} 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: AppBskyActorDefs.ProfileView | null | ||||
|   placeholderData?: AppBskyActorDefs.ProfileView | null | ||||
|   moderationOpts: ModerationOpts | null | ||||
|   hideBackButton?: boolean | ||||
|   isProfilePreview?: boolean | ||||
| } | ||||
| 
 | ||||
| export function ProfileHeader({ | ||||
|   profile, | ||||
|   moderationOpts, | ||||
|   hideBackButton = false, | ||||
|   isProfilePreview, | ||||
| }: Props) { | ||||
| let ProfileHeaderLoading = (_props: {}): React.ReactNode => { | ||||
|   const pal = usePalette('default') | ||||
| 
 | ||||
|   // loading
 | ||||
|   // =
 | ||||
|   if (!profile || !moderationOpts) { | ||||
|     return ( | ||||
|       <View style={pal.view}> | ||||
|         <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} /> | ||||
|         </View> | ||||
|         <View style={styles.content}> | ||||
|           <View style={[styles.buttonsLine]}> | ||||
|             <LoadingPlaceholder width={167} height={31} style={styles.br50} /> | ||||
|           </View> | ||||
|   return ( | ||||
|     <View style={pal.view}> | ||||
|       <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} /> | ||||
|       </View> | ||||
|       <View style={styles.content}> | ||||
|         <View style={[styles.buttonsLine]}> | ||||
|           <LoadingPlaceholder width={167} height={31} style={styles.br50} /> | ||||
|         </View> | ||||
|       </View> | ||||
|     ) | ||||
|   } | ||||
| 
 | ||||
|   // loaded
 | ||||
|   // =
 | ||||
|   return ( | ||||
|     <ProfileHeaderLoaded | ||||
|       profile={profile} | ||||
|       moderationOpts={moderationOpts} | ||||
|       hideBackButton={hideBackButton} | ||||
|       isProfilePreview={isProfilePreview} | ||||
|     /> | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
| ProfileHeaderLoading = memo(ProfileHeaderLoading) | ||||
| export {ProfileHeaderLoading} | ||||
| 
 | ||||
| interface LoadedProps { | ||||
| interface Props { | ||||
|   profile: AppBskyActorDefs.ProfileViewDetailed | ||||
|   descriptionRT: RichTextAPI | null | ||||
|   moderationOpts: ModerationOpts | ||||
|   hideBackButton?: boolean | ||||
|   isProfilePreview?: boolean | ||||
|   isPlaceholderProfile?: boolean | ||||
| } | ||||
| 
 | ||||
| let ProfileHeaderLoaded = ({ | ||||
| let ProfileHeader = ({ | ||||
|   profile: profileUnshadowed, | ||||
|   descriptionRT, | ||||
|   moderationOpts, | ||||
|   hideBackButton = false, | ||||
|   isProfilePreview, | ||||
| }: LoadedProps): React.ReactNode => { | ||||
|   isPlaceholderProfile, | ||||
| }: Props): React.ReactNode => { | ||||
|   const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> = | ||||
|     useProfileShadow(profileUnshadowed) | ||||
|   const pal = usePalette('default') | ||||
|  | @ -144,37 +115,6 @@ let ProfileHeaderLoaded = ({ | |||
|     [profile, moderationOpts], | ||||
|   ) | ||||
| 
 | ||||
|   /* | ||||
|    * BEGIN handle bio facet resolution | ||||
|    */ | ||||
|   // should be undefined on first render to trigger a resolution
 | ||||
|   const prevProfileDescription = React.useRef<string | undefined>() | ||||
|   const [descriptionRT, setDescriptionRT] = React.useState< | ||||
|     RichTextAPI | undefined | ||||
|   >( | ||||
|     profile.description | ||||
|       ? new RichTextAPI({text: profile.description}) | ||||
|       : undefined, | ||||
|   ) | ||||
|   React.useEffect(() => { | ||||
|     async function resolveRTFacets() { | ||||
|       // new each time
 | ||||
|       const rt = new RichTextAPI({text: profile.description || ''}) | ||||
|       await rt.detectFacets(getAgent()) | ||||
|       // replace existing RT instance
 | ||||
|       setDescriptionRT(rt) | ||||
|     } | ||||
| 
 | ||||
|     if (profile.description !== prevProfileDescription.current) { | ||||
|       // update prev immediately
 | ||||
|       prevProfileDescription.current = profile.description | ||||
|       resolveRTFacets() | ||||
|     } | ||||
|   }, [profile.description, setDescriptionRT]) | ||||
|   /* | ||||
|    * END handle bio facet resolution | ||||
|    */ | ||||
| 
 | ||||
|   const invalidateProfileQuery = React.useCallback(() => { | ||||
|     queryClient.invalidateQueries({ | ||||
|       queryKey: profileQueryKey(profile.did), | ||||
|  | @ -454,14 +394,9 @@ let ProfileHeaderLoaded = ({ | |||
|   const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower') | ||||
| 
 | ||||
|   return ( | ||||
|     <View | ||||
|       style={[ | ||||
|         pal.view, | ||||
|         isProfilePreview && isDesktop && styles.loadingBorderStyle, | ||||
|       ]} | ||||
|       pointerEvents="box-none"> | ||||
|     <View style={[pal.view]} pointerEvents="box-none"> | ||||
|       <View pointerEvents="none"> | ||||
|         {isProfilePreview ? ( | ||||
|         {isPlaceholderProfile ? ( | ||||
|           <LoadingPlaceholder | ||||
|             width="100%" | ||||
|             height={150} | ||||
|  | @ -622,7 +557,7 @@ let ProfileHeaderLoaded = ({ | |||
|             {invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`} | ||||
|           </ThemedText> | ||||
|         </View> | ||||
|         {!isProfilePreview && !blockHide && ( | ||||
|         {!isPlaceholderProfile && !blockHide && ( | ||||
|           <> | ||||
|             <View style={styles.metricsLine} pointerEvents="box-none"> | ||||
|               <Link | ||||
|  | @ -737,7 +672,8 @@ let ProfileHeaderLoaded = ({ | |||
|     </View> | ||||
|   ) | ||||
| } | ||||
| ProfileHeaderLoaded = memo(ProfileHeaderLoaded) | ||||
| ProfileHeader = memo(ProfileHeader) | ||||
| export {ProfileHeader} | ||||
| 
 | ||||
| const styles = StyleSheet.create({ | ||||
|   banner: { | ||||
|  | @ -845,9 +781,4 @@ const styles = StyleSheet.create({ | |||
| 
 | ||||
|   br40: {borderRadius: 40}, | ||||
|   br50: {borderRadius: 50}, | ||||
| 
 | ||||
|   loadingBorderStyle: { | ||||
|     borderLeftWidth: 1, | ||||
|     borderRightWidth: 1, | ||||
|   }, | ||||
| }) | ||||
|  |  | |||
|  | @ -123,6 +123,7 @@ let UserAvatar = ({ | |||
|   usePlainRNImage = false, | ||||
| }: UserAvatarProps): React.ReactNode => { | ||||
|   const pal = usePalette('default') | ||||
|   const backgroundColor = pal.colors.backgroundLight | ||||
| 
 | ||||
|   const aviStyle = useMemo(() => { | ||||
|     if (type === 'algo' || type === 'list') { | ||||
|  | @ -130,14 +131,16 @@ let UserAvatar = ({ | |||
|         width: size, | ||||
|         height: size, | ||||
|         borderRadius: size > 32 ? 8 : 3, | ||||
|         backgroundColor, | ||||
|       } | ||||
|     } | ||||
|     return { | ||||
|       width: size, | ||||
|       height: size, | ||||
|       borderRadius: Math.floor(size / 2), | ||||
|       backgroundColor, | ||||
|     } | ||||
|   }, [type, size]) | ||||
|   }, [type, size, backgroundColor]) | ||||
| 
 | ||||
|   const alert = useMemo(() => { | ||||
|     if (!moderation?.alert) { | ||||
|  |  | |||
|  | @ -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', | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue