diff --git a/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm b/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm index caa5540..c5d4e67 100644 --- a/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm +++ b/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm @@ -73,7 +73,7 @@ @implementation RCTFileReaderModule } else { NSString *type = [RCTConvert NSString:blob[@"type"]]; NSString *text = [NSString stringWithFormat:@"data:%@;base64,%@", - type != nil && [type length] > 0 ? type : @"application/octet-stream", + ![type isEqual:[NSNull null]] && [type length] > 0 ? type : @"application/octet-stream", [data base64EncodedStringWithOptions:0]]; resolve(text); diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm index b0d71dc..41b9a0e 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm @@ -377,10 +377,6 @@ - (void)textInputDidBeginEditing self.backedTextInputView.attributedText = [NSAttributedString new]; } - if (_selectTextOnFocus) { - [self.backedTextInputView selectAll:nil]; - } - [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus reactTag:self.reactTag text:[self.backedTextInputView.attributedText.string copy] @@ -611,6 +607,10 @@ - (UIView *)reactAccessibilityElement - (void)reactFocus { [self.backedTextInputView reactFocus]; + + if (_selectTextOnFocus) { + [self.backedTextInputView selectAll:nil]; + } } - (void)reactBlur diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h index e9b330f..ec5f58c 100644 --- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h +++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h @@ -15,5 +15,8 @@ @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) RCTDirectEventBlock onRefresh; @property (nonatomic, weak) UIScrollView *scrollView; +@property (nonatomic, copy) UIColor *customTintColor; + +- (void)forwarderBeginRefreshing; @end diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m index b09e653..d2b4e05 100644 --- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m +++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m @@ -22,6 +22,7 @@ @implementation RCTRefreshControl { NSString *_title; UIColor *_titleColor; CGFloat _progressViewOffset; + UIColor *_customTintColor; } - (instancetype)init @@ -56,6 +57,12 @@ - (void)layoutSubviews _isInitialRender = false; } +- (void)didMoveToSuperview +{ + [super didMoveToSuperview]; + [self setTintColor:_customTintColor]; +} + - (void)beginRefreshingProgrammatically { UInt64 beginRefreshingTimestamp = _currentRefreshingStateTimestamp; @@ -203,4 +210,58 @@ - (void)refreshControlValueChanged } } +- (void)setCustomTintColor:(UIColor *)customTintColor +{ + _customTintColor = customTintColor; + [self setTintColor:customTintColor]; +} + +// Fix for https://github.com/facebook/react-native/issues/43388 +// A bug in iOS 17.4 causes the haptic to not play when refreshing if the tintColor +// is set before the refresh control gets added to the scrollview. We'll call this +// function whenever the superview changes. We'll also call it if the value of customTintColor +// changes. +- (void)setTintColor:(UIColor *)tintColor +{ + if ([self.superview isKindOfClass:[UIScrollView class]] && self.tintColor != tintColor) { + [super setTintColor:tintColor]; + } +} + +/* + This method is used by Bluesky's ExpoScrollForwarder. This allows other React Native + libraries to perform a refresh of a scrollview and access the refresh control's onRefresh + function. + */ +- (void)forwarderBeginRefreshing +{ + _refreshingProgrammatically = NO; + + [self sizeToFit]; + + if (!self.scrollView) { + return; + } + + UIScrollView *scrollView = (UIScrollView *)self.scrollView; + + [UIView animateWithDuration:0.3 + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState + animations:^(void) { + // Whenever we call this method, the scrollview will always be at a position of + // -130 or less. Scrolling back to -65 simulates the default behavior of RCTRefreshControl + [scrollView setContentOffset:CGPointMake(0, -65)]; + } + completion:^(__unused BOOL finished) { + [super beginRefreshing]; + [self setCurrentRefreshingState:super.refreshing]; + + if (self->_onRefresh) { + self->_onRefresh(nil); + } + } + ]; +} + @end diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m index 40aaf9c..1c60164 100644 --- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m +++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m @@ -22,11 +22,12 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL) -RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(title, NSString) RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat) +RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor) + RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing) { [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m index 1aead51..39e6244 100644 --- a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m +++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m @@ -159,31 +159,6 @@ - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view return !shouldDisableScrollInteraction; } -/* - * Automatically centers the content such that if the content is smaller than the - * ScrollView, we force it to be centered, but when you zoom or the content otherwise - * becomes larger than the ScrollView, there is no padding around the content but it - * can still fill the whole view. - */ -- (void)setContentOffset:(CGPoint)contentOffset -{ - UIView *contentView = [self contentView]; - if (contentView && _centerContent && !CGSizeEqualToSize(contentView.frame.size, CGSizeZero)) { - CGSize subviewSize = contentView.frame.size; - CGSize scrollViewSize = self.bounds.size; - if (subviewSize.width <= scrollViewSize.width) { - contentOffset.x = -(scrollViewSize.width - subviewSize.width) / 2.0; - } - if (subviewSize.height <= scrollViewSize.height) { - contentOffset.y = -(scrollViewSize.height - subviewSize.height) / 2.0; - } - } - - super.contentOffset = CGPointMake( - RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"), - RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y")); -} - - (void)setFrame:(CGRect)frame { // Preserving and revalidating `contentOffset`. @@ -427,6 +402,11 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews // Does nothing } +- (void)setFrame:(CGRect)frame { + [super setFrame:frame]; + [self centerContentIfNeeded]; +} + - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex { [super insertReactSubview:view atIndex:atIndex]; @@ -444,6 +424,8 @@ - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex RCTApplyTransformationAccordingLayoutDirection(_contentView, self.reactLayoutDirection); [_scrollView addSubview:view]; } + + [self centerContentIfNeeded]; } - (void)removeReactSubview:(UIView *)subview @@ -652,9 +634,46 @@ -(void)delegateMethod : (UIScrollView *)scrollView \ } RCT_SCROLL_EVENT_HANDLER(scrollViewWillBeginDecelerating, onMomentumScrollBegin) -RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll) RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop) +-(void)scrollViewDidZoom : (UIScrollView *)scrollView +{ + [self centerContentIfNeeded]; + + RCT_SEND_SCROLL_EVENT(onScroll, nil); + RCT_FORWARD_SCROLL_EVENT(scrollViewDidZoom : scrollView); +} + +/* + * Automatically centers the content such that if the content is smaller than the + * ScrollView, we force it to be centered, but when you zoom or the content otherwise + * becomes larger than the ScrollView, there is no padding around the content but it + * can still fill the whole view. + * + * PATCHED: This deviates from the original React Native implementation to fix two issues: + * + * - The scroll view was swallowing any taps immediately after pinching + * - The scroll view was jittering when crossing the full screen threshold while pinching + * + * This implementation is based on https://petersteinberger.com/blog/2013/how-to-center-uiscrollview/. + */ +-(void)centerContentIfNeeded +{ + if (_scrollView.centerContent && + !CGSizeEqualToSize(self.contentSize, CGSizeZero) && + !CGSizeEqualToSize(self.bounds.size, CGSizeZero) + ) { + CGFloat top = 0, left = 0; + if (self.contentSize.width < self.bounds.size.width) { + left = (self.bounds.size.width - self.contentSize.width) * 0.5f; + } + if (self.contentSize.height < self.bounds.size.height) { + top = (self.bounds.size.height - self.contentSize.height) * 0.5f; + } + _scrollView.contentInset = UIEdgeInsetsMake(top, left, top, left); + } +} + - (void)addScrollListener:(NSObject *)scrollListener { [_scrollListeners addObject:scrollListener]; @@ -913,6 +932,7 @@ - (void)updateContentSizeIfNeeded CGSize contentSize = self.contentSize; if (!CGSizeEqualToSize(_scrollView.contentSize, contentSize)) { _scrollView.contentSize = contentSize; + [self centerContentIfNeeded]; } } diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java index 5f5e1ab..aac00b6 100644 --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java @@ -99,8 +99,9 @@ public class JavaTimerManager { } // If the JS thread is busy for multiple frames we cancel any other pending runnable. - if (mCurrentIdleCallbackRunnable != null) { - mCurrentIdleCallbackRunnable.cancel(); + IdleCallbackRunnable currentRunnable = mCurrentIdleCallbackRunnable; + if (currentRunnable != null) { + currentRunnable.cancel(); } mCurrentIdleCallbackRunnable = new IdleCallbackRunnable(frameTimeNanos);