From f8bd850465916d9d7755edb95f678c2f64e261ea Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 14 Apr 2025 19:00:39 +0300 Subject: 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) --- src/components/Layout/index.tsx | 108 +++++++------- src/screens/Profile/Sections/Feed.tsx | 7 +- src/screens/Profile/Sections/Labels.tsx | 251 ++++++++++++++++---------------- 3 files changed, 184 insertions(+), 182 deletions(-) (limited to 'src') diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index a61192b86..5891ca863 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -1,12 +1,12 @@ -import React, {useContext, useMemo} from 'react' -import {StyleSheet, View, ViewProps, ViewStyle} from 'react-native' -import {StyleProp} from 'react-native' +import {forwardRef, memo, useContext, useMemo} from 'react' +import {StyleSheet, View, type ViewProps, type ViewStyle} from 'react-native' +import {type StyleProp} from 'react-native' import { KeyboardAwareScrollView, - KeyboardAwareScrollViewProps, + type KeyboardAwareScrollViewProps, } from 'react-native-keyboard-controller' import Animated, { - AnimatedScrollViewProps, + type AnimatedScrollViewProps, useAnimatedProps, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' @@ -35,7 +35,7 @@ export type ScreenProps = React.ComponentProps & { /** * Outermost component of every screen */ -export const Screen = React.memo(function Screen({ +export const Screen = memo(function Screen({ style, noInsetTop, ...props @@ -61,49 +61,55 @@ export type ContentProps = AnimatedScrollViewProps & { /** * Default scroll view for simple pages */ -export const Content = React.memo(function Content({ - children, - style, - contentContainerStyle, - ignoreTabletLayoutOffset, - ...props -}: ContentProps) { - const t = useTheme() - const {footerHeight} = useShellLayout() - const animatedProps = useAnimatedProps(() => { - return { - scrollIndicatorInsets: { - bottom: footerHeight.get(), - top: 0, - right: 1, - }, - } satisfies AnimatedScrollViewProps - }) +export const Content = memo( + forwardRef(function Content( + { + children, + style, + contentContainerStyle, + ignoreTabletLayoutOffset, + ...props + }, + ref, + ) { + const t = useTheme() + const {footerHeight} = useShellLayout() + const animatedProps = useAnimatedProps(() => { + return { + scrollIndicatorInsets: { + bottom: footerHeight.get(), + top: 0, + right: 1, + }, + } satisfies AnimatedScrollViewProps + }) - return ( - - {isWeb ? ( -
- {/* @ts-expect-error web only -esb */} - {children} -
- ) : ( - children - )} -
- ) -}) + return ( + + {isWeb ? ( +
+ {/* @ts-expect-error web only -esb */} + {children} +
+ ) : ( + children + )} +
+ ) + }), +) const scrollViewStyles = StyleSheet.create({ common: { @@ -124,7 +130,7 @@ export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & { * * 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, style, contentContainerStyle, @@ -147,7 +153,7 @@ export const KeyboardAwareContent = React.memo(function LayoutScrollView({ /** * Utility component to center content within the screen */ -export const Center = React.memo(function LayoutContent({ +export const Center = memo(function LayoutCenter({ children, style, ignoreTabletLayoutOffset, @@ -192,7 +198,7 @@ export const Center = React.memo(function LayoutContent({ /** * Only used within `Layout.Screen`, not for reuse */ -const WebCenterBorders = React.memo(function LayoutContent() { +const WebCenterBorders = memo(function LayoutWebCenterBorders() { const t = useTheme() const {gtMobile} = useBreakpoints() const {centerColumnOffset} = useLayoutBreakpoints() diff --git a/src/screens/Profile/Sections/Feed.tsx b/src/screens/Profile/Sections/Feed.tsx index 3e3fe973e..0691fd729 100644 --- a/src/screens/Profile/Sections/Feed.tsx +++ b/src/screens/Profile/Sections/Feed.tsx @@ -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, })) diff --git a/src/screens/Profile/Sections/Labels.tsx b/src/screens/Profile/Sections/Labels.tsx index 770464a71..b7f702f11 100644 --- a/src/screens/Profile/Sections/Labels.tsx +++ b/src/screens/Profile/Sections/Labels.tsx @@ -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,6 +55,29 @@ export const ProfileLabelsSection = React.forwardRef< const {_} = useLingui() const {height: minHeight} = useSafeAreaFrame() + // Intentionally destructured outside the main thread closure. + // See https://github.com/bluesky-social/social-app/pull/4108. + const { + onBeginDrag: onBeginDragFromContext, + onEndDrag: onEndDragFromContext, + onScroll: onScrollFromContext, + onMomentumEnd: onMomentumEndFromContext, + } = useScrollHandlers() + const scrollHandler = useAnimatedScrollHandler({ + onBeginDrag(e, ctx) { + onBeginDragFromContext?.(e, ctx) + }, + onEndDrag(e, ctx) { + onEndDragFromContext?.(e, ctx) + }, + onScroll(e, ctx) { + onScrollFromContext?.(e, ctx) + }, + onMomentumEnd(e, ctx) { + onMomentumEndFromContext?.(e, ctx) + }, + }) + const onScrollToTop = React.useCallback(() => { // @ts-ignore TODO fix this scrollElRef.current?.scrollTo({ @@ -75,26 +99,36 @@ export const ProfileLabelsSection = React.forwardRef< }, [isFocused, scrollElRef, setScrollViewTag]) return ( - - {isLabelerLoading ? ( - - - - ) : labelerError || !labelerInfo ? ( - - ) : ( - - )} + + } + scrollEventThrottle={1} + contentContainerStyle={{ + paddingTop: headerHeight, + borderWidth: 0, + }} + contentOffset={{x: 0, y: headerHeight * -1}} + onScroll={scrollHandler}> + {isLabelerLoading ? ( + + + + ) : labelerError || !labelerInfo ? ( + + + + ) : ( + + )} + ) }) @@ -102,39 +136,12 @@ export const ProfileLabelsSection = React.forwardRef< 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 { - onBeginDrag: onBeginDragFromContext, - onEndDrag: onEndDragFromContext, - onScroll: onScrollFromContext, - onMomentumEnd: onMomentumEndFromContext, - } = useScrollHandlers() - const scrollHandler = useAnimatedScrollHandler({ - onBeginDrag(e, ctx) { - onBeginDragFromContext?.(e, ctx) - }, - onEndDrag(e, ctx) { - onEndDragFromContext?.(e, ctx) - }, - onScroll(e, ctx) { - onScrollFromContext?.(e, ctx) - }, - onMomentumEnd(e, ctx) { - onMomentumEndFromContext?.(e, ctx) - }, - }) - const {labelValues} = labelerInfo.policies const isSubscribed = isLabelerSubscribed(labelerInfo, moderationOpts) const labelDefs = React.useMemo(() => { @@ -147,88 +154,76 @@ export function ProfileLabelsSectionInner({ }, [labelerInfo, labelValues]) return ( - - - - - - Labels are annotations on users and content. They can be used to - hide, warn, and categorize the network. - - - {labelerInfo.creator.viewer?.blocking ? ( - - - - - Blocking does not prevent this labeler from placing labels on - your account. - - - - ) : null} - {labelValues.length === 0 ? ( + + + + + Labels are annotations on users and content. They can be used to + hide, warn, and categorize the network. + + + {labelerInfo.creator.viewer?.blocking ? ( + + + style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}> - This labeler hasn't declared what labels it publishes, and may - not be active. + Blocking does not prevent this labeler from placing labels on + your account. - ) : !isSubscribed ? ( - - - Subscribe to @{labelerInfo.creator.handle} to use these labels: - - - ) : null} - - {labelDefs.length > 0 && ( - + ) : null} + {labelValues.length === 0 ? ( + - {labelDefs.map((labelDef, i) => { - return ( - - {i !== 0 && } - - - ) - })} - - )} - + + This labeler hasn't declared what labels it publishes, and may not + be active. + + + ) : !isSubscribed ? ( + + + Subscribe to @{labelerInfo.creator.handle} to use these labels: + + + ) : null} - + {labelDefs.length > 0 && ( + + {labelDefs.map((labelDef, i) => { + return ( + + {i !== 0 && } + + + ) + })} + + )} + ) } -- cgit 1.4.1