From 4e517720030184ef8c003ffad9b3ca5100619d2e Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 11 Apr 2024 15:20:38 -0700 Subject: [PATCH] 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 --- .../expo-module.config.json | 6 + modules/expo-scroll-forwarder/index.ts | 1 + .../ios/ExpoScrollForwarder.podspec | 21 ++ .../ios/ExpoScrollForwarderModule.swift | 13 ++ .../ios/ExpoScrollForwarderView.swift | 215 ++++++++++++++++++ .../src/ExpoScrollForwarder.types.ts | 6 + .../src/ExpoScrollForwarderView.ios.tsx | 13 ++ .../src/ExpoScrollForwarderView.tsx | 7 + patches/react-native+0.73.2.patch | 58 ++++- patches/react-native+0.73.2.patch.md | 12 +- src/screens/Profile/Sections/Feed.tsx | 34 ++- src/screens/Profile/Sections/Labels.tsx | 13 +- src/view/com/feeds/ProfileFeedgens.tsx | 45 ++-- src/view/com/lists/ProfileLists.tsx | 43 ++-- src/view/screens/Profile.tsx | 69 ++++-- 15 files changed, 491 insertions(+), 65 deletions(-) create mode 100644 modules/expo-scroll-forwarder/expo-module.config.json create mode 100644 modules/expo-scroll-forwarder/index.ts create mode 100644 modules/expo-scroll-forwarder/ios/ExpoScrollForwarder.podspec create mode 100644 modules/expo-scroll-forwarder/ios/ExpoScrollForwarderModule.swift create mode 100644 modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift create mode 100644 modules/expo-scroll-forwarder/src/ExpoScrollForwarder.types.ts create mode 100644 modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.ios.tsx create mode 100644 modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx diff --git a/modules/expo-scroll-forwarder/expo-module.config.json b/modules/expo-scroll-forwarder/expo-module.config.json new file mode 100644 index 00000000..1fd49f79 --- /dev/null +++ b/modules/expo-scroll-forwarder/expo-module.config.json @@ -0,0 +1,6 @@ +{ + "platforms": ["ios"], + "ios": { + "modules": ["ExpoScrollForwarderModule"] + } +} diff --git a/modules/expo-scroll-forwarder/index.ts b/modules/expo-scroll-forwarder/index.ts new file mode 100644 index 00000000..a4ad4b85 --- /dev/null +++ b/modules/expo-scroll-forwarder/index.ts @@ -0,0 +1 @@ +export {ExpoScrollForwarderView} from './src/ExpoScrollForwarderView' diff --git a/modules/expo-scroll-forwarder/ios/ExpoScrollForwarder.podspec b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarder.podspec new file mode 100644 index 00000000..78ca9812 --- /dev/null +++ b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarder.podspec @@ -0,0 +1,21 @@ +Pod::Spec.new do |s| + s.name = 'ExpoScrollForwarder' + s.version = '1.0.0' + s.summary = 'Forward scroll gesture from UIView to UIScrollView' + s.description = 'Forward scroll gesture from UIView to UIScrollView' + s.author = 'bluesky-social' + s.homepage = 'https://github.com/bluesky-social/social-app' + s.platforms = { :ios => '13.4', :tvos => '13.4' } + s.source = { git: '' } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + + # Swift/Objective-C compatibility + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'SWIFT_COMPILATION_MODE' => 'wholemodule' + } + + s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" +end diff --git a/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderModule.swift b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderModule.swift new file mode 100644 index 00000000..c4ecc788 --- /dev/null +++ b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderModule.swift @@ -0,0 +1,13 @@ +import ExpoModulesCore + +public class ExpoScrollForwarderModule: Module { + public func definition() -> ModuleDefinition { + Name("ExpoScrollForwarder") + + View(ExpoScrollForwarderView.self) { + Prop("scrollViewTag") { (view: ExpoScrollForwarderView, prop: Int) in + view.scrollViewTag = prop + } + } + } +} diff --git a/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift new file mode 100644 index 00000000..9c0e2f87 --- /dev/null +++ b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift @@ -0,0 +1,215 @@ +import ExpoModulesCore + +// This view will be used as a native component. Make sure to inherit from `ExpoView` +// to apply the proper styling (e.g. border radius and shadows). +class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate { + var scrollViewTag: Int? { + didSet { + self.tryFindScrollView() + } + } + + private var rctScrollView: RCTScrollView? + private var rctRefreshCtrl: RCTRefreshControl? + private var cancelGestureRecognizers: [UIGestureRecognizer]? + private var animTimer: Timer? + private var initialOffset: CGFloat = 0.0 + private var didImpact: Bool = false + + required init(appContext: AppContext? = nil) { + super.init(appContext: appContext) + + let pg = UIPanGestureRecognizer(target: self, action: #selector(callOnPan(_:))) + pg.delegate = self + self.addGestureRecognizer(pg) + + let tg = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:))) + tg.isEnabled = false + tg.delegate = self + + let lpg = UILongPressGestureRecognizer(target: self, action: #selector(callOnPress(_:))) + lpg.minimumPressDuration = 0.01 + lpg.isEnabled = false + lpg.delegate = self + + self.cancelGestureRecognizers = [lpg, tg] + } + + + // We don't want to recognize the scroll pan gesture and the swipe back gesture together + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer { + return false + } + + return true + } + + // We only want the "scroll" gesture to happen whenever the pan is vertical, otherwise it will + // interfere with the native swipe back gesture. + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { + return true + } + + let velocity = gestureRecognizer.velocity(in: self) + return abs(velocity.y) > abs(velocity.x) + } + + // This will be used to cancel the scroll animation whenever we tap inside of the header. We don't need another + // recognizer for this one. + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + self.stopTimer() + } + + // This will be used to cancel the animation whenever we press inside of the scroll view. We don't want to change + // the scroll view gesture's delegate, so we add an additional recognizer to detect this. + @IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void { + self.stopTimer() + } + + @IBAction func callOnPan(_ sender: UIPanGestureRecognizer) -> Void { + guard let rctsv = self.rctScrollView, let sv = rctsv.scrollView else { + return + } + + let translation = sender.translation(in: self).y + + if sender.state == .began { + if sv.contentOffset.y < 0 { + sv.contentOffset.y = 0 + } + + self.initialOffset = sv.contentOffset.y + } + + if sender.state == .changed { + sv.contentOffset.y = self.dampenOffset(-translation + self.initialOffset) + + if sv.contentOffset.y <= -130, !didImpact { + let generator = UIImpactFeedbackGenerator(style: .light) + generator.impactOccurred() + + self.didImpact = true + } + } + + if sender.state == .ended { + let velocity = sender.velocity(in: self).y + self.didImpact = false + + if sv.contentOffset.y <= -130 { + self.rctRefreshCtrl?.forwarderBeginRefreshing() + return + } + + // A check for a velocity under 250 prevents animations from occurring when they wouldn't in a normal + // scroll view + if abs(velocity) < 250, sv.contentOffset.y >= 0 { + return + } + + self.startDecayAnimation(translation, velocity) + } + } + + func startDecayAnimation(_ translation: CGFloat, _ velocity: CGFloat) { + guard let sv = self.rctScrollView?.scrollView else { + return + } + + var velocity = velocity + + self.enableCancelGestureRecognizers() + + if velocity > 0 { + velocity = min(velocity, 5000) + } else { + velocity = max(velocity, -5000) + } + + var animTranslation = -translation + self.animTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 120, repeats: true) { timer in + velocity *= 0.9875 + animTranslation = (-velocity / 120) + animTranslation + + let nextOffset = self.dampenOffset(animTranslation + self.initialOffset) + + if nextOffset <= 0 { + if self.initialOffset <= 1 { + self.scrollToOffset(0) + } else { + sv.contentOffset.y = 0 + } + + self.stopTimer() + return + } else { + sv.contentOffset.y = nextOffset + } + + if abs(velocity) < 5 { + self.stopTimer() + } + } + } + + func dampenOffset(_ offset: CGFloat) -> CGFloat { + if offset < 0 { + return offset - (offset * 0.55) + } + + return offset + } + + func tryFindScrollView() { + guard let scrollViewTag = scrollViewTag else { + return + } + + // Before we switch to a different scrollview, we always want to remove the cancel gesture recognizer. + // Otherwise we might end up with duplicates when we switch back to that scrollview. + self.removeCancelGestureRecognizers() + + self.rctScrollView = self.appContext? + .findView(withTag: scrollViewTag, ofType: RCTScrollView.self) + self.rctRefreshCtrl = self.rctScrollView?.scrollView.refreshControl as? RCTRefreshControl + + self.addCancelGestureRecognizers() + } + + func addCancelGestureRecognizers() { + self.cancelGestureRecognizers?.forEach { r in + self.rctScrollView?.scrollView?.addGestureRecognizer(r) + } + } + + func removeCancelGestureRecognizers() { + self.cancelGestureRecognizers?.forEach { r in + self.rctScrollView?.scrollView?.removeGestureRecognizer(r) + } + } + + + func enableCancelGestureRecognizers() { + self.cancelGestureRecognizers?.forEach { r in + r.isEnabled = true + } + } + + func disableCancelGestureRecognizers() { + self.cancelGestureRecognizers?.forEach { r in + r.isEnabled = false + } + } + + func scrollToOffset(_ offset: Int, animated: Bool = true) -> Void { + self.rctScrollView?.scroll(toOffset: CGPoint(x: 0, y: offset), animated: animated) + } + + func stopTimer() -> Void { + self.disableCancelGestureRecognizers() + self.animTimer?.invalidate() + self.animTimer = nil + } +} diff --git a/modules/expo-scroll-forwarder/src/ExpoScrollForwarder.types.ts b/modules/expo-scroll-forwarder/src/ExpoScrollForwarder.types.ts new file mode 100644 index 00000000..26b9e755 --- /dev/null +++ b/modules/expo-scroll-forwarder/src/ExpoScrollForwarder.types.ts @@ -0,0 +1,6 @@ +import React from 'react' + +export interface ExpoScrollForwarderViewProps { + scrollViewTag: number | null + children: React.ReactNode +} diff --git a/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.ios.tsx b/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.ios.tsx new file mode 100644 index 00000000..a91aebd4 --- /dev/null +++ b/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.ios.tsx @@ -0,0 +1,13 @@ +import {requireNativeViewManager} from 'expo-modules-core' +import * as React from 'react' +import {ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types' + +const NativeView: React.ComponentType = + requireNativeViewManager('ExpoScrollForwarder') + +export function ExpoScrollForwarderView({ + children, + ...rest +}: ExpoScrollForwarderViewProps) { + return {children} +} diff --git a/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx b/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx new file mode 100644 index 00000000..93e69333 --- /dev/null +++ b/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx @@ -0,0 +1,7 @@ +import React from 'react' +import {ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types' +export function ExpoScrollForwarderView({ + children, +}: React.PropsWithChildren) { + return children +} diff --git a/patches/react-native+0.73.2.patch b/patches/react-native+0.73.2.patch index 8db23da0..db8b7da2 100644 --- a/patches/react-native+0.73.2.patch +++ b/patches/react-native+0.73.2.patch @@ -1,11 +1,22 @@ +diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h +index e9b330f..1ecdf0a 100644 +--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h ++++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h +@@ -16,4 +16,6 @@ + @property (nonatomic, copy) RCTDirectEventBlock onRefresh; + @property (nonatomic, weak) UIScrollView *scrollView; + ++- (void)forwarderBeginRefreshing; ++ + @end diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m -index b09e653..d290dab 100644 +index b09e653..4c32b31 100644 --- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m +++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m -@@ -198,6 +198,14 @@ - (void)refreshControlValueChanged +@@ -198,9 +198,53 @@ - (void)refreshControlValueChanged [self setCurrentRefreshingState:super.refreshing]; _refreshingProgrammatically = NO; - + + if (@available(iOS 17.4, *)) { + if (_currentRefreshingState) { + UIImpactFeedbackGenerator *feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight]; @@ -16,4 +27,43 @@ index b09e653..d290dab 100644 + if (_onRefresh) { _onRefresh(nil); - } \ No newline at end of file + } + } + ++/* ++ This method is used by Bluesky's ExpoScrollForwarder. This allows other React Native ++ libraries to perform a refresh of a scrollview and access the refresh control's onRefresh ++ function. ++ */ ++- (void)forwarderBeginRefreshing ++{ ++ _refreshingProgrammatically = NO; ++ ++ [self sizeToFit]; ++ ++ if (!self.scrollView) { ++ return; ++ } ++ ++ UIScrollView *scrollView = (UIScrollView *)self.scrollView; ++ ++ [UIView animateWithDuration:0.3 ++ delay:0 ++ options:UIViewAnimationOptionBeginFromCurrentState ++ animations:^(void) { ++ // Whenever we call this method, the scrollview will always be at a position of ++ // -130 or less. Scrolling back to -65 simulates the default behavior of RCTRefreshControl ++ [scrollView setContentOffset:CGPointMake(0, -65)]; ++ } ++ completion:^(__unused BOOL finished) { ++ [super beginRefreshing]; ++ [self setCurrentRefreshingState:super.refreshing]; ++ ++ if (self->_onRefresh) { ++ self->_onRefresh(nil); ++ } ++ } ++ ]; ++} ++ + @end diff --git a/patches/react-native+0.73.2.patch.md b/patches/react-native+0.73.2.patch.md index 7f70baf2..9c93aee5 100644 --- a/patches/react-native+0.73.2.patch.md +++ b/patches/react-native+0.73.2.patch.md @@ -1,5 +1,13 @@ -# RefreshControl Patch +# ***This second part of this patch is load bearing, do not remove.*** + +## RefreshControl Patch - iOS 17.4 Haptic Regression Patching `RCTRefreshControl.mm` temporarily to play an impact haptic on refresh when using iOS 17.4 or higher. Since 17.4, there has been a regression somewhere causing haptics to not play on iOS on refresh. Should monitor for an update -in the RN repo: https://github.com/facebook/react-native/issues/43388 \ No newline at end of file +in the RN repo: https://github.com/facebook/react-native/issues/43388 + +## RefreshControl Path - ScrollForwarder + +Patching `RCTRefreshControl.m` and `RCTRefreshControl.h` to add a new `forwarderBeginRefreshing` method to the class. +This method is used by `ExpoScrollForwarder` to initiate a refresh of the underlying `UIScrollView` from inside that +module. diff --git a/src/screens/Profile/Sections/Feed.tsx b/src/screens/Profile/Sections/Feed.tsx index 0a5e2208..bc106fcf 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 }, [_]) + React.useEffect(() => { + if (isFocused && scrollElRef.current) { + const nativeTag = findNodeHandle(scrollElRef.current) + setScrollViewTag(nativeTag) + } + }, [isFocused, scrollElRef, setScrollViewTag]) + return ( 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 ( {isLabelerLoading ? ( diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx index e9cf9e53..a006b11c 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 {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 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 ( testID?: string + setScrollViewTag: (tag: number | null) => void } export const ProfileLists = React.forwardRef( 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( [error, refetch, onPressRetryLoadMore, pal, _], ) + React.useEffect(() => { + if (enabled && scrollElRef.current) { + const nativeTag = findNodeHandle(scrollElRef.current) + setScrollViewTag(nativeTag) + } + }, [enabled, scrollElRef, setScrollViewTag]) + return ( (null) + const postsSectionRef = React.useRef(null) const repliesSectionRef = React.useRef(null) const mediaSectionRef = React.useRef(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 ( - - ) + if (shouldUseScrollableHeader) { + return ( + + + + ) + } else { + return ( + + ) + } }, [ + 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}) => ( ) : 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}