Dejank navigation between thread posts (#2625)

* Dejank parent thread spinner

* Fix bottom border/spinner jank

* Revert unnecessary change
zio/stable
dan 2024-01-25 21:32:17 +00:00 committed by GitHub
parent de6b380f4e
commit ef84f3a25e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 23 additions and 45 deletions

View File

@ -45,10 +45,9 @@ import {isAndroid, isNative} from '#/platform/detection'
import {logger} from '#/logger' import {logger} from '#/logger'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 2} const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 1}
const TOP_COMPONENT = {_reactKey: '__top_component__'} const TOP_COMPONENT = {_reactKey: '__top_component__'}
const PARENT_SPINNER = {_reactKey: '__parent_spinner__'}
const REPLY_PROMPT = {_reactKey: '__reply__'} const REPLY_PROMPT = {_reactKey: '__reply__'}
const DELETED = {_reactKey: '__deleted__'} const DELETED = {_reactKey: '__deleted__'}
const BLOCKED = {_reactKey: '__blocked__'} const BLOCKED = {_reactKey: '__blocked__'}
@ -59,11 +58,9 @@ const BOTTOM_COMPONENT = {_reactKey: '__bottom_component__'}
type YieldedItem = type YieldedItem =
| ThreadPost | ThreadPost
| typeof TOP_COMPONENT | typeof TOP_COMPONENT
| typeof PARENT_SPINNER
| typeof REPLY_PROMPT | typeof REPLY_PROMPT
| typeof DELETED | typeof DELETED
| typeof BLOCKED | typeof BLOCKED
| typeof PARENT_SPINNER
export function PostThread({ export function PostThread({
uri, uri,
@ -152,7 +149,7 @@ function PostThreadLoaded({
const {hasSession} = useSession() const {hasSession} = useSession()
const {_} = useLingui() const {_} = useLingui()
const pal = usePalette('default') const pal = usePalette('default')
const {isTablet, isMobile, isDesktop, isTabletOrMobile} = useWebMediaQueries() const {isMobile, isTabletOrMobile} = useWebMediaQueries()
const ref = useRef<ListMethods>(null) const ref = useRef<ListMethods>(null)
const highlightedPostRef = useRef<View | null>(null) const highlightedPostRef = useRef<View | null>(null)
const needsScrollAdjustment = useRef<boolean>( const needsScrollAdjustment = useRef<boolean>(
@ -168,13 +165,11 @@ function PostThreadLoaded({
// construct content // construct content
const posts = React.useMemo(() => { const posts = React.useMemo(() => {
let arr = [TOP_COMPONENT].concat( let arr = Array.from(
Array.from( flattenThreadSkeleton(
flattenThreadSkeleton( sortThread(thread, threadViewPrefs),
sortThread(thread, threadViewPrefs), hasSession,
hasSession, treeView,
treeView,
),
), ),
) )
if (arr.length > maxVisible) { if (arr.length > maxVisible) {
@ -215,15 +210,9 @@ function PostThreadLoaded({
// wait for loading to finish // wait for loading to finish
if (thread.type === 'post' && !!thread.parent) { if (thread.type === 'post' && !!thread.parent) {
function onMeasure(pageY: number) { function onMeasure(pageY: number) {
let spinnerHeight = 0
if (isDesktop) {
spinnerHeight = 40
} else if (isTabletOrMobile) {
spinnerHeight = 82
}
ref.current?.scrollToOffset({ ref.current?.scrollToOffset({
animated: false, animated: false,
offset: pageY - spinnerHeight, offset: pageY,
}) })
} }
if (isNative) { if (isNative) {
@ -242,7 +231,7 @@ function PostThreadLoaded({
} }
needsScrollAdjustment.current = false needsScrollAdjustment.current = false
} }
}, [thread, isDesktop, isTabletOrMobile]) }, [thread])
const onPTR = React.useCallback(async () => { const onPTR = React.useCallback(async () => {
setIsPTRing(true) setIsPTRing(true)
@ -257,17 +246,11 @@ function PostThreadLoaded({
const renderItem = React.useCallback( const renderItem = React.useCallback(
({item, index}: {item: YieldedItem; index: number}) => { ({item, index}: {item: YieldedItem; index: number}) => {
if (item === TOP_COMPONENT) { if (item === TOP_COMPONENT) {
return isTablet ? ( return isTabletOrMobile ? (
<ViewHeader <ViewHeader
title={_(msg({message: `Post`, context: 'description'}))} title={_(msg({message: `Post`, context: 'description'}))}
/> />
) : null ) : null
} else if (item === PARENT_SPINNER) {
return (
<View style={styles.parentSpinner}>
<ActivityIndicator />
</View>
)
} else if (item === REPLY_PROMPT && hasSession) { } else if (item === REPLY_PROMPT && hasSession) {
return ( return (
<View> <View>
@ -318,7 +301,7 @@ function PostThreadLoaded({
// @ts-ignore web-only // @ts-ignore web-only
style={{ style={{
// Leave enough space below that the scroll doesn't jump // Leave enough space below that the scroll doesn't jump
height: isNative ? 400 : '100vh', height: isNative ? 600 : '100vh',
borderTopWidth: 1, borderTopWidth: 1,
borderColor: pal.colors.border, borderColor: pal.colors.border,
}} }}
@ -326,7 +309,7 @@ function PostThreadLoaded({
) )
} else if (item === CHILD_SPINNER) { } else if (item === CHILD_SPINNER) {
return ( return (
<View style={styles.childSpinner}> <View style={[pal.border, styles.childSpinner]}>
<ActivityIndicator /> <ActivityIndicator />
</View> </View>
) )
@ -361,7 +344,7 @@ function PostThreadLoaded({
}, },
[ [
hasSession, hasSession,
isTablet, isTabletOrMobile,
isMobile, isMobile,
onPressReply, onPressReply,
pal.border, pal.border,
@ -507,12 +490,15 @@ function* flattenThreadSkeleton(
node: ThreadNode, node: ThreadNode,
hasSession: boolean, hasSession: boolean,
treeView: boolean, treeView: boolean,
isTraversingReplies: boolean = false,
): Generator<YieldedItem, void> { ): Generator<YieldedItem, void> {
if (node.type === 'post') { if (node.type === 'post') {
if (node.parent) { if (!node.ctx.isParentLoading) {
yield* flattenThreadSkeleton(node.parent, hasSession, treeView) if (node.parent) {
} else if (node.ctx.isParentLoading) { yield* flattenThreadSkeleton(node.parent, hasSession, treeView, false)
yield PARENT_SPINNER } else if (!isTraversingReplies) {
yield TOP_COMPONENT
}
} }
if (!hasSession && node.ctx.depth > 0 && hasPwiOptOut(node)) { if (!hasSession && node.ctx.depth > 0 && hasPwiOptOut(node)) {
return return
@ -523,7 +509,7 @@ function* flattenThreadSkeleton(
} }
if (node.replies?.length) { if (node.replies?.length) {
for (const reply of node.replies) { for (const reply of node.replies) {
yield* flattenThreadSkeleton(reply, hasSession, treeView) yield* flattenThreadSkeleton(reply, hasSession, treeView, true)
if (!treeView && !node.ctx.isHighlightedPost) { if (!treeView && !node.ctx.isHighlightedPost) {
break break
} }
@ -567,10 +553,9 @@ const styles = StyleSheet.create({
paddingHorizontal: 18, paddingHorizontal: 18,
paddingVertical: 18, paddingVertical: 18,
}, },
parentSpinner: {
paddingVertical: 10,
},
childSpinner: { childSpinner: {
borderTopWidth: 1,
paddingTop: 40,
paddingBottom: 200, paddingBottom: 200,
}, },
}) })

View File

@ -5,7 +5,6 @@ import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {makeRecordUri} from 'lib/strings/url-helpers' import {makeRecordUri} from 'lib/strings/url-helpers'
import {ViewHeader} from '../com/util/ViewHeader'
import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread' import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
import {ComposePrompt} from 'view/com/composer/Prompt' import {ComposePrompt} from 'view/com/composer/Prompt'
import {s} from 'lib/styles' import {s} from 'lib/styles'
@ -18,8 +17,6 @@ import {clamp} from 'lodash'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useResolveUriQuery} from '#/state/queries/resolve-uri' import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {ErrorMessage} from '../com/util/error/ErrorMessage' import {ErrorMessage} from '../com/util/error/ErrorMessage'
import {CenteredView} from '../com/util/Views' import {CenteredView} from '../com/util/Views'
@ -30,7 +27,6 @@ import {isWeb} from '#/platform/detection'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
export function PostThreadScreen({route}: Props) { export function PostThreadScreen({route}: Props) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {_} = useLingui()
const {hasSession} = useSession() const {hasSession} = useSession()
const {fabMinimalShellTransform} = useMinimalShellMode() const {fabMinimalShellTransform} = useMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
@ -79,9 +75,6 @@ export function PostThreadScreen({route}: Props) {
return ( return (
<View style={s.hContentRegion}> <View style={s.hContentRegion}>
{isMobile && (
<ViewHeader title={_(msg({message: 'Post', context: 'description'}))} />
)}
<View style={s.flex1}> <View style={s.flex1}>
{uriError ? ( {uriError ? (
<CenteredView> <CenteredView>