Fix labeler header scroll and loading/error states (#8088)

* add forwardRef to Layout.Content

* lift scrollview up out of inner component

* fix scrolling on android (#8188)
This commit is contained in:
Samuel Newman
2025-04-14 19:00:39 +03:00
committed by GitHub
parent 238b00d193
commit f8bd850465
3 changed files with 196 additions and 194 deletions
+4 -3
View File
@@ -7,16 +7,16 @@ import {useQueryClient} from '@tanstack/react-query'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {usePalette} from '#/lib/hooks/usePalette'
import {isNative} from '#/platform/detection'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {type FeedDescriptor} from '#/state/queries/post-feed'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {truncateAndInvalidate} from '#/state/queries/util'
import {PostFeed} from '#/view/com/posts/PostFeed'
import {EmptyState} from '#/view/com/util/EmptyState'
import {ListRef} from '#/view/com/util/List'
import {type ListRef} from '#/view/com/util/List'
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
import {Text} from '#/view/com/util/text/Text'
import {ios} from '#/alf'
import {SectionRef} from './types'
import {type SectionRef} from './types'
interface FeedSectionProps {
feed: FeedDescriptor
@@ -58,6 +58,7 @@ export const ProfileFeedSection = React.forwardRef<
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
setHasNew(false)
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
React.useImperativeHandle(ref, () => ({
scrollToTop: onScrollToTop,
}))
+135 -140
View File
@@ -1,11 +1,12 @@
import React from 'react'
import {findNodeHandle, View} from 'react-native'
import type Animated from 'react-native-reanimated'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {
AppBskyLabelerDefs,
InterpretedLabelValueDefinition,
type AppBskyLabelerDefs,
type InterpretedLabelValueDefinition,
interpretLabelValueDefinitions,
ModerationOpts,
type ModerationOpts,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -14,7 +15,7 @@ import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIX
import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {isNative} from '#/platform/detection'
import {ListRef} from '#/view/com/util/List'
import {type ListRef} from '#/view/com/util/List'
import {atoms as a, useTheme} from '#/alf'
import {Divider} from '#/components/Divider'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
@@ -23,7 +24,7 @@ import {Loader} from '#/components/Loader'
import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
import {Text} from '#/components/Typography'
import {ErrorState} from '../ErrorState'
import {SectionRef} from './types'
import {type SectionRef} from './types'
interface LabelsSectionProps {
isLabelerLoading: boolean
@@ -54,64 +55,6 @@ export const ProfileLabelsSection = React.forwardRef<
const {_} = useLingui()
const {height: minHeight} = useSafeAreaFrame()
const onScrollToTop = React.useCallback(() => {
// @ts-ignore TODO fix this
scrollElRef.current?.scrollTo({
animated: isNative,
x: 0,
y: -headerHeight,
})
}, [scrollElRef, headerHeight])
React.useImperativeHandle(ref, () => ({
scrollToTop: onScrollToTop,
}))
React.useEffect(() => {
if (isFocused && scrollElRef.current) {
const nativeTag = findNodeHandle(scrollElRef.current)
setScrollViewTag(nativeTag)
}
}, [isFocused, scrollElRef, setScrollViewTag])
return (
<Layout.Center style={{flex: 1, minHeight}}>
{isLabelerLoading ? (
<View style={[a.w_full, a.align_center]}>
<Loader size="xl" />
</View>
) : labelerError || !labelerInfo ? (
<ErrorState
error={
labelerError?.toString() ||
_(msg`Something went wrong, please try again.`)
}
/>
) : (
<ProfileLabelsSectionInner
moderationOpts={moderationOpts}
labelerInfo={labelerInfo}
scrollElRef={scrollElRef}
headerHeight={headerHeight}
/>
)}
</Layout.Center>
)
})
export function ProfileLabelsSectionInner({
moderationOpts,
labelerInfo,
scrollElRef,
headerHeight,
}: {
moderationOpts: ModerationOpts
labelerInfo: AppBskyLabelerDefs.LabelerViewDetailed
scrollElRef: ListRef
headerHeight: number
}) {
const t = useTheme()
// Intentionally destructured outside the main thread closure.
// See https://github.com/bluesky-social/social-app/pull/4108.
const {
@@ -135,6 +78,70 @@ export function ProfileLabelsSectionInner({
},
})
const onScrollToTop = React.useCallback(() => {
// @ts-ignore TODO fix this
scrollElRef.current?.scrollTo({
animated: isNative,
x: 0,
y: -headerHeight,
})
}, [scrollElRef, headerHeight])
React.useImperativeHandle(ref, () => ({
scrollToTop: onScrollToTop,
}))
React.useEffect(() => {
if (isFocused && scrollElRef.current) {
const nativeTag = findNodeHandle(scrollElRef.current)
setScrollViewTag(nativeTag)
}
}, [isFocused, scrollElRef, setScrollViewTag])
return (
<Layout.Center style={{minHeight}}>
<Layout.Content
ref={scrollElRef as React.Ref<Animated.ScrollView>}
scrollEventThrottle={1}
contentContainerStyle={{
paddingTop: headerHeight,
borderWidth: 0,
}}
contentOffset={{x: 0, y: headerHeight * -1}}
onScroll={scrollHandler}>
{isLabelerLoading ? (
<View style={[a.w_full, a.align_center, a.py_4xl]}>
<Loader size="xl" />
</View>
) : labelerError || !labelerInfo ? (
<View style={[a.w_full, a.align_center, a.py_4xl]}>
<ErrorState
error={
labelerError?.toString() ||
_(msg`Something went wrong, please try again.`)
}
/>
</View>
) : (
<ProfileLabelsSectionInner
moderationOpts={moderationOpts}
labelerInfo={labelerInfo}
/>
)}
</Layout.Content>
</Layout.Center>
)
})
export function ProfileLabelsSectionInner({
moderationOpts,
labelerInfo,
}: {
moderationOpts: ModerationOpts
labelerInfo: AppBskyLabelerDefs.LabelerViewDetailed
}) {
const t = useTheme()
const {labelValues} = labelerInfo.policies
const isSubscribed = isLabelerSubscribed(labelerInfo, moderationOpts)
const labelDefs = React.useMemo(() => {
@@ -147,88 +154,76 @@ export function ProfileLabelsSectionInner({
}, [labelerInfo, labelValues])
return (
<Layout.Content
// @ts-expect-error TODO fix this
ref={scrollElRef}
scrollEventThrottle={1}
contentContainerStyle={{
paddingTop: headerHeight,
borderWidth: 0,
}}
contentOffset={{x: 0, y: headerHeight * -1}}
onScroll={scrollHandler}>
<View style={[a.pt_xl, a.px_lg, a.border_t, t.atoms.border_contrast_low]}>
<View>
<Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}>
<View style={[a.pt_xl, a.px_lg, a.border_t, t.atoms.border_contrast_low]}>
<View>
<Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}>
<Trans>
Labels are annotations on users and content. They can be used to
hide, warn, and categorize the network.
</Trans>
</Text>
{labelerInfo.creator.viewer?.blocking ? (
<View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_md]}>
<CircleInfo size="sm" fill={t.atoms.text_contrast_medium.color} />
<Text
style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}>
<Trans>
Blocking does not prevent this labeler from placing labels on
your account.
</Trans>
</Text>
</View>
) : null}
{labelValues.length === 0 ? (
<Text
style={[
a.pt_xl,
t.atoms.text_contrast_high,
a.leading_snug,
a.text_sm,
]}>
<Trans>
Labels are annotations on users and content. They can be used to
hide, warn, and categorize the network.
This labeler hasn't declared what labels it publishes, and may not
be active.
</Trans>
</Text>
{labelerInfo.creator.viewer?.blocking ? (
<View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_md]}>
<CircleInfo size="sm" fill={t.atoms.text_contrast_medium.color} />
<Text
style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}>
<Trans>
Blocking does not prevent this labeler from placing labels on
your account.
</Trans>
</Text>
</View>
) : null}
{labelValues.length === 0 ? (
<Text
style={[
a.pt_xl,
t.atoms.text_contrast_high,
a.leading_snug,
a.text_sm,
]}>
<Trans>
This labeler hasn't declared what labels it publishes, and may
not be active.
</Trans>
</Text>
) : !isSubscribed ? (
<Text
style={[
a.pt_xl,
t.atoms.text_contrast_high,
a.leading_snug,
a.text_sm,
]}>
<Trans>
Subscribe to @{labelerInfo.creator.handle} to use these labels:
</Trans>
</Text>
) : null}
</View>
{labelDefs.length > 0 && (
<View
) : !isSubscribed ? (
<Text
style={[
a.mt_xl,
a.w_full,
a.rounded_md,
a.overflow_hidden,
t.atoms.bg_contrast_25,
a.pt_xl,
t.atoms.text_contrast_high,
a.leading_snug,
a.text_sm,
]}>
{labelDefs.map((labelDef, i) => {
return (
<React.Fragment key={labelDef.identifier}>
{i !== 0 && <Divider />}
<LabelerLabelPreference
disabled={isSubscribed ? undefined : true}
labelDefinition={labelDef}
labelerDid={labelerInfo.creator.did}
/>
</React.Fragment>
)
})}
</View>
)}
<View style={{height: 100}} />
<Trans>
Subscribe to @{labelerInfo.creator.handle} to use these labels:
</Trans>
</Text>
) : null}
</View>
</Layout.Content>
{labelDefs.length > 0 && (
<View
style={[
a.mt_xl,
a.w_full,
a.rounded_md,
a.overflow_hidden,
t.atoms.bg_contrast_25,
]}>
{labelDefs.map((labelDef, i) => {
return (
<React.Fragment key={labelDef.identifier}>
{i !== 0 && <Divider />}
<LabelerLabelPreference
disabled={isSubscribed ? undefined : true}
labelDefinition={labelDef}
labelerDid={labelerInfo.creator.did}
/>
</React.Fragment>
)
})}
</View>
)}
</View>
)
}