Compare commits

...

8 Commits

Author SHA1 Message Date
Samuel Newman cb42857c49 rm unneccessary async 2025-08-13 10:43:09 +03:00
Samuel Newman 5c87187171 stack sentry patches 2025-08-13 10:41:32 +03:00
hailey 2c7cab5ae5 fabric: update scroll forwarder for profile headers (#8366)
* progress

* remove old module

* nil delegates

* clean up problems

* fix refreshing

* bump
2025-08-13 10:36:10 +03:00
Hailey 3d447cc841 fix bottom sheet 2025-08-13 10:36:10 +03:00
Hailey 355181284e working 2025-08-13 10:36:10 +03:00
Samuel Newman d799265b3f patch broken packages 2025-08-13 10:34:37 +03:00
Samuel Newman 95ecdf7373 bump reanimated to nightly 2025-08-13 10:34:37 +03:00
Samuel Newman 0ba6dac299 upgrade rn 2025-08-13 10:33:03 +03:00
33 changed files with 1940 additions and 1102 deletions
+1 -1
View File
@@ -1 +1 @@
20
20.19.4
+1 -1
View File
@@ -207,7 +207,7 @@ module.exports = function (_config) {
{
ios: {
deploymentTarget: '15.1',
newArchEnabled: false,
newArchEnabled: true,
},
android: {
compileSdkVersion: 35,
+19 -18
View File
@@ -5,7 +5,7 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
// Views
private var sheetVc: SheetViewController?
private var innerView: UIView?
private var touchHandler: RCTTouchHandler?
private var touchHandler: RCTSurfaceTouchHandler?
// Events
private let onAttemptDismiss = EventDispatcher()
@@ -73,33 +73,35 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
required init (appContext: AppContext? = nil) {
super.init(appContext: appContext)
self.maxHeight = Util.getScreenHeight()
self.touchHandler = RCTTouchHandler(bridge: appContext?.reactBridge)
self.touchHandler = RCTSurfaceTouchHandler()
SheetManager.shared.add(self)
}
deinit {
self.destroy()
}
// We don't want this view to actually get added to the tree, so we'll simply store it for adding
// to the SheetViewController
override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
self.touchHandler?.attach(to: subview)
self.innerView = subview
override func mountChildComponentView(
_ childComponentView: UIView,
index: Int
) {
self.innerView = childComponentView
touchHandler?.attach(to: childComponentView)
}
override func unmountChildComponentView(
_ childComponentView: UIView,
index: Int
) {
touchHandler?.detach(from: childComponentView)
if self.innerView === childComponentView {
self.innerView = nil
}
}
// We'll grab the content height from here so we know the initial detent to set
override func layoutSubviews() {
super.layoutSubviews()
guard let innerView = self.innerView else {
return
}
if innerView.subviews.count != 1 {
return
}
self.present()
}
@@ -107,7 +109,6 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
self.isClosing = false
self.isOpen = false
self.sheetVc = nil
self.touchHandler?.detach(from: self.innerView)
self.touchHandler = nil
self.innerView = nil
SheetManager.shared.remove(self)
@@ -0,0 +1,3 @@
#if __has_include(<React/RCTSurfaceTouchHandler.h>)
#import <React/RCTSurfaceTouchHandler.h>
#endif
@@ -1,6 +0,0 @@
{
"platforms": ["ios"],
"ios": {
"modules": ["ExpoScrollForwarderModule"]
}
}
-1
View File
@@ -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'
+51 -48
View File
@@ -3,7 +3,7 @@
"version": "1.106.0",
"private": true,
"engines": {
"node": ">=20"
"node": ">=20.19.4"
},
"packageManager": "yarn@1.22.22",
"expo": {
@@ -89,17 +89,17 @@
"@haileyok/bluesky-video": "0.3.2",
"@ipld/dag-cbor": "^9.2.0",
"@lingui/react": "^4.14.1",
"@mattermost/react-native-paste-input": "mattermost/react-native-paste-input",
"@mattermost/react-native-paste-input": "bluesky-social/react-native-paste-input#112383d08e1581214dc4beb864b996cb32e023c7",
"@miblanchard/react-native-slider": "^2.6.0",
"@mozzius/expo-dynamic-app-icon": "1.5.0",
"@react-native-async-storage/async-storage": "2.1.2",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-menu/menu": "^1.2.3",
"@react-native-picker/picker": "2.11.0",
"@react-native-picker/picker": "2.11.1",
"@react-navigation/bottom-tabs": "^7.3.13",
"@react-navigation/drawer": "^7.3.12",
"@react-navigation/native": "^7.1.9",
"@react-navigation/native-stack": "^7.3.13",
"@sentry/react-native": "~6.14.0",
"@sentry/react-native": "~6.15.1",
"@tanstack/query-async-storage-persister": "^5.25.0",
"@tanstack/react-query": "^5.8.1",
"@tanstack/react-query-persist-client": "^5.25.0",
@@ -131,35 +131,35 @@
"emoji-mart": "^5.5.2",
"emoji-regex": "^10.4.0",
"eventemitter3": "^5.0.1",
"expo": "53.0.11",
"expo-application": "~6.1.4",
"expo-blur": "~14.1.5",
"expo-build-properties": "~0.14.6",
"expo-camera": "~16.1.8",
"expo-clipboard": "~7.1.4",
"expo-dev-client": "~5.2.0",
"expo-device": "~7.1.4",
"expo-file-system": "~18.1.10",
"expo-font": "~13.3.1",
"expo-haptics": "~14.1.4",
"expo-image": "^2.4.0",
"expo": "^54.0.0-canary-20250729-d8899ae",
"expo-application": "6.1.6-canary-20250729-d8899ae",
"expo-blur": "14.1.6-canary-20250729-d8899ae",
"expo-build-properties": "0.15.0-canary-20250729-d8899ae",
"expo-camera": "16.2.0-canary-20250729-d8899ae",
"expo-clipboard": "7.1.6-canary-20250729-d8899ae",
"expo-dev-client": "5.1.9-canary-20250729-d8899ae",
"expo-device": "7.1.5-canary-20250729-d8899ae",
"expo-file-system": "18.2.0-canary-20250729-d8899ae",
"expo-font": "13.4.0-canary-20250729-d8899ae",
"expo-haptics": "14.1.5-canary-20250729-d8899ae",
"expo-image": "2.5.0-canary-20250729-d8899ae",
"expo-image-crop-tool": "^0.1.8",
"expo-image-manipulator": "~13.1.7",
"expo-image-picker": "~16.1.4",
"expo-intent-launcher": "^12.1.5",
"expo-linear-gradient": "~14.1.5",
"expo-linking": "~7.1.5",
"expo-localization": "~16.1.5",
"expo-media-library": "~17.1.7",
"expo-notifications": "~0.31.3",
"expo-screen-orientation": "~8.1.7",
"expo-sharing": "~13.1.5",
"expo-splash-screen": "~0.30.9",
"expo-system-ui": "~5.0.8",
"expo-task-manager": "~13.1.5",
"expo-updates": "~0.28.14",
"expo-video": "~2.2.1",
"expo-web-browser": "~14.1.6",
"expo-image-manipulator": "13.1.8-canary-20250729-d8899ae",
"expo-image-picker": "17.0.0-canary-20250729-d8899ae",
"expo-intent-launcher": "13.0.0-canary-20250729-d8899ae",
"expo-linear-gradient": "14.1.6-canary-20250729-d8899ae",
"expo-linking": "7.1.8-canary-20250729-d8899ae",
"expo-localization": "16.2.0-canary-20250729-d8899ae",
"expo-media-library": "18.0.0-canary-20250729-d8899ae",
"expo-notifications": "0.31.5-canary-20250729-d8899ae",
"expo-screen-orientation": "8.1.8-canary-20250729-d8899ae",
"expo-sharing": "13.1.6-canary-20250729-d8899ae",
"expo-splash-screen": "0.30.11-canary-20250729-d8899ae",
"expo-system-ui": "5.0.11-canary-20250729-d8899ae",
"expo-task-manager": "13.1.7-canary-20250729-d8899ae",
"expo-updates": "0.29.0-canary-20250729-d8899ae",
"expo-video": "3.0.0-canary-20250729-d8899ae",
"expo-web-browser": "14.1.7-canary-20250729-d8899ae",
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
"hls.js": "^1.6.2",
@@ -171,6 +171,7 @@
"lodash.isequal": "^4.5.0",
"lodash.shuffle": "^4.2.0",
"lodash.throttle": "^4.1.1",
"metro-cache": "^0.83.1",
"multiformats": "9.9.0",
"nanoid": "^5.0.5",
"normalize-url": "^8.0.0",
@@ -178,38 +179,39 @@
"postinstall-postinstall": "^2.1.0",
"psl": "^1.9.0",
"radix-ui": "^1.2.0",
"react": "19.0.0",
"react": "^19.1.0",
"react-compiler-runtime": "^19.1.0-rc.1",
"react-dom": "19.0.0",
"react-dom": "19.1.0",
"react-image-crop": "^11.0.7",
"react-is": "19",
"react-keyed-flatten-children": "^5.0.0",
"react-native": "^0.79.3",
"react-native": "0.81.0",
"react-native-compressor": "^1.11.0",
"react-native-date-picker": "^5.0.12",
"react-native-device-attest": "^0.1.6",
"react-native-drawer-layout": "^4.1.8",
"react-native-edge-to-edge": "^1.6.0",
"react-native-gesture-handler": "2.25.0",
"react-native-gesture-handler": "~2.26.0",
"react-native-get-random-values": "~1.11.0",
"react-native-ios-context-menu": "^1.15.3",
"react-native-keyboard-controller": "^1.17.5",
"react-native-mmkv": "^2.12.2",
"react-native-mmkv": "^3.3.0",
"react-native-pager-view": "6.8.0",
"react-native-progress": "bluesky-social/react-native-progress",
"react-native-qrcode-styled": "^0.3.3",
"react-native-reanimated": "~3.17.5",
"react-native-reanimated": "^4.0.2",
"react-native-root-siblings": "^5.0.1",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "^4.11.1",
"react-native-svg": "15.12.0",
"react-native-uitextview": "^1.4.0",
"react-native-uitextview": "^2.1.0-rc.0",
"react-native-url-polyfill": "^1.3.0",
"react-native-uuid": "^2.0.3",
"react-native-view-shot": "^4.0.3",
"react-native-web": "~0.20.0",
"react-native-web-webview": "^1.0.2",
"react-native-webview": "^13.13.5",
"react-native-webview": "13.13.5",
"react-native-worklets": "^0.4.1",
"react-remove-scroll-bar": "^2.3.8",
"react-responsive": "^9.0.2",
"react-textarea-autosize": "^8.5.3",
@@ -218,7 +220,8 @@
"tlds": "^1.234.0",
"tldts": "^6.1.46",
"zeego": "^1.6.2",
"zod": "^3.20.2"
"zod": "^3.20.2",
"react-native-scroll-forwarder": "link:./modules/react-native-scroll-forwarder"
},
"devDependencies": {
"@atproto/dev-env": "^0.3.160",
@@ -229,9 +232,9 @@
"@lingui/cli": "^4.14.1",
"@lingui/macro": "^4.14.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@react-native/babel-preset": "0.79.3",
"@react-native/eslint-config": "^0.79.3",
"@react-native/typescript-config": "^0.79.3",
"@react-native/babel-preset": "0.81.0",
"@react-native/eslint-config": "0.81.0",
"@react-native/typescript-config": "0.81.0",
"@sentry/webpack-plugin": "^3.2.2",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "^13.2.0",
@@ -263,7 +266,7 @@
"husky": "^8.0.3",
"is-ci": "^3.0.1",
"jest": "^29.7.0",
"jest-expo": "~53.0.7",
"jest-expo": "54.0.0-canary-20250613-b29d676",
"jest-junit": "^16.0.0",
"lint-staged": "^13.2.3",
"lockfile-lint": "^4.14.0",
@@ -278,8 +281,8 @@
},
"resolutions": {
"@expo/image-utils": "0.6.3",
"@react-native/babel-preset": "0.79.3",
"@react-native/normalize-colors": "0.79.3",
"@react-native/babel-preset": "0.80.0",
"@react-native/normalize-colors": "0.80.0",
"@types/react": "^18",
"**/expo-constants": "17.0.3",
"**/expo-device": "7.1.4",
@@ -0,0 +1,18 @@
diff --git a/node_modules/@sentry/react-native/RNSentry.podspec b/node_modules/@sentry/react-native/RNSentry.podspec
index 2ad70ce..711d7f0 100644
--- a/node_modules/@sentry/react-native/RNSentry.podspec
+++ b/node_modules/@sentry/react-native/RNSentry.podspec
@@ -7,12 +7,9 @@ rn_version = get_rn_version(rn_package)
is_hermes_default = is_hermes_default(rn_version)
is_profiling_supported = is_profiling_supported(rn_version)
-folly_flags = ' -DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1'
-folly_compiler_flags = folly_flags + ' ' + '-Wno-comma -Wno-shorten-64-to-32'
-
is_new_arch_enabled = ENV["RCT_NEW_ARCH_ENABLED"] == "1"
is_using_hermes = (ENV['USE_HERMES'] == nil && is_hermes_default) || ENV['USE_HERMES'] == '1'
-new_arch_enabled_flag = (is_new_arch_enabled ? folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED" : "")
+new_arch_enabled_flag = (is_new_arch_enabled ? " -DRCT_NEW_ARCH_ENABLED" : "")
sentry_profiling_supported_flag = (is_profiling_supported ? " -DSENTRY_PROFILING_SUPPORTED=1" : "")
other_cflags = "$(inherited)" + new_arch_enabled_flag + sentry_profiling_supported_flag
@@ -15,7 +15,7 @@ diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/Scro
index d029337..0f63ea3 100644
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
@@ -1003,6 +1003,11 @@ - (void)_adjustForMaintainVisibleContentPosition
@@ -1038,6 +1038,11 @@ - (void)_adjustForMaintainVisibleContentPosition
}
}
@@ -0,0 +1,26 @@
diff --git a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt
index 0958648..42040b4 100644
--- a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt
+++ b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt
@@ -58,7 +58,7 @@ class AudioHelper {
var destinationBitrate = originalBitrate
Utils.addLog("source bitrate: $originalBitrate")
- when (quality.toLowerCase()) {
+ when (quality.lowercase()) {
"low" -> destinationBitrate = maxOf(64, (originalBitrate * 0.3).toInt())
"medium" -> destinationBitrate = (originalBitrate * 0.5).toInt()
"high" -> destinationBitrate = minOf(320, (originalBitrate * 0.7).toInt())
diff --git a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Utils/Uploader.kt b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Utils/Uploader.kt
index 8fd7c6a..7b6b498 100644
--- a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Utils/Uploader.kt
+++ b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Utils/Uploader.kt
@@ -161,7 +161,7 @@ class Uploader(private val reactContext: ReactApplicationContext) {
if (mimeType == null) {
val fileExtension = MimeTypeMap.getFileExtensionFromUrl(fileUri.toString())
if (fileExtension != null) {
- return MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase())
+ return MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.lowercase())
}
}
@@ -1,44 +0,0 @@
diff --git a/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.cpp b/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.cpp
index eae3989..432745a 100644
--- a/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.cpp
+++ b/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.cpp
@@ -416,6 +416,10 @@ void NativeProxy::progressLayoutAnimation(
tag, newPropsJNI, isSharedTransition);
}
+void NativeProxy::endLayoutAnimation(int tag, bool shouldRemove) {
+ layoutAnimations_->cthis()->endLayoutAnimation(tag, shouldRemove);
+}
+
PlatformDepMethodsHolder NativeProxy::getPlatformDependentMethods() {
#ifdef RCT_NEW_ARCH_ENABLED
// nothing
@@ -455,14 +459,7 @@ PlatformDepMethodsHolder NativeProxy::getPlatformDependentMethods() {
auto progressLayoutAnimation =
bindThis(&NativeProxy::progressLayoutAnimation);
- auto endLayoutAnimation = [weakThis = weak_from_this()](
- int tag, bool removeView) {
- auto strongThis = weakThis.lock();
- if (!strongThis) {
- return;
- }
- strongThis->layoutAnimations_->cthis()->endLayoutAnimation(tag, removeView);
- };
+ auto endLayoutAnimation = bindThis(&NativeProxy::endLayoutAnimation);
auto maybeFlushUiUpdatesQueueFunction =
bindThis(&NativeProxy::maybeFlushUIUpdatesQueue);
diff --git a/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.h b/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.h
index 2ee2cc8..2edb5c9 100644
--- a/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.h
+++ b/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.h
@@ -234,6 +234,8 @@ class NativeProxy : public jni::HybridClass<NativeProxy>,
const jsi::Object &newProps,
bool isSharedTransition);
+ void endLayoutAnimation(int tag, bool shouldRemove);
+
/***
* Wraps a method of `NativeProxy` in a function object capturing `this`
* @tparam TReturn return type of passed method
+1 -1
View File
@@ -182,7 +182,7 @@ export async function dynamicActivate(locale: AppLanguage) {
i18n.activate(locale)
}
export async function useLocaleLanguage() {
export function useLocaleLanguage() {
const {appLanguage} = useLanguagePrefs()
useEffect(() => {
const sanitizedLanguage = sanitizeAppLanguageSetting(appLanguage)
+7 -1
View File
@@ -9,7 +9,7 @@ import {isIOS, isNative} from '#/platform/detection'
import {type FeedDescriptor} from '#/state/queries/post-feed'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
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 {type ListRef} from '#/view/com/util/List'
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
@@ -24,6 +24,8 @@ interface FeedSectionProps {
scrollElRef: ListRef
ignoreFilterFor?: string
setScrollViewTag: (tag: number | null) => void
postFeedRef?: React.RefObject<PostFeedRef | undefined>
onRefreshEnd?: () => void
}
export const ProfileFeedSection = React.forwardRef<
SectionRef,
@@ -36,6 +38,8 @@ export const ProfileFeedSection = React.forwardRef<
scrollElRef,
ignoreFilterFor,
setScrollViewTag,
postFeedRef,
onRefreshEnd,
},
ref,
) {
@@ -91,6 +95,8 @@ export const ProfileFeedSection = React.forwardRef<
shouldUseAdjustedNumToRender ? adjustedInitialNumToRender : undefined
}
isVideoFeed={isVideoFeed}
ref={postFeedRef}
onRefreshEnd={onRefreshEnd}
/>
{(isScrolledDown || hasNew) && (
<LoadLatestBtn
+27 -4
View File
@@ -1,4 +1,12 @@
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {
memo,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import {
ActivityIndicator,
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
// const REFRESH_AFTER = STALE.HOURS.ONE
const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY
@@ -189,6 +201,7 @@ let PostFeed = ({
savedFeedConfig,
initialNumToRender: initialNumToRenderOverride,
isVideoFeed = false,
ref,
}: {
feed: FeedDescriptor
feedParams?: FeedParams
@@ -211,6 +224,7 @@ let PostFeed = ({
savedFeedConfig?: AppBskyActorDefs.SavedFeed
initialNumToRender?: number
isVideoFeed?: boolean
ref?: React.ForwardedRef<PostFeedRef>
}): React.ReactNode => {
const {_} = useLingui()
const queryClient = useQueryClient()
@@ -629,22 +643,31 @@ let PostFeed = ({
// events
// =
//
const onRefresh = useCallback(async () => {
const refreshFeed = async () => {
logEvent('feed:refresh', {
feedType: feedType,
feedUrl: feed,
reason: 'pull-to-refresh',
})
setIsPTRing(true)
try {
await refetch()
onHasNew?.(false)
} catch (err) {
logger.error('Failed to refresh posts feed', {message: err})
}
}
const onRefresh = async () => {
setIsPTRing(true)
await refreshFeed()
setIsPTRing(false)
}, [refetch, setIsPTRing, onHasNew, feed, feedType])
}
useImperativeHandle(ref, () => ({
refreshFeed,
}))
const onEndReached = useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
+18 -4
View File
@@ -1,4 +1,4 @@
import React, {useCallback, useMemo} from 'react'
import React, {useCallback, useMemo, useRef, useState} from 'react'
import {StyleSheet} from 'react-native'
import {SafeAreaView} from 'react-native-safe-area-context'
import {
@@ -11,6 +11,7 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {ScrollForwarderView} from 'modules/react-native-scroll-forwarder/src'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useSetTitle} from '#/lib/hooks/useSetTitle'
@@ -46,7 +47,7 @@ import * as Layout from '#/components/Layout'
import {ScreenHider} from '#/components/moderation/ScreenHider'
import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks'
import {navigate} from '#/Navigation'
import {ExpoScrollForwarderView} from '../../../modules/expo-scroll-forwarder'
import {type PostFeedRef} from '../com/posts/PostFeed'
interface SectionRef {
scrollToTop: () => void
@@ -178,6 +179,7 @@ function ProfileScreenLoaded({
enabled: !!profile.associated?.labeler,
})
const [currentPage, setCurrentPage] = React.useState(0)
const [isRefreshing, setIsRefreshing] = useState(false)
const {_} = useLingui()
const [scrollViewTag, setScrollViewTag] = React.useState<number | null>(null)
@@ -334,6 +336,14 @@ function ProfileScreenLoaded({
scrollSectionToTop(index)
}
const postFeedRef = useRef<PostFeedRef>()
const onRefresh = async () => {
setIsRefreshing(true)
await postFeedRef.current?.refreshFeed()
setIsRefreshing(false)
}
// rendering
// =
@@ -343,7 +353,10 @@ function ProfileScreenLoaded({
setMinimumHeight: (height: number) => void
}) => {
return (
<ExpoScrollForwarderView scrollViewTag={scrollViewTag}>
<ScrollForwarderView
scrollViewTag={scrollViewTag}
refreshing={isRefreshing}
onRefresh={onRefresh}>
<ProfileHeader
profile={profile}
labeler={labelerInfo}
@@ -353,7 +366,7 @@ function ProfileScreenLoaded({
isPlaceholderProfile={showPlaceholder}
setMinimumHeight={setMinimumHeight}
/>
</ExpoScrollForwarderView>
</ScrollForwarderView>
)
}
@@ -408,6 +421,7 @@ function ProfileScreenLoaded({
scrollElRef={scrollElRef as ListRef}
ignoreFilterFor={profile.did}
setScrollViewTag={setScrollViewTag}
postFeedRef={postFeedRef}
/>
)
: null}
+1249 -701
View File
File diff suppressed because it is too large Load Diff