mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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)

authored by samuel.fm and committed by

GitHub f8bd8504 238b00d1

+184 -182
+57 -51
src/components/Layout/index.tsx
··· 1 - import React, {useContext, useMemo} from 'react' 2 - import {StyleSheet, View, ViewProps, ViewStyle} from 'react-native' 3 - import {StyleProp} from 'react-native' 1 + import {forwardRef, memo, useContext, useMemo} from 'react' 2 + import {StyleSheet, View, type ViewProps, type ViewStyle} from 'react-native' 3 + import {type StyleProp} from 'react-native' 4 4 import { 5 5 KeyboardAwareScrollView, 6 - KeyboardAwareScrollViewProps, 6 + type KeyboardAwareScrollViewProps, 7 7 } from 'react-native-keyboard-controller' 8 8 import Animated, { 9 - AnimatedScrollViewProps, 9 + type AnimatedScrollViewProps, 10 10 useAnimatedProps, 11 11 } from 'react-native-reanimated' 12 12 import {useSafeAreaInsets} from 'react-native-safe-area-context' ··· 35 35 /** 36 36 * Outermost component of every screen 37 37 */ 38 - export const Screen = React.memo(function Screen({ 38 + export const Screen = memo(function Screen({ 39 39 style, 40 40 noInsetTop, 41 41 ...props ··· 61 61 /** 62 62 * Default scroll view for simple pages 63 63 */ 64 - export const Content = React.memo(function Content({ 65 - children, 66 - style, 67 - contentContainerStyle, 68 - ignoreTabletLayoutOffset, 69 - ...props 70 - }: ContentProps) { 71 - const t = useTheme() 72 - const {footerHeight} = useShellLayout() 73 - const animatedProps = useAnimatedProps(() => { 74 - return { 75 - scrollIndicatorInsets: { 76 - bottom: footerHeight.get(), 77 - top: 0, 78 - right: 1, 79 - }, 80 - } satisfies AnimatedScrollViewProps 81 - }) 64 + export const Content = memo( 65 + forwardRef<Animated.ScrollView, ContentProps>(function Content( 66 + { 67 + children, 68 + style, 69 + contentContainerStyle, 70 + ignoreTabletLayoutOffset, 71 + ...props 72 + }, 73 + ref, 74 + ) { 75 + const t = useTheme() 76 + const {footerHeight} = useShellLayout() 77 + const animatedProps = useAnimatedProps(() => { 78 + return { 79 + scrollIndicatorInsets: { 80 + bottom: footerHeight.get(), 81 + top: 0, 82 + right: 1, 83 + }, 84 + } satisfies AnimatedScrollViewProps 85 + }) 82 86 83 - return ( 84 - <Animated.ScrollView 85 - id="content" 86 - automaticallyAdjustsScrollIndicatorInsets={false} 87 - indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'} 88 - // sets the scroll inset to the height of the footer 89 - animatedProps={animatedProps} 90 - style={[scrollViewStyles.common, style]} 91 - contentContainerStyle={[ 92 - scrollViewStyles.contentContainer, 93 - contentContainerStyle, 94 - ]} 95 - {...props}> 96 - {isWeb ? ( 97 - <Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}> 98 - {/* @ts-expect-error web only -esb */} 99 - {children} 100 - </Center> 101 - ) : ( 102 - children 103 - )} 104 - </Animated.ScrollView> 105 - ) 106 - }) 87 + return ( 88 + <Animated.ScrollView 89 + ref={ref} 90 + id="content" 91 + automaticallyAdjustsScrollIndicatorInsets={false} 92 + indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'} 93 + // sets the scroll inset to the height of the footer 94 + animatedProps={animatedProps} 95 + style={[scrollViewStyles.common, style]} 96 + contentContainerStyle={[ 97 + scrollViewStyles.contentContainer, 98 + contentContainerStyle, 99 + ]} 100 + {...props}> 101 + {isWeb ? ( 102 + <Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}> 103 + {/* @ts-expect-error web only -esb */} 104 + {children} 105 + </Center> 106 + ) : ( 107 + children 108 + )} 109 + </Animated.ScrollView> 110 + ) 111 + }), 112 + ) 107 113 108 114 const scrollViewStyles = StyleSheet.create({ 109 115 common: { ··· 124 130 * 125 131 * BE SURE TO TEST THIS WHEN USING, it's untested as of writing this comment. 126 132 */ 127 - export const KeyboardAwareContent = React.memo(function LayoutScrollView({ 133 + export const KeyboardAwareContent = memo(function LayoutKeyboardAwareContent({ 128 134 children, 129 135 style, 130 136 contentContainerStyle, ··· 147 153 /** 148 154 * Utility component to center content within the screen 149 155 */ 150 - export const Center = React.memo(function LayoutContent({ 156 + export const Center = memo(function LayoutCenter({ 151 157 children, 152 158 style, 153 159 ignoreTabletLayoutOffset, ··· 192 198 /** 193 199 * Only used within `Layout.Screen`, not for reuse 194 200 */ 195 - const WebCenterBorders = React.memo(function LayoutContent() { 201 + const WebCenterBorders = memo(function LayoutWebCenterBorders() { 196 202 const t = useTheme() 197 203 const {gtMobile} = useBreakpoints() 198 204 const {centerColumnOffset} = useLayoutBreakpoints()
+4 -3
src/screens/Profile/Sections/Feed.tsx
··· 7 7 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 8 8 import {usePalette} from '#/lib/hooks/usePalette' 9 9 import {isNative} from '#/platform/detection' 10 - import {FeedDescriptor} from '#/state/queries/post-feed' 10 + import {type FeedDescriptor} from '#/state/queries/post-feed' 11 11 import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' 12 12 import {truncateAndInvalidate} from '#/state/queries/util' 13 13 import {PostFeed} from '#/view/com/posts/PostFeed' 14 14 import {EmptyState} from '#/view/com/util/EmptyState' 15 - import {ListRef} from '#/view/com/util/List' 15 + import {type ListRef} from '#/view/com/util/List' 16 16 import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' 17 17 import {Text} from '#/view/com/util/text/Text' 18 18 import {ios} from '#/alf' 19 - import {SectionRef} from './types' 19 + import {type SectionRef} from './types' 20 20 21 21 interface FeedSectionProps { 22 22 feed: FeedDescriptor ··· 58 58 truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) 59 59 setHasNew(false) 60 60 }, [scrollElRef, headerHeight, queryClient, feed, setHasNew]) 61 + 61 62 React.useImperativeHandle(ref, () => ({ 62 63 scrollToTop: onScrollToTop, 63 64 }))
+123 -128
src/screens/Profile/Sections/Labels.tsx
··· 1 1 import React from 'react' 2 2 import {findNodeHandle, View} from 'react-native' 3 + import type Animated from 'react-native-reanimated' 3 4 import {useSafeAreaFrame} from 'react-native-safe-area-context' 4 5 import { 5 - AppBskyLabelerDefs, 6 - InterpretedLabelValueDefinition, 6 + type AppBskyLabelerDefs, 7 + type InterpretedLabelValueDefinition, 7 8 interpretLabelValueDefinitions, 8 - ModerationOpts, 9 + type ModerationOpts, 9 10 } from '@atproto/api' 10 11 import {msg, Trans} from '@lingui/macro' 11 12 import {useLingui} from '@lingui/react' ··· 14 15 import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation' 15 16 import {useScrollHandlers} from '#/lib/ScrollContext' 16 17 import {isNative} from '#/platform/detection' 17 - import {ListRef} from '#/view/com/util/List' 18 + import {type ListRef} from '#/view/com/util/List' 18 19 import {atoms as a, useTheme} from '#/alf' 19 20 import {Divider} from '#/components/Divider' 20 21 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' ··· 23 24 import {LabelerLabelPreference} from '#/components/moderation/LabelPreference' 24 25 import {Text} from '#/components/Typography' 25 26 import {ErrorState} from '../ErrorState' 26 - import {SectionRef} from './types' 27 + import {type SectionRef} from './types' 27 28 28 29 interface LabelsSectionProps { 29 30 isLabelerLoading: boolean ··· 54 55 const {_} = useLingui() 55 56 const {height: minHeight} = useSafeAreaFrame() 56 57 58 + // Intentionally destructured outside the main thread closure. 59 + // See https://github.com/bluesky-social/social-app/pull/4108. 60 + const { 61 + onBeginDrag: onBeginDragFromContext, 62 + onEndDrag: onEndDragFromContext, 63 + onScroll: onScrollFromContext, 64 + onMomentumEnd: onMomentumEndFromContext, 65 + } = useScrollHandlers() 66 + const scrollHandler = useAnimatedScrollHandler({ 67 + onBeginDrag(e, ctx) { 68 + onBeginDragFromContext?.(e, ctx) 69 + }, 70 + onEndDrag(e, ctx) { 71 + onEndDragFromContext?.(e, ctx) 72 + }, 73 + onScroll(e, ctx) { 74 + onScrollFromContext?.(e, ctx) 75 + }, 76 + onMomentumEnd(e, ctx) { 77 + onMomentumEndFromContext?.(e, ctx) 78 + }, 79 + }) 80 + 57 81 const onScrollToTop = React.useCallback(() => { 58 82 // @ts-ignore TODO fix this 59 83 scrollElRef.current?.scrollTo({ ··· 75 99 }, [isFocused, scrollElRef, setScrollViewTag]) 76 100 77 101 return ( 78 - <Layout.Center style={{flex: 1, minHeight}}> 79 - {isLabelerLoading ? ( 80 - <View style={[a.w_full, a.align_center]}> 81 - <Loader size="xl" /> 82 - </View> 83 - ) : labelerError || !labelerInfo ? ( 84 - <ErrorState 85 - error={ 86 - labelerError?.toString() || 87 - _(msg`Something went wrong, please try again.`) 88 - } 89 - /> 90 - ) : ( 91 - <ProfileLabelsSectionInner 92 - moderationOpts={moderationOpts} 93 - labelerInfo={labelerInfo} 94 - scrollElRef={scrollElRef} 95 - headerHeight={headerHeight} 96 - /> 97 - )} 102 + <Layout.Center style={{minHeight}}> 103 + <Layout.Content 104 + ref={scrollElRef as React.Ref<Animated.ScrollView>} 105 + scrollEventThrottle={1} 106 + contentContainerStyle={{ 107 + paddingTop: headerHeight, 108 + borderWidth: 0, 109 + }} 110 + contentOffset={{x: 0, y: headerHeight * -1}} 111 + onScroll={scrollHandler}> 112 + {isLabelerLoading ? ( 113 + <View style={[a.w_full, a.align_center, a.py_4xl]}> 114 + <Loader size="xl" /> 115 + </View> 116 + ) : labelerError || !labelerInfo ? ( 117 + <View style={[a.w_full, a.align_center, a.py_4xl]}> 118 + <ErrorState 119 + error={ 120 + labelerError?.toString() || 121 + _(msg`Something went wrong, please try again.`) 122 + } 123 + /> 124 + </View> 125 + ) : ( 126 + <ProfileLabelsSectionInner 127 + moderationOpts={moderationOpts} 128 + labelerInfo={labelerInfo} 129 + /> 130 + )} 131 + </Layout.Content> 98 132 </Layout.Center> 99 133 ) 100 134 }) ··· 102 136 export function ProfileLabelsSectionInner({ 103 137 moderationOpts, 104 138 labelerInfo, 105 - scrollElRef, 106 - headerHeight, 107 139 }: { 108 140 moderationOpts: ModerationOpts 109 141 labelerInfo: AppBskyLabelerDefs.LabelerViewDetailed 110 - scrollElRef: ListRef 111 - headerHeight: number 112 142 }) { 113 143 const t = useTheme() 114 144 115 - // Intentionally destructured outside the main thread closure. 116 - // See https://github.com/bluesky-social/social-app/pull/4108. 117 - const { 118 - onBeginDrag: onBeginDragFromContext, 119 - onEndDrag: onEndDragFromContext, 120 - onScroll: onScrollFromContext, 121 - onMomentumEnd: onMomentumEndFromContext, 122 - } = useScrollHandlers() 123 - const scrollHandler = useAnimatedScrollHandler({ 124 - onBeginDrag(e, ctx) { 125 - onBeginDragFromContext?.(e, ctx) 126 - }, 127 - onEndDrag(e, ctx) { 128 - onEndDragFromContext?.(e, ctx) 129 - }, 130 - onScroll(e, ctx) { 131 - onScrollFromContext?.(e, ctx) 132 - }, 133 - onMomentumEnd(e, ctx) { 134 - onMomentumEndFromContext?.(e, ctx) 135 - }, 136 - }) 137 - 138 145 const {labelValues} = labelerInfo.policies 139 146 const isSubscribed = isLabelerSubscribed(labelerInfo, moderationOpts) 140 147 const labelDefs = React.useMemo(() => { ··· 147 154 }, [labelerInfo, labelValues]) 148 155 149 156 return ( 150 - <Layout.Content 151 - // @ts-expect-error TODO fix this 152 - ref={scrollElRef} 153 - scrollEventThrottle={1} 154 - contentContainerStyle={{ 155 - paddingTop: headerHeight, 156 - borderWidth: 0, 157 - }} 158 - contentOffset={{x: 0, y: headerHeight * -1}} 159 - onScroll={scrollHandler}> 160 - <View style={[a.pt_xl, a.px_lg, a.border_t, t.atoms.border_contrast_low]}> 161 - <View> 162 - <Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}> 163 - <Trans> 164 - Labels are annotations on users and content. They can be used to 165 - hide, warn, and categorize the network. 166 - </Trans> 167 - </Text> 168 - {labelerInfo.creator.viewer?.blocking ? ( 169 - <View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_md]}> 170 - <CircleInfo size="sm" fill={t.atoms.text_contrast_medium.color} /> 171 - <Text 172 - style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}> 173 - <Trans> 174 - Blocking does not prevent this labeler from placing labels on 175 - your account. 176 - </Trans> 177 - </Text> 178 - </View> 179 - ) : null} 180 - {labelValues.length === 0 ? ( 157 + <View style={[a.pt_xl, a.px_lg, a.border_t, t.atoms.border_contrast_low]}> 158 + <View> 159 + <Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}> 160 + <Trans> 161 + Labels are annotations on users and content. They can be used to 162 + hide, warn, and categorize the network. 163 + </Trans> 164 + </Text> 165 + {labelerInfo.creator.viewer?.blocking ? ( 166 + <View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_md]}> 167 + <CircleInfo size="sm" fill={t.atoms.text_contrast_medium.color} /> 181 168 <Text 182 - style={[ 183 - a.pt_xl, 184 - t.atoms.text_contrast_high, 185 - a.leading_snug, 186 - a.text_sm, 187 - ]}> 169 + style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}> 188 170 <Trans> 189 - This labeler hasn't declared what labels it publishes, and may 190 - not be active. 171 + Blocking does not prevent this labeler from placing labels on 172 + your account. 191 173 </Trans> 192 174 </Text> 193 - ) : !isSubscribed ? ( 194 - <Text 195 - style={[ 196 - a.pt_xl, 197 - t.atoms.text_contrast_high, 198 - a.leading_snug, 199 - a.text_sm, 200 - ]}> 201 - <Trans> 202 - Subscribe to @{labelerInfo.creator.handle} to use these labels: 203 - </Trans> 204 - </Text> 205 - ) : null} 206 - </View> 207 - {labelDefs.length > 0 && ( 208 - <View 175 + </View> 176 + ) : null} 177 + {labelValues.length === 0 ? ( 178 + <Text 179 + style={[ 180 + a.pt_xl, 181 + t.atoms.text_contrast_high, 182 + a.leading_snug, 183 + a.text_sm, 184 + ]}> 185 + <Trans> 186 + This labeler hasn't declared what labels it publishes, and may not 187 + be active. 188 + </Trans> 189 + </Text> 190 + ) : !isSubscribed ? ( 191 + <Text 209 192 style={[ 210 - a.mt_xl, 211 - a.w_full, 212 - a.rounded_md, 213 - a.overflow_hidden, 214 - t.atoms.bg_contrast_25, 193 + a.pt_xl, 194 + t.atoms.text_contrast_high, 195 + a.leading_snug, 196 + a.text_sm, 215 197 ]}> 216 - {labelDefs.map((labelDef, i) => { 217 - return ( 218 - <React.Fragment key={labelDef.identifier}> 219 - {i !== 0 && <Divider />} 220 - <LabelerLabelPreference 221 - disabled={isSubscribed ? undefined : true} 222 - labelDefinition={labelDef} 223 - labelerDid={labelerInfo.creator.did} 224 - /> 225 - </React.Fragment> 226 - ) 227 - })} 228 - </View> 229 - )} 230 - <View style={{height: 100}} /> 198 + <Trans> 199 + Subscribe to @{labelerInfo.creator.handle} to use these labels: 200 + </Trans> 201 + </Text> 202 + ) : null} 231 203 </View> 232 - </Layout.Content> 204 + {labelDefs.length > 0 && ( 205 + <View 206 + style={[ 207 + a.mt_xl, 208 + a.w_full, 209 + a.rounded_md, 210 + a.overflow_hidden, 211 + t.atoms.bg_contrast_25, 212 + ]}> 213 + {labelDefs.map((labelDef, i) => { 214 + return ( 215 + <React.Fragment key={labelDef.identifier}> 216 + {i !== 0 && <Divider />} 217 + <LabelerLabelPreference 218 + disabled={isSubscribed ? undefined : true} 219 + labelDefinition={labelDef} 220 + labelerDid={labelerInfo.creator.did} 221 + /> 222 + </React.Fragment> 223 + ) 224 + })} 225 + </View> 226 + )} 227 + </View> 233 228 ) 234 229 }