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:
dan 2024-02-09 05:00:50 +00:00 committed by GitHub
parent 0d00c7d851
commit d36b91fe67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 141 additions and 135 deletions

View file

@ -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(

View file

@ -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[] {

View file

@ -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,
},
})

View file

@ -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) {