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 unusedzio/stable
parent
0d00c7d851
commit
d36b91fe67
|
@ -22,15 +22,15 @@ export interface ProfileShadow {
|
||||||
blockingUri: string | undefined
|
blockingUri: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfileView =
|
const shadows: WeakMap<
|
||||||
| AppBskyActorDefs.ProfileView
|
AppBskyActorDefs.ProfileView,
|
||||||
| AppBskyActorDefs.ProfileViewBasic
|
Partial<ProfileShadow>
|
||||||
| AppBskyActorDefs.ProfileViewDetailed
|
> = new WeakMap()
|
||||||
|
|
||||||
const shadows: WeakMap<ProfileView, Partial<ProfileShadow>> = new WeakMap()
|
|
||||||
const emitter = new EventEmitter()
|
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 [shadow, setShadow] = useState(() => shadows.get(profile))
|
||||||
const [prevPost, setPrevPost] = useState(profile)
|
const [prevPost, setPrevPost] = useState(profile)
|
||||||
if (profile !== prevPost) {
|
if (profile !== prevPost) {
|
||||||
|
@ -70,10 +70,10 @@ export function updateProfileShadow(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeShadow(
|
function mergeShadow<TProfileView extends AppBskyActorDefs.ProfileView>(
|
||||||
profile: ProfileView,
|
profile: TProfileView,
|
||||||
shadow: Partial<ProfileShadow>,
|
shadow: Partial<ProfileShadow>,
|
||||||
): Shadow<ProfileView> {
|
): Shadow<TProfileView> {
|
||||||
return castAsShadow({
|
return castAsShadow({
|
||||||
...profile,
|
...profile,
|
||||||
viewer: {
|
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* findAllProfilesInListMembersQueryData(queryClient, did)
|
||||||
yield* findAllProfilesInMyBlockedAccountsQueryData(queryClient, did)
|
yield* findAllProfilesInMyBlockedAccountsQueryData(queryClient, did)
|
||||||
yield* findAllProfilesInMyMutedAccountsQueryData(queryClient, did)
|
yield* findAllProfilesInMyMutedAccountsQueryData(queryClient, did)
|
||||||
|
|
|
@ -61,25 +61,21 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
||||||
const headerHeight = headerOnlyHeight + tabBarHeight
|
const headerHeight = headerOnlyHeight + tabBarHeight
|
||||||
|
|
||||||
// capture the header bar sizing
|
// 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) => {
|
(evt: LayoutChangeEvent) => {
|
||||||
const height = evt.nativeEvent.layout.height
|
const height = evt.nativeEvent.layout.height
|
||||||
if (height > 0) {
|
if (height > 0 && isHeaderReady) {
|
||||||
// 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) {
|
|
||||||
// The rounding is necessary to prevent jumps on iOS
|
// The rounding is necessary to prevent jumps on iOS
|
||||||
setHeaderOnlyHeight(Math.round(height))
|
setHeaderOnlyHeight(Math.round(height))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setHeaderOnlyHeight],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderTabBar = React.useCallback(
|
const renderTabBar = React.useCallback(
|
||||||
|
|
|
@ -31,6 +31,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
||||||
children,
|
children,
|
||||||
testID,
|
testID,
|
||||||
items,
|
items,
|
||||||
|
isHeaderReady,
|
||||||
renderHeader,
|
renderHeader,
|
||||||
initialPage,
|
initialPage,
|
||||||
onPageSelected,
|
onPageSelected,
|
||||||
|
@ -46,6 +47,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
||||||
<PagerTabBar
|
<PagerTabBar
|
||||||
items={items}
|
items={items}
|
||||||
renderHeader={renderHeader}
|
renderHeader={renderHeader}
|
||||||
|
isHeaderReady={isHeaderReady}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
onCurrentPageSelected={onCurrentPageSelected}
|
onCurrentPageSelected={onCurrentPageSelected}
|
||||||
onSelect={props.onSelect}
|
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(
|
const onPageSelectedInner = React.useCallback(
|
||||||
|
@ -80,8 +89,14 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
||||||
{toArray(children)
|
{toArray(children)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((child, i) => {
|
.map((child, i) => {
|
||||||
|
const isReady = isHeaderReady
|
||||||
return (
|
return (
|
||||||
<View key={i} collapsable={false}>
|
<View
|
||||||
|
key={i}
|
||||||
|
collapsable={false}
|
||||||
|
style={{
|
||||||
|
display: isReady ? undefined : 'none',
|
||||||
|
}}>
|
||||||
<PagerItem isFocused={i === currentPage} renderTab={child} />
|
<PagerItem isFocused={i === currentPage} renderTab={child} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
@ -94,6 +109,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
||||||
let PagerTabBar = ({
|
let PagerTabBar = ({
|
||||||
currentPage,
|
currentPage,
|
||||||
items,
|
items,
|
||||||
|
isHeaderReady,
|
||||||
testID,
|
testID,
|
||||||
renderHeader,
|
renderHeader,
|
||||||
onCurrentPageSelected,
|
onCurrentPageSelected,
|
||||||
|
@ -104,6 +120,7 @@ let PagerTabBar = ({
|
||||||
items: string[]
|
items: string[]
|
||||||
testID?: string
|
testID?: string
|
||||||
renderHeader?: () => JSX.Element
|
renderHeader?: () => JSX.Element
|
||||||
|
isHeaderReady: boolean
|
||||||
onCurrentPageSelected?: (index: number) => void
|
onCurrentPageSelected?: (index: number) => void
|
||||||
onSelect?: (index: number) => void
|
onSelect?: (index: number) => void
|
||||||
tabBarAnchor?: JSX.Element | null | undefined
|
tabBarAnchor?: JSX.Element | null | undefined
|
||||||
|
@ -112,7 +129,12 @@ let PagerTabBar = ({
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View style={[!isMobile && styles.headerContainerDesktop, pal.border]}>
|
<View
|
||||||
|
style={[
|
||||||
|
!isMobile && styles.headerContainerDesktop,
|
||||||
|
pal.border,
|
||||||
|
!isHeaderReady && styles.loadingHeader,
|
||||||
|
]}>
|
||||||
{renderHeader?.()}
|
{renderHeader?.()}
|
||||||
</View>
|
</View>
|
||||||
{tabBarAnchor}
|
{tabBarAnchor}
|
||||||
|
@ -123,6 +145,9 @@ let PagerTabBar = ({
|
||||||
? styles.tabBarContainerMobile
|
? styles.tabBarContainerMobile
|
||||||
: styles.tabBarContainerDesktop,
|
: styles.tabBarContainerDesktop,
|
||||||
pal.border,
|
pal.border,
|
||||||
|
{
|
||||||
|
display: isHeaderReady ? undefined : 'none',
|
||||||
|
},
|
||||||
]}>
|
]}>
|
||||||
<TabBar
|
<TabBar
|
||||||
testID={testID}
|
testID={testID}
|
||||||
|
@ -183,6 +208,9 @@ const styles = StyleSheet.create({
|
||||||
paddingLeft: 14,
|
paddingLeft: 14,
|
||||||
paddingRight: 14,
|
paddingRight: 14,
|
||||||
},
|
},
|
||||||
|
loadingHeader: {
|
||||||
|
borderColor: 'transparent',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function toArray<T>(v: T | T[]): T[] {
|
function toArray<T>(v: T | T[]): T[] {
|
||||||
|
|
|
@ -51,76 +51,47 @@ import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
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'
|
||||||
import {useSession, getAgent} from '#/state/session'
|
import {useSession} 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'
|
import {useProfileShadow} from 'state/cache/profile-shadow'
|
||||||
|
|
||||||
interface Props {
|
let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
|
||||||
profile: AppBskyActorDefs.ProfileView | null
|
|
||||||
placeholderData?: AppBskyActorDefs.ProfileView | null
|
|
||||||
moderationOpts: ModerationOpts | null
|
|
||||||
hideBackButton?: boolean
|
|
||||||
isProfilePreview?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProfileHeader({
|
|
||||||
profile,
|
|
||||||
moderationOpts,
|
|
||||||
hideBackButton = false,
|
|
||||||
isProfilePreview,
|
|
||||||
}: Props) {
|
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
return (
|
||||||
// loading
|
<View style={pal.view}>
|
||||||
// =
|
<LoadingPlaceholder width="100%" height={150} style={{borderRadius: 0}} />
|
||||||
if (!profile || !moderationOpts) {
|
<View
|
||||||
return (
|
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
|
||||||
<View style={pal.view}>
|
<LoadingPlaceholder width={80} height={80} style={styles.br40} />
|
||||||
<LoadingPlaceholder
|
</View>
|
||||||
width="100%"
|
<View style={styles.content}>
|
||||||
height={150}
|
<View style={[styles.buttonsLine]}>
|
||||||
style={{borderRadius: 0}}
|
<LoadingPlaceholder width={167} height={31} style={styles.br50} />
|
||||||
/>
|
|
||||||
<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>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
</View>
|
||||||
}
|
|
||||||
|
|
||||||
// loaded
|
|
||||||
// =
|
|
||||||
return (
|
|
||||||
<ProfileHeaderLoaded
|
|
||||||
profile={profile}
|
|
||||||
moderationOpts={moderationOpts}
|
|
||||||
hideBackButton={hideBackButton}
|
|
||||||
isProfilePreview={isProfilePreview}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
ProfileHeaderLoading = memo(ProfileHeaderLoading)
|
||||||
|
export {ProfileHeaderLoading}
|
||||||
|
|
||||||
interface LoadedProps {
|
interface Props {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
|
descriptionRT: RichTextAPI | null
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
hideBackButton?: boolean
|
hideBackButton?: boolean
|
||||||
isProfilePreview?: boolean
|
isPlaceholderProfile?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
let ProfileHeaderLoaded = ({
|
let ProfileHeader = ({
|
||||||
profile: profileUnshadowed,
|
profile: profileUnshadowed,
|
||||||
|
descriptionRT,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
hideBackButton = false,
|
hideBackButton = false,
|
||||||
isProfilePreview,
|
isPlaceholderProfile,
|
||||||
}: LoadedProps): React.ReactNode => {
|
}: Props): React.ReactNode => {
|
||||||
const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
|
const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
|
||||||
useProfileShadow(profileUnshadowed)
|
useProfileShadow(profileUnshadowed)
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
@ -144,37 +115,6 @@ let ProfileHeaderLoaded = ({
|
||||||
[profile, moderationOpts],
|
[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(() => {
|
const invalidateProfileQuery = React.useCallback(() => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: profileQueryKey(profile.did),
|
queryKey: profileQueryKey(profile.did),
|
||||||
|
@ -454,14 +394,9 @@ let ProfileHeaderLoaded = ({
|
||||||
const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
|
const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View style={[pal.view]} pointerEvents="box-none">
|
||||||
style={[
|
|
||||||
pal.view,
|
|
||||||
isProfilePreview && isDesktop && styles.loadingBorderStyle,
|
|
||||||
]}
|
|
||||||
pointerEvents="box-none">
|
|
||||||
<View pointerEvents="none">
|
<View pointerEvents="none">
|
||||||
{isProfilePreview ? (
|
{isPlaceholderProfile ? (
|
||||||
<LoadingPlaceholder
|
<LoadingPlaceholder
|
||||||
width="100%"
|
width="100%"
|
||||||
height={150}
|
height={150}
|
||||||
|
@ -622,7 +557,7 @@ let ProfileHeaderLoaded = ({
|
||||||
{invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`}
|
{invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
{!isProfilePreview && !blockHide && (
|
{!isPlaceholderProfile && !blockHide && (
|
||||||
<>
|
<>
|
||||||
<View style={styles.metricsLine} pointerEvents="box-none">
|
<View style={styles.metricsLine} pointerEvents="box-none">
|
||||||
<Link
|
<Link
|
||||||
|
@ -737,7 +672,8 @@ let ProfileHeaderLoaded = ({
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ProfileHeaderLoaded = memo(ProfileHeaderLoaded)
|
ProfileHeader = memo(ProfileHeader)
|
||||||
|
export {ProfileHeader}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
banner: {
|
banner: {
|
||||||
|
@ -845,9 +781,4 @@ const styles = StyleSheet.create({
|
||||||
|
|
||||||
br40: {borderRadius: 40},
|
br40: {borderRadius: 40},
|
||||||
br50: {borderRadius: 50},
|
br50: {borderRadius: 50},
|
||||||
|
|
||||||
loadingBorderStyle: {
|
|
||||||
borderLeftWidth: 1,
|
|
||||||
borderRightWidth: 1,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -123,6 +123,7 @@ let UserAvatar = ({
|
||||||
usePlainRNImage = false,
|
usePlainRNImage = false,
|
||||||
}: UserAvatarProps): React.ReactNode => {
|
}: UserAvatarProps): React.ReactNode => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const backgroundColor = pal.colors.backgroundLight
|
||||||
|
|
||||||
const aviStyle = useMemo(() => {
|
const aviStyle = useMemo(() => {
|
||||||
if (type === 'algo' || type === 'list') {
|
if (type === 'algo' || type === 'list') {
|
||||||
|
@ -130,14 +131,16 @@ let UserAvatar = ({
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
borderRadius: size > 32 ? 8 : 3,
|
borderRadius: size > 32 ? 8 : 3,
|
||||||
|
backgroundColor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
borderRadius: Math.floor(size / 2),
|
borderRadius: Math.floor(size / 2),
|
||||||
|
backgroundColor,
|
||||||
}
|
}
|
||||||
}, [type, size])
|
}, [type, size, backgroundColor])
|
||||||
|
|
||||||
const alert = useMemo(() => {
|
const alert = useMemo(() => {
|
||||||
if (!moderation?.alert) {
|
if (!moderation?.alert) {
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import React, {useMemo} from 'react'
|
import React, {useMemo} from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
import {StyleSheet, View} from 'react-native'
|
||||||
import {useFocusEffect} from '@react-navigation/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 {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
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 {Feed} from 'view/com/posts/Feed'
|
||||||
import {ProfileLists} from '../com/lists/ProfileLists'
|
import {ProfileLists} from '../com/lists/ProfileLists'
|
||||||
import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
|
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 {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
|
||||||
import {ErrorScreen} from '../com/util/error/ErrorScreen'
|
import {ErrorScreen} from '../com/util/error/ErrorScreen'
|
||||||
import {EmptyState} from '../com/util/EmptyState'
|
import {EmptyState} from '../com/util/EmptyState'
|
||||||
|
@ -28,7 +33,7 @@ import {
|
||||||
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
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 {useModerationOpts} from '#/state/queries/preferences'
|
||||||
import {useProfileExtraInfoQuery} from '#/state/queries/profile-extra-info'
|
import {useProfileExtraInfoQuery} from '#/state/queries/profile-extra-info'
|
||||||
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
||||||
|
@ -87,14 +92,10 @@ export function ProfileScreen({route}: Props) {
|
||||||
}, [profile?.viewer?.blockedBy, resolvedDid])
|
}, [profile?.viewer?.blockedBy, resolvedDid])
|
||||||
|
|
||||||
// Most pushes will happen here, since we will have only placeholder data
|
// Most pushes will happen here, since we will have only placeholder data
|
||||||
if (isLoadingDid || isLoadingProfile || isPlaceholderProfile) {
|
if (isLoadingDid || isLoadingProfile) {
|
||||||
return (
|
return (
|
||||||
<CenteredView>
|
<CenteredView>
|
||||||
<ProfileHeader
|
<ProfileHeaderLoading />
|
||||||
profile={profile ?? null}
|
|
||||||
moderationOpts={moderationOpts ?? null}
|
|
||||||
isProfilePreview={true}
|
|
||||||
/>
|
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -114,6 +115,7 @@ export function ProfileScreen({route}: Props) {
|
||||||
<ProfileScreenLoaded
|
<ProfileScreenLoaded
|
||||||
profile={profile}
|
profile={profile}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
|
isPlaceholderProfile={isPlaceholderProfile}
|
||||||
hideBackButton={!!route.params.hideBackButton}
|
hideBackButton={!!route.params.hideBackButton}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -132,12 +134,14 @@ export function ProfileScreen({route}: Props) {
|
||||||
|
|
||||||
function ProfileScreenLoaded({
|
function ProfileScreenLoaded({
|
||||||
profile: profileUnshadowed,
|
profile: profileUnshadowed,
|
||||||
|
isPlaceholderProfile,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
hideBackButton,
|
hideBackButton,
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
hideBackButton: boolean
|
hideBackButton: boolean
|
||||||
|
isPlaceholderProfile: boolean
|
||||||
}) {
|
}) {
|
||||||
const profile = useProfileShadow(profileUnshadowed)
|
const profile = useProfileShadow(profileUnshadowed)
|
||||||
const {hasSession, currentAccount} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
|
@ -157,6 +161,10 @@ function ProfileScreenLoaded({
|
||||||
|
|
||||||
useSetTitle(combinedDisplayName(profile))
|
useSetTitle(combinedDisplayName(profile))
|
||||||
|
|
||||||
|
const description = profile.description ?? ''
|
||||||
|
const hasDescription = description !== ''
|
||||||
|
const [descriptionRT, isResolvingDescriptionRT] = useRichText(description)
|
||||||
|
const showPlaceholder = isPlaceholderProfile || isResolvingDescriptionRT
|
||||||
const moderation = useMemo(
|
const moderation = useMemo(
|
||||||
() => moderateProfile(profile, moderationOpts),
|
() => moderateProfile(profile, moderationOpts),
|
||||||
[profile, moderationOpts],
|
[profile, moderationOpts],
|
||||||
|
@ -270,11 +278,20 @@ function ProfileScreenLoaded({
|
||||||
return (
|
return (
|
||||||
<ProfileHeader
|
<ProfileHeader
|
||||||
profile={profile}
|
profile={profile}
|
||||||
|
descriptionRT={hasDescription ? descriptionRT : null}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
hideBackButton={hideBackButton}
|
hideBackButton={hideBackButton}
|
||||||
|
isPlaceholderProfile={showPlaceholder}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}, [profile, moderationOpts, hideBackButton])
|
}, [
|
||||||
|
profile,
|
||||||
|
descriptionRT,
|
||||||
|
hasDescription,
|
||||||
|
moderationOpts,
|
||||||
|
hideBackButton,
|
||||||
|
showPlaceholder,
|
||||||
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenHider
|
<ScreenHider
|
||||||
|
@ -284,7 +301,7 @@ function ProfileScreenLoaded({
|
||||||
moderation={moderation.account}>
|
moderation={moderation.account}>
|
||||||
<PagerWithHeader
|
<PagerWithHeader
|
||||||
testID="profilePager"
|
testID="profilePager"
|
||||||
isHeaderReady={true}
|
isHeaderReady={!showPlaceholder}
|
||||||
items={sectionTitles}
|
items={sectionTitles}
|
||||||
onPageSelected={onPageSelected}
|
onPageSelected={onPageSelected}
|
||||||
onCurrentPageSelected={onCurrentPageSelected}
|
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({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
|
Loading…
Reference in New Issue