about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-04-11 15:20:38 -0700
committerGitHub <noreply@github.com>2024-04-11 15:20:38 -0700
commit4e517720030184ef8c003ffad9b3ca5100619d2e (patch)
tree81f5467bcb7c0a5eae02fe9c23158bb2e1ad59c6 /src
parent740cd029d7162a936d16b427201eb8972e365b94 (diff)
downloadvoidsky-4e517720030184ef8c003ffad9b3ca5100619d2e.tar.zst
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
Diffstat (limited to 'src')
-rw-r--r--src/screens/Profile/Sections/Feed.tsx34
-rw-r--r--src/screens/Profile/Sections/Labels.tsx13
-rw-r--r--src/view/com/feeds/ProfileFeedgens.tsx43
-rw-r--r--src/view/com/lists/ProfileLists.tsx43
-rw-r--r--src/view/screens/Profile.tsx69
5 files changed, 144 insertions, 58 deletions
diff --git a/src/screens/Profile/Sections/Feed.tsx b/src/screens/Profile/Sections/Feed.tsx
index 0a5e2208d..bc106fcfb 100644
--- a/src/screens/Profile/Sections/Feed.tsx
+++ b/src/screens/Profile/Sections/Feed.tsx
@@ -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
diff --git a/src/screens/Profile/Sections/Labels.tsx b/src/screens/Profile/Sections/Labels.tsx
index 5ba8f00a5..f43e3633d 100644
--- a/src/screens/Profile/Sections/Labels.tsx
+++ b/src/screens/Profile/Sections/Labels.tsx
@@ -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 ? (
diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx
index e9cf9e535..a006b11c0 100644
--- a/src/view/com/feeds/ProfileFeedgens.tsx
+++ b/src/view/com/feeds/ProfileFeedgens.tsx
@@ -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 {logger} from '#/logger'
+import {isNative} from '#/platform/detection'
 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 {isNative} from '#/platform/detection'
-import {useLingui} from '@lingui/react'
+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
diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx
index a47b25bed..003d1c60e 100644
--- a/src/view/com/lists/ProfileLists.tsx
+++ b/src/view/com/lists/ProfileLists.tsx
@@ -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
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 6073b9571..c391f8050 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -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}