fabric: update scroll forwarder for profile headers (#8366)
* progress * remove old module * nil delegates * clean up problems * fix refreshing * bump
This commit is contained in:
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"platforms": ["ios"],
|
|
||||||
"ios": {
|
|
||||||
"modules": ["ExpoScrollForwarderModule"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export {ExpoScrollForwarderView} from './src/ExpoScrollForwarderView'
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
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<UITouch>, 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) {
|
|
||||||
self.stopTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func callOnPan(_ sender: UIPanGestureRecognizer) {
|
|
||||||
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) { _ 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) {
|
|
||||||
self.rctScrollView?.scroll(toOffset: CGPoint(x: 0, y: offset), animated: animated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopTimer() {
|
|
||||||
self.disableCancelGestureRecognizers()
|
|
||||||
self.animTimer?.invalidate()
|
|
||||||
self.animTimer = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export interface ExpoScrollForwarderViewProps {
|
|
||||||
scrollViewTag: number | null
|
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import {requireNativeViewManager} from 'expo-modules-core'
|
|
||||||
|
|
||||||
import {type ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types'
|
|
||||||
|
|
||||||
const NativeView: React.ComponentType<ExpoScrollForwarderViewProps> =
|
|
||||||
requireNativeViewManager('ExpoScrollForwarder')
|
|
||||||
|
|
||||||
export function ExpoScrollForwarderView({
|
|
||||||
children,
|
|
||||||
...rest
|
|
||||||
}: ExpoScrollForwarderViewProps) {
|
|
||||||
return <NativeView {...rest}>{children}</NativeView>
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import {type ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types'
|
|
||||||
|
|
||||||
export function ExpoScrollForwarderView({
|
|
||||||
children,
|
|
||||||
}: React.PropsWithChildren<ExpoScrollForwarderViewProps>) {
|
|
||||||
return children
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
require "json"
|
||||||
|
|
||||||
|
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
||||||
|
|
||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = "ScrollForwarder"
|
||||||
|
s.version = package["version"]
|
||||||
|
s.summary = package["description"]
|
||||||
|
s.homepage = package["homepage"]
|
||||||
|
s.license = package["license"]
|
||||||
|
s.authors = package["author"]
|
||||||
|
|
||||||
|
s.platforms = { :ios => min_ios_version_supported }
|
||||||
|
s.source = { :git => ".git", :tag => "#{s.version}" }
|
||||||
|
|
||||||
|
s.source_files = "ios/**/*.{h,m,mm,cpp}"
|
||||||
|
s.private_header_files = "ios/**/*.h"
|
||||||
|
|
||||||
|
install_modules_dependencies(s)
|
||||||
|
end
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#import <React/RCTViewComponentView.h>
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
#ifndef ScrollForwarderViewNativeComponent_h
|
||||||
|
#define ScrollForwarderViewNativeComponent_h
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface ScrollForwarderView : RCTViewComponentView
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
||||||
|
#endif /* ScrollForwarderViewNativeComponent_h */
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
#import "ScrollForwarderView.h"
|
||||||
|
|
||||||
|
#import <React/RCTEnhancedScrollView.h>
|
||||||
|
#import <React/RCTScrollViewComponentView.h>
|
||||||
|
#import <react/renderer/components/ScrollForwarderViewSpec/ComponentDescriptors.h>
|
||||||
|
#import <react/renderer/components/ScrollForwarderViewSpec/EventEmitters.h>
|
||||||
|
#import <react/renderer/components/ScrollForwarderViewSpec/Props.h>
|
||||||
|
#import <react/renderer/components/ScrollForwarderViewSpec/RCTComponentViewHelpers.h>
|
||||||
|
|
||||||
|
#import "RCTFabricComponentsPlugins.h"
|
||||||
|
|
||||||
|
using namespace facebook::react;
|
||||||
|
|
||||||
|
// How far down a pull needs to be to trigger a refresh
|
||||||
|
static const CGFloat kPullThreshold = 130.0;
|
||||||
|
static const CGFloat kDampingFactor = 0.55;
|
||||||
|
// The top speed that free scrolling can have
|
||||||
|
static const CGFloat kMaxVelocity = 5000.0;
|
||||||
|
// Free scrolling decay. This seems to be close to the default iOS value
|
||||||
|
static const CGFloat kVelocityDecay = 0.9875;
|
||||||
|
// What scroll release velocity will actually trigger free scrolling
|
||||||
|
static const CGFloat kMinimumVelocity = 5.0;
|
||||||
|
|
||||||
|
@interface ScrollForwarderView () <RCTScrollForwarderViewViewProtocol, UIGestureRecognizerDelegate>
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation ScrollForwarderView {
|
||||||
|
NSArray<UIGestureRecognizer *> * _cancelGestureRecognizers;
|
||||||
|
RCTScrollViewComponentView * _svcv;
|
||||||
|
CGPoint _initialOffset;
|
||||||
|
|
||||||
|
CADisplayLink * _displayLink;
|
||||||
|
CGFloat _currentVelocity;
|
||||||
|
CGFloat _accumulatedTranslation;
|
||||||
|
|
||||||
|
bool _didImpact;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
||||||
|
{
|
||||||
|
return concreteComponentDescriptorProvider<ScrollForwarderViewComponentDescriptor>();
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithFrame:(CGRect)frame
|
||||||
|
{
|
||||||
|
if (self = [super initWithFrame:frame]) {
|
||||||
|
static const auto defaultProps = std::make_shared<const ScrollForwarderViewProps>();
|
||||||
|
_props = defaultProps;
|
||||||
|
|
||||||
|
UIPanGestureRecognizer *pg = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
|
||||||
|
pg.delegate = self;
|
||||||
|
pg.cancelsTouchesInView = false;
|
||||||
|
[self addGestureRecognizer:pg];
|
||||||
|
|
||||||
|
UITapGestureRecognizer *tg = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
|
||||||
|
[tg setEnabled:false];
|
||||||
|
tg.delegate = self;
|
||||||
|
|
||||||
|
UILongPressGestureRecognizer *lpg = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||||
|
[lpg setMinimumPressDuration:0.01];
|
||||||
|
[lpg setEnabled:false];
|
||||||
|
lpg.delegate = self;
|
||||||
|
|
||||||
|
NSArray<UIGestureRecognizer *> *cancelGestureRecognizers = [NSArray arrayWithObjects:lpg, tg, nil];
|
||||||
|
_cancelGestureRecognizers = cancelGestureRecognizers;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
|
- (void)dealloc
|
||||||
|
{
|
||||||
|
[self stopAnimation];
|
||||||
|
[self removeCancelGestureRecognizers];
|
||||||
|
_svcv = nil;
|
||||||
|
|
||||||
|
for (UIGestureRecognizer *gr in _cancelGestureRecognizers) {
|
||||||
|
gr.delegate = nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)prepareForRecycle
|
||||||
|
{
|
||||||
|
[super prepareForRecycle];
|
||||||
|
[self stopAnimation];
|
||||||
|
[self removeCancelGestureRecognizers];
|
||||||
|
_svcv = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Props
|
||||||
|
|
||||||
|
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
||||||
|
{
|
||||||
|
const auto &oldViewProps = *std::static_pointer_cast<ScrollForwarderViewProps const>(_props);
|
||||||
|
const auto &newViewProps = *std::static_pointer_cast<ScrollForwarderViewProps const>(props);
|
||||||
|
|
||||||
|
if (oldViewProps.scrollViewTag != newViewProps.scrollViewTag) {
|
||||||
|
[self tryFindScrollView];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldViewProps.refreshing != newViewProps.refreshing) {
|
||||||
|
if (!newViewProps.refreshing) {
|
||||||
|
[self endRefreshing];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[super updateProps:props oldProps:oldProps];
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIGestureRecognizerDelegate
|
||||||
|
|
||||||
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
||||||
|
{
|
||||||
|
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] && [otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
|
||||||
|
{
|
||||||
|
if (![gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIPanGestureRecognizer *pg = (UIPanGestureRecognizer *)gestureRecognizer;
|
||||||
|
CGPoint velocity = [pg velocityInView:self];
|
||||||
|
|
||||||
|
return fabs(velocity.y) > fabs(velocity.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
|
||||||
|
{
|
||||||
|
[self stopAnimation];
|
||||||
|
[super touchesBegan:touches withEvent:event];
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Scroll Forwarding
|
||||||
|
|
||||||
|
- (void)removeCancelGestureRecognizers
|
||||||
|
{
|
||||||
|
if (!_svcv) return;
|
||||||
|
for (UIGestureRecognizer *gr in _cancelGestureRecognizers) {
|
||||||
|
[_svcv.scrollView removeGestureRecognizer:gr];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)addCancelGestureRecognizers
|
||||||
|
{
|
||||||
|
if (!_svcv) return;
|
||||||
|
for (UIGestureRecognizer *gr in _cancelGestureRecognizers) {
|
||||||
|
[_svcv.scrollView addGestureRecognizer:gr];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)enableCancelGestureRecognizers
|
||||||
|
{
|
||||||
|
for (UIGestureRecognizer *gr in _cancelGestureRecognizers) {
|
||||||
|
[gr setEnabled:true];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)disableCancelGestureRecognizers
|
||||||
|
{
|
||||||
|
for (UIGestureRecognizer *gr in _cancelGestureRecognizers) {
|
||||||
|
[gr setEnabled:false];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)scrollToOffset:(CGPoint)offset animated:(bool)animated
|
||||||
|
{
|
||||||
|
if (!_svcv) return;
|
||||||
|
[_svcv scrollToOffset:offset animated:animated];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)stopAnimation
|
||||||
|
{
|
||||||
|
[self disableCancelGestureRecognizers];
|
||||||
|
[_displayLink invalidate];
|
||||||
|
_displayLink = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)handlePan:(UIPanGestureRecognizer *)gesture {
|
||||||
|
if (!_svcv) return;
|
||||||
|
|
||||||
|
UIScrollView *sv = _svcv.scrollView;
|
||||||
|
|
||||||
|
CGPoint translation = [gesture translationInView:self];
|
||||||
|
|
||||||
|
if (gesture.state == UIGestureRecognizerStateBegan) {
|
||||||
|
_didImpact = false;
|
||||||
|
|
||||||
|
if (sv.contentOffset.y < 0) {
|
||||||
|
CGPoint newOffset = CGPointMake(sv.contentOffset.x, 0);
|
||||||
|
sv.contentOffset = newOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
_initialOffset = sv.contentOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gesture.state == UIGestureRecognizerStateChanged) {
|
||||||
|
CGPoint newOffset = CGPointMake(sv.contentOffset.x, [self dampenOffset:(-translation.y + _initialOffset.y)]);
|
||||||
|
sv.contentOffset = newOffset;
|
||||||
|
|
||||||
|
if (sv.contentOffset.y <= -kPullThreshold && !_didImpact) {
|
||||||
|
UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
|
||||||
|
[generator impactOccurred];
|
||||||
|
_didImpact = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gesture.state == UIGestureRecognizerStateEnded) {
|
||||||
|
CGPoint velocity = [gesture velocityInView:self];
|
||||||
|
|
||||||
|
if (sv.contentOffset.y <= -kPullThreshold) {
|
||||||
|
[self refresh];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sv.contentOffset.y < 0) {
|
||||||
|
CGPoint newOffset = CGPointMake(sv.contentOffset.x, 0);
|
||||||
|
[self scrollToOffset:newOffset animated:true];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abs(velocity.y) < 250 && sv.contentOffset.y >= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self startDecayWithInitialTranslation:translation.y velocity:velocity.y];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (CGFloat)dampenOffset:(CGFloat)offset
|
||||||
|
{
|
||||||
|
if (offset < 0) {
|
||||||
|
return offset - (offset * kDampingFactor);
|
||||||
|
}
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)handleTap:(UITapGestureRecognizer *)gesture {
|
||||||
|
[self stopAnimation];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture {
|
||||||
|
[self stopAnimation];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)startDecayWithInitialTranslation:(CGFloat)translation velocity:(CGFloat)startVelocity
|
||||||
|
{
|
||||||
|
if (!_svcv) return;
|
||||||
|
|
||||||
|
startVelocity = MAX(-kMaxVelocity, MIN(kMaxVelocity, startVelocity));
|
||||||
|
_currentVelocity = startVelocity;
|
||||||
|
_accumulatedTranslation = -translation;
|
||||||
|
|
||||||
|
[self enableCancelGestureRecognizers];
|
||||||
|
|
||||||
|
[_displayLink invalidate];
|
||||||
|
|
||||||
|
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDecayStep:)];
|
||||||
|
|
||||||
|
link.preferredFramesPerSecond = 60;
|
||||||
|
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
||||||
|
_displayLink = link;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)handleDecayStep:(CADisplayLink *)link
|
||||||
|
{
|
||||||
|
_currentVelocity *= kVelocityDecay;
|
||||||
|
|
||||||
|
CGFloat delta = -_currentVelocity / link.preferredFramesPerSecond;
|
||||||
|
_accumulatedTranslation += delta;
|
||||||
|
|
||||||
|
CGFloat rawY = _accumulatedTranslation + _initialOffset.y;
|
||||||
|
CGFloat nextY = rawY > 0 ? rawY : 0;
|
||||||
|
|
||||||
|
CGPoint newOffset = CGPointMake(
|
||||||
|
_svcv.scrollView.contentOffset.x,
|
||||||
|
nextY
|
||||||
|
);
|
||||||
|
_svcv.scrollView.contentOffset = newOffset;
|
||||||
|
|
||||||
|
if (fabs(_currentVelocity) < kMinimumVelocity || nextY <= 0) {
|
||||||
|
[link invalidate];
|
||||||
|
_displayLink = nil;
|
||||||
|
[self disableCancelGestureRecognizers];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We use this component on profile pages. The screne consists of a header component, a scrollview with buttons to
|
||||||
|
* switch between profile tabs, and a pager view (RNCPagerViewComponentView). Both the header and the tab bar are
|
||||||
|
* inside the same RCTViewComponentView. The view heirarchy looks something like this:
|
||||||
|
* - RCTViewComponentView
|
||||||
|
* -- RNCPagerViewComponentView
|
||||||
|
* ----- (Many views deep) RCTScrollViewComponentView
|
||||||
|
* ------ RCTEnhancedScrollView
|
||||||
|
* -- RCTViewComponentView
|
||||||
|
* --- RCTViewComponentView
|
||||||
|
* ---- ScrollForwarderView
|
||||||
|
* --- RCTScrollViewComponentView
|
||||||
|
* ---- RCTEnhancedScrollView
|
||||||
|
*
|
||||||
|
* We want to find that RCTScrollViewComponentView inside of the RNCPagerViewComponentView. To achieve this, we can
|
||||||
|
* use self.superview.superview.superview to get to the root RCTViewComponentView, find the RNCPagerViewComponentView,
|
||||||
|
* then iterate through that view's subviews until we find a RCTScrollViewComponentView.
|
||||||
|
*
|
||||||
|
* This isn't great, because if we reorder the React components, we'll need to update this logic. There's probably
|
||||||
|
* an easier way to achieve this, similar to how we used to do it in Paper (ie, get the scroll view's tag and find that),
|
||||||
|
* but this also comes with some benefits, eg being able to reduce a lot of the logic in the JS code and just find the
|
||||||
|
* scrollview when subviews change.
|
||||||
|
*/
|
||||||
|
- (void)tryFindScrollView
|
||||||
|
{
|
||||||
|
[self removeCancelGestureRecognizers];
|
||||||
|
|
||||||
|
// The root RCTViewComponentView
|
||||||
|
UIView *rootView = self.superview.superview.superview;
|
||||||
|
UIView *pagerView;
|
||||||
|
|
||||||
|
NSString *targetClsName = @"RNCPagerViewComponentView";
|
||||||
|
Class targetCls = NSClassFromString(targetClsName);
|
||||||
|
|
||||||
|
for (UIView *subview in rootView.subviews) {
|
||||||
|
if ([subview isKindOfClass:targetCls]) {
|
||||||
|
pagerView = subview;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pagerView) return;
|
||||||
|
|
||||||
|
RCTScrollViewComponentView *svcv = [self findRTCScrollViewComponentViewInView:pagerView];
|
||||||
|
|
||||||
|
if (!svcv) return;
|
||||||
|
|
||||||
|
_svcv = svcv;
|
||||||
|
[self addCancelGestureRecognizers];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (RCTScrollViewComponentView *)findRTCScrollViewComponentViewInView:(UIView *)view
|
||||||
|
{
|
||||||
|
for (UIView *subview in view.subviews) {
|
||||||
|
if ([subview isKindOfClass:[RCTScrollViewComponentView class]]) {
|
||||||
|
RCTScrollViewComponentView *svcv = (RCTScrollViewComponentView *) subview;
|
||||||
|
return svcv;
|
||||||
|
}
|
||||||
|
|
||||||
|
RCTScrollViewComponentView *svcv = [self findRTCScrollViewComponentViewInView:subview];
|
||||||
|
if (svcv) return svcv;
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIRefreshControl *)refreshContorl
|
||||||
|
{
|
||||||
|
if (!_svcv) return nil;
|
||||||
|
return _svcv.scrollView.refreshControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)refresh
|
||||||
|
{
|
||||||
|
__weak ScrollForwarderView *weakSelf = self;
|
||||||
|
|
||||||
|
[_svcv.scrollView.refreshControl beginRefreshing];
|
||||||
|
|
||||||
|
[UIView animateWithDuration:0.3
|
||||||
|
delay:0
|
||||||
|
options:UIViewAnimationOptionBeginFromCurrentState
|
||||||
|
animations:^(void) {
|
||||||
|
if (!weakSelf) return;
|
||||||
|
|
||||||
|
__strong ScrollForwarderView *self = weakSelf;
|
||||||
|
|
||||||
|
// Whenever we call this method, the scrollview will always be at a position of
|
||||||
|
// -130 or less. Scrolling back to -80 simulates the default behavior of RCTRefreshControl
|
||||||
|
[self->_svcv.scrollView setContentOffset:CGPointMake(0, -65)];
|
||||||
|
}
|
||||||
|
completion:^(__unused BOOL finished) {
|
||||||
|
__strong ScrollForwarderView *self = weakSelf;
|
||||||
|
|
||||||
|
if (self->_eventEmitter != nullptr) {
|
||||||
|
std::dynamic_pointer_cast<const facebook::react::ScrollForwarderViewEventEmitter>(self->_eventEmitter)
|
||||||
|
->onRefresh(facebook::react::ScrollForwarderViewEventEmitter::OnRefresh{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)endRefreshing
|
||||||
|
{
|
||||||
|
UIRefreshControl *rc = [self refreshContorl];
|
||||||
|
|
||||||
|
CGPoint newOffset = CGPointMake(_svcv.scrollView.contentOffset.x, 0.0);
|
||||||
|
[self scrollToOffset:newOffset animated:true];
|
||||||
|
|
||||||
|
[rc endRefreshing];
|
||||||
|
}
|
||||||
|
|
||||||
|
Class<RCTComponentViewProtocol> ScrollForwarderViewCls(void)
|
||||||
|
{
|
||||||
|
return ScrollForwarderView.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#import <React/RCTViewManager.h>
|
||||||
|
#import <React/RCTUIManager.h>
|
||||||
|
#import "RCTBridge.h"
|
||||||
|
|
||||||
|
@interface ScrollForwarderViewManager : RCTViewManager
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation ScrollForwarderViewManager
|
||||||
|
|
||||||
|
RCT_EXPORT_MODULE(ScrollForwarderView)
|
||||||
|
|
||||||
|
- (UIView *)view
|
||||||
|
{
|
||||||
|
return [[UIView alloc] init];
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(scrollViewTag, NSNumber)
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "react-native-scroll-forwarder",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Scroll forwarder module for Bluesky profile headers",
|
||||||
|
"main": "src/index",
|
||||||
|
"codegenConfig": {
|
||||||
|
"name": "ScrollForwarderViewSpec",
|
||||||
|
"type": "all",
|
||||||
|
"jsSrcsDir": "src"
|
||||||
|
},
|
||||||
|
"author": "Hailey <me@haileyok.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "#readme",
|
||||||
|
"create-react-native-library": {
|
||||||
|
"languages": "kotlin-objc",
|
||||||
|
"type": "fabric-view",
|
||||||
|
"version": "0.50.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import {
|
||||||
|
default as NativeScrollForwarderView,
|
||||||
|
type NativeProps,
|
||||||
|
} from './ScrollForwarderViewNativeComponent'
|
||||||
|
|
||||||
|
export function ScrollForwarderView({children, ...rest}: NativeProps) {
|
||||||
|
return (
|
||||||
|
<NativeScrollForwarderView {...rest} style={{flex: 1}}>
|
||||||
|
{children}
|
||||||
|
</NativeScrollForwarderView>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {type NativeProps} from './ScrollForwarderViewNativeComponent'
|
||||||
|
|
||||||
|
export function ScrollForwarderView({children}: NativeProps) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import {type ViewProps} from 'react-native'
|
||||||
|
import {
|
||||||
|
type BubblingEventHandler,
|
||||||
|
type Int32,
|
||||||
|
} from 'react-native/Libraries/Types/CodegenTypes'
|
||||||
|
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'
|
||||||
|
|
||||||
|
type OnRefreshEvent = {}
|
||||||
|
|
||||||
|
export interface NativeProps extends ViewProps {
|
||||||
|
scrollViewTag: Int32 | null
|
||||||
|
refreshing?: boolean
|
||||||
|
onRefresh?: BubblingEventHandler<OnRefreshEvent>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default codegenNativeComponent<NativeProps>('ScrollForwarderView')
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export {ScrollForwarderView} from './ScrollForwarderView'
|
||||||
|
export * from './ScrollForwarderViewNativeComponent'
|
||||||
+2
-1
@@ -220,7 +220,8 @@
|
|||||||
"tlds": "^1.234.0",
|
"tlds": "^1.234.0",
|
||||||
"tldts": "^6.1.46",
|
"tldts": "^6.1.46",
|
||||||
"zeego": "^1.6.2",
|
"zeego": "^1.6.2",
|
||||||
"zod": "^3.20.2"
|
"zod": "^3.20.2",
|
||||||
|
"react-native-scroll-forwarder": "link:./modules/react-native-scroll-forwarder"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@atproto/dev-env": "^0.3.160",
|
"@atproto/dev-env": "^0.3.160",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {isIOS, isNative} from '#/platform/detection'
|
|||||||
import {type 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 {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
||||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||||
import {PostFeed} from '#/view/com/posts/PostFeed'
|
import {PostFeed, type PostFeedRef} from '#/view/com/posts/PostFeed'
|
||||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||||
import {type ListRef} from '#/view/com/util/List'
|
import {type ListRef} from '#/view/com/util/List'
|
||||||
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
|
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
|
||||||
@@ -24,6 +24,8 @@ interface FeedSectionProps {
|
|||||||
scrollElRef: ListRef
|
scrollElRef: ListRef
|
||||||
ignoreFilterFor?: string
|
ignoreFilterFor?: string
|
||||||
setScrollViewTag: (tag: number | null) => void
|
setScrollViewTag: (tag: number | null) => void
|
||||||
|
postFeedRef?: React.RefObject<PostFeedRef | undefined>
|
||||||
|
onRefreshEnd?: () => void
|
||||||
}
|
}
|
||||||
export const ProfileFeedSection = React.forwardRef<
|
export const ProfileFeedSection = React.forwardRef<
|
||||||
SectionRef,
|
SectionRef,
|
||||||
@@ -36,6 +38,8 @@ export const ProfileFeedSection = React.forwardRef<
|
|||||||
scrollElRef,
|
scrollElRef,
|
||||||
ignoreFilterFor,
|
ignoreFilterFor,
|
||||||
setScrollViewTag,
|
setScrollViewTag,
|
||||||
|
postFeedRef,
|
||||||
|
onRefreshEnd,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
@@ -91,6 +95,8 @@ export const ProfileFeedSection = React.forwardRef<
|
|||||||
shouldUseAdjustedNumToRender ? adjustedInitialNumToRender : undefined
|
shouldUseAdjustedNumToRender ? adjustedInitialNumToRender : undefined
|
||||||
}
|
}
|
||||||
isVideoFeed={isVideoFeed}
|
isVideoFeed={isVideoFeed}
|
||||||
|
ref={postFeedRef}
|
||||||
|
onRefreshEnd={onRefreshEnd}
|
||||||
/>
|
/>
|
||||||
{(isScrolledDown || hasNew) && (
|
{(isScrolledDown || hasNew) && (
|
||||||
<LoadLatestBtn
|
<LoadLatestBtn
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
import {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
AppState,
|
AppState,
|
||||||
@@ -163,6 +171,10 @@ export function getItemsForFeedback(feedRow: FeedRow): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PostFeedRef = {
|
||||||
|
refreshFeed: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
// DISABLED need to check if this is causing random feed refreshes -prf
|
// DISABLED need to check if this is causing random feed refreshes -prf
|
||||||
// const REFRESH_AFTER = STALE.HOURS.ONE
|
// const REFRESH_AFTER = STALE.HOURS.ONE
|
||||||
const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY
|
const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY
|
||||||
@@ -189,6 +201,7 @@ let PostFeed = ({
|
|||||||
savedFeedConfig,
|
savedFeedConfig,
|
||||||
initialNumToRender: initialNumToRenderOverride,
|
initialNumToRender: initialNumToRenderOverride,
|
||||||
isVideoFeed = false,
|
isVideoFeed = false,
|
||||||
|
ref,
|
||||||
}: {
|
}: {
|
||||||
feed: FeedDescriptor
|
feed: FeedDescriptor
|
||||||
feedParams?: FeedParams
|
feedParams?: FeedParams
|
||||||
@@ -211,6 +224,7 @@ let PostFeed = ({
|
|||||||
savedFeedConfig?: AppBskyActorDefs.SavedFeed
|
savedFeedConfig?: AppBskyActorDefs.SavedFeed
|
||||||
initialNumToRender?: number
|
initialNumToRender?: number
|
||||||
isVideoFeed?: boolean
|
isVideoFeed?: boolean
|
||||||
|
ref?: React.ForwardedRef<PostFeedRef>
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -629,22 +643,31 @@ let PostFeed = ({
|
|||||||
|
|
||||||
// events
|
// events
|
||||||
// =
|
// =
|
||||||
|
//
|
||||||
|
|
||||||
const onRefresh = useCallback(async () => {
|
const refreshFeed = async () => {
|
||||||
logEvent('feed:refresh', {
|
logEvent('feed:refresh', {
|
||||||
feedType: feedType,
|
feedType: feedType,
|
||||||
feedUrl: feed,
|
feedUrl: feed,
|
||||||
reason: 'pull-to-refresh',
|
reason: 'pull-to-refresh',
|
||||||
})
|
})
|
||||||
setIsPTRing(true)
|
|
||||||
try {
|
try {
|
||||||
await refetch()
|
await refetch()
|
||||||
onHasNew?.(false)
|
onHasNew?.(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Failed to refresh posts feed', {message: err})
|
logger.error('Failed to refresh posts feed', {message: err})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRefresh = async () => {
|
||||||
|
setIsPTRing(true)
|
||||||
|
await refreshFeed()
|
||||||
setIsPTRing(false)
|
setIsPTRing(false)
|
||||||
}, [refetch, setIsPTRing, onHasNew, feed, feedType])
|
}
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
refreshFeed,
|
||||||
|
}))
|
||||||
|
|
||||||
const onEndReached = useCallback(async () => {
|
const onEndReached = useCallback(async () => {
|
||||||
if (isFetching || !hasNextPage || isError) return
|
if (isFetching || !hasNextPage || isError) return
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, {useCallback, useMemo} from 'react'
|
import React, {useCallback, useMemo, useRef, useState} from 'react'
|
||||||
import {StyleSheet} from 'react-native'
|
import {StyleSheet} from 'react-native'
|
||||||
import {SafeAreaView} from 'react-native-safe-area-context'
|
import {SafeAreaView} from 'react-native-safe-area-context'
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +11,7 @@ import {msg} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
import {ScrollForwarderView} from 'modules/react-native-scroll-forwarder/src'
|
||||||
|
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||||
@@ -46,7 +47,7 @@ import * as Layout from '#/components/Layout'
|
|||||||
import {ScreenHider} from '#/components/moderation/ScreenHider'
|
import {ScreenHider} from '#/components/moderation/ScreenHider'
|
||||||
import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks'
|
import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks'
|
||||||
import {navigate} from '#/Navigation'
|
import {navigate} from '#/Navigation'
|
||||||
import {ExpoScrollForwarderView} from '../../../modules/expo-scroll-forwarder'
|
import {type PostFeedRef} from '../com/posts/PostFeed'
|
||||||
|
|
||||||
interface SectionRef {
|
interface SectionRef {
|
||||||
scrollToTop: () => void
|
scrollToTop: () => void
|
||||||
@@ -178,6 +179,7 @@ function ProfileScreenLoaded({
|
|||||||
enabled: !!profile.associated?.labeler,
|
enabled: !!profile.associated?.labeler,
|
||||||
})
|
})
|
||||||
const [currentPage, setCurrentPage] = React.useState(0)
|
const [currentPage, setCurrentPage] = React.useState(0)
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
const [scrollViewTag, setScrollViewTag] = React.useState<number | null>(null)
|
const [scrollViewTag, setScrollViewTag] = React.useState<number | null>(null)
|
||||||
@@ -334,6 +336,14 @@ function ProfileScreenLoaded({
|
|||||||
scrollSectionToTop(index)
|
scrollSectionToTop(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const postFeedRef = useRef<PostFeedRef>()
|
||||||
|
|
||||||
|
const onRefresh = async () => {
|
||||||
|
setIsRefreshing(true)
|
||||||
|
await postFeedRef.current?.refreshFeed()
|
||||||
|
setIsRefreshing(false)
|
||||||
|
}
|
||||||
|
|
||||||
// rendering
|
// rendering
|
||||||
// =
|
// =
|
||||||
|
|
||||||
@@ -343,7 +353,10 @@ function ProfileScreenLoaded({
|
|||||||
setMinimumHeight: (height: number) => void
|
setMinimumHeight: (height: number) => void
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ExpoScrollForwarderView scrollViewTag={scrollViewTag}>
|
<ScrollForwarderView
|
||||||
|
scrollViewTag={scrollViewTag}
|
||||||
|
refreshing={isRefreshing}
|
||||||
|
onRefresh={onRefresh}>
|
||||||
<ProfileHeader
|
<ProfileHeader
|
||||||
profile={profile}
|
profile={profile}
|
||||||
labeler={labelerInfo}
|
labeler={labelerInfo}
|
||||||
@@ -353,7 +366,7 @@ function ProfileScreenLoaded({
|
|||||||
isPlaceholderProfile={showPlaceholder}
|
isPlaceholderProfile={showPlaceholder}
|
||||||
setMinimumHeight={setMinimumHeight}
|
setMinimumHeight={setMinimumHeight}
|
||||||
/>
|
/>
|
||||||
</ExpoScrollForwarderView>
|
</ScrollForwarderView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,6 +421,7 @@ function ProfileScreenLoaded({
|
|||||||
scrollElRef={scrollElRef as ListRef}
|
scrollElRef={scrollElRef as ListRef}
|
||||||
ignoreFilterFor={profile.did}
|
ignoreFilterFor={profile.did}
|
||||||
setScrollViewTag={setScrollViewTag}
|
setScrollViewTag={setScrollViewTag}
|
||||||
|
postFeedRef={postFeedRef}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
|
|||||||
@@ -17436,6 +17436,10 @@ react-native-screens@^4.11.1:
|
|||||||
react-native-is-edge-to-edge "^1.1.7"
|
react-native-is-edge-to-edge "^1.1.7"
|
||||||
warn-once "^0.1.0"
|
warn-once "^0.1.0"
|
||||||
|
|
||||||
|
"react-native-scroll-forwarder@link:./modules/react-native-scroll-forwarder":
|
||||||
|
version "0.0.0"
|
||||||
|
uid ""
|
||||||
|
|
||||||
react-native-svg@15.12.0:
|
react-native-svg@15.12.0:
|
||||||
version "15.12.0"
|
version "15.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.12.0.tgz#0e2d476961e8b07f8c549fe4489c99b5130dc150"
|
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.12.0.tgz#0e2d476961e8b07f8c549fe4489c99b5130dc150"
|
||||||
|
|||||||
Reference in New Issue
Block a user