Make bio area scrollable on iOS (#2931)

* fix dampen logic

prevent ghost presses

handle refreshes, animations, and clamps

handle most cases for cancelling the scroll animation

handle animations

save point

simplify

remove unnecessary context

readme

apply offset on pan

find the RCTScrollView

send props, add native gesture recognizer

get the react tag

wrap the profile in context

create module

* fix swiping to go back

* remove debug

* use `findNodeHandle`

* create an expo module view

* port most of it to expo modules

* finish most of expomodules impl

* experiments

* remove refresh ability for now

* remove rn module

* changes

* cleanup a few issues

allow swipe back gesture

clean up types

always run animation if the final offset is < 0

separate logic

update patch readme

get the `RCTRefreshControl` working nicely

* gate new header

* organize
This commit is contained in:
Hailey 2024-04-11 15:20:38 -07:00 committed by GitHub
parent 740cd029d7
commit 4e51772003
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 491 additions and 65 deletions

View file

@ -1,18 +1,19 @@
import React from 'react'
import {View} from 'react-native'
import {findNodeHandle, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ListRef} from 'view/com/util/List'
import {Feed} from 'view/com/posts/Feed'
import {EmptyState} from 'view/com/util/EmptyState'
import {useQueryClient} from '@tanstack/react-query'
import {isNative} from '#/platform/detection'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {useQueryClient} from '@tanstack/react-query'
import {truncateAndInvalidate} from '#/state/queries/util'
import {Text} from '#/view/com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {isNative} from '#/platform/detection'
import {Text} from '#/view/com/util/text/Text'
import {Feed} from 'view/com/posts/Feed'
import {EmptyState} from 'view/com/util/EmptyState'
import {ListRef} from 'view/com/util/List'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {SectionRef} from './types'
interface FeedSectionProps {
@ -21,12 +22,20 @@ interface FeedSectionProps {
isFocused: boolean
scrollElRef: ListRef
ignoreFilterFor?: string
setScrollViewTag: (tag: number | null) => void
}
export const ProfileFeedSection = React.forwardRef<
SectionRef,
FeedSectionProps
>(function FeedSectionImpl(
{feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor},
{
feed,
headerHeight,
isFocused,
scrollElRef,
ignoreFilterFor,
setScrollViewTag,
},
ref,
) {
const {_} = useLingui()
@ -50,6 +59,13 @@ export const ProfileFeedSection = React.forwardRef<
return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} />
}, [_])
React.useEffect(() => {
if (isFocused && scrollElRef.current) {
const nativeTag = findNodeHandle(scrollElRef.current)
setScrollViewTag(nativeTag)
}
}, [isFocused, scrollElRef, setScrollViewTag])
return (
<View>
<Feed

View file

@ -1,5 +1,5 @@
import React from 'react'
import {View} from 'react-native'
import {findNodeHandle, View} from 'react-native'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {
AppBskyLabelerDefs,
@ -32,6 +32,8 @@ interface LabelsSectionProps {
moderationOpts: ModerationOpts
scrollElRef: ListRef
headerHeight: number
isFocused: boolean
setScrollViewTag: (tag: number | null) => void
}
export const ProfileLabelsSection = React.forwardRef<
SectionRef,
@ -44,6 +46,8 @@ export const ProfileLabelsSection = React.forwardRef<
moderationOpts,
scrollElRef,
headerHeight,
isFocused,
setScrollViewTag,
},
ref,
) {
@ -63,6 +67,13 @@ export const ProfileLabelsSection = React.forwardRef<
scrollToTop: onScrollToTop,
}))
React.useEffect(() => {
if (isFocused && scrollElRef.current) {
const nativeTag = findNodeHandle(scrollElRef.current)
setScrollViewTag(nativeTag)
}
}, [isFocused, scrollElRef, setScrollViewTag])
return (
<CenteredView style={{flex: 1, minHeight}} sideBorders>
{isLabelerLoading ? (

View file

@ -1,22 +1,29 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {
findNodeHandle,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {List, ListRef} from '../util/List'
import {FeedSourceCardLoaded} from './FeedSourceCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {useProfileFeedgensQuery, RQKEY} from '#/state/queries/profile-feedgens'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {cleanError} from '#/lib/strings/errors'
import {useTheme} from '#/lib/ThemeContext'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {hydrateFeedGenerator} from '#/state/queries/feed'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {useLingui} from '@lingui/react'
import {hydrateFeedGenerator} from '#/state/queries/feed'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
import {usePalette} from 'lib/hooks/usePalette'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {List, ListRef} from '../util/List'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {Text} from '../util/text/Text'
import {FeedSourceCardLoaded} from './FeedSourceCard'
const LOADING = {_reactKey: '__loading__'}
const EMPTY = {_reactKey: '__empty__'}
@ -34,13 +41,14 @@ interface ProfileFeedgensProps {
enabled?: boolean
style?: StyleProp<ViewStyle>
testID?: string
setScrollViewTag: (tag: number | null) => void
}
export const ProfileFeedgens = React.forwardRef<
SectionRef,
ProfileFeedgensProps
>(function ProfileFeedgensImpl(
{did, scrollElRef, headerOffset, enabled, style, testID},
{did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag},
ref,
) {
const pal = usePalette('default')
@ -169,6 +177,13 @@ export const ProfileFeedgens = React.forwardRef<
[error, refetch, onPressRetryLoadMore, pal, preferences, _],
)
React.useEffect(() => {
if (enabled && scrollElRef.current) {
const nativeTag = findNodeHandle(scrollElRef.current)
setScrollViewTag(nativeTag)
}
}, [enabled, scrollElRef, setScrollViewTag])
return (
<View testID={testID} style={style}>
<List

View file

@ -1,21 +1,28 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {
findNodeHandle,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {List, ListRef} from '../util/List'
import {ListCard} from './ListCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {Text} from '../util/text/Text'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useProfileListsQuery, RQKEY} from '#/state/queries/profile-lists'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {cleanError} from '#/lib/strings/errors'
import {useTheme} from '#/lib/ThemeContext'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {useLingui} from '@lingui/react'
import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {List, ListRef} from '../util/List'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {Text} from '../util/text/Text'
import {ListCard} from './ListCard'
const LOADING = {_reactKey: '__loading__'}
const EMPTY = {_reactKey: '__empty__'}
@ -33,11 +40,12 @@ interface ProfileListsProps {
enabled?: boolean
style?: StyleProp<ViewStyle>
testID?: string
setScrollViewTag: (tag: number | null) => void
}
export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
function ProfileListsImpl(
{did, scrollElRef, headerOffset, enabled, style, testID},
{did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag},
ref,
) {
const pal = usePalette('default')
@ -171,6 +179,13 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
[error, refetch, onPressRetryLoadMore, pal, _],
)
React.useEffect(() => {
if (enabled && scrollElRef.current) {
const nativeTag = findNodeHandle(scrollElRef.current)
setScrollViewTag(nativeTag)
}
}, [enabled, scrollElRef, setScrollViewTag])
return (
<View testID={testID} style={style}>
<List

View file

@ -12,9 +12,7 @@ import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {cleanError} from '#/lib/strings/errors'
import {isInvalidHandle} from '#/lib/strings/handles'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {listenSoftReset} from '#/state/events'
import {useLabelerInfoQuery} from '#/state/queries/labeler'
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import {useModerationOpts} from '#/state/queries/preferences'
@ -27,13 +25,17 @@ import {useAnalytics} from 'lib/analytics/analytics'
import {useSetTitle} from 'lib/hooks/useSetTitle'
import {ComposeIcon2} from 'lib/icons'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {useGate} from 'lib/statsig/statsig'
import {combinedDisplayName} from 'lib/strings/display-names'
import {isInvalidHandle} from 'lib/strings/handles'
import {colors, s} from 'lib/styles'
import {listenSoftReset} from 'state/events'
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
import {ScreenHider} from '#/components/moderation/ScreenHider'
import {ExpoScrollForwarderView} from '../../../modules/expo-scroll-forwarder'
import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
import {ProfileLists} from '../com/lists/ProfileLists'
import {ErrorScreen} from '../com/util/error/ErrorScreen'
@ -141,6 +143,7 @@ function ProfileScreenLoaded({
const setMinimalShellMode = useSetMinimalShellMode()
const {openComposer} = useComposerControls()
const {screen, track} = useAnalytics()
const shouldUseScrollableHeader = useGate('new_profile_scroll_component')
const {
data: labelerInfo,
error: labelerError,
@ -152,6 +155,9 @@ function ProfileScreenLoaded({
const [currentPage, setCurrentPage] = React.useState(0)
const {_} = useLingui()
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
const [scrollViewTag, setScrollViewTag] = React.useState<number | null>(null)
const postsSectionRef = React.useRef<SectionRef>(null)
const repliesSectionRef = React.useRef<SectionRef>(null)
const mediaSectionRef = React.useRef<SectionRef>(null)
@ -297,12 +303,9 @@ function ProfileScreenLoaded({
openComposer({mention})
}, [openComposer, currentAccount, track, profile])
const onPageSelected = React.useCallback(
(i: number) => {
setCurrentPage(i)
},
[setCurrentPage],
)
const onPageSelected = React.useCallback((i: number) => {
setCurrentPage(i)
}, [])
const onCurrentPageSelected = React.useCallback(
(index: number) => {
@ -315,21 +318,38 @@ function ProfileScreenLoaded({
// =
const renderHeader = React.useCallback(() => {
return (
<ProfileHeader
profile={profile}
labeler={labelerInfo}
descriptionRT={hasDescription ? descriptionRT : null}
moderationOpts={moderationOpts}
hideBackButton={hideBackButton}
isPlaceholderProfile={showPlaceholder}
/>
)
if (shouldUseScrollableHeader) {
return (
<ExpoScrollForwarderView scrollViewTag={scrollViewTag}>
<ProfileHeader
profile={profile}
labeler={labelerInfo}
descriptionRT={hasDescription ? descriptionRT : null}
moderationOpts={moderationOpts}
hideBackButton={hideBackButton}
isPlaceholderProfile={showPlaceholder}
/>
</ExpoScrollForwarderView>
)
} else {
return (
<ProfileHeader
profile={profile}
labeler={labelerInfo}
descriptionRT={hasDescription ? descriptionRT : null}
moderationOpts={moderationOpts}
hideBackButton={hideBackButton}
isPlaceholderProfile={showPlaceholder}
/>
)
}
}, [
shouldUseScrollableHeader,
scrollViewTag,
profile,
labelerInfo,
descriptionRT,
hasDescription,
descriptionRT,
moderationOpts,
hideBackButton,
showPlaceholder,
@ -349,7 +369,7 @@ function ProfileScreenLoaded({
onCurrentPageSelected={onCurrentPageSelected}
renderHeader={renderHeader}>
{showFiltersTab
? ({headerHeight, scrollElRef}) => (
? ({headerHeight, isFocused, scrollElRef}) => (
<ProfileLabelsSection
ref={labelsSectionRef}
labelerInfo={labelerInfo}
@ -358,6 +378,8 @@ function ProfileScreenLoaded({
moderationOpts={moderationOpts}
scrollElRef={scrollElRef as ListRef}
headerHeight={headerHeight}
isFocused={isFocused}
setScrollViewTag={setScrollViewTag}
/>
)
: null}
@ -369,6 +391,7 @@ function ProfileScreenLoaded({
scrollElRef={scrollElRef as ListRef}
headerOffset={headerHeight}
enabled={isFocused}
setScrollViewTag={setScrollViewTag}
/>
)
: null}
@ -381,6 +404,7 @@ function ProfileScreenLoaded({
isFocused={isFocused}
scrollElRef={scrollElRef as ListRef}
ignoreFilterFor={profile.did}
setScrollViewTag={setScrollViewTag}
/>
)
: null}
@ -393,6 +417,7 @@ function ProfileScreenLoaded({
isFocused={isFocused}
scrollElRef={scrollElRef as ListRef}
ignoreFilterFor={profile.did}
setScrollViewTag={setScrollViewTag}
/>
)
: null}
@ -405,6 +430,7 @@ function ProfileScreenLoaded({
isFocused={isFocused}
scrollElRef={scrollElRef as ListRef}
ignoreFilterFor={profile.did}
setScrollViewTag={setScrollViewTag}
/>
)
: null}
@ -417,6 +443,7 @@ function ProfileScreenLoaded({
isFocused={isFocused}
scrollElRef={scrollElRef as ListRef}
ignoreFilterFor={profile.did}
setScrollViewTag={setScrollViewTag}
/>
)
: null}
@ -428,6 +455,7 @@ function ProfileScreenLoaded({
scrollElRef={scrollElRef as ListRef}
headerOffset={headerHeight}
enabled={isFocused}
setScrollViewTag={setScrollViewTag}
/>
)
: null}
@ -439,6 +467,7 @@ function ProfileScreenLoaded({
scrollElRef={scrollElRef as ListRef}
headerOffset={headerHeight}
enabled={isFocused}
setScrollViewTag={setScrollViewTag}
/>
)
: null}