Revert "fabric: update scroll forwarder for profile headers (#8366)"
This reverts commit 46531f0e8d059bd07614947bdf76452526ece70d.
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"platforms": ["ios"],
|
||||||
|
"ios": {
|
||||||
|
"modules": ["ExpoScrollForwarderModule"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {ExpoScrollForwarderView} from './src/ExpoScrollForwarderView'
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
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 scrollView: UIScrollView?
|
||||||
|
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 sv = self.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.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
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findScrollView(in view: UIView, foundCount: Int) -> UIScrollView? {
|
||||||
|
var foundCount = foundCount
|
||||||
|
if let sv = view as? UIScrollView { return sv }
|
||||||
|
for child in view.subviews {
|
||||||
|
if let found = findScrollView(in: child, foundCount: foundCount) {
|
||||||
|
if foundCount == 1 {
|
||||||
|
print("found sv: \(found)")
|
||||||
|
// return found
|
||||||
|
} else {
|
||||||
|
print("found sv: \(found)")
|
||||||
|
foundCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryFindScrollView() {
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
guard let sv = self.findScrollView(in: self.superview!.superview!.superview!, foundCount: 0) else {
|
||||||
|
print("⚠️ ExpoScrollForwarder: couldn’t find UIScrollView under tag \(tag)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.scrollView = sv
|
||||||
|
self.rctRefreshCtrl = sv.refreshControl as? RCTRefreshControl
|
||||||
|
|
||||||
|
self.addCancelGestureRecognizers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCancelGestureRecognizers() {
|
||||||
|
self.cancelGestureRecognizers?.forEach { r in
|
||||||
|
self.scrollView?.addGestureRecognizer(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeCancelGestureRecognizers() {
|
||||||
|
self.cancelGestureRecognizers?.forEach { r in
|
||||||
|
self.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.scrollView?.scrollRectToVisible(CGRect(x: 0, y: offset, width: 0, height: 0), animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopTimer() {
|
||||||
|
self.disableCancelGestureRecognizers()
|
||||||
|
self.animTimer?.invalidate()
|
||||||
|
self.animTimer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ExpoScrollForwarderViewProps {
|
||||||
|
scrollViewTag: number | null
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import {type ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types'
|
||||||
|
|
||||||
|
export function ExpoScrollForwarderView({
|
||||||
|
children,
|
||||||
|
}: React.PropsWithChildren<ExpoScrollForwarderViewProps>) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#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 */
|
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import {
|
|
||||||
default as NativeScrollForwarderView,
|
|
||||||
type NativeProps,
|
|
||||||
} from './ScrollForwarderViewNativeComponent'
|
|
||||||
|
|
||||||
export function ScrollForwarderView({children, ...rest}: NativeProps) {
|
|
||||||
return (
|
|
||||||
<NativeScrollForwarderView {...rest} style={{flex: 1}}>
|
|
||||||
{children}
|
|
||||||
</NativeScrollForwarderView>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import {NativeProps} from './ScrollForwarderViewNativeComponent'
|
|
||||||
|
|
||||||
export function ScrollForwarderView({children}: NativeProps) {
|
|
||||||
return children
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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')
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export {ScrollForwarderView} from './ScrollForwarderView'
|
|
||||||
export * from './ScrollForwarderViewNativeComponent'
|
|
||||||
+1
-3
@@ -215,8 +215,7 @@
|
|||||||
"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.133",
|
"@atproto/dev-env": "^0.3.133",
|
||||||
@@ -230,7 +229,6 @@
|
|||||||
"@react-native/babel-preset": "0.79.2",
|
"@react-native/babel-preset": "0.79.2",
|
||||||
"@react-native/eslint-config": "^0.79.2",
|
"@react-native/eslint-config": "^0.79.2",
|
||||||
"@react-native/typescript-config": "^0.79.2",
|
"@react-native/typescript-config": "^0.79.2",
|
||||||
"@react-native/babel-preset": "0.79.2",
|
|
||||||
"@sentry/webpack-plugin": "^3.2.2",
|
"@sentry/webpack-plugin": "^3.2.2",
|
||||||
"@testing-library/jest-native": "^5.4.3",
|
"@testing-library/jest-native": "^5.4.3",
|
||||||
"@testing-library/react-native": "^13.2.0",
|
"@testing-library/react-native": "^13.2.0",
|
||||||
|
|||||||
@@ -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, PostFeedRef} from '#/view/com/posts/PostFeed'
|
import {PostFeed} 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,8 +24,6 @@ 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,
|
||||||
@@ -38,8 +36,6 @@ export const ProfileFeedSection = React.forwardRef<
|
|||||||
scrollElRef,
|
scrollElRef,
|
||||||
ignoreFilterFor,
|
ignoreFilterFor,
|
||||||
setScrollViewTag,
|
setScrollViewTag,
|
||||||
postFeedRef,
|
|
||||||
onRefreshEnd,
|
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
@@ -95,8 +91,6 @@ 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,12 +1,4 @@
|
|||||||
import {
|
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||||
memo,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
AppState,
|
AppState,
|
||||||
@@ -163,10 +155,6 @@ 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
|
||||||
@@ -193,7 +181,6 @@ let PostFeed = ({
|
|||||||
savedFeedConfig,
|
savedFeedConfig,
|
||||||
initialNumToRender: initialNumToRenderOverride,
|
initialNumToRender: initialNumToRenderOverride,
|
||||||
isVideoFeed = false,
|
isVideoFeed = false,
|
||||||
ref,
|
|
||||||
}: {
|
}: {
|
||||||
feed: FeedDescriptor
|
feed: FeedDescriptor
|
||||||
feedParams?: FeedParams
|
feedParams?: FeedParams
|
||||||
@@ -216,7 +203,6 @@ 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()
|
||||||
@@ -599,31 +585,22 @@ let PostFeed = ({
|
|||||||
|
|
||||||
// events
|
// events
|
||||||
// =
|
// =
|
||||||
//
|
|
||||||
|
|
||||||
const refreshFeed = async () => {
|
const onRefresh = React.useCallback(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, useRef, useState} from 'react'
|
import React, {useCallback, useMemo} 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 {
|
||||||
@@ -46,8 +46,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 {ScrollForwarderView} from 'modules/react-native-scroll-forwarder/src'
|
import {ExpoScrollForwarderView} from '../../../modules/expo-scroll-forwarder'
|
||||||
import {PostFeedRef} from '../com/posts/PostFeed'
|
|
||||||
|
|
||||||
interface SectionRef {
|
interface SectionRef {
|
||||||
scrollToTop: () => void
|
scrollToTop: () => void
|
||||||
@@ -179,7 +178,6 @@ 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)
|
||||||
@@ -336,14 +334,6 @@ function ProfileScreenLoaded({
|
|||||||
scrollSectionToTop(index)
|
scrollSectionToTop(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
const postFeedRef = useRef<PostFeedRef>()
|
|
||||||
|
|
||||||
const onRefresh = async () => {
|
|
||||||
setIsRefreshing(true)
|
|
||||||
await postFeedRef.current?.refreshFeed()
|
|
||||||
setIsRefreshing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// rendering
|
// rendering
|
||||||
// =
|
// =
|
||||||
|
|
||||||
@@ -353,10 +343,7 @@ function ProfileScreenLoaded({
|
|||||||
setMinimumHeight: (height: number) => void
|
setMinimumHeight: (height: number) => void
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ScrollForwarderView
|
<ExpoScrollForwarderView scrollViewTag={scrollViewTag}>
|
||||||
scrollViewTag={scrollViewTag}
|
|
||||||
refreshing={isRefreshing}
|
|
||||||
onRefresh={onRefresh}>
|
|
||||||
<ProfileHeader
|
<ProfileHeader
|
||||||
profile={profile}
|
profile={profile}
|
||||||
labeler={labelerInfo}
|
labeler={labelerInfo}
|
||||||
@@ -366,7 +353,7 @@ function ProfileScreenLoaded({
|
|||||||
isPlaceholderProfile={showPlaceholder}
|
isPlaceholderProfile={showPlaceholder}
|
||||||
setMinimumHeight={setMinimumHeight}
|
setMinimumHeight={setMinimumHeight}
|
||||||
/>
|
/>
|
||||||
</ScrollForwarderView>
|
</ExpoScrollForwarderView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,7 +408,6 @@ function ProfileScreenLoaded({
|
|||||||
scrollElRef={scrollElRef as ListRef}
|
scrollElRef={scrollElRef as ListRef}
|
||||||
ignoreFilterFor={profile.did}
|
ignoreFilterFor={profile.did}
|
||||||
setScrollViewTag={setScrollViewTag}
|
setScrollViewTag={setScrollViewTag}
|
||||||
postFeedRef={postFeedRef}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
|
|||||||
@@ -16975,10 +16975,6 @@ 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