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
+18 -12
View File
@@ -1,12 +1,12 @@
import React, {useContext, useMemo} from 'react' import {forwardRef, memo, useContext, useMemo} from 'react'
import {StyleSheet, View, ViewProps, ViewStyle} from 'react-native' import {StyleSheet, View, type ViewProps, type ViewStyle} from 'react-native'
import {StyleProp} from 'react-native' import {type StyleProp} from 'react-native'
import { import {
KeyboardAwareScrollView, KeyboardAwareScrollView,
KeyboardAwareScrollViewProps, type KeyboardAwareScrollViewProps,
} from 'react-native-keyboard-controller' } from 'react-native-keyboard-controller'
import Animated, { import Animated, {
AnimatedScrollViewProps, type AnimatedScrollViewProps,
useAnimatedProps, useAnimatedProps,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
@@ -35,7 +35,7 @@ export type ScreenProps = React.ComponentProps<typeof View> & {
/** /**
* Outermost component of every screen * Outermost component of every screen
*/ */
export const Screen = React.memo(function Screen({ export const Screen = memo(function Screen({
style, style,
noInsetTop, noInsetTop,
...props ...props
@@ -61,13 +61,17 @@ export type ContentProps = AnimatedScrollViewProps & {
/** /**
* Default scroll view for simple pages * Default scroll view for simple pages
*/ */
export const Content = React.memo(function Content({ export const Content = memo(
forwardRef<Animated.ScrollView, ContentProps>(function Content(
{
children, children,
style, style,
contentContainerStyle, contentContainerStyle,
ignoreTabletLayoutOffset, ignoreTabletLayoutOffset,
...props ...props
}: ContentProps) { },
ref,
) {
const t = useTheme() const t = useTheme()
const {footerHeight} = useShellLayout() const {footerHeight} = useShellLayout()
const animatedProps = useAnimatedProps(() => { const animatedProps = useAnimatedProps(() => {
@@ -82,6 +86,7 @@ export const Content = React.memo(function Content({
return ( return (
<Animated.ScrollView <Animated.ScrollView
ref={ref}
id="content" id="content"
automaticallyAdjustsScrollIndicatorInsets={false} automaticallyAdjustsScrollIndicatorInsets={false}
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'} indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
@@ -103,7 +108,8 @@ export const Content = React.memo(function Content({
)} )}
</Animated.ScrollView> </Animated.ScrollView>
) )
}) }),
)
const scrollViewStyles = StyleSheet.create({ const scrollViewStyles = StyleSheet.create({
common: { common: {
@@ -124,7 +130,7 @@ export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & {
* *
* BE SURE TO TEST THIS WHEN USING, it's untested as of writing this comment. * BE SURE TO TEST THIS WHEN USING, it's untested as of writing this comment.
*/ */
export const KeyboardAwareContent = React.memo(function LayoutScrollView({ export const KeyboardAwareContent = memo(function LayoutKeyboardAwareContent({
children, children,
style, style,
contentContainerStyle, contentContainerStyle,
@@ -147,7 +153,7 @@ export const KeyboardAwareContent = React.memo(function LayoutScrollView({
/** /**
* Utility component to center content within the screen * Utility component to center content within the screen
*/ */
export const Center = React.memo(function LayoutContent({ export const Center = memo(function LayoutCenter({
children, children,
style, style,
ignoreTabletLayoutOffset, ignoreTabletLayoutOffset,
@@ -192,7 +198,7 @@ export const Center = React.memo(function LayoutContent({
/** /**
* Only used within `Layout.Screen`, not for reuse * Only used within `Layout.Screen`, not for reuse
*/ */
const WebCenterBorders = React.memo(function LayoutContent() { const WebCenterBorders = memo(function LayoutWebCenterBorders() {
const t = useTheme() const t = useTheme()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const {centerColumnOffset} = useLayoutBreakpoints() const {centerColumnOffset} = useLayoutBreakpoints()
+4 -3
View File
@@ -7,16 +7,16 @@ import {useQueryClient} from '@tanstack/react-query'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {isNative} from '#/platform/detection' 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 {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {truncateAndInvalidate} from '#/state/queries/util' import {truncateAndInvalidate} from '#/state/queries/util'
import {PostFeed} from '#/view/com/posts/PostFeed' import {PostFeed} from '#/view/com/posts/PostFeed'
import {EmptyState} from '#/view/com/util/EmptyState' 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 {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
import {Text} from '#/view/com/util/text/Text' import {Text} from '#/view/com/util/text/Text'
import {ios} from '#/alf' import {ios} from '#/alf'
import {SectionRef} from './types' import {type SectionRef} from './types'
interface FeedSectionProps { interface FeedSectionProps {
feed: FeedDescriptor feed: FeedDescriptor
@@ -58,6 +58,7 @@ export const ProfileFeedSection = React.forwardRef<
truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
setHasNew(false) setHasNew(false)
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew]) }, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
scrollToTop: onScrollToTop, scrollToTop: onScrollToTop,
})) }))
+72 -77
View File
@@ -1,11 +1,12 @@
import React from 'react' import React from 'react'
import {findNodeHandle, View} from 'react-native' import {findNodeHandle, View} from 'react-native'
import type Animated from 'react-native-reanimated'
import {useSafeAreaFrame} from 'react-native-safe-area-context' import {useSafeAreaFrame} from 'react-native-safe-area-context'
import { import {
AppBskyLabelerDefs, type AppBskyLabelerDefs,
InterpretedLabelValueDefinition, type InterpretedLabelValueDefinition,
interpretLabelValueDefinitions, interpretLabelValueDefinitions,
ModerationOpts, type ModerationOpts,
} from '@atproto/api' } 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'
@@ -14,7 +15,7 @@ import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIX
import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation' import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
import {useScrollHandlers} from '#/lib/ScrollContext' import {useScrollHandlers} from '#/lib/ScrollContext'
import {isNative} from '#/platform/detection' 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 {atoms as a, useTheme} from '#/alf'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 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 {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {ErrorState} from '../ErrorState' import {ErrorState} from '../ErrorState'
import {SectionRef} from './types' import {type SectionRef} from './types'
interface LabelsSectionProps { interface LabelsSectionProps {
isLabelerLoading: boolean isLabelerLoading: boolean
@@ -54,64 +55,6 @@ export const ProfileLabelsSection = React.forwardRef<
const {_} = useLingui() const {_} = useLingui()
const {height: minHeight} = useSafeAreaFrame() 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. // Intentionally destructured outside the main thread closure.
// See https://github.com/bluesky-social/social-app/pull/4108. // See https://github.com/bluesky-social/social-app/pull/4108.
const { 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 {labelValues} = labelerInfo.policies
const isSubscribed = isLabelerSubscribed(labelerInfo, moderationOpts) const isSubscribed = isLabelerSubscribed(labelerInfo, moderationOpts)
const labelDefs = React.useMemo(() => { const labelDefs = React.useMemo(() => {
@@ -147,16 +154,6 @@ export function ProfileLabelsSectionInner({
}, [labelerInfo, labelValues]) }, [labelerInfo, labelValues])
return ( 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 style={[a.pt_xl, a.px_lg, a.border_t, t.atoms.border_contrast_low]}>
<View> <View>
<Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}> <Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}>
@@ -186,8 +183,8 @@ export function ProfileLabelsSectionInner({
a.text_sm, a.text_sm,
]}> ]}>
<Trans> <Trans>
This labeler hasn't declared what labels it publishes, and may This labeler hasn't declared what labels it publishes, and may not
not be active. be active.
</Trans> </Trans>
</Text> </Text>
) : !isSubscribed ? ( ) : !isSubscribed ? (
@@ -227,8 +224,6 @@ export function ProfileLabelsSectionInner({
})} })}
</View> </View>
)} )}
<View style={{height: 100}} />
</View> </View>
</Layout.Content>
) )
} }