From 9092198b1b9c65ad4c0bc0aee7a24ecc809b6a9a Mon Sep 17 00:00:00 2001 From: David Aurelio Date: Tue, 17 May 2016 09:48:00 -0700 Subject: [PATCH 001/843] Terminate startup code with a null byte, too (indexed file random access bundles) Reviewed By: javache Differential Revision: D3310269 fbshipit-source-id: d0fbf5afd6baecf50ec568e8694a15c96e6a9c85 --- local-cli/bundle/output/unbundle/as-indexed-file.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/local-cli/bundle/output/unbundle/as-indexed-file.js b/local-cli/bundle/output/unbundle/as-indexed-file.js index a475f9ab0ae3b7..45eb412c8fc60a 100644 --- a/local-cli/bundle/output/unbundle/as-indexed-file.js +++ b/local-cli/bundle/output/unbundle/as-indexed-file.js @@ -67,13 +67,14 @@ function writeBuffers(stream, buffers) { }); } +function nullTerminatedBuffer(contents, encoding) { + return Buffer.concat([Buffer(contents, encoding), nullByteBuffer]); +} + function moduleToBuffer(id, code, encoding) { return { id, - buffer: Buffer.concat([ - Buffer(code, encoding), - nullByteBuffer // create \0-terminated strings - ]) + buffer: nullTerminatedBuffer(code, encoding), }; } @@ -129,7 +130,7 @@ function buildTableAndContents(startupCode, modules, encoding) { // - code blob char[] null-terminated code strings, starting with // the startup code - const startupCodeBuffer = Buffer(startupCode, encoding); + const startupCodeBuffer = nullTerminatedBuffer(startupCode, encoding); const moduleBuffers = buildModuleBuffers(modules, encoding); const table = buildModuleTable(startupCodeBuffer, moduleBuffers); From 908041b80b859187407b840d80541c28c2f9d8eb Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Tue, 17 May 2016 10:10:23 -0700 Subject: [PATCH 002/843] Round alpha channel when interpolating colors to the nearest thousandth. Summary: This fixes an issue where animations for values near zero could end up formatted with exponents (e.g. `1.452e-10`), which is not valid for an `rgba` color spec. This commit arbitrarily rounds it to the nearest thousandth to prevent this type of formatting while still maintaining high-enough resolution in the alpha channel. One way this could bubble up to the user is as PropType validation failures: ``` Failed propType: Invalid prop `backgroundColor` supplied to `RCTView`: rgba(0, 0, 0, 9.838983123336224e-7) ``` Closes https://github.com/facebook/react-native/pull/7597 Differential Revision: D3310941 Pulled By: vjeux fbshipit-source-id: 0c95facaef5b69c021662af9fb6f268d890ecc3e --- Libraries/Animated/src/Interpolation.js | 2 +- Libraries/Animated/src/__tests__/Interpolation-test.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Libraries/Animated/src/Interpolation.js b/Libraries/Animated/src/Interpolation.js index 90f90efa1f293a..32d363ccbbed51 100644 --- a/Libraries/Animated/src/Interpolation.js +++ b/Libraries/Animated/src/Interpolation.js @@ -233,7 +233,7 @@ function createInterpolationFromStringOutputRange( // 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...' return outputRange[0].replace(stringShapeRegex, () => { const val = +interpolations[i++](input); - const rounded = shouldRound && i < 4 ? Math.round(val) : val; + const rounded = shouldRound && i < 4 ? Math.round(val) : Math.round(val * 1000) / 1000; return String(rounded); }); }; diff --git a/Libraries/Animated/src/__tests__/Interpolation-test.js b/Libraries/Animated/src/__tests__/Interpolation-test.js index af8677ac2e1760..54d3ee46026e7a 100644 --- a/Libraries/Animated/src/__tests__/Interpolation-test.js +++ b/Libraries/Animated/src/__tests__/Interpolation-test.js @@ -294,4 +294,14 @@ describe('Interpolation', () => { outputRange: ['20deg', '30rad'], })).toThrow(); }); + + it('should round the alpha channel of a color to the nearest thousandth', () => { + var interpolation = Interpolation.create({ + inputRange: [0, 1], + outputRange: ['rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 1)'], + }); + + expect(interpolation(1e-12)).toBe('rgba(0, 0, 0, 0)'); + expect(interpolation(2 / 3)).toBe('rgba(0, 0, 0, 0.667)'); + }); }); From fe5c0d2d0696b4fc5cdd65f1f2198c4f4363e543 Mon Sep 17 00:00:00 2001 From: Adam Comella Date: Tue, 17 May 2016 10:31:07 -0700 Subject: [PATCH 003/843] iOS: Enable views to be nested within Summary: Previously, only Text and Image could be nested within Text. Now, any view can be nested within Text. One restriction of this feature is that developers must give inline views a width and a height via the style prop. Previously, inline Images were supported by using iOS's built-in support for rendering images with an NSAttributedString via NSTextAttachment. However, NSAttributedString doesn't support rendering arbitrary views. This change adds support for nesting views within Text by creating one NSTextAttachment per inline view. The NSTextAttachments act as placeholders. They are set to be the size of the corresponding view. After the text is laid out, we query the text system to find out where it has positioned each NSTextAttachment. We then position the views to be at those locations. This commit also contains a change in `RCTShadowText.m` `_setParagraphStyleOnAttributedString:heightOfTallestSubview:`. It now only sets `lineHeight`, `textAlign`, and `writingDirection` when they've actua Closes https://github.com/facebook/react-native/pull/7304 Differential Revision: D3269333 Pulled By: nicklockwood fbshipit-source-id: 2b59f1c5445a4012f9c29df9f10f5010060ea517 --- Examples/UIExplorer/TextExample.ios.js | 12 +- Libraries/Image/Image.ios.js | 12 -- .../Image/RCTImage.xcodeproj/project.pbxproj | 10 -- Libraries/Image/RCTImageView.h | 3 +- Libraries/Image/RCTShadowVirtualImage.h | 28 ---- Libraries/Image/RCTShadowVirtualImage.m | 82 ------------ Libraries/Image/RCTVirtualImageManager.h | 14 -- Libraries/Image/RCTVirtualImageManager.m | 25 ---- Libraries/Text/RCTShadowText.m | 121 +++++++++++++----- Libraries/Text/RCTText.m | 20 +++ Libraries/Text/RCTTextManager.m | 13 ++ React/Modules/RCTUIManager.m | 8 ++ React/React.xcodeproj/project.pbxproj | 2 - React/Views/RCTImageComponent.h | 19 --- React/Views/RCTShadowView.h | 26 ++++ React/Views/RCTShadowView.m | 39 ++++++ docs/Text.md | 14 ++ 17 files changed, 223 insertions(+), 225 deletions(-) delete mode 100644 Libraries/Image/RCTShadowVirtualImage.h delete mode 100644 Libraries/Image/RCTShadowVirtualImage.m delete mode 100644 Libraries/Image/RCTVirtualImageManager.h delete mode 100644 Libraries/Image/RCTVirtualImageManager.m delete mode 100644 React/Views/RCTImageComponent.h diff --git a/Examples/UIExplorer/TextExample.ios.js b/Examples/UIExplorer/TextExample.ios.js index f9aef3d8d24ae4..41839fff36ad41 100644 --- a/Examples/UIExplorer/TextExample.ios.js +++ b/Examples/UIExplorer/TextExample.ios.js @@ -1,4 +1,11 @@ /** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * * The examples provided by Facebook are for non-commercial testing and * evaluation purposes only. * @@ -415,12 +422,13 @@ exports.examples = [ ); }, }, { - title: 'Inline images', + title: 'Inline views', render: function() { return ( - This text contains an inline image . Neat, huh? + This text contains an inline blue view and + an inline image . Neat, huh? ); diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 7c496656869bf2..b2f4718f7f120d 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -201,10 +201,6 @@ var Image = React.createClass({ validAttributes: ReactNativeViewAttributes.UIView }, - contextTypes: { - isInAParentText: React.PropTypes.bool - }, - render: function() { var source = resolveAssetSource(this.props.source) || {}; var {width, height, uri} = source; @@ -225,13 +221,6 @@ var Image = React.createClass({ console.warn('The component requires a `source` property rather than `src`.'); } - if (this.context.isInAParentText) { - RawImage = RCTVirtualImage; - if (!width || !height) { - console.warn('You must specify a width and height for the image %s', uri); - } - } - return ( -#import "RCTImageComponent.h" #import "RCTResizeMode.h" @class RCTBridge; @class RCTImageSource; -@interface RCTImageView : UIImageView +@interface RCTImageView : UIImageView - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; diff --git a/Libraries/Image/RCTShadowVirtualImage.h b/Libraries/Image/RCTShadowVirtualImage.h deleted file mode 100644 index b9623f736f239e..00000000000000 --- a/Libraries/Image/RCTShadowVirtualImage.h +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTShadowView.h" -#import "RCTImageComponent.h" -#import "RCTImageSource.h" -#import "RCTResizeMode.h" - -@class RCTBridge; - -/** - * Shadow image component, used for embedding images in non-view contexts such - * as text. This is NOT used for ordinary views. - */ -@interface RCTShadowVirtualImage : RCTShadowView - -- (instancetype)initWithBridge:(RCTBridge *)bridge; - -@property (nonatomic, strong) RCTImageSource *source; -@property (nonatomic, assign) RCTResizeMode resizeMode; - -@end diff --git a/Libraries/Image/RCTShadowVirtualImage.m b/Libraries/Image/RCTShadowVirtualImage.m deleted file mode 100644 index 757a48c24ea056..00000000000000 --- a/Libraries/Image/RCTShadowVirtualImage.m +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTShadowVirtualImage.h" -#import "RCTImageLoader.h" -#import "RCTImageUtils.h" -#import "RCTBridge.h" -#import "RCTConvert.h" -#import "RCTUIManager.h" -#import "RCTUtils.h" - -@implementation RCTShadowVirtualImage -{ - RCTBridge *_bridge; - RCTImageLoaderCancellationBlock _cancellationBlock; -} - -@synthesize image = _image; - -- (instancetype)initWithBridge:(RCTBridge *)bridge -{ - if ((self = [super init])) { - _bridge = bridge; - } - return self; -} - -RCT_NOT_IMPLEMENTED(-(instancetype)init) - -- (void)didSetProps:(NSArray *)changedProps -{ - [super didSetProps:changedProps]; - - if (changedProps.count == 0) { - // No need to reload image - return; - } - - // Cancel previous request - if (_cancellationBlock) { - _cancellationBlock(); - } - - CGSize imageSize = { - RCTZeroIfNaN(self.width), - RCTZeroIfNaN(self.height), - }; - - __weak RCTShadowVirtualImage *weakSelf = self; - _cancellationBlock = [_bridge.imageLoader loadImageWithTag:_source.imageURL.absoluteString - size:imageSize - scale:RCTScreenScale() - resizeMode:_resizeMode - progressBlock:nil - completionBlock:^(NSError *error, UIImage *image) { - - dispatch_async(_bridge.uiManager.methodQueue, ^{ - RCTShadowVirtualImage *strongSelf = weakSelf; - if (![_source isEqual:strongSelf.source]) { - // Bail out if source has changed since we started loading - return; - } - strongSelf->_image = image; - [strongSelf dirtyText]; - }); - }]; -} - -- (void)dealloc -{ - if (_cancellationBlock) { - _cancellationBlock(); - } -} - -@end diff --git a/Libraries/Image/RCTVirtualImageManager.h b/Libraries/Image/RCTVirtualImageManager.h deleted file mode 100644 index b92896235d7f79..00000000000000 --- a/Libraries/Image/RCTVirtualImageManager.h +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTViewManager.h" - -@interface RCTVirtualImageManager : RCTViewManager - -@end diff --git a/Libraries/Image/RCTVirtualImageManager.m b/Libraries/Image/RCTVirtualImageManager.m deleted file mode 100644 index 6311010f4ce263..00000000000000 --- a/Libraries/Image/RCTVirtualImageManager.m +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTVirtualImageManager.h" -#import "RCTShadowVirtualImage.h" - -@implementation RCTVirtualImageManager - -RCT_EXPORT_MODULE() - -- (RCTShadowView *)shadowView -{ - return [[RCTShadowVirtualImage alloc] initWithBridge:self.bridge]; -} - -RCT_EXPORT_SHADOW_PROPERTY(source, RCTImageSource) -RCT_EXPORT_SHADOW_PROPERTY(resizeMode, UIViewContentMode) - -@end diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index adf7be667d5520..3220cca7615391 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -13,12 +13,12 @@ #import "RCTUIManager.h" #import "RCTBridge.h" #import "RCTConvert.h" -#import "RCTImageComponent.h" #import "RCTLog.h" #import "RCTShadowRawText.h" #import "RCTText.h" #import "RCTUtils.h" +NSString *const RCTShadowViewAttributeName = @"RCTShadowViewAttributeName"; NSString *const RCTIsHighlightedAttributeName = @"IsHighlightedAttributeName"; NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName"; @@ -114,6 +114,45 @@ - (void)applyLayoutNode:(css_node_t *)node [self dirtyPropagation]; } +- (void)applyLayoutToChildren:(css_node_t *)node + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + absolutePosition:(CGPoint)absolutePosition +{ + // Run layout on subviews. + NSTextStorage *textStorage = [self buildTextStorageForWidth:self.frame.size.width widthMode:CSS_MEASURE_MODE_EXACTLY]; + NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; + NSTextContainer *textContainer = layoutManager.textContainers.firstObject; + NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; + NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL]; + [layoutManager.textStorage enumerateAttribute:RCTShadowViewAttributeName inRange:characterRange options:0 usingBlock:^(RCTShadowView *child, NSRange range, BOOL *_) { + if (child != nil) { + css_node_t *childNode = child.cssNode; + float width = childNode->style.dimensions[CSS_WIDTH]; + float height = childNode->style.dimensions[CSS_HEIGHT]; + if (isUndefined(width) || isUndefined(height)) { + RCTLogError(@"Views nested within a must have a width and height"); + } + UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:range.location effectiveRange:nil]; + CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range inTextContainer:textContainer]; + CGRect childFrame = {{ + RCTRoundPixelValue(glyphRect.origin.x), + RCTRoundPixelValue(glyphRect.origin.y + glyphRect.size.height - height + font.descender) + }, { + RCTRoundPixelValue(width), + RCTRoundPixelValue(height) + }}; + + NSRange truncatedGlyphRange = [layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:range.location]; + BOOL childIsTruncated = NSIntersectionRange(range, truncatedGlyphRange).length != 0; + + [child collectUpdatedFrames:viewsWithNewFrame + withFrame:childFrame + hidden:childIsTruncated + absolutePosition:absolutePosition]; + } + }]; +} + - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width widthMode:(css_measure_mode_t)widthMode { if (_cachedTextStorage && width == _cachedTextStorageWidth && widthMode == _cachedTextStorageWidthMode) { @@ -199,33 +238,48 @@ - (NSAttributedString *)_attributedStringWithFontFamily:(NSString *)fontFamily _effectiveLetterSpacing = letterSpacing.doubleValue; + UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily + size:fontSize weight:fontWeight style:fontStyle + scaleMultiplier:_allowFontScaling ? _fontSizeMultiplier : 1.0]; + + CGFloat heightOfTallestSubview = 0.0; NSMutableAttributedString *attributedString = [NSMutableAttributedString new]; for (RCTShadowView *child in [self reactSubviews]) { if ([child isKindOfClass:[RCTShadowText class]]) { RCTShadowText *shadowText = (RCTShadowText *)child; [attributedString appendAttributedString: - [shadowText _attributedStringWithFontFamily:fontFamily - fontSize:fontSize - fontWeight:fontWeight - fontStyle:fontStyle - letterSpacing:letterSpacing - useBackgroundColor:YES - foregroundColor:shadowText.color ?: foregroundColor - backgroundColor:shadowText.backgroundColor ?: backgroundColor - opacity:opacity * shadowText.opacity]]; + [shadowText _attributedStringWithFontFamily:fontFamily + fontSize:fontSize + fontWeight:fontWeight + fontStyle:fontStyle + letterSpacing:letterSpacing + useBackgroundColor:YES + foregroundColor:shadowText.color ?: foregroundColor + backgroundColor:shadowText.backgroundColor ?: backgroundColor + opacity:opacity * shadowText.opacity]]; + [child setTextComputed]; } else if ([child isKindOfClass:[RCTShadowRawText class]]) { RCTShadowRawText *shadowRawText = (RCTShadowRawText *)child; [attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:shadowRawText.text ?: @""]]; - } else if ([child conformsToProtocol:@protocol(RCTImageComponent)]) { - NSTextAttachment *imageAttachment = [NSTextAttachment new]; - imageAttachment.image = ((id)child).image; - imageAttachment.bounds = (CGRect){CGPointZero, {RCTZeroIfNaN(child.width), RCTZeroIfNaN(child.height)}}; - [attributedString appendAttributedString:[NSAttributedString attributedStringWithAttachment:imageAttachment]]; + [child setTextComputed]; } else { - RCTLogError(@" can't have any children except , or raw strings"); + float width = child.cssNode->style.dimensions[CSS_WIDTH]; + float height = child.cssNode->style.dimensions[CSS_HEIGHT]; + if (isUndefined(width) || isUndefined(height)) { + RCTLogError(@"Views nested within a must have a width and height"); + } + NSTextAttachment *attachment = [NSTextAttachment new]; + attachment.bounds = (CGRect){CGPointZero, {width, height}}; + NSMutableAttributedString *attachmentString = [NSMutableAttributedString new]; + [attachmentString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]]; + [attachmentString addAttribute:RCTShadowViewAttributeName value:child range:(NSRange){0, attachmentString.length}]; + [attributedString appendAttributedString:attachmentString]; + if (height > heightOfTallestSubview) { + heightOfTallestSubview = height; + } + // Don't call setTextComputed on this child. RCTTextManager takes care of + // processing inline UIViews. } - - [child setTextComputed]; } [self _addAttribute:NSForegroundColorAttributeName @@ -241,13 +295,10 @@ - (NSAttributedString *)_attributedStringWithFontFamily:(NSString *)fontFamily toAttributedString:attributedString]; } - UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily - size:fontSize weight:fontWeight style:fontStyle - scaleMultiplier:_allowFontScaling ? _fontSizeMultiplier : 1.0]; [self _addAttribute:NSFontAttributeName withValue:font toAttributedString:attributedString]; [self _addAttribute:NSKernAttributeName withValue:letterSpacing toAttributedString:attributedString]; [self _addAttribute:RCTReactTagAttributeName withValue:self.reactTag toAttributedString:attributedString]; - [self _setParagraphStyleOnAttributedString:attributedString]; + [self _setParagraphStyleOnAttributedString:attributedString heightOfTallestSubview:heightOfTallestSubview]; // create a non-mutable attributedString for use by the Text system which avoids copies down the line _cachedAttributedString = [[NSAttributedString alloc] initWithAttributedString:attributedString]; @@ -270,6 +321,7 @@ - (void)_addAttribute:(NSString *)attribute withValue:(id)attributeValue toAttri * varying lineHeights, we simply take the max. */ - (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attributedString + heightOfTallestSubview:(CGFloat)heightOfTallestSubview { // check if we have lineHeight set on self __block BOOL hasParagraphStyle = NO; @@ -277,9 +329,7 @@ - (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attrib hasParagraphStyle = YES; } - if (!_lineHeight) { - self.lineHeight = 0.0; - } + __block float newLineHeight = _lineHeight ?: 0.0; CGFloat fontSizeMultiplier = _allowFontScaling ? _fontSizeMultiplier : 1.0; @@ -288,15 +338,25 @@ - (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attrib if (value) { NSParagraphStyle *paragraphStyle = (NSParagraphStyle *)value; CGFloat maximumLineHeight = round(paragraphStyle.maximumLineHeight / fontSizeMultiplier); - if (maximumLineHeight > self.lineHeight) { - self.lineHeight = maximumLineHeight; + if (maximumLineHeight > newLineHeight) { + newLineHeight = maximumLineHeight; } hasParagraphStyle = YES; } }]; - self.textAlign = _textAlign ?: NSTextAlignmentNatural; - self.writingDirection = _writingDirection ?: NSWritingDirectionNatural; + if (self.lineHeight != newLineHeight) { + self.lineHeight = newLineHeight; + } + + NSTextAlignment newTextAlign = _textAlign ?: NSTextAlignmentNatural; + if (self.textAlign != newTextAlign) { + self.textAlign = newTextAlign; + } + NSWritingDirection newWritingDirection = _writingDirection ?: NSWritingDirectionNatural; + if (self.writingDirection != newWritingDirection) { + self.writingDirection = newWritingDirection; + } // if we found anything, set it :D if (hasParagraphStyle) { @@ -304,6 +364,9 @@ - (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attrib paragraphStyle.alignment = _textAlign; paragraphStyle.baseWritingDirection = _writingDirection; CGFloat lineHeight = round(_lineHeight * fontSizeMultiplier); + if (heightOfTallestSubview > lineHeight) { + lineHeight = ceilf(heightOfTallestSubview); + } paragraphStyle.minimumLineHeight = lineHeight; paragraphStyle.maximumLineHeight = lineHeight; [attributedString addAttribute:NSParagraphStyleAttributeName diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index e57b93aa191f7a..fd37c4b75ff265 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -13,6 +13,17 @@ #import "RCTUtils.h" #import "UIView+React.h" +static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDescendants) +{ + for (UIView *child in view.reactSubviews) { + if ([child isKindOfClass:[RCTText class]]) { + collectNonTextDescendants((RCTText *)child, nonTextDescendants); + } else { + [nonTextDescendants addObject:child]; + } + } +} + @implementation RCTText { NSTextStorage *_textStorage; @@ -119,6 +130,15 @@ - (void)drawRect:(CGRect)rect [_highlightLayer removeFromSuperlayer]; _highlightLayer = nil; } + + for (UIView *child in [self subviews]) { + [child removeFromSuperview]; + } + NSMutableArray *nonTextDescendants = [NSMutableArray new]; + collectNonTextDescendants(self, nonTextDescendants); + for (UIView *child in nonTextDescendants) { + [self addSubview:child]; + } } - (NSNumber *)reactTagAtPoint:(CGPoint)point diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index 92fa589496d2a0..9104ea72c073f6 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -20,6 +20,18 @@ #import "RCTTextView.h" #import "UIView+React.h" +static void collectDirtyNonTextDescendants(RCTShadowText *shadowView, NSMutableArray *nonTextDescendants) { + for (RCTShadowView *child in shadowView.reactSubviews) { + if ([child isKindOfClass:[RCTShadowText class]]) { + collectDirtyNonTextDescendants((RCTShadowText *)child, nonTextDescendants); + } else if ([child isKindOfClass:[RCTShadowRawText class]]) { + // no-op + } else if ([child isTextDirty]) { + [nonTextDescendants addObject:child]; + } + } +} + @interface RCTShadowText (Private) - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width widthMode:(css_measure_mode_t)widthMode; @@ -85,6 +97,7 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(NSDictionary tag. Not rendering string: '%@'", [(RCTShadowRawText *)shadowView text]); diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index e9e47043002254..a819847d0a70d2 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -560,12 +560,15 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableDictionary *updateBlocks = [NSMutableDictionary new]; + NSMutableArray *areHidden = + [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; for (RCTShadowView *shadowView in viewsWithNewFrames) { [frameReactTags addObject:shadowView.reactTag]; [frames addObject:[NSValue valueWithCGRect:shadowView.frame]]; [areNew addObject:@(shadowView.isNewView)]; [parentsAreNew addObject:@(shadowView.superview.isNewView)]; + [areHidden addObject:@(shadowView.isHidden)]; } for (RCTShadowView *shadowView in viewsWithNewFrames) { @@ -620,6 +623,7 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * UIView *view = viewRegistry[reactTag]; CGRect frame = [frames[ii] CGRectValue]; + BOOL isHidden = [areHidden[ii] boolValue]; BOOL isNew = [areNew[ii] boolValue]; RCTAnimation *updateAnimation = isNew ? nil : layoutAnimation.updateAnimation; BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue]; @@ -636,6 +640,10 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * } }; + if (view.isHidden != isHidden) { + view.hidden = isHidden; + } + // Animate view creation if (createAnimation) { [view reactSetFrame:frame]; diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 53c851f4216f6c..9173e9bbb45d08 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -216,7 +216,6 @@ 13E067501A70F44B002CDEE1 /* RCTView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTView.m; sourceTree = ""; }; 13E067531A70F44B002CDEE1 /* UIView+React.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+React.h"; sourceTree = ""; }; 13E067541A70F44B002CDEE1 /* UIView+React.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+React.m"; sourceTree = ""; }; - 13EF7F441BC69646003F47DD /* RCTImageComponent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTImageComponent.h; sourceTree = ""; }; 13F17A831B8493E5007D4C75 /* RCTRedBox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRedBox.h; sourceTree = ""; }; 13F17A841B8493E5007D4C75 /* RCTRedBox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRedBox.m; sourceTree = ""; }; 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptLoader.h; sourceTree = ""; }; @@ -385,7 +384,6 @@ 13AB90BF1B6FA36700713B4F /* RCTComponentData.h */, 13456E941ADAD482009F94A7 /* RCTConvert+MapKit.h */, 13AB90C01B6FA36700713B4F /* RCTComponentData.m */, - 13EF7F441BC69646003F47DD /* RCTImageComponent.h */, 13456E911ADAD2DE009F94A7 /* RCTConvert+CoreLocation.h */, 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */, 13456E951ADAD482009F94A7 /* RCTConvert+MapKit.m */, diff --git a/React/Views/RCTImageComponent.h b/React/Views/RCTImageComponent.h deleted file mode 100644 index a6916c9aa63be0..00000000000000 --- a/React/Views/RCTImageComponent.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import - -/** - * Generic interface for components that contain an image. - */ -@protocol RCTImageComponent - -@property (nonatomic, strong, readonly) UIImage *image; - -@end diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index 77875ca60261b2..95bae0e21b30dc 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -52,6 +52,12 @@ typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry */ @property (nonatomic, assign, getter=isNewView) BOOL newView; +/** + * isHidden - RCTUIManager uses this to determine whether or not the UIView should be hidden. Useful if the + * ShadowView determines that its UIView will be clipped and wants to hide it. + */ +@property (nonatomic, assign, getter=isHidden) BOOL hidden; + /** * Position and dimensions. * Defaults to { 0, 0, NAN, NAN }. @@ -133,12 +139,32 @@ typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry - (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties NS_REQUIRES_SUPER; +/** + * Can be called by a parent on a child in order to calculate all views whose frame needs + * updating in that branch. Adds these frames to `viewsWithNewFrame`. Useful if layout + * enters a view where flex doesn't apply (e.g. Text) and then you want to resume flex + * layout on a subview. + */ +- (void)collectUpdatedFrames:(NSMutableSet *)viewsWithNewFrame + withFrame:(CGRect)frame + hidden:(BOOL)hidden + absolutePosition:(CGPoint)absolutePosition; + /** * Recursively apply layout to children. + * The job of applyLayoutNode:viewsWithNewFrame:absolutePosition is to apply the layout to + * this node. + * The job of applyLayoutToChildren:viewsWithNewFrame:absolutePosition is to enumerate the + * children and tell them to apply layout. + * The functionality is split into two methods so subclasses can override applyLayoutToChildren + * while using the default implementation of applyLayoutNode. */ - (void)applyLayoutNode:(css_node_t *)node viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame absolutePosition:(CGPoint)absolutePosition NS_REQUIRES_SUPER; +- (void)applyLayoutToChildren:(css_node_t *)node + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + absolutePosition:(CGPoint)absolutePosition; /** * The following are implementation details exposed to subclasses. Do not call them directly diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index d55e80c624c96e..0472b272c7dd72 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -157,6 +157,13 @@ - (void)applyLayoutNode:(css_node_t *)node absolutePosition.x += node->layout.position[CSS_LEFT]; absolutePosition.y += node->layout.position[CSS_TOP]; + [self applyLayoutToChildren:node viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition]; +} + +- (void)applyLayoutToChildren:(css_node_t *)node + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + absolutePosition:(CGPoint)absolutePosition +{ for (int i = 0; i < node->children_count; ++i) { RCTShadowView *child = (RCTShadowView *)_reactSubviews[i]; [child applyLayoutNode:node->get_child(node->context, i) @@ -209,6 +216,37 @@ - (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks } } +- (void)collectUpdatedFrames:(NSMutableSet *)viewsWithNewFrame + withFrame:(CGRect)frame + hidden:(BOOL)hidden + absolutePosition:(CGPoint)absolutePosition +{ + if (_hidden != hidden) { + // The hidden state has changed. Even if the frame hasn't changed, add + // this ShadowView to viewsWithNewFrame so the UIManager will process + // this ShadowView's UIView and update its hidden state. + _hidden = hidden; + [viewsWithNewFrame addObject:self]; + } + + if (!CGRectEqualToRect(frame, _frame)) { + _cssNode->style.position_type = CSS_POSITION_ABSOLUTE; + _cssNode->style.dimensions[CSS_WIDTH] = frame.size.width; + _cssNode->style.dimensions[CSS_HEIGHT] = frame.size.height; + _cssNode->style.position[CSS_LEFT] = frame.origin.x; + _cssNode->style.position[CSS_TOP] = frame.origin.y; + // Our parent has asked us to change our cssNode->styles. Dirty the layout + // so that we can rerun layout on this node. The request came from our parent + // so there's no need to dirty our ancestors by calling dirtyLayout. + _layoutLifecycle = RCTUpdateLifecycleDirtied; + } + + [self fillCSSNode:_cssNode]; + resetNodeLayout(self.cssNode); + layoutNode(_cssNode, frame.size.width, frame.size.height, CSS_DIRECTION_INHERIT); + [self applyLayoutNode:_cssNode viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition]; +} + - (CGRect)measureLayoutRelativeToAncestor:(RCTShadowView *)ancestor { CGPoint offset = CGPointZero; @@ -459,6 +497,7 @@ - (void)set##setProp:(CGFloat)value \ { \ _cssNode->style.dimensions[CSS_##cssProp] = value; \ [self dirtyLayout]; \ + [self dirtyText]; \ } \ - (CGFloat)getProp \ { \ diff --git a/docs/Text.md b/docs/Text.md index 800137b03b0b71..48853606e432fe 100644 --- a/docs/Text.md +++ b/docs/Text.md @@ -21,6 +21,20 @@ Behind the scenes, React Native converts this to a flat `NSAttributedString` or 9-17: bold, red ``` +## Nested Views (iOS Only) + +On iOS, you can nest views within your Text component. Here's an example: + +```javascript + + There is a blue square + + in between my text. + +``` + +In order to use this feature, you must give the view a `width` and a `height`. + ## Containers The `` element is special relative to layout: everything inside is no longer using the flexbox layout but using text layout. This means that elements inside of a `` are no longer rectangles, but wrap when they see the end of the line. From 5bb5ea71355c1e39e4966471b550e9b2d6185d27 Mon Sep 17 00:00:00 2001 From: Ben Nham Date: Tue, 17 May 2016 10:41:45 -0700 Subject: [PATCH 004/843] Reload image without dispatch_async if completion handler is already on main thread Summary: This works with D3305161 to minimize image flashing. After D3305161, the completion handler passed to `-[RCTImageLoader loadImageWithoutClipping:size:scale:resizeMode:progressBlock:completionBlock:]` may be called back on the main queue in the case of a cached image. In this case, we want to set the image view's image property synchronously rather than on the next runloop iteration via dispatch_async. This minimizes the amount of image flashing the user sees when displaying a cached image. The exception to this case is for blurred images. A blur can be an expensive (taking multiple ms on the CPU), so we always make sure to perform the blur off the main queue even if the image is cached and the callback came back on the main queue. Reviewed By: nicklockwood Differential Revision: D3310176 fbshipit-source-id: 6820782527b65e4956879cf06e8ed2c09c622a58 --- Libraries/Image/RCTImageView.m | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index b78ce896ce291a..b805cd9b6935d1 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -207,13 +207,9 @@ - (void)reloadImage scale:imageScale resizeMode:(RCTResizeMode)self.contentMode progressBlock:progressHandler - completionBlock:^(NSError *error, UIImage *image) { + completionBlock:^(NSError *error, UIImage *loadedImage) { RCTImageView *strongSelf = weakSelf; - if (blurRadius > __FLT_EPSILON__) { - // Do this on the background thread to avoid blocking interaction - image = RCTBlurredImageWithRadius(image, blurRadius); - } - dispatch_async(dispatch_get_main_queue(), ^{ + void (^setImageBlock)(UIImage *) = ^(UIImage *image) { if (![source isEqual:strongSelf.source]) { // Bail out if source has changed since we started loading return; @@ -234,9 +230,31 @@ - (void)reloadImage } } if (strongSelf->_onLoadEnd) { - strongSelf->_onLoadEnd(nil); + strongSelf->_onLoadEnd(nil); } - }); + }; + + if (blurRadius > __FLT_EPSILON__) { + // Blur on a background thread to avoid blocking interaction + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + UIImage *image = RCTBlurredImageWithRadius(loadedImage, blurRadius); + RCTExecuteOnMainThread(^{ + setImageBlock(image); + }, NO); + }); + } else { + // No blur, so try to set the image on the main thread synchronously to minimize image + // flashing. (For instance, if this view gets attached to a window, then -didMoveToWindow + // calls -reloadImage, and we want to set the image synchronously if possible so that the + // image property is set in the same CATransaction that attaches this view to the window.) + if ([NSThread isMainThread]) { + setImageBlock(loadedImage); + } else { + RCTExecuteOnMainThread(^{ + setImageBlock(loadedImage); + }, NO); + } + } }]; } else { [self clearImage]; From 7446d42d334ea6983dc221b2033981c9b9bff1dd Mon Sep 17 00:00:00 2001 From: Alexander Blom Date: Tue, 17 May 2016 11:15:10 -0700 Subject: [PATCH 005/843] Delete jsc-internal Reviewed By: dcaspi Differential Revision: D3304892 fbshipit-source-id: fd7ac66c81c93785c3c7887a107c8f1df222aefa --- ReactAndroid/DEFS | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ReactAndroid/DEFS b/ReactAndroid/DEFS index 104c161896f566..a11e2436fa00f6 100644 --- a/ReactAndroid/DEFS +++ b/ReactAndroid/DEFS @@ -28,15 +28,8 @@ JSC_DEPS = [ '//native/third-party/jsc:jsc_legacy_profiler', ] -JSC_INTERNAL_DEPS = [ - '//native/third-party/jsc-internal:jsc', - '//native/third-party/jsc-internal:jsc_legacy_profiler', -] - FBGLOGINIT_TARGET = '//ReactAndroid/src/main/jni/first-party/fbgloginit:fbgloginit' -INTERNAL_APP = 'PUBLIC' - # React property preprocessor original_android_library=android_library def android_library( From fdc62400185beeb5c35d9066fe561771096e3d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Tue, 17 May 2016 11:45:49 -0700 Subject: [PATCH 006/843] Reset hasError flag after HMR request Summary: This got broken recently. As a result of this, when a module throws while being required, either by regular load or hot reload, the module object is marked as `hasError`. Form that point all subsequent HMR updates will fail because it will think the module failed while executing the factory but the failure could have been on an old run of an old factory. The fix is very simple: just reset `hasError` to false when accepting a module. Closes https://github.com/facebook/react-native/pull/7567 Differential Revision: D3310685 fbshipit-source-id: 2f0b48ab7432b7c221d0c88a019a28969a8862b2 --- packager/react-packager/src/Resolver/polyfills/require.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packager/react-packager/src/Resolver/polyfills/require.js b/packager/react-packager/src/Resolver/polyfills/require.js index 219a4bad653276..094bdeff304ca7 100644 --- a/packager/react-packager/src/Resolver/polyfills/require.js +++ b/packager/react-packager/src/Resolver/polyfills/require.js @@ -205,6 +205,7 @@ if (__DEV__) { if (factory) { mod.factory = factory; } + mod.hasError = false; mod.isInitialized = false; require(id); From d62f9612f211f0472e7b594ba273560200a3efb0 Mon Sep 17 00:00:00 2001 From: Victor Calvello Date: Tue, 17 May 2016 11:58:36 -0700 Subject: [PATCH 007/843] Add 'Vorterix' app to Showcase Summary: Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: (You can skip this if you're fixing a typo or adding an app to the Showcase.) Explain the **motivation** for making this change. What existing problem does the pull request solve? Prefer **small pull requests**. These are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise please split it. **Test plan (required)** Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. Make sure tests pass on both Travis and Circle CI. **Code formatting** Look around. Match the style of the rest of the codebase. See also the simple [style guide](https://github.com/facebook/react-native/blob/master/CONTRIBUTING.md#style-guide). For more info, see the ["Pull Requests" section of our "Contributing" guidelines](https://github.com/facebook/react-native/blob/mas Closes https://github.com/facebook/react-native/pull/7590 Differential Revision: D3310675 fbshipit-source-id: 8dc4d50eed9237988b409c336695568832c0fe34 --- website/src/react-native/showcase.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/website/src/react-native/showcase.js b/website/src/react-native/showcase.js index fad5ec178552fa..30be4b22bda723 100644 --- a/website/src/react-native/showcase.js +++ b/website/src/react-native/showcase.js @@ -871,6 +871,13 @@ var apps = [ link: 'https://itunes.apple.com/es/app/veggies-in-season/id1088215278?mt=8', author: 'Victor Delgado', }, + { + name: 'Vorterix', + icon: 'http://a2.mzstatic.com/us/r30/Purple49/v4/3a/f0/b4/3af0b475-f0e8-e81d-eb38-1ec1dfa9b2f4/icon175x175.jpeg', + linkAppStore: 'https://itunes.apple.com/ar/app/vorterix/id577990518?mt=8', + linkPlayStore: 'https://play.google.com/store/apps/details?id=com.vorterix', + author: 'Dift & underscope.io', + }, { name: 'WEARVR', icon: 'http://a2.mzstatic.com/eu/r30/Purple69/v4/4f/5a/28/4f5a2876-9530-ef83-e399-c5ef5b2dab80/icon175x175.png', From 5047f6f54c5db262509c87cef35c507f424361eb Mon Sep 17 00:00:00 2001 From: Chris Evans Date: Tue, 17 May 2016 12:27:10 -0700 Subject: [PATCH 008/843] Add Copy Stack button to Red Box (iOS) Summary: Added a button to the iOS Red Box to enable copying of the error and stack trace to the clipboard to make it easier to paste the error and stack into bug reports or other text windows. Clicking the Copy button on the bottom of the RedBox screen or pressing Cmd-Option-C keyboard shortcut in the simulator will copy the full stack trace as text to the iOS pasteboard and when in the simulator you can then hit Command-C to get the text to the Mac's pasteboard for pasting in your favorite reporting/tracking app. No tests for this change - I don't see any tests to extend for the RCTRedBoxWindow class. No impact on APIs or core React Native functionality. I validated it by running test React Native apps with red box errors and both hitting the copy button or using the command key shortcut in the emulator to get the text of the stack to the iOS pasteboard. ![redboxcopybutton](https://cloud.githubusercontent.com/assets/7173455/15258992/7b1e849c-1903-11e6-8813-6e853db5db54.png) Closes https://github.com/facebook/react-native/pull/7557 Differential Revision: D3311743 Pulled By: javache fbshipit-source-id: 76d1ac8ab93f40696c6a2369dae2245194db095a --- React/Modules/RCTRedBox.m | 59 ++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/React/Modules/RCTRedBox.m b/React/Modules/RCTRedBox.m index ddd47b18781a94..f732459e3a3686 100644 --- a/React/Modules/RCTRedBox.m +++ b/React/Modules/RCTRedBox.m @@ -81,11 +81,22 @@ - (instancetype)initWithFrame:(CGRect)frame [reloadButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted]; [reloadButton addTarget:self action:@selector(reload) forControlEvents:UIControlEventTouchUpInside]; - CGFloat buttonWidth = self.bounds.size.width / 2; + UIButton *copyButton = [UIButton buttonWithType:UIButtonTypeCustom]; + copyButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin; + copyButton.accessibilityIdentifier = @"redbox-copy"; + copyButton.titleLabel.font = [UIFont systemFontOfSize:14]; + [copyButton setTitle:@"Copy (\u2325\u2318C)" forState:UIControlStateNormal]; + [copyButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateNormal]; + [copyButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted]; + [copyButton addTarget:self action:@selector(copyStack) forControlEvents:UIControlEventTouchUpInside]; + + CGFloat buttonWidth = self.bounds.size.width / 3; dismissButton.frame = CGRectMake(0, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight); reloadButton.frame = CGRectMake(buttonWidth, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight); + copyButton.frame = CGRectMake(buttonWidth * 2, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight); [rootView addSubview:dismissButton]; [rootView addSubview:reloadButton]; + [rootView addSubview:copyButton]; } return self; } @@ -133,6 +144,33 @@ - (void)reload [_actionDelegate reloadFromRedBoxWindow:self]; } +- (void)copyStack +{ + NSMutableString *fullStackTrace; + + if (_lastErrorMessage != nil) { + fullStackTrace = [_lastErrorMessage mutableCopy]; + [fullStackTrace appendString:@"\n\n"]; + } + else { + fullStackTrace = [NSMutableString string]; + } + + for (NSDictionary *stackFrame in _lastStackTrace) { + [fullStackTrace appendString:[NSString stringWithFormat:@"%@\n", stackFrame[@"methodName"]]]; + if (stackFrame[@"file"]) { + NSString *lineInfo = [NSString stringWithFormat:@" %@ @ %zd:%zd\n", + [stackFrame[@"file"] lastPathComponent], + [stackFrame[@"lineNumber"] integerValue], + [stackFrame[@"column"] integerValue]]; + [fullStackTrace appendString:lineInfo]; + } + } + + UIPasteboard *pb = [UIPasteboard generalPasteboard]; + [pb setString:fullStackTrace]; +} + #pragma mark - TableView - (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView @@ -240,14 +278,21 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath // Dismiss red box [UIKeyCommand keyCommandWithInput:UIKeyInputEscape - modifierFlags:0 - action:@selector(dismiss)], + modifierFlags:0 + action:@selector(dismiss)], // Reload [UIKeyCommand keyCommandWithInput:@"r" - modifierFlags:UIKeyModifierCommand - action:@selector(reload)] - ]; + modifierFlags:UIKeyModifierCommand + action:@selector(reload)], + + // Copy = Cmd-Option C since Cmd-C in the simulator copies the pasteboard from + // the simulator to the desktop pasteboard. + [UIKeyCommand keyCommandWithInput:@"c" + modifierFlags:UIKeyModifierCommand | UIKeyModifierAlternate + action:@selector(copyStack)] + + ]; } - (BOOL)canBecomeFirstResponder @@ -321,7 +366,7 @@ - (void)invalidate [self dismiss]; } -- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(NSDictionary *)stackFrame; +- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(NSDictionary *)stackFrame { if (![_bridge.bundleURL.scheme hasPrefix:@"http"]) { RCTLogWarn(@"Cannot open stack frame in editor because you're not connected to the packager."); From 6bbaff2944dafd6fa7e5b77ef46dece0ec2c9983 Mon Sep 17 00:00:00 2001 From: Andrew Jack Date: Tue, 17 May 2016 12:37:50 -0700 Subject: [PATCH 009/843] Upgrade to OkHttp3 Summary: Update to [OkHttp](https://github.com/square/okhttp) to [OkHttp3](https://publicobject.com/2015/12/12/com-squareup-okhttp3/) We must also update: - Fresco to 0.10.0 - okio to 1.8.0 **Motivation** Reasons for upgrading: * Issue #4021 * "We discovered that RN Android sometimes fails to connect to the latest stable version of NGINX when HTTP/2 is enabled. We aren't seeing errors with other HTTP clients so we think it's specific to RN and OkHttp. Square has fixed several HTTP/2 bugs over the past eight months." - ide * OkHttp3 will be maintained & improved, but OkHttp2 will only receive [security fixes](https://publicobject.com/2016/02/11/okhttp-certificate-pinning-vulnerability/) * Cleaner APIs - "Get and Set prefixes are avoided" * Deprecated/Removed - HttpURLConnection & Apache HTTP * React Native apps are currently being forced to bundle two versions of OkHttp (v2 & v3), if another library uses v3 * Improved WebSocket performance - [CHANGELOG.md](https://github.com/square/okhttp/blob/master Closes https://github.com/facebook/react-native/pull/6113 Reviewed By: andreicoman11, lexs Differential Revision: D3292375 Pulled By: bestander fbshipit-source-id: 7c7043eaa2ea63f95854108b401c4066098d67f7 --- ReactAndroid/build.gradle | 11 +-- .../main/java/com/facebook/react/bridge/BUCK | 6 +- .../bridge/JSDebuggerWebSocketClient.java | 39 +++++---- .../react/bridge/webworkers/WebWorkers.java | 6 +- .../main/java/com/facebook/react/common/BUCK | 1 + .../react/common/network/OkHttpCallUtil.java | 35 ++++++++ .../java/com/facebook/react/devsupport/BUCK | 13 ++- .../react/devsupport/DevServerHelper.java | 55 ++++++------- .../react/devsupport/RedBoxDialog.java | 8 +- .../com/facebook/react/modules/fresco/BUCK | 16 ++-- .../react/modules/fresco/FrescoModule.java | 8 +- .../com/facebook/react/modules/network/BUCK | 9 ++- .../modules/network/CookieJarContainer.java | 11 +++ .../network/NetworkInterceptorCreator.java | 2 +- .../modules/network/NetworkingModule.java | 80 +++++++++---------- .../modules/network/OkHttpClientProvider.java | 25 +++--- .../network/ReactCookieJarContainer.java | 44 ++++++++++ .../modules/network/RequestBodyUtil.java | 6 +- .../com/facebook/react/modules/websocket/BUCK | 12 +-- .../modules/websocket/WebSocketModule.java | 48 +++++------ .../libraries/fresco/fresco-react-native/BUCK | 38 ++++++--- .../src/main/third-party/java/okhttp/BUCK | 32 +++++--- .../src/main/third-party/java/okio/BUCK | 4 +- .../src/test/java/com/facebook/react/BUCK | 20 +++-- .../test/java/com/facebook/react/modules/BUCK | 19 +++-- .../modules/network/NetworkingModuleTest.java | 48 +++++------ .../java/com/facebook/react/uimanager/BUCK | 21 +++-- .../test/java/com/facebook/react/views/BUCK | 28 +++---- .../templates/src/app/proguard-rules.pro | 6 +- 29 files changed, 378 insertions(+), 273 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/network/CookieJarContainer.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/network/ReactCookieJarContainer.java diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle index abd0fec39039bc..5f5ba755ce07f5 100644 --- a/ReactAndroid/build.gradle +++ b/ReactAndroid/build.gradle @@ -259,13 +259,14 @@ dependencies { compile fileTree(dir: 'src/main/third-party/java/infer-annotations/', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.0.1' compile 'com.android.support:recyclerview-v7:23.0.1' - compile 'com.facebook.fresco:fresco:0.8.1' - compile 'com.facebook.fresco:imagepipeline-okhttp:0.8.1' + compile 'com.facebook.fresco:fresco:0.10.0' + compile 'com.facebook.fresco:imagepipeline-okhttp3:0.10.0' compile 'com.fasterxml.jackson.core:jackson-core:2.2.3' compile 'com.google.code.findbugs:jsr305:3.0.0' - compile 'com.squareup.okhttp:okhttp:2.5.0' - compile 'com.squareup.okhttp:okhttp-ws:2.5.0' - compile 'com.squareup.okio:okio:1.6.0' + compile 'com.squareup.okhttp3:okhttp:3.2.0' + compile 'com.squareup.okhttp3:okhttp-urlconnection:3.2.0' + compile 'com.squareup.okhttp3:okhttp-ws:3.2.0' + compile 'com.squareup.okio:okio:1.8.0' compile 'org.webkit:android-jsc:r174650' testCompile "junit:junit:${JUNIT_VERSION}" diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/BUCK b/ReactAndroid/src/main/java/com/facebook/react/bridge/BUCK index 43aaf9c5d40f3e..6164c32a8bc8a0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/BUCK @@ -21,16 +21,16 @@ android_library( react_native_dep('java/com/facebook/proguard/annotations:annotations'), ], deps = [ - react_native_target('java/com/facebook/react/common:common'), react_native_dep('java/com/facebook/systrace:systrace'), react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'), react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'), react_native_dep('third-party/java/infer-annotations:infer-annotations'), react_native_dep('third-party/java/jackson:core'), react_native_dep('third-party/java/jsr-305:jsr-305'), + react_native_dep('third-party/java/okhttp:okhttp3'), + react_native_dep('third-party/java/okhttp:okhttp3-ws'), react_native_dep('third-party/java/okio:okio'), - react_native_dep('third-party/java/okhttp:okhttp'), - react_native_dep('third-party/java/okhttp:okhttp-ws'), + react_native_target('java/com/facebook/react/common:common'), ], visibility = [ 'PUBLIC', diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSDebuggerWebSocketClient.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSDebuggerWebSocketClient.java index 849a0ea79c9c53..9370f46b4a3c7e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSDebuggerWebSocketClient.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSDebuggerWebSocketClient.java @@ -25,14 +25,15 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.Response; -import com.squareup.okhttp.ws.WebSocket; -import com.squareup.okhttp.ws.WebSocketCall; -import com.squareup.okhttp.ws.WebSocketListener; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okhttp3.ws.WebSocket; +import okhttp3.ws.WebSocketCall; +import okhttp3.ws.WebSocketListener; import okio.Buffer; -import okio.BufferedSource; /** * A wrapper around WebSocketClient that recognizes RN debugging message format. @@ -59,11 +60,11 @@ public void connect(String url, JSDebuggerCallback callback) { throw new IllegalStateException("JSDebuggerWebSocketClient is already initialized."); } mConnectCallback = callback; - mHttpClient = new OkHttpClient(); - mHttpClient.setConnectTimeout(10, TimeUnit.SECONDS); - mHttpClient.setWriteTimeout(10, TimeUnit.SECONDS); - // Disable timeouts for read - mHttpClient.setReadTimeout(0, TimeUnit.MINUTES); + mHttpClient = new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read + .build(); Request request = new Request.Builder().url(url).build(); WebSocketCall call = WebSocketCall.create(mHttpClient, request); @@ -162,10 +163,8 @@ private void sendMessage(int requestID, String message) { new IllegalStateException("WebSocket connection no longer valid")); return; } - Buffer messageBuffer = new Buffer(); - messageBuffer.writeUtf8(message); try { - mWebSocket.sendMessage(WebSocket.PayloadType.TEXT, messageBuffer); + mWebSocket.sendMessage(RequestBody.create(WebSocket.TEXT, message)); } catch (IOException e) { triggerRequestFailure(requestID, e); } @@ -188,17 +187,17 @@ private void triggerRequestSuccess(int requestID, @Nullable String response) { } @Override - public void onMessage(BufferedSource payload, WebSocket.PayloadType type) throws IOException { - if (type != WebSocket.PayloadType.TEXT) { - FLog.w(TAG, "Websocket received unexpected message with payload of type " + type); + public void onMessage(ResponseBody response) throws IOException { + if (response.contentType() != WebSocket.TEXT) { + FLog.w(TAG, "Websocket received unexpected message with payload of type " + response.contentType()); return; } String message = null; try { - message = payload.readUtf8(); + message = response.source().readUtf8(); } finally { - payload.close(); + response.close(); } Integer replyID = null; diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/webworkers/WebWorkers.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/webworkers/WebWorkers.java index c2b897b249f836..e0e3a20ee3f688 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/webworkers/WebWorkers.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/webworkers/WebWorkers.java @@ -18,9 +18,9 @@ import com.facebook.react.bridge.queue.ProxyQueueThreadExceptionHandler; import com.facebook.react.common.build.ReactBuildConfig; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.Response; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; import okio.Okio; import okio.Sink; diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/BUCK b/ReactAndroid/src/main/java/com/facebook/react/common/BUCK index e98c528e86bce3..289daab3d45baa 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/common/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/common/BUCK @@ -7,6 +7,7 @@ android_library( ':build_config', react_native_dep('third-party/java/infer-annotations:infer-annotations'), react_native_dep('third-party/java/jsr-305:jsr-305'), + react_native_dep('third-party/java/okhttp:okhttp3'), ], exported_deps = [ react_native_dep('java/com/facebook/proguard/annotations:annotations'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java b/ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java new file mode 100644 index 00000000000000..4da3a5e9b80349 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + *

+ * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.common.network; + +import okhttp3.Call; +import okhttp3.OkHttpClient; + +/** + * Helper class that provides the necessary methods for canceling queued and running OkHttp calls + */ +public class OkHttpCallUtil { + + private OkHttpCallUtil() { + } + + public static void cancelTag(OkHttpClient client, Object tag) { + for (Call call : client.dispatcher().queuedCalls()) { + if (tag.equals(call.request().tag())) { + call.cancel(); + } + } + for (Call call : client.dispatcher().runningCalls()) { + if (tag.equals(call.request().tag())) { + call.cancel(); + } + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/BUCK b/ReactAndroid/src/main/java/com/facebook/react/devsupport/BUCK index 8bd5582484fbb8..0757fe3791619a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/BUCK @@ -5,17 +5,16 @@ android_library( manifest = 'AndroidManifest.xml', srcs = glob(['**/*.java']), deps = [ - react_native_target('res:devsupport'), - react_native_target('java/com/facebook/react/bridge:bridge'), - react_native_target('java/com/facebook/react/common:common'), - react_native_target('java/com/facebook/react/modules/debug:debug'), - react_native_target('java/com/facebook/react/modules/systeminfo:systeminfo'), react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'), react_native_dep('third-party/java/infer-annotations:infer-annotations'), react_native_dep('third-party/java/jsr-305:jsr-305'), - react_native_dep('third-party/java/okhttp:okhttp'), + react_native_dep('third-party/java/okhttp:okhttp3'), react_native_dep('third-party/java/okio:okio'), - + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/modules/debug:debug'), + react_native_target('java/com/facebook/react/modules/systeminfo:systeminfo'), + react_native_target('res:devsupport'), ], visibility = [ 'PUBLIC', diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java index ffd53e5bde2a88..95d347852eef02 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java @@ -17,14 +17,8 @@ import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.network.OkHttpCallUtil; import com.facebook.react.modules.systeminfo.AndroidInfoHelpers; -import com.squareup.okhttp.Call; -import com.squareup.okhttp.Callback; -import com.squareup.okhttp.ConnectionPool; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.Response; -import com.squareup.okhttp.ResponseBody; import java.io.File; import java.io.IOException; @@ -33,6 +27,13 @@ import javax.annotation.Nullable; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; import okio.Okio; import okio.Sink; @@ -91,12 +92,12 @@ public interface PackagerStatusCallback { public DevServerHelper(DevInternalSettings settings) { mSettings = settings; - mClient = new OkHttpClient(); - mClient.setConnectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS); + mClient = new OkHttpClient.Builder() + .connectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .readTimeout(0, TimeUnit.MILLISECONDS) + .writeTimeout(0, TimeUnit.MILLISECONDS) + .build(); - // No read or write timeouts by default - mClient.setReadTimeout(0, TimeUnit.MILLISECONDS); - mClient.setWriteTimeout(0, TimeUnit.MILLISECONDS); mRestartOnChangePollingHandler = new Handler(); } @@ -176,7 +177,7 @@ public void downloadBundleFromURL( mDownloadBundleFromURLCall = Assertions.assertNotNull(mClient.newCall(request)); mDownloadBundleFromURLCall.enqueue(new Callback() { @Override - public void onFailure(Request request, IOException e) { + public void onFailure(Call call, IOException e) { // ignore callback if call was cancelled if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall.isCanceled()) { mDownloadBundleFromURLCall = null; @@ -191,12 +192,12 @@ public void onFailure(Request request, IOException e) { .append("\u2022 Ensure that your device/emulator is connected to your machine and has USB debugging enabled - run 'adb devices' to see a list of connected devices\n") .append("\u2022 If you're on a physical device connected to the same machine, run 'adb reverse tcp:8081 tcp:8081' to forward requests from your device\n") .append("\u2022 If your device is on the same Wi-Fi network, set 'Debug server host & port for device' in 'Dev settings' to your machine's IP address and the port of the local dev server - e.g. 10.0.1.1:8081\n\n") - .append("URL: ").append(request.urlString()); + .append("URL: ").append(call.request().url().toString()); callback.onFailure(new DebugServerException(sb.toString())); } @Override - public void onResponse(Response response) throws IOException { + public void onResponse(Call call, Response response) throws IOException { // ignore callback if call was cancelled if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall.isCanceled()) { mDownloadBundleFromURLCall = null; @@ -213,7 +214,7 @@ public void onResponse(Response response) throws IOException { } else { StringBuilder sb = new StringBuilder(); sb.append("The development server returned response error code: ").append(response.code()).append("\n\n") - .append("URL: ").append(request.urlString()).append("\n\n") + .append("URL: ").append(call.request().url().toString()).append("\n\n") .append("Body:\n") .append(body); callback.onFailure(new DebugServerException(sb.toString())); @@ -251,7 +252,7 @@ public void isPackagerRunning(final PackagerStatusCallback callback) { mClient.newCall(request).enqueue( new Callback() { @Override - public void onFailure(Request request, IOException e) { + public void onFailure(Call call, IOException e) { FLog.w( ReactConstants.TAG, "The packager does not seem to be running as we got an IOException requesting " + @@ -260,7 +261,7 @@ public void onFailure(Request request, IOException e) { } @Override - public void onResponse(Response response) throws IOException { + public void onResponse(Call call, Response response) throws IOException { if (!response.isSuccessful()) { FLog.e( ReactConstants.TAG, @@ -297,7 +298,7 @@ public void stopPollingOnChangeEndpoint() { mOnChangePollingEnabled = false; mRestartOnChangePollingHandler.removeCallbacksAndMessages(null); if (mOnChangePollingClient != null) { - mOnChangePollingClient.cancel(this); + OkHttpCallUtil.cancelTag(mOnChangePollingClient, this); mOnChangePollingClient = null; } mOnServerContentChangeListener = null; @@ -311,10 +312,10 @@ public void startPollingOnChangeEndpoint( } mOnChangePollingEnabled = true; mOnServerContentChangeListener = onServerContentChangeListener; - mOnChangePollingClient = new OkHttpClient(); - mOnChangePollingClient - .setConnectionPool(new ConnectionPool(1, LONG_POLL_KEEP_ALIVE_DURATION_MS)) - .setConnectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS); + mOnChangePollingClient = new OkHttpClient.Builder() + .connectionPool(new ConnectionPool(1, LONG_POLL_KEEP_ALIVE_DURATION_MS, TimeUnit.MINUTES)) + .connectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .build(); enqueueOnChangeEndpointLongPolling(); } @@ -338,7 +339,7 @@ private void enqueueOnChangeEndpointLongPolling() { Request request = new Request.Builder().url(createOnChangeEndpointUrl()).tag(this).build(); Assertions.assertNotNull(mOnChangePollingClient).newCall(request).enqueue(new Callback() { @Override - public void onFailure(Request request, IOException e) { + public void onFailure(Call call, IOException e) { if (mOnChangePollingEnabled) { // this runnable is used by onchange endpoint poller to delay subsequent requests in case // of a failure, so that we don't flood network queue with frequent requests in case when @@ -356,7 +357,7 @@ public void run() { } @Override - public void onResponse(Response response) throws IOException { + public void onResponse(Call call, Response response) throws IOException { handleOnChangePollingResponse(response.code() == 205); } }); @@ -376,13 +377,13 @@ public void launchJSDevtools() { .build(); mClient.newCall(request).enqueue(new Callback() { @Override - public void onFailure(Request request, IOException e) { + public void onFailure(Call call, IOException e) { // ignore HTTP call response, this is just to open a debugger page and there is no reason // to report failures from here } @Override - public void onResponse(Response response) throws IOException { + public void onResponse(Call call, Response response) throws IOException { // ignore HTTP call response - see above } }); diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java index e6b777e1b8ba7c..eefb1d7359de21 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java @@ -30,10 +30,10 @@ import com.facebook.react.common.ReactConstants; import com.facebook.react.devsupport.StackTraceHelper.StackFrame; -import com.squareup.okhttp.MediaType; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.RequestBody; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; import org.json.JSONObject; /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/BUCK index 18cae1994ea5cd..0f462adbc3a35b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/BUCK @@ -4,21 +4,21 @@ android_library( name = 'fresco', srcs = glob(['**/*.java']), deps = [ - react_native_target('java/com/facebook/react/bridge:bridge'), - react_native_target('java/com/facebook/react/modules/common:common'), - react_native_target('java/com/facebook/react/modules/network:network'), react_native_dep('java/com/facebook/systrace:systrace'), + react_native_dep('libraries/fresco/fresco-react-native:fbcore'), + react_native_dep('libraries/fresco/fresco-react-native:fresco-drawee'), react_native_dep('libraries/fresco/fresco-react-native:fresco-react-native'), react_native_dep('libraries/fresco/fresco-react-native:imagepipeline'), - react_native_dep('libraries/fresco/fresco-react-native:imagepipeline-okhttp'), + react_native_dep('libraries/fresco/fresco-react-native:imagepipeline-okhttp3'), react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'), react_native_dep('third-party/android/support-annotations:android-support-annotations'), react_native_dep('third-party/android/support/v4:lib-support-v4'), react_native_dep('third-party/java/jsr-305:jsr-305'), - react_native_dep('third-party/java/okhttp:okhttp'), - react_native_dep('libraries/fresco/fresco-react-native:fbcore'), - react_native_dep('libraries/fresco/fresco-react-native:fresco-drawee'), -], + react_native_dep('third-party/java/okhttp:okhttp3'), + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/modules/common:common'), + react_native_target('java/com/facebook/react/modules/network:network'), + ], visibility = [ 'PUBLIC', ], diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java index 3f2302ab9ab4d9..210f89e35bf147 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.java @@ -19,7 +19,7 @@ import com.facebook.common.internal.AndroidPredicates; import com.facebook.common.soloader.SoLoaderShim; import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.imagepipeline.backends.okhttp.OkHttpImagePipelineConfigFactory; +import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory; import com.facebook.imagepipeline.core.ImagePipelineConfig; import com.facebook.imagepipeline.core.ImagePipelineFactory; import com.facebook.imagepipeline.listener.RequestListener; @@ -29,7 +29,7 @@ import com.facebook.react.modules.network.OkHttpClientProvider; import com.facebook.soloader.SoLoader; -import com.squareup.okhttp.OkHttpClient; +import okhttp3.OkHttpClient; /** * Module to initialize the Fresco library. @@ -84,8 +84,8 @@ public void clearSensitiveData() { ImagePipelineFactory imagePipelineFactory = Fresco.getImagePipelineFactory(); imagePipelineFactory.getBitmapMemoryCache().removeAll(AndroidPredicates.True()); imagePipelineFactory.getEncodedMemoryCache().removeAll(AndroidPredicates.True()); - imagePipelineFactory.getMainDiskStorageCache().clearAll(); - imagePipelineFactory.getSmallImageDiskStorageCache().clearAll(); + imagePipelineFactory.getMainFileCache().clearAll(); + imagePipelineFactory.getSmallImageFileCache().clearAll(); } private static ImagePipelineConfig getDefaultConfig( diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/network/BUCK index 42f9d30b09809b..aa6b475a035f92 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/BUCK @@ -4,15 +4,16 @@ android_library( name = 'network', srcs = glob(['**/*.java']), deps = [ - react_native_target('java/com/facebook/react/bridge:bridge'), - react_native_target('java/com/facebook/react/modules/core:core'), - react_native_target('java/com/facebook/react/common:common'), react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'), react_native_dep('third-party/android/support/v4:lib-support-v4'), react_native_dep('third-party/java/infer-annotations:infer-annotations'), react_native_dep('third-party/java/jsr-305:jsr-305'), - react_native_dep('third-party/java/okhttp:okhttp'), + react_native_dep('third-party/java/okhttp:okhttp3'), + react_native_dep('third-party/java/okhttp:okhttp3-urlconnection'), react_native_dep('third-party/java/okio:okio'), + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/modules/core:core'), ], visibility = [ 'PUBLIC', diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/CookieJarContainer.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/CookieJarContainer.java new file mode 100644 index 00000000000000..fcf375f08dd97d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/CookieJarContainer.java @@ -0,0 +1,11 @@ +package com.facebook.react.modules.network; + +import okhttp3.CookieJar; + +public interface CookieJarContainer extends CookieJar { + + void setCookieJar(CookieJar cookieJar); + + void removeCookieJar(); + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkInterceptorCreator.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkInterceptorCreator.java index b2b7ac250d1732..15f136753ca6b6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkInterceptorCreator.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkInterceptorCreator.java @@ -9,7 +9,7 @@ package com.facebook.react.modules.network; -import com.squareup.okhttp.Interceptor; +import okhttp3.Interceptor; /** * Classes implementing this interface return a new {@link Interceptor} when the {@link #create} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java index 5db80193b69b49..8a67aa10401b7e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java @@ -9,17 +9,6 @@ package com.facebook.react.modules.network; -import javax.annotation.Nullable; - -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; - -import java.net.SocketTimeoutException; - -import java.util.List; -import java.util.concurrent.TimeUnit; - import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ExecutorToken; import com.facebook.react.bridge.GuardedAsyncTask; @@ -30,19 +19,29 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.network.OkHttpCallUtil; import com.facebook.react.modules.core.DeviceEventManagerModule; -import com.squareup.okhttp.Callback; -import com.squareup.okhttp.Headers; -import com.squareup.okhttp.MediaType; -import com.squareup.okhttp.MultipartBuilder; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.RequestBody; -import com.squareup.okhttp.Response; -import com.squareup.okhttp.ResponseBody; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.net.SocketTimeoutException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nullable; -import static java.lang.Math.min; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Headers; +import okhttp3.JavaNetCookieJar; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; /** * Implements the XMLHttpRequest JavaScript interface. @@ -61,6 +60,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule { private final OkHttpClient mClient; private final ForwardingCookieHandler mCookieHandler; private final @Nullable String mDefaultUserAgent; + private final CookieJarContainer mCookieJarContainer; private boolean mShuttingDown; /* package */ NetworkingModule( @@ -76,6 +76,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule { } } mCookieHandler = new ForwardingCookieHandler(reactContext); + mCookieJarContainer = (CookieJarContainer) mClient.cookieJar(); mShuttingDown = false; mDefaultUserAgent = defaultUserAgent; } @@ -126,7 +127,7 @@ public NetworkingModule(ReactApplicationContext reactContext, OkHttpClient clien @Override public void initialize() { - mClient.setCookieHandler(mCookieHandler); + mCookieJarContainer.setCookieJar(new JavaNetCookieJar(mCookieHandler)); } @Override @@ -137,10 +138,10 @@ public String getName() { @Override public void onCatalystInstanceDestroy() { mShuttingDown = true; - mClient.cancel(null); + OkHttpCallUtil.cancelTag(mClient, null); mCookieHandler.destroy(); - mClient.setCookieHandler(null); + mCookieJarContainer.removeCookieJar(); } @ReactMethod @@ -167,9 +168,10 @@ public void sendRequest( // client and set the timeout explicitly on the clone. This is cheap as everything else is // shared under the hood. // See https://github.com/square/okhttp/wiki/Recipes#per-call-configuration for more information - if (timeout != mClient.getConnectTimeout()) { - client = mClient.clone(); - client.setReadTimeout(timeout, TimeUnit.MILLISECONDS); + if (timeout != mClient.connectTimeoutMillis()) { + client = mClient.newBuilder() + .readTimeout(timeout, TimeUnit.MILLISECONDS) + .build(); } Headers requestHeaders = extractHeaders(headers, data); @@ -228,7 +230,7 @@ public void sendRequest( contentType = "multipart/form-data"; } ReadableArray parts = data.getArray(REQUEST_BODY_KEY_FORMDATA); - MultipartBuilder multipartBuilder = + MultipartBody.Builder multipartBuilder = constructMultipartBody(executorToken, parts, contentType, requestId); if (multipartBuilder == null) { return; @@ -242,7 +244,7 @@ public void sendRequest( client.newCall(requestBuilder.build()).enqueue( new Callback() { @Override - public void onFailure(Request request, IOException e) { + public void onFailure(Call call, IOException e) { if (mShuttingDown) { return; } @@ -250,7 +252,7 @@ public void onFailure(Request request, IOException e) { } @Override - public void onResponse(Response response) throws IOException { + public void onResponse(Call call, Response response) throws IOException { if (mShuttingDown) { return; } @@ -328,7 +330,7 @@ private void onResponseReceived( args.pushInt(requestId); args.pushInt(response.code()); args.pushMap(headers); - args.pushString(response.request().urlString()); + args.pushString(response.request().url().toString()); getEventEmitter(ExecutorToken).emit("didReceiveNetworkResponse", args); } @@ -356,7 +358,7 @@ public void abortRequest(ExecutorToken executorToken, final int requestId) { new GuardedAsyncTask(getReactApplicationContext()) { @Override protected void doInBackgroundGuarded(Void... params) { - mClient.cancel(requestId); + OkHttpCallUtil.cancelTag(mClient, requestId); } }.execute(); } @@ -373,15 +375,13 @@ public boolean supportsWebWorkers() { return true; } - private - @Nullable - MultipartBuilder constructMultipartBody( + private @Nullable MultipartBody.Builder constructMultipartBody( ExecutorToken ExecutorToken, ReadableArray body, String contentType, int requestId) { - MultipartBuilder multipartBuilder = new MultipartBuilder(); - multipartBuilder.type(MediaType.parse(contentType)); + MultipartBody.Builder multipartBuilder = new MultipartBody.Builder(); + multipartBuilder.setType(MediaType.parse(contentType)); for (int i = 0, size = body.size(); i < size; i++) { ReadableMap bodyPart = body.getMap(i); @@ -440,9 +440,7 @@ MultipartBuilder constructMultipartBody( /** * Extracts the headers from the Array. If the format is invalid, this method will return null. */ - private - @Nullable - Headers extractHeaders( + private @Nullable Headers extractHeaders( @Nullable ReadableArray headersArray, @Nullable ReadableMap requestData) { if (headersArray == null) { @@ -475,4 +473,4 @@ private DeviceEventManagerModule.RCTDeviceEventEmitter getEventEmitter(ExecutorT return getReactApplicationContext() .getJSModule(ExecutorToken, DeviceEventManagerModule.RCTDeviceEventEmitter.class); } -} \ No newline at end of file +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java index 956b5fbae3c60f..dcd8972cf6e157 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.java @@ -9,11 +9,11 @@ package com.facebook.react.modules.network; -import javax.annotation.Nullable; - import java.util.concurrent.TimeUnit; -import com.squareup.okhttp.OkHttpClient; +import javax.annotation.Nullable; + +import okhttp3.OkHttpClient; /** * Helper class that provides the same OkHttpClient instance that will be used for all networking @@ -30,15 +30,20 @@ public static OkHttpClient getOkHttpClient() { } return sClient; } + + // okhttp3 OkHttpClient is immutable + // This allows app to init an OkHttpClient with custom settings. + public static void replaceOkHttpClient(OkHttpClient client) { + sClient = client; + } private static OkHttpClient createClient() { - OkHttpClient client = new OkHttpClient(); - // No timeouts by default - client.setConnectTimeout(0, TimeUnit.MILLISECONDS); - client.setReadTimeout(0, TimeUnit.MILLISECONDS); - client.setWriteTimeout(0, TimeUnit.MILLISECONDS); - - return client; + return new OkHttpClient.Builder() + .connectTimeout(0, TimeUnit.MILLISECONDS) + .readTimeout(0, TimeUnit.MILLISECONDS) + .writeTimeout(0, TimeUnit.MILLISECONDS) + .cookieJar(new ReactCookieJarContainer()) + .build(); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/ReactCookieJarContainer.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ReactCookieJarContainer.java new file mode 100644 index 00000000000000..a8436c21875ebc --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ReactCookieJarContainer.java @@ -0,0 +1,44 @@ +package com.facebook.react.modules.network; + +import java.util.Collections; +import java.util.List; + +import javax.annotation.Nullable; + +import okhttp3.Cookie; +import okhttp3.CookieJar; +import okhttp3.HttpUrl; + +/** + * Basic okhttp3 CookieJar container + */ +public class ReactCookieJarContainer implements CookieJarContainer { + + @Nullable + private CookieJar cookieJar = null; + + @Override + public void setCookieJar(CookieJar cookieJar) { + this.cookieJar = cookieJar; + } + + @Override + public void removeCookieJar() { + this.cookieJar = null; + } + + @Override + public void saveFromResponse(HttpUrl url, List cookies) { + if (cookieJar != null) { + cookieJar.saveFromResponse(url, cookies); + } + } + + @Override + public List loadForRequest(HttpUrl url) { + if (cookieJar != null) { + return cookieJar.loadForRequest(url); + } + return Collections.emptyList(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java index f834120acaf15b..0290a23fd2670f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java @@ -23,9 +23,9 @@ import com.facebook.common.logging.FLog; import com.facebook.react.common.ReactConstants; -import com.squareup.okhttp.MediaType; -import com.squareup.okhttp.RequestBody; -import com.squareup.okhttp.internal.Util; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okhttp3.internal.Util; import okio.BufferedSink; import okio.ByteString; import okio.Okio; diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/BUCK index 6289e05986f1c3..7c8af9cca7a7db 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/BUCK @@ -4,16 +4,16 @@ android_library( name = 'websocket', srcs = glob(['**/*.java']), deps = [ - react_native_target('java/com/facebook/react/bridge:bridge'), - react_native_target('java/com/facebook/react/common:common'), - react_native_target('java/com/facebook/react/modules/core:core'), react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'), react_native_dep('third-party/java/infer-annotations:infer-annotations'), react_native_dep('third-party/java/jsr-305:jsr-305'), - react_native_dep('third-party/java/okhttp:okhttp-ws'), - react_native_dep('third-party/java/okhttp:okhttp'), + react_native_dep('third-party/java/okhttp:okhttp3'), + react_native_dep('third-party/java/okhttp:okhttp3-ws'), react_native_dep('third-party/java/okio:okio'), -], + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/modules/core:core'), + ], visibility = [ 'PUBLIC', ], diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java index 938136f3655ab0..e7eae279bc14b0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java @@ -29,12 +29,14 @@ import com.facebook.react.common.ReactConstants; import com.facebook.react.modules.core.DeviceEventManagerModule; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.Response; -import com.squareup.okhttp.ws.WebSocket; -import com.squareup.okhttp.ws.WebSocketCall; -import com.squareup.okhttp.ws.WebSocketListener; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okhttp3.ws.WebSocket; +import okhttp3.ws.WebSocketCall; +import okhttp3.ws.WebSocketListener; import java.net.URISyntaxException; import java.net.URI; @@ -43,7 +45,6 @@ import java.util.concurrent.TimeUnit; import okio.Buffer; -import okio.BufferedSource; import okio.ByteString; public class WebSocketModule extends ReactContextBaseJavaModule { @@ -69,12 +70,11 @@ public String getName() { @ReactMethod public void connect(final String url, @Nullable final ReadableArray protocols, @Nullable final ReadableMap headers, final int id) { - OkHttpClient client = new OkHttpClient(); - - client.setConnectTimeout(10, TimeUnit.SECONDS); - client.setWriteTimeout(10, TimeUnit.SECONDS); - // Disable timeouts for read - client.setReadTimeout(0, TimeUnit.MINUTES); + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read + .build(); Request.Builder builder = new Request.Builder() .tag(id) @@ -145,20 +145,20 @@ public void onPong(Buffer buffer) { } @Override - public void onMessage(BufferedSource bufferedSource, WebSocket.PayloadType payloadType) { + public void onMessage(ResponseBody response) throws IOException { String message; try { - if (payloadType == WebSocket.PayloadType.BINARY) { - message = Base64.encodeToString(bufferedSource.readByteArray(), Base64.NO_WRAP); + if (response.contentType() == WebSocket.BINARY) { + message = Base64.encodeToString(response.source().readByteArray(), Base64.NO_WRAP); } else { - message = bufferedSource.readUtf8(); + message = response.source().readUtf8(); } } catch (IOException e) { notifyWebSocketFailed(id, e.getMessage()); return; } try { - bufferedSource.close(); + response.source().close(); } catch (IOException e) { FLog.e( ReactConstants.TAG, @@ -169,13 +169,13 @@ public void onMessage(BufferedSource bufferedSource, WebSocket.PayloadType paylo WritableMap params = Arguments.createMap(); params.putInt("id", id); params.putString("data", message); - params.putString("type", payloadType == WebSocket.PayloadType.BINARY ? "binary" : "text"); + params.putString("type", response.contentType() == WebSocket.BINARY ? "binary" : "text"); sendEvent("websocketMessage", params); } }); // Trigger shutdown of the dispatcher's executor so this process can exit cleanly - client.getDispatcher().getExecutorService().shutdown(); + client.dispatcher().executorService().shutdown(); } @ReactMethod @@ -209,9 +209,7 @@ public void send(String message, int id) { throw new RuntimeException("Cannot send a message. Unknown WebSocket id " + id); } try { - client.sendMessage( - WebSocket.PayloadType.TEXT, - new Buffer().writeUtf8(message)); + client.sendMessage(RequestBody.create(WebSocket.TEXT, message)); } catch (IOException | IllegalStateException e) { notifyWebSocketFailed(id, e.getMessage()); } @@ -225,9 +223,7 @@ public void sendBinary(String base64String, int id) { throw new RuntimeException("Cannot send a message. Unknown WebSocket id " + id); } try { - client.sendMessage( - WebSocket.PayloadType.BINARY, - new Buffer().write(ByteString.decodeBase64(base64String))); + client.sendMessage(RequestBody.create(WebSocket.TEXT, ByteString.decodeBase64(base64String))); } catch (IOException | IllegalStateException e) { notifyWebSocketFailed(id, e.getMessage()); } diff --git a/ReactAndroid/src/main/libraries/fresco/fresco-react-native/BUCK b/ReactAndroid/src/main/libraries/fresco/fresco-react-native/BUCK index 7b410d076afeb1..ee04bc4f0f3358 100644 --- a/ReactAndroid/src/main/libraries/fresco/fresco-react-native/BUCK +++ b/ReactAndroid/src/main/libraries/fresco/fresco-react-native/BUCK @@ -6,8 +6,8 @@ android_prebuilt_aar( remote_file( name = 'fresco-binary-aar', - url = 'mvn:com.facebook.fresco:fresco:aar:0.8.1', - sha1 = 'f0a4f04318123e1597514b2abf56b7e66581f3f8', + url = 'mvn:com.facebook.fresco:fresco:aar:0.10.0', + sha1 = '8576630feea3d08eb681dec389a472623179f84d', ) android_prebuilt_aar( @@ -18,19 +18,31 @@ android_prebuilt_aar( remote_file( name = 'drawee-binary-aar', - url = 'mvn:com.facebook.fresco:drawee:aar:0.8.1', - sha1 = 'a944015ddf50fdad79302e42a85a351633c24472', + url = 'mvn:com.facebook.fresco:drawee:aar:0.10.0', + sha1 = 'c594f9c23f844d08ecd4c0e42df40d80767d4b18', ) android_library( name = 'imagepipeline', exported_deps = [ + ':imagepipeline-base', ':imagepipeline-core', ':bolts', ], visibility = ['//ReactAndroid/...',], ) +android_prebuilt_aar( + name = 'imagepipeline-base', + aar = ':imagepipeline-base-aar', + visibility = ['//ReactAndroid/...',], +) + +remote_file( + name = 'imagepipeline-base-aar', + url = 'mvn:com.facebook.fresco:imagepipeline-base:aar:0.10.0', + sha1 = '1390f28d0e4f16b0008fed481eb1b107d93f35b8', +) android_prebuilt_aar( name = 'imagepipeline-core', @@ -40,8 +52,8 @@ android_prebuilt_aar( remote_file( name = 'imagepipeline-aar', - url = 'mvn:com.facebook.fresco:imagepipeline:aar:0.8.1', - sha1 = '93fe3e629c03aea8f63dabd80a0e616b0caef65b', + url = 'mvn:com.facebook.fresco:imagepipeline:aar:0.10.0', + sha1 = 'ecec308b714039dd9e0cf4b7595a427765f43a01', ) prebuilt_jar( @@ -64,18 +76,18 @@ android_prebuilt_aar( remote_file( name = 'fbcore-aar', - url = 'mvn:com.facebook.fresco:fbcore:aar:0.8.1', - sha1 = 'cc46b3d564139bf63bb41534c7a723ee8119ae5f', + url = 'mvn:com.facebook.fresco:fbcore:aar:0.10.0', + sha1 = '4650dd9a46064c254e0468bba23804e8f32e6144', ) android_prebuilt_aar( - name = 'imagepipeline-okhttp', - aar = ':imagepipeline-okhttp-binary-aar', + name = 'imagepipeline-okhttp3', + aar = ':imagepipeline-okhttp3-binary-aar', visibility = ['//ReactAndroid/...',], ) remote_file( - name = 'imagepipeline-okhttp-binary-aar', - url = 'mvn:com.facebook.fresco:imagepipeline-okhttp:aar:0.8.1', - sha1 = 'd6b16dbaab8b810620347355a425fb2982e33ef8', + name = 'imagepipeline-okhttp3-binary-aar', + url = 'mvn:com.facebook.fresco:imagepipeline-okhttp3:aar:0.10.0', + sha1 = 'a3e95483f116b528bdf2d06f7ff2212cb9b372e2', ) diff --git a/ReactAndroid/src/main/third-party/java/okhttp/BUCK b/ReactAndroid/src/main/third-party/java/okhttp/BUCK index f4da3fa0b0a1c6..07d870bdbbe52d 100644 --- a/ReactAndroid/src/main/third-party/java/okhttp/BUCK +++ b/ReactAndroid/src/main/third-party/java/okhttp/BUCK @@ -1,23 +1,35 @@ prebuilt_jar( - name = 'okhttp', - binary_jar = ':okhttp-binary-jar', + name = 'okhttp3', + binary_jar = ':okhttp3-binary-jar', visibility = ['//ReactAndroid/...',], ) remote_file( - name = 'okhttp-binary-jar', - url = 'mvn:com.squareup.okhttp:okhttp:jar:2.5.0', - sha1 = '4de2b4ed3445c37ec1720a7d214712e845a24636' + name = 'okhttp3-binary-jar', + url = 'mvn:com.squareup.okhttp3:okhttp:jar:3.2.0', + sha1 = 'f7873a2ebde246a45c2a8d6f3247108b4c88a879' ) prebuilt_jar( - name = 'okhttp-ws', - binary_jar = ':okhttp-ws-binary-jar', + name = 'okhttp3-urlconnection', + binary_jar = ':okhttp3-urlconnection-binary-jar', + visibility = ['//ReactAndroid/...',], +) + +remote_file( + name = 'okhttp3-urlconnection-binary-jar', + url = 'mvn:com.squareup.okhttp3:okhttp-urlconnection:jar:3.2.0', + sha1 = '6f8a4b1435c9e0a6f9c5fe4a1be46627b848fd0c' +) + +prebuilt_jar( + name = 'okhttp3-ws', + binary_jar = ':okhttp3-ws-binary-jar', visibility = ['//ReactAndroid/...',], ) remote_file( - name = 'okhttp-ws-binary-jar', - url = 'mvn:com.squareup.okhttp:okhttp-ws:jar:2.5.0', - sha1 = '0e9753b7dcae5deca92e871c5c759067070b07bc', + name = 'okhttp3-ws-binary-jar', + url = 'mvn:com.squareup.okhttp3:okhttp-ws:jar:3.2.0', + sha1 = '1ea229d6984444c8c58b8e97ba4c8429d9d135b3', ) diff --git a/ReactAndroid/src/main/third-party/java/okio/BUCK b/ReactAndroid/src/main/third-party/java/okio/BUCK index 61ad012c23cd8d..3f3339840c4265 100644 --- a/ReactAndroid/src/main/third-party/java/okio/BUCK +++ b/ReactAndroid/src/main/third-party/java/okio/BUCK @@ -6,6 +6,6 @@ prebuilt_jar( remote_file( name = 'okio-binary-jar', - url = 'mvn:com.squareup.okio:okio:jar:1.6.0', - sha1 = '98476622f10715998eacf9240d6b479f12c66143', + url = 'mvn:com.squareup.okio:okio:jar:1.8.0', + sha1 = '05ea7af56cc7c567ed9856d99efb30740e9b17ff', ) diff --git a/ReactAndroid/src/test/java/com/facebook/react/BUCK b/ReactAndroid/src/test/java/com/facebook/react/BUCK index 0396f42ae08a66..9d3626d2319731 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/BUCK +++ b/ReactAndroid/src/test/java/com/facebook/react/BUCK @@ -6,8 +6,15 @@ robolectric3_test( contacts = ['oncall+fbandroid_sheriff@xmail.facebook.com'], srcs = glob(['*.java']), deps = [ + react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'), + react_native_dep('third-party/java/fest:fest'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + react_native_dep('third-party/java/junit:junit'), + react_native_dep('third-party/java/mockito:mockito'), + react_native_dep('third-party/java/okhttp:okhttp3'), + react_native_dep('third-party/java/okio:okio'), + react_native_dep('third-party/java/robolectric3/robolectric:robolectric'), react_native_target('java/com/facebook/csslayout:csslayout'), - react_native_target('java/com/facebook/react:react'), react_native_target('java/com/facebook/react/animation:animation'), react_native_target('java/com/facebook/react/bridge:bridge'), react_native_target('java/com/facebook/react/common:common'), @@ -15,20 +22,11 @@ robolectric3_test( react_native_target('java/com/facebook/react/uimanager:uimanager'), react_native_target('java/com/facebook/react/views/text:text'), react_native_target('java/com/facebook/react/views/view:view'), + react_native_target('java/com/facebook/react:react'), react_native_tests_target('java/com/facebook/react/bridge:testhelpers'), - - react_native_dep('third-party/java/robolectric3/robolectric:robolectric'), - react_native_dep('third-party/java/fest:fest'), - react_native_dep('third-party/java/junit:junit'), - react_native_dep('third-party/java/okio:okio'), - react_native_dep('third-party/java/mockito:mockito'), - react_native_dep('third-party/java/okhttp:okhttp'), - react_native_dep('third-party/java/jsr-305:jsr-305'), - react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'), ], ) project_config( test_target = ':react', ) - diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK b/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK index 5e9df2b0b99d4f..19787b1312f6a0 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK @@ -6,8 +6,14 @@ robolectric3_test( name = 'modules', srcs = glob(['**/*.java']), deps = [ + react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'), + react_native_dep('third-party/java/fest:fest'), + react_native_dep('third-party/java/junit:junit'), + react_native_dep('third-party/java/mockito:mockito'), + react_native_dep('third-party/java/okhttp:okhttp3'), + react_native_dep('third-party/java/okio:okio'), + react_native_dep('third-party/java/robolectric3/robolectric:robolectric'), react_native_target('java/com/facebook/csslayout:csslayout'), - react_native_target('java/com/facebook/react:react'), react_native_target('java/com/facebook/react/animation:animation'), react_native_target('java/com/facebook/react/bridge:bridge'), react_native_target('java/com/facebook/react/common:common'), @@ -19,17 +25,10 @@ robolectric3_test( react_native_target('java/com/facebook/react/modules/network:network'), react_native_target('java/com/facebook/react/modules/storage:storage'), react_native_target('java/com/facebook/react/modules/systeminfo:systeminfo'), - react_native_target('java/com/facebook/react/uimanager:uimanager'), react_native_target('java/com/facebook/react/touch:touch'), - + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_target('java/com/facebook/react:react'), react_native_tests_target('java/com/facebook/react/bridge:testhelpers'), - react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'), - react_native_dep('third-party/java/robolectric3/robolectric:robolectric'), - react_native_dep('third-party/java/fest:fest'), - react_native_dep('third-party/java/junit:junit'), - react_native_dep('third-party/java/okio:okio'), - react_native_dep('third-party/java/mockito:mockito'), - react_native_dep('third-party/java/okhttp:okhttp'), ], visibility = [ 'PUBLIC' diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java index ce38961233cbca..b805611834397b 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java @@ -23,22 +23,19 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; -import com.squareup.okhttp.Call; -import com.squareup.okhttp.Headers; -import com.squareup.okhttp.MediaType; -import com.squareup.okhttp.MultipartBuilder; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.RequestBody; +import okhttp3.Call; +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; import okio.Buffer; -import org.junit.After; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -46,9 +43,7 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.modules.junit4.rule.PowerMockRule; -import org.robolectric.RuntimeEnvironment; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.Mockito.any; @@ -65,7 +60,8 @@ Arguments.class, Call.class, RequestBodyUtil.class, - MultipartBuilder.class, + MultipartBody.class, + MultipartBody.Builder.class, NetworkingModule.class, OkHttpClient.class}) @RunWith(RobolectricTestRunner.class) @@ -100,7 +96,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); verify(httpClient).newCall(argumentCaptor.capture()); - assertThat(argumentCaptor.getValue().urlString()).isEqualTo("http://somedomain/foo"); + assertThat(argumentCaptor.getValue().url().toString()).isEqualTo("http://somedomain/foo"); // We set the User-Agent header by default assertThat(argumentCaptor.getValue().headers().size()).isEqualTo(1); assertThat(argumentCaptor.getValue().method()).isEqualTo("GET"); @@ -215,7 +211,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); verify(httpClient).newCall(argumentCaptor.capture()); - assertThat(argumentCaptor.getValue().urlString()).isEqualTo("http://somedomain/bar"); + assertThat(argumentCaptor.getValue().url().toString()).isEqualTo("http://somedomain/bar"); assertThat(argumentCaptor.getValue().headers().size()).isEqualTo(2); assertThat(argumentCaptor.getValue().method()).isEqualTo("POST"); assertThat(argumentCaptor.getValue().body().contentType().type()).isEqualTo("text"); @@ -302,12 +298,12 @@ public Object answer(InvocationOnMock invocation) throws Throwable { // verify url, method, headers ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); verify(httpClient).newCall(argumentCaptor.capture()); - assertThat(argumentCaptor.getValue().urlString()).isEqualTo("http://someurl/uploadFoo"); + assertThat(argumentCaptor.getValue().url().toString()).isEqualTo("http://someurl/uploadFoo"); assertThat(argumentCaptor.getValue().method()).isEqualTo("POST"); assertThat(argumentCaptor.getValue().body().contentType().type()). - isEqualTo(MultipartBuilder.FORM.type()); + isEqualTo(MultipartBody.FORM.type()); assertThat(argumentCaptor.getValue().body().contentType().subtype()). - isEqualTo(MultipartBuilder.FORM.subtype()); + isEqualTo(MultipartBody.FORM.subtype()); Headers requestHeaders = argumentCaptor.getValue().headers(); assertThat(requestHeaders.size()).isEqualTo(1); } @@ -361,12 +357,12 @@ public Object answer(InvocationOnMock invocation) throws Throwable { // verify url, method, headers ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); verify(httpClient).newCall(argumentCaptor.capture()); - assertThat(argumentCaptor.getValue().urlString()).isEqualTo("http://someurl/uploadFoo"); + assertThat(argumentCaptor.getValue().url().toString()).isEqualTo("http://someurl/uploadFoo"); assertThat(argumentCaptor.getValue().method()).isEqualTo("POST"); assertThat(argumentCaptor.getValue().body().contentType().type()). - isEqualTo(MultipartBuilder.FORM.type()); + isEqualTo(MultipartBody.FORM.type()); assertThat(argumentCaptor.getValue().body().contentType().subtype()). - isEqualTo(MultipartBuilder.FORM.subtype()); + isEqualTo(MultipartBody.FORM.subtype()); Headers requestHeaders = argumentCaptor.getValue().headers(); assertThat(requestHeaders.size()).isEqualTo(3); assertThat(requestHeaders.get("Accept")).isEqualTo("text/plain"); @@ -383,9 +379,9 @@ public void testMultipartPostRequestBody() throws Exception { when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))).thenCallRealMethod(); when(inputStream.available()).thenReturn("imageUri".length()); - final MultipartBuilder multipartBuilder = mock(MultipartBuilder.class); - PowerMockito.whenNew(MultipartBuilder.class).withNoArguments().thenReturn(multipartBuilder); - when(multipartBuilder.type(any(MediaType.class))).thenAnswer( + final MultipartBody.Builder multipartBuilder = mock(MultipartBody.Builder.class); + PowerMockito.whenNew(MultipartBody.Builder.class).withNoArguments().thenReturn(multipartBuilder); + when(multipartBuilder.setType(any(MediaType.class))).thenAnswer( new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { @@ -403,7 +399,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { - return mock(RequestBody.class); + return mock(MultipartBody.class); } }); @@ -462,7 +458,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { // verify body verify(multipartBuilder).build(); - verify(multipartBuilder).type(MultipartBuilder.FORM); + verify(multipartBuilder).setType(MultipartBody.FORM); ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); ArgumentCaptor bodyArgumentCaptor = ArgumentCaptor.forClass(RequestBody.class); verify(multipartBuilder, times(2)). diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/BUCK b/ReactAndroid/src/test/java/com/facebook/react/uimanager/BUCK index 9fe1b6977714b0..ccfb2bc70b74b1 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/uimanager/BUCK +++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/BUCK @@ -6,26 +6,25 @@ robolectric3_test( contacts = ['oncall+fbandroid_sheriff@xmail.facebook.com'], srcs = glob(['**/*.java']), deps = [ + react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'), + react_native_dep('third-party/java/fest:fest'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + react_native_dep('third-party/java/junit:junit'), + react_native_dep('third-party/java/mockito:mockito'), + react_native_dep('third-party/java/okhttp:okhttp3'), + react_native_dep('third-party/java/okio:okio'), + react_native_dep('third-party/java/robolectric3/robolectric:robolectric'), react_native_target('java/com/facebook/csslayout:csslayout'), - react_native_target('java/com/facebook/react:react'), react_native_target('java/com/facebook/react/animation:animation'), react_native_target('java/com/facebook/react/bridge:bridge'), react_native_target('java/com/facebook/react/common:common'), react_native_target('java/com/facebook/react/touch:touch'), - react_native_target('java/com/facebook/react/uimanager:uimanager'), react_native_target('java/com/facebook/react/uimanager/annotations:annotations'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), react_native_target('java/com/facebook/react/views/text:text'), react_native_target('java/com/facebook/react/views/view:view'), - + react_native_target('java/com/facebook/react:react'), react_native_tests_target('java/com/facebook/react/bridge:testhelpers'), - react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'), - react_native_dep('third-party/java/robolectric3/robolectric:robolectric'), - react_native_dep('third-party/java/fest:fest'), - react_native_dep('third-party/java/junit:junit'), - react_native_dep('third-party/java/okio:okio'), - react_native_dep('third-party/java/mockito:mockito'), - react_native_dep('third-party/java/okhttp:okhttp'), - react_native_dep('third-party/java/jsr-305:jsr-305'), ], visibility = [ 'PUBLIC' diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/BUCK b/ReactAndroid/src/test/java/com/facebook/react/views/BUCK index 5834b9374d1b8a..673085e0357b0e 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/views/BUCK +++ b/ReactAndroid/src/test/java/com/facebook/react/views/BUCK @@ -6,31 +6,29 @@ robolectric3_test( contacts = ['oncall+fbandroid_sheriff@xmail.facebook.com'], srcs = glob(['**/*.java']), deps = [ + react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'), + react_native_dep('libraries/fresco/fresco-react-native:fresco-drawee'), + react_native_dep('libraries/fresco/fresco-react-native:fresco-react-native'), + react_native_dep('libraries/fresco/fresco-react-native:imagepipeline'), + react_native_dep('third-party/java/fest:fest'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + react_native_dep('third-party/java/junit:junit'), + react_native_dep('third-party/java/mockito:mockito'), + react_native_dep('third-party/java/okhttp:okhttp3'), + react_native_dep('third-party/java/okio:okio'), + react_native_dep('third-party/java/robolectric3/robolectric:robolectric'), react_native_target('java/com/facebook/csslayout:csslayout'), - react_native_target('java/com/facebook/react:react'), react_native_target('java/com/facebook/react/bridge:bridge'), react_native_target('java/com/facebook/react/common:common'), react_native_target('java/com/facebook/react/touch:touch'), - react_native_target('java/com/facebook/react/uimanager:uimanager'), react_native_target('java/com/facebook/react/uimanager/annotations:annotations'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), react_native_target('java/com/facebook/react/views/image:image'), react_native_target('java/com/facebook/react/views/text:text'), react_native_target('java/com/facebook/react/views/textinput:textinput'), react_native_target('java/com/facebook/react/views/view:view'), - + react_native_target('java/com/facebook/react:react'), react_native_tests_target('java/com/facebook/react/bridge:testhelpers'), - react_native_dep('third-party/java/robolectric3/robolectric:robolectric'), - react_native_dep('third-party/java/fest:fest'), - react_native_dep('third-party/java/junit:junit'), - react_native_dep('third-party/java/okio:okio'), - react_native_dep('third-party/java/mockito:mockito'), - react_native_dep('third-party/java/okhttp:okhttp'), - react_native_dep('third-party/java/jsr-305:jsr-305'), - - react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'), - react_native_dep('libraries/fresco/fresco-react-native:fresco-react-native'), - react_native_dep('libraries/fresco/fresco-react-native:imagepipeline'), - react_native_dep('libraries/fresco/fresco-react-native:fresco-drawee'), ], ) diff --git a/local-cli/generator-android/templates/src/app/proguard-rules.pro b/local-cli/generator-android/templates/src/app/proguard-rules.pro index 347a13ce10378b..9852871bd0de35 100644 --- a/local-cli/generator-android/templates/src/app/proguard-rules.pro +++ b/local-cli/generator-android/templates/src/app/proguard-rules.pro @@ -51,9 +51,9 @@ -keepattributes Signature -keepattributes *Annotation* --keep class com.squareup.okhttp.** { *; } --keep interface com.squareup.okhttp.** { *; } --dontwarn com.squareup.okhttp.** +-keep class okhttp3.** { *; } +-keep interface okhttp3.** { *; } +-dontwarn okhttp3.** # okio From 2a67f9b24935f1d9ef78b9700cff6fce671ef54c Mon Sep 17 00:00:00 2001 From: Alexander Blom Date: Tue, 17 May 2016 12:42:14 -0700 Subject: [PATCH 010/843] Move infoLog to OSS Reviewed By: sahrens Differential Revision: D3310270 fbshipit-source-id: 93261e6083a35e6869b384c8c230c861427e4060 --- Libraries/Utilities/infoLog.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Libraries/Utilities/infoLog.js diff --git a/Libraries/Utilities/infoLog.js b/Libraries/Utilities/infoLog.js new file mode 100644 index 00000000000000..a3798fbd810168 --- /dev/null +++ b/Libraries/Utilities/infoLog.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule infoLog + */ +'use strict'; + +/** + * Intentional info-level logging for clear separation from ad-hoc console debug logging. + */ +function infoLog(...args) { + return console.log(...args); // eslint-disable-line no-console-disallow +} + +module.exports = infoLog; From 0694a2991c8f518b4df9ee2b7990210d5830c6ac Mon Sep 17 00:00:00 2001 From: Alexander Blom Date: Tue, 17 May 2016 12:42:17 -0700 Subject: [PATCH 011/843] Use infoLog instead of console.log Reviewed By: sahrens Differential Revision: D3304886 fbshipit-source-id: 79d3fd2b833380a18dfbee666f9bd5eaf25b0744 --- Libraries/AppRegistry/AppRegistry.js | 3 ++- Libraries/BugReporting/BugReporting.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Libraries/AppRegistry/AppRegistry.js b/Libraries/AppRegistry/AppRegistry.js index b76afb379183af..a5fa0e3056e70a 100644 --- a/Libraries/AppRegistry/AppRegistry.js +++ b/Libraries/AppRegistry/AppRegistry.js @@ -17,6 +17,7 @@ var ReactNative = require('ReactNative'); var invariant = require('fbjs/lib/invariant'); var renderApplication = require('renderApplication'); +const infoLog = require('infoLog'); if (__DEV__) { // In order to use Cmd+P to record/dump perf data, we need to make sure @@ -87,7 +88,7 @@ var AppRegistry = { '__DEV__ === ' + String(__DEV__) + ', development-level warning are ' + (__DEV__ ? 'ON' : 'OFF') + ', performance optimizations are ' + (__DEV__ ? 'OFF' : 'ON'); - console.log(msg); + infoLog(msg); BugReporting.init(); BugReporting.addSource('AppRegistry.runApplication' + runCount++, () => msg); BugReporting.addFileSource('react_hierarchy.txt', () => require('dumpReactTree')()); diff --git a/Libraries/BugReporting/BugReporting.js b/Libraries/BugReporting/BugReporting.js index ceeb0df5886654..e86eeacdcbe736 100644 --- a/Libraries/BugReporting/BugReporting.js +++ b/Libraries/BugReporting/BugReporting.js @@ -14,6 +14,7 @@ const BugReportingNativeModule = require('NativeModules').BugReporting; const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); const Map = require('Map'); +const infoLog = require('infoLog'); import type EmitterSubscription from 'EmitterSubscription'; @@ -89,7 +90,7 @@ class BugReporting { for (const [key, callback] of BugReporting._fileSources) { fileData[key] = callback(); } - console.log('BugReporting extraData:', extraData); + infoLog('BugReporting extraData:', extraData); BugReportingNativeModule && BugReportingNativeModule.setExtraData && BugReportingNativeModule.setExtraData(extraData, fileData); From 81998935749f8bc8aa5c679fcf90c596e725909f Mon Sep 17 00:00:00 2001 From: Marc Horowitz Date: Tue, 17 May 2016 14:15:51 -0700 Subject: [PATCH 012/843] Remove script argument to proxy loadApplicationScript Summary: The JSCExecutor API gets passed script data and a URL, but the proxy's purpose in life is to load from the network, so the script data is useless. The code was failing to handle null script data, so rather than just passing nullptr all the time, I removed the argument. if there's a use case in the future for it, we can put it back. Reviewed By: steveluscher Differential Revision: D3312467 fbshipit-source-id: 986c48f1ef3c24e6b5569046ccb08d7864cdcd3d --- .../main/java/com/facebook/react/bridge/JavaJSExecutor.java | 3 +-- .../com/facebook/react/bridge/WebsocketJavaScriptExecutor.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaJSExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaJSExecutor.java index 991fdfb6f292b9..0b119ce7f664c1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaJSExecutor.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaJSExecutor.java @@ -36,11 +36,10 @@ public ProxyExecutorException(Throwable cause) { /** * Load javascript into the js context - * @param script script contet to be executed * @param sourceURL url or file location from which script content was loaded */ @DoNotStrip - void loadApplicationScript(String script, String sourceURL) throws ProxyExecutorException; + void loadApplicationScript(String sourceURL) throws ProxyExecutorException; /** * Execute javascript method within js context diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WebsocketJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WebsocketJavaScriptExecutor.java index b9e1da2adbf016..6198e56560af76 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/WebsocketJavaScriptExecutor.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WebsocketJavaScriptExecutor.java @@ -159,7 +159,7 @@ public void close() { } @Override - public void loadApplicationScript(String script, String sourceURL) + public void loadApplicationScript(String sourceURL) throws ProxyExecutorException { JSExecutorCallbackFuture callback = new JSExecutorCallbackFuture(); Assertions.assertNotNull(mWebSocketClient).loadApplicationScript( From 17be4c754e6f58441e5c72370c8c9285a6654ee7 Mon Sep 17 00:00:00 2001 From: Joel Marcey Date: Tue, 17 May 2016 18:14:16 -0700 Subject: [PATCH 013/843] Core components tutorial Summary: Create the initial Core Components tutorial. The core components are `Text`, `Image`, `View`, `TextInput`, `ListView`. 1. Provide a summary for each core component, including a runnable sample. 2. Allow the tutorials for each component to be extended with more details and detailed examples, particularly after we add other tutorials (i.e., around state and props). 3. The samples should be runnable in a React Native simulator, if we can get that going in the docs. 4. Reorganize the docs sidebar to make the current Tutorial actually a Sample App, etc. Closes https://github.com/facebook/react-native/pull/7593 Differential Revision: D3313563 Pulled By: JoelMarcey fbshipit-source-id: cfe1d397d60822b8c531405d66b4e73694c7dbf9 --- ...tarted.md => QuickStart-GettingStarted.md} | 4 +- docs/SampleApplication-F8App.md | 8 + ...utorial.md => SampleApplication-Movies.md} | 49 +++--- docs/Tutorial-CoreComponents.md | 141 ++++++++++++++++++ website/src/react-native/css/react-native.css | 2 +- 5 files changed, 173 insertions(+), 31 deletions(-) rename docs/{GettingStarted.md => QuickStart-GettingStarted.md} (99%) create mode 100644 docs/SampleApplication-F8App.md rename docs/{Tutorial.md => SampleApplication-Movies.md} (86%) create mode 100644 docs/Tutorial-CoreComponents.md diff --git a/docs/GettingStarted.md b/docs/QuickStart-GettingStarted.md similarity index 99% rename from docs/GettingStarted.md rename to docs/QuickStart-GettingStarted.md index 2ad8bf30992f1d..516d6df046135a 100644 --- a/docs/GettingStarted.md +++ b/docs/QuickStart-GettingStarted.md @@ -1,10 +1,10 @@ --- -id: getting-started +id: quick-start-getting-started title: Getting Started layout: docs category: Quick Start permalink: docs/getting-started.html -next: tutorial +next: tutorial-core-components --- diff --git a/docs/SampleApplication-F8App.md b/docs/SampleApplication-F8App.md new file mode 100644 index 00000000000000..258906b4cb64dd --- /dev/null +++ b/docs/SampleApplication-F8App.md @@ -0,0 +1,8 @@ +--- +id: sample-application-f8 +title: F8 2016 App +layout: docs +category: Sample Applications +permalink: http://makeitopen.com/ +next: videos +--- diff --git a/docs/Tutorial.md b/docs/SampleApplication-Movies.md similarity index 86% rename from docs/Tutorial.md rename to docs/SampleApplication-Movies.md index d0cb7507fd9e9d..e35d4bbf6b0522 100644 --- a/docs/Tutorial.md +++ b/docs/SampleApplication-Movies.md @@ -1,25 +1,20 @@ --- -id: tutorial -title: Tutorial +id: sample-application-movies +title: Movie Fetcher layout: docs -category: Quick Start -permalink: docs/tutorial.html -next: videos +category: Sample Applications +permalink: docs/sample-application-movies.html +next: sample-application-f8 --- -## Preface - -This tutorial aims to get you up to speed with writing iOS and Android apps using React Native. If you're wondering what React Native is and why Facebook built it, this [blog post](https://code.facebook.com/posts/1014532261909640/react-native-bringing-modern-web-techniques-to-mobile/) explains that. - -We assume you have experience writing applications with React. If not, you can learn about it on the [React website](http://facebook.github.io/react/). - -### Building a real-world app +## Overview -This tutorial explains how to build a simple app to get you started. If you're looking for a more advanced tutorial on building a real-world app, check out [makeitopen.com](http://makeitopen.com/). +In this tutorial we'll be building a simple version of a Movies app that fetches 25 movies that are in theaters and displays them in a `ListView`. ## Setup -React Native requires the basic setup explained at [React Native Getting Started](docs/getting-started.html#content). +> This sample application requires the basic setup explained at +> [React Native Getting Started](docs/quick-start/getting-started.html#content). After installing these dependencies there are two simple commands to get a React Native project all set up for development. @@ -27,22 +22,17 @@ After installing these dependencies there are two simple commands to get a React react-native-cli is a command line interface that does the rest of the set up. It’s installable via npm. This will install `react-native` as a command in your terminal. You only ever need to do this once. -2. `react-native init AwesomeProject` - - This command fetches the React Native source code and dependencies and then creates a new Xcode project in `AwesomeProject/iOS/AwesomeProject.xcodeproj` and a gradle project in `AwesomeProject/android/app`. +2. `react-native init SampleAppMovies` - -## Overview - -In this tutorial we'll be building a simple version of the Movies app that fetches 25 movies that are in theaters and displays them in a ListView. + This command fetches the React Native source code and dependencies and then creates a new Xcode project in `SampleAppMovies/iOS/SampleAppMovies.xcodeproj` and a gradle project in `SampleAppMovies/android/app`. ### Starting the app on iOS -Open this new project (`AwesomeProject/ios/AwesomeProject.xcodeproj`) in Xcode and simply build and run it with `⌘+R`. Doing so will also start a Node server which enables live code reloading. With this you can see your changes by pressing `⌘+R` in the simulator rather than recompiling in Xcode. +Open this new project (`SampleAppMovies/ios/SampleAppMovies.xcodeproj`) in Xcode and simply build and run it with `⌘+R`. Doing so will also start a Node server which enables live code reloading. With this you can see your changes by pressing `⌘+R` in the simulator rather than recompiling in Xcode. ### Starting the app on Android -In your terminal navigate into the `AwesomeProject` and run: +In your terminal navigate into the `SampleAppMovies` and run: react-native run-android @@ -50,7 +40,11 @@ This will install the generated app on your emulator or device, as well as start ### Hello World -`react-native init` will generate an app with the name of your project, in this case AwesomeProject. This is a simple hello world app. For iOS, you can edit `index.ios.js` to make changes to the app and then press ⌘+R in the simulator to see the changes. For Android, you can edit `index.android.js` to make changes to the app and press `Reload JS` from the rage shake menu to see the changes. +`react-native init` will generate an app with the name of your project, in this case `SampleAppMovies`. This is a simple hello world app. For iOS, you can edit `index.ios.js` to make changes to the app and then press ⌘+R in the simulator to see the changes. For Android, you can edit `index.android.js` to make changes to the app and press `Reload JS` from the rage shake menu to see the changes. + +## Actual App + +Now that we have initialized our React Native project, we can begin creating our Movie application. ### Mocking data @@ -62,7 +56,6 @@ var MOCKED_MOVIES_DATA = [ ]; ``` - ### Render a movie We're going to render the title, year, and thumbnail for the movie. Since thumbnail is an Image component in React Native, add Image to the list of React imports below. @@ -210,7 +203,7 @@ Go ahead and press `⌘+R` / `Reload JS` and you'll see the updated view. Fetching data from Rotten Tomatoes's API isn't really relevant to learning React Native so feel free to breeze through this section. -Add the following constants to the top of the file (typically below the imports) to create the REQUEST_URL used to request data with. +Add the following constants to the top of the file (typically below the imports) to create the `REQUEST_URL`s used to request data with. ```javascript /** @@ -411,7 +404,7 @@ import { var REQUEST_URL = 'https://raw.githubusercontent.com/facebook/react-native/master/docs/MoviesExample.json'; -class AwesomeProject extends Component { +class SampleAppMovies extends Component { constructor(props) { super(props); this.state = { @@ -507,5 +500,5 @@ var styles = StyleSheet.create({ }, }); -AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject); +AppRegistry.registerComponent('SampleAppMovies', () => SampleAppMovies); ``` diff --git a/docs/Tutorial-CoreComponents.md b/docs/Tutorial-CoreComponents.md new file mode 100644 index 00000000000000..a117ce9fdca611 --- /dev/null +++ b/docs/Tutorial-CoreComponents.md @@ -0,0 +1,141 @@ +--- +id: tutorial-core-components +title: Core Components +layout: docs +category: Tutorials +permalink: docs/tutorial-core-components.html +next: sample-application-movies +--- + +Components are the building blocks for a React Native application. A React Native user interface (UI) is specified by declaring components, possibly nested, and then those components are mapped to the native UI on the targeted platform. + +React Native has a number of core components that are commonly used in applications, either on their own or combined to build new components. + +## Text + +The most basic component in React Native is the [`Text`](/react-native/docs/text.html#content) component. The `Text` component simply renders text. + +This example displays the `string` `"Hello"` on the device. + +```JavaScript +import React from 'react'; +import { AppRegistry, Text } from 'react-native'; + +const App = () => { + return ( + Hello World! + ); +} + +// App registration and rendering +AppRegistry.registerComponent('MyApp', () => App); +``` + +## Image + +The other basic React Native component is the [`Image`](/react-native/docs/image.html#content) component. Like `Text`, the `Image` component simply renders an image. + +> An `Image` is analogous to using `img` when building websites. + +The simplest way to render an image is to provide a source file to that image via the `source` attribute. + +This example displays a checkbox `Image` on the device. + +```JavaScript +import React from 'react'; +import { AppRegistry, Image } from 'react-native'; + +const App = () => { + return ( + + ); +} + +// App registration and rendering +AppRegistry.registerComponent('MyApp', () => App); +``` + +## View + +A [`View`](/react-native/docs/view.html#content) is the most basic building block for a React Native application. The `View` is an abstraction on top of the target platform's native equivalent, such as iOS's `UIView`. + +> A `View` is analogous to using a `div` for building websites. + +While basic components such as `Text` and `Image`, can be displayed without a `View`, this is not generally recommended since the `View` gives you the control for styling and layout of those components. + +This example creates a `View` that aligns the `string` `Hello` in the top center of the device, something which could not be done with a `Text` component alone (i.e., a `Text` component without a `View` would place the `string` in a fixed location in the upper corner): + +```JavaScript +import React from 'react'; +import { AppRegistry, Text, View } from 'react-native'; + +const App = () => { + return ( + + Hello! + + ); +} + +// App registration and rendering +AppRegistry.registerComponent('MyApp', () => App); +``` + +## TextInput + +Direct text-based user input is a foundation for many apps. Writing a post or comment on a page is a canonical example of this. [`TextInput`](/react-native/docs/textinput.html#content) is a basic component that allows the user to enter text. + +This example creates a simple `TextInput` box with the `string` `Hello` as the placeholder when the `TextInput` is empty. + +```JavaScript +import React, { AppRegistry, TextInput, View } from 'react-native' + +const App = () => { + return ( + + + + ); +} + +// App registration and rendering +AppRegistry.registerComponent('MyApp', () => App); +``` + +## ListView + +On mobile devices, lists are a core element in many applications. The [`ListView`](/react-native/docs/listview.html#content) component is a special type of [`View`](/react-native/docs/tutorials/core-components.html#view) that displays a vertically scrolling list of changing data. + +The `ListView` component requires two properties, `dataSource` and `renderRow`. `dataSource` is the actual source of information that will be part of the list. `renderRow` takes the data and returns a renderable component to display. + +This example creates a simple `ListView` of hardcoded data. It first initializes the `datasource` that will be used to populate the `ListView`. Then it renders that `ListView` with that data. + +> A `rowHasChanged` function is required to use `ListView`. Here we just say a row has changed if the row we are on is not the same as the previous row. + +```JavaScript +import React from 'react'; +import { AppRegistry, Text, View, ListView} from 'react-native'; + +var SimpleList = React.createClass({ + // Initialize the hardcoded data + getInitialState: function() { + var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); + return { + dataSource: ds.cloneWithRows(['John', 'Joel', 'James', 'Jimmy', 'Jackson', 'Jillian', 'Julie']) + }; + }, + render: function() { + return ( + + {rowData}} + /> + + ); + } +}); + +// App registration and rendering +AppRegistry.registerComponent('MyApp', () => SimpleList); +``` diff --git a/website/src/react-native/css/react-native.css b/website/src/react-native/css/react-native.css index b657d2ab80fce3..8ba8d434a7e106 100644 --- a/website/src/react-native/css/react-native.css +++ b/website/src/react-native/css/react-native.css @@ -670,7 +670,7 @@ h2 { .documentationContent blockquote { padding: 15px 30px 15px 15px; margin: 20px 0; - background-color: rgba(204, 122, 111, 0.1); + background-color: rgba(248, 245, 236, 0.1); border-left: 5px solid rgba(191, 87, 73, 0.2); } From f203c5d78c7ed62b3125fb408065f704393fec01 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Tue, 17 May 2016 20:55:28 -0700 Subject: [PATCH 014/843] Add "Open file" button to elements inspector Summary: Depends on #6351 Now you can open file directly from Elements Inspector! Credit for the original implementation goes to jaredly ![zcichu7kem](https://cloud.githubusercontent.com/assets/192222/14573876/cb100f6e-030c-11e6-925f-6a6dff510145.gif) **Test plan** Made sure it doesn't crash the app with or without #6351 (i.e. can be merged safely before #6351 gets in). Closes https://github.com/facebook/react-native/pull/7005 Differential Revision: D3313714 Pulled By: frantic fbshipit-source-id: 3b80abd3e81a0db5ca5136e2d2c94c775fa04f3a --- Libraries/Inspector/ElementProperties.js | 53 +++++++++++++++++++++++- Libraries/Inspector/Inspector.js | 4 ++ Libraries/Inspector/InspectorPanel.js | 1 + 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/Libraries/Inspector/ElementProperties.js b/Libraries/Inspector/ElementProperties.js index 1e808d0d172905..bbbf248407c304 100644 --- a/Libraries/Inspector/ElementProperties.js +++ b/Libraries/Inspector/ElementProperties.js @@ -20,6 +20,8 @@ var Text = require('Text'); var TouchableHighlight = require('TouchableHighlight'); var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); var View = require('View'); +var {SourceCode} = require('NativeModules'); +var {fetch} = require('fetch'); var flattenStyle = require('flattenStyle'); var mapWithSeparator = require('mapWithSeparator'); @@ -32,11 +34,31 @@ var ElementProperties = React.createClass({ PropTypes.array, PropTypes.number, ]), + source: PropTypes.shape({ + fileName: PropTypes.string, + lineNumber: PropTypes.number, + }), }, render: function() { var style = flattenStyle(this.props.style); var selection = this.props.selection; + var openFileButton; + var source = this.props.source; + var {fileName, lineNumber} = source || {}; + if (fileName && lineNumber) { + var parts = fileName.split('/'); + var fileNameShort = parts[parts.length - 1]; + openFileButton = ( + + + {fileNameShort}:{lineNumber} + + + ); + } // Without the `TouchableWithoutFeedback`, taps on this inspector pane // would change the inspected element to whatever is under the inspector return ( @@ -63,13 +85,26 @@ var ElementProperties = React.createClass({ )} - + + + {openFileButton} + ); - } + }, + + _openFile: function(fileName: string, lineNumber: number) { + var match = SourceCode.scriptURL && SourceCode.scriptURL.match(/^https?:\/\/.*?\//); + var baseURL = match ? match[0] : 'http://localhost:8081/'; + + fetch(baseURL + 'open-stack-frame', { + method: 'POST', + body: JSON.stringify({file: fileName, lineNumber}), + }); + }, }); var styles = StyleSheet.create({ @@ -101,6 +136,9 @@ var styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'space-between', }, + col: { + flex: 1, + }, info: { padding: 10, }, @@ -108,6 +146,17 @@ var styles = StyleSheet.create({ color: 'white', fontSize: 9, }, + openButton: { + padding: 10, + backgroundColor: '#000', + marginVertical: 5, + marginRight: 5, + borderRadius: 2, + }, + openButtonTitle: { + color: 'white', + fontSize: 8, + } }); module.exports = ElementProperties; diff --git a/Libraries/Inspector/Inspector.js b/Libraries/Inspector/Inspector.js index a415f1f3b58c74..d1ee7c93838190 100644 --- a/Libraries/Inspector/Inspector.js +++ b/Libraries/Inspector/Inspector.js @@ -129,11 +129,13 @@ class Inspector extends React.Component { // if we inspect a stateless component we can't use the getPublicInstance method // therefore we use the internal _instance property directly. var publicInstance = instance['_instance'] || {}; + var source = instance._currentElement && instance._currentElement._source; UIManager.measure(instance.getNativeNode(), (x, y, width, height, left, top) => { this.setState({ inspected: { frame: {left, top, width, height}, style: publicInstance.props ? publicInstance.props.style : {}, + source, }, selection: i, }); @@ -149,6 +151,7 @@ class Inspector extends React.Component { // therefore we use the internal _instance property directly. var publicInstance = instance._instance || {}; var props = publicInstance.props || {}; + var source = instance._currentElement && instance._currentElement._source; this.setState({ panelPos: pointerY > Dimensions.get('window').height / 2 ? 'top' : 'bottom', selection: hierarchy.length - 1, @@ -156,6 +159,7 @@ class Inspector extends React.Component { inspected: { style: props.style || {}, frame, + source, }, }); } diff --git a/Libraries/Inspector/InspectorPanel.js b/Libraries/Inspector/InspectorPanel.js index 0b62999d11e745..9078a8e46673e1 100644 --- a/Libraries/Inspector/InspectorPanel.js +++ b/Libraries/Inspector/InspectorPanel.js @@ -41,6 +41,7 @@ class InspectorPanel extends React.Component { Date: Wed, 18 May 2016 04:15:53 -0700 Subject: [PATCH 015/843] =?UTF-8?q?Add=20QQ=E9=9F=B3=E4=B9=90=20=E5=85=A8?= =?UTF-8?q?=E6=B0=91K=E6=AD=8C=20QQ=E7=A9=BA=E9=97=B4=20QQ=20to=20showcase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Add QQ音乐 全民K歌 QQ空间 QQ to the showcase page Closes https://github.com/facebook/react-native/pull/7582 Differential Revision: D3316209 fbshipit-source-id: 47e5ce7ec91fb5f4ff747e4ea9ea77476a49fdbc --- website/src/react-native/showcase.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/website/src/react-native/showcase.js b/website/src/react-native/showcase.js index 30be4b22bda723..40130b28a35959 100644 --- a/website/src/react-native/showcase.js +++ b/website/src/react-native/showcase.js @@ -688,6 +688,24 @@ var apps = [ link: 'https://itunes.apple.com/us/app/project-september/id1074075331?ls=1&mt=8', author: 'ProjectSeptember.com', }, + { + name: 'QQ', + icon: 'http://pp.myapp.com/ma_icon/0/icon_6633_1461768893/96', + link: 'http://android.myapp.com/myapp/detail.htm?apkName=com.tencent.mobileqq', + author: '邱俊', + }, + { + name: 'QQ空间', + icon: 'http://pp.myapp.com/ma_icon/0/icon_9959_1460036593/96', + link: 'http://android.myapp.com/myapp/detail.htm?apkName=com.qzone', + author: '薛丰', + }, + { + name: 'QQ音乐', + icon: 'http://pp.myapp.com/ma_icon/0/icon_6259_1462429453/96', + link: 'http://android.myapp.com/myapp/detail.htm?apkName=com.tencent.qqmusic', + author: '石玉磊', + }, { name: 'Raindrop.io', icon: 'http://a5.mzstatic.com/us/r30/Purple3/v4/f0/a4/57/f0a4574e-4a59-033f-05ff-5c421f0a0b00/icon175x175.png', @@ -984,6 +1002,12 @@ var apps = [ linkAppStore: 'https://itunes.apple.com/cn/app/e-xiao-xian/id1092025196', author: 'Eleme', }, + { + name: '全民K歌', + icon: 'http://pp.myapp.com/ma_icon/0/icon_10966186_1460087288/96', + link: 'http://android.myapp.com/myapp/detail.htm?apkName=com.tencent.karaoke', + author: '石玉磊', + }, ]; var AppList = React.createClass({ From 921d0de2e8f572122651e1e239d4aa03f1fc1580 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Wed, 18 May 2016 04:55:50 -0700 Subject: [PATCH 016/843] Fixes NetowrkingModule construction with interceptors Reviewed By: andreicoman11 Differential Revision: D3316229 fbshipit-source-id: 0dc5cce68b6649ebd2ff44c313ab94a0f5a06f66 --- .../react/modules/network/NetworkingModule.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java index 8a67aa10401b7e..93ab7f7b0b5848 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java @@ -69,12 +69,16 @@ public final class NetworkingModule extends ReactContextBaseJavaModule { OkHttpClient client, @Nullable List networkInterceptorCreators) { super(reactContext); - mClient = client; + if (networkInterceptorCreators != null) { + OkHttpClient.Builder clientBuilder = client.newBuilder(); for (NetworkInterceptorCreator networkInterceptorCreator : networkInterceptorCreators) { - mClient.networkInterceptors().add(networkInterceptorCreator.create()); + clientBuilder.addNetworkInterceptor(networkInterceptorCreator.create()); } + client = clientBuilder.build(); } + mClient = client; + OkHttpClientProvider.replaceOkHttpClient(client); mCookieHandler = new ForwardingCookieHandler(reactContext); mCookieJarContainer = (CookieJarContainer) mClient.cookieJar(); mShuttingDown = false; @@ -87,7 +91,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule { * caller does not provide one explicitly * @param client the {@link OkHttpClient} to be used for networking */ - public NetworkingModule( + /* package */ NetworkingModule( ReactApplicationContext context, @Nullable String defaultUserAgent, OkHttpClient client) { @@ -121,10 +125,6 @@ public NetworkingModule(ReactApplicationContext context, String defaultUserAgent this(context, defaultUserAgent, OkHttpClientProvider.getOkHttpClient(), null); } - public NetworkingModule(ReactApplicationContext reactContext, OkHttpClient client) { - this(reactContext, null, client, null); - } - @Override public void initialize() { mCookieJarContainer.setCookieJar(new JavaNetCookieJar(mCookieHandler)); From e3bbd300f6eb93c6734976ae6de1219793e1b5ff Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Wed, 18 May 2016 07:23:17 -0700 Subject: [PATCH 017/843] Update upgrade command with 0.26 breaking changes Summary: Generally it feels that at some point we will just integrate `rnpm upgrade` directly here, but this is a super temporary quick workaround for the people upgrading to 0.26. We might want to change it later to 0.27 and update this message in case we have any other stuff to upgrade. ![screen shot 2016-05-18 at 14 28 47](https://cloud.githubusercontent.com/assets/2464966/15358460/df3b1b66-1d04-11e6-825c-fa18fa96ee6e.png) It is supposed to work for 0.26.0, 0.26.1 and all the others up to 0.27. Closes https://github.com/facebook/react-native/pull/7614 Differential Revision: D3316420 fbshipit-source-id: 3861b4228ee878464e18ba3de1f3e0c12d5f30d1 --- local-cli/upgrade/upgrade.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/local-cli/upgrade/upgrade.js b/local-cli/upgrade/upgrade.js index a65828b4af7d4e..03d17d7fba4cd2 100644 --- a/local-cli/upgrade/upgrade.js +++ b/local-cli/upgrade/upgrade.js @@ -57,6 +57,17 @@ module.exports = function upgrade(args, config) { ); return Promise.resolve(); } + + if (semver.satisfies(v, '~0.26.0')) { + console.log( + chalk.yellow( + 'React Native 0.26 introduced some breaking changes to the native files on iOS. You can\n' + + 'perform them manually by checking the release notes or use \'rnpm\' to do it automatically.\n' + + 'Just run:\n' + + '\'npm install -g rnpm && npm install rnpm-plugin-upgrade@0.26 --save-dev\', then run \'rnpm upgrade\'' + ) + ); + } } else { console.log( chalk.yellow( From 42e517b7a3c00ef9e16b6586a9dd51eb84a468e0 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Wed, 18 May 2016 08:30:51 -0700 Subject: [PATCH 018/843] Added timeouts to fs operations in packager Reviewed By: davidaurelio Differential Revision: D3316555 fbshipit-source-id: edf6e08569b6cb434219f4460367eec0827530fd --- .../react-packager/src/AssetServer/index.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packager/react-packager/src/AssetServer/index.js b/packager/react-packager/src/AssetServer/index.js index 2b25218c3a45ab..1a7bb75efe7394 100644 --- a/packager/react-packager/src/AssetServer/index.js +++ b/packager/react-packager/src/AssetServer/index.js @@ -16,9 +16,21 @@ const fs = require('fs'); const getAssetDataFromName = require('node-haste').getAssetDataFromName; const path = require('path'); -const stat = Promise.denodeify(fs.stat); -const readDir = Promise.denodeify(fs.readdir); -const readFile = Promise.denodeify(fs.readFile); +const createTimeoutPromise = (timeout) => new Promise((resolve, reject) => { + setTimeout(reject, timeout, 'fs operation timeout'); +}); +function timeoutableDenodeify(fsFunc, timeout) { + return function raceWrapper(...args) { + return new Promise.race([ + createTimeoutPromise(timeout), + Promise.denodeify(fsFunc).apply(this, args) + ]); + }; +} + +const stat = timeoutableDenodeify(fs.stat, 5000); +const readDir = timeoutableDenodeify(fs.readdir, 5000); +const readFile = timeoutableDenodeify(fs.readFile, 5000); const validateOpts = declareOpts({ projectRoots: { From 18e1b1f1976436684c0d5906b5f335eb15fc9413 Mon Sep 17 00:00:00 2001 From: Joel Marcey Date: Wed, 18 May 2016 09:10:30 -0700 Subject: [PATCH 019/843] Best effort selection of target platform and dev environment Summary: Right now, if you do a search and select a document in Getting Started, it will always default to iOS/Mac. This adds a bit of JavaScript to do a best effort selection based on the hashtags of the headers. If a header is associated with multiple environments (e.g., Android Studio), we just choose the first one. So it is not 100% perfect, but it is decent. ref: https://github.com/facebook/react-native/issues/7574 ** Test Plan ** Test locally by adding hash tags to the end of a doc URL and ensured that the toggler had the right selection. e.g., `http://localhost:8079/react-native/docs/getting-started.html#chocolatey` had `Android` and `Windows` chosen. Closes https://github.com/facebook/react-native/pull/7608 Differential Revision: D3316802 Pulled By: JoelMarcey fbshipit-source-id: 6e94d76725fb97b19b3708ddee8fba5df9350cdd --- docs/QuickStart-GettingStarted.md | 57 ++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/docs/QuickStart-GettingStarted.md b/docs/QuickStart-GettingStarted.md index 516d6df046135a..b645dc0537cf38 100644 --- a/docs/QuickStart-GettingStarted.md +++ b/docs/QuickStart-GettingStarted.md @@ -807,8 +807,57 @@ function display(type, value) { container.className.replace(RegExp('display-' + type + '-[a-z]+ ?'), ''); event && event.preventDefault(); } -var isMac = navigator.platform === 'MacIntel'; -var isWindows = navigator.platform === 'Win32'; -display('os', isMac ? 'mac' : (isWindows ? 'windows' : 'linux')); -display('platform', isMac ? 'ios' : 'android'); + +// If we are coming to the page with a hash in it (i.e. from a search, for example), try to get +// us as close as possible to the correct platform and dev os using the hashtag and block walk up. +var foundHash = false; +if (window.location.hash !== '' && window.location.hash !== 'content') { // content is default + var hashLinks = document.querySelectorAll('a.hash-link'); + for (var i = 0; i < hashLinks.length && !foundHash; ++i) { + if (hashLinks[i].hash === window.location.hash) { + var parent = hashLinks[i].parentElement; + while (parent) { + if (parent.tagName === 'BLOCK') { + var devOS = null; + var targetPlatform = null; + // Could be more than one target os and dev platform, but just choose some sort of order + // of priority here. + + // Dev OS + if (parent.className.indexOf('mac') > -1) { + devOS = 'mac'; + } else if (parent.className.indexOf('linux') > -1) { + devOS = 'linux'; + } else if (parent.className.indexOf('windows') > -1) { + devOS = 'windows'; + } else { + break; // assume we don't have anything. + } + + // Target Platform + if (parent.className.indexOf('ios') > -1) { + targetPlatform = 'ios'; + } else if (parent.className.indexOf('android') > -1) { + targetPlatform = 'android'; + } else { + break; // assume we don't have anything. + } + // We would have broken out if both targetPlatform and devOS hadn't been filled. + display('os', devOS); + display('platform', targetPlatform); + foundHash = true; + break; + } + parent = parent.parentElement; + } + } + } +} +// Do the default if there is no matching hash +if (!foundHash) { + var isMac = navigator.platform === 'MacIntel'; + var isWindows = navigator.platform === 'Win32'; + display('os', isMac ? 'mac' : (isWindows ? 'windows' : 'linux')); + display('platform', isMac ? 'ios' : 'android'); +} From 858643dbdf7c8e84714e2f703419487f45eac5a1 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Wed, 18 May 2016 12:32:20 -0700 Subject: [PATCH 020/843] Add transform-react-jsx-source to react-native preset Summary: Putting this up as request for comments. The PR adds [transform-react-jsx-source](https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-source) to the list of plugins that come by default with the `react-native` preset. It will enable the use of a bunch of really cool tooling around JSX, however those are generally useful only in development mode. Is changing `react-native` preset the right thing to do in this case? Is there a way to enable this transform only in DEV? Should I add this somewhere else? Closes https://github.com/facebook/react-native/pull/6351 Differential Revision: D3302906 Pulled By: frantic fbshipit-source-id: 012d3a4142168f9f90d30d1686115d4dc3996eb9 --- babel-preset/configs/main.js | 5 +++++ babel-preset/package.json | 3 ++- babel-preset/plugins.js | 1 + package.json | 2 +- packager/transformer.js | 25 ++++++++++++++++--------- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/babel-preset/configs/main.js b/babel-preset/configs/main.js index 3e8b6616711a45..f56d7ed689d391 100644 --- a/babel-preset/configs/main.js +++ b/babel-preset/configs/main.js @@ -39,6 +39,11 @@ module.exports = { ['transform-es2015-for-of', { loose: true }], require('../transforms/transform-symbol-member'), ]), + env: { + development: { + plugins: resolvePlugins(['transform-react-jsx-source']), + }, + }, retainLines: true, sourceMaps: false, }; diff --git a/babel-preset/package.json b/babel-preset/package.json index 3ea605804f9af6..6c296dc8a2b745 100644 --- a/babel-preset/package.json +++ b/babel-preset/package.json @@ -1,6 +1,6 @@ { "name": "babel-preset-react-native", - "version": "1.7.0", + "version": "1.8.0", "description": "Babel preset for React Native applications", "main": "index.js", "repository": "https://github.com/facebook/react-native/tree/master/babel-preset", @@ -39,6 +39,7 @@ "babel-plugin-transform-object-assign": "^6.5.0", "babel-plugin-transform-object-rest-spread": "^6.5.0", "babel-plugin-transform-react-display-name": "^6.5.0", + "babel-plugin-transform-react-jsx-source": "^6.5.0", "babel-plugin-transform-react-jsx": "^6.5.0", "babel-plugin-transform-regenerator": "^6.5.0", "react-transform-hmr": "^1.0.4" diff --git a/babel-preset/plugins.js b/babel-preset/plugins.js index e52fdfe8c8afba..ceecb1889272a9 100644 --- a/babel-preset/plugins.js +++ b/babel-preset/plugins.js @@ -30,6 +30,7 @@ module.exports = { 'babel-plugin-transform-object-assign': require('babel-plugin-transform-object-assign'), 'babel-plugin-transform-object-rest-spread': require('babel-plugin-transform-object-rest-spread'), 'babel-plugin-transform-react-display-name': require('babel-plugin-transform-react-display-name'), + 'babel-plugin-transform-react-jsx-source': require('babel-plugin-transform-react-jsx-source'), 'babel-plugin-transform-react-jsx': require('babel-plugin-transform-react-jsx'), 'babel-plugin-transform-regenerator': require('babel-plugin-transform-regenerator'), 'babel-plugin-transform-es2015-for-of': require('babel-plugin-transform-es2015-for-of'), diff --git a/package.json b/package.json index e8dbf2ac00979e..6ecdcb28406df2 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "babel-core": "^6.6.4", "babel-plugin-external-helpers": "^6.5.0", "babel-polyfill": "^6.6.1", - "babel-preset-react-native": "^1.7.0", + "babel-preset-react-native": "^1.8.0", "babel-register": "^6.6.0", "babel-types": "^6.6.4", "babylon": "^6.6.4", diff --git a/packager/transformer.js b/packager/transformer.js index 18da16f28d4126..faf01669776c7c 100644 --- a/packager/transformer.js +++ b/packager/transformer.js @@ -100,15 +100,22 @@ function buildBabelConfig(filename, options) { function transform(src, filename, options) { options = options || {}; - const babelConfig = buildBabelConfig(filename, options); - const result = babel.transform(src, babelConfig); - - return { - ast: result.ast, - code: result.code, - map: result.map, - filename: filename, - }; + const OLD_BABEL_ENV = process.env.BABEL_ENV; + process.env.BABEL_ENV = options.dev ? 'development' : 'production'; + + try { + const babelConfig = buildBabelConfig(filename, options); + const result = babel.transform(src, babelConfig); + + return { + ast: result.ast, + code: result.code, + map: result.map, + filename: filename, + }; + } finally { + process.env.BABEL_ENV = OLD_BABEL_ENV; + } } module.exports = function(data, callback) { From 7f1346bfdd94c9fa85e5e1587b0001d0b23bf3e5 Mon Sep 17 00:00:00 2001 From: Fred Liu Date: Wed, 18 May 2016 12:33:52 -0700 Subject: [PATCH 021/843] Top align quick action buttons Summary: Top align the button icons Reviewed By: fkgozali Differential Revision: D3317805 fbshipit-source-id: b385f9f4e41db1253bb4d523cb07093c63e39da0 --- .../Experimental/SwipeableRow/SwipeableQuickActionButton.js | 1 - 1 file changed, 1 deletion(-) diff --git a/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js b/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js index 686c45d2278402..f273a19a9a1d63 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js @@ -74,7 +74,6 @@ const styles = StyleSheet.create({ alignItems: 'center', flex: 1, flexDirection: 'column', - justifyContent: 'center', width: 76, }, image: { From 779314a413851e7ae3d9ff483ba6421f1eb33630 Mon Sep 17 00:00:00 2001 From: Marc Horowitz Date: Wed, 18 May 2016 12:46:01 -0700 Subject: [PATCH 022/843] Implement incremental module requires Differential Revision: D3234844 fbshipit-source-id: 24615528ad6a049aad7c2dbb7ce55e8b034c79e7 --- ReactCommon/bridge/Bridge.cpp | 277 -------------- ReactCommon/bridge/Executor.h | 53 ++- ReactCommon/bridge/Instance.cpp | 118 +----- ReactCommon/bridge/Instance.h | 17 +- ReactCommon/bridge/JSCExecutor.cpp | 74 +++- ReactCommon/bridge/JSCExecutor.h | 24 +- ReactCommon/bridge/ModuleRegistry.cpp | 99 +++-- ReactCommon/bridge/ModuleRegistry.h | 7 +- ReactCommon/bridge/NativeModule.h | 5 - ReactCommon/bridge/NativeToJsBridge.cpp | 347 ++++++++++++++++++ .../bridge/{Bridge.h => NativeToJsBridge.h} | 90 ++--- 11 files changed, 591 insertions(+), 520 deletions(-) delete mode 100644 ReactCommon/bridge/Bridge.cpp create mode 100644 ReactCommon/bridge/NativeToJsBridge.cpp rename ReactCommon/bridge/{Bridge.h => NativeToJsBridge.h} (64%) diff --git a/ReactCommon/bridge/Bridge.cpp b/ReactCommon/bridge/Bridge.cpp deleted file mode 100644 index 9437657bd4eced..00000000000000 --- a/ReactCommon/bridge/Bridge.cpp +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright 2004-present Facebook. All Rights Reserved. - -#include "Bridge.h" - -#ifdef WITH_FBSYSTRACE -#include -using fbsystrace::FbSystraceAsyncFlow; -#endif - -#include -#include -#include - -#include "Platform.h" -#include "SystraceSection.h" - -namespace facebook { -namespace react { - -Bridge::Bridge( - JSExecutorFactory* jsExecutorFactory, - std::shared_ptr jsQueue, - std::unique_ptr executorTokenFactory, - std::unique_ptr callback) : - m_callback(std::move(callback)), - m_destroyed(std::make_shared(false)), - m_executorTokenFactory(std::move(executorTokenFactory)) { - std::unique_ptr mainExecutor = jsExecutorFactory->createJSExecutor(this, jsQueue); - // cached to avoid locked map lookup in the common case - m_mainExecutor = mainExecutor.get(); - m_mainExecutorToken = folly::make_unique(registerExecutor( - std::move(mainExecutor), jsQueue)); -} - -// This must be called on the same thread on which the constructor was called. -Bridge::~Bridge() { - CHECK(*m_destroyed) << "Bridge::destroy() must be called before deallocating the Bridge!"; -} - -void Bridge::loadApplicationScript(std::unique_ptr script, - std::string sourceURL) { - // TODO(t11144533): Add assert that we are on the correct thread - m_mainExecutor->loadApplicationScript(std::move(script), std::move(sourceURL)); -} - -void Bridge::loadApplicationUnbundle( - std::unique_ptr unbundle, - std::unique_ptr startupScript, - std::string startupScriptSourceURL) { - runOnExecutorQueue( - *m_mainExecutorToken, - [unbundle=folly::makeMoveWrapper(std::move(unbundle)), - startupScript=folly::makeMoveWrapper(std::move(startupScript)), - startupScriptSourceURL=std::move(startupScriptSourceURL)] - (JSExecutor* executor) mutable { - - executor->setJSModulesUnbundle(unbundle.move()); - executor->loadApplicationScript(std::move(*startupScript), - std::move(startupScriptSourceURL)); - }); -} - -void Bridge::callFunction( - ExecutorToken executorToken, - const std::string& moduleId, - const std::string& methodId, - const folly::dynamic& arguments, - const std::string& tracingName) { - #ifdef WITH_FBSYSTRACE - int systraceCookie = m_systraceCookie++; - FbSystraceAsyncFlow::begin( - TRACE_TAG_REACT_CXX_BRIDGE, - tracingName.c_str(), - systraceCookie); - #endif - - runOnExecutorQueue(executorToken, [moduleId, methodId, arguments, tracingName, systraceCookie] (JSExecutor* executor) { - #ifdef WITH_FBSYSTRACE - FbSystraceAsyncFlow::end( - TRACE_TAG_REACT_CXX_BRIDGE, - tracingName.c_str(), - systraceCookie); - SystraceSection s(tracingName.c_str()); - #endif - - // This is safe because we are running on the executor's thread: it won't - // destruct until after it's been unregistered (which we check above) and - // that will happen on this thread - executor->callFunction(moduleId, methodId, arguments); - }); -} - -void Bridge::invokeCallback(ExecutorToken executorToken, const double callbackId, const folly::dynamic& arguments) { - #ifdef WITH_FBSYSTRACE - int systraceCookie = m_systraceCookie++; - FbSystraceAsyncFlow::begin( - TRACE_TAG_REACT_CXX_BRIDGE, - "", - systraceCookie); - #endif - - runOnExecutorQueue(executorToken, [callbackId, arguments, systraceCookie] (JSExecutor* executor) { - #ifdef WITH_FBSYSTRACE - FbSystraceAsyncFlow::end( - TRACE_TAG_REACT_CXX_BRIDGE, - "", - systraceCookie); - SystraceSection s("Bridge.invokeCallback"); - #endif - - executor->invokeCallback(callbackId, arguments); - }); -} - -void Bridge::setGlobalVariable(std::string propName, - std::unique_ptr jsonValue) { - runOnExecutorQueue( - *m_mainExecutorToken, - [propName=std::move(propName), jsonValue=folly::makeMoveWrapper(std::move(jsonValue))] - (JSExecutor* executor) mutable { - executor->setGlobalVariable(propName, jsonValue.move()); - }); -} - -void* Bridge::getJavaScriptContext() { - // TODO(cjhopman): this seems unsafe unless we require that it is only called on the main js queue. - return m_mainExecutor->getJavaScriptContext(); -} - -bool Bridge::supportsProfiling() { - // Intentionally doesn't post to jsqueue. supportsProfiling() can be called from any thread. - return m_mainExecutor->supportsProfiling(); -} - -void Bridge::startProfiler(const std::string& title) { - runOnExecutorQueue(*m_mainExecutorToken, [=] (JSExecutor* executor) { - executor->startProfiler(title); - }); -} - -void Bridge::stopProfiler(const std::string& title, const std::string& filename) { - runOnExecutorQueue(*m_mainExecutorToken, [=] (JSExecutor* executor) { - executor->stopProfiler(title, filename); - }); -} - -void Bridge::handleMemoryPressureModerate() { - runOnExecutorQueue(*m_mainExecutorToken, [=] (JSExecutor* executor) { - executor->handleMemoryPressureModerate(); - }); -} - -void Bridge::handleMemoryPressureCritical() { - runOnExecutorQueue(*m_mainExecutorToken, [=] (JSExecutor* executor) { - executor->handleMemoryPressureCritical(); - }); -} - -void Bridge::callNativeModules(JSExecutor& executor, const std::string& callJSON, bool isEndOfBatch) { - // This is called by the executor and thus runs on the executor's own queue. - // This means that the executor has not yet been unregistered (and we are - // guaranteed to be able to get the token). - m_callback->onCallNativeModules(getTokenForExecutor(executor), callJSON, isEndOfBatch); -} - -MethodCallResult Bridge::callSerializableNativeHook(unsigned int moduleId, unsigned int methodId, const std::string& argsJSON) { - return m_callback->callSerializableNativeHook(*m_mainExecutorToken, moduleId, methodId, folly::parseJson(argsJSON)); -} - -ExecutorToken Bridge::getMainExecutorToken() const { - return *m_mainExecutorToken.get(); -} - -ExecutorToken Bridge::registerExecutor( - std::unique_ptr executor, - std::shared_ptr messageQueueThread) { - auto token = m_executorTokenFactory->createExecutorToken(); - - std::lock_guard registrationGuard(m_registrationMutex); - - CHECK(m_executorTokenMap.find(executor.get()) == m_executorTokenMap.end()) - << "Trying to register an already registered executor!"; - - m_executorTokenMap.emplace(executor.get(), token); - m_executorMap.emplace( - token, - folly::make_unique(std::move(executor), std::move(messageQueueThread))); - - return token; -} - -std::unique_ptr Bridge::unregisterExecutor(ExecutorToken executorToken) { - std::unique_ptr executor; - - { - std::lock_guard registrationGuard(m_registrationMutex); - - auto it = m_executorMap.find(executorToken); - CHECK(it != m_executorMap.end()) - << "Trying to unregister an executor that was never registered!"; - - executor = std::move(it->second->executor_); - m_executorMap.erase(it); - m_executorTokenMap.erase(executor.get()); - } - - m_callback->onExecutorUnregistered(executorToken); - - return executor; -} - -MessageQueueThread* Bridge::getMessageQueueThread(const ExecutorToken& executorToken) { - std::lock_guard registrationGuard(m_registrationMutex); - auto it = m_executorMap.find(executorToken); - if (it == m_executorMap.end()) { - return nullptr; - } - return it->second->messageQueueThread_.get(); -} - -JSExecutor* Bridge::getExecutor(const ExecutorToken& executorToken) { - std::lock_guard registrationGuard(m_registrationMutex); - auto it = m_executorMap.find(executorToken); - if (it == m_executorMap.end()) { - return nullptr; - } - return it->second->executor_.get(); -} - -ExecutorToken Bridge::getTokenForExecutor(JSExecutor& executor) { - std::lock_guard registrationGuard(m_registrationMutex); - return m_executorTokenMap.at(&executor); -} - -void Bridge::destroy() { - auto executorMessageQueueThread = getMessageQueueThread(*m_mainExecutorToken); - executorMessageQueueThread->runOnQueueSync([this, &executorMessageQueueThread] { - executorMessageQueueThread->quitSynchronous(); - *m_destroyed = true; - m_mainExecutor = nullptr; - std::unique_ptr mainExecutor = unregisterExecutor(*m_mainExecutorToken); - mainExecutor->destroy(); - }); -} - -void Bridge::runOnExecutorQueue(ExecutorToken executorToken, std::function task) { - if (*m_destroyed) { - return; - } - - auto executorMessageQueueThread = getMessageQueueThread(executorToken); - if (executorMessageQueueThread == nullptr) { - LOG(WARNING) << "Dropping JS action for executor that has been unregistered..."; - return; - } - - std::shared_ptr isDestroyed = m_destroyed; - executorMessageQueueThread->runOnQueue([this, isDestroyed, executorToken, task=std::move(task)] { - if (*isDestroyed) { - return; - } - - JSExecutor *executor = getExecutor(executorToken); - if (executor == nullptr) { - LOG(WARNING) << "Dropping JS call for executor that has been unregistered..."; - return; - } - - // The executor is guaranteed to be valid for the duration of the task because: - // 1. the executor is only destroyed after it is unregistered - // 2. the executor is unregistered on this queue - // 3. we just confirmed that the executor hasn't been unregistered above - task(executor); - }); -} - -} } diff --git a/ReactCommon/bridge/Executor.h b/ReactCommon/bridge/Executor.h index d005701be93f45..08af931d511cff 100644 --- a/ReactCommon/bridge/Executor.h +++ b/ReactCommon/bridge/Executor.h @@ -7,26 +7,45 @@ #include #include -#include "JSModulesUnbundle.h" - -namespace folly { - -struct dynamic; +#include -} +#include "JSModulesUnbundle.h" namespace facebook { namespace react { -class Bridge; class JSExecutor; class MessageQueueThread; +struct MethodCallResult { + folly::dynamic result; + bool isUndefined; +}; + +// This interface describes the delegate interface required by +// Executor implementations to call from JS into native code. +class ExecutorDelegate { + public: + virtual ~ExecutorDelegate() {} + + virtual void registerExecutor(std::unique_ptr executor, + std::shared_ptr queue) = 0; + virtual std::unique_ptr unregisterExecutor(JSExecutor& executor) = 0; + + virtual std::vector moduleNames() = 0; + virtual folly::dynamic getModuleConfig(const std::string& name) = 0; + virtual void callNativeModules( + JSExecutor& executor, std::string callJSON, bool isEndOfBatch) = 0; + virtual MethodCallResult callSerializableNativeHook( + JSExecutor& executor, unsigned int moduleId, unsigned int methodId, folly::dynamic&& args) = 0; +}; + class JSExecutorFactory { public: virtual std::unique_ptr createJSExecutor( - Bridge *bridge, std::shared_ptr jsQueue) = 0; - virtual ~JSExecutorFactory() {}; + std::shared_ptr delegate, + std::shared_ptr jsQueue) = 0; + virtual ~JSExecutorFactory() {} }; // JSExecutor functions sometimes take large strings, on the order of @@ -143,18 +162,18 @@ class JSExecutor { std::unique_ptr jsonValue) = 0; virtual void* getJavaScriptContext() { return nullptr; - }; + } virtual bool supportsProfiling() { return false; - }; - virtual void startProfiler(const std::string &titleString) {}; - virtual void stopProfiler(const std::string &titleString, const std::string &filename) {}; - virtual void handleMemoryPressureModerate() {}; + } + virtual void startProfiler(const std::string &titleString) {} + virtual void stopProfiler(const std::string &titleString, const std::string &filename) {} + virtual void handleMemoryPressureModerate() {} virtual void handleMemoryPressureCritical() { handleMemoryPressureModerate(); - }; - virtual void destroy() {}; - virtual ~JSExecutor() {}; + } + virtual void destroy() {} + virtual ~JSExecutor() {} }; } } diff --git a/ReactCommon/bridge/Instance.cpp b/ReactCommon/bridge/Instance.cpp index a5b435e15bd87c..fa70b02abbb554 100644 --- a/ReactCommon/bridge/Instance.cpp +++ b/ReactCommon/bridge/Instance.cpp @@ -8,6 +8,7 @@ #include #include +#include #include @@ -19,42 +20,9 @@ namespace facebook { namespace react { -namespace { -struct ExecutorTokenFactoryImpl : ExecutorTokenFactory { - ExecutorTokenFactoryImpl(InstanceCallback* callback): callback_(callback) {} - virtual ExecutorToken createExecutorToken() const { - return callback_->createExecutorToken(); - } - private: - InstanceCallback* callback_; -}; -} - -class Instance::BridgeCallbackImpl : public BridgeCallback { - public: - explicit BridgeCallbackImpl(Instance* instance) : instance_(instance) {} - virtual void onCallNativeModules( - ExecutorToken executorToken, - const std::string& calls, - bool isEndOfBatch) override { - instance_->callNativeModules(executorToken, calls, isEndOfBatch); - } - - virtual void onExecutorUnregistered(ExecutorToken executorToken) override { - // TODO(cjhopman): implement this. - } - - virtual MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int moduleId, unsigned int hookId, folly::dynamic&& params) override { - return instance_->callSerializableNativeHook(token, moduleId, hookId, std::move(params)); - } - private: - Instance* instance_; -}; - Instance::~Instance() { - if (nativeQueue_) { - nativeQueue_->quitSynchronous(); - bridge_->destroy(); + if (nativeToJsBridge_) { + nativeToJsBridge_->destroy(); } } @@ -65,32 +33,15 @@ void Instance::initializeBridge( std::unique_ptr nativeQueue, std::shared_ptr moduleRegistry) { callback_ = std::move(callback); - nativeQueue_ = std::move(nativeQueue); - jsQueue_ = jsQueue; - moduleRegistry_ = moduleRegistry; - - jsQueue_->runOnQueueSync([this, &jsef] { - bridge_ = folly::make_unique( - jsef.get(), jsQueue_, folly::make_unique(callback_.get()), folly::make_unique(this)); - }); - SystraceSection s("setBatchedBridgeConfig"); - CHECK(bridge_); - - folly::dynamic nativeModuleDescriptions = folly::dynamic::array(); - { - SystraceSection s("collectNativeModuleDescriptions"); - nativeModuleDescriptions = moduleRegistry_->moduleDescriptions(); - } - - folly::dynamic config = - folly::dynamic::object - ("remoteModuleConfig", std::move(nativeModuleDescriptions)); + jsQueue->runOnQueueSync( + [this, &jsef, moduleRegistry, jsQueue, + nativeQueue=folly::makeMoveWrapper(std::move(nativeQueue))] () mutable { + nativeToJsBridge_ = folly::make_unique( + jsef.get(), moduleRegistry, jsQueue, nativeQueue.move(), callback_); + }); - SystraceSection t("setGlobalVariable"); - setGlobalVariable( - "__fbBatchedBridgeConfig", - folly::make_unique(folly::toJson(config))); + CHECK(nativeToJsBridge_); } void Instance::loadScriptFromString(std::unique_ptr string, @@ -99,7 +50,7 @@ void Instance::loadScriptFromString(std::unique_ptr string, SystraceSection s("reactbridge_xplat_loadScriptFromString", "sourceURL", sourceURL); // TODO mhorowitz: ReactMarker around loadApplicationScript - bridge_->loadApplicationScript(std::move(string), std::move(sourceURL)); + nativeToJsBridge_->loadApplicationScript(std::move(string), std::move(sourceURL)); } void Instance::loadScriptFromFile(const std::string& filename, @@ -129,71 +80,42 @@ void Instance::loadUnbundle(std::unique_ptr unbundle, std::string startupScriptSourceURL) { callback_->incrementPendingJSCalls(); SystraceSection s("reactbridge_xplat_setJSModulesUnbundle"); - bridge_->loadApplicationUnbundle(std::move(unbundle), std::move(startupScript), - std::move(startupScriptSourceURL)); + nativeToJsBridge_->loadApplicationUnbundle(std::move(unbundle), std::move(startupScript), + std::move(startupScriptSourceURL)); } bool Instance::supportsProfiling() { - return bridge_->supportsProfiling(); + return nativeToJsBridge_->supportsProfiling(); } void Instance::startProfiler(const std::string& title) { - return bridge_->startProfiler(title); + return nativeToJsBridge_->startProfiler(title); } void Instance::stopProfiler(const std::string& title, const std::string& filename) { - return bridge_->stopProfiler(title, filename); + return nativeToJsBridge_->stopProfiler(title, filename); } void Instance::setGlobalVariable(std::string propName, std::unique_ptr jsonValue) { - bridge_->setGlobalVariable(std::move(propName), std::move(jsonValue)); + nativeToJsBridge_->setGlobalVariable(std::move(propName), std::move(jsonValue)); } void Instance::callJSFunction(ExecutorToken token, const std::string& module, const std::string& method, folly::dynamic&& params, const std::string& tracingName) { SystraceSection s(tracingName.c_str()); callback_->incrementPendingJSCalls(); - bridge_->callFunction(token, module, method, std::move(params), tracingName); + nativeToJsBridge_->callFunction(token, module, method, std::move(params), tracingName); } void Instance::callJSCallback(ExecutorToken token, uint64_t callbackId, folly::dynamic&& params) { SystraceSection s(""); callback_->incrementPendingJSCalls(); - bridge_->invokeCallback(token, (double) callbackId, std::move(params)); + nativeToJsBridge_->invokeCallback(token, (double) callbackId, std::move(params)); } ExecutorToken Instance::getMainExecutorToken() { - return bridge_->getMainExecutorToken(); -} - -void Instance::callNativeModules(ExecutorToken token, const std::string& calls, bool isEndOfBatch) { - // TODO mhorowitz: avoid copying calls here. - nativeQueue_->runOnQueue([this, token, calls, isEndOfBatch] { - try { - // An exception anywhere in here stops processing of the batch. This - // was the behavior of the Android bridge, and since exception handling - // terminates the whole bridge, there's not much point in continuing. - for (auto& call : react::parseMethodCalls(calls)) { - moduleRegistry_->callNativeMethod( - token, call.moduleId, call.methodId, std::move(call.arguments), call.callId); - } - if (isEndOfBatch) { - callback_->onBatchComplete(); - callback_->decrementPendingJSCalls(); - } - } catch (const std::exception& e) { - LOG(ERROR) << folly::exceptionStr(e).toStdString(); - callback_->onNativeException(folly::exceptionStr(e).toStdString()); - } catch (...) { - LOG(ERROR) << "Unknown exception"; - callback_->onNativeException("Unknown exception"); - } - }); -} - -MethodCallResult Instance::callSerializableNativeHook(ExecutorToken token, unsigned int moduleId, unsigned int methodId, folly::dynamic&& params) { - return moduleRegistry_->callSerializableNativeHook(token, moduleId, methodId, std::move(params)); + return nativeToJsBridge_->getMainExecutorToken(); } } // namespace react diff --git a/ReactCommon/bridge/Instance.h b/ReactCommon/bridge/Instance.h index 53ec7ac198338b..9aee9d238b2008 100644 --- a/ReactCommon/bridge/Instance.h +++ b/ReactCommon/bridge/Instance.h @@ -6,7 +6,7 @@ #include -#include "Bridge.h" +#include "NativeToJsBridge.h" #include "ModuleRegistry.h" #include "NativeModule.h" @@ -22,6 +22,7 @@ struct InstanceCallback { virtual void decrementPendingJSCalls() = 0; virtual void onNativeException(const std::string& what) = 0; virtual ExecutorToken createExecutorToken() = 0; + virtual void onExecutorStopped(ExecutorToken) = 0; }; class Instance { @@ -46,21 +47,15 @@ class Instance { void callJSFunction(ExecutorToken token, const std::string& module, const std::string& method, folly::dynamic&& params, const std::string& tracingName); void callJSCallback(ExecutorToken token, uint64_t callbackId, folly::dynamic&& params); - MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int moduleId, unsigned int methodId, folly::dynamic&& args); + MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int moduleId, + unsigned int methodId, folly::dynamic&& args); ExecutorToken getMainExecutorToken(); private: - class BridgeCallbackImpl; - void callNativeModules(ExecutorToken token, const std::string& calls, bool isEndOfBatch); - std::unique_ptr callback_; - std::shared_ptr moduleRegistry_; - // TODO #10487027: clean up the ownership of this. - std::shared_ptr jsQueue_; - std::unique_ptr nativeQueue_; - - std::unique_ptr bridge_; + std::shared_ptr callback_; + std::unique_ptr nativeToJsBridge_; }; } diff --git a/ReactCommon/bridge/JSCExecutor.cpp b/ReactCommon/bridge/JSCExecutor.cpp index ad14e99068eb52..4bc0d99a04af66 100644 --- a/ReactCommon/bridge/JSCExecutor.cpp +++ b/ReactCommon/bridge/JSCExecutor.cpp @@ -14,7 +14,6 @@ #include #include -#include "Bridge.h" #include "JSCHelpers.h" #include "Platform.h" #include "SystraceSection.h" @@ -107,29 +106,51 @@ static std::string executeJSCallWithJSC( } std::unique_ptr JSCExecutorFactory::createJSExecutor( - Bridge *bridge, std::shared_ptr jsQueue) { + std::shared_ptr delegate, std::shared_ptr jsQueue) { return std::unique_ptr( - new JSCExecutor(bridge, jsQueue, cacheDir_, m_jscConfig)); + new JSCExecutor(delegate, jsQueue, m_cacheDir, m_jscConfig)); } -JSCExecutor::JSCExecutor(Bridge *bridge, std::shared_ptr messageQueueThread, - const std::string& cacheDir, const folly::dynamic& jscConfig) : - m_bridge(bridge), +JSCExecutor::JSCExecutor(std::shared_ptr delegate, + std::shared_ptr messageQueueThread, + const std::string& cacheDir, + const folly::dynamic& jscConfig) : + m_delegate(delegate), m_deviceCacheDir(cacheDir), m_messageQueueThread(messageQueueThread), m_jscConfig(jscConfig) { initOnJSVMThread(); + + SystraceSection s("setBatchedBridgeConfig"); + + folly::dynamic nativeModuleConfig = folly::dynamic::array(); + + { + SystraceSection s("collectNativeModuleNames"); + std::vector names = delegate->moduleNames(); + for (auto& name : delegate->moduleNames()) { + nativeModuleConfig.push_back(folly::dynamic::array(std::move(name))); + } + } + + folly::dynamic config = + folly::dynamic::object("remoteModuleConfig", std::move(nativeModuleConfig)); + + SystraceSection t("setGlobalVariable"); + setGlobalVariable( + "__fbBatchedBridgeConfig", + folly::make_unique(folly::toJson(config))); } JSCExecutor::JSCExecutor( - Bridge *bridge, + std::shared_ptr delegate, std::shared_ptr messageQueueThread, int workerId, JSCExecutor *owner, std::string scriptURL, std::unordered_map globalObjAsJSON, const folly::dynamic& jscConfig) : - m_bridge(bridge), + m_delegate(delegate), m_workerId(workerId), m_owner(owner), m_deviceCacheDir(owner->m_deviceCacheDir), @@ -198,6 +219,7 @@ void JSCExecutor::initOnJSVMThread() { // Add a pointer to ourselves so we can retrieve it later in our hooks JSObjectSetPrivate(JSContextGetGlobalObject(m_context), this); + installNativeHook<&JSCExecutor::nativeRequireModuleConfig>("nativeRequireModuleConfig"); installNativeHook<&JSCExecutor::nativeFlushQueueImmediate>("nativeFlushQueueImmediate"); installNativeHook<&JSCExecutor::nativeStartWorker>("nativeStartWorker"); installNativeHook<&JSCExecutor::nativePostMessageToWorker>("nativePostMessageToWorker"); @@ -280,7 +302,7 @@ void JSCExecutor::setJSModulesUnbundle(std::unique_ptr unbund void JSCExecutor::flush() { // TODO: Make this a first class function instead of evaling. #9317773 std::string calls = executeJSCallWithJSC(m_context, "flushedQueue", std::vector()); - m_bridge->callNativeModules(*this, calls, true); + m_delegate->callNativeModules(*this, std::move(calls), true); } void JSCExecutor::callFunction(const std::string& moduleId, const std::string& methodId, const folly::dynamic& arguments) { @@ -291,7 +313,7 @@ void JSCExecutor::callFunction(const std::string& moduleId, const std::string& m std::move(arguments), }; std::string calls = executeJSCallWithJSC(m_context, "callFunctionReturnFlushedQueue", std::move(call)); - m_bridge->callNativeModules(*this, calls, true); + m_delegate->callNativeModules(*this, std::move(calls), true); } void JSCExecutor::invokeCallback(const double callbackId, const folly::dynamic& arguments) { @@ -301,7 +323,7 @@ void JSCExecutor::invokeCallback(const double callbackId, const folly::dynamic& std::move(arguments) }; std::string calls = executeJSCallWithJSC(m_context, "invokeCallbackAndReturnFlushedQueue", std::move(call)); - m_bridge->callNativeModules(*this, calls, true); + m_delegate->callNativeModules(*this, std::move(calls), true); } void JSCExecutor::setGlobalVariable(std::string propName, std::unique_ptr jsonValue) { @@ -362,7 +384,7 @@ void JSCExecutor::handleMemoryPressureCritical() { } void JSCExecutor::flushQueueImmediate(std::string queueJSON) { - m_bridge->callNativeModules(*this, queueJSON, false); + m_delegate->callNativeModules(*this, std::move(queueJSON), false); } void JSCExecutor::loadModule(uint32_t moduleId) { @@ -388,7 +410,7 @@ int JSCExecutor::addWebWorker( WebWorkerUtil::createWebWorkerThread(workerId, m_messageQueueThread.get()); std::unique_ptr worker; workerMQT->runOnQueueSync([this, &worker, &workerMQT, &scriptURL, &globalObj, workerId, &workerJscConfig] () { - worker.reset(new JSCExecutor(m_bridge, workerMQT, workerId, this, scriptURL, + worker.reset(new JSCExecutor(m_delegate, workerMQT, workerId, this, std::move(scriptURL), globalObj.toJSONMap(), workerJscConfig)); }); @@ -397,14 +419,14 @@ int JSCExecutor::addWebWorker( JSCExecutor *workerPtr = worker.get(); std::shared_ptr sharedMessageQueueThread = worker->m_messageQueueThread; - ExecutorToken token = m_bridge->registerExecutor( + m_delegate->registerExecutor( std::move(worker), std::move(sharedMessageQueueThread)); m_ownedWorkers.emplace( std::piecewise_construct, std::forward_as_tuple(workerId), - std::forward_as_tuple(workerPtr, token, std::move(workerObj))); + std::forward_as_tuple(workerPtr, std::move(workerObj))); return workerId; } @@ -464,12 +486,11 @@ void JSCExecutor::receiveMessageFromOwner(const std::string& msgString) { void JSCExecutor::terminateOwnedWebWorker(int workerId) { auto& workerRegistration = m_ownedWorkers.at(workerId); std::shared_ptr workerMQT = workerRegistration.executor->m_messageQueueThread; - ExecutorToken workerExecutorToken = workerRegistration.executorToken; m_ownedWorkers.erase(workerId); - workerMQT->runOnQueueSync([this, workerExecutorToken, &workerMQT] { + workerMQT->runOnQueueSync([this, &workerMQT] { workerMQT->quitSynchronous(); - std::unique_ptr worker = m_bridge->unregisterExecutor(workerExecutorToken); + std::unique_ptr worker = m_delegate->unregisterExecutor(*this); worker->destroy(); worker.reset(); }); @@ -521,6 +542,18 @@ JSValueRef JSCExecutor::nativeRequire( return JSValueMakeUndefined(m_context); } +JSValueRef JSCExecutor::nativeRequireModuleConfig( + size_t argumentCount, + const JSValueRef arguments[]) { + if (argumentCount != 1) { + throw std::invalid_argument("Got wrong number of args"); + } + + std::string moduleName = Value(m_context, arguments[0]).toString().str(); + folly::dynamic config = m_delegate->getModuleConfig(moduleName); + return JSValueMakeString(m_context, String(folly::toJson(config).c_str())); +} + JSValueRef JSCExecutor::nativeFlushQueueImmediate( size_t argumentCount, const JSValueRef arguments[]) { @@ -529,7 +562,7 @@ JSValueRef JSCExecutor::nativeFlushQueueImmediate( } std::string resStr = Value(m_context, arguments[0]).toJSONString(); - flushQueueImmediate(resStr); + flushQueueImmediate(std::move(resStr)); return JSValueMakeUndefined(m_context); } @@ -595,7 +628,8 @@ JSValueRef JSCExecutor::nativeCallSyncHook( unsigned int methodId = Value(m_context, arguments[1]).asUnsignedInteger(); std::string argsJson = Value(m_context, arguments[2]).toJSONString(); - MethodCallResult result = m_bridge->callSerializableNativeHook( + MethodCallResult result = m_delegate->callSerializableNativeHook( + *this, moduleId, methodId, argsJson); diff --git a/ReactCommon/bridge/JSCExecutor.h b/ReactCommon/bridge/JSCExecutor.h index a0aac1c2774538..e869946d76d51d 100644 --- a/ReactCommon/bridge/JSCExecutor.h +++ b/ReactCommon/bridge/JSCExecutor.h @@ -23,25 +23,24 @@ class MessageQueueThread; class JSCExecutorFactory : public JSExecutorFactory { public: JSCExecutorFactory(const std::string& cacheDir, const folly::dynamic& jscConfig) : - cacheDir_(cacheDir), + m_cacheDir(cacheDir), m_jscConfig(jscConfig) {} virtual std::unique_ptr createJSExecutor( - Bridge *bridge, std::shared_ptr jsQueue) override; + std::shared_ptr delegate, + std::shared_ptr jsQueue) override; private: - std::string cacheDir_; + std::string m_cacheDir; folly::dynamic m_jscConfig; }; class JSCExecutor; class WorkerRegistration : public noncopyable { public: - explicit WorkerRegistration(JSCExecutor* executor_, ExecutorToken executorToken_, Object jsObj_) : + explicit WorkerRegistration(JSCExecutor* executor_, Object jsObj_) : executor(executor_), - executorToken(executorToken_), jsObj(std::move(jsObj_)) {} JSCExecutor *executor; - ExecutorToken executorToken; Object jsObj; }; @@ -50,8 +49,10 @@ class JSCExecutor : public JSExecutor { /** * Must be invoked from thread this Executor will run on. */ - explicit JSCExecutor(Bridge *bridge, std::shared_ptr messageQueueThread, - const std::string& cacheDir, const folly::dynamic& jscConfig); + explicit JSCExecutor(std::shared_ptr delegate, + std::shared_ptr messageQueueThread, + const std::string& cacheDir, + const folly::dynamic& jscConfig); ~JSCExecutor() override; virtual void loadApplicationScript( @@ -79,7 +80,7 @@ class JSCExecutor : public JSExecutor { private: JSGlobalContextRef m_context; - Bridge *m_bridge; + std::shared_ptr m_delegate; int m_workerId = 0; // if this is a worker executor, this is non-zero JSCExecutor *m_owner = nullptr; // if this is a worker executor, this is non-null std::shared_ptr m_isDestroyed = std::shared_ptr(new bool(false)); @@ -93,7 +94,7 @@ class JSCExecutor : public JSExecutor { * WebWorker constructor. Must be invoked from thread this Executor will run on. */ JSCExecutor( - Bridge *bridge, + std::shared_ptr delegate, std::shared_ptr messageQueueThread, int workerId, JSCExecutor *owner, @@ -118,6 +119,9 @@ class JSCExecutor : public JSExecutor { template< JSValueRef (JSCExecutor::*method)(size_t, const JSValueRef[])> void installNativeHook(const char* name); + JSValueRef nativeRequireModuleConfig( + size_t argumentCount, + const JSValueRef arguments[]); JSValueRef nativeStartWorker( size_t argumentCount, const JSValueRef arguments[]); diff --git a/ReactCommon/bridge/ModuleRegistry.cpp b/ReactCommon/bridge/ModuleRegistry.cpp index 18ee5f1782595a..6cd6671c1fee28 100644 --- a/ReactCommon/bridge/ModuleRegistry.cpp +++ b/ReactCommon/bridge/ModuleRegistry.cpp @@ -8,45 +8,86 @@ namespace facebook { namespace react { +namespace { + +std::string normalizeName(std::string name) { + // TODO mhorowitz #10487027: This is super ugly. We should just + // change iOS to emit normalized names, drop the "RK..." from + // names hardcoded in Android, and then delete this and the + // similar hacks in js. + if (name.compare(0, 3, "RCT") == 0) { + return name.substr(3); + } else if (name.compare(0, 2, "RK") == 0) { + return name.substr(2); + } + return name; +} + +} + ModuleRegistry::ModuleRegistry(std::vector> modules) : modules_(std::move(modules)) {} -folly::dynamic ModuleRegistry::moduleDescriptions() { - folly::dynamic modDescs = folly::dynamic::object; +std::vector ModuleRegistry::moduleNames() { + std::vector names; + for (size_t i = 0; i < modules_.size(); i++) { + std::string name = normalizeName(modules_[i]->getName()); + modulesByName_[name] = i; + names.push_back(std::move(name)); + } + return names; +} - for (size_t moduleId = 0; moduleId < modules_.size(); ++moduleId) { - const auto& module = modules_[moduleId]; +folly::dynamic ModuleRegistry::getConfig(const std::string& name) { + auto it = modulesByName_.find(name); + if (it == modulesByName_.end()) { + return nullptr; + } + CHECK(it->second < modules_.size()); - folly::dynamic methodDescs = folly::dynamic::object; - std::vector methods; - { - SystraceSection s("getMethods", - "module", module->getName()); - methods = module->getMethods(); - } - for (size_t methodId = 0; methodId < methods.size(); ++methodId) { - methodDescs.insert(std::move(methods[methodId].name), - folly::dynamic::object - ("methodID", methodId) - ("type", std::move(methods[methodId].type))); + NativeModule* module = modules_[it->second].get(); + + // string name, [object constants,] array methodNames (methodId is index), [array asyncMethodIds] + folly::dynamic config = folly::dynamic::array(name); + + { + SystraceSection s("getConfig constants", + "module", name); + folly::dynamic constants = module->getConstants(); + if (constants.isObject() && constants.size() > 0) { + config.push_back(std::move(constants)); } + } + + { + SystraceSection s("getConfig methods", + "module", name); + std::vector methods = module->getMethods(); - folly::dynamic constants = folly::dynamic::array(); - { - SystraceSection s("getConstants", - "module", module->getName()); - constants = module->getConstants(); + folly::dynamic methodNames = folly::dynamic::array; + folly::dynamic asyncMethodIds = folly::dynamic::array; + + for (auto& descriptor : methods) { + methodNames.push_back(std::move(descriptor.name)); + if (descriptor.type == "remoteAsync") { + asyncMethodIds.push_back(methodNames.size() - 1); + } } - modDescs.insert(module->getName(), - folly::dynamic::object - ("supportsWebWorkers", module->supportsWebWorkers()) - ("moduleID", moduleId) - ("methods", std::move(methodDescs)) - ("constants", std::move(constants))); + if (!methodNames.empty()) { + config.push_back(std::move(methodNames)); + if (!asyncMethodIds.empty()) { + config.push_back(std::move(asyncMethodIds)); + } + } + } + if (config.size() == 1) { + // no constants or methods + return nullptr; + } else { + return config; } - return modDescs; } void ModuleRegistry::callNativeMethod(ExecutorToken token, unsigned int moduleId, unsigned int methodId, @@ -75,7 +116,7 @@ void ModuleRegistry::callNativeMethod(ExecutorToken token, unsigned int moduleId // fall through; } - std::string moduleName = modules_[moduleId]->getName(); + std::string moduleName = normalizeName(modules_[moduleId]->getName()); auto descs = modules_[moduleId]->getMethods(); std::string methodName; if (methodId < descs.size()) { diff --git a/ReactCommon/bridge/ModuleRegistry.h b/ReactCommon/bridge/ModuleRegistry.h index 2bc85b61b40e54..a96a20bcf82179 100644 --- a/ReactCommon/bridge/ModuleRegistry.h +++ b/ReactCommon/bridge/ModuleRegistry.h @@ -25,13 +25,18 @@ class ModuleRegistry { // notifyCatalystInstanceDestroy: use RAII instead ModuleRegistry(std::vector> modules); - folly::dynamic moduleDescriptions(); + std::vector moduleNames(); + folly::dynamic getConfig(const std::string& name); void callNativeMethod(ExecutorToken token, unsigned int moduleId, unsigned int methodId, folly::dynamic&& params, int callId); MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int moduleId, unsigned int methodId, folly::dynamic&& args); private: + // This is always populated std::vector> modules_; + + // This is only populated if moduleNames() is called. Values are indices into modules_. + std::unordered_map modulesByName_; }; } diff --git a/ReactCommon/bridge/NativeModule.h b/ReactCommon/bridge/NativeModule.h index 6a71ef0d6ece01..cbc82aa46c337f 100644 --- a/ReactCommon/bridge/NativeModule.h +++ b/ReactCommon/bridge/NativeModule.h @@ -12,11 +12,6 @@ namespace facebook { namespace react { -struct MethodCallResult { - folly::dynamic result; - bool isUndefined; -}; - struct MethodDescriptor { std::string name; // type is one of js MessageQueue.MethodTypes diff --git a/ReactCommon/bridge/NativeToJsBridge.cpp b/ReactCommon/bridge/NativeToJsBridge.cpp new file mode 100644 index 00000000000000..c7c0292698b3a8 --- /dev/null +++ b/ReactCommon/bridge/NativeToJsBridge.cpp @@ -0,0 +1,347 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "NativeToJsBridge.h" + +#ifdef WITH_FBSYSTRACE +#include +using fbsystrace::FbSystraceAsyncFlow; +#endif + +#include +#include +#include + +#include "Instance.h" +#include "ModuleRegistry.h" +#include "Platform.h" +#include "SystraceSection.h" + +namespace facebook { +namespace react { + +// This class manages calls from JS to native code. +class JsToNativeBridge : public react::ExecutorDelegate { +public: + JsToNativeBridge(NativeToJsBridge* nativeToJs, + std::shared_ptr registry, + std::unique_ptr nativeQueue, + std::shared_ptr callback) + : m_nativeToJs(nativeToJs) + , m_registry(registry) + , m_nativeQueue(std::move(nativeQueue)) + , m_callback(callback) {} + + void registerExecutor(std::unique_ptr executor, + std::shared_ptr queue) override { + m_nativeToJs->registerExecutor(m_callback->createExecutorToken(), std::move(executor), queue); + } + + std::unique_ptr unregisterExecutor(JSExecutor& executor) override { + m_callback->onExecutorStopped(m_nativeToJs->getTokenForExecutor(executor)); + return m_nativeToJs->unregisterExecutor(executor); + } + + std::vector moduleNames() override { + // If this turns out to be too expensive to run on the js thread, + // we can compute it in the ctor, and just return std::move() it + // here. + return m_registry->moduleNames(); + } + + folly::dynamic getModuleConfig(const std::string& name) override { + return m_registry->getConfig(name); + } + + void callNativeModules( + JSExecutor& executor, std::string callJSON, bool isEndOfBatch) override { + ExecutorToken token = m_nativeToJs->getTokenForExecutor(executor); + m_nativeQueue->runOnQueue([this, token, callJSON=std::move(callJSON), isEndOfBatch] { + // An exception anywhere in here stops processing of the batch. This + // was the behavior of the Android bridge, and since exception handling + // terminates the whole bridge, there's not much point in continuing. + for (auto& call : react::parseMethodCalls(callJSON)) { + m_registry->callNativeMethod( + token, call.moduleId, call.methodId, std::move(call.arguments), call.callId); + } + if (isEndOfBatch) { + m_callback->onBatchComplete(); + m_callback->decrementPendingJSCalls(); + } + }); + } + + MethodCallResult callSerializableNativeHook( + JSExecutor& executor, unsigned int moduleId, unsigned int methodId, + folly::dynamic&& args) override { + ExecutorToken token = m_nativeToJs->getTokenForExecutor(executor); + return m_registry->callSerializableNativeHook(token, moduleId, methodId, std::move(args)); + } + + void quitQueueSynchronous() { + m_nativeQueue->quitSynchronous(); + } + +private: + + // These methods are always invoked from an Executor. The NativeToJsBridge + // keeps a reference to the root executor, and when destroy() is + // called, the Executors are all destroyed synchronously on their + // bridges. So, the bridge pointer will will always point to a + // valid object during a call to a delegate method from an exectuto. + NativeToJsBridge* m_nativeToJs; + std::shared_ptr m_registry; + std::unique_ptr m_nativeQueue; + std::shared_ptr m_callback; +}; + +NativeToJsBridge::NativeToJsBridge( + JSExecutorFactory* jsExecutorFactory, + std::shared_ptr registry, + std::shared_ptr jsQueue, + std::unique_ptr nativeQueue, + std::shared_ptr callback) + : m_destroyed(std::make_shared(false)) + , m_mainExecutorToken(callback->createExecutorToken()) + , m_delegate( + std::make_shared( + this, registry, std::move(nativeQueue), callback)) { + std::unique_ptr mainExecutor = + jsExecutorFactory->createJSExecutor(m_delegate, jsQueue); + // cached to avoid locked map lookup in the common case + m_mainExecutor = mainExecutor.get(); + registerExecutor(m_mainExecutorToken, std::move(mainExecutor), jsQueue); +} + +// This must be called on the same thread on which the constructor was called. +NativeToJsBridge::~NativeToJsBridge() { + CHECK(*m_destroyed) << + "NativeToJsBridge::destroy() must be called before deallocating the NativeToJsBridge!"; +} + +void NativeToJsBridge::loadApplicationScript(std::unique_ptr script, + std::string sourceURL) { + // TODO(t11144533): Add assert that we are on the correct thread + m_mainExecutor->loadApplicationScript(std::move(script), std::move(sourceURL)); +} + +void NativeToJsBridge::loadApplicationUnbundle( + std::unique_ptr unbundle, + std::unique_ptr startupScript, + std::string startupScriptSourceURL) { + runOnExecutorQueue( + m_mainExecutorToken, + [unbundle=folly::makeMoveWrapper(std::move(unbundle)), + startupScript=folly::makeMoveWrapper(std::move(startupScript)), + startupScriptSourceURL=std::move(startupScriptSourceURL)] + (JSExecutor* executor) mutable { + + executor->setJSModulesUnbundle(unbundle.move()); + executor->loadApplicationScript(std::move(*startupScript), + std::move(startupScriptSourceURL)); + }); +} + +void NativeToJsBridge::callFunction( + ExecutorToken executorToken, + const std::string& moduleId, + const std::string& methodId, + const folly::dynamic& arguments, + const std::string& tracingName) { + #ifdef WITH_FBSYSTRACE + int systraceCookie = m_systraceCookie++; + FbSystraceAsyncFlow::begin( + TRACE_TAG_REACT_CXX_BRIDGE, + tracingName.c_str(), + systraceCookie); + #endif + + runOnExecutorQueue(executorToken, [moduleId, methodId, arguments, tracingName, systraceCookie] (JSExecutor* executor) { + #ifdef WITH_FBSYSTRACE + FbSystraceAsyncFlow::end( + TRACE_TAG_REACT_CXX_BRIDGE, + tracingName.c_str(), + systraceCookie); + SystraceSection s(tracingName.c_str()); + #endif + + // This is safe because we are running on the executor's thread: it won't + // destruct until after it's been unregistered (which we check above) and + // that will happen on this thread + executor->callFunction(moduleId, methodId, arguments); + }); +} + +void NativeToJsBridge::invokeCallback(ExecutorToken executorToken, const double callbackId, + const folly::dynamic& arguments) { + #ifdef WITH_FBSYSTRACE + int systraceCookie = m_systraceCookie++; + FbSystraceAsyncFlow::begin( + TRACE_TAG_REACT_CXX_BRIDGE, + "", + systraceCookie); + #endif + + runOnExecutorQueue(executorToken, [callbackId, arguments, systraceCookie] (JSExecutor* executor) { + #ifdef WITH_FBSYSTRACE + FbSystraceAsyncFlow::end( + TRACE_TAG_REACT_CXX_BRIDGE, + "", + systraceCookie); + SystraceSection s("NativeToJsBridge.invokeCallback"); + #endif + + executor->invokeCallback(callbackId, arguments); + }); +} + +void NativeToJsBridge::setGlobalVariable(std::string propName, + std::unique_ptr jsonValue) { + runOnExecutorQueue( + m_mainExecutorToken, + [propName=std::move(propName), jsonValue=folly::makeMoveWrapper(std::move(jsonValue))] + (JSExecutor* executor) mutable { + executor->setGlobalVariable(propName, jsonValue.move()); + }); +} + +void* NativeToJsBridge::getJavaScriptContext() { + // TODO(cjhopman): this seems unsafe unless we require that it is only called on the main js queue. + return m_mainExecutor->getJavaScriptContext(); +} + +bool NativeToJsBridge::supportsProfiling() { + // Intentionally doesn't post to jsqueue. supportsProfiling() can be called from any thread. + return m_mainExecutor->supportsProfiling(); +} + +void NativeToJsBridge::startProfiler(const std::string& title) { + runOnExecutorQueue(m_mainExecutorToken, [=] (JSExecutor* executor) { + executor->startProfiler(title); + }); +} + +void NativeToJsBridge::stopProfiler(const std::string& title, const std::string& filename) { + runOnExecutorQueue(m_mainExecutorToken, [=] (JSExecutor* executor) { + executor->stopProfiler(title, filename); + }); +} + +void NativeToJsBridge::handleMemoryPressureModerate() { + runOnExecutorQueue(m_mainExecutorToken, [=] (JSExecutor* executor) { + executor->handleMemoryPressureModerate(); + }); +} + +void NativeToJsBridge::handleMemoryPressureCritical() { + runOnExecutorQueue(m_mainExecutorToken, [=] (JSExecutor* executor) { + executor->handleMemoryPressureCritical(); + }); +} + +ExecutorToken NativeToJsBridge::getMainExecutorToken() const { + return m_mainExecutorToken; +} + +ExecutorToken NativeToJsBridge::registerExecutor( + ExecutorToken token, + std::unique_ptr executor, + std::shared_ptr messageQueueThread) { + std::lock_guard registrationGuard(m_registrationMutex); + + CHECK(m_executorTokenMap.find(executor.get()) == m_executorTokenMap.end()) + << "Trying to register an already registered executor!"; + + m_executorTokenMap.emplace(executor.get(), token); + m_executorMap.emplace( + token, + ExecutorRegistration(std::move(executor), messageQueueThread)); + + return token; +} + +std::unique_ptr NativeToJsBridge::unregisterExecutor(JSExecutor& executor) { + std::unique_ptr ret; + + { + std::lock_guard registrationGuard(m_registrationMutex); + + auto it = m_executorTokenMap.find(&executor); + CHECK(it != m_executorTokenMap.end()) + << "Trying to unregister an executor that was never registered!"; + auto it2 = m_executorMap.find(it->second); + ret = std::move(it2->second.executor_); + + m_executorTokenMap.erase(it); + m_executorMap.erase(it2); + } + + return ret; +} + +MessageQueueThread* NativeToJsBridge::getMessageQueueThread(const ExecutorToken& executorToken) { + std::lock_guard registrationGuard(m_registrationMutex); + auto it = m_executorMap.find(executorToken); + if (it == m_executorMap.end()) { + return nullptr; + } + return it->second.messageQueueThread_.get(); +} + +JSExecutor* NativeToJsBridge::getExecutor(const ExecutorToken& executorToken) { + std::lock_guard registrationGuard(m_registrationMutex); + auto it = m_executorMap.find(executorToken); + if (it == m_executorMap.end()) { + return nullptr; + } + return it->second.executor_.get(); +} + +ExecutorToken NativeToJsBridge::getTokenForExecutor(JSExecutor& executor) { + std::lock_guard registrationGuard(m_registrationMutex); + return m_executorTokenMap.at(&executor); +} + +void NativeToJsBridge::destroy() { + m_delegate->quitQueueSynchronous(); + auto executorMessageQueueThread = getMessageQueueThread(m_mainExecutorToken); + executorMessageQueueThread->runOnQueueSync([this, &executorMessageQueueThread] { + executorMessageQueueThread->quitSynchronous(); + *m_destroyed = true; + std::unique_ptr mainExecutor = unregisterExecutor(*m_mainExecutor); + m_mainExecutor = nullptr; + mainExecutor->destroy(); + }); +} + +void NativeToJsBridge::runOnExecutorQueue(ExecutorToken executorToken, std::function task) { + if (*m_destroyed) { + return; + } + + auto executorMessageQueueThread = getMessageQueueThread(executorToken); + if (executorMessageQueueThread == nullptr) { + LOG(WARNING) << "Dropping JS action for executor that has been unregistered..."; + return; + } + + std::shared_ptr isDestroyed = m_destroyed; + executorMessageQueueThread->runOnQueue([this, isDestroyed, executorToken, task=std::move(task)] { + if (*isDestroyed) { + return; + } + + JSExecutor *executor = getExecutor(executorToken); + if (executor == nullptr) { + LOG(WARNING) << "Dropping JS call for executor that has been unregistered..."; + return; + } + + // The executor is guaranteed to be valid for the duration of the task because: + // 1. the executor is only destroyed after it is unregistered + // 2. the executor is unregistered on this queue + // 3. we just confirmed that the executor hasn't been unregistered above + task(executor); + }); +} + +} } diff --git a/ReactCommon/bridge/Bridge.h b/ReactCommon/bridge/NativeToJsBridge.h similarity index 64% rename from ReactCommon/bridge/Bridge.h rename to ReactCommon/bridge/NativeToJsBridge.h index ebee2b8aafc417..3f3230a6c980ad 100644 --- a/ReactCommon/bridge/Bridge.h +++ b/ReactCommon/bridge/NativeToJsBridge.h @@ -11,7 +11,6 @@ #include "Executor.h" #include "ExecutorToken.h" -#include "ExecutorTokenFactory.h" #include "JSModulesUnbundle.h" #include "MessageQueueThread.h" #include "MethodCall.h" @@ -27,21 +26,9 @@ struct dynamic; namespace facebook { namespace react { -class BridgeCallback { -public: - virtual ~BridgeCallback() {}; - - virtual void onCallNativeModules( - ExecutorToken executorToken, - const std::string& callJSON, - bool isEndOfBatch) = 0; - - virtual void onExecutorUnregistered(ExecutorToken executorToken) = 0; +struct InstanceCallback; +class ModuleRegistry; - virtual MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int moduleId, unsigned int methodId, folly::dynamic&& args) = 0; -}; - -class Bridge; class ExecutorRegistration { public: ExecutorRegistration( @@ -54,17 +41,26 @@ class ExecutorRegistration { std::shared_ptr messageQueueThread_; }; -class Bridge { +class JsToNativeBridge; + +// This class manages calls from native code to JS. It also manages +// executors and their threads. This part is used by both bridges for +// now, but further refactorings should separate the bridges more +// fully #11247981. +class NativeToJsBridge { public: + friend class JsToNativeBridge; + /** * This must be called on the main JS thread. */ - Bridge( + NativeToJsBridge( JSExecutorFactory* jsExecutorFactory, + std::shared_ptr registry, std::shared_ptr jsQueue, - std::unique_ptr executorTokenFactory, - std::unique_ptr callback); - virtual ~Bridge(); + std::unique_ptr nativeQueue, + std::shared_ptr callback); + virtual ~NativeToJsBridge(); /** * Executes a function with the module ID and method ID and any additional @@ -108,58 +104,48 @@ class Bridge { void handleMemoryPressureModerate(); void handleMemoryPressureCritical(); - /** - * Invokes a set of native module calls on behalf of the given executor. - * - * TODO: get rid of isEndOfBatch - */ - void callNativeModules(JSExecutor& executor, const std::string& callJSON, bool isEndOfBatch); - - MethodCallResult callSerializableNativeHook(unsigned int moduleId, unsigned int methodId, const std::string& argsJSON); - /** * Returns the ExecutorToken corresponding to the main JSExecutor. */ ExecutorToken getMainExecutorToken() const; + /** + * Synchronously tears down the bridge and the main executor. + */ + void destroy(); +private: /** * Registers the given JSExecutor which runs on the given MessageQueueThread - * with the Bridge. Part of this registration is transfering ownership of this - * JSExecutor to the Bridge for the duration of the registration. + * with the NativeToJsBridge. Part of this registration is transfering + * ownership of this JSExecutor to the NativeToJsBridge for the duration of + * the registration. * * Returns a ExecutorToken which can be used to refer to this JSExecutor - * in the Bridge. + * in the NativeToJsBridge. */ ExecutorToken registerExecutor( + ExecutorToken token, std::unique_ptr executor, std::shared_ptr executorMessageQueueThread); /** - * Unregisters a JSExecutor that was previously registered with this Bridge - * using registerExecutor. Use the ExecutorToken returned from this - * registerExecutor call. This method will return ownership of the unregistered - * executor to the caller for it to retain or tear down. - * - * Returns ownership of the unregistered executor. + * Unregisters a JSExecutor that was previously registered with this NativeToJsBridge + * using registerExecutor. */ - std::unique_ptr unregisterExecutor(ExecutorToken executorToken); + std::unique_ptr unregisterExecutor(JSExecutor& executorToken); - /** - * Synchronously tears down the bridge and the main executor. - */ - void destroy(); -private: void runOnExecutorQueue(ExecutorToken token, std::function task); - std::unique_ptr m_callback; - // This is used to avoid a race condition where a proxyCallback gets queued after ~Bridge(), - // on the same thread. In that case, the callback will try to run the task on m_callback which - // will have been destroyed within ~Bridge(), thus causing a SIGSEGV. + + // This is used to avoid a race condition where a proxyCallback gets queued + // after ~NativeToJsBridge(), on the same thread. In that case, the callback + // will try to run the task on m_callback which will have been destroyed + // within ~NativeToJsBridge(), thus causing a SIGSEGV. std::shared_ptr m_destroyed; JSExecutor* m_mainExecutor; - std::unique_ptr m_mainExecutorToken; - std::unique_ptr m_executorTokenFactory; + ExecutorToken m_mainExecutorToken; + std::shared_ptr m_delegate; std::unordered_map m_executorTokenMap; - std::unordered_map> m_executorMap; + std::unordered_map m_executorMap; std::mutex m_registrationMutex; #ifdef WITH_FBSYSTRACE std::atomic_uint_least32_t m_systraceCookie = ATOMIC_VAR_INIT(); @@ -167,7 +153,7 @@ class Bridge { MessageQueueThread* getMessageQueueThread(const ExecutorToken& executorToken); JSExecutor* getExecutor(const ExecutorToken& executorToken); - inline ExecutorToken getTokenForExecutor(JSExecutor& executor); + ExecutorToken getTokenForExecutor(JSExecutor& executor); }; } } From 6796f136f18ae4a20e4adec3cf1438393f505413 Mon Sep 17 00:00:00 2001 From: Marc Horowitz Date: Wed, 18 May 2016 12:46:05 -0700 Subject: [PATCH 023/843] shut down JSC before quitting its thread Differential Revision: D3280544 fbshipit-source-id: 2a3ef2c445aaa1b224ac391b020467e087c0b81d --- ReactCommon/bridge/NativeToJsBridge.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ReactCommon/bridge/NativeToJsBridge.cpp b/ReactCommon/bridge/NativeToJsBridge.cpp index c7c0292698b3a8..eaa50d38f86259 100644 --- a/ReactCommon/bridge/NativeToJsBridge.cpp +++ b/ReactCommon/bridge/NativeToJsBridge.cpp @@ -303,13 +303,13 @@ ExecutorToken NativeToJsBridge::getTokenForExecutor(JSExecutor& executor) { void NativeToJsBridge::destroy() { m_delegate->quitQueueSynchronous(); - auto executorMessageQueueThread = getMessageQueueThread(m_mainExecutorToken); - executorMessageQueueThread->runOnQueueSync([this, &executorMessageQueueThread] { + auto* executorMessageQueueThread = getMessageQueueThread(m_mainExecutorToken); + executorMessageQueueThread->runOnQueueSync([this, executorMessageQueueThread] { + m_mainExecutor->destroy(); executorMessageQueueThread->quitSynchronous(); - *m_destroyed = true; - std::unique_ptr mainExecutor = unregisterExecutor(*m_mainExecutor); + unregisterExecutor(*m_mainExecutor); m_mainExecutor = nullptr; - mainExecutor->destroy(); + *m_destroyed = true; }); } From 0e997c6eabae79c99823459b39b885547f824955 Mon Sep 17 00:00:00 2001 From: Joe Noon Date: Wed, 18 May 2016 13:01:13 -0700 Subject: [PATCH 024/843] fixes a bug where NavigationPropTypes.SceneRenderer was a plain object Summary: ... used as both a shape and plain object. this splits them out so both parts can be used as needed. NavigationPropTypes.SceneRenderer is a PropTypes shape NavigationPropTypes.SceneRendererProps is the plain object that makes up the shape. Closes https://github.com/facebook/react-native/pull/7518 Differential Revision: D3317322 Pulled By: ericvicenti fbshipit-source-id: e8a31e05130e6647b63f68dbef31bc874550948c --- .../NavigationExperimental/NavigationCard.js | 2 +- .../NavigationExperimental/NavigationCardStack.js | 2 +- .../NavigationExperimental/NavigationHeader.js | 2 +- Libraries/NavigationExperimental/NavigationPropTypes.js | 7 +++++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js b/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js index 403d7da6525bea..8e2d296606e720 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js @@ -92,7 +92,7 @@ class NavigationCard extends React.Component { props: Props; static propTypes = { - ...NavigationPropTypes.SceneRenderer, + ...NavigationPropTypes.SceneRendererProps, onComponentRef: PropTypes.func.isRequired, panHandlers: NavigationPropTypes.panHandlers, pointerEvents: PropTypes.string.isRequired, diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js index 90e0837b52b9cf..d167af6fa86848 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js @@ -90,7 +90,7 @@ class NavigationCardStack extends React.Component { static propTypes = { direction: PropTypes.oneOf([Directions.HORIZONTAL, Directions.VERTICAL]), navigationState: NavigationPropTypes.navigationParentState.isRequired, - onNavigate: NavigationPropTypes.SceneRenderer.onNavigate, + onNavigate: NavigationPropTypes.SceneRendererProps.onNavigate, renderOverlay: PropTypes.func, renderScene: PropTypes.func.isRequired, }; diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js b/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js index f65beb616b80b1..172b14fc2acbcf 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js @@ -103,7 +103,7 @@ class NavigationHeader extends React.Component { }; static propTypes = { - ...NavigationPropTypes.SceneRenderer, + ...NavigationPropTypes.SceneRendererProps, renderLeftComponent: PropTypes.func, renderRightComponent: PropTypes.func, renderTitleComponent: PropTypes.func, diff --git a/Libraries/NavigationExperimental/NavigationPropTypes.js b/Libraries/NavigationExperimental/NavigationPropTypes.js index 6a8a0488614b90..6939e8193465fc 100644 --- a/Libraries/NavigationExperimental/NavigationPropTypes.js +++ b/Libraries/NavigationExperimental/NavigationPropTypes.js @@ -65,7 +65,7 @@ const scene = PropTypes.shape({ }); /* NavigationSceneRendererProps */ -const SceneRenderer = { +const SceneRendererProps = { layout: layout.isRequired, navigationState: navigationParentState.isRequired, onNavigate: PropTypes.func.isRequired, @@ -74,6 +74,8 @@ const SceneRenderer = { scenes: PropTypes.arrayOf(scene).isRequired, }; +const SceneRenderer = PropTypes.shape(SceneRendererProps); + /* NavigationPanPanHandlers */ const panHandlers = PropTypes.shape({ onMoveShouldSetResponder: PropTypes.func.isRequired, @@ -111,11 +113,12 @@ module.exports = { extractSceneRendererProps, // Bundled propTypes. - SceneRenderer, + SceneRendererProps, // propTypes action, navigationParentState, navigationState, panHandlers, + SceneRenderer, }; From 7db7f78dc7d2b85843707f75565bcfcb538e8e51 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Wed, 18 May 2016 13:32:52 -0700 Subject: [PATCH 025/843] Fork NavigationAnimatedView to NavigationTransitioner Summary: - Fork NavigationAnimatedView to NavigationTransitioner - NavigationAnimatedView will soon be deprecated and we'd ask people to use NavigationTransitioner instead. Difference between NavigationTransitioner and NavigationAnimatedView - prop `applyAnimation` is removed. - new prop `configureTransition`, `onTransitionStart`, and `onTransitionEnd` are added. tl;dr; In NavigationAnimatedView, we `position` (an Animated.Value object) as a proxy of the transtion which happens whenever the index of navigation state changes. Because `position` does not change unless navigation index changes, it won't be possible to build animations for actions that changes the navigation state without changing the index. Also, we believe that the name `Transitioner` is a better name for this core component that focuses on transitioning. Note that the actual animation work is done via `` returnd from the `renderScene` prop. Reviewed By: ericvicenti Differential Revision: D3302688 fbshipit-source-id: 720c3a4d3ccf97eb05b038baa44c9e780aad120b --- .../NavigationAnimatedView.js | 13 + .../NavigationExperimental.js | 4 +- .../NavigationPropTypes.js | 2 + .../NavigationTransitioner.js | 266 ++++++++++++++++++ .../NavigationTypeDefinition.js | 19 +- .../Reducer/NavigationScenesReducer.js | 5 +- 6 files changed, 304 insertions(+), 5 deletions(-) create mode 100644 Libraries/NavigationExperimental/NavigationTransitioner.js diff --git a/Libraries/NavigationExperimental/NavigationAnimatedView.js b/Libraries/NavigationExperimental/NavigationAnimatedView.js index 059407b206554a..5f307850640a42 100644 --- a/Libraries/NavigationExperimental/NavigationAnimatedView.js +++ b/Libraries/NavigationExperimental/NavigationAnimatedView.js @@ -11,6 +11,11 @@ */ 'use strict'; +/** + * WARNING: NavigationAnimatedView will be deprecated soon. + * Use NavigationTransitioner instead. + */ + const Animated = require('Animated'); const NavigationPropTypes = require('NavigationPropTypes'); const NavigationScenesReducer = require('NavigationScenesReducer'); @@ -40,6 +45,7 @@ type Props = { type State = { layout: NavigationLayout, position: NavigationAnimatedValue, + progress: NavigationAnimatedValue, scenes: Array, }; @@ -96,6 +102,9 @@ class NavigationAnimatedView this.state = { layout, position: new Animated.Value(this.props.navigationState.index), + // This `progress` is a adummy placeholder value to meet the values + // as `NavigationSceneRendererProps` requires. + progress: new Animated.Value(1), scenes: NavigationScenesReducer([], this.props.navigationState), }; } @@ -179,6 +188,7 @@ class NavigationAnimatedView const { position, + progress, scenes, } = this.state; @@ -187,6 +197,7 @@ class NavigationAnimatedView navigationState, onNavigate, position, + progress, scene, scenes, }); @@ -202,6 +213,7 @@ class NavigationAnimatedView const { position, + progress, scenes, } = this.state; @@ -210,6 +222,7 @@ class NavigationAnimatedView navigationState, onNavigate, position, + progress, scene: scenes[navigationState.index], scenes, }); diff --git a/Libraries/NavigationExperimental/NavigationExperimental.js b/Libraries/NavigationExperimental/NavigationExperimental.js index da03980cf7be88..ff8179189fd474 100644 --- a/Libraries/NavigationExperimental/NavigationExperimental.js +++ b/Libraries/NavigationExperimental/NavigationExperimental.js @@ -15,9 +15,10 @@ const NavigationAnimatedView = require('NavigationAnimatedView'); const NavigationCard = require('NavigationCard'); const NavigationCardStack = require('NavigationCardStack'); const NavigationHeader = require('NavigationHeader'); +const NavigationPropTypes = require('NavigationPropTypes'); const NavigationReducer = require('NavigationReducer'); const NavigationStateUtils = require('NavigationStateUtils'); -const NavigationPropTypes = require('NavigationPropTypes'); +const NavigationTransitioner = require('NavigationTransitioner'); const NavigationExperimental = { // Core @@ -26,6 +27,7 @@ const NavigationExperimental = { // Views AnimatedView: NavigationAnimatedView, + Transitioner: NavigationTransitioner, // CustomComponents: Card: NavigationCard, diff --git a/Libraries/NavigationExperimental/NavigationPropTypes.js b/Libraries/NavigationExperimental/NavigationPropTypes.js index 6939e8193465fc..bdbab9474c5fb0 100644 --- a/Libraries/NavigationExperimental/NavigationPropTypes.js +++ b/Libraries/NavigationExperimental/NavigationPropTypes.js @@ -70,6 +70,7 @@ const SceneRendererProps = { navigationState: navigationParentState.isRequired, onNavigate: PropTypes.func.isRequired, position: animatedValue.isRequired, + progress: animatedValue.isRequired, scene: scene.isRequired, scenes: PropTypes.arrayOf(scene).isRequired, }; @@ -103,6 +104,7 @@ function extractSceneRendererProps( navigationState: props.navigationState, onNavigate: props.onNavigate, position: props.position, + progress: props.progress, scene: props.scene, scenes: props.scenes, }; diff --git a/Libraries/NavigationExperimental/NavigationTransitioner.js b/Libraries/NavigationExperimental/NavigationTransitioner.js new file mode 100644 index 00000000000000..04776afa0be162 --- /dev/null +++ b/Libraries/NavigationExperimental/NavigationTransitioner.js @@ -0,0 +1,266 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule NavigationTransitioner + * @flow + */ +'use strict'; + +const Animated = require('Animated'); +const Easing = require('Easing'); +const NavigationPropTypes = require('NavigationPropTypes'); +const NavigationScenesReducer = require('NavigationScenesReducer'); +const React = require('React'); +const StyleSheet = require('StyleSheet'); +const View = require('View'); + +import type { + NavigationActionCaller, + NavigationAnimatedValue, + NavigationLayout, + NavigationParentState, + NavigationScene, + NavigationSceneRenderer, + NavigationTransitionConfigurator, +} from 'NavigationTypeDefinition'; + +type Props = { + configureTransition: NavigationTransitionConfigurator, + navigationState: NavigationParentState, + onNavigate: NavigationActionCaller, + onTransitionEnd: () => void, + onTransitionStart: () => void, + renderOverlay: ?NavigationSceneRenderer, + renderScene: NavigationSceneRenderer, + style: any, +}; + +type State = { + layout: NavigationLayout, + position: NavigationAnimatedValue, + progress: NavigationAnimatedValue, + scenes: Array, +}; + +const {PropTypes} = React; + +const DefaultTransitionSpec = { + duration: 250, + easing: Easing.inOut(Easing.ease), +}; + +function isSceneNotStale(scene: NavigationScene): boolean { + return !scene.isStale; +} + +class NavigationTransitioner extends React.Component { + + _onLayout: (event: any) => void; + _onTransitionEnd: () => void; + + props: Props; + state: State; + + static propTypes = { + configureTransition: PropTypes.func, + navigationState: NavigationPropTypes.navigationState.isRequired, + onNavigate: PropTypes.func.isRequired, + onTransitionEnd: PropTypes.func, + onTransitionStart: PropTypes.func, + renderOverlay: PropTypes.func, + renderScene: PropTypes.func.isRequired, + }; + + constructor(props: Props, context: any) { + super(props, context); + + // The initial layout isn't measured. Measured layout will be only available + // when the component is mounted. + const layout = { + height: new Animated.Value(0), + initHeight: 0, + initWidth: 0, + isMeasured: false, + width: new Animated.Value(0), + }; + + this.state = { + layout, + position: new Animated.Value(this.props.navigationState.index), + progress: new Animated.Value(1), + scenes: NavigationScenesReducer([], this.props.navigationState), + }; + } + + componentWillMount(): void { + this._onLayout = this._onLayout.bind(this); + this._onTransitionEnd = this._onTransitionEnd.bind(this); + } + + componentWillReceiveProps(nextProps: Props): void { + const nextScenes = NavigationScenesReducer( + this.state.scenes, + nextProps.navigationState, + this.props.navigationState + ); + + if (nextScenes === this.state.scenes) { + return; + } + + const { + position, + progress, + } = this.state; + + // update scenes. + this.setState({ + scenes: nextScenes, + }); + + // get the transition spec. + const transitionUserSpec = nextProps.configureTransition ? + nextProps.configureTransition() : + null; + + const transtionSpec = { + ...DefaultTransitionSpec, + ...transitionUserSpec, + }; + + progress.setValue(0); + + const animations = [ + Animated.timing( + progress, + { + ...transtionSpec, + toValue: 1, + }, + ), + ]; + + if (nextProps.navigationState.index !== this.props.navigationState.index) { + animations.push( + Animated.timing( + position, + { + ...transtionSpec, + toValue: nextProps.navigationState.index, + }, + ), + ); + } + + // play the transition. + nextProps.onTransitionStart && nextProps.onTransitionStart(); + Animated.parallel(animations).start(this._onTransitionEnd); + } + + render(): ReactElement { + const overlay = this._renderOverlay(); + const scenes = this._renderScenes(); + return ( + + + {scenes} + + {overlay} + + ); + } + + _renderScenes(): Array { + return this.state.scenes.map(this._renderScene, this); + } + + _renderScene(scene: NavigationScene): ?ReactElement { + const { + navigationState, + onNavigate, + renderScene, + } = this.props; + + const { + position, + progress, + scenes, + } = this.state; + + return renderScene({ + layout: this.state.layout, + navigationState, + onNavigate, + position, + progress, + scene, + scenes, + }); + } + + _renderOverlay(): ?ReactElement { + if (this.props.renderOverlay) { + const { + navigationState, + onNavigate, + renderOverlay, + } = this.props; + + const { + position, + progress, + scenes, + } = this.state; + + return renderOverlay({ + layout: this.state.layout, + navigationState, + onNavigate, + position, + progress, + scene: scenes[navigationState.index], + scenes, + }); + } + return null; + } + + _onLayout(event: any): void { + const {height, width} = event.nativeEvent.layout; + + const layout = { + ...this.state.layout, + initHeight: height, + initWidth: width, + isMeasured: true, + }; + + layout.height.setValue(height); + layout.width.setValue(width); + + this.setState({ layout }); + } + + _onTransitionEnd(): void { + const scenes = this.state.scenes.filter(isSceneNotStale); + if (scenes.length !== this.state.scenes.length) { + this.setState({ scenes }); + } + this.props.onTransitionEnd && this.props.onTransitionEnd(); + } +} + +const styles = StyleSheet.create({ + scenes: { + flex: 1, + }, +}); + +module.exports = NavigationTransitioner; diff --git a/Libraries/NavigationExperimental/NavigationTypeDefinition.js b/Libraries/NavigationExperimental/NavigationTypeDefinition.js index f0dff93ed9d5bd..8ee36194ac1418 100644 --- a/Libraries/NavigationExperimental/NavigationTypeDefinition.js +++ b/Libraries/NavigationExperimental/NavigationTypeDefinition.js @@ -41,8 +41,6 @@ export type NavigationLayout = { width: NavigationAnimatedValue, }; -export type NavigationPosition = NavigationAnimatedValue; - export type NavigationScene = { index: number, isStale: boolean, @@ -61,7 +59,14 @@ export type NavigationSceneRendererProps = { onNavigate: NavigationActionCaller, // The progressive index of the containing view's navigation state. - position: NavigationPosition, + position: NavigationAnimatedValue, + + // The value that represents the progress of the transition when navigation + // state changes from one to another. Its numberic value will range from 0 + // to 1. + // progress.__getAnimatedValue() < 1 : transtion is happening. + // progress.__getAnimatedValue() == 1 : transtion completes. + progress: NavigationAnimatedValue, // The scene to render. scene: NavigationScene, @@ -85,6 +90,12 @@ export type NavigationPanPanHandlers = { onStartShouldSetResponderCapture: Function, }; +export type NavigationTransitionSpec = { + duration?: number, + // An easing function from `Easing`. + easing?: () => any, +}; + // Functions. export type NavigationActionCaller = Function; @@ -112,3 +123,5 @@ export type NavigationSceneRenderer = ( export type NavigationStyleInterpolator = ( props: NavigationSceneRendererProps, ) => Object; + +export type NavigationTransitionConfigurator = () => NavigationTransitionSpec; diff --git a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js index 9fe5781bb38697..450e28b5cb62b0 100644 --- a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js +++ b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js @@ -24,7 +24,7 @@ const SCENE_KEY_PREFIX = 'scene_'; * Helper function to compare route keys (e.g. "9", "11"). */ function compareKey(one: string, two: string): number { - var delta = one.length - two.length; + const delta = one.length - two.length; if (delta > 0) { return 1; } @@ -72,6 +72,9 @@ function NavigationScenesReducer( nextState: NavigationParentState, prevState: ?NavigationParentState, ): Array { + if (prevState === nextState) { + return scenes; + } const prevScenes = new Map(); const freshScenes = new Map(); From dc6a44713df7f691a8b9d708cdc7e5d517806d84 Mon Sep 17 00:00:00 2001 From: Alexander Blom Date: Wed, 18 May 2016 14:23:40 -0700 Subject: [PATCH 026/843] Refactor BugReporing to avoid multiple registers Reviewed By: sahrens Differential Revision: D3316146 fbshipit-source-id: 80df4ea73150ba6920a03fe336b3ddb207ba535a --- Libraries/AppRegistry/AppRegistry.js | 2 -- Libraries/BugReporting/BugReporting.js | 11 +++++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Libraries/AppRegistry/AppRegistry.js b/Libraries/AppRegistry/AppRegistry.js index a5fa0e3056e70a..8e85c89402704e 100644 --- a/Libraries/AppRegistry/AppRegistry.js +++ b/Libraries/AppRegistry/AppRegistry.js @@ -89,9 +89,7 @@ var AppRegistry = { ', development-level warning are ' + (__DEV__ ? 'ON' : 'OFF') + ', performance optimizations are ' + (__DEV__ ? 'OFF' : 'ON'); infoLog(msg); - BugReporting.init(); BugReporting.addSource('AppRegistry.runApplication' + runCount++, () => msg); - BugReporting.addFileSource('react_hierarchy.txt', () => require('dumpReactTree')()); invariant( runnables[appKey] && runnables[appKey].run, 'Application ' + appKey + ' has not been registered. This ' + diff --git a/Libraries/BugReporting/BugReporting.js b/Libraries/BugReporting/BugReporting.js index e86eeacdcbe736..9af291d6caedba 100644 --- a/Libraries/BugReporting/BugReporting.js +++ b/Libraries/BugReporting/BugReporting.js @@ -22,6 +22,10 @@ type ExtraData = { [key: string]: string }; type SourceCallback = () => string; type DebugData = { extras: ExtraData, files: ExtraData }; +function defaultExtras() { + BugReporting.addFileSource('react_hierarchy.txt', () => require('dumpReactTree')()); +} + /** * A simple class for collecting bug report data. Components can add sources that will be queried when a bug report * is created via `collectExtraData`. For example, a list component might add a source that provides the list of rows @@ -33,13 +37,11 @@ class BugReporting { static _fileSources: Map = new Map(); static _subscription: ?EmitterSubscription = null; - /** - * `init` is called in `AppRegistry.runApplication`, so you shouldn't have to worry about it. - */ - static init() { + static _maybeInit() { if (!BugReporting._subscription) { BugReporting._subscription = RCTDeviceEventEmitter .addListener('collectBugExtraData', BugReporting.collectExtraData, null); + defaultExtras(); } } @@ -68,6 +70,7 @@ class BugReporting { } static _addSource(key: string, callback: SourceCallback, source: Map): {remove: () => void} { + BugReporting._maybeInit(); if (source.has(key)) { console.warn(`BugReporting.add* called multiple times for same key '${key}'`); } From 514677525a7b524e6f885fab10b98958942e0e1d Mon Sep 17 00:00:00 2001 From: Fred Liu Date: Wed, 18 May 2016 16:28:32 -0700 Subject: [PATCH 027/843] Prevent `SwipeableRow` bleed Summary: Default image icon size for Quick Actions could cause slideout view to bleed over the slideable view. All styling has been removed and must now be passed in, thus allowing every caller to control size for best fit. Reviewed By: furdei Differential Revision: D3315696 fbshipit-source-id: 8f8b3d2cf7a005d42a18d434d9e0080c64597be0 --- .../SwipeableQuickActionButton.js | 25 +++---------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js b/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js index f273a19a9a1d63..9f472729c867a8 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js @@ -25,7 +25,6 @@ const Image = require('Image'); const React = require('React'); -const StyleSheet = require('StyleSheet'); const Text = require('Text'); const TouchableHighlight = require('TouchableHighlight'); const View = require('View'); @@ -54,13 +53,13 @@ const SwipeableQuickActionButton = React.createClass({ onPress={this.props.onPress} testID={this.props.testID} underlayColor="transparent"> - + - + {this.props.text} @@ -69,22 +68,4 @@ const SwipeableQuickActionButton = React.createClass({ }, }); -const styles = StyleSheet.create({ - button: { - alignItems: 'center', - flex: 1, - flexDirection: 'column', - width: 76, - }, - image: { - height: 33, - width: 33, - }, - text: { - color: '#ffffff', - fontSize: 12, - textAlign: 'center', // For Android to align properly - }, -}); - module.exports = SwipeableQuickActionButton; From 838d8d4094eaf5a91e8151425da994d347999025 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Wed, 18 May 2016 17:05:50 -0700 Subject: [PATCH 028/843] adaptive render window throughput Summary: Incremental rendering is a tradeoff between throughput and responsiveness because it yields. When we have plenty of buffer (say 50% of the target), we render incrementally to keep the app responsive. If we are dangerously low on buffer (say below 25%) we always disable incremental to try to catch up as fast as possible. In between, we only disable incremental while actively scrolling since it's unlikely the user will try to press a button while scrolling. This also optimizes some things then incremental is switching back and forth. I played around with making the render window itself adaptive, but it seems pretty futile to predict - once the user decides to scroll quickly in some direction, it's pretty much too late and increasing the render window size won't help because we're already limited by the render throughput at that point. Reviewed By: ericvicenti Differential Revision: D3250916 fbshipit-source-id: 930d418522a3bf3e20083e60f6eb6f891497a2b8 --- Libraries/Experimental/Incremental.js | 14 +-- Libraries/Experimental/WindowedListView.js | 104 ++++++++++++++------- 2 files changed, 78 insertions(+), 40 deletions(-) diff --git a/Libraries/Experimental/Incremental.js b/Libraries/Experimental/Incremental.js index 8345130b99c40c..ce1583e2b9542f 100644 --- a/Libraries/Experimental/Incremental.js +++ b/Libraries/Experimental/Incremental.js @@ -14,6 +14,8 @@ const InteractionManager = require('InteractionManager'); const React = require('React'); +const infoLog = require('infoLog'); + const DEBUG = false; /** @@ -121,12 +123,12 @@ class Incremental extends React.Component { } getName(): string { - var ctx = this.context.incrementalGroup || {}; + const ctx = this.context.incrementalGroup || {}; return ctx.groupId + ':' + this._incrementId + '-' + this.props.name; } componentWillMount() { - var ctx = this.context.incrementalGroup; + const ctx = this.context.incrementalGroup; if (!ctx) { return; } @@ -134,15 +136,15 @@ class Incremental extends React.Component { InteractionManager.runAfterInteractions({ name: 'Incremental:' + this.getName(), gen: () => new Promise(resolve => { - if (!this._mounted) { + if (!this._mounted || this._rendered) { resolve(); return; } - DEBUG && console.log('set doIncrementalRender for ' + this.getName()); + DEBUG && infoLog('set doIncrementalRender for ' + this.getName()); this.setState({doIncrementalRender: true}, resolve); }), }).then(() => { - DEBUG && console.log('call onDone for ' + this.getName()); + DEBUG && infoLog('call onDone for ' + this.getName()); this._mounted && this.props.onDone && this.props.onDone(); }).catch((ex) => { ex.message = `Incremental render failed for ${this.getName()}: ${ex.message}`; @@ -154,7 +156,7 @@ class Incremental extends React.Component { if (this._rendered || // Make sure that once we render once, we stay rendered even if incrementalGroupEnabled gets flipped. !this.context.incrementalGroupEnabled || this.state.doIncrementalRender) { - DEBUG && console.log('render ' + this.getName()); + DEBUG && infoLog('render ' + this.getName()); this._rendered = true; return this.props.children; } diff --git a/Libraries/Experimental/WindowedListView.js b/Libraries/Experimental/WindowedListView.js index ba5ef3095cc880..90fbd1aac72ef3 100644 --- a/Libraries/Experimental/WindowedListView.js +++ b/Libraries/Experimental/WindowedListView.js @@ -43,6 +43,7 @@ const ViewabilityHelper = require('ViewabilityHelper'); const clamp = require('clamp'); const deepDiffer = require('deepDiffer'); +const infoLog = require('infoLog'); const invariant = require('invariant'); const nullthrows = require('nullthrows'); @@ -177,13 +178,14 @@ class WindowedListView extends React.Component { _firstVisible: number = -1; _lastVisible: number = -1; _scrollOffsetY: number = 0; + _isScrolling: boolean = false; _frameHeight: number = 0; _rowFrames: Array = []; _rowFramesDirty: boolean = false; - _hasCalledOnEndReached: bool = false; - _willComputeRowsToRender: bool = false; + _hasCalledOnEndReached: boolean = false; + _willComputeRowsToRender: boolean = false; _timeoutHandle: number = 0; - _incrementPending: bool = false; + _incrementPending: boolean = false; _viewableRows: Array = []; _cellsInProgress: Set = new Set(); _scrollRef: ?Object; @@ -196,7 +198,7 @@ class WindowedListView extends React.Component { viewablePercentThreshold: 50, renderScrollComponent: (props) => , disableIncrementalRendering: false, - recomputeRowsBatchingPeriod: 100, + recomputeRowsBatchingPeriod: 10, // This should capture most events that will happen in one frame }; constructor(props: Props) { @@ -244,8 +246,13 @@ class WindowedListView extends React.Component { } this._computeRowsToRender(newProps); } + _onMomentumScrollEnd = (e: Object) => { + this._onScroll(e); + }; _onScroll = (e: Object) => { - this._scrollOffsetY = e.nativeEvent.contentOffset.y; + const newScrollY = e.nativeEvent.contentOffset.y; + this._isScrolling = this._scrollOffsetY !== newScrollY; + this._scrollOffsetY = newScrollY; this._frameHeight = e.nativeEvent.layoutMeasurement.height; // We don't want to enqueue any updates if any cells are in the middle of an incremental render, // because it would just be wasted work. @@ -271,7 +278,7 @@ class WindowedListView extends React.Component { const {rowIndex, layout} = params; if (DEBUG) { const layoutPrev = this._rowFrames[rowIndex] || {}; - console.log( + infoLog( 'record layout for row: ', {i: rowIndex, h: layout.height, y: layout.y, x: layout.x, hp: layoutPrev.height, yp: layoutPrev.y} ); @@ -404,7 +411,10 @@ class WindowedListView extends React.Component { if (this._rowFramesDirty || rowsShouldChange) { if (rowsShouldChange) { this.props.onMountedRowsWillChange && this.props.onMountedRowsWillChange(firstRow, lastRow - firstRow + 1); - console.log('WLV: row render range will change:', {firstRow, lastRow}); + infoLog( + 'WLV: row render range will change:', + {firstRow, firstVis: this._firstVisible, lastVis: this._lastVisible, lastRow}, + ); } this._rowFramesDirty = false; this.setState({firstRow, lastRow}); @@ -442,7 +452,7 @@ class WindowedListView extends React.Component { showIndicator = true; spacerHeight -= this.state.boundaryIndicatorHeight || 0; } - DEBUG && console.log('render top spacer with height ', spacerHeight); + DEBUG && infoLog('render top spacer with height ', spacerHeight); rows.push(); if (this.props.renderWindowBoundaryIndicator) { // Always render it, even if removed, so that we can get the height right away and don't waste time creating/ @@ -461,6 +471,17 @@ class WindowedListView extends React.Component { ); } + // Incremental rendering is a tradeoff between throughput and responsiveness. When we have plenty of buffer (say 50% + // of the target), we render incrementally to keep the app responsive. If we are dangerously low on buffer (say + // below 25%) we always disable incremental to try to catch up as fast as possible. In the middle, we only disable + // incremental while scrolling since it's unlikely the user will try to press a button while scrolling. We also + // ignore the "buffer" size when we are bumped up against the edge of the available data. + const firstBuffer = firstRow === 0 ? Infinity : this._firstVisible - firstRow; + const lastBuffer = lastRow === this.props.data.length - 1 ? Infinity : lastRow - this._lastVisible; + const minBuffer = Math.min(firstBuffer, lastBuffer); + const disableIncrementalRendering = this.props.disableIncrementalRendering || + (this._isScrolling && minBuffer < this.props.numToRenderAhead * 0.5) || + (minBuffer < this.props.numToRenderAhead * 0.25); for (let idx = firstRow; idx <= lastRow; idx++) { const key = '' + (this.props.enableDangerousRecycling ? (idx % this.props.maxNumToRender) : idx); rows.push( @@ -470,7 +491,7 @@ class WindowedListView extends React.Component { rowIndex={idx} onNewLayout={this._onNewLayout} onWillUnmount={this._onWillUnmountCell} - includeInLayout={this.props.disableIncrementalRendering || + includeInLayout={disableIncrementalRendering || (this._rowFrames[idx] && this._rowFrames[idx].offscreenLayoutDone)} onProgressChange={this._onProgressChange} asyncRowPerfEventName={this.props.asyncRowPerfEventName} @@ -517,6 +538,7 @@ class WindowedListView extends React.Component { contentInset, ref: (ref) => { this._scrollRef = ref; }, onScroll: this._onScroll, + onMomentumScrollEnd: this._onMomentumScrollEnd, children: rows, })} @@ -570,8 +592,9 @@ type CellProps = { }; class CellRenderer extends React.Component { props: CellProps; + _containerRef: View; _offscreenRenderDone = false; - _timer = 0; + _timeout = 0; _lastLayout: ?Object = null; _perfUpdateID: number = 0; _asyncCookie: any; @@ -580,7 +603,7 @@ class CellRenderer extends React.Component { if (this.props.asyncRowPerfEventName) { this._perfUpdateID = g_perf_update_id++; this._asyncCookie = Systrace.beginAsyncEvent(this.props.asyncRowPerfEventName + this._perfUpdateID); - console.log(`perf_asynctest_${this.props.asyncRowPerfEventName}_start ${this._perfUpdateID} ${Date.now()}`); + infoLog(`perf_asynctest_${this.props.asyncRowPerfEventName}_start ${this._perfUpdateID} ${Date.now()}`); } if (this.props.includeInLayout) { this._includeInLayoutLatch = true; @@ -599,29 +622,36 @@ class CellRenderer extends React.Component { layout, }); }; - _onOffscreenRenderDone = () => { - DEBUG && console.log('_onOffscreenRenderDone for row ' + this.props.rowIndex); - this._timer = setTimeout(() => { // Flush any pending layout events. - invariant(!this._offscreenRenderDone, 'should only finish rendering once'); - this._offscreenRenderDone = true; + _updateParent() { + invariant(!this._offscreenRenderDone, 'should only finish rendering once'); + this._offscreenRenderDone = true; - // If this is not called before calling onNewLayout, the number of inProgress cells will remain non-zero, - // and thus the onNewLayout call will not fire the needed state change update. - this.props.onProgressChange({rowIndex: this.props.rowIndex, inProgress: false}); + // If this is not called before calling onNewLayout, the number of inProgress cells will remain non-zero, + // and thus the onNewLayout call will not fire the needed state change update. + this.props.onProgressChange({rowIndex: this.props.rowIndex, inProgress: false}); - // If an onLayout event hasn't come in yet, then we skip here and assume it will come in later. This happens - // when Incremental is disabled and _onOffscreenRenderDone is called faster than layout can happen. - this._lastLayout && this.props.onNewLayout({rowIndex: this.props.rowIndex, layout: this._lastLayout}); + // If an onLayout event hasn't come in yet, then we skip here and assume it will come in later. This happens + // when Incremental is disabled and _onOffscreenRenderDone is called faster than layout can happen. + this._lastLayout && this.props.onNewLayout({rowIndex: this.props.rowIndex, layout: this._lastLayout}); - DEBUG && console.log('\n >>>>> display row ' + this.props.rowIndex + '\n\n\n'); - if (this.props.asyncRowPerfEventName) { - Systrace.endAsyncEvent(this.props.asyncRowPerfEventName + this._perfUpdateID, this._asyncCookie); - console.log(`perf_asynctest_${this.props.asyncRowPerfEventName}_end ${this._perfUpdateID} ${Date.now()}`); - } - }, 1); + DEBUG && infoLog('\n >>>>> display row ' + this.props.rowIndex + '\n\n\n'); + if (this.props.asyncRowPerfEventName) { + // Note this doesn't include the native render time but is more accurate than also including the JS render + // time of anything that has been queued up. + Systrace.endAsyncEvent(this.props.asyncRowPerfEventName + this._perfUpdateID, this._asyncCookie); + infoLog(`perf_asynctest_${this.props.asyncRowPerfEventName}_end ${this._perfUpdateID} ${Date.now()}`); + } + } + _onOffscreenRenderDone = () => { + DEBUG && infoLog('_onOffscreenRenderDone for row ' + this.props.rowIndex); + if (this._includeInLayoutLatch) { + this._updateParent(); // rendered straight into layout, so no need to flush + } else { + this._timeout = setTimeout(() => this._updateParent(), 1); // Flush any pending layout events. + } }; componentWillUnmount() { - clearTimeout(this._timer); + clearTimeout(this._timeout); this.props.onProgressChange({rowIndex: this.props.rowIndex, inProgress: false}); this.props.onWillUnmount(this.props.rowIndex); } @@ -629,26 +659,32 @@ class CellRenderer extends React.Component { if (newProps.includeInLayout && !this.props.includeInLayout) { invariant(this._offscreenRenderDone, 'Should never try to add to layout before render done'); this._includeInLayoutLatch = true; // Once we render in layout, make sure it sticks. - this.refs.container.setNativeProps({style: styles.include}); + this._containerRef.setNativeProps({style: styles.include}); } } shouldComponentUpdate(newProps) { return newProps.data !== this.props.data; } + _setRef = (ref) => { + this._containerRef = ref; + }; render() { let debug; if (DEBUG) { - console.log('render cell ' + this.props.rowIndex); + infoLog('render cell ' + this.props.rowIndex); const Text = require('Text'); debug = Row: {this.props.rowIndex} ; } - const style = (this._includeInLayoutLatch || this.props.includeInLayout) ? styles.include : styles.remove; + const style = this._includeInLayoutLatch ? styles.include : styles.remove; return ( - + {debug} From 8a201b842af31423a2fb6dce662504ec45e59476 Mon Sep 17 00:00:00 2001 From: Leonardo Tegon Date: Wed, 18 May 2016 19:36:21 -0700 Subject: [PATCH 029/843] Fixes syntax typo in DirectManipulation section of docs Summary: Fixing JSX tag in DirectManipulation section of the docs Closes https://github.com/facebook/react-native/pull/7622 Differential Revision: D3321130 fbshipit-source-id: e315529dc94c88597e4855a63ba6931301d4dba7 --- docs/DirectManipulation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DirectManipulation.md b/docs/DirectManipulation.md index f705eda39dc1eb..c3fb6f7f871255 100644 --- a/docs/DirectManipulation.md +++ b/docs/DirectManipulation.md @@ -49,7 +49,7 @@ any knowledge of that fact or requiring any changes to its implementation: Press me! - + ``` Let's imagine that `setNativeProps` was not available. One way that we From 6a34c9c3da0646253f83563bb50b54fd15523e8d Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Thu, 19 May 2016 03:44:09 -0700 Subject: [PATCH 030/843] Disabled flow in e2e test because it causes memory crashes Summary: Closes https://github.com/facebook/react-native/pull/7620 Differential Revision: D3322029 fbshipit-source-id: e0406770d011956af6b47d15c5b49a68b1b611f5 --- circle.yml | 4 ++-- scripts/run-ci-e2e-tests.js | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/circle.yml b/circle.yml index 4aa1e4405eeb28..ad2d1bb998f207 100644 --- a/circle.yml +++ b/circle.yml @@ -48,9 +48,9 @@ test: override: # eslint bot - - cat <(echo eslint; npm run lint --silent -- --format=json; echo flow; npm run flow --silent -- check --json) | GITHUB_TOKEN="af6ef0d15709bc91d""06a6217a5a826a226fb57b7" CI_USER=$CIRCLE_PROJECT_USERNAME CI_REPO=$CIRCLE_PROJECT_REPONAME PULL_REQUEST_NUMBER=$CIRCLE_PR_NUMBER node bots/code-analysis-bot.js + # - cat <(echo eslint; npm run lint --silent -- --format=json; echo flow; npm run flow --silent -- check --json) | GITHUB_TOKEN="af6ef0d15709bc91d""06a6217a5a826a226fb57b7" CI_USER=$CIRCLE_PROJECT_USERNAME CI_REPO=$CIRCLE_PROJECT_REPONAME PULL_REQUEST_NUMBER=$CIRCLE_PR_NUMBER node bots/code-analysis-bot.js # JS tests for dependencies installed with npm3 - - npm run flow check + # - npm run flow check - npm test -- --maxWorkers=1 # build app diff --git a/scripts/run-ci-e2e-tests.js b/scripts/run-ci-e2e-tests.js index cf3534b6408d6e..8633be8b7425e4 100644 --- a/scripts/run-ci-e2e-tests.js +++ b/scripts/run-ci-e2e-tests.js @@ -195,11 +195,12 @@ try { exitCode = 1; throw Error(exitCode); } - if (exec(`${ROOT}/node_modules/.bin/flow check`).code) { - echo('Flow check does not pass'); - exitCode = 1; - throw Error(exitCode); - } +// TODO disabled while flow 0.25.0 is crashing + // if (exec(`${ROOT}/node_modules/.bin/flow check`).code) { + // echo('Flow check does not pass'); + // exitCode = 1; + // throw Error(exitCode); + // } } exitCode = 0; From 942b9332b2e9eb779e84d996648e66550965af82 Mon Sep 17 00:00:00 2001 From: Alexander Blom Date: Thu, 19 May 2016 12:49:53 -0700 Subject: [PATCH 031/843] Make BugReporting not pull in native module on require Reviewed By: astreet Differential Revision: D3322838 fbshipit-source-id: 4f12fda213b6a4ab7b19d3dafad7f31eb627f85a --- Libraries/BugReporting/BugReporting.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/BugReporting/BugReporting.js b/Libraries/BugReporting/BugReporting.js index 9af291d6caedba..277f16b45e1186 100644 --- a/Libraries/BugReporting/BugReporting.js +++ b/Libraries/BugReporting/BugReporting.js @@ -11,7 +11,6 @@ */ 'use strict'; -const BugReportingNativeModule = require('NativeModules').BugReporting; const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); const Map = require('Map'); const infoLog = require('infoLog'); @@ -94,6 +93,7 @@ class BugReporting { fileData[key] = callback(); } infoLog('BugReporting extraData:', extraData); + const BugReportingNativeModule = require('NativeModules').BugReporting; BugReportingNativeModule && BugReportingNativeModule.setExtraData && BugReportingNativeModule.setExtraData(extraData, fileData); From 5e91a2a3124a76e5216975497e63c04e012aa0d1 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Thu, 19 May 2016 14:44:39 -0700 Subject: [PATCH 032/843] Fix 95% of WindowedListView jumpiness Summary: - Replace some fixes that were accidentally lost in local rebase that prevent jumpiness when incremental is disabled. - Require each row to have a key specified by the caller to prevent jumping because of accidental duplicates or unneeded/problematic row re-rendering because of legit re-ordering. Reviewed By: steveluscher Differential Revision: D3322006 fbshipit-source-id: 0019aab07cb1fe2b148a14b5818de53aa373eb50 --- Libraries/Experimental/IncrementalGroup.js | 6 +- Libraries/Experimental/ViewabilityHelper.js | 17 +- Libraries/Experimental/WindowedListView.js | 185 +++++++++++--------- 3 files changed, 114 insertions(+), 94 deletions(-) diff --git a/Libraries/Experimental/IncrementalGroup.js b/Libraries/Experimental/IncrementalGroup.js index 27b92a07709bfb..2c03624092480a 100644 --- a/Libraries/Experimental/IncrementalGroup.js +++ b/Libraries/Experimental/IncrementalGroup.js @@ -14,6 +14,8 @@ const Incremental = require('Incremental'); const React = require('React'); +const infoLog = require('infoLog'); + let _groupCounter = -1; const DEBUG = false; @@ -31,12 +33,12 @@ import type {Props, Context} from 'Incremental'; * See Incremental.js for more info. */ class IncrementalGroup extends React.Component { - props: Props; + props: Props & {disabled?: boolean}; context: Context; _groupInc: string; componentWillMount() { this._groupInc = `g${++_groupCounter}-`; - DEBUG && console.log( + DEBUG && infoLog( 'create IncrementalGroup with id ' + this.getGroupId() ); } diff --git a/Libraries/Experimental/ViewabilityHelper.js b/Libraries/Experimental/ViewabilityHelper.js index e8ced0e61e037f..90798e1e68f73b 100644 --- a/Libraries/Experimental/ViewabilityHelper.js +++ b/Libraries/Experimental/ViewabilityHelper.js @@ -20,19 +20,20 @@ const ViewabilityHelper = { computeViewableRows( viewablePercentThreshold: number, - rowFrames: Array, + rowFrames: {[key: string]: Object}, + data: Array<{rowKey: string, rowData: any}>, scrollOffsetY: number, viewportHeight: number ): Array { - var viewableRows = []; - var firstVisible = -1; - for (var idx = 0; idx < rowFrames.length; idx++) { - var frame = rowFrames[idx]; + const viewableRows = []; + let firstVisible = -1; + for (let idx = 0; idx < data.length; idx++) { + const frame = rowFrames[data[idx].rowKey]; if (!frame) { continue; } - var top = frame.y - scrollOffsetY; - var bottom = top + frame.height; + const top = frame.y - scrollOffsetY; + const bottom = top + frame.height; if ((top < viewportHeight) && (bottom > 0)) { firstVisible = idx; if (_isViewable( @@ -67,7 +68,7 @@ function _getPercentOccupied( bottom: number, viewportHeight: number ): number { - var visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0); + let visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0); visibleHeight = Math.max(0, visibleHeight); return Math.max(0, visibleHeight * 100 / viewportHeight); } diff --git a/Libraries/Experimental/WindowedListView.js b/Libraries/Experimental/WindowedListView.js index 90fbd1aac72ef3..e0e261de8d62f1 100644 --- a/Libraries/Experimental/WindowedListView.js +++ b/Libraries/Experimental/WindowedListView.js @@ -84,13 +84,13 @@ type Props = { * A simple array of data blobs that are passed to the renderRow function in * order. Note there is no dataSource like in the standard `ListView`. */ - data: Array; + data: Array<{rowKey: string, rowData: any}>; /** * Takes a data blob from the `data` array prop plus some meta info and should * return a row. */ renderRow: ( - data: any, sectionIdx: number, rowIdx: number, key?: string + rowData: any, sectionIdx: number, rowIdx: number, rowKey: string ) => ?ReactElement; /** * Rendered when the list is scrolled faster than rows can be rendered. @@ -131,13 +131,6 @@ type Props = { * determine how many rows to render above the viewport. */ numToRenderAhead: number; - /** - * Super dangerous and experimental - rows and all their decendents must be - * fully stateless otherwise recycling their instances may introduce nasty - * bugs. Some apps may see an improvement in perf, but sometimes perf and - * memory usage can actually get worse with this. - */ - enableDangerousRecycling: boolean; /** * Used to log perf events for async row rendering. */ @@ -180,18 +173,18 @@ class WindowedListView extends React.Component { _scrollOffsetY: number = 0; _isScrolling: boolean = false; _frameHeight: number = 0; - _rowFrames: Array = []; + _rowFrames: {[key: string]: Object} = {}; + _rowRenderMode: {[key: string]: null | 'async' | 'sync'} = {}; _rowFramesDirty: boolean = false; _hasCalledOnEndReached: boolean = false; _willComputeRowsToRender: boolean = false; _timeoutHandle: number = 0; _incrementPending: boolean = false; _viewableRows: Array = []; - _cellsInProgress: Set = new Set(); + _cellsInProgress: Set = new Set(); _scrollRef: ?Object; static defaultProps = { - enableDangerousRecycling: false, initialNumToRender: 10, maxNumToRender: 30, numToRenderAhead: 10, @@ -241,9 +234,6 @@ class WindowedListView extends React.Component { componentWillReceiveProps(newProps: Object) { // This has to happen immediately otherwise we could crash, e.g. if the data // array has gotten shorter. - if (newProps.data.length < this._rowFrames.length) { - this._rowFrames = this._rowFrames.splice(0, newProps.data.length); - } this._computeRowsToRender(newProps); } _onMomentumScrollEnd = (e: Object) => { @@ -259,10 +249,11 @@ class WindowedListView extends React.Component { if (this._cellsInProgress.size === 0) { this._enqueueComputeRowsToRender(); } - if (this.props.onViewableRowsChanged && this._rowFrames.length) { + if (this.props.onViewableRowsChanged && Object.keys(this._rowFrames).length) { const viewableRows = ViewabilityHelper.computeViewableRows( this.props.viewablePercentThreshold, this._rowFrames, + this.props.data, e.nativeEvent.contentOffset.y, e.nativeEvent.layoutMeasurement.height ); @@ -274,35 +265,44 @@ class WindowedListView extends React.Component { this.props.onScroll && this.props.onScroll(e); }; // Caller does the diffing so we don't have to. - _onNewLayout = (params: {rowIndex: number, layout: Object}) => { - const {rowIndex, layout} = params; + _onNewLayout = (params: {rowKey: string, layout: Object}) => { + const {rowKey, layout} = params; if (DEBUG) { - const layoutPrev = this._rowFrames[rowIndex] || {}; + const layoutPrev = this._rowFrames[rowKey] || {}; infoLog( 'record layout for row: ', - {i: rowIndex, h: layout.height, y: layout.y, x: layout.x, hp: layoutPrev.height, yp: layoutPrev.y} + {k: rowKey, h: layout.height, y: layout.y, x: layout.x, hp: layoutPrev.height, yp: layoutPrev.y} ); } - this._rowFrames[rowIndex] = {...layout, offscreenLayoutDone: true}; + if (this._rowFrames[rowKey]) { + const deltaY = Math.abs(this._rowFrames[rowKey].y - layout.y); + const deltaH = Math.abs(this._rowFrames[rowKey].height - layout.height); + if (deltaY > 2 || deltaH > 2) { + const dataEntry = this.props.data.find((datum) => datum.rowKey === rowKey); + console.warn('layout jump: ', {dataEntry, prevLayout: this._rowFrames[rowKey], newLayout: layout}); + } + } + this._rowFrames[rowKey] = {...layout, offscreenLayoutDone: true}; this._rowFramesDirty = true; if (this._cellsInProgress.size === 0) { this._enqueueComputeRowsToRender(); } }; - _onWillUnmountCell = (rowIndex: number) => { - if (this._rowFrames[rowIndex]) { - this._rowFrames[rowIndex].offscreenLayoutDone = false; + _onWillUnmountCell = (rowKey: string) => { + if (this._rowFrames[rowKey]) { + this._rowFrames[rowKey].offscreenLayoutDone = false; + this._rowRenderMode[rowKey] = null; } }; /** * This is used to keep track of cells that are in the process of rendering. If any cells are in progress, then * other updates are skipped because they will just be wasted work. */ - _onProgressChange = ({rowIndex, inProgress}: {rowIndex: number, inProgress: boolean}) => { + _onProgressChange = ({rowKey, inProgress}: {rowKey: string, inProgress: boolean}) => { if (inProgress) { - this._cellsInProgress.add(rowIndex); + this._cellsInProgress.add(rowKey); } else { - this._cellsInProgress.delete(rowIndex); + this._cellsInProgress.delete(rowKey); } }; /** @@ -339,10 +339,11 @@ class WindowedListView extends React.Component { const rowFrames = this._rowFrames; let firstVisible = -1; let lastVisible = 0; + let lastRow = this.state.lastRow; const top = this._scrollOffsetY; const bottom = top + this._frameHeight; - for (let idx = 0; idx < rowFrames.length; idx++) { - const frame = rowFrames[idx]; + for (let idx = 0; idx < lastRow; idx++) { + const frame = rowFrames[props.data[idx].rowKey]; if (!frame) { // No frame - sometimes happens when they come out of order, so just wait for the rest. return; @@ -364,14 +365,13 @@ class WindowedListView extends React.Component { // Unfortuantely, we can't use to simplify our increment logic in this function because we need to // make sure that cells are rendered in the right order one at a time when scrolling back up. - const numRendered = this.state.lastRow - this.state.firstRow + 1; + const numRendered = lastRow - this.state.firstRow + 1; // Our last row target that we will approach incrementally const targetLastRow = clamp( numRendered - 1, // Don't reduce numRendered when scrolling back up lastVisible + props.numToRenderAhead, // Primary goal totalRows - 1, // Don't render past the end ); - let lastRow = this.state.lastRow; // Increment the last row one at a time per JS event loop if (!this._incrementPending) { if (targetLastRow > this.state.lastRow) { @@ -410,7 +410,7 @@ class WindowedListView extends React.Component { const rowsShouldChange = firstRow !== this.state.firstRow || lastRow !== this.state.lastRow; if (this._rowFramesDirty || rowsShouldChange) { if (rowsShouldChange) { - this.props.onMountedRowsWillChange && this.props.onMountedRowsWillChange(firstRow, lastRow - firstRow + 1); + props.onMountedRowsWillChange && props.onMountedRowsWillChange(firstRow, lastRow - firstRow + 1); infoLog( 'WLV: row render range will change:', {firstRow, firstVis: this._firstVisible, lastVis: this._lastVisible, lastRow}, @@ -435,15 +435,40 @@ class WindowedListView extends React.Component { const rowFrames = this._rowFrames; const rows = []; let spacerHeight = 0; + // Incremental rendering is a tradeoff between throughput and responsiveness. When we have plenty of buffer (say 50% + // of the target), we render incrementally to keep the app responsive. If we are dangerously low on buffer (say + // below 25%) we always disable incremental to try to catch up as fast as possible. In the middle, we only disable + // incremental while scrolling since it's unlikely the user will try to press a button while scrolling. We also + // ignore the "buffer" size when we are bumped up against the edge of the available data. + const firstBuffer = firstRow === 0 ? Infinity : this._firstVisible - firstRow; + const lastBuffer = lastRow === this.props.data.length - 1 ? Infinity : lastRow - this._lastVisible; + const minBuffer = Math.min(firstBuffer, lastBuffer); + const disableIncrementalRendering = this.props.disableIncrementalRendering || + (this._isScrolling && minBuffer < this.props.numToRenderAhead * 0.5) || + (minBuffer < this.props.numToRenderAhead * 0.25); + // Render mode is sticky while the component is mounted. + for (let ii = firstRow; ii <= lastRow; ii++) { + const rowKey = this.props.data[ii].rowKey; + if (this._rowRenderMode[rowKey] === 'sync' || (disableIncrementalRendering && this._rowRenderMode[rowKey] !== 'async')) { + this._rowRenderMode[rowKey] = 'sync'; + } else { + this._rowRenderMode[rowKey] = 'async'; + } + } for (let ii = firstRow; ii <= lastRow; ii++) { - if (!rowFrames[ii]) { + const rowKey = this.props.data[ii].rowKey; + if (!rowFrames[rowKey]) { break; // if rowFrame missing, no following ones will exist so quit early } - // Look for the first row where offscreen layout is done (only true for mounted rows) and set the spacer height - // such that it will offset all the unmounted rows before that one using the saved frame data. - if (rowFrames[ii].offscreenLayoutDone) { - const frame = rowFrames[ii - 1]; - spacerHeight = frame ? frame.y + frame.height : 0; + // Look for the first row where offscreen layout is done (only true for mounted rows) or it will be rendered + // synchronously and set the spacer height such that it will offset all the unmounted rows before that one using + // the saved frame data. + if (rowFrames[rowKey].offscreenLayoutDone || this._rowRenderMode[rowKey] === 'sync') { + if (ii > 0) { + const prevRowKey = this.props.data[ii - 1].rowKey; + const frame = rowFrames[prevRowKey]; + spacerHeight = frame ? frame.y + frame.height : 0; + } break; } } @@ -471,37 +496,28 @@ class WindowedListView extends React.Component { ); } - // Incremental rendering is a tradeoff between throughput and responsiveness. When we have plenty of buffer (say 50% - // of the target), we render incrementally to keep the app responsive. If we are dangerously low on buffer (say - // below 25%) we always disable incremental to try to catch up as fast as possible. In the middle, we only disable - // incremental while scrolling since it's unlikely the user will try to press a button while scrolling. We also - // ignore the "buffer" size when we are bumped up against the edge of the available data. - const firstBuffer = firstRow === 0 ? Infinity : this._firstVisible - firstRow; - const lastBuffer = lastRow === this.props.data.length - 1 ? Infinity : lastRow - this._lastVisible; - const minBuffer = Math.min(firstBuffer, lastBuffer); - const disableIncrementalRendering = this.props.disableIncrementalRendering || - (this._isScrolling && minBuffer < this.props.numToRenderAhead * 0.5) || - (minBuffer < this.props.numToRenderAhead * 0.25); for (let idx = firstRow; idx <= lastRow; idx++) { - const key = '' + (this.props.enableDangerousRecycling ? (idx % this.props.maxNumToRender) : idx); + const rowKey = this.props.data[idx].rowKey; + const includeInLayout = this._rowRenderMode[rowKey] === 'sync' || + (this._rowFrames[rowKey] && this._rowFrames[rowKey].offscreenLayoutDone); rows.push( ); } - const showFooter = this._rowFrames[lastRow] && - this._rowFrames[lastRow].offscreenLayoutDone && + const lastRowKey = this.props.data[lastRow].rowKey; + const showFooter = this._rowFrames[lastRowKey] && + this._rowFrames[lastRowKey].offscreenLayoutDone && lastRow === this.props.data.length - 1; if (this.props.renderFooter) { rows.push( @@ -530,18 +546,16 @@ class WindowedListView extends React.Component { // Prevent user from scrolling into empty space of unmounted rows. const contentInset = {top: firstRow === 0 ? 0 : -spacerHeight}; return ( - - {this.props.renderScrollComponent({ - scrollEventThrottle: 50, - removeClippedSubviews: true, - ...this.props, - contentInset, - ref: (ref) => { this._scrollRef = ref; }, - onScroll: this._onScroll, - onMomentumScrollEnd: this._onMomentumScrollEnd, - children: rows, - })} - + this.props.renderScrollComponent({ + scrollEventThrottle: 50, + removeClippedSubviews: true, + ...this.props, + contentInset, + ref: (ref) => { this._scrollRef = ref; }, + onScroll: this._onScroll, + onMomentumScrollEnd: this._onMomentumScrollEnd, + children: rows, + }) ); } } @@ -553,11 +567,14 @@ type CellProps = { /** * Row-specific data passed to renderRow and used in shouldComponentUpdate with === */ - data: mixed; + rowData: mixed; + rowKey: string; /** * Renders the actual row contents. */ - renderRow: (data: mixed, sectionIdx: number, rowIdx: number) => ?ReactElement; + renderRow: ( + rowData: mixed, sectionIdx: number, rowIdx: number, rowKey: string + ) => ?ReactElement; /** * Index of the row, passed through to other callbacks. */ @@ -579,16 +596,16 @@ type CellProps = { * Updates the parent with the latest layout. Only called when incremental rendering is done and triggers the parent * to re-render this row with includeInLayout true. */ - onNewLayout: (params: {rowIndex: number, layout: Object}) => void; + onNewLayout: (params: {rowKey: string, layout: Object}) => void; /** * Used to track when rendering is in progress so the parent can avoid wastedful re-renders that are just going to be * invalidated once the cell finishes. */ - onProgressChange: (progress: {rowIndex: number; inProgress: boolean}) => void; + onProgressChange: (progress: {rowKey: string; inProgress: boolean}) => void; /** * Used to invalidate the layout so the parent knows it needs to compensate for the height in the placeholder size. */ - onWillUnmount: (rowIndex: number) => void; + onWillUnmount: (rowKey: string) => void; }; class CellRenderer extends React.Component { props: CellProps; @@ -608,7 +625,7 @@ class CellRenderer extends React.Component { if (this.props.includeInLayout) { this._includeInLayoutLatch = true; } - this.props.onProgressChange({rowIndex: this.props.rowIndex, inProgress: true}); + this.props.onProgressChange({rowKey: this.props.rowKey, inProgress: true}); } _onLayout = (e) => { const layout = e.nativeEvent.layout; @@ -618,7 +635,7 @@ class CellRenderer extends React.Component { return; // Don't send premature or duplicate updates } this.props.onNewLayout({ - rowIndex: this.props.rowIndex, + rowKey: this.props.rowKey, layout, }); }; @@ -628,11 +645,11 @@ class CellRenderer extends React.Component { // If this is not called before calling onNewLayout, the number of inProgress cells will remain non-zero, // and thus the onNewLayout call will not fire the needed state change update. - this.props.onProgressChange({rowIndex: this.props.rowIndex, inProgress: false}); + this.props.onProgressChange({rowKey: this.props.rowKey, inProgress: false}); // If an onLayout event hasn't come in yet, then we skip here and assume it will come in later. This happens // when Incremental is disabled and _onOffscreenRenderDone is called faster than layout can happen. - this._lastLayout && this.props.onNewLayout({rowIndex: this.props.rowIndex, layout: this._lastLayout}); + this._lastLayout && this.props.onNewLayout({rowKey: this.props.rowKey, layout: this._lastLayout}); DEBUG && infoLog('\n >>>>> display row ' + this.props.rowIndex + '\n\n\n'); if (this.props.asyncRowPerfEventName) { @@ -652,8 +669,8 @@ class CellRenderer extends React.Component { }; componentWillUnmount() { clearTimeout(this._timeout); - this.props.onProgressChange({rowIndex: this.props.rowIndex, inProgress: false}); - this.props.onWillUnmount(this.props.rowIndex); + this.props.onProgressChange({rowKey: this.props.rowKey, inProgress: false}); + this.props.onWillUnmount(this.props.rowKey); } componentWillReceiveProps(newProps) { if (newProps.includeInLayout && !this.props.includeInLayout) { @@ -662,8 +679,8 @@ class CellRenderer extends React.Component { this._containerRef.setNativeProps({style: styles.include}); } } - shouldComponentUpdate(newProps) { - return newProps.data !== this.props.data; + shouldComponentUpdate(newProps: CellProps) { + return newProps.rowData !== this.props.rowData; } _setRef = (ref) => { this._containerRef = ref; @@ -680,7 +697,7 @@ class CellRenderer extends React.Component { const style = this._includeInLayoutLatch ? styles.include : styles.remove; return ( {debug} - {this.props.renderRow(this.props.data, 0, this.props.rowIndex)} + {this.props.renderRow(this.props.rowData, 0, this.props.rowIndex, this.props.rowKey)} {debug} From 830197847ad8678f45baaea4cf64291a4909f3ee Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Thu, 19 May 2016 18:52:46 -0700 Subject: [PATCH 033/843] Move more new bridge code into OSS Reviewed By: mhorowitz Differential Revision: D3296231 fbshipit-source-id: 5a05b1ddacdfecda067a08398dd5652df76c1f14 --- ReactAndroid/src/main/jni/xreact/jni/BUCK | 48 +++ .../jni/xreact/jni/CatalystInstanceImpl.cpp | 228 ++++++++++++ .../jni/xreact/jni/CatalystInstanceImpl.h | 67 ++++ .../main/jni/xreact/jni/CxxModuleWrapper.cpp | 219 +++++++++++ .../main/jni/xreact/jni/CxxModuleWrapper.h | 53 +++ .../src/main/jni/xreact/jni/JCallback.h | 44 +++ .../main/jni/xreact/jni/JExecutorToken.cpp | 25 ++ .../src/main/jni/xreact/jni/JExecutorToken.h | 60 +++ .../jni/xreact/jni/JMessageQueueThread.cpp | 66 ++++ .../main/jni/xreact/jni/JMessageQueueThread.h | 60 +++ .../src/main/jni/xreact/jni/JNativeRunnable.h | 44 +++ .../main/jni/xreact/jni/JSCPerfLogging.cpp | 244 +++++++++++++ .../src/main/jni/xreact/jni/JSCPerfLogging.h | 11 + .../src/main/jni/xreact/jni/JSLoader.cpp | 99 +++++ .../src/main/jni/xreact/jni/JSLoader.h | 33 ++ .../src/main/jni/xreact/jni/JSLogging.cpp | 36 ++ .../src/main/jni/xreact/jni/JSLogging.h | 15 + .../jni/xreact/jni/JavaScriptExecutorHolder.h | 29 ++ .../jni/xreact/jni/JniJSModulesUnbundle.cpp | 83 +++++ .../jni/xreact/jni/JniJSModulesUnbundle.h | 31 ++ .../src/main/jni/xreact/jni/JniWebWorkers.h | 32 ++ .../src/main/jni/xreact/jni/MethodInvoker.cpp | 230 ++++++++++++ .../src/main/jni/xreact/jni/MethodInvoker.h | 37 ++ .../jni/xreact/jni/ModuleRegistryHolder.cpp | 344 ++++++++++++++++++ .../jni/xreact/jni/ModuleRegistryHolder.h | 94 +++++ .../src/main/jni/xreact/jni/OnLoad.cpp | 186 ++++++++++ ReactAndroid/src/main/jni/xreact/jni/OnLoad.h | 14 + .../src/main/jni/xreact/jni/ProxyExecutor.cpp | 116 ++++++ .../src/main/jni/xreact/jni/ProxyExecutor.h | 56 +++ .../src/main/jni/xreact/jni/WebWorkers.h | 50 +++ .../src/main/jni/xreact/perftests/BUCK | 21 ++ .../src/main/jni/xreact/perftests/OnLoad.cpp | 156 ++++++++ 32 files changed, 2831 insertions(+) create mode 100644 ReactAndroid/src/main/jni/xreact/jni/BUCK create mode 100644 ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp create mode 100644 ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp create mode 100644 ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JCallback.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JExecutorToken.cpp create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JExecutorToken.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JMessageQueueThread.cpp create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JMessageQueueThread.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JNativeRunnable.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JSLoader.cpp create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JSLoader.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JSLogging.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JavaScriptExecutorHolder.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JniJSModulesUnbundle.cpp create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JniJSModulesUnbundle.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/JniWebWorkers.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp create mode 100644 ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp create mode 100644 ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp create mode 100644 ReactAndroid/src/main/jni/xreact/jni/OnLoad.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp create mode 100644 ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.h create mode 100644 ReactAndroid/src/main/jni/xreact/jni/WebWorkers.h create mode 100644 ReactAndroid/src/main/jni/xreact/perftests/BUCK create mode 100644 ReactAndroid/src/main/jni/xreact/perftests/OnLoad.cpp diff --git a/ReactAndroid/src/main/jni/xreact/jni/BUCK b/ReactAndroid/src/main/jni/xreact/jni/BUCK new file mode 100644 index 00000000000000..a68771a8e42b87 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/BUCK @@ -0,0 +1,48 @@ +include_defs('//ReactAndroid/DEFS') + +# We depend on JSC, support the same platforms +SUPPORTED_PLATFORMS = '^android-(armv7|x86)$' + +EXPORTED_HEADERS = [ + 'CxxModuleWrapper.h', +] + +cxx_library( + name='jni', + soname = 'libreactnativejnifb.so', + header_namespace = 'react/jni', + supported_platforms_regex = SUPPORTED_PLATFORMS, + deps = JSC_DEPS + [ + '//native/fb:fb', + '//native/third-party/android-ndk:android', + '//xplat/folly:molly', + '//xplat/fbsystrace:fbsystrace', + '//xplat/react/module:module', + react_native_target('jni/react/jni:jni'), + react_native_xplat_target('bridge:bridge'), + ], + srcs = glob(['*.cpp']), + exported_headers = EXPORTED_HEADERS, + headers = glob(['*.h'], excludes=EXPORTED_HEADERS), + preprocessor_flags = [ + '-DLOG_TAG="ReactNativeJNI"', + '-DWITH_FBSYSTRACE=1', + ], + compiler_flags = [ + '-Wall', + '-Werror', + '-fexceptions', + '-std=c++11', + '-fvisibility=hidden', + '-frtti', + '-Wno-pessimizing-move', + '-Wno-inconsistent-missing-override', + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':jni', +) diff --git a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp new file mode 100644 index 00000000000000..6bdeae504ca15f --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp @@ -0,0 +1,228 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "CatalystInstanceImpl.h" + +#include +#include + +#include +#include + +#include + +#include +#include + +#include + +#include +#include +#include + +#include "JSLoader.h" +#include "JavaScriptExecutorHolder.h" +#include "JniJSModulesUnbundle.h" +#include "ModuleRegistryHolder.h" +#include "JNativeRunnable.h" + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +namespace { + + +class Exception : public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Ljava/lang/Exception;"; +}; + +class JInstanceCallback : public InstanceCallback { + public: + explicit JInstanceCallback(alias_ref jobj) + : jobj_(make_global(jobj)) {} + + void onBatchComplete() override { + static auto method = + ReactCallback::javaClassStatic()->getMethod("onBatchComplete"); + method(jobj_); + } + + void incrementPendingJSCalls() override { + static auto method = + ReactCallback::javaClassStatic()->getMethod("incrementPendingJSCalls"); + method(jobj_); + } + + void decrementPendingJSCalls() override { + static auto method = + ReactCallback::javaClassStatic()->getMethod("decrementPendingJSCalls"); + method(jobj_); + } + + void onNativeException(const std::string& what) override { + static auto exCtor = + Exception::javaClassStatic()->getConstructor(); + static auto method = + ReactCallback::javaClassStatic()->getMethod("onNativeException"); + + method(jobj_, Exception::javaClassStatic()->newObject( + exCtor, jni::make_jstring(what).get()).get()); + } + + ExecutorToken createExecutorToken() override { + auto jobj = JExecutorToken::newObjectCxxArgs(); + return jobj->cthis()->getExecutorToken(jobj); + } + + void onExecutorStopped(ExecutorToken) override { + // TODO(cjhopman): implement this. + } + + private: + global_ref jobj_; +}; + +} + +jni::local_ref CatalystInstanceImpl::initHybrid( + jni::alias_ref) { + return makeCxxInstance(); +} + +CatalystInstanceImpl::CatalystInstanceImpl() + : instance_(folly::make_unique()) {} + +void CatalystInstanceImpl::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", CatalystInstanceImpl::initHybrid), + makeNativeMethod("initializeBridge", CatalystInstanceImpl::initializeBridge), + makeNativeMethod("loadScriptFromAssets", + "(Landroid/content/res/AssetManager;Ljava/lang/String;)V", + CatalystInstanceImpl::loadScriptFromAssets), + makeNativeMethod("loadScriptFromFile", CatalystInstanceImpl::loadScriptFromFile), + makeNativeMethod("callJSFunction", CatalystInstanceImpl::callJSFunction), + makeNativeMethod("callJSCallback", CatalystInstanceImpl::callJSCallback), + makeNativeMethod("getMainExecutorToken", CatalystInstanceImpl::getMainExecutorToken), + makeNativeMethod("setGlobalVariable", CatalystInstanceImpl::setGlobalVariable), + makeNativeMethod("supportsProfiling", CatalystInstanceImpl::supportsProfiling), + makeNativeMethod("startProfiler", CatalystInstanceImpl::startProfiler), + makeNativeMethod("stopProfiler", CatalystInstanceImpl::stopProfiler), + }); + + JNativeRunnable::registerNatives(); +} + +void CatalystInstanceImpl::initializeBridge( + jni::alias_ref callback, + // This executor is actually a factory holder. + JavaScriptExecutorHolder* jseh, + jni::alias_ref jsQueue, + jni::alias_ref moduleQueue, + ModuleRegistryHolder* mrh) { + // TODO mhorowitz: how to assert here? + // Assertions.assertCondition(mBridge == null, "initializeBridge should be called once"); + + // This used to be: + // + // Java CatalystInstanceImpl -> C++ CatalystInstanceImpl -> Bridge -> Bridge::Callback + // --weak--> ReactCallback -> Java CatalystInstanceImpl + // + // Now the weak ref is a global ref. So breaking the loop depends on + // CatalystInstanceImpl#destroy() calling mHybridData.resetNative(), which + // should cause all the C++ pointers to be cleaned up (except C++ + // CatalystInstanceImpl might be kept alive for a short time by running + // callbacks). This also means that all native calls need to be pre-checked + // to avoid NPE. + + // See the comment in callJSFunction. Once js calls switch to strings, we + // don't need jsModuleDescriptions any more, all the way up and down the + // stack. + + instance_->initializeBridge(folly::make_unique(callback), + jseh->getExecutorFactory(), + folly::make_unique(jsQueue), + folly::make_unique(moduleQueue), + mrh->getModuleRegistry()); +} + +void CatalystInstanceImpl::loadScriptFromAssets(jobject assetManager, + const std::string& assetURL) { + const int kAssetsLength = 9; // strlen("assets://"); + auto sourceURL = assetURL.substr(kAssetsLength); + + auto manager = react::extractAssetManager(assetManager); + auto script = react::loadScriptFromAssets(manager, sourceURL); + if (JniJSModulesUnbundle::isUnbundle(manager, sourceURL)) { + instance_->loadUnbundle( + folly::make_unique(manager, sourceURL), + std::move(script), + sourceURL); + } else { + instance_->loadScriptFromString(std::move(script), std::move(sourceURL)); + } +} + +void CatalystInstanceImpl::loadScriptFromFile(jni::alias_ref fileName, + const std::string& sourceURL) { + return instance_->loadScriptFromFile(fileName ? fileName->toStdString() : "", + sourceURL); +} + +void CatalystInstanceImpl::callJSFunction( + JExecutorToken* token, std::string module, std::string method, NativeArray* arguments, + const std::string& tracingName) { + // We want to share the C++ code, and on iOS, modules pass module/method + // names as strings all the way through to JS, and there's no way to do + // string -> id mapping on the objc side. So on Android, we convert the + // number to a string, here which gets passed as-is to JS. There, they they + // used as ids if isFinite(), which handles this case, and looked up as + // strings otherwise. Eventually, we'll probably want to modify the stack + // from the JS proxy through here to use strings, too. + instance_->callJSFunction(token->getExecutorToken(nullptr), + module, + method, + std::move(arguments->array), + tracingName); +} + +void CatalystInstanceImpl::callJSCallback(JExecutorToken* token, jint callbackId, NativeArray* arguments) { + instance_->callJSCallback(token->getExecutorToken(nullptr), callbackId, std::move(arguments->array)); +} + +local_ref CatalystInstanceImpl::getMainExecutorToken() { + return JExecutorToken::extractJavaPartFromToken(instance_->getMainExecutorToken()); +} + +void CatalystInstanceImpl::setGlobalVariable(std::string propName, + std::string&& jsonValue) { + // This is only ever called from Java with short strings, and only + // for testing, so no need to try hard for zero-copy here. + + instance_->setGlobalVariable(std::move(propName), + folly::make_unique(std::move(jsonValue))); +} + +jboolean CatalystInstanceImpl::supportsProfiling() { + if (!instance_) { + return false; + } + return instance_->supportsProfiling(); +} + +void CatalystInstanceImpl::startProfiler(const std::string& title) { + if (!instance_) { + return; + } + return instance_->startProfiler(title); +} + +void CatalystInstanceImpl::stopProfiler(const std::string& title, const std::string& filename) { + if (!instance_) { + return; + } + return instance_->stopProfiler(title, filename); +} + +}} diff --git a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h new file mode 100644 index 00000000000000..0bb6302fca7cba --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h @@ -0,0 +1,67 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include + +#include + +#include + +#include "JMessageQueueThread.h" +#include "JExecutorToken.h" + +namespace facebook { +namespace react { + +class Instance; +class JavaScriptExecutorHolder; +class ModuleRegistryHolder; +class NativeArray; + +struct ReactCallback : public jni::JavaClass { + static constexpr auto kJavaDescriptor = + "Lcom/facebook/react/cxxbridge/ReactCallback;"; +}; + +class CatalystInstanceImpl : public jni::HybridClass { + public: + static constexpr auto kJavaDescriptor = + "Lcom/facebook/react/cxxbridge/CatalystInstanceImpl;"; + + static jni::local_ref initHybrid(jni::alias_ref); + + static void registerNatives(); + + std::shared_ptr getInstance() { + return instance_; + } + + private: + friend HybridBase; + + CatalystInstanceImpl(); + + void initializeBridge( + jni::alias_ref callback, + // This executor is actually a factory holder. + JavaScriptExecutorHolder* jseh, + jni::alias_ref jsQueue, + jni::alias_ref moduleQueue, + ModuleRegistryHolder* mrh); + void loadScriptFromAssets(jobject assetManager, const std::string& assetURL); + void loadScriptFromFile(jni::alias_ref fileName, const std::string& sourceURL); + void callJSFunction(JExecutorToken* token, std::string module, std::string method, NativeArray* arguments, + const std::string& tracingName); + void callJSCallback(JExecutorToken* token, jint callbackId, NativeArray* arguments); + local_ref getMainExecutorToken(); + void setGlobalVariable(std::string propName, + std::string&& jsonValue); + jboolean supportsProfiling(); + void startProfiler(const std::string& title); + void stopProfiler(const std::string& title, const std::string& filename); + + // This should be the only long-lived strong reference, but every C++ class + // will have a weak reference. + std::shared_ptr instance_; +}; + +}} diff --git a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp new file mode 100644 index 00000000000000..182f0364602414 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp @@ -0,0 +1,219 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "CxxModuleWrapper.h" + +#include + +#include +#include +#include +#include + +#include + +#include + +#include +#include + +#include +#include + +using namespace facebook::jni; +using namespace facebook::xplat::module; +using namespace facebook::react; + +namespace { + +class ExecutorToken : public HybridClass { +public: + constexpr static const char *const kJavaDescriptor = "Lcom/facebook/react/bridge/ExecutorToken;"; +}; + +class CxxMethodWrapper : public HybridClass { +public: + constexpr static const char *const kJavaDescriptor = + "Lcom/facebook/react/cxxbridge/CxxModuleWrapper$MethodWrapper;"; + + static local_ref initHybrid(alias_ref) { + return makeCxxInstance(); + } + + static void registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", CxxMethodWrapper::initHybrid), + makeNativeMethod("invoke", + "(Lcom/facebook/react/bridge/CatalystInstance;Lcom/facebook/react/bridge/ExecutorToken;Lcom/facebook/react/bridge/ReadableNativeArray;)V", + CxxMethodWrapper::invoke), + }); + } + + void invoke(jobject catalystinstance, ExecutorToken::jhybridobject executorToken, NativeArray* args); + + const CxxModule::Method* method_; +}; + +void CxxMethodWrapper::invoke(jobject jCatalystInstance, ExecutorToken::jhybridobject jExecutorToken, NativeArray* arguments) { + CxxModule::Callback first; + CxxModule::Callback second; + + if (method_->callbacks >= 1) { + auto catalystInstance = make_global(adopt_local(jCatalystInstance)); + global_ref executorToken = make_global(jExecutorToken); + // TODO(10184774): Support ExecutorToken in CxxModules + static auto sCatalystInstanceInvokeCallback = + catalystInstance->getClass()->getMethod( + "invokeCallback"); + + int id1; + + if (!arguments->array[arguments->array.size() - 1].isInt()) { + throwNewJavaException(gJavaLangIllegalArgumentException, + "Expected callback as last argument"); + } + + if (method_->callbacks == 2) { + if (!arguments->array[arguments->array.size() - 2].isInt()) { + throwNewJavaException(gJavaLangIllegalArgumentException, + "Expected callback as penultimate argument"); + return; + } + + id1 = arguments->array[arguments->array.size() - 2].getInt(); + int id2 = arguments->array[arguments->array.size() - 1].getInt(); + second = [catalystInstance, executorToken, id2](std::vector args) mutable { + ThreadScope guard; + sCatalystInstanceInvokeCallback( + catalystInstance.get(), executorToken.get(), id2, + ReadableNativeArray::newObjectCxxArgs(std::move(args)).get()); + catalystInstance.reset(); + executorToken.reset(); + }; + } else { + id1 = arguments->array[arguments->array.size() - 1].getInt(); + } + + first = [catalystInstance, executorToken, id1](std::vector args) mutable { + ThreadScope guard; + sCatalystInstanceInvokeCallback( + catalystInstance.get(), executorToken.get(), id1, + ReadableNativeArray::newObjectCxxArgs(std::move(args)).get()); + // This is necessary because by the time the lambda's dtor runs, + // the guard has been destroyed, and it may not be possible to + // get a JNIEnv* to clean up the captured global_ref. + catalystInstance.reset(); + executorToken.reset(); + }; + } + + // I've got a few flawed options here. I can catch C++ exceptions + // here, and log/convert them to java exceptions. This lets all the + // java handling work ok, but the only info I can capture about the + // C++ exception is the what() string, not the stack. I can let the + // C++ exception escape, crashing the app. This causes the full, + // accurate C++ stack trace to be added to logcat by debuggerd. The + // java state is lost, but in practice, the java stack is always the + // same in this case since the javascript stack is not visible. The + // what() value is also lost. Finally, I can catch, log the java + // stack, then rethrow the C++ exception. In this case I get java + // and C++ stack data, but the C++ stack is as of the rethrow, not + // the original throw, both the C++ and java stacks always look the + // same. + // + // I am going with option 2, since that seems like the most useful + // choice. It would be nice to be able to get what() and the C++ + // stack. I'm told that will be possible in the future. TODO + // mhorowitz #7128529: convert C++ exceptions to Java + + folly::dynamic dargs = arguments->array; + dargs.resize(arguments->array.size() - method_->callbacks); + + try { + method_->func(dargs, first, second); + } catch (const facebook::xplat::JsArgumentException& ex) { + throwNewJavaException(gJavaLangIllegalArgumentException, ex.what()); + } +} + +} + +// CxxModuleWrapper + +void CxxModuleWrapper::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", CxxModuleWrapper::initHybrid), + makeNativeMethod("getName", CxxModuleWrapper::getName), + makeNativeMethod("getConstantsJson", CxxModuleWrapper::getConstantsJson), + makeNativeMethod("getMethods", "()Ljava/util/Map;", CxxModuleWrapper::getMethods), + }); + + CxxMethodWrapper::registerNatives(); +} + +CxxModuleWrapper::CxxModuleWrapper(const std::string& soPath, const std::string& fname) { + // soPath is the path of a library which has already been loaded by + // java SoLoader.loadLibrary(). So this returns the same handle, + // and increments the reference counter. We can't just use + // dlsym(RTLD_DEFAULT, ...), because that crashes on 4.4.2 and + // earlier: https://code.google.com/p/android/issues/detail?id=61799 + void* handle = dlopen(soPath.c_str(), RTLD_NOW); + if (!handle) { + throwNewJavaException(gJavaLangIllegalArgumentException, + "module shared library %s is not found", soPath.c_str()); + } + // Now, arrange to close the handle so the counter is decremented. + // The handle will remain valid until java closes it. There's no + // way to do this on Android, but that's no reason to be sloppy + // here. + auto guard = folly::makeGuard([&] { FBASSERT(dlclose(handle) == 0); }); + + void* sym = dlsym(handle, fname.c_str()); + if (!sym) { + throwNewJavaException(gJavaLangIllegalArgumentException, + "module function %s in shared library %s is not found", + fname.c_str(), soPath.c_str()); + } + auto factory = reinterpret_cast(sym); + module_.reset((*factory)()); + methods_ = module_->getMethods(); +} + +std::string CxxModuleWrapper::getName() { + return module_->getName(); +} + +std::string CxxModuleWrapper::getConstantsJson() { + std::map constants = module_->getConstants(); + folly::dynamic constsobject = folly::dynamic::object; + + for (auto& c : constants) { + constsobject.insert(std::move(c.first), std::move(c.second)); + } + + return folly::toJson(constsobject); +} + +jobject CxxModuleWrapper::getMethods() { + static auto hashmap = findClassStatic("java/util/HashMap"); + static auto hashmap_put = hashmap->getMethod("put"); + + auto methods = hashmap->newObject(hashmap->getConstructor()); + + std::unordered_set names; + for (const auto& m : methods_) { + if (names.find(m.name) != names.end()) { + throwNewJavaException(gJavaLangIllegalArgumentException, + "C++ Module %s method name already registered: %s", + module_->getName().c_str(), m.name.c_str()); + } + names.insert(m.name); + auto name = make_jstring(m.name); + static auto ctor = + CxxMethodWrapper::javaClassStatic()->getConstructor(); + auto method = CxxMethodWrapper::javaClassStatic()->newObject(ctor); + cthis(method)->method_ = &m; + hashmap_put(methods.get(), name.get(), method.get()); + } + + return methods.release(); +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.h b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.h new file mode 100644 index 00000000000000..206eddc565fb4f --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.h @@ -0,0 +1,53 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include +#include +#include + +namespace facebook { +namespace react { + +class CxxModuleWrapper : public jni::HybridClass { +public: + constexpr static const char *const kJavaDescriptor = + "Lcom/facebook/react/cxxbridge/CxxModuleWrapper;"; + + static void registerNatives(); + + CxxModuleWrapper(const std::string& soPath, const std::string& fname); + + static jni::local_ref initHybrid( + jni::alias_ref, const std::string& soPath, const std::string& fname) { + return makeCxxInstance(soPath, fname); + } + + // JNI methods + std::string getName(); + std::string getConstantsJson(); + jobject getMethods(); + + // This steals ownership of the underlying module for use by the C++ bridge + std::unique_ptr getModule() { + // TODO mhorowitz: remove this (and a lot of other code) once the java + // bridge is dead. + methods_.clear(); + return std::move(module_); + } + +protected: + friend HybridBase; + + explicit CxxModuleWrapper(std::unique_ptr module) + : module_(std::move(module)) + , methods_(module_->getMethods()) {} + + std::unique_ptr module_; + std::vector methods_; +}; + +} +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/JCallback.h b/ReactAndroid/src/main/jni/xreact/jni/JCallback.h new file mode 100644 index 00000000000000..539273104dced2 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JCallback.h @@ -0,0 +1,44 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include +#include + +#include + +namespace facebook { +namespace react { + +class Instance; + +struct JCallback : public jni::JavaClass { + constexpr static auto kJavaDescriptor = "Lcom/facebook/react/bridge/Callback;"; +}; + +class JCallbackImpl : public jni::HybridClass { +public: + constexpr static auto kJavaDescriptor = "Lcom/facebook/react/cxxbridge/CallbackImpl;"; + + static void registerNatives() { + javaClassStatic()->registerNatives({ + makeNativeMethod("nativeInvoke", JCallbackImpl::invoke), + }); + } +private: + friend HybridBase; + + using Callback = std::function; + JCallbackImpl(Callback callback) : callback_(std::move(callback)) {} + + void invoke(NativeArray* arguments) { + callback_(std::move(arguments->array)); + } + + Callback callback_; +}; + +} +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/JExecutorToken.cpp b/ReactAndroid/src/main/jni/xreact/jni/JExecutorToken.cpp new file mode 100644 index 00000000000000..c55e8f861318e1 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JExecutorToken.cpp @@ -0,0 +1,25 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JExecutorToken.h" + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +ExecutorToken JExecutorToken::getExecutorToken(alias_ref jobj) { + std::lock_guard guard(createTokenGuard_); + auto sharedOwner = owner_.lock(); + if (!sharedOwner) { + sharedOwner = std::shared_ptr(new JExecutorTokenHolder(jobj)); + owner_ = sharedOwner; + } + return ExecutorToken(sharedOwner); +} + +local_ref JExecutorToken::extractJavaPartFromToken(ExecutorToken token) { + return make_local(static_cast(token.getPlatformExecutorToken().get())->getJobj()); +} + + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JExecutorToken.h b/ReactAndroid/src/main/jni/xreact/jni/JExecutorToken.h new file mode 100644 index 00000000000000..0505801ea5d425 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JExecutorToken.h @@ -0,0 +1,60 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include + +#include + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +class JExecutorTokenHolder; +class JExecutorToken : public HybridClass { +public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/ExecutorToken;"; + + ExecutorToken getExecutorToken(alias_ref jobj); + + static local_ref extractJavaPartFromToken(ExecutorToken token); +private: + friend HybridBase; + friend JExecutorTokenHolder; + + JExecutorToken() {} + + std::weak_ptr owner_; + std::mutex createTokenGuard_; +}; + +/** + * Wrapper class to hold references to both the c++ and Java parts of the + * ExecutorToken object. The goal is to allow a reference to a token from either + * c++ or Java to keep both the Java object and c++ hybrid part alive. For c++ + * references, we accomplish this by having JExecutorTokenHolder keep a reference + * to the Java object (which has a reference to the JExecutorToken hybrid part). + * For Java references, we allow the JExecutorTokenHolder to be deallocated if there + * are no references to it in c++ from a PlatformExecutorToken, but will dynamically + * create a new one in JExecutorToken.getExecutorToken if needed. + */ +class JExecutorTokenHolder : public PlatformExecutorToken, public noncopyable { +public: + explicit JExecutorTokenHolder(alias_ref jobj) : + jobj_(make_global(jobj)), + impl_(cthis(jobj)) { + } + + JExecutorToken::javaobject getJobj() { + return jobj_.get(); + } + +private: + global_ref jobj_; + JExecutorToken *impl_; +}; + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JMessageQueueThread.cpp b/ReactAndroid/src/main/jni/xreact/jni/JMessageQueueThread.cpp new file mode 100644 index 00000000000000..6cba3235cd2f83 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JMessageQueueThread.cpp @@ -0,0 +1,66 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JMessageQueueThread.h" + +#include +#include + +#include +#include +#include + +#include "JNativeRunnable.h" + +namespace facebook { +namespace react { + +JMessageQueueThread::JMessageQueueThread(alias_ref jobj) : + m_jobj(make_global(jobj)) { +} + +void JMessageQueueThread::runOnQueue(std::function&& runnable) { + static auto method = JavaMessageQueueThread::javaClassStatic()-> + getMethod("runOnQueue"); + method(m_jobj, JNativeRunnable::newObjectCxxArgs(runnable).get()); +} + +void JMessageQueueThread::runOnQueueSync(std::function&& runnable) { + static auto jIsOnThread = JavaMessageQueueThread::javaClassStatic()-> + getMethod("isOnThread"); + + if (jIsOnThread(m_jobj)) { + runnable(); + } else { + std::mutex signalMutex; + std::condition_variable signalCv; + bool runnableComplete = false; + + runOnQueue([&] () mutable { + std::lock_guard lock(signalMutex); + + runnable(); + runnableComplete = true; + + signalCv.notify_one(); + }); + + std::unique_lock lock(signalMutex); + signalCv.wait(lock, [&runnableComplete] { return runnableComplete; }); + } +} + +void JMessageQueueThread::quitSynchronous() { + static auto method = JavaMessageQueueThread::javaClassStatic()-> + getMethod("quitSynchronous"); + method(m_jobj); +} + +/* static */ +std::unique_ptr JMessageQueueThread::currentMessageQueueThread() { + static auto method = MessageQueueThreadRegistry::javaClassStatic()-> + getStaticMethod("myMessageQueueThread"); + return folly::make_unique(method(MessageQueueThreadRegistry::javaClassStatic())); +} + +} } + diff --git a/ReactAndroid/src/main/jni/xreact/jni/JMessageQueueThread.h b/ReactAndroid/src/main/jni/xreact/jni/JMessageQueueThread.h new file mode 100644 index 00000000000000..8615f496c9728c --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JMessageQueueThread.h @@ -0,0 +1,60 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include + +#include + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +class JavaMessageQueueThread : public jni::JavaClass { +public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/queue/MessageQueueThread;"; +}; + +class JMessageQueueThread : public MessageQueueThread { +public: + JMessageQueueThread(alias_ref jobj); + + /** + * Enqueues the given function to run on this MessageQueueThread. + */ + void runOnQueue(std::function&& runnable) override; + + /** + * Synchronously executes the given function to run on this + * MessageQueueThread, waiting until it completes. Can be called from any + * thread, but will block if not called on this MessageQueueThread. + */ + void runOnQueueSync(std::function&& runnable) override; + + /** + * Synchronously quits the current MessageQueueThread. Can be called from any thread, but will + * block if not called on this MessageQueueThread. + */ + void quitSynchronous() override; + + JavaMessageQueueThread::javaobject jobj() { + return m_jobj.get(); + } + + /** + * Returns the current MessageQueueThread that owns this thread. + */ + static std::unique_ptr currentMessageQueueThread(); +private: + global_ref m_jobj; +}; + +class MessageQueueThreadRegistry : public jni::JavaClass { +public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/queue/MessageQueueThreadRegistry;"; +}; + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JNativeRunnable.h b/ReactAndroid/src/main/jni/xreact/jni/JNativeRunnable.h new file mode 100644 index 00000000000000..5550af34299b16 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JNativeRunnable.h @@ -0,0 +1,44 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +class Runnable : public JavaClass { +public: + static constexpr auto kJavaDescriptor = "Ljava/lang/Runnable;"; +}; + +/** + * The c++ interface for the Java NativeRunnable class + */ +class JNativeRunnable : public HybridClass { +public: + static auto constexpr kJavaDescriptor = "Lcom/facebook/react/bridge/queue/NativeRunnable;"; + + void run() { + m_runnable(); + } + + static void registerNatives() { + javaClassStatic()->registerNatives({ + makeNativeMethod("run", JNativeRunnable::run), + }); + } +private: + friend HybridBase; + + JNativeRunnable(std::function runnable) + : m_runnable(std::move(runnable)) {} + + std::function m_runnable; +}; + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp new file mode 100644 index 00000000000000..1b7b5e6887b6fa --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp @@ -0,0 +1,244 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JSCPerfLogging.h" + +#include +#include +#include + +using namespace facebook::jni; + +struct _jqplProvider : _jobject {}; +using jqplProvider = _jqplProvider*; + +struct _jqpl : _jobject {}; +using jqpl = _jqpl*; + +namespace facebook { namespace jni { + + +template<> +class JObjectWrapper : public JObjectWrapper { + + public: + static constexpr const char* kJavaDescriptor = "Lcom/facebook/quicklog/QuickPerformanceLogger;"; + + using JObjectWrapper::JObjectWrapper; + + void markerStart(int markerId, int instanceKey, long timestamp) { + static auto markerStartMethod = + qplClass()->getMethod("markerStart"); + markerStartMethod(this_, markerId, instanceKey, timestamp); + } + + void markerEnd(int markerId, int instanceKey, short actionId, long timestamp) { + static auto markerEndMethod = + qplClass()->getMethod("markerEnd"); + markerEndMethod(this_, markerId, instanceKey, actionId, timestamp); + } + + void markerNote(int markerId, int instanceKey, short actionId, long timestamp) { + static auto markerNoteMethod = + qplClass()->getMethod("markerNote"); + markerNoteMethod(this_, markerId, instanceKey, actionId, timestamp); + } + + void markerCancel(int markerId, int instanceKey) { + static auto markerCancelMethod = + qplClass()->getMethod("markerCancel"); + markerCancelMethod(this_, markerId, instanceKey); + } + + int64_t currentMonotonicTimestamp() { + static auto currentTimestampMethod = + qplClass()->getMethod("currentMonotonicTimestamp"); + return currentTimestampMethod(this_); + } + + private: + + static alias_ref qplClass() { + static auto cls = findClassStatic("com/facebook/quicklog/QuickPerformanceLogger"); + return cls; + } + +}; +using JQuickPerformanceLogger = JObjectWrapper; + + +template<> +class JObjectWrapper : public JObjectWrapper { + public: + static constexpr const char* kJavaDescriptor = + "Lcom/facebook/quicklog/QuickPerformanceLoggerProvider;"; + + using JObjectWrapper::JObjectWrapper; + + static global_ref get() { + static auto getQPLInstMethod = qplProviderClass()->getStaticMethod("getQPLInstance"); + static global_ref theQpl = make_global(getQPLInstMethod(qplProviderClass().get())); + return theQpl; + } + + static bool check() { + static auto getQPLInstMethod = qplProviderClass()->getStaticMethod("getQPLInstance"); + auto theQpl = getQPLInstMethod(qplProviderClass().get()); + return (theQpl.get() != nullptr); + } + + private: + + static alias_ref qplProviderClass() { + static auto cls = findClassStatic("com/facebook/quicklog/QuickPerformanceLoggerProvider"); + return cls; + } +}; +using JQuickPerformanceLoggerProvider = JObjectWrapper; + +}} + +static bool isReady() { + static bool ready = false; + if (!ready) { + try { + findClassStatic("com/facebook/quicklog/QuickPerformanceLoggerProvider"); + } catch(...) { + // Swallow this exception - we don't want to crash the app, an error is enough. + FBLOGE("Calling QPL from JS before class has been loaded in Java. Ignored."); + return false; + } + if (JQuickPerformanceLoggerProvider::check()) { + ready = true; + } else { + FBLOGE("Calling QPL from JS before it has been initialized in Java. Ignored."); + return false; + } + } + return ready; +} + +// After having read the implementation of PNaN that is returned from JSValueToNumber, and some +// more material on how NaNs are constructed, I think this is the most consistent way to verify +// NaN with how we generate it. +// Once the integration completes, I'll play around with it some more and potentially change this +// implementation to use std::isnan() if it is exactly commensurate with our usage. +static bool isNan(double value) { + return (value != value); +} + +// Safely translates JSValues to an array of doubles. +static bool grabDoubles( + size_t targetsCount, + double targets[], + JSContextRef ctx, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + if (argumentCount < targetsCount) { + return false; + } + for (size_t i = 0 ; i < targetsCount ; i++) { + targets[i] = JSValueToNumber(ctx, arguments[i], exception); + if (isNan(targets[i])) { + return false; + } + } + return true; +} + +static JSValueRef nativeQPLMarkerStart( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + double targets[3]; + if (isReady() && grabDoubles(3, targets, ctx, argumentCount, arguments, exception)) { + int32_t markerId = (int32_t) targets[0]; + int32_t instanceKey = (int32_t) targets[1]; + int64_t timestamp = (int64_t) targets[2]; + JQuickPerformanceLoggerProvider::get()->markerStart(markerId, instanceKey, timestamp); + } + return JSValueMakeUndefined(ctx); +} + +static JSValueRef nativeQPLMarkerEnd( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + double targets[4]; + if (isReady() && grabDoubles(4, targets, ctx, argumentCount, arguments, exception)) { + int32_t markerId = (int32_t) targets[0]; + int32_t instanceKey = (int32_t) targets[1]; + int16_t actionId = (int16_t) targets[2]; + int64_t timestamp = (int64_t) targets[3]; + JQuickPerformanceLoggerProvider::get()->markerEnd(markerId, instanceKey, actionId, timestamp); + } + return JSValueMakeUndefined(ctx); +} + +static JSValueRef nativeQPLMarkerNote( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + double targets[4]; + if (isReady() && grabDoubles(4, targets, ctx, argumentCount, arguments, exception)) { + int32_t markerId = (int32_t) targets[0]; + int32_t instanceKey = (int32_t) targets[1]; + int16_t actionId = (int16_t) targets[2]; + int64_t timestamp = (int64_t) targets[3]; + JQuickPerformanceLoggerProvider::get()->markerNote(markerId, instanceKey, actionId, timestamp); + } + return JSValueMakeUndefined(ctx); +} + +static JSValueRef nativeQPLMarkerCancel( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + double targets[2]; + if (isReady() && grabDoubles(2, targets, ctx, argumentCount, arguments, exception)) { + int32_t markerId = (int32_t) targets[0]; + int32_t instanceKey = (int32_t) targets[1]; + JQuickPerformanceLoggerProvider::get()->markerCancel(markerId, instanceKey); + } + return JSValueMakeUndefined(ctx); +} + +static JSValueRef nativeQPLTimestamp( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + if (!isReady()) { + return JSValueMakeNumber(ctx, 0); + } + int64_t timestamp = JQuickPerformanceLoggerProvider::get()->currentMonotonicTimestamp(); + // Since this is monotonic time, I assume the 52 bits of mantissa are enough in the double value. + return JSValueMakeNumber(ctx, timestamp); +} + +namespace facebook { +namespace react { + +void addNativePerfLoggingHooks(JSGlobalContextRef ctx) { + installGlobalFunction(ctx, "nativeQPLMarkerStart", nativeQPLMarkerStart); + installGlobalFunction(ctx, "nativeQPLMarkerEnd", nativeQPLMarkerEnd); + installGlobalFunction(ctx, "nativeQPLMarkerNote", nativeQPLMarkerNote); + installGlobalFunction(ctx, "nativeQPLMarkerCancel", nativeQPLMarkerCancel); + installGlobalFunction(ctx, "nativeQPLTimestamp", nativeQPLTimestamp); +} + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.h b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.h new file mode 100644 index 00000000000000..256755b1facd3d --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.h @@ -0,0 +1,11 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +namespace facebook { +namespace react { + +void addNativePerfLoggingHooks(JSGlobalContextRef ctx); + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSLoader.cpp b/ReactAndroid/src/main/jni/xreact/jni/JSLoader.cpp new file mode 100644 index 00000000000000..62d8cb9e56fca0 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JSLoader.cpp @@ -0,0 +1,99 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JSLoader.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef WITH_FBSYSTRACE +#include +using fbsystrace::FbSystraceSection; +#endif + +namespace facebook { +namespace react { + +static jclass gApplicationHolderClass; +static jmethodID gGetApplicationMethod; +static jmethodID gGetAssetManagerMethod; + +std::unique_ptr loadScriptFromAssets(const std::string& assetName) { + JNIEnv *env = jni::Environment::current(); + jobject application = env->CallStaticObjectMethod( + gApplicationHolderClass, + gGetApplicationMethod); + jobject assetManager = env->CallObjectMethod(application, gGetAssetManagerMethod); + return loadScriptFromAssets(AAssetManager_fromJava(env, assetManager), assetName); +} + +AAssetManager *extractAssetManager(jobject jassetManager) { + auto env = jni::Environment::current(); + return AAssetManager_fromJava(env, jassetManager); +} + +std::unique_ptr loadScriptFromAssets( + AAssetManager *manager, + const std::string& assetName) { + #ifdef WITH_FBSYSTRACE + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "reactbridge_jni_loadScriptFromAssets", + "assetName", assetName); + #endif + if (manager) { + auto asset = AAssetManager_open( + manager, + assetName.c_str(), + AASSET_MODE_STREAMING); // Optimized for sequential read: see AssetManager.java for docs + if (asset) { + auto buf = folly::make_unique(AAsset_getLength(asset)); + size_t offset = 0; + int readbytes; + while ((readbytes = AAsset_read(asset, buf->data() + offset, buf->size() - offset)) > 0) { + offset += readbytes; + } + AAsset_close(asset); + if (offset == buf->size()) { + return std::move(buf); + } + } + } + FBLOGE("Unable to load script from assets: %s", assetName.c_str()); + return folly::make_unique(""); +} + +std::string loadScriptFromFile(const std::string& fileName) { + #ifdef WITH_FBSYSTRACE + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "reactbridge_jni_loadScriptFromFile", + "fileName", fileName); + #endif + std::ifstream jsfile(fileName); + if (jsfile) { + std::string output; + jsfile.seekg(0, std::ios::end); + output.reserve(jsfile.tellg()); + jsfile.seekg(0, std::ios::beg); + output.assign( + (std::istreambuf_iterator(jsfile)), + std::istreambuf_iterator()); + return output; + } + + FBLOGE("Unable to load script from file: %s", fileName.c_str()); + return ""; +} + +void registerJSLoaderNatives() { + JNIEnv *env = jni::Environment::current(); + jclass applicationHolderClass = env->FindClass("com/facebook/react/common/ApplicationHolder"); + gApplicationHolderClass = (jclass)env->NewGlobalRef(applicationHolderClass); + gGetApplicationMethod = env->GetStaticMethodID(applicationHolderClass, "getApplication", "()Landroid/app/Application;"); + + jclass appClass = env->FindClass("android/app/Application"); + gGetAssetManagerMethod = env->GetMethodID(appClass, "getAssets", "()Landroid/content/res/AssetManager;"); +} + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSLoader.h b/ReactAndroid/src/main/jni/xreact/jni/JSLoader.h new file mode 100644 index 00000000000000..6dc1d90bd9fd28 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JSLoader.h @@ -0,0 +1,33 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include +#include + +namespace facebook { +namespace react { + +/** + * Helper method for loading a JS script from Android assets without + * a reference to an AssetManager. + */ +std::unique_ptr loadScriptFromAssets(const std::string& assetName); + +/** + * Helper method for loading JS script from android asset + */ +AAssetManager *extractAssetManager(jobject jassetManager); + +std::unique_ptr loadScriptFromAssets(AAssetManager *assetManager, const std::string& assetName); + +/** + * Helper method for loading JS script from a file + */ +std::string loadScriptFromFile(const std::string& fileName); + +void registerJSLoaderNatives(); + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp new file mode 100644 index 00000000000000..7690a5656ebd8d --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp @@ -0,0 +1,36 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JSLogging.h" + +#include +#include +#include +#include + +namespace facebook { +namespace react { + +JSValueRef nativeLoggingHook( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], JSValueRef *exception) { + android_LogPriority logLevel = ANDROID_LOG_DEBUG; + if (argumentCount > 1) { + int level = (int) JSValueToNumber(ctx, arguments[1], NULL); + // The lowest log level we get from JS is 0. We shift and cap it to be + // in the range the Android logging method expects. + logLevel = std::min( + static_cast(level + ANDROID_LOG_DEBUG), + ANDROID_LOG_FATAL); + } + if (argumentCount > 0) { + JSStringRef jsString = JSValueToStringCopy(ctx, arguments[0], NULL); + String message = String::adopt(jsString); + FBLOG_PRI(logLevel, "ReactNativeJS", "%s", message.str().c_str()); + } + return JSValueMakeUndefined(ctx); +} + +}}; diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSLogging.h b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.h new file mode 100644 index 00000000000000..27756b6650760d --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.h @@ -0,0 +1,15 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +namespace facebook { +namespace react { +JSValueRef nativeLoggingHook( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], JSValueRef *exception); +}} diff --git a/ReactAndroid/src/main/jni/xreact/jni/JavaScriptExecutorHolder.h b/ReactAndroid/src/main/jni/xreact/jni/JavaScriptExecutorHolder.h new file mode 100644 index 00000000000000..f59d9ad55272fd --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JavaScriptExecutorHolder.h @@ -0,0 +1,29 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include + +#include + +#include + +namespace facebook { +namespace react { + +class JavaScriptExecutorHolder : public jni::HybridClass { + public: + static constexpr auto kJavaDescriptor = + "Lcom/facebook/react/cxxbridge/JavaScriptExecutor;"; + + std::shared_ptr getExecutorFactory() { + return mExecutorFactory; + } + + protected: + JavaScriptExecutorHolder(std::shared_ptr factory) + : mExecutorFactory(factory) {} + + private: + std::shared_ptr mExecutorFactory; +}; + +}} diff --git a/ReactAndroid/src/main/jni/xreact/jni/JniJSModulesUnbundle.cpp b/ReactAndroid/src/main/jni/xreact/jni/JniJSModulesUnbundle.cpp new file mode 100644 index 00000000000000..20c71bc8c11a10 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JniJSModulesUnbundle.cpp @@ -0,0 +1,83 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JniJSModulesUnbundle.h" + +#include +#include +#include +#include +#include +#include +#include + +using magic_number_t = uint32_t; +const magic_number_t MAGIC_FILE_HEADER = 0xFB0BD1E5; +const std::string MAGIC_FILE_NAME = "UNBUNDLE"; + +namespace facebook { +namespace react { + +using asset_ptr = + std::unique_ptr>; + +static std::string jsModulesDir(const std::string& entryFile) { + std::string dir = dirname(entryFile.c_str()); + + // android's asset manager does not work with paths that start with a dot + return dir == "." ? "js-modules/" : dir + "/js-modules/"; +} + +static asset_ptr openAsset( + AAssetManager *manager, + const std::string& fileName, + int mode = AASSET_MODE_STREAMING) { + return asset_ptr( + AAssetManager_open(manager, fileName.c_str(), mode), + AAsset_close); +} + +JniJSModulesUnbundle::JniJSModulesUnbundle(AAssetManager *assetManager, const std::string& entryFile) : + m_assetManager(assetManager), + m_moduleDirectory(jsModulesDir(entryFile)) {} + +bool JniJSModulesUnbundle::isUnbundle( + AAssetManager *assetManager, + const std::string& assetName) { + if (!assetManager) { + return false; + } + + auto magicFileName = jsModulesDir(assetName) + MAGIC_FILE_NAME; + auto asset = openAsset(assetManager, magicFileName.c_str()); + if (asset == nullptr) { + return false; + } + + magic_number_t fileHeader = 0; + AAsset_read(asset.get(), &fileHeader, sizeof(fileHeader)); + return fileHeader == htole32(MAGIC_FILE_HEADER); +} + +JSModulesUnbundle::Module JniJSModulesUnbundle::getModule(uint32_t moduleId) const { + // can be nullptr for default constructor. + FBASSERTMSGF(m_assetManager != nullptr, "Unbundle has not been initialized with an asset manager"); + + std::ostringstream sourceUrlBuilder; + sourceUrlBuilder << moduleId << ".js"; + auto sourceUrl = sourceUrlBuilder.str(); + + auto fileName = m_moduleDirectory + sourceUrl; + auto asset = openAsset(m_assetManager, fileName, AASSET_MODE_BUFFER); + + const char *buffer = nullptr; + if (asset != nullptr) { + buffer = static_cast(AAsset_getBuffer(asset.get())); + } + if (buffer == nullptr) { + throw ModuleNotFound("Module not found: " + sourceUrl); + } + return {sourceUrl, std::string(buffer, AAsset_getLength(asset.get()))}; +} + +} +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/JniJSModulesUnbundle.h b/ReactAndroid/src/main/jni/xreact/jni/JniJSModulesUnbundle.h new file mode 100644 index 00000000000000..daf88df516406a --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JniJSModulesUnbundle.h @@ -0,0 +1,31 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include + +namespace facebook { +namespace react { + +class JniJSModulesUnbundle : public JSModulesUnbundle { + /** + * This implementation reads modules as single file from the assets of an apk. + */ +public: + JniJSModulesUnbundle() = default; + JniJSModulesUnbundle(AAssetManager *assetManager, const std::string& entryFile); + JniJSModulesUnbundle(JniJSModulesUnbundle&& other) = delete; + JniJSModulesUnbundle& operator= (JSModulesUnbundle&& other) = delete; + + static bool isUnbundle( + AAssetManager *assetManager, + const std::string& assetName); + virtual Module getModule(uint32_t moduleId) const override; +private: + AAssetManager *m_assetManager = nullptr; + std::string m_moduleDirectory; +}; + +} +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/JniWebWorkers.h b/ReactAndroid/src/main/jni/xreact/jni/JniWebWorkers.h new file mode 100644 index 00000000000000..e560fd1e9ce2fa --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/JniWebWorkers.h @@ -0,0 +1,32 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include +#include + +#include "JMessageQueueThread.h" +#include + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +class JniWebWorkers : public JavaClass { +public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/webworkers/WebWorkers;"; + + static std::unique_ptr createWebWorkerQueue(int id, MessageQueue* ownerMessageQueue) { + static auto method = JniWebWorkers::javaClassStatic()-> + getStaticMethod("createWebWorkerThread"); + + JMessageQueueThread* ownerMessageQueueThread = static_cast(ownerMessageQueue); + auto res = method(JniWebWorkers::javaClassStatic(), id, ownerMessageQueueThread->jobj()); + return folly::make_unique(res); + } +}; + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp new file mode 100644 index 00000000000000..8c304378dc3cde --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp @@ -0,0 +1,230 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "MethodInvoker.h" + +#include +#ifdef WITH_FBSYSTRACE +#include +#endif + +#include "ModuleRegistryHolder.h" +#include "JCallback.h" +#include "JExecutorToken.h" + +namespace facebook { +namespace react { + +namespace { + +using dynamic_iterator = folly::dynamic::const_iterator; + +struct JPromiseImpl : public jni::JavaClass { + constexpr static auto kJavaDescriptor = "Lcom/facebook/react/bridge/PromiseImpl;"; + + static jni::local_ref create(jni::local_ref resolve, jni::local_ref reject) { + return newInstance(resolve, reject); + } +}; + +// HACK: Exposes constructor +struct ExposedReadableNativeArray : public ReadableNativeArray { + explicit ExposedReadableNativeArray(folly::dynamic array) + : ReadableNativeArray(std::move(array)) {} +}; + +jdouble extractDouble(const folly::dynamic& value) { + if (value.isInt()) { + return static_cast(value.getInt()); + } else { + return static_cast(value.getDouble()); + } +} + +jni::local_ref extractCallback(std::weak_ptr& instance, ExecutorToken token, const folly::dynamic& value) { + if (value.isNull()) { + return jni::local_ref(nullptr); + } else { + return JCallbackImpl::newObjectCxxArgs(makeCallback(instance, token, value)); + } +} + +jni::local_ref extractPromise(std::weak_ptr& instance, ExecutorToken token, dynamic_iterator& it, dynamic_iterator& end) { + auto resolve = extractCallback(instance, token, *it++); + CHECK(it != end); + auto reject = extractCallback(instance, token, *it++); + return JPromiseImpl::create(resolve, reject); +} + +jobject valueOf(jboolean value) { + static auto kClass = jni::findClassStatic("java/lang/Boolean"); + static auto kValueOf = kClass->getStaticMethod("valueOf"); + return kValueOf(kClass, value).release(); +} + +jobject valueOf(jint value) { + static auto kClass = jni::findClassStatic("java/lang/Integer"); + static auto kValueOf = kClass->getStaticMethod("valueOf"); + return kValueOf(kClass, value).release(); +} + +jobject valueOf(jdouble value) { + static auto kClass = jni::findClassStatic("java/lang/Double"); + static auto kValueOf = kClass->getStaticMethod("valueOf"); + return kValueOf(kClass, value).release(); +} + +jobject valueOf(jfloat value) { + static auto kClass = jni::findClassStatic("java/lang/Float"); + static auto kValueOf = kClass->getStaticMethod("valueOf"); + return kValueOf(kClass, value).release(); +} + +bool isNullable(char type) { + switch (type) { + case 'Z': + case 'I': + case 'F': + case 'S': + case 'A': + case 'M': + case 'X': + return true; + default: + return false;; + } +} + +jvalue extract(std::weak_ptr& instance, ExecutorToken token, char type, dynamic_iterator& it, dynamic_iterator& end) { + CHECK(it != end); + jvalue value; + if (type == 'P') { + value.l = extractPromise(instance, token, it, end).release(); + return value; + } else if (type == 'T') { + value.l = JExecutorToken::extractJavaPartFromToken(token).release(); + return value; + } + + const auto& arg = *it++; + if (isNullable(type) && arg.isNull()) { + value.l = nullptr; + return value; + } + + switch (type) { + case 'z': + value.z = static_cast(arg.getBool()); + break; + case 'Z': + value.l = valueOf(static_cast(arg.getBool())); + break; + case 'i': + value.i = static_cast(arg.getInt()); + break; + case 'I': + value.l = valueOf(static_cast(arg.getInt())); + break; + case 'f': + value.f = static_cast(extractDouble(arg)); + break; + case 'F': + value.l = valueOf(static_cast(extractDouble(arg))); + break; + case 'd': + value.d = extractDouble(arg); + break; + case 'D': + value.l = valueOf(extractDouble(arg)); + break; + case 'S': + value.l = jni::make_jstring(arg.getString()).release(); + break; + case 'A': + value.l = ReadableNativeArray::newObjectCxxArgs(arg).release(); + break; + case 'M': + // HACK: Workaround for constructing ReadableNativeMap + value.l = ExposedReadableNativeArray(folly::dynamic::array(arg)).getMap(0); + break; + case 'X': + value.l = extractCallback(instance, token, arg).release(); + break; + default: + LOG(FATAL) << "Unknown param type: " << type; + } + return value; +} + +std::size_t countJsArgs(const std::string& signature) { + std::size_t count = 0; + for (char c : signature) { + switch (c) { + case 'T': + break; + case 'P': + count += 2; + break; + default: + count += 1; + break; + } + } + return count; +} + +} + +MethodInvoker::MethodInvoker(jni::alias_ref method, std::string signature, std::string traceName, bool isSync) + : method_(method->getMethodID()), + jsArgCount_(countJsArgs(signature) - 2), + signature_(std::move(signature)), + traceName_(std::move(traceName)), + isSync_(isSync) { + CHECK(signature_.at(1) == '.') << "Improper module method signature"; + CHECK(!isSync || signature_.at(0) == 'v') << "Non-sync hooks cannot have a non-void return type"; + } + +MethodCallResult MethodInvoker::invoke(std::weak_ptr& instance, JBaseJavaModule::javaobject module, ExecutorToken token, const folly::dynamic& params) { + #ifdef WITH_FBSYSTRACE + fbsystrace::FbSystraceSection s( + TRACE_TAG_REACT_CXX_BRIDGE, + isSync_ ? "callJavaSyncHook" : "callJavaModuleMethod", + "method", + traceName_); + #endif + if (params.size() != jsArgCount_) { + throw std::invalid_argument(folly::to("expected ", jsArgCount_, " arguments, got ", params.size())); + } + auto argCount = signature_.size() - 2; + jni::JniLocalScope scope(jni::Environment::current(), argCount); + jvalue args[argCount]; + std::transform( + signature_.begin() + 2, + signature_.end(), + args, + [&instance, token, it = params.begin(), end = params.end()] (char type) mutable { + return extract(instance, token, type, it, end); + }); + + // TODO(t10768795): Use fbjni here + folly::dynamic ret = folly::dynamic::object(); + bool isReturnUndefined = false; + char returnType = signature_.at(0); + switch (returnType) { + case 'v': + jni::Environment::current()->CallVoidMethodA(module, method_, args); + ret = nullptr; + isReturnUndefined = true; + break; + default: + LOG(FATAL) << "Unknown return type: " << returnType; + // TODO: other cases + } + + jni::throwPendingJniExceptionAsCppException(); + + return MethodCallResult{ret, isReturnUndefined}; +} + +} +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.h b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.h new file mode 100644 index 00000000000000..ef78213d6aa83d --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.h @@ -0,0 +1,37 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include +#include + +#include + +#include "ModuleRegistryHolder.h" + +namespace facebook { +namespace react { + +class Instance; + +class MethodInvoker { +public: + MethodInvoker(jni::alias_ref method, std::string signature, std::string traceName, bool isSync); + + MethodCallResult invoke(std::weak_ptr& instance, JBaseJavaModule::javaobject module, ExecutorToken token, const folly::dynamic& params); + + bool isSyncHook() const { + return isSync_; + } +private: + jmethodID method_; + std::size_t jsArgCount_; + std::string signature_; + std::string traceName_; + bool isSync_; +}; + +} +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp new file mode 100644 index 00000000000000..c71c95936b96a4 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp @@ -0,0 +1,344 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "ModuleRegistryHolder.h" + +#include + +#include + +#include +#include + +#include +#include +#include + +#include "MethodInvoker.h" + +#include "CatalystInstanceImpl.h" + +using facebook::xplat::module::CxxModule; + +namespace facebook { +namespace react { + +namespace { + +class JavaNativeModule : public NativeModule { + public: + JavaNativeModule(jni::alias_ref wrapper) + : wrapper_(make_global(wrapper)) {} + + std::string getName() override { + static auto getNameMethod = wrapper_->getClass()->getMethod("getName"); + return getNameMethod(wrapper_)->toStdString(); + } + + std::vector getMethods() override { + static auto getMDMethod = + wrapper_->getClass()->getMethod::javaobject()>( + "getMethodDescriptors"); + + std::vector ret; + auto descs = getMDMethod(wrapper_); + for (const auto& desc : *descs) { + static auto nameField = + JMethodDescriptor::javaClassStatic()->getField("name"); + static auto typeField = + JMethodDescriptor::javaClassStatic()->getField("type"); + + ret.emplace_back( + desc->getFieldValue(nameField)->toStdString(), + desc->getFieldValue(typeField)->toStdString() + ); + } + return ret; + } + + folly::dynamic getConstants() override { + static auto constantsMethod = + wrapper_->getClass()->getMethod("getConstants"); + auto constants = constantsMethod(wrapper_); + if (!constants) { + return nullptr; + } else { + // See JavaModuleWrapper#getConstants for the other side of this hack. + return cthis(constants)->array[0]; + } + } + + virtual bool supportsWebWorkers() override { + static auto supportsWebWorkersMethod = + wrapper_->getClass()->getMethod("supportsWebWorkers"); + return supportsWebWorkersMethod(wrapper_); + } + + void invoke(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) override { + static auto invokeMethod = + wrapper_->getClass()->getMethod("invoke"); + invokeMethod(wrapper_, JExecutorToken::extractJavaPartFromToken(token).get(), static_cast(reactMethodId), + ReadableNativeArray::newObjectCxxArgs(std::move(params)).get()); + } + + MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) override { + throw std::runtime_error("Unsupported operation."); + } + + private: + jni::global_ref wrapper_; +}; + +class NewJavaNativeModule : public NativeModule { + public: + NewJavaNativeModule(std::weak_ptr instance, jni::alias_ref wrapper) + : instance_(std::move(instance)), + wrapper_(make_global(wrapper)), + module_(make_global(wrapper->getModule())) { + auto descs = wrapper_->getMethodDescriptors(); + std::string moduleName = getName(); + methods_.reserve(descs->size()); + + for (const auto& desc : *descs) { + auto type = desc->getType(); + auto name = desc->getName(); + methods_.emplace_back( + desc->getMethod(), + desc->getSignature(), + moduleName + "." + name, + type == "syncHook"); + + methodDescriptors_.emplace_back(name, type); + } + } + + std::string getName() override { + static auto getNameMethod = wrapper_->getClass()->getMethod("getName"); + return getNameMethod(wrapper_)->toStdString(); + } + + std::vector getMethods() override { + return methodDescriptors_; + } + + folly::dynamic getConstants() override { + static auto constantsMethod = + wrapper_->getClass()->getMethod("getConstants"); + auto constants = constantsMethod(wrapper_); + if (!constants) { + return nullptr; + } else { + // See JavaModuleWrapper#getConstants for the other side of this hack. + return cthis(constants)->array[0]; + } + } + + virtual bool supportsWebWorkers() override { + static auto supportsWebWorkersMethod = + wrapper_->getClass()->getMethod("supportsWebWorkers"); + return supportsWebWorkersMethod(wrapper_); + } + + void invoke(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) override { + if (reactMethodId >= methods_.size()) { + throw std::invalid_argument( + folly::to("methodId ", reactMethodId, " out of range [0..", methods_.size(), "]")); + } + CHECK(!methods_[reactMethodId].isSyncHook()) << "Trying to invoke a synchronous hook asynchronously"; + invokeInner(token, reactMethodId, std::move(params)); + } + + MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) override { + if (reactMethodId >= methods_.size()) { + throw std::invalid_argument( + folly::to("methodId ", reactMethodId, " out of range [0..", methods_.size(), "]")); + } + CHECK(methods_[reactMethodId].isSyncHook()) << "Trying to invoke a asynchronous method as synchronous hook"; + return invokeInner(token, reactMethodId, std::move(params)); + } + + private: + std::weak_ptr instance_; + jni::global_ref wrapper_; + jni::global_ref module_; + std::vector methods_; + std::vector methodDescriptors_; + + MethodCallResult invokeInner(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) { + if (!params.isArray()) { + throw std::invalid_argument( + folly::to("method parameters should be array, but are ", params.typeName())); + } + return methods_[reactMethodId].invoke(instance_, module_.get(), token, params); + } +}; + +class CxxNativeModule : public NativeModule { + public: + CxxNativeModule(std::weak_ptr instance, + std::unique_ptr module) + : instance_(instance) + , module_(std::move(module)) + , methods_(module_->getMethods()) {} + + std::string getName() override { + return module_->getName(); + } + + virtual std::vector getMethods() override { + // Same as MessageQueue.MethodTypes.remote + static const auto kMethodTypeRemote = "remote"; + + std::vector descs; + for (auto& method : methods_) { + descs.emplace_back(method.name, kMethodTypeRemote); + } + return descs; + } + + virtual folly::dynamic getConstants() override { + folly::dynamic constants = folly::dynamic::object(); + for (auto& pair : module_->getConstants()) { + constants.insert(std::move(pair.first), std::move(pair.second)); + } + return constants; + } + + virtual bool supportsWebWorkers() override { + // TODO(andrews): web worker support in cxxmodules + return true; + } + + // TODO mhorowitz: do we need initialize()/onCatalystInstanceDestroy() in C++ + // or only Java? + virtual void invoke(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) override { + if (reactMethodId >= methods_.size()) { + throw std::invalid_argument( + folly::to("methodId ", reactMethodId, " out of range [0..", methods_.size(), "]")); + } + if (!params.isArray()) { + throw std::invalid_argument( + folly::to("method parameters should be array, but are ", params.typeName())); + } + + CxxModule::Callback first; + CxxModule::Callback second; + + const auto& method = methods_[reactMethodId]; + + if (params.size() < method.callbacks) { + throw std::invalid_argument( + folly::to("Expected ", method.callbacks, " callbacks, but only ", + params.size(), " parameters provided")); + } + + if (method.callbacks == 1) { + first = makeCallback(instance_, token, params[params.size() - 1]); + } else if (method.callbacks == 2) { + first = makeCallback(instance_, token, params[params.size() - 2]); + second = makeCallback(instance_, token, params[params.size() - 1]); + } + + params.resize(params.size() - method.callbacks); + + // I've got a few flawed options here. I can let the C++ exception + // propogate, and the registry will log/convert them to java exceptions. + // This lets all the java and red box handling work ok, but the only info I + // can capture about the C++ exception is the what() string, not the stack. + // I can std::terminate() the app. This causes the full, accurate C++ + // stack trace to be added to logcat by debuggerd. The java state is lost, + // but in practice, the java stack is always the same in this case since + // the javascript stack is not visible, and the crash is unfriendly to js + // developers, but crucial to C++ developers. The what() value is also + // lost. Finally, I can catch, log the java stack, then rethrow the C++ + // exception. In this case I get java and C++ stack data, but the C++ + // stack is as of the rethrow, not the original throw, both the C++ and + // java stacks always look the same. + // + // I am going with option 2, since that seems like the most useful + // choice. It would be nice to be able to get what() and the C++ + // stack. I'm told that will be possible in the future. TODO + // mhorowitz #7128529: convert C++ exceptions to Java + + try { + method.func(std::move(params), first, second); + } catch (const facebook::xplat::JsArgumentException& ex) { + // This ends up passed to the onNativeException callback. + throw; + } catch (...) { + // This means some C++ code is buggy. As above, we fail hard so the C++ + // developer can debug and fix it. + std::terminate(); + } + } + + MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int hookId, folly::dynamic&& args) override { + throw std::runtime_error("Not supported"); + } + + private: + std::weak_ptr instance_; + std::unique_ptr module_; + std::vector methods_; +}; + +} + +jni::local_ref JMethodDescriptor::getMethod() const { + static auto method = javaClassStatic()->getField("method"); + return getFieldValue(method); +} + +std::string JMethodDescriptor::getSignature() const { + static auto signature = javaClassStatic()->getField("signature"); + return getFieldValue(signature)->toStdString(); +} + +std::string JMethodDescriptor::getName() const { + static auto name = javaClassStatic()->getField("name"); + return getFieldValue(name)->toStdString(); +} + +std::string JMethodDescriptor::getType() const { + static auto type = javaClassStatic()->getField("type"); + return getFieldValue(type)->toStdString(); +} + +void ModuleRegistryHolder::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", ModuleRegistryHolder::initHybrid), + }); +} + +ModuleRegistryHolder::ModuleRegistryHolder( + CatalystInstanceImpl* catalystInstanceImpl, + jni::alias_ref::javaobject> javaModules, + jni::alias_ref::javaobject> cxxModules) { + std::vector> modules; + std::weak_ptr winstance(catalystInstanceImpl->getInstance()); + for (const auto& jm : *javaModules) { + modules.emplace_back(folly::make_unique(jm)); + } + for (const auto& cm : *cxxModules) { + modules.emplace_back( + folly::make_unique(winstance, std::move(cthis(cm)->getModule()))); + } + + registry_ = std::make_shared(std::move(modules)); +} + +Callback makeCallback(std::weak_ptr instance, ExecutorToken token, const folly::dynamic& callbackId) { + if (!callbackId.isInt()) { + throw std::invalid_argument("Expected callback(s) as final argument"); + } + + auto id = callbackId.getInt(); + return [winstance = std::move(instance), token, id](folly::dynamic args) { + if (auto instance = winstance.lock()) { + jni::ThreadScope guard; + instance->callJSCallback(token, id, std::move(args)); + } + }; +} + +} +} diff --git a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.h b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.h new file mode 100644 index 00000000000000..40a1143636a9ec --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.h @@ -0,0 +1,94 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include +#include + +namespace facebook { +namespace react { + +class Instance; +class CatalystInstanceImpl; + +struct JReflectMethod : public jni::JavaClass { + static constexpr auto kJavaDescriptor = "Ljava/lang/reflect/Method;"; + + jmethodID getMethodID() { + auto id = jni::Environment::current()->FromReflectedMethod(self()); + jni::throwPendingJniExceptionAsCppException(); + return id; + } +}; + +struct JMethodDescriptor : public jni::JavaClass { + static constexpr auto kJavaDescriptor = + "Lcom/facebook/react/cxxbridge/JavaModuleWrapper$MethodDescriptor;"; + + jni::local_ref getMethod() const; + std::string getSignature() const; + std::string getName() const; + std::string getType() const; +}; + +struct JBaseJavaModule : public jni::JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/BaseJavaModule;"; +}; + +struct JavaModuleWrapper : jni::JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/cxxbridge/JavaModuleWrapper;"; + + jni::local_ref getModule() { + static auto getModule = javaClassStatic()->getMethod("getModule"); + return getModule(self()); + } + + jni::local_ref::javaobject> getMethodDescriptors() { + static auto getMethods = + getClass()->getMethod::javaobject()>("getMethodDescriptors"); + return getMethods(self()); + } + + jni::local_ref::javaobject> newGetMethodDescriptors() { + static auto getMethods = + getClass()->getMethod::javaobject()>("newGetMethodDescriptors"); + return getMethods(self()); + } +}; + +class ModuleRegistryHolder : public jni::HybridClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/cxxbridge/ModuleRegistryHolder;"; + + std::shared_ptr getModuleRegistry() { + return registry_; + } + + static jni::local_ref initHybrid( + jni::alias_ref, + CatalystInstanceImpl* catalystInstanceImpl, + jni::alias_ref::javaobject> javaModules, + jni::alias_ref::javaobject> cxxModules) { + return makeCxxInstance(catalystInstanceImpl, javaModules, cxxModules); + } + + static void registerNatives(); + + private: + friend HybridBase; + ModuleRegistryHolder( + CatalystInstanceImpl* catalystInstanceImpl, + jni::alias_ref::javaobject> javaModules, + jni::alias_ref::javaobject> cxxModules); + + facebook::xplat::module::CxxModule::Callback makeCallback(const folly::dynamic& callbackId); + + std::shared_ptr registry_; +}; + +using Callback = std::function; +Callback makeCallback(std::weak_ptr instance, ExecutorToken token, const folly::dynamic& callbackId); + +}} diff --git a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp new file mode 100644 index 00000000000000..3242c59ca2bd63 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp @@ -0,0 +1,186 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include +#include +#include +#include +#include +#include +#include +#include +#include "CatalystInstanceImpl.h" +#include "JavaScriptExecutorHolder.h" +#include "JSCPerfLogging.h" +#include "JSLoader.h" +#include "ModuleRegistryHolder.h" +#include "ProxyExecutor.h" +#include "WebWorkers.h" +#include "JCallback.h" + +#include + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +namespace { + +static std::string getApplicationDir(const char* methodName) { + // Get the Application Context object + auto getApplicationClass = findClassLocal( + "com/facebook/react/common/ApplicationHolder"); + auto getApplicationMethod = getApplicationClass->getStaticMethod( + "getApplication", + "()Landroid/app/Application;" + ); + auto application = getApplicationMethod(getApplicationClass); + + // Get getCacheDir() from the context + auto getDirMethod = findClassLocal("android/app/Application") + ->getMethod(methodName, + "()Ljava/io/File;" + ); + auto dirObj = getDirMethod(application); + + // Call getAbsolutePath() on the returned File object + auto getAbsolutePathMethod = findClassLocal("java/io/File") + ->getMethod("getAbsolutePath"); + return getAbsolutePathMethod(dirObj)->toStdString(); +} + +static std::string getApplicationCacheDir() { + return getApplicationDir("getCacheDir"); +} + +static std::string getApplicationPersistentDir() { + return getApplicationDir("getFilesDir"); +} + +static JSValueRef nativeLoggingHook( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], JSValueRef *exception) { + android_LogPriority logLevel = ANDROID_LOG_DEBUG; + if (argumentCount > 1) { + int level = (int) JSValueToNumber(ctx, arguments[1], NULL); + // The lowest log level we get from JS is 0. We shift and cap it to be + // in the range the Android logging method expects. + logLevel = std::min( + static_cast(level + ANDROID_LOG_DEBUG), + ANDROID_LOG_FATAL); + } + if (argumentCount > 0) { + JSStringRef jsString = JSValueToStringCopy(ctx, arguments[0], NULL); + String message = String::adopt(jsString); + FBLOG_PRI(logLevel, "ReactNativeJS", "%s", message.str().c_str()); + } + return JSValueMakeUndefined(ctx); +} + +static JSValueRef nativePerformanceNow( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], JSValueRef *exception) { + static const int64_t NANOSECONDS_IN_SECOND = 1000000000LL; + static const int64_t NANOSECONDS_IN_MILLISECOND = 1000000LL; + + // This is equivalent to android.os.SystemClock.elapsedRealtime() in native + struct timespec now; + clock_gettime(CLOCK_MONOTONIC_RAW, &now); + int64_t nano = now.tv_sec * NANOSECONDS_IN_SECOND + now.tv_nsec; + return JSValueMakeNumber(ctx, (nano / (double)NANOSECONDS_IN_MILLISECOND)); +} + +class JSCJavaScriptExecutorHolder : public HybridClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/cxxbridge/JSCJavaScriptExecutor;"; + + static local_ref initHybrid(alias_ref, ReadableNativeArray* jscConfigArray) { + // See JSCJavaScriptExecutor.Factory() for the other side of this hack. + folly::dynamic jscConfigMap = jscConfigArray->array[0]; + jscConfigMap["PersistentDirectory"] = getApplicationPersistentDir(); + return makeCxxInstance( + std::make_shared(getApplicationCacheDir(), std::move(jscConfigMap))); + } + + static void registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", JSCJavaScriptExecutorHolder::initHybrid), + }); + } + + private: + friend HybridBase; + using HybridBase::HybridBase; +}; + +struct JavaJSExecutor : public JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/JavaJSExecutor;"; +}; + +class ProxyJavaScriptExecutorHolder : public HybridClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/cxxbridge/ProxyJavaScriptExecutor;"; + + static local_ref initHybrid( + alias_ref, alias_ref executorInstance) { + return makeCxxInstance( + std::make_shared( + make_global(executorInstance))); + } + + static void registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", ProxyJavaScriptExecutorHolder::initHybrid), + }); + } + + private: + friend HybridBase; + using HybridBase::HybridBase; +}; + + +class JReactMarker : public JavaClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/ReactMarker;"; + static void logMarker(const std::string& marker) { + static auto cls = javaClassStatic(); + static auto meth = cls->getStaticMethod("logMarker"); + meth(cls, marker); + } +}; + +} + +extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { + return initialize(vm, [] { + // Inject some behavior into react/ + ReactMarker::logMarker = JReactMarker::logMarker; + WebWorkerUtil::createWebWorkerThread = WebWorkers::createWebWorkerThread; + WebWorkerUtil::loadScriptFromAssets = + [] (const std::string& assetName) { + return loadScriptFromAssets(assetName); + }; + WebWorkerUtil::loadScriptFromNetworkSync = WebWorkers::loadScriptFromNetworkSync; + PerfLogging::installNativeHooks = addNativePerfLoggingHooks; + JSNativeHooks::loggingHook = nativeLoggingHook; + JSNativeHooks::nowHook = nativePerformanceNow; + JSCJavaScriptExecutorHolder::registerNatives(); + ProxyJavaScriptExecutorHolder::registerNatives(); + CatalystInstanceImpl::registerNatives(); + ModuleRegistryHolder::registerNatives(); + CxxModuleWrapper::registerNatives(); + JCallbackImpl::registerNatives(); + registerJSLoaderNatives(); + }); +} + +}} diff --git a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.h b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.h new file mode 100644 index 00000000000000..5cd3c1749e62bf --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.h @@ -0,0 +1,14 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include + +namespace facebook { +namespace react { + +jmethodID getLogMarkerMethod(); +} // namespace react +} // namespace facebook diff --git a/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp b/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp new file mode 100644 index 00000000000000..659c9f84186c14 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp @@ -0,0 +1,116 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "ProxyExecutor.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace facebook { +namespace react { + +const auto EXECUTOR_BASECLASS = "com/facebook/react/bridge/JavaJSExecutor"; + +static std::string executeJSCallWithProxy( + jobject executor, + const std::string& methodName, + const std::vector& arguments) { + static auto executeJSCall = + jni::findClassStatic(EXECUTOR_BASECLASS)->getMethod("executeJSCall"); + + auto result = executeJSCall( + executor, + jni::make_jstring(methodName).get(), + jni::make_jstring(folly::toJson(arguments).c_str()).get()); + return result->toString(); +} + +std::unique_ptr ProxyExecutorOneTimeFactory::createJSExecutor( + std::shared_ptr delegate, std::shared_ptr) { + return folly::make_unique(std::move(m_executor), delegate); +} + +ProxyExecutor::ProxyExecutor(jni::global_ref&& executorInstance, + std::shared_ptr delegate) + : m_executor(std::move(executorInstance)) + , m_delegate(delegate) { + + folly::dynamic nativeModuleConfig = folly::dynamic::array; + + { + SystraceSection s("collectNativeModuleDescriptions"); + for (const auto& name : delegate->moduleNames()) { + nativeModuleConfig.push_back(delegate->getModuleConfig(name)); + } + } + + folly::dynamic config = + folly::dynamic::object + ("remoteModuleConfig", std::move(nativeModuleConfig)); + + SystraceSection t("setGlobalVariable"); + setGlobalVariable( + "__fbBatchedBridgeConfig", + folly::make_unique(folly::toJson(config))); +} + +ProxyExecutor::~ProxyExecutor() { + m_executor.reset(); +} + +void ProxyExecutor::loadApplicationScript( + std::unique_ptr, + std::string sourceURL) { + static auto loadApplicationScript = + jni::findClassStatic(EXECUTOR_BASECLASS)->getMethod("loadApplicationScript"); + + // The proxy ignores the script data passed in. + + loadApplicationScript( + m_executor.get(), + jni::make_jstring(sourceURL).get()); + executeJSCallWithProxy(m_executor.get(), "flushedQueue", std::vector()); +} + +void ProxyExecutor::setJSModulesUnbundle(std::unique_ptr) { + jni::throwNewJavaException( + "java/lang/UnsupportedOperationException", + "Loading application unbundles is not supported for proxy executors"); +} + +void ProxyExecutor::callFunction(const std::string& moduleId, const std::string& methodId, const folly::dynamic& arguments) { + std::vector call{ + moduleId, + methodId, + std::move(arguments), + }; + std::string result = executeJSCallWithProxy(m_executor.get(), "callFunctionReturnFlushedQueue", std::move(call)); + m_delegate->callNativeModules(*this, result, true); +} + +void ProxyExecutor::invokeCallback(const double callbackId, const folly::dynamic& arguments) { + std::vector call{ + (double) callbackId, + std::move(arguments) + }; + std::string result = executeJSCallWithProxy(m_executor.get(), "invokeCallbackAndReturnFlushedQueue", std::move(call)); + m_delegate->callNativeModules(*this, result, true); +} + +void ProxyExecutor::setGlobalVariable(std::string propName, + std::unique_ptr jsonValue) { + static auto setGlobalVariable = + jni::findClassStatic(EXECUTOR_BASECLASS)->getMethod("setGlobalVariable"); + + setGlobalVariable( + m_executor.get(), + jni::make_jstring(propName).get(), + jni::make_jstring(jsonValue->c_str()).get()); +} + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.h b/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.h new file mode 100644 index 00000000000000..02328dd7b6e6f8 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.h @@ -0,0 +1,56 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include +#include +#include "OnLoad.h" + +namespace facebook { +namespace react { + +/** + * This executor factory can only create a single executor instance because it moves + * executorInstance global reference to the executor instance it creates. + */ +class ProxyExecutorOneTimeFactory : public JSExecutorFactory { +public: + ProxyExecutorOneTimeFactory(jni::global_ref&& executorInstance) : + m_executor(std::move(executorInstance)) {} + virtual std::unique_ptr createJSExecutor( + std::shared_ptr delegate, + std::shared_ptr queue) override; + +private: + jni::global_ref m_executor; +}; + +class ProxyExecutor : public JSExecutor { +public: + ProxyExecutor(jni::global_ref&& executorInstance, + std::shared_ptr delegate); + virtual ~ProxyExecutor() override; + virtual void loadApplicationScript( + std::unique_ptr script, + std::string sourceURL) override; + virtual void setJSModulesUnbundle( + std::unique_ptr bundle) override; + virtual void callFunction( + const std::string& moduleId, + const std::string& methodId, + const folly::dynamic& arguments) override; + virtual void invokeCallback( + const double callbackId, + const folly::dynamic& arguments) override; + virtual void setGlobalVariable( + std::string propName, + std::unique_ptr jsonValue) override; + +private: + jni::global_ref m_executor; + std::shared_ptr m_delegate; +}; + +} } diff --git a/ReactAndroid/src/main/jni/xreact/jni/WebWorkers.h b/ReactAndroid/src/main/jni/xreact/jni/WebWorkers.h new file mode 100644 index 00000000000000..a37df26c6a12d0 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/WebWorkers.h @@ -0,0 +1,50 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "JMessageQueueThread.h" + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +class WebWorkers : public JavaClass { +public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/webworkers/WebWorkers;"; + + static std::unique_ptr createWebWorkerThread(int id, MessageQueueThread *ownerMessageQueueThread) { + static auto method = WebWorkers::javaClassStatic()-> + getStaticMethod("createWebWorkerThread"); + auto res = method(WebWorkers::javaClassStatic(), id, static_cast(ownerMessageQueueThread)->jobj()); + return folly::make_unique(res); + } + + static std::string loadScriptFromNetworkSync(const std::string& url, const std::string& tempfileName) { + static auto method = WebWorkers::javaClassStatic()-> + getStaticMethod("downloadScriptToFileSync"); + method( + WebWorkers::javaClassStatic(), + jni::make_jstring(url).get(), + jni::make_jstring(tempfileName).get()); + + std::ifstream tempFile(tempfileName); + if (!tempFile.good()) { + throw std::runtime_error("Didn't find worker script file at " + tempfileName); + } + std::stringstream buffer; + buffer << tempFile.rdbuf(); + std::remove(tempfileName.c_str()); + return buffer.str(); + } +}; + +} } diff --git a/ReactAndroid/src/main/jni/xreact/perftests/BUCK b/ReactAndroid/src/main/jni/xreact/perftests/BUCK new file mode 100644 index 00000000000000..ee130d8517d1e2 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/perftests/BUCK @@ -0,0 +1,21 @@ +cxx_library( + name = 'perftests', + srcs = [ 'OnLoad.cpp' ], + soname = 'libnativereactperftests.so', + compiler_flags = [ + '-fexceptions', + ], + deps = [ + '//native:base', + '//native/fb:fb', + '//xplat/folly:molly', + '//xplat/react/module:module', + ], + visibility = [ + '//instrumentation_tests/com/facebook/react/...', + ], +) + +project_config( + src_target = ':perftests', +) diff --git a/ReactAndroid/src/main/jni/xreact/perftests/OnLoad.cpp b/ReactAndroid/src/main/jni/xreact/perftests/OnLoad.cpp new file mode 100644 index 00000000000000..ba262b67207f2c --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/perftests/OnLoad.cpp @@ -0,0 +1,156 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include +#include +#include +#include + +#include +#include + +namespace facebook { +namespace react { + +using facebook::jni::alias_ref; + +namespace { + +// This is a wrapper around the Java proxy to the javascript module. This +// allows us to call functions on the js module from c++. +class JavaJSModule : public jni::JavaClass { +public: + static constexpr auto kJavaDescriptor = + "Lcom/facebook/react/CatalystBridgeBenchmarks$BridgeBenchmarkModule;"; + + static void bounceCxx(alias_ref obj, int iters) { + static auto method = javaClassLocal()->getMethod("bounceCxx"); + method(obj, iters); + } + + static void bounceArgsCxx( + alias_ref obj, + int iters, + int a, int b, + double x, double y, + const std::string& s, const std::string& t) { + static auto method = + javaClassLocal()->getMethod("bounceArgsCxx"); + method(obj, iters, a, b, x, y, jni::make_jstring(s).get(), jni::make_jstring(t).get()); + } +}; + +// This is just the test instance itself. Used only to countdown the latch. +class CatalystBridgeBenchmarks : public jni::JavaClass { +public: + static constexpr auto kJavaDescriptor = + "Lcom/facebook/react/CatalystBridgeBenchmarks;"; + + static void countDown(alias_ref obj) { + static auto method = javaClassLocal()->getMethod("countDown"); + method(obj); + } +}; + +// This is the shared data for two cxx bounce threads. +struct Data { + std::mutex m; + std::condition_variable cv; + bool leftActive; + Data() : leftActive(true) {} +}; +Data data; + +void runBounce(jni::alias_ref, bool isLeft, int iters) { + for (int i = 0; i < iters; i++) { + std::unique_lock lk(data.m); + data.cv.wait(lk, [&]{ return data.leftActive == isLeft; }); + data.leftActive = !isLeft; + data.cv.notify_one(); + } +} + +static jni::global_ref jsModule; +static jni::global_ref javaTestInstance; + +class CxxBenchmarkModule : public xplat::module::CxxModule { +public: + virtual std::string getName() override { + return "CxxBenchmarkModule"; + } + + virtual auto getConstants() -> std::map override { + return std::map(); + } + + virtual auto getMethods() -> std::vector override { + return std::vector{ + Method("bounce", [this] (folly::dynamic args) { + this->bounce(xplat::jsArgAsInt(args, 0)); + }), + Method("bounceArgs", [this] (folly::dynamic args) { + this->bounceArgs( + xplat::jsArgAsInt(args, 0), + xplat::jsArgAsInt(args, 1), + xplat::jsArgAsInt(args, 2), + xplat::jsArgAsDouble(args, 3), + xplat::jsArgAsDouble(args, 4), + xplat::jsArgAsString(args, 5), + xplat::jsArgAsString(args, 6)); + }), + }; + } + + void bounce(int iters) { + if (iters == 0) { + CatalystBridgeBenchmarks::countDown(javaTestInstance); + } else { + JavaJSModule::bounceCxx(jsModule, iters - 1); + } + } + + void bounceArgs( + int iters, + int a, int b, + double x, double y, + const std::string& s, const std::string& t) { + if (iters == 0) { + CatalystBridgeBenchmarks::countDown(javaTestInstance); + } else { + JavaJSModule::bounceArgsCxx(jsModule, iters - 1, a, b, x, y, s, t); + } + } +}; + + +void setUp( + alias_ref obj, + alias_ref mod) { + javaTestInstance = jni::make_global(obj); + jsModule = jni::make_global(mod); +} + +void tearDown( + alias_ref) { + javaTestInstance.reset(); + jsModule.reset(); +} + +} +} +} + +extern "C" facebook::xplat::module::CxxModule* CxxBenchmarkModule() { + return new facebook::react::CxxBenchmarkModule(); +} + +extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { + return facebook::jni::initialize(vm, [] { + facebook::jni::registerNatives( + "com/facebook/react/CatalystBridgeBenchmarks", { + makeNativeMethod("runNativeBounce", facebook::react::runBounce), + makeNativeMethod("nativeSetUp", facebook::react::setUp), + makeNativeMethod("nativeTearDown", facebook::react::tearDown), + }); + }); +} + From 62e588beced7252cb1b3468577a5957895885a85 Mon Sep 17 00:00:00 2001 From: Fred Liu Date: Thu, 19 May 2016 21:39:58 -0700 Subject: [PATCH 034/843] Fix Android flash and iOS juttering Summary: Before: - Android had the slideout row flash upon render due to it being rendered first - iOS had the left side of each row load first, then rerender to show entire row when `scrollViewWidth` is available Reason: - Android was loading the slideout view first without an opacity check - iOS was loading the swipeable view with width 0 first then stretching to `scrollViewWidth` when it was available via `onLayout` Fix: Render swipeable view with `flex: 1` then render slideout view Reviewed By: fkgozali Differential Revision: D3321466 fbshipit-source-id: 92a3b5e22034e06d05986ddb8c348796bafbbf34 --- .../Experimental/SwipeableRow/SwipeableRow.js | 65 +++++++------------ 1 file changed, 23 insertions(+), 42 deletions(-) diff --git a/Libraries/Experimental/SwipeableRow/SwipeableRow.js b/Libraries/Experimental/SwipeableRow/SwipeableRow.js index 1880f559e9f0c5..dfac8e99b98854 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableRow.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableRow.js @@ -25,7 +25,6 @@ const Animated = require('Animated'); const PanResponder = require('PanResponder'); -const Platform = require('Platform'); const React = require('React'); const StyleSheet = require('StyleSheet'); const View = require('View'); @@ -49,11 +48,7 @@ const SwipeableRow = React.createClass({ propTypes: { isOpen: PropTypes.bool, - /** - * Left position of the maximum open swipe. If unspecified, swipe will open - * fully to the left - */ - maxSwipeDistance: PropTypes.number, + maxSwipeDistance: PropTypes.number.isRequired, onOpen: PropTypes.func, onSwipeEnd: PropTypes.func.isRequired, onSwipeStart: PropTypes.func.isRequired, @@ -79,17 +74,13 @@ const SwipeableRow = React.createClass({ * component A to be transparent until component B is loaded. */ isSwipeableViewRendered: false, - /** - * scrollViewWidth can change based on orientation, thus it's stored as a - * state variable. This means all styles depending on it will be inline - */ - scrollViewWidth: 0, }; }, getDefaultProps(): Object { return { isOpen: false, + maxSwipeDistance: 0, onSwipeEnd: emptyFunction, onSwipeStart: emptyFunction, swipeThreshold: 30, @@ -122,26 +113,26 @@ const SwipeableRow = React.createClass({ }, render(): ReactElement { - const slideoutStyle = [styles.slideOutContainer]; - if (Platform.OS === 'ios') { - slideoutStyle.push({opacity: this.state.isSwipeableViewRendered ? 1 : 0}); - } - // The view hidden behind the main view - const slideOutView = ( - - {this.props.slideoutView} - - ); + let slideOutView; + if (this.state.isSwipeableViewRendered) { + slideOutView = ( + + {this.props.slideoutView} + + ); + } - // The swipable item + // The swipeable item const swipeableView = ( + style={[ + styles.swipeableContainer, + { + transform: [{translateX: this.state.currentLeft}], + }, + ]}> {this.props.children} ); @@ -149,8 +140,7 @@ const SwipeableRow = React.createClass({ return ( + style={styles.container}> {slideOutView} {swipeableView} @@ -158,7 +148,7 @@ const SwipeableRow = React.createClass({ }, _onSwipeableViewLayout(event: Object): void { - if (!this._isSwipeableViewRendered && this.state.scrollViewWidth !== 0) { + if (!this.state.isSwipeableViewRendered) { this.setState({ isSwipeableViewRendered: true, }); @@ -198,10 +188,7 @@ const SwipeableRow = React.createClass({ }, _animateToOpenPosition(): void { - const toValue = this.props.maxSwipeDistance - ? -this.props.maxSwipeDistance - : -this.state.scrollViewWidth; - this._animateTo(toValue); + this._animateTo(-this.props.maxSwipeDistance); }, _animateToClosedPosition(): void { @@ -235,15 +222,6 @@ const SwipeableRow = React.createClass({ this.props.onSwipeEnd(); }, - - _onLayoutChange(event: Object): void { - const width = event.nativeEvent.layout.width; - if (width && width !== this.state.scrollViewWidth) { - this.setState({ - scrollViewWidth: width, - }); - } - }, }); const styles = StyleSheet.create({ @@ -259,6 +237,9 @@ const styles = StyleSheet.create({ right: 0, top: 0, }, + swipeableContainer: { + flex: 1, + }, }); module.exports = SwipeableRow; From 3ccfb58701d61449b74e387d2b9eced25bdefe31 Mon Sep 17 00:00:00 2001 From: Fred Liu Date: Thu, 19 May 2016 21:40:03 -0700 Subject: [PATCH 035/843] Simplify SwipeableRow styling Summary: - Removed some styling from `SwipeableRow` that wasn't doing much and made slide out view full height Reviewed By: fkgozali Differential Revision: D3322849 fbshipit-source-id: 811eee9032c142c61d303ae7e966d8ef7903adaf --- .../Experimental/SwipeableRow/SwipeableRow.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Libraries/Experimental/SwipeableRow/SwipeableRow.js b/Libraries/Experimental/SwipeableRow/SwipeableRow.js index dfac8e99b98854..e22c6878ea3932 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableRow.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableRow.js @@ -74,6 +74,7 @@ const SwipeableRow = React.createClass({ * component A to be transparent until component B is loaded. */ isSwipeableViewRendered: false, + rowHeight: (null: ?number), }; }, @@ -117,7 +118,10 @@ const SwipeableRow = React.createClass({ let slideOutView; if (this.state.isSwipeableViewRendered) { slideOutView = ( - + {this.props.slideoutView} ); @@ -139,8 +143,7 @@ const SwipeableRow = React.createClass({ return ( + {...this._panResponder.panHandlers}> {slideOutView} {swipeableView} @@ -151,6 +154,7 @@ const SwipeableRow = React.createClass({ if (!this.state.isSwipeableViewRendered) { this.setState({ isSwipeableViewRendered: true, + rowHeight: event.nativeEvent.layout.height, }); } }, @@ -225,13 +229,8 @@ const SwipeableRow = React.createClass({ }); const styles = StyleSheet.create({ - container: { - flex: 1, - flexDirection: 'row', - }, slideOutContainer: { bottom: 0, - flex: 1, left: 0, position: 'absolute', right: 0, From 00c77800c9f5390de26feceb7fec06e06e90236e Mon Sep 17 00:00:00 2001 From: Andrej Badin Date: Fri, 20 May 2016 02:41:21 -0700 Subject: [PATCH 036/843] Improve Docs navigation on handheld devices. Summary: Fixes #7519 JS detects handheld device by sniffing UA string (very primitive detection). If on handheld device, event listener is registered. Event handler toggles Docs Navigation overlay after clicking on "Docs" nav button. Original Docs Navigation panel is taken out of the natural page flow using pure CSS and is styled to look "good" on device. As a result of this, Navigation overlay is ONLY visible when you are at Docs page, otherwise "Docs" nav button takes you Docs page first. iPhone/iPad previews ![iphone](https://cloud.githubusercontent.com/assets/829963/15409630/f1a64b1a-1e15-11e6-92eb-f85c5cd06754.gif) ![ipad](https://cloud.githubusercontent.com/assets/829963/15409631/f1a6f952-1e15-11e6-8f5c-6f89f54e6814.gif) Closes https://github.com/facebook/react-native/pull/7640 Differential Revision: D3325440 Pulled By: vjeux fbshipit-source-id: a06b21d743d56bfea5db5b750836856c3af9bbe2 --- website/core/DocsSidebar.js | 43 ++++--- website/core/HeaderLinks.js | 5 +- website/src/react-native/css/react-native.css | 105 +++++++++++++++--- website/src/react-native/js/scripts.js | 18 +++ 4 files changed, 133 insertions(+), 38 deletions(-) diff --git a/website/core/DocsSidebar.js b/website/core/DocsSidebar.js index d8640e40d7908c..7b643aba98d79d 100644 --- a/website/core/DocsSidebar.js +++ b/website/core/DocsSidebar.js @@ -69,32 +69,31 @@ var DocsSidebar = React.createClass({ }, getLink: function(metadata) { - if (metadata.permalink.match(/^https?:/)) { - return metadata.permalink; - } - return metadata.permalink + '#content'; + return metadata.permalink; }, render: function() { return
- {this.getCategories().map((category) => -
-

{category.name}

- -
- )} +
+ {this.getCategories().map((category) => +
+

{category.name}

+ +
+ )} +
; } }); diff --git a/website/core/HeaderLinks.js b/website/core/HeaderLinks.js index b5dbffde94262c..2cb72850be8d84 100644 --- a/website/core/HeaderLinks.js +++ b/website/core/HeaderLinks.js @@ -14,7 +14,7 @@ var AlgoliaDocSearch = require('AlgoliaDocSearch'); var HeaderLinks = React.createClass({ linksInternal: [ - {section: 'docs', href: 'docs/getting-started.html', text: 'Docs'}, + {section: 'docs', href: 'docs/getting-started.html', text: 'Docs', target: '.nav-docs'}, {section: 'support', href: 'support.html', text: 'Support'}, {section: 'showcase', href: 'showcase.html', text: 'Showcase'}, {section: 'blog', href: 'blog/', text: 'Blog'}, @@ -30,7 +30,8 @@ var HeaderLinks = React.createClass({
  • + className={link.section === this.props.section ? 'active' : ''} + data-target={link.target}> {link.text}
  • diff --git a/website/src/react-native/css/react-native.css b/website/src/react-native/css/react-native.css index 8ba8d434a7e106..c768d8370e935d 100644 --- a/website/src/react-native/css/react-native.css +++ b/website/src/react-native/css/react-native.css @@ -481,6 +481,92 @@ h1:hover .hash-link, h2:hover .hash-link, h3:hover .hash-link, h4:hover .hash-li border-bottom: 0; } +@media screen and (max-device-width: 960px) { + .nav-docs { + position: fixed; + z-index: 90; + top: 0; + left: 0; + width: 100%; + height: 100%; + margin: 0; + padding: 53px 0 0 0; + background: #3B3738; + /* Transition these properties */ + transition: opacity 0.3s, visibility 0.3s; + visibility: hidden; + opacity: 0; + } + + .nav-docs-viewport { + border-top: 1px solid rgb(5, 165, 209); + height: 100%; + padding: 25px; + overflow: scroll; + -webkit-overflow-scrolling: touch; + top: -30px; + position: relative; + transition: top 0.3s; + } + + /* Active state */ + .nav-docs.in { + visibility: visible; + opacity: 1; + } + + .nav-docs.in .nav-docs-viewport { + top: 0; + } + + .nav-docs * { + -webkit-font-smoothing: antialiased; + } + + .nav-docs-section + .nav-docs-section { + margin-top: 50px; + } + + .nav-docs-section li { + margin: 5px 0; + } + + .nav-docs-section h3, + .nav-docs-section a { + color: white; + } + + .nav-docs-section h3 { + border-bottom: 1px solid white; + margin-bottom: 10px; + opacity: 0.3; + } + + .nav-docs-section a { + margin-right: 25px; + font-size: 120%; + padding: 5px 0; + } + + .nav-docs-section a.active { + border-bottom-style: solid; + border-bottom-width: 1px; + color: rgb(5, 165, 209); + } +} + +@media screen and (min-device-width: 641px) and (max-device-width: 1024px) { + .nav-docs-section ul { + display: flex; + flex-wrap: wrap; + } + + /* Display 2 columns on tablet */ + .nav-docs-section li { + width: 50%; + } +} + .nav-blog li { margin-bottom: 5px; } @@ -999,13 +1085,13 @@ small code, li code, p code { outline: none; } -@media screen and (max-width: 960px) { - .nav-main { - position: static; +@media screen and (max-width: 680px) { + .container { + padding-top: 100px; } - .container { - padding-top: 0; + .nav-docs { + padding-top: 103px; } } @@ -1345,15 +1431,6 @@ div[data-twttr-id] iframe { border: none; padding: 0; } - .nav-docs h3 { - margin: 0; - } - .nav-docs { - float: none; - width: auto; - margin-top: -8px; - margin-bottom: 20px; - } h1 { font-size: 30px; line-height: 30px; diff --git a/website/src/react-native/js/scripts.js b/website/src/react-native/js/scripts.js index 9bed77965b3cdd..0932c31ff18130 100644 --- a/website/src/react-native/js/scripts.js +++ b/website/src/react-native/js/scripts.js @@ -7,6 +7,10 @@ document.addEventListener('DOMContentLoaded', init); function init() { + if (isMobile()) { + document.querySelector('.nav-site-wrapper a[data-target]').addEventListener('click', toggleTargetNav); + } + var backdrop = document.querySelector('.modal-backdrop'); if (!backdrop) return; @@ -42,4 +46,18 @@ modal.classList.remove('modal-open'); } + function toggleTargetNav(event) { + var target = document.body.querySelector(event.target.getAttribute('data-target')); + + if (target) { + event.preventDefault(); + target.classList.toggle('in'); + } + } + + // Primitive mobile detection + function isMobile() { + return ( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ); + } + }()); From 149d0b91c2707323a140fa7fc6486cd705e434bc Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Fri, 20 May 2016 04:52:08 -0700 Subject: [PATCH 037/843] Merge rnpm into react-native Summary: This is initial (first step) in the merging process. For now, we are just going to move our code as is into `local-cli` folder (first commit). There were other tweaks made in separate commits to make it easier to go through the code as the diff is expected to be rather large. The purpose of this is to make it easier to start working in small batches and improving the CLI incrementally on a daily basis. Current codebase will still leave in `rnpm` organisation on Github where we keep working on new features, bugs and ship releases to `npm` until we finish our integration and provide a nice interface for users to migrate (in case it changes at all) Flow, Jest and npm will ignore this folder for now until we integrate it properly. Tests are to be rewritten from mocha to jest in `rnpm/link`. We will hook them all up as soon as we start using them in local-cli. For now, there's no point in having them running and possibly breaking the builds. We will announce next steps with Kureev later this week Closes https://github.com/facebook/react-native/pull/7550 Differential Revision: D3327772 Pulled By: mkonicek fbshipit-source-id: 90faa4bd78476d93ed21b1253e0d95c755d28a30 --- .flowconfig | 3 + .npmignore | 2 + local-cli/rnpm/core/package.json | 50 ++ .../config/android/findAndroidAppFolder.js | 21 + .../core/src/config/android/findManifest.js | 17 + .../config/android/findPackageClassName.js | 20 + .../rnpm/core/src/config/android/index.js | 111 +++ .../core/src/config/android/readManifest.js | 10 + local-cli/rnpm/core/src/config/findAssets.js | 20 + local-cli/rnpm/core/src/config/index.js | 44 + .../rnpm/core/src/config/ios/findProject.js | 47 + local-cli/rnpm/core/src/config/ios/index.js | 31 + .../rnpm/core/src/config/wrapCommands.js | 9 + local-cli/rnpm/core/src/findPlugins.js | 37 + local-cli/rnpm/core/src/getCommands.js | 20 + local-cli/rnpm/core/src/makeCommand.js | 25 + .../test/android/findAndroidAppFolder.spec.js | 30 + .../core/test/android/findManifest.spec.js | 25 + .../test/android/findPackageClassName.spec.js | 25 + .../test/android/getDependencyConfig.spec.js | 49 ++ .../test/android/getProjectConfig.spec.js | 56 ++ .../core/test/android/readManifest.spec.js | 31 + local-cli/rnpm/core/test/findAssets.spec.js | 31 + local-cli/rnpm/core/test/findPlugins.js | 46 + local-cli/rnpm/core/test/fixtures/android.js | 49 ++ local-cli/rnpm/core/test/fixtures/commands.js | 15 + .../rnpm/core/test/fixtures/dependencies.js | 27 + .../test/fixtures/files/AndroidManifest.xml | 4 + .../rnpm/core/test/fixtures/files/Main.java | 8 + .../test/fixtures/files/ReactPackage.java | 35 + .../core/test/fixtures/files/package.json | 62 ++ .../core/test/fixtures/files/project.pbxproj | 804 ++++++++++++++++++ local-cli/rnpm/core/test/fixtures/ios.js | 14 + local-cli/rnpm/core/test/fixtures/projects.js | 24 + local-cli/rnpm/core/test/getCommands.spec.js | 171 ++++ .../rnpm/core/test/ios/findProject.spec.js | 84 ++ .../core/test/ios/getProjectConfig.spec.js | 26 + local-cli/rnpm/core/test/makeCommand.spec.js | 35 + local-cli/rnpm/install/index.js | 11 + local-cli/rnpm/install/package.json | 26 + local-cli/rnpm/install/src/install.js | 26 + local-cli/rnpm/install/src/uninstall.js | 26 + local-cli/rnpm/link/index.js | 9 + local-cli/rnpm/link/package.json | 52 ++ local-cli/rnpm/link/src/android/copyAssets.js | 17 + local-cli/rnpm/link/src/android/fs.js | 8 + local-cli/rnpm/link/src/android/getPrefix.js | 16 + .../rnpm/link/src/android/isInstalled.js | 8 + .../android/patches/0.17/makeImportPatch.js | 6 + .../android/patches/0.17/makePackagePatch.js | 10 + .../android/patches/0.18/makeImportPatch.js | 6 + .../android/patches/0.18/makePackagePatch.js | 10 + .../android/patches/0.20/makeImportPatch.js | 6 + .../android/patches/0.20/makePackagePatch.js | 10 + .../link/src/android/patches/applyParams.js | 14 + .../link/src/android/patches/applyPatch.js | 8 + .../src/android/patches/makeBuildPatch.js | 6 + .../src/android/patches/makeSettingsPatch.js | 26 + .../src/android/patches/makeStringsPatch.js | 14 + .../link/src/android/patches/revokePatch.js | 8 + .../link/src/android/registerNativeModule.js | 38 + .../rnpm/link/src/android/unlinkAssets.js | 20 + .../src/android/unregisterNativeModule.js | 47 + .../rnpm/link/src/getDependencyConfig.js | 17 + .../rnpm/link/src/getProjectDependencies.js | 9 + .../rnpm/link/src/getReactNativeVersion.js | 5 + local-cli/rnpm/link/src/groupFilesByType.js | 27 + .../rnpm/link/src/ios/addFileToProject.js | 14 + .../link/src/ios/addProjectToLibraries.js | 13 + .../rnpm/link/src/ios/addSharedLibraries.js | 3 + .../link/src/ios/addToHeaderSearchPaths.js | 5 + local-cli/rnpm/link/src/ios/copyAssets.js | 57 ++ local-cli/rnpm/link/src/ios/createGroup.js | 27 + .../rnpm/link/src/ios/getBuildProperty.js | 18 + local-cli/rnpm/link/src/ios/getGroup.js | 34 + .../rnpm/link/src/ios/getHeaderSearchPath.js | 52 ++ .../rnpm/link/src/ios/getHeadersInFolder.js | 18 + local-cli/rnpm/link/src/ios/getPlist.js | 20 + local-cli/rnpm/link/src/ios/getPlistPath.js | 15 + local-cli/rnpm/link/src/ios/getProducts.js | 12 + .../rnpm/link/src/ios/hasLibraryImported.js | 10 + local-cli/rnpm/link/src/ios/isInstalled.js | 18 + .../rnpm/link/src/ios/mapHeaderSearchPaths.js | 36 + .../rnpm/link/src/ios/registerNativeModule.js | 67 ++ .../src/ios/removeFromHeaderSearchPaths.js | 10 + .../removeFromPbxItemContainerProxySection.js | 16 + .../ios/removeFromPbxReferenceProxySection.js | 15 + .../src/ios/removeFromProjectReferences.js | 26 + .../link/src/ios/removeFromStaticLibraries.js | 21 + .../rnpm/link/src/ios/removeProductGroup.js | 11 + .../src/ios/removeProjectFromLibraries.js | 12 + .../link/src/ios/removeProjectFromProject.js | 26 + .../link/src/ios/removeSharedLibraries.js | 3 + local-cli/rnpm/link/src/ios/unlinkAssets.js | 54 ++ .../link/src/ios/unregisterNativeModule.js | 54 ++ local-cli/rnpm/link/src/link.js | 136 +++ local-cli/rnpm/link/src/pollParams.js | 9 + local-cli/rnpm/link/src/promiseWaterfall.js | 14 + local-cli/rnpm/link/src/unlink.js | 117 +++ .../link/test/android/isInstalled.spec.js | 28 + .../android/patches/0.17/makeImportPatch.js | 31 + .../android/patches/0.17/makePackagePatch.js | 36 + .../android/patches/0.18/makeImportPatch.js | 31 + .../android/patches/0.18/makePackagePatch.js | 36 + .../android/patches/0.20/makeImportPatch.js | 31 + .../android/patches/0.20/makePackagePatch.js | 36 + .../link/test/android/patches/applyPatch.js | 17 + .../android/patches/makeBuildPatch.spec.js | 17 + .../android/patches/makeSettingsPatch.spec.js | 36 + .../fixtures/android/0.17/MainActivity.java | 78 ++ .../android/0.17/patchedMainActivity.java | 80 ++ .../fixtures/android/0.18/MainActivity.java | 39 + .../android/0.18/patchedMainActivity.java | 41 + .../fixtures/android/0.20/MainActivity.java | 40 + .../link/test/fixtures/android/build.gradle | 5 + .../test/fixtures/android/patchedBuild.gradle | 6 + .../fixtures/android/patchedSettings.gradle | 5 + .../test/fixtures/android/settings.gradle | 3 + .../link/test/fixtures/linearGradient.pbxproj | 258 ++++++ .../rnpm/link/test/fixtures/project.pbxproj | 778 +++++++++++++++++ .../link/test/getDependencyConfig.spec.js | 23 + local-cli/rnpm/link/test/getPrefix.spec.js | 20 + .../link/test/getProjectDependencies.spec.js | 27 + .../rnpm/link/test/groupFilesByType.spec.js | 23 + .../link/test/ios/addFileToProject.spec.js | 22 + .../test/ios/addProjectToLibraries.spec.js | 28 + .../rnpm/link/test/ios/createGroup.spec.js | 49 ++ .../link/test/ios/getBuildProperty.spec.js | 19 + local-cli/rnpm/link/test/ios/getGroup.spec.js | 36 + .../link/test/ios/getHeaderSearchPath.spec.js | 58 ++ .../link/test/ios/getHeadersInFolder.spec.js | 45 + local-cli/rnpm/link/test/ios/getPlist.spec.js | 23 + .../rnpm/link/test/ios/getPlistPath.spec.js | 19 + .../rnpm/link/test/ios/getProducts.spec.js | 20 + .../link/test/ios/hasLibraryImported.spec.js | 24 + .../rnpm/link/test/ios/isInstalled.spec.js | 39 + .../test/ios/mapHeaderSearchPaths.spec.js | 23 + .../test/ios/removeProjectFromLibraries.js | 33 + .../test/ios/removeProjectFromProject.spec.js | 32 + local-cli/rnpm/link/test/link.spec.js | 199 +++++ .../rnpm/link/test/promiseWaterfall.spec.js | 37 + package.json | 3 +- 142 files changed, 5980 insertions(+), 1 deletion(-) create mode 100644 .npmignore create mode 100644 local-cli/rnpm/core/package.json create mode 100644 local-cli/rnpm/core/src/config/android/findAndroidAppFolder.js create mode 100644 local-cli/rnpm/core/src/config/android/findManifest.js create mode 100644 local-cli/rnpm/core/src/config/android/findPackageClassName.js create mode 100644 local-cli/rnpm/core/src/config/android/index.js create mode 100644 local-cli/rnpm/core/src/config/android/readManifest.js create mode 100644 local-cli/rnpm/core/src/config/findAssets.js create mode 100644 local-cli/rnpm/core/src/config/index.js create mode 100644 local-cli/rnpm/core/src/config/ios/findProject.js create mode 100644 local-cli/rnpm/core/src/config/ios/index.js create mode 100644 local-cli/rnpm/core/src/config/wrapCommands.js create mode 100644 local-cli/rnpm/core/src/findPlugins.js create mode 100644 local-cli/rnpm/core/src/getCommands.js create mode 100644 local-cli/rnpm/core/src/makeCommand.js create mode 100644 local-cli/rnpm/core/test/android/findAndroidAppFolder.spec.js create mode 100644 local-cli/rnpm/core/test/android/findManifest.spec.js create mode 100644 local-cli/rnpm/core/test/android/findPackageClassName.spec.js create mode 100644 local-cli/rnpm/core/test/android/getDependencyConfig.spec.js create mode 100644 local-cli/rnpm/core/test/android/getProjectConfig.spec.js create mode 100644 local-cli/rnpm/core/test/android/readManifest.spec.js create mode 100644 local-cli/rnpm/core/test/findAssets.spec.js create mode 100644 local-cli/rnpm/core/test/findPlugins.js create mode 100644 local-cli/rnpm/core/test/fixtures/android.js create mode 100644 local-cli/rnpm/core/test/fixtures/commands.js create mode 100644 local-cli/rnpm/core/test/fixtures/dependencies.js create mode 100644 local-cli/rnpm/core/test/fixtures/files/AndroidManifest.xml create mode 100644 local-cli/rnpm/core/test/fixtures/files/Main.java create mode 100644 local-cli/rnpm/core/test/fixtures/files/ReactPackage.java create mode 100644 local-cli/rnpm/core/test/fixtures/files/package.json create mode 100644 local-cli/rnpm/core/test/fixtures/files/project.pbxproj create mode 100644 local-cli/rnpm/core/test/fixtures/ios.js create mode 100644 local-cli/rnpm/core/test/fixtures/projects.js create mode 100644 local-cli/rnpm/core/test/getCommands.spec.js create mode 100644 local-cli/rnpm/core/test/ios/findProject.spec.js create mode 100644 local-cli/rnpm/core/test/ios/getProjectConfig.spec.js create mode 100644 local-cli/rnpm/core/test/makeCommand.spec.js create mode 100644 local-cli/rnpm/install/index.js create mode 100644 local-cli/rnpm/install/package.json create mode 100644 local-cli/rnpm/install/src/install.js create mode 100644 local-cli/rnpm/install/src/uninstall.js create mode 100644 local-cli/rnpm/link/index.js create mode 100644 local-cli/rnpm/link/package.json create mode 100644 local-cli/rnpm/link/src/android/copyAssets.js create mode 100644 local-cli/rnpm/link/src/android/fs.js create mode 100644 local-cli/rnpm/link/src/android/getPrefix.js create mode 100644 local-cli/rnpm/link/src/android/isInstalled.js create mode 100644 local-cli/rnpm/link/src/android/patches/0.17/makeImportPatch.js create mode 100644 local-cli/rnpm/link/src/android/patches/0.17/makePackagePatch.js create mode 100644 local-cli/rnpm/link/src/android/patches/0.18/makeImportPatch.js create mode 100644 local-cli/rnpm/link/src/android/patches/0.18/makePackagePatch.js create mode 100644 local-cli/rnpm/link/src/android/patches/0.20/makeImportPatch.js create mode 100644 local-cli/rnpm/link/src/android/patches/0.20/makePackagePatch.js create mode 100644 local-cli/rnpm/link/src/android/patches/applyParams.js create mode 100644 local-cli/rnpm/link/src/android/patches/applyPatch.js create mode 100644 local-cli/rnpm/link/src/android/patches/makeBuildPatch.js create mode 100644 local-cli/rnpm/link/src/android/patches/makeSettingsPatch.js create mode 100644 local-cli/rnpm/link/src/android/patches/makeStringsPatch.js create mode 100644 local-cli/rnpm/link/src/android/patches/revokePatch.js create mode 100644 local-cli/rnpm/link/src/android/registerNativeModule.js create mode 100644 local-cli/rnpm/link/src/android/unlinkAssets.js create mode 100644 local-cli/rnpm/link/src/android/unregisterNativeModule.js create mode 100644 local-cli/rnpm/link/src/getDependencyConfig.js create mode 100644 local-cli/rnpm/link/src/getProjectDependencies.js create mode 100644 local-cli/rnpm/link/src/getReactNativeVersion.js create mode 100644 local-cli/rnpm/link/src/groupFilesByType.js create mode 100644 local-cli/rnpm/link/src/ios/addFileToProject.js create mode 100644 local-cli/rnpm/link/src/ios/addProjectToLibraries.js create mode 100644 local-cli/rnpm/link/src/ios/addSharedLibraries.js create mode 100644 local-cli/rnpm/link/src/ios/addToHeaderSearchPaths.js create mode 100644 local-cli/rnpm/link/src/ios/copyAssets.js create mode 100644 local-cli/rnpm/link/src/ios/createGroup.js create mode 100644 local-cli/rnpm/link/src/ios/getBuildProperty.js create mode 100644 local-cli/rnpm/link/src/ios/getGroup.js create mode 100644 local-cli/rnpm/link/src/ios/getHeaderSearchPath.js create mode 100644 local-cli/rnpm/link/src/ios/getHeadersInFolder.js create mode 100644 local-cli/rnpm/link/src/ios/getPlist.js create mode 100644 local-cli/rnpm/link/src/ios/getPlistPath.js create mode 100644 local-cli/rnpm/link/src/ios/getProducts.js create mode 100644 local-cli/rnpm/link/src/ios/hasLibraryImported.js create mode 100644 local-cli/rnpm/link/src/ios/isInstalled.js create mode 100644 local-cli/rnpm/link/src/ios/mapHeaderSearchPaths.js create mode 100644 local-cli/rnpm/link/src/ios/registerNativeModule.js create mode 100644 local-cli/rnpm/link/src/ios/removeFromHeaderSearchPaths.js create mode 100644 local-cli/rnpm/link/src/ios/removeFromPbxItemContainerProxySection.js create mode 100644 local-cli/rnpm/link/src/ios/removeFromPbxReferenceProxySection.js create mode 100644 local-cli/rnpm/link/src/ios/removeFromProjectReferences.js create mode 100644 local-cli/rnpm/link/src/ios/removeFromStaticLibraries.js create mode 100644 local-cli/rnpm/link/src/ios/removeProductGroup.js create mode 100644 local-cli/rnpm/link/src/ios/removeProjectFromLibraries.js create mode 100644 local-cli/rnpm/link/src/ios/removeProjectFromProject.js create mode 100644 local-cli/rnpm/link/src/ios/removeSharedLibraries.js create mode 100644 local-cli/rnpm/link/src/ios/unlinkAssets.js create mode 100644 local-cli/rnpm/link/src/ios/unregisterNativeModule.js create mode 100644 local-cli/rnpm/link/src/link.js create mode 100644 local-cli/rnpm/link/src/pollParams.js create mode 100644 local-cli/rnpm/link/src/promiseWaterfall.js create mode 100644 local-cli/rnpm/link/src/unlink.js create mode 100644 local-cli/rnpm/link/test/android/isInstalled.spec.js create mode 100644 local-cli/rnpm/link/test/android/patches/0.17/makeImportPatch.js create mode 100644 local-cli/rnpm/link/test/android/patches/0.17/makePackagePatch.js create mode 100644 local-cli/rnpm/link/test/android/patches/0.18/makeImportPatch.js create mode 100644 local-cli/rnpm/link/test/android/patches/0.18/makePackagePatch.js create mode 100644 local-cli/rnpm/link/test/android/patches/0.20/makeImportPatch.js create mode 100644 local-cli/rnpm/link/test/android/patches/0.20/makePackagePatch.js create mode 100644 local-cli/rnpm/link/test/android/patches/applyPatch.js create mode 100644 local-cli/rnpm/link/test/android/patches/makeBuildPatch.spec.js create mode 100644 local-cli/rnpm/link/test/android/patches/makeSettingsPatch.spec.js create mode 100644 local-cli/rnpm/link/test/fixtures/android/0.17/MainActivity.java create mode 100644 local-cli/rnpm/link/test/fixtures/android/0.17/patchedMainActivity.java create mode 100644 local-cli/rnpm/link/test/fixtures/android/0.18/MainActivity.java create mode 100644 local-cli/rnpm/link/test/fixtures/android/0.18/patchedMainActivity.java create mode 100644 local-cli/rnpm/link/test/fixtures/android/0.20/MainActivity.java create mode 100644 local-cli/rnpm/link/test/fixtures/android/build.gradle create mode 100644 local-cli/rnpm/link/test/fixtures/android/patchedBuild.gradle create mode 100644 local-cli/rnpm/link/test/fixtures/android/patchedSettings.gradle create mode 100644 local-cli/rnpm/link/test/fixtures/android/settings.gradle create mode 100644 local-cli/rnpm/link/test/fixtures/linearGradient.pbxproj create mode 100644 local-cli/rnpm/link/test/fixtures/project.pbxproj create mode 100644 local-cli/rnpm/link/test/getDependencyConfig.spec.js create mode 100644 local-cli/rnpm/link/test/getPrefix.spec.js create mode 100644 local-cli/rnpm/link/test/getProjectDependencies.spec.js create mode 100644 local-cli/rnpm/link/test/groupFilesByType.spec.js create mode 100644 local-cli/rnpm/link/test/ios/addFileToProject.spec.js create mode 100644 local-cli/rnpm/link/test/ios/addProjectToLibraries.spec.js create mode 100644 local-cli/rnpm/link/test/ios/createGroup.spec.js create mode 100644 local-cli/rnpm/link/test/ios/getBuildProperty.spec.js create mode 100644 local-cli/rnpm/link/test/ios/getGroup.spec.js create mode 100644 local-cli/rnpm/link/test/ios/getHeaderSearchPath.spec.js create mode 100644 local-cli/rnpm/link/test/ios/getHeadersInFolder.spec.js create mode 100644 local-cli/rnpm/link/test/ios/getPlist.spec.js create mode 100644 local-cli/rnpm/link/test/ios/getPlistPath.spec.js create mode 100644 local-cli/rnpm/link/test/ios/getProducts.spec.js create mode 100644 local-cli/rnpm/link/test/ios/hasLibraryImported.spec.js create mode 100644 local-cli/rnpm/link/test/ios/isInstalled.spec.js create mode 100644 local-cli/rnpm/link/test/ios/mapHeaderSearchPaths.spec.js create mode 100644 local-cli/rnpm/link/test/ios/removeProjectFromLibraries.js create mode 100644 local-cli/rnpm/link/test/ios/removeProjectFromProject.spec.js create mode 100644 local-cli/rnpm/link/test/link.spec.js create mode 100644 local-cli/rnpm/link/test/promiseWaterfall.spec.js diff --git a/.flowconfig b/.flowconfig index 92991af156f8bb..dd29ffc7434af9 100644 --- a/.flowconfig +++ b/.flowconfig @@ -46,6 +46,9 @@ # Ignore BUCK generated folders .*\.buckd/ +# Ignore RNPM +.*/local-cli/rnpm/.* + .*/node_modules/is-my-json-valid/test/.*\.json .*/node_modules/iconv-lite/encodings/tables/.*\.json .*/node_modules/y18n/test/.*\.json diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000000000..65a8df8d40829e --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +# rnpm +/local-cli/rnpm diff --git a/local-cli/rnpm/core/package.json b/local-cli/rnpm/core/package.json new file mode 100644 index 00000000000000..2ff57ffd09f8ac --- /dev/null +++ b/local-cli/rnpm/core/package.json @@ -0,0 +1,50 @@ +{ + "name": "rnpm", + "version": "1.7.0", + "description": "React Native Package Manager", + "main": "./src/getCommands.js", + "scripts": { + "test": "jest" + }, + "jest": { + "testDirectoryName": "test", + "collectCoverage": true, + "testRunner": "/node_modules/jest-cli/src/testRunners/jasmine/jasmine2.js" + }, + "author": "Amazing React Native Community (https://github.com/facebook/react-native)", + "contributors": [ + "Alexey Kureev (https://github.com/Kureev)", + "Mike Grabowski (https://github.com/grabbou)" + ], + "engines": { + "node": ">= 4.0.0" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/rnpm/rnpm/issues" + }, + "keywords": [ + "react-native", + "native-modules", + "packager", + "rnpm" + ], + "homepage": "https://github.com/rnpm/rnpm#readme", + "dependencies": { + "commander": "^2.9.0", + "glob": "^7.0.1", + "lodash": "^3.10.1", + "rnpm-plugin-install": "^1.1.0", + "rnpm-plugin-link": "^1.7.4", + "update-notifier": "^0.6.0", + "xmldoc": "^0.4.0" + }, + "devDependencies": { + "babel-eslint": "^4.1.5", + "eslint": "^1.9.0", + "mock-fs": "^3.5.0", + "mock-require": "^1.2.1", + "rewire": "^2.5.1", + "jest-cli": "^0.9.0-fb2" + } +} diff --git a/local-cli/rnpm/core/src/config/android/findAndroidAppFolder.js b/local-cli/rnpm/core/src/config/android/findAndroidAppFolder.js new file mode 100644 index 00000000000000..2f1ddb355fc1a1 --- /dev/null +++ b/local-cli/rnpm/core/src/config/android/findAndroidAppFolder.js @@ -0,0 +1,21 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * @param {String} folder Folder to seek in + * @return {String} + */ +module.exports = function findAndroidAppFolder(folder) { + const flat = 'android'; + const nested = path.join('android', 'app'); + + if (fs.existsSync(path.join(folder, nested))) { + return nested; + } + + if (fs.existsSync(path.join(folder, flat))) { + return flat; + } + + return null; +}; diff --git a/local-cli/rnpm/core/src/config/android/findManifest.js b/local-cli/rnpm/core/src/config/android/findManifest.js new file mode 100644 index 00000000000000..3827b4276e7ee5 --- /dev/null +++ b/local-cli/rnpm/core/src/config/android/findManifest.js @@ -0,0 +1,17 @@ +const glob = require('glob'); +const path = require('path'); + +/** + * Find an android application path in the folder + * + * @param {String} folder Name of the folder where to seek + * @return {String} + */ +module.exports = function findManifest(folder) { + const manifestPath = glob.sync(path.join('**', 'AndroidManifest.xml'), { + cwd: folder, + ignore: ['node_modules/**', '**/build/**', 'Examples/**', 'examples/**'], + })[0]; + + return manifestPath ? path.join(folder, manifestPath) : null; +}; diff --git a/local-cli/rnpm/core/src/config/android/findPackageClassName.js b/local-cli/rnpm/core/src/config/android/findPackageClassName.js new file mode 100644 index 00000000000000..30a33b1337096d --- /dev/null +++ b/local-cli/rnpm/core/src/config/android/findPackageClassName.js @@ -0,0 +1,20 @@ +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); + +/** + * Gets package's class name (class that implements ReactPackage) + * by searching for its declaration in all Java files present in the folder + * + * @param {String} folder Folder to find java files + */ +module.exports = function getPackageClassName(folder) { + const files = glob.sync('**/*.java', { cwd: folder }); + + const packages = files + .map(filePath => fs.readFileSync(path.join(folder, filePath), 'utf8')) + .map(file => file.match(/class (.*) implements ReactPackage/)) + .filter(match => match); + + return packages.length ? packages[0][1] : null; +}; diff --git a/local-cli/rnpm/core/src/config/android/index.js b/local-cli/rnpm/core/src/config/android/index.js new file mode 100644 index 00000000000000..f8277307f1844f --- /dev/null +++ b/local-cli/rnpm/core/src/config/android/index.js @@ -0,0 +1,111 @@ +const path = require('path'); +const fs = require('fs'); +const glob = require('glob'); +const findAndroidAppFolder = require('./findAndroidAppFolder'); +const findManifest = require('./findManifest'); +const readManifest = require('./readManifest'); +const findPackageClassName = require('./findPackageClassName'); + +const getPackageName = (manifest) => manifest.attr.package; + +/** + * Gets android project config by analyzing given folder and taking some + * defaults specified by user into consideration + */ +exports.projectConfig = function projectConfigAndroid(folder, userConfig) { + const src = userConfig.sourceDir || findAndroidAppFolder(folder); + + if (!src) { + return null; + } + + const sourceDir = path.join(folder, src); + const isFlat = sourceDir.indexOf('app') === -1; + const manifestPath = findManifest(sourceDir); + + if (!manifestPath) { + return null; + } + + const manifest = readManifest(manifestPath); + + const packageName = userConfig.packageName || getPackageName(manifest); + const packageFolder = userConfig.packageFolder || + packageName.replace(/\./g, path.sep); + + const mainActivityPath = path.join( + sourceDir, + userConfig.mainActivityPath || `src/main/java/${packageFolder}/MainActivity.java` + ); + + const stringsPath = path.join( + sourceDir, + userConfig.stringsPath || 'src/main/res/values/strings.xml' + ); + + const settingsGradlePath = path.join( + folder, + 'android', + userConfig.settingsGradlePath || 'settings.gradle' + ); + + const assetsPath = path.join( + sourceDir, + userConfig.assetsPath || 'src/main/assets' + ); + + const buildGradlePath = path.join( + sourceDir, + userConfig.buildGradlePath || 'build.gradle' + ); + + return { + sourceDir, + isFlat, + folder, + stringsPath, + manifestPath, + buildGradlePath, + settingsGradlePath, + assetsPath, + mainActivityPath, + }; +}; + +/** + * Same as projectConfigAndroid except it returns + * different config that applies to packages only + */ +exports.dependencyConfig = function dependencyConfigAndroid(folder, userConfig) { + const src = userConfig.sourceDir || findAndroidAppFolder(folder); + + if (!src) { + return null; + } + + const sourceDir = path.join(folder, src); + const manifestPath = findManifest(sourceDir); + + if (!manifestPath) { + return null; + } + + const manifest = readManifest(manifestPath); + const packageName = userConfig.packageName || getPackageName(manifest); + const packageClassName = findPackageClassName(sourceDir); + + /** + * This module has no package to export + */ + if (!packageClassName) { + return null; + } + + const packageImportPath = userConfig.packageImportPath || + `import ${packageName}.${packageClassName};`; + + const packageInstance = userConfig.packageInstance || + `new ${packageClassName}()`; + + return { sourceDir, folder, manifest, packageImportPath, packageInstance }; +}; diff --git a/local-cli/rnpm/core/src/config/android/readManifest.js b/local-cli/rnpm/core/src/config/android/readManifest.js new file mode 100644 index 00000000000000..056447695c19d6 --- /dev/null +++ b/local-cli/rnpm/core/src/config/android/readManifest.js @@ -0,0 +1,10 @@ +const fs = require('fs'); +const xml = require('xmldoc'); + +/** + * @param {String} manifestPath + * @return {XMLDocument} Parsed manifest's content + */ +module.exports = function readManifest(manifestPath) { + return new xml.XmlDocument(fs.readFileSync(manifestPath, 'utf8')); +}; diff --git a/local-cli/rnpm/core/src/config/findAssets.js b/local-cli/rnpm/core/src/config/findAssets.js new file mode 100644 index 00000000000000..0cd2f0dd3b1dfa --- /dev/null +++ b/local-cli/rnpm/core/src/config/findAssets.js @@ -0,0 +1,20 @@ +const glob = require('glob'); +const path = require('path'); + +const findAssetsInFolder = (folder) => + glob.sync(path.join(folder, '**'), { nodir: true }); + +/** + * Given an array of assets folders, e.g. ['Fonts', 'Images'], + * it globs in them to find all files that can be copied. + * + * It returns an array of absolute paths to files found. + */ +module.exports = function findAssets(folder, assets) { + return (assets || []) + .map(assetsFolder => path.join(folder, assetsFolder)) + .reduce((assets, assetsFolder) => + assets.concat(findAssetsInFolder(assetsFolder)), + [] + ); +}; diff --git a/local-cli/rnpm/core/src/config/index.js b/local-cli/rnpm/core/src/config/index.js new file mode 100644 index 00000000000000..ac75735108204d --- /dev/null +++ b/local-cli/rnpm/core/src/config/index.js @@ -0,0 +1,44 @@ +const path = require('path'); + +const android = require('./android'); +const ios = require('./ios'); +const findAssets = require('./findAssets'); +const wrapCommands = require('./wrapCommands'); + +const getRNPMConfig = (folder) => + require(path.join(folder, './package.json')).rnpm || {}; + +/** + * Returns project config from the current working directory + * @return {Object} + */ +exports.getProjectConfig = function getProjectConfig() { + const folder = process.cwd(); + const rnpm = getRNPMConfig(folder); + + return Object.assign({}, rnpm, { + ios: ios.projectConfig(folder, rnpm.ios || {}), + android: android.projectConfig(folder, rnpm.android || {}), + assets: findAssets(folder, rnpm.assets), + }); +}; + +/** + * Returns a dependency config from node_modules/ + * @param {String} packageName Dependency name + * @return {Object} + */ +exports.getDependencyConfig = function getDependencyConfig(packageName) { + const folder = path.join(process.cwd(), 'node_modules', packageName); + const rnpm = getRNPMConfig( + path.join(process.cwd(), 'node_modules', packageName.split('/')[0]) + ); + + return Object.assign({}, rnpm, { + ios: ios.dependencyConfig(folder, rnpm.ios || {}), + android: android.dependencyConfig(folder, rnpm.android || {}), + assets: findAssets(folder, rnpm.assets), + commands: wrapCommands(rnpm.commands), + params: rnpm.params || [], + }); +}; diff --git a/local-cli/rnpm/core/src/config/ios/findProject.js b/local-cli/rnpm/core/src/config/ios/findProject.js new file mode 100644 index 00000000000000..17c99cf4880354 --- /dev/null +++ b/local-cli/rnpm/core/src/config/ios/findProject.js @@ -0,0 +1,47 @@ +const glob = require('glob'); +const path = require('path'); + +/** + * Glob pattern to look for xcodeproj + */ +const GLOB_PATTERN = '**/*.xcodeproj'; + +/** + * Regexp matching all test projects + */ +const TEST_PROJECTS = /test|example|sample/i; + +/** + * Base iOS folder + */ +const IOS_BASE = 'ios'; + +/** + * These folders will be excluded from search to speed it up + */ +const GLOB_EXCLUDE_PATTERN = ['**/@(Pods|node_modules)/**']; + +/** + * Finds iOS project by looking for all .xcodeproj files + * in given folder. + * + * Returns first match if files are found or null + * + * Note: `./ios/*.xcodeproj` are returned regardless of the name + */ +module.exports = function findProject(folder) { + const projects = glob + .sync(GLOB_PATTERN, { + cwd: folder, + ignore: GLOB_EXCLUDE_PATTERN, + }) + .filter(project => { + return path.dirname(project) === IOS_BASE || !TEST_PROJECTS.test(project); + }); + + if (projects.length === 0) { + return null; + } + + return projects[0]; +}; diff --git a/local-cli/rnpm/core/src/config/ios/index.js b/local-cli/rnpm/core/src/config/ios/index.js new file mode 100644 index 00000000000000..04ada20bfc84f8 --- /dev/null +++ b/local-cli/rnpm/core/src/config/ios/index.js @@ -0,0 +1,31 @@ +const path = require('path'); +const findProject = require('./findProject'); + +/** + * Returns project config by analyzing given folder and applying some user defaults + * when constructing final object + */ +exports.projectConfig = function projectConfigIOS(folder, userConfig) { + const project = userConfig.project || findProject(folder); + + /** + * No iOS config found here + */ + if (!project) { + return null; + } + + const projectPath = path.join(folder, project); + + return { + sourceDir: path.dirname(projectPath), + folder: folder, + pbxprojPath: path.join(projectPath, 'project.pbxproj'), + projectPath: projectPath, + projectName: path.basename(projectPath), + libraryFolder: userConfig.libraryFolder || 'Libraries', + plist: userConfig.plist || [], + }; +}; + +exports.dependencyConfig = exports.projectConfig; diff --git a/local-cli/rnpm/core/src/config/wrapCommands.js b/local-cli/rnpm/core/src/config/wrapCommands.js new file mode 100644 index 00000000000000..7be7f1cf36a95c --- /dev/null +++ b/local-cli/rnpm/core/src/config/wrapCommands.js @@ -0,0 +1,9 @@ +const makeCommand = require('../makeCommand'); + +module.exports = function wrapCommands(commands) { + const mappedCommands = {}; + Object.keys(commands || []).forEach((k) => + mappedCommands[k] = makeCommand(commands[k]) + ); + return mappedCommands; +}; diff --git a/local-cli/rnpm/core/src/findPlugins.js b/local-cli/rnpm/core/src/findPlugins.js new file mode 100644 index 00000000000000..58d38559ee9e86 --- /dev/null +++ b/local-cli/rnpm/core/src/findPlugins.js @@ -0,0 +1,37 @@ +const path = require('path'); +const fs = require('fs'); +const union = require('lodash').union; +const uniq = require('lodash').uniq; +const flatten = require('lodash').flatten; + +/** + * Filter dependencies by name pattern + * @param {String} dependency Name of the dependency + * @return {Boolean} If dependency is a rnpm plugin + */ +const isPlugin = (dependency) => !!~dependency.indexOf('rnpm-plugin-'); + +const findPluginInFolder = (folder) => { + var pjson; + try { + pjson = require(path.join(folder, 'package.json')); + } catch (e) { + return []; + } + + const deps = union( + Object.keys(pjson.dependencies || {}), + Object.keys(pjson.devDependencies || {}) + ); + + return deps.filter(isPlugin); +}; + +/** + * Find plugins in package.json of the given folder + * @param {String} folder Path to the folder to get the package.json from + * @type {Array} Array of plugins or an empty array if no package.json found + */ +module.exports = function findPlugins(folders) { + return uniq(flatten(folders.map(findPluginInFolder))); +}; diff --git a/local-cli/rnpm/core/src/getCommands.js b/local-cli/rnpm/core/src/getCommands.js new file mode 100644 index 00000000000000..0de2cf7feadbaa --- /dev/null +++ b/local-cli/rnpm/core/src/getCommands.js @@ -0,0 +1,20 @@ +const path = require('path'); +const fs = require('fs'); +const uniq = require('lodash').uniq; +const flattenDeep = require('lodash').flattenDeep; +const findPlugins = require('./findPlugins'); + +/** + * @return {Array} Array of commands + */ +module.exports = function getCommands() { + const rnpmRoot = path.join(__dirname, '..'); + const appRoot = process.cwd(); + + return uniq( + flattenDeep([ + findPlugins([rnpmRoot]).map(require), + findPlugins([appRoot]).map(name => require(path.join(appRoot, 'node_modules', name))), + ]) + , 'name'); +}; diff --git a/local-cli/rnpm/core/src/makeCommand.js b/local-cli/rnpm/core/src/makeCommand.js new file mode 100644 index 00000000000000..5175f0e6877b2b --- /dev/null +++ b/local-cli/rnpm/core/src/makeCommand.js @@ -0,0 +1,25 @@ +const spawn = require('child_process').spawn; + +module.exports = function makeCommand(command) { + return (cb) => { + if (!cb) { + throw new Error(`You missed a callback function for the ${command} command`); + } + + const args = command.split(' '); + const cmd = args.shift(); + + const commandProcess = spawn(cmd, args, { + stdio: 'inherit', + stdin: 'inherit', + }); + + commandProcess.on('close', function prelink(code) { + if (code) { + throw new Error(`Error occured during executing "${command}" command`); + } + + cb(); + }); + }; +}; diff --git a/local-cli/rnpm/core/test/android/findAndroidAppFolder.spec.js b/local-cli/rnpm/core/test/android/findAndroidAppFolder.spec.js new file mode 100644 index 00000000000000..fe6223aadda65b --- /dev/null +++ b/local-cli/rnpm/core/test/android/findAndroidAppFolder.spec.js @@ -0,0 +1,30 @@ +jest.autoMockOff(); + +const findAndroidAppFolder = require('../../src/config/android/findAndroidAppFolder'); +const mockFs = require('mock-fs'); +const mocks = require('../fixtures/android'); + +describe('android::findAndroidAppFolder', () => { + beforeAll(() => mockFs({ + empty: {}, + nested: { + android: { + app: mocks.valid, + }, + }, + flat: { + android: mocks.valid, + }, + })); + + it('should return an android app folder if it exists in the given folder', () => { + expect(findAndroidAppFolder('flat')).toBe('android'); + expect(findAndroidAppFolder('nested')).toBe('android/app'); + }); + + it('should return `null` if there\'s no android app folder', () => { + expect(findAndroidAppFolder('empty')).toBe(null); + }); + + afterAll(mockFs.restore); +}); diff --git a/local-cli/rnpm/core/test/android/findManifest.spec.js b/local-cli/rnpm/core/test/android/findManifest.spec.js new file mode 100644 index 00000000000000..27a81fd6d4d954 --- /dev/null +++ b/local-cli/rnpm/core/test/android/findManifest.spec.js @@ -0,0 +1,25 @@ +jest.autoMockOff(); + +const findManifest = require('../../src/config/android/findManifest'); +const mockFs = require('mock-fs'); +const mocks = require('../fixtures/android'); + +describe('android::findManifest', () => { + + beforeAll(() => mockFs({ + empty: {}, + flat: { + android: mocks.valid, + }, + })); + + it('should return a manifest path if file exists in the folder', () => { + expect(typeof findManifest('flat')).toBe('string'); + }); + + it('should return `null` if there is no manifest in the folder', () => { + expect(findManifest('empty')).toBe(null); + }); + + afterAll(mockFs.restore); +}); diff --git a/local-cli/rnpm/core/test/android/findPackageClassName.spec.js b/local-cli/rnpm/core/test/android/findPackageClassName.spec.js new file mode 100644 index 00000000000000..fbdd614a16acdb --- /dev/null +++ b/local-cli/rnpm/core/test/android/findPackageClassName.spec.js @@ -0,0 +1,25 @@ +jest.autoMockOff(); + +const findPackageClassName = require('../../src/config/android/findPackageClassName'); +const mockFs = require('mock-fs'); +const mocks = require('../fixtures/android'); + +describe('android::findPackageClassName', () => { + + beforeAll(() => mockFs({ + empty: {}, + flat: { + android: mocks.valid, + }, + })); + + it('should return manifest content if file exists in the folder', () => { + expect(typeof findPackageClassName('flat')).toBe('string'); + }); + + it('should return `null` if there\'s no matches', () => { + expect(findPackageClassName('empty')).toBe(null); + }); + + afterAll(mockFs.restore); +}); diff --git a/local-cli/rnpm/core/test/android/getDependencyConfig.spec.js b/local-cli/rnpm/core/test/android/getDependencyConfig.spec.js new file mode 100644 index 00000000000000..15126a2de97166 --- /dev/null +++ b/local-cli/rnpm/core/test/android/getDependencyConfig.spec.js @@ -0,0 +1,49 @@ +jest.autoMockOff(); + +const getDependencyConfig = require('../../src/config/android').dependencyConfig; +const mockFs = require('mock-fs'); +const mocks = require('../fixtures/android'); +const userConfig = {}; + +describe('android::getDependencyConfig', () => { + + beforeAll(() => mockFs({ + empty: {}, + nested: { + android: { + app: mocks.valid, + }, + }, + corrupted: { + android: { + app: mocks.corrupted, + }, + }, + noPackage: { + android: {}, + }, + })); + + it('should return an object with android project configuration', () => { + expect(getDependencyConfig('nested', userConfig)).not.toBe(null); + expect(typeof getDependencyConfig('nested', userConfig)).toBe('object'); + }); + + it('should return `null` if manifest file hasn\'t been found', () => { + expect(getDependencyConfig('empty', userConfig)).toBe(null); + }); + + it('should return `null` if android project was not found', () => { + expect(getDependencyConfig('empty', userConfig)).toBe(null); + }); + + it('should return `null` if android project does not contain ReactPackage', () => { + expect(getDependencyConfig('noPackage', userConfig)).toBe(null); + }); + + it('should return `null` if it can\'t find a packageClassName', () => { + expect(getDependencyConfig('corrupted', userConfig)).toBe(null); + }); + + afterAll(mockFs.restore); +}); diff --git a/local-cli/rnpm/core/test/android/getProjectConfig.spec.js b/local-cli/rnpm/core/test/android/getProjectConfig.spec.js new file mode 100644 index 00000000000000..1e62dbeca0b5bc --- /dev/null +++ b/local-cli/rnpm/core/test/android/getProjectConfig.spec.js @@ -0,0 +1,56 @@ +jest.autoMockOff(); + +const getProjectConfig = require('../../src/config/android').projectConfig; +const mockFs = require('mock-fs'); +const mocks = require('../fixtures/android'); + +describe('android::getProjectConfig', () => { + beforeAll(() => mockFs({ + empty: {}, + nested: { + android: { + app: mocks.valid, + }, + }, + flat: { + android: mocks.valid, + }, + noManifest: { + android: {}, + }, + })); + + it('should return `null` if manifest file hasn\'t been found', () => { + const userConfig = {}; + const folder = 'noManifest'; + + expect(getProjectConfig(folder, userConfig)).toBe(null); + }); + + describe('return an object with android project configuration for', () => { + it('nested structure', () => { + const userConfig = {}; + const folder = 'nested'; + + expect(getProjectConfig(folder, userConfig)).not.toBe(null); + expect(typeof getProjectConfig(folder, userConfig)).toBe('object'); + }); + + it('flat structure', () => { + const userConfig = {}; + const folder = 'flat'; + + expect(getProjectConfig(folder, userConfig)).not.toBe(null); + expect(typeof getProjectConfig(folder, userConfig)).toBe('object'); + }); + }); + + it('should return `null` if android project was not found', () => { + const userConfig = {}; + const folder = 'empty'; + + expect(getProjectConfig(folder, userConfig)).toBe(null); + }); + + afterAll(mockFs.restore); +}); diff --git a/local-cli/rnpm/core/test/android/readManifest.spec.js b/local-cli/rnpm/core/test/android/readManifest.spec.js new file mode 100644 index 00000000000000..571e183415361e --- /dev/null +++ b/local-cli/rnpm/core/test/android/readManifest.spec.js @@ -0,0 +1,31 @@ +jest.autoMockOff(); + +const findManifest = require('../../src/config/android/findManifest'); +const readManifest = require('../../src/config/android/readManifest'); +const mockFs = require('mock-fs'); +const mocks = require('../fixtures/android'); + +describe('android::readManifest', () => { + + beforeAll(() => mockFs({ + empty: {}, + nested: { + android: { + app: mocks.valid, + }, + }, + })); + + it('should return manifest content if file exists in the folder', () => { + const manifestPath = findManifest('nested'); + expect(readManifest(manifestPath)).not.toBe(null); + expect(typeof readManifest(manifestPath)).toBe('object'); + }); + + it('should throw an error if there is no manifest in the folder', () => { + const fakeManifestPath = findManifest('empty'); + expect(() => readManifest(fakeManifestPath)).toThrow(); + }); + + afterAll(mockFs.restore); +}); diff --git a/local-cli/rnpm/core/test/findAssets.spec.js b/local-cli/rnpm/core/test/findAssets.spec.js new file mode 100644 index 00000000000000..b31d5fac30cb61 --- /dev/null +++ b/local-cli/rnpm/core/test/findAssets.spec.js @@ -0,0 +1,31 @@ +jest.autoMockOff(); + +const findAssets = require('../src/config/findAssets'); +const mockFs = require('mock-fs'); +const dependencies = require('./fixtures/dependencies'); +const isArray = (arg) => + Object.prototype.toString.call(arg) === '[object Array]'; + +describe('findAssets', () => { + + beforeEach(() => mockFs({ testDir: dependencies.withAssets })); + + it('should return an array of all files in given folders', () => { + const assets = findAssets('testDir', ['fonts', 'images']); + + expect(isArray(assets)).toBeTruthy(); + expect(assets.length).toEqual(3); + }); + + it('should prepend assets paths with the folder path', () => { + const assets = findAssets('testDir', ['fonts', 'images']); + + assets.forEach(assetPath => expect(assetPath).toContain('testDir')); + }); + + it('should return an empty array if given assets are null', () => { + expect(findAssets('testDir', null).length).toEqual(0); + }); + + afterEach(mockFs.restore); +}); diff --git a/local-cli/rnpm/core/test/findPlugins.js b/local-cli/rnpm/core/test/findPlugins.js new file mode 100644 index 00000000000000..c7285687357351 --- /dev/null +++ b/local-cli/rnpm/core/test/findPlugins.js @@ -0,0 +1,46 @@ +jest.autoMockOff(); + +const path = require('path'); +const findPlugins = require('../src/findPlugins'); + +const pjsonPath = path.join(process.cwd(), 'package.json'); +const isArray = (arg) => + Object.prototype.toString.call(arg) === '[object Array]'; + +describe('findPlugins', () => { + + it('should return an array of dependencies', () => { + jest.setMock(pjsonPath, { + dependencies: { 'rnpm-plugin-test': '*' }, + }); + expect(findPlugins([process.cwd()]).length).toBe(1); + expect(findPlugins([process.cwd()])[0]).toBe('rnpm-plugin-test'); + }); + + it('should return an empty array if there\'re no plugins in this folder', () => { + jest.setMock(pjsonPath, {}); + expect(findPlugins([process.cwd()]).length).toBe(0); + }); + + it('should return an empty array if there\'s no package.json in the supplied folder', () => { + expect(isArray(findPlugins(['fake-path']))).toBeTruthy(); + expect(findPlugins(['fake-path']).length).toBe(0); + }); + + it('should return plugins from both dependencies and dev dependencies', () => { + jest.setMock(pjsonPath, { + dependencies: { 'rnpm-plugin-test': '*' }, + devDependencies: { 'rnpm-plugin-test-2': '*' }, + }); + expect(findPlugins([process.cwd()]).length).toEqual(2); + }); + + it('should return unique list of plugins', () => { + jest.setMock(pjsonPath, { + dependencies: { 'rnpm-plugin-test': '*' }, + devDependencies: { 'rnpm-plugin-test': '*' }, + }); + expect(findPlugins([process.cwd()]).length).toEqual(1); + }); + +}); diff --git a/local-cli/rnpm/core/test/fixtures/android.js b/local-cli/rnpm/core/test/fixtures/android.js new file mode 100644 index 00000000000000..ebecf9b8c9029f --- /dev/null +++ b/local-cli/rnpm/core/test/fixtures/android.js @@ -0,0 +1,49 @@ +const fs = require('fs'); +const path = require('path'); + +const manifest = fs.readFileSync(path.join(__dirname, './files/AndroidManifest.xml')); +const mainJavaClass = fs.readFileSync(path.join(__dirname, './files/Main.java')); + +exports.valid = { + src: { + 'AndroidManifest.xml': manifest, + main: { + com: { + some: { + example: { + 'Main.java': mainJavaClass, + 'ReactPackage.java': fs.readFileSync(path.join(__dirname, './files/ReactPackage.java')), + }, + }, + }, + }, + }, +}; + +exports.corrupted = { + src: { + 'AndroidManifest.xml': manifest, + main: { + com: { + some: { + example: {}, + }, + }, + }, + }, +}; + +exports.noPackage = { + src: { + 'AndroidManifest.xml': manifest, + main: { + com: { + some: { + example: { + 'Main.java': mainJavaClass, + }, + }, + }, + }, + }, +}; diff --git a/local-cli/rnpm/core/test/fixtures/commands.js b/local-cli/rnpm/core/test/fixtures/commands.js new file mode 100644 index 00000000000000..cbe4193be29e78 --- /dev/null +++ b/local-cli/rnpm/core/test/fixtures/commands.js @@ -0,0 +1,15 @@ +exports.single = { + func: () => {}, + description: 'Test action', + name: 'test', +}; + +exports.multiple = [{ + func: () => {}, + description: 'Test action #1', + name: 'test1', +}, { + func: () => {}, + description: 'Test action #2', + name: 'test2', +}]; diff --git a/local-cli/rnpm/core/test/fixtures/dependencies.js b/local-cli/rnpm/core/test/fixtures/dependencies.js new file mode 100644 index 00000000000000..ad0ce62529d81a --- /dev/null +++ b/local-cli/rnpm/core/test/fixtures/dependencies.js @@ -0,0 +1,27 @@ +const fs = require('fs'); +const path = require('path'); +const android = require('./android'); + +const pjson = fs.readFileSync(path.join(__dirname, 'files', 'package.json')); + +module.exports = { + valid: { + 'package.json': pjson, + android: android.valid, + }, + withAssets: { + 'package.json': pjson, + android: android.valid, + fonts: { + 'A.ttf': '', + 'B.ttf': '', + }, + images: { + 'C.jpg': '', + }, + }, + noPackage: { + 'package.json': pjson, + android: android.noPackage, + }, +}; diff --git a/local-cli/rnpm/core/test/fixtures/files/AndroidManifest.xml b/local-cli/rnpm/core/test/fixtures/files/AndroidManifest.xml new file mode 100644 index 00000000000000..83cf6c79916298 --- /dev/null +++ b/local-cli/rnpm/core/test/fixtures/files/AndroidManifest.xml @@ -0,0 +1,4 @@ + + diff --git a/local-cli/rnpm/core/test/fixtures/files/Main.java b/local-cli/rnpm/core/test/fixtures/files/Main.java new file mode 100644 index 00000000000000..08dc35a58bfabf --- /dev/null +++ b/local-cli/rnpm/core/test/fixtures/files/Main.java @@ -0,0 +1,8 @@ +package com.some.example; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class SomeExamplePackage {} diff --git a/local-cli/rnpm/core/test/fixtures/files/ReactPackage.java b/local-cli/rnpm/core/test/fixtures/files/ReactPackage.java new file mode 100644 index 00000000000000..b1ea146323fc0b --- /dev/null +++ b/local-cli/rnpm/core/test/fixtures/files/ReactPackage.java @@ -0,0 +1,35 @@ +package com.some.example; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class SomeExamplePackage implements ReactPackage { + + public SomeExamplePackage() {} + + @Override + public List createNativeModules( + ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new SomeExampleModule(reactContext)); + return modules; + } + + @Override + public List> createJSModules() { + return Collections.emptyList(); + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} diff --git a/local-cli/rnpm/core/test/fixtures/files/package.json b/local-cli/rnpm/core/test/fixtures/files/package.json new file mode 100644 index 00000000000000..cce13033d3a405 --- /dev/null +++ b/local-cli/rnpm/core/test/fixtures/files/package.json @@ -0,0 +1,62 @@ +{ + "name": "react-native-vector-icons", + "version": "1.0.0", + "description": "Customizable Icons for React Native with support for NavBar/TabBar, image source and full styling. Choose from 3000+ bundled icons or use your own.", + "main": "index.js", + "bin": { + "generate-icon": "./generate-icon.js" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "rm -rf {Fonts,Entypo.js,EvilIcons.js,FontAwesome.js,Foundation.js,Ionicons.js,MaterialIcons.js,Octicons.js,Zocial.js} && mkdir Fonts && npm run build-entypo && npm run build-evilicons && npm run build-fontawesome && npm run build-foundation && npm run build-ionicons && npm run build-materialicons && npm run build-octicons && npm run build-zocial", + "build-entypo": "mkdir -p tmp/svg && curl https://dl.dropboxusercontent.com/u/4339492/entypo.zip > tmp/entypo.zip && unzip -j tmp/entypo.zip *.svg -x __MACOSX/* -d tmp/svg && fontcustom compile tmp/svg -o tmp -n Entypo -t css -h && node generate-icon tmp/Entypo.css --componentName=Entypo --fontFamily=Entypo > Entypo.js && cp tmp/Entypo.ttf Fonts && rm -rf {tmp,.fontcustom-manifest.json}", + "build-evilicons": "fontcustom compile node_modules/evil-icons/assets/icons -o tmp -n EvilIcons -t css -h && node generate-icon tmp/EvilIcons.css --prefix=.icon-ei- --componentName=EvilIcons --fontFamily=EvilIcons > EvilIcons.js && cp tmp/EvilIcons.ttf Fonts && rm -rf {tmp,.fontcustom-manifest.json}", + "build-fontawesome": "node generate-icon node_modules/font-awesome/css/font-awesome.css --prefix=.fa- --componentName=FontAwesome --fontFamily=FontAwesome > FontAwesome.js && cp node_modules/font-awesome/fonts/fontawesome-webfont.ttf Fonts/FontAwesome.ttf", + "build-foundation": "node generate-icon bower_components/foundation-icon-fonts/foundation-icons.css --prefix=.fi- --componentName=Foundation --fontFamily=fontcustom > Foundation.js && cp bower_components/foundation-icon-fonts/foundation-icons.ttf Fonts/Foundation.ttf", + "build-ionicons": "node generate-icon bower_components/ionicons/css/ionicons.css --prefix=.ion- --componentName=Ionicons --fontFamily=Ionicons > Ionicons.js && cp bower_components/ionicons/fonts/ionicons.ttf Fonts/Ionicons.ttf", + "build-materialicons": "mkdir -p tmp/svg && for f in ./node_modules/material-design-icons/*/svg/production/ic_*_48px.svg; do t=${f/*\\/ic_/}; t=${t/_48px/}; cp \"$f\" \"./tmp/svg/${t//_/-}\"; done && fontcustom compile tmp/svg -o tmp -n MaterialIcons -t css -h && node generate-icon tmp/MaterialIcons.css --componentName=MaterialIcons --fontFamily=MaterialIcons > MaterialIcons.js && cp tmp/MaterialIcons.ttf Fonts && rm -rf {tmp,.fontcustom-manifest.json}", + "build-octicons": "node generate-icon bower_components/octicons/octicons/octicons.css --prefix=.octicon- --componentName=Octicons --fontFamily=octicons > Octicons.js && cp bower_components/octicons/octicons/octicons.ttf Fonts/Octicons.ttf", + "build-zocial": "node generate-icon bower_components/css-social-buttons/css/zocial.css --prefix=.zocial. --componentName=Zocial --fontFamily=zocial > Zocial.js && cp bower_components/css-social-buttons/css/zocial.ttf Fonts/Zocial.ttf" + }, + "keywords": [ + "react-native", + "react-component", + "react-native-component", + "react", + "mobile", + "ios", + "android", + "ui", + "icon", + "icons", + "vector", + "retina", + "font" + ], + "author": { + "name": "Joel Arvidsson", + "email": "joel@oblador.se" + }, + "homepage": "https://github.com/oblador/react-native-vector-icons", + "bugs": { + "url": "https://github.com/oblador/react-native-vector-icons/issues" + }, + "repository": { + "type": "git", + "url": "git://github.com/oblador/react-native-vector-icons.git" + }, + "license": "MIT", + "peerDependencies": { + "react-native": ">=0.4.0 || 0.5.0-rc1 || 0.6.0-rc || 0.7.0-rc || 0.7.0-rc.2 || 0.8.0-rc || 0.8.0-rc.2 || 0.9.0-rc || 0.10.0-rc || 0.11.0-rc || 0.12.0-rc || 0.13.0-rc || 0.14.0-rc || 0.15.0-rc || 0.16.0-rc" + }, + "dependencies": { + "lodash": "^3.8.0", + "yargs": "^3.30.0", + "rnpm-plugin-test": "*" + }, + "devDependencies": { + "evil-icons": "^1.7.6", + "font-awesome": "^4.4.0", + "material-design-icons": "^2.1.1" + } +} diff --git a/local-cli/rnpm/core/test/fixtures/files/project.pbxproj b/local-cli/rnpm/core/test/fixtures/files/project.pbxproj new file mode 100644 index 00000000000000..e5ac7a27b54e32 --- /dev/null +++ b/local-cli/rnpm/core/test/fixtures/files/project.pbxproj @@ -0,0 +1,804 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { +/* Begin PBXBuildFile section */ + 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; + 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; + 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; + 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; }; + 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; }; + 00E356F31AD99517003FC87E /* androidTestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* androidTestTests.m */; }; + 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; }; + 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */; }; + 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */; }; + 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; + 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; + 672DE8B31B124B8088D0D29F /* libBVLinearGradient.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B5255B7628A54AC2A9B4B2A0 /* libBVLinearGradient.a */; }; + 68FEB18F24414EF981BD7940 /* libCodePush.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 53C67FE8F7294B7A83790610 /* libCodePush.a */; }; + 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; + C6C437D070BA42D6BE39198B /* libRCTVideo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A7396DFBAFA4CA092E367F5 /* libRCTVideo.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTActionSheet; + }; + 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTGeolocation; + }; + 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5115D1A9E6B3D00147676; + remoteInfo = RCTImage; + }; + 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B511DB1A9E6C8500147676; + remoteInfo = RCTNetwork; + }; + 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; + remoteInfo = RCTVibration; + }; + 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = androidTest; + }; + 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTSettings; + }; + 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3C86DF461ADF2C930047B81A; + remoteInfo = RCTWebSocket; + }; + 146834031AC3E56700842450 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; + remoteInfo = React; + }; + 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTLinking; + }; + 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5119B1A9E6C1200147676; + remoteInfo = RCTText; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; + 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = "../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj"; sourceTree = ""; }; + 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = "../node_modules/react-native/Libraries/Geolocation/RCTGeolocation.xcodeproj"; sourceTree = ""; }; + 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = "../node_modules/react-native/Libraries/Image/RCTImage.xcodeproj"; sourceTree = ""; }; + 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = "../node_modules/react-native/Libraries/Network/RCTNetwork.xcodeproj"; sourceTree = ""; }; + 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = "../node_modules/react-native/Libraries/Vibration/RCTVibration.xcodeproj"; sourceTree = ""; }; + 00E356EE1AD99517003FC87E /* androidTestTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = androidTestTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 00E356F21AD99517003FC87E /* androidTestTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = androidTestTests.m; sourceTree = ""; }; + 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = "../node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj"; sourceTree = ""; }; + 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = "../node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj"; sourceTree = ""; }; + 13B07F961A680F5B00A75B9A /* androidTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = androidTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = androidTest/AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = androidTest/AppDelegate.m; sourceTree = ""; }; + 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = androidTest/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = androidTest/Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = androidTest/main.m; sourceTree = ""; }; + 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../node_modules/react-native/React/React.xcodeproj"; sourceTree = ""; }; + 53C67FE8F7294B7A83790610 /* libCodePush.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libCodePush.a; sourceTree = ""; }; + 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; + 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; + B5255B7628A54AC2A9B4B2A0 /* libBVLinearGradient.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libBVLinearGradient.a; sourceTree = ""; }; + 467A6CBCB2164E7D9B673D4C /* CodePush.xcodeproj */ = {isa = PBXFileReference; name = "CodePush.xcodeproj"; path = "../node_modules/react-native-code-push/CodePush.xcodeproj"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; }; + FD7121847BA447D8B737F22A /* BVLinearGradient.xcodeproj */ = {isa = PBXFileReference; name = "BVLinearGradient.xcodeproj"; path = "../node_modules/react-native-linear-gradient/BVLinearGradient.xcodeproj"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; }; + 409DA945815C46DEB4F254DB /* RCTVideo.xcodeproj */ = {isa = PBXFileReference; name = "RCTVideo.xcodeproj"; path = "../node_modules/react-native-video/RCTVideo.xcodeproj"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; }; + 3A7396DFBAFA4CA092E367F5 /* libRCTVideo.a */ = {isa = PBXFileReference; name = "libRCTVideo.a"; path = "libRCTVideo.a"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 00E356EB1AD99517003FC87E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 146834051AC3E58100842450 /* libReact.a in Frameworks */, + 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, + 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */, + 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */, + 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */, + 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */, + 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */, + 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, + 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, + 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */, + 672DE8B31B124B8088D0D29F /* libBVLinearGradient.a in Frameworks */, + 68FEB18F24414EF981BD7940 /* libCodePush.a in Frameworks */, + C6C437D070BA42D6BE39198B /* libRCTVideo.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 00C302A81ABCB8CE00DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302B61ABCB90400DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302BC1ABCB91800DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302D41ABCB9D200DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302E01ABCB9EE00DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */, + ); + name = Products; + sourceTree = ""; + }; + 00E356EF1AD99517003FC87E /* androidTestTests */ = { + isa = PBXGroup; + children = ( + 00E356F21AD99517003FC87E /* androidTestTests.m */, + 00E356F01AD99517003FC87E /* Supporting Files */, + ); + path = androidTestTests; + sourceTree = ""; + }; + 00E356F01AD99517003FC87E /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 00E356F11AD99517003FC87E /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 139105B71AF99BAD00B5F7CC /* Products */ = { + isa = PBXGroup; + children = ( + 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */, + ); + name = Products; + sourceTree = ""; + }; + 139FDEE71B06529A00C62182 /* Products */ = { + isa = PBXGroup; + children = ( + 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */, + ); + name = Products; + sourceTree = ""; + }; + 13B07FAE1A68108700A75B9A /* androidTest */ = { + isa = PBXGroup; + children = ( + 008F07F21AC5B25A0029DE68 /* main.jsbundle */, + 13B07FAF1A68108700A75B9A /* AppDelegate.h */, + 13B07FB01A68108700A75B9A /* AppDelegate.m */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, + 13B07FB71A68108700A75B9A /* main.m */, + ); + name = androidTest; + sourceTree = ""; + }; + 146834001AC3E56700842450 /* Products */ = { + isa = PBXGroup; + children = ( + 146834041AC3E56700842450 /* libReact.a */, + ); + name = Products; + sourceTree = ""; + }; + 78C398B11ACF4ADC00677621 /* Products */ = { + isa = PBXGroup; + children = ( + 78C398B91ACF4ADC00677621 /* libRCTLinking.a */, + ); + name = Products; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + 146833FF1AC3E56700842450 /* React.xcodeproj */, + 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */, + 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */, + 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */, + 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */, + 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */, + 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */, + 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, + 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, + 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */, + 467A6CBCB2164E7D9B673D4C /* CodePush.xcodeproj */, + FD7121847BA447D8B737F22A /* BVLinearGradient.xcodeproj */, + 409DA945815C46DEB4F254DB /* RCTVideo.xcodeproj */, + ); + name = Libraries; + sourceTree = ""; + }; + 832341B11AAA6A8300B99B32 /* Products */ = { + isa = PBXGroup; + children = ( + 832341B51AAA6A8300B99B32 /* libRCTText.a */, + ); + name = Products; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* androidTest */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 00E356EF1AD99517003FC87E /* androidTestTests */, + 83CBBA001A601CBA00E9B192 /* Products */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* androidTest.app */, + 00E356EE1AD99517003FC87E /* androidTestTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 00E356ED1AD99517003FC87E /* androidTestTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "androidTestTests" */; + buildPhases = ( + 00E356EA1AD99517003FC87E /* Sources */, + 00E356EB1AD99517003FC87E /* Frameworks */, + 00E356EC1AD99517003FC87E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 00E356F51AD99517003FC87E /* PBXTargetDependency */, + ); + name = androidTestTests; + productName = androidTestTests; + productReference = 00E356EE1AD99517003FC87E /* androidTestTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 13B07F861A680F5B00A75B9A /* androidTest */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "androidTest" */; + buildPhases = ( + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = androidTest; + productName = "Hello World"; + productReference = 13B07F961A680F5B00A75B9A /* androidTest.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 00E356ED1AD99517003FC87E = { + CreatedOnToolsVersion = 6.2; + TestTargetID = 13B07F861A680F5B00A75B9A; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "androidTest" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 00C302A81ABCB8CE00DB3ED1 /* Products */; + ProjectRef = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; + }, + { + ProductGroup = 00C302B61ABCB90400DB3ED1 /* Products */; + ProjectRef = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; + }, + { + ProductGroup = 00C302BC1ABCB91800DB3ED1 /* Products */; + ProjectRef = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; + }, + { + ProductGroup = 78C398B11ACF4ADC00677621 /* Products */; + ProjectRef = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; + }, + { + ProductGroup = 00C302D41ABCB9D200DB3ED1 /* Products */; + ProjectRef = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; + }, + { + ProductGroup = 139105B71AF99BAD00B5F7CC /* Products */; + ProjectRef = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; + }, + { + ProductGroup = 832341B11AAA6A8300B99B32 /* Products */; + ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; + }, + { + ProductGroup = 00C302E01ABCB9EE00DB3ED1 /* Products */; + ProjectRef = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; + }, + { + ProductGroup = 139FDEE71B06529A00C62182 /* Products */; + ProjectRef = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; + }, + { + ProductGroup = 146834001AC3E56700842450 /* Products */; + ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* androidTest */, + 00E356ED1AD99517003FC87E /* androidTestTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTActionSheet.a; + remoteRef = 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTGeolocation.a; + remoteRef = 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTImage.a; + remoteRef = 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTNetwork.a; + remoteRef = 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTVibration.a; + remoteRef = 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTSettings.a; + remoteRef = 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTWebSocket.a; + remoteRef = 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 146834041AC3E56700842450 /* libReact.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libReact.a; + remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTLinking.a; + remoteRef = 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 832341B51AAA6A8300B99B32 /* libRCTText.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTText.a; + remoteRef = 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 00E356EC1AD99517003FC87E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "../node_modules/react-native/packager/react-native-xcode.sh"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 00E356EA1AD99517003FC87E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 00E356F31AD99517003FC87E /* androidTestTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, + 13B07FC11A68108700A75B9A /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* androidTest */; + targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { + isa = PBXVariantGroup; + children = ( + 13B07FB21A68108700A75B9A /* Base */, + ); + name = LaunchScreen.xib; + path = androidTest; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 00E356F61AD99517003FC87E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = androidTestTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/androidTest.app/androidTest"; + }; + name = Debug; + }; + 00E356F71AD99517003FC87E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + COPY_PHASE_STRIP = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + INFOPLIST_FILE = androidTestTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/androidTest.app/androidTest"; + }; + name = Release; + }; + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEAD_CODE_STRIPPING = NO; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + "$(SRCROOT)", + "$(SRCROOT)/../node_modules/react-native-code-push", + "$(SRCROOT)/../node_modules/react-native-linear-gradient/**", + "$(SRCROOT)/../node_modules/react-native-video", + ); + INFOPLIST_FILE = androidTest/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = androidTest; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + "$(SRCROOT)", + "$(SRCROOT)/../node_modules/react-native-code-push", + "$(SRCROOT)/../node_modules/react-native-linear-gradient/**", + "$(SRCROOT)/../node_modules/react-native-video", + ); + INFOPLIST_FILE = androidTest/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = androidTest; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + "$(SRCROOT)", + "$(SRCROOT)/../node_modules/react-native-code-push", + "$(SRCROOT)/../node_modules/react-native-linear-gradient/**", + "$(SRCROOT)/../node_modules/react-native-video", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + "$(SRCROOT)", + "$(SRCROOT)/../node_modules/react-native-code-push", + "$(SRCROOT)/../node_modules/react-native-linear-gradient/**", + "$(SRCROOT)/../node_modules/react-native-video", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "androidTestTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00E356F61AD99517003FC87E /* Debug */, + 00E356F71AD99517003FC87E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "androidTest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "androidTest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/local-cli/rnpm/core/test/fixtures/ios.js b/local-cli/rnpm/core/test/fixtures/ios.js new file mode 100644 index 00000000000000..50943559889555 --- /dev/null +++ b/local-cli/rnpm/core/test/fixtures/ios.js @@ -0,0 +1,14 @@ +const fs = require('fs'); +const path = require('path'); + +exports.valid = { + 'demoProject.xcodeproj': { + 'project.pbxproj': fs.readFileSync(path.join(__dirname, './files/project.pbxproj')), + }, +}; + +exports.validTestName = { + 'MyTestProject.xcodeproj': { + 'project.pbxproj': fs.readFileSync(path.join(__dirname, './files/project.pbxproj')), + }, +}; diff --git a/local-cli/rnpm/core/test/fixtures/projects.js b/local-cli/rnpm/core/test/fixtures/projects.js new file mode 100644 index 00000000000000..673aec6483f575 --- /dev/null +++ b/local-cli/rnpm/core/test/fixtures/projects.js @@ -0,0 +1,24 @@ +const fs = require('fs'); +const path = require('path'); +const android = require('./android'); +const ios = require('./ios'); + +const flat = { + android: android.valid, + ios: ios.valid, +}; + +const nested = { + android: { + app: android.valid, + }, + ios: ios.valid, +}; + +const withExamples = { + Examples: flat, + ios: ios.valid, + android: android.valid, +}; + +module.exports = { flat, nested, withExamples }; diff --git a/local-cli/rnpm/core/test/getCommands.spec.js b/local-cli/rnpm/core/test/getCommands.spec.js new file mode 100644 index 00000000000000..58c25446fb9200 --- /dev/null +++ b/local-cli/rnpm/core/test/getCommands.spec.js @@ -0,0 +1,171 @@ +jest.autoMockOff(); + +const path = require('path'); +const mock = require('mock-require'); +const rewire = require('rewire'); + +const commands = require('./fixtures/commands'); +const isArray = (arg) => + Object.prototype.toString.call(arg) === '[object Array]'; + +/** + * Paths to two possible `node_modules` locations `rnpm` can be installed + */ +const LOCAL_NODE_MODULES = path.join(process.cwd(), 'node_modules'); +const GLOBAL_NODE_MODULES = '/usr/local/lib/node_modules'; + +/** + * Paths to `package.json` of project, and rnpm - in two installation locations + */ +const APP_JSON = path.join(process.cwd(), 'package.json'); +const GLOBAL_RNPM_PJSON = path.join(GLOBAL_NODE_MODULES, '/rnpm/package.json'); +const LOCAL_RNPM_PJSON = path.join(LOCAL_NODE_MODULES, 'rnpm/package.json'); + +/** + * Sample `rnpm` plugin used in test cases + */ +const SAMPLE_RNPM_PLUGIN = 'rnpm-plugin-test'; + +/** + * Sample `package.json` of RNPM that will be used in test cases + */ +const SAMPLE_RNPM_JSON = { + dependencies: { + [SAMPLE_RNPM_PLUGIN]: '*', + }, +}; + +/** + * Project without `rnpm` plugins defined + */ +const NO_PLUGINS_JSON = { + dependencies: {}, +}; + +const getCommands = rewire('../src/getCommands'); +var revert; + +describe('getCommands', () => { + + afterEach(mock.stopAll); + + describe('in all installations', () => { + + beforeEach(() => { + revert = getCommands.__set__({ + __dirname: path.join(LOCAL_NODE_MODULES, 'rnpm/src'), + }); + mock(APP_JSON, NO_PLUGINS_JSON); + }); + + afterEach(() => revert()); + + it('list of the commands should be a non-empty array', () => { + mock(APP_JSON, NO_PLUGINS_JSON); + mock(LOCAL_RNPM_PJSON, SAMPLE_RNPM_JSON); + mock(SAMPLE_RNPM_PLUGIN, commands.single); + + expect(getCommands().length).not.toBe(0); + expect(isArray(getCommands())).toBeTruthy(); + }); + + it('should export one command', () => { + mock(LOCAL_RNPM_PJSON, SAMPLE_RNPM_JSON); + mock(SAMPLE_RNPM_PLUGIN, commands.single); + + expect(getCommands().length).toEqual(1); + }); + + it('should export multiple commands', () => { + mock(LOCAL_RNPM_PJSON, SAMPLE_RNPM_JSON); + mock(SAMPLE_RNPM_PLUGIN, commands.multiple); + + expect(getCommands().length).toEqual(2); + }); + + it('should export unique list of commands by name', () => { + mock(LOCAL_RNPM_PJSON, { + dependencies: { + [SAMPLE_RNPM_PLUGIN]: '*', + [`${SAMPLE_RNPM_PLUGIN}-2`]: '*', + }, + }); + + mock(SAMPLE_RNPM_PLUGIN, commands.single); + mock(`${SAMPLE_RNPM_PLUGIN}-2`, commands.single); + + expect(getCommands().length).toEqual(1); + }); + + }); + + describe('project plugins', () => { + /** + * In this test suite we only test project plugins thus we make sure + * `rnpm` package.json is properly mocked + */ + beforeEach(() => { + mock(LOCAL_RNPM_PJSON, NO_PLUGINS_JSON); + mock(GLOBAL_RNPM_PJSON, NO_PLUGINS_JSON); + }); + + afterEach(() => revert()); + + it('shoud load when installed locally', () => { + revert = getCommands.__set__({ + __dirname: path.join(LOCAL_NODE_MODULES, 'rnpm/src'), + }); + + mock(APP_JSON, SAMPLE_RNPM_JSON); + mock( + path.join(LOCAL_NODE_MODULES, SAMPLE_RNPM_PLUGIN), + commands.single + ); + + expect(getCommands()[0]).toEqual(commands.single); + }); + + it('should load when installed globally', () => { + revert = getCommands.__set__({ + __dirname: path.join(GLOBAL_NODE_MODULES, 'rnpm/src'), + }); + + mock(APP_JSON, SAMPLE_RNPM_JSON); + mock( + path.join(LOCAL_NODE_MODULES, SAMPLE_RNPM_PLUGIN), + commands.single + ); + + expect(getCommands()[0]).toEqual(commands.single); + }); + + }); + + describe('rnpm and project plugins', () => { + + beforeEach(() => { + revert = getCommands.__set__({ + __dirname: path.join(LOCAL_NODE_MODULES, 'rnpm/src'), + }); + }); + + afterEach(() => revert()); + + it('should load concatenated list of plugins', () => { + mock(APP_JSON, SAMPLE_RNPM_JSON); + mock(LOCAL_RNPM_PJSON, { + dependencies: { + [`${SAMPLE_RNPM_PLUGIN}-2`]: '*', + }, + }); + + mock( + path.join(LOCAL_NODE_MODULES, SAMPLE_RNPM_PLUGIN), + commands.multiple + ); + mock(`${SAMPLE_RNPM_PLUGIN}-2`, commands.single); + + expect(getCommands().length).toEqual(3); + }); + }); +}); diff --git a/local-cli/rnpm/core/test/ios/findProject.spec.js b/local-cli/rnpm/core/test/ios/findProject.spec.js new file mode 100644 index 00000000000000..6626921ba14731 --- /dev/null +++ b/local-cli/rnpm/core/test/ios/findProject.spec.js @@ -0,0 +1,84 @@ +jest.autoMockOff(); + +const findProject = require('../../src/config/ios/findProject'); +const mockFs = require('mock-fs'); +const projects = require('../fixtures/projects'); +const ios = require('../fixtures/ios'); +const userConfig = {}; + +describe('ios::findProject', () => { + it('should return path to xcodeproj if found', () => { + mockFs(projects.flat); + expect(findProject('')).not.toBe(null); + }); + + it('should return null if there\'re no projects', () => { + mockFs({ testDir: projects }); + expect(findProject('')).toBe(null); + }); + + it('should return ios project regardless of its name', () => { + mockFs({ ios: ios.validTestName }); + expect(findProject('')).not.toBe(null); + }); + + it('should ignore node_modules', () => { + mockFs({ node_modules: projects.flat }); + expect(findProject('')).toBe(null); + }); + + it('should ignore Pods', () => { + mockFs({ Pods: projects.flat }); + expect(findProject('')).toBe(null); + }); + + it('should ignore Pods inside `ios` folder', () => { + mockFs({ + ios: { + Pods: projects.flat, + DemoApp: projects.flat.ios, + }, + }); + expect(findProject('')).toBe('ios/DemoApp/demoProject.xcodeproj'); + }); + + it('should ignore xcodeproj from example folders', () => { + mockFs({ + examples: projects.flat, + Examples: projects.flat, + example: projects.flat, + KeychainExample: projects.flat, + Zpp: projects.flat, + }); + + expect(findProject('').toLowerCase()).not.toContain('example'); + }); + + it('should ignore xcodeproj from sample folders', () => { + mockFs({ + samples: projects.flat, + Samples: projects.flat, + sample: projects.flat, + KeychainSample: projects.flat, + Zpp: projects.flat, + }); + + expect(findProject('').toLowerCase()).not.toContain('sample'); + }); + + it('should ignore xcodeproj from test folders at any level', () => { + mockFs({ + test: projects.flat, + IntegrationTests: projects.flat, + tests: projects.flat, + Zpp: { + tests: projects.flat, + src: projects.flat, + }, + }); + + expect(findProject('').toLowerCase()).not.toContain('test'); + }); + + afterEach(mockFs.restore); +}); diff --git a/local-cli/rnpm/core/test/ios/getProjectConfig.spec.js b/local-cli/rnpm/core/test/ios/getProjectConfig.spec.js new file mode 100644 index 00000000000000..bba3bedb13071b --- /dev/null +++ b/local-cli/rnpm/core/test/ios/getProjectConfig.spec.js @@ -0,0 +1,26 @@ +jest.autoMockOff(); + +const getProjectConfig = require('../../src/config/ios').projectConfig; +const mockFs = require('mock-fs'); +const projects = require('../fixtures/projects'); + +describe('ios::getProjectConfig', () => { + const userConfig = {}; + + beforeEach(() => mockFs({ testDir: projects })); + + it('should return an object with ios project configuration', () => { + const folder = 'testDir/nested'; + + expect(getProjectConfig(folder, userConfig)).not.toBe(null); + expect(typeof getProjectConfig(folder, userConfig)).toBe('object'); + }); + + it('should return `null` if ios project was not found', () => { + const folder = 'testDir/empty'; + + expect(getProjectConfig(folder, userConfig)).toBe(null); + }); + + afterEach(mockFs.restore); +}); diff --git a/local-cli/rnpm/core/test/makeCommand.spec.js b/local-cli/rnpm/core/test/makeCommand.spec.js new file mode 100644 index 00000000000000..1fb50dfe6629a0 --- /dev/null +++ b/local-cli/rnpm/core/test/makeCommand.spec.js @@ -0,0 +1,35 @@ +var spawnError = false; + +jest.setMock('child_process', { + spawn: () => ({ + on: (ev, cb) => cb(spawnError), + }), +}); +jest.dontMock('../src/makeCommand'); + +const makeCommand = require('../src/makeCommand'); + +describe('makeCommand', () => { + const command = makeCommand('echo'); + + it('should generate a function around shell command', () => { + expect(typeof command).toBe('function'); + }); + + it('should throw an error if there\'s no callback provided', () => { + expect(command).toThrow(); + }); + + it('should invoke a callback after command execution', () => { + const spy = jest.genMockFunction(); + command(spy); + + expect(spy.mock.calls.length).toBe(1); + }); + + it('should throw an error if spawn ended up with error', () => { + spawnError = true; + const cb = jest.genMockFunction(); + expect(() => command(cb)).toThrow(); + }); +}); diff --git a/local-cli/rnpm/install/index.js b/local-cli/rnpm/install/index.js new file mode 100644 index 00000000000000..66891919c75adc --- /dev/null +++ b/local-cli/rnpm/install/index.js @@ -0,0 +1,11 @@ +module.exports = [ + { + func: require('./src/install'), + description: 'Install and link native dependencies', + name: 'install [packageName]', + }, { + func: require('./src/uninstall'), + description: 'Uninstall and unlink native dependencies', + name: 'uninstall [packageName]', + }, +]; diff --git a/local-cli/rnpm/install/package.json b/local-cli/rnpm/install/package.json new file mode 100644 index 00000000000000..e0f4987356749b --- /dev/null +++ b/local-cli/rnpm/install/package.json @@ -0,0 +1,26 @@ +{ + "name": "rnpm-plugin-install", + "version": "1.1.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rnpm/rnpm-plugin-install.git" + }, + "author": "Alexey Kureev (https://github.com/Kureev)", + "contributors": [ + "Alexey Kureev (https://github.com/Kureev)", + "Mike Grabowski (https://github.com/grabbou)" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/rnpm/rnpm-plugin-install/issues" + }, + "homepage": "https://github.com/rnpm/rnpm-plugin-install#readme", + "dependencies": { + "npmlog": "^2.0.2" + } +} diff --git a/local-cli/rnpm/install/src/install.js b/local-cli/rnpm/install/src/install.js new file mode 100644 index 00000000000000..d9eed2db433ac1 --- /dev/null +++ b/local-cli/rnpm/install/src/install.js @@ -0,0 +1,26 @@ +const spawnSync = require('child_process').spawnSync; +const log = require('npmlog'); +const spawnOpts = { + stdio: 'inherit', + stdin: 'inherit', +}; + +log.heading = 'rnpm-install'; + +module.exports = function install(config, args, callback) { + const name = args[0]; + + var res = spawnSync('npm', ['install', name, '--save'], spawnOpts); + + if (res.status) { + process.exit(res.status); + } + + res = spawnSync('rnpm', ['link', name], spawnOpts); + + if (res.status) { + process.exit(res.status); + } + + log.info(`Module ${name} has been successfully installed & linked`); +}; diff --git a/local-cli/rnpm/install/src/uninstall.js b/local-cli/rnpm/install/src/uninstall.js new file mode 100644 index 00000000000000..a1d7d9fc2d6b49 --- /dev/null +++ b/local-cli/rnpm/install/src/uninstall.js @@ -0,0 +1,26 @@ +const spawnSync = require('child_process').spawnSync; +const log = require('npmlog'); +const spawnOpts = { + stdio: 'inherit', + stdin: 'inherit', +}; + +log.heading = 'rnpm-install'; + +module.exports = function install(config, args, callback) { + const name = args[0]; + + var res = spawnSync('rnpm', ['unlink', name], spawnOpts); + + if (res.status) { + process.exit(res.status); + } + + res = spawnSync('npm', ['uninstall', name], spawnOpts); + + if (res.status) { + process.exit(res.status); + } + + log.info(`Module ${name} has been successfully uninstalled & unlinked`); +}; diff --git a/local-cli/rnpm/link/index.js b/local-cli/rnpm/link/index.js new file mode 100644 index 00000000000000..13b39b59956315 --- /dev/null +++ b/local-cli/rnpm/link/index.js @@ -0,0 +1,9 @@ +module.exports = [{ + func: require('./src/link'), + description: 'Links all native dependencies', + name: 'link [packageName]', +}, { + func: require('./src/unlink'), + description: 'Unlink native dependency', + name: 'unlink ', +}]; diff --git a/local-cli/rnpm/link/package.json b/local-cli/rnpm/link/package.json new file mode 100644 index 00000000000000..74f49e1a3aac4e --- /dev/null +++ b/local-cli/rnpm/link/package.json @@ -0,0 +1,52 @@ +{ + "name": "rnpm-plugin-link", + "version": "1.7.4", + "description": "rnpm plugin that links native dependencies to your project", + "main": "index.js", + "scripts": { + "test": "eslint ./ && mocha ./test --recursive" + }, + "keywords": [ + "rnpm", + "react-native", + "react-native link" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/rnpm/rnpm.git" + }, + "engines": { + "node": ">= 4.0.0" + }, + "author": "Amazing React Native Community (https://github.com/facebook/react-native)", + "contributors": [ + "Alexey Kureev (https://github.com/Kureev)", + "Mike Grabowski (https://github.com/grabbou)" + ], + "bugs": { + "url": "https://github.com/rnpm/rnpm/issues" + }, + "homepage": "https://github.com/rnpm/rnpm#readme", + "license": "MIT", + "dependencies": { + "fs-extra": "^0.26.2", + "glob": "^7.0.0", + "inquirer": "^0.12.0", + "lodash": "^3.10.1", + "mime": "^1.3.4", + "npmlog": "^2.0.0", + "plist": "^1.2.0", + "semver": "^5.1.0", + "to-camel-case": "^1.0.0", + "xcode": "^0.8.2" + }, + "devDependencies": { + "babel-eslint": "^4.1.5", + "chai": "^3.4.1", + "eslint": "^1.9.0", + "mocha": "^2.3.4", + "mock-fs": "^3.5.0", + "mock-require": "^1.2.1", + "sinon": "^1.17.2" + } +} diff --git a/local-cli/rnpm/link/src/android/copyAssets.js b/local-cli/rnpm/link/src/android/copyAssets.js new file mode 100644 index 00000000000000..709bf0b95c770b --- /dev/null +++ b/local-cli/rnpm/link/src/android/copyAssets.js @@ -0,0 +1,17 @@ +const fs = require('fs-extra'); +const path = require('path'); +const groupFilesByType = require('../groupFilesByType'); + +/** + * Copies each file from an array of assets provided to targetPath directory + * + * For now, the only types of files that are handled are: + * - Fonts (otf, ttf) - copied to targetPath/fonts under original name + */ +module.exports = function copyAssetsAndroid(files, targetPath) { + const assets = groupFilesByType(files); + + (assets.font || []).forEach(asset => + fs.copySync(asset, path.join(targetPath, 'fonts', path.basename(asset))) + ); +}; diff --git a/local-cli/rnpm/link/src/android/fs.js b/local-cli/rnpm/link/src/android/fs.js new file mode 100644 index 00000000000000..faa31d671ddb12 --- /dev/null +++ b/local-cli/rnpm/link/src/android/fs.js @@ -0,0 +1,8 @@ +const fs = require('fs-extra'); + +exports.readFile = (file) => + () => fs.readFileSync(file, 'utf8'); + +exports.writeFile = (file, content) => content ? + fs.writeFileSync(file, content, 'utf8') : + (c) => fs.writeFileSync(file, c, 'utf8'); diff --git a/local-cli/rnpm/link/src/android/getPrefix.js b/local-cli/rnpm/link/src/android/getPrefix.js new file mode 100644 index 00000000000000..1c40029e9e0c3f --- /dev/null +++ b/local-cli/rnpm/link/src/android/getPrefix.js @@ -0,0 +1,16 @@ +const semver = require('semver'); +const versions = ['0.20', '0.18', '0.17']; + +module.exports = function getPrefix(rnVersion) { + const version = rnVersion.replace(/-.*/, ''); + var prefix = 'patches/0.20'; + + versions.forEach((item, i) => { + const nextVersion = versions[i + 1]; + if (semver.lt(version, item + '.0') && nextVersion) { + prefix = `patches/${nextVersion}`; + } + }); + + return prefix; +}; diff --git a/local-cli/rnpm/link/src/android/isInstalled.js b/local-cli/rnpm/link/src/android/isInstalled.js new file mode 100644 index 00000000000000..3389a383fbbd7c --- /dev/null +++ b/local-cli/rnpm/link/src/android/isInstalled.js @@ -0,0 +1,8 @@ +const fs = require('fs'); +const makeBuildPatch = require(`./patches/makeBuildPatch`); + +module.exports = function isInstalled(config, name) { + return fs + .readFileSync(config.buildGradlePath) + .indexOf(makeBuildPatch(name).patch) > -1; +}; diff --git a/local-cli/rnpm/link/src/android/patches/0.17/makeImportPatch.js b/local-cli/rnpm/link/src/android/patches/0.17/makeImportPatch.js new file mode 100644 index 00000000000000..c7bcf3d794e8b1 --- /dev/null +++ b/local-cli/rnpm/link/src/android/patches/0.17/makeImportPatch.js @@ -0,0 +1,6 @@ +module.exports = function makeImportPatch(packageImportPath) { + return { + pattern: 'import android.app.Activity;', + patch: '\n' + packageImportPath, + }; +}; diff --git a/local-cli/rnpm/link/src/android/patches/0.17/makePackagePatch.js b/local-cli/rnpm/link/src/android/patches/0.17/makePackagePatch.js new file mode 100644 index 00000000000000..ea4552c198bbe7 --- /dev/null +++ b/local-cli/rnpm/link/src/android/patches/0.17/makePackagePatch.js @@ -0,0 +1,10 @@ +const applyParams = require('../applyParams'); + +module.exports = function makePackagePatch(packageInstance, params, prefix) { + const processedInstance = applyParams(packageInstance, params, prefix); + + return { + pattern: '.addPackage(new MainReactPackage())', + patch: `\n .addPackage(${processedInstance})`, + }; +}; diff --git a/local-cli/rnpm/link/src/android/patches/0.18/makeImportPatch.js b/local-cli/rnpm/link/src/android/patches/0.18/makeImportPatch.js new file mode 100644 index 00000000000000..687d61bd4fd6c4 --- /dev/null +++ b/local-cli/rnpm/link/src/android/patches/0.18/makeImportPatch.js @@ -0,0 +1,6 @@ +module.exports = function makeImportPatch(packageImportPath) { + return { + pattern: 'import com.facebook.react.ReactActivity;', + patch: '\n' + packageImportPath, + }; +}; diff --git a/local-cli/rnpm/link/src/android/patches/0.18/makePackagePatch.js b/local-cli/rnpm/link/src/android/patches/0.18/makePackagePatch.js new file mode 100644 index 00000000000000..6609303902de3e --- /dev/null +++ b/local-cli/rnpm/link/src/android/patches/0.18/makePackagePatch.js @@ -0,0 +1,10 @@ +const applyParams = require('../applyParams'); + +module.exports = function makePackagePatch(packageInstance, params, prefix) { + const processedInstance = applyParams(packageInstance, params, prefix); + + return { + pattern: 'new MainReactPackage()', + patch: ',\n ' + processedInstance, + }; +}; diff --git a/local-cli/rnpm/link/src/android/patches/0.20/makeImportPatch.js b/local-cli/rnpm/link/src/android/patches/0.20/makeImportPatch.js new file mode 100644 index 00000000000000..687d61bd4fd6c4 --- /dev/null +++ b/local-cli/rnpm/link/src/android/patches/0.20/makeImportPatch.js @@ -0,0 +1,6 @@ +module.exports = function makeImportPatch(packageImportPath) { + return { + pattern: 'import com.facebook.react.ReactActivity;', + patch: '\n' + packageImportPath, + }; +}; diff --git a/local-cli/rnpm/link/src/android/patches/0.20/makePackagePatch.js b/local-cli/rnpm/link/src/android/patches/0.20/makePackagePatch.js new file mode 100644 index 00000000000000..17120b14272211 --- /dev/null +++ b/local-cli/rnpm/link/src/android/patches/0.20/makePackagePatch.js @@ -0,0 +1,10 @@ +const applyParams = require('../applyParams'); + +module.exports = function makePackagePatch(packageInstance, params, prefix) { + const processedInstance = applyParams(packageInstance, params, prefix); + + return { + pattern: 'new MainReactPackage()', + patch: ',\n ' + processedInstance, + }; +}; diff --git a/local-cli/rnpm/link/src/android/patches/applyParams.js b/local-cli/rnpm/link/src/android/patches/applyParams.js new file mode 100644 index 00000000000000..90973c2f589a91 --- /dev/null +++ b/local-cli/rnpm/link/src/android/patches/applyParams.js @@ -0,0 +1,14 @@ +const toCamelCase = require('to-camel-case'); + +module.exports = function applyParams(str, params, prefix) { + return str.replace( + /\$\{(\w+)\}/g, + (pattern, param) => { + const name = toCamelCase(prefix) + '_' + param; + + return params[param] + ? `this.getResources().getString(R.strings.${name})` + : null; + } + ); +}; diff --git a/local-cli/rnpm/link/src/android/patches/applyPatch.js b/local-cli/rnpm/link/src/android/patches/applyPatch.js new file mode 100644 index 00000000000000..67337d41ac243f --- /dev/null +++ b/local-cli/rnpm/link/src/android/patches/applyPatch.js @@ -0,0 +1,8 @@ +const fs = require('fs'); + +module.exports = function applyPatch(file, patch) { + fs.writeFileSync(file, fs + .readFileSync(file, 'utf8') + .replace(patch.pattern, match => `${match}${patch.patch}`) + ); +}; diff --git a/local-cli/rnpm/link/src/android/patches/makeBuildPatch.js b/local-cli/rnpm/link/src/android/patches/makeBuildPatch.js new file mode 100644 index 00000000000000..879acbd059b2ef --- /dev/null +++ b/local-cli/rnpm/link/src/android/patches/makeBuildPatch.js @@ -0,0 +1,6 @@ +module.exports = function makeBuildPatch(name) { + return { + pattern: /[^ \t]dependencies {\n/, + patch: ` compile project(':${name}')\n`, + }; +}; diff --git a/local-cli/rnpm/link/src/android/patches/makeSettingsPatch.js b/local-cli/rnpm/link/src/android/patches/makeSettingsPatch.js new file mode 100644 index 00000000000000..af93d058703f34 --- /dev/null +++ b/local-cli/rnpm/link/src/android/patches/makeSettingsPatch.js @@ -0,0 +1,26 @@ +const path = require('path'); +const isWin = process.platform === 'win32'; + +module.exports = function makeSettingsPatch(name, androidConfig, projectConfig) { + var projectDir = path.relative( + path.dirname(projectConfig.settingsGradlePath), + androidConfig.sourceDir + ); + + /* + * Fix for Windows + * Backslashes is the escape character and will result in + * an invalid path in settings.gradle + * https://github.com/rnpm/rnpm/issues/113 + */ + if (isWin) { + projectDir = projectDir.replace(/\\/g, '/'); + } + + return { + pattern: 'include \':app\'\n', + patch: `include ':${name}'\n` + + `project(':${name}').projectDir = ` + + `new File(rootProject.projectDir, '${projectDir}')\n`, + }; +}; diff --git a/local-cli/rnpm/link/src/android/patches/makeStringsPatch.js b/local-cli/rnpm/link/src/android/patches/makeStringsPatch.js new file mode 100644 index 00000000000000..06300085dad429 --- /dev/null +++ b/local-cli/rnpm/link/src/android/patches/makeStringsPatch.js @@ -0,0 +1,14 @@ +const toCamelCase = require('to-camel-case'); + +module.exports = function makeStringsPatch(params, prefix) { + const patch = Object.keys(params).map(param => { + const name = toCamelCase(prefix) + '_' + param; + return ' ' + + `${params[param]}`; + }).join('\n') + '\n'; + + return { + pattern: '\n', + patch: patch, + }; +}; diff --git a/local-cli/rnpm/link/src/android/patches/revokePatch.js b/local-cli/rnpm/link/src/android/patches/revokePatch.js new file mode 100644 index 00000000000000..8905923b1b3413 --- /dev/null +++ b/local-cli/rnpm/link/src/android/patches/revokePatch.js @@ -0,0 +1,8 @@ +const fs = require('fs'); + +module.exports = function revokePatch(file, patch) { + fs.writeFileSync(file, fs + .readFileSync(file, 'utf8') + .replace(patch.patch, '') + ); +}; diff --git a/local-cli/rnpm/link/src/android/registerNativeModule.js b/local-cli/rnpm/link/src/android/registerNativeModule.js new file mode 100644 index 00000000000000..fa01c2081bd222 --- /dev/null +++ b/local-cli/rnpm/link/src/android/registerNativeModule.js @@ -0,0 +1,38 @@ +const fs = require('fs'); +const getReactVersion = require('../getReactNativeVersion'); +const getPrefix = require('./getPrefix'); + +const applyPatch = require('./patches/applyPatch'); +const makeStringsPatch = require('./patches/makeStringsPatch'); +const makeSettingsPatch = require(`./patches/makeSettingsPatch`); +const makeBuildPatch = require(`./patches/makeBuildPatch`); + +module.exports = function registerNativeAndroidModule( + name, + androidConfig, + params, + projectConfig +) { + const buildPatch = makeBuildPatch(name); + const prefix = getPrefix(getReactVersion(projectConfig.folder)); + const makeImportPatch = require(`./${prefix}/makeImportPatch`); + const makePackagePatch = require(`./${prefix}/makePackagePatch`); + + applyPatch( + projectConfig.settingsGradlePath, + makeSettingsPatch(name, androidConfig, projectConfig) + ); + + applyPatch(projectConfig.buildGradlePath, buildPatch); + applyPatch(projectConfig.stringsPath, makeStringsPatch(params, name)); + + applyPatch( + projectConfig.mainActivityPath, + makePackagePatch(androidConfig.packageInstance, params, name) + ); + + applyPatch( + projectConfig.mainActivityPath, + makeImportPatch(androidConfig.packageImportPath) + ); +}; diff --git a/local-cli/rnpm/link/src/android/unlinkAssets.js b/local-cli/rnpm/link/src/android/unlinkAssets.js new file mode 100644 index 00000000000000..79c51a1864515e --- /dev/null +++ b/local-cli/rnpm/link/src/android/unlinkAssets.js @@ -0,0 +1,20 @@ +const fs = require('fs-extra'); +const path = require('path'); +const groupFilesByType = require('../groupFilesByType'); + +/** + * Copies each file from an array of assets provided to targetPath directory + * + * For now, the only types of files that are handled are: + * - Fonts (otf, ttf) - copied to targetPath/fonts under original name + */ +module.exports = function unlinkAssetsAndroid(files, targetPath) { + const grouped = groupFilesByType(files); + + grouped.font.forEach((file) => { + const filename = path.basename(file); + if (fs.existsSync(filename)) { + fs.unlinkSync(path.join(targetPath, 'fonts', filename)); + } + }); +}; diff --git a/local-cli/rnpm/link/src/android/unregisterNativeModule.js b/local-cli/rnpm/link/src/android/unregisterNativeModule.js new file mode 100644 index 00000000000000..64db4a0fb6ce36 --- /dev/null +++ b/local-cli/rnpm/link/src/android/unregisterNativeModule.js @@ -0,0 +1,47 @@ +const fs = require('fs'); +const getReactVersion = require('../getReactNativeVersion'); +const getPrefix = require('./getPrefix'); +const toCamelCase = require('to-camel-case'); + +const revokePatch = require('./patches/revokePatch'); +const makeSettingsPatch = require('./patches/makeSettingsPatch'); +const makeBuildPatch = require('./patches/makeBuildPatch'); +const makeStringsPatch = require('./patches/makeStringsPatch'); + +module.exports = function unregisterNativeAndroidModule( + name, + androidConfig, + projectConfig +) { + const buildPatch = makeBuildPatch(name); + const prefix = getPrefix(getReactVersion(projectConfig.folder)); + const makeImportPatch = require(`./${prefix}/makeImportPatch`); + const makePackagePatch = require(`./${prefix}/makePackagePatch`); + const strings = fs.readFileSync(projectConfig.stringsPath, 'utf8'); + var params = {}; + + strings.replace( + /moduleConfig="true" name="(\w+)">(.*) { + params[param.slice(toCamelCase(name).length + 1)] = value; + } + ); + + revokePatch( + projectConfig.settingsGradlePath, + makeSettingsPatch(name, androidConfig, projectConfig) + ); + + revokePatch(projectConfig.buildGradlePath, buildPatch); + revokePatch(projectConfig.stringsPath, makeStringsPatch(params, name)); + + revokePatch( + projectConfig.mainActivityPath, + makePackagePatch(androidConfig.packageInstance, params, name) + ); + + revokePatch( + projectConfig.mainActivityPath, + makeImportPatch(androidConfig.packageImportPath) + ); +}; diff --git a/local-cli/rnpm/link/src/getDependencyConfig.js b/local-cli/rnpm/link/src/getDependencyConfig.js new file mode 100644 index 00000000000000..597be6aab5cb30 --- /dev/null +++ b/local-cli/rnpm/link/src/getDependencyConfig.js @@ -0,0 +1,17 @@ +/** + * Given an array of dependencies - it returns their RNPM config + * if they were valid. + */ +module.exports = function getDependencyConfig(config, deps) { + return deps.reduce((acc, name) => { + try { + return acc.concat({ + config: config.getDependencyConfig(name), + name, + }); + } catch (err) { + console.log(err); + return acc; + } + }, []); +}; diff --git a/local-cli/rnpm/link/src/getProjectDependencies.js b/local-cli/rnpm/link/src/getProjectDependencies.js new file mode 100644 index 00000000000000..1897395371f9fe --- /dev/null +++ b/local-cli/rnpm/link/src/getProjectDependencies.js @@ -0,0 +1,9 @@ +const path = require('path'); + +/** + * Returns an array of dependencies that should be linked/checked. + */ +module.exports = function getProjectDependencies() { + const pjson = require(path.join(process.cwd(), './package.json')); + return Object.keys(pjson.dependencies || {}).filter(name => name !== 'react-native'); +}; diff --git a/local-cli/rnpm/link/src/getReactNativeVersion.js b/local-cli/rnpm/link/src/getReactNativeVersion.js new file mode 100644 index 00000000000000..a1756e9e0bb3dc --- /dev/null +++ b/local-cli/rnpm/link/src/getReactNativeVersion.js @@ -0,0 +1,5 @@ +const path = require('path'); + +module.exports = (folder) => require( + path.join(folder, 'node_modules', 'react-native', 'package.json') +).version; diff --git a/local-cli/rnpm/link/src/groupFilesByType.js b/local-cli/rnpm/link/src/groupFilesByType.js new file mode 100644 index 00000000000000..ddf0df6dbe00fa --- /dev/null +++ b/local-cli/rnpm/link/src/groupFilesByType.js @@ -0,0 +1,27 @@ +const groupBy = require('lodash').groupBy; +const mime = require('mime'); + +/** + * Since there are no officialy registered MIME types + * for ttf/otf yet http://www.iana.org/assignments/media-types/media-types.xhtml, + * we define two non-standard ones for the sake of parsing + */ +mime.define({ + 'font/opentype': ['otf'], + 'font/truetype': ['ttf'], +}); + +/** + * Given an array of files, it groups it by it's type. + * Type of the file is inferred from it's mimetype based on the extension + * file ends up with. The returned value is an object with properties that + * correspond to the first part of the mimetype, e.g. images will be grouped + * under `image` key since the mimetype for them is `image/jpg` etc. + * + * Example: + * Given an array ['fonts/a.ttf', 'images/b.jpg'], + * the returned object will be: {font: ['fonts/a.ttf'], image: ['images/b.jpg']} + */ +module.exports = function groupFilesByType(assets) { + return groupBy(assets, type => mime.lookup(type).split('/')[0]); +}; diff --git a/local-cli/rnpm/link/src/ios/addFileToProject.js b/local-cli/rnpm/link/src/ios/addFileToProject.js new file mode 100644 index 00000000000000..eab1d12535ef75 --- /dev/null +++ b/local-cli/rnpm/link/src/ios/addFileToProject.js @@ -0,0 +1,14 @@ +const PbxFile = require('xcode/lib/pbxFile'); + +/** + * Given xcodeproj and filePath, it creates new file + * from path provided, adds it to the project + * and returns newly created instance of a file + */ +module.exports = function addFileToProject(project, filePath) { + const file = new PbxFile(filePath); + file.uuid = project.generateUuid(); + file.fileRef = project.generateUuid(); + project.addToPbxFileReferenceSection(file); + return file; +}; diff --git a/local-cli/rnpm/link/src/ios/addProjectToLibraries.js b/local-cli/rnpm/link/src/ios/addProjectToLibraries.js new file mode 100644 index 00000000000000..4e65ef30f35241 --- /dev/null +++ b/local-cli/rnpm/link/src/ios/addProjectToLibraries.js @@ -0,0 +1,13 @@ +/** + * Given an array of xcodeproj libraries and pbxFile, + * it appends it to that group + * + * Important: That function mutates `libraries` and it's not pure. + * It's mainly due to limitations of `xcode` library. + */ +module.exports = function addProjectToLibraries(libraries, file) { + return libraries.children.push({ + value: file.fileRef, + comment: file.basename, + }); +}; diff --git a/local-cli/rnpm/link/src/ios/addSharedLibraries.js b/local-cli/rnpm/link/src/ios/addSharedLibraries.js new file mode 100644 index 00000000000000..f84603abb30eeb --- /dev/null +++ b/local-cli/rnpm/link/src/ios/addSharedLibraries.js @@ -0,0 +1,3 @@ +module.exports = function addSharedLibraries(project, libraries) { + +}; diff --git a/local-cli/rnpm/link/src/ios/addToHeaderSearchPaths.js b/local-cli/rnpm/link/src/ios/addToHeaderSearchPaths.js new file mode 100644 index 00000000000000..1eff6055e4b6a7 --- /dev/null +++ b/local-cli/rnpm/link/src/ios/addToHeaderSearchPaths.js @@ -0,0 +1,5 @@ +const mapHeaderSearchPaths = require('./mapHeaderSearchPaths'); + +module.exports = function addToHeaderSearchPaths(project, path) { + mapHeaderSearchPaths(project, searchPaths => searchPaths.concat(path)); +}; diff --git a/local-cli/rnpm/link/src/ios/copyAssets.js b/local-cli/rnpm/link/src/ios/copyAssets.js new file mode 100644 index 00000000000000..538381a035f8ca --- /dev/null +++ b/local-cli/rnpm/link/src/ios/copyAssets.js @@ -0,0 +1,57 @@ +const fs = require('fs-extra'); +const path = require('path'); +const xcode = require('xcode'); +const log = require('npmlog'); +const plistParser = require('plist'); +const groupFilesByType = require('../groupFilesByType'); +const createGroup = require('./createGroup'); +const getPlist = require('./getPlist'); +const getPlistPath = require('./getPlistPath'); + +/** + * This function works in a similar manner to its Android version, + * except it does not copy fonts but creates XCode Group references + */ +module.exports = function linkAssetsIOS(files, projectConfig) { + const project = xcode.project(projectConfig.pbxprojPath).parseSync(); + const assets = groupFilesByType(files); + const plist = getPlist(project, projectConfig.sourceDir); + + if (!plist) { + return log.error( + 'ERRPLIST', + `Could not locate Info.plist. Check if your project has 'INFOPLIST_FILE' set properly` + ); + } + + if (!project.pbxGroupByName('Resources')) { + createGroup(project, 'Resources'); + + log.warn( + 'ERRGROUP', + `Group 'Resources' does not exist in your XCode project. We have created it automatically for you.` + ); + } + + const fonts = (assets.font || []) + .map(asset => + project.addResourceFile( + path.relative(projectConfig.sourceDir, asset), + { target: project.getFirstTarget().uuid } + ) + ) + .filter(file => file) // xcode returns false if file is already there + .map(file => file.basename); + + plist.UIAppFonts = (plist.UIAppFonts || []).concat(fonts); + + fs.writeFileSync( + projectConfig.pbxprojPath, + project.writeSync() + ); + + fs.writeFileSync( + getPlistPath(project, projectConfig.sourceDir), + plistParser.build(plist) + ); +}; diff --git a/local-cli/rnpm/link/src/ios/createGroup.js b/local-cli/rnpm/link/src/ios/createGroup.js new file mode 100644 index 00000000000000..dac9ec66c5df40 --- /dev/null +++ b/local-cli/rnpm/link/src/ios/createGroup.js @@ -0,0 +1,27 @@ +const getGroup = require('./getGroup'); + +const hasGroup = (pbxGroup, name) => pbxGroup.children.find(group => group.comment === name); + +/** + * Given project and path of the group, it deeply creates a given group + * making all outer groups if neccessary + * + * Returns newly created group + */ +module.exports = function createGroup(project, path) { + return path.split('/').reduce( + (group, name) => { + if (!hasGroup(group, name)) { + const uuid = project.pbxCreateGroup(name, '""'); + + group.children.push({ + value: uuid, + comment: name, + }); + } + + return project.pbxGroupByName(name); + }, + getGroup(project) + ); +}; diff --git a/local-cli/rnpm/link/src/ios/getBuildProperty.js b/local-cli/rnpm/link/src/ios/getBuildProperty.js new file mode 100644 index 00000000000000..181a74ec9302c3 --- /dev/null +++ b/local-cli/rnpm/link/src/ios/getBuildProperty.js @@ -0,0 +1,18 @@ +/** + * Gets build property from the main target build section + * + * It differs from the project.getBuildProperty exposed by xcode in the way that: + * - it only checks for build property in the main target `Debug` section + * - `xcode` library iterates over all build sections and because it misses + * an early return when property is found, it will return undefined/wrong value + * when there's another build section typically after the one you want to access + * without the property defined (e.g. CocoaPods sections appended to project + * miss INFOPLIST_FILE), see: https://github.com/alunny/node-xcode/blob/master/lib/pbxProject.js#L1765 + */ +module.exports = function getBuildProperty(project, prop) { + const target = project.getFirstTarget().firstTarget; + const config = project.pbxXCConfigurationList()[target.buildConfigurationList]; + const buildSection = project.pbxXCBuildConfigurationSection()[config.buildConfigurations[0].value]; + + return buildSection.buildSettings[prop]; +}; diff --git a/local-cli/rnpm/link/src/ios/getGroup.js b/local-cli/rnpm/link/src/ios/getGroup.js new file mode 100644 index 00000000000000..ab24df8d3d511d --- /dev/null +++ b/local-cli/rnpm/link/src/ios/getGroup.js @@ -0,0 +1,34 @@ +const getFirstProject = (project) => project.getFirstProject().firstProject; + +const findGroup = (group, name) => group.children.find(group => group.comment === name); + +/** + * Returns group from .xcodeproj if one exists, null otherwise + * + * Unlike node-xcode `pbxGroupByName` - it does not return `first-matching` + * group if multiple groups with the same name exist + * + * If path is not provided, it returns top-level group + */ +module.exports = function getGroup(project, path) { + const firstProject = getFirstProject(project); + + var group = project.getPBXGroupByKey(firstProject.mainGroup); + + if (!path) { + return group; + } + + for (var name of path.split('/')) { + var foundGroup = findGroup(group, name); + + if (foundGroup) { + group = project.getPBXGroupByKey(foundGroup.value); + } else { + group = null; + break; + } + } + + return group; +}; diff --git a/local-cli/rnpm/link/src/ios/getHeaderSearchPath.js b/local-cli/rnpm/link/src/ios/getHeaderSearchPath.js new file mode 100644 index 00000000000000..750c060914acb9 --- /dev/null +++ b/local-cli/rnpm/link/src/ios/getHeaderSearchPath.js @@ -0,0 +1,52 @@ +const path = require('path'); +const union = require('lodash').union; +const last = require('lodash').last; + +/** + * Given an array of directories, it returns the one that contains + * all the other directories in a given array inside it. + * + * Example: + * Given an array of directories: ['/Users/Kureev/a', '/Users/Kureev/b'] + * the returned folder is `/Users/Kureev` + * + * Check `getHeaderSearchPath.spec.js` for more use-cases. + */ +const getOuterDirectory = (directories) => + directories.reduce((topDir, currentDir) => { + const currentFolders = currentDir.split(path.sep); + const topMostFolders = topDir.split(path.sep); + + if (currentFolders.length === topMostFolders.length + && last(currentFolders) !== last(topMostFolders)) { + return currentFolders.slice(0, -1).join(path.sep); + } + + return currentFolders.length < topMostFolders.length + ? currentDir + : topDir; + }); + +/** + * Given an array of headers it returns search path so Xcode can resolve + * headers when referenced like below: + * ``` + * #import "CodePush.h" + * ``` + * If all files are located in one directory (directories.length === 1), + * we simply return a relative path to that location. + * + * Otherwise, we loop through them all to find the outer one that contains + * all the headers inside. That location is then returned with /** appended at + * the end so Xcode marks that location as `recursive` and will look inside + * every folder of it to locate correct headers. + */ +module.exports = function getHeaderSearchPath(sourceDir, headers) { + const directories = union( + headers.map(path.dirname) + ); + + return directories.length === 1 + ? `"$(SRCROOT)${path.sep}${path.relative(sourceDir, directories[0])}"` + : `"$(SRCROOT)${path.sep}${path.relative(sourceDir, getOuterDirectory(directories))}/**"`; +}; diff --git a/local-cli/rnpm/link/src/ios/getHeadersInFolder.js b/local-cli/rnpm/link/src/ios/getHeadersInFolder.js new file mode 100644 index 00000000000000..7dc954db59a6c6 --- /dev/null +++ b/local-cli/rnpm/link/src/ios/getHeadersInFolder.js @@ -0,0 +1,18 @@ +const glob = require('glob'); +const path = require('path'); + +const GLOB_EXCLUDE_PATTERN = ['node_modules/**', 'Pods/**', 'Examples/**', 'examples/**']; + +/** + * Given folder, it returns an array of all header files + * inside it, ignoring node_modules and examples + */ +module.exports = function getHeadersInFolder(folder) { + return glob + .sync('**/*.h', { + cwd: folder, + nodir: true, + ignore: GLOB_EXCLUDE_PATTERN, + }) + .map(file => path.join(folder, file)); +}; diff --git a/local-cli/rnpm/link/src/ios/getPlist.js b/local-cli/rnpm/link/src/ios/getPlist.js new file mode 100644 index 00000000000000..9190aa3459d86e --- /dev/null +++ b/local-cli/rnpm/link/src/ios/getPlist.js @@ -0,0 +1,20 @@ +const plistParser = require('plist'); +const getPlistPath = require('./getPlistPath'); +const fs = require('fs'); + +/** + * Returns Info.plist located in the iOS project + * + * Returns `null` if INFOPLIST_FILE is not specified. + */ +module.exports = function getPlist(project, sourceDir) { + const plistPath = getPlistPath(project, sourceDir); + + if (!plistPath || !fs.existsSync(plistPath)) { + return null; + } + + return plistParser.parse( + fs.readFileSync(plistPath, 'utf-8') + ); +}; diff --git a/local-cli/rnpm/link/src/ios/getPlistPath.js b/local-cli/rnpm/link/src/ios/getPlistPath.js new file mode 100644 index 00000000000000..44b810ab1e298b --- /dev/null +++ b/local-cli/rnpm/link/src/ios/getPlistPath.js @@ -0,0 +1,15 @@ +const path = require('path'); +const getBuildProperty = require('./getBuildProperty'); + +module.exports = function getPlistPath(project, sourceDir) { + const plistFile = getBuildProperty(project, 'INFOPLIST_FILE'); + + if (!plistFile) { + return null; + } + + return path.join( + sourceDir, + plistFile.replace(/"/g, '').replace('$(SRCROOT)', '') + ); +}; diff --git a/local-cli/rnpm/link/src/ios/getProducts.js b/local-cli/rnpm/link/src/ios/getProducts.js new file mode 100644 index 00000000000000..b51492734c35b2 --- /dev/null +++ b/local-cli/rnpm/link/src/ios/getProducts.js @@ -0,0 +1,12 @@ +/** + * Given xcodeproj it returns list of products ending with + * .a extension, so that we know what elements add to target + * project static library + */ +module.exports = function getProducts(project) { + return project + .pbxGroupByName('Products') + .children + .map(c => c.comment) + .filter(c => c.indexOf('.a') > -1); +}; diff --git a/local-cli/rnpm/link/src/ios/hasLibraryImported.js b/local-cli/rnpm/link/src/ios/hasLibraryImported.js new file mode 100644 index 00000000000000..b380309344e41e --- /dev/null +++ b/local-cli/rnpm/link/src/ios/hasLibraryImported.js @@ -0,0 +1,10 @@ +/** + * Given an array of libraries already imported and packageName that will be + * added, returns true or false depending on whether the library is already linked + * or not + */ +module.exports = function hasLibraryImported(libraries, packageName) { + return libraries.children + .filter(library => library.comment === packageName) + .length > 0; +}; diff --git a/local-cli/rnpm/link/src/ios/isInstalled.js b/local-cli/rnpm/link/src/ios/isInstalled.js new file mode 100644 index 00000000000000..81b1fff01fb9ff --- /dev/null +++ b/local-cli/rnpm/link/src/ios/isInstalled.js @@ -0,0 +1,18 @@ +const xcode = require('xcode'); +const getGroup = require('./getGroup'); +const hasLibraryImported = require('./hasLibraryImported'); + +/** + * Returns true if `xcodeproj` specified by dependencyConfig is present + * in a top level `libraryFolder` + */ +module.exports = function isInstalled(projectConfig, dependencyConfig) { + const project = xcode.project(projectConfig.pbxprojPath).parseSync(); + const libraries = getGroup(project, projectConfig.libraryFolder); + + if (!libraries) { + return false; + } + + return hasLibraryImported(libraries, dependencyConfig.projectName); +}; diff --git a/local-cli/rnpm/link/src/ios/mapHeaderSearchPaths.js b/local-cli/rnpm/link/src/ios/mapHeaderSearchPaths.js new file mode 100644 index 00000000000000..ea76aac832d3bc --- /dev/null +++ b/local-cli/rnpm/link/src/ios/mapHeaderSearchPaths.js @@ -0,0 +1,36 @@ +/** + * Given Xcode project and path, iterate over all build configurations + * and execute func with HEADER_SEARCH_PATHS from current section + * + * We cannot use builtin addToHeaderSearchPaths method since react-native init does not + * use $(TARGET_NAME) for PRODUCT_NAME, but sets it manually so that method will skip + * that target. + * + * To workaround that issue and make it more bullet-proof for different names, + * we iterate over all configurations and look if React is already there. If it is, + * we assume we want to modify that section either + * + * Important: That function mutates `buildSettings` and it's not pure thus you should + * not rely on its return value + */ +module.exports = function headerSearchPathIter(project, func) { + const config = project.pbxXCBuildConfigurationSection(); + + Object + .keys(config) + .filter(ref => ref.indexOf('_comment') === -1) + .forEach(ref => { + const buildSettings = config[ref].buildSettings; + const shouldVisitBuildSettings = ( + Array.isArray(buildSettings.HEADER_SEARCH_PATHS) ? + buildSettings.HEADER_SEARCH_PATHS : + [] + ) + .filter(path => path.indexOf('react-native/React/**')) + .length > 0; + + if (shouldVisitBuildSettings) { + buildSettings.HEADER_SEARCH_PATHS = func(buildSettings.HEADER_SEARCH_PATHS); + } + }); +}; diff --git a/local-cli/rnpm/link/src/ios/registerNativeModule.js b/local-cli/rnpm/link/src/ios/registerNativeModule.js new file mode 100644 index 00000000000000..6d8bce262e761e --- /dev/null +++ b/local-cli/rnpm/link/src/ios/registerNativeModule.js @@ -0,0 +1,67 @@ +const xcode = require('xcode'); +const fs = require('fs'); +const path = require('path'); +const log = require('npmlog'); + +const addToHeaderSearchPaths = require('./addToHeaderSearchPaths'); +const getHeadersInFolder = require('./getHeadersInFolder'); +const getHeaderSearchPath = require('./getHeaderSearchPath'); +const getProducts = require('./getProducts'); +const createGroup = require('./createGroup'); +const hasLibraryImported = require('./hasLibraryImported'); +const addFileToProject = require('./addFileToProject'); +const addProjectToLibraries = require('./addProjectToLibraries'); +const addSharedLibraries = require('./addSharedLibraries'); +const isEmpty = require('lodash').isEmpty; +const getGroup = require('./getGroup'); + +/** + * Register native module IOS adds given dependency to project by adding + * its xcodeproj to project libraries as well as attaching static library + * to the first target (the main one) + * + * If library is already linked, this action is a no-op. + */ +module.exports = function registerNativeModuleIOS(dependencyConfig, projectConfig) { + const project = xcode.project(projectConfig.pbxprojPath).parseSync(); + const dependencyProject = xcode.project(dependencyConfig.pbxprojPath).parseSync(); + + var libraries = getGroup(project, projectConfig.libraryFolder); + + if (!libraries) { + libraries = createGroup(project, projectConfig.libraryFolder); + + log.warn( + 'ERRGROUP', + `Group ${projectConfig.libraryFolder} does not exist in your XCode project. We have created it automatically for you.` + ); + } + + const file = addFileToProject( + project, + path.relative(projectConfig.sourceDir, dependencyConfig.projectPath) + ); + + addProjectToLibraries(libraries, file); + + getProducts(dependencyProject).forEach(product => { + project.addStaticLibrary(product, { + target: project.getFirstTarget().uuid, + }); + }); + + addSharedLibraries(project, dependencyConfig.sharedLibraries); + + const headers = getHeadersInFolder(dependencyConfig.folder); + if (!isEmpty(headers)) { + addToHeaderSearchPaths( + project, + getHeaderSearchPath(projectConfig.sourceDir, headers) + ); + } + + fs.writeFileSync( + projectConfig.pbxprojPath, + project.writeSync() + ); +}; diff --git a/local-cli/rnpm/link/src/ios/removeFromHeaderSearchPaths.js b/local-cli/rnpm/link/src/ios/removeFromHeaderSearchPaths.js new file mode 100644 index 00000000000000..07259f1d223e0b --- /dev/null +++ b/local-cli/rnpm/link/src/ios/removeFromHeaderSearchPaths.js @@ -0,0 +1,10 @@ +const mapHeaderSearchPaths = require('./mapHeaderSearchPaths'); + +/** + * Given Xcode project and absolute path, it makes sure there are no headers referring to it + */ +module.exports = function addToHeaderSearchPaths(project, path) { + mapHeaderSearchPaths(project, + searchPaths => searchPaths.filter(searchPath => searchPath !== path) + ); +}; diff --git a/local-cli/rnpm/link/src/ios/removeFromPbxItemContainerProxySection.js b/local-cli/rnpm/link/src/ios/removeFromPbxItemContainerProxySection.js new file mode 100644 index 00000000000000..e0a6a84a7a65dc --- /dev/null +++ b/local-cli/rnpm/link/src/ios/removeFromPbxItemContainerProxySection.js @@ -0,0 +1,16 @@ +/** + * For all files that are created and referenced from another `.xcodeproj` - + * a new PBXItemContainerProxy is created that contains `containerPortal` value + * which equals to xcodeproj file.uuid from PBXFileReference section. + */ +module.exports = function removeFromPbxItemContainerProxySection(project, file) { + const section = project.hash.project.objects.PBXContainerItemProxy; + + for (var key of Object.keys(section)) { + if (section[key].containerPortal === file.uuid) { + delete section[key]; + } + } + + return; +}; diff --git a/local-cli/rnpm/link/src/ios/removeFromPbxReferenceProxySection.js b/local-cli/rnpm/link/src/ios/removeFromPbxReferenceProxySection.js new file mode 100644 index 00000000000000..b867964ff6ef0a --- /dev/null +++ b/local-cli/rnpm/link/src/ios/removeFromPbxReferenceProxySection.js @@ -0,0 +1,15 @@ +/** + * Every file added to the project from another project is attached to + * `PBXItemContainerProxy` through `PBXReferenceProxy`. + */ +module.exports = function removeFromPbxReferenceProxySection(project, file) { + const section = project.hash.project.objects.PBXReferenceProxy; + + for (var key of Object.keys(section)) { + if (section[key].path === file.basename) { + delete section[key]; + } + } + + return; +}; diff --git a/local-cli/rnpm/link/src/ios/removeFromProjectReferences.js b/local-cli/rnpm/link/src/ios/removeFromProjectReferences.js new file mode 100644 index 00000000000000..652b189295c297 --- /dev/null +++ b/local-cli/rnpm/link/src/ios/removeFromProjectReferences.js @@ -0,0 +1,26 @@ +/** + * For each file (.xcodeproj), there's an entry in `projectReferences` created + * that has two entries - `ProjectRef` - reference to a file.uuid and + * `ProductGroup` - uuid of a Products group. + * + * When projectReference is found - it's deleted and the removed value is returned + * so that ProductGroup in PBXGroup section can be removed as well. + * + * Otherwise returns null + */ +module.exports = function removeFromProjectReferences(project, file) { + const firstProject = project.getFirstProject().firstProject; + + const projectRef = firstProject.projectReferences.find(item => item.ProjectRef === file.uuid); + + if (!projectRef) { + return null; + } + + firstProject.projectReferences.splice( + firstProject.projectReferences.indexOf(projectRef), + 1 + ); + + return projectRef; +}; diff --git a/local-cli/rnpm/link/src/ios/removeFromStaticLibraries.js b/local-cli/rnpm/link/src/ios/removeFromStaticLibraries.js new file mode 100644 index 00000000000000..2a66fa44aac879 --- /dev/null +++ b/local-cli/rnpm/link/src/ios/removeFromStaticLibraries.js @@ -0,0 +1,21 @@ +const PbxFile = require('xcode/lib/pbxFile'); +const removeFromPbxReferenceProxySection = require('./removeFromPbxReferenceProxySection'); + +/** + * Removes file from static libraries + * + * Similar to `node-xcode` addStaticLibrary + */ +module.exports = function removeFromStaticLibraries(project, path, opts) { + const file = new PbxFile(path); + + file.target = opts ? opts.target : undefined; + + project.removeFromPbxFileReferenceSection(file); + project.removeFromPbxBuildFileSection(file); + project.removeFromPbxFrameworksBuildPhase(file); + project.removeFromLibrarySearchPaths(file); + removeFromPbxReferenceProxySection(project, file); + + return file; +}; diff --git a/local-cli/rnpm/link/src/ios/removeProductGroup.js b/local-cli/rnpm/link/src/ios/removeProductGroup.js new file mode 100644 index 00000000000000..186aed7f7b17f8 --- /dev/null +++ b/local-cli/rnpm/link/src/ios/removeProductGroup.js @@ -0,0 +1,11 @@ +module.exports = function removeProductGroup(project, productGroupId) { + const section = project.hash.project.objects.PBXGroup; + + for (var key of Object.keys(section)) { + if (key === productGroupId) { + delete section[key]; + } + } + + return; +}; diff --git a/local-cli/rnpm/link/src/ios/removeProjectFromLibraries.js b/local-cli/rnpm/link/src/ios/removeProjectFromLibraries.js new file mode 100644 index 00000000000000..20965a67e09292 --- /dev/null +++ b/local-cli/rnpm/link/src/ios/removeProjectFromLibraries.js @@ -0,0 +1,12 @@ +/** + * Given an array of xcodeproj libraries and pbxFile, + * it removes it from that group by comparing basenames + * + * Important: That function mutates `libraries` and it's not pure. + * It's mainly due to limitations of `xcode` library. + */ +module.exports = function removeProjectFromLibraries(libraries, file) { + libraries.children = libraries.children.filter(library => + library.comment !== file.basename + ); +}; diff --git a/local-cli/rnpm/link/src/ios/removeProjectFromProject.js b/local-cli/rnpm/link/src/ios/removeProjectFromProject.js new file mode 100644 index 00000000000000..3a3f173dbe682d --- /dev/null +++ b/local-cli/rnpm/link/src/ios/removeProjectFromProject.js @@ -0,0 +1,26 @@ +const PbxFile = require('xcode/lib/pbxFile'); +const removeFromPbxItemContainerProxySection = require('./removeFromPbxItemContainerProxySection'); +const removeFromProjectReferences = require('./removeFromProjectReferences'); +const removeProductGroup = require('./removeProductGroup'); + +/** + * Given xcodeproj and filePath, it creates new file + * from path provided and removes it. That operation is required since + * underlying method requires PbxFile instance to be passed (it does not + * have to have uuid or fileRef defined since it will do equality check + * by path) + * + * Returns removed file (that one will have UUID) + */ +module.exports = function removeProjectFromProject(project, filePath) { + const file = project.removeFromPbxFileReferenceSection(new PbxFile(filePath)); + const projectRef = removeFromProjectReferences(project, file); + + if (projectRef) { + removeProductGroup(project, projectRef.ProductGroup); + } + + removeFromPbxItemContainerProxySection(project, file); + + return file; +}; diff --git a/local-cli/rnpm/link/src/ios/removeSharedLibraries.js b/local-cli/rnpm/link/src/ios/removeSharedLibraries.js new file mode 100644 index 00000000000000..82e611f5fa63b0 --- /dev/null +++ b/local-cli/rnpm/link/src/ios/removeSharedLibraries.js @@ -0,0 +1,3 @@ +module.exports = function removeSharedLibraries(project, libraries) { + +}; diff --git a/local-cli/rnpm/link/src/ios/unlinkAssets.js b/local-cli/rnpm/link/src/ios/unlinkAssets.js new file mode 100644 index 00000000000000..44422592985cce --- /dev/null +++ b/local-cli/rnpm/link/src/ios/unlinkAssets.js @@ -0,0 +1,54 @@ +const fs = require('fs-extra'); +const path = require('path'); +const xcode = require('xcode'); +const log = require('npmlog'); +const plistParser = require('plist'); +const groupFilesByType = require('../groupFilesByType'); +const getPlist = require('./getPlist'); +const getPlistPath = require('./getPlistPath'); +const difference = require('lodash').difference; + +/** + * Unlinks assets from iOS project. Removes references for fonts from `Info.plist` + * fonts provided by application and from `Resources` group + */ +module.exports = function unlinkAssetsIOS(files, projectConfig) { + const project = xcode.project(projectConfig.pbxprojPath).parseSync(); + const assets = groupFilesByType(files); + const plist = getPlist(project, projectConfig.sourceDir); + + if (!plist) { + return log.error( + 'ERRPLIST', + `Could not locate Info.plist file. Check if your project has 'INFOPLIST_FILE' set properly` + ); + } + + if (!project.pbxGroupByName('Resources')) { + return log.error( + 'ERRGROUP', + `Group 'Resources' does not exist in your XCode project. There is nothing to unlink.` + ); + } + + const fonts = (assets.font || []) + .map(asset => + project.removeResourceFile( + path.relative(projectConfig.sourceDir, asset), + { target: project.getFirstTarget().uuid } + ) + ) + .map(file => file.basename); + + plist.UIAppFonts = difference(plist.UIAppFonts || [], fonts); + + fs.writeFileSync( + projectConfig.pbxprojPath, + project.writeSync() + ); + + fs.writeFileSync( + getPlistPath(project, projectConfig.sourceDir), + plistParser.build(plist) + ); +}; diff --git a/local-cli/rnpm/link/src/ios/unregisterNativeModule.js b/local-cli/rnpm/link/src/ios/unregisterNativeModule.js new file mode 100644 index 00000000000000..509848a5552897 --- /dev/null +++ b/local-cli/rnpm/link/src/ios/unregisterNativeModule.js @@ -0,0 +1,54 @@ +const xcode = require('xcode'); +const path = require('path'); +const fs = require('fs'); + +const getProducts = require('./getProducts'); +const getHeadersInFolder = require('./getHeadersInFolder'); +const isEmpty = require('lodash').isEmpty; +const getHeaderSearchPath = require('./getHeaderSearchPath'); +const removeProjectFromProject = require('./removeProjectFromProject'); +const removeProjectFromLibraries = require('./removeProjectFromLibraries'); +const removeFromStaticLibraries = require('./removeFromStaticLibraries'); +const removeFromHeaderSearchPaths = require('./removeFromHeaderSearchPaths'); +const removeSharedLibraries = require('./addSharedLibraries'); +const getGroup = require('./getGroup'); + +/** + * Unregister native module IOS + * + * If library is already unlinked, this action is a no-op. + */ +module.exports = function unregisterNativeModule(dependencyConfig, projectConfig) { + const project = xcode.project(projectConfig.pbxprojPath).parseSync(); + const dependencyProject = xcode.project(dependencyConfig.pbxprojPath).parseSync(); + + const libraries = getGroup(project, projectConfig.libraryFolder); + + const file = removeProjectFromProject( + project, + path.relative(projectConfig.sourceDir, dependencyConfig.projectPath) + ); + + removeProjectFromLibraries(libraries, file); + + getProducts(dependencyProject).forEach(product => { + removeFromStaticLibraries(project, product, { + target: project.getFirstTarget().uuid, + }); + }); + + removeSharedLibraries(project, dependencyConfig.sharedLibraries); + + const headers = getHeadersInFolder(dependencyConfig.folder); + if (!isEmpty(headers)) { + removeFromHeaderSearchPaths( + project, + getHeaderSearchPath(projectConfig.sourceDir, headers) + ); + } + + fs.writeFileSync( + projectConfig.pbxprojPath, + project.writeSync() + ); +}; diff --git a/local-cli/rnpm/link/src/link.js b/local-cli/rnpm/link/src/link.js new file mode 100644 index 00000000000000..51261aa30f741c --- /dev/null +++ b/local-cli/rnpm/link/src/link.js @@ -0,0 +1,136 @@ +const log = require('npmlog'); +const path = require('path'); +const uniq = require('lodash').uniq; +const flatten = require('lodash').flatten; +const pkg = require('../package.json'); + +const isEmpty = require('lodash').isEmpty; +const promiseWaterfall = require('./promiseWaterfall'); +const registerDependencyAndroid = require('./android/registerNativeModule'); +const registerDependencyIOS = require('./ios/registerNativeModule'); +const isInstalledAndroid = require('./android/isInstalled'); +const isInstalledIOS = require('./ios/isInstalled'); +const copyAssetsAndroid = require('./android/copyAssets'); +const copyAssetsIOS = require('./ios/copyAssets'); +const getProjectDependencies = require('./getProjectDependencies'); +const getDependencyConfig = require('./getDependencyConfig'); +const pollParams = require('./pollParams'); + +log.heading = 'rnpm-link'; + +const commandStub = (cb) => cb(); +const dedupeAssets = (assets) => uniq(assets, asset => path.basename(asset)); + +const promisify = (func) => new Promise((resolve, reject) => + func((err, res) => err ? reject(err) : resolve(res)) +); + +const linkDependencyAndroid = (androidProject, dependency) => { + if (!androidProject || !dependency.config.android) { + return null; + } + + const isInstalled = isInstalledAndroid(androidProject, dependency.name); + + if (isInstalled) { + log.info(`Android module ${dependency.name} is already linked`); + return null; + } + + return pollParams(dependency.config.params).then(params => { + log.info(`Linking ${dependency.name} android dependency`); + + registerDependencyAndroid( + dependency.name, + dependency.config.android, + params, + androidProject + ); + + log.info(`Android module ${dependency.name} has been successfully linked`); + }); +}; + +const linkDependencyIOS = (iOSProject, dependency) => { + if (!iOSProject || !dependency.config.ios) { + return; + } + + const isInstalled = isInstalledIOS(iOSProject, dependency.config.ios); + + if (isInstalled) { + log.info(`iOS module ${dependency.name} is already linked`); + return; + } + + log.info(`Linking ${dependency.name} ios dependency`); + + registerDependencyIOS(dependency.config.ios, iOSProject); + + log.info(`iOS module ${dependency.name} has been successfully linked`); +}; + +const linkAssets = (project, assets) => { + if (isEmpty(assets)) { + return; + } + + if (project.ios) { + log.info('Linking assets to ios project'); + copyAssetsIOS(assets, project.ios); + } + + if (project.android) { + log.info('Linking assets to android project'); + copyAssetsAndroid(assets, project.android.assetsPath); + } + + log.info(`Assets has been successfully linked to your project`); +}; + +/** + * Updates project and linkes all dependencies to it + * + * If optional argument [packageName] is provided, it's the only one that's checked + */ +module.exports = function link(config, args) { + var project; + try { + project = config.getProjectConfig(); + } catch (err) { + log.error( + 'ERRPACKAGEJSON', + 'No package found. Are you sure it\'s a React Native project?' + ); + return Promise.reject(err); + } + + const packageName = args[0]; + + const dependencies = getDependencyConfig( + config, + packageName ? [packageName] : getProjectDependencies() + ); + + const assets = dedupeAssets(dependencies.reduce( + (assets, dependency) => assets.concat(dependency.config.assets), + project.assets + )); + + const tasks = flatten(dependencies.map(dependency => [ + () => promisify(dependency.config.commands.prelink || commandStub), + () => linkDependencyAndroid(project.android, dependency), + () => linkDependencyIOS(project.ios, dependency), + () => promisify(dependency.config.commands.postlink || commandStub), + ])); + + tasks.push(() => linkAssets(project, assets)); + + return promiseWaterfall(tasks).catch(err => { + log.error( + `It seems something went wrong while linking. Error: ${err.message} \n` + + `Please file an issue here: ${pkg.bugs.url}` + ); + throw err; + }); +}; diff --git a/local-cli/rnpm/link/src/pollParams.js b/local-cli/rnpm/link/src/pollParams.js new file mode 100644 index 00000000000000..65540147951aac --- /dev/null +++ b/local-cli/rnpm/link/src/pollParams.js @@ -0,0 +1,9 @@ +var inquirer = require('inquirer'); + +module.exports = (questions) => new Promise((resolve, reject) => { + if (!questions) { + return resolve({}); + } + + inquirer.prompt(questions, resolve); +}); diff --git a/local-cli/rnpm/link/src/promiseWaterfall.js b/local-cli/rnpm/link/src/promiseWaterfall.js new file mode 100644 index 00000000000000..e6df880587403e --- /dev/null +++ b/local-cli/rnpm/link/src/promiseWaterfall.js @@ -0,0 +1,14 @@ +/** + * Given an array of promise creators, executes them in a sequence. + * + * If any of the promises in the chain fails, all subsequent promises + * will be skipped + * + * Returns the value last promise from a sequence resolved + */ +module.exports = function promiseWaterfall(tasks) { + return tasks.reduce( + (prevTaskPromise, task) => prevTaskPromise.then(task), + Promise.resolve() + ); +}; diff --git a/local-cli/rnpm/link/src/unlink.js b/local-cli/rnpm/link/src/unlink.js new file mode 100644 index 00000000000000..e40b091e9737bd --- /dev/null +++ b/local-cli/rnpm/link/src/unlink.js @@ -0,0 +1,117 @@ +const path = require('path'); +const log = require('npmlog'); + +const getProjectDependencies = require('./getProjectDependencies'); +const unregisterDependencyAndroid = require('./android/unregisterNativeModule'); +const unregisterDependencyIOS = require('./ios/unregisterNativeModule'); +const isInstalledAndroid = require('./android/isInstalled'); +const isInstalledIOS = require('./ios/isInstalled'); +const unlinkAssetsAndroid = require('./android/unlinkAssets'); +const unlinkAssetsIOS = require('./ios/unlinkAssets'); +const getDependencyConfig = require('./getDependencyConfig'); +const difference = require('lodash').difference; +const isEmpty = require('lodash').isEmpty; +const flatten = require('lodash').flatten; + +log.heading = 'rnpm-link'; + +const unlinkDependencyAndroid = (androidProject, dependency, packageName) => { + if (!androidProject || !dependency.android) { + return; + } + + const isInstalled = isInstalledAndroid(androidProject, packageName); + + if (!isInstalled) { + log.info(`Android module ${packageName} is not installed`); + return; + } + + log.info(`Unlinking ${packageName} android dependency`); + + unregisterDependencyAndroid(packageName, dependency.android, androidProject); + + log.info(`Android module ${packageName} has been successfully unlinked`); +}; + +const unlinkDependencyIOS = (iOSProject, dependency, packageName) => { + if (!iOSProject || !dependency.ios) { + return; + } + + const isInstalled = isInstalledIOS(iOSProject, dependency.ios); + + if (!isInstalled) { + log.info(`iOS module ${packageName} is not installed`); + return; + } + + log.info(`Unlinking ${packageName} ios dependency`); + + unregisterDependencyIOS(dependency.ios, iOSProject); + + log.info(`iOS module ${packageName} has been successfully unlinked`); +}; + +/** + * Updates project and unlink specific dependency + * + * If optional argument [packageName] is provided, it's the only one + * that's checked + */ +module.exports = function unlink(config, args) { + const packageName = args[0]; + + var project; + var dependency; + + try { + project = config.getProjectConfig(); + } catch (err) { + log.error( + 'ERRPACKAGEJSON', + 'No package found. Are you sure it\'s a React Native project?' + ); + return Promise.reject(err); + } + + try { + dependency = config.getDependencyConfig(packageName); + } catch (err) { + log.warn( + 'ERRINVALIDPROJ', + `Project ${packageName} is not a react-native library` + ); + return Promise.reject(err); + } + + unlinkDependencyAndroid(project.android, dependency, packageName); + unlinkDependencyIOS(project.ios, dependency, packageName); + + const allDependencies = getDependencyConfig(config, getProjectDependencies()); + + const assets = difference( + dependency.assets, + flatten(allDependencies, d => d.assets) + ); + + if (isEmpty(assets)) { + return Promise.resolve(); + } + + if (project.ios) { + log.info('Unlinking assets from ios project'); + unlinkAssetsIOS(assets, project.ios); + } + + if (project.android) { + log.info('Unlinking assets from android project'); + unlinkAssetsAndroid(assets, project.android.assetsPath); + } + + log.info( + `${packageName} assets has been successfully unlinked from your project` + ); + + return Promise.resolve(); +}; diff --git a/local-cli/rnpm/link/test/android/isInstalled.spec.js b/local-cli/rnpm/link/test/android/isInstalled.spec.js new file mode 100644 index 00000000000000..bde18c09e43dd4 --- /dev/null +++ b/local-cli/rnpm/link/test/android/isInstalled.spec.js @@ -0,0 +1,28 @@ +const chai = require('chai'); +const expect = chai.expect; +const mock = require('mock-fs'); +const fs = require('fs'); +const path = require('path'); +const isInstalled = require('../../src/android/isInstalled'); + +const projectConfig = { + buildGradlePath: 'build.gradle', +}; + +describe('android::isInstalled', () => { + before(() => mock({ + 'build.gradle': fs.readFileSync( + path.join(__dirname, '../fixtures/android/patchedBuild.gradle') + ), + })); + + it('should return true when project is already in build.gradle', () => + expect(isInstalled(projectConfig, 'test')).to.be.true + ); + + it('should return false when project is not in build.gradle', () => + expect(isInstalled(projectConfig, 'test2')).to.be.false + ); + + after(mock.restore); +}); diff --git a/local-cli/rnpm/link/test/android/patches/0.17/makeImportPatch.js b/local-cli/rnpm/link/test/android/patches/0.17/makeImportPatch.js new file mode 100644 index 00000000000000..d2a5f43c728ac6 --- /dev/null +++ b/local-cli/rnpm/link/test/android/patches/0.17/makeImportPatch.js @@ -0,0 +1,31 @@ +const chai = require('chai'); +const expect = chai.expect; +const mock = require('mock-fs'); +const fs = require('fs'); +const path = require('path'); +const makeImportPatch = require('../../../../src/android/patches/0.17/makeImportPatch'); +const applyPatch = require('../../../../src/android/patches/applyPatch'); + +const projectConfig = { + mainActivityPath: 'MainActivity.java', +}; + +const packageImportPath = 'import some.example.project'; + +describe('makeImportPatch@0.17', () => { + before(() => mock({ + 'MainActivity.java': fs.readFileSync( + path.join(__dirname, '../../../fixtures/android/0.17/MainActivity.java') + ), + })); + + it('MainActivity contains a correct 0.17 import patch', () => { + const importPatch = makeImportPatch(packageImportPath); + + applyPatch('MainActivity.java', importPatch); + expect(fs.readFileSync('MainActivity.java', 'utf8')) + .to.have.string(importPatch.patch); + }); + + after(mock.restore); +}); diff --git a/local-cli/rnpm/link/test/android/patches/0.17/makePackagePatch.js b/local-cli/rnpm/link/test/android/patches/0.17/makePackagePatch.js new file mode 100644 index 00000000000000..99857a5713df09 --- /dev/null +++ b/local-cli/rnpm/link/test/android/patches/0.17/makePackagePatch.js @@ -0,0 +1,36 @@ +const chai = require('chai'); +const expect = chai.expect; +const mock = require('mock-fs'); +const fs = require('fs'); +const path = require('path'); +const makePackagePatch = require('../../../../src/android/patches/0.17/makePackagePatch'); +const applyPatch = require('../../../../src/android/patches/applyPatch'); + +const projectConfig = { + mainActivityPath: 'MainActivity.java', +}; + +const packageInstance = 'new SomeLibrary(${foo}, ${bar}, \'something\')'; +const name = 'some-library'; +const params = { + foo: 'foo', + bar: 'bar', +}; + +describe('makePackagePatch@0.17', () => { + before(() => mock({ + 'MainActivity.java': fs.readFileSync( + path.join(__dirname, '../../../fixtures/android/0.17/MainActivity.java') + ), + })); + + it('MainActivity contains a correct 0.17 package patch', () => { + const packagePatch = makePackagePatch(packageInstance, params, name); + + applyPatch('MainActivity.java', packagePatch); + expect(fs.readFileSync('MainActivity.java', 'utf8')) + .to.have.string(packagePatch.patch); + }); + + after(mock.restore); +}); diff --git a/local-cli/rnpm/link/test/android/patches/0.18/makeImportPatch.js b/local-cli/rnpm/link/test/android/patches/0.18/makeImportPatch.js new file mode 100644 index 00000000000000..ffff9f045f60c3 --- /dev/null +++ b/local-cli/rnpm/link/test/android/patches/0.18/makeImportPatch.js @@ -0,0 +1,31 @@ +const chai = require('chai'); +const expect = chai.expect; +const mock = require('mock-fs'); +const fs = require('fs'); +const path = require('path'); +const makeImportPatch = require('../../../../src/android/patches/0.18/makeImportPatch'); +const applyPatch = require('../../../../src/android/patches/applyPatch'); + +const projectConfig = { + mainActivityPath: 'MainActivity.java', +}; + +const packageImportPath = 'import some.example.project'; + +describe('makeImportPatch@0.18', () => { + before(() => mock({ + 'MainActivity.java': fs.readFileSync( + path.join(__dirname, '../../../fixtures/android/0.18/MainActivity.java') + ), + })); + + it('MainActivity contains a correct 0.18 import patch', () => { + const importPatch = makeImportPatch(packageImportPath); + + applyPatch('MainActivity.java', importPatch); + expect(fs.readFileSync('MainActivity.java', 'utf8')) + .to.have.string(importPatch.patch); + }); + + after(mock.restore); +}); diff --git a/local-cli/rnpm/link/test/android/patches/0.18/makePackagePatch.js b/local-cli/rnpm/link/test/android/patches/0.18/makePackagePatch.js new file mode 100644 index 00000000000000..a2d4802f4e484b --- /dev/null +++ b/local-cli/rnpm/link/test/android/patches/0.18/makePackagePatch.js @@ -0,0 +1,36 @@ +const chai = require('chai'); +const expect = chai.expect; +const mock = require('mock-fs'); +const fs = require('fs'); +const path = require('path'); +const makePackagePatch = require('../../../../src/android/patches/0.18/makePackagePatch'); +const applyPatch = require('../../../../src/android/patches/applyPatch'); + +const projectConfig = { + mainActivityPath: 'MainActivity.java', +}; + +const packageInstance = 'new SomeLibrary(${foo}, ${bar}, \'something\')'; +const name = 'some-library'; +const params = { + foo: 'foo', + bar: 'bar', +}; + +describe('makePackagePatch@0.18', () => { + before(() => mock({ + 'MainActivity.java': fs.readFileSync( + path.join(__dirname, '../../../fixtures/android/0.18/MainActivity.java') + ), + })); + + it('MainActivity contains a correct 0.18 package patch', () => { + const packagePatch = makePackagePatch(packageInstance, params, name); + + applyPatch('MainActivity.java', packagePatch); + expect(fs.readFileSync('MainActivity.java', 'utf8')) + .to.have.string(packagePatch.patch); + }); + + after(mock.restore); +}); diff --git a/local-cli/rnpm/link/test/android/patches/0.20/makeImportPatch.js b/local-cli/rnpm/link/test/android/patches/0.20/makeImportPatch.js new file mode 100644 index 00000000000000..b80fc9a577d76e --- /dev/null +++ b/local-cli/rnpm/link/test/android/patches/0.20/makeImportPatch.js @@ -0,0 +1,31 @@ +const chai = require('chai'); +const expect = chai.expect; +const mock = require('mock-fs'); +const fs = require('fs'); +const path = require('path'); +const makeImportPatch = require('../../../../src/android/patches/0.20/makeImportPatch'); +const applyPatch = require('../../../../src/android/patches/applyPatch'); + +const projectConfig = { + mainActivityPath: 'MainActivity.java', +}; + +const packageImportPath = 'import some.example.project'; + +describe('makeImportPatch@0.20', () => { + before(() => mock({ + 'MainActivity.java': fs.readFileSync( + path.join(__dirname, '../../../fixtures/android/0.20/MainActivity.java') + ), + })); + + it('MainActivity contains a correct 0.20 import patch', () => { + const importPatch = makeImportPatch(packageImportPath); + + applyPatch('MainActivity.java', importPatch); + expect(fs.readFileSync('MainActivity.java', 'utf8')) + .to.have.string(importPatch.patch); + }); + + after(mock.restore); +}); diff --git a/local-cli/rnpm/link/test/android/patches/0.20/makePackagePatch.js b/local-cli/rnpm/link/test/android/patches/0.20/makePackagePatch.js new file mode 100644 index 00000000000000..87f807636e192b --- /dev/null +++ b/local-cli/rnpm/link/test/android/patches/0.20/makePackagePatch.js @@ -0,0 +1,36 @@ +const chai = require('chai'); +const expect = chai.expect; +const mock = require('mock-fs'); +const fs = require('fs'); +const path = require('path'); +const makePackagePatch = require('../../../../src/android/patches/0.20/makePackagePatch'); +const applyPatch = require('../../../../src/android/patches/applyPatch'); + +const projectConfig = { + mainActivityPath: 'MainActivity.java', +}; + +const packageInstance = 'new SomeLibrary(${foo}, ${bar}, \'something\')'; +const name = 'some-library'; +const params = { + foo: 'foo', + bar: 'bar', +}; + +describe('makePackagePatch@0.20', () => { + before(() => mock({ + 'MainActivity.java': fs.readFileSync( + path.join(__dirname, '../../../fixtures/android/0.20/MainActivity.java') + ), + })); + + it('MainActivity contains a correct 0.20 package patch', () => { + const packagePatch = makePackagePatch(packageInstance, params, name); + + applyPatch('MainActivity.java', packagePatch); + expect(fs.readFileSync('MainActivity.java', 'utf8')) + .to.have.string(packagePatch.patch); + }); + + after(mock.restore); +}); diff --git a/local-cli/rnpm/link/test/android/patches/applyPatch.js b/local-cli/rnpm/link/test/android/patches/applyPatch.js new file mode 100644 index 00000000000000..cc5ba34f1d7092 --- /dev/null +++ b/local-cli/rnpm/link/test/android/patches/applyPatch.js @@ -0,0 +1,17 @@ +const chai = require('chai'); +const expect = chai.expect; +const applyParams = require('../../../src/android/patches/applyParams'); + +describe('applyParams', () => { + it('apply params to the string', () => { + expect( + applyParams('${foo}', {foo: 'foo'}, 'react-native') + ).to.be.equal('this.getResources().getString(R.strings.reactNative_foo)'); + }); + + it('use null if no params provided', () => { + expect( + applyParams('${foo}', {}, 'react-native') + ).to.be.equal('null'); + }); +}); diff --git a/local-cli/rnpm/link/test/android/patches/makeBuildPatch.spec.js b/local-cli/rnpm/link/test/android/patches/makeBuildPatch.spec.js new file mode 100644 index 00000000000000..51136a72ebf241 --- /dev/null +++ b/local-cli/rnpm/link/test/android/patches/makeBuildPatch.spec.js @@ -0,0 +1,17 @@ +const chai = require('chai'); +const expect = chai.expect; +const makeBuildPatch = require('../../../src/android/patches/makeBuildPatch'); +const applyPatch = require('../../../src/android/patches/applyPatch'); + +const name = 'test'; + +describe('makeBuildPatch', () => { + it('should build a patch function', () => { + expect(makeBuildPatch(name)).to.be.an('object'); + }); + + it('should make a correct patch', () => { + expect(makeBuildPatch(name).patch) + .to.be.equal(` compile project(':${name}')\n`); + }); +}); diff --git a/local-cli/rnpm/link/test/android/patches/makeSettingsPatch.spec.js b/local-cli/rnpm/link/test/android/patches/makeSettingsPatch.spec.js new file mode 100644 index 00000000000000..2dfe97c6c49f66 --- /dev/null +++ b/local-cli/rnpm/link/test/android/patches/makeSettingsPatch.spec.js @@ -0,0 +1,36 @@ +const fs = require('fs'); +const path = require('path'); +const chai = require('chai'); +const expect = chai.expect; +const makeSettingsPatch = require('../../../src/android/patches/makeSettingsPatch'); + +const name = 'test'; +const projectConfig = { + sourceDir: '/home/project/android/app', + settingsGradlePath: '/home/project/android/settings.gradle', +}; +const dependencyConfig = { + sourceDir: `/home/project/node_modules/${name}/android`, +}; + +describe('makeSettingsPatch', () => { + it('should build a patch function', () => { + expect( + makeSettingsPatch(name, dependencyConfig, {}, projectConfig) + ).to.be.an('object'); + }); + + it('should make a correct patch', () => { + const projectDir = path.relative( + path.dirname(projectConfig.settingsGradlePath), + dependencyConfig.sourceDir + ); + + expect(makeSettingsPatch(name, dependencyConfig, projectConfig).patch) + .to.be.equal( + `include ':${name}'\n` + + `project(':${name}').projectDir = ` + + `new File(rootProject.projectDir, '${projectDir}')\n` + ); + }); +}); diff --git a/local-cli/rnpm/link/test/fixtures/android/0.17/MainActivity.java b/local-cli/rnpm/link/test/fixtures/android/0.17/MainActivity.java new file mode 100644 index 00000000000000..0a532a7eddfa85 --- /dev/null +++ b/local-cli/rnpm/link/test/fixtures/android/0.17/MainActivity.java @@ -0,0 +1,78 @@ +package com.basic; + +import android.app.Activity; +import android.os.Bundle; +import android.view.KeyEvent; + +import com.facebook.react.LifecycleState; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactRootView; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.shell.MainReactPackage; +import com.facebook.soloader.SoLoader; + +public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler { + + private ReactInstanceManager mReactInstanceManager; + private ReactRootView mReactRootView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mReactRootView = new ReactRootView(this); + + mReactInstanceManager = ReactInstanceManager.builder() + .setApplication(getApplication()) + .setBundleAssetName("index.android.bundle") + .setJSMainModuleName("index.android") + .addPackage(new MainReactPackage()) + .setUseDeveloperSupport(BuildConfig.DEBUG) + .setInitialLifecycleState(LifecycleState.RESUMED) + .build(); + + mReactRootView.startReactApplication(mReactInstanceManager, "Basic", null); + + setContentView(mReactRootView); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) { + mReactInstanceManager.showDevOptionsDialog(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + public void onBackPressed() { + if (mReactInstanceManager != null) { + mReactInstanceManager.onBackPressed(); + } else { + super.onBackPressed(); + } + } + + @Override + public void invokeDefaultOnBackPressed() { + super.onBackPressed(); + } + + @Override + protected void onPause() { + super.onPause(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onPause(); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onResume(this, this); + } + } +} diff --git a/local-cli/rnpm/link/test/fixtures/android/0.17/patchedMainActivity.java b/local-cli/rnpm/link/test/fixtures/android/0.17/patchedMainActivity.java new file mode 100644 index 00000000000000..edd841d76211c3 --- /dev/null +++ b/local-cli/rnpm/link/test/fixtures/android/0.17/patchedMainActivity.java @@ -0,0 +1,80 @@ +package com.basic; + +import android.app.Activity; +import com.oblador.vectoricons.VectorIconsPackage; +import android.os.Bundle; +import android.view.KeyEvent; + +import com.facebook.react.LifecycleState; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactRootView; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.shell.MainReactPackage; +import com.facebook.soloader.SoLoader; + +public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler { + + private ReactInstanceManager mReactInstanceManager; + private ReactRootView mReactRootView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mReactRootView = new ReactRootView(this); + + mReactInstanceManager = ReactInstanceManager.builder() + .setApplication(getApplication()) + .setBundleAssetName("index.android.bundle") + .setJSMainModuleName("index.android") + .addPackage(new MainReactPackage()) + .addPackage(new VectorIconsPackage()) + .setUseDeveloperSupport(BuildConfig.DEBUG) + .setInitialLifecycleState(LifecycleState.RESUMED) + .build(); + + mReactRootView.startReactApplication(mReactInstanceManager, "Basic", null); + + setContentView(mReactRootView); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) { + mReactInstanceManager.showDevOptionsDialog(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + public void onBackPressed() { + if (mReactInstanceManager != null) { + mReactInstanceManager.onBackPressed(); + } else { + super.onBackPressed(); + } + } + + @Override + public void invokeDefaultOnBackPressed() { + super.onBackPressed(); + } + + @Override + protected void onPause() { + super.onPause(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onPause(); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onResume(this, this); + } + } +} diff --git a/local-cli/rnpm/link/test/fixtures/android/0.18/MainActivity.java b/local-cli/rnpm/link/test/fixtures/android/0.18/MainActivity.java new file mode 100644 index 00000000000000..c2ccca8905f7b6 --- /dev/null +++ b/local-cli/rnpm/link/test/fixtures/android/0.18/MainActivity.java @@ -0,0 +1,39 @@ +package com.testrn; + +import com.facebook.react.ReactActivity; +import com.facebook.react.ReactPackage; +import com.facebook.react.shell.MainReactPackage; + +import java.util.Arrays; +import java.util.List; + +public class MainActivity extends ReactActivity { + + /** + * Returns the name of the main component registered from JavaScript. + * This is used to schedule rendering of the component. + */ + @Override + protected String getMainComponentName() { + return "TestRN"; + } + + /** + * Returns whether dev mode should be enabled. + * This enables e.g. the dev menu. + */ + @Override + protected boolean getUseDeveloperSupport() { + return BuildConfig.DEBUG; + } + + /** + * A list of packages used by the app. If the app uses additional views + * or modules besides the default ones, add more packages here. + */ + @Override + protected List getPackages() { + return Arrays.asList( + new MainReactPackage()); + } +} diff --git a/local-cli/rnpm/link/test/fixtures/android/0.18/patchedMainActivity.java b/local-cli/rnpm/link/test/fixtures/android/0.18/patchedMainActivity.java new file mode 100644 index 00000000000000..3c6331eaf9fb78 --- /dev/null +++ b/local-cli/rnpm/link/test/fixtures/android/0.18/patchedMainActivity.java @@ -0,0 +1,41 @@ +package com.testrn; + +import com.facebook.react.ReactActivity; +import com.oblador.vectoricons.VectorIconsPackage; +import com.facebook.react.ReactPackage; +import com.facebook.react.shell.MainReactPackage; + +import java.util.Arrays; +import java.util.List; + +public class MainActivity extends ReactActivity { + + /** + * Returns the name of the main component registered from JavaScript. + * This is used to schedule rendering of the component. + */ + @Override + protected String getMainComponentName() { + return "TestRN"; + } + + /** + * Returns whether dev mode should be enabled. + * This enables e.g. the dev menu. + */ + @Override + protected boolean getUseDeveloperSupport() { + return BuildConfig.DEBUG; + } + + /** + * A list of packages used by the app. If the app uses additional views + * or modules besides the default ones, add more packages here. + */ + @Override + protected List getPackages() { + return Arrays.asList( + new MainReactPackage(), + new VectorIconsPackage()); + } +} diff --git a/local-cli/rnpm/link/test/fixtures/android/0.20/MainActivity.java b/local-cli/rnpm/link/test/fixtures/android/0.20/MainActivity.java new file mode 100644 index 00000000000000..d9df1311598a37 --- /dev/null +++ b/local-cli/rnpm/link/test/fixtures/android/0.20/MainActivity.java @@ -0,0 +1,40 @@ +package com.myawesomeproject; + +import com.facebook.react.ReactActivity; +import com.facebook.react.ReactPackage; +import com.facebook.react.shell.MainReactPackage; + +import java.util.Arrays; +import java.util.List; + +public class MainActivity extends ReactActivity { + + /** + * Returns the name of the main component registered from JavaScript. + * This is used to schedule rendering of the component. + */ + @Override + protected String getMainComponentName() { + return "TestRN"; + } + + /** + * Returns whether dev mode should be enabled. + * This enables e.g. the dev menu. + */ + @Override + protected boolean getUseDeveloperSupport() { + return BuildConfig.DEBUG; + } + + /** + * A list of packages used by the app. If the app uses additional views + * or modules besides the default ones, add more packages here. + */ + @Override + protected List getPackages() { + return Arrays.asList( + new MainReactPackage() + ); + } +} diff --git a/local-cli/rnpm/link/test/fixtures/android/build.gradle b/local-cli/rnpm/link/test/fixtures/android/build.gradle new file mode 100644 index 00000000000000..07f260d6d48032 --- /dev/null +++ b/local-cli/rnpm/link/test/fixtures/android/build.gradle @@ -0,0 +1,5 @@ +dependencies { + compile fileTree(dir: "libs", include: ["*.jar"]) + compile "com.android.support:appcompat-v7:23.0.1" + compile "com.facebook.react:react-native:0.18.+" +} diff --git a/local-cli/rnpm/link/test/fixtures/android/patchedBuild.gradle b/local-cli/rnpm/link/test/fixtures/android/patchedBuild.gradle new file mode 100644 index 00000000000000..8c75995d38e543 --- /dev/null +++ b/local-cli/rnpm/link/test/fixtures/android/patchedBuild.gradle @@ -0,0 +1,6 @@ +dependencies { + compile project(':test') + compile fileTree(dir: "libs", include: ["*.jar"]) + compile "com.android.support:appcompat-v7:23.0.1" + compile "com.facebook.react:react-native:0.18.+" +} diff --git a/local-cli/rnpm/link/test/fixtures/android/patchedSettings.gradle b/local-cli/rnpm/link/test/fixtures/android/patchedSettings.gradle new file mode 100644 index 00000000000000..8854e3d106fae0 --- /dev/null +++ b/local-cli/rnpm/link/test/fixtures/android/patchedSettings.gradle @@ -0,0 +1,5 @@ +rootProject.name = 'TestRN' + +include ':app' +include ':test' +project(':test').projectDir = new File(rootProject.projectDir, '../node_modules/test/android') diff --git a/local-cli/rnpm/link/test/fixtures/android/settings.gradle b/local-cli/rnpm/link/test/fixtures/android/settings.gradle new file mode 100644 index 00000000000000..ded35f713e9413 --- /dev/null +++ b/local-cli/rnpm/link/test/fixtures/android/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'TestRN' + +include ':app' diff --git a/local-cli/rnpm/link/test/fixtures/linearGradient.pbxproj b/local-cli/rnpm/link/test/fixtures/linearGradient.pbxproj new file mode 100644 index 00000000000000..89f34dc5a34376 --- /dev/null +++ b/local-cli/rnpm/link/test/fixtures/linearGradient.pbxproj @@ -0,0 +1,258 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + BBD49E3F1AC8DEF000610F8E /* BVLinearGradient.m in Sources */ = {isa = PBXBuildFile; fileRef = BBD49E3A1AC8DEF000610F8E /* BVLinearGradient.m */; }; + BBD49E401AC8DEF000610F8E /* BVLinearGradientManager.m in Sources */ = {isa = PBXBuildFile; fileRef = BBD49E3C1AC8DEF000610F8E /* BVLinearGradientManager.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 58B511D91A9E6C8500147676 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 134814201AA4EA6300B7C361 /* libBVLinearGradient.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libBVLinearGradient.a; sourceTree = BUILT_PRODUCTS_DIR; }; + BBD49E391AC8DEF000610F8E /* BVLinearGradient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BVLinearGradient.h; sourceTree = ""; }; + BBD49E3A1AC8DEF000610F8E /* BVLinearGradient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BVLinearGradient.m; sourceTree = ""; }; + BBD49E3B1AC8DEF000610F8E /* BVLinearGradientManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BVLinearGradientManager.h; sourceTree = ""; }; + BBD49E3C1AC8DEF000610F8E /* BVLinearGradientManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BVLinearGradientManager.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 58B511D81A9E6C8500147676 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 134814211AA4EA7D00B7C361 /* Products */ = { + isa = PBXGroup; + children = ( + 134814201AA4EA6300B7C361 /* libBVLinearGradient.a */, + ); + name = Products; + sourceTree = ""; + }; + 58B511D21A9E6C8500147676 = { + isa = PBXGroup; + children = ( + BBD49E391AC8DEF000610F8E /* BVLinearGradient.h */, + BBD49E3A1AC8DEF000610F8E /* BVLinearGradient.m */, + BBD49E3B1AC8DEF000610F8E /* BVLinearGradientManager.h */, + BBD49E3C1AC8DEF000610F8E /* BVLinearGradientManager.m */, + 134814211AA4EA7D00B7C361 /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 58B511DA1A9E6C8500147676 /* BVLinearGradient */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "BVLinearGradient" */; + buildPhases = ( + 58B511D71A9E6C8500147676 /* Sources */, + 58B511D81A9E6C8500147676 /* Frameworks */, + 58B511D91A9E6C8500147676 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = BVLinearGradient; + productName = RCTDataManager; + productReference = 134814201AA4EA6300B7C361 /* libBVLinearGradient.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 58B511D31A9E6C8500147676 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 58B511DA1A9E6C8500147676 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "BVLinearGradient" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 58B511D21A9E6C8500147676; + productRefGroup = 58B511D21A9E6C8500147676; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 58B511DA1A9E6C8500147676 /* BVLinearGradient */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 58B511D71A9E6C8500147676 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BBD49E3F1AC8DEF000610F8E /* BVLinearGradient.m in Sources */, + BBD49E401AC8DEF000610F8E /* BVLinearGradientManager.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 58B511ED1A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 58B511EE1A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 58B511F01A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + "$(SRCROOT)/../react-native/React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = BVLinearGradient; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 58B511F11A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + "$(SRCROOT)/../react-native/React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = BVLinearGradient; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "BVLinearGradient" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511ED1A9E6C8500147676 /* Debug */, + 58B511EE1A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "BVLinearGradient" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511F01A9E6C8500147676 /* Debug */, + 58B511F11A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 58B511D31A9E6C8500147676 /* Project object */; +} diff --git a/local-cli/rnpm/link/test/fixtures/project.pbxproj b/local-cli/rnpm/link/test/fixtures/project.pbxproj new file mode 100644 index 00000000000000..e812b4ca885f0e --- /dev/null +++ b/local-cli/rnpm/link/test/fixtures/project.pbxproj @@ -0,0 +1,778 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; + 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; + 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; + 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; }; + 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; }; + 00E356F31AD99517003FC87E /* BasicTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* BasicTests.m */; }; + 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; }; + 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */; }; + 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */; }; + 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; + 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; + 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTActionSheet; + }; + 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTGeolocation; + }; + 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5115D1A9E6B3D00147676; + remoteInfo = RCTImage; + }; + 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B511DB1A9E6C8500147676; + remoteInfo = RCTNetwork; + }; + 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; + remoteInfo = RCTVibration; + }; + 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = Basic; + }; + 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTSettings; + }; + 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3C86DF461ADF2C930047B81A; + remoteInfo = RCTWebSocket; + }; + 146834031AC3E56700842450 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; + remoteInfo = React; + }; + 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTLinking; + }; + 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5119B1A9E6C1200147676; + remoteInfo = RCTText; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; + 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = "../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj"; sourceTree = ""; }; + 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = "../node_modules/react-native/Libraries/Geolocation/RCTGeolocation.xcodeproj"; sourceTree = ""; }; + 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = "../node_modules/react-native/Libraries/Image/RCTImage.xcodeproj"; sourceTree = ""; }; + 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = "../node_modules/react-native/Libraries/Network/RCTNetwork.xcodeproj"; sourceTree = ""; }; + 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = "../node_modules/react-native/Libraries/Vibration/RCTVibration.xcodeproj"; sourceTree = ""; }; + 00E356EE1AD99517003FC87E /* BasicTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BasicTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 00E356F21AD99517003FC87E /* BasicTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BasicTests.m; sourceTree = ""; }; + 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = "../node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj"; sourceTree = ""; }; + 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = "../node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj"; sourceTree = ""; }; + 13B07F961A680F5B00A75B9A /* Basic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Basic.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Basic/AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = Basic/AppDelegate.m; sourceTree = ""; }; + 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Basic/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Basic/Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Basic/main.m; sourceTree = ""; }; + 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../node_modules/react-native/React/React.xcodeproj"; sourceTree = ""; }; + 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; + 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 00E356EB1AD99517003FC87E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 146834051AC3E58100842450 /* libReact.a in Frameworks */, + 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, + 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */, + 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */, + 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */, + 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */, + 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */, + 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, + 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, + 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 00C302A81ABCB8CE00DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302B61ABCB90400DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302BC1ABCB91800DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302D41ABCB9D200DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302E01ABCB9EE00DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */, + ); + name = Products; + sourceTree = ""; + }; + 00E356EF1AD99517003FC87E /* BasicTests */ = { + isa = PBXGroup; + children = ( + 00E356F21AD99517003FC87E /* BasicTests.m */, + 00E356F01AD99517003FC87E /* Supporting Files */, + ); + path = BasicTests; + sourceTree = ""; + }; + 00E356F01AD99517003FC87E /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 00E356F11AD99517003FC87E /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 139105B71AF99BAD00B5F7CC /* Products */ = { + isa = PBXGroup; + children = ( + 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */, + ); + name = Products; + sourceTree = ""; + }; + 139FDEE71B06529A00C62182 /* Products */ = { + isa = PBXGroup; + children = ( + 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */, + ); + name = Products; + sourceTree = ""; + }; + 13B07FAE1A68108700A75B9A /* Basic */ = { + isa = PBXGroup; + children = ( + 008F07F21AC5B25A0029DE68 /* main.jsbundle */, + 13B07FAF1A68108700A75B9A /* AppDelegate.h */, + 13B07FB01A68108700A75B9A /* AppDelegate.m */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, + 13B07FB71A68108700A75B9A /* main.m */, + ); + name = Basic; + sourceTree = ""; + }; + 146834001AC3E56700842450 /* Products */ = { + isa = PBXGroup; + children = ( + 146834041AC3E56700842450 /* libReact.a */, + ); + name = Products; + sourceTree = ""; + }; + 78C398B11ACF4ADC00677621 /* Products */ = { + isa = PBXGroup; + children = ( + 78C398B91ACF4ADC00677621 /* libRCTLinking.a */, + ); + name = Products; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + 146833FF1AC3E56700842450 /* React.xcodeproj */, + 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */, + 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */, + 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */, + 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */, + 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */, + 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */, + 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, + 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, + 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */, + ); + name = Libraries; + sourceTree = ""; + }; + 832341B11AAA6A8300B99B32 /* Products */ = { + isa = PBXGroup; + children = ( + 832341B51AAA6A8300B99B32 /* libRCTText.a */, + ); + name = Products; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + AD9196DA1CABA83E000E8D91 /* NestedGroup */, + 13B07FAE1A68108700A75B9A /* Basic */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 00E356EF1AD99517003FC87E /* BasicTests */, + 83CBBA001A601CBA00E9B192 /* Products */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* Basic.app */, + 00E356EE1AD99517003FC87E /* BasicTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + AD9196DA1CABA83E000E8D91 /* NestedGroup */ = { + isa = PBXGroup; + children = ( + AD9196DB1CABA844000E8D91 /* Libraries */, + ); + name = NestedGroup; + sourceTree = ""; + }; + AD9196DB1CABA844000E8D91 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 00E356ED1AD99517003FC87E /* BasicTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "BasicTests" */; + buildPhases = ( + 00E356EA1AD99517003FC87E /* Sources */, + 00E356EB1AD99517003FC87E /* Frameworks */, + 00E356EC1AD99517003FC87E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 00E356F51AD99517003FC87E /* PBXTargetDependency */, + ); + name = BasicTests; + productName = BasicTests; + productReference = 00E356EE1AD99517003FC87E /* BasicTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 13B07F861A680F5B00A75B9A /* Basic */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Basic" */; + buildPhases = ( + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Basic; + productName = "Hello World"; + productReference = 13B07F961A680F5B00A75B9A /* Basic.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 00E356ED1AD99517003FC87E = { + CreatedOnToolsVersion = 6.2; + TestTargetID = 13B07F861A680F5B00A75B9A; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "a" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 00C302A81ABCB8CE00DB3ED1 /* Products */; + ProjectRef = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; + }, + { + ProductGroup = 00C302B61ABCB90400DB3ED1 /* Products */; + ProjectRef = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; + }, + { + ProductGroup = 00C302BC1ABCB91800DB3ED1 /* Products */; + ProjectRef = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; + }, + { + ProductGroup = 78C398B11ACF4ADC00677621 /* Products */; + ProjectRef = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; + }, + { + ProductGroup = 00C302D41ABCB9D200DB3ED1 /* Products */; + ProjectRef = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; + }, + { + ProductGroup = 139105B71AF99BAD00B5F7CC /* Products */; + ProjectRef = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; + }, + { + ProductGroup = 832341B11AAA6A8300B99B32 /* Products */; + ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; + }, + { + ProductGroup = 00C302E01ABCB9EE00DB3ED1 /* Products */; + ProjectRef = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; + }, + { + ProductGroup = 139FDEE71B06529A00C62182 /* Products */; + ProjectRef = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; + }, + { + ProductGroup = 146834001AC3E56700842450 /* Products */; + ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* Basic */, + 00E356ED1AD99517003FC87E /* BasicTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTActionSheet.a; + remoteRef = 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTGeolocation.a; + remoteRef = 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTImage.a; + remoteRef = 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTNetwork.a; + remoteRef = 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTVibration.a; + remoteRef = 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTSettings.a; + remoteRef = 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTWebSocket.a; + remoteRef = 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 146834041AC3E56700842450 /* libReact.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libReact.a; + remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTLinking.a; + remoteRef = 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 832341B51AAA6A8300B99B32 /* libRCTText.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTText.a; + remoteRef = 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 00E356EC1AD99517003FC87E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "../node_modules/react-native/packager/react-native-xcode.sh"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 00E356EA1AD99517003FC87E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 00E356F31AD99517003FC87E /* BasicTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, + 13B07FC11A68108700A75B9A /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* Basic */; + targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { + isa = PBXVariantGroup; + children = ( + 13B07FB21A68108700A75B9A /* Base */, + ); + name = LaunchScreen.xib; + path = Basic; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 00E356F61AD99517003FC87E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = BasicTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Basic.app/Basic"; + }; + name = Debug; + }; + 00E356F71AD99517003FC87E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + COPY_PHASE_STRIP = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + INFOPLIST_FILE = BasicTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Basic.app/Basic"; + }; + name = Release; + }; + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEAD_CODE_STRIPPING = NO; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + ); + INFOPLIST_FILE = "Basic/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = Basic; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + ); + INFOPLIST_FILE = "Basic/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = Basic; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "BasicTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00E356F61AD99517003FC87E /* Debug */, + 00E356F71AD99517003FC87E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Basic" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "a" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/local-cli/rnpm/link/test/getDependencyConfig.spec.js b/local-cli/rnpm/link/test/getDependencyConfig.spec.js new file mode 100644 index 00000000000000..7fba3b65bccd99 --- /dev/null +++ b/local-cli/rnpm/link/test/getDependencyConfig.spec.js @@ -0,0 +1,23 @@ +const chai = require('chai'); +const expect = chai.expect; +const getDependencyConfig = require('../src/getDependencyConfig'); +const sinon = require('sinon'); + +describe('getDependencyConfig', () => { + it('should return an array of dependencies\' rnpm config', () => { + const config = { + getDependencyConfig: sinon.stub(), + }; + + expect(getDependencyConfig(config, ['abcd'])).to.be.an.array; + expect(config.getDependencyConfig.callCount).to.equals(1); + }); + + it('should filter out invalid react-native projects', () => { + const config = { + getDependencyConfig: sinon.stub().throws(new Error('Cannot require')), + }; + + expect(getDependencyConfig(config, ['abcd'])).to.deep.equal([]); + }); +}); diff --git a/local-cli/rnpm/link/test/getPrefix.spec.js b/local-cli/rnpm/link/test/getPrefix.spec.js new file mode 100644 index 00000000000000..1b5e7262732d7e --- /dev/null +++ b/local-cli/rnpm/link/test/getPrefix.spec.js @@ -0,0 +1,20 @@ +const chai = require('chai'); +const expect = chai.expect; +const getMainActivityPatch = require('../src/android/getPrefix'); +const newPrefix = 'patches/0.18'; +const oldPrefix = 'patches/0.17'; + +describe('getPrefix', () => { + it('require a specific patch for react-native < 0.18', () => { + expect(getMainActivityPatch('0.17.0-rc')).to.equals(oldPrefix); + expect(getMainActivityPatch('0.17.1-rc2')).to.equals(oldPrefix); + expect(getMainActivityPatch('0.17.2')).to.equals(oldPrefix); + }); + + it('require a specific patch for react-native > 0.18', () => { + expect(getMainActivityPatch('0.19.0')).to.equals(newPrefix); + expect(getMainActivityPatch('0.19.0-rc')).to.equals(newPrefix); + expect(getMainActivityPatch('0.18.0-rc1')).to.equals(newPrefix); + expect(getMainActivityPatch('0.18.2')).to.equals(newPrefix); + }); +}); diff --git a/local-cli/rnpm/link/test/getProjectDependencies.spec.js b/local-cli/rnpm/link/test/getProjectDependencies.spec.js new file mode 100644 index 00000000000000..21abb764828b07 --- /dev/null +++ b/local-cli/rnpm/link/test/getProjectDependencies.spec.js @@ -0,0 +1,27 @@ +const chai = require('chai'); +const expect = chai.expect; +const getProjectDependencies = require('../src/getProjectDependencies'); +const mock = require('mock-require'); +const path = require('path'); + +describe('getProjectDependencies', () => { + + it('should return an array of project dependencies', () => { + mock( + path.join(process.cwd(), './package.json'), + { dependencies: { lodash: '^6.0.0', 'react-native': '^16.0.0' } } + ); + + expect(getProjectDependencies()).to.deep.equals(['lodash']); + }); + + it('should return an empty array when no dependencies set', () => { + mock(path.join(process.cwd(), './package.json'), {}); + expect(getProjectDependencies()).to.deep.equals([]); + }); + + afterEach(() => { + mock.stopAll(); + }); + +}); diff --git a/local-cli/rnpm/link/test/groupFilesByType.spec.js b/local-cli/rnpm/link/test/groupFilesByType.spec.js new file mode 100644 index 00000000000000..ac5ecfc691a582 --- /dev/null +++ b/local-cli/rnpm/link/test/groupFilesByType.spec.js @@ -0,0 +1,23 @@ +const chai = require('chai'); +const expect = chai.expect; +const groupFilesByType = require('../src/groupFilesByType'); + +describe('groupFilesByType', () => { + + it('should group files by its type', () => { + const fonts = [ + 'fonts/a.ttf', + 'fonts/b.ttf', + ]; + const images = [ + 'images/a.jpg', + 'images/c.jpeg', + ]; + + const groupedFiles = groupFilesByType(fonts.concat(images)); + + expect(groupedFiles.font).to.deep.equal(fonts); + expect(groupedFiles.image).to.deep.equal(images); + }); + +}); diff --git a/local-cli/rnpm/link/test/ios/addFileToProject.spec.js b/local-cli/rnpm/link/test/ios/addFileToProject.spec.js new file mode 100644 index 00000000000000..141d2ba7130f45 --- /dev/null +++ b/local-cli/rnpm/link/test/ios/addFileToProject.spec.js @@ -0,0 +1,22 @@ +const chai = require('chai'); +const expect = chai.expect; +const xcode = require('xcode'); +const addFileToProject = require('../../src/ios/addFileToProject'); + +const project = xcode.project('test/fixtures/project.pbxproj'); + +describe('ios::addFileToProject', () => { + + beforeEach(() => { + project.parseSync(); + }); + + it('should add file to a project', () => { + const file = addFileToProject(project, '../fixtures/linearGradient.pbxproj'); + + expect( + project.pbxFileReferenceSection() + ).to.include.keys(file.fileRef); + }); + +}); diff --git a/local-cli/rnpm/link/test/ios/addProjectToLibraries.spec.js b/local-cli/rnpm/link/test/ios/addProjectToLibraries.spec.js new file mode 100644 index 00000000000000..696cce114c4218 --- /dev/null +++ b/local-cli/rnpm/link/test/ios/addProjectToLibraries.spec.js @@ -0,0 +1,28 @@ +const chai = require('chai'); +const expect = chai.expect; +const xcode = require('xcode'); +const PbxFile = require('xcode/lib/pbxFile'); +const addProjectToLibraries = require('../../src/ios/addProjectToLibraries'); +const last = require('lodash').last; + +const project = xcode.project('test/fixtures/project.pbxproj'); + +describe('ios::addProjectToLibraries', () => { + + beforeEach(() => { + project.parseSync(); + }); + + it('should append file to Libraries group', () => { + const file = new PbxFile('fakePath'); + const libraries = project.pbxGroupByName('Libraries'); + + addProjectToLibraries(libraries, file); + + const child = last(libraries.children); + + expect(child).to.have.keys(['value', 'comment']); + expect(child.comment).to.equals(file.basename); + }); + +}); diff --git a/local-cli/rnpm/link/test/ios/createGroup.spec.js b/local-cli/rnpm/link/test/ios/createGroup.spec.js new file mode 100644 index 00000000000000..f4653ea612c08c --- /dev/null +++ b/local-cli/rnpm/link/test/ios/createGroup.spec.js @@ -0,0 +1,49 @@ +const chai = require('chai'); +const expect = chai.expect; +const xcode = require('xcode'); +const createGroup = require('../../src/ios/createGroup'); +const getGroup = require('../../src/ios/getGroup'); +const last = require('lodash').last; + +const project = xcode.project('test/fixtures/project.pbxproj'); + +describe('ios::createGroup', () => { + + beforeEach(() => { + project.parseSync(); + }); + + it('should create a group with given name', () => { + const createdGroup = createGroup(project, 'Resources'); + expect(createdGroup.name).to.equals('Resources'); + }); + + it('should attach group to main project group', () => { + const createdGroup = createGroup(project, 'Resources'); + const mainGroup = getGroup(project); + + expect( + last(mainGroup.children).comment + ).to.equals(createdGroup.name); + }); + + it('should create a nested group with given path', () => { + const createdGroup = createGroup(project, 'NewGroup/NewNestedGroup'); + const outerGroup = getGroup(project, 'NewGroup'); + + expect( + last(outerGroup.children).comment + ).to.equals(createdGroup.name); + }); + + it('should-not create already created groups', () => { + const createdGroup = createGroup(project, 'Libraries/NewNestedGroup'); + const outerGroup = getGroup(project, 'Libraries'); + const mainGroup = getGroup(project); + + expect( + mainGroup.children.filter(group => group.comment === 'Libraries').length + ).to.equals(1); + expect(last(outerGroup.children).comment).to.equals(createdGroup.name); + }); +}); diff --git a/local-cli/rnpm/link/test/ios/getBuildProperty.spec.js b/local-cli/rnpm/link/test/ios/getBuildProperty.spec.js new file mode 100644 index 00000000000000..1ca3f30a605ed7 --- /dev/null +++ b/local-cli/rnpm/link/test/ios/getBuildProperty.spec.js @@ -0,0 +1,19 @@ +const chai = require('chai'); +const expect = chai.expect; +const xcode = require('xcode'); +const getBuildProperty = require('../../src/ios/getBuildProperty'); + +const project = xcode.project('test/fixtures/project.pbxproj'); + +describe('ios::getBuildProperty', () => { + + beforeEach(() => { + project.parseSync(); + }); + + it('should return build property from main target', () => { + const plistPath = getBuildProperty(project, 'INFOPLIST_FILE'); + expect(plistPath).to.equals('"Basic/Info.plist"'); + }); + +}); diff --git a/local-cli/rnpm/link/test/ios/getGroup.spec.js b/local-cli/rnpm/link/test/ios/getGroup.spec.js new file mode 100644 index 00000000000000..5df45f7f000f78 --- /dev/null +++ b/local-cli/rnpm/link/test/ios/getGroup.spec.js @@ -0,0 +1,36 @@ +const chai = require('chai'); +const expect = chai.expect; +const xcode = require('xcode'); +const getGroup = require('../../src/ios/getGroup'); + +const project = xcode.project('test/fixtures/project.pbxproj'); + +describe('ios::getGroup', () => { + beforeEach(() => { + project.parseSync(); + }); + + it('should return a top-level group', () => { + const group = getGroup(project, 'Libraries'); + expect(group.children.length > 0).to.be.true; // our test top-level Libraries has children + expect(group.name).to.equals('Libraries'); + }); + + it('should return nested group when specified', () => { + const group = getGroup(project, 'NestedGroup/Libraries'); + expect(group.children.length).to.equals(0); // our test nested Libraries is empty + expect(group.name).to.equals('Libraries'); + }); + + it('should return null when no group found', () => { + const group = getGroup(project, 'I-Dont-Exist'); + expect(group).to.be.null; + }); + + it('should return top-level group when name not specified', () => { + const mainGroupId = project.getFirstProject().firstProject.mainGroup; + const mainGroup = project.getPBXGroupByKey(mainGroupId); + const group = getGroup(project); + expect(group).to.equals(mainGroup); + }); +}); diff --git a/local-cli/rnpm/link/test/ios/getHeaderSearchPath.spec.js b/local-cli/rnpm/link/test/ios/getHeaderSearchPath.spec.js new file mode 100644 index 00000000000000..216979ca41273c --- /dev/null +++ b/local-cli/rnpm/link/test/ios/getHeaderSearchPath.spec.js @@ -0,0 +1,58 @@ +const chai = require('chai'); +const expect = chai.expect; +const getHeaderSearchPath = require('../../src/ios/getHeaderSearchPath'); +const path = require('path'); + +const SRC_DIR = path.join('react-native-project', 'ios'); + +describe('ios::getHeaderSearchPath', () => { + + /** + * See https://github.com/Microsoft/react-native-code-push + */ + it('should return correct path when all headers are in root folder', () => { + const files = [ + path.join('react-native-project', 'node_modules', 'package', 'Gradient.h'), + path.join('react-native-project', 'node_modules', 'package', 'Manager.h'), + ]; + + const searchPath = getHeaderSearchPath(SRC_DIR, files); + + expect(searchPath).to.equal( + `"${['$(SRCROOT)', '..', 'node_modules', 'package'].join(path.sep)}"` + ); + }); + + /** + * See https://github.com/facebook/react-native/tree/master/React + */ + it('should return correct path when headers are in multiple folders', () => { + const files = [ + path.join('react-native-project', 'node_modules', 'package', 'src', 'folderA', 'Gradient.h'), + path.join('react-native-project', 'node_modules', 'package', 'src', 'folderB', 'Manager.h'), + ]; + + const searchPath = getHeaderSearchPath(SRC_DIR, files); + + expect(searchPath).to.equal( + `"${['$(SRCROOT)', '..', 'node_modules', 'package', 'src'].join(path.sep)}/**"` + ); + }); + + /** + * This is just to make sure the above two does not collide with each other + */ + it('should return correct path when headers are in root and nested folders', () => { + const files = [ + path.join('react-native-project', 'node_modules', 'package', 'src', 'folderA', 'Gradient.h'), + path.join('react-native-project', 'node_modules', 'package', 'src', 'folderB', 'Manager.h'), + path.join('react-native-project', 'node_modules', 'package', 'src', 'Manager.h'), + ]; + + const searchPath = getHeaderSearchPath(SRC_DIR, files); + + expect(searchPath).to.equal( + `"${['$(SRCROOT)', '..', 'node_modules', 'package', 'src'].join(path.sep)}/**"` + ); + }); +}); diff --git a/local-cli/rnpm/link/test/ios/getHeadersInFolder.spec.js b/local-cli/rnpm/link/test/ios/getHeadersInFolder.spec.js new file mode 100644 index 00000000000000..54f4c0f6e914fa --- /dev/null +++ b/local-cli/rnpm/link/test/ios/getHeadersInFolder.spec.js @@ -0,0 +1,45 @@ +const chai = require('chai'); +const expect = chai.expect; +const getHeadersInFolder = require('../../src/ios/getHeadersInFolder'); +const mock = require('mock-fs'); + +describe('ios::getHeadersInFolder', () => { + + it('should return an array of all headers in given folder', () => { + mock({ + 'FileA.h': '', + 'FileB.h': '', + }); + + const foundHeaders = getHeadersInFolder(process.cwd()); + + expect(foundHeaders.length).to.equals(2); + + getHeadersInFolder(process.cwd()).forEach(headerPath => { + expect(headerPath).to.contain(process.cwd()); + }); + }); + + it('should ignore all headers in Pods, Examples & node_modules', () => { + mock({ + 'FileA.h': '', + 'FileB.h': '', + Pods: { + 'FileC.h': '', + }, + Examples: { + 'FileD.h': '', + }, + node_modules: { + 'FileE.h': '', + }, + }); + + expect(getHeadersInFolder(process.cwd()).length).to.equals(2); + }); + + afterEach(() => { + mock.restore(); + }); + +}); diff --git a/local-cli/rnpm/link/test/ios/getPlist.spec.js b/local-cli/rnpm/link/test/ios/getPlist.spec.js new file mode 100644 index 00000000000000..76e072755c2661 --- /dev/null +++ b/local-cli/rnpm/link/test/ios/getPlist.spec.js @@ -0,0 +1,23 @@ +const chai = require('chai'); +const expect = chai.expect; +const xcode = require('xcode'); +const getPlist = require('../../src/ios/getPlist'); + +const project = xcode.project('test/fixtures/project.pbxproj'); + +describe('ios::getPlist', () => { + + beforeEach(() => { + project.parseSync(); + }); + + it('should return null when `.plist` file missing', () => { + const plistPath = getPlist(project, process.cwd()); + expect(plistPath).to.equals(null); + }); + + it.skip('should return parsed `plist`', () => { + // @todo mock fs here + }); + +}); diff --git a/local-cli/rnpm/link/test/ios/getPlistPath.spec.js b/local-cli/rnpm/link/test/ios/getPlistPath.spec.js new file mode 100644 index 00000000000000..c8c758ac04e3b1 --- /dev/null +++ b/local-cli/rnpm/link/test/ios/getPlistPath.spec.js @@ -0,0 +1,19 @@ +const chai = require('chai'); +const expect = chai.expect; +const xcode = require('xcode'); +const getPlistPath = require('../../src/ios/getPlistPath'); + +const project = xcode.project('test/fixtures/project.pbxproj'); + +describe('ios::getPlistPath', () => { + + beforeEach(() => { + project.parseSync(); + }); + + it('should return path without Xcode $(SRCROOT)', () => { + const plistPath = getPlistPath(project, '/'); + expect(plistPath).to.equals('/Basic/Info.plist'); + }); + +}); diff --git a/local-cli/rnpm/link/test/ios/getProducts.spec.js b/local-cli/rnpm/link/test/ios/getProducts.spec.js new file mode 100644 index 00000000000000..bf483f1b2e69ad --- /dev/null +++ b/local-cli/rnpm/link/test/ios/getProducts.spec.js @@ -0,0 +1,20 @@ +const chai = require('chai'); +const expect = chai.expect; +const xcode = require('xcode'); +const getProducts = require('../../src/ios/getProducts'); + +const project = xcode.project('test/fixtures/linearGradient.pbxproj'); + +describe('ios::getProducts', () => { + + beforeEach(() => { + project.parseSync(); + }); + + it('should return an array of static libraries project exports', () => { + const products = getProducts(project); + expect(products.length).to.equals(1); + expect(products).to.contains('libBVLinearGradient.a'); + }); + +}); diff --git a/local-cli/rnpm/link/test/ios/hasLibraryImported.spec.js b/local-cli/rnpm/link/test/ios/hasLibraryImported.spec.js new file mode 100644 index 00000000000000..c321418799eafa --- /dev/null +++ b/local-cli/rnpm/link/test/ios/hasLibraryImported.spec.js @@ -0,0 +1,24 @@ +const chai = require('chai'); +const expect = chai.expect; +const xcode = require('xcode'); +const hasLibraryImported = require('../../src/ios/hasLibraryImported'); + +const project = xcode.project('test/fixtures/project.pbxproj'); + +describe('ios::hasLibraryImported', () => { + + beforeEach(() => { + project.parseSync(); + }); + + it('should return true if project has been already imported', () => { + const libraries = project.pbxGroupByName('Libraries'); + expect(hasLibraryImported(libraries, 'React.xcodeproj')).to.be.true; + }); + + it('should return false if project is not imported', () => { + const libraries = project.pbxGroupByName('Libraries'); + expect(hasLibraryImported(libraries, 'ACME.xcodeproj')).to.be.false; + }); + +}); diff --git a/local-cli/rnpm/link/test/ios/isInstalled.spec.js b/local-cli/rnpm/link/test/ios/isInstalled.spec.js new file mode 100644 index 00000000000000..65983c6f9922fd --- /dev/null +++ b/local-cli/rnpm/link/test/ios/isInstalled.spec.js @@ -0,0 +1,39 @@ +const chai = require('chai'); +const expect = chai.expect; +const mock = require('mock-fs'); +const fs = require('fs'); +const path = require('path'); +const isInstalled = require('../../src/ios/isInstalled'); + +const baseProjectConfig = { + pbxprojPath: 'project.pbxproj', + libraryFolder: 'Libraries', +}; + +describe('ios::isInstalled', () => { + + before(() => { + mock({ + 'project.pbxproj': fs.readFileSync(path.join(__dirname, '../fixtures/project.pbxproj')), + }); + }); + + it('should return true when .xcodeproj in Libraries', () => { + const dependencyConfig = { projectName: 'React.xcodeproj' }; + expect(isInstalled(baseProjectConfig, dependencyConfig)).to.be.true; + }); + + it('should return false when .xcodeproj not in Libraries', () => { + const dependencyConfig = { projectName: 'Missing.xcodeproj' }; + expect(isInstalled(baseProjectConfig, dependencyConfig)).to.be.false; + }); + + it('should return false when `LibraryFolder` is missing', () => { + const dependencyConfig = { projectName: 'React.xcodeproj' }; + const projectConfig = Object.assign({}, baseProjectConfig, { libraryFolder: 'Missing' }); + expect(isInstalled(projectConfig, dependencyConfig)).to.be.false; + }); + + after(mock.restore); + +}); diff --git a/local-cli/rnpm/link/test/ios/mapHeaderSearchPaths.spec.js b/local-cli/rnpm/link/test/ios/mapHeaderSearchPaths.spec.js new file mode 100644 index 00000000000000..f885fbd8f39164 --- /dev/null +++ b/local-cli/rnpm/link/test/ios/mapHeaderSearchPaths.spec.js @@ -0,0 +1,23 @@ +const chai = require('chai'); +const expect = chai.expect; +const xcode = require('xcode'); +const mapHeaderSearchPaths = require('../../src/ios/mapHeaderSearchPaths'); + +const project = xcode.project('test/fixtures/project.pbxproj'); +const reactPath = '"$(SRCROOT)/../node_modules/react-native/React/**"'; + +describe('ios::mapHeaderSearchPaths', () => { + + beforeEach(() => { + project.parseSync(); + }); + + it('should iterate over headers with `react` added only', () => { + const path = '../../node_modules/path-to-module/**'; + + mapHeaderSearchPaths(project, paths => { + expect(paths.find(path => path.indexOf(reactPath))).to.be.not.empty; + }); + }); + +}); diff --git a/local-cli/rnpm/link/test/ios/removeProjectFromLibraries.js b/local-cli/rnpm/link/test/ios/removeProjectFromLibraries.js new file mode 100644 index 00000000000000..49a4c42b463061 --- /dev/null +++ b/local-cli/rnpm/link/test/ios/removeProjectFromLibraries.js @@ -0,0 +1,33 @@ +const chai = require('chai'); +const expect = chai.expect; +const xcode = require('xcode'); +const PbxFile = require('xcode/lib/pbxFile'); +const addProjectToLibraries = require('../../src/ios/addProjectToLibraries'); +const removeProjectFromLibraries = require('../../src/ios/removeProjectFromLibraries'); +const last = require('lodash').last; + +const project = xcode.project('test/fixtures/project.pbxproj'); + +describe('ios::removeProjectFromLibraries', () => { + + beforeEach(() => { + project.parseSync(); + + addProjectToLibraries( + project.pbxGroupByName('Libraries'), + new PbxFile('fakePath') + ); + }); + + it('should remove file from Libraries group', () => { + const file = new PbxFile('fakePath'); + const libraries = project.pbxGroupByName('Libraries'); + + removeProjectFromLibraries(libraries, file); + + const child = last(libraries.children); + + expect(child.comment).to.not.equals(file.basename); + }); + +}); diff --git a/local-cli/rnpm/link/test/ios/removeProjectFromProject.spec.js b/local-cli/rnpm/link/test/ios/removeProjectFromProject.spec.js new file mode 100644 index 00000000000000..8220028aeafbec --- /dev/null +++ b/local-cli/rnpm/link/test/ios/removeProjectFromProject.spec.js @@ -0,0 +1,32 @@ +const chai = require('chai'); +const expect = chai.expect; +const xcode = require('xcode'); +const pbxFile = require('xcode/lib/pbxFile'); +const addFileToProject = require('../../src/ios/addFileToProject'); +const removeProjectFromProject = require('../../src/ios/removeProjectFromProject'); + +const project = xcode.project('test/fixtures/project.pbxproj'); +const filePath = '../fixtures/linearGradient.pbxproj'; + +describe('ios::addFileToProject', () => { + + beforeEach(() => { + project.parseSync(); + addFileToProject(project, filePath); + }); + + it('should return removed file', () => { + expect(removeProjectFromProject(project, filePath)).to.be.instanceof(pbxFile); + }); + + it('should remove file from a project', () => { + const file = removeProjectFromProject(project, filePath); + expect(project.pbxFileReferenceSection()).to.not.include.keys(file.fileRef); + }); + + it.skip('should remove file from PBXContainerProxy', () => { + // todo(mike): add in .xcodeproj after Xcode modifications so we can test extra + // removals later. + }); + +}); diff --git a/local-cli/rnpm/link/test/link.spec.js b/local-cli/rnpm/link/test/link.spec.js new file mode 100644 index 00000000000000..9cce9988da09df --- /dev/null +++ b/local-cli/rnpm/link/test/link.spec.js @@ -0,0 +1,199 @@ +const chai = require('chai'); +const expect = chai.expect; +const sinon = require('sinon'); +const mock = require('mock-require'); +const log = require('npmlog'); +const path = require('path'); + +const link = require('../src/link'); + +log.level = 'silent'; + +describe('link', () => { + + beforeEach(() => { + delete require.cache[require.resolve('../src/link')]; + }); + + it('should reject when run in a folder without package.json', (done) => { + const config = { + getProjectConfig: () => { + throw new Error('No package.json found'); + }, + }; + + link(config).catch(() => done()); + }); + + it('should accept a name of a dependency to link', (done) => { + const config = { + getProjectConfig: () => ({ assets: [] }), + getDependencyConfig: sinon.stub().returns({ assets: [], commands: {} }), + }; + + link(config, ['react-native-gradient']).then(() => { + expect( + config.getDependencyConfig.calledWith('react-native-gradient') + ).to.be.true; + done(); + }); + }); + + it('should read dependencies from package.json when name not provided', (done) => { + const config = { + getProjectConfig: () => ({ assets: [] }), + getDependencyConfig: sinon.stub().returns({ assets: [], commands: {} }), + }; + + mock( + path.join(process.cwd(), 'package.json'), + { + dependencies: { + 'react-native-test': '*', + }, + } + ); + + link(config, []).then(() => { + expect( + config.getDependencyConfig.calledWith('react-native-test') + ).to.be.true; + done(); + }); + }); + + it('should register native module when android/ios projects are present', (done) => { + const registerNativeModule = sinon.stub(); + const dependencyConfig = {android: {}, ios: {}, assets: [], commands: {}}; + const config = { + getProjectConfig: () => ({android: {}, ios: {}, assets: []}), + getDependencyConfig: sinon.stub().returns(dependencyConfig), + }; + + mock( + '../src/android/isInstalled.js', + sinon.stub().returns(false) + ); + + mock( + '../src/android/registerNativeModule.js', + registerNativeModule + ); + + mock( + '../src/ios/isInstalled.js', + sinon.stub().returns(false) + ); + + mock( + '../src/ios/registerNativeModule.js', + registerNativeModule + ); + + const link = require('../src/link'); + + link(config, ['react-native-blur']).then(() => { + expect(registerNativeModule.calledTwice).to.be.true; + done(); + }); + }); + + it('should not register modules when they are already installed', (done) => { + const registerNativeModule = sinon.stub(); + const dependencyConfig = {ios: {}, android: {}, assets: [], commands: {}}; + const config = { + getProjectConfig: () => ({ ios: {}, android: {}, assets: [] }), + getDependencyConfig: sinon.stub().returns(dependencyConfig), + }; + + mock( + '../src/ios/isInstalled.js', + sinon.stub().returns(true) + ); + + mock( + '../src/android/isInstalled.js', + sinon.stub().returns(true) + ); + + mock( + '../src/ios/registerNativeModule.js', + registerNativeModule + ); + + mock( + '../src/android/registerNativeModule.js', + registerNativeModule + ); + + const link = require('../src/link'); + + link(config, ['react-native-blur']).then(() => { + expect(registerNativeModule.callCount).to.equal(0); + done(); + }); + }); + + it('should run prelink and postlink commands at the appropriate times', (done) => { + const registerNativeModule = sinon.stub(); + const prelink = sinon.stub().yieldsAsync(); + const postlink = sinon.stub().yieldsAsync(); + + mock( + '../src/ios/registerNativeModule.js', + registerNativeModule + ); + + mock( + '../src/ios/isInstalled.js', + sinon.stub().returns(false) + ); + + const config = { + getProjectConfig: () => ({ ios: {}, assets: [] }), + getDependencyConfig: sinon.stub().returns({ + ios: {}, assets: [], commands: { prelink, postlink }, + }), + }; + + const link = require('../src/link'); + + link(config, ['react-native-blur']).then(() => { + expect(prelink.calledBefore(registerNativeModule)).to.be.true; + expect(postlink.calledAfter(registerNativeModule)).to.be.true; + done(); + }); + }); + + it('should copy assets from both project and dependencies projects', (done) => { + const dependencyAssets = ['Fonts/Font.ttf']; + const dependencyConfig = {assets: dependencyAssets, commands: {}}; + const projectAssets = ['Fonts/FontC.ttf']; + const copyAssets = sinon.stub(); + + mock( + '../src/ios/copyAssets.js', + copyAssets + ); + + const config = { + getProjectConfig: () => ({ ios: {}, assets: projectAssets }), + getDependencyConfig: sinon.stub().returns(dependencyConfig), + }; + + const link = require('../src/link'); + + link(config, ['react-native-blur']).then(() => { + expect(copyAssets.calledOnce).to.be.true; + expect(copyAssets.getCall(0).args[0]).to.deep.equals( + projectAssets.concat(dependencyAssets) + ); + done(); + }); + }); + + afterEach(() => { + mock.stopAll(); + }); + +}); diff --git a/local-cli/rnpm/link/test/promiseWaterfall.spec.js b/local-cli/rnpm/link/test/promiseWaterfall.spec.js new file mode 100644 index 00000000000000..55d215038f03cb --- /dev/null +++ b/local-cli/rnpm/link/test/promiseWaterfall.spec.js @@ -0,0 +1,37 @@ +const chai = require('chai'); +const expect = chai.expect; +const sinon = require('sinon'); +const promiseWaterfall = require('../src/promiseWaterfall'); + +describe('promiseWaterfall', () => { + + it('should run promises in a sequence', (done) => { + const tasks = [sinon.stub(), sinon.stub()]; + + promiseWaterfall(tasks).then(() => { + expect(tasks[0].calledBefore(tasks[1])).to.be.true; + done(); + }); + }); + + it('should resolve with last promise value', (done) => { + const tasks = [sinon.stub().returns(1), sinon.stub().returns(2)]; + + promiseWaterfall(tasks).then(value => { + expect(value).to.equal(2); + done(); + }); + }); + + it('should stop the sequence when one of promises is rejected', (done) => { + const error = new Error(); + const tasks = [sinon.stub().throws(error), sinon.stub().returns(2)]; + + promiseWaterfall(tasks).catch(err => { + expect(err).to.equal(error); + expect(tasks[1].callCount).to.equal(0); + done(); + }); + }); + +}); diff --git a/package.json b/package.json index 6ecdcb28406df2..df2d7d249f1dc5 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "^[./a-zA-Z0-9$_-]+\\.png$": "RelativeImageStub" }, "testPathIgnorePatterns": [ - "/node_modules/" + "/node_modules/", + "/local-cli/rnpm/" ], "haste": { "defaultPlatform": "ios", From ef21d99ded9cc06f3091ee2ea58b8f64e82aa0ad Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Fri, 20 May 2016 05:27:13 -0700 Subject: [PATCH 038/843] Adds packager configuration to support windows platform Summary: This pull request is a prerequisite to enabling the react-native-windows platform extension. In the Resolver component, we need to add 'windows' to the list of platforms that are allowed in the DependencyGraph. We also need to add 'react-native-windows' (the name of the Windows platform extension NPM module) to the `providesModuleNodeModules` option. This allows the node_module folder check in the DependencyGraphHelper from node-haste to be bypassed for *.windows.js files in the Windows NPM package. For good measure, I also included a change to blacklist.js to ensure .windows.js files are ignored when the packager is parameterized on a platform. Closes https://github.com/facebook/react-native/pull/7639 Differential Revision: D3327771 Pulled By: mkonicek fbshipit-source-id: d1080b045ff6aa0cbf05d8070ceb0eb4cdb6dceb --- packager/blacklist.js | 8 ++++++++ packager/react-packager/src/Resolver/index.js | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packager/blacklist.js b/packager/blacklist.js index 2a1346f97ce706..3b222145c42a3c 100644 --- a/packager/blacklist.js +++ b/packager/blacklist.js @@ -27,14 +27,22 @@ var platformBlacklists = { web: [ '.ios.js', '.android.js', + '.windows.js' ], ios: [ '.web.js', '.android.js', + '.windows.js', ], android: [ '.web.js', '.ios.js', + '.windows.js' + ], + windows: [ + '.web.js', + '.ios.js', + '.android.js' ], }; diff --git a/packager/react-packager/src/Resolver/index.js b/packager/react-packager/src/Resolver/index.js index 89fd40b96ebc16..5f5dc98e6569b0 100644 --- a/packager/react-packager/src/Resolver/index.js +++ b/packager/react-packager/src/Resolver/index.js @@ -95,13 +95,14 @@ class Resolver { providesModuleNodeModules: [ 'react', 'react-native', + 'react-native-windows', // Parse requires AsyncStorage. They will // change that to require('react-native') which // should work after this release and we can // remove it from here. 'parse', ], - platforms: ['ios', 'android'], + platforms: ['ios', 'android', 'windows'], preferNativePlatform: true, fileWatcher: opts.fileWatcher, cache: opts.cache, From d6c2f5f3f1e731b4caadb360730fd45982215e98 Mon Sep 17 00:00:00 2001 From: Alexander Blom Date: Fri, 20 May 2016 07:25:38 -0700 Subject: [PATCH 039/843] Don't find module methods until needed Reviewed By: astreet Differential Revision: D3322974 fbshipit-source-id: 4cb47cc2ebdffd77b62c5d54e65574d44eba286e --- .../facebook/react/bridge/BaseJavaModule.java | 67 +++++++++++-------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java index b5b6f65ac68e1d..7da66012a7ccd9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java @@ -21,6 +21,9 @@ import java.util.HashMap; import java.util.Map; +import static com.facebook.infer.annotation.Assertions.assertNotNull; +import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; + /** * Base class for Catalyst native modules whose implementations are written in Java. Default * implementations for {@link #initialize} and {@link #onCatalystInstanceDestroy} are provided for @@ -279,7 +282,7 @@ private String getAffectedRange(int startIndex, int jsArgumentsNeeded) { @Override public void invoke(CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray parameters) { - SystraceMessage.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "callJavaModuleMethod") + SystraceMessage.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "callJavaModuleMethod") .arg("method", mTraceName) .flush(); try { @@ -330,7 +333,7 @@ public void invoke(CatalystInstance catalystInstance, ExecutorToken executorToke "Could not invoke " + BaseJavaModule.this.getName() + "." + mMethod.getName(), ite); } } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); } } @@ -390,39 +393,46 @@ private String buildSignature(Method method) { } } - private final Map mMethods = new HashMap<>(); - private final Map mHooks = new HashMap<>(); - - public BaseJavaModule() { - Method[] targetMethods = getClass().getDeclaredMethods(); - for (int i = 0; i < targetMethods.length; i++) { - Method targetMethod = targetMethods[i]; - if (targetMethod.getAnnotation(ReactMethod.class) != null) { - String methodName = targetMethod.getName(); - if (mHooks.containsKey(methodName) || mMethods.containsKey(methodName)) { - // We do not support method overloading since js sees a function as an object regardless - // of number of params. - throw new IllegalArgumentException( - "Java Module " + getName() + " sync method name already registered: " + methodName); + private @Nullable Map mMethods; + private @Nullable Map mHooks; + + private void findMethods() { + if (mMethods == null) { + Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "findMethods"); + mMethods = new HashMap<>(); + mHooks = new HashMap<>(); + + Method[] targetMethods = getClass().getDeclaredMethods(); + for (Method targetMethod : targetMethods) { + if (targetMethod.getAnnotation(ReactMethod.class) != null) { + String methodName = targetMethod.getName(); + if (mHooks.containsKey(methodName) || mMethods.containsKey(methodName)) { + // We do not support method overloading since js sees a function as an object regardless + // of number of params. + throw new IllegalArgumentException( + "Java Module " + getName() + " sync method name already registered: " + methodName); + } + mMethods.put(methodName, new JavaMethod(targetMethod)); } - mMethods.put(methodName, new JavaMethod(targetMethod)); - } - if (targetMethod.getAnnotation(ReactSyncHook.class) != null) { - String methodName = targetMethod.getName(); - if (mHooks.containsKey(methodName) || mMethods.containsKey(methodName)) { - // We do not support method overloading since js sees a function as an object regardless - // of number of params. - throw new IllegalArgumentException( - "Java Module " + getName() + " sync method name already registered: " + methodName); + if (targetMethod.getAnnotation(ReactSyncHook.class) != null) { + String methodName = targetMethod.getName(); + if (mHooks.containsKey(methodName) || mMethods.containsKey(methodName)) { + // We do not support method overloading since js sees a function as an object regardless + // of number of params. + throw new IllegalArgumentException( + "Java Module " + getName() + " sync method name already registered: " + methodName); + } + mHooks.put(methodName, new SyncJavaHook(targetMethod)); } - mHooks.put(methodName, new SyncJavaHook(targetMethod)); } + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); } } @Override public final Map getMethods() { - return mMethods; + findMethods(); + return assertNotNull(mMethods); } /** @@ -433,7 +443,8 @@ public final Map getMethods() { } public final Map getSyncHooks() { - return mHooks; + findMethods(); + return assertNotNull(mHooks); } @Override From 0aea74ebeac2600d70a8dd1bd8dd20a3c563a718 Mon Sep 17 00:00:00 2001 From: dmueller39 Date: Fri, 20 May 2016 08:04:10 -0700 Subject: [PATCH 040/843] Fixed incorrect rows reported in onChangeVisibleRows Summary: There is a bug in onChangeVisibleRows when the renderSeparator is not null. The _updateVisibleRows function does not account for the presence of separator frames in the _childFrames array. When renderSeparator is not null, increment the totalIndex an additional time for each row that isn't the last in its section, or the last in the entire list. This continues a slightly brittle precedent of having a set of parallel conditions in render and _updateVisibleRows. (i.e. renderSectionHeader is used in both functions, in render as a condition to create a sectionHeader, and in _updateVisibleRows as a condition to increment the totalIndex. Before change: ![yeeqnmvmif](https://cloud.githubusercontent.com/assets/9422359/14515342/38543952-01c7-11e6-984c-7c1a3fc3d820.gif) After change: ![gzbrljclzm](https://cloud.githubusercontent.com/assets/9422359/14515340/3296e294-01c7-11e6-8ae9-1ad313600956.gif) Built using https://gist.github.com/dmueller39/f95028f6fe8bd251944bb604e51f18b2 Closes https://github.com/facebook/react-native/pull/6965 Differential Revision: D3328001 Pulled By: vjeux fbshipit-source-id: 977e54382ee07be7a432a54febafcae6acaae905 --- Libraries/CustomComponents/ListView/ListView.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Libraries/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index f2f0ac4a4dc8a2..13457c28b7fd44 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -596,6 +596,10 @@ var ListView = React.createClass({ var rowID = rowIDs[rowIdx]; var frame = this._childFrames[totalIndex]; totalIndex++; + if(this.props.renderSeparator && + (rowIdx !== rowIDs.length - 1 || sectionIdx === allRowIDs.length - 1)){ + totalIndex++; + } if (!frame) { break; } From bc634ea35044c84295904a6bb5f04e2d52ba688e Mon Sep 17 00:00:00 2001 From: Hugo Agbonon Date: Fri, 20 May 2016 08:59:50 -0700 Subject: [PATCH 041/843] More helpful error message when react-native start is forgotten Summary: Developing with react-native on Linux, I found myself facing this message: ![chpao3jwuaehr_p jpg large](https://cloud.githubusercontent.com/assets/1598317/15032665/ae90ee88-1263-11e6-9acd-3fe261c08c28.jpeg) The problem is actually quite simple: I hadn't used `react-native start` before starting `react-native run-android`, which caused this error, both on an emulator and a real Android device. As the message is currently unhelpful, but can be shown because of a simple mistake, I updated it. ~~Additionally, I clarified the fact that `react-native start` is still necessary on Linux, updating a title on the Linux and Windows Support documentation page.~~ Closes https://github.com/facebook/react-native/pull/7396 Differential Revision: D3305078 fbshipit-source-id: 2d87e02ff2ad15d8239fbcc0ada4a4e67b982e94 --- ReactAndroid/src/main/jni/react/JSCExecutor.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ReactAndroid/src/main/jni/react/JSCExecutor.cpp b/ReactAndroid/src/main/jni/react/JSCExecutor.cpp index c26f413844eea8..e8027a064d55ec 100644 --- a/ReactAndroid/src/main/jni/react/JSCExecutor.cpp +++ b/ReactAndroid/src/main/jni/react/JSCExecutor.cpp @@ -277,9 +277,12 @@ void JSCExecutor::flush() { if (!ensureBatchedBridgeObject()) { throwJSExecutionException( - "Couldn't get the native call queue: bridge configuration isn't available. This " - "probably indicates there was an issue loading the JS bundle, e.g. it wasn't packaged " - "into the app or was malformed. Check your logs (`adb logcat`) for more information."); + "Could not connect to development server.\n" + "Try the following to fix the issue:\n" + "Ensure that the packager server is running\n" + "Ensure that your device/emulator is connected to your machine and has USB debugging enabled - run 'adb devices' to see a list of connected devices\n" + "If you're on a physical device connected to the same machine, run 'adb reverse tcp:8081 tcp:8081' to forward requests from your device\n" + "If your device is on the same Wi-Fi network, set 'Debug server host & port for device' in 'Dev settings' to your machine's IP address and the port of the local dev server - e.g. 10.0.1.1:8081"); } std::string calls = m_flushedQueueObj->callAsFunction().toJSONString(); From a5083a3cd5f463a94f4e4bd382a768c2f960b841 Mon Sep 17 00:00:00 2001 From: Kyle Corbitt Date: Fri, 20 May 2016 10:26:24 -0700 Subject: [PATCH 042/843] Add Emberall to showcase Summary: I'm really excited to finally have been able to share with the world the app that I've been building with React Native for the last six months! Emberall is the best way to save and share family videos, and record special moments with your kids as they're growing up. (vjeux ping me if you want a premium account for free :P ) More details are on our company [blog](http://emberall.com/blog). I've been heads down for a while just getting this out the door, but I'll try to have a blog post out soon with lessons learned in getting the app finished and published in the App Store. Closes https://github.com/facebook/react-native/pull/7657 Differential Revision: D3328341 Pulled By: vjeux fbshipit-source-id: ef0c6a8e68fa1cab6caf6daa1a0d7d177d752e7f --- website/src/react-native/showcase.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/website/src/react-native/showcase.js b/website/src/react-native/showcase.js index 40130b28a35959..98e5b0ff9f2481 100644 --- a/website/src/react-native/showcase.js +++ b/website/src/react-native/showcase.js @@ -332,7 +332,7 @@ var apps = [ icon: 'http://a4.mzstatic.com/us/r30/Purple60/v4/1a/42/5b/1a425b56-848a-91f5-8078-9f5473c9021f/icon350x350.png', link: 'https://itunes.apple.com/us/app/clapit/id1062124740?mt=8', author: 'Refined Edge Solutions, LLC' - }, + }, { name: 'Codementor - Live 1:1 Expert Developer Help', icon: 'http://a1.mzstatic.com/us/r30/Purple3/v4/db/cf/35/dbcf3523-bac7-0f54-c6a8-a80bf4f43c38/icon175x175.jpeg', @@ -1008,6 +1008,12 @@ var apps = [ link: 'http://android.myapp.com/myapp/detail.htm?apkName=com.tencent.karaoke', author: '石玉磊', }, + { + name: 'Emberall', + icon: 'http://assets.emberall.com/images/app/icons/medium.png', + link: 'https://emberall.com/', + author: 'Kyle Corbitt', + }, ]; var AppList = React.createClass({ From e4753867eaf27724cb6e37c90cdbe169cd8135e7 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Fri, 20 May 2016 12:10:20 -0700 Subject: [PATCH 043/843] Fix internal fields access for inspector Reviewed By: sebmarkbage Differential Revision: D3325549 fbshipit-source-id: d6cf89f5dbbe9db5bec37632e6976c6e537575fb --- Libraries/Inspector/Inspector.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Libraries/Inspector/Inspector.js b/Libraries/Inspector/Inspector.js index d1ee7c93838190..0f8276f5bc2268 100644 --- a/Libraries/Inspector/Inspector.js +++ b/Libraries/Inspector/Inspector.js @@ -129,7 +129,7 @@ class Inspector extends React.Component { // if we inspect a stateless component we can't use the getPublicInstance method // therefore we use the internal _instance property directly. var publicInstance = instance['_instance'] || {}; - var source = instance._currentElement && instance._currentElement._source; + var source = instance['_currentElement'] && instance['_currentElement']['_source']; UIManager.measure(instance.getNativeNode(), (x, y, width, height, left, top) => { this.setState({ inspected: { @@ -149,9 +149,9 @@ class Inspector extends React.Component { var hierarchy = InspectorUtils.getOwnerHierarchy(instance); // if we inspect a stateless component we can't use the getPublicInstance method // therefore we use the internal _instance property directly. - var publicInstance = instance._instance || {}; + var publicInstance = instance['_instance'] || {}; var props = publicInstance.props || {}; - var source = instance._currentElement && instance._currentElement._source; + var source = instance['_currentElement'] && instance['_currentElement']['_source']; this.setState({ panelPos: pointerY > Dimensions.get('window').height / 2 ? 'top' : 'bottom', selection: hierarchy.length - 1, From 62e74f3832cc6deca4d32083930221bea18e0503 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Fri, 20 May 2016 12:12:58 -0700 Subject: [PATCH 044/843] Symbolicate JS stacktrace using RN Packager Summary: The way we currently symbolicate JS stack traces in RN during development time (e.g. inside the RedBox) is the following: we download the source map from RN, parse it and use `source-map` find original file/line numbers. All happens inside running JSC VM in a simulator. The problem with this approach is that the source map size is pretty big and it is very expensive to load/parse. Before we load sourcemaps: {F60869250} After we load sourcemaps: {F60869249} In the past it wasn't a big problem, however the sourcemap file is only getting larger and soon we will be loading it for yellow boxes too: https://github.com/facebook/react-native/pull/7459 Moving stack trace symbolication to server side will let us: - save a bunch of memory on device - improve performance (no need to JSON serialize/deserialize and transfer sourcemap via HTTP and bridge) - remove ugly workaround with `RCTExceptionsManager.updateExceptionMessage` - we will be able to symbolicate from native by simply sending HTTP request, which means symbolication can be more robust (no need to depend on crashed JS to do symbolication) and we can pause JSC to avoid getting too many redboxes that hide original error. - reduce the bundle by ~65KB (the size of source-map parsing library we ship, see SourceMap module) Reviewed By: davidaurelio Differential Revision: D3291793 fbshipit-source-id: 29dce5f40100259264f57254e6715ace8ea70174 --- .../src/Server/__tests__/Server-test.js | 57 +++++++++++++++++ packager/react-packager/src/Server/index.js | 62 +++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/packager/react-packager/src/Server/__tests__/Server-test.js b/packager/react-packager/src/Server/__tests__/Server-test.js index c96b9ce1dc468b..9d5ec47a9c92bf 100644 --- a/packager/react-packager/src/Server/__tests__/Server-test.js +++ b/packager/react-packager/src/Server/__tests__/Server-test.js @@ -14,6 +14,7 @@ jest.setMock('worker-farm', function() { return () => {}; }) .setMock('timers', { setImmediate: (fn) => setTimeout(fn, 0) }) .setMock('uglify-js') .setMock('crypto') + .setMock('source-map', { SourceMapConsumer: (fn) => {}}) .mock('../../Bundler') .mock('../../AssetServer') .mock('../../lib/declareOpts') @@ -21,6 +22,7 @@ jest.setMock('worker-farm', function() { return () => {}; }) .mock('../../Activity'); const Promise = require('promise'); +const SourceMapConsumer = require('source-map').SourceMapConsumer; const Bundler = require('../../Bundler'); const Server = require('../'); @@ -392,4 +394,59 @@ describe('processRequest', () => { ); }); }); + + describe('/symbolicate endpoint', () => { + pit('should symbolicate given stack trace', () => { + const body = JSON.stringify({stack: [{ + file: 'foo.bundle?platform=ios', + lineNumber: 2100, + column: 44, + customPropShouldBeLeftUnchanged: 'foo', + }]}); + + SourceMapConsumer.prototype.originalPositionFor = jest.fn((frame) => { + expect(frame.line).toEqual(2100); + expect(frame.column).toEqual(44); + return { + source: 'foo.js', + line: 21, + column: 4, + }; + }); + + return makeRequest( + requestHandler, + '/symbolicate', + { rawBody: body } + ).then(response => { + expect(JSON.parse(response.body)).toEqual({ + stack: [{ + file: 'foo.js', + lineNumber: 21, + column: 4, + customPropShouldBeLeftUnchanged: 'foo', + }] + }); + }); + }); + }); + + describe('/symbolicate handles errors', () => { + pit('should symbolicate given stack trace', () => { + const body = 'clearly-not-json'; + console.error = jest.fn(); + + return makeRequest( + requestHandler, + '/symbolicate', + { rawBody: body } + ).then(response => { + expect(response.statusCode).toEqual(500); + expect(JSON.parse(response.body)).toEqual({ + error: jasmine.any(String), + }); + expect(console.error).toBeCalled(); + }); + }); + }); }); diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index ecce02fde55f06..10258d4fb78367 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -14,6 +14,7 @@ const FileWatcher = require('node-haste').FileWatcher; const getPlatformExtension = require('node-haste').getPlatformExtension; const Bundler = require('../Bundler'); const Promise = require('promise'); +const SourceMapConsumer = require('source-map').SourceMapConsumer; const _ = require('lodash'); const declareOpts = require('../lib/declareOpts'); @@ -428,6 +429,9 @@ class Server { } else if (pathname.match(/^\/assets\//)) { this._processAssetsRequest(req, res); return; + } else if (pathname === '/symbolicate') { + this._symbolicate(req, res); + return; } else { next(); return; @@ -480,6 +484,64 @@ class Server { ).done(); } + _symbolicate(req, res) { + const startReqEventId = Activity.startEvent('symbolicate'); + new Promise.resolve(req.rawBody).then(body => { + const stack = JSON.parse(body).stack; + + // In case of multiple bundles / HMR, some stack frames can have + // different URLs from others + const urls = stack.map(frame => frame.file); + const uniqueUrls = urls.filter((elem, idx) => urls.indexOf(elem) === idx); + + const sourceMaps = uniqueUrls.map(sourceUrl => this._sourceMapForURL(sourceUrl)); + return Promise.all(sourceMaps).then(consumers => { + return stack.map(frame => { + const idx = uniqueUrls.indexOf(frame.file); + const consumer = consumers[idx]; + + const original = consumer.originalPositionFor({ + line: frame.lineNumber, + column: frame.column, + }); + + if (!original) { + return frame; + } + + return Object.assign({}, frame, { + file: original.source, + lineNumber: original.line, + column: original.column, + }); + }); + }); + }).then( + stack => res.end(JSON.stringify({stack: stack})), + error => { + console.error(error.stack || error); + res.statusCode = 500; + res.end(JSON.stringify({error: error.message})); + } + ).done(() => { + Activity.endEvent(startReqEventId); + }); + } + + _sourceMapForURL(reqUrl) { + const options = this._getOptionsFromUrl(reqUrl); + const optionsJson = JSON.stringify(options); + const building = this._bundles[optionsJson] || this.buildBundle(options); + this._bundles[optionsJson] = building; + return building.then(p => { + const sourceMap = p.getSourceMap({ + minify: options.minify, + dev: options.dev, + }); + return new SourceMapConsumer(sourceMap); + }); + } + _handleError(res, bundleID, error) { res.writeHead(error.status || 500, { 'Content-Type': 'application/json; charset=UTF-8', From 946ec48a07c8f205c8ea09f466dd5c89659eeec3 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Fri, 20 May 2016 12:13:02 -0700 Subject: [PATCH 045/843] Use RN Packager to symbolicate stack in redbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: See "D3291793 Symbolicate JS stacktrace using RN Packager" for the background of why this matters. TLDR - saving tons of memory by moving symbolication from JSC into server and cleaning up old hacks. Before this change: a redbox causes +73MB JSC size {F60869250} {F60869249} After this change: a redbox causes +1MB JSC size {F61005061} {F61005062} Next step – replace JS implementation by native to show better progress and clean up even more old APIs (ExceptionsManager.updateExceptionMessage). Reviewed By: davidaurelio Differential Revision: D3319151 fbshipit-source-id: 48ff4df27642ea4e1bc2414f48a8dd4d32adee50 --- .../Initialization/ExceptionsManager.js | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js index a75b166e29d8fb..19d651336d6846 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js +++ b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js @@ -29,23 +29,28 @@ function reportException(e: Error, isFatal: bool) { RCTExceptionsManager.reportSoftException(e.message, stack, currentExceptionID); } if (__DEV__) { - require('SourceMapsCache').getSourceMaps().then(sourceMaps => { - const prettyStack = parseErrorStack(e, sourceMaps); - RCTExceptionsManager.updateExceptionMessage( - e.message, - prettyStack, - currentExceptionID, - ); - }) - .catch(error => { - // This can happen in a variety of normal situations, such as - // Network module not being available, or when running locally - console.warn('Unable to load source map: ' + error.message); - }); + symbolicateAndUpdateStack(currentExceptionID, e.message, stack); } } } +function symbolicateAndUpdateStack(id, message, stack) { + const {fetch} = require('fetch'); + const {SourceCode, ExceptionsManager} = require('NativeModules'); + const match = SourceCode.scriptURL && SourceCode.scriptURL.match(/^https?:\/\/.*?\//); + const endpoint = (match && match[0] : 'http://localhost:8081/') + 'symbolicate'; + + fetch(endpoint, { method: 'POST', body: JSON.stringify({stack}) }) + .then(response => response.json()) + .then(response => + ExceptionsManager.updateExceptionMessage(message, response.stack, id)) + .catch(error => { + // This can happen in a variety of normal situations, such as + // Network module not being available, or when running locally + console.warn('Unable to symbolicate stack trace: ' + error.message); + }); +} + /** * Logs exceptions to the (native) console and displays them */ From 0525d4a1284b9f9f8fe9d8ee903268938e1c3201 Mon Sep 17 00:00:00 2001 From: Jimmy Mayoukou Date: Fri, 20 May 2016 13:25:29 -0700 Subject: [PATCH 046/843] Fix Linking Example Summary: It seems like the examples weren't working since the merge of `IntentAndroid` and `LinkingIOS` since they were still using `TouchableNativeFeedback` only available on Android. The problem was also reported here : https://github.com/facebook/react-native/issues/7615 Closes https://github.com/facebook/react-native/pull/7655 Differential Revision: D3329233 fbshipit-source-id: 8c9cabaab0a616dab406c4e444c9fa6da5f54b6d --- Examples/UIExplorer/LinkingExample.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/UIExplorer/LinkingExample.js b/Examples/UIExplorer/LinkingExample.js index 2c1a1cbd188e72..390d00314623ba 100644 --- a/Examples/UIExplorer/LinkingExample.js +++ b/Examples/UIExplorer/LinkingExample.js @@ -19,7 +19,7 @@ var { Linking, StyleSheet, Text, - TouchableNativeFeedback, + TouchableOpacity, View, } = ReactNative; var UIExplorerBlock = require('./UIExplorerBlock'); @@ -42,12 +42,12 @@ var OpenURLButton = React.createClass({ render: function() { return ( - Open {this.props.url} - + ); } }); From 3977d0f5b9abecc5ef268bb15581ec82ba588663 Mon Sep 17 00:00:00 2001 From: Christoph Pojer Date: Fri, 20 May 2016 13:54:57 -0700 Subject: [PATCH 047/843] Update to 12.1. Reviewed By: vjeux Differential Revision: D3329411 fbshipit-source-id: b59977beedcaa6c1f59d422566fae785c8537b16 --- package.json | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index df2d7d249f1dc5..8dd766fcd66b00 100644 --- a/package.json +++ b/package.json @@ -83,12 +83,10 @@ "downstream/core/toArray.js", "node_modules/jest-cli", "node_modules/react/dist", - "/node_modules/fbjs/.*/__mocks__/", + "node_modules/fbjs/.*/__mocks__/", + "node_modules/fbjs/node_modules/", "/website/" ], - "testFileExtensions": [ - "js" - ], "unmockedModulePathPatterns": [ "promise", "source-map", @@ -187,7 +185,7 @@ "eslint-plugin-flow-vars": "^0.2.1", "eslint-plugin-react": "^4.2.1", "flow-bin": "^0.25.0", - "jest-cli": "11.0.2", + "jest": "12.1.1", "portfinder": "0.4.0", "react": "^15.1.0-alpha.1", "shelljs": "0.6.0" From 807726bcb47ef5b17860c5969a661bee1f8fc651 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Fri, 20 May 2016 14:24:24 -0700 Subject: [PATCH 048/843] Rename `NavigationState` to `NavigationRoute`, rename `NavigationParentState` to `NavigationState`. Summary: This is the first step to clarify and simplify the type definations about navigation state. For now, `NavigationParentState` is actually used as the real navigation state and `NavigationState` is used as a route in navigation, which has been confusion among the APIs. To be clear, our APIs has no intention and interest in dealing with nested or hierarchical navigation states, and we should avoid have the name like `ParentState` or `children`. To fully migrate the types, theer will be a lot of code changes and this is just the first step to rename. = What's Next? 1. rename `navigationState.children` to `navigationState.routes` (breaking change!) 2. remove `navigationState.key` from its type defination. Reviewed By: ericvicenti Differential Revision: D3321403 fbshipit-source-id: 3e39b60f736c1135bc85d8bf2b89027d665e28d4 --- .../NavigationCompositionExample.js | 4 +-- .../UIExplorer/UIExplorerStateTitleMap.js | 11 +++++-- .../NavigationCardStack.js | 4 +-- .../NavigationAnimatedView.js | 6 ++-- .../NavigationPropTypes.js | 4 +-- .../NavigationStateUtils.js | 30 +++++++++---------- .../NavigationTransitioner.js | 4 +-- .../NavigationTypeDefinition.js | 20 ++++++------- .../Reducer/NavigationFindReducer.js | 6 ++-- .../Reducer/NavigationScenesReducer.js | 6 ++-- .../Reducer/NavigationStackReducer.js | 12 ++++---- .../Reducer/NavigationTabsReducer.js | 8 ++--- 12 files changed, 61 insertions(+), 54 deletions(-) diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationCompositionExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationCompositionExample.js index 049b1f6e06df71..60c573250ae326 100644 --- a/Examples/UIExplorer/NavigationExperimental/NavigationCompositionExample.js +++ b/Examples/UIExplorer/NavigationExperimental/NavigationCompositionExample.js @@ -42,7 +42,7 @@ const { import type { - NavigationParentState, + NavigationState, NavigationSceneRenderer, NavigationSceneRendererProps, } from 'NavigationTypeDefinition'; @@ -225,7 +225,7 @@ class ExampleTabScreen extends React.Component { } class NavigationCompositionExample extends React.Component { - state: NavigationParentState; + state: NavigationState; constructor() { super(); this.state = ExampleAppReducer(undefined, {}); diff --git a/Examples/UIExplorer/UIExplorerStateTitleMap.js b/Examples/UIExplorer/UIExplorerStateTitleMap.js index a7facd838e6a90..37e4bcd72a487b 100644 --- a/Examples/UIExplorer/UIExplorerStateTitleMap.js +++ b/Examples/UIExplorer/UIExplorerStateTitleMap.js @@ -1,4 +1,11 @@ /** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * * The examples provided by Facebook are for non-commercial testing and * evaluation purposes only. * @@ -18,9 +25,9 @@ // $FlowFixMe : This is a platform-forked component, and flow seems to only run on iOS? const UIExplorerList = require('./UIExplorerList'); -import type {NavigationState} from 'NavigationTypeDefinition'; +import type {NavigationRoute} from 'NavigationTypeDefinition'; -function StateTitleMap(state: NavigationState): string { +function StateTitleMap(state: NavigationRoute): string { if (UIExplorerList.Modules[state.key]) { return UIExplorerList.Modules[state.key].title } diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js index d167af6fa86848..4a535311099c5e 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js @@ -48,7 +48,7 @@ const {Directions} = NavigationCardStackPanResponder; import type { NavigationActionCaller, - NavigationParentState, + NavigationState, NavigationSceneRenderer, NavigationSceneRendererProps, } from 'NavigationTypeDefinition'; @@ -59,7 +59,7 @@ import type { type Props = { direction: NavigationGestureDirection, - navigationState: NavigationParentState, + navigationState: NavigationState, onNavigate: NavigationActionCaller, renderOverlay: ?NavigationSceneRenderer, renderScene: NavigationSceneRenderer, diff --git a/Libraries/NavigationExperimental/NavigationAnimatedView.js b/Libraries/NavigationExperimental/NavigationAnimatedView.js index 5f307850640a42..3661612951d92f 100644 --- a/Libraries/NavigationExperimental/NavigationAnimatedView.js +++ b/Libraries/NavigationExperimental/NavigationAnimatedView.js @@ -28,14 +28,14 @@ import type { NavigationAnimatedValue, NavigationAnimationSetter, NavigationLayout, - NavigationParentState, + NavigationState, NavigationScene, NavigationSceneRenderer, } from 'NavigationTypeDefinition'; type Props = { applyAnimation: NavigationAnimationSetter, - navigationState: NavigationParentState, + navigationState: NavigationState, onNavigate: NavigationActionCaller, renderOverlay: ?NavigationSceneRenderer, renderScene: NavigationSceneRenderer, @@ -53,7 +53,7 @@ const {PropTypes} = React; function applyDefaultAnimation( position: NavigationAnimatedValue, - navigationState: NavigationParentState, + navigationState: NavigationState, ): void { Animated.spring( position, diff --git a/Libraries/NavigationExperimental/NavigationPropTypes.js b/Libraries/NavigationExperimental/NavigationPropTypes.js index bdbab9474c5fb0..e3e72ec77b4a39 100644 --- a/Libraries/NavigationExperimental/NavigationPropTypes.js +++ b/Libraries/NavigationExperimental/NavigationPropTypes.js @@ -35,12 +35,12 @@ const action = PropTypes.shape({ /* NavigationAnimatedValue */ const animatedValue = PropTypes.instanceOf(Animated.Value); -/* NavigationState */ +/* NavigationRoute */ const navigationState = PropTypes.shape({ key: PropTypes.string.isRequired, }); -/* NavigationParentState */ +/* NavigationState */ const navigationParentState = PropTypes.shape({ index: PropTypes.number.isRequired, key: PropTypes.string.isRequired, diff --git a/Libraries/NavigationExperimental/NavigationStateUtils.js b/Libraries/NavigationExperimental/NavigationStateUtils.js index 63da07faf4c53d..50470957ddc0e2 100644 --- a/Libraries/NavigationExperimental/NavigationStateUtils.js +++ b/Libraries/NavigationExperimental/NavigationStateUtils.js @@ -14,11 +14,11 @@ const invariant = require('fbjs/lib/invariant'); import type { + NavigationRoute, NavigationState, - NavigationParentState, } from 'NavigationTypeDefinition'; -function getParent(state: NavigationState): ?NavigationParentState { +function getParent(state: NavigationRoute): ?NavigationState { if ( (state instanceof Object) && (state.children instanceof Array) && @@ -31,7 +31,7 @@ function getParent(state: NavigationState): ?NavigationParentState { return null; } -function get(state: NavigationState, key: string): ?NavigationState { +function get(state: NavigationRoute, key: string): ?NavigationRoute { const parentState = getParent(state); if (!parentState) { return null; @@ -40,7 +40,7 @@ function get(state: NavigationState, key: string): ?NavigationState { return childState || null; } -function indexOf(state: NavigationState, key: string): ?number { +function indexOf(state: NavigationRoute, key: string): ?number { const parentState = getParent(state); if (!parentState) { return null; @@ -52,8 +52,8 @@ function indexOf(state: NavigationState, key: string): ?number { return index; } -function push(state: NavigationParentState, newChildState: NavigationState): NavigationParentState { - var lastChildren: Array = state.children; +function push(state: NavigationState, newChildState: NavigationRoute): NavigationState { + var lastChildren: Array = state.children; return { ...state, children: [ @@ -64,7 +64,7 @@ function push(state: NavigationParentState, newChildState: NavigationState): Nav }; } -function pop(state: NavigationParentState): NavigationParentState { +function pop(state: NavigationState): NavigationState { const lastChildren = state.children; return { ...state, @@ -73,7 +73,7 @@ function pop(state: NavigationParentState): NavigationParentState { }; } -function reset(state: NavigationState, nextChildren: ?Array, nextIndex: ?number): NavigationState { +function reset(state: NavigationRoute, nextChildren: ?Array, nextIndex: ?number): NavigationRoute { const parentState = getParent(state); if (!parentState) { return state; @@ -90,7 +90,7 @@ function reset(state: NavigationState, nextChildren: ?Array, ne }; } -function set(state: ?NavigationState, key: string, nextChildren: Array, nextIndex: number): NavigationState { +function set(state: ?NavigationRoute, key: string, nextChildren: Array, nextIndex: number): NavigationRoute { if (!state) { return { children: nextChildren, @@ -117,7 +117,7 @@ function set(state: ?NavigationState, key: string, nextChildren: Array child.key === key)); invariant( index !== -1, - 'Cannot find child with matching key in this NavigationState' + 'Cannot find child with matching key in this NavigationRoute' ); return { ...parentState, @@ -144,7 +144,7 @@ function jumpTo(state: NavigationState, key: string): NavigationState { }; } -function replaceAt(state: NavigationState, key: string, newState: NavigationState): NavigationState { +function replaceAt(state: NavigationRoute, key: string, newState: NavigationRoute): NavigationRoute { const parentState = getParent(state); if (!parentState) { return state; @@ -153,7 +153,7 @@ function replaceAt(state: NavigationState, key: string, newState: NavigationStat const index = parentState.children.indexOf(parentState.children.find(child => child.key === key)); invariant( index !== -1, - 'Cannot find child with matching key in this NavigationState' + 'Cannot find child with matching key in this NavigationRoute' ); children[index] = newState; return { @@ -162,7 +162,7 @@ function replaceAt(state: NavigationState, key: string, newState: NavigationStat }; } -function replaceAtIndex(state: NavigationState, index: number, newState: NavigationState): NavigationState { +function replaceAtIndex(state: NavigationRoute, index: number, newState: NavigationRoute): NavigationRoute { const parentState = getParent(state); if (!parentState) { return state; diff --git a/Libraries/NavigationExperimental/NavigationTransitioner.js b/Libraries/NavigationExperimental/NavigationTransitioner.js index 04776afa0be162..65d573c1a55000 100644 --- a/Libraries/NavigationExperimental/NavigationTransitioner.js +++ b/Libraries/NavigationExperimental/NavigationTransitioner.js @@ -23,7 +23,7 @@ import type { NavigationActionCaller, NavigationAnimatedValue, NavigationLayout, - NavigationParentState, + NavigationState, NavigationScene, NavigationSceneRenderer, NavigationTransitionConfigurator, @@ -31,7 +31,7 @@ import type { type Props = { configureTransition: NavigationTransitionConfigurator, - navigationState: NavigationParentState, + navigationState: NavigationState, onNavigate: NavigationActionCaller, onTransitionEnd: () => void, onTransitionStart: () => void, diff --git a/Libraries/NavigationExperimental/NavigationTypeDefinition.js b/Libraries/NavigationExperimental/NavigationTypeDefinition.js index 8ee36194ac1418..05646548614e74 100644 --- a/Libraries/NavigationExperimental/NavigationTypeDefinition.js +++ b/Libraries/NavigationExperimental/NavigationTypeDefinition.js @@ -21,14 +21,14 @@ export type NavigationAnimatedValue = Animated.Value; export type NavigationGestureDirection = 'horizontal' | 'vertical'; -export type NavigationState = { +export type NavigationRoute = { key: string, }; -export type NavigationParentState = { +export type NavigationState = { index: number, key: string, - children: Array, + children: Array, }; export type NavigationAction = any; @@ -45,7 +45,7 @@ export type NavigationScene = { index: number, isStale: boolean, key: string, - navigationState: NavigationState, + navigationState: NavigationRoute, }; export type NavigationSceneRendererProps = { @@ -53,7 +53,7 @@ export type NavigationSceneRendererProps = { layout: NavigationLayout, // The navigation state of the containing view. - navigationState: NavigationParentState, + navigationState: NavigationState, // Callback to navigation with an action. onNavigate: NavigationActionCaller, @@ -102,19 +102,19 @@ export type NavigationActionCaller = Function; export type NavigationAnimationSetter = ( position: NavigationAnimatedValue, - newState: NavigationParentState, - lastState: NavigationParentState, + newState: NavigationState, + lastState: NavigationState, ) => void; export type NavigationRenderer = ( - navigationState: ?NavigationState, + navigationState: ?NavigationRoute, onNavigate: NavigationActionCaller, ) => ReactElement; export type NavigationReducer = ( - state: ?NavigationState, + state: ?NavigationRoute, action: ?NavigationAction, -) => NavigationState; +) => NavigationRoute; export type NavigationSceneRenderer = ( props: NavigationSceneRendererProps, diff --git a/Libraries/NavigationExperimental/Reducer/NavigationFindReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationFindReducer.js index d7e2d1afa5fd55..fd9e74bda9a89c 100644 --- a/Libraries/NavigationExperimental/Reducer/NavigationFindReducer.js +++ b/Libraries/NavigationExperimental/Reducer/NavigationFindReducer.js @@ -18,15 +18,15 @@ */ import type { - NavigationState, + NavigationRoute, NavigationReducer } from 'NavigationTypeDefinition'; function NavigationFindReducer( reducers: Array, - defaultState: NavigationState, + defaultState: NavigationRoute, ): NavigationReducer { - return function(lastState: ?NavigationState, action: ?any): NavigationState { + return function(lastState: ?NavigationRoute, action: ?any): NavigationRoute { for (let i = 0; i < reducers.length; i++) { let reducer = reducers[i]; let newState = reducer(lastState, action); diff --git a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js index 450e28b5cb62b0..ca6617c0461ebf 100644 --- a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js +++ b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js @@ -14,7 +14,7 @@ const invariant = require('fbjs/lib/invariant'); import type { - NavigationParentState, + NavigationState, NavigationScene, } from 'NavigationTypeDefinition'; @@ -69,8 +69,8 @@ function areScenesShallowEqual( function NavigationScenesReducer( scenes: Array, - nextState: NavigationParentState, - prevState: ?NavigationParentState, + nextState: NavigationState, + prevState: ?NavigationState, ): Array { if (prevState === nextState) { return scenes; diff --git a/Libraries/NavigationExperimental/Reducer/NavigationStackReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationStackReducer.js index 6fd3ac39d1ae0b..681b0acc0d0511 100644 --- a/Libraries/NavigationExperimental/Reducer/NavigationStackReducer.js +++ b/Libraries/NavigationExperimental/Reducer/NavigationStackReducer.js @@ -14,19 +14,19 @@ const NavigationStateUtils = require('NavigationStateUtils'); import type { + NavigationRoute, NavigationState, - NavigationParentState, NavigationReducer, } from 'NavigationTypeDefinition'; -export type ReducerForStateHandler = (state: NavigationState) => NavigationReducer; +export type ReducerForStateHandler = (state: NavigationRoute) => NavigationReducer; -export type PushedReducerForActionHandler = (action: any, lastState: NavigationParentState) => ?NavigationReducer; +export type PushedReducerForActionHandler = (action: any, lastState: NavigationState) => ?NavigationReducer; export type StackReducerConfig = { /* * The initialState is that the reducer will use when there is no previous state. - * Must be a NavigationParentState: + * Must be a NavigationState: * * { * children: [ @@ -37,7 +37,7 @@ export type StackReducerConfig = { * key: 'navStackKey' * } */ - initialState: NavigationParentState; + initialState: NavigationState; /* * Returns the sub-reducer for a particular state to handle. This will be called @@ -57,7 +57,7 @@ const defaultGetReducerForState = (initialState) => (state) => state || initialS function NavigationStackReducer({initialState, getReducerForState, getPushedReducerForAction}: StackReducerConfig): NavigationReducer { const getReducerForStateWithDefault = getReducerForState || defaultGetReducerForState; - return function (lastState: ?NavigationState, action: any): NavigationState { + return function (lastState: ?NavigationRoute, action: any): NavigationRoute { if (!lastState) { return initialState; } diff --git a/Libraries/NavigationExperimental/Reducer/NavigationTabsReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationTabsReducer.js index 5063b72bb07958..b4241f43fa00c0 100644 --- a/Libraries/NavigationExperimental/Reducer/NavigationTabsReducer.js +++ b/Libraries/NavigationExperimental/Reducer/NavigationTabsReducer.js @@ -16,7 +16,7 @@ const NavigationStateUtils = require('NavigationStateUtils'); import type { NavigationReducer, - NavigationState, + NavigationRoute, } from 'NavigationTypeDefinition'; const ActionTypes = { @@ -41,7 +41,7 @@ type TabsReducerConfig = { }; function NavigationTabsReducer({key, initialIndex, tabReducers}: TabsReducerConfig): NavigationReducer { - return function(lastNavState: ?NavigationState, action: ?any): NavigationState { + return function(lastNavState: ?NavigationRoute, action: ?any): NavigationRoute { if (!lastNavState) { lastNavState = { children: tabReducers.map(reducer => reducer(null, null)), @@ -63,7 +63,7 @@ function NavigationTabsReducer({key, initialIndex, tabReducers}: TabsReducerConf ); } const subReducers = tabReducers.map((tabReducer, tabIndex) => { - return function(navState: ?NavigationState, tabAction: any): NavigationState { + return function(navState: ?NavigationRoute, tabAction: any): NavigationRoute { if (!navState) { return lastParentNavState; } @@ -83,7 +83,7 @@ function NavigationTabsReducer({key, initialIndex, tabReducers}: TabsReducerConf }; }); let selectedTabReducer = subReducers.splice(lastParentNavState.index, 1)[0]; - subReducers.unshift(function(navState: ?NavigationState, action: any): NavigationState { + subReducers.unshift(function(navState: ?NavigationRoute, action: any): NavigationRoute { if (navState && action.type === 'BackAction') { return NavigationStateUtils.jumpToIndex( lastParentNavState, From 4446e31b5d7c737dab5c3f4851e712507e18804e Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Fri, 20 May 2016 17:03:34 -0700 Subject: [PATCH 049/843] ActionSheetIOS - support share sheet on modals Summary: Fixes #6913 - follow up to this commit https://github.com/facebook/react-native/commit/43dcdaffe2caa8a6a8a38932a3e97ccb172bbc13 to support this in the 2nd method as well. Closes https://github.com/facebook/react-native/pull/7244 Differential Revision: D3330843 fbshipit-source-id: 0923440550a7635202158b4afaba87e12f1c1d54 --- Libraries/ActionSheetIOS/RCTActionSheetManager.m | 7 ++----- Libraries/CameraRoll/RCTImagePickerManager.m | 4 ++-- React/Base/RCTUtils.h | 6 +++++- React/Base/RCTUtils.m | 15 +++++++++++++++ React/Modules/RCTAlertManager.m | 9 +-------- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/Libraries/ActionSheetIOS/RCTActionSheetManager.m b/Libraries/ActionSheetIOS/RCTActionSheetManager.m index 1be710f233e684..d38058fc493478 100644 --- a/Libraries/ActionSheetIOS/RCTActionSheetManager.m +++ b/Libraries/ActionSheetIOS/RCTActionSheetManager.m @@ -66,10 +66,7 @@ - (CGRect)sourceRectInView:(UIView *)sourceView NSInteger destructiveButtonIndex = options[@"destructiveButtonIndex"] ? [RCTConvert NSInteger:options[@"destructiveButtonIndex"]] : -1; NSInteger cancelButtonIndex = options[@"cancelButtonIndex"] ? [RCTConvert NSInteger:options[@"cancelButtonIndex"]] : -1; - UIViewController *controller = RCTKeyWindow().rootViewController; - while (controller.presentedViewController) { - controller = controller.presentedViewController; - } + UIViewController *controller = RCTPresentedViewController(); if (controller == nil) { RCTLogError(@"Tried to display action sheet but there is no application window. options: %@", options); @@ -195,7 +192,7 @@ - (CGRect)sourceRectInView:(UIView *)sourceView shareController.excludedActivityTypes = excludedActivityTypes; } - UIViewController *controller = RCTKeyWindow().rootViewController; + UIViewController *controller = RCTPresentedViewController(); #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 diff --git a/Libraries/CameraRoll/RCTImagePickerManager.m b/Libraries/CameraRoll/RCTImagePickerManager.m index a86062fab57a5c..8955bc1ef21a2d 100644 --- a/Libraries/CameraRoll/RCTImagePickerManager.m +++ b/Libraries/CameraRoll/RCTImagePickerManager.m @@ -150,7 +150,7 @@ - (void)_presentPicker:(UIImagePickerController *)imagePicker [_pickerCallbacks addObject:callback]; [_pickerCancelCallbacks addObject:cancelCallback]; - UIViewController *rootViewController = RCTKeyWindow().rootViewController; + UIViewController *rootViewController = RCTPresentedViewController(); [rootViewController presentViewController:imagePicker animated:YES completion:nil]; } @@ -164,7 +164,7 @@ - (void)_dismissPicker:(UIImagePickerController *)picker args:(NSArray *)args [_pickerCallbacks removeObjectAtIndex:index]; [_pickerCancelCallbacks removeObjectAtIndex:index]; - UIViewController *rootViewController = RCTKeyWindow().rootViewController; + UIViewController *rootViewController = RCTPresentedViewController(); [rootViewController dismissViewControllerAnimated:YES completion:nil]; if (args) { diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index ea319814588cf2..d004648297172b 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -72,9 +72,13 @@ RCT_EXTERN BOOL RCTRunningInAppExtension(void); RCT_EXTERN UIApplication *__nullable RCTSharedApplication(void); // Returns the current main window, useful if you need to access the root view -// or view controller, e.g. to present a modal view controller or alert. +// or view controller RCT_EXTERN UIWindow *__nullable RCTKeyWindow(void); +// Returns the presented view controller, useful if you need +// e.g. to present a modal view controller or alert over it +RCT_EXTERN UIViewController *__nullable RCTPresentedViewController(void); + // Does this device support force touch (aka 3D Touch)? RCT_EXTERN BOOL RCTForceTouchAvailable(void); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index aa0d11645b5174..a477c394c24ffe 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -439,6 +439,21 @@ BOOL RCTRunningInAppExtension(void) return RCTSharedApplication().keyWindow; } +UIViewController *__nullable RCTPresentedViewController(void) +{ + if (RCTRunningInAppExtension()) { + return nil; + } + + UIViewController *controller = RCTKeyWindow().rootViewController; + + while (controller.presentedViewController) { + controller = controller.presentedViewController; + } + + return controller; +} + BOOL RCTForceTouchAvailable(void) { static BOOL forceSupported; diff --git a/React/Modules/RCTAlertManager.m b/React/Modules/RCTAlertManager.m index 0d655733530dcb..53ea9a1fbc1fee 100644 --- a/React/Modules/RCTAlertManager.m +++ b/React/Modules/RCTAlertManager.m @@ -150,19 +150,12 @@ - (void)invalidate #endif { - UIViewController *presentingController = RCTKeyWindow().rootViewController; + UIViewController *presentingController = RCTPresentedViewController(); if (presentingController == nil) { RCTLogError(@"Tried to display alert view but there is no application window. args: %@", args); return; } - // Walk the chain up to get the topmost modal view controller. If modals are - // presented the root view controller's view might not be in the window - // hierarchy, and presenting from it will fail. - while (presentingController.presentedViewController) { - presentingController = presentingController.presentedViewController; - } - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:nil From a45d02538595ea692d7adff9271136ea0436a6f8 Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Fri, 20 May 2016 17:17:29 -0700 Subject: [PATCH 050/843] TabBarIOS itemPositioning - Fixes #4136 Summary: The default itemPositioning is `automatic` (referred to `auto` in this pull request) - you can check its behaviour in the docs attached. Sometimes that value has to be modified to have more predictable appearance as described in #4136. Closes https://github.com/facebook/react-native/pull/7217 Differential Revision: D3220958 Pulled By: mkonicek fbshipit-source-id: d4bf648b16e71825cd31c06d6b6396479767d19f --- Libraries/Components/TabBarIOS/TabBarIOS.ios.js | 11 +++++++++++ React/Views/RCTTabBar.m | 14 ++++++++++++-- React/Views/RCTTabBarManager.m | 11 +++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/Libraries/Components/TabBarIOS/TabBarIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarIOS.ios.js index c2165da17948a3..57208b92e063dd 100644 --- a/Libraries/Components/TabBarIOS/TabBarIOS.ios.js +++ b/Libraries/Components/TabBarIOS/TabBarIOS.ios.js @@ -43,6 +43,16 @@ var TabBarIOS = React.createClass({ * A Boolean value that indicates whether the tab bar is translucent */ translucent: React.PropTypes.bool, + /** + * Specifies tab bar item positioning. Available values are: + * - fill - distributes items across the entire width of the tab bar + * - center - centers item in the available tab bar space + * - auto (default) - distributes items dynamically according to the + * user interface idiom. In a horizontally compact environment (e.g. iPhone 5) + * this value defaults to `fill`, in a horizontally regular one (e.g. iPad) + * it defaults to center. + */ + itemPositioning: React.PropTypes.oneOf(['fill', 'center', 'auto']), }, render: function() { @@ -52,6 +62,7 @@ var TabBarIOS = React.createClass({ unselectedTintColor={this.props.unselectedTintColor} tintColor={this.props.tintColor} barTintColor={this.props.barTintColor} + itemPositioning={this.props.itemPositioning} translucent={this.props.translucent !== false}> {this.props.children} diff --git a/React/Views/RCTTabBar.m b/React/Views/RCTTabBar.m index f8103c022faa80..6f31f69985e20c 100644 --- a/React/Views/RCTTabBar.m +++ b/React/Views/RCTTabBar.m @@ -112,9 +112,9 @@ - (void)reactBridgeDidFinishTransaction if (_unselectedTintColor) { [tab.barItem setTitleTextAttributes:@{NSForegroundColorAttributeName: _unselectedTintColor} forState:UIControlStateNormal]; } - + [tab.barItem setTitleTextAttributes:@{NSForegroundColorAttributeName: self.tintColor} forState:UIControlStateSelected]; - + controller.tabBarItem = tab.barItem; if (tab.selected) { _tabController.selectedViewController = controller; @@ -150,6 +150,16 @@ - (void)setTranslucent:(BOOL)translucent { _tabController.tabBar.translucent = translucent; } +- (UITabBarItemPositioning)itemPositoning +{ + return _tabController.tabBar.itemPositioning; +} + +- (void)setItemPositioning:(UITabBarItemPositioning)itemPositioning +{ + _tabController.tabBar.itemPositioning = itemPositioning; +} + #pragma mark - UITabBarControllerDelegate - (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController diff --git a/React/Views/RCTTabBarManager.m b/React/Views/RCTTabBarManager.m index 10b215f56fd79d..582c1e6fc455d1 100644 --- a/React/Views/RCTTabBarManager.m +++ b/React/Views/RCTTabBarManager.m @@ -12,6 +12,16 @@ #import "RCTBridge.h" #import "RCTTabBar.h" +@implementation RCTConvert (UITabBar) + +RCT_ENUM_CONVERTER(UITabBarItemPositioning, (@{ + @"fill" : @(UITabBarItemPositioningFill), + @"auto" : @(UITabBarItemPositioningAutomatic), + @"center" : @(UITabBarItemPositioningCentered) +}), UITabBarItemPositioningAutomatic, integerValue) + +@end + @implementation RCTTabBarManager RCT_EXPORT_MODULE() @@ -25,5 +35,6 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(translucent, BOOL) +RCT_EXPORT_VIEW_PROPERTY(itemPositioning, UITabBarItemPositioning) @end From fb5d0ff587063dc5dec1985716375a9721e48c59 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Fri, 20 May 2016 18:09:57 -0700 Subject: [PATCH 051/843] D3321403 [NavigationExperimental][CleanUp]: Rename `scene.navigationState` to `scene.route`. Summary: [public / experimental API breaking change] The data type of `scene.navigationState` is `NavigationRoute`. Rename `scene.navigationState` to `scene.route` to avoid confusion such as treating `scene.navigationState` as the actual global navigation state (type: NavigationState). Reviewed By: ericvicenti Differential Revision: D3331076 fbshipit-source-id: 3ed989cc8492d398cbeb1b12186459deb261d1fb --- .../NavigationAnimatedExample.js | 6 ++-- .../NavigationCardStackExample.js | 2 +- .../NavigationCompositionExample.js | 2 +- Examples/UIExplorer/UIExplorerApp.ios.js | 4 +-- .../UIExplorer/UIExplorerStateTitleMap.js | 8 ++--- .../NavigationExperimental/NavigationCard.js | 7 ++-- .../NavigationCardStack.js | 2 +- .../NavigationPropTypes.js | 16 +++++----- .../NavigationTypeDefinition.js | 4 +-- .../Reducer/NavigationScenesReducer.js | 25 ++++++++------- .../__tests__/NavigationScenesReducer-test.js | 32 +++++++++---------- 11 files changed, 54 insertions(+), 54 deletions(-) diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js index 4c7c3377c085af..8764df483642c0 100644 --- a/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js +++ b/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js @@ -113,7 +113,7 @@ class NavigationAnimatedExample extends React.Component { _renderTitleComponent(/*NavigationSceneRendererProps*/ props) { return ( - {props.scene.navigationState.key} + {props.scene.route.key} ); } @@ -122,7 +122,7 @@ class NavigationAnimatedExample extends React.Component { return ( ); @@ -132,7 +132,7 @@ class NavigationAnimatedExample extends React.Component { return ( - {stateTypeTitleMap(props.scene.navigationState)} + {stateTypeTitleMap(props.scene.route)} ); } diff --git a/Examples/UIExplorer/UIExplorerApp.ios.js b/Examples/UIExplorer/UIExplorerApp.ios.js index 6739b56455e9ef..760a960438b2c6 100644 --- a/Examples/UIExplorer/UIExplorerApp.ios.js +++ b/Examples/UIExplorer/UIExplorerApp.ios.js @@ -152,13 +152,13 @@ class UIExplorerApp extends React.Component { _renderTitleComponent(props: NavigationSceneRendererProps): ReactElement { return ( - {UIExplorerStateTitleMap(props.scene.navigationState)} + {UIExplorerStateTitleMap(props.scene.route)} ); } _renderScene(props: NavigationSceneRendererProps): ?ReactElement { - const state = props.scene.navigationState; + const state = props.scene.route; if (state.key === 'AppList') { return ( { static propTypes = { sceneRenderer: PropTypes.func.isRequired, - sceneRendererProps: - PropTypes.shape(NavigationPropTypes.SceneRenderer).isRequired, + sceneRendererProps: NavigationPropTypes.SceneRenderer, }; shouldComponentUpdate(nextProps: SceneViewProps, nextState: any): boolean { return ( - nextProps.sceneRendererProps.scene.navigationState !== - this.props.sceneRendererProps.scene.navigationState + nextProps.sceneRendererProps.scene.route !== + this.props.sceneRendererProps.scene.route ); } diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js index 4a535311099c5e..1dc535cdd51c93 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js @@ -89,7 +89,7 @@ class NavigationCardStack extends React.Component { static propTypes = { direction: PropTypes.oneOf([Directions.HORIZONTAL, Directions.VERTICAL]), - navigationState: NavigationPropTypes.navigationParentState.isRequired, + navigationState: NavigationPropTypes.navigationState.isRequired, onNavigate: NavigationPropTypes.SceneRendererProps.onNavigate, renderOverlay: PropTypes.func, renderScene: PropTypes.func.isRequired, diff --git a/Libraries/NavigationExperimental/NavigationPropTypes.js b/Libraries/NavigationExperimental/NavigationPropTypes.js index e3e72ec77b4a39..9389ac1d68a91a 100644 --- a/Libraries/NavigationExperimental/NavigationPropTypes.js +++ b/Libraries/NavigationExperimental/NavigationPropTypes.js @@ -36,15 +36,15 @@ const action = PropTypes.shape({ const animatedValue = PropTypes.instanceOf(Animated.Value); /* NavigationRoute */ -const navigationState = PropTypes.shape({ +const navigationRoute = PropTypes.shape({ key: PropTypes.string.isRequired, }); -/* NavigationState */ -const navigationParentState = PropTypes.shape({ +/* navigationRoute */ +const navigationState = PropTypes.shape({ index: PropTypes.number.isRequired, key: PropTypes.string.isRequired, - children: PropTypes.arrayOf(navigationState), + children: PropTypes.arrayOf(navigationRoute), }); /* NavigationLayout */ @@ -61,13 +61,13 @@ const scene = PropTypes.shape({ index: PropTypes.number.isRequired, isStale: PropTypes.bool.isRequired, key: PropTypes.string.isRequired, - navigationState, + route: navigationRoute.isRequired, }); /* NavigationSceneRendererProps */ const SceneRendererProps = { layout: layout.isRequired, - navigationState: navigationParentState.isRequired, + navigationState: navigationState.isRequired, onNavigate: PropTypes.func.isRequired, position: animatedValue.isRequired, progress: animatedValue.isRequired, @@ -118,9 +118,9 @@ module.exports = { SceneRendererProps, // propTypes + SceneRenderer, action, - navigationParentState, navigationState, + navigationRoute, panHandlers, - SceneRenderer, }; diff --git a/Libraries/NavigationExperimental/NavigationTypeDefinition.js b/Libraries/NavigationExperimental/NavigationTypeDefinition.js index 05646548614e74..42ef78d5a69772 100644 --- a/Libraries/NavigationExperimental/NavigationTypeDefinition.js +++ b/Libraries/NavigationExperimental/NavigationTypeDefinition.js @@ -26,8 +26,8 @@ export type NavigationRoute = { }; export type NavigationState = { - index: number, key: string, + index: number, children: Array, }; @@ -45,7 +45,7 @@ export type NavigationScene = { index: number, isStale: boolean, key: string, - navigationState: NavigationRoute, + route: NavigationRoute, }; export type NavigationSceneRendererProps = { diff --git a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js index ca6617c0461ebf..80d791cd7e18fe 100644 --- a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js +++ b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js @@ -14,8 +14,9 @@ const invariant = require('fbjs/lib/invariant'); import type { - NavigationState, + NavigationRoute, NavigationScene, + NavigationState, } from 'NavigationTypeDefinition'; const SCENE_KEY_PREFIX = 'scene_'; @@ -62,8 +63,8 @@ function areScenesShallowEqual( one.key === two.key && one.index === two.index && one.isStale === two.isStale && - one.navigationState === two.navigationState && - one.navigationState.key === two.navigationState.key + one.route === two.route && + one.route.key === two.route.key ); } @@ -76,9 +77,9 @@ function NavigationScenesReducer( return scenes; } - const prevScenes = new Map(); - const freshScenes = new Map(); - const staleScenes = new Map(); + const prevScenes: Map = new Map(); + const freshScenes: Map = new Map(); + const staleScenes: Map = new Map(); // Populate stale scenes from previous scenes marked as stale. scenes.forEach(scene => { @@ -90,13 +91,13 @@ function NavigationScenesReducer( }); const nextKeys = new Set(); - nextState.children.forEach((navigationState, index) => { - const key = SCENE_KEY_PREFIX + navigationState.key; + nextState.children.forEach((route, index) => { + const key = SCENE_KEY_PREFIX + route.key; const scene = { index, isStale: false, key, - navigationState, + route, }; invariant( !nextKeys.has(key), @@ -115,8 +116,8 @@ function NavigationScenesReducer( if (prevState) { // Look at the previous children and classify any removed scenes as `stale`. - prevState.children.forEach((navigationState, index) => { - const key = SCENE_KEY_PREFIX + navigationState.key; + prevState.children.forEach((route: NavigationRoute, index) => { + const key = SCENE_KEY_PREFIX + route.key; if (freshScenes.has(key)) { return; } @@ -124,7 +125,7 @@ function NavigationScenesReducer( index, isStale: true, key, - navigationState, + route, }); }); } diff --git a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js index 9cced5b718ab13..6d543bdde7dfc1 100644 --- a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js +++ b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js @@ -17,7 +17,7 @@ const NavigationScenesReducer = require('NavigationScenesReducer'); * Simulate scenes transtion with changes of navigation states. */ function testTransition(states) { - const navigationStates = states.map(keys => { + const routes = states.map(keys => { return { children: keys.map(key => { return { key }; @@ -27,7 +27,7 @@ function testTransition(states) { let scenes = []; let prevState = null; - navigationStates.forEach((nextState) => { + routes.forEach((nextState) => { scenes = NavigationScenesReducer(scenes, nextState, prevState); prevState = nextState; }); @@ -47,7 +47,7 @@ describe('NavigationScenesReducer', () => { 'index': 0, 'isStale': false, 'key': 'scene_1', - 'navigationState': { + 'route': { 'key': '1' }, }, @@ -55,7 +55,7 @@ describe('NavigationScenesReducer', () => { 'index': 1, 'isStale': false, 'key': 'scene_2', - 'navigationState': { + 'route': { 'key': '2' }, }, @@ -74,7 +74,7 @@ describe('NavigationScenesReducer', () => { 'index': 0, 'isStale': false, 'key': 'scene_1', - 'navigationState': { + 'route': { 'key': '1' }, }, @@ -82,7 +82,7 @@ describe('NavigationScenesReducer', () => { 'index': 1, 'isStale': false, 'key': 'scene_2', - 'navigationState': { + 'route': { 'key': '2' }, }, @@ -90,7 +90,7 @@ describe('NavigationScenesReducer', () => { 'index': 2, 'isStale': false, 'key': 'scene_3', - 'navigationState': { + 'route': { 'key': '3' }, }, @@ -109,7 +109,7 @@ describe('NavigationScenesReducer', () => { 'index': 0, 'isStale': false, 'key': 'scene_1', - 'navigationState': { + 'route': { 'key': '1' }, }, @@ -117,7 +117,7 @@ describe('NavigationScenesReducer', () => { 'index': 1, 'isStale': false, 'key': 'scene_2', - 'navigationState': { + 'route': { 'key': '2' }, }, @@ -125,7 +125,7 @@ describe('NavigationScenesReducer', () => { 'index': 2, 'isStale': true, 'key': 'scene_3', - 'navigationState': { + 'route': { 'key': '3' }, }, @@ -143,7 +143,7 @@ describe('NavigationScenesReducer', () => { 'index': 0, 'isStale': true, 'key': 'scene_1', - 'navigationState': { + 'route': { 'key': '1' }, }, @@ -151,7 +151,7 @@ describe('NavigationScenesReducer', () => { 'index': 0, 'isStale': false, 'key': 'scene_3', - 'navigationState': { + 'route': { 'key': '3' }, }, @@ -159,7 +159,7 @@ describe('NavigationScenesReducer', () => { 'index': 1, 'isStale': true, 'key': 'scene_2', - 'navigationState': { + 'route': { 'key': '2' }, }, @@ -178,7 +178,7 @@ describe('NavigationScenesReducer', () => { 'index': 0, 'isStale': true, 'key': 'scene_1', - 'navigationState': { + 'route': { 'key': '1' }, }, @@ -186,7 +186,7 @@ describe('NavigationScenesReducer', () => { 'index': 0, 'isStale': false, 'key': 'scene_2', - 'navigationState': { + 'route': { 'key': '2' }, }, @@ -194,7 +194,7 @@ describe('NavigationScenesReducer', () => { 'index': 0, 'isStale': true, 'key': 'scene_3', - 'navigationState': { + 'route': { 'key': '3' }, }, From c1558bc7db9ad08df0437b17585be9cadfaedf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Corn=C3=A9=20Dorrestijn?= Date: Fri, 20 May 2016 18:40:59 -0700 Subject: [PATCH 052/843] Add Image#getSize for Android Summary: I've implemented the getSize method on Image for Android. **Test plan (required)** The result in the UIExample app can be seen here: ![android-getsize](https://cloud.githubusercontent.com/assets/570297/15442613/a29c9178-1ee2-11e6-97df-adc20aad0c32.jpg) Closes https://github.com/facebook/react-native/pull/7664 Differential Revision: D3331704 fbshipit-source-id: d784c861cbc653cd6b49310f4b5516c6583486ca --- Examples/UIExplorer/ImageExample.js | 1 - Libraries/Image/Image.android.js | 15 +++++ .../modules/image/ImageLoaderModule.java | 64 +++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js index 426909bba18286..0c374b19e9841e 100644 --- a/Examples/UIExplorer/ImageExample.js +++ b/Examples/UIExplorer/ImageExample.js @@ -509,7 +509,6 @@ exports.examples = [ render: function() { return ; }, - platform: 'ios', }, ]; diff --git a/Libraries/Image/Image.android.js b/Libraries/Image/Image.android.js index 3ec42358cc65bf..91dee727acd1ee 100644 --- a/Libraries/Image/Image.android.js +++ b/Libraries/Image/Image.android.js @@ -113,6 +113,21 @@ var Image = React.createClass({ statics: { resizeMode: ImageResizeMode, + + getSize( + url: string, + success: (width: number, height: number) => void, + failure: (error: any) => void, + ) { + return ImageLoader.getSize(url) + .then(function(sizes) { + success(sizes.width, sizes.height); + }) + .catch(failure || function() { + console.warn('Failed to get size for image: ' + url); + }); + }, + /** * Prefetches a remote image for later use by downloading it to the disk * cache diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.java index 84154ca2a67818..3596a46cb63df2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.java @@ -11,11 +11,16 @@ import android.net.Uri; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.WritableMap; import com.facebook.common.executors.CallerThreadExecutor; +import com.facebook.common.references.CloseableReference; import com.facebook.datasource.BaseDataSubscriber; import com.facebook.datasource.DataSource; import com.facebook.datasource.DataSubscriber; import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.imagepipeline.image.CloseableImage; import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequestBuilder; import com.facebook.react.bridge.Promise; @@ -27,6 +32,7 @@ public class ImageLoaderModule extends ReactContextBaseJavaModule { private static final String ERROR_INVALID_URI = "E_INVALID_URI"; private static final String ERROR_PREFETCH_FAILURE = "E_PREFETCH_FAILURE"; + private static final String ERROR_GET_SIZE_FAILURE = "E_GET_SIZE_FAILURE"; private final Object mCallerContext; @@ -45,6 +51,64 @@ public String getName() { return "ImageLoader"; } + @ReactMethod + public void getSize( + String uriString, + final Promise promise) { + if (uriString == null || uriString.isEmpty()) { + promise.reject(ERROR_INVALID_URI, "Cannot get the size of an image for an empty URI"); + return; + } + + Uri uri = Uri.parse(uriString); + ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri).build(); + + DataSource> dataSource = + Fresco.getImagePipeline().fetchDecodedImage(request, mCallerContext); + + DataSubscriber> dataSubscriber = + new BaseDataSubscriber>() { + @Override + protected void onNewResultImpl( + DataSource> dataSource) { + if (!dataSource.isFinished()) { + return; + } + CloseableReference ref = dataSource.getResult(); + if (ref != null) { + try { + CloseableImage image = ref.get(); + + WritableMap sizes = Arguments.createMap(); + sizes.putInt("width", image.getWidth()); + sizes.putInt("height", image.getHeight()); + + image.close(); + promise.resolve(sizes); + } catch (Exception e) { + promise.reject(ERROR_GET_SIZE_FAILURE, e); + } finally { + CloseableReference.closeSafely(ref); + dataSource.close(); + } + } else { + dataSource.close(); + promise.reject(ERROR_GET_SIZE_FAILURE); + } + } + + @Override + protected void onFailureImpl(DataSource> dataSource) { + try { + promise.reject(ERROR_GET_SIZE_FAILURE, dataSource.getFailureCause()); + } finally { + dataSource.close(); + } + } + }; + dataSource.subscribe(dataSubscriber, CallerThreadExecutor.getInstance()); + } + /** * Prefetches the given image to the Fresco image disk cache. * From 2ec9e0c6904f760c841d043d55c2af94d17747ab Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Fri, 20 May 2016 18:51:26 -0700 Subject: [PATCH 053/843] Stop catching exceptions in the cxx bridge Reviewed By: mhorowitz Differential Revision: D3186631 fbshipit-source-id: 5664897b82277159c2b74b289f9a5d416633e243 --- ReactCommon/bridge/ModuleRegistry.cpp | 32 +-------------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/ReactCommon/bridge/ModuleRegistry.cpp b/ReactCommon/bridge/ModuleRegistry.cpp index 6cd6671c1fee28..adfdf1dbce5eb4 100644 --- a/ReactCommon/bridge/ModuleRegistry.cpp +++ b/ReactCommon/bridge/ModuleRegistry.cpp @@ -104,37 +104,7 @@ void ModuleRegistry::callNativeMethod(ExecutorToken token, unsigned int moduleId } #endif - // TODO mhorowitz: systrace - std::string what; - try { - modules_[moduleId]->invoke(token, methodId, std::move(params)); - return; - } catch (const std::exception& e) { - what = e.what(); - // fall through; - } catch (...) { - // fall through; - } - - std::string moduleName = normalizeName(modules_[moduleId]->getName()); - auto descs = modules_[moduleId]->getMethods(); - std::string methodName; - if (methodId < descs.size()) { - methodName = descs[methodId].name; - } else { - methodName = folly::to("id ", methodId, " (out of range [0..", - descs.size(), "))"); - } - - if (what.empty()) { - throw std::runtime_error( - folly::to("Unknown native exception in module '", moduleName, - "' method '", methodName, "'")); - } else { - throw std::runtime_error( - folly::to("Native exception in module '", moduleName, - "' method '", methodName, "': ", what)); - } + modules_[moduleId]->invoke(token, methodId, std::move(params)); } MethodCallResult ModuleRegistry::callSerializableNativeHook(ExecutorToken token, unsigned int moduleId, unsigned int methodId, folly::dynamic&& params) { From 25ea176f63e4dd43df94499eb599e76cfd46c960 Mon Sep 17 00:00:00 2001 From: Nicolas Charpentier Date: Sat, 21 May 2016 00:09:46 -0700 Subject: [PATCH 054/843] Fix tab in Examples/UIExplorer/SwitchExample.js Summary: Closes https://github.com/facebook/react-native/pull/7585 Differential Revision: D3332470 fbshipit-source-id: 660dbb3e635a45cff35583b1a01094ec09ecb6fb --- Examples/UIExplorer/SwitchExample.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/UIExplorer/SwitchExample.js b/Examples/UIExplorer/SwitchExample.js index 02851057370818..19cf72596de660 100644 --- a/Examples/UIExplorer/SwitchExample.js +++ b/Examples/UIExplorer/SwitchExample.js @@ -109,7 +109,7 @@ var EventSwitchExample = React.createClass({ onValueChange={(value) => this.setState({eventSwitchIsOn: value})} style={{marginBottom: 10}} value={this.state.eventSwitchIsOn} /> - {this.state.eventSwitchIsOn ? 'On' : 'Off'} + {this.state.eventSwitchIsOn ? 'On' : 'Off'}
    Date: Sat, 21 May 2016 06:53:32 -0700 Subject: [PATCH 055/843] Use a separate babel config for the local-cli and the packager Summary: This separates the babel config of the local-cli and the packager from the one used by the transforms of the packager since it doesn't run in the same environment and the local-cli/packager doesn't require react specific transforms and runs in node 4 so we can also avoid some es2015 transforms that node already supports. I had to move the code in cli.js so it can still run in node 0.12 that doesn't support `const` since it is no longer transformed. **Test plan** Run the local-cli on node 0.12 and there should be a message saying that it requires at least node 4. Run the local-cli on node 4 and 5 and everything should work the same as before. I was also hoping for some perf gains but there was nothing noticeable. I did benchmark the babel-register call and it stayed pretty much the same. As for runtime performance it can help if there are optimisations for es2015 features in node. Closes https://github.com/facebook/react-native/pull/6155 Reviewed By: bestander Differential Revision: D3301008 Pulled By: davidaurelio fbshipit-source-id: 504180d158a1e50bc03e28fb0d1e53d0731ce32f --- local-cli/cli.js | 158 ++------------------------ local-cli/cliEntry.js | 161 +++++++++++++++++++++++++++ local-cli/server/checkNodeVersion.js | 10 +- local-cli/server/server.js | 3 - package.json | 4 + packager/babelRegisterOnly.js | 23 ++-- packager/package.json | 2 +- 7 files changed, 189 insertions(+), 172 deletions(-) create mode 100644 local-cli/cliEntry.js diff --git a/local-cli/cli.js b/local-cli/cli.js index a1418cd25aef7e..c8837ee7ce7a2b 100644 --- a/local-cli/cli.js +++ b/local-cli/cli.js @@ -8,164 +8,20 @@ */ 'use strict'; +// This file must be able to run in node 0.12 without babel so we can show that +// it is not supported. This is why the rest of the cli code is in `cliEntry.js`. +require('./server/checkNodeVersion')(); + require('../packager/babelRegisterOnly')([ /private-cli\/src/, /local-cli/, /react-packager\/src/ ]); -var bundle = require('./bundle/bundle'); -var childProcess = require('child_process'); -var Config = require('./util/Config'); -var defaultConfig = require('./default.config'); -var dependencies = require('./dependencies/dependencies'); -var generate = require('./generate/generate'); -var library = require('./library/library'); -var path = require('path'); -var Promise = require('promise'); -var runAndroid = require('./runAndroid/runAndroid'); -var runIOS = require('./runIOS/runIOS'); -var server = require('./server/server'); -var TerminalAdapter = require('yeoman-environment/lib/adapter.js'); -var yeoman = require('yeoman-environment'); -var unbundle = require('./bundle/unbundle'); -var upgrade = require('./upgrade/upgrade'); -var version = require('./version/version'); - -var fs = require('fs'); -var gracefulFs = require('graceful-fs'); - -// graceful-fs helps on getting an error when we run out of file -// descriptors. When that happens it will enqueue the operation and retry it. -gracefulFs.gracefulify(fs); - -var documentedCommands = { - 'start': [server, 'starts the webserver'], - 'bundle': [bundle, 'builds the javascript bundle for offline use'], - 'unbundle': [unbundle, 'builds javascript as "unbundle" for offline use'], - 'new-library': [library, 'generates a native library bridge'], - 'android': [generateWrapper, 'generates an Android project for your app'], - 'run-android': [runAndroid, 'builds your app and starts it on a connected Android emulator or device'], - 'run-ios': [runIOS, 'builds your app and starts it on iOS simulator'], - 'upgrade': [upgrade, 'upgrade your app\'s template files to the latest version; run this after ' + - 'updating the react-native version in your package.json and running npm install'] -}; - -var exportedCommands = {dependencies: dependencies}; -Object.keys(documentedCommands).forEach(function(command) { - exportedCommands[command] = documentedCommands[command][0]; -}); - -var undocumentedCommands = { - '--version': [version, ''], - 'init': [printInitWarning, ''], -}; - -var commands = Object.assign({}, documentedCommands, undocumentedCommands); - -/** - * Parses the command line and runs a command of the CLI. - */ -function run() { - var args = process.argv.slice(2); - if (args.length === 0) { - printUsage(); - } - - var setupEnvScript = /^win/.test(process.platform) - ? 'setup_env.bat' - : 'setup_env.sh'; - childProcess.execFileSync(path.join(__dirname, setupEnvScript)); - - var command = commands[args[0]]; - if (!command) { - console.error('Command `%s` unrecognized', args[0]); - printUsage(); - return; - } - - command[0](args, Config.get(__dirname, defaultConfig)).done(); -} - -function generateWrapper(args, config) { - return generate([ - '--platform', 'android', - '--project-path', process.cwd(), - '--project-name', JSON.parse( - fs.readFileSync('package.json', 'utf8') - ).name - ], config); -} - -function printUsage() { - console.log([ - 'Usage: react-native ', - '', - 'Commands:' - ].concat(Object.keys(documentedCommands).map(function(name) { - return ' - ' + name + ': ' + documentedCommands[name][1]; - })).join('\n')); - process.exit(1); -} - -// The user should never get here because projects are inited by -// using `react-native-cli` from outside a project directory. -function printInitWarning() { - return Promise.resolve().then(function() { - console.log([ - 'Looks like React Native project already exists in the current', - 'folder. Run this command from a different folder or remove node_modules/react-native' - ].join('\n')); - process.exit(1); - }); -} - -class CreateSuppressingTerminalAdapter extends TerminalAdapter { - constructor() { - super(); - // suppress 'create' output generated by yeoman - this.log.create = function() {}; - } -} - -/** - * Creates the template for a React Native project given the provided - * parameters: - * - projectDir: templates will be copied here. - * - argsOrName: project name or full list of custom arguments to pass to the - * generator. - */ -function init(projectDir, argsOrName) { - console.log('Setting up new React Native app in ' + projectDir); - var env = yeoman.createEnv( - undefined, - undefined, - new CreateSuppressingTerminalAdapter() - ); - - env.register( - require.resolve(path.join(__dirname, 'generator')), - 'react:app' - ); - - // argv is for instance - // ['node', 'react-native', 'init', 'AwesomeApp', '--verbose'] - // args should be ['AwesomeApp', '--verbose'] - var args = Array.isArray(argsOrName) - ? argsOrName - : [argsOrName].concat(process.argv.slice(4)); - - var generator = env.create('react:app', {args: args}); - generator.destinationRoot(projectDir); - generator.run(); -} +var cliEntry = require('./cliEntry'); if (require.main === module) { - run(); + cliEntry.run(); } -module.exports = { - run: run, - init: init, - commands: exportedCommands -}; +module.exports = cliEntry; diff --git a/local-cli/cliEntry.js b/local-cli/cliEntry.js new file mode 100644 index 00000000000000..6ec0f794fe569c --- /dev/null +++ b/local-cli/cliEntry.js @@ -0,0 +1,161 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const bundle = require('./bundle/bundle'); +const childProcess = require('child_process'); +const Config = require('./util/Config'); +const defaultConfig = require('./default.config'); +const dependencies = require('./dependencies/dependencies'); +const generate = require('./generate/generate'); +const library = require('./library/library'); +const path = require('path'); +const Promise = require('promise'); +const runAndroid = require('./runAndroid/runAndroid'); +const runIOS = require('./runIOS/runIOS'); +const server = require('./server/server'); +const TerminalAdapter = require('yeoman-environment/lib/adapter.js'); +const yeoman = require('yeoman-environment'); +const unbundle = require('./bundle/unbundle'); +const upgrade = require('./upgrade/upgrade'); +const version = require('./version/version'); + +const fs = require('fs'); +const gracefulFs = require('graceful-fs'); + +// graceful-fs helps on getting an error when we run out of file +// descriptors. When that happens it will enqueue the operation and retry it. +gracefulFs.gracefulify(fs); + +const documentedCommands = { + 'start': [server, 'starts the webserver'], + 'bundle': [bundle, 'builds the javascript bundle for offline use'], + 'unbundle': [unbundle, 'builds javascript as "unbundle" for offline use'], + 'new-library': [library, 'generates a native library bridge'], + 'android': [generateWrapper, 'generates an Android project for your app'], + 'run-android': [runAndroid, 'builds your app and starts it on a connected Android emulator or device'], + 'run-ios': [runIOS, 'builds your app and starts it on iOS simulator'], + 'upgrade': [upgrade, 'upgrade your app\'s template files to the latest version; run this after ' + + 'updating the react-native version in your package.json and running npm install'] +}; + +const exportedCommands = {dependencies: dependencies}; +Object.keys(documentedCommands).forEach(function(command) { + exportedCommands[command] = documentedCommands[command][0]; +}); + +const undocumentedCommands = { + '--version': [version, ''], + 'init': [printInitWarning, ''], +}; + +const commands = Object.assign({}, documentedCommands, undocumentedCommands); + +/** + * Parses the command line and runs a command of the CLI. + */ +function run() { + const args = process.argv.slice(2); + if (args.length === 0) { + printUsage(); + } + + const setupEnvScript = /^win/.test(process.platform) + ? 'setup_env.bat' + : 'setup_env.sh'; + childProcess.execFileSync(path.join(__dirname, setupEnvScript)); + + const command = commands[args[0]]; + if (!command) { + console.error('Command `%s` unrecognized', args[0]); + printUsage(); + return; + } + + command[0](args, Config.get(__dirname, defaultConfig)).done(); +} + +function generateWrapper(args, config) { + return generate([ + '--platform', 'android', + '--project-path', process.cwd(), + '--project-name', JSON.parse( + fs.readFileSync('package.json', 'utf8') + ).name + ], config); +} + +function printUsage() { + console.log([ + 'Usage: react-native ', + '', + 'Commands:' + ].concat(Object.keys(documentedCommands).map(function(name) { + return ' - ' + name + ': ' + documentedCommands[name][1]; + })).join('\n')); + process.exit(1); +} + +// The user should never get here because projects are inited by +// using `react-native-cli` from outside a project directory. +function printInitWarning() { + return Promise.resolve().then(function() { + console.log([ + 'Looks like React Native project already exists in the current', + 'folder. Run this command from a different folder or remove node_modules/react-native' + ].join('\n')); + process.exit(1); + }); +} + +class CreateSuppressingTerminalAdapter extends TerminalAdapter { + constructor() { + super(); + // suppress 'create' output generated by yeoman + this.log.create = function() {}; + } +} + +/** + * Creates the template for a React Native project given the provided + * parameters: + * - projectDir: templates will be copied here. + * - argsOrName: project name or full list of custom arguments to pass to the + * generator. + */ +function init(projectDir, argsOrName) { + console.log('Setting up new React Native app in ' + projectDir); + const env = yeoman.createEnv( + undefined, + undefined, + new CreateSuppressingTerminalAdapter() + ); + + env.register( + require.resolve(path.join(__dirname, 'generator')), + 'react:app' + ); + + // argv is for instance + // ['node', 'react-native', 'init', 'AwesomeApp', '--verbose'] + // args should be ['AwesomeApp', '--verbose'] + const args = Array.isArray(argsOrName) + ? argsOrName + : [argsOrName].concat(process.argv.slice(4)); + + const generator = env.create('react:app', {args: args}); + generator.destinationRoot(projectDir); + generator.run(); +} + +module.exports = { + run: run, + init: init, + commands: exportedCommands +}; diff --git a/local-cli/server/checkNodeVersion.js b/local-cli/server/checkNodeVersion.js index 406c08cf825770..013913d539830a 100644 --- a/local-cli/server/checkNodeVersion.js +++ b/local-cli/server/checkNodeVersion.js @@ -8,17 +8,17 @@ */ 'use strict'; -const chalk = require('chalk'); -const formatBanner = require('./formatBanner'); -const semver = require('semver'); +var chalk = require('chalk'); +var formatBanner = require('./formatBanner'); +var semver = require('semver'); module.exports = function() { if (!semver.satisfies(process.version, '>=4')) { - const engine = semver.satisfies(process.version, '<1 >=4') + var engine = semver.satisfies(process.version, '<1') ? 'Node' : 'io.js'; - const message = 'You are currently running ' + engine + ' ' + + var message = 'You are currently running ' + engine + ' ' + process.version + '.\n' + '\n' + 'React Native runs on Node 4.0 or newer. There are several ways to ' + diff --git a/local-cli/server/server.js b/local-cli/server/server.js index 21d1d8e2cb06ee..2b7efe8038e0d8 100644 --- a/local-cli/server/server.js +++ b/local-cli/server/server.js @@ -9,7 +9,6 @@ 'use strict'; const chalk = require('chalk'); -const checkNodeVersion = require('./checkNodeVersion'); const formatBanner = require('./formatBanner'); const parseCommandLine = require('../util/parseCommandLine'); const path = require('path'); @@ -88,8 +87,6 @@ function _server(argv, config, resolve, reject) { ) : config.getAssetRoots(); - checkNodeVersion(); - console.log(formatBanner( 'Running packager on port ' + args.port + '.\n\n' + 'Keep this packager running while developing on any JS projects. ' + diff --git a/package.json b/package.json index 8dd766fcd66b00..ddf4d240e2648d 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,11 @@ "art": "^0.10.0", "babel-core": "^6.6.4", "babel-plugin-external-helpers": "^6.5.0", + "babel-plugin-syntax-trailing-function-commas": "^6.5.0", + "babel-plugin-transform-flow-strip-types": "^6.6.5", + "babel-plugin-transform-object-rest-spread": "^6.6.5", "babel-polyfill": "^6.6.1", + "babel-preset-es2015-node": "^4.0.2", "babel-preset-react-native": "^1.8.0", "babel-register": "^6.6.0", "babel-types": "^6.6.4", diff --git a/packager/babelRegisterOnly.js b/packager/babelRegisterOnly.js index bb973ba4eb47c7..703ba3385bc005 100644 --- a/packager/babelRegisterOnly.js +++ b/packager/babelRegisterOnly.js @@ -12,20 +12,19 @@ Array.prototype.values || require('core-js/fn/array/values'); Object.entries || require('core-js/fn/object/entries'); Object.values || require('core-js/fn/object/values'); -var fs = require('fs'); -var path = require('path'); - var _only = []; -function readBabelRC() { - var rcpath = path.join(__dirname, 'react-packager', 'rn-babelrc.json'); - var source = fs.readFileSync(rcpath).toString(); - return JSON.parse(source); -} - module.exports = function(onlyList) { _only = _only.concat(onlyList); - var config = readBabelRC(); - config.only = _only; - require('babel-register')(config); + + require('babel-register')({ + presets: ['es2015-node'], + plugins: [ + 'transform-flow-strip-types', + 'syntax-trailing-function-commas', + 'transform-object-rest-spread', + ], + only: _only, + sourceMaps: 'inline', + }); }; diff --git a/packager/package.json b/packager/package.json index 36f258d6ce48d1..f18fc259b0b239 100644 --- a/packager/package.json +++ b/packager/package.json @@ -1,5 +1,5 @@ { - "version": "0.3.2", + "version": "0.4.0", "name": "react-native-packager", "description": "Build native apps with React!", "repository": { From 6d616d0e5b92ee2f26efd0392c8a98ccd8806fe4 Mon Sep 17 00:00:00 2001 From: Andrej Badin Date: Sat, 21 May 2016 09:03:16 -0700 Subject: [PATCH 056/843] Improve Navigation panel column layout for landscape/portrait Summary: Improve column layout for _phone vs. tablet_ & _portrait vs landscape_ variations. This change only affects handheld devices, or devices with really small (physical) screen dimensions. See previews bellow: Phone - landscape ![phone-landscape](https://cloud.githubusercontent.com/assets/829963/15448311/53863428-1f5e-11e6-9e05-d98587b9752f.png) Phone - portrait ![phone-portrait](https://cloud.githubusercontent.com/assets/829963/15448309/5385eb9e-1f5e-11e6-8d12-6bda7e1d2e7f.png) Tablet - landscape ![tablet-landscape](https://cloud.githubusercontent.com/assets/829963/15448312/538719a6-1f5e-11e6-9a2c-52d0c9109f09.png) Tablet - portrait ![tablet-portrait](https://cloud.githubusercontent.com/assets/829963/15448310/53862636-1f5e-11e6-87ff-727568d7dd1f.png) Closes https://github.com/facebook/react-native/pull/7669 Differential Revision: D3332676 Pulled By: vjeux fbshipit-source-id: 8cfc1903e35fd62c82fcd8dd1e05a1e2062c555d --- website/src/react-native/css/react-native.css | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/website/src/react-native/css/react-native.css b/website/src/react-native/css/react-native.css index c768d8370e935d..6209da6516f517 100644 --- a/website/src/react-native/css/react-native.css +++ b/website/src/react-native/css/react-native.css @@ -481,7 +481,7 @@ h1:hover .hash-link, h2:hover .hash-link, h3:hover .hash-link, h4:hover .hash-li border-bottom: 0; } -@media screen and (max-device-width: 960px) { +@media only screen and (max-device-width: 1024px) { .nav-docs { position: fixed; z-index: 90; @@ -555,18 +555,38 @@ h1:hover .hash-link, h2:hover .hash-link, h3:hover .hash-link, h4:hover .hash-li } } -@media screen and (min-device-width: 641px) and (max-device-width: 1024px) { +/** + * Multicolumn layout for phone (landscape only) & tablet (regardless its screen orientation)/ + */ +@media only screen and (min-device-width : 375px) and (max-device-width : 1024px) { .nav-docs-section ul { display: flex; flex-wrap: wrap; } - /* Display 2 columns on tablet */ + .nav-docs-section li { + width: 100%; + } +} + +/* 2 columns layout */ +@media + /*Phone, landscape screen orientation*/ + only screen and (min-device-width : 375px) and (max-device-width : 1024px) and (orientation : landscape), + /*Tablet, portrait screen orientation*/ + only screen and (min-device-width : 768px) and (max-device-width : 1024px) and (orientation : portrait) { .nav-docs-section li { width: 50%; } } +/* 3 columns layout on tablet (landscape screen orientation) */ +@media only screen and (min-device-width : 768px) and (max-device-width : 1024px) and (orientation : landscape) { + .nav-docs-section li { + width: 33%; + } +} + .nav-blog li { margin-bottom: 5px; } From 97f803bc2e0e38e5317f24d18da161ef37b27943 Mon Sep 17 00:00:00 2001 From: Joel Marcey Date: Sat, 21 May 2016 09:51:07 -0700 Subject: [PATCH 057/843] Add information about verifying Xcode Command Line Tools are installed Summary: screen shot 2016-05-19 at 10 12 35 am Closes https://github.com/facebook/react-native/pull/7637 Differential Revision: D3332705 Pulled By: JoelMarcey fbshipit-source-id: a01b98305fe1b11559e7c9893e7844cb37599fc4 --- docs/QuickStart-GettingStarted.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/QuickStart-GettingStarted.md b/docs/QuickStart-GettingStarted.md index b645dc0537cf38..2f4b9053ef176b 100644 --- a/docs/QuickStart-GettingStarted.md +++ b/docs/QuickStart-GettingStarted.md @@ -97,7 +97,9 @@ npm install -g react-native-cli #### Xcode -[Xcode](https://developer.apple.com/xcode/downloads/) 7.0 or higher. Open the App Store or go to https://developer.apple.com/xcode/downloads/. This will also install `git` as well. +[Xcode](https://developer.apple.com/xcode/downloads/) 7.0 or higher is required. You can install Xcode via the App Store or [Apple developer downloads](https://developer.apple.com/xcode/downloads/). This will install the Xcode IDE and Xcode Command Line Tools. + +> While generally installed by default, you can verify that the Xcode Command Line Tools are installed by launching Xcode and selecting `Xcode | Preferences | Locations` and ensuring there is a version of the command line tools shown in the `Command Line Tools` list box. The Command Line Tools give you `git`, etc. From 14dd3845c94d6cd8d53c86772351769597776103 Mon Sep 17 00:00:00 2001 From: Joel Marcey Date: Sat, 21 May 2016 11:42:53 -0700 Subject: [PATCH 058/843] Make Getting Started labels a bit more clear Summary: "Target" over "Platform" to represent target for your RN app "Development OS" over "OS" to represent your RN Development environment screenshot 2016-05-21 09 48 47 Closes https://github.com/facebook/react-native/pull/7674 Differential Revision: D3332780 fbshipit-source-id: b0acfc843e00a9bd71b940c5f51507667ba5123e --- docs/QuickStart-GettingStarted.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/QuickStart-GettingStarted.md b/docs/QuickStart-GettingStarted.md index 2f4b9053ef176b..c78d7501ceef24 100644 --- a/docs/QuickStart-GettingStarted.md +++ b/docs/QuickStart-GettingStarted.md @@ -35,10 +35,10 @@ block { display: none; } .display-platform-android.display-os-windows .android.windows { display: block; } -Platform: +Target: iOS Android -OS: +Development OS: Mac Linux Windows From 75d538e3ebb25f9ca188f3949784f8f4102c190f Mon Sep 17 00:00:00 2001 From: Philip Heinser Date: Sat, 21 May 2016 12:17:42 -0700 Subject: [PATCH 059/843] Added passpoints to showcase Summary: Adding an app to the Showcase. Closes https://github.com/facebook/react-native/pull/7645 Differential Revision: D3332779 fbshipit-source-id: 2fa4ce62106b9a2fbc2126a2e4126cb52f773c53 --- website/src/react-native/showcase.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/website/src/react-native/showcase.js b/website/src/react-native/showcase.js index 98e5b0ff9f2481..a554d54f2f5e5a 100644 --- a/website/src/react-native/showcase.js +++ b/website/src/react-native/showcase.js @@ -670,6 +670,13 @@ var apps = [ link: 'https://itunes.apple.com/us/app/okanagan-news-reader-for-viewing/id1049147148?mt=8', author: 'Levi Cabral', }, + { + name: 'passpoints', + icon: 'http://a5.mzstatic.com/eu/r30/Purple1/v4/8c/a0/72/8ca072ac-2304-1bf6-16e5-701e71921f42/icon350x350.png', + linkAppStore: 'https://itunes.apple.com/app/passpoints/id930988932', + linkPlayStore: 'https://play.google.com/store/apps/details?id=com.passpointsreactnative', + author: 'passpoints.de', + }, { name: 'Pimmr', icon: 'http://a2.mzstatic.com/eu/r30/Purple69/v4/99/da/0e/99da0ee6-bc87-e1a6-1d95-7027c78f50e1/icon175x175.jpeg', From 1e626027f58093297ee3b5b73baa9a645ff6a36e Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Sun, 22 May 2016 16:27:53 -0700 Subject: [PATCH 060/843] Rename `navigationState.children` to `navigationState.routes`. Summary: [Experimental API breaking changes] The notion of `parent` or `children` in navigaton is misleading. We have no intention to maintain or build the nested or hierarchical navigation states. To be clear, rename `navigationState.children` to `navigationState.route`. Reviewed By: ericvicenti Differential Revision: D3332115 fbshipit-source-id: c72ed08acaf030fb9c60abf22fb15cc0f44b3485 --- .../NavigationAnimatedExample.js | 2 +- .../NavigationBasicExample.js | 8 +-- .../NavigationCardStackExample.js | 2 +- .../NavigationCompositionExample.js | 10 +-- .../NavigationTabsExample.js | 4 +- .../UIExplorer/UIExplorerNavigationReducer.js | 13 +++- .../NavigationPropTypes.js | 2 +- .../NavigationStateUtils.js | 66 +++++++++---------- .../NavigationTypeDefinition.js | 2 +- .../Reducer/NavigationScenesReducer.js | 10 +-- .../Reducer/NavigationStackReducer.js | 12 ++-- .../Reducer/NavigationTabsReducer.js | 8 +-- .../__tests__/NavigationScenesReducer-test.js | 2 +- .../__tests__/NavigationStackReducer-test.js | 52 +++++++-------- .../__tests__/NavigationTabsReducer-test.js | 16 ++--- .../__tests__/NavigationStateUtils-test.js | 20 +++--- 16 files changed, 118 insertions(+), 111 deletions(-) diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js index 8764df483642c0..020408a5d0fabe 100644 --- a/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js +++ b/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js @@ -50,7 +50,7 @@ const ExampleReducer = NavigationReducer.StackReducer({ initialState: { key: 'AnimatedExampleStackKey', index: 0, - children: [ + routes: [ {key: 'First Route'}, ], }, diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationBasicExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationBasicExample.js index 9922227bfc1021..4424f09137fcd7 100644 --- a/Examples/UIExplorer/NavigationExperimental/NavigationBasicExample.js +++ b/Examples/UIExplorer/NavigationExperimental/NavigationBasicExample.js @@ -43,7 +43,7 @@ const ExampleReducer = NavigationReducer.StackReducer({ initialState: { key: 'BasicExampleStackKey', index: 0, - children: [ + routes: [ {key: 'First Route'}, ], }, @@ -59,12 +59,12 @@ const NavigationBasicExample = React.createClass({ return ( { - this._handleAction({ type: 'push', key: 'page #' + this.state.children.length }); + this._handleAction({ type: 'push', key: 'page #' + this.state.routes.length }); }} /> @@ -284,7 +284,7 @@ class ExampleMainView extends React.Component { _renderScene(): ReactElement { const {navigationState} = this.props; - const childState = navigationState.children[navigationState.index]; + const childState = navigationState.routes[navigationState.index]; return ( diff --git a/Examples/UIExplorer/UIExplorerNavigationReducer.js b/Examples/UIExplorer/UIExplorerNavigationReducer.js index 64a4f5f3481467..cec32ee91ba6ec 100644 --- a/Examples/UIExplorer/UIExplorerNavigationReducer.js +++ b/Examples/UIExplorer/UIExplorerNavigationReducer.js @@ -1,4 +1,11 @@ /** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * * The examples provided by Facebook are for non-commercial testing and * evaluation purposes only. * @@ -39,7 +46,7 @@ export type UIExplorerNavigationState = { const UIExplorerStackReducer = StackReducer({ getPushedReducerForAction: (action, lastState) => { if (action.type === 'UIExplorerExampleAction' && UIExplorerList.Modules[action.openExample]) { - if (lastState.children.find(child => child.key === action.openExample)) { + if (lastState.routes.find(route => route.key === action.openExample)) { // The example is already open, we should avoid pushing examples twice return null; } @@ -51,7 +58,7 @@ const UIExplorerStackReducer = StackReducer({ initialState: { key: 'UIExplorerMainStack', index: 0, - children: [ + routes: [ {key: 'AppList'}, ], }, @@ -70,7 +77,7 @@ function UIExplorerNavigationReducer(lastState: ?UIExplorerNavigationState, acti stack: { key: 'UIExplorerMainStack', index: 0, - children: [ + routes: [ { key: 'AppList', filter: action.filter, diff --git a/Libraries/NavigationExperimental/NavigationPropTypes.js b/Libraries/NavigationExperimental/NavigationPropTypes.js index 9389ac1d68a91a..6183078a3c1601 100644 --- a/Libraries/NavigationExperimental/NavigationPropTypes.js +++ b/Libraries/NavigationExperimental/NavigationPropTypes.js @@ -44,7 +44,7 @@ const navigationRoute = PropTypes.shape({ const navigationState = PropTypes.shape({ index: PropTypes.number.isRequired, key: PropTypes.string.isRequired, - children: PropTypes.arrayOf(navigationRoute), + routes: PropTypes.arrayOf(navigationRoute), }); /* NavigationLayout */ diff --git a/Libraries/NavigationExperimental/NavigationStateUtils.js b/Libraries/NavigationExperimental/NavigationStateUtils.js index 50470957ddc0e2..aa7bb26109e46c 100644 --- a/Libraries/NavigationExperimental/NavigationStateUtils.js +++ b/Libraries/NavigationExperimental/NavigationStateUtils.js @@ -18,34 +18,34 @@ import type { NavigationState, } from 'NavigationTypeDefinition'; -function getParent(state: NavigationRoute): ?NavigationState { +function getParent(state: NavigationState): ?NavigationState { if ( (state instanceof Object) && - (state.children instanceof Array) && - (state.children[0] !== undefined) && + (state.routes instanceof Array) && + (state.routes[0] !== undefined) && (typeof state.index === 'number') && - (state.children[state.index] !== undefined) + (state.routes[state.index] !== undefined) ) { return state; } return null; } -function get(state: NavigationRoute, key: string): ?NavigationRoute { +function get(state: NavigationState, key: string): ?NavigationRoute { const parentState = getParent(state); if (!parentState) { return null; } - const childState = parentState.children.find(child => child.key === key); + const childState = parentState.routes.find(child => child.key === key); return childState || null; } -function indexOf(state: NavigationRoute, key: string): ?number { +function indexOf(state: NavigationState, key: string): ?number { const parentState = getParent(state); if (!parentState) { return null; } - const index = parentState.children.map(child => child.key).indexOf(key); + const index = parentState.routes.map(child => child.key).indexOf(key); if (index === -1) { return null; } @@ -53,10 +53,10 @@ function indexOf(state: NavigationRoute, key: string): ?number { } function push(state: NavigationState, newChildState: NavigationRoute): NavigationState { - var lastChildren: Array = state.children; + var lastChildren: Array = state.routes; return { ...state, - children: [ + routes: [ ...lastChildren, newChildState, ], @@ -65,35 +65,35 @@ function push(state: NavigationState, newChildState: NavigationRoute): Navigatio } function pop(state: NavigationState): NavigationState { - const lastChildren = state.children; + const lastChildren = state.routes; return { ...state, - children: lastChildren.slice(0, lastChildren.length - 1), + routes: lastChildren.slice(0, lastChildren.length - 1), index: lastChildren.length - 2, }; } -function reset(state: NavigationRoute, nextChildren: ?Array, nextIndex: ?number): NavigationRoute { +function reset(state: NavigationState, nextChildren: ?Array, nextIndex: ?number): NavigationState { const parentState = getParent(state); if (!parentState) { return state; } - const children = nextChildren || parentState.children; + const routes = nextChildren || parentState.routes; const index = nextIndex == null ? parentState.index : nextIndex; - if (children === parentState.children && index === parentState.index) { + if (routes === parentState.routes && index === parentState.index) { return state; } return { ...parentState, - children, + routes, index, }; } -function set(state: ?NavigationRoute, key: string, nextChildren: Array, nextIndex: number): NavigationRoute { +function set(state: ?NavigationState, key: string, nextChildren: Array, nextIndex: number): NavigationState { if (!state) { return { - children: nextChildren, + routes: nextChildren, index: nextIndex, key, }; @@ -101,23 +101,23 @@ function set(state: ?NavigationRoute, key: string, nextChildren: Array child.key === key)); + const index = parentState.routes.indexOf(parentState.routes.find(child => child.key === key)); invariant( index !== -1, 'Cannot find child with matching key in this NavigationRoute' @@ -144,34 +144,34 @@ function jumpTo(state: NavigationRoute, key: string): NavigationRoute { }; } -function replaceAt(state: NavigationRoute, key: string, newState: NavigationRoute): NavigationRoute { +function replaceAt(state: NavigationState, key: string, newState: NavigationState): NavigationState { const parentState = getParent(state); if (!parentState) { return state; } - const children = [...parentState.children]; - const index = parentState.children.indexOf(parentState.children.find(child => child.key === key)); + const routes = [...parentState.routes]; + const index = parentState.routes.indexOf(parentState.routes.find(child => child.key === key)); invariant( index !== -1, 'Cannot find child with matching key in this NavigationRoute' ); - children[index] = newState; + routes[index] = newState; return { ...parentState, - children, + routes, }; } -function replaceAtIndex(state: NavigationRoute, index: number, newState: NavigationRoute): NavigationRoute { +function replaceAtIndex(state: NavigationState, index: number, newState: NavigationState): NavigationState { const parentState = getParent(state); if (!parentState) { return state; } - const children = [...parentState.children]; - children[index] = newState; + const routes = [...parentState.routes]; + routes[index] = newState; return { ...parentState, - children, + routes, }; } diff --git a/Libraries/NavigationExperimental/NavigationTypeDefinition.js b/Libraries/NavigationExperimental/NavigationTypeDefinition.js index 42ef78d5a69772..638ce3b95afa9e 100644 --- a/Libraries/NavigationExperimental/NavigationTypeDefinition.js +++ b/Libraries/NavigationExperimental/NavigationTypeDefinition.js @@ -28,7 +28,7 @@ export type NavigationRoute = { export type NavigationState = { key: string, index: number, - children: Array, + routes: Array, }; export type NavigationAction = any; diff --git a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js index 80d791cd7e18fe..98c5e76a3840bf 100644 --- a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js +++ b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js @@ -91,7 +91,7 @@ function NavigationScenesReducer( }); const nextKeys = new Set(); - nextState.children.forEach((route, index) => { + nextState.routes.forEach((route, index) => { const key = SCENE_KEY_PREFIX + route.key; const scene = { index, @@ -101,8 +101,8 @@ function NavigationScenesReducer( }; invariant( !nextKeys.has(key), - `navigationState.children[${index}].key "${key}" conflicts with` + - 'another child!' + `navigationState.routes[${index}].key "${key}" conflicts with` + + 'another route!' ); nextKeys.add(key); @@ -115,8 +115,8 @@ function NavigationScenesReducer( }); if (prevState) { - // Look at the previous children and classify any removed scenes as `stale`. - prevState.children.forEach((route: NavigationRoute, index) => { + // Look at the previous routes and classify any removed scenes as `stale`. + prevState.routes.forEach((route: NavigationRoute, index) => { const key = SCENE_KEY_PREFIX + route.key; if (freshScenes.has(key)) { return; diff --git a/Libraries/NavigationExperimental/Reducer/NavigationStackReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationStackReducer.js index 681b0acc0d0511..9c884d55eabbd2 100644 --- a/Libraries/NavigationExperimental/Reducer/NavigationStackReducer.js +++ b/Libraries/NavigationExperimental/Reducer/NavigationStackReducer.js @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule NavigationStackReducer - * @flow + * @flow-broken */ 'use strict'; @@ -29,7 +29,7 @@ export type StackReducerConfig = { * Must be a NavigationState: * * { - * children: [ + * routes: [ * {key: 'subState0'}, * {key: 'subState1'}, * ], @@ -66,15 +66,15 @@ function NavigationStackReducer({initialState, getReducerForState, getPushedRedu return lastState; } - const activeSubState = lastParentState.children[lastParentState.index]; + const activeSubState = lastParentState.routes[lastParentState.index]; const activeSubReducer = getReducerForStateWithDefault(activeSubState); const nextActiveState = activeSubReducer(activeSubState, action); if (nextActiveState !== activeSubState) { - const nextChildren = [...lastParentState.children]; + const nextChildren = [...lastParentState.routes]; nextChildren[lastParentState.index] = nextActiveState; return { ...lastParentState, - children: nextChildren, + routes: nextChildren, }; } @@ -89,7 +89,7 @@ function NavigationStackReducer({initialState, getReducerForState, getPushedRedu switch (action.type) { case 'back': case 'BackAction': - if (lastParentState.index === 0 || lastParentState.children.length === 1) { + if (lastParentState.index === 0 || lastParentState.routes.length === 1) { return lastParentState; } return NavigationStateUtils.pop(lastParentState); diff --git a/Libraries/NavigationExperimental/Reducer/NavigationTabsReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationTabsReducer.js index b4241f43fa00c0..5e60cf8660479b 100644 --- a/Libraries/NavigationExperimental/Reducer/NavigationTabsReducer.js +++ b/Libraries/NavigationExperimental/Reducer/NavigationTabsReducer.js @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule NavigationTabsReducer - * @flow + * @flow-broken */ 'use strict'; @@ -44,7 +44,7 @@ function NavigationTabsReducer({key, initialIndex, tabReducers}: TabsReducerConf return function(lastNavState: ?NavigationRoute, action: ?any): NavigationRoute { if (!lastNavState) { lastNavState = { - children: tabReducers.map(reducer => reducer(null, null)), + routes: tabReducers.map(reducer => reducer(null, null)), index: initialIndex || 0, key, }; @@ -68,10 +68,10 @@ function NavigationTabsReducer({key, initialIndex, tabReducers}: TabsReducerConf return lastParentNavState; } const parentState = NavigationStateUtils.getParent(navState); - const tabState = parentState && parentState.children[tabIndex]; + const tabState = parentState && parentState.routes[tabIndex]; const nextTabState = tabReducer(tabState, tabAction); if (nextTabState && tabState !== nextTabState) { - const tabs = parentState && parentState.children || []; + const tabs = parentState && parentState.routes || []; tabs[tabIndex] = nextTabState; return { ...lastParentNavState, diff --git a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js index 6d543bdde7dfc1..1788d0dd563f2c 100644 --- a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js +++ b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js @@ -19,7 +19,7 @@ const NavigationScenesReducer = require('NavigationScenesReducer'); function testTransition(states) { const routes = states.map(keys => { return { - children: keys.map(key => { + routes: keys.map(key => { return { key }; }), }; diff --git a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationStackReducer-test.js b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationStackReducer-test.js index 840dc0d0d6d188..23828086f61495 100644 --- a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationStackReducer-test.js +++ b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationStackReducer-test.js @@ -23,7 +23,7 @@ describe('NavigationStackReducer', () => { it('provides default/initial state', () => { const initialState = { - children: [ + routes: [ {key: 'a'}, ], index: 0, @@ -48,7 +48,7 @@ describe('NavigationStackReducer', () => { }, getReducerForState: (state) => () => state, initialState: { - children: [ + routes: [ {key: 'first'}, ], index: 0, @@ -56,15 +56,15 @@ describe('NavigationStackReducer', () => { } }); const state1 = reducer(null, {type: 'default'}); - expect(state1.children.length).toBe(1); - expect(state1.children[0].key).toBe('first'); + expect(state1.routes.length).toBe(1); + expect(state1.routes[0].key).toBe('first'); expect(state1.index).toBe(0); const action = {type: 'TestPushAction', testValue: 'second'}; const state2 = reducer(state1, action); - expect(state2.children.length).toBe(2); - expect(state2.children[0].key).toBe('first'); - expect(state2.children[1].key).toBe('second'); + expect(state2.routes.length).toBe(2); + expect(state2.routes[0].key).toBe('first'); + expect(state2.routes[1].key).toBe('second'); expect(state2.index).toBe(1); }); @@ -78,7 +78,7 @@ describe('NavigationStackReducer', () => { }, getReducerForState: (state) => () => state, initialState: { - children: [ + routes: [ {key: 'a'}, {key: 'b'}, ], @@ -88,15 +88,15 @@ describe('NavigationStackReducer', () => { }); const state1 = reducer(null, {type: 'MyDefaultAction'}); - expect(state1.children[0].key).toBe('a'); - expect(state1.children[1].key).toBe('b'); - expect(state1.children.length).toBe(2); + expect(state1.routes[0].key).toBe('a'); + expect(state1.routes[1].key).toBe('b'); + expect(state1.routes.length).toBe(2); expect(state1.index).toBe(1); expect(state1.key).toBe('myStack'); const state2 = reducer(state1, NavigationRootContainer.getBackAction()); - expect(state2.children[0].key).toBe('a'); - expect(state2.children.length).toBe(1); + expect(state2.routes[0].key).toBe('a'); + expect(state2.routes.length).toBe(1); expect(state2.index).toBe(0); const state3 = reducer(state2, NavigationRootContainer.getBackAction()); @@ -107,7 +107,7 @@ describe('NavigationStackReducer', () => { const subReducer = NavigationStackReducer({ getPushedReducerForAction: () => {}, initialState: { - children: [ + routes: [ {key: 'first'}, {key: 'second'}, ], @@ -131,7 +131,7 @@ describe('NavigationStackReducer', () => { return () => state; }, initialState: { - children: [ + routes: [ {key: 'a'}, ], index: 0, @@ -141,18 +141,18 @@ describe('NavigationStackReducer', () => { const state1 = reducer(null, {type: 'MyDefaultAction'}); const state2 = reducer(state1, {type: 'TestPushAction'}); - expect(state2.children.length).toBe(2); - expect(state2.children[0].key).toBe('a'); - expect(state2.children[1].key).toBe('myInnerStack'); - expect(state2.children[1].children.length).toBe(2); - expect(state2.children[1].children[0].key).toBe('first'); - expect(state2.children[1].children[1].key).toBe('second'); + expect(state2.routes.length).toBe(2); + expect(state2.routes[0].key).toBe('a'); + expect(state2.routes[1].key).toBe('myInnerStack'); + expect(state2.routes[1].routes.length).toBe(2); + expect(state2.routes[1].routes[0].key).toBe('first'); + expect(state2.routes[1].routes[1].key).toBe('second'); const state3 = reducer(state2, NavigationRootContainer.getBackAction()); - expect(state3.children.length).toBe(2); - expect(state3.children[0].key).toBe('a'); - expect(state3.children[1].key).toBe('myInnerStack'); - expect(state3.children[1].children.length).toBe(1); - expect(state3.children[1].children[0].key).toBe('first'); + expect(state3.routes.length).toBe(2); + expect(state3.routes[0].key).toBe('a'); + expect(state3.routes[1].key).toBe('myInnerStack'); + expect(state3.routes[1].routes.length).toBe(1); + expect(state3.routes[1].routes[0].key).toBe('first'); }); }); diff --git a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationTabsReducer-test.js b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationTabsReducer-test.js index bb91b68fd768af..f97379cb738b2d 100644 --- a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationTabsReducer-test.js +++ b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationTabsReducer-test.js @@ -35,10 +35,10 @@ describe('NavigationTabsReducer', () => { let navState = reducer(); - expect(navState.children[0]).toBe('a'); - expect(navState.children[1]).toBe('b'); - expect(navState.children[2]).toBe('c'); - expect(navState.children.length).toBe(3); + expect(navState.routes[0]).toBe('a'); + expect(navState.routes[1]).toBe('b'); + expect(navState.routes[2]).toBe('c'); + expect(navState.routes.length).toBe(3); expect(navState.index).toBe(1); navState = reducer( @@ -46,10 +46,10 @@ describe('NavigationTabsReducer', () => { JumpToAction(2) ); - expect(navState.children[0]).toEqual('a'); - expect(navState.children[1]).toEqual('b'); - expect(navState.children[2]).toEqual('c'); - expect(navState.children.length).toBe(3); + expect(navState.routes[0]).toEqual('a'); + expect(navState.routes[1]).toEqual('b'); + expect(navState.routes[2]).toEqual('c'); + expect(navState.routes.length).toBe(3); expect(navState.index).toBe(2); }); diff --git a/Libraries/NavigationExperimental/__tests__/NavigationStateUtils-test.js b/Libraries/NavigationExperimental/__tests__/NavigationStateUtils-test.js index 97cd5d220cf481..cccd07fb4eabfa 100644 --- a/Libraries/NavigationExperimental/__tests__/NavigationStateUtils-test.js +++ b/Libraries/NavigationExperimental/__tests__/NavigationStateUtils-test.js @@ -16,18 +16,18 @@ jest var NavigationStateUtils = require('NavigationStateUtils'); var VALID_PARENT_STATES = [ - {children: ['a','b'], index: 0}, - {children: [{key: 'a'},{key: 'b', foo: 123}], index: 1}, - {children: [{key: 'a'},{key: 'b'}], index: 0}, - {children: [{key: 'a'},{key: 'b'}], index: 2}, + {routes: ['a','b'], index: 0}, + {routes: [{key: 'a'},{key: 'b', foo: 123}], index: 1}, + {routes: [{key: 'a'},{key: 'b'}], index: 0}, + {routes: [{key: 'a'},{key: 'b'}], index: 2}, ]; var INVALID_PARENT_STATES = [ 'foo', {}, - {children: [{key: 'a'}], index: 4}, - {children: [{key: 'a'}], index: -1}, - {children: [{key: 'a'}]}, - {children: {key: 'foo'}}, + {routes: [{key: 'a'}], index: 4}, + {routes: [{key: 'a'}], index: -1}, + {routes: [{key: 'a'}]}, + {routes: {key: 'foo'}}, 12, null, undefined, @@ -47,9 +47,9 @@ describe('NavigationStateUtils', () => { } }); - it('can get children', () => { + it('can get routes', () => { var fooState = {key: 'foo'}; - var navState = {children: [{key: 'foobar'}, fooState], index: 0}; + var navState = {routes: [{key: 'foobar'}, fooState], index: 0}; expect(NavigationStateUtils.get(navState, 'foo')).toBe(fooState); expect(NavigationStateUtils.get(navState, 'missing')).toBe(null); }); From c8f39c3d2c26963792a0e90e8e28db394b7d76f4 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 23 May 2016 03:31:51 -0700 Subject: [PATCH 061/843] Added small decoded image cache to prevent images flashing when component is reloaded Summary: In RN we cache image data after loading/downloading an image, however the data we store is the compressed image data, and we decode this asynchronously each time it is displayed. This can lead to a slight flicker when reloading image components because the decoded image is discarded and then re-decoded. This diff adds a small (5MB) cache for decoded images so that images that are currently on screen shouldn't flicker any more if the component is reloaded. Reviewed By: bnham Differential Revision: D3305161 fbshipit-source-id: 9969012f576784dd6f37d9386cbced2df00c3e07 --- Libraries/Image/RCTImageLoader.m | 63 +++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 75d1292e4d0819..b576913ac8c37a 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -23,6 +23,35 @@ static NSString *const RCTErrorInvalidURI = @"E_INVALID_URI"; static NSString *const RCTErrorPrefetchFailure = @"E_PREFETCH_FAILURE"; +static const NSUInteger RCTMaxCachableDecodedImageSizeInBytes = 1048576; // 1MB + +static NSCache *RCTGetDecodedImageCache(void) +{ + static NSCache *cache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cache = [NSCache new]; + cache.totalCostLimit = 5 * 1024 * 1024; // 5MB + + // Clear cache in the event of a memory warning, or if app enters background + [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(__unused NSNotification *note) { + [cache removeAllObjects]; + }]; + [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillResignActiveNotification object:nil queue:nil usingBlock:^(__unused NSNotification *note) { + [cache removeAllObjects]; + }]; + }); + return cache; + +} + +static NSString *RCTCacheKeyForImage(NSString *imageTag, CGSize size, + CGFloat scale, RCTResizeMode resizeMode) +{ + return [NSString stringWithFormat:@"%@|%f|%f|%f|%zd", + imageTag, size.width, size.height, scale, resizeMode]; +} + @implementation UIImage (React) - (CAKeyframeAnimation *)reactKeyframeAnimation @@ -476,16 +505,40 @@ - (RCTImageLoaderCancellationBlock)loadImageWithoutClipping:(NSString *)imageTag __block void(^cancelLoad)(void) = nil; __weak RCTImageLoader *weakSelf = self; - void (^completionHandler)(NSError *error, id imageOrData) = ^(NSError *error, id imageOrData) { + // Check decoded image cache + NSString *cacheKey = RCTCacheKeyForImage(imageTag, size, scale, resizeMode); + { + UIImage *image = [RCTGetDecodedImageCache() objectForKey:cacheKey]; + if (image) { + // Most loaders do not return on the main thread, so caller is probably not + // expecting it, and may do expensive post-processing in the callback + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + completionBlock(nil, image); + }); + return ^{}; + } + } + + RCTImageLoaderCompletionBlock cacheResultHandler = ^(NSError *error, UIImage *image) { + if (image) { + CGFloat bytes = image.size.width * image.size.height * image.scale * image.scale * 4; + if (bytes <= RCTMaxCachableDecodedImageSizeInBytes) { + [RCTGetDecodedImageCache() setObject:image forKey:cacheKey cost:bytes]; + } + } + completionBlock(error, image); + }; + + void (^completionHandler)(NSError *, id) = ^(NSError *error, id imageOrData) { if (!cancelled) { if (!imageOrData || [imageOrData isKindOfClass:[UIImage class]]) { - completionBlock(error, imageOrData); + cacheResultHandler(error, imageOrData); } else { cancelLoad = [weakSelf decodeImageDataWithoutClipping:imageOrData size:size scale:scale resizeMode:resizeMode - completionBlock:completionBlock] ?: ^{}; + completionBlock:cacheResultHandler]; } } }; @@ -495,7 +548,7 @@ - (RCTImageLoaderCancellationBlock)loadImageWithoutClipping:(NSString *)imageTag scale:scale resizeMode:resizeMode progressBlock:progressHandler - completionBlock:completionHandler] ?: ^{}; + completionBlock:completionHandler]; return ^{ if (cancelLoad) { cancelLoad(); @@ -551,7 +604,7 @@ - (RCTImageLoaderCancellationBlock)decodeImageDataWithoutClipping:(NSData *)data size:size scale:scale resizeMode:resizeMode - completionHandler:completionHandler]; + completionHandler:completionHandler] ?: ^{}; } else { if (!_URLCacheQueue) { From 913b4ccee492780ae180657094c261805455deea Mon Sep 17 00:00:00 2001 From: Nathan Azaria Date: Mon, 23 May 2016 04:36:22 -0700 Subject: [PATCH 062/843] Changed RUNNING_ON_CI to CI_USE_PACKAGER environment variable. Reviewed By: javache Differential Revision: D3327726 fbshipit-source-id: 6293b71a2ca7783b089f16fe2089c8893e93a621 --- .../xcshareddata/xcschemes/UIExplorer.xcscheme | 9 ++++++++- Examples/UIExplorer/UIExplorer/AppDelegate.m | 8 ++++---- .../UIExplorerIntegrationTests/RCTLoggingTests.m | 16 +++++++++------- Libraries/RCTTest/RCTTestRunner.m | 13 +++++++------ 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme index 52560f0d68562e..4ed46558bb15ca 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme @@ -54,7 +54,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO"> @@ -86,6 +86,13 @@ ReferencedContainer = "container:UIExplorer.xcodeproj"> + + + + diff --git a/Examples/UIExplorer/UIExplorer/AppDelegate.m b/Examples/UIExplorer/UIExplorer/AppDelegate.m index 39bb963e2c0fca..f7c5bf49b8b7a7 100644 --- a/Examples/UIExplorer/UIExplorer/AppDelegate.m +++ b/Examples/UIExplorer/UIExplorer/AppDelegate.m @@ -28,7 +28,7 @@ - (BOOL)application:(__unused UIApplication *)application didFinishLaunchingWith { _bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; - + // Appetizer.io params check NSDictionary *initProps = nil; NSString *_routeUri = [[NSUserDefaults standardUserDefaults] stringForKey:@"route"]; @@ -36,7 +36,7 @@ - (BOOL)application:(__unused UIApplication *)application didFinishLaunchingWith initProps = @{@"exampleFromAppetizeParams": [NSString stringWithFormat:@"rnuiexplorer://example/%@Example", _routeUri]}; } - + RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:_bridge moduleName:@"UIExplorerApp" initialProperties:initProps]; @@ -81,9 +81,9 @@ - (NSURL *)sourceURLForBridge:(__unused RCTBridge *)bridge // sourceURL = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; - #if RUNNING_ON_CI + if (!getenv("CI_USE_PACKAGER")) { sourceURL = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; - #endif + } return sourceURL; } diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/RCTLoggingTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/RCTLoggingTests.m index 0f34cfb26e54a4..b46d2cae91ffa9 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/RCTLoggingTests.m +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/RCTLoggingTests.m @@ -34,13 +34,15 @@ @implementation RCTLoggingTests - (void)setUp { -#if RUNNING_ON_CI - NSURL *scriptURL = [[NSBundle bundleForClass:[RCTBridge class]] URLForResource:@"main" withExtension:@"jsbundle"]; - RCTAssert(scriptURL != nil, @"Could not locate main.jsBundle"); -#else - NSString *app = @"Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestsApp"; - NSURL *scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.bundle?platform=ios&dev=true", app]]; -#endif + NSURL *scriptURL; + if (getenv("CI_USE_PACKAGER")) { + NSString *app = @"Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestsApp"; + scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.bundle?platform=ios&dev=true", app]]; + RCTAssert(scriptURL != nil, @"No scriptURL set"); + } else { + scriptURL = [[NSBundle bundleForClass:[RCTBridge class]] URLForResource:@"main" withExtension:@"jsbundle"]; + RCTAssert(scriptURL != nil, @"Could not locate main.jsBundle"); + } _bridge = [[RCTBridge alloc] initWithBundleURL:scriptURL moduleProvider:NULL launchOptions:nil]; NSDate *date = [NSDate dateWithTimeIntervalSinceNow:5]; diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 6203d0f1dbf3bc..d6d8f59e4d7b79 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -45,12 +45,13 @@ - (instancetype)initWithApp:(NSString *)app _testController.referenceImagesDirectory = referenceDirectory; _moduleProvider = [block copy]; -#if RUNNING_ON_CI - _scriptURL = [[NSBundle bundleForClass:[RCTBridge class]] URLForResource:@"main" withExtension:@"jsbundle"]; - RCTAssert(_scriptURL != nil, @"Could not locate main.jsBundle"); -#else - _scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.bundle?platform=ios&dev=true", app]]; -#endif + if (getenv("CI_USE_PACKAGER")) { + _scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.bundle?platform=ios&dev=true", app]]; + RCTAssert(_scriptURL != nil, @"No scriptURL set"); + } else { + _scriptURL = [[NSBundle bundleForClass:[RCTBridge class]] URLForResource:@"main" withExtension:@"jsbundle"]; + RCTAssert(_scriptURL != nil, @"Could not locate main.jsBundle"); + } } return self; } From 4378ecb8e174ebcc63e5dc8204f55a9015367a45 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Mon, 23 May 2016 04:40:20 -0700 Subject: [PATCH 063/843] Fixes NPE when NetworkingModule runs onCatalystInstanceDestroy Reviewed By: andreicoman11 Differential Revision: D3328278 fbshipit-source-id: 7cf43a4564b2b799b13307f8604eb48e5c512379 --- .../com/facebook/react/common/network/OkHttpCallUtil.java | 4 ++++ .../com/facebook/react/modules/network/NetworkingModule.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java b/ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java index 4da3a5e9b80349..7fa7d967ae30dc 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java +++ b/ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java @@ -32,4 +32,8 @@ public static void cancelTag(OkHttpClient client, Object tag) { } } } + + public static void cancelAll(OkHttpClient client) { + client.dispatcher().cancelAll(); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java index 93ab7f7b0b5848..68e6e93b012ce3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java @@ -138,7 +138,7 @@ public String getName() { @Override public void onCatalystInstanceDestroy() { mShuttingDown = true; - OkHttpCallUtil.cancelTag(mClient, null); + OkHttpCallUtil.cancelAll(mClient); mCookieHandler.destroy(); mCookieJarContainer.removeCookieJar(); From 8f9a3aa0e2c588e25f63938a0b46e4a50787fba9 Mon Sep 17 00:00:00 2001 From: "MIYOKAWA, Nobuyoshi" Date: Mon, 23 May 2016 04:49:47 -0700 Subject: [PATCH 064/843] Add .iml to ignore target. Summary: Sorry for trivial one, but I feel no need to share '.iml' in a project, so it should be ignored. Closes https://github.com/facebook/react-native/pull/7689 Differential Revision: D3334367 fbshipit-source-id: 2a68aa93084a51d0b3d24427c71bc7c12e41ed78 --- local-cli/generator/templates/_gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/local-cli/generator/templates/_gitignore b/local-cli/generator/templates/_gitignore index 42c9490e508a08..eb1535e41e3df7 100644 --- a/local-cli/generator/templates/_gitignore +++ b/local-cli/generator/templates/_gitignore @@ -24,6 +24,7 @@ project.xcworkspace # Android/IJ # +*.iml .idea .gradle local.properties From 4880309e72ac2b1f09b77d5411dbcec1360580d9 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Mon, 23 May 2016 04:51:25 -0700 Subject: [PATCH 065/843] Prevent Systrace from including React in the preloaded modules section Reviewed By: tadeuzagallo Differential Revision: D3304923 fbshipit-source-id: 25dea5b57002578ccb3e38e8946c6b73db008ed0 --- Libraries/Utilities/Systrace.js | 59 ++++++++++++++------------------- 1 file changed, 24 insertions(+), 35 deletions(-) diff --git a/Libraries/Utilities/Systrace.js b/Libraries/Utilities/Systrace.js index 7ed2dbeddea7ce..8dc207e1e2c74a 100644 --- a/Libraries/Utilities/Systrace.js +++ b/Libraries/Utilities/Systrace.js @@ -23,55 +23,44 @@ type RelayProfiler = { ): void, }; -var TRACE_TAG_REACT_APPS = 1 << 17; -var TRACE_TAG_JSC_CALLS = 1 << 27; - -var _enabled = false; -var _asyncCookie = 0; -var _ReactDebugTool = null; -var _ReactComponentTreeDevtool = null; -function ReactDebugTool() { - if (!_ReactDebugTool) { - _ReactDebugTool = require('ReactDebugTool'); - } - return _ReactDebugTool; -} -function ReactComponentTreeDevtool() { - if (!_ReactComponentTreeDevtool) { - _ReactComponentTreeDevtool = require('ReactComponentTreeDevtool'); - } - return _ReactComponentTreeDevtool; -} +/* eslint no-bitwise: 0 */ +const TRACE_TAG_REACT_APPS = 1 << 17; +const TRACE_TAG_JSC_CALLS = 1 << 27; -var ReactSystraceDevtool = { +let _enabled = false; +let _asyncCookie = 0; + +const ReactSystraceDevtool = __DEV__ ? { onBeginReconcilerTimer(debugID, timerType) { - var displayName = ReactComponentTreeDevtool().getDisplayName(debugID); + const displayName = require('ReactComponentTreeDevtool').getDisplayName(debugID); Systrace.beginEvent(`ReactReconciler.${timerType}(${displayName})`); }, onEndReconcilerTimer(debugID, timerType) { Systrace.endEvent(); }, onBeginLifeCycleTimer(debugID, timerType) { - var displayName = ReactComponentTreeDevtool().getDisplayName(debugID); + const displayName = require('ReactComponentTreeDevtool').getDisplayName(debugID); Systrace.beginEvent(`${displayName}.${timerType}()`); }, onEndLifeCycleTimer(debugID, timerType) { Systrace.endEvent(); }, -}; +} : null; -var Systrace = { +const Systrace = { setEnabled(enabled: boolean) { if (_enabled !== enabled) { - if (enabled) { - global.nativeTraceBeginLegacy && global.nativeTraceBeginLegacy(TRACE_TAG_JSC_CALLS); - ReactDebugTool().addDevtool(ReactSystraceDevtool); - } else { - global.nativeTraceEndLegacy && global.nativeTraceEndLegacy(TRACE_TAG_JSC_CALLS); - ReactDebugTool().removeDevtool(ReactSystraceDevtool); + if (__DEV__) { + if (enabled) { + global.nativeTraceBeginLegacy && global.nativeTraceBeginLegacy(TRACE_TAG_JSC_CALLS); + require('ReactDebugTool').addDevtool(ReactSystraceDevtool); + } else { + global.nativeTraceEndLegacy && global.nativeTraceEndLegacy(TRACE_TAG_JSC_CALLS); + require('ReactDebugTool').removeDevtool(ReactSystraceDevtool); + } } + _enabled = enabled; } - _enabled = enabled; }, /** @@ -97,7 +86,7 @@ var Systrace = { * the returned cookie variable should be used as input into the endAsyncEvent call to end the profile **/ beginAsyncEvent(profileName?: any): any { - var cookie = _asyncCookie; + const cookie = _asyncCookie; if (_enabled) { _asyncCookie++; profileName = typeof profileName === 'function' ? @@ -133,7 +122,7 @@ var Systrace = { **/ attachToRelayProfiler(relayProfiler: RelayProfiler) { relayProfiler.attachProfileHandler('*', (name) => { - var cookie = Systrace.beginAsyncEvent(name); + const cookie = Systrace.beginAsyncEvent(name); return () => { Systrace.endAsyncEvent(name, cookie); }; @@ -191,14 +180,14 @@ var Systrace = { return func; } - var profileName = `${objName}.${fnName}`; + const profileName = `${objName}.${fnName}`; return function() { if (!_enabled) { return func.apply(this, arguments); } Systrace.beginEvent(profileName); - var ret = func.apply(this, arguments); + const ret = func.apply(this, arguments); Systrace.endEvent(); return ret; }; From 69bf0bd8c13b7bf0a891ea4f391a93ffed5d2e6b Mon Sep 17 00:00:00 2001 From: James Ide Date: Mon, 23 May 2016 05:08:28 -0700 Subject: [PATCH 066/843] Run CircleCI tests on the latest version of Node Summary: Travis CI runs Node 4 (LTS), so make CircleCI run Node `` which is Node 6. Closes https://github.com/facebook/react-native/pull/7542 Differential Revision: D3334397 fbshipit-source-id: 15a758011626fca4efdc4c1b36891a4d73c52e94 --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index ad2d1bb998f207..8717c9f4139ff6 100644 --- a/circle.yml +++ b/circle.yml @@ -4,7 +4,7 @@ general: - gh-pages # list of branches to ignore machine: node: - version: 5.6.0 + version: 6.2.0 environment: PATH: "~/$CIRCLE_PROJECT_REPONAME/gradle-2.9/bin:/home/ubuntu/buck/bin:$PATH" TERM: "dumb" From bb5aede6e320084b1aee04f8b61089e5578fd4da Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Mon, 23 May 2016 05:42:37 -0700 Subject: [PATCH 067/843] Fix RefreshControl race condition Summary: Improved version of #7317. `setRefreshing` and `setProgressViewOffset` needs to be called after the view has been layed out. Instead of using `post` to do that we update the `refreshing` and `progressViewOffset` values in the first call to `onLayout`. I also noticed that `progressViewOffset` default value wasn't exactly the same as when not calling `setProgressViewOffset` at all. Tweaked the values to match android defaults. **Test plan (required)** Make sure the integration test passes, In UIExplorer: test RefreshControl with `refreshing = true` initially, test `progressViewOffset`. Closes https://github.com/facebook/react-native/pull/7683 Differential Revision: D3334426 fbshipit-source-id: ddd63a5e9a6afe2b8b7fe6a25e875a40f4e888c6 --- .../swiperefresh/ReactSwipeRefreshLayout.java | 47 +++++++++++++++++++ .../SwipeRefreshLayoutManager.java | 26 ++-------- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.java b/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.java index f458c0ab06b914..d16e58de1b8e3c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.java @@ -14,16 +14,63 @@ import com.facebook.react.bridge.ReactContext; import com.facebook.react.uimanager.events.NativeGestureUtil; +import com.facebook.react.uimanager.PixelUtil; /** * Basic extension of {@link SwipeRefreshLayout} with ReactNative-specific functionality. */ public class ReactSwipeRefreshLayout extends SwipeRefreshLayout { + private static final float DEFAULT_CIRCLE_TARGET = 64; + + private boolean mDidLayout = false; + + private boolean mRefreshing = false; + private float mProgressViewOffset = 0; + + public ReactSwipeRefreshLayout(ReactContext reactContext) { super(reactContext); } + @Override + public void setRefreshing(boolean refreshing) { + mRefreshing = refreshing; + + // `setRefreshing` must be called after the initial layout otherwise it + // doesn't work when mounting the component with `refreshing = true`. + // Known Android issue: https://code.google.com/p/android/issues/detail?id=77712 + if (mDidLayout) { + super.setRefreshing(refreshing); + } + } + + public void setProgressViewOffset(float offset) { + mProgressViewOffset = offset; + + // The view must be measured before calling `getProgressCircleDiameter` so + // don't do it before the initial layout. + if (mDidLayout) { + int diameter = getProgressCircleDiameter(); + int start = Math.round(PixelUtil.toPixelFromDIP(offset)) - diameter; + int end = Math.round(PixelUtil.toPixelFromDIP(offset + DEFAULT_CIRCLE_TARGET) - diameter); + setProgressViewOffset(false, start, end); + } + } + + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + if (!mDidLayout) { + mDidLayout = true; + + // Update values that must be set after initial layout. + setProgressViewOffset(mProgressViewOffset); + setRefreshing(mRefreshing); + } + } + @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (super.onInterceptTouchEvent(ev)) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.java index 5db04a1a77f39c..1ae2bacf5a36b9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.java @@ -20,7 +20,6 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.common.MapBuilder; import com.facebook.react.common.SystemClock; -import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.ViewGroupManager; @@ -33,8 +32,6 @@ */ public class SwipeRefreshLayoutManager extends ViewGroupManager { - public static final float REFRESH_TRIGGER_DISTANCE = 48; - @Override protected ReactSwipeRefreshLayout createViewInstance(ThemedReactContext reactContext) { return new ReactSwipeRefreshLayout(reactContext); @@ -74,30 +71,13 @@ public void setSize(ReactSwipeRefreshLayout view, int size) { } @ReactProp(name = "refreshing") - public void setRefreshing(final ReactSwipeRefreshLayout view, final boolean refreshing) { - // Use `post` otherwise the control won't start refreshing if refreshing is true when - // the component gets mounted. - view.post(new Runnable() { - @Override - public void run() { - view.setRefreshing(refreshing); - } - }); + public void setRefreshing(ReactSwipeRefreshLayout view, boolean refreshing) { + view.setRefreshing(refreshing); } @ReactProp(name = "progressViewOffset", defaultFloat = 0) public void setProgressViewOffset(final ReactSwipeRefreshLayout view, final float offset) { - // Use `post` to get progress circle diameter properly - // Otherwise returns 0 - view.post(new Runnable() { - @Override - public void run() { - int diameter = view.getProgressCircleDiameter(); - int start = Math.round(PixelUtil.toPixelFromDIP(offset)) - diameter; - int end = Math.round(PixelUtil.toPixelFromDIP(offset + REFRESH_TRIGGER_DISTANCE)); - view.setProgressViewOffset(false, start, end); - } - }); + view.setProgressViewOffset(offset); } @Override From a3f50ec8a05bd81fb21aa44ec3585afb298a6f19 Mon Sep 17 00:00:00 2001 From: Shiran Ginige Date: Mon, 23 May 2016 06:10:35 -0700 Subject: [PATCH 068/843] Fixing the issue on packager (failing with siblings error) Summary: The packager was failing with below error after a fresh clone npm ERR! peerinvalid The package react@15.1.0 does not satisfy its siblings' peerDependencies requirements! Changed the dependancy for **^react@15.1.0-alpha.1** to **^react@15.1.0** and it was fixed. Closes https://github.com/facebook/react-native/pull/7692 Differential Revision: D3334425 fbshipit-source-id: ee86fc2e3c6f19b2430658d91d0a88c50bcf11de --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ddf4d240e2648d..e63c04132af28f 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "react-native": "local-cli/wrong-react-native.js" }, "peerDependencies": { - "react": "15.1.0-alpha.1" + "react": "15.1.0" }, "dependencies": { "absolute-path": "^0.0.0", @@ -191,7 +191,7 @@ "flow-bin": "^0.25.0", "jest": "12.1.1", "portfinder": "0.4.0", - "react": "^15.1.0-alpha.1", + "react": "15.1.0", "shelljs": "0.6.0" } } From 5d06402b4a54ba67f4a7758855c9185a9b50216d Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Mon, 23 May 2016 06:44:37 -0700 Subject: [PATCH 069/843] Made npm deployment to be independent of git tags cache Summary: Fixes npm deployment script as in https://circleci.com/gh/facebook/react-native/6745. Example situation: - admin is ready to release 0.26.0 - admin adds tag v0.26.0 and pushes to origin - Circle CI build fails because of code error - admin cherry-picks a fix and tags v0.26.0 again and pushes to origin - Circle does not update local tag v0.26.0 because it already has it cached, compiles the code but fails to deploy to npm because v0.26.0 is not on branch HEAD from Circle point of view The previous version of the script was checking the tags on current commit as well but it used the local branch tags. This change fetches tags from `origin` fresh and allows us to redeploy the same version multiple times. Closes https://github.com/facebook/react-native/pull/7619 Differential Revision: D3334469 fbshipit-source-id: b423fc19516dc4f5f7f2605224e62b8a378c8a7d --- scripts/publish-npm.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/scripts/publish-npm.js b/scripts/publish-npm.js index ba5efc2d394c1d..3986a9924d9c44 100644 --- a/scripts/publish-npm.js +++ b/scripts/publish-npm.js @@ -60,12 +60,17 @@ if (buildBranch.indexOf(`-stable`) !== -1) { exit(0); } -// ['latest', 'v0.33.0', 'v0.33.0-rc', 'v0.33.0-rc1', 'v0.33.0-rc2', 'v0.34.0', ''] -const tagsWithVersion = exec(`git tag -l --points-at HEAD`).stdout.split(/\s/) - // ['v0.33.0', 'v0.33.0-rc', 'v0.33.0-rc1', 'v0.33.0-rc2', 'v0.34.0'] - .filter(version => !!version && version.indexOf(`v${branchVersion}`) === 0) +// 34c034298dc9cad5a4553964a5a324450fda0385 +const currentCommit = exec(`git rev-parse HEAD`, {silent: true}).stdout.trim(); +// [34c034298dc9cad5a4553964a5a324450fda0385, refs/heads/0.33-stable, refs/tags/latest, refs/tags/v0.33.1, refs/tags/v0.34.1-rc] +const tagsWithVersion = exec(`git ls-remote origin | grep ${currentCommit}`, {silent: true}) + .stdout.split(/\s/) + // ['refs/tags/v0.33.0', 'refs/tags/v0.33.0-rc', 'refs/tags/v0.33.0-rc1', 'refs/tags/v0.33.0-rc2', 'refs/tags/v0.34.0'] + .filter(version => !!version && version.indexOf(`refs/tags/v${branchVersion}`) === 0) + // ['refs/tags/v0.33.0', 'refs/tags/v0.33.0-rc', 'refs/tags/v0.33.0-rc1', 'refs/tags/v0.33.0-rc2'] + .filter(version => version.indexOf(branchVersion) !== -1) // ['v0.33.0', 'v0.33.0-rc', 'v0.33.0-rc1', 'v0.33.0-rc2'] - .filter(version => version.indexOf(branchVersion) !== -1); + .map(version => version.slice(`refs/tags/`.length)); if (tagsWithVersion.length === 0) { echo(`Error: Can't find version tag in current commit. To deploy to NPM you must add tag v0.XY.Z[-rc] to your commit`); From 4c76e5476c022a4ae022d5cb37aac3efdd30d15a Mon Sep 17 00:00:00 2001 From: David Aurelio Date: Mon, 23 May 2016 07:18:55 -0700 Subject: [PATCH 070/843] remove premature optimizations Reviewed By: javache Differential Revision: D3325805 fbshipit-source-id: cdf8f4f3744d70fd37e8fa35c44ca8110ea4395a --- React/Executors/RCTJSCExecutor.mm | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/React/Executors/RCTJSCExecutor.mm b/React/Executors/RCTJSCExecutor.mm index 0dd0039577449b..7841834e8316ac 100644 --- a/React/Executors/RCTJSCExecutor.mm +++ b/React/Executors/RCTJSCExecutor.mm @@ -47,14 +47,13 @@ struct __attribute__((packed)) ModuleData { using file_ptr = std::unique_ptr; using memory_ptr = std::unique_ptr; -using table_ptr = std::unique_ptr; struct RandomAccessBundleData { file_ptr bundle; size_t baseOffset; size_t numTableEntries; - table_ptr table; - RandomAccessBundleData(): bundle(nullptr, fclose), table(nullptr, free) {} + std::unique_ptr table; + RandomAccessBundleData(): bundle(nullptr, fclose) {} }; struct RandomAccessBundleStartupCode { @@ -718,13 +717,13 @@ static bool readRandomAccessModule(const RandomAccessBundleData& bundleData, siz static void executeRandomAccessModule(RCTJSCExecutor *executor, uint32_t moduleID, size_t offset, size_t size) { - auto data = std::unique_ptr(new char[size]); + auto data = std::make_unique(size); if (!readRandomAccessModule(executor->_randomAccessBundle, offset, size, data.get())) { RCTFatal(RCTErrorWithMessage(@"Error loading RAM module")); return; } - static char url[14]; // 10 = maximum decimal digits in a 32bit unsigned int + ".js" + null byte + char url[14]; // 10 = maximum decimal digits in a 32bit unsigned int + ".js" + null byte sprintf(url, "%" PRIu32 ".js", moduleID); JSStringRef code = JSStringCreateWithUTF8CString(data.get()); @@ -758,7 +757,7 @@ - (void)registerNativeRequire RCTPerformanceLoggerAdd(RCTPLRAMNativeRequiresCount, 1); RCTPerformanceLoggerAppendStart(RCTPLRAMNativeRequires); - RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, [@"nativeRequire_" stringByAppendingFormat:@"%@", moduleID], nil); const uint32_t ID = [moduleID unsignedIntValue]; @@ -794,7 +793,7 @@ static RandomAccessBundleStartupCode readRAMBundle(file_ptr bundle, RandomAccess const size_t tableSize = numTableEntries * sizeof(ModuleData); // allocate memory for meta data and lookup table. malloc instead of new to avoid constructor calls - table_ptr table(static_cast(malloc(tableSize)), free); + auto table = std::make_unique(numTableEntries); if (!table) { return RandomAccessBundleStartupCode::empty(); } From c87b737ca1076f952bb704e8cf735c07325c3483 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 23 May 2016 08:27:14 -0700 Subject: [PATCH 071/843] Fixed tests Reviewed By: bestander Differential Revision: D3334669 fbshipit-source-id: ba0ae2ec5ea06c27d40ceed3ca8bf7a9da27dda9 --- Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderTests.m | 4 ++-- React/React.xcodeproj/project.pbxproj | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderTests.m index 5a7cc12bd28eee..e67edb0b40481a 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderTests.m @@ -107,7 +107,7 @@ - (void)testImageDecoding XCTAssertEqualObjects(decodedImage, image); XCTAssertNil(decodeError); }]; - XCTAssertNil(cancelBlock); + XCTAssertNotNil(cancelBlock); } - (void)testImageLoaderUsesImageDecoderWithHighestPriority @@ -136,7 +136,7 @@ - (void)testImageLoaderUsesImageDecoderWithHighestPriority XCTAssertEqualObjects(decodedImage, image); XCTAssertNil(decodeError); }]; - XCTAssertNil(cancelBlock); + XCTAssertNotNil(cancelBlock); } @end diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 9173e9bbb45d08..c967aea8534d99 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -859,6 +859,7 @@ 83CBBA401A601D0F00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "c++14"; CLANG_STATIC_ANALYZER_MODE = deep; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; @@ -876,6 +877,7 @@ 83CBBA411A601D0F00E9B192 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "c++14"; CLANG_STATIC_ANALYZER_MODE = deep; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; From d9737571c43d39af41d539de2dd12c2ceb5cda0e Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 23 May 2016 09:08:51 -0700 Subject: [PATCH 072/843] Updated AppState module to use new emitter system Summary: AppState now subclasses NativeEventEmitter instead of using global RCTDeviceEventEmitter. Reviewed By: javache Differential Revision: D3310488 fbshipit-source-id: f0116599223f4411307385c0dab683659d8d63b6 --- .../RCTRootViewIntegrationTests.m | 4 + .../RCTEventDispatcherTests.m | 5 + .../RCTModuleInitNotificationRaceTests.m | 5 + Libraries/AppState/AppState.js | 104 ++++++++----- .../AppStateIOS.js} | 18 +-- Libraries/AppStateIOS/AppStateIOS.ios.js | 147 ------------------ .../EventEmitter/RCTDeviceEventEmitter.js | 42 ++--- Libraries/Geolocation/RCTLocationObserver.m | 6 + Libraries/Network/RCTNetInfo.m | 3 + Libraries/Network/RCTNetworking.m | 12 ++ Libraries/RCTTest/RCTTestModule.m | 3 + Libraries/Settings/RCTSettingsManager.m | 3 + Libraries/Text/RCTTextView.h | 1 + Libraries/Text/RCTTextView.m | 8 +- React/Base/RCTEventDispatcher.h | 21 ++- React/Base/RCTEventDispatcher.m | 3 + React/Modules/RCTAppState.h | 4 +- React/Modules/RCTAppState.m | 41 +++-- React/Modules/RCTEventEmitter.m | 16 +- React/Views/RCTComponentData.m | 3 + 20 files changed, 186 insertions(+), 263 deletions(-) rename Libraries/{AppStateIOS/AppStateIOS.android.js => AppState/AppStateIOS.js} (50%) delete mode 100644 Libraries/AppStateIOS/AppStateIOS.ios.js diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/RCTRootViewIntegrationTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/RCTRootViewIntegrationTests.m index 7e3875d7146aeb..8695d27c2646ea 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/RCTRootViewIntegrationTests.m +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/RCTRootViewIntegrationTests.m @@ -66,9 +66,13 @@ @implementation SizeFlexibilityTestDelegate - (void)rootViewDidChangeIntrinsicSize:(RCTRootView *)rootView { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [rootView.bridge.eventDispatcher sendAppEventWithName:@"rootViewDidChangeIntrinsicSize" body:@{@"width": @(rootView.intrinsicSize.width), @"height": @(rootView.intrinsicSize.height)}]; +#pragma clang diagnostic pop } @end diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m index 6c43e2dd47a63b..4fe88ee933b28f 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m @@ -103,8 +103,13 @@ - (void)testLegacyEventsAreImmediatelyDispatched [[_bridge expect] enqueueJSCall:_JSMethod args:[_testEvent arguments]]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_eventDispatcher sendDeviceEventWithName:_eventName body:_body]; +#pragma clang diagnostic pop + [_bridge verify]; } diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitNotificationRaceTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitNotificationRaceTests.m index 77a997a4baa59c..2e9220d2d6b442 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitNotificationRaceTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitNotificationRaceTests.m @@ -41,11 +41,16 @@ @implementation RCTTestViewManager RCT_EXPORT_MODULE() +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + - (NSArray *)customDirectEventTypes { return @[@"foo"]; } +#pragma clang diagnostic pop + @end diff --git a/Libraries/AppState/AppState.js b/Libraries/AppState/AppState.js index 8f9a7cf4be9aba..1573a90129c9dd 100644 --- a/Libraries/AppState/AppState.js +++ b/Libraries/AppState/AppState.js @@ -11,18 +11,12 @@ */ 'use strict'; -var Map = require('Map'); -var NativeModules = require('NativeModules'); -var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); -var RCTAppState = NativeModules.AppState; +const NativeEventEmitter = require('NativeEventEmitter'); +const NativeModules = require('NativeModules'); +const RCTAppState = NativeModules.AppState; -var logError = require('logError'); -var invariant = require('fbjs/lib/invariant'); - -var _eventHandlers = { - change: new Map(), - memoryWarning: new Map(), -}; +const logError = require('logError'); +const invariant = require('fbjs/lib/invariant'); /** * `AppState` can tell you if the app is in the foreground or background, @@ -36,8 +30,9 @@ var _eventHandlers = { * - `active` - The app is running in the foreground * - `background` - The app is running in the background. The user is either * in another app or on the home screen - * - `inactive` - This is a transition state that currently never happens for - * typical React Native apps. + * - `inactive` - This is a state that occurs when transitioning between + * foreground & background, and during periods of inactivity such as + * entering the Multitasking view or in the event of an incoming call * * For more information, see * [Apple's documentation](https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/TheAppLifeCycle/TheAppLifeCycle.html) @@ -75,13 +70,58 @@ var _eventHandlers = { * state will happen only momentarily. */ -var AppState = { +class AppState extends NativeEventEmitter { - /** + _eventHandlers: Object; + currentState: ?string; + + constructor() { + super(RCTAppState); + + this._eventHandlers = { + change: new Map(), + memoryWarning: new Map(), + }; + + // TODO: getCurrentAppState callback seems to be called at a really late stage + // after app launch. Trying to get currentState when mounting App component + // will likely to have the initial value here. + // Initialize to 'active' instead of null. + this.currentState = 'active'; + + // TODO: this is a terrible solution - in order to ensure `currentState` prop + // is up to date, we have to register an observer that updates it whenever + // the state changes, even if nobody cares. We should just deprecate the + // `currentState` property and get rid of this. + this.addListener( + 'appStateDidChange', + (appStateData) => { + this.currentState = appStateData.app_state; + } + ); + + // TODO: see above - this request just populates the value of `currentState` + // when the module is first initialized. Would be better to get rid of the prop + // and expose `getCurrentAppState` method directly. + RCTAppState.getCurrentAppState( + (appStateData) => { + this.currentState = appStateData.app_state; + }, + logError + ); + } + + /** * Add a handler to AppState changes by listening to the `change` event type * and providing the handler + * + * TODO: now that AppState is a subclass of NativeEventEmitter, we could deprecate + * `addEventListener` and `removeEventListener` and just use `addListener` and + * `listener.remove()` directly. That will be a breaking change though, as both + * the method and event names are different (addListener events are currently + * required to be globally unique). */ - addEventListener: function( + addEventListener( type: string, handler: Function ) { @@ -90,24 +130,24 @@ var AppState = { 'Trying to subscribe to unknown event: "%s"', type ); if (type === 'change') { - _eventHandlers[type].set(handler, RCTDeviceEventEmitter.addListener( + this._eventHandlers[type].set(handler, this.addListener( 'appStateDidChange', (appStateData) => { handler(appStateData.app_state); } )); } else if (type === 'memoryWarning') { - _eventHandlers[type].set(handler, RCTDeviceEventEmitter.addListener( + this._eventHandlers[type].set(handler, this.addListener( 'memoryWarning', handler )); } - }, + } /** * Remove a handler by passing the `change` event type and the handler */ - removeEventListener: function( + removeEventListener( type: string, handler: Function ) { @@ -115,28 +155,14 @@ var AppState = { ['change', 'memoryWarning'].indexOf(type) !== -1, 'Trying to remove listener for unknown event: "%s"', type ); - if (!_eventHandlers[type].has(handler)) { + if (!this._eventHandlers[type].has(handler)) { return; } - _eventHandlers[type].get(handler).remove(); - _eventHandlers[type].delete(handler); - }, - - currentState: ('active' : ?string), -}; - -RCTDeviceEventEmitter.addListener( - 'appStateDidChange', - (appStateData) => { - AppState.currentState = appStateData.app_state; + this._eventHandlers[type].get(handler).remove(); + this._eventHandlers[type].delete(handler); } -); +}; -RCTAppState.getCurrentAppState( - (appStateData) => { - AppState.currentState = appStateData.app_state; - }, - logError -); +AppState = new AppState(); module.exports = AppState; diff --git a/Libraries/AppStateIOS/AppStateIOS.android.js b/Libraries/AppState/AppStateIOS.js similarity index 50% rename from Libraries/AppStateIOS/AppStateIOS.android.js rename to Libraries/AppState/AppStateIOS.js index 51117413856223..c2c29969e7a514 100644 --- a/Libraries/AppStateIOS/AppStateIOS.android.js +++ b/Libraries/AppState/AppStateIOS.js @@ -11,20 +11,8 @@ */ 'use strict'; -var warning = require('fbjs/lib/warning'); +const AppState = require('AppState'); -class AppStateIOS { +console.warn('AppStateIOS is deprecated. Use AppState instead'); - static addEventListener(type, handler) { - warning(false, 'Cannot listen to AppStateIOS events on Android.'); - } - - static removeEventListener(type, handler) { - warning(false, 'Cannot remove AppStateIOS listener on Android.'); - } - -} - -AppStateIOS.currentState = null; - -module.exports = AppStateIOS; +module.exports = AppState; diff --git a/Libraries/AppStateIOS/AppStateIOS.ios.js b/Libraries/AppStateIOS/AppStateIOS.ios.js deleted file mode 100644 index 807f7701e7f8f6..00000000000000 --- a/Libraries/AppStateIOS/AppStateIOS.ios.js +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule AppStateIOS - * @flow - */ -'use strict'; - -var NativeModules = require('NativeModules'); -var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); -var RCTAppState = NativeModules.AppState; - -var logError = require('logError'); -var invariant = require('fbjs/lib/invariant'); - -var _eventHandlers = { - change: new Map(), - memoryWarning: new Map(), -}; - -/** - * `AppStateIOS` can tell you if the app is in the foreground or background, - * and notify you when the state changes. - * - * AppStateIOS is frequently used to determine the intent and proper behavior when - * handling push notifications. - * - * ### iOS App States - * - * - `active` - The app is running in the foreground - * - `background` - The app is running in the background. The user is either - * in another app or on the home screen - * - `inactive` - This is a state that occurs when transitioning between - * foreground & background, and during periods of inactivity such as - * entering the Multitasking view or in the event of an incoming call - * - * For more information, see - * [Apple's documentation](https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/TheAppLifeCycle/TheAppLifeCycle.html) - * - * ### Basic Usage - * - * To see the current state, you can check `AppStateIOS.currentState`, which - * will be kept up-to-date. However, `currentState` will be null at launch - * while `AppStateIOS` retrieves it over the bridge. - * - * ``` - * getInitialState: function() { - * return { - * currentAppState: AppStateIOS.currentState, - * }; - * }, - * componentDidMount: function() { - * AppStateIOS.addEventListener('change', this._handleAppStateChange); - * }, - * componentWillUnmount: function() { - * AppStateIOS.removeEventListener('change', this._handleAppStateChange); - * }, - * _handleAppStateChange: function(currentAppState) { - * this.setState({ currentAppState, }); - * }, - * render: function() { - * return ( - * Current state is: {this.state.currentAppState} - * ); - * }, - * ``` - * - * This example will only ever appear to say "Current state is: active" because - * the app is only visible to the user when in the `active` state, and the null - * state will happen only momentarily. - */ - -var AppStateIOS = { - - /** - * Add a handler to AppState changes by listening to the `change` event type - * and providing the handler - */ - addEventListener: function( - type: string, - handler: Function - ) { - invariant( - ['change', 'memoryWarning'].indexOf(type) !== -1, - 'Trying to subscribe to unknown event: "%s"', type - ); - if (type === 'change') { - _eventHandlers[type].set(handler, RCTDeviceEventEmitter.addListener( - 'appStateDidChange', - (appStateData) => { - handler(appStateData.app_state); - } - )); - } else if (type === 'memoryWarning') { - _eventHandlers[type].set(handler, RCTDeviceEventEmitter.addListener( - 'memoryWarning', - handler - )); - } - }, - - /** - * Remove a handler by passing the `change` event type and the handler - */ - removeEventListener: function( - type: string, - handler: Function - ) { - invariant( - ['change', 'memoryWarning'].indexOf(type) !== -1, - 'Trying to remove listener for unknown event: "%s"', type - ); - if (!_eventHandlers[type].has(handler)) { - return; - } - _eventHandlers[type].get(handler).remove(); - _eventHandlers[type].delete(handler); - }, - - // TODO: getCurrentAppState callback seems to be called at a really late stage - // after app launch. Trying to get currentState when mounting App component - // will likely to have the initial value here. - // Initialize to 'active' instead of null. - currentState: ('active' : ?string), - -}; - -RCTDeviceEventEmitter.addListener( - 'appStateDidChange', - (appStateData) => { - AppStateIOS.currentState = appStateData.app_state; - } -); - -RCTAppState.getCurrentAppState( - (appStateData) => { - AppStateIOS.currentState = appStateData.app_state; - }, - logError -); - -module.exports = AppStateIOS; diff --git a/Libraries/EventEmitter/RCTDeviceEventEmitter.js b/Libraries/EventEmitter/RCTDeviceEventEmitter.js index 8a6bbea4724757..ba5b5f16a1c05e 100644 --- a/Libraries/EventEmitter/RCTDeviceEventEmitter.js +++ b/Libraries/EventEmitter/RCTDeviceEventEmitter.js @@ -30,31 +30,35 @@ class RCTDeviceEventEmitter extends EventEmitter { super(sharedSubscriber); this.sharedSubscriber = sharedSubscriber; } - - addListener(eventType: string, listener: Function, context: ?Object): EmitterSubscription { - if (eventType.lastIndexOf('statusBar', 0) === 0) { - console.warn('`%s` event should be registered via the StatusBarIOS module', eventType); - return require('StatusBarIOS').addListener(eventType, listener, context); - } - if (eventType.lastIndexOf('keyboard', 0) === 0) { - console.warn('`%s` event should be registered via the Keyboard module', eventType); - return require('Keyboard').addListener(eventType, listener, context); - } - return super.addListener(eventType, listener, context); - } - - removeAllListeners(eventType: ?string) { + + _nativeEventModule(eventType: ?string) { if (eventType) { if (eventType.lastIndexOf('statusBar', 0) === 0) { - console.warn('statusBar events should be unregistered via the StatusBarIOS module'); - return require('StatusBarIOS').removeAllListeners(eventType); + console.warn('`%s` event should be registered via the StatusBarIOS module', eventType); + return require('StatusBarIOS'); } if (eventType.lastIndexOf('keyboard', 0) === 0) { - console.warn('keyboard events should be unregistered via the Keyboard module'); - return require('Keyboard').removeAllListeners(eventType); + console.warn('`%s` event should be registered via the Keyboard module', eventType); + return require('Keyboard'); + } + if (eventType === 'appStateDidChange' || eventType === 'memoryWarning') { + console.warn('`%s` event should be registered via the AppState module', eventType); + return require('AppState'); } } - super.removeAllListeners(eventType); + return null; + } + + addListener(eventType: string, listener: Function, context: ?Object): EmitterSubscription { + const eventModule = this._nativeEventModule(eventType); + return eventModule ? eventModule.addListener(eventType, listener, context) + : super.addListener(eventType, listener, context); + } + + removeAllListeners(eventType: ?string) { + const eventModule = this._nativeEventModule(eventType); + (eventModule && eventType) ? eventModule.removeAllListeners(eventType) + : super.removeAllListeners(eventType); } removeSubscription(subscription: EmitterSubscription) { diff --git a/Libraries/Geolocation/RCTLocationObserver.m b/Libraries/Geolocation/RCTLocationObserver.m index 85f4e86b2bae50..23179c5cab214e 100644 --- a/Libraries/Geolocation/RCTLocationObserver.m +++ b/Libraries/Geolocation/RCTLocationObserver.m @@ -279,8 +279,11 @@ - (void)locationManager:(CLLocationManager *)manager // Send event if (_observingLocation) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"geolocationDidChange" body:_lastLocationEvent]; +#pragma clang diagnostic pop } // Fire all queued callbacks @@ -321,8 +324,11 @@ - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError * // Send event if (_observingLocation) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"geolocationError" body:jsError]; +#pragma clang diagnostic pop } // Fire all queued error callbacks diff --git a/Libraries/Network/RCTNetInfo.m b/Libraries/Network/RCTNetInfo.m index c5440ce5d1ca4a..28d0fc07387039 100644 --- a/Libraries/Network/RCTNetInfo.m +++ b/Libraries/Network/RCTNetInfo.m @@ -51,8 +51,11 @@ static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SC if (![status isEqualToString:self->_status]) { self->_status = status; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [self->_bridge.eventDispatcher sendDeviceEventWithName:@"networkStatusDidChange" body:@{@"network_info": status}]; +#pragma clang diagnostic pop } } diff --git a/Libraries/Network/RCTNetworking.m b/Libraries/Network/RCTNetworking.m index 27f7e05f5f731b..5c7e3b9ee0edcb 100644 --- a/Libraries/Network/RCTNetworking.m +++ b/Libraries/Network/RCTNetworking.m @@ -346,8 +346,11 @@ - (void)sendData:(NSData *)data forTask:(RCTNetworkTask *)task } NSArray *responseJSON = @[task.requestID, responseText ?: @""]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkData" body:responseJSON]; +#pragma clang diagnostic pop } - (void)sendRequest:(NSURLRequest *)request @@ -361,7 +364,10 @@ - (void)sendRequest:(NSURLRequest *)request RCTURLRequestProgressBlock uploadProgressBlock = ^(int64_t progress, int64_t total) { dispatch_async(_methodQueue, ^{ NSArray *responseJSON = @[task.requestID, @((double)progress), @((double)total)]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"didSendNetworkData" body:responseJSON]; +#pragma clang diagnostic pop }); }; @@ -379,8 +385,11 @@ - (void)sendRequest:(NSURLRequest *)request } id responseURL = response.URL ? response.URL.absoluteString : [NSNull null]; NSArray *responseJSON = @[task.requestID, @(status), headers, responseURL]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse" body:responseJSON]; +#pragma clang diagnostic pop }); }; @@ -401,8 +410,11 @@ - (void)sendRequest:(NSURLRequest *)request error.code == kCFURLErrorTimedOut ? @YES : @NO ]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse" body:responseJSON]; +#pragma clang diagnostic pop [_tasksByRequestID removeObjectForKey:task.requestID]; }); diff --git a/Libraries/RCTTest/RCTTestModule.m b/Libraries/RCTTest/RCTTestModule.m index a889b8522a4e79..2d86a1c6ba2115 100644 --- a/Libraries/RCTTest/RCTTestModule.m +++ b/Libraries/RCTTest/RCTTestModule.m @@ -52,7 +52,10 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_METHOD(sendAppEvent:(NSString *)name body:(nullable id)body) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendAppEventWithName:name body:body]; +#pragma clang diagnostic pop } RCT_REMAP_METHOD(shouldResolve, shouldResolve_resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) diff --git a/Libraries/Settings/RCTSettingsManager.m b/Libraries/Settings/RCTSettingsManager.m index c0c915df777cb1..859506e671e12a 100644 --- a/Libraries/Settings/RCTSettingsManager.m +++ b/Libraries/Settings/RCTSettingsManager.m @@ -62,9 +62,12 @@ - (void)userDefaultsDidChange:(NSNotification *)note return; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"settingsUpdated" body:RCTJSONClean([_defaults dictionaryRepresentation])]; +#pragma clang diagnostic pop } /** diff --git a/Libraries/Text/RCTTextView.h b/Libraries/Text/RCTTextView.h index 5279eafb673101..93a93d409972aa 100644 --- a/Libraries/Text/RCTTextView.h +++ b/Libraries/Text/RCTTextView.h @@ -28,6 +28,7 @@ @property (nonatomic, assign) NSInteger mostRecentEventCount; @property (nonatomic, strong) NSNumber *maxLength; +@property (nonatomic, copy) RCTDirectEventBlock onChange; @property (nonatomic, copy) RCTDirectEventBlock onSelectionChange; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index fad4719d455ee3..d32463333bfa7b 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -472,7 +472,7 @@ - (void)textViewDidChange:(UITextView *)textView [self _setPlaceholderVisibility]; _nativeEventCount++; - if (!self.reactTag) { + if (!self.reactTag || !_onChange) { return; } @@ -490,8 +490,7 @@ - (void)textViewDidChange:(UITextView *)textView } _previousTextLength = textLength; _previousContentHeight = contentHeight; - - NSDictionary *event = @{ + _onChange(@{ @"text": self.text, @"contentSize": @{ @"height": @(contentHeight), @@ -499,8 +498,7 @@ - (void)textViewDidChange:(UITextView *)textView }, @"target": self.reactTag, @"eventCount": @(_nativeEventCount), - }; - [_eventDispatcher sendInputEventWithName:@"change" body:event]; + }); } - (void)textViewDidEndEditing:(UITextView *)textView diff --git a/React/Base/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h index 11045f667a0590..6b5b448c1d1a63 100644 --- a/React/Base/RCTEventDispatcher.h +++ b/React/Base/RCTEventDispatcher.h @@ -35,7 +35,6 @@ RCT_EXTERN const NSInteger RCTTextUpdateLagWarningThreshold; RCT_EXTERN NSString *RCTNormalizeInputEventName(NSString *eventName); @protocol RCTEvent - @required @property (nonatomic, strong, readonly) NSNumber *viewTag; @@ -60,25 +59,25 @@ RCT_EXTERN NSString *RCTNormalizeInputEventName(NSString *eventName); @interface RCTEventDispatcher : NSObject /** - * Send an application-specific event that does not relate to a specific - * view, e.g. a navigation or data update notification. + * Deprecated, do not use. */ -- (void)sendAppEventWithName:(NSString *)name body:(id)body; +- (void)sendAppEventWithName:(NSString *)name body:(id)body +__deprecated_msg("Subclass RCTEventEmitter instead"); /** - * Send a device or iOS event that does not relate to a specific view, - * e.g.rotation, location, keyboard show/hide, background/awake, etc. + * Deprecated, do not use. */ -- (void)sendDeviceEventWithName:(NSString *)name body:(id)body; +- (void)sendDeviceEventWithName:(NSString *)name body:(id)body +__deprecated_msg("Subclass RCTEventEmitter instead"); /** - * Send a user input event. The body dictionary must contain a "target" - * parameter, representing the React tag of the view sending the event + * Deprecated, do not use. */ -- (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body; +- (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body +__deprecated_msg("Use RCTDirectEventBlock or RCTBubblingEventBlock instead"); /** - * Send a text input/focus event. + * Send a text input/focus event. For internal use only. */ - (void)sendTextEventWithType:(RCTTextEventType)type reactTag:(NSNumber *)reactTag diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m index 18a6c9f44737d9..dbfd7a0df1075f 100644 --- a/React/Base/RCTEventDispatcher.m +++ b/React/Base/RCTEventDispatcher.m @@ -126,7 +126,10 @@ - (void)sendTextEventWithType:(RCTTextEventType)type body[@"key"] = key; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [self sendInputEventWithName:events[type] body:body]; +#pragma clang diagnostic pop } - (void)sendEvent:(id)event diff --git a/React/Modules/RCTAppState.h b/React/Modules/RCTAppState.h index 4c2e758e215b06..99553d756835ba 100644 --- a/React/Modules/RCTAppState.h +++ b/React/Modules/RCTAppState.h @@ -7,8 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTBridgeModule.h" +#import "RCTEventEmitter.h" -@interface RCTAppState : NSObject +@interface RCTAppState : RCTEventEmitter @end diff --git a/React/Modules/RCTAppState.m b/React/Modules/RCTAppState.m index 2cdac21bf163b3..3f4c178b35901b 100644 --- a/React/Modules/RCTAppState.m +++ b/React/Modules/RCTAppState.m @@ -16,6 +16,8 @@ static NSString *RCTCurrentAppBackgroundState() { + RCTAssertMainThread(); + static NSDictionary *states; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -37,26 +39,22 @@ @implementation RCTAppState NSString *_lastKnownState; } -@synthesize bridge = _bridge; - RCT_EXPORT_MODULE() +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + #pragma mark - Lifecycle -- (instancetype)init +- (NSArray *)supportedEvents { - if ((self = [super init])) { - - // Needs to be called on the main thread, as it accesses UIApplication - _lastKnownState = RCTCurrentAppBackgroundState(); - } - return self; + return @[@"appStateDidChange", @"memoryWarning"]; } -- (void)setBridge:(RCTBridge *)bridge +- (void)startObserving { - _bridge = bridge; - for (NSString *name in @[UIApplicationDidBecomeActiveNotification, UIApplicationDidEnterBackgroundNotification, UIApplicationDidFinishLaunchingNotification, @@ -75,19 +73,18 @@ - (void)setBridge:(RCTBridge *)bridge object:nil]; } -- (void)handleMemoryWarning -{ - [_bridge.eventDispatcher sendDeviceEventWithName:@"memoryWarning" - body:nil]; -} - -- (void)dealloc +- (void)stopObserving { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - App Notification Methods +- (void)handleMemoryWarning +{ + [self sendEventWithName:@"memoryWarning" body:nil]; +} + - (void)handleAppStateDidChange:(NSNotification *)notification { NSString *newState; @@ -102,8 +99,8 @@ - (void)handleAppStateDidChange:(NSNotification *)notification if (![newState isEqualToString:_lastKnownState]) { _lastKnownState = newState; - [_bridge.eventDispatcher sendDeviceEventWithName:@"appStateDidChange" - body:@{@"app_state": _lastKnownState}]; + [self sendEventWithName:@"appStateDidChange" + body:@{@"app_state": _lastKnownState}]; } } @@ -115,7 +112,7 @@ - (void)handleAppStateDidChange:(NSNotification *)notification RCT_EXPORT_METHOD(getCurrentAppState:(RCTResponseSenderBlock)callback error:(__unused RCTResponseSenderBlock)error) { - callback(@[@{@"app_state": _lastKnownState}]); + callback(@[@{@"app_state": RCTCurrentAppBackgroundState()}]); } @end diff --git a/React/Modules/RCTEventEmitter.m b/React/Modules/RCTEventEmitter.m index a3b22fa82e9840..5f399e8305f9ff 100644 --- a/React/Modules/RCTEventEmitter.m +++ b/React/Modules/RCTEventEmitter.m @@ -9,6 +9,7 @@ #import "RCTEventEmitter.h" #import "RCTAssert.h" +#import "RCTUtils.h" #import "RCTLog.h" @implementation RCTEventEmitter @@ -21,9 +22,16 @@ + (NSString *)moduleName return @""; } ++ (void)initialize +{ + if (self != [RCTEventEmitter class]) { + RCTAssert(RCTClassOverridesInstanceMethod(self, @selector(supportedEvents)), + @"You must override the `supportedEvents` method of %@", self); + } +} + - (NSArray *)supportedEvents { - RCTAssert(NO, @"You must override the `supportedEvents` method of %@", [self class]); return nil; } @@ -32,7 +40,8 @@ - (void)sendEventWithName:(NSString *)eventName body:(id)body RCTAssert(_bridge != nil, @"bridge is not set."); if (RCT_DEBUG && ![[self supportedEvents] containsObject:eventName]) { - RCTLogError(@"`%@` is not a supported event type for %@", eventName, [self class]); + RCTLogError(@"`%@` is not a supported event type for %@. Supported events are: `%@`", + eventName, [self class], [[self supportedEvents] componentsJoinedByString:@"`, `"]); } if (_listenerCount > 0) { [_bridge enqueueJSCall:@"RCTDeviceEventEmitter.emit" @@ -62,7 +71,8 @@ - (void)dealloc RCT_EXPORT_METHOD(addListener:(NSString *)eventName) { if (RCT_DEBUG && ![[self supportedEvents] containsObject:eventName]) { - RCTLogError(@"`%@` is not a supported event type for %@", eventName, [self class]); + RCTLogError(@"`%@` is not a supported event type for %@. Supported events are: `%@`", + eventName, [self class], [[self supportedEvents] componentsJoinedByString:@"`, `"]); } if (_listenerCount == 0) { [self startObserving]; diff --git a/React/Views/RCTComponentData.m b/React/Views/RCTComponentData.m index d00c178cfe7169..9259540c19801e 100644 --- a/React/Views/RCTComponentData.m +++ b/React/Views/RCTComponentData.m @@ -180,7 +180,10 @@ - (RCTPropBlock)propBlockForKey:(NSString *)name ((void (*)(id, SEL, id))objc_msgSend)(target, setter, [RCTConvert BOOL:json] ? ^(NSDictionary *body) { body = [NSMutableDictionary dictionaryWithDictionary:body]; ((NSMutableDictionary *)body)[@"target"] = weakTarget.reactTag; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [weakManager.bridge.eventDispatcher sendInputEventWithName:RCTNormalizeInputEventName(name) body:body]; +#pragma clang diagnostic pop } : nil); }; From 81e4b4202aa2ef11ca25d95dcdcc55d07426af27 Mon Sep 17 00:00:00 2001 From: Alexander Blom Date: Mon, 23 May 2016 09:15:45 -0700 Subject: [PATCH 073/843] Don't block waiting for UI thread to be ready Reviewed By: astreet Differential Revision: D3323227 fbshipit-source-id: bc8a815f9dca43c8c1adebc7b2554f931f135f33 --- .../facebook/react/bridge/queue/MessageQueueThreadImpl.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.java index 41a191faed8852..318765b69c2f48 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.java @@ -147,20 +147,16 @@ private static MessageQueueThreadImpl createForMainThread( final MessageQueueThreadImpl mqt = new MessageQueueThreadImpl(name, mainLooper, exceptionHandler); - // Ensure that the MQT is registered by the time this method returns if (UiThreadUtil.isOnUiThread()) { MessageQueueThreadRegistry.register(mqt); } else { - final SimpleSettableFuture registrationFuture = new SimpleSettableFuture<>(); UiThreadUtil.runOnUiThread( new Runnable() { @Override public void run() { MessageQueueThreadRegistry.register(mqt); - registrationFuture.set(null); } }); - registrationFuture.getOrThrow(); } return mqt; } From f7c4ed8926bb883fdb245bd73a77804dd974a14f Mon Sep 17 00:00:00 2001 From: Marc Horowitz Date: Mon, 23 May 2016 09:32:10 -0700 Subject: [PATCH 074/843] Fix chrome debugging on android Summary: Java loadApplicationScript changed, but the C++ code in the debug ProxyExecutor which called it did not. This fixes the fbjni method lookup. fixes #7659 Reviewed By: AaaChiuuu Differential Revision: D3331472 fbshipit-source-id: 33312dccc3c7687f51742e42f9e0397f9c925e76 --- ReactAndroid/src/main/jni/react/jni/ProxyExecutor.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ReactAndroid/src/main/jni/react/jni/ProxyExecutor.cpp b/ReactAndroid/src/main/jni/react/jni/ProxyExecutor.cpp index 999f139d18215e..f0dc3bda030e4d 100644 --- a/ReactAndroid/src/main/jni/react/jni/ProxyExecutor.cpp +++ b/ReactAndroid/src/main/jni/react/jni/ProxyExecutor.cpp @@ -41,14 +41,15 @@ ProxyExecutor::~ProxyExecutor() { } void ProxyExecutor::loadApplicationScript( - const std::string& script, + const std::string&, const std::string& sourceURL) { static auto loadApplicationScript = - jni::findClassStatic(EXECUTOR_BASECLASS)->getMethod("loadApplicationScript"); + jni::findClassStatic(EXECUTOR_BASECLASS)->getMethod("loadApplicationScript"); + + // The proxy ignores the script data passed in. loadApplicationScript( m_executor.get(), - jni::make_jstring(script).get(), jni::make_jstring(sourceURL).get()); } From 1767a0a82858b6329d6ffecac2734d00b59b6057 Mon Sep 17 00:00:00 2001 From: Emil Sjolander Date: Mon, 23 May 2016 10:15:57 -0700 Subject: [PATCH 075/843] Correctly implement measure callback with measure modes Reviewed By: astreet Differential Revision: D3330139 fbshipit-source-id: 2ea6e4b463817e9ccc5f2d9395736f68f8d972e4 --- .../views/textinput/ReactTextInputShadowNode.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java index d3b4b68410720c..ad2ea0b55ca3c4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java @@ -13,6 +13,7 @@ import android.text.Spannable; import android.util.TypedValue; +import android.view.View.MeasureSpec; import android.view.ViewGroup; import android.widget.EditText; @@ -75,7 +76,6 @@ public void measure( // measure() should never be called before setThemedContext() EditText editText = Assertions.assertNotNull(mEditText); - measureOutput.width = widthMode == CSSMeasureMode.UNDEFINED ? CSSConstants.UNDEFINED : width; editText.setTextSize( TypedValue.COMPLEX_UNIT_PX, mFontSize == UNSET ? @@ -91,10 +91,21 @@ public void measure( editText.setLines(mNumberOfLines); } - editText.measure(0 /* unspecified */, 0 /* unspecified */); + editText.measure(getMeasureSpec(width, widthMode), getMeasureSpec(height, heightMode)); + measureOutput.width = editText.getMeasuredWidth(); measureOutput.height = editText.getMeasuredHeight(); } + private int getMeasureSpec(float size, CSSMeasureMode mode) { + if (mode == CSSMeasureMode.EXACTLY) { + return MeasureSpec.makeMeasureSpec((int) size, MeasureSpec.EXACTLY); + } else if (mode == CSSMeasureMode.AT_MOST) { + return MeasureSpec.makeMeasureSpec((int) size, MeasureSpec.AT_MOST); + } else { + return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + } + @Override public void onBeforeLayout() { // We don't have to measure the text within the text input. From ca71143ff4b8232ebdd06de1ddacffaaa8ae314f Mon Sep 17 00:00:00 2001 From: Fred Liu Date: Mon, 23 May 2016 10:21:07 -0700 Subject: [PATCH 076/843] Faster animation Summary: Reduce auto-swipe animation duration to 200 ms to increase swipe speed. Reviewed By: zjj010104 Differential Revision: D3333942 fbshipit-source-id: a9e0b15926ef854adcd9e1d2c1e67fd080139d84 --- Libraries/Experimental/SwipeableRow/SwipeableRow.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Libraries/Experimental/SwipeableRow/SwipeableRow.js b/Libraries/Experimental/SwipeableRow/SwipeableRow.js index e22c6878ea3932..f0def1ac30feca 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableRow.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableRow.js @@ -37,6 +37,8 @@ const emptyFunction = require('emptyFunction'); const CLOSED_LEFT_POSITION = 0; // Minimum swipe distance before we recognize it as such const HORIZONTAL_SWIPE_DISTANCE_THRESHOLD = 15; +// Time, in milliseconds, of how long the animated swipe should be +const SWIPE_DURATION = 200; /** * Creates a swipable row that allows taps on the main item and a custom View @@ -184,6 +186,7 @@ const SwipeableRow = React.createClass({ Animated.timing( this.state.currentLeft, { + duration: SWIPE_DURATION, toValue: toValue, }, ).start(() => { From 68af89dcbfe96735389bc2909ff10400908a356d Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Mon, 23 May 2016 10:27:23 -0700 Subject: [PATCH 077/843] Update node-haste dependency to 2.12.0 Summary: Update to node-haste 2.12.0 to support pass through configuration of supported platforms. Closes https://github.com/facebook/react-native/pull/7660 Differential Revision: D3335034 Pulled By: mkonicek fbshipit-source-id: d238b90a90d51654301d61251ceb26d183fef57a --- package.json | 2 +- packager/react-packager/src/AssetServer/index.js | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e63c04132af28f..aa91a3b49f11ab 100644 --- a/package.json +++ b/package.json @@ -160,7 +160,7 @@ "mkdirp": "^0.5.1", "module-deps": "^3.9.1", "node-fetch": "^1.3.3", - "node-haste": "~2.11.0", + "node-haste": "~2.12.0", "opn": "^3.0.2", "optimist": "^0.6.1", "progress": "^1.1.8", diff --git a/packager/react-packager/src/AssetServer/index.js b/packager/react-packager/src/AssetServer/index.js index 1a7bb75efe7394..c9b198a500ff7a 100644 --- a/packager/react-packager/src/AssetServer/index.js +++ b/packager/react-packager/src/AssetServer/index.js @@ -51,7 +51,7 @@ class AssetServer { } get(assetPath, platform = null) { - const assetData = getAssetDataFromName(assetPath); + const assetData = getAssetDataFromName(assetPath, new Set([platform])); return this._getAssetRecord(assetPath, platform).then(record => { for (let i = 0; i < record.scales.length; i++) { if (record.scales[i] >= assetData.resolution) { @@ -64,7 +64,7 @@ class AssetServer { } getAssetData(assetPath, platform = null) { - const nameData = getAssetDataFromName(assetPath); + const nameData = getAssetDataFromName(assetPath, new Set([platform])); const data = { name: nameData.name, type: nameData.type, @@ -115,7 +115,7 @@ class AssetServer { .then(res => { const dir = res[0]; const files = res[1]; - const assetData = getAssetDataFromName(filename); + const assetData = getAssetDataFromName(filename, new Set([platform])); const map = this._buildAssetMap(dir, files, platform); @@ -166,8 +166,8 @@ class AssetServer { }); } - _buildAssetMap(dir, files) { - const assets = files.map(getAssetDataFromName); + _buildAssetMap(dir, files, platform) { + const assets = files.map(this._getAssetDataFromName.bind(this, new Set([platform]))); const map = Object.create(null); assets.forEach(function(asset, i) { const file = files[i]; @@ -194,6 +194,10 @@ class AssetServer { return map; } + + _getAssetDataFromName(platform, file) { + return getAssetDataFromName(file, platform); + } } function getAssetKey(assetName, platform) { From 0b72eba8698c5d1ef014fe735c299321e6030352 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Mon, 23 May 2016 10:48:35 -0700 Subject: [PATCH 078/843] Fix UI Explorer in Android. Summary: The refactor work (renamed `navigationState.children` to `navigationState.routes`) from D3333735 broken UI Explorer in Android. Fix it. Reviewed By: ericvicenti Differential Revision: D3334014 fbshipit-source-id: 345fbb5eabc792a49c6f2f82ce8c03679bc75940 --- .../NavigationExperimental/NavigationBasicExample.js | 2 +- Examples/UIExplorer/UIExplorerApp.android.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationBasicExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationBasicExample.js index 4424f09137fcd7..d296ebbd7715bb 100644 --- a/Examples/UIExplorer/NavigationExperimental/NavigationBasicExample.js +++ b/Examples/UIExplorer/NavigationExperimental/NavigationBasicExample.js @@ -59,7 +59,7 @@ const NavigationBasicExample = React.createClass({ return ( ); } - const title = UIExplorerStateTitleMap(stack.children[stack.index]); - const index = stack.children.length <= 1 ? 1 : stack.index; + const title = UIExplorerStateTitleMap(stack.routes[stack.index]); + const index = stack.routes.length <= 1 ? 1 : stack.index; - if (stack && stack.children[index]) { - const {key} = stack.children[index]; + if (stack && stack.routes[index]) { + const {key} = stack.routes[index]; const ExampleModule = UIExplorerList.Modules[key]; const ExampleComponent = UIExplorerExampleList.makeRenderable(ExampleModule); return ( @@ -171,7 +171,7 @@ class UIExplorerApp extends React.Component { ); From be38ae1169053adf21b907dcffa53ae4ca73744f Mon Sep 17 00:00:00 2001 From: Anoop Chaurasiya Date: Mon, 23 May 2016 11:03:17 -0700 Subject: [PATCH 079/843] provide verbose error-message on bundle-load-failure Reviewed By: fkgozali Differential Revision: D3329107 fbshipit-source-id: 33c02ff066ec9ad415a5cc4639347ebdef67c604 --- React/Base/RCTJavaScriptLoader.m | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m index 71169816dd4a12..f677db64e23517 100755 --- a/React/Base/RCTJavaScriptLoader.m +++ b/React/Base/RCTJavaScriptLoader.m @@ -25,12 +25,15 @@ @implementation RCTJavaScriptLoader + (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(RCTSourceLoadBlock)onComplete { + NSString *unsanitizedScriptURLString = scriptURL.absoluteString; // Sanitize the script URL - scriptURL = [RCTConvert NSURL:scriptURL.absoluteString]; + scriptURL = [RCTConvert NSURL:unsanitizedScriptURLString]; if (!scriptURL) { + NSString *errorDescription = [NSString stringWithFormat:@"No script URL provided." + @"unsanitizedScriptURLString:(%@)", unsanitizedScriptURLString]; NSError *error = [NSError errorWithDomain:@"JavaScriptLoader" code:1 userInfo:@{ - NSLocalizedDescriptionKey: @"No script URL provided." + NSLocalizedDescriptionKey: errorDescription }]; onComplete(error, nil); return; From f60cabf34b79ee88a124c12757ca9455b8ba3bc3 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Mon, 23 May 2016 12:25:35 -0700 Subject: [PATCH 080/843] website generation Summary: TestPlan: - cd website - node ./server/generate.js Closes https://github.com/facebook/react-native/pull/7706 Differential Revision: D3335839 fbshipit-source-id: bd4a65d23ce242a5aa1d6a545dafa6be565e43c5 --- website/server/extractDocs.js | 1 - 1 file changed, 1 deletion(-) diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index 01b59e59e8ae06..77d153861d886c 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -290,7 +290,6 @@ const apis = [ '../Libraries/Utilities/AlertIOS.js', '../Libraries/Animated/src/AnimatedImplementation.js', '../Libraries/AppRegistry/AppRegistry.js', - '../Libraries/AppStateIOS/AppStateIOS.ios.js', '../Libraries/AppState/AppState.js', '../Libraries/Storage/AsyncStorage.js', '../Libraries/Utilities/BackAndroid.android.js', From 1343248bbe8e371ba4a4891901ec6f5ded52b7d4 Mon Sep 17 00:00:00 2001 From: James Ide Date: Mon, 23 May 2016 20:39:13 -0700 Subject: [PATCH 081/843] Set statics on the NavigationCard container Summary: NavigationCard is wrapped in a container, which breaks statics so apply the statics to the wrapped class instead. Closes https://github.com/facebook/react-native/pull/7483 Differential Revision: D3336226 Pulled By: ericvicenti fbshipit-source-id: ae28e7be7a6227700c2c11081c5605f3667d1494 --- .../NavigationExperimental/NavigationCard.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js b/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js index d117ceb9124ace..d405a7fd05fa60 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js @@ -137,11 +137,6 @@ class NavigationCard extends React.Component { ); } - - static CardStackPanResponder = NavigationCardStackPanResponder; - static CardStackStyleInterpolator = NavigationCardStackStyleInterpolator; - static PagerPanResponder = NavigationPagerPanResponder; - static PagerStyleInterpolator = NavigationPagerStyleInterpolator; } const styles = StyleSheet.create({ @@ -161,4 +156,13 @@ const styles = StyleSheet.create({ NavigationCard = NavigationPointerEventsContainer.create(NavigationCard); +// $FlowFixMe: Figure out how to declare these properties on the container class +NavigationCard.CardStackPanResponder = NavigationCardStackPanResponder; +// $FlowFixMe +NavigationCard.CardStackStyleInterpolator = NavigationCardStackStyleInterpolator; +// $FlowFixMe +NavigationCard.PagerPanResponder = NavigationPagerPanResponder; +// $FlowFixMe +NavigationCard.PagerStyleInterpolator = NavigationPagerStyleInterpolator; + module.exports = NavigationCard; From ddc374fac813c5e66da47bbd10a9de7c30661a99 Mon Sep 17 00:00:00 2001 From: Urban Cvek Date: Tue, 24 May 2016 01:52:01 -0700 Subject: [PATCH 082/843] Add renderAsOriginal to selectedIcon Summary: Hey, I've created a PR a few weeks ago #7264. It got merged in and then I received some emails and got mentioned in a few issues that it doesn't use renderAsOriginal prop on selectedIcon. Instead the app would use tint color. The problem can be seen here #7467. I've now added a method in TabBarItem that sets selectedIcon to renderAsOriginal if the prop is set. I added a "relay" icon to UIExplorer TabBarIOS example so you can see the item is now rendered in color as the image supplied. Oh and also should this PR be made from master. Had to work on this issue from 0.27 because the master was broken for me. Closes https://github.com/facebook/react-native/pull/7709 Differential Revision: D3339795 fbshipit-source-id: d8d4699bb617ecae8996a6627f3774c6473c19e0 --- Examples/UIExplorer/TabBarIOSExample.js | 1 + Examples/UIExplorer/relay@3x.png | Bin 0 -> 1809 bytes React/Views/RCTTabBarItem.h | 1 + React/Views/RCTTabBarItem.m | 11 +++++++++++ React/Views/RCTTabBarItemManager.m | 2 +- 5 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 Examples/UIExplorer/relay@3x.png diff --git a/Examples/UIExplorer/TabBarIOSExample.js b/Examples/UIExplorer/TabBarIOSExample.js index e1ef10a25c8961..435f2de21edcab 100644 --- a/Examples/UIExplorer/TabBarIOSExample.js +++ b/Examples/UIExplorer/TabBarIOSExample.js @@ -82,6 +82,7 @@ var TabBarExample = React.createClass({ {@$?R%uZCUMgZ~ zDV|i-R#ZI1QhJNJNNvyi2k*Q&XJ*cvIrDi^ZBS->++y4S0Duo+Zen-o*#8p5d8y$m z`3{%H7HMZ@1Zbd(Zvgtohops8vk|zNRIg`Z&J$SjmdmhOE-lGh`PloFP?`}(KhoA#DPC1}p3?qi zfr5z6zR(C8Sd7jklNY)feNFzocM89s_Oj@q>q=5Yetw{l0SLqIT;sX9yd^!aaeIy6|HH$wg2S3r zHDxglVfga5et;!2Hh%E_Q_DBGi8UIv0^PFj*;KMz4{C|x`fFIE8%Cy!1C;7QQkjN* zOzAGAUa7eg-IenM6HX|4Pt9r5tc<8Ww%etah!IrS^vzKd!&(XErP~lA<=M}+P`s^O zx?wlWUdnxca^}1)p{d;lOwpW2o?<uW7$ttBP64dLadKeTXnPh@rte}B4*Byr2-J+RO)DwfI2 zCTkW@TWt%ij`^)#3Uy4#Y0T)fK8v*pHJex9xw{j{?-no&TAB#9Uk?fsRz8TAupVv{ zRNm{HlsV3_!23DokxwQEd>4cB2@(wf{BBM7mGSl(Ash&ww-H!o&t%v}FYErqSKuCT zEy|!oH*U_te7)S?n7L$4j>acJG-dH?Yc|w%Bre4r)hubsd|-G;>c=?#o#UAnP>OjS zH~H_Wf;02VV2n3^I>#LRi&s}EUpLk75Cx)n=cqgAK7FAbbw{+-YL*<2C5lvL=;)`i z9ZEa9j6}|y8ZW4#{^9FId?$4y1plw{rs-cwDn^n~E_1U#vRVe5r0A#9NOYN^XAa{x z_-grZP1{3l-L>*GL0|rcHZhj_;wWV$2#;^0tTl_dYG(?~;*jW(xEbbtOc717G=a-*l-^5sOK=m12;ZLr{75I^-w8I456 z1jc7c1R>}0$tnp0a{G&AqW!(Bl}CT-Yb9IulE9Rzh$2XYD>}bc)+6b-81gh!a;UJq zl1C>@iBlfiTyQqnd3$L7xuOFSzoY24NjB?(9=YRWmNdI<>SW_ZB4o5lU;Cewt{^1| zcgeyp)Y}AlrK=lbtP{@EnHg8#YVxV}A4b&GbI?Ri!u3e-vU)*|VD8(62Nwf*LGliHv5)M^?_;gdxBjzhAh zjA6Kzghc;kZ66ix@vtkwXEGWf+ELI;qEhoU`nEY^$Yl%_1Z#mJ6M_aq?`?nvx{I!F zzg6X}D3TPLX+~&GdfjUPGa^?Wyb(4DRt&@Fko4GjtILtGlYEAbm$j8oq5TYL z)-d!*=9JWxvl^a`0ej5C{Mr-koW29z=QPv&ld-Lkk>5jGqDJD5_B1yjN3gb{WzbG6 zUh^W=Y6)=N@0q|d^mDbY3GMIARr|h%xA(&j>4??gMaHq-k?#%fJ4-LzradQ0urf#d zZg-6mGC5<~?W}61JbJV#$De~8!t8xe4?41R=77x0zEw+>a&gwld(Ro4@hvd-lNzU%8>+y8)=h!<2qWi8b zOFlkdZh&2LnVmoc(*OKn$@vFWNiM1nrm7PvoQw`U21S0n-FVSAZf3U`<%dIhtC2V! zF+6ihuvb6ToDd#YMj6%}rWpb|0;u1Oflg*ESO2e9Fp|OnFMw!?cO`ksBCi3LK>UF+ JX)y9k`VS_=QW^jN literal 0 HcmV?d00001 diff --git a/React/Views/RCTTabBarItem.h b/React/Views/RCTTabBarItem.h index 30919ed8bf7a56..5631f58b7bb133 100644 --- a/React/Views/RCTTabBarItem.h +++ b/React/Views/RCTTabBarItem.h @@ -15,6 +15,7 @@ @property (nonatomic, copy) id /* NSString or NSNumber */ badge; @property (nonatomic, strong) UIImage *icon; +@property (nonatomic, strong) UIImage *selectedIcon; @property (nonatomic, assign) UITabBarSystemItem systemIcon; @property (nonatomic, assign) BOOL renderAsOriginal; @property (nonatomic, assign, getter=isSelected) BOOL selected; diff --git a/React/Views/RCTTabBarItem.m b/React/Views/RCTTabBarItem.m index 06f313f1a73955..fdc0ee52cf61fd 100644 --- a/React/Views/RCTTabBarItem.m +++ b/React/Views/RCTTabBarItem.m @@ -92,6 +92,17 @@ - (void)setIcon:(UIImage *)icon } } +- (void)setSelectedIcon:(UIImage *)selectedIcon +{ + _selectedIcon = selectedIcon; + + if (_renderAsOriginal) { + self.barItem.selectedImage = [_selectedIcon imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + } else { + self.barItem.selectedImage = _selectedIcon; + } +} + - (UIViewController *)reactViewController { return self.superview.reactViewController; diff --git a/React/Views/RCTTabBarItemManager.m b/React/Views/RCTTabBarItemManager.m index ca807c9df8ac78..9bfa13deb2a3ae 100644 --- a/React/Views/RCTTabBarItemManager.m +++ b/React/Views/RCTTabBarItemManager.m @@ -25,7 +25,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(renderAsOriginal, BOOL) RCT_EXPORT_VIEW_PROPERTY(selected, BOOL) RCT_EXPORT_VIEW_PROPERTY(icon, UIImage) -RCT_REMAP_VIEW_PROPERTY(selectedIcon, barItem.selectedImage, UIImage) +RCT_EXPORT_VIEW_PROPERTY(selectedIcon, UIImage) RCT_EXPORT_VIEW_PROPERTY(systemIcon, UITabBarSystemItem) RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) RCT_CUSTOM_VIEW_PROPERTY(title, NSString, RCTTabBarItem) From 18d6d85320cfc20605219bbcfa7ff27b57528a53 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 24 May 2016 02:43:38 -0700 Subject: [PATCH 083/843] Fixed UIExplorer Summary: CI_USE_PACKAGER flag was set only for testing, not running. Reviewed By: nathanajah Differential Revision: D3334982 fbshipit-source-id: 3f98e697bd144a4ac6d8d7cfabf546bb55de1695 --- .../xcshareddata/xcschemes/UIExplorer.xcscheme | 16 ++++++++-------- Examples/UIExplorer/UIExplorer/AppDelegate.m | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme index 4ed46558bb15ca..833aaaa4a65686 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme @@ -54,7 +54,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "NO"> + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -86,13 +86,6 @@ ReferencedContainer = "container:UIExplorer.xcodeproj"> - - - - @@ -116,6 +109,13 @@ ReferencedContainer = "container:UIExplorer.xcodeproj"> + + + + diff --git a/Examples/UIExplorer/UIExplorer/AppDelegate.m b/Examples/UIExplorer/UIExplorer/AppDelegate.m index f7c5bf49b8b7a7..1ca2716f27b630 100644 --- a/Examples/UIExplorer/UIExplorer/AppDelegate.m +++ b/Examples/UIExplorer/UIExplorer/AppDelegate.m @@ -24,6 +24,7 @@ @interface AppDelegate() @end @implementation AppDelegate + - (BOOL)application:(__unused UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { _bridge = [[RCTBridge alloc] initWithDelegate:self From a71a9efe9683f9d7cb1293a3f9e2fc10d788abc9 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Tue, 24 May 2016 03:13:21 -0700 Subject: [PATCH 084/843] Remove deprecated PullToRefreshViewAndroid and onRefreshStart / endRefreshing Summary: Removes the deprecated APIs that were replaced by `RefreshControl`. Those API have been deprecated for a while already so I think it's fine to remove them at this point. Also ported the `SwipeRefreshLayoutTestModule` test to use `RefreshControl` instead of `PullToRefreshViewAndroid`. **Test plan (required)** Made sure no references are left in the codebase to `PullToRefreshViewAndroid`, `onRefreshStart` and `endRefreshing`. Tested that `ScrollView` examples in UIExplorer still work properly. Check that the `SwipeRefreshLayoutTestModule` passes on CI. Closes https://github.com/facebook/react-native/pull/7447 Reviewed By: mkonicek Differential Revision: D3292391 Pulled By: bestander fbshipit-source-id: 27eb2443861e04a9f7319586ce2ada381b714d47 --- Libraries/Components/ScrollView/ScrollView.js | 26 ----- .../PullToRefreshViewAndroid.android.js | 107 ------------------ .../PullToRefreshViewAndroid.ios.js | 13 --- Libraries/react-native/react-native.js | 1 - Libraries/react-native/react-native.js.flow | 1 - React/Views/RCTScrollView.h | 4 - React/Views/RCTScrollView.m | 28 ----- React/Views/RCTScrollViewManager.m | 16 --- .../js/SwipeRefreshLayoutTestModule.js | 18 +-- 9 files changed, 11 insertions(+), 203 deletions(-) delete mode 100644 Libraries/PullToRefresh/PullToRefreshViewAndroid.android.js delete mode 100644 Libraries/PullToRefresh/PullToRefreshViewAndroid.ios.js diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index fdca28932f2c70..ad5e9edbf96d61 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -24,7 +24,6 @@ const StyleSheetPropType = require('StyleSheetPropType'); const View = require('View'); const ViewStylePropTypes = require('ViewStylePropTypes'); -const deprecatedPropType = require('deprecatedPropType'); const dismissKeyboard = require('dismissKeyboard'); const flattenStyle = require('flattenStyle'); const invariant = require('fbjs/lib/invariant'); @@ -311,14 +310,6 @@ const ScrollView = React.createClass({ */ refreshControl: PropTypes.element, - /** - * @platform ios - */ - onRefreshStart: deprecatedPropType( - PropTypes.func, - 'Use the `refreshControl` prop instead.' - ), - /** * Sometimes a scrollview takes up more space than its content fills. When this is * the case, this prop will fill the rest of the scrollview with a color to avoid setting @@ -348,15 +339,6 @@ const ScrollView = React.createClass({ this._scrollViewRef && this._scrollViewRef.setNativeProps(props); }, - /** - * Deprecated. Use `RefreshControl` instead. - */ - endRefreshing: function() { - RCTScrollViewManager.endRefreshing( - ReactNative.findNodeHandle(this) - ); - }, - /** * Returns a reference to the underlying scroll responder, which supports * operations like `scrollTo`. All ScrollView-like components should @@ -510,14 +492,6 @@ const ScrollView = React.createClass({ sendMomentumEvents: (this.props.onMomentumScrollBegin || this.props.onMomentumScrollEnd) ? true : false, }; - const onRefreshStart = this.props.onRefreshStart; - if (onRefreshStart) { - // this is necessary because if we set it on props, even when empty, - // it'll trigger the default pull-to-refresh behavior on native. - props.onRefreshStart = - function() { onRefreshStart && onRefreshStart(this.endRefreshing); }.bind(this); - } - const { decelerationRate } = this.props; if (decelerationRate) { props.decelerationRate = processDecelerationRate(decelerationRate); diff --git a/Libraries/PullToRefresh/PullToRefreshViewAndroid.android.js b/Libraries/PullToRefresh/PullToRefreshViewAndroid.android.js deleted file mode 100644 index ea7d5e25c41835..00000000000000 --- a/Libraries/PullToRefresh/PullToRefreshViewAndroid.android.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule PullToRefreshViewAndroid - */ -'use strict'; - -var ColorPropType = require('ColorPropType'); -var React = require('React'); -var RefreshLayoutConsts = require('UIManager').AndroidSwipeRefreshLayout.Constants; -var View = require('View'); - -var onlyChild = require('onlyChild'); -var requireNativeComponent = require('requireNativeComponent'); - -var NATIVE_REF = 'native_swiperefreshlayout'; - -/** - * Deprecated. Use `RefreshControl` instead. - * - * React view that supports a single scrollable child view (e.g. `ScrollView`). When this child - * view is at `scrollY: 0`, swiping down triggers an `onRefresh` event. - * - * The style `{flex: 1}` might be required to ensure the expected behavior of the child component - * (e.g. when the child is expected to scroll with `ScrollView` or `ListView`). - */ -var PullToRefreshViewAndroid = React.createClass({ - statics: { - SIZE: RefreshLayoutConsts.SIZE, - }, - - propTypes: { - ...View.propTypes, - /** - * Whether the pull to refresh functionality is enabled - */ - enabled: React.PropTypes.bool, - /** - * The colors (at least one) that will be used to draw the refresh indicator - */ - colors: React.PropTypes.arrayOf(ColorPropType), - /** - * The background color of the refresh indicator - */ - progressBackgroundColor: ColorPropType, - /** - * Progress view top offset - * @platform android - */ - progressViewOffset: React.PropTypes.number, - /** - * Whether the view should be indicating an active refresh - */ - refreshing: React.PropTypes.bool, - /** - * Size of the refresh indicator, see PullToRefreshViewAndroid.SIZE - */ - size: React.PropTypes.oneOf(RefreshLayoutConsts.SIZE.DEFAULT, RefreshLayoutConsts.SIZE.LARGE), - }, - - componentDidMount: function() { - console.warn('`PullToRefreshViewAndroid` is deprecated. Use `RefreshControl` instead.'); - }, - - getInnerViewNode: function() { - return this.refs[NATIVE_REF]; - }, - - setNativeProps: function(props) { - let innerViewNode = this.getInnerViewNode(); - return innerViewNode && innerViewNode.setNativeProps(props); - }, - - render: function() { - return ( - - {onlyChild(this.props.children)} - - ); - }, - - _onRefresh: function() { - this.props.onRefresh && this.props.onRefresh(); - this.setNativeProps({refreshing: !!this.props.refreshing}); - } -}); - -var NativePullToRefresh = requireNativeComponent( - 'AndroidSwipeRefreshLayout', - PullToRefreshViewAndroid -); - -module.exports = PullToRefreshViewAndroid; diff --git a/Libraries/PullToRefresh/PullToRefreshViewAndroid.ios.js b/Libraries/PullToRefresh/PullToRefreshViewAndroid.ios.js deleted file mode 100644 index 2a16f851b827c8..00000000000000 --- a/Libraries/PullToRefresh/PullToRefreshViewAndroid.ios.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule PullToRefreshViewAndroid - */ - 'use strict'; - - module.exports = require('UnimplementedView'); diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 8519008320f183..d867bd59581256 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -49,7 +49,6 @@ var ReactNative = { get SliderIOS() { return require('SliderIOS'); }, get SnapshotViewIOS() { return require('SnapshotViewIOS'); }, get Switch() { return require('Switch'); }, - get PullToRefreshViewAndroid() { return require('PullToRefreshViewAndroid'); }, get RecyclerViewBackedScrollView() { return require('RecyclerViewBackedScrollView'); }, get RefreshControl() { return require('RefreshControl'); }, get StatusBar() { return require('StatusBar'); }, diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index 0bb568e5cd242d..d300af0c4d04f2 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -48,7 +48,6 @@ var ReactNative = Object.assign(Object.create(require('ReactNative')), { SnapshotViewIOS: require('SnapshotViewIOS'), StatusBar: require('StatusBar'), Switch: require('Switch'), - PullToRefreshViewAndroid: require('PullToRefreshViewAndroid'), RecyclerViewBackedScrollView: require('RecyclerViewBackedScrollView'), RefreshControl: require('RefreshControl'), SwitchAndroid: require('SwitchAndroid'), diff --git a/React/Views/RCTScrollView.h b/React/Views/RCTScrollView.h index 7bc9c617c36248..c688fbf1ff023b 100644 --- a/React/Views/RCTScrollView.h +++ b/React/Views/RCTScrollView.h @@ -58,10 +58,6 @@ @property (nonatomic, copy) RCTDirectEventBlock onMomentumScrollEnd; @property (nonatomic, copy) RCTDirectEventBlock onScrollAnimationEnd; -// Pull-to-refresh support (deprecated - use RCTPullToRefreshControl instead) -@property (nonatomic, copy) RCTDirectEventBlock onRefreshStart; -- (void)endRefreshing; - @end @interface RCTEventDispatcher (RCTScrollView) diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index 33db538abc4cde..18aa3c952c98de 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -934,34 +934,6 @@ - (type)getter \ RCT_SET_AND_PRESERVE_OFFSET(setZoomScale, zoomScale, CGFloat); RCT_SET_AND_PRESERVE_OFFSET(setScrollIndicatorInsets, scrollIndicatorInsets, UIEdgeInsets); -- (void)setOnRefreshStart:(RCTDirectEventBlock)onRefreshStart -{ - if (!onRefreshStart) { - _onRefreshStart = nil; - _scrollView.refreshControl = nil; - return; - } - _onRefreshStart = [onRefreshStart copy]; - - if (!_scrollView.refreshControl) { - RCTRefreshControl *refreshControl = [RCTRefreshControl new]; - [refreshControl addTarget:self action:@selector(refreshControlValueChanged) forControlEvents:UIControlEventValueChanged]; - _scrollView.refreshControl = refreshControl; - } -} - -- (void)refreshControlValueChanged -{ - if (self.onRefreshStart) { - self.onRefreshStart(nil); - } -} - -- (void)endRefreshing -{ - [_scrollView.refreshControl endRefreshing]; -} - - (void)sendScrollEventWithName:(NSString *)eventName scrollView:(UIScrollView *)scrollView userData:(NSDictionary *)userData diff --git a/React/Views/RCTScrollViewManager.m b/React/Views/RCTScrollViewManager.m index 2261c4ba88da6c..9eb99a7c53d9a1 100644 --- a/React/Views/RCTScrollViewManager.m +++ b/React/Views/RCTScrollViewManager.m @@ -72,7 +72,6 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int) RCT_EXPORT_VIEW_PROPERTY(snapToAlignment, NSString) RCT_REMAP_VIEW_PROPERTY(contentOffset, scrollView.contentOffset, CGPoint) -RCT_EXPORT_VIEW_PROPERTY(onRefreshStart, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onScrollBeginDrag, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onScrollEndDrag, RCTDirectEventBlock) @@ -119,21 +118,6 @@ - (UIView *)view }]; } -RCT_EXPORT_METHOD(endRefreshing:(nonnull NSNumber *)reactTag) -{ - [self.bridge.uiManager addUIBlock: - ^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - - RCTScrollView *view = viewRegistry[reactTag]; - if (!view || ![view isKindOfClass:[RCTScrollView class]]) { - RCTLogError(@"Cannot find RCTScrollView with tag #%@", reactTag); - return; - } - - [view endRefreshing]; - }]; -} - RCT_EXPORT_METHOD(scrollTo:(nonnull NSNumber *)reactTag offsetX:(CGFloat)x offsetY:(CGFloat)y diff --git a/ReactAndroid/src/androidTest/js/SwipeRefreshLayoutTestModule.js b/ReactAndroid/src/androidTest/js/SwipeRefreshLayoutTestModule.js index 7a8e6f5224cee9..3c1c289a47b6b7 100644 --- a/ReactAndroid/src/androidTest/js/SwipeRefreshLayoutTestModule.js +++ b/ReactAndroid/src/androidTest/js/SwipeRefreshLayoutTestModule.js @@ -15,7 +15,7 @@ var BatchedBridge = require('BatchedBridge'); var React = require('React'); var RecordingModule = require('NativeModules').SwipeRefreshLayoutRecordingModule; var ScrollView = require('ScrollView'); -var PullToRefreshViewAndroid = require('PullToRefreshViewAndroid'); +var RefreshControl = require('RefreshControl'); var Text = require('Text'); var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); var View = require('View'); @@ -62,13 +62,17 @@ var SwipeRefreshLayoutTestApp = React.createClass({ rows.push(); } return ( - RecordingModule.onRefresh()}> - - {rows} - - + refreshControl={ + RecordingModule.onRefresh()} + /> + }> + {rows} + ); }, }); From 2c8abebccc7bcea59a604f9b965aeeb0de483aa6 Mon Sep 17 00:00:00 2001 From: Martin Kralik Date: Tue, 24 May 2016 05:05:51 -0700 Subject: [PATCH 085/843] improved description for RCTTouchEvent Summary: This will make error messages more helpful. Reviewed By: javache Differential Revision: D3292400 fbshipit-source-id: d1e0bb24593058b75422824c0d351ede1320029e --- React/Base/RCTTouchEvent.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/React/Base/RCTTouchEvent.m b/React/Base/RCTTouchEvent.m index dcf6e48f0c3e73..16b5cc0abecf4c 100644 --- a/React/Base/RCTTouchEvent.m +++ b/React/Base/RCTTouchEvent.m @@ -82,4 +82,9 @@ - (uint16_t)coalescingKey return _coalescingKey; } +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p; name = %@; coalescing key = %hu>", [self class], self, _eventName, _coalescingKey]; +} + @end From 6629ae9fdfd3bd14a4eac65df27ff78fbeeea3ad Mon Sep 17 00:00:00 2001 From: David Aurelio Date: Tue, 24 May 2016 05:09:39 -0700 Subject: [PATCH 086/843] adapt instantiation of node-haste/DependencyGraph to new version Reviewed By: bestander Differential Revision: D3339969 fbshipit-source-id: 2b81f8019223b060f3e3afb940cc58360ed024e5 --- packager/react-packager/src/Resolver/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packager/react-packager/src/Resolver/index.js b/packager/react-packager/src/Resolver/index.js index 5f5dc98e6569b0..865cd683b5dcc8 100644 --- a/packager/react-packager/src/Resolver/index.js +++ b/packager/react-packager/src/Resolver/index.js @@ -102,7 +102,7 @@ class Resolver { // remove it from here. 'parse', ], - platforms: ['ios', 'android', 'windows'], + platforms: ['ios', 'android', 'windows', 'web'], preferNativePlatform: true, fileWatcher: opts.fileWatcher, cache: opts.cache, From 3d8725dfdbdc1345eb34c409e1a514c845b79dcf Mon Sep 17 00:00:00 2001 From: David Aurelio Date: Tue, 24 May 2016 06:48:42 -0700 Subject: [PATCH 087/843] Respect original enumerability/writability when polyfilling globals Summary: Reuses the original property descriptor when overwriting / polyfilling globals to ensure enumerability and writability are the same Closes https://github.com/facebook/react-native/pull/7704 Differential Revision: D3338119 Pulled By: davidaurelio fbshipit-source-id: ab456324a3346cd3ec8b2c3e3a2378408c92087c --- .../InitializeJavaScriptAppEngine.js | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index acc5890a789353..bcdc888297f00b 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -64,40 +64,47 @@ function setUpConsole() { * https://github.com/facebook/react-native/issues/934 */ function polyfillGlobal(name, newValue, scope = global) { - const descriptor = Object.getOwnPropertyDescriptor(scope, name) || { - // jest for some bad reasons runs the polyfill code multiple times. In jest - // environment, XmlHttpRequest doesn't exist so getOwnPropertyDescriptor - // returns undefined and defineProperty default for writable is false. - // Therefore, the second time it runs, defineProperty will fatal :( - writable: true, - }; - - if (scope[name] !== undefined) { + const descriptor = Object.getOwnPropertyDescriptor(scope, name); + if (descriptor) { const backupName = `original${name[0].toUpperCase()}${name.substr(1)}`; Object.defineProperty(scope, backupName, {...descriptor, value: scope[name]}); } - Object.defineProperty(scope, name, {...descriptor, value: newValue}); + const {enumerable, writable} = descriptor || {}; + + // jest for some bad reasons runs the polyfill code multiple times. In jest + // environment, XmlHttpRequest doesn't exist so getOwnPropertyDescriptor + // returns undefined and defineProperty default for writable is false. + // Therefore, the second time it runs, defineProperty will fatal :( + + Object.defineProperty(scope, name, { + configurable: true, + enumerable: enumerable !== false, + writable: writable !== false, + value: newValue, + }); } function polyfillLazyGlobal(name, valueFn, scope = global) { - if (scope[name] !== undefined) { - const descriptor = Object.getOwnPropertyDescriptor(scope, name); + const descriptor = getPropertyDescriptor(scope, name); + if (descriptor) { const backupName = `original${name[0].toUpperCase()}${name.substr(1)}`; - Object.defineProperty(scope, backupName, {...descriptor, value: scope[name]}); + Object.defineProperty(scope, backupName, descriptor); } + const {enumerable, writable} = descriptor || {}; Object.defineProperty(scope, name, { configurable: true, - enumerable: true, + enumerable: enumerable !== false, get() { return (this[name] = valueFn()); }, set(value) { Object.defineProperty(this, name, { configurable: true, - enumerable: true, - value + enumerable: enumerable !== false, + writable: writable !== false, + value, }); } }); @@ -211,6 +218,16 @@ function setUpDevTools() { } } +function getPropertyDescriptor(object, name) { + while (object) { + const descriptor = Object.getOwnPropertyDescriptor(object, name); + if (descriptor) { + return descriptor; + } + object = Object.getPrototypeOf(object); + } +} + setUpProcess(); setUpConsole(); setUpTimers(); From 71133670007ed9b6f0f072b6998e66aebf27e2b9 Mon Sep 17 00:00:00 2001 From: taelimoh Date: Tue, 24 May 2016 08:01:18 -0700 Subject: [PATCH 088/843] Changed module name from 'ReactNative Summary: Platform is in React-Native not React. Closes https://github.com/facebook/react-native/pull/7718 Differential Revision: D3340334 fbshipit-source-id: 3816f8966bc5b675097182aaf3304d465fbebb95 --- docs/PlatformSpecificInformation.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/PlatformSpecificInformation.md b/docs/PlatformSpecificInformation.md index 1f43f610a6ca5e..260b00b4661972 100644 --- a/docs/PlatformSpecificInformation.md +++ b/docs/PlatformSpecificInformation.md @@ -46,7 +46,7 @@ React Native will import the correct component for the running platform. A module is provided by React Native to detect what is the platform in which the app is running. This piece of functionality can be useful when only small parts of a component are platform specific. ```javascript -var { Platform } = React; +var { Platform } = ReactNative; var styles = StyleSheet.create({ height: (Platform.OS === 'ios') ? 200 : 100, @@ -59,7 +59,7 @@ There is also a `Platform.select` method available, that given an object contain returns the value for the platform you are currently running on. ```javascript -var { Platform } = React; +var { Platform } = ReactNative; var styles = StyleSheet.create({ container: { @@ -94,7 +94,7 @@ var Component = Platform.select({ On Android, the Platform module can be also used to detect which is the version of the Android Platform in which the app is running ```javascript -var {Platform} = React; +var {Platform} = ReactNative; if(Platform.Version === 21){ console.log('Running on Lollipop!'); From 71bf8a3e48f8da93746938330aa3660b015af7c8 Mon Sep 17 00:00:00 2001 From: Ben Nham Date: Tue, 24 May 2016 08:39:40 -0700 Subject: [PATCH 089/843] Only clear image contents on memory warning Summary: Some apps are complaining about flashing images when performing navigation transitions. An example issue would be: 1. Load a master list view with many images 2. Click on an image to go to a detail view 3. Go back to the master list view At step (3), users see a number of images flash from a placeholder image back to the final image because `-[RCTImageView didMoveToWindow]` calls `clearImage` when the image view exits the view hierarchy between (1) and (2) and calls `reloadImage` (which sets the image property asynchronously) when the image view re-enters the view hiearchy between (2) and (3). This diff fixes the issue by being less aggressive about clearing image contents. It only clears image contents when the app receives a memory warning or the app goes into the background. For comparison, CKNetworkImageComponent in ComponentKit doesn't have this purging behavior at all. Reviewed By: javache Differential Revision: D3325009 fbshipit-source-id: efca10099cdfdb49afbb3f550854d4b8a40511d0 --- Libraries/Image/RCTImageView.m | 41 +++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index b805cd9b6935d1..1376fb79649487 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -62,10 +62,25 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge { if ((self = [super init])) { _bridge = bridge; + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(clearImageIfDetached) + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; + [center addObserver:self + selector:@selector(clearImageIfDetached) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; } return self; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + RCT_NOT_IMPLEMENTED(- (instancetype)init) - (void)updateImage @@ -172,6 +187,13 @@ - (void)clearImage self.image = nil; } +- (void)clearImageIfDetached +{ + if (!self.window) { + [self clearImage]; + } +} + - (void)reloadImage { [self cancelImageLoad]; @@ -294,24 +316,7 @@ - (void)didMoveToWindow { [super didMoveToWindow]; - if (!self.window) { - // Don't keep self alive through the asynchronous dispatch, if the intention - // was to remove the view so it would deallocate. - __weak typeof(self) weakSelf = self; - - dispatch_async(dispatch_get_main_queue(), ^{ - __strong typeof(self) strongSelf = weakSelf; - if (!strongSelf) { - return; - } - - // If we haven't been re-added to a window by this run loop iteration, - // clear out the image to save memory. - if (!strongSelf.window) { - [strongSelf clearImage]; - } - }); - } else if (!self.image || self.image == _defaultImage) { + if (self.window && (!self.image || self.image == _defaultImage)) { [self reloadImage]; } } From 3f08fe4b7f6ce7f0cd6850e7598ebb5c3b104700 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 24 May 2016 10:26:33 -0700 Subject: [PATCH 090/843] Update RCTNetworking, RCTNetInfo and RCTLocationManager to use new events system Summary: Updated networking and geolocation to use the new events system. Reviewed By: javache Differential Revision: D3339945 fbshipit-source-id: f1332fb2aab8560e4783739e223c1f31d583cfcf --- Examples/UIExplorer/AppStateExample.js | 32 +++++- Examples/UIExplorer/AppStateIOSExample.js | 99 ------------------- Examples/UIExplorer/UIExplorerList.ios.js | 4 - .../RCTEventDispatcherTests.m | 2 - Examples/UIExplorer/XHRExample.ios.js | 11 ++- Libraries/Geolocation/Geolocation.js | 17 ++-- Libraries/Geolocation/RCTLocationObserver.h | 4 +- Libraries/Geolocation/RCTLocationObserver.m | 20 ++-- Libraries/LinkingIOS/RCTLinkingManager.m | 3 + Libraries/Network/NetInfo.js | 6 +- Libraries/Network/RCTNetInfo.h | 6 +- Libraries/Network/RCTNetInfo.m | 41 ++++---- Libraries/Network/RCTNetworking.android.js | 55 ++++++++--- Libraries/Network/RCTNetworking.h | 6 +- Libraries/Network/RCTNetworking.ios.js | 38 +++++-- Libraries/Network/RCTNetworking.m | 37 +++---- Libraries/Network/XMLHttpRequest.android.js | 62 ------------ Libraries/Network/XMLHttpRequest.ios.js | 47 --------- ...MLHttpRequestBase.js => XMLHttpRequest.js} | 27 +++-- ...estBase-test.js => XMLHttpRequest-test.js} | 8 +- .../RCTPushNotificationManager.m | 9 ++ Libraries/react-native/react-native.js | 14 +-- Libraries/react-native/react-native.js.flow | 2 + React/Modules/RCTAccessibilityManager.m | 3 + React/Modules/RCTDevMenu.m | 9 ++ React/Modules/RCTEventEmitter.m | 4 +- React/Modules/RCTUIManager.m | 3 + 27 files changed, 229 insertions(+), 340 deletions(-) delete mode 100644 Examples/UIExplorer/AppStateIOSExample.js delete mode 100644 Libraries/Network/XMLHttpRequest.android.js delete mode 100644 Libraries/Network/XMLHttpRequest.ios.js rename Libraries/Network/{XMLHttpRequestBase.js => XMLHttpRequest.js} (95%) rename Libraries/Network/__tests__/{XMLHttpRequestBase-test.js => XMLHttpRequest-test.js} (95%) diff --git a/Examples/UIExplorer/AppStateExample.js b/Examples/UIExplorer/AppStateExample.js index 8140394142024d..a5f2e55edeec69 100644 --- a/Examples/UIExplorer/AppStateExample.js +++ b/Examples/UIExplorer/AppStateExample.js @@ -1,4 +1,11 @@ /** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * * The examples provided by Facebook are for non-commercial testing and * evaluation purposes only. * @@ -16,9 +23,9 @@ */ 'use strict'; -var React = require('react'); -var ReactNative = require('react-native'); -var { +const React = require('react'); +const ReactNative = require('react-native'); +const { AppState, Text, View @@ -29,13 +36,19 @@ var AppStateSubscription = React.createClass({ return { appState: AppState.currentState, previousAppStates: [], + memoryWarnings: 0, }; }, componentDidMount: function() { AppState.addEventListener('change', this._handleAppStateChange); + AppState.addEventListener('memoryWarning', this._handleMemoryWarning); }, componentWillUnmount: function() { AppState.removeEventListener('change', this._handleAppStateChange); + AppState.removeEventListener('memoryWarning', this._handleMemoryWarning); + }, + _handleMemoryWarning: function() { + this.setState({memoryWarnings: this.state.memoryWarnings + 1}); }, _handleAppStateChange: function(appState) { var previousAppStates = this.state.previousAppStates.slice(); @@ -46,6 +59,13 @@ var AppStateSubscription = React.createClass({ }); }, render() { + if (this.props.showMemoryWarnings) { + return ( + + {this.state.memoryWarnings} + + ); + } if (this.props.showCurrentOnly) { return ( @@ -78,4 +98,10 @@ exports.examples = [ title: 'Previous states:', render(): ReactElement { return ; } }, + { + platform: 'ios', + title: 'Memory Warnings', + description: 'In the IOS simulator, hit Shift+Command+M to simulate a memory warning.', + render(): ReactElement { return ; } + }, ]; diff --git a/Examples/UIExplorer/AppStateIOSExample.js b/Examples/UIExplorer/AppStateIOSExample.js deleted file mode 100644 index 10aaed8aadd0ff..00000000000000 --- a/Examples/UIExplorer/AppStateIOSExample.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - * @providesModule AppStateIOSExample - * @flow - */ -'use strict'; - -var React = require('react'); -var ReactNative = require('react-native'); -var { - AppStateIOS, - Text, - View -} = ReactNative; - -var AppStateSubscription = React.createClass({ - getInitialState() { - return { - appState: AppStateIOS.currentState, - previousAppStates: [], - memoryWarnings: 0, - }; - }, - componentDidMount: function() { - AppStateIOS.addEventListener('change', this._handleAppStateChange); - AppStateIOS.addEventListener('memoryWarning', this._handleMemoryWarning); - }, - componentWillUnmount: function() { - AppStateIOS.removeEventListener('change', this._handleAppStateChange); - AppStateIOS.removeEventListener('memoryWarning', this._handleMemoryWarning); - }, - _handleMemoryWarning: function() { - this.setState({memoryWarnings: this.state.memoryWarnings + 1}); - }, - _handleAppStateChange: function(appState) { - var previousAppStates = this.state.previousAppStates.slice(); - previousAppStates.push(this.state.appState); - this.setState({ - appState, - previousAppStates, - }); - }, - render() { - if (this.props.showMemoryWarnings) { - return ( - - {this.state.memoryWarnings} - - ); - } - if (this.props.showCurrentOnly) { - return ( - - {this.state.appState} - - ); - } - return ( - - {JSON.stringify(this.state.previousAppStates)} - - ); - } -}); - -exports.title = 'AppStateIOS'; -exports.description = 'iOS app background status'; -exports.examples = [ - { - title: 'AppStateIOS.currentState', - description: 'Can be null on app initialization', - render() { return {AppStateIOS.currentState}; } - }, - { - title: 'Subscribed AppStateIOS:', - description: 'This changes according to the current state, so you can only ever see it rendered as "active"', - render(): ReactElement { return ; } - }, - { - title: 'Previous states:', - render(): ReactElement { return ; } - }, - { - title: 'Memory Warnings', - description: 'In the simulator, hit Shift+Command+M to simulate a memory warning.', - render(): ReactElement { return ; } - }, -]; diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index c63763e1e5813d..e9ddd367b7572d 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -171,10 +171,6 @@ const APIExamples: Array = [ key: 'AnExApp', module: require('./AnimatedGratuitousApp/AnExApp'), }, - { - key: 'AppStateIOSExample', - module: require('./AppStateIOSExample'), - }, { key: 'AppStateExample', module: require('./AppStateExample'), diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m index 4fe88ee933b28f..e9559d55840dda 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m @@ -105,9 +105,7 @@ - (void)testLegacyEventsAreImmediatelyDispatched #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_eventDispatcher sendDeviceEventWithName:_eventName body:_body]; - #pragma clang diagnostic pop [_bridge verify]; diff --git a/Examples/UIExplorer/XHRExample.ios.js b/Examples/UIExplorer/XHRExample.ios.js index b84c8b0a090918..0deaa4eedf45fe 100644 --- a/Examples/UIExplorer/XHRExample.ios.js +++ b/Examples/UIExplorer/XHRExample.ios.js @@ -1,4 +1,11 @@ /** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * * The examples provided by Facebook are for non-commercial testing and * evaluation purposes only. * @@ -21,7 +28,7 @@ var { AlertIOS, CameraRoll, Image, - LinkingIOS, + Linking, ProgressViewIOS, StyleSheet, Text, @@ -215,7 +222,7 @@ class FormUploader extends React.Component { return; } var url = xhr.responseText.slice(index).split('\n')[0]; - LinkingIOS.openURL(url); + Linking.openURL(url); }; var formdata = new FormData(); if (this.state.randomPhoto) { diff --git a/Libraries/Geolocation/Geolocation.js b/Libraries/Geolocation/Geolocation.js index f49a9ebf0eaf94..f2eed384b1c745 100644 --- a/Libraries/Geolocation/Geolocation.js +++ b/Libraries/Geolocation/Geolocation.js @@ -11,15 +11,16 @@ */ 'use strict'; -var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); -var RCTLocationObserver = require('NativeModules').LocationObserver; +const NativeEventEmitter = require('NativeEventEmitter'); +const RCTLocationObserver = require('NativeModules').LocationObserver; -var invariant = require('fbjs/lib/invariant'); -var logError = require('logError'); -var warning = require('fbjs/lib/warning'); +const invariant = require('fbjs/lib/invariant'); +const logError = require('logError'); +const warning = require('fbjs/lib/warning'); -var subscriptions = []; +const LocationEventEmitter = new NativeEventEmitter(RCTLocationObserver); +var subscriptions = []; var updatesEnabled = false; type GeoOptions = { @@ -80,11 +81,11 @@ var Geolocation = { } var watchID = subscriptions.length; subscriptions.push([ - RCTDeviceEventEmitter.addListener( + LocationEventEmitter.addListener( 'geolocationDidChange', success ), - error ? RCTDeviceEventEmitter.addListener( + error ? LocationEventEmitter.addListener( 'geolocationError', error ) : null, diff --git a/Libraries/Geolocation/RCTLocationObserver.h b/Libraries/Geolocation/RCTLocationObserver.h index 873eeff6a11375..a607155584a7fa 100644 --- a/Libraries/Geolocation/RCTLocationObserver.h +++ b/Libraries/Geolocation/RCTLocationObserver.h @@ -7,8 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTBridgeModule.h" +#import "RCTEventEmitter.h" -@interface RCTLocationObserver : NSObject +@interface RCTLocationObserver : RCTEventEmitter @end diff --git a/Libraries/Geolocation/RCTLocationObserver.m b/Libraries/Geolocation/RCTLocationObserver.m index 23179c5cab214e..319477c99ac0b7 100644 --- a/Libraries/Geolocation/RCTLocationObserver.m +++ b/Libraries/Geolocation/RCTLocationObserver.m @@ -113,8 +113,6 @@ @implementation RCTLocationObserver RCT_EXPORT_MODULE() -@synthesize bridge = _bridge; - #pragma mark - Lifecycle - (void)dealloc @@ -128,8 +126,12 @@ - (dispatch_queue_t)methodQueue return dispatch_get_main_queue(); } -#pragma mark - Private API +- (NSArray *)supportedEvents +{ + return @[@"geolocationDidChange", @"geolocationError"]; +} +#pragma mark - Private API - (void)beginLocationUpdatesWithDesiredAccuracy:(CLLocationAccuracy)desiredAccuracy distanceFilter:(CLLocationDistance)distanceFilter { @@ -279,11 +281,7 @@ - (void)locationManager:(CLLocationManager *)manager // Send event if (_observingLocation) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"geolocationDidChange" - body:_lastLocationEvent]; -#pragma clang diagnostic pop + [self sendEventWithName:@"geolocationDidChange" body:_lastLocationEvent]; } // Fire all queued callbacks @@ -324,11 +322,7 @@ - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError * // Send event if (_observingLocation) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"geolocationError" - body:jsError]; -#pragma clang diagnostic pop + [self sendEventWithName:@"geolocationError" body:jsError]; } // Fire all queued error callbacks diff --git a/Libraries/LinkingIOS/RCTLinkingManager.m b/Libraries/LinkingIOS/RCTLinkingManager.m index 96f9e75b49c905..ca4112fc75c9fe 100644 --- a/Libraries/LinkingIOS/RCTLinkingManager.m +++ b/Libraries/LinkingIOS/RCTLinkingManager.m @@ -88,8 +88,11 @@ + (BOOL)application:(UIApplication *)application - (void)handleOpenURLNotification:(NSNotification *)notification { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"openURL" body:notification.userInfo]; +#pragma clang diagnostic pop } RCT_EXPORT_METHOD(openURL:(NSURL *)URL diff --git a/Libraries/Network/NetInfo.js b/Libraries/Network/NetInfo.js index 768fb795f8196a..70d513cc8d813b 100644 --- a/Libraries/Network/NetInfo.js +++ b/Libraries/Network/NetInfo.js @@ -12,12 +12,14 @@ 'use strict'; const Map = require('Map'); +const NativeEventEmitter = require('NativeEventEmitter'); const NativeModules = require('NativeModules'); const Platform = require('Platform'); -const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); const RCTNetInfo = NativeModules.NetInfo; const deprecatedCallback = require('deprecatedCallback'); +const NetInfoEventEmitter = new NativeEventEmitter(RCTNetInfo); + const DEVICE_CONNECTIVITY_EVENT = 'networkStatusDidChange'; type ChangeEventName = $Enum<{ @@ -176,7 +178,7 @@ const NetInfo = { eventName: ChangeEventName, handler: Function ): {remove: () => void} { - const listener = RCTDeviceEventEmitter.addListener( + const listener = NetInfoEventEmitter.addListener( DEVICE_CONNECTIVITY_EVENT, (appStateData) => { handler(appStateData.network_info); diff --git a/Libraries/Network/RCTNetInfo.h b/Libraries/Network/RCTNetInfo.h index 6c2556e0a50b6e..9c280b2cf4073a 100644 --- a/Libraries/Network/RCTNetInfo.h +++ b/Libraries/Network/RCTNetInfo.h @@ -9,10 +9,10 @@ #import -#import "RCTBridgeModule.h" +#import "RCTEventEmitter.h" -@interface RCTNetInfo : NSObject +@interface RCTNetInfo : RCTEventEmitter -- (instancetype)initWithHost:(NSString *)host NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithHost:(NSString *)host; @end diff --git a/Libraries/Network/RCTNetInfo.m b/Libraries/Network/RCTNetInfo.m index 28d0fc07387039..5981e9f7f82a08 100644 --- a/Libraries/Network/RCTNetInfo.m +++ b/Libraries/Network/RCTNetInfo.m @@ -22,10 +22,9 @@ @implementation RCTNetInfo { SCNetworkReachabilityRef _reachability; NSString *_status; + NSString *_host; } -@synthesize bridge = _bridge; - RCT_EXPORT_MODULE() static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info) @@ -51,11 +50,7 @@ static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SC if (![status isEqualToString:self->_status]) { self->_status = status; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [self->_bridge.eventDispatcher sendDeviceEventWithName:@"networkStatusDidChange" - body:@{@"network_info": status}]; -#pragma clang diagnostic pop + [self sendEventWithName:@"networkStatusDidChange" body:@{@"network_info": status}]; } } @@ -66,34 +61,40 @@ - (instancetype)initWithHost:(NSString *)host RCTAssertParam(host); RCTAssert(![host hasPrefix:@"http"], @"Host value should just contain the domain, not the URL scheme."); - if ((self = [super init])) { - _status = RCTReachabilityStateUnknown; - _reachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, host.UTF8String); - SCNetworkReachabilityContext context = { 0, ( __bridge void *)self, NULL, NULL, NULL }; - SCNetworkReachabilitySetCallback(_reachability, RCTReachabilityCallback, &context); - SCNetworkReachabilityScheduleWithRunLoop(_reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); + if ((self = [self init])) { + _host = [host copy]; } return self; } -- (instancetype)init +- (NSArray *)supportedEvents { - return [self initWithHost:@"apple.com"]; + return @[@"networkStatusDidChange"]; } -- (void)dealloc +- (void)startObserving { - SCNetworkReachabilityUnscheduleFromRunLoop(_reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); - CFRelease(_reachability); + _status = RCTReachabilityStateUnknown; + _reachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, _host.UTF8String ?: "apple.com"); + SCNetworkReachabilityContext context = { 0, ( __bridge void *)self, NULL, NULL, NULL }; + SCNetworkReachabilitySetCallback(_reachability, RCTReachabilityCallback, &context); + SCNetworkReachabilityScheduleWithRunLoop(_reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); +} + +- (void)stopObserving +{ + if (_reachability) { + SCNetworkReachabilityUnscheduleFromRunLoop(_reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); + CFRelease(_reachability); + } } #pragma mark - Public API -// TODO: remove error callback - not needed except by Subscribable interface RCT_EXPORT_METHOD(getCurrentConnectivity:(RCTPromiseResolveBlock)resolve reject:(__unused RCTPromiseRejectBlock)reject) { - resolve(@{@"network_info": _status}); + resolve(@{@"network_info": _status ?: RCTReachabilityStateUnknown}); } @end diff --git a/Libraries/Network/RCTNetworking.android.js b/Libraries/Network/RCTNetworking.android.js index 38f694d5452f57..23aa9e8bf54199 100644 --- a/Libraries/Network/RCTNetworking.android.js +++ b/Libraries/Network/RCTNetworking.android.js @@ -12,39 +12,66 @@ // Do not require the native RCTNetworking module directly! Use this wrapper module instead. // It will add the necessary requestId, so that you don't have to generate it yourself. -var RCTNetworkingNative = require('NativeModules').Networking; +const FormData = require('FormData'); +const NativeEventEmitter = require('NativeEventEmitter'); +const RCTNetworkingNative = require('NativeModules').Networking; -var _requestId = 1; -var generateRequestId = function() { +type Header = [string, string]; + +function convertHeadersMapToArray(headers: Object): Array
    { + const headerArray = []; + for (const name in headers) { + headerArray.push([name, headers[name]]); + } + return headerArray; +} + +let _requestId = 1; +function generateRequestId() { return _requestId++; -}; +} /** * This class is a wrapper around the native RCTNetworking module. It adds a necessary unique * requestId to each network request that can be used to abort that request later on. */ -class RCTNetworking { +class RCTNetworking extends NativeEventEmitter { + + constructor() { + super(RCTNetworkingNative); + } - static sendRequest(method, url, headers, data, useIncrementalUpdates, timeout) { - var requestId = generateRequestId(); + sendRequest(method, url, headers, data, incrementalUpdates, timeout, callback) { + if (typeof data === 'string') { + data = {string: data}; + } else if (data instanceof FormData) { + data = { + formData: data.getParts().map((part) => { + part.headers = convertHeadersMapToArray(part.headers); + return part; + }), + }; + } + const requestId = generateRequestId(); RCTNetworkingNative.sendRequest( method, url, requestId, - headers, + convertHeadersMapToArray(headers), data, - useIncrementalUpdates, - timeout); - return requestId; + incrementalUpdates, + timeout + ); + callback(requestId); } - static abortRequest(requestId) { + abortRequest(requestId) { RCTNetworkingNative.abortRequest(requestId); } - static clearCookies(callback) { + clearCookies(callback) { RCTNetworkingNative.clearCookies(callback); } } -module.exports = RCTNetworking; +module.exports = new RCTNetworking(); diff --git a/Libraries/Network/RCTNetworking.h b/Libraries/Network/RCTNetworking.h index cd8b99dde4df3d..a06a1eeab95550 100644 --- a/Libraries/Network/RCTNetworking.h +++ b/Libraries/Network/RCTNetworking.h @@ -7,12 +7,10 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import - -#import "RCTBridge.h" +#import "RCTEventEmitter.h" #import "RCTNetworkTask.h" -@interface RCTNetworking : NSObject +@interface RCTNetworking : RCTEventEmitter /** * Does a handler exist for the specified request? diff --git a/Libraries/Network/RCTNetworking.ios.js b/Libraries/Network/RCTNetworking.ios.js index 677f2c1c94a720..3e0e4c35284119 100644 --- a/Libraries/Network/RCTNetworking.ios.js +++ b/Libraries/Network/RCTNetworking.ios.js @@ -10,21 +10,39 @@ */ 'use strict'; -var RCTNetworkingNative = require('NativeModules').Networking; +const FormData = require('FormData'); +const NativeEventEmitter = require('NativeEventEmitter'); +const RCTNetworkingNative = require('NativeModules').Networking; -/** - * This class is a wrapper around the native RCTNetworking module. - */ -class RCTNetworking { +class RCTNetworking extends NativeEventEmitter { - static sendRequest(query, callback) { - RCTNetworkingNative.sendRequest(query, callback); + constructor() { + super(RCTNetworkingNative); } - static abortRequest(requestId) { - RCTNetworkingNative.cancelRequest(requestId); + sendRequest(method, url, headers, data, incrementalUpdates, timeout, callback) { + if (typeof data === 'string') { + data = {string: data}; + } else if (data instanceof FormData) { + data = {formData: data.getParts()}; + } + RCTNetworkingNative.sendRequest({ + method, + url, + data, + headers, + incrementalUpdates, + timeout + }, callback); } + abortRequest(requestId) { + RCTNetworkingNative.abortRequest(requestId); + } + + clearCookies(callback) { + console.warn('RCTNetworking.clearCookies is not supported on iOS'); + } } -module.exports = RCTNetworking; +module.exports = new RCTNetworking(); diff --git a/Libraries/Network/RCTNetworking.m b/Libraries/Network/RCTNetworking.m index 5c7e3b9ee0edcb..07c21f095d3fad 100644 --- a/Libraries/Network/RCTNetworking.m +++ b/Libraries/Network/RCTNetworking.m @@ -129,11 +129,18 @@ @implementation RCTNetworking NSArray> *_handlers; } -@synthesize bridge = _bridge; @synthesize methodQueue = _methodQueue; RCT_EXPORT_MODULE() +- (NSArray *)supportedEvents +{ + return @[@"didCompleteNetworkResponse", + @"didReceiveNetworkResponse", + @"didSendNetworkData", + @"didReceiveNetworkData"]; +} + - (id)handlerForRequest:(NSURLRequest *)request { if (!request.URL) { @@ -142,7 +149,7 @@ @implementation RCTNetworking if (!_handlers) { // Get handlers, sorted in reverse priority order (highest priority first) - _handlers = [[_bridge modulesConformingToProtocol:@protocol(RCTURLRequestHandler)] sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { + _handlers = [[self.bridge modulesConformingToProtocol:@protocol(RCTURLRequestHandler)] sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { float priorityA = [a respondsToSelector:@selector(handlerPriority)] ? [a handlerPriority] : 0; float priorityB = [b respondsToSelector:@selector(handlerPriority)] ? [b handlerPriority] : 0; if (priorityA > priorityB) { @@ -346,11 +353,7 @@ - (void)sendData:(NSData *)data forTask:(RCTNetworkTask *)task } NSArray *responseJSON = @[task.requestID, responseText ?: @""]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkData" - body:responseJSON]; -#pragma clang diagnostic pop + [self sendEventWithName:@"didReceiveNetworkData" body:responseJSON]; } - (void)sendRequest:(NSURLRequest *)request @@ -364,10 +367,7 @@ - (void)sendRequest:(NSURLRequest *)request RCTURLRequestProgressBlock uploadProgressBlock = ^(int64_t progress, int64_t total) { dispatch_async(_methodQueue, ^{ NSArray *responseJSON = @[task.requestID, @((double)progress), @((double)total)]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"didSendNetworkData" body:responseJSON]; -#pragma clang diagnostic pop + [self sendEventWithName:@"didSendNetworkData" body:responseJSON]; }); }; @@ -385,11 +385,7 @@ - (void)sendRequest:(NSURLRequest *)request } id responseURL = response.URL ? response.URL.absoluteString : [NSNull null]; NSArray *responseJSON = @[task.requestID, @(status), headers, responseURL]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse" - body:responseJSON]; -#pragma clang diagnostic pop + [self sendEventWithName:@"didReceiveNetworkResponse" body:responseJSON]; }); }; @@ -410,12 +406,7 @@ - (void)sendRequest:(NSURLRequest *)request error.code == kCFURLErrorTimedOut ? @YES : @NO ]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse" - body:responseJSON]; -#pragma clang diagnostic pop - + [self sendEventWithName:@"didCompleteNetworkResponse" body:responseJSON]; [_tasksByRequestID removeObjectForKey:task.requestID]; }); }; @@ -469,7 +460,7 @@ - (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request }]; } -RCT_EXPORT_METHOD(cancelRequest:(nonnull NSNumber *)requestID) +RCT_EXPORT_METHOD(abortRequest:(nonnull NSNumber *)requestID) { [_tasksByRequestID[requestID] cancel]; [_tasksByRequestID removeObjectForKey:requestID]; diff --git a/Libraries/Network/XMLHttpRequest.android.js b/Libraries/Network/XMLHttpRequest.android.js deleted file mode 100644 index 099df7526f0511..00000000000000 --- a/Libraries/Network/XMLHttpRequest.android.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule XMLHttpRequest - * @flow - */ -'use strict'; - -var FormData = require('FormData'); -var RCTNetworking = require('RCTNetworking'); -var XMLHttpRequestBase = require('XMLHttpRequestBase'); - -type Header = [string, string]; - -function convertHeadersMapToArray(headers: Object): Array
    { - var headerArray = []; - for (var name in headers) { - headerArray.push([name, headers[name]]); - } - return headerArray; -} - -class XMLHttpRequest extends XMLHttpRequestBase { - sendImpl( - method: ?string, - url: ?string, - headers: Object, - data: any, - useIncrementalUpdates: boolean, - timeout: number, - ): void { - var body; - if (typeof data === 'string') { - body = {string: data}; - } else if (data instanceof FormData) { - body = { - formData: data.getParts().map((part) => { - part.headers = convertHeadersMapToArray(part.headers); - return part; - }), - }; - } else { - body = data; - } - var requestId = RCTNetworking.sendRequest( - method, - url, - convertHeadersMapToArray(headers), - body, - useIncrementalUpdates, - timeout - ); - this.didCreateRequest(requestId); - } -} - -module.exports = XMLHttpRequest; diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js deleted file mode 100644 index dd4ee0dd63d3c6..00000000000000 --- a/Libraries/Network/XMLHttpRequest.ios.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule XMLHttpRequest - * @flow - */ -'use strict'; - -var FormData = require('FormData'); -var RCTNetworking = require('RCTNetworking'); - -var XMLHttpRequestBase = require('XMLHttpRequestBase'); - -class XMLHttpRequest extends XMLHttpRequestBase { - sendImpl( - method: ?string, - url: ?string, - headers: Object, - data: any, - incrementalUpdates: boolean, - timeout: number, - ): void { - if (typeof data === 'string') { - data = {string: data}; - } else if (data instanceof FormData) { - data = {formData: data.getParts()}; - } - RCTNetworking.sendRequest( - { - method, - url, - data, - headers, - incrementalUpdates, - timeout - }, - this.didCreateRequest.bind(this) - ); - } -} - -module.exports = XMLHttpRequest; diff --git a/Libraries/Network/XMLHttpRequestBase.js b/Libraries/Network/XMLHttpRequest.js similarity index 95% rename from Libraries/Network/XMLHttpRequestBase.js rename to Libraries/Network/XMLHttpRequest.js index d5d5da5c57707e..7b742c62fb6133 100644 --- a/Libraries/Network/XMLHttpRequestBase.js +++ b/Libraries/Network/XMLHttpRequest.js @@ -6,13 +6,12 @@ * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * - * @providesModule XMLHttpRequestBase + * @providesModule XMLHttpRequest * @flow */ 'use strict'; -var RCTNetworking = require('RCTNetworking'); -var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +const RCTNetworking = require('RCTNetworking'); const EventTarget = require('event-target-shim'); const invariant = require('fbjs/lib/invariant'); @@ -212,19 +211,19 @@ class XMLHttpRequestBase extends EventTarget(...XHR_EVENTS) { didCreateRequest(requestId: number): void { this._requestId = requestId; - this._subscriptions.push(RCTDeviceEventEmitter.addListener( + this._subscriptions.push(RCTNetworking.addListener( 'didSendNetworkData', (args) => this.__didUploadProgress(...args) )); - this._subscriptions.push(RCTDeviceEventEmitter.addListener( + this._subscriptions.push(RCTNetworking.addListener( 'didReceiveNetworkResponse', (args) => this._didReceiveResponse(...args) )); - this._subscriptions.push(RCTDeviceEventEmitter.addListener( + this._subscriptions.push(RCTNetworking.addListener( 'didReceiveNetworkData', (args) => this._didReceiveData(...args) )); - this._subscriptions.push(RCTDeviceEventEmitter.addListener( + this._subscriptions.push(RCTNetworking.addListener( 'didCompleteNetworkResponse', (args) => this.__didCompleteResponse(...args) )); @@ -337,10 +336,18 @@ class XMLHttpRequestBase extends EventTarget(...XHR_EVENTS) { url: ?string, headers: Object, data: any, - incrementalEvents: boolean, - timeout: number + useIncrementalUpdates: boolean, + timeout: number, ): void { - throw new Error('Subclass must define sendImpl method'); + RCTNetworking.sendRequest( + method, + url, + headers, + data, + useIncrementalUpdates, + timeout, + this.didCreateRequest.bind(this), + ); } send(data: any): void { diff --git a/Libraries/Network/__tests__/XMLHttpRequestBase-test.js b/Libraries/Network/__tests__/XMLHttpRequest-test.js similarity index 95% rename from Libraries/Network/__tests__/XMLHttpRequestBase-test.js rename to Libraries/Network/__tests__/XMLHttpRequest-test.js index 9d967d4b34b072..a08d67c5dfd627 100644 --- a/Libraries/Network/__tests__/XMLHttpRequestBase-test.js +++ b/Libraries/Network/__tests__/XMLHttpRequest-test.js @@ -12,13 +12,11 @@ jest .disableAutomock() .dontMock('event-target-shim') - .dontMock('XMLHttpRequestBase'); + .dontMock('XMLHttpRequest'); -const XMLHttpRequestBase = require('XMLHttpRequestBase'); +const XMLHttpRequest = require('XMLHttpRequest'); -class XMLHttpRequest extends XMLHttpRequestBase {} - -describe('XMLHttpRequestBase', function(){ +describe('XMLHttpRequest', function(){ var xhr; var handleTimeout; var handleError; diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m index 0d6453a36095f6..ac360e254b91a7 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -137,20 +137,29 @@ + (void)didReceiveLocalNotification:(UILocalNotification *)notification - (void)handleLocalNotificationReceived:(NSNotification *)notification { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"localNotificationReceived" body:notification.userInfo]; +#pragma clang diagnostic pop } - (void)handleRemoteNotificationReceived:(NSNotification *)notification { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"remoteNotificationReceived" body:notification.userInfo]; +#pragma clang diagnostic pop } - (void)handleRemoteNotificationsRegistered:(NSNotification *)notification { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"remoteNotificationsRegistered" body:notification.userInfo]; +#pragma clang diagnostic pop } /** diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index d867bd59581256..6200bd42df7ae7 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -10,11 +10,11 @@ */ 'use strict'; -var warning = require('fbjs/lib/warning'); +const warning = require('fbjs/lib/warning'); if (__DEV__) { - var warningDedupe = {}; - var addonWarn = function(prevName, newPackageName) { + const warningDedupe = {}; + const addonWarn = function(prevName, newPackageName) { warning( warningDedupe[prevName], 'React.addons.' + prevName + ' is deprecated. Please import the "' + @@ -25,7 +25,7 @@ if (__DEV__) { } // Export React, plus some native additions. -var ReactNative = { +const ReactNative = { // Components get ActivityIndicatorIOS() { return require('ActivityIndicatorIOS'); }, get ART() { return require('ReactNativeART'); }, @@ -87,9 +87,11 @@ var ReactNative = { get ImagePickerIOS() { return require('ImagePickerIOS'); }, get IntentAndroid() { return require('IntentAndroid'); }, get InteractionManager() { return require('InteractionManager'); }, + get Keyboard() { return require('Keyboard'); }, get LayoutAnimation() { return require('LayoutAnimation'); }, get Linking() { return require('Linking'); }, get LinkingIOS() { return require('LinkingIOS'); }, + get NativeEventEmitter() { return require('NativeEventEmitter'); }, get NavigationExperimental() { return require('NavigationExperimental'); }, get NetInfo() { return require('NetInfo'); }, get PanResponder() { return require('PanResponder'); }, @@ -171,7 +173,7 @@ var ReactNative = { // Preserve getters with warnings on the internal ReactNative copy without // invoking them. -var ReactNativeInternal = require('ReactNative'); +const ReactNativeInternal = require('ReactNative'); function applyForwarding(key) { if (__DEV__) { Object.defineProperty( @@ -183,7 +185,7 @@ function applyForwarding(key) { } ReactNative[key] = ReactNativeInternal[key]; } -for (var key in ReactNativeInternal) { +for (const key in ReactNativeInternal) { applyForwarding(key); } diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index d300af0c4d04f2..066736dac7fbd8 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -85,9 +85,11 @@ var ReactNative = Object.assign(Object.create(require('ReactNative')), { ImagePickerIOS: require('ImagePickerIOS'), IntentAndroid: require('IntentAndroid'), InteractionManager: require('InteractionManager'), + Keyboard: require('Keyboard'), LayoutAnimation: require('LayoutAnimation'), Linking: require('Linking'), LinkingIOS: require('LinkingIOS'), + NativeEventEmitter: require('NativeEventEmitter'), NavigationExperimental: require('NavigationExperimental'), NetInfo: require('NetInfo'), PanResponder: require('PanResponder'), diff --git a/React/Modules/RCTAccessibilityManager.m b/React/Modules/RCTAccessibilityManager.m index fd629cae33c0c3..3fcef1e5d3aa49 100644 --- a/React/Modules/RCTAccessibilityManager.m +++ b/React/Modules/RCTAccessibilityManager.m @@ -92,8 +92,11 @@ - (void)didReceiveNewVoiceOverStatus:(__unused NSNotification *)notification BOOL newIsVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning(); if (_isVoiceOverEnabled != newIsVoiceOverEnabled) { _isVoiceOverEnabled = newIsVoiceOverEnabled; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"voiceOverDidChange" body:@(_isVoiceOverEnabled)]; +#pragma clang diagnostic pop } } diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m index e853659a547cde..f574f7b06c0a38 100644 --- a/React/Modules/RCTDevMenu.m +++ b/React/Modules/RCTDevMenu.m @@ -182,7 +182,10 @@ - (instancetype)init selectedTitle:@"Hide Inspector" handler:^(__unused BOOL enabled) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [weakSelf.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; +#pragma clang diagnostic pop }]]; _webSocketExecutorName = [_defaults objectForKey:@"websocket-executor-name"] ?: @"JS Remotely"; @@ -214,8 +217,11 @@ - (instancetype)init modifierFlags:UIKeyModifierCommand action:^(__unused UIKeyCommand *command) { [weakSelf.bridge.eventDispatcher +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" sendDeviceEventWithName:@"toggleElementInspector" body:nil]; +#pragma clang diagnostic pop }]; // Reload in normal mode @@ -388,7 +394,10 @@ - (void)jsLoaded:(NSNotification *)notification // Inspector can only be shown after JS has loaded if ([_settings[@"showInspector"] boolValue]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [self.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; +#pragma clang diagnostic pop } }); } diff --git a/React/Modules/RCTEventEmitter.m b/React/Modules/RCTEventEmitter.m index 5f399e8305f9ff..3d4f70a5f234f0 100644 --- a/React/Modules/RCTEventEmitter.m +++ b/React/Modules/RCTEventEmitter.m @@ -37,7 +37,9 @@ + (void)initialize - (void)sendEventWithName:(NSString *)eventName body:(id)body { - RCTAssert(_bridge != nil, @"bridge is not set."); + RCTAssert(_bridge != nil, @"bridge is not set. This is probably because you've " + "explicitly synthesized the bridge in %@, even though it's inherited " + "from RCTEventEmitter.", [self class]); if (RCT_DEBUG && ![[self supportedEvents] containsObject:eventName]) { RCTLogError(@"`%@` is not a supported event type for %@. Supported events are: `%@`", diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index a819847d0a70d2..11112f92a9dc6e 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -253,8 +253,11 @@ - (void)interfaceOrientationWillChange:(NSNotification *)notification !UIInterfaceOrientationIsPortrait(nextOrientation)) || (UIInterfaceOrientationIsLandscape(_currentInterfaceOrientation) && !UIInterfaceOrientationIsLandscape(nextOrientation))) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"didUpdateDimensions" body:RCTExportedDimensions(YES)]; +#pragma clang diagnostic pop } _currentInterfaceOrientation = nextOrientation; From 986f67ac4f735c7d184c55cff4a7636a4431dbf4 Mon Sep 17 00:00:00 2001 From: Neo Date: Tue, 24 May 2016 11:23:38 -0700 Subject: [PATCH 091/843] Add NeoReader to showcase Summary: detail about NeoReader https://github.com/nihgwu/NeoReader/ Closes https://github.com/facebook/react-native/pull/7716 Differential Revision: D3341121 fbshipit-source-id: 91bea4e906c68b9a687be4edfb17a0e3ea815d64 --- website/src/react-native/showcase.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/website/src/react-native/showcase.js b/website/src/react-native/showcase.js index a554d54f2f5e5a..16bf8477306dc2 100644 --- a/website/src/react-native/showcase.js +++ b/website/src/react-native/showcase.js @@ -639,6 +639,13 @@ var apps = [ link: 'https://play.google.com/store/apps/details?id=com.rhyble.nalathekerala', author: 'Rhyble', }, + { + name: 'NeoReader', + icon: 'http://firicon.fir.im/a4c5f1e8c52d9d98e7b990e5098a161a8f698653', + linkAppStore: 'https://itunes.apple.com/cn/app/niu-du-neoreader/id1111443079?l=cn&mt=8', + linkPlayStore: 'http://fir.im/neoreader', + author: 'Neo Nie', + }, { name: 'New Music - listen to recent albums, EPs & singles', icon: 'http://a5.mzstatic.com/us/r30/Purple60/v4/fa/d9/be/fad9be3d-474c-4380-6391-37f234a81901/icon175x175.png', From 0e8c3ff463420cc24f3bcc225cb9b0b76dd8eb3b Mon Sep 17 00:00:00 2001 From: Rahul Jiresal Date: Tue, 24 May 2016 11:41:33 -0700 Subject: [PATCH 092/843] =?UTF-8?q?Add=20a=20removeListener=20method=20to?= =?UTF-8?q?=20DeviceEventEmitter=20for=20Framework=20consi=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: The Framework is inconsistent in how listeners are removed in certain classes. This issue has been discussed in https://github.com/facebook/react-native/issues/6493. For example, **DeviceEventEmitter** ```javascript /* Current */ this.keyboardHideObserver = DeviceEventEmitter.addListener('keyboardWillHide', this.keyboardWillHide); this.keyboardHideObserver.remove(); /* Expected (maybe in addition to the current API) */ DeviceEventEmitter.addListener('keyboardWillHide', this.keyboardWillHide); DeviceEventEmitter.removeListener('keyboardWillHide', this.keyboardWillHide); ``` **AppStateIOS** ```javascript AppStateIOS.addEventListener('change', this.handleAppStateChange); AppStateIOS.removeEventListener('change', this.handleAppStateChange); ``` The API should be consistent, and preferably should allow both ways of removing the listeners. Currently, developers who tried to use the second way of removing the listeners get an error for function not found. Due to the lack of documenta Closes https://github.com/facebook/react-native/pull/6884 Differential Revision: D3341235 Pulled By: nicklockwood fbshipit-source-id: 87431e8b667f46ad002d4a6e3ca07cbc1e6b4007 --- Libraries/EventEmitter/EventEmitter.js | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Libraries/EventEmitter/EventEmitter.js b/Libraries/EventEmitter/EventEmitter.js index 2745cdda02be47..4a67898804dfb8 100644 --- a/Libraries/EventEmitter/EventEmitter.js +++ b/Libraries/EventEmitter/EventEmitter.js @@ -185,6 +185,34 @@ class EventEmitter { this._currentSubscription = null; } } + + /** + * Removes the given listener for event of specific type. + * + * @param {string} eventType - Name of the event to emit + * @param {function} listener - Function to invoke when the specified event is + * emitted + * + * @example + * emitter.removeListener('someEvent', function(message) { + * console.log(message); + * }); // removes the listener if already registered + * + */ + removeListener(eventType: String, listener) { + const subscriptions: ?[EmitterSubscription] = (this._subscriber.getSubscriptionsForType(eventType): any); + if (subscriptions) { + for (let i = 0, l = subscriptions.length; i < l; i++) { + const subscription = subscriptions[i]; + + // The subscription may have been removed during this event loop. + // its listener matches the listener in method parameters + if (subscription && subscription.listener === listener) { + subscription.remove(); + } + } + } + } } module.exports = EventEmitter; From 11449359e5aede03b29a454dce7601cdbe944a67 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Tue, 24 May 2016 11:54:28 -0700 Subject: [PATCH 093/843] Add index page for packager Summary: When packager is running, visiting localhost:8081 produces ugly 404 "GET / not found" This diff adds a simple index.html page that has a title and link to documentation. It's a super tiny detail, but I hope it makes things a little nicer. Improvements are welcome. Maybe we could include an offline copy of React Native's docs website? Reviewed By: vjeux Differential Revision: D3341242 fbshipit-source-id: c8cd4b647e69eb520ea8bc978bea070551225912 --- local-cli/server/middleware/index.html | 10 ++++++++++ local-cli/server/middleware/indexPage.js | 20 ++++++++++++++++++++ local-cli/server/runServer.js | 2 ++ 3 files changed, 32 insertions(+) create mode 100644 local-cli/server/middleware/index.html create mode 100644 local-cli/server/middleware/indexPage.js diff --git a/local-cli/server/middleware/index.html b/local-cli/server/middleware/index.html new file mode 100644 index 00000000000000..fdf6bf418090e1 --- /dev/null +++ b/local-cli/server/middleware/index.html @@ -0,0 +1,10 @@ + + + + React Native + + +

    React Native packager is running.

    +

    Visit documentation

    + + diff --git a/local-cli/server/middleware/indexPage.js b/local-cli/server/middleware/indexPage.js new file mode 100644 index 00000000000000..1ba7dca5b77c3d --- /dev/null +++ b/local-cli/server/middleware/indexPage.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +module.exports = function(req, res, next) { + if (req.url === '/') { + res.end(fs.readFileSync(path.join(__dirname, 'index.html'))); + } else { + next(); + } +}; diff --git a/local-cli/server/runServer.js b/local-cli/server/runServer.js index 79992f7f725a69..362a09ca4a88d0 100644 --- a/local-cli/server/runServer.js +++ b/local-cli/server/runServer.js @@ -20,6 +20,7 @@ const openStackFrameInEditorMiddleware = require('./middleware/openStackFrameInE const path = require('path'); const ReactPackager = require('../../packager/react-packager'); const statusPageMiddleware = require('./middleware/statusPageMiddleware.js'); +const indexPageMiddleware = require('./middleware/indexPage'); const systraceProfileMiddleware = require('./middleware/systraceProfileMiddleware.js'); const webSocketProxy = require('./util/webSocketProxy.js'); @@ -36,6 +37,7 @@ function runServer(args, config, readyCallback) { .use(statusPageMiddleware) .use(systraceProfileMiddleware) .use(cpuProfilerMiddleware) + .use(indexPageMiddleware) .use(packagerServer.processRequest.bind(packagerServer)); args.projectRoots.forEach(root => app.use(connect.static(root))); From 8876eaaea0a4321e5e92aba11d101be6525f62c5 Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Tue, 24 May 2016 12:02:31 -0700 Subject: [PATCH 094/843] Set up Systrace during initialization Summary: When we're profiling, we want to load Systrace immediately and profile all the startup and initial render code. The code here used to load Systrace during startup only if `__DEV__` but would always start profiling once Systrace was otherwise required. That seems pretty hard to reason about, so I'm switching to always requiring Systrace during the startup path and enabling profiling if appropriate. In actual production that'll always be false, of course. Reviewed By: javache Differential Revision: D3338216 fbshipit-source-id: f173e82f34e110d83e7ff04f11af9b302a54b859 --- .../Initialization/InitializeJavaScriptAppEngine.js | 6 ++++++ Libraries/Utilities/Systrace.js | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index bcdc888297f00b..372030698cc40c 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -40,6 +40,11 @@ function setUpProcess() { } } +function setUpProfile() { + const Systrace = require('Systrace'); + Systrace.setEnabled(global.__RCTProfileIsProfiling || false); +} + function setUpConsole() { // ExceptionsManager transitively requires Promise so we install it after const ExceptionsManager = require('ExceptionsManager'); @@ -229,6 +234,7 @@ function getPropertyDescriptor(object, name) { } setUpProcess(); +setUpProfile(); setUpConsole(); setUpTimers(); setUpAlert(); diff --git a/Libraries/Utilities/Systrace.js b/Libraries/Utilities/Systrace.js index 8dc207e1e2c74a..bdd2c599262cb7 100644 --- a/Libraries/Utilities/Systrace.js +++ b/Libraries/Utilities/Systrace.js @@ -194,8 +194,6 @@ const Systrace = { }, }; -Systrace.setEnabled(global.__RCTProfileIsProfiling || false); - if (__DEV__) { // This is needed, because require callis in polyfills are not processed as // other files. Therefore, calls to `require('moduleId')` are not replaced From 2de03231820c00ad6991cdba9f9658decaff0d90 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Tue, 24 May 2016 12:33:57 -0700 Subject: [PATCH 095/843] Reverted commit D3339945 Summary: Updated networking and geolocation to use the new events system. Reviewed By: javache Differential Revision: D3339945 fbshipit-source-id: 01d307cf8a0aea3a404c87c6205132c42290abb1 --- Examples/UIExplorer/AppStateExample.js | 32 +----- Examples/UIExplorer/AppStateIOSExample.js | 99 +++++++++++++++++++ Examples/UIExplorer/UIExplorerList.ios.js | 4 + .../RCTEventDispatcherTests.m | 2 + Examples/UIExplorer/XHRExample.ios.js | 11 +-- Libraries/Geolocation/Geolocation.js | 17 ++-- Libraries/Geolocation/RCTLocationObserver.h | 4 +- Libraries/Geolocation/RCTLocationObserver.m | 20 ++-- Libraries/LinkingIOS/RCTLinkingManager.m | 3 - Libraries/Network/NetInfo.js | 6 +- Libraries/Network/RCTNetInfo.h | 6 +- Libraries/Network/RCTNetInfo.m | 41 ++++---- Libraries/Network/RCTNetworking.android.js | 55 +++-------- Libraries/Network/RCTNetworking.h | 6 +- Libraries/Network/RCTNetworking.ios.js | 38 ++----- Libraries/Network/RCTNetworking.m | 37 ++++--- Libraries/Network/XMLHttpRequest.android.js | 62 ++++++++++++ Libraries/Network/XMLHttpRequest.ios.js | 47 +++++++++ ...MLHttpRequest.js => XMLHttpRequestBase.js} | 27 ++--- ...est-test.js => XMLHttpRequestBase-test.js} | 8 +- .../RCTPushNotificationManager.m | 9 -- Libraries/react-native/react-native.js | 14 ++- Libraries/react-native/react-native.js.flow | 2 - React/Modules/RCTAccessibilityManager.m | 3 - React/Modules/RCTDevMenu.m | 9 -- React/Modules/RCTEventEmitter.m | 4 +- React/Modules/RCTUIManager.m | 3 - 27 files changed, 340 insertions(+), 229 deletions(-) create mode 100644 Examples/UIExplorer/AppStateIOSExample.js create mode 100644 Libraries/Network/XMLHttpRequest.android.js create mode 100644 Libraries/Network/XMLHttpRequest.ios.js rename Libraries/Network/{XMLHttpRequest.js => XMLHttpRequestBase.js} (95%) rename Libraries/Network/__tests__/{XMLHttpRequest-test.js => XMLHttpRequestBase-test.js} (95%) diff --git a/Examples/UIExplorer/AppStateExample.js b/Examples/UIExplorer/AppStateExample.js index a5f2e55edeec69..8140394142024d 100644 --- a/Examples/UIExplorer/AppStateExample.js +++ b/Examples/UIExplorer/AppStateExample.js @@ -1,11 +1,4 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * * The examples provided by Facebook are for non-commercial testing and * evaluation purposes only. * @@ -23,9 +16,9 @@ */ 'use strict'; -const React = require('react'); -const ReactNative = require('react-native'); -const { +var React = require('react'); +var ReactNative = require('react-native'); +var { AppState, Text, View @@ -36,19 +29,13 @@ var AppStateSubscription = React.createClass({ return { appState: AppState.currentState, previousAppStates: [], - memoryWarnings: 0, }; }, componentDidMount: function() { AppState.addEventListener('change', this._handleAppStateChange); - AppState.addEventListener('memoryWarning', this._handleMemoryWarning); }, componentWillUnmount: function() { AppState.removeEventListener('change', this._handleAppStateChange); - AppState.removeEventListener('memoryWarning', this._handleMemoryWarning); - }, - _handleMemoryWarning: function() { - this.setState({memoryWarnings: this.state.memoryWarnings + 1}); }, _handleAppStateChange: function(appState) { var previousAppStates = this.state.previousAppStates.slice(); @@ -59,13 +46,6 @@ var AppStateSubscription = React.createClass({ }); }, render() { - if (this.props.showMemoryWarnings) { - return ( - - {this.state.memoryWarnings} - - ); - } if (this.props.showCurrentOnly) { return ( @@ -98,10 +78,4 @@ exports.examples = [ title: 'Previous states:', render(): ReactElement { return ; } }, - { - platform: 'ios', - title: 'Memory Warnings', - description: 'In the IOS simulator, hit Shift+Command+M to simulate a memory warning.', - render(): ReactElement { return ; } - }, ]; diff --git a/Examples/UIExplorer/AppStateIOSExample.js b/Examples/UIExplorer/AppStateIOSExample.js new file mode 100644 index 00000000000000..10aaed8aadd0ff --- /dev/null +++ b/Examples/UIExplorer/AppStateIOSExample.js @@ -0,0 +1,99 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @providesModule AppStateIOSExample + * @flow + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + AppStateIOS, + Text, + View +} = ReactNative; + +var AppStateSubscription = React.createClass({ + getInitialState() { + return { + appState: AppStateIOS.currentState, + previousAppStates: [], + memoryWarnings: 0, + }; + }, + componentDidMount: function() { + AppStateIOS.addEventListener('change', this._handleAppStateChange); + AppStateIOS.addEventListener('memoryWarning', this._handleMemoryWarning); + }, + componentWillUnmount: function() { + AppStateIOS.removeEventListener('change', this._handleAppStateChange); + AppStateIOS.removeEventListener('memoryWarning', this._handleMemoryWarning); + }, + _handleMemoryWarning: function() { + this.setState({memoryWarnings: this.state.memoryWarnings + 1}); + }, + _handleAppStateChange: function(appState) { + var previousAppStates = this.state.previousAppStates.slice(); + previousAppStates.push(this.state.appState); + this.setState({ + appState, + previousAppStates, + }); + }, + render() { + if (this.props.showMemoryWarnings) { + return ( + + {this.state.memoryWarnings} + + ); + } + if (this.props.showCurrentOnly) { + return ( + + {this.state.appState} + + ); + } + return ( + + {JSON.stringify(this.state.previousAppStates)} + + ); + } +}); + +exports.title = 'AppStateIOS'; +exports.description = 'iOS app background status'; +exports.examples = [ + { + title: 'AppStateIOS.currentState', + description: 'Can be null on app initialization', + render() { return {AppStateIOS.currentState}; } + }, + { + title: 'Subscribed AppStateIOS:', + description: 'This changes according to the current state, so you can only ever see it rendered as "active"', + render(): ReactElement { return ; } + }, + { + title: 'Previous states:', + render(): ReactElement { return ; } + }, + { + title: 'Memory Warnings', + description: 'In the simulator, hit Shift+Command+M to simulate a memory warning.', + render(): ReactElement { return ; } + }, +]; diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index e9ddd367b7572d..c63763e1e5813d 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -171,6 +171,10 @@ const APIExamples: Array = [ key: 'AnExApp', module: require('./AnimatedGratuitousApp/AnExApp'), }, + { + key: 'AppStateIOSExample', + module: require('./AppStateIOSExample'), + }, { key: 'AppStateExample', module: require('./AppStateExample'), diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m index e9559d55840dda..4fe88ee933b28f 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m @@ -105,7 +105,9 @@ - (void)testLegacyEventsAreImmediatelyDispatched #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_eventDispatcher sendDeviceEventWithName:_eventName body:_body]; + #pragma clang diagnostic pop [_bridge verify]; diff --git a/Examples/UIExplorer/XHRExample.ios.js b/Examples/UIExplorer/XHRExample.ios.js index 0deaa4eedf45fe..b84c8b0a090918 100644 --- a/Examples/UIExplorer/XHRExample.ios.js +++ b/Examples/UIExplorer/XHRExample.ios.js @@ -1,11 +1,4 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * * The examples provided by Facebook are for non-commercial testing and * evaluation purposes only. * @@ -28,7 +21,7 @@ var { AlertIOS, CameraRoll, Image, - Linking, + LinkingIOS, ProgressViewIOS, StyleSheet, Text, @@ -222,7 +215,7 @@ class FormUploader extends React.Component { return; } var url = xhr.responseText.slice(index).split('\n')[0]; - Linking.openURL(url); + LinkingIOS.openURL(url); }; var formdata = new FormData(); if (this.state.randomPhoto) { diff --git a/Libraries/Geolocation/Geolocation.js b/Libraries/Geolocation/Geolocation.js index f2eed384b1c745..f49a9ebf0eaf94 100644 --- a/Libraries/Geolocation/Geolocation.js +++ b/Libraries/Geolocation/Geolocation.js @@ -11,16 +11,15 @@ */ 'use strict'; -const NativeEventEmitter = require('NativeEventEmitter'); -const RCTLocationObserver = require('NativeModules').LocationObserver; +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var RCTLocationObserver = require('NativeModules').LocationObserver; -const invariant = require('fbjs/lib/invariant'); -const logError = require('logError'); -const warning = require('fbjs/lib/warning'); - -const LocationEventEmitter = new NativeEventEmitter(RCTLocationObserver); +var invariant = require('fbjs/lib/invariant'); +var logError = require('logError'); +var warning = require('fbjs/lib/warning'); var subscriptions = []; + var updatesEnabled = false; type GeoOptions = { @@ -81,11 +80,11 @@ var Geolocation = { } var watchID = subscriptions.length; subscriptions.push([ - LocationEventEmitter.addListener( + RCTDeviceEventEmitter.addListener( 'geolocationDidChange', success ), - error ? LocationEventEmitter.addListener( + error ? RCTDeviceEventEmitter.addListener( 'geolocationError', error ) : null, diff --git a/Libraries/Geolocation/RCTLocationObserver.h b/Libraries/Geolocation/RCTLocationObserver.h index a607155584a7fa..873eeff6a11375 100644 --- a/Libraries/Geolocation/RCTLocationObserver.h +++ b/Libraries/Geolocation/RCTLocationObserver.h @@ -7,8 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTEventEmitter.h" +#import "RCTBridgeModule.h" -@interface RCTLocationObserver : RCTEventEmitter +@interface RCTLocationObserver : NSObject @end diff --git a/Libraries/Geolocation/RCTLocationObserver.m b/Libraries/Geolocation/RCTLocationObserver.m index 319477c99ac0b7..23179c5cab214e 100644 --- a/Libraries/Geolocation/RCTLocationObserver.m +++ b/Libraries/Geolocation/RCTLocationObserver.m @@ -113,6 +113,8 @@ @implementation RCTLocationObserver RCT_EXPORT_MODULE() +@synthesize bridge = _bridge; + #pragma mark - Lifecycle - (void)dealloc @@ -126,13 +128,9 @@ - (dispatch_queue_t)methodQueue return dispatch_get_main_queue(); } -- (NSArray *)supportedEvents -{ - return @[@"geolocationDidChange", @"geolocationError"]; -} - #pragma mark - Private API + - (void)beginLocationUpdatesWithDesiredAccuracy:(CLLocationAccuracy)desiredAccuracy distanceFilter:(CLLocationDistance)distanceFilter { if (!_locationManager) { @@ -281,7 +279,11 @@ - (void)locationManager:(CLLocationManager *)manager // Send event if (_observingLocation) { - [self sendEventWithName:@"geolocationDidChange" body:_lastLocationEvent]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_bridge.eventDispatcher sendDeviceEventWithName:@"geolocationDidChange" + body:_lastLocationEvent]; +#pragma clang diagnostic pop } // Fire all queued callbacks @@ -322,7 +324,11 @@ - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError * // Send event if (_observingLocation) { - [self sendEventWithName:@"geolocationError" body:jsError]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_bridge.eventDispatcher sendDeviceEventWithName:@"geolocationError" + body:jsError]; +#pragma clang diagnostic pop } // Fire all queued error callbacks diff --git a/Libraries/LinkingIOS/RCTLinkingManager.m b/Libraries/LinkingIOS/RCTLinkingManager.m index ca4112fc75c9fe..96f9e75b49c905 100644 --- a/Libraries/LinkingIOS/RCTLinkingManager.m +++ b/Libraries/LinkingIOS/RCTLinkingManager.m @@ -88,11 +88,8 @@ + (BOOL)application:(UIApplication *)application - (void)handleOpenURLNotification:(NSNotification *)notification { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"openURL" body:notification.userInfo]; -#pragma clang diagnostic pop } RCT_EXPORT_METHOD(openURL:(NSURL *)URL diff --git a/Libraries/Network/NetInfo.js b/Libraries/Network/NetInfo.js index 70d513cc8d813b..768fb795f8196a 100644 --- a/Libraries/Network/NetInfo.js +++ b/Libraries/Network/NetInfo.js @@ -12,14 +12,12 @@ 'use strict'; const Map = require('Map'); -const NativeEventEmitter = require('NativeEventEmitter'); const NativeModules = require('NativeModules'); const Platform = require('Platform'); +const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); const RCTNetInfo = NativeModules.NetInfo; const deprecatedCallback = require('deprecatedCallback'); -const NetInfoEventEmitter = new NativeEventEmitter(RCTNetInfo); - const DEVICE_CONNECTIVITY_EVENT = 'networkStatusDidChange'; type ChangeEventName = $Enum<{ @@ -178,7 +176,7 @@ const NetInfo = { eventName: ChangeEventName, handler: Function ): {remove: () => void} { - const listener = NetInfoEventEmitter.addListener( + const listener = RCTDeviceEventEmitter.addListener( DEVICE_CONNECTIVITY_EVENT, (appStateData) => { handler(appStateData.network_info); diff --git a/Libraries/Network/RCTNetInfo.h b/Libraries/Network/RCTNetInfo.h index 9c280b2cf4073a..6c2556e0a50b6e 100644 --- a/Libraries/Network/RCTNetInfo.h +++ b/Libraries/Network/RCTNetInfo.h @@ -9,10 +9,10 @@ #import -#import "RCTEventEmitter.h" +#import "RCTBridgeModule.h" -@interface RCTNetInfo : RCTEventEmitter +@interface RCTNetInfo : NSObject -- (instancetype)initWithHost:(NSString *)host; +- (instancetype)initWithHost:(NSString *)host NS_DESIGNATED_INITIALIZER; @end diff --git a/Libraries/Network/RCTNetInfo.m b/Libraries/Network/RCTNetInfo.m index 5981e9f7f82a08..28d0fc07387039 100644 --- a/Libraries/Network/RCTNetInfo.m +++ b/Libraries/Network/RCTNetInfo.m @@ -22,9 +22,10 @@ @implementation RCTNetInfo { SCNetworkReachabilityRef _reachability; NSString *_status; - NSString *_host; } +@synthesize bridge = _bridge; + RCT_EXPORT_MODULE() static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info) @@ -50,7 +51,11 @@ static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SC if (![status isEqualToString:self->_status]) { self->_status = status; - [self sendEventWithName:@"networkStatusDidChange" body:@{@"network_info": status}]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self->_bridge.eventDispatcher sendDeviceEventWithName:@"networkStatusDidChange" + body:@{@"network_info": status}]; +#pragma clang diagnostic pop } } @@ -61,40 +66,34 @@ - (instancetype)initWithHost:(NSString *)host RCTAssertParam(host); RCTAssert(![host hasPrefix:@"http"], @"Host value should just contain the domain, not the URL scheme."); - if ((self = [self init])) { - _host = [host copy]; + if ((self = [super init])) { + _status = RCTReachabilityStateUnknown; + _reachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, host.UTF8String); + SCNetworkReachabilityContext context = { 0, ( __bridge void *)self, NULL, NULL, NULL }; + SCNetworkReachabilitySetCallback(_reachability, RCTReachabilityCallback, &context); + SCNetworkReachabilityScheduleWithRunLoop(_reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); } return self; } -- (NSArray *)supportedEvents +- (instancetype)init { - return @[@"networkStatusDidChange"]; + return [self initWithHost:@"apple.com"]; } -- (void)startObserving +- (void)dealloc { - _status = RCTReachabilityStateUnknown; - _reachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, _host.UTF8String ?: "apple.com"); - SCNetworkReachabilityContext context = { 0, ( __bridge void *)self, NULL, NULL, NULL }; - SCNetworkReachabilitySetCallback(_reachability, RCTReachabilityCallback, &context); - SCNetworkReachabilityScheduleWithRunLoop(_reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); -} - -- (void)stopObserving -{ - if (_reachability) { - SCNetworkReachabilityUnscheduleFromRunLoop(_reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); - CFRelease(_reachability); - } + SCNetworkReachabilityUnscheduleFromRunLoop(_reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); + CFRelease(_reachability); } #pragma mark - Public API +// TODO: remove error callback - not needed except by Subscribable interface RCT_EXPORT_METHOD(getCurrentConnectivity:(RCTPromiseResolveBlock)resolve reject:(__unused RCTPromiseRejectBlock)reject) { - resolve(@{@"network_info": _status ?: RCTReachabilityStateUnknown}); + resolve(@{@"network_info": _status}); } @end diff --git a/Libraries/Network/RCTNetworking.android.js b/Libraries/Network/RCTNetworking.android.js index 23aa9e8bf54199..38f694d5452f57 100644 --- a/Libraries/Network/RCTNetworking.android.js +++ b/Libraries/Network/RCTNetworking.android.js @@ -12,66 +12,39 @@ // Do not require the native RCTNetworking module directly! Use this wrapper module instead. // It will add the necessary requestId, so that you don't have to generate it yourself. -const FormData = require('FormData'); -const NativeEventEmitter = require('NativeEventEmitter'); -const RCTNetworkingNative = require('NativeModules').Networking; +var RCTNetworkingNative = require('NativeModules').Networking; -type Header = [string, string]; - -function convertHeadersMapToArray(headers: Object): Array
    { - const headerArray = []; - for (const name in headers) { - headerArray.push([name, headers[name]]); - } - return headerArray; -} - -let _requestId = 1; -function generateRequestId() { +var _requestId = 1; +var generateRequestId = function() { return _requestId++; -} +}; /** * This class is a wrapper around the native RCTNetworking module. It adds a necessary unique * requestId to each network request that can be used to abort that request later on. */ -class RCTNetworking extends NativeEventEmitter { - - constructor() { - super(RCTNetworkingNative); - } +class RCTNetworking { - sendRequest(method, url, headers, data, incrementalUpdates, timeout, callback) { - if (typeof data === 'string') { - data = {string: data}; - } else if (data instanceof FormData) { - data = { - formData: data.getParts().map((part) => { - part.headers = convertHeadersMapToArray(part.headers); - return part; - }), - }; - } - const requestId = generateRequestId(); + static sendRequest(method, url, headers, data, useIncrementalUpdates, timeout) { + var requestId = generateRequestId(); RCTNetworkingNative.sendRequest( method, url, requestId, - convertHeadersMapToArray(headers), + headers, data, - incrementalUpdates, - timeout - ); - callback(requestId); + useIncrementalUpdates, + timeout); + return requestId; } - abortRequest(requestId) { + static abortRequest(requestId) { RCTNetworkingNative.abortRequest(requestId); } - clearCookies(callback) { + static clearCookies(callback) { RCTNetworkingNative.clearCookies(callback); } } -module.exports = new RCTNetworking(); +module.exports = RCTNetworking; diff --git a/Libraries/Network/RCTNetworking.h b/Libraries/Network/RCTNetworking.h index a06a1eeab95550..cd8b99dde4df3d 100644 --- a/Libraries/Network/RCTNetworking.h +++ b/Libraries/Network/RCTNetworking.h @@ -7,10 +7,12 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTEventEmitter.h" +#import + +#import "RCTBridge.h" #import "RCTNetworkTask.h" -@interface RCTNetworking : RCTEventEmitter +@interface RCTNetworking : NSObject /** * Does a handler exist for the specified request? diff --git a/Libraries/Network/RCTNetworking.ios.js b/Libraries/Network/RCTNetworking.ios.js index 3e0e4c35284119..677f2c1c94a720 100644 --- a/Libraries/Network/RCTNetworking.ios.js +++ b/Libraries/Network/RCTNetworking.ios.js @@ -10,39 +10,21 @@ */ 'use strict'; -const FormData = require('FormData'); -const NativeEventEmitter = require('NativeEventEmitter'); -const RCTNetworkingNative = require('NativeModules').Networking; +var RCTNetworkingNative = require('NativeModules').Networking; -class RCTNetworking extends NativeEventEmitter { - - constructor() { - super(RCTNetworkingNative); - } +/** + * This class is a wrapper around the native RCTNetworking module. + */ +class RCTNetworking { - sendRequest(method, url, headers, data, incrementalUpdates, timeout, callback) { - if (typeof data === 'string') { - data = {string: data}; - } else if (data instanceof FormData) { - data = {formData: data.getParts()}; - } - RCTNetworkingNative.sendRequest({ - method, - url, - data, - headers, - incrementalUpdates, - timeout - }, callback); + static sendRequest(query, callback) { + RCTNetworkingNative.sendRequest(query, callback); } - abortRequest(requestId) { - RCTNetworkingNative.abortRequest(requestId); + static abortRequest(requestId) { + RCTNetworkingNative.cancelRequest(requestId); } - clearCookies(callback) { - console.warn('RCTNetworking.clearCookies is not supported on iOS'); - } } -module.exports = new RCTNetworking(); +module.exports = RCTNetworking; diff --git a/Libraries/Network/RCTNetworking.m b/Libraries/Network/RCTNetworking.m index 07c21f095d3fad..5c7e3b9ee0edcb 100644 --- a/Libraries/Network/RCTNetworking.m +++ b/Libraries/Network/RCTNetworking.m @@ -129,18 +129,11 @@ @implementation RCTNetworking NSArray> *_handlers; } +@synthesize bridge = _bridge; @synthesize methodQueue = _methodQueue; RCT_EXPORT_MODULE() -- (NSArray *)supportedEvents -{ - return @[@"didCompleteNetworkResponse", - @"didReceiveNetworkResponse", - @"didSendNetworkData", - @"didReceiveNetworkData"]; -} - - (id)handlerForRequest:(NSURLRequest *)request { if (!request.URL) { @@ -149,7 +142,7 @@ @implementation RCTNetworking if (!_handlers) { // Get handlers, sorted in reverse priority order (highest priority first) - _handlers = [[self.bridge modulesConformingToProtocol:@protocol(RCTURLRequestHandler)] sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { + _handlers = [[_bridge modulesConformingToProtocol:@protocol(RCTURLRequestHandler)] sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { float priorityA = [a respondsToSelector:@selector(handlerPriority)] ? [a handlerPriority] : 0; float priorityB = [b respondsToSelector:@selector(handlerPriority)] ? [b handlerPriority] : 0; if (priorityA > priorityB) { @@ -353,7 +346,11 @@ - (void)sendData:(NSData *)data forTask:(RCTNetworkTask *)task } NSArray *responseJSON = @[task.requestID, responseText ?: @""]; - [self sendEventWithName:@"didReceiveNetworkData" body:responseJSON]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkData" + body:responseJSON]; +#pragma clang diagnostic pop } - (void)sendRequest:(NSURLRequest *)request @@ -367,7 +364,10 @@ - (void)sendRequest:(NSURLRequest *)request RCTURLRequestProgressBlock uploadProgressBlock = ^(int64_t progress, int64_t total) { dispatch_async(_methodQueue, ^{ NSArray *responseJSON = @[task.requestID, @((double)progress), @((double)total)]; - [self sendEventWithName:@"didSendNetworkData" body:responseJSON]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_bridge.eventDispatcher sendDeviceEventWithName:@"didSendNetworkData" body:responseJSON]; +#pragma clang diagnostic pop }); }; @@ -385,7 +385,11 @@ - (void)sendRequest:(NSURLRequest *)request } id responseURL = response.URL ? response.URL.absoluteString : [NSNull null]; NSArray *responseJSON = @[task.requestID, @(status), headers, responseURL]; - [self sendEventWithName:@"didReceiveNetworkResponse" body:responseJSON]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse" + body:responseJSON]; +#pragma clang diagnostic pop }); }; @@ -406,7 +410,12 @@ - (void)sendRequest:(NSURLRequest *)request error.code == kCFURLErrorTimedOut ? @YES : @NO ]; - [self sendEventWithName:@"didCompleteNetworkResponse" body:responseJSON]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse" + body:responseJSON]; +#pragma clang diagnostic pop + [_tasksByRequestID removeObjectForKey:task.requestID]; }); }; @@ -460,7 +469,7 @@ - (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request }]; } -RCT_EXPORT_METHOD(abortRequest:(nonnull NSNumber *)requestID) +RCT_EXPORT_METHOD(cancelRequest:(nonnull NSNumber *)requestID) { [_tasksByRequestID[requestID] cancel]; [_tasksByRequestID removeObjectForKey:requestID]; diff --git a/Libraries/Network/XMLHttpRequest.android.js b/Libraries/Network/XMLHttpRequest.android.js new file mode 100644 index 00000000000000..099df7526f0511 --- /dev/null +++ b/Libraries/Network/XMLHttpRequest.android.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule XMLHttpRequest + * @flow + */ +'use strict'; + +var FormData = require('FormData'); +var RCTNetworking = require('RCTNetworking'); +var XMLHttpRequestBase = require('XMLHttpRequestBase'); + +type Header = [string, string]; + +function convertHeadersMapToArray(headers: Object): Array
    { + var headerArray = []; + for (var name in headers) { + headerArray.push([name, headers[name]]); + } + return headerArray; +} + +class XMLHttpRequest extends XMLHttpRequestBase { + sendImpl( + method: ?string, + url: ?string, + headers: Object, + data: any, + useIncrementalUpdates: boolean, + timeout: number, + ): void { + var body; + if (typeof data === 'string') { + body = {string: data}; + } else if (data instanceof FormData) { + body = { + formData: data.getParts().map((part) => { + part.headers = convertHeadersMapToArray(part.headers); + return part; + }), + }; + } else { + body = data; + } + var requestId = RCTNetworking.sendRequest( + method, + url, + convertHeadersMapToArray(headers), + body, + useIncrementalUpdates, + timeout + ); + this.didCreateRequest(requestId); + } +} + +module.exports = XMLHttpRequest; diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js new file mode 100644 index 00000000000000..dd4ee0dd63d3c6 --- /dev/null +++ b/Libraries/Network/XMLHttpRequest.ios.js @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule XMLHttpRequest + * @flow + */ +'use strict'; + +var FormData = require('FormData'); +var RCTNetworking = require('RCTNetworking'); + +var XMLHttpRequestBase = require('XMLHttpRequestBase'); + +class XMLHttpRequest extends XMLHttpRequestBase { + sendImpl( + method: ?string, + url: ?string, + headers: Object, + data: any, + incrementalUpdates: boolean, + timeout: number, + ): void { + if (typeof data === 'string') { + data = {string: data}; + } else if (data instanceof FormData) { + data = {formData: data.getParts()}; + } + RCTNetworking.sendRequest( + { + method, + url, + data, + headers, + incrementalUpdates, + timeout + }, + this.didCreateRequest.bind(this) + ); + } +} + +module.exports = XMLHttpRequest; diff --git a/Libraries/Network/XMLHttpRequest.js b/Libraries/Network/XMLHttpRequestBase.js similarity index 95% rename from Libraries/Network/XMLHttpRequest.js rename to Libraries/Network/XMLHttpRequestBase.js index 7b742c62fb6133..d5d5da5c57707e 100644 --- a/Libraries/Network/XMLHttpRequest.js +++ b/Libraries/Network/XMLHttpRequestBase.js @@ -6,12 +6,13 @@ * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * - * @providesModule XMLHttpRequest + * @providesModule XMLHttpRequestBase * @flow */ 'use strict'; -const RCTNetworking = require('RCTNetworking'); +var RCTNetworking = require('RCTNetworking'); +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); const EventTarget = require('event-target-shim'); const invariant = require('fbjs/lib/invariant'); @@ -211,19 +212,19 @@ class XMLHttpRequestBase extends EventTarget(...XHR_EVENTS) { didCreateRequest(requestId: number): void { this._requestId = requestId; - this._subscriptions.push(RCTNetworking.addListener( + this._subscriptions.push(RCTDeviceEventEmitter.addListener( 'didSendNetworkData', (args) => this.__didUploadProgress(...args) )); - this._subscriptions.push(RCTNetworking.addListener( + this._subscriptions.push(RCTDeviceEventEmitter.addListener( 'didReceiveNetworkResponse', (args) => this._didReceiveResponse(...args) )); - this._subscriptions.push(RCTNetworking.addListener( + this._subscriptions.push(RCTDeviceEventEmitter.addListener( 'didReceiveNetworkData', (args) => this._didReceiveData(...args) )); - this._subscriptions.push(RCTNetworking.addListener( + this._subscriptions.push(RCTDeviceEventEmitter.addListener( 'didCompleteNetworkResponse', (args) => this.__didCompleteResponse(...args) )); @@ -336,18 +337,10 @@ class XMLHttpRequestBase extends EventTarget(...XHR_EVENTS) { url: ?string, headers: Object, data: any, - useIncrementalUpdates: boolean, - timeout: number, + incrementalEvents: boolean, + timeout: number ): void { - RCTNetworking.sendRequest( - method, - url, - headers, - data, - useIncrementalUpdates, - timeout, - this.didCreateRequest.bind(this), - ); + throw new Error('Subclass must define sendImpl method'); } send(data: any): void { diff --git a/Libraries/Network/__tests__/XMLHttpRequest-test.js b/Libraries/Network/__tests__/XMLHttpRequestBase-test.js similarity index 95% rename from Libraries/Network/__tests__/XMLHttpRequest-test.js rename to Libraries/Network/__tests__/XMLHttpRequestBase-test.js index a08d67c5dfd627..9d967d4b34b072 100644 --- a/Libraries/Network/__tests__/XMLHttpRequest-test.js +++ b/Libraries/Network/__tests__/XMLHttpRequestBase-test.js @@ -12,11 +12,13 @@ jest .disableAutomock() .dontMock('event-target-shim') - .dontMock('XMLHttpRequest'); + .dontMock('XMLHttpRequestBase'); -const XMLHttpRequest = require('XMLHttpRequest'); +const XMLHttpRequestBase = require('XMLHttpRequestBase'); -describe('XMLHttpRequest', function(){ +class XMLHttpRequest extends XMLHttpRequestBase {} + +describe('XMLHttpRequestBase', function(){ var xhr; var handleTimeout; var handleError; diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m index ac360e254b91a7..0d6453a36095f6 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -137,29 +137,20 @@ + (void)didReceiveLocalNotification:(UILocalNotification *)notification - (void)handleLocalNotificationReceived:(NSNotification *)notification { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"localNotificationReceived" body:notification.userInfo]; -#pragma clang diagnostic pop } - (void)handleRemoteNotificationReceived:(NSNotification *)notification { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"remoteNotificationReceived" body:notification.userInfo]; -#pragma clang diagnostic pop } - (void)handleRemoteNotificationsRegistered:(NSNotification *)notification { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"remoteNotificationsRegistered" body:notification.userInfo]; -#pragma clang diagnostic pop } /** diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 6200bd42df7ae7..d867bd59581256 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -10,11 +10,11 @@ */ 'use strict'; -const warning = require('fbjs/lib/warning'); +var warning = require('fbjs/lib/warning'); if (__DEV__) { - const warningDedupe = {}; - const addonWarn = function(prevName, newPackageName) { + var warningDedupe = {}; + var addonWarn = function(prevName, newPackageName) { warning( warningDedupe[prevName], 'React.addons.' + prevName + ' is deprecated. Please import the "' + @@ -25,7 +25,7 @@ if (__DEV__) { } // Export React, plus some native additions. -const ReactNative = { +var ReactNative = { // Components get ActivityIndicatorIOS() { return require('ActivityIndicatorIOS'); }, get ART() { return require('ReactNativeART'); }, @@ -87,11 +87,9 @@ const ReactNative = { get ImagePickerIOS() { return require('ImagePickerIOS'); }, get IntentAndroid() { return require('IntentAndroid'); }, get InteractionManager() { return require('InteractionManager'); }, - get Keyboard() { return require('Keyboard'); }, get LayoutAnimation() { return require('LayoutAnimation'); }, get Linking() { return require('Linking'); }, get LinkingIOS() { return require('LinkingIOS'); }, - get NativeEventEmitter() { return require('NativeEventEmitter'); }, get NavigationExperimental() { return require('NavigationExperimental'); }, get NetInfo() { return require('NetInfo'); }, get PanResponder() { return require('PanResponder'); }, @@ -173,7 +171,7 @@ const ReactNative = { // Preserve getters with warnings on the internal ReactNative copy without // invoking them. -const ReactNativeInternal = require('ReactNative'); +var ReactNativeInternal = require('ReactNative'); function applyForwarding(key) { if (__DEV__) { Object.defineProperty( @@ -185,7 +183,7 @@ function applyForwarding(key) { } ReactNative[key] = ReactNativeInternal[key]; } -for (const key in ReactNativeInternal) { +for (var key in ReactNativeInternal) { applyForwarding(key); } diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index 066736dac7fbd8..d300af0c4d04f2 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -85,11 +85,9 @@ var ReactNative = Object.assign(Object.create(require('ReactNative')), { ImagePickerIOS: require('ImagePickerIOS'), IntentAndroid: require('IntentAndroid'), InteractionManager: require('InteractionManager'), - Keyboard: require('Keyboard'), LayoutAnimation: require('LayoutAnimation'), Linking: require('Linking'), LinkingIOS: require('LinkingIOS'), - NativeEventEmitter: require('NativeEventEmitter'), NavigationExperimental: require('NavigationExperimental'), NetInfo: require('NetInfo'), PanResponder: require('PanResponder'), diff --git a/React/Modules/RCTAccessibilityManager.m b/React/Modules/RCTAccessibilityManager.m index 3fcef1e5d3aa49..fd629cae33c0c3 100644 --- a/React/Modules/RCTAccessibilityManager.m +++ b/React/Modules/RCTAccessibilityManager.m @@ -92,11 +92,8 @@ - (void)didReceiveNewVoiceOverStatus:(__unused NSNotification *)notification BOOL newIsVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning(); if (_isVoiceOverEnabled != newIsVoiceOverEnabled) { _isVoiceOverEnabled = newIsVoiceOverEnabled; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"voiceOverDidChange" body:@(_isVoiceOverEnabled)]; -#pragma clang diagnostic pop } } diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m index f574f7b06c0a38..e853659a547cde 100644 --- a/React/Modules/RCTDevMenu.m +++ b/React/Modules/RCTDevMenu.m @@ -182,10 +182,7 @@ - (instancetype)init selectedTitle:@"Hide Inspector" handler:^(__unused BOOL enabled) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" [weakSelf.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; -#pragma clang diagnostic pop }]]; _webSocketExecutorName = [_defaults objectForKey:@"websocket-executor-name"] ?: @"JS Remotely"; @@ -217,11 +214,8 @@ - (instancetype)init modifierFlags:UIKeyModifierCommand action:^(__unused UIKeyCommand *command) { [weakSelf.bridge.eventDispatcher -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" sendDeviceEventWithName:@"toggleElementInspector" body:nil]; -#pragma clang diagnostic pop }]; // Reload in normal mode @@ -394,10 +388,7 @@ - (void)jsLoaded:(NSNotification *)notification // Inspector can only be shown after JS has loaded if ([_settings[@"showInspector"] boolValue]) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" [self.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; -#pragma clang diagnostic pop } }); } diff --git a/React/Modules/RCTEventEmitter.m b/React/Modules/RCTEventEmitter.m index 3d4f70a5f234f0..5f399e8305f9ff 100644 --- a/React/Modules/RCTEventEmitter.m +++ b/React/Modules/RCTEventEmitter.m @@ -37,9 +37,7 @@ + (void)initialize - (void)sendEventWithName:(NSString *)eventName body:(id)body { - RCTAssert(_bridge != nil, @"bridge is not set. This is probably because you've " - "explicitly synthesized the bridge in %@, even though it's inherited " - "from RCTEventEmitter.", [self class]); + RCTAssert(_bridge != nil, @"bridge is not set."); if (RCT_DEBUG && ![[self supportedEvents] containsObject:eventName]) { RCTLogError(@"`%@` is not a supported event type for %@. Supported events are: `%@`", diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 11112f92a9dc6e..a819847d0a70d2 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -253,11 +253,8 @@ - (void)interfaceOrientationWillChange:(NSNotification *)notification !UIInterfaceOrientationIsPortrait(nextOrientation)) || (UIInterfaceOrientationIsLandscape(_currentInterfaceOrientation) && !UIInterfaceOrientationIsLandscape(nextOrientation))) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"didUpdateDimensions" body:RCTExportedDimensions(YES)]; -#pragma clang diagnostic pop } _currentInterfaceOrientation = nextOrientation; From 1926fdf156e077ad8b0c73505c6447d1a16687f8 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Tue, 24 May 2016 12:42:32 -0700 Subject: [PATCH 096/843] Inline images package name fix Summary: Renaming package name from textfrescosupport to text.frescosupport Reviewed By: dmmiller Differential Revision: D3340363 fbshipit-source-id: 6c73d9e05751ccba299489fbc13447dc85a9c0df --- .../main/java/com/facebook/react/shell/MainReactPackage.java | 2 +- .../FrescoBasedReactTextInlineImageShadowNode.java | 3 ++- .../FrescoBasedReactTextInlineImageViewManager.java | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index d389d0f1020655..c17cfe8d04d25f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -54,7 +54,7 @@ import com.facebook.react.views.text.ReactRawTextManager; import com.facebook.react.views.text.ReactTextViewManager; import com.facebook.react.views.text.ReactVirtualTextViewManager; -import com.facebook.react.views.textfrescosupport.FrescoBasedReactTextInlineImageViewManager; +import com.facebook.react.views.text.frescosupport.FrescoBasedReactTextInlineImageViewManager; import com.facebook.react.views.textinput.ReactTextInputManager; import com.facebook.react.views.toolbar.ReactToolbarManager; import com.facebook.react.views.view.ReactViewManager; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java index 643a2c136fe577..b1d3c012af7613 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -package com.facebook.react.views.textfrescosupport; +package com.facebook.react.views.text.frescosupport; import javax.annotation.Nullable; @@ -23,6 +23,7 @@ import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.views.text.ReactTextInlineImageShadowNode; import com.facebook.react.views.text.TextInlineImageSpan; +import com.facebook.react.views.textfrescosupport.FrescoBasedReactTextInlineImageSpan; /** * {@link CSSNode} that represents an inline image. Loading is done using Fresco. diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageViewManager.java index c272bf39d89a6c..5425121e87690a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageViewManager.java @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -package com.facebook.react.views.textfrescosupport; +package com.facebook.react.views.text.frescosupport; import javax.annotation.Nullable; From 0a027167de6ae8fb895f2efd734ffde1ad0ef5db Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Tue, 24 May 2016 13:19:43 -0700 Subject: [PATCH 097/843] Fix WLV bug when data shrinks Differential Revision: D3341453 fbshipit-source-id: 008cb65db6da74f2525ae8e667b702bc48f9f9ad --- Libraries/Experimental/WindowedListView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Experimental/WindowedListView.js b/Libraries/Experimental/WindowedListView.js index e0e261de8d62f1..1721e5fb3b7608 100644 --- a/Libraries/Experimental/WindowedListView.js +++ b/Libraries/Experimental/WindowedListView.js @@ -339,7 +339,7 @@ class WindowedListView extends React.Component { const rowFrames = this._rowFrames; let firstVisible = -1; let lastVisible = 0; - let lastRow = this.state.lastRow; + let lastRow = clamp(0, this.state.lastRow, totalRows - 1); const top = this._scrollOffsetY; const bottom = top + this._frameHeight; for (let idx = 0; idx < lastRow; idx++) { From caa2baee9d95c8047ed6436c4ea9d34c719aa5ec Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Tue, 24 May 2016 14:48:23 -0700 Subject: [PATCH 098/843] Fix leak in Nodes due to removeClippedSubviews Reviewed By: astreet Differential Revision: D3337513 fbshipit-source-id: b7b798f024488cd19cdcf93aee7f99eed512dd6a --- .../facebook/react/uimanager/NativeViewHierarchyManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index be555c82a224a3..eeeb4c11ed8dd3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -466,7 +466,7 @@ protected final void addRootViewGroup( /** * Releases all references to given native View. */ - protected final void dropView(View view) { + protected void dropView(View view) { UiThreadUtil.assertOnUiThread(); if (!mRootTags.get(view.getId())) { // For non-root views we notify viewmanager with {@link ViewManager#onDropInstance} From ac5636dd59f97a7381acd548e701e83fadcba408 Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Tue, 24 May 2016 18:20:12 -0700 Subject: [PATCH 099/843] explicit type args in react-native-github Reviewed By: vjeux Differential Revision: D3342856 fbshipit-source-id: ba5a4d5529fc9d1d1efe98cc175d718c5f044a5b --- Examples/UIExplorer/AccessibilityIOSExample.js | 2 +- Examples/UIExplorer/ActionSheetIOSExample.js | 10 +++++----- .../UIExplorer/ActivityIndicatorIOSExample.js | 2 +- Examples/UIExplorer/AdSupportIOSExample.js | 2 +- Examples/UIExplorer/AlertIOSExample.js | 2 +- .../AnimatedGratuitousApp/AnExApp.js | 4 ++-- .../AnimatedGratuitousApp/AnExBobble.js | 2 +- .../AnimatedGratuitousApp/AnExSet.js | 2 +- .../AnimatedGratuitousApp/AnExTilt.js | 2 +- Examples/UIExplorer/AppStateExample.js | 4 ++-- Examples/UIExplorer/AppStateIOSExample.js | 6 +++--- Examples/UIExplorer/AsyncStorageExample.js | 2 +- Examples/UIExplorer/CameraRollExample.js | 2 +- Examples/UIExplorer/DatePickerIOSExample.js | 2 +- Examples/UIExplorer/GeolocationExample.js | 2 +- Examples/UIExplorer/LayoutAnimationExample.js | 4 ++-- Examples/UIExplorer/LayoutEventsExample.js | 2 +- Examples/UIExplorer/ListViewPagingExample.js | 2 +- .../NavigationCompositionExample.js | 2 +- Examples/UIExplorer/NetInfoExample.js | 8 ++++---- Examples/UIExplorer/PickerIOSExample.js | 4 ++-- .../UIExplorer/PushNotificationIOSExample.js | 6 +++--- Examples/UIExplorer/RCTRootViewIOSExample.js | 4 ++-- .../UIExplorer/SegmentedControlIOSExample.js | 12 ++++++------ Examples/UIExplorer/SliderExample.js | 18 +++++++++--------- Examples/UIExplorer/SliderIOSExample.js | 14 +++++++------- Examples/UIExplorer/SnapshotExample.js | 2 +- Examples/UIExplorer/SwitchExample.js | 10 +++++----- Examples/UIExplorer/TextExample.ios.js | 2 +- Examples/UIExplorer/TextInputExample.ios.js | 4 ++-- Examples/UIExplorer/TimerExample.js | 2 +- Examples/UIExplorer/TouchableExample.js | 12 ++++++------ Examples/UIExplorer/TransformExample.js | 2 +- .../UIExplorer/TransparentHitTestExample.js | 2 +- Examples/UIExplorer/UIExplorerApp.ios.js | 6 +++--- Examples/UIExplorer/UIExplorerExampleList.js | 12 ++++++------ Examples/UIExplorer/WebSocketExample.js | 6 +++--- Examples/UIExplorer/WebViewExample.js | 10 +++++----- IntegrationTests/PromiseTest.js | 6 +++--- Libraries/Components/StaticRenderer.js | 2 +- Libraries/Components/StatusBar/StatusBar.js | 2 +- .../Components/Touchable/TouchableBounce.js | 2 +- .../Touchable/TouchableWithoutFeedback.js | 2 +- .../NavigationExperimental/NavigationCard.js | 8 ++------ .../NavigationCardStack.js | 4 ++-- .../NavigationExperimental/NavigationHeader.js | 10 +++++----- .../NavigationHeaderTitle.js | 2 +- .../NavigationPointerEventsContainer.js | 4 ++-- Libraries/Experimental/Incremental.js | 2 +- Libraries/Experimental/IncrementalExample.js | 2 +- Libraries/Experimental/IncrementalGroup.js | 2 +- .../SwipeableRow/SwipeableListView.js | 4 ++-- .../SwipeableRow/SwipeableQuickActionButton.js | 2 +- .../SwipeableRow/SwipeableQuickActions.js | 2 +- .../Experimental/SwipeableRow/SwipeableRow.js | 2 +- Libraries/Experimental/WindowedListView.js | 12 ++++++------ Libraries/Interaction/InteractionManager.js | 2 +- Libraries/Interaction/TaskQueue.js | 2 +- Libraries/Modal/Modal.js | 2 +- .../NavigationAnimatedView.js | 8 ++++---- .../NavigationTransitioner.js | 8 ++++---- .../NavigationTypeDefinition.js | 4 ++-- Libraries/Network/NetInfo.js | 6 +++--- Libraries/ReactIOS/YellowBox.js | 2 +- Libraries/Text/Text.js | 2 +- Libraries/Utilities/UIManager.js | 2 +- Libraries/Utilities/deprecatedCallback.js | 2 +- 67 files changed, 149 insertions(+), 153 deletions(-) diff --git a/Examples/UIExplorer/AccessibilityIOSExample.js b/Examples/UIExplorer/AccessibilityIOSExample.js index 8f8329f5ef83aa..ca5628d6e407fb 100644 --- a/Examples/UIExplorer/AccessibilityIOSExample.js +++ b/Examples/UIExplorer/AccessibilityIOSExample.js @@ -61,6 +61,6 @@ exports.description = 'Interface to show iOS\' accessibility samples'; exports.examples = [ { title: 'Accessibility elements', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, ]; diff --git a/Examples/UIExplorer/ActionSheetIOSExample.js b/Examples/UIExplorer/ActionSheetIOSExample.js index 0442682142ab30..4be6c776071621 100644 --- a/Examples/UIExplorer/ActionSheetIOSExample.js +++ b/Examples/UIExplorer/ActionSheetIOSExample.js @@ -198,27 +198,27 @@ exports.description = 'Interface to show iOS\' action sheets'; exports.examples = [ { title: 'Show Action Sheet', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'Show Action Sheet with tinted buttons', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'Show Share Action Sheet', - render(): ReactElement { + render(): ReactElement { return ; } }, { title: 'Share Local Image', - render(): ReactElement { + render(): ReactElement { return ; } }, { title: 'Share Screenshot', - render(): ReactElement { + render(): ReactElement { return ; } } diff --git a/Examples/UIExplorer/ActivityIndicatorIOSExample.js b/Examples/UIExplorer/ActivityIndicatorIOSExample.js index 6923936d4528a7..967f042755d9ed 100644 --- a/Examples/UIExplorer/ActivityIndicatorIOSExample.js +++ b/Examples/UIExplorer/ActivityIndicatorIOSExample.js @@ -142,7 +142,7 @@ exports.examples = [ }, { title: 'Start/stop', - render: function(): ReactElement { + render: function(): ReactElement { return ; } }, diff --git a/Examples/UIExplorer/AdSupportIOSExample.js b/Examples/UIExplorer/AdSupportIOSExample.js index dab5e88fb072ca..7a98fb1dcbdb36 100644 --- a/Examples/UIExplorer/AdSupportIOSExample.js +++ b/Examples/UIExplorer/AdSupportIOSExample.js @@ -31,7 +31,7 @@ exports.description = 'Example of using the ad support API.'; exports.examples = [ { title: 'Ad Support IOS', - render: function(): ReactElement { + render: function(): ReactElement { return ; }, } diff --git a/Examples/UIExplorer/AlertIOSExample.js b/Examples/UIExplorer/AlertIOSExample.js index 112149e165733d..85d66d4a62cc99 100644 --- a/Examples/UIExplorer/AlertIOSExample.js +++ b/Examples/UIExplorer/AlertIOSExample.js @@ -38,7 +38,7 @@ exports.examples = [{ }, { title: 'Prompt Options', - render(): ReactElement { + render(): ReactElement { return ; } }, diff --git a/Examples/UIExplorer/AnimatedGratuitousApp/AnExApp.js b/Examples/UIExplorer/AnimatedGratuitousApp/AnExApp.js index a6c161d9744108..58af5045a197e6 100644 --- a/Examples/UIExplorer/AnimatedGratuitousApp/AnExApp.js +++ b/Examples/UIExplorer/AnimatedGratuitousApp/AnExApp.js @@ -82,7 +82,7 @@ class Circle extends React.Component { }); } - render(): ReactElement { + render(): ReactElement { if (this.state.panResponder) { var handlers = this.state.panResponder.panHandlers; var dragStyle = { // Used to position while dragging @@ -183,7 +183,7 @@ class AnExApp extends React.Component { this._onMove = this._onMove.bind(this); } - render(): ReactElement { + render(): ReactElement { var circles = this.state.keys.map((key, idx) => { if (key === this.state.activeKey) { return ; diff --git a/Examples/UIExplorer/AnimatedGratuitousApp/AnExBobble.js b/Examples/UIExplorer/AnimatedGratuitousApp/AnExBobble.js index 574e1a2b7d0d22..ebf26f0138b368 100644 --- a/Examples/UIExplorer/AnimatedGratuitousApp/AnExBobble.js +++ b/Examples/UIExplorer/AnimatedGratuitousApp/AnExBobble.js @@ -89,7 +89,7 @@ class AnExBobble extends React.Component { }); } - render(): ReactElement { + render(): ReactElement { return ( {this.state.bobbles.map((_, i) => { diff --git a/Examples/UIExplorer/AnimatedGratuitousApp/AnExSet.js b/Examples/UIExplorer/AnimatedGratuitousApp/AnExSet.js index d67931387ad27a..1df1f084c91730 100644 --- a/Examples/UIExplorer/AnimatedGratuitousApp/AnExSet.js +++ b/Examples/UIExplorer/AnimatedGratuitousApp/AnExSet.js @@ -45,7 +45,7 @@ class AnExSet extends React.Component { openColor: randColor(), }; } - render(): ReactElement { + render(): ReactElement { var backgroundColor = this.props.openVal ? this.props.openVal.interpolate({ inputRange: [0, 1], diff --git a/Examples/UIExplorer/AnimatedGratuitousApp/AnExTilt.js b/Examples/UIExplorer/AnimatedGratuitousApp/AnExTilt.js index 8eb97844bb797b..31c23c3bcb5d20 100644 --- a/Examples/UIExplorer/AnimatedGratuitousApp/AnExTilt.js +++ b/Examples/UIExplorer/AnimatedGratuitousApp/AnExTilt.js @@ -90,7 +90,7 @@ class AnExTilt extends React.Component { this._startBurnsZoom(); } - render(): ReactElement { + render(): ReactElement { return ( ; } + render(): ReactElement { return ; } }, { title: 'Previous states:', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, ]; diff --git a/Examples/UIExplorer/AppStateIOSExample.js b/Examples/UIExplorer/AppStateIOSExample.js index 10aaed8aadd0ff..7598df7fb9ec0a 100644 --- a/Examples/UIExplorer/AppStateIOSExample.js +++ b/Examples/UIExplorer/AppStateIOSExample.js @@ -85,15 +85,15 @@ exports.examples = [ { title: 'Subscribed AppStateIOS:', description: 'This changes according to the current state, so you can only ever see it rendered as "active"', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'Previous states:', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'Memory Warnings', description: 'In the simulator, hit Shift+Command+M to simulate a memory warning.', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, ]; diff --git a/Examples/UIExplorer/AsyncStorageExample.js b/Examples/UIExplorer/AsyncStorageExample.js index f2ee7da47381c6..ac8bd78899abff 100644 --- a/Examples/UIExplorer/AsyncStorageExample.js +++ b/Examples/UIExplorer/AsyncStorageExample.js @@ -115,6 +115,6 @@ exports.description = 'Asynchronous local disk storage.'; exports.examples = [ { title: 'Basics - getItem, setItem, removeItem', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, ]; diff --git a/Examples/UIExplorer/CameraRollExample.js b/Examples/UIExplorer/CameraRollExample.js index 9716e6b8030408..f39b78f39879e4 100644 --- a/Examples/UIExplorer/CameraRollExample.js +++ b/Examples/UIExplorer/CameraRollExample.js @@ -137,6 +137,6 @@ exports.description = 'Example component that uses CameraRoll to list user\'s ph exports.examples = [ { title: 'Photos', - render(): ReactElement { return ; } + render(): ReactElement { return ; } } ]; diff --git a/Examples/UIExplorer/DatePickerIOSExample.js b/Examples/UIExplorer/DatePickerIOSExample.js index 412f2af33e5201..a3dd7876c7e6f0 100644 --- a/Examples/UIExplorer/DatePickerIOSExample.js +++ b/Examples/UIExplorer/DatePickerIOSExample.js @@ -132,7 +132,7 @@ exports.description = 'Select dates and times using the native UIDatePicker.'; exports.examples = [ { title: '', - render: function(): ReactElement { + render: function(): ReactElement { return ; }, }]; diff --git a/Examples/UIExplorer/GeolocationExample.js b/Examples/UIExplorer/GeolocationExample.js index 98177f437650d2..230127d665adcb 100644 --- a/Examples/UIExplorer/GeolocationExample.js +++ b/Examples/UIExplorer/GeolocationExample.js @@ -32,7 +32,7 @@ exports.description = 'Examples of using the Geolocation API.'; exports.examples = [ { title: 'navigator.geolocation', - render: function(): ReactElement { + render: function(): ReactElement { return ; }, } diff --git a/Examples/UIExplorer/LayoutAnimationExample.js b/Examples/UIExplorer/LayoutAnimationExample.js index f2ac7b35087101..ee2038dbfc9d69 100644 --- a/Examples/UIExplorer/LayoutAnimationExample.js +++ b/Examples/UIExplorer/LayoutAnimationExample.js @@ -160,12 +160,12 @@ exports.title = 'Layout Animation'; exports.description = 'Layout animation'; exports.examples = [{ title: 'Add and remove views', - render(): ReactElement { + render(): ReactElement { return ; }, }, { title: 'Cross fade views', - render(): ReactElement { + render(): ReactElement { return ; }, }]; diff --git a/Examples/UIExplorer/LayoutEventsExample.js b/Examples/UIExplorer/LayoutEventsExample.js index ab663c0cb1f668..8325194847c2cc 100644 --- a/Examples/UIExplorer/LayoutEventsExample.js +++ b/Examples/UIExplorer/LayoutEventsExample.js @@ -158,7 +158,7 @@ exports.description = 'Examples that show how Layout events can be used to ' + exports.examples = [ { title: 'LayoutEventExample', - render: function(): ReactElement { + render: function(): ReactElement { return ; }, }]; diff --git a/Examples/UIExplorer/ListViewPagingExample.js b/Examples/UIExplorer/ListViewPagingExample.js index 103ad958994444..4d31db208c1bbd 100644 --- a/Examples/UIExplorer/ListViewPagingExample.js +++ b/Examples/UIExplorer/ListViewPagingExample.js @@ -131,7 +131,7 @@ var ListViewPagingExample = React.createClass({ }; }, - renderRow: function(rowData: string, sectionID: string, rowID: string): ReactElement { + renderRow: function(rowData: string, sectionID: string, rowID: string): ReactElement { return (); }, diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationCompositionExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationCompositionExample.js index 898806b6806fcd..261e8060dff3be 100644 --- a/Examples/UIExplorer/NavigationExperimental/NavigationCompositionExample.js +++ b/Examples/UIExplorer/NavigationExperimental/NavigationCompositionExample.js @@ -282,7 +282,7 @@ class ExampleMainView extends React.Component { ); } - _renderScene(): ReactElement { + _renderScene(): ReactElement { const {navigationState} = this.props; const childState = navigationState.routes[navigationState.index]; return ( diff --git a/Examples/UIExplorer/NetInfoExample.js b/Examples/UIExplorer/NetInfoExample.js index b07f086bf592c0..e64886ea52e8a8 100644 --- a/Examples/UIExplorer/NetInfoExample.js +++ b/Examples/UIExplorer/NetInfoExample.js @@ -162,22 +162,22 @@ exports.examples = [ { title: 'NetInfo.isConnected', description: 'Asynchronously load and observe connectivity', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'NetInfo.update', description: 'Asynchronously load and observe connectionInfo', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'NetInfo.updateHistory', description: 'Observed updates to connectionInfo', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { platform: 'android', title: 'NetInfo.isConnectionExpensive (Android)', description: 'Asynchronously check isConnectionExpensive', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, ]; diff --git a/Examples/UIExplorer/PickerIOSExample.js b/Examples/UIExplorer/PickerIOSExample.js index 4550759d099649..5530135386a68a 100644 --- a/Examples/UIExplorer/PickerIOSExample.js +++ b/Examples/UIExplorer/PickerIOSExample.js @@ -144,13 +144,13 @@ exports.description = 'Render lists of selectable options with UIPickerView.'; exports.examples = [ { title: '', - render: function(): ReactElement { + render: function(): ReactElement { return ; }, }, { title: ' with custom styling', - render: function(): ReactElement { + render: function(): ReactElement { return ; }, }]; diff --git a/Examples/UIExplorer/PushNotificationIOSExample.js b/Examples/UIExplorer/PushNotificationIOSExample.js index 8553f1f79d3d40..ee137d83a1a7c2 100644 --- a/Examples/UIExplorer/PushNotificationIOSExample.js +++ b/Examples/UIExplorer/PushNotificationIOSExample.js @@ -162,7 +162,7 @@ exports.description = 'Apple PushNotification and badge value'; exports.examples = [ { title: 'Badge Number', - render(): ReactElement { + render(): ReactElement { PushNotificationIOS.requestPermissions(); return ( @@ -181,13 +181,13 @@ exports.examples = [ }, { title: 'Push Notifications', - render(): ReactElement { + render(): ReactElement { return ; } }, { title: 'Notifications Permissions', - render(): ReactElement { + render(): ReactElement { return ; } }]; diff --git a/Examples/UIExplorer/RCTRootViewIOSExample.js b/Examples/UIExplorer/RCTRootViewIOSExample.js index e7942d2f97f4cc..349b022d9a4fff 100644 --- a/Examples/UIExplorer/RCTRootViewIOSExample.js +++ b/Examples/UIExplorer/RCTRootViewIOSExample.js @@ -83,7 +83,7 @@ exports.description = 'Examples that show useful methods when embedding React Na exports.examples = [ { title: 'Updating app properties in runtime', - render(): ReactElement { + render(): ReactElement { return ( ); @@ -91,7 +91,7 @@ exports.examples = [ }, { title: 'RCTRootView\'s size flexibility', - render(): ReactElement { + render(): ReactElement { return ( ); diff --git a/Examples/UIExplorer/SegmentedControlIOSExample.js b/Examples/UIExplorer/SegmentedControlIOSExample.js index b76fcb4a808575..bdac8e9e451d35 100644 --- a/Examples/UIExplorer/SegmentedControlIOSExample.js +++ b/Examples/UIExplorer/SegmentedControlIOSExample.js @@ -145,26 +145,26 @@ exports.description = 'Native segmented control'; exports.examples = [ { title: 'Segmented controls can have values', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'Segmented controls can have a pre-selected value', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'Segmented controls can be momentary', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'Segmented controls can be disabled', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'Custom colors can be provided', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'Change events can be detected', - render(): ReactElement { return ; } + render(): ReactElement { return ; } } ]; diff --git a/Examples/UIExplorer/SliderExample.js b/Examples/UIExplorer/SliderExample.js index 15fa624647a67e..a987671b50f53c 100644 --- a/Examples/UIExplorer/SliderExample.js +++ b/Examples/UIExplorer/SliderExample.js @@ -94,19 +94,19 @@ exports.description = 'Slider input for numeric values'; exports.examples = [ { title: 'Default settings', - render(): ReactElement { + render(): ReactElement { return ; } }, { title: 'Initial value: 0.5', - render(): ReactElement { + render(): ReactElement { return ; } }, { title: 'minimumValue: -1, maximumValue: 2', - render(): ReactElement { + render(): ReactElement { return ( { return ; } }, { title: 'onSlidingComplete', - render(): ReactElement { + render(): ReactElement { return ( ); @@ -132,7 +132,7 @@ exports.examples = [ { title: 'Custom min/max track tint color', platform: 'ios', - render(): ReactElement { + render(): ReactElement { return ( { return ; } }, { title: 'Custom track image', platform: 'ios', - render(): ReactElement { + render(): ReactElement { return ; } }, { title: 'Custom min/max track image', platform: 'ios', - render(): ReactElement { + render(): ReactElement { return ( { return ; } }, { title: 'minimumValue: -1, maximumValue: 2', - render(): ReactElement { + render(): ReactElement { return ( { return ; } }, { title: 'Custom min/max track tint color', - render(): ReactElement { + render(): ReactElement { return ( { return ; } }, { title: 'Custom track image', - render(): ReactElement { + render(): ReactElement { return ; } }, { title: 'Custom min/max track image', - render(): ReactElement { + render(): ReactElement { return ( ; } + render(): ReactElement { return ; } }, ]; diff --git a/Examples/UIExplorer/SwitchExample.js b/Examples/UIExplorer/SwitchExample.js index 19cf72596de660..dc4d1e391da789 100644 --- a/Examples/UIExplorer/SwitchExample.js +++ b/Examples/UIExplorer/SwitchExample.js @@ -130,26 +130,26 @@ var EventSwitchExample = React.createClass({ var examples = [ { title: 'Switches can be set to true or false', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'Switches can be disabled', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'Change events can be detected', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'Switches are controlled components', - render(): ReactElement { return ; } + render(): ReactElement { return ; } } ]; if (Platform.OS === 'ios') { examples.push({ title: 'Custom colors can be provided', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }); } diff --git a/Examples/UIExplorer/TextExample.ios.js b/Examples/UIExplorer/TextExample.ios.js index 41839fff36ad41..8c7b4ff86ef006 100644 --- a/Examples/UIExplorer/TextExample.ios.js +++ b/Examples/UIExplorer/TextExample.ios.js @@ -351,7 +351,7 @@ exports.examples = [ }, }, { title: 'Toggling Attributes', - render: function(): ReactElement { + render: function(): ReactElement { return ; }, }, { diff --git a/Examples/UIExplorer/TextInputExample.ios.js b/Examples/UIExplorer/TextInputExample.ios.js index fdf4daca175672..1f2e83d2e6a815 100644 --- a/Examples/UIExplorer/TextInputExample.ios.js +++ b/Examples/UIExplorer/TextInputExample.ios.js @@ -532,7 +532,7 @@ exports.examples = [ }, { title: 'Event handling', - render: function(): ReactElement { return ; }, + render: function(): ReactElement { return ; }, }, { title: 'Colored input text', @@ -630,7 +630,7 @@ exports.examples = [ }, { title: 'Blur on submit', - render: function(): ReactElement { return ; }, + render: function(): ReactElement { return ; }, }, { title: 'Multiline blur on submit', diff --git a/Examples/UIExplorer/TimerExample.js b/Examples/UIExplorer/TimerExample.js index b2a39b74988ec0..8fdc92081f045d 100644 --- a/Examples/UIExplorer/TimerExample.js +++ b/Examples/UIExplorer/TimerExample.js @@ -149,7 +149,7 @@ exports.examples = [ title: 'this.setInterval(fn, t)', description: 'Execute function fn every t milliseconds until cancelled ' + 'or component is unmounted.', - render: function(): ReactElement { + render: function(): ReactElement { var IntervalExample = React.createClass({ getInitialState: function() { return { diff --git a/Examples/UIExplorer/TouchableExample.js b/Examples/UIExplorer/TouchableExample.js index 5e7243c8dee6a8..a78d27e001a5ba 100644 --- a/Examples/UIExplorer/TouchableExample.js +++ b/Examples/UIExplorer/TouchableExample.js @@ -71,14 +71,14 @@ exports.examples = [ }, }, { title: ' with highlight', - render: function(): ReactElement { + render: function(): ReactElement { return ; }, }, { title: 'Touchable feedback events', description: ' components accept onPress, onPressIn, ' + 'onPressOut, and onLongPress as props.', - render: function(): ReactElement { + render: function(): ReactElement { return ; }, }, { @@ -86,13 +86,13 @@ exports.examples = [ description: ' components also accept delayPressIn, ' + 'delayPressOut, and delayLongPress as props. These props impact the ' + 'timing of feedback events.', - render: function(): ReactElement { + render: function(): ReactElement { return ; }, }, { title: '3D Touch / Force Touch', description: 'iPhone 6s and 6s plus support 3D touch, which adds a force property to touches', - render: function(): ReactElement { + render: function(): ReactElement { return ; }, platform: 'ios', @@ -100,14 +100,14 @@ exports.examples = [ title: 'Touchable Hit Slop', description: ' components accept hitSlop prop which extends the touch area ' + 'without changing the view bounds.', - render: function(): ReactElement { + render: function(): ReactElement { return ; }, }, { title: 'Disabled Touchable*', description: ' components accept disabled prop which prevents ' + 'any interaction with component', - render: function(): ReactElement { + render: function(): ReactElement { return ; }, }]; diff --git a/Examples/UIExplorer/TransformExample.js b/Examples/UIExplorer/TransformExample.js index 3ce782b1c62436..c85cac0fd0ec44 100644 --- a/Examples/UIExplorer/TransformExample.js +++ b/Examples/UIExplorer/TransformExample.js @@ -208,7 +208,7 @@ exports.examples = [ { title: 'Perspective', description: 'perspective: 850, rotateX: Animated.timing(0 -> 360)', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'Translate, Rotate, Scale', diff --git a/Examples/UIExplorer/TransparentHitTestExample.js b/Examples/UIExplorer/TransparentHitTestExample.js index ac9431944223f0..b049c94bbacc46 100644 --- a/Examples/UIExplorer/TransparentHitTestExample.js +++ b/Examples/UIExplorer/TransparentHitTestExample.js @@ -46,6 +46,6 @@ exports.description = 'Transparent view receiving touch events'; exports.examples = [ { title: 'TransparentHitTestExample', - render(): ReactElement { return ; } + render(): ReactElement { return ; } } ]; diff --git a/Examples/UIExplorer/UIExplorerApp.ios.js b/Examples/UIExplorer/UIExplorerApp.ios.js index 760a960438b2c6..1e05c795cbaab4 100644 --- a/Examples/UIExplorer/UIExplorerApp.ios.js +++ b/Examples/UIExplorer/UIExplorerApp.ios.js @@ -140,7 +140,7 @@ class UIExplorerApp extends React.Component { ); } - _renderOverlay(props: NavigationSceneRendererProps): ReactElement { + _renderOverlay(props: NavigationSceneRendererProps): ReactElement { return ( { return ( {UIExplorerStateTitleMap(props.scene.route)} @@ -157,7 +157,7 @@ class UIExplorerApp extends React.Component { ); } - _renderScene(props: NavigationSceneRendererProps): ?ReactElement { + _renderScene(props: NavigationSceneRendererProps): ?ReactElement { const state = props.scene.route; if (state.key === 'AppList') { return ( diff --git a/Examples/UIExplorer/UIExplorerExampleList.js b/Examples/UIExplorer/UIExplorerExampleList.js index f9cb279d6c2392..e0331bde98e4dc 100644 --- a/Examples/UIExplorer/UIExplorerExampleList.js +++ b/Examples/UIExplorer/UIExplorerExampleList.js @@ -62,7 +62,7 @@ class UIExplorerExampleList extends React.Component { example; } - render(): ?ReactElement { + render(): ?ReactElement { const filterText = this.props.filter || ''; const filterRegex = new RegExp(String(filterText), 'i'); const filter = (example) => filterRegex.test(example.module.title); @@ -88,7 +88,7 @@ class UIExplorerExampleList extends React.Component { ); } - _renderTitleRow(): ?ReactElement { + _renderTitleRow(): ?ReactElement { if (!this.props.displayTitleRow) { return null; } @@ -104,7 +104,7 @@ class UIExplorerExampleList extends React.Component { ); } - _renderTextInput(): ?ReactElement { + _renderTextInput(): ?ReactElement { if (this.props.disableSearch) { return null; } @@ -126,7 +126,7 @@ class UIExplorerExampleList extends React.Component { ); } - _renderSectionHeader(data: any, section: string): ?ReactElement { + _renderSectionHeader(data: any, section: string): ?ReactElement { return ( {section.toUpperCase()} @@ -134,7 +134,7 @@ class UIExplorerExampleList extends React.Component { ); } - _renderExampleRow(example: {key: string, module: Object}): ?ReactElement { + _renderExampleRow(example: {key: string, module: Object}): ?ReactElement { return this._renderRow( example.module.title, example.module.description, @@ -143,7 +143,7 @@ class UIExplorerExampleList extends React.Component { ); } - _renderRow(title: string, description: string, key: ?string, handler: ?Function): ?ReactElement { + _renderRow(title: string, description: string, key: ?string, handler: ?Function): ?ReactElement { return ( diff --git a/Examples/UIExplorer/WebSocketExample.js b/Examples/UIExplorer/WebSocketExample.js index 4439d29ff2ce83..c43079162a31ee 100644 --- a/Examples/UIExplorer/WebSocketExample.js +++ b/Examples/UIExplorer/WebSocketExample.js @@ -43,7 +43,7 @@ const WS_STATES = [ ]; class Button extends React.Component { - render(): ReactElement { + render(): ReactElement { const label = {this.props.label}; if (this.props.disabled) { return ( @@ -63,7 +63,7 @@ class Button extends React.Component { } class Row extends React.Component { - render(): ReactElement { + render(): ReactElement { return ( {this.props.label} @@ -162,7 +162,7 @@ class WebSocketExample extends React.Component { this.setState({outgoingMessage: ''}); }; - render(): ReactElement { + render(): ReactElement { const socketState = WS_STATES[this.state.socketState || -1]; const canConnect = !this.state.socket || diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js index 9c3020ef44e846..9e58ae67c5fa11 100644 --- a/Examples/UIExplorer/WebViewExample.js +++ b/Examples/UIExplorer/WebViewExample.js @@ -336,15 +336,15 @@ exports.description = 'Base component to display web content'; exports.examples = [ { title: 'Simple Browser', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'Scale Page to Fit', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, { title: 'Bundled HTML', - render(): ReactElement { + render(): ReactElement { return ( { return ( { return ( this.shouldReject = true); }, - async testShouldSucceedAsync() : Promise { + async testShouldSucceedAsync() : Promise { try { await TestModule.shouldResolve(); this.shouldSucceedAsync = true; @@ -56,7 +56,7 @@ var PromiseTest = React.createClass({ } }, - async testShouldThrowAsync() : Promise { + async testShouldThrowAsync() : Promise { try { await TestModule.shouldReject(); this.shouldThrowAsync = false; @@ -65,7 +65,7 @@ var PromiseTest = React.createClass({ } }, - render() : ReactElement { + render() : ReactElement { return ; } diff --git a/Libraries/Components/StaticRenderer.js b/Libraries/Components/StaticRenderer.js index 8f6cabde51e7a4..03a7d8d08911b0 100644 --- a/Libraries/Components/StaticRenderer.js +++ b/Libraries/Components/StaticRenderer.js @@ -23,7 +23,7 @@ var StaticRenderer = React.createClass({ return nextProps.shouldUpdate; }, - render: function(): ReactElement { + render: function(): ReactElement { return this.props.render(); }, }); diff --git a/Libraries/Components/StatusBar/StatusBar.js b/Libraries/Components/StatusBar/StatusBar.js index 3a7bb1a93955a6..e49f7c930a087b 100644 --- a/Libraries/Components/StatusBar/StatusBar.js +++ b/Libraries/Components/StatusBar/StatusBar.js @@ -321,7 +321,7 @@ const StatusBar = React.createClass({ }); }, - render(): ?ReactElement { + render(): ?ReactElement { return null; }, }); diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js index c06cbd7d95588e..72eee1e18ed62c 100644 --- a/Libraries/Components/Touchable/TouchableBounce.js +++ b/Libraries/Components/Touchable/TouchableBounce.js @@ -125,7 +125,7 @@ var TouchableBounce = React.createClass({ return 0; }, - render: function(): ReactElement { + render: function(): ReactElement { return ( { // Note(avik): remove dynamic typecast once Flow has been upgraded const child = onlyChild(this.props.children); let children = child.props.children; diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js b/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js index d405a7fd05fa60..b4b6aa8ce21153 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationCard.js @@ -79,7 +79,7 @@ class SceneView extends React.Component { ); } - render(): ?ReactElement { + render(): ?ReactElement { return this.props.sceneRenderer(this.props.sceneRendererProps); } } @@ -107,7 +107,7 @@ class NavigationCard extends React.Component { ); } - render(): ReactElement { + render(): ReactElement { const { panHandlers, pointerEvents, @@ -156,13 +156,9 @@ const styles = StyleSheet.create({ NavigationCard = NavigationPointerEventsContainer.create(NavigationCard); -// $FlowFixMe: Figure out how to declare these properties on the container class NavigationCard.CardStackPanResponder = NavigationCardStackPanResponder; -// $FlowFixMe NavigationCard.CardStackStyleInterpolator = NavigationCardStackStyleInterpolator; -// $FlowFixMe NavigationCard.PagerPanResponder = NavigationPagerPanResponder; -// $FlowFixMe NavigationCard.PagerStyleInterpolator = NavigationPagerStyleInterpolator; module.exports = NavigationCard; diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js index 1dc535cdd51c93..28abd0a4a15ce5 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js @@ -116,7 +116,7 @@ class NavigationCardStack extends React.Component { ); } - render(): ReactElement { + render(): ReactElement { return ( { ); } - _renderScene(props: NavigationSceneRendererProps): ReactElement { + _renderScene(props: NavigationSceneRendererProps): ReactElement { const isVertical = this.props.direction === 'vertical'; const style = isVertical ? diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js b/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js index 172b14fc2acbcf..0140f3b50a1bc3 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js @@ -119,7 +119,7 @@ class NavigationHeader extends React.Component { ); } - render(): ReactElement { + render(): ReactElement { const { scenes, style, viewProps } = this.props; const scenesProps = scenes.map(scene => { @@ -137,7 +137,7 @@ class NavigationHeader extends React.Component { ); } - _renderLeft(props: NavigationSceneRendererProps): ?ReactElement { + _renderLeft(props: NavigationSceneRendererProps): ?ReactElement { return this._renderSubView( props, 'left', @@ -146,7 +146,7 @@ class NavigationHeader extends React.Component { ); } - _renderTitle(props: NavigationSceneRendererProps): ?ReactElement { + _renderTitle(props: NavigationSceneRendererProps): ?ReactElement { return this._renderSubView( props, 'title', @@ -155,7 +155,7 @@ class NavigationHeader extends React.Component { ); } - _renderRight(props: NavigationSceneRendererProps): ?ReactElement { + _renderRight(props: NavigationSceneRendererProps): ?ReactElement { return this._renderSubView( props, 'right', @@ -169,7 +169,7 @@ class NavigationHeader extends React.Component { name: SubViewName, renderer: NavigationSceneRenderer, styleInterpolator: NavigationStyleInterpolator, - ): ?ReactElement { + ): ?ReactElement { const { scene, navigationState, diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationHeaderTitle.js b/Libraries/CustomComponents/NavigationExperimental/NavigationHeaderTitle.js index 1f4e14b5d9f2a3..4ee95e0ae96a02 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationHeaderTitle.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationHeaderTitle.js @@ -38,7 +38,7 @@ const { } = ReactNative; type Props = { - children: ReactElement; + children: ReactElement; style: any; textStyle: any; viewProps: any; diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationPointerEventsContainer.js b/Libraries/CustomComponents/NavigationExperimental/NavigationPointerEventsContainer.js index f0826473b77545..e581b9300cc0f1 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationPointerEventsContainer.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationPointerEventsContainer.js @@ -52,7 +52,7 @@ const MIN_POSITION_OFFSET = 0.01; */ function create( Component: ReactClass, -): ReactClass { +): ReactClass { class Container extends React.Component { @@ -86,7 +86,7 @@ function create( this._bindPosition(nextProps); } - render(): ReactElement { + render(): ReactElement { this._pointerEvents = this._computePointerEvents(); return ( { }).done(); } - render(): ?ReactElement { + render(): ?ReactElement { if (this._rendered || // Make sure that once we render once, we stay rendered even if incrementalGroupEnabled gets flipped. !this.context.incrementalGroupEnabled || this.state.doIncrementalRender) { diff --git a/Libraries/Experimental/IncrementalExample.js b/Libraries/Experimental/IncrementalExample.js index 1fb3cebc2dcccf..a2b9176681ef35 100644 --- a/Libraries/Experimental/IncrementalExample.js +++ b/Libraries/Experimental/IncrementalExample.js @@ -124,7 +124,7 @@ class IncrementalExample extends React.Component { console.log('onDone:', stats); }, 0); } - render(): ReactElement { + render(): ReactElement { return ( { return ( { return ( { const slideoutView = this.props.renderQuickActions(rowData, sectionID, rowID); // If renderRowSlideout is unspecified or returns falsey, don't allow swipe diff --git a/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js b/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js index 9f472729c867a8..de7f27ea268256 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js @@ -43,7 +43,7 @@ const SwipeableQuickActionButton = React.createClass({ textStyle: Text.propTypes.style, }, - render(): ?ReactElement { + render(): ?ReactElement { if (!this.props.imageSource && !this.props.text) { return null; } diff --git a/Libraries/Experimental/SwipeableRow/SwipeableQuickActions.js b/Libraries/Experimental/SwipeableRow/SwipeableQuickActions.js index b7c09747a86abf..13e97ab5c28b50 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableQuickActions.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableQuickActions.js @@ -34,7 +34,7 @@ const SwipeableQuickActions = React.createClass({ style: View.propTypes.style, }, - render(): ReactElement { + render(): ReactElement { const children = this.props.children; let buttons = []; diff --git a/Libraries/Experimental/SwipeableRow/SwipeableRow.js b/Libraries/Experimental/SwipeableRow/SwipeableRow.js index f0def1ac30feca..ee71fa040fcf1c 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableRow.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableRow.js @@ -115,7 +115,7 @@ const SwipeableRow = React.createClass({ } }, - render(): ReactElement { + render(): ReactElement { // The view hidden behind the main view let slideOutView; if (this.state.isSwipeableViewRendered) { diff --git a/Libraries/Experimental/WindowedListView.js b/Libraries/Experimental/WindowedListView.js index 1721e5fb3b7608..3e077414d24abf 100644 --- a/Libraries/Experimental/WindowedListView.js +++ b/Libraries/Experimental/WindowedListView.js @@ -91,15 +91,15 @@ type Props = { */ renderRow: ( rowData: any, sectionIdx: number, rowIdx: number, rowKey: string - ) => ?ReactElement; + ) => ?ReactElement; /** * Rendered when the list is scrolled faster than rows can be rendered. */ - renderWindowBoundaryIndicator?: () => ?ReactElement; + renderWindowBoundaryIndicator?: () => ?ReactElement; /** * Always rendered at the bottom of all the rows. */ - renderFooter?: () => ?ReactElement; + renderFooter?: () => ?ReactElement; /** * Pipes through normal onScroll events from the underlying `ScrollView`. */ @@ -139,7 +139,7 @@ type Props = { * A function that returns the scrollable component in which the list rows * are rendered. Defaults to returning a ScrollView with the given props. */ - renderScrollComponent: (props: ?Object) => ReactElement; + renderScrollComponent: (props: ?Object) => ReactElement; /** * Use to disable incremental rendering when not wanted, e.g. to speed up initial render. */ @@ -430,7 +430,7 @@ class WindowedListView extends React.Component { this._firstVisible = newFirstVisible; this._lastVisible = newLastVisible; } - render(): ReactElement { + render(): ReactElement { const {firstRow, lastRow} = this.state; const rowFrames = this._rowFrames; const rows = []; @@ -574,7 +574,7 @@ type CellProps = { */ renderRow: ( rowData: mixed, sectionIdx: number, rowIdx: number, rowKey: string - ) => ?ReactElement; + ) => ?ReactElement; /** * Index of the row, passed through to other callbacks. */ diff --git a/Libraries/Interaction/InteractionManager.js b/Libraries/Interaction/InteractionManager.js index 421e88208f1745..6d36468508eff7 100644 --- a/Libraries/Interaction/InteractionManager.js +++ b/Libraries/Interaction/InteractionManager.js @@ -86,7 +86,7 @@ var InteractionManager = { /** * Schedule a function to run after all interactions have completed. */ - runAfterInteractions(task: ?Task): Promise { + runAfterInteractions(task: ?Task): Promise { return new Promise(resolve => { _scheduleUpdate(); if (task) { diff --git a/Libraries/Interaction/TaskQueue.js b/Libraries/Interaction/TaskQueue.js index 3b6b15839305e4..5d1d1160bdc615 100644 --- a/Libraries/Interaction/TaskQueue.js +++ b/Libraries/Interaction/TaskQueue.js @@ -19,7 +19,7 @@ type SimpleTask = { }; type PromiseTask = { name: string; - gen: () => Promise; + gen: () => Promise; }; export type Task = Function | SimpleTask | PromiseTask; diff --git a/Libraries/Modal/Modal.js b/Libraries/Modal/Modal.js index 87b76fe14a5a58..30b5954d0881f5 100644 --- a/Libraries/Modal/Modal.js +++ b/Libraries/Modal/Modal.js @@ -52,7 +52,7 @@ class Modal extends React.Component { visible: true, }; - render(): ?ReactElement { + render(): ?ReactElement { if (this.props.visible === false) { return null; } diff --git a/Libraries/NavigationExperimental/NavigationAnimatedView.js b/Libraries/NavigationExperimental/NavigationAnimatedView.js index 3661612951d92f..830ab066b52965 100644 --- a/Libraries/NavigationExperimental/NavigationAnimatedView.js +++ b/Libraries/NavigationExperimental/NavigationAnimatedView.js @@ -160,7 +160,7 @@ class NavigationAnimatedView } } - render(): ReactElement { + render(): ReactElement { const overlay = this._renderOverlay(); const scenes = this._renderScenes(); return ( @@ -175,11 +175,11 @@ class NavigationAnimatedView ); } - _renderScenes(): Array { + _renderScenes(): Array> { return this.state.scenes.map(this._renderScene, this); } - _renderScene(scene: NavigationScene): ?ReactElement { + _renderScene(scene: NavigationScene): ?ReactElement { const { navigationState, onNavigate, @@ -203,7 +203,7 @@ class NavigationAnimatedView }); } - _renderOverlay(): ?ReactElement { + _renderOverlay(): ?ReactElement { if (this.props.renderOverlay) { const { navigationState, diff --git a/Libraries/NavigationExperimental/NavigationTransitioner.js b/Libraries/NavigationExperimental/NavigationTransitioner.js index 65d573c1a55000..f319fe55b5c546 100644 --- a/Libraries/NavigationExperimental/NavigationTransitioner.js +++ b/Libraries/NavigationExperimental/NavigationTransitioner.js @@ -162,7 +162,7 @@ class NavigationTransitioner extends React.Component { Animated.parallel(animations).start(this._onTransitionEnd); } - render(): ReactElement { + render(): ReactElement { const overlay = this._renderOverlay(); const scenes = this._renderScenes(); return ( @@ -177,11 +177,11 @@ class NavigationTransitioner extends React.Component { ); } - _renderScenes(): Array { + _renderScenes(): Array> { return this.state.scenes.map(this._renderScene, this); } - _renderScene(scene: NavigationScene): ?ReactElement { + _renderScene(scene: NavigationScene): ?ReactElement { const { navigationState, onNavigate, @@ -205,7 +205,7 @@ class NavigationTransitioner extends React.Component { }); } - _renderOverlay(): ?ReactElement { + _renderOverlay(): ?ReactElement { if (this.props.renderOverlay) { const { navigationState, diff --git a/Libraries/NavigationExperimental/NavigationTypeDefinition.js b/Libraries/NavigationExperimental/NavigationTypeDefinition.js index 638ce3b95afa9e..62f0e04b36b44f 100644 --- a/Libraries/NavigationExperimental/NavigationTypeDefinition.js +++ b/Libraries/NavigationExperimental/NavigationTypeDefinition.js @@ -109,7 +109,7 @@ export type NavigationAnimationSetter = ( export type NavigationRenderer = ( navigationState: ?NavigationRoute, onNavigate: NavigationActionCaller, -) => ReactElement; +) => ReactElement; export type NavigationReducer = ( state: ?NavigationRoute, @@ -118,7 +118,7 @@ export type NavigationReducer = ( export type NavigationSceneRenderer = ( props: NavigationSceneRendererProps, -) => ?ReactElement; +) => ?ReactElement; export type NavigationStyleInterpolator = ( props: NavigationSceneRendererProps, diff --git a/Libraries/Network/NetInfo.js b/Libraries/Network/NetInfo.js index 768fb795f8196a..7d4900f8ed88ee 100644 --- a/Libraries/Network/NetInfo.js +++ b/Libraries/Network/NetInfo.js @@ -207,7 +207,7 @@ const NetInfo = { * Returns a promise that resolves with one of the connectivity types listed * above. */ - fetch(): Promise { + fetch(): Promise { return RCTNetInfo.getCurrentConnectivity().then(resp => resp.network_info); }, @@ -248,14 +248,14 @@ const NetInfo = { _isConnectedSubscriptions.delete(handler); }, - fetch(): Promise { + fetch(): Promise { return NetInfo.fetch().then( (connection) => _isConnected(connection) ); }, }, - isConnectionExpensive(): Promise { + isConnectionExpensive(): Promise { return deprecatedCallback( Platform.OS === 'android' ? RCTNetInfo.isConnectionMetered() : Promise.reject(new Error('Currently not supported on iOS')), Array.prototype.slice.call(arguments), diff --git a/Libraries/ReactIOS/YellowBox.js b/Libraries/ReactIOS/YellowBox.js index 3aa51d537c8239..777a777581d44b 100644 --- a/Libraries/ReactIOS/YellowBox.js +++ b/Libraries/ReactIOS/YellowBox.js @@ -177,7 +177,7 @@ const WarningInspector = ({ class YellowBox extends React.Component { state: { inspecting: ?string; - warningMap: Map; + warningMap: Map; }; _listener: ?EmitterSubscription; dismissWarning: (warning: ?string) => void; diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js index bf313a8cc7c84b..4da6d1d5259ba1 100644 --- a/Libraries/Text/Text.js +++ b/Libraries/Text/Text.js @@ -144,7 +144,7 @@ const Text = React.createClass({ touchableHandlePress: (null: ?Function), touchableHandleLongPress: (null: ?Function), touchableGetPressRectOffset: (null: ?Function), - render(): ReactElement { + render(): ReactElement { let newProps = this.props; if (this.props.onStartShouldSetResponder || this._hasPressHandler()) { if (!this._handlers) { diff --git a/Libraries/Utilities/UIManager.js b/Libraries/Utilities/UIManager.js index 22d97004a82e4a..af12f8ce6d874c 100644 --- a/Libraries/Utilities/UIManager.js +++ b/Libraries/Utilities/UIManager.js @@ -37,7 +37,7 @@ const _takeSnapshot = UIManager.takeSnapshot; * @platform ios */ UIManager.takeSnapshot = async function( - view ?: 'window' | ReactElement | number, + view ?: 'window' | ReactElement | number, options ?: { width ?: number; height ?: number; diff --git a/Libraries/Utilities/deprecatedCallback.js b/Libraries/Utilities/deprecatedCallback.js index 3beefdc483a8dd..c1373da2a64367 100644 --- a/Libraries/Utilities/deprecatedCallback.js +++ b/Libraries/Utilities/deprecatedCallback.js @@ -14,7 +14,7 @@ 'use strict'; -module.exports = function(promise: Promise, callbacks: Array, type: string, warning: string): Promise { +module.exports = function(promise: Promise, callbacks: Array, type: string, warning: string): Promise { if (callbacks.length === 0) { return promise; } From 04d86d819e7082206ba765d1db4da507817f46e4 Mon Sep 17 00:00:00 2001 From: "MIYOKAWA, Nobuyoshi" Date: Tue, 24 May 2016 19:18:53 -0700 Subject: [PATCH 100/843] Update XHR information for Android. Summary: Currently XHR also supports Android platform, so document should include this information. Closes https://github.com/facebook/react-native/pull/7729 Differential Revision: D3345168 fbshipit-source-id: 8dee7d573a47aede5dc5be97640fc711747df65c --- docs/Network.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Network.md b/docs/Network.md index d3bcf9c03ce732..1d1d99f8a34f66 100644 --- a/docs/Network.md +++ b/docs/Network.md @@ -102,7 +102,7 @@ ws.onclose = (e) => { ## XMLHttpRequest -XMLHttpRequest API is implemented on-top of [iOS networking apis](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html). The notable difference from web is the security model: you can read from arbitrary websites on the internet since there is no concept of [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing). +XMLHttpRequest API is implemented on-top of [iOS networking apis](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html) and [OkHttp](http://square.github.io/okhttp/). The notable difference from web is the security model: you can read from arbitrary websites on the internet since there is no concept of [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing). ```js var request = new XMLHttpRequest(); From a8acf8a5ce74228c94a502b4e875dd6b755316bb Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Tue, 24 May 2016 19:24:57 -0700 Subject: [PATCH 101/843] Move cxx module support into oss Reviewed By: mhorowitz Differential Revision: D3319751 fbshipit-source-id: 87f91a541cfbbe45bd8561d94f269ba976a9f702 --- ReactAndroid/DEFS | 4 + ReactAndroid/src/main/jni/xreact/jni/BUCK | 2 +- .../main/jni/xreact/jni/CxxModuleWrapper.cpp | 2 +- .../main/jni/xreact/jni/CxxModuleWrapper.h | 2 +- .../jni/xreact/jni/ModuleRegistryHolder.cpp | 5 +- .../src/main/jni/xreact/perftests/BUCK | 4 +- .../src/main/jni/xreact/perftests/OnLoad.cpp | 4 +- ReactCommon/bridge/BUCK | 91 ++++++++++-- ReactCommon/bridge/CxxModule.h | 132 ++++++++++++++++++ ReactCommon/bridge/JsArgumentHelpers-inl.h | 90 ++++++++++++ ReactCommon/bridge/JsArgumentHelpers.h | 105 ++++++++++++++ ReactCommon/bridge/SampleCxxModule.cpp | 130 +++++++++++++++++ ReactCommon/bridge/SampleCxxModule.h | 51 +++++++ 13 files changed, 602 insertions(+), 20 deletions(-) create mode 100644 ReactCommon/bridge/CxxModule.h create mode 100644 ReactCommon/bridge/JsArgumentHelpers-inl.h create mode 100644 ReactCommon/bridge/JsArgumentHelpers.h create mode 100644 ReactCommon/bridge/SampleCxxModule.cpp create mode 100644 ReactCommon/bridge/SampleCxxModule.h diff --git a/ReactAndroid/DEFS b/ReactAndroid/DEFS index a11e2436fa00f6..028e49c077935c 100644 --- a/ReactAndroid/DEFS +++ b/ReactAndroid/DEFS @@ -10,6 +10,10 @@ import os def react_native_target(path): return '//ReactAndroid/src/main/' + path +# Example: react_native_target('bridge:bridge') +def react_native_xplat_target(path): + return '//ReactCommon/' + path + # Example: react_native_tests_target('java/com/facebook/react/modules:modules') def react_native_tests_target(path): return '//ReactAndroid/src/test/' + path diff --git a/ReactAndroid/src/main/jni/xreact/jni/BUCK b/ReactAndroid/src/main/jni/xreact/jni/BUCK index a68771a8e42b87..57b172b4a9a2ee 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/BUCK +++ b/ReactAndroid/src/main/jni/xreact/jni/BUCK @@ -17,9 +17,9 @@ cxx_library( '//native/third-party/android-ndk:android', '//xplat/folly:molly', '//xplat/fbsystrace:fbsystrace', - '//xplat/react/module:module', react_native_target('jni/react/jni:jni'), react_native_xplat_target('bridge:bridge'), + react_native_xplat_target('bridge:module'), ], srcs = glob(['*.cpp']), exported_headers = EXPORTED_HEADERS, diff --git a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp index 182f0364602414..f0c3e4c0eacdb8 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include diff --git a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.h b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.h index 206eddc565fb4f..53a4d5be153174 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.h +++ b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.h @@ -2,7 +2,7 @@ #pragma once -#include +#include #include #include #include diff --git a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp index c71c95936b96a4..29349f741af1dc 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp @@ -6,10 +6,9 @@ #include -#include -#include - +#include #include +#include #include #include diff --git a/ReactAndroid/src/main/jni/xreact/perftests/BUCK b/ReactAndroid/src/main/jni/xreact/perftests/BUCK index ee130d8517d1e2..d111fa394175a3 100644 --- a/ReactAndroid/src/main/jni/xreact/perftests/BUCK +++ b/ReactAndroid/src/main/jni/xreact/perftests/BUCK @@ -1,3 +1,5 @@ +include_defs('//ReactAndroid/DEFS') + cxx_library( name = 'perftests', srcs = [ 'OnLoad.cpp' ], @@ -9,7 +11,7 @@ cxx_library( '//native:base', '//native/fb:fb', '//xplat/folly:molly', - '//xplat/react/module:module', + react_native_xplat_target('bridge:module'), ], visibility = [ '//instrumentation_tests/com/facebook/react/...', diff --git a/ReactAndroid/src/main/jni/xreact/perftests/OnLoad.cpp b/ReactAndroid/src/main/jni/xreact/perftests/OnLoad.cpp index ba262b67207f2c..4c79034aa6e47f 100644 --- a/ReactAndroid/src/main/jni/xreact/perftests/OnLoad.cpp +++ b/ReactAndroid/src/main/jni/xreact/perftests/OnLoad.cpp @@ -2,8 +2,8 @@ #include #include -#include -#include +#include +#include #include #include diff --git a/ReactCommon/bridge/BUCK b/ReactCommon/bridge/BUCK index 9d5a04cb02650e..cff2c6c6962ed2 100644 --- a/ReactCommon/bridge/BUCK +++ b/ReactCommon/bridge/BUCK @@ -38,7 +38,6 @@ if THIS_IS_FBANDROID: ) elif THIS_IS_FBOBJC: - def react_library(**kwargs): ios_library( name = 'bridge', @@ -55,20 +54,91 @@ elif THIS_IS_FBOBJC: ) ) -LOCAL_HEADERS = [ - 'JSCTracing.h', - 'JSCLegacyProfiler.h', - 'JSCLegacyTracing.h', - 'JSCMemory.h', - 'JSCPerfStats.h', -] +cxx_library( + name = 'module', + header_namespace = 'cxxreact', + force_static = True, + exported_headers = [ + 'CxxModule.h', + 'JsArgumentHelpers.h', + 'JsArgumentHelpers-inl.h', + ], + deps = [ + '//xplat/folly:molly', + ], + visibility = [ + 'PUBLIC', + ], +) + +cxx_library( + name = 'samplemodule', + soname = 'libxplat_react_module_samplemodule.so', + srcs = ['SampleCxxModule.cpp'], + exported_headers = ['SampleCxxModule.h'], + header_namespace = '', + compiler_flags = [ + '-fno-omit-frame-pointer', + '-Wall', + '-Werror', + '-std=c++11', + '-fexceptions', + ], + deps = [ + ':module', + '//xplat/folly:molly', + ], + visibility = [ + 'PUBLIC', + ], +) react_library( soname = 'libreactnativefb.so', header_namespace = 'cxxreact', force_static = True, - srcs = glob(['*.cpp']), - headers = LOCAL_HEADERS, + srcs = [ + 'Instance.cpp', + 'JSCExecutor.cpp', + 'JSCHelpers.cpp', + 'JSCLegacyProfiler.cpp', + 'JSCLegacyTracing.cpp', + 'JSCMemory.cpp', + 'JSCPerfStats.cpp', + 'JSCTracing.cpp', + 'JSCWebWorker.cpp', + 'MethodCall.cpp', + 'ModuleRegistry.cpp', + 'NativeToJsBridge.cpp', + 'Platform.cpp', + 'Value.cpp', + ], + headers = [ + 'JSCLegacyProfiler.h', + 'JSCLegacyTracing.h', + 'JSCMemory.h', + 'JSCPerfStats.h', + 'JSCTracing.h', + ], + exported_headers = [ + 'Executor.h', + 'ExecutorToken.h', + 'ExecutorTokenFactory.h', + 'Instance.h', + 'JSCExecutor.h', + 'JSCHelpers.h', + 'JSCWebWorker.h', + 'JSModulesUnbundle.h', + 'MessageQueueThread.h', + 'MethodCall.h', + 'ModuleRegistry.h', + 'NativeModule.h', + 'NativeToJsBridge.h', + 'noncopyable.h', + 'Platform.h', + 'SystraceSection.h', + 'Value.h', + ], preprocessor_flags = [ '-DLOG_TAG="ReactNative"', '-DWITH_FBSYSTRACE=1', @@ -79,7 +149,6 @@ react_library( '-fvisibility=hidden', '-frtti', ], - exported_headers = glob(['*.h'], excludes=LOCAL_HEADERS), deps = [ '//xplat/fbsystrace:fbsystrace', ], diff --git a/ReactCommon/bridge/CxxModule.h b/ReactCommon/bridge/CxxModule.h new file mode 100644 index 00000000000000..7c4b72ec1289f4 --- /dev/null +++ b/ReactCommon/bridge/CxxModule.h @@ -0,0 +1,132 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#ifndef FBXPLATMODULE +#define FBXPLATMODULE + +#include + +#include + +#include +#include +#include + +using namespace std::placeholders; + +namespace facebook { namespace xplat { namespace module { + +/** + * Base class for Catalyst native modules whose implementations are + * written in C++. Native methods are represented by instances of the + * Method struct. Generally, a derived class will manage an instance + * which represents the data for the module, and non-Catalyst-specific + * methods can be wrapped in lambdas which convert between + * folly::dynamic and native C++ objects. The Callback arguments will + * pass through to js functions passed to the analogous javascript + * methods. At most two callbacks will be converted. Results should + * be passed to the first callback, and errors to the second callback. + * Exceptions thrown by a method will be converted to platform + * exceptions, and handled however they are handled on that platform. + * (TODO mhorowitz #7128529: this exception behavior is not yet + * implemented.) + * + * There are two sets of constructors here. The first set initializes + * a Method using a name and anything convertible to a std::function. + * This is most useful for registering a lambda as a RN method. There + * are overloads to support functions which take no arguments, + * arguments only, and zero, one, or two callbacks. + * + * The second set of methods is similar, but instead of taking a + * function, takes the method name, an object, and a pointer to a + * method on that object. + */ + +class CxxModule { +public: + typedef std::function)> Callback; + + struct Method { + std::string name; + size_t callbacks; + std::function func; + + // std::function/lambda ctors + + Method(std::string aname, + std::function&& afunc) + : name(std::move(aname)) + , callbacks(0) + , func(std::bind(std::move(afunc))) {} + + Method(std::string aname, + std::function&& afunc) + : name(std::move(aname)) + , callbacks(0) + , func(std::bind(std::move(afunc), _1)) {} + + Method(std::string aname, + std::function&& afunc) + : name(std::move(aname)) + , callbacks(1) + , func(std::bind(std::move(afunc), _1, _2)) {} + + Method(std::string aname, + std::function&& afunc) + : name(std::move(aname)) + , callbacks(2) + , func(std::move(afunc)) {} + + // method pointer ctors + + template + Method(std::string aname, T* t, void (T::*method)()) + : name(std::move(aname)) + , callbacks(0) + , func(std::bind(method, t)) {} + + template + Method(std::string aname, T* t, void (T::*method)(folly::dynamic)) + : name(std::move(aname)) + , callbacks(0) + , func(std::bind(method, t, _1)) {} + + template + Method(std::string aname, T* t, void (T::*method)(folly::dynamic, Callback)) + : name(std::move(aname)) + , callbacks(1) + , func(std::bind(method, t, _1, _2)) {} + + template + Method(std::string aname, T* t, void (T::*method)(folly::dynamic, Callback, Callback)) + : name(std::move(aname)) + , callbacks(2) + , func(std::bind(method, t, _1, _2, _3)) {} + }; + + /** + * This may block, if necessary to complete cleanup before the + * object is destroyed. + */ + virtual ~CxxModule() {} + + /** + * @return the name of this module. This will be the name used to {@code require()} this module + * from javascript. + */ + virtual std::string getName() = 0; + + /** + * Each entry in the map will be exported as a property to JS. The + * key is the property name, and the value can be anything. + */ + virtual auto getConstants() -> std::map = 0; + + /** + * @return a list of methods this module exports to JS. + */ + virtual auto getMethods() -> std::vector = 0; +}; + +}}} + +#endif diff --git a/ReactCommon/bridge/JsArgumentHelpers-inl.h b/ReactCommon/bridge/JsArgumentHelpers-inl.h new file mode 100644 index 00000000000000..ce8de89364b5d0 --- /dev/null +++ b/ReactCommon/bridge/JsArgumentHelpers-inl.h @@ -0,0 +1,90 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +namespace facebook { +namespace xplat { + +namespace detail { + +template +R jsArg1(const folly::dynamic& arg, M asFoo, const T&... desc) { + try { + return (arg.*asFoo)(); + } catch (const folly::TypeError& ex) { + throw JsArgumentException( + folly::to( + "Error converting javascript arg ", desc..., " to C++: ", ex.what())); + } catch (const std::range_error& ex) { + throw JsArgumentException( + folly::to( + "Could not convert argument ", desc..., " to required type: ", ex.what())); + } +} + +} + +template +R jsArg(const folly::dynamic& arg, R (folly::dynamic::*asFoo)() const, const T&... desc) { + return detail::jsArg1(arg, asFoo, desc...); +} + +template +R jsArg(const folly::dynamic& arg, R (folly::dynamic::*asFoo)() const&, const T&... desc) { + return detail::jsArg1(arg, asFoo, desc...); +} + +template +typename detail::is_dynamic::type& jsArgAsDynamic(T&& args, size_t n) { + try { + return args[n]; + } catch (const std::out_of_range& ex) { + // Use 1-base counting for argument description. + throw JsArgumentException( + folly::to( + "JavaScript provided ", args.size(), + " arguments for C++ method which references at least ", n + 1, + " arguments: ", ex.what())); + } +} + +template +R jsArgN(const folly::dynamic& args, size_t n, R (folly::dynamic::*asFoo)() const) { + return jsArg(jsArgAsDynamic(args, n), asFoo, n); +} +template +R jsArgN(const folly::dynamic& args, size_t n, R (folly::dynamic::*asFoo)() const&) { + return jsArg(jsArgAsDynamic(args, n), asFoo, n); +} + +namespace detail { + +// This is a helper for jsArgAsArray and jsArgAsObject. + +template +typename detail::is_dynamic::type& jsArgAsType(T&& args, size_t n, const char* required, + bool (folly::dynamic::*isFoo)() const) { + T& ret = jsArgAsDynamic(args, n); + if ((ret.*isFoo)()) { + return ret; + } + + // Use 1-base counting for argument description. + throw JsArgumentException( + folly::to( + "Argument ", n + 1, " of type ", ret.typeName(), " is not required type ", required)); +} + +} // end namespace detail + +template +typename detail::is_dynamic::type& jsArgAsArray(T&& args, size_t n) { + return detail::jsArgAsType(args, n, "Array", &folly::dynamic::isArray); +} + +template +typename detail::is_dynamic::type& jsArgAsObject(T&& args, size_t n) { + return detail::jsArgAsType(args, n, "Object", &folly::dynamic::isObject); +} + +}} diff --git a/ReactCommon/bridge/JsArgumentHelpers.h b/ReactCommon/bridge/JsArgumentHelpers.h new file mode 100644 index 00000000000000..80e3e48bd950dd --- /dev/null +++ b/ReactCommon/bridge/JsArgumentHelpers.h @@ -0,0 +1,105 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include + +#include + +// When building a cross-platform module for React Native, arguments passed +// from JS are represented as a folly::dynamic. This class provides helpers to +// extract arguments from the folly::dynamic to concrete types usable by +// cross-platform code, and converting exceptions to a JsArgumentException so +// they can be caught and reported to RN consistently. The goal is to make the +// jsArgAs... methods at the end simple to use should be most common, but any +// non-detail method can be used when needed. + +namespace facebook { +namespace xplat { + +class JsArgumentException : public std::logic_error { +public: + JsArgumentException(const std::string& msg) : std::logic_error(msg) {} +}; + +// This extracts a single argument by calling the given method pointer on it. +// If an exception is thrown, the additional arguments are passed to +// folly::to<> to be included in the exception string. This will be most +// commonly used when extracting values from non-scalar argument. The second +// overload accepts ref-qualified member functions. + +template +R jsArg(const folly::dynamic& arg, R (folly::dynamic::*asFoo)() const, const T&... desc); +template +R jsArg(const folly::dynamic& arg, R (folly::dynamic::*asFoo)() const&, const T&... desc); + +// This is like jsArg, but a operates on a dynamic representing an array of +// arguments. The argument n is used both to index the array and build the +// exception message, if any. It can be used directly, but will more often be +// used by the type-specific methods following. + +template +R jsArgN(const folly::dynamic& args, size_t n, R (folly::dynamic::*asFoo)() const); +template +R jsArgN(const folly::dynamic& args, size_t n, R (folly::dynamic::*asFoo)() const&); + +namespace detail { + +// This is a type helper to implement functions which should work on both const +// and non-const folly::dynamic arguments, and return a type with the same +// constness. Basically, it causes the templates which use it to be defined +// only for types compatible with folly::dynamic. +template +struct is_dynamic { + typedef typename std::enable_if::value, T>::type type; +}; + +} // end namespace detail + +// Easy to use conversion helpers are here: + +// Extract the n'th arg from the given dynamic, as a dynamic. Throws a +// JsArgumentException if there is no n'th arg in the input. +template +typename detail::is_dynamic::type& jsArgAsDynamic(T&& args, size_t n); + +// Extract the n'th arg from the given dynamic, as a dynamic Array. Throws a +// JsArgumentException if there is no n'th arg in the input, or it is not an +// Array. +template +typename detail::is_dynamic::type& jsArgAsArray(T&& args, size_t n); + +// Extract the n'th arg from the given dynamic, as a dynamic Object. Throws a +// JsArgumentException if there is no n'th arg in the input, or it is not an +// Object. +template +typename detail::is_dynamic::type& jsArgAsObject(T&& args, size_t n); + +// Extract the n'th arg from the given dynamic, as a bool. Throws a +// JsArgumentException if this fails for some reason. +inline bool jsArgAsBool(const folly::dynamic& args, size_t n) { + return jsArgN(args, n, &folly::dynamic::asBool); +} + +// Extract the n'th arg from the given dynamic, as an integer. Throws a +// JsArgumentException if this fails for some reason. +inline int64_t jsArgAsInt(const folly::dynamic& args, size_t n) { + return jsArgN(args, n, &folly::dynamic::asInt); +} + +// Extract the n'th arg from the given dynamic, as a double. Throws a +// JsArgumentException if this fails for some reason. +inline double jsArgAsDouble(const folly::dynamic& args, size_t n) { + return jsArgN(args, n, &folly::dynamic::asDouble); +} + +// Extract the n'th arg from the given dynamic, as a string. Throws a +// JsArgumentException if this fails for some reason. +inline std::string jsArgAsString(const folly::dynamic& args, size_t n) { + return jsArgN(args, n, &folly::dynamic::asString); +} + +}} + +#include "JsArgumentHelpers-inl.h" diff --git a/ReactCommon/bridge/SampleCxxModule.cpp b/ReactCommon/bridge/SampleCxxModule.cpp new file mode 100644 index 00000000000000..08ba2ce211bc10 --- /dev/null +++ b/ReactCommon/bridge/SampleCxxModule.cpp @@ -0,0 +1,130 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "SampleCxxModule.h" +#include + +#include +#include + +#include + +using namespace folly; + +namespace facebook { namespace xplat { namespace samples { + +std::string Sample::hello() { + LOG(WARNING) << "glog: hello, world"; + return "hello"; +} + +double Sample::add(double a, double b) { + return a + b; +} + +std::string Sample::concat(const std::string& a, const std::string& b) { + return a + b; +} + +std::string Sample::repeat(int count, const std::string& str) { + std::string ret; + for (int i = 0; i < count; i++) { + ret += str; + } + + return ret; +} + +void Sample::save(std::map dict) +{ + state_ = std::move(dict); +} + +std::map Sample::load() { + return state_; +} + +void Sample::except() { +// TODO mhorowitz #7128529: There's no way to automatically test this +// right now. + // throw std::runtime_error("oops"); +} + +void Sample::call_later(int msec, std::function f) { + std::thread t([=] { + std::this_thread::sleep_for(std::chrono::milliseconds(msec)); + f(); + }); + t.detach(); +} + +SampleCxxModule::SampleCxxModule(std::unique_ptr sample) + : sample_(std::move(sample)) {} + +std::string SampleCxxModule::getName() { + return "Sample"; +} + +auto SampleCxxModule::getConstants() -> std::map { + return { + { "one", 1 }, + { "two", 2 }, + { "animal", "fox" }, + }; +} + +auto SampleCxxModule::getMethods() -> std::vector { + return { + Method("hello", [this] { + sample_->hello(); + }), + Method("add", [this](dynamic args, Callback cb) { + LOG(WARNING) << "Sample: add => " + << sample_->add(jsArgAsDouble(args, 0), jsArgAsDouble(args, 1)); + cb({sample_->add(jsArgAsDouble(args, 0), jsArgAsDouble(args, 1))}); + }), + Method("concat", [this](dynamic args, Callback cb) { + cb({sample_->concat(jsArgAsString(args, 0), + jsArgAsString(args, 1))}); + }), + Method("repeat", [this](dynamic args, Callback cb) { + cb({sample_->repeat(jsArgAsInt(args, 0), + jsArgAsString(args, 1))}); + }), + Method("save", this, &SampleCxxModule::save), + Method("load", this, &SampleCxxModule::load), + Method("call_later", [this](dynamic args, Callback cb) { + sample_->call_later(jsArgAsInt(args, 0), [cb] { + cb({}); + }); + }), + Method("except", [this] { + sample_->except(); + }), + }; +} + +void SampleCxxModule::save(folly::dynamic args) { + std::map m; + for (const auto& p : jsArgN(args, 0, &dynamic::items)) { + m.emplace(jsArg(p.first, &dynamic::asString, "map key"), + jsArg(p.second, &dynamic::asString, "map value")); + } + sample_->save(std::move(m)); +} + +void SampleCxxModule::load(folly::dynamic args, Callback cb) { + dynamic d = dynamic::object; + for (const auto& p : sample_->load()) { + d.insert(p.first, p.second); + } + cb({d}); +} + +}}} + +// By convention, the function name should be the same as the class +// name. +extern "C" facebook::xplat::module::CxxModule *SampleCxxModule() { + return new facebook::xplat::samples::SampleCxxModule( + folly::make_unique()); +} diff --git a/ReactCommon/bridge/SampleCxxModule.h b/ReactCommon/bridge/SampleCxxModule.h new file mode 100644 index 00000000000000..5867208b686aa9 --- /dev/null +++ b/ReactCommon/bridge/SampleCxxModule.h @@ -0,0 +1,51 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#ifndef FBSAMPLEXPLATMODULE +#define FBSAMPLEXPLATMODULE + +#include + +#include +#include + +namespace facebook { namespace xplat { namespace samples { + +// In a less contrived example, Sample would be part of a traditional +// C++ library. + +class Sample { +public: + std::string hello(); + double add(double a, double b); + std::string concat(const std::string& a, const std::string& b); + std::string repeat(int count, const std::string& str); + void save(std::map dict); + std::map load(); + void call_later(int msec, std::function f); + void except(); + +private: + std::map state_; +}; + +class SampleCxxModule : public module::CxxModule { +public: + SampleCxxModule(std::unique_ptr sample); + + std::string getName(); + + virtual auto getConstants() -> std::map; + + virtual auto getMethods() -> std::vector; + +private: + void save(folly::dynamic args); + void load(folly::dynamic args, Callback cb); + + std::unique_ptr sample_; +}; + +}}} + +#endif + From 62f53ffaa7f5ccdebe133ce0d212ee80fc85c37c Mon Sep 17 00:00:00 2001 From: Raincal Date: Wed, 25 May 2016 00:08:01 -0700 Subject: [PATCH 102/843] remove paddingTop in AnimatedGratuitousApp Summary: Please see the screenshot below. ![qq20160525-0](https://cloud.githubusercontent.com/assets/6279478/15526182/74170a9e-2262-11e6-8f99-5e121ed7b9c1.png) Closes https://github.com/facebook/react-native/pull/7733 Differential Revision: D3345791 fbshipit-source-id: 4e5885197a42b760a800970156fcdebadf227575 --- Examples/UIExplorer/AnimatedGratuitousApp/AnExApp.js | 1 - 1 file changed, 1 deletion(-) diff --git a/Examples/UIExplorer/AnimatedGratuitousApp/AnExApp.js b/Examples/UIExplorer/AnimatedGratuitousApp/AnExApp.js index 58af5045a197e6..20153889ab471e 100644 --- a/Examples/UIExplorer/AnimatedGratuitousApp/AnExApp.js +++ b/Examples/UIExplorer/AnimatedGratuitousApp/AnExApp.js @@ -281,7 +281,6 @@ function moveToClosest({activeKey, keys, restLayouts}, position) { var styles = StyleSheet.create({ container: { flex: 1, - paddingTop: 64, // push content below nav bar }, grid: { flex: 1, From b71db1155440ec433dcf51b54c015166ecca8732 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Wed, 25 May 2016 04:17:35 -0700 Subject: [PATCH 103/843] Update RCTNetworking, RCTNetInfo and RCTLocationManager to use new events system Summary: Updated networking and geolocation to use the new events system. Reviewed By: bestander Differential Revision: D3346129 fbshipit-source-id: 957716e54d7af8c4a6783f684098e92e92f19654 --- Examples/UIExplorer/AppStateExample.js | 32 +++++- Examples/UIExplorer/AppStateIOSExample.js | 99 ------------------- Examples/UIExplorer/UIExplorerList.ios.js | 4 - .../RCTEventDispatcherTests.m | 2 - Examples/UIExplorer/XHRExample.ios.js | 11 ++- Libraries/Geolocation/Geolocation.js | 17 ++-- Libraries/Geolocation/RCTLocationObserver.h | 4 +- Libraries/Geolocation/RCTLocationObserver.m | 20 ++-- Libraries/LinkingIOS/RCTLinkingManager.m | 3 + Libraries/Network/NetInfo.js | 6 +- Libraries/Network/RCTNetInfo.h | 6 +- Libraries/Network/RCTNetInfo.m | 41 ++++---- Libraries/Network/RCTNetworking.android.js | 55 ++++++++--- Libraries/Network/RCTNetworking.h | 6 +- Libraries/Network/RCTNetworking.ios.js | 38 +++++-- Libraries/Network/RCTNetworking.m | 37 +++---- Libraries/Network/XMLHttpRequest.android.js | 62 ------------ Libraries/Network/XMLHttpRequest.ios.js | 47 --------- ...MLHttpRequestBase.js => XMLHttpRequest.js} | 34 ++++--- ...estBase-test.js => XMLHttpRequest-test.js} | 17 ++-- .../RCTPushNotificationManager.m | 9 ++ Libraries/react-native/react-native.js | 14 +-- Libraries/react-native/react-native.js.flow | 2 + React/Modules/RCTAccessibilityManager.m | 3 + React/Modules/RCTDevMenu.m | 9 ++ React/Modules/RCTEventEmitter.m | 4 +- React/Modules/RCTUIManager.m | 3 + 27 files changed, 240 insertions(+), 345 deletions(-) delete mode 100644 Examples/UIExplorer/AppStateIOSExample.js delete mode 100644 Libraries/Network/XMLHttpRequest.android.js delete mode 100644 Libraries/Network/XMLHttpRequest.ios.js rename Libraries/Network/{XMLHttpRequestBase.js => XMLHttpRequest.js} (94%) rename Libraries/Network/__tests__/{XMLHttpRequestBase-test.js => XMLHttpRequest-test.js} (91%) diff --git a/Examples/UIExplorer/AppStateExample.js b/Examples/UIExplorer/AppStateExample.js index b409e836479fb6..0f9c3c321a3da2 100644 --- a/Examples/UIExplorer/AppStateExample.js +++ b/Examples/UIExplorer/AppStateExample.js @@ -1,4 +1,11 @@ /** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * * The examples provided by Facebook are for non-commercial testing and * evaluation purposes only. * @@ -16,9 +23,9 @@ */ 'use strict'; -var React = require('react'); -var ReactNative = require('react-native'); -var { +const React = require('react'); +const ReactNative = require('react-native'); +const { AppState, Text, View @@ -29,13 +36,19 @@ var AppStateSubscription = React.createClass({ return { appState: AppState.currentState, previousAppStates: [], + memoryWarnings: 0, }; }, componentDidMount: function() { AppState.addEventListener('change', this._handleAppStateChange); + AppState.addEventListener('memoryWarning', this._handleMemoryWarning); }, componentWillUnmount: function() { AppState.removeEventListener('change', this._handleAppStateChange); + AppState.removeEventListener('memoryWarning', this._handleMemoryWarning); + }, + _handleMemoryWarning: function() { + this.setState({memoryWarnings: this.state.memoryWarnings + 1}); }, _handleAppStateChange: function(appState) { var previousAppStates = this.state.previousAppStates.slice(); @@ -46,6 +59,13 @@ var AppStateSubscription = React.createClass({ }); }, render() { + if (this.props.showMemoryWarnings) { + return ( + + {this.state.memoryWarnings} + + ); + } if (this.props.showCurrentOnly) { return ( @@ -78,4 +98,10 @@ exports.examples = [ title: 'Previous states:', render(): ReactElement { return ; } }, + { + platform: 'ios', + title: 'Memory Warnings', + description: 'In the IOS simulator, hit Shift+Command+M to simulate a memory warning.', + render(): ReactElement { return ; } + }, ]; diff --git a/Examples/UIExplorer/AppStateIOSExample.js b/Examples/UIExplorer/AppStateIOSExample.js deleted file mode 100644 index 7598df7fb9ec0a..00000000000000 --- a/Examples/UIExplorer/AppStateIOSExample.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - * @providesModule AppStateIOSExample - * @flow - */ -'use strict'; - -var React = require('react'); -var ReactNative = require('react-native'); -var { - AppStateIOS, - Text, - View -} = ReactNative; - -var AppStateSubscription = React.createClass({ - getInitialState() { - return { - appState: AppStateIOS.currentState, - previousAppStates: [], - memoryWarnings: 0, - }; - }, - componentDidMount: function() { - AppStateIOS.addEventListener('change', this._handleAppStateChange); - AppStateIOS.addEventListener('memoryWarning', this._handleMemoryWarning); - }, - componentWillUnmount: function() { - AppStateIOS.removeEventListener('change', this._handleAppStateChange); - AppStateIOS.removeEventListener('memoryWarning', this._handleMemoryWarning); - }, - _handleMemoryWarning: function() { - this.setState({memoryWarnings: this.state.memoryWarnings + 1}); - }, - _handleAppStateChange: function(appState) { - var previousAppStates = this.state.previousAppStates.slice(); - previousAppStates.push(this.state.appState); - this.setState({ - appState, - previousAppStates, - }); - }, - render() { - if (this.props.showMemoryWarnings) { - return ( - - {this.state.memoryWarnings} - - ); - } - if (this.props.showCurrentOnly) { - return ( - - {this.state.appState} - - ); - } - return ( - - {JSON.stringify(this.state.previousAppStates)} - - ); - } -}); - -exports.title = 'AppStateIOS'; -exports.description = 'iOS app background status'; -exports.examples = [ - { - title: 'AppStateIOS.currentState', - description: 'Can be null on app initialization', - render() { return {AppStateIOS.currentState}; } - }, - { - title: 'Subscribed AppStateIOS:', - description: 'This changes according to the current state, so you can only ever see it rendered as "active"', - render(): ReactElement { return ; } - }, - { - title: 'Previous states:', - render(): ReactElement { return ; } - }, - { - title: 'Memory Warnings', - description: 'In the simulator, hit Shift+Command+M to simulate a memory warning.', - render(): ReactElement { return ; } - }, -]; diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index c63763e1e5813d..e9ddd367b7572d 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -171,10 +171,6 @@ const APIExamples: Array = [ key: 'AnExApp', module: require('./AnimatedGratuitousApp/AnExApp'), }, - { - key: 'AppStateIOSExample', - module: require('./AppStateIOSExample'), - }, { key: 'AppStateExample', module: require('./AppStateExample'), diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m index 4fe88ee933b28f..e9559d55840dda 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m @@ -105,9 +105,7 @@ - (void)testLegacyEventsAreImmediatelyDispatched #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_eventDispatcher sendDeviceEventWithName:_eventName body:_body]; - #pragma clang diagnostic pop [_bridge verify]; diff --git a/Examples/UIExplorer/XHRExample.ios.js b/Examples/UIExplorer/XHRExample.ios.js index b84c8b0a090918..0deaa4eedf45fe 100644 --- a/Examples/UIExplorer/XHRExample.ios.js +++ b/Examples/UIExplorer/XHRExample.ios.js @@ -1,4 +1,11 @@ /** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * * The examples provided by Facebook are for non-commercial testing and * evaluation purposes only. * @@ -21,7 +28,7 @@ var { AlertIOS, CameraRoll, Image, - LinkingIOS, + Linking, ProgressViewIOS, StyleSheet, Text, @@ -215,7 +222,7 @@ class FormUploader extends React.Component { return; } var url = xhr.responseText.slice(index).split('\n')[0]; - LinkingIOS.openURL(url); + Linking.openURL(url); }; var formdata = new FormData(); if (this.state.randomPhoto) { diff --git a/Libraries/Geolocation/Geolocation.js b/Libraries/Geolocation/Geolocation.js index f49a9ebf0eaf94..f2eed384b1c745 100644 --- a/Libraries/Geolocation/Geolocation.js +++ b/Libraries/Geolocation/Geolocation.js @@ -11,15 +11,16 @@ */ 'use strict'; -var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); -var RCTLocationObserver = require('NativeModules').LocationObserver; +const NativeEventEmitter = require('NativeEventEmitter'); +const RCTLocationObserver = require('NativeModules').LocationObserver; -var invariant = require('fbjs/lib/invariant'); -var logError = require('logError'); -var warning = require('fbjs/lib/warning'); +const invariant = require('fbjs/lib/invariant'); +const logError = require('logError'); +const warning = require('fbjs/lib/warning'); -var subscriptions = []; +const LocationEventEmitter = new NativeEventEmitter(RCTLocationObserver); +var subscriptions = []; var updatesEnabled = false; type GeoOptions = { @@ -80,11 +81,11 @@ var Geolocation = { } var watchID = subscriptions.length; subscriptions.push([ - RCTDeviceEventEmitter.addListener( + LocationEventEmitter.addListener( 'geolocationDidChange', success ), - error ? RCTDeviceEventEmitter.addListener( + error ? LocationEventEmitter.addListener( 'geolocationError', error ) : null, diff --git a/Libraries/Geolocation/RCTLocationObserver.h b/Libraries/Geolocation/RCTLocationObserver.h index 873eeff6a11375..a607155584a7fa 100644 --- a/Libraries/Geolocation/RCTLocationObserver.h +++ b/Libraries/Geolocation/RCTLocationObserver.h @@ -7,8 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTBridgeModule.h" +#import "RCTEventEmitter.h" -@interface RCTLocationObserver : NSObject +@interface RCTLocationObserver : RCTEventEmitter @end diff --git a/Libraries/Geolocation/RCTLocationObserver.m b/Libraries/Geolocation/RCTLocationObserver.m index 23179c5cab214e..319477c99ac0b7 100644 --- a/Libraries/Geolocation/RCTLocationObserver.m +++ b/Libraries/Geolocation/RCTLocationObserver.m @@ -113,8 +113,6 @@ @implementation RCTLocationObserver RCT_EXPORT_MODULE() -@synthesize bridge = _bridge; - #pragma mark - Lifecycle - (void)dealloc @@ -128,8 +126,12 @@ - (dispatch_queue_t)methodQueue return dispatch_get_main_queue(); } -#pragma mark - Private API +- (NSArray *)supportedEvents +{ + return @[@"geolocationDidChange", @"geolocationError"]; +} +#pragma mark - Private API - (void)beginLocationUpdatesWithDesiredAccuracy:(CLLocationAccuracy)desiredAccuracy distanceFilter:(CLLocationDistance)distanceFilter { @@ -279,11 +281,7 @@ - (void)locationManager:(CLLocationManager *)manager // Send event if (_observingLocation) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"geolocationDidChange" - body:_lastLocationEvent]; -#pragma clang diagnostic pop + [self sendEventWithName:@"geolocationDidChange" body:_lastLocationEvent]; } // Fire all queued callbacks @@ -324,11 +322,7 @@ - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError * // Send event if (_observingLocation) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"geolocationError" - body:jsError]; -#pragma clang diagnostic pop + [self sendEventWithName:@"geolocationError" body:jsError]; } // Fire all queued error callbacks diff --git a/Libraries/LinkingIOS/RCTLinkingManager.m b/Libraries/LinkingIOS/RCTLinkingManager.m index 96f9e75b49c905..ca4112fc75c9fe 100644 --- a/Libraries/LinkingIOS/RCTLinkingManager.m +++ b/Libraries/LinkingIOS/RCTLinkingManager.m @@ -88,8 +88,11 @@ + (BOOL)application:(UIApplication *)application - (void)handleOpenURLNotification:(NSNotification *)notification { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"openURL" body:notification.userInfo]; +#pragma clang diagnostic pop } RCT_EXPORT_METHOD(openURL:(NSURL *)URL diff --git a/Libraries/Network/NetInfo.js b/Libraries/Network/NetInfo.js index 7d4900f8ed88ee..0cbb88f48a44bb 100644 --- a/Libraries/Network/NetInfo.js +++ b/Libraries/Network/NetInfo.js @@ -12,12 +12,14 @@ 'use strict'; const Map = require('Map'); +const NativeEventEmitter = require('NativeEventEmitter'); const NativeModules = require('NativeModules'); const Platform = require('Platform'); -const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); const RCTNetInfo = NativeModules.NetInfo; const deprecatedCallback = require('deprecatedCallback'); +const NetInfoEventEmitter = new NativeEventEmitter(RCTNetInfo); + const DEVICE_CONNECTIVITY_EVENT = 'networkStatusDidChange'; type ChangeEventName = $Enum<{ @@ -176,7 +178,7 @@ const NetInfo = { eventName: ChangeEventName, handler: Function ): {remove: () => void} { - const listener = RCTDeviceEventEmitter.addListener( + const listener = NetInfoEventEmitter.addListener( DEVICE_CONNECTIVITY_EVENT, (appStateData) => { handler(appStateData.network_info); diff --git a/Libraries/Network/RCTNetInfo.h b/Libraries/Network/RCTNetInfo.h index 6c2556e0a50b6e..9c280b2cf4073a 100644 --- a/Libraries/Network/RCTNetInfo.h +++ b/Libraries/Network/RCTNetInfo.h @@ -9,10 +9,10 @@ #import -#import "RCTBridgeModule.h" +#import "RCTEventEmitter.h" -@interface RCTNetInfo : NSObject +@interface RCTNetInfo : RCTEventEmitter -- (instancetype)initWithHost:(NSString *)host NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithHost:(NSString *)host; @end diff --git a/Libraries/Network/RCTNetInfo.m b/Libraries/Network/RCTNetInfo.m index 28d0fc07387039..5981e9f7f82a08 100644 --- a/Libraries/Network/RCTNetInfo.m +++ b/Libraries/Network/RCTNetInfo.m @@ -22,10 +22,9 @@ @implementation RCTNetInfo { SCNetworkReachabilityRef _reachability; NSString *_status; + NSString *_host; } -@synthesize bridge = _bridge; - RCT_EXPORT_MODULE() static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info) @@ -51,11 +50,7 @@ static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SC if (![status isEqualToString:self->_status]) { self->_status = status; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [self->_bridge.eventDispatcher sendDeviceEventWithName:@"networkStatusDidChange" - body:@{@"network_info": status}]; -#pragma clang diagnostic pop + [self sendEventWithName:@"networkStatusDidChange" body:@{@"network_info": status}]; } } @@ -66,34 +61,40 @@ - (instancetype)initWithHost:(NSString *)host RCTAssertParam(host); RCTAssert(![host hasPrefix:@"http"], @"Host value should just contain the domain, not the URL scheme."); - if ((self = [super init])) { - _status = RCTReachabilityStateUnknown; - _reachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, host.UTF8String); - SCNetworkReachabilityContext context = { 0, ( __bridge void *)self, NULL, NULL, NULL }; - SCNetworkReachabilitySetCallback(_reachability, RCTReachabilityCallback, &context); - SCNetworkReachabilityScheduleWithRunLoop(_reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); + if ((self = [self init])) { + _host = [host copy]; } return self; } -- (instancetype)init +- (NSArray *)supportedEvents { - return [self initWithHost:@"apple.com"]; + return @[@"networkStatusDidChange"]; } -- (void)dealloc +- (void)startObserving { - SCNetworkReachabilityUnscheduleFromRunLoop(_reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); - CFRelease(_reachability); + _status = RCTReachabilityStateUnknown; + _reachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, _host.UTF8String ?: "apple.com"); + SCNetworkReachabilityContext context = { 0, ( __bridge void *)self, NULL, NULL, NULL }; + SCNetworkReachabilitySetCallback(_reachability, RCTReachabilityCallback, &context); + SCNetworkReachabilityScheduleWithRunLoop(_reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); +} + +- (void)stopObserving +{ + if (_reachability) { + SCNetworkReachabilityUnscheduleFromRunLoop(_reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); + CFRelease(_reachability); + } } #pragma mark - Public API -// TODO: remove error callback - not needed except by Subscribable interface RCT_EXPORT_METHOD(getCurrentConnectivity:(RCTPromiseResolveBlock)resolve reject:(__unused RCTPromiseRejectBlock)reject) { - resolve(@{@"network_info": _status}); + resolve(@{@"network_info": _status ?: RCTReachabilityStateUnknown}); } @end diff --git a/Libraries/Network/RCTNetworking.android.js b/Libraries/Network/RCTNetworking.android.js index 38f694d5452f57..23aa9e8bf54199 100644 --- a/Libraries/Network/RCTNetworking.android.js +++ b/Libraries/Network/RCTNetworking.android.js @@ -12,39 +12,66 @@ // Do not require the native RCTNetworking module directly! Use this wrapper module instead. // It will add the necessary requestId, so that you don't have to generate it yourself. -var RCTNetworkingNative = require('NativeModules').Networking; +const FormData = require('FormData'); +const NativeEventEmitter = require('NativeEventEmitter'); +const RCTNetworkingNative = require('NativeModules').Networking; -var _requestId = 1; -var generateRequestId = function() { +type Header = [string, string]; + +function convertHeadersMapToArray(headers: Object): Array
    { + const headerArray = []; + for (const name in headers) { + headerArray.push([name, headers[name]]); + } + return headerArray; +} + +let _requestId = 1; +function generateRequestId() { return _requestId++; -}; +} /** * This class is a wrapper around the native RCTNetworking module. It adds a necessary unique * requestId to each network request that can be used to abort that request later on. */ -class RCTNetworking { +class RCTNetworking extends NativeEventEmitter { + + constructor() { + super(RCTNetworkingNative); + } - static sendRequest(method, url, headers, data, useIncrementalUpdates, timeout) { - var requestId = generateRequestId(); + sendRequest(method, url, headers, data, incrementalUpdates, timeout, callback) { + if (typeof data === 'string') { + data = {string: data}; + } else if (data instanceof FormData) { + data = { + formData: data.getParts().map((part) => { + part.headers = convertHeadersMapToArray(part.headers); + return part; + }), + }; + } + const requestId = generateRequestId(); RCTNetworkingNative.sendRequest( method, url, requestId, - headers, + convertHeadersMapToArray(headers), data, - useIncrementalUpdates, - timeout); - return requestId; + incrementalUpdates, + timeout + ); + callback(requestId); } - static abortRequest(requestId) { + abortRequest(requestId) { RCTNetworkingNative.abortRequest(requestId); } - static clearCookies(callback) { + clearCookies(callback) { RCTNetworkingNative.clearCookies(callback); } } -module.exports = RCTNetworking; +module.exports = new RCTNetworking(); diff --git a/Libraries/Network/RCTNetworking.h b/Libraries/Network/RCTNetworking.h index cd8b99dde4df3d..a06a1eeab95550 100644 --- a/Libraries/Network/RCTNetworking.h +++ b/Libraries/Network/RCTNetworking.h @@ -7,12 +7,10 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import - -#import "RCTBridge.h" +#import "RCTEventEmitter.h" #import "RCTNetworkTask.h" -@interface RCTNetworking : NSObject +@interface RCTNetworking : RCTEventEmitter /** * Does a handler exist for the specified request? diff --git a/Libraries/Network/RCTNetworking.ios.js b/Libraries/Network/RCTNetworking.ios.js index 677f2c1c94a720..3e0e4c35284119 100644 --- a/Libraries/Network/RCTNetworking.ios.js +++ b/Libraries/Network/RCTNetworking.ios.js @@ -10,21 +10,39 @@ */ 'use strict'; -var RCTNetworkingNative = require('NativeModules').Networking; +const FormData = require('FormData'); +const NativeEventEmitter = require('NativeEventEmitter'); +const RCTNetworkingNative = require('NativeModules').Networking; -/** - * This class is a wrapper around the native RCTNetworking module. - */ -class RCTNetworking { +class RCTNetworking extends NativeEventEmitter { - static sendRequest(query, callback) { - RCTNetworkingNative.sendRequest(query, callback); + constructor() { + super(RCTNetworkingNative); } - static abortRequest(requestId) { - RCTNetworkingNative.cancelRequest(requestId); + sendRequest(method, url, headers, data, incrementalUpdates, timeout, callback) { + if (typeof data === 'string') { + data = {string: data}; + } else if (data instanceof FormData) { + data = {formData: data.getParts()}; + } + RCTNetworkingNative.sendRequest({ + method, + url, + data, + headers, + incrementalUpdates, + timeout + }, callback); } + abortRequest(requestId) { + RCTNetworkingNative.abortRequest(requestId); + } + + clearCookies(callback) { + console.warn('RCTNetworking.clearCookies is not supported on iOS'); + } } -module.exports = RCTNetworking; +module.exports = new RCTNetworking(); diff --git a/Libraries/Network/RCTNetworking.m b/Libraries/Network/RCTNetworking.m index 5c7e3b9ee0edcb..07c21f095d3fad 100644 --- a/Libraries/Network/RCTNetworking.m +++ b/Libraries/Network/RCTNetworking.m @@ -129,11 +129,18 @@ @implementation RCTNetworking NSArray> *_handlers; } -@synthesize bridge = _bridge; @synthesize methodQueue = _methodQueue; RCT_EXPORT_MODULE() +- (NSArray *)supportedEvents +{ + return @[@"didCompleteNetworkResponse", + @"didReceiveNetworkResponse", + @"didSendNetworkData", + @"didReceiveNetworkData"]; +} + - (id)handlerForRequest:(NSURLRequest *)request { if (!request.URL) { @@ -142,7 +149,7 @@ @implementation RCTNetworking if (!_handlers) { // Get handlers, sorted in reverse priority order (highest priority first) - _handlers = [[_bridge modulesConformingToProtocol:@protocol(RCTURLRequestHandler)] sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { + _handlers = [[self.bridge modulesConformingToProtocol:@protocol(RCTURLRequestHandler)] sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { float priorityA = [a respondsToSelector:@selector(handlerPriority)] ? [a handlerPriority] : 0; float priorityB = [b respondsToSelector:@selector(handlerPriority)] ? [b handlerPriority] : 0; if (priorityA > priorityB) { @@ -346,11 +353,7 @@ - (void)sendData:(NSData *)data forTask:(RCTNetworkTask *)task } NSArray *responseJSON = @[task.requestID, responseText ?: @""]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkData" - body:responseJSON]; -#pragma clang diagnostic pop + [self sendEventWithName:@"didReceiveNetworkData" body:responseJSON]; } - (void)sendRequest:(NSURLRequest *)request @@ -364,10 +367,7 @@ - (void)sendRequest:(NSURLRequest *)request RCTURLRequestProgressBlock uploadProgressBlock = ^(int64_t progress, int64_t total) { dispatch_async(_methodQueue, ^{ NSArray *responseJSON = @[task.requestID, @((double)progress), @((double)total)]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"didSendNetworkData" body:responseJSON]; -#pragma clang diagnostic pop + [self sendEventWithName:@"didSendNetworkData" body:responseJSON]; }); }; @@ -385,11 +385,7 @@ - (void)sendRequest:(NSURLRequest *)request } id responseURL = response.URL ? response.URL.absoluteString : [NSNull null]; NSArray *responseJSON = @[task.requestID, @(status), headers, responseURL]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse" - body:responseJSON]; -#pragma clang diagnostic pop + [self sendEventWithName:@"didReceiveNetworkResponse" body:responseJSON]; }); }; @@ -410,12 +406,7 @@ - (void)sendRequest:(NSURLRequest *)request error.code == kCFURLErrorTimedOut ? @YES : @NO ]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse" - body:responseJSON]; -#pragma clang diagnostic pop - + [self sendEventWithName:@"didCompleteNetworkResponse" body:responseJSON]; [_tasksByRequestID removeObjectForKey:task.requestID]; }); }; @@ -469,7 +460,7 @@ - (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request }]; } -RCT_EXPORT_METHOD(cancelRequest:(nonnull NSNumber *)requestID) +RCT_EXPORT_METHOD(abortRequest:(nonnull NSNumber *)requestID) { [_tasksByRequestID[requestID] cancel]; [_tasksByRequestID removeObjectForKey:requestID]; diff --git a/Libraries/Network/XMLHttpRequest.android.js b/Libraries/Network/XMLHttpRequest.android.js deleted file mode 100644 index 099df7526f0511..00000000000000 --- a/Libraries/Network/XMLHttpRequest.android.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule XMLHttpRequest - * @flow - */ -'use strict'; - -var FormData = require('FormData'); -var RCTNetworking = require('RCTNetworking'); -var XMLHttpRequestBase = require('XMLHttpRequestBase'); - -type Header = [string, string]; - -function convertHeadersMapToArray(headers: Object): Array
    { - var headerArray = []; - for (var name in headers) { - headerArray.push([name, headers[name]]); - } - return headerArray; -} - -class XMLHttpRequest extends XMLHttpRequestBase { - sendImpl( - method: ?string, - url: ?string, - headers: Object, - data: any, - useIncrementalUpdates: boolean, - timeout: number, - ): void { - var body; - if (typeof data === 'string') { - body = {string: data}; - } else if (data instanceof FormData) { - body = { - formData: data.getParts().map((part) => { - part.headers = convertHeadersMapToArray(part.headers); - return part; - }), - }; - } else { - body = data; - } - var requestId = RCTNetworking.sendRequest( - method, - url, - convertHeadersMapToArray(headers), - body, - useIncrementalUpdates, - timeout - ); - this.didCreateRequest(requestId); - } -} - -module.exports = XMLHttpRequest; diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js deleted file mode 100644 index dd4ee0dd63d3c6..00000000000000 --- a/Libraries/Network/XMLHttpRequest.ios.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule XMLHttpRequest - * @flow - */ -'use strict'; - -var FormData = require('FormData'); -var RCTNetworking = require('RCTNetworking'); - -var XMLHttpRequestBase = require('XMLHttpRequestBase'); - -class XMLHttpRequest extends XMLHttpRequestBase { - sendImpl( - method: ?string, - url: ?string, - headers: Object, - data: any, - incrementalUpdates: boolean, - timeout: number, - ): void { - if (typeof data === 'string') { - data = {string: data}; - } else if (data instanceof FormData) { - data = {formData: data.getParts()}; - } - RCTNetworking.sendRequest( - { - method, - url, - data, - headers, - incrementalUpdates, - timeout - }, - this.didCreateRequest.bind(this) - ); - } -} - -module.exports = XMLHttpRequest; diff --git a/Libraries/Network/XMLHttpRequestBase.js b/Libraries/Network/XMLHttpRequest.js similarity index 94% rename from Libraries/Network/XMLHttpRequestBase.js rename to Libraries/Network/XMLHttpRequest.js index d5d5da5c57707e..24c9445124fd15 100644 --- a/Libraries/Network/XMLHttpRequestBase.js +++ b/Libraries/Network/XMLHttpRequest.js @@ -6,13 +6,12 @@ * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * - * @providesModule XMLHttpRequestBase + * @providesModule XMLHttpRequest * @flow */ 'use strict'; -var RCTNetworking = require('RCTNetworking'); -var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +const RCTNetworking = require('RCTNetworking'); const EventTarget = require('event-target-shim'); const invariant = require('fbjs/lib/invariant'); @@ -61,7 +60,7 @@ class XMLHttpRequestEventTarget extends EventTarget(...REQUEST_EVENTS) { /** * Shared base for platform-specific XMLHttpRequest implementations. */ -class XMLHttpRequestBase extends EventTarget(...XHR_EVENTS) { +class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { static UNSENT: number = UNSENT; static OPENED: number = OPENED; @@ -210,21 +209,22 @@ class XMLHttpRequestBase extends EventTarget(...XHR_EVENTS) { return this._cachedResponse; } - didCreateRequest(requestId: number): void { + // exposed for testing + __didCreateRequest(requestId: number): void { this._requestId = requestId; - this._subscriptions.push(RCTDeviceEventEmitter.addListener( + this._subscriptions.push(RCTNetworking.addListener( 'didSendNetworkData', (args) => this.__didUploadProgress(...args) )); - this._subscriptions.push(RCTDeviceEventEmitter.addListener( + this._subscriptions.push(RCTNetworking.addListener( 'didReceiveNetworkResponse', (args) => this._didReceiveResponse(...args) )); - this._subscriptions.push(RCTDeviceEventEmitter.addListener( + this._subscriptions.push(RCTNetworking.addListener( 'didReceiveNetworkData', (args) => this._didReceiveData(...args) )); - this._subscriptions.push(RCTDeviceEventEmitter.addListener( + this._subscriptions.push(RCTNetworking.addListener( 'didCompleteNetworkResponse', (args) => this.__didCompleteResponse(...args) )); @@ -337,10 +337,18 @@ class XMLHttpRequestBase extends EventTarget(...XHR_EVENTS) { url: ?string, headers: Object, data: any, - incrementalEvents: boolean, - timeout: number + useIncrementalUpdates: boolean, + timeout: number, ): void { - throw new Error('Subclass must define sendImpl method'); + RCTNetworking.sendRequest( + method, + url, + headers, + data, + useIncrementalUpdates, + timeout, + this.__didCreateRequest.bind(this), + ); } send(data: any): void { @@ -439,4 +447,4 @@ function toArrayBuffer(text: string, contentType: string): ArrayBuffer { } } -module.exports = XMLHttpRequestBase; +module.exports = XMLHttpRequest; diff --git a/Libraries/Network/__tests__/XMLHttpRequestBase-test.js b/Libraries/Network/__tests__/XMLHttpRequest-test.js similarity index 91% rename from Libraries/Network/__tests__/XMLHttpRequestBase-test.js rename to Libraries/Network/__tests__/XMLHttpRequest-test.js index 9d967d4b34b072..a7c1a995638626 100644 --- a/Libraries/Network/__tests__/XMLHttpRequestBase-test.js +++ b/Libraries/Network/__tests__/XMLHttpRequest-test.js @@ -12,13 +12,16 @@ jest .disableAutomock() .dontMock('event-target-shim') - .dontMock('XMLHttpRequestBase'); + .setMock('NativeModules', { + Networking: { + addListener: function(){}, + removeListeners: function(){}, + } + }); -const XMLHttpRequestBase = require('XMLHttpRequestBase'); +const XMLHttpRequest = require('XMLHttpRequest'); -class XMLHttpRequest extends XMLHttpRequestBase {} - -describe('XMLHttpRequestBase', function(){ +describe('XMLHttpRequest', function(){ var xhr; var handleTimeout; var handleError; @@ -43,7 +46,7 @@ describe('XMLHttpRequestBase', function(){ xhr.addEventListener('load', handleLoad); xhr.addEventListener('readystatechange', handleReadyStateChange); - xhr.didCreateRequest(1); + xhr.__didCreateRequest(1); }); afterEach(() => { @@ -53,7 +56,7 @@ describe('XMLHttpRequestBase', function(){ handleLoad = null; }); - it('should transition readyState correctly', function() { + it('should transition readyState correctly', function() { expect(xhr.readyState).toBe(xhr.UNSENT); xhr.open('GET', 'blabla'); diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m index 0d6453a36095f6..ac360e254b91a7 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -137,20 +137,29 @@ + (void)didReceiveLocalNotification:(UILocalNotification *)notification - (void)handleLocalNotificationReceived:(NSNotification *)notification { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"localNotificationReceived" body:notification.userInfo]; +#pragma clang diagnostic pop } - (void)handleRemoteNotificationReceived:(NSNotification *)notification { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"remoteNotificationReceived" body:notification.userInfo]; +#pragma clang diagnostic pop } - (void)handleRemoteNotificationsRegistered:(NSNotification *)notification { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"remoteNotificationsRegistered" body:notification.userInfo]; +#pragma clang diagnostic pop } /** diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index d867bd59581256..6200bd42df7ae7 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -10,11 +10,11 @@ */ 'use strict'; -var warning = require('fbjs/lib/warning'); +const warning = require('fbjs/lib/warning'); if (__DEV__) { - var warningDedupe = {}; - var addonWarn = function(prevName, newPackageName) { + const warningDedupe = {}; + const addonWarn = function(prevName, newPackageName) { warning( warningDedupe[prevName], 'React.addons.' + prevName + ' is deprecated. Please import the "' + @@ -25,7 +25,7 @@ if (__DEV__) { } // Export React, plus some native additions. -var ReactNative = { +const ReactNative = { // Components get ActivityIndicatorIOS() { return require('ActivityIndicatorIOS'); }, get ART() { return require('ReactNativeART'); }, @@ -87,9 +87,11 @@ var ReactNative = { get ImagePickerIOS() { return require('ImagePickerIOS'); }, get IntentAndroid() { return require('IntentAndroid'); }, get InteractionManager() { return require('InteractionManager'); }, + get Keyboard() { return require('Keyboard'); }, get LayoutAnimation() { return require('LayoutAnimation'); }, get Linking() { return require('Linking'); }, get LinkingIOS() { return require('LinkingIOS'); }, + get NativeEventEmitter() { return require('NativeEventEmitter'); }, get NavigationExperimental() { return require('NavigationExperimental'); }, get NetInfo() { return require('NetInfo'); }, get PanResponder() { return require('PanResponder'); }, @@ -171,7 +173,7 @@ var ReactNative = { // Preserve getters with warnings on the internal ReactNative copy without // invoking them. -var ReactNativeInternal = require('ReactNative'); +const ReactNativeInternal = require('ReactNative'); function applyForwarding(key) { if (__DEV__) { Object.defineProperty( @@ -183,7 +185,7 @@ function applyForwarding(key) { } ReactNative[key] = ReactNativeInternal[key]; } -for (var key in ReactNativeInternal) { +for (const key in ReactNativeInternal) { applyForwarding(key); } diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index d300af0c4d04f2..066736dac7fbd8 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -85,9 +85,11 @@ var ReactNative = Object.assign(Object.create(require('ReactNative')), { ImagePickerIOS: require('ImagePickerIOS'), IntentAndroid: require('IntentAndroid'), InteractionManager: require('InteractionManager'), + Keyboard: require('Keyboard'), LayoutAnimation: require('LayoutAnimation'), Linking: require('Linking'), LinkingIOS: require('LinkingIOS'), + NativeEventEmitter: require('NativeEventEmitter'), NavigationExperimental: require('NavigationExperimental'), NetInfo: require('NetInfo'), PanResponder: require('PanResponder'), diff --git a/React/Modules/RCTAccessibilityManager.m b/React/Modules/RCTAccessibilityManager.m index fd629cae33c0c3..3fcef1e5d3aa49 100644 --- a/React/Modules/RCTAccessibilityManager.m +++ b/React/Modules/RCTAccessibilityManager.m @@ -92,8 +92,11 @@ - (void)didReceiveNewVoiceOverStatus:(__unused NSNotification *)notification BOOL newIsVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning(); if (_isVoiceOverEnabled != newIsVoiceOverEnabled) { _isVoiceOverEnabled = newIsVoiceOverEnabled; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"voiceOverDidChange" body:@(_isVoiceOverEnabled)]; +#pragma clang diagnostic pop } } diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m index e853659a547cde..f574f7b06c0a38 100644 --- a/React/Modules/RCTDevMenu.m +++ b/React/Modules/RCTDevMenu.m @@ -182,7 +182,10 @@ - (instancetype)init selectedTitle:@"Hide Inspector" handler:^(__unused BOOL enabled) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [weakSelf.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; +#pragma clang diagnostic pop }]]; _webSocketExecutorName = [_defaults objectForKey:@"websocket-executor-name"] ?: @"JS Remotely"; @@ -214,8 +217,11 @@ - (instancetype)init modifierFlags:UIKeyModifierCommand action:^(__unused UIKeyCommand *command) { [weakSelf.bridge.eventDispatcher +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" sendDeviceEventWithName:@"toggleElementInspector" body:nil]; +#pragma clang diagnostic pop }]; // Reload in normal mode @@ -388,7 +394,10 @@ - (void)jsLoaded:(NSNotification *)notification // Inspector can only be shown after JS has loaded if ([_settings[@"showInspector"] boolValue]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [self.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; +#pragma clang diagnostic pop } }); } diff --git a/React/Modules/RCTEventEmitter.m b/React/Modules/RCTEventEmitter.m index 5f399e8305f9ff..3d4f70a5f234f0 100644 --- a/React/Modules/RCTEventEmitter.m +++ b/React/Modules/RCTEventEmitter.m @@ -37,7 +37,9 @@ + (void)initialize - (void)sendEventWithName:(NSString *)eventName body:(id)body { - RCTAssert(_bridge != nil, @"bridge is not set."); + RCTAssert(_bridge != nil, @"bridge is not set. This is probably because you've " + "explicitly synthesized the bridge in %@, even though it's inherited " + "from RCTEventEmitter.", [self class]); if (RCT_DEBUG && ![[self supportedEvents] containsObject:eventName]) { RCTLogError(@"`%@` is not a supported event type for %@. Supported events are: `%@`", diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index a819847d0a70d2..11112f92a9dc6e 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -253,8 +253,11 @@ - (void)interfaceOrientationWillChange:(NSNotification *)notification !UIInterfaceOrientationIsPortrait(nextOrientation)) || (UIInterfaceOrientationIsLandscape(_currentInterfaceOrientation) && !UIInterfaceOrientationIsLandscape(nextOrientation))) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"didUpdateDimensions" body:RCTExportedDimensions(YES)]; +#pragma clang diagnostic pop } _currentInterfaceOrientation = nextOrientation; From 1e4d9d4897edc868b136140b88ad62fc1d987ca4 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Wed, 25 May 2016 05:21:36 -0700 Subject: [PATCH 104/843] Expose Systrace from the 'react-native' package Summary: This allows React Native apps to instrument their own code via Systrace.measureMethod() and the like. **Test Plan:** Used require('react-native').Systrace in my own app successfully to instrument my own code. Closes https://github.com/facebook/react-native/pull/7734 Differential Revision: D3346222 Pulled By: javache fbshipit-source-id: 08ffc781a1187db89c6e9a0714d644dbc485724c --- Libraries/react-native/react-native.js | 1 + Libraries/react-native/react-native.js.flow | 1 + 2 files changed, 2 insertions(+) diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 6200bd42df7ae7..bd5e082a010893 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -100,6 +100,7 @@ const ReactNative = { get Settings() { return require('Settings'); }, get StatusBarIOS() { return require('StatusBarIOS'); }, get StyleSheet() { return require('StyleSheet'); }, + get Systrace() { return require('Systrace'); }, get TimePickerAndroid() { return require('TimePickerAndroid'); }, get UIManager() { return require('UIManager'); }, get Vibration() { return require('Vibration'); }, diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index 066736dac7fbd8..74c306f2cba2fe 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -98,6 +98,7 @@ var ReactNative = Object.assign(Object.create(require('ReactNative')), { Settings: require('Settings'), StatusBarIOS: require('StatusBarIOS'), StyleSheet: require('StyleSheet'), + Systrace: require('Systrace'), TimePickerAndroid: require('TimePickerAndroid'), UIManager: require('UIManager'), Vibration: require('Vibration'), From 57e9df4af236b09a0322bf8211fab991659b25cf Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Wed, 25 May 2016 06:07:35 -0700 Subject: [PATCH 105/843] Fixed multiline textinput onChange event Summary: Broken in https://github.com/facebook/react-native/commit/d9737571c43d39af41d539de2dd12c2ceb5cda0e Reviewed By: tadeuzagallo Differential Revision: D3346276 fbshipit-source-id: 0cd67edb38b9f80b0976616059f9c2454cb34448 --- Libraries/Text/RCTTextViewManager.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Libraries/Text/RCTTextViewManager.m b/Libraries/Text/RCTTextViewManager.m index f447c905c54aca..c3fe596f41224b 100644 --- a/Libraries/Text/RCTTextViewManager.m +++ b/Libraries/Text/RCTTextViewManager.m @@ -34,6 +34,7 @@ - (UIView *)view RCT_REMAP_VIEW_PROPERTY(keyboardType, textView.keyboardType, UIKeyboardType) RCT_REMAP_VIEW_PROPERTY(keyboardAppearance, textView.keyboardAppearance, UIKeyboardAppearance) RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber) +RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString) RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor) From b49633e0201a54d06a107d23b1e1c5ee518bf1c0 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Wed, 25 May 2016 06:50:16 -0700 Subject: [PATCH 106/843] Fixed image test flakiness by tying decoded image cache to module lifecycle Summary: The decoded image cache was previously static, meaning that cached images could persist beyond the lifetime of the module. This resulted in some flakiness in the RCTImageLoaderTests due to the loader returning cached image instanced from previous tests instead of the correct instance. Reviewed By: bestander Differential Revision: D3346329 fbshipit-source-id: 375af8894cef1c5b6303c6cdfd7eb57ebcfe3251 --- Libraries/Image/RCTImageLoader.m | 39 +++++++++++++------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index b576913ac8c37a..c6f4e2ec871f9c 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -25,30 +25,10 @@ static const NSUInteger RCTMaxCachableDecodedImageSizeInBytes = 1048576; // 1MB -static NSCache *RCTGetDecodedImageCache(void) -{ - static NSCache *cache; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - cache = [NSCache new]; - cache.totalCostLimit = 5 * 1024 * 1024; // 5MB - - // Clear cache in the event of a memory warning, or if app enters background - [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(__unused NSNotification *note) { - [cache removeAllObjects]; - }]; - [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillResignActiveNotification object:nil queue:nil usingBlock:^(__unused NSNotification *note) { - [cache removeAllObjects]; - }]; - }); - return cache; - -} - static NSString *RCTCacheKeyForImage(NSString *imageTag, CGSize size, CGFloat scale, RCTResizeMode resizeMode) { - return [NSString stringWithFormat:@"%@|%f|%f|%f|%zd", + return [NSString stringWithFormat:@"%@|%g|%g|%g|%zd", imageTag, size.width, size.height, scale, resizeMode]; } @@ -73,6 +53,7 @@ @implementation RCTImageLoader NSOperationQueue *_imageDecodeQueue; dispatch_queue_t _URLCacheQueue; NSURLCache *_URLCache; + NSCache *_decodedImageCache; NSMutableArray *_pendingTasks; NSInteger _activeTasks; NSMutableArray *_pendingDecodes; @@ -92,6 +73,18 @@ - (void)setUp _maxConcurrentDecodingBytes = _maxConcurrentDecodingBytes ?: 30 * 1024 *1024; // 30MB _URLCacheQueue = dispatch_queue_create("com.facebook.react.ImageLoaderURLCacheQueue", DISPATCH_QUEUE_SERIAL); + + _decodedImageCache = [NSCache new]; + _decodedImageCache.totalCostLimit = 5 * 1024 * 1024; // 5MB + + // Clear cache in the event of a memory warning, or if app enters background + [[NSNotificationCenter defaultCenter] addObserver:_decodedImageCache selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:_decodedImageCache selector:@selector(removeAllObjects) name:UIApplicationWillResignActiveNotification object:nil]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:_decodedImageCache]; } - (id)imageURLLoaderForURL:(NSURL *)URL @@ -508,7 +501,7 @@ - (RCTImageLoaderCancellationBlock)loadImageWithoutClipping:(NSString *)imageTag // Check decoded image cache NSString *cacheKey = RCTCacheKeyForImage(imageTag, size, scale, resizeMode); { - UIImage *image = [RCTGetDecodedImageCache() objectForKey:cacheKey]; + UIImage *image = [_decodedImageCache objectForKey:cacheKey]; if (image) { // Most loaders do not return on the main thread, so caller is probably not // expecting it, and may do expensive post-processing in the callback @@ -523,7 +516,7 @@ - (RCTImageLoaderCancellationBlock)loadImageWithoutClipping:(NSString *)imageTag if (image) { CGFloat bytes = image.size.width * image.size.height * image.scale * image.scale * 4; if (bytes <= RCTMaxCachableDecodedImageSizeInBytes) { - [RCTGetDecodedImageCache() setObject:image forKey:cacheKey cost:bytes]; + [_decodedImageCache setObject:image forKey:cacheKey cost:bytes]; } } completionBlock(error, image); From 84f82e5cdd06e1e4ed9ae0ebaf0dab57c5046f64 Mon Sep 17 00:00:00 2001 From: Francois Dumas Date: Wed, 25 May 2016 07:48:01 -0700 Subject: [PATCH 107/843] use RN version from node_modules instead of jcenter Summary: the current `build.gradle` configuration make all third-party plugins to pull react native from jcenter instead of using version from `node_modules`. For example, a newly generated project with RN 0.25 using https://github.com/ProjectSeptemberInc/gl-react-native leads to the following gradle output: ``` Download https://jcenter.bintray.com/com/facebook/stetho/stetho-okhttp/1.2.0/stetho-okhttp-1.2.0.pom Download https://jcenter.bintray.com/com/facebook/stetho/stetho/1.2.0/stetho-1.2.0.pom Download https://jcenter.bintray.com/com/facebook/stetho/stetho/1.2.0/stetho-1.2.0.jar Download https://jcenter.bintray.com/com/facebook/stetho/stetho-okhttp/1.2.0/stetho-okhttp-1.2.0.jar Download https://jcenter.bintray.com/com/facebook/react/react-native/0.20.1/react-native-0.20.1.aar ``` Closes https://github.com/facebook/react-native/pull/7470 Differential Revision: D3346526 Pulled By: mkonicek fbshipit-source-id: 0f1d051f4340f35403931727982900a9fc7abad5 --- local-cli/generator-android/templates/src/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-cli/generator-android/templates/src/build.gradle b/local-cli/generator-android/templates/src/build.gradle index 403a007564d948..fcba4c587f1ce3 100644 --- a/local-cli/generator-android/templates/src/build.gradle +++ b/local-cli/generator-android/templates/src/build.gradle @@ -18,7 +18,7 @@ allprojects { jcenter() maven { // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm - url "$projectDir/../../node_modules/react-native/android" + url "$rootDir/../node_modules/react-native/android" } } } From 298dc7c15214e85564e5b76105d335fc4b4a6acd Mon Sep 17 00:00:00 2001 From: yuyaohshimo Date: Wed, 25 May 2016 08:23:56 -0700 Subject: [PATCH 108/843] Add "Coiney Madoguchi" to showcase Summary: "Coiney Madoguchi" is a registration app for Coiney (http://coiney.com/). Closes https://github.com/facebook/react-native/pull/7299 Differential Revision: D3245067 Pulled By: mkonicek fbshipit-source-id: 207a975593625d910b0c06e794fa95ed7b8d2789 --- website/src/react-native/showcase.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/src/react-native/showcase.js b/website/src/react-native/showcase.js index 16bf8477306dc2..f3d3e66b47d5e6 100644 --- a/website/src/react-native/showcase.js +++ b/website/src/react-native/showcase.js @@ -339,6 +339,12 @@ var apps = [ link: 'https://www.codementor.io/downloads', author: 'Codementor', }, + { + name: 'Coiney窓口', + icon: 'http://a4.mzstatic.com/us/r30/Purple69/v4/c9/bc/3a/c9bc3a29-9c11-868f-b960-ca46d5fcd509/icon175x175.jpeg', + link: 'https://itunes.apple.com/jp/app/coiney-chuang-kou/id1069271336?mt=8', + author: 'Coiney, Inc.' + }, { name: 'Collegiate - Learn Anywhere', icon: 'http://a3.mzstatic.com/us/r30/Purple69/v4/17/a9/60/17a960d3-8cbd-913a-9f08-ebd9139c116c/icon175x175.png', From 1586a32fa739b0ac918b5877ea3df4c6d0022729 Mon Sep 17 00:00:00 2001 From: Anoop Chaurasiya Date: Wed, 25 May 2016 10:17:36 -0700 Subject: [PATCH 109/843] add support to provide fallback-sourceURL: in case primary-sourceURL fails to load Reviewed By: javache Differential Revision: D3339692 fbshipit-source-id: 93fa1821bf4abca878832d4f75c6b9968d8d0460 --- React/Base/RCTBatchedBridge.m | 13 ++++++++++++- React/Base/RCTBridgeDelegate.h | 9 +++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index 4b210e072e5a3a..ac3cd7e80c8315 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -170,7 +170,18 @@ - (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:withBlock:)]) { [self.delegate loadSourceForBridge:_parentBridge withBlock:onSourceLoad]; } else if (self.bundleURL) { - [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onComplete:onSourceLoad]; + [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onComplete:^(NSError *error, NSData *source) { + if (error && [self.delegate respondsToSelector:@selector(fallbackSourceURLForBridge:)]) { + NSURL *fallbackURL = [self.delegate fallbackSourceURLForBridge:_parentBridge]; + if (fallbackURL && ![fallbackURL isEqual:self.bundleURL]) { + RCTLogError(@"Failed to load bundle(%@) with error:(%@)", self.bundleURL, error.localizedDescription); + self.bundleURL = fallbackURL; + [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onComplete:onSourceLoad]; + return; + } + } + onSourceLoad(error, source); + }]; } else { // Allow testing without a script dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/React/Base/RCTBridgeDelegate.h b/React/Base/RCTBridgeDelegate.h index d8606c8d953ace..8d85f78b7a862b 100644 --- a/React/Base/RCTBridgeDelegate.h +++ b/React/Base/RCTBridgeDelegate.h @@ -23,6 +23,15 @@ typedef void (^RCTSourceLoadBlock)(NSError *error, NSData *source); @optional +/** + * The bridge will attempt to load the JS source code from the location specified + * by the `sourceURLForBridge:` method, if loading fails, you can implement this + * method to specify fallbackSourceURL. + * NOTE: We don't plan to support this API permanently (this method will be + * removed after we track down why a valid sourceURL fails to load sometimes). + */ +- (NSURL *)fallbackSourceURLForBridge:(RCTBridge *)bridge; + /** * The bridge initializes any registered RCTBridgeModules automatically, however * if you wish to instantiate your own module instances, you can return them From 4b0f0881eb3b4908eb600f09d61f8303faa38bbb Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Wed, 25 May 2016 10:28:40 -0700 Subject: [PATCH 110/843] Make RCTTiming module lazy Reviewed By: nicklockwood Differential Revision: D3346796 fbshipit-source-id: e7fa02f47bfca44272857864472c3f8ef59f56e5 --- React/Modules/RCTTiming.m | 45 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/React/Modules/RCTTiming.m b/React/Modules/RCTTiming.m index 8e2f865bd726d6..dd10ca9ca80ee8 100644 --- a/React/Modules/RCTTiming.m +++ b/React/Modules/RCTTiming.m @@ -66,32 +66,31 @@ @implementation RCTTiming RCT_EXPORT_MODULE() -- (instancetype)init +- (void)setBridge:(RCTBridge *)bridge { - if ((self = [super init])) { - _paused = YES; - _timers = [NSMutableDictionary new]; - - for (NSString *name in @[UIApplicationWillResignActiveNotification, - UIApplicationDidEnterBackgroundNotification, - UIApplicationWillTerminateNotification]) { - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(stopTimers) - name:name - object:nil]; - } - - for (NSString *name in @[UIApplicationDidBecomeActiveNotification, - UIApplicationWillEnterForegroundNotification]) { + RCTAssert(!_bridge, @"Should never be initialized twice!"); + + _paused = YES; + _timers = [NSMutableDictionary new]; + + for (NSString *name in @[UIApplicationWillResignActiveNotification, + UIApplicationDidEnterBackgroundNotification, + UIApplicationWillTerminateNotification]) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(stopTimers) + name:name + object:nil]; + } - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(startTimers) - name:name - object:nil]; - } + for (NSString *name in @[UIApplicationDidBecomeActiveNotification, + UIApplicationWillEnterForegroundNotification]) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(startTimers) + name:name + object:nil]; } - return self; + + _bridge = bridge; } - (void)dealloc From 80741a170aafd967b3ec1cf6db6a955fb3de30ee Mon Sep 17 00:00:00 2001 From: Olivier Notteghem Date: Wed, 25 May 2016 11:00:29 -0700 Subject: [PATCH 111/843] Prevent race condition leading to deadlock when destroying activity w/ catalyst instance. Reviewed By: astreet Differential Revision: D3345567 fbshipit-source-id: 8ac550456c99549a6c4bc2d2cdb19334f9e85c71 --- .../react/bridge/CatalystInstanceImpl.java | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java index f13ef865877aa2..8def48cb504973 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java @@ -46,6 +46,7 @@ public class CatalystInstanceImpl implements CatalystInstance { private final AtomicInteger mPendingJSCalls = new AtomicInteger(0); private final String mJsPendingCallsTitleForTrace = "pending_js_calls_instance" + sNextInstanceIdForTrace.getAndIncrement(); + private volatile boolean mIsBeingDestroyed = false; private volatile boolean mDestroyed = false; private final TraceListener mTraceListener; private final JavaScriptModuleRegistry mJSModuleRegistry; @@ -165,6 +166,10 @@ public void callFunction( String method, NativeArray arguments, String tracingName) { + if (mIsBeingDestroyed) { + FLog.w(ReactConstants.TAG, "Calling JS function after bridge has been destroyed."); + return; + } synchronized (mJavaToJSCallsTeardownLock) { if (mDestroyed) { FLog.w(ReactConstants.TAG, "Calling JS function after bridge has been destroyed."); @@ -184,6 +189,10 @@ public void callFunction( @DoNotStrip @Override public void invokeCallback(ExecutorToken executorToken, int callbackID, NativeArray arguments) { + if (mIsBeingDestroyed) { + FLog.w(ReactConstants.TAG, "Invoking JS callback after bridge has been destroyed."); + return; + } synchronized (mJavaToJSCallsTeardownLock) { if (mDestroyed) { FLog.w(ReactConstants.TAG, "Invoking JS callback after bridge has been destroyed."); @@ -207,6 +216,7 @@ public void destroy() { // This ordering is important. A JS to Java call that triggers a Java to JS call will also // acquire these locks in the same order + mIsBeingDestroyed = true; synchronized (mJSToJavaCallsTeardownLock) { synchronized (mJavaToJSCallsTeardownLock) { if (mDestroyed) { @@ -411,12 +421,14 @@ private class NativeModulesReactCallback implements ReactCallback { public void call(ExecutorToken executorToken, int moduleId, int methodId, ReadableNativeArray parameters) { mReactQueueConfiguration.getNativeModulesQueueThread().assertIsOnThread(); + if (mIsBeingDestroyed) { + return; + } synchronized (mJSToJavaCallsTeardownLock) { // Suppress any callbacks if destroyed - will only lead to sadness. if (mDestroyed) { return; } - mJavaRegistry.call(CatalystInstanceImpl.this, executorToken, moduleId, methodId, parameters); } } @@ -429,14 +441,18 @@ public void onBatchComplete() { // native modules could be in a bad state so we don't want to call anything on them. We // still want to trigger the debug listener since it allows instrumentation tests to end and // check their assertions without waiting for a timeout. + if (mIsBeingDestroyed) { + return; + } synchronized (mJSToJavaCallsTeardownLock) { - if (!mDestroyed) { - Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "onBatchComplete"); - try { - mJavaRegistry.onBatchComplete(); - } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); - } + if (mDestroyed) { + return; + } + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "onBatchComplete"); + try { + mJavaRegistry.onBatchComplete(); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); } } @@ -450,14 +466,18 @@ public void onExecutorUnregistered(ExecutorToken executorToken) { // Since onCatalystInstanceDestroy happens on the UI thread, we don't want to also execute // this callback on the native modules thread at the same time. Longer term, onCatalystInstanceDestroy // should probably be executed on the native modules thread as well instead. + if (mIsBeingDestroyed) { + return; + } synchronized (mJSToJavaCallsTeardownLock) { - if (!mDestroyed) { - Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "onExecutorUnregistered"); - try { - mJavaRegistry.onExecutorUnregistered(executorToken); - } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); - } + if (mDestroyed) { + return; + } + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "onExecutorUnregistered"); + try { + mJavaRegistry.onExecutorUnregistered(executorToken); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); } } } From 8097fcf4e7a10acc1563698182f5edcbc6596fb1 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Wed, 25 May 2016 11:26:22 -0700 Subject: [PATCH 112/843] Fix NavigationTransitioner. Summary: - Address the issues reported at https://github.com/facebook/react-native/commit/7db7f78dc7d2b85843707f75565bcfcb538e8e51#commitcomment-17575647 - Fix the logic that reduces the scenes. - Fix the logic that computes the active scene for `renderOverlay`. Reviewed By: ericvicenti Differential Revision: D3344716 fbshipit-source-id: 3fce517ff1de212f412a77936012695bd2dcfc3c --- .../NavigationTransitioner.js | 14 +++++++- .../Reducer/NavigationScenesReducer.js | 16 ++++++++-- .../__tests__/NavigationScenesReducer-test.js | 32 +++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/Libraries/NavigationExperimental/NavigationTransitioner.js b/Libraries/NavigationExperimental/NavigationTransitioner.js index f319fe55b5c546..49a999978049a6 100644 --- a/Libraries/NavigationExperimental/NavigationTransitioner.js +++ b/Libraries/NavigationExperimental/NavigationTransitioner.js @@ -19,6 +19,8 @@ const React = require('React'); const StyleSheet = require('StyleSheet'); const View = require('View'); +const invariant = require('fbjs/lib/invariant'); + import type { NavigationActionCaller, NavigationAnimatedValue, @@ -219,13 +221,23 @@ class NavigationTransitioner extends React.Component { scenes, } = this.state; + const route = navigationState.routes[navigationState.index]; + + const activeScene = scenes.find(scene => { + return (!scene.isStale && scene.route === route) ? + scene : + undefined; + }); + + invariant(!!activeScene, 'no active scene found'); + return renderOverlay({ layout: this.state.layout, navigationState, onNavigate, position, progress, - scene: scenes[navigationState.index], + scene: activeScene, scenes, }); } diff --git a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js index 98c5e76a3840bf..a858a11e9791be 100644 --- a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js +++ b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js @@ -63,7 +63,6 @@ function areScenesShallowEqual( one.key === two.key && one.index === two.index && one.isStale === two.isStale && - one.route === two.route && one.route.key === two.route.key ); } @@ -147,7 +146,20 @@ function NavigationScenesReducer( staleScenes.forEach(mergeScene); freshScenes.forEach(mergeScene); - return nextScenes.sort(compareScenes); + nextScenes.sort(compareScenes); + + if (nextScenes.length !== scenes.length) { + return nextScenes; + } + + if (nextScenes.some( + (scene, index) => !areScenesShallowEqual(scenes[index], scene) + )) { + return nextScenes; + } + + // scenes haven't changed. + return scenes; } module.exports = NavigationScenesReducer; diff --git a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js index 1788d0dd563f2c..66373efe68d182 100644 --- a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js +++ b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js @@ -97,6 +97,38 @@ describe('NavigationScenesReducer', () => { ]); }); + it('gets same scenes', () => { + const state1 = { + index: 0, + routes: [{key: '1'}, {key: '2'}], + }; + + const state2 = { + index: 0, + routes: [{key: '1'}, {key: '2'}], + }; + + const scenes1 = NavigationScenesReducer([], state1, null); + const scenes2 = NavigationScenesReducer(scenes1, state2, state1); + expect(scenes1).toBe(scenes2); + }); + + it('gets different scenes', () => { + const state1 = { + index: 0, + routes: [{key: '1'}, {key: '2'}], + }; + + const state2 = { + index: 0, + routes: [{key: '2'}, {key: '1'}], + }; + + const scenes1 = NavigationScenesReducer([], state1, null); + const scenes2 = NavigationScenesReducer(scenes1, state2, state1); + expect(scenes1).not.toBe(scenes2); + }); + it('pops scenes', () => { // Transition from ['1', '2', '3'] to ['1', '2']. const scenes = testTransition([ From ed47efe4a17a6fa3f0a2a8a36600efdcd1c65b86 Mon Sep 17 00:00:00 2001 From: Yann Pringault Date: Wed, 25 May 2016 11:50:53 -0700 Subject: [PATCH 113/843] Add Array.prototype.includes polyfill Summary: Add `Array.prototype.includes` polyfill. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes I had all my `includes` running well on iOS 9 but when switching to Android `includes` threw an error. [The compatibility table](http://kangax.github.io/compat-table/esnext/#test-Array.prototype.includes_Array.prototype.includes) shows that is not supported on Android yet as well as iOS 6-8. With Chrome debugging it's working on both environment. Closes https://github.com/facebook/react-native/pull/7756 Reviewed By: davidaurelio Differential Revision: D3346873 Pulled By: vjeux fbshipit-source-id: 2e17d29992873fbe4448b962df0423e516455b4b --- docs/JavaScriptEnvironment.md | 2 +- .../Resolver/polyfills/Array.prototype.es6.js | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/JavaScriptEnvironment.md b/docs/JavaScriptEnvironment.md index 988604fc2c183f..4e2e3c1f21d579 100644 --- a/docs/JavaScriptEnvironment.md +++ b/docs/JavaScriptEnvironment.md @@ -73,7 +73,7 @@ ES6 * [Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) * String.prototype.{[startsWith](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith), [endsWith](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith), [repeat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeats), [includes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes)} * [Array.from](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) -* Array.prototype.{[find](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find), [findIndex](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex)} +* Array.prototype.{[find](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find), [findIndex](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex), [includes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes)} ES7 diff --git a/packager/react-packager/src/Resolver/polyfills/Array.prototype.es6.js b/packager/react-packager/src/Resolver/polyfills/Array.prototype.es6.js index 2752aab5e5d0dc..7e2078f8e9221b 100644 --- a/packager/react-packager/src/Resolver/polyfills/Array.prototype.es6.js +++ b/packager/react-packager/src/Resolver/polyfills/Array.prototype.es6.js @@ -53,3 +53,39 @@ if (!Array.prototype.find) { } }); } + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes +if (!Array.prototype.includes) { + Object.defineProperty(Array.prototype, 'includes', { + enumerable: false, + writable: true, + configurable: true, + value: function (searchElement) { + var O = Object(this); + var len = parseInt(O.length) || 0; + if (len === 0) { + return false; + } + var n = parseInt(arguments[1]) || 0; + var k; + if (n >= 0) { + k = n; + } else { + k = len + n; + if (k < 0) { + k = 0; + } + } + var currentElement; + while (k < len) { + currentElement = O[k]; + if (searchElement === currentElement || + (searchElement !== searchElement && currentElement !== currentElement)) { + return true; + } + k++; + } + return false; + } + }); +} From 7e62c1165b227d3ad70418f9465bbd6006d4776f Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Wed, 25 May 2016 13:33:26 -0700 Subject: [PATCH 114/843] Fix Gradle setup in 'Integrating with existing apps' Summary: Based on https://github.com/facebook/react-native/pull/7470, fixing the case when an existing Android app uses React Native as well as 3rd-party React Native modules. With this PR Gradle should always pick up React Native binaries from node_modules rather than fetching old binaries from JCenter. Closes https://github.com/facebook/react-native/pull/7759 Differential Revision: D3348640 fbshipit-source-id: 7509eb54bba6e59cf7f4a116bf444fc4983d2d33 --- docs/EmbeddedAppAndroid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/EmbeddedAppAndroid.md b/docs/EmbeddedAppAndroid.md index 1c336b78c22eb9..632fc13044b656 100644 --- a/docs/EmbeddedAppAndroid.md +++ b/docs/EmbeddedAppAndroid.md @@ -28,7 +28,7 @@ allprojects { ... maven { // All of React Native (JS, Android binaries) is installed from npm - url "$projectDir/node_modules/react-native/android" + url "$rootDir/node_modules/react-native/android" } } ... From af149b3a117ad3caf0b6f56a48b9782580e505c4 Mon Sep 17 00:00:00 2001 From: Jeremy Wyld Date: Wed, 25 May 2016 17:28:39 -0700 Subject: [PATCH 115/843] Correctly reference cwd instead of __dirname Summary: This is for issue #7670. I consider this a typo, but maybe you don't. In order to see the problem, you need to have the packager search for the configuration in a place that doesn't have one and the default configuration can't be provided. It's likely that no one is doing this and also why this probably wasn't seen. Closes https://github.com/facebook/react-native/pull/7671 Differential Revision: D3350412 fbshipit-source-id: 5f9b520f7d5cbc749e2b898e7bbf2cd84d81ace0 --- local-cli/util/Config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-cli/util/Config.js b/local-cli/util/Config.js index 95ce3d4b11ff6a..bc555733d15615 100644 --- a/local-cli/util/Config.js +++ b/local-cli/util/Config.js @@ -33,7 +33,7 @@ const Config = { const parentDir = findParentDirectory(cwd, RN_CLI_CONFIG); if (!parentDir && !defaultConfig) { throw new Error( - `Can't find "rn-cli.config.js" file in any parent folder of "${__dirname}"` + `Can't find "rn-cli.config.js" file in any parent folder of "${cwd}"` ); } From 5c6bbf7f1fe23800e4f8b3e1a8911c5b5a280d2c Mon Sep 17 00:00:00 2001 From: Zhao Han Date: Wed, 25 May 2016 17:39:16 -0700 Subject: [PATCH 116/843] Add WeatherEh to showcase Summary: Skipping eveything in the contribution guide because adding an app to the Showcase Closes https://github.com/facebook/react-native/pull/7627 Differential Revision: D3348782 fbshipit-source-id: 0994fd64be5ec132c70090b0852b00b6da0177ee --- website/src/react-native/showcase.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/src/react-native/showcase.js b/website/src/react-native/showcase.js index f3d3e66b47d5e6..a61ecf4021aa40 100644 --- a/website/src/react-native/showcase.js +++ b/website/src/react-native/showcase.js @@ -923,6 +923,12 @@ var apps = [ linkPlayStore: 'https://play.google.com/store/apps/details?id=com.wearvr.app', author: 'WEARVR LLC', }, + { + name: 'WeatherEh - Canada weather', + icon: 'http://a2.mzstatic.com/us/r30/Purple18/v4/39/cf/84/39cf8411-acc3-f7d6-3923-39973c2eb511/icon175x175.jpeg', + link: 'https://itunes.apple.com/app/id1112813447', + author: 'Zhao Han', + }, { name: 'wego concerts', icon: 'http://a5.mzstatic.com/us/r30/Purple69/v4/03/91/2d/03912daa-fae7-6a25-5f11-e6b19290b3f4/icon175x175.png', From 3b3b46d86e1b6d8d4debbab3aafaf1fd454749c0 Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Wed, 25 May 2016 17:43:52 -0700 Subject: [PATCH 117/843] Move new bridge java stuff to OSS Reviewed By: mhorowitz Differential Revision: D3300152 fbshipit-source-id: 9a76b10579bbfc5bde3a5094b99b64c38f4c1da9 --- .../src/main/java/com/facebook/react/BUCK | 49 +- .../facebook/react/XReactInstanceManager.java | 68 ++ .../react/XReactInstanceManagerImpl.java | 865 ++++++++++++++++++ .../facebook/react/cxxbridge/Arguments.java | 159 ++++ .../java/com/facebook/react/cxxbridge/BUCK | 38 + .../react/cxxbridge/CallbackImpl.java | 30 + .../react/cxxbridge/CatalystInstanceImpl.java | 461 ++++++++++ .../react/cxxbridge/CxxModuleWrapper.java | 110 +++ .../react/cxxbridge/ExecutorToken.java | 0 .../react/cxxbridge/JSBundleLoader.java | 90 ++ .../cxxbridge/JSCJavaScriptExecutor.java | 46 + .../react/cxxbridge/JavaModuleWrapper.java | 141 +++ .../react/cxxbridge/JavaScriptExecutor.java | 35 + .../react/cxxbridge/ModuleRegistryHolder.java | 28 + .../react/cxxbridge/NativeModuleRegistry.java | 138 +++ .../cxxbridge/ProxyJavaScriptExecutor.java | 67 ++ .../react/cxxbridge/ReactCallback.java | 27 + .../facebook/react/cxxbridge/ReactMarker.java | 31 + .../react/cxxbridge/SoftAssertions.java | 52 ++ .../react/cxxbridge/UiThreadUtil.java | 56 ++ .../com/facebook/react/cxxbridge/bridge.pro | 7 + 21 files changed, 2483 insertions(+), 15 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/Arguments.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CallbackImpl.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CxxModuleWrapper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ExecutorToken.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSBundleLoader.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSCJavaScriptExecutor.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaScriptExecutor.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ModuleRegistryHolder.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ProxyJavaScriptExecutor.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactCallback.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactMarker.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/SoftAssertions.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/UiThreadUtil.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/cxxbridge/bridge.pro diff --git a/ReactAndroid/src/main/java/com/facebook/react/BUCK b/ReactAndroid/src/main/java/com/facebook/react/BUCK index 0cc6b1e0d734da..6fcd3551cfe4a5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/BUCK @@ -1,22 +1,41 @@ include_defs('//ReactAndroid/DEFS') +XREACT_SRCS = [ + 'XReactInstanceManager.java', + 'XReactInstanceManagerImpl.java', +] + +DEPS = [ + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/devsupport:devsupport'), + react_native_target('java/com/facebook/react/modules/core:core'), + react_native_target('java/com/facebook/react/modules/debug:debug'), + react_native_target('java/com/facebook/react/modules/systeminfo:systeminfo'), + react_native_target('java/com/facebook/react/modules/toast:toast'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_dep('java/com/facebook/systrace:systrace'), + react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'), + react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jsr-305:jsr-305'), +] + android_library( name = 'react', - srcs = glob(['*.java']), - deps = [ - react_native_target('java/com/facebook/react/bridge:bridge'), - react_native_target('java/com/facebook/react/common:common'), - react_native_target('java/com/facebook/react/devsupport:devsupport'), - react_native_target('java/com/facebook/react/modules/core:core'), - react_native_target('java/com/facebook/react/modules/debug:debug'), - react_native_target('java/com/facebook/react/modules/systeminfo:systeminfo'), - react_native_target('java/com/facebook/react/modules/toast:toast'), - react_native_target('java/com/facebook/react/uimanager:uimanager'), - react_native_dep('java/com/facebook/systrace:systrace'), - react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'), - react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'), - react_native_dep('third-party/java/infer-annotations:infer-annotations'), - react_native_dep('third-party/java/jsr-305:jsr-305'), + srcs = glob(['*.java'], excludes=XREACT_SRCS), + deps = DEPS, + visibility = [ + 'PUBLIC', + ], +) + +android_library( + name = 'xreact', + srcs = XREACT_SRCS, + deps = DEPS + [ + ':react', + react_native_target('java/com/facebook/react/cxxbridge:bridge'), ], visibility = [ 'PUBLIC', diff --git a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java new file mode 100644 index 00000000000000..9ac350808b87c5 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react; + +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.uimanager.UIImplementationProvider; + +public abstract class XReactInstanceManager { + /** + * Creates a builder that is capable of creating an instance of {@link XReactInstanceManagerImpl}. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for {@link XReactInstanceManagerImpl} + */ + public static class Builder extends ReactInstanceManager.Builder { + /** + * Instantiates a new {@link ReactInstanceManagerImpl}. + * Before calling {@code build}, the following must be called: + *
      + *
    • {@link #setApplication} + *
    • {@link #setCurrentActivity} if the activity has already resumed + *
    • {@link #setDefaultHardwareBackBtnHandler} if the activity has already resumed + *
    • {@link #setJSBundleFile} or {@link #setJSMainModuleName} + *
    + */ + public ReactInstanceManager build() { + Assertions.assertCondition( + mUseDeveloperSupport || mJSBundleFile != null, + "JS Bundle File has to be provided when dev support is disabled"); + + Assertions.assertCondition( + mJSMainModuleName != null || mJSBundleFile != null, + "Either MainModuleName or JS Bundle File needs to be provided"); + + if (mUIImplementationProvider == null) { + // create default UIImplementationProvider if the provided one is null. + mUIImplementationProvider = new UIImplementationProvider(); + } + + return new XReactInstanceManagerImpl( + Assertions.assertNotNull( + mApplication, + "Application property has not been set with this builder"), + mCurrentActivity, + mDefaultHardwareBackBtnHandler, + mJSBundleFile, + mJSMainModuleName, + mPackages, + mUseDeveloperSupport, + mBridgeIdleDebugListener, + Assertions.assertNotNull(mInitialLifecycleState, "Initial lifecycle state was not set"), + mUIImplementationProvider, + mNativeModuleCallExceptionHandler, + mJSCConfig); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java new file mode 100644 index 00000000000000..0cc29def3a9ca2 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java @@ -0,0 +1,865 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react; + +import javax.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.View; + +import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.JavaJSExecutor; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.JavaScriptModuleRegistry; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.NativeModuleCallExceptionHandler; +import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactMarker; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec; +import com.facebook.react.common.ApplicationHolder; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.cxxbridge.Arguments; +import com.facebook.react.cxxbridge.CatalystInstanceImpl; +import com.facebook.react.cxxbridge.JSBundleLoader; +import com.facebook.react.cxxbridge.JSCJavaScriptExecutor; +import com.facebook.react.cxxbridge.JavaScriptExecutor; +import com.facebook.react.cxxbridge.NativeModuleRegistry; +import com.facebook.react.cxxbridge.ProxyJavaScriptExecutor; +import com.facebook.react.cxxbridge.UiThreadUtil; +import com.facebook.react.devsupport.DevServerHelper; +import com.facebook.react.devsupport.DevSupportManager; +import com.facebook.react.devsupport.DevSupportManagerFactory; +import com.facebook.react.devsupport.ReactInstanceDevCommandsHandler; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.uimanager.AppRegistry; +import com.facebook.react.uimanager.DisplayMetricsHolder; +import com.facebook.react.uimanager.UIImplementationProvider; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.soloader.SoLoader; +import com.facebook.systrace.Systrace; + +import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_JS_MODULE_CONFIG_END; +import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_JS_MODULE_CONFIG_START; +import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_NATIVE_MODULE_REGISTRY_END; +import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_NATIVE_MODULE_REGISTRY_START; +import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_CATALYST_INSTANCE_END; +import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_CATALYST_INSTANCE_START; +import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_REACT_CONTEXT_START; +import static com.facebook.react.bridge.ReactMarkerConstants.PROCESS_PACKAGES_END; +import static com.facebook.react.bridge.ReactMarkerConstants.PROCESS_PACKAGES_START; +import static com.facebook.react.bridge.ReactMarkerConstants.RUN_JS_BUNDLE_END; +import static com.facebook.react.bridge.ReactMarkerConstants.RUN_JS_BUNDLE_START; + +/** + * This class is managing instances of {@link CatalystInstance}. It expose a way to configure + * catalyst instance using {@link ReactPackage} and keeps track of the lifecycle of that + * instance. It also sets up connection between the instance and developers support functionality + * of the framework. + * + * An instance of this manager is required to start JS application in {@link ReactRootView} (see + * {@link ReactRootView#startReactApplication} for more info). + * + * The lifecycle of the instance of {@link XReactInstanceManagerImpl} should be bound to the + * activity that owns the {@link ReactRootView} that is used to render react application using this + * instance manager (see {@link ReactRootView#startReactApplication}). It's required to pass owning + * activity's lifecycle events to the instance manager (see {@link #onHostPause}, {@link + * #onHostDestroy} and {@link #onHostResume}). + * + * To instantiate an instance of this class use {@link #builder}. + */ +/* package */ class XReactInstanceManagerImpl extends ReactInstanceManager { + + /* should only be accessed from main thread (UI thread) */ + private final List mAttachedRootViews = new ArrayList<>(); + private LifecycleState mLifecycleState; + private @Nullable ReactContextInitParams mPendingReactContextInitParams; + private @Nullable ReactContextInitAsyncTask mReactContextInitAsyncTask; + + /* accessed from any thread */ + private @Nullable String mJSBundleFile; /* path to JS bundle on file system */ + private final @Nullable String mJSMainModuleName; /* path to JS bundle root on packager server */ + private final List mPackages; + private final DevSupportManager mDevSupportManager; + private final boolean mUseDeveloperSupport; + private final @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener; + private @Nullable volatile ReactContext mCurrentReactContext; + private final Context mApplicationContext; + private @Nullable DefaultHardwareBackBtnHandler mDefaultBackButtonImpl; + private String mSourceUrl; + private @Nullable Activity mCurrentActivity; + private final Collection mReactInstanceEventListeners = + Collections.synchronizedSet(new HashSet()); + private volatile boolean mHasStartedCreatingInitialContext = false; + private final UIImplementationProvider mUIImplementationProvider; + private final MemoryPressureRouter mMemoryPressureRouter; + private final @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; + private final @Nullable JSCConfig mJSCConfig; + + private final ReactInstanceDevCommandsHandler mDevInterface = + new ReactInstanceDevCommandsHandler() { + + @Override + public void onReloadWithJSDebugger(JavaJSExecutor.Factory jsExecutorFactory) { + XReactInstanceManagerImpl.this.onReloadWithJSDebugger(jsExecutorFactory); + } + + @Override + public void onJSBundleLoadedFromServer() { + XReactInstanceManagerImpl.this.onJSBundleLoadedFromServer(); + } + + @Override + public void toggleElementInspector() { + XReactInstanceManagerImpl.this.toggleElementInspector(); + } + }; + + private final DefaultHardwareBackBtnHandler mBackBtnHandler = + new DefaultHardwareBackBtnHandler() { + @Override + public void invokeDefaultOnBackPressed() { + XReactInstanceManagerImpl.this.invokeDefaultOnBackPressed(); + } + }; + + private class ReactContextInitParams { + private final JavaScriptExecutor.Factory mJsExecutorFactory; + private final JSBundleLoader mJsBundleLoader; + + public ReactContextInitParams( + JavaScriptExecutor.Factory jsExecutorFactory, + JSBundleLoader jsBundleLoader) { + mJsExecutorFactory = Assertions.assertNotNull(jsExecutorFactory); + mJsBundleLoader = Assertions.assertNotNull(jsBundleLoader); + } + + public JavaScriptExecutor.Factory getJsExecutorFactory() { + return mJsExecutorFactory; + } + + public JSBundleLoader getJsBundleLoader() { + return mJsBundleLoader; + } + } + + /* + * Task class responsible for (re)creating react context in the background. These tasks can only + * be executing one at time, see {@link #recreateReactContextInBackground()}. + */ + private final class ReactContextInitAsyncTask extends + AsyncTask> { + @Override + protected void onPreExecute() { + if (mCurrentReactContext != null) { + tearDownReactContext(mCurrentReactContext); + mCurrentReactContext = null; + } + } + + @Override + protected Result doInBackground(ReactContextInitParams... params) { + Assertions.assertCondition(params != null && params.length > 0 && params[0] != null); + try { + JavaScriptExecutor jsExecutor = params[0].getJsExecutorFactory().create(); + return Result.of(createReactContext(jsExecutor, params[0].getJsBundleLoader())); + } catch (Exception e) { + // Pass exception to onPostExecute() so it can be handled on the main thread + return Result.of(e); + } + } + + @Override + protected void onPostExecute(Result result) { + try { + setupReactContext(result.get()); + } catch (Exception e) { + mDevSupportManager.handleException(e); + } finally { + mReactContextInitAsyncTask = null; + } + + // Handle enqueued request to re-initialize react context. + if (mPendingReactContextInitParams != null) { + recreateReactContextInBackground( + mPendingReactContextInitParams.getJsExecutorFactory(), + mPendingReactContextInitParams.getJsBundleLoader()); + mPendingReactContextInitParams = null; + } + } + + @Override + protected void onCancelled(Result reactApplicationContextResult) { + try { + mMemoryPressureRouter.destroy(reactApplicationContextResult.get()); + } catch (Exception e) { + FLog.w(ReactConstants.TAG, "Caught exception after cancelling react context init", e); + } finally { + mReactContextInitAsyncTask = null; + } + } + } + + private static class Result { + @Nullable private final T mResult; + @Nullable private final Exception mException; + + public static Result of(U result) { + return new Result(result); + } + + public static Result of(Exception exception) { + return new Result<>(exception); + } + + private Result(T result) { + mException = null; + mResult = result; + } + + private Result(Exception exception) { + mException = exception; + mResult = null; + } + + public T get() throws Exception { + if (mException != null) { + throw mException; + } + + Assertions.assertNotNull(mResult); + + return mResult; + } + } + + /* package */ XReactInstanceManagerImpl( + Context applicationContext, + @Nullable Activity currentActivity, + @Nullable DefaultHardwareBackBtnHandler defaultHardwareBackBtnHandler, + @Nullable String jsBundleFile, + @Nullable String jsMainModuleName, + List packages, + boolean useDeveloperSupport, + @Nullable NotThreadSafeBridgeIdleDebugListener bridgeIdleDebugListener, + LifecycleState initialLifecycleState, + UIImplementationProvider uiImplementationProvider, + NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler, + @Nullable JSCConfig jscConfig) { + initializeSoLoaderIfNecessary(applicationContext); + + // TODO(9577825): remove this + ApplicationHolder.setApplication((Application) applicationContext.getApplicationContext()); + DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(applicationContext); + + mApplicationContext = applicationContext; + mCurrentActivity = currentActivity; + mDefaultBackButtonImpl = defaultHardwareBackBtnHandler; + mJSBundleFile = jsBundleFile; + mJSMainModuleName = jsMainModuleName; + mPackages = packages; + mUseDeveloperSupport = useDeveloperSupport; + mDevSupportManager = DevSupportManagerFactory.create( + applicationContext, + mDevInterface, + mJSMainModuleName, + useDeveloperSupport); + mBridgeIdleDebugListener = bridgeIdleDebugListener; + mLifecycleState = initialLifecycleState; + mUIImplementationProvider = uiImplementationProvider; + mMemoryPressureRouter = new MemoryPressureRouter(applicationContext); + mNativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler; + mJSCConfig = jscConfig; + } + + @Override + public DevSupportManager getDevSupportManager() { + return mDevSupportManager; + } + + @Override + public MemoryPressureRouter getMemoryPressureRouter() { + return mMemoryPressureRouter; + } + + private static void initializeSoLoaderIfNecessary(Context applicationContext) { + // Call SoLoader.initialize here, this is required for apps that does not use exopackage and + // does not use SoLoader for loading other native code except from the one used by React Native + // This way we don't need to require others to have additional initialization code and to + // subclass android.app.Application. + + // Method SoLoader.init is idempotent, so if you wish to use native exopackage, just call + // SoLoader.init with appropriate args before initializing XReactInstanceManagerImpl + SoLoader.init(applicationContext, /* native exopackage */ false); + } + + /** + * Trigger react context initialization asynchronously in a background async task. This enables + * applications to pre-load the application JS, and execute global code before + * {@link ReactRootView} is available and measured. This should only be called the first time the + * application is set up, which is enforced to keep developers from accidentally creating their + * application multiple times without realizing it. + * + * Called from UI thread. + */ + @Override + public void createReactContextInBackground() { + Assertions.assertCondition( + !mHasStartedCreatingInitialContext, + "createReactContextInBackground should only be called when creating the react " + + "application for the first time. When reloading JS, e.g. from a new file, explicitly" + + "use recreateReactContextInBackground"); + + mHasStartedCreatingInitialContext = true; + recreateReactContextInBackgroundInner(); + } + + /** + * Recreate the react application and context. This should be called if configuration has + * changed or the developer has requested the app to be reloaded. It should only be called after + * an initial call to createReactContextInBackground. + * + * Called from UI thread. + */ + public void recreateReactContextInBackground() { + Assertions.assertCondition( + mHasStartedCreatingInitialContext, + "recreateReactContextInBackground should only be called after the initial " + + "createReactContextInBackground call."); + recreateReactContextInBackgroundInner(); + } + + private void recreateReactContextInBackgroundInner() { + UiThreadUtil.assertOnUiThread(); + + if (mUseDeveloperSupport && mJSMainModuleName != null) { + if (mDevSupportManager.hasUpToDateJSBundleInCache()) { + // If there is a up-to-date bundle downloaded from server, always use that + onJSBundleLoadedFromServer(); + } else if (mJSBundleFile == null) { + mDevSupportManager.handleReloadJS(); + } else { + mDevSupportManager.isPackagerRunning( + new DevServerHelper.PackagerStatusCallback() { + @Override + public void onPackagerStatusFetched(final boolean packagerIsRunning) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (packagerIsRunning) { + mDevSupportManager.handleReloadJS(); + } else { + recreateReactContextInBackgroundFromBundleFile(); + } + } + }); + } + }); + } + return; + } + + recreateReactContextInBackgroundFromBundleFile(); + } + + private void recreateReactContextInBackgroundFromBundleFile() { + recreateReactContextInBackground( + new JSCJavaScriptExecutor.Factory( + mJSCConfig == null ? new WritableNativeMap() : mJSCConfig.getConfigMap()), + JSBundleLoader.createFileLoader(mApplicationContext, mJSBundleFile)); + } + + /** + * @return whether createReactContextInBackground has been called. Will return false after + * onDestroy until a new initial context has been created. + */ + public boolean hasStartedCreatingInitialContext() { + return mHasStartedCreatingInitialContext; + } + + /** + * This method will give JS the opportunity to consume the back button event. If JS does not + * consume the event, mDefaultBackButtonImpl will be invoked at the end of the round trip to JS. + */ + @Override + public void onBackPressed() { + UiThreadUtil.assertOnUiThread(); + ReactContext reactContext = mCurrentReactContext; + if (mCurrentReactContext == null) { + // Invoke without round trip to JS. + FLog.w(ReactConstants.TAG, "Instance detached from instance manager"); + invokeDefaultOnBackPressed(); + } else { + DeviceEventManagerModule deviceEventManagerModule = + Assertions.assertNotNull(reactContext).getNativeModule(DeviceEventManagerModule.class); + deviceEventManagerModule.emitHardwareBackPressed(); + } + } + + private void invokeDefaultOnBackPressed() { + UiThreadUtil.assertOnUiThread(); + if (mDefaultBackButtonImpl != null) { + mDefaultBackButtonImpl.invokeDefaultOnBackPressed(); + } + } + + private void toggleElementInspector() { + if (mCurrentReactContext != null) { + mCurrentReactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("toggleElementInspector", null); + } + } + + @Override + public void onHostPause() { + UiThreadUtil.assertOnUiThread(); + + mDefaultBackButtonImpl = null; + if (mUseDeveloperSupport) { + mDevSupportManager.setDevSupportEnabled(false); + } + + moveToBeforeResumeLifecycleState(); + mCurrentActivity = null; + } + + /** + * Use this method when the activity resumes to enable invoking the back button directly from JS. + * + * This method retains an instance to provided mDefaultBackButtonImpl. Thus it's important to pass + * from the activity instance that owns this particular instance of {@link + * XReactInstanceManagerImpl}, so that once this instance receive {@link #onHostDestroy} event it + * will clear the reference to that defaultBackButtonImpl. + * + * @param defaultBackButtonImpl a {@link DefaultHardwareBackBtnHandler} from an Activity that owns + * this instance of {@link XReactInstanceManagerImpl}. + */ + @Override + public void onHostResume(Activity activity, DefaultHardwareBackBtnHandler defaultBackButtonImpl) { + UiThreadUtil.assertOnUiThread(); + + mDefaultBackButtonImpl = defaultBackButtonImpl; + if (mUseDeveloperSupport) { + mDevSupportManager.setDevSupportEnabled(true); + } + + mCurrentActivity = activity; + moveToResumedLifecycleState(false); + } + + @Override + public void onHostDestroy() { + UiThreadUtil.assertOnUiThread(); + + if (mUseDeveloperSupport) { + mDevSupportManager.setDevSupportEnabled(false); + } + + moveToBeforeCreateLifecycleState(); + mCurrentActivity = null; + } + + @Override + public void destroy() { + UiThreadUtil.assertOnUiThread(); + + if (mUseDeveloperSupport) { + mDevSupportManager.setDevSupportEnabled(false); + } + + moveToBeforeCreateLifecycleState(); + + if (mReactContextInitAsyncTask != null) { + mReactContextInitAsyncTask.cancel(true); + } + + mMemoryPressureRouter.destroy(mApplicationContext); + + if (mCurrentReactContext != null) { + mCurrentReactContext.destroy(); + mCurrentReactContext = null; + mHasStartedCreatingInitialContext = false; + } + mCurrentActivity = null; + } + + private void moveToResumedLifecycleState(boolean force) { + if (mCurrentReactContext != null) { + // we currently don't have an onCreate callback so we call onResume for both transitions + if (force || + mLifecycleState == LifecycleState.BEFORE_RESUME || + mLifecycleState == LifecycleState.BEFORE_CREATE) { + mCurrentReactContext.onHostResume(mCurrentActivity); + } + } + mLifecycleState = LifecycleState.RESUMED; + } + + private void moveToBeforeResumeLifecycleState() { + if (mCurrentReactContext != null) { + if (mLifecycleState == LifecycleState.BEFORE_CREATE) { + mCurrentReactContext.onHostResume(mCurrentActivity); + mCurrentReactContext.onHostPause(); + } else if (mLifecycleState == LifecycleState.RESUMED) { + mCurrentReactContext.onHostPause(); + } + } + mLifecycleState = LifecycleState.BEFORE_RESUME; + } + + private void moveToBeforeCreateLifecycleState() { + if (mCurrentReactContext != null) { + if (mLifecycleState == LifecycleState.RESUMED) { + mCurrentReactContext.onHostPause(); + mLifecycleState = LifecycleState.BEFORE_RESUME; + } + if (mLifecycleState == LifecycleState.BEFORE_RESUME) { + mCurrentReactContext.onHostDestroy(); + } + } + mLifecycleState = LifecycleState.BEFORE_CREATE; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mCurrentReactContext != null) { + mCurrentReactContext.onActivityResult(requestCode, resultCode, data); + } + } + + @Override + public void showDevOptionsDialog() { + UiThreadUtil.assertOnUiThread(); + mDevSupportManager.showDevOptionsDialog(); + } + + /** + * Get the URL where the last bundle was loaded from. + */ + @Override + public String getSourceUrl() { + return Assertions.assertNotNull(mSourceUrl); + } + + /** + * Attach given {@param rootView} to a catalyst instance manager and start JS application using + * JS module provided by {@link ReactRootView#getJSModuleName}. If the react context is currently + * being (re)-created, or if react context has not been created yet, the JS application associated + * with the provided root view will be started asynchronously, i.e this method won't block. + * This view will then be tracked by this manager and in case of catalyst instance restart it will + * be re-attached. + */ + @Override + public void attachMeasuredRootView(ReactRootView rootView) { + UiThreadUtil.assertOnUiThread(); + mAttachedRootViews.add(rootView); + + // If react context is being created in the background, JS application will be started + // automatically when creation completes, as root view is part of the attached root view list. + if (mReactContextInitAsyncTask == null && mCurrentReactContext != null) { + attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance()); + } + } + + /** + * Detach given {@param rootView} from current catalyst instance. It's safe to call this method + * multiple times on the same {@param rootView} - in that case view will be detached with the + * first call. + */ + @Override + public void detachRootView(ReactRootView rootView) { + UiThreadUtil.assertOnUiThread(); + if (mAttachedRootViews.remove(rootView)) { + if (mCurrentReactContext != null && mCurrentReactContext.hasActiveCatalystInstance()) { + detachViewFromInstance(rootView, mCurrentReactContext.getCatalystInstance()); + } + } + } + + /** + * Uses configured {@link ReactPackage} instances to create all view managers + */ + @Override + public List createAllViewManagers( + ReactApplicationContext catalystApplicationContext) { + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "createAllViewManagers"); + try { + List allViewManagers = new ArrayList<>(); + for (ReactPackage reactPackage : mPackages) { + allViewManagers.addAll(reactPackage.createViewManagers(catalystApplicationContext)); + } + return allViewManagers; + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + + @Override + public void addReactInstanceEventListener(ReactInstanceEventListener listener) { + mReactInstanceEventListeners.add(listener); + } + + @Override + public void removeReactInstanceEventListener(ReactInstanceEventListener listener) { + mReactInstanceEventListeners.remove(listener); + } + + @VisibleForTesting + @Override + public @Nullable ReactContext getCurrentReactContext() { + return mCurrentReactContext; + } + + @Override + public LifecycleState getLifecycleState() { + return mLifecycleState; + } + + private void onReloadWithJSDebugger(JavaJSExecutor.Factory jsExecutorFactory) { + recreateReactContextInBackground( + new ProxyJavaScriptExecutor.Factory(jsExecutorFactory), + JSBundleLoader.createRemoteDebuggerBundleLoader( + mDevSupportManager.getJSBundleURLForRemoteDebugging(), + mDevSupportManager.getSourceUrl())); + } + + private void onJSBundleLoadedFromServer() { + recreateReactContextInBackground( + new JSCJavaScriptExecutor.Factory( + mJSCConfig == null ? new WritableNativeMap() : mJSCConfig.getConfigMap()), + JSBundleLoader.createCachedBundleFromNetworkLoader( + mDevSupportManager.getSourceUrl(), + mDevSupportManager.getDownloadedJSBundleFile())); + } + + private void recreateReactContextInBackground( + JavaScriptExecutor.Factory jsExecutorFactory, + JSBundleLoader jsBundleLoader) { + UiThreadUtil.assertOnUiThread(); + + ReactContextInitParams initParams = + new ReactContextInitParams(jsExecutorFactory, jsBundleLoader); + if (mReactContextInitAsyncTask == null) { + // No background task to create react context is currently running, create and execute one. + mReactContextInitAsyncTask = new ReactContextInitAsyncTask(); + mReactContextInitAsyncTask.execute(initParams); + } else { + // Background task is currently running, queue up most recent init params to recreate context + // once task completes. + mPendingReactContextInitParams = initParams; + } + } + + private void setupReactContext(ReactApplicationContext reactContext) { + UiThreadUtil.assertOnUiThread(); + Assertions.assertCondition(mCurrentReactContext == null); + mCurrentReactContext = Assertions.assertNotNull(reactContext); + CatalystInstance catalystInstance = + Assertions.assertNotNull(reactContext.getCatalystInstance()); + + catalystInstance.initialize(); + mDevSupportManager.onNewReactContextCreated(reactContext); + mMemoryPressureRouter.addMemoryPressureListener(catalystInstance); + moveReactContextToCurrentLifecycleState(); + + for (ReactRootView rootView : mAttachedRootViews) { + attachMeasuredRootViewToInstance(rootView, catalystInstance); + } + + ReactInstanceEventListener[] listeners = + new ReactInstanceEventListener[mReactInstanceEventListeners.size()]; + listeners = mReactInstanceEventListeners.toArray(listeners); + + for (ReactInstanceEventListener listener : listeners) { + listener.onReactContextInitialized(reactContext); + } + } + + private void attachMeasuredRootViewToInstance( + ReactRootView rootView, + CatalystInstance catalystInstance) { + UiThreadUtil.assertOnUiThread(); + + // Reset view content as it's going to be populated by the application content from JS + rootView.removeAllViews(); + rootView.setId(View.NO_ID); + + UIManagerModule uiManagerModule = catalystInstance.getNativeModule(UIManagerModule.class); + int rootTag = uiManagerModule.addMeasuredRootView(rootView); + @Nullable Bundle launchOptions = rootView.getLaunchOptions(); + WritableMap initialProps = Arguments.makeNativeMap(launchOptions); + String jsAppModuleName = rootView.getJSModuleName(); + + WritableNativeMap appParams = new WritableNativeMap(); + appParams.putDouble("rootTag", rootTag); + appParams.putMap("initialProps", initialProps); + catalystInstance.getJSModule(AppRegistry.class).runApplication(jsAppModuleName, appParams); + } + + private void detachViewFromInstance( + ReactRootView rootView, + CatalystInstance catalystInstance) { + UiThreadUtil.assertOnUiThread(); + catalystInstance.getJSModule(AppRegistry.class) + .unmountApplicationComponentAtRootTag(rootView.getId()); + } + + private void tearDownReactContext(ReactContext reactContext) { + UiThreadUtil.assertOnUiThread(); + if (mLifecycleState == LifecycleState.RESUMED) { + reactContext.onHostPause(); + } + for (ReactRootView rootView : mAttachedRootViews) { + detachViewFromInstance(rootView, reactContext.getCatalystInstance()); + } + reactContext.destroy(); + mDevSupportManager.onReactInstanceDestroyed(reactContext); + mMemoryPressureRouter.removeMemoryPressureListener(reactContext.getCatalystInstance()); + } + + /** + * @return instance of {@link ReactContext} configured a {@link CatalystInstance} set + */ + private ReactApplicationContext createReactContext( + JavaScriptExecutor jsExecutor, + JSBundleLoader jsBundleLoader) { + FLog.i(ReactConstants.TAG, "Creating react context."); + ReactMarker.logMarker(CREATE_REACT_CONTEXT_START); + mSourceUrl = jsBundleLoader.getSourceUrl(); + NativeModuleRegistry.Builder nativeRegistryBuilder = new NativeModuleRegistry.Builder(); + JavaScriptModuleRegistry.Builder jsModulesBuilder = new JavaScriptModuleRegistry.Builder(); + + ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext); + if (mUseDeveloperSupport) { + reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager); + } + + ReactMarker.logMarker(PROCESS_PACKAGES_START); + Systrace.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "createAndProcessCoreModulesPackage"); + try { + CoreModulesPackage coreModulesPackage = + new CoreModulesPackage(this, mBackBtnHandler, mUIImplementationProvider); + processPackage(coreModulesPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + + // TODO(6818138): Solve use-case of native/js modules overriding + for (ReactPackage reactPackage : mPackages) { + Systrace.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "createAndProcessCustomReactPackage"); + try { + processPackage(reactPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + ReactMarker.logMarker(PROCESS_PACKAGES_END); + + ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_START); + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "buildNativeModuleRegistry"); + NativeModuleRegistry nativeModuleRegistry; + try { + nativeModuleRegistry = nativeRegistryBuilder.build(); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_END); + } + + NativeModuleCallExceptionHandler exceptionHandler = mNativeModuleCallExceptionHandler != null + ? mNativeModuleCallExceptionHandler + : mDevSupportManager; + CatalystInstanceImpl.Builder catalystInstanceBuilder = new CatalystInstanceImpl.Builder() + .setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault()) + .setJSExecutor(jsExecutor) + .setRegistry(nativeModuleRegistry) + .setJSModuleRegistry(jsModulesBuilder.build()) + .setJSBundleLoader(jsBundleLoader) + .setNativeModuleCallExceptionHandler(exceptionHandler); + + ReactMarker.logMarker(CREATE_CATALYST_INSTANCE_START); + // CREATE_CATALYST_INSTANCE_END is in JSCExecutor.cpp + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "createCatalystInstance"); + final CatalystInstance catalystInstance; + try { + catalystInstance = catalystInstanceBuilder.build(); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + ReactMarker.logMarker(CREATE_CATALYST_INSTANCE_END); + } + + if (mBridgeIdleDebugListener != null) { + catalystInstance.addBridgeIdleDebugListener(mBridgeIdleDebugListener); + } + + reactContext.initializeWithInstance(catalystInstance); + + ReactMarker.logMarker(RUN_JS_BUNDLE_START); + catalystInstance.getReactQueueConfiguration().getJSQueueThread().runOnQueue(new Runnable() { + @Override + public void run() { + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "runJSBundle"); + try { + catalystInstance.runJSBundle(); + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + ReactMarker.logMarker(RUN_JS_BUNDLE_END); + } + } + }); + + return reactContext; + } + + private void processPackage( + ReactPackage reactPackage, + ReactApplicationContext reactContext, + NativeModuleRegistry.Builder nativeRegistryBuilder, + JavaScriptModuleRegistry.Builder jsModulesBuilder) { + for (NativeModule nativeModule : reactPackage.createNativeModules(reactContext)) { + nativeRegistryBuilder.add(nativeModule); + } + for (Class jsModuleClass : reactPackage.createJSModules()) { + jsModulesBuilder.add(jsModuleClass); + } + } + + private void moveReactContextToCurrentLifecycleState() { + if (mLifecycleState == LifecycleState.RESUMED) { + moveToResumedLifecycleState(true); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/Arguments.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/Arguments.java new file mode 100644 index 00000000000000..fbfe0db1128144 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/Arguments.java @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.cxxbridge; + +import java.lang.reflect.Array; + +import java.util.AbstractList; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +import android.os.Bundle; + +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.ReadableType; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; + +public class Arguments { + private static Object makeNativeObject(Object object) { + if (object == null) { + return null; + } else if (object instanceof Float || + object instanceof Long || + object instanceof Byte || + object instanceof Short) { + return new Double(((Number) object).doubleValue()); + } else if (object.getClass().isArray()) { + return makeNativeArray(object); + } else if (object instanceof List) { + return makeNativeArray((List) object); + } else if (object instanceof Map) { + return makeNativeMap((Map) object); + } else if (object instanceof Bundle) { + return makeNativeMap((Bundle) object); + } else { + // Boolean, Integer, Double, String, WritableNativeArray, WritableNativeMap + return object; + } + } + + /** + * This method converts a List into a NativeArray. The data types supported + * are boolean, int, float, double, and String. List, Map, and Bundle + * objects, as well as arrays, containing values of the above types and/or + * null, or any recursive arrangement of these, are also supported. The best + * way to think of this is a way to generate a Java representation of a json + * list, from Java types which have a natural representation in json. + */ + public static WritableNativeArray makeNativeArray(List objects) { + WritableNativeArray nativeArray = new WritableNativeArray(); + if (objects == null) { + return nativeArray; + } + for (Object elem : objects) { + elem = makeNativeObject(elem); + if (elem == null) { + nativeArray.pushNull(); + } else if (elem instanceof Boolean) { + nativeArray.pushBoolean((Boolean) elem); + } else if (elem instanceof Integer) { + nativeArray.pushInt((Integer) elem); + } else if (elem instanceof Double) { + nativeArray.pushDouble((Double) elem); + } else if (elem instanceof String) { + nativeArray.pushString((String) elem); + } else if (elem instanceof WritableNativeArray) { + nativeArray.pushArray((WritableNativeArray) elem); + } else if (elem instanceof WritableNativeMap) { + nativeArray.pushMap((WritableNativeMap) elem); + } else { + throw new IllegalArgumentException("Could not convert " + elem.getClass()); + } + } + return nativeArray; + } + + + /** + * This overload is like the above, but uses reflection to operate on any + * primitive or object type. + */ + public static WritableNativeArray makeNativeArray(final Object objects) { + if (objects == null) { + return new WritableNativeArray(); + } + // No explicit check for objects's type here. If it's not an array, the + // Array methods will throw IllegalArgumentException. + return makeNativeArray(new AbstractList() { + public int size() { + return Array.getLength(objects); + } + public Object get(int index) { + return Array.get(objects, index); + } + }); + } + + private static void addEntry(WritableNativeMap nativeMap, String key, Object value) { + value = makeNativeObject(value); + if (value == null) { + nativeMap.putNull(key); + } else if (value instanceof Boolean) { + nativeMap.putBoolean(key, (Boolean) value); + } else if (value instanceof Integer) { + nativeMap.putInt(key, (Integer) value); + } else if (value instanceof Number) { + nativeMap.putDouble(key, ((Number) value).doubleValue()); + } else if (value instanceof String) { + nativeMap.putString(key, (String) value); + } else if (value instanceof WritableNativeArray) { + nativeMap.putArray(key, (WritableNativeArray) value); + } else if (value instanceof WritableNativeMap) { + nativeMap.putMap(key, (WritableNativeMap) value); + } else { + throw new IllegalArgumentException("Could not convert " + value.getClass()); + } + } + + /** + * This method converts a Map into a NativeMap. Value types are supported as + * with makeNativeArray. The best way to think of this is a way to generate + * a Java representation of a json object, from Java types which have a + * natural representation in json. + */ + public static WritableNativeMap makeNativeMap(Map objects) { + WritableNativeMap nativeMap = new WritableNativeMap(); + if (objects == null) { + return nativeMap; + } + for (Map.Entry entry : objects.entrySet()) { + addEntry(nativeMap, entry.getKey(), entry.getValue()); + } + return nativeMap; + } + + /** + * Like the above, but takes a Bundle instead of a Map. + */ + public static WritableNativeMap makeNativeMap(Bundle bundle) { + WritableNativeMap nativeMap = new WritableNativeMap(); + if (bundle == null) { + return nativeMap; + } + for (String key: bundle.keySet()) { + addEntry(nativeMap, key, bundle.get(key)); + } + return nativeMap; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK new file mode 100644 index 00000000000000..feed577072e8bb --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK @@ -0,0 +1,38 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'bridge', + srcs = glob(['**/*.java']), + exported_deps = [ + react_native_dep('java/com/facebook/jni:jni'), + react_native_dep('java/com/facebook/proguard/annotations:annotations'), + ], + proguard_config = 'bridge.pro', + deps = [ + '//libraries/fbcore/src/main/java/com/facebook/common/logging:logging', + react_native_dep('java/com/facebook/systrace:systrace'), + react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'), + # TODO mhorowitz: + # java/com/facebook/catalyst/js/react-native-github/ReactAndroid/src/main/java/com/facebook/react/bridge/ + # lacks a similar dependency to this. This means that the + # loadLibrary calls in it are not guaranteed to succeed. This is + # kind of a mess for the jni/jni-internal stuff. In theory, we + # should be creating -internal android_library rules, too. In + # practice, since these are resolved at runtime, putting the + # dependency in the app works, too. gross. + # '//native/react/jni:jni-internal', + react_native_dep('third-party/java/infer-annotations:infer-annotations'), + react_native_dep('third-party/java/jackson:core'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + react_native_dep('third-party/java/okhttp:okhttp3-ws'), + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + ], + visibility = [ + 'PUBLIC', + ], +) + +project_config( + src_target = ':bridge', +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CallbackImpl.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CallbackImpl.java new file mode 100644 index 00000000000000..6c5f27ae300a3c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CallbackImpl.java @@ -0,0 +1,30 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.cxxbridge; + +import com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.*; + +import static com.facebook.react.bridge.Arguments.*; + +/** + * Callback impl that calls directly into the cxxbridge. Created from C++. + */ +@DoNotStrip +public class CallbackImpl implements Callback { + @DoNotStrip + private final HybridData mHybridData; + + @DoNotStrip + private CallbackImpl(HybridData hybridData) { + mHybridData = hybridData; + } + + @Override + public void invoke(Object... args) { + nativeInvoke(fromJavaArgs(args)); + } + + private native void nativeInvoke(NativeArray arguments); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java new file mode 100644 index 00000000000000..3a5de280d1a071 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java @@ -0,0 +1,461 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.cxxbridge; + +import javax.annotation.Nullable; + +import java.lang.ref.WeakReference; +import java.util.Collection; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import android.content.res.AssetManager; + +import com.facebook.common.logging.FLog; +import com.facebook.jni.HybridData; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.ExecutorToken; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.JavaScriptModuleRegistry; +import com.facebook.react.bridge.MemoryPressure; +import com.facebook.react.bridge.NativeArray; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.NativeModuleCallExceptionHandler; +import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener; +import com.facebook.react.bridge.queue.ReactQueueConfiguration; +import com.facebook.react.bridge.queue.MessageQueueThread; +import com.facebook.react.bridge.queue.QueueThreadExceptionHandler; +import com.facebook.react.bridge.queue.ReactQueueConfigurationImpl; +import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.infer.annotation.Assertions; +import com.facebook.soloader.SoLoader; +import com.facebook.systrace.Systrace; +import com.facebook.systrace.TraceListener; + +/** + * This provides an implementation of the public CatalystInstance instance. It is public because + * it is built by XReactInstanceManager which is in a different package. + */ +@DoNotStrip +public class CatalystInstanceImpl implements CatalystInstance { + + /* package */ static final String REACT_NATIVE_LIB = "reactnativejnifb"; + + static { + SoLoader.loadLibrary(REACT_NATIVE_LIB); + } + + private static final int BRIDGE_SETUP_TIMEOUT_MS = 30000; + private static final int LOAD_JS_BUNDLE_TIMEOUT_MS = 30000; + + private static final AtomicInteger sNextInstanceIdForTrace = new AtomicInteger(1); + + // Access from any thread + private final ReactQueueConfigurationImpl mReactQueueConfiguration; + private final CopyOnWriteArrayList mBridgeIdleListeners; + private final AtomicInteger mPendingJSCalls = new AtomicInteger(0); + private final String mJsPendingCallsTitleForTrace = + "pending_js_calls_instance" + sNextInstanceIdForTrace.getAndIncrement(); + private volatile boolean mDestroyed = false; + private final TraceListener mTraceListener; + private final JavaScriptModuleRegistry mJSModuleRegistry; + private final JSBundleLoader mJSBundleLoader; + private ExecutorToken mMainExecutorToken; + + private final NativeModuleRegistry mJavaRegistry; + private final NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; + private boolean mInitialized = false; + + private boolean mJSBundleHasLoaded; + + // C++ parts + private final HybridData mHybridData; + private native static HybridData initHybrid(); + + private CatalystInstanceImpl( + final ReactQueueConfigurationSpec ReactQueueConfigurationSpec, + final JavaScriptExecutor jsExecutor, + final NativeModuleRegistry registry, + final JavaScriptModuleRegistry jsModuleRegistry, + final JSBundleLoader jsBundleLoader, + NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler) { + FLog.d(ReactConstants.TAG, "Initializing React Xplat Bridge."); + mHybridData = initHybrid(); + + mReactQueueConfiguration = ReactQueueConfigurationImpl.create( + ReactQueueConfigurationSpec, + new NativeExceptionHandler()); + mBridgeIdleListeners = new CopyOnWriteArrayList<>(); + mJavaRegistry = registry; + mJSModuleRegistry = jsModuleRegistry; + mJSBundleLoader = jsBundleLoader; + mNativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler; + mTraceListener = new JSProfilerTraceListener(this); + + initializeBridge( + new BridgeCallback(this), + jsExecutor, + mReactQueueConfiguration.getJSQueueThread(), + mReactQueueConfiguration.getNativeModulesQueueThread(), + mJavaRegistry.getModuleRegistryHolder(this)); + mMainExecutorToken = getMainExecutorToken(); + } + + private static class BridgeCallback implements ReactCallback { + // We do this so the callback doesn't keep the CatalystInstanceImpl alive. + // In this case, the callback is held in C++ code, so the GC can't see it + // and determine there's an inaccessible cycle. + private final WeakReference mOuter; + + public BridgeCallback(CatalystInstanceImpl outer) { + mOuter = new WeakReference(outer); + } + + @Override + public void onBatchComplete() { + CatalystInstanceImpl impl = mOuter.get(); + if (impl != null) { + impl.mJavaRegistry.onBatchComplete(); + } + } + + @Override + public void incrementPendingJSCalls() { + CatalystInstanceImpl impl = mOuter.get(); + if (impl != null) { + impl.incrementPendingJSCalls(); + } + } + + @Override + public void decrementPendingJSCalls() { + CatalystInstanceImpl impl = mOuter.get(); + if (impl != null) { + impl.decrementPendingJSCalls(); + } + } + + @Override + public void onNativeException(Exception e) { + CatalystInstanceImpl impl = mOuter.get(); + if (impl != null) { + impl.onNativeException(e); + } + } + } + + private native void initializeBridge(ReactCallback callback, + JavaScriptExecutor jsExecutor, + MessageQueueThread jsQueue, + MessageQueueThread moduleQueue, + ModuleRegistryHolder registryHolder); + + /* package */ native void loadScriptFromAssets(AssetManager assetManager, String assetURL); + /* package */ native void loadScriptFromFile(String fileName, String sourceURL); + + @Override + public void runJSBundle() { + Assertions.assertCondition(!mJSBundleHasLoaded, "JS bundle was already loaded!"); + mJSBundleHasLoaded = true; + // incrementPendingJSCalls(); + mJSBundleLoader.loadScript(CatalystInstanceImpl.this); + // This is registered after JS starts since it makes a JS call + Systrace.registerListener(mTraceListener); + } + + private native void callJSFunction( + ExecutorToken token, + String module, + String method, + NativeArray arguments, + String tracingName); + + @Override + public void callFunction( + ExecutorToken executorToken, + final String module, + final String method, + final NativeArray arguments, + final String tracingName) { + if (mDestroyed) { + FLog.w(ReactConstants.TAG, "Calling JS function after bridge has been destroyed."); + return; + } + + callJSFunction(executorToken, module, method, arguments, tracingName); + } + + private native void callJSCallback(ExecutorToken executorToken, int callbackID, NativeArray arguments); + + @Override + public void invokeCallback(ExecutorToken executorToken, final int callbackID, final NativeArray arguments) { + if (mDestroyed) { + FLog.w(ReactConstants.TAG, "Invoking JS callback after bridge has been destroyed."); + return; + } + + callJSCallback(executorToken, callbackID, arguments); + } + + /** + * Destroys this catalyst instance, waiting for any other threads in ReactQueueConfiguration + * (besides the UI thread) to finish running. Must be called from the UI thread so that we can + * fully shut down other threads. + */ + @Override + public void destroy() { + UiThreadUtil.assertOnUiThread(); + + if (mDestroyed) { + return; + } + + // TODO: tell all APIs to shut down + mDestroyed = true; + mHybridData.resetNative(); + mJavaRegistry.notifyCatalystInstanceDestroy(); + boolean wasIdle = (mPendingJSCalls.getAndSet(0) == 0); + if (!wasIdle && !mBridgeIdleListeners.isEmpty()) { + for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) { + listener.onTransitionToBridgeIdle(); + } + } + + // This is a noop if the listener was not yet registered. + Systrace.unregisterListener(mTraceListener); + } + + @Override + public boolean isDestroyed() { + return mDestroyed; + } + + /** + * Initialize all the native modules + */ + @VisibleForTesting + @Override + public void initialize() { + UiThreadUtil.assertOnUiThread(); + Assertions.assertCondition( + !mInitialized, + "This catalyst instance has already been initialized"); + mInitialized = true; + mJavaRegistry.notifyCatalystInstanceInitialized(); + } + + @Override + public ReactQueueConfiguration getReactQueueConfiguration() { + return mReactQueueConfiguration; + } + + @Override + public T getJSModule(Class jsInterface) { + return getJSModule(mMainExecutorToken, jsInterface); + } + + @Override + public T getJSModule(ExecutorToken executorToken, Class jsInterface) { + return Assertions.assertNotNull(mJSModuleRegistry) + .getJavaScriptModule(this, executorToken, jsInterface); + } + + private native ExecutorToken getMainExecutorToken(); + + @Override + public boolean hasNativeModule(Class nativeModuleInterface) { + return mJavaRegistry.hasModule(nativeModuleInterface); + } + + // This is only ever called with UIManagerModule or CurrentViewerModule. + @Override + public T getNativeModule(Class nativeModuleInterface) { + return mJavaRegistry.getModule(nativeModuleInterface); + } + + // This is only used by com.facebook.react.modules.common.ModuleDataCleaner + @Override + public Collection getNativeModules() { + return mJavaRegistry.getAllModules(); + } + + @Override + public void handleMemoryPressure(MemoryPressure level) { + } + + /** + * Adds a idle listener for this Catalyst instance. The listener will receive notifications + * whenever the bridge transitions from idle to busy and vice-versa, where the busy state is + * defined as there being some non-zero number of calls to JS that haven't resolved via a + * onBatchComplete call. The listener should be purely passive and not affect application logic. + */ + @Override + public void addBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) { + mBridgeIdleListeners.add(listener); + } + + /** + * Removes a NotThreadSafeBridgeIdleDebugListener previously added with + * {@link #addBridgeIdleDebugListener} + */ + @Override + public void removeBridgeIdleDebugListener(NotThreadSafeBridgeIdleDebugListener listener) { + mBridgeIdleListeners.remove(listener); + } + + @Override + public native void setGlobalVariable(String propName, String jsonValue); + + // TODO mhorowitz: add mDestroyed checks to the next three methods + + @Override + public native boolean supportsProfiling(); + + @Override + public native void startProfiler(String title); + + @Override + public native void stopProfiler(String title, String filename); + + private void incrementPendingJSCalls() { + int oldPendingCalls = mPendingJSCalls.getAndIncrement(); + boolean wasIdle = oldPendingCalls == 0; + Systrace.traceCounter( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + mJsPendingCallsTitleForTrace, + oldPendingCalls + 1); + if (wasIdle && !mBridgeIdleListeners.isEmpty()) { + for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) { + listener.onTransitionToBridgeBusy(); + } + } + } + + private void decrementPendingJSCalls() { + int newPendingCalls = mPendingJSCalls.decrementAndGet(); + // TODO(9604406): handle case of web workers injecting messages to main thread + //Assertions.assertCondition(newPendingCalls >= 0); + boolean isNowIdle = newPendingCalls == 0; + Systrace.traceCounter( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + mJsPendingCallsTitleForTrace, + newPendingCalls); + + if (isNowIdle && !mBridgeIdleListeners.isEmpty()) { + for (NotThreadSafeBridgeIdleDebugListener listener : mBridgeIdleListeners) { + listener.onTransitionToBridgeIdle(); + } + } + } + + private void onNativeException(Exception e) { + mNativeModuleCallExceptionHandler.handleException(e); + mReactQueueConfiguration.getUIQueueThread().runOnQueue( + new Runnable() { + @Override + public void run() { + destroy(); + } + }); + } + + private class NativeExceptionHandler implements QueueThreadExceptionHandler { + @Override + public void handleException(Exception e) { + // Any Exception caught here is because of something in JS. Even if it's a bug in the + // framework/native code, it was triggered by JS and theoretically since we were able + // to set up the bridge, JS could change its logic, reload, and not trigger that crash. + onNativeException(e); + } + } + + private static class JSProfilerTraceListener implements TraceListener { + // We do this so the callback doesn't keep the CatalystInstanceImpl alive. + // In this case, Systrace will keep the registered listener around forever + // if the CatalystInstanceImpl is not explicitly destroyed. These instances + // can still leak, but they are at least small. + private final WeakReference mOuter; + + public JSProfilerTraceListener(CatalystInstanceImpl outer) { + mOuter = new WeakReference(outer); + } + + @Override + public void onTraceStarted() { + CatalystInstanceImpl impl = mOuter.get(); + if (impl != null) { + impl.getJSModule(com.facebook.react.bridge.Systrace.class).setEnabled(true); + } + } + + @Override + public void onTraceStopped() { + CatalystInstanceImpl impl = mOuter.get(); + if (impl != null) { + impl.getJSModule(com.facebook.react.bridge.Systrace.class).setEnabled(false); + } + } + } + + public static class Builder { + + private @Nullable ReactQueueConfigurationSpec mReactQueueConfigurationSpec; + private @Nullable JSBundleLoader mJSBundleLoader; + private @Nullable NativeModuleRegistry mRegistry; + private @Nullable JavaScriptModuleRegistry mJSModuleRegistry; + private @Nullable JavaScriptExecutor mJSExecutor; + private @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; + + public Builder setReactQueueConfigurationSpec( + ReactQueueConfigurationSpec ReactQueueConfigurationSpec) { + mReactQueueConfigurationSpec = ReactQueueConfigurationSpec; + return this; + } + + public Builder setRegistry(NativeModuleRegistry registry) { + mRegistry = registry; + return this; + } + + public Builder setJSModuleRegistry(JavaScriptModuleRegistry jsModuleRegistry) { + mJSModuleRegistry = jsModuleRegistry; + return this; + } + + public Builder setJSBundleLoader(JSBundleLoader jsBundleLoader) { + mJSBundleLoader = jsBundleLoader; + return this; + } + + public Builder setJSExecutor(JavaScriptExecutor jsExecutor) { + mJSExecutor = jsExecutor; + return this; + } + + public Builder setNativeModuleCallExceptionHandler( + NativeModuleCallExceptionHandler handler) { + mNativeModuleCallExceptionHandler = handler; + return this; + } + + public CatalystInstanceImpl build() { + return new CatalystInstanceImpl( + Assertions.assertNotNull(mReactQueueConfigurationSpec), + Assertions.assertNotNull(mJSExecutor), + Assertions.assertNotNull(mRegistry), + Assertions.assertNotNull(mJSModuleRegistry), + Assertions.assertNotNull(mJSBundleLoader), + Assertions.assertNotNull(mNativeModuleCallExceptionHandler)); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CxxModuleWrapper.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CxxModuleWrapper.java new file mode 100644 index 00000000000000..14d8a7dfd56a7d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CxxModuleWrapper.java @@ -0,0 +1,110 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.cxxbridge; + +import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.ExecutorToken; +import com.facebook.react.bridge.JsonWriter; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactBridge; +import com.facebook.react.bridge.ReadableNativeArray; + +import com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.soloader.SoLoader; + +import java.io.IOException; +import java.util.Map; + +/** + * A Java Object which represents a cross-platform C++ module + * + */ +@DoNotStrip +public class CxxModuleWrapper implements NativeModule +{ + static { + SoLoader.loadLibrary(CatalystInstanceImpl.REACT_NATIVE_LIB); + } + + @DoNotStrip + private HybridData mHybridData; + + @DoNotStrip + private static class MethodWrapper implements NativeMethod + { + @DoNotStrip + HybridData mHybridData; + + MethodWrapper() { + mHybridData = initHybrid(); + } + + public native HybridData initHybrid(); + + @Override + public native void invoke(CatalystInstance catalystInstance, ExecutorToken executorToken, ReadableNativeArray args); + + @Override + public String getType() { + return BaseJavaModule.METHOD_TYPE_REMOTE; + } + } + + public CxxModuleWrapper(String library, String factory) { + SoLoader.loadLibrary(library); + mHybridData = + initHybrid(SoLoader.unpackLibraryAndDependencies(library).getAbsolutePath(), factory); + } + + @Override + public native String getName(); + + @Override + public native Map getMethods(); + + @Override + public void writeConstantsField(JsonWriter writer, String fieldName) throws IOException { + String constants = getConstantsJson(); + if (constants == null || constants.isEmpty()) { + return; + } + + writer.name(fieldName).rawValue(constants); + } + + public native String getConstantsJson(); + + @Override + public void initialize() { + // do nothing + } + + @Override + public boolean canOverrideExistingModule() { + return false; + } + + @Override + public boolean supportsWebWorkers() { + return false; + } + + @Override + public void onReactBridgeInitialized(ReactBridge bridge) { + // do nothing + } + + @Override + public void onCatalystInstanceDestroy() { + mHybridData.resetNative(); + } + + // For creating a wrapper from C++, or from a derived class. + protected CxxModuleWrapper(HybridData hd) { + mHybridData = hd; + } + + private native HybridData initHybrid(String soPath, String factory); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ExecutorToken.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ExecutorToken.java new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSBundleLoader.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSBundleLoader.java new file mode 100644 index 00000000000000..09fd6a4a64ff6d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSBundleLoader.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.cxxbridge; + +import android.content.Context; + +/** + * A class that stores JS bundle information and allows {@link CatalystInstance} to load a correct + * bundle through {@link ReactBridge}. + */ +public abstract class JSBundleLoader { + + /** + * This loader is recommended one for release version of your app. In that case local JS executor + * should be used. JS bundle will be read from assets directory in native code to save on passing + * large strings from java to native memory. + */ + public static JSBundleLoader createFileLoader( + final Context context, + final String fileName) { + return new JSBundleLoader() { + @Override + public void loadScript(CatalystInstanceImpl instance) { + if (fileName.startsWith("assets://")) { + instance.loadScriptFromAssets(context.getAssets(), fileName); + } else { + instance.loadScriptFromFile(fileName, fileName); + } + } + + @Override + public String getSourceUrl() { + return fileName; + } + }; + } + + /** + * This loader is used when bundle gets reloaded from dev server. In that case loader expect JS + * bundle to be prefetched and stored in local file. We do that to avoid passing large strings + * between java and native code and avoid allocating memory in java to fit whole JS bundle in it. + * Providing correct {@param sourceURL} of downloaded bundle is required for JS stacktraces to + * work correctly and allows for source maps to correctly symbolize those. + */ + public static JSBundleLoader createCachedBundleFromNetworkLoader( + final String sourceURL, + final String cachedFileLocation) { + return new JSBundleLoader() { + @Override + public void loadScript(CatalystInstanceImpl instance) { + instance.loadScriptFromFile(cachedFileLocation, sourceURL); + } + + @Override + public String getSourceUrl() { + return sourceURL; + } + }; + } + + /** + * This loader is used when proxy debugging is enabled. In that case there is no point in fetching + * the bundle from device as remote executor will have to do it anyway. + */ + public static JSBundleLoader createRemoteDebuggerBundleLoader( + final String proxySourceURL, + final String realSourceURL) { + return new JSBundleLoader() { + @Override + public void loadScript(CatalystInstanceImpl instance) { + instance.loadScriptFromFile(null, proxySourceURL); + } + + @Override + public String getSourceUrl() { + return realSourceURL; + } + }; + } + + public abstract void loadScript(CatalystInstanceImpl instance); + public abstract String getSourceUrl(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSCJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSCJavaScriptExecutor.java new file mode 100644 index 00000000000000..36e0bb29efea43 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JSCJavaScriptExecutor.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.cxxbridge; + +import com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.ReadableNativeArray; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.soloader.SoLoader; + +@DoNotStrip +public class JSCJavaScriptExecutor extends JavaScriptExecutor { + public static class Factory implements JavaScriptExecutor.Factory { + private ReadableNativeArray mJSCConfig; + + public Factory(WritableNativeMap jscConfig) { + // TODO (t10707444): use NativeMap, which requires moving NativeMap out of OnLoad. + WritableNativeArray array = new WritableNativeArray(); + array.pushMap(jscConfig); + mJSCConfig = array; + } + + @Override + public JavaScriptExecutor create() throws Exception { + return new JSCJavaScriptExecutor(mJSCConfig); + } + } + + static { + SoLoader.loadLibrary(CatalystInstanceImpl.REACT_NATIVE_LIB); + } + + public JSCJavaScriptExecutor(ReadableNativeArray jscConfig) { + super(initHybrid(jscConfig)); + } + + private native static HybridData initHybrid(ReadableNativeArray jscConfig); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java new file mode 100644 index 00000000000000..f9dc5cbb5e9efd --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaModuleWrapper.java @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.cxxbridge; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.facebook.proguard.annotations.DoNotStrip; + +import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.ExecutorToken; +import com.facebook.react.bridge.NativeArray; +import com.facebook.react.bridge.ReadableNativeArray; +import com.facebook.react.bridge.WritableNativeArray; + +/** + * This is part of the glue which wraps a java BaseJavaModule in a C++ + * NativeModule. This could all be in C++, but it's android-specific + * initialization code, and writing it this way is easier to read and means + * fewer JNI calls. + */ + +@DoNotStrip +/* package */ class JavaModuleWrapper { + @DoNotStrip + public class MethodDescriptor { + @DoNotStrip + Method method; + @DoNotStrip + String signature; + @DoNotStrip + String name; + @DoNotStrip + String type; + } + + private final CatalystInstance mCatalystInstance; + private final BaseJavaModule mModule; + private final ArrayList mMethods; + + public JavaModuleWrapper(CatalystInstance catalystinstance, BaseJavaModule module) { + mCatalystInstance = catalystinstance; + mModule = module; + mMethods = new ArrayList(); + } + + @DoNotStrip + public BaseJavaModule getModule() { + return mModule; + } + + @DoNotStrip + public String getName() { + return mModule.getName(); + } + + @DoNotStrip + public List getMethodDescriptors() { + ArrayList descs = new ArrayList<>(); + + for (Map.Entry entry : + mModule.getMethods().entrySet()) { + MethodDescriptor md = new MethodDescriptor(); + md.name = entry.getKey(); + md.type = entry.getValue().getType(); + + BaseJavaModule.JavaMethod method = (BaseJavaModule.JavaMethod) entry.getValue(); + mMethods.add(method); + + descs.add(md); + } + + return descs; + } + + @DoNotStrip + public List newGetMethodDescriptors() { + ArrayList descs = new ArrayList<>(); + + for (Map.Entry entry : + mModule.getMethods().entrySet()) { + MethodDescriptor md = new MethodDescriptor(); + md.name = entry.getKey(); + md.type = entry.getValue().getType(); + + BaseJavaModule.JavaMethod method = (BaseJavaModule.JavaMethod) entry.getValue(); + md.method = method.getMethod(); + md.signature = method.getSignature(); + + descs.add(md); + } + + for (Map.Entry entry : + mModule.getSyncHooks().entrySet()) { + MethodDescriptor md = new MethodDescriptor(); + md.name = entry.getKey(); + md.type = BaseJavaModule.METHOD_TYPE_SYNC_HOOK; + + BaseJavaModule.SyncJavaHook method = (BaseJavaModule.SyncJavaHook) entry.getValue(); + md.method = method.getMethod(); + md.signature = method.getSignature(); + + descs.add(md); + } + + return descs; + } + + // TODO mhorowitz: make this return NativeMap, which requires moving + // NativeMap out of OnLoad. + @DoNotStrip + public NativeArray getConstants() { + WritableNativeArray array = new WritableNativeArray(); + array.pushMap(Arguments.makeNativeMap(mModule.getConstants())); + return array; + } + + @DoNotStrip + public boolean supportsWebWorkers() { + return mModule.supportsWebWorkers(); + } + + @DoNotStrip + public void invoke(ExecutorToken token, int methodId, ReadableNativeArray parameters) { + if (mMethods == null || methodId >= mMethods.size()) { + return; + } + + mMethods.get(methodId).invoke(mCatalystInstance, token, parameters); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaScriptExecutor.java new file mode 100644 index 00000000000000..7f15732406c9bb --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/JavaScriptExecutor.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.cxxbridge; + +import com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; + +@DoNotStrip +public abstract class JavaScriptExecutor { + public interface Factory { + JavaScriptExecutor create() throws Exception; + } + + private final HybridData mHybridData; + + protected JavaScriptExecutor(HybridData hybridData) { + mHybridData = hybridData; + } + + /** + * Close this executor and cleanup any resources that it was using. No further calls are + * expected after this. + * TODO mhorowitz: This may no longer be used; check and delete if possible. + */ + public void close() { + mHybridData.resetNative(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ModuleRegistryHolder.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ModuleRegistryHolder.java new file mode 100644 index 00000000000000..da271d27ad7eb0 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ModuleRegistryHolder.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.cxxbridge; + +import java.util.Collection; + +import com.facebook.jni.HybridData; + +public class ModuleRegistryHolder { + private final HybridData mHybridData; + private static native HybridData initHybrid( + CatalystInstanceImpl catalystInstanceImpl, + Collection javaModules, + Collection cxxModules); + + public ModuleRegistryHolder(CatalystInstanceImpl catalystInstanceImpl, + Collection javaModules, + Collection cxxModules) { + mHybridData = initHybrid(catalystInstanceImpl, javaModules, cxxModules); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java new file mode 100644 index 00000000000000..c8030aa8146de4 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/NativeModuleRegistry.java @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.cxxbridge; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nullable; + +import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.OnBatchCompleteListener; +import com.facebook.react.bridge.ReadableNativeArray; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.common.SetBuilder; +import com.facebook.infer.annotation.Assertions; +import com.facebook.systrace.Systrace; + +/** + * A set of Java APIs to expose to a particular JavaScript instance. + */ +public class NativeModuleRegistry { + private final Map, NativeModule> mModuleInstances; + private final ArrayList mBatchCompleteListenerModules; + + private NativeModuleRegistry(Map, NativeModule> moduleInstances) { + mModuleInstances = moduleInstances; + mBatchCompleteListenerModules = new ArrayList(mModuleInstances.size()); + for (NativeModule module : mModuleInstances.values()) { + if (module instanceof OnBatchCompleteListener) { + mBatchCompleteListenerModules.add((OnBatchCompleteListener) module); + } + } + } + + /* package */ ModuleRegistryHolder getModuleRegistryHolder( + CatalystInstanceImpl catalystInstanceImpl) { + ArrayList javaModules = new ArrayList<>(); + ArrayList cxxModules = new ArrayList<>(); + for (NativeModule module : mModuleInstances.values()) { + if (module instanceof BaseJavaModule) { + javaModules.add(new JavaModuleWrapper(catalystInstanceImpl, (BaseJavaModule) module)); + } else if (module instanceof CxxModuleWrapper) { + cxxModules.add((CxxModuleWrapper) module); + } else { + throw new IllegalArgumentException("Unknown module type " + module.getClass()); + } + } + return new ModuleRegistryHolder(catalystInstanceImpl, javaModules, cxxModules); + } + + /* package */ void notifyCatalystInstanceDestroy() { + UiThreadUtil.assertOnUiThread(); + Systrace.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "NativeModuleRegistry_notifyCatalystInstanceDestroy"); + try { + for (NativeModule nativeModule : mModuleInstances.values()) { + nativeModule.onCatalystInstanceDestroy(); + } + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + } + } + + /* package */ void notifyCatalystInstanceInitialized() { + UiThreadUtil.assertOnUiThread(); + + ReactMarker.logMarker("NativeModule_start"); + Systrace.beginSection( + Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + "NativeModuleRegistry_notifyCatalystInstanceInitialized"); + try { + for (NativeModule nativeModule : mModuleInstances.values()) { + nativeModule.initialize(); + } + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + ReactMarker.logMarker("NativeModule_end"); + } + } + + public void onBatchComplete() { + for (int i = 0; i < mBatchCompleteListenerModules.size(); i++) { + mBatchCompleteListenerModules.get(i).onBatchComplete(); + } + } + + public boolean hasModule(Class moduleInterface) { + return mModuleInstances.containsKey(moduleInterface); + } + + public T getModule(Class moduleInterface) { + return (T) Assertions.assertNotNull(mModuleInstances.get(moduleInterface)); + } + + public Collection getAllModules() { + return mModuleInstances.values(); + } + + public static class Builder { + private final HashMap mModules = MapBuilder.newHashMap(); + + public Builder add(NativeModule module) { + NativeModule existing = mModules.get(module.getName()); + if (existing != null && !module.canOverrideExistingModule()) { + throw new IllegalStateException("Native module " + module.getClass().getSimpleName() + + " tried to override " + existing.getClass().getSimpleName() + " for module name " + + module.getName() + ". If this was your intention, return true from " + + module.getClass().getSimpleName() + "#canOverrideExistingModule()"); + } + mModules.put(module.getName(), module); + return this; + } + + public NativeModuleRegistry build() { + Map, NativeModule> moduleInstances = new HashMap<>(); + for (NativeModule module : mModules.values()) { + moduleInstances.put((Class)module.getClass(), module); + } + return new NativeModuleRegistry(moduleInstances); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ProxyJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ProxyJavaScriptExecutor.java new file mode 100644 index 00000000000000..48fd281e304ac2 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ProxyJavaScriptExecutor.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.cxxbridge; + +import javax.annotation.Nullable; + +import com.facebook.jni.HybridData; +import com.facebook.soloader.SoLoader; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.JavaJSExecutor; + +/** + * JavaScript executor that delegates JS calls processed by native code back to a java version + * of the native executor interface. + * + * When set as a executor with {@link CatalystInstance.Builder}, catalyst native code will delegate + * low level javascript calls to the implementation of {@link JavaJSExecutor} interface provided + * with the constructor of this class. + */ +@DoNotStrip +public class ProxyJavaScriptExecutor extends JavaScriptExecutor { + public static class Factory implements JavaScriptExecutor.Factory { + private final JavaJSExecutor.Factory mJavaJSExecutorFactory; + + public Factory(JavaJSExecutor.Factory javaJSExecutorFactory) { + mJavaJSExecutorFactory = javaJSExecutorFactory; + } + + @Override + public JavaScriptExecutor create() throws Exception { + return new ProxyJavaScriptExecutor(mJavaJSExecutorFactory.create()); + } + } + + static { + SoLoader.loadLibrary(CatalystInstanceImpl.REACT_NATIVE_LIB); + } + + private @Nullable JavaJSExecutor mJavaJSExecutor; + + /** + * Create {@link ProxyJavaScriptExecutor} instance + * @param executor implementation of {@link JavaJSExecutor} which will be responsible for handling + * javascript calls + */ + public ProxyJavaScriptExecutor(JavaJSExecutor executor) { + super(initHybrid(executor)); + mJavaJSExecutor = executor; + } + + @Override + public void close() { + if (mJavaJSExecutor != null) { + mJavaJSExecutor.close(); + mJavaJSExecutor = null; + } + } + + private native static HybridData initHybrid(JavaJSExecutor executor); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactCallback.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactCallback.java new file mode 100644 index 00000000000000..6eb743dbe3035d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactCallback.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.cxxbridge; + +import com.facebook.proguard.annotations.DoNotStrip; + +@DoNotStrip +/* package */ interface ReactCallback { + @DoNotStrip + void onBatchComplete(); + + @DoNotStrip + void incrementPendingJSCalls(); + + @DoNotStrip + void decrementPendingJSCalls(); + + @DoNotStrip + void onNativeException(Exception e); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactMarker.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactMarker.java new file mode 100644 index 00000000000000..3d742f73629c54 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/ReactMarker.java @@ -0,0 +1,31 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.cxxbridge; + +import javax.annotation.Nullable; +import com.facebook.proguard.annotations.DoNotStrip; +/** + * Static class that allows markers to be placed in React code and responded to in a + * configurable way + */ +@DoNotStrip +public class ReactMarker { + + public interface MarkerListener { + void logMarker(String name); + }; + + @Nullable static private MarkerListener sMarkerListener = null; + + static public void setMarkerListener(MarkerListener listener) { + sMarkerListener = listener; + } + + @DoNotStrip + static public void logMarker(String name) { + if (sMarkerListener != null) { + sMarkerListener.logMarker(name); + } + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/SoftAssertions.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/SoftAssertions.java new file mode 100644 index 00000000000000..8809540a08595a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/SoftAssertions.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.cxxbridge; + +import javax.annotation.Nullable; + +import com.facebook.react.bridge.AssertionException; + +/** + * Utility class to make assertions that should not hard-crash the app but instead be handled by the + * Catalyst app {@link NativeModuleCallExceptionHandler}. See the javadoc on that class for + * more information about our opinion on when these assertions should be used as opposed to + * assertions that might throw AssertionError Throwables that will cause the app to hard crash. + */ +public class SoftAssertions { + + /** + * Throw {@link AssertionException} with a given message. Use this method surrounded with + * {@code if} block with assert condition in case you plan to do string concatenation to produce + * the message. + */ + public static void assertUnreachable(String message) { + throw new AssertionException(message); + } + + /** + * Asserts the given condition, throwing an {@link AssertionException} if the condition doesn't + * hold. + */ + public static void assertCondition(boolean condition, String message) { + if (!condition) { + throw new AssertionException(message); + } + } + + /** + * Asserts that the given Object isn't null, throwing an {@link AssertionException} if it was. + */ + public static T assertNotNull(@Nullable T instance) { + if (instance == null) { + throw new AssertionException("Expected object to not be null!"); + } + return instance; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/UiThreadUtil.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/UiThreadUtil.java new file mode 100644 index 00000000000000..d8624474bc76c3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/UiThreadUtil.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.cxxbridge; + +import javax.annotation.Nullable; + +import android.os.Handler; +import android.os.Looper; + +/** + * Utility for interacting with the UI thread. + */ +public class UiThreadUtil { + + @Nullable private static Handler sMainHandler; + + /** + * @return {@code true} if the current thread is the UI thread. + */ + public static boolean isOnUiThread() { + return Looper.getMainLooper().getThread() == Thread.currentThread(); + } + + /** + * Throws an {@link AssertionException} if the current thread is not the UI thread. + */ + public static void assertOnUiThread() { + SoftAssertions.assertCondition(isOnUiThread(), "Expected to run on UI thread!"); + } + + /** + * Throws an {@link AssertionException} if the current thread is the UI thread. + */ + public static void assertNotOnUiThread() { + SoftAssertions.assertCondition(!isOnUiThread(), "Expected not to run on UI thread!"); + } + + /** + * Runs the given {@code Runnable} on the UI thread. + */ + public static void runOnUiThread(Runnable runnable) { + synchronized (UiThreadUtil.class) { + if (sMainHandler == null) { + sMainHandler = new Handler(Looper.getMainLooper()); + } + } + sMainHandler.post(runnable); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/bridge.pro b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/bridge.pro new file mode 100644 index 00000000000000..a03ce0287e5e84 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/bridge.pro @@ -0,0 +1,7 @@ +## Putting this here is kind of a hack. I don't want to modify the OSS bridge. +## TODO mhorowitz: add @DoNotStrip to the interface directly. + +-keepclassmembers class com.facebook.react.bridge.queue.MessageQueueThread { + public boolean isOnThread(); + public void assertIsOnThread(); +} From 9150cc5f967598687fa21473f1fe3c1b51f8eb2f Mon Sep 17 00:00:00 2001 From: YuTin Liu Date: Wed, 25 May 2016 19:07:52 -0700 Subject: [PATCH 118/843] add AirPoPo to showcase Summary: as title, thank you very much. Closes https://github.com/facebook/react-native/pull/7747 Differential Revision: D3351212 fbshipit-source-id: 06622222cc198c9558222fadc152611ee77970ac --- website/src/react-native/showcase.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/website/src/react-native/showcase.js b/website/src/react-native/showcase.js index a61ecf4021aa40..b85d86eeea820c 100644 --- a/website/src/react-native/showcase.js +++ b/website/src/react-native/showcase.js @@ -221,6 +221,13 @@ var apps = [ link: 'https://itunes.apple.com/us/app/accio-on-demand-delivery/id1047060673?mt=8', author: 'Accio Delivery Inc.', }, + { + name: 'AirPoPo', + icon: 'http://a1.mzstatic.com/us/r30/Purple49/v4/47/1a/07/471a07e1-50d9-a432-060b-76f32df8c345/icon175x175.jpeg', + linkAppStore: 'https://itunes.apple.com/us/app/airpopo/id1100540816', + linkPlayStore: 'https://play.google.com/store/apps/details?id=com.airpopo.client', + author: 'DingTaxi', + }, { name: 'ArcChat.com', icon: 'https://lh3.googleusercontent.com/mZJjidMobu3NAZApdtp-vdBBzIWzCNTaIcKShbGqwXRRzL3B9bbi6E0eRuykgT6vmg=w300-rw', From a4f1857bf5c01d8900414359716b44d6992329b3 Mon Sep 17 00:00:00 2001 From: strazi Date: Wed, 25 May 2016 20:39:22 -0700 Subject: [PATCH 119/843] Adding Readzi application to showcase. Summary: Adding my application Readzi to the showcase. Closes https://github.com/facebook/react-native/pull/7721 Differential Revision: D3349649 fbshipit-source-id: 8d21943e71163e545d7e1bb683e0733250290c01 --- website/src/react-native/showcase.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/website/src/react-native/showcase.js b/website/src/react-native/showcase.js index b85d86eeea820c..8ec9ea8614ec3f 100644 --- a/website/src/react-native/showcase.js +++ b/website/src/react-native/showcase.js @@ -761,6 +761,15 @@ var apps = [ 'http://richard-cao.github.io/2016/02/06/Reading-App-Write-In-React-Native/', ], }, + { + name: 'Readzi', + icon: 'http://a3.mzstatic.com/us/r30/Purple18/v4/b9/32/99/b9329992-1677-9ee2-5d04-e901e4bbb2b7/icon175x175.png', + link: 'https://readzi.io', + author: 'Kevin Kennedy', + blogs: [ + 'https://strazi.org/journal/building-readzi/', + ], + }, { name: 'RenovationFind', icon: 'http://a2.mzstatic.com/us/r30/Purple3/v4/4f/89/af/4f89af72-9733-2f59-6876-161983a0ee82/icon175x175.png', From 4879f88a75204588fbe461ab43f8cd50a09dd707 Mon Sep 17 00:00:00 2001 From: taelimoh Date: Thu, 26 May 2016 01:37:09 -0700 Subject: [PATCH 120/843] change undeclared variable to intended value Summary: Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: (You can skip this if you're fixing a typo or adding an app to the Showcase.) Explain the **motivation** for making this change. What existing problem does the pull request solve? Prefer **small pull requests**. These are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise please split it. **Test plan (required)** Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. Make sure tests pass on both Travis and Circle CI. **Code formatting** Look around. Match the style of the rest of the codebase. See also the simple [style guide](https://github.com/facebook/react-native/blob/master/CONTRIBUTING.md#style-guide). For more info, see the ["Pull Requests" section of our "Contributing" guidelines](https://github.com/facebook/react-native/blob/mas Closes https://github.com/facebook/react-native/pull/7770 Differential Revision: D3352007 fbshipit-source-id: eedb964d245445b61fed79245380f0803473c455 --- Libraries/Storage/AsyncStorage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Storage/AsyncStorage.js b/Libraries/Storage/AsyncStorage.js index 48b9f59bc39469..01d76748d86869 100644 --- a/Libraries/Storage/AsyncStorage.js +++ b/Libraries/Storage/AsyncStorage.js @@ -122,7 +122,7 @@ var AsyncStorage = { * traits: {eyes: 'blue', shoe_size: 10} * }; - * AsyncStorage.setItem(store_key, JSON.stringify(UID123_object), () => { + * AsyncStorage.setItem('UID123', JSON.stringify(UID123_object), () => { * AsyncStorage.mergeItem('UID123', JSON.stringify(UID123_delta), () => { * AsyncStorage.getItem('UID123', (err, result) => { * console.log(result); From 0fb5ccf6afa8ac0ff599ce1a9f625d96d19edec8 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 26 May 2016 05:20:51 -0700 Subject: [PATCH 121/843] Add support for delete animation in LayoutAnimation on Android Summary: Android follow up to #6779 **Test plan** Tested add/removing views in the UIExample explorer with and without setting a LayoutAnimation. Tested that user interation during the animation is properly disabled. ![layout-anim-2](https://cloud.githubusercontent.com/assets/2677334/14760549/d60ebe2a-0914-11e6-8f17-ea04d8bf813b.gif) Closes https://github.com/facebook/react-native/pull/7171 Differential Revision: D3352450 Pulled By: astreet fbshipit-source-id: 233efa041626eb26d99511d12a924e54a10f96cc --- Examples/UIExplorer/UIExplorerApp.android.js | 3 + .../uimanager/NativeViewHierarchyManager.java | 41 ++++++++++-- .../react/uimanager/ViewGroupManager.java | 9 +++ .../LayoutAnimationController.java | 67 +++++++++++++++++-- .../LayoutAnimationListener.java | 10 +++ .../layoutanimation/LayoutAnimationType.java | 3 +- .../LayoutDeleteAnimation.java | 15 +++++ 7 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationListener.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutDeleteAnimation.java diff --git a/Examples/UIExplorer/UIExplorerApp.android.js b/Examples/UIExplorer/UIExplorerApp.android.js index 3553b812da37d3..bd9f03ab47d2de 100644 --- a/Examples/UIExplorer/UIExplorerApp.android.js +++ b/Examples/UIExplorer/UIExplorerApp.android.js @@ -37,9 +37,12 @@ const UIExplorerExampleList = require('./UIExplorerExampleList'); const UIExplorerList = require('./UIExplorerList'); const UIExplorerNavigationReducer = require('./UIExplorerNavigationReducer'); const UIExplorerStateTitleMap = require('./UIExplorerStateTitleMap'); +const UIManager = require('UIManager'); const URIActionMap = require('./URIActionMap'); const View = require('View'); +UIManager.setLayoutAnimationEnabledExperimental(true); + const DRAWER_WIDTH_LEFT = 56; type Props = { diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index eeeb4c11ed8dd3..36ab83c8fa871f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -35,6 +35,7 @@ import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.touch.JSResponderHandler; import com.facebook.react.uimanager.layoutanimation.LayoutAnimationController; +import com.facebook.react.uimanager.layoutanimation.LayoutAnimationListener; import com.facebook.systrace.Systrace; import com.facebook.systrace.SystraceMessage; @@ -294,8 +295,8 @@ public void manageChildren( @Nullable int[] indicesToRemove, @Nullable ViewAtIndex[] viewsToAdd, @Nullable int[] tagsToDelete) { - ViewGroup viewToManage = (ViewGroup) mTagsToViews.get(tag); - ViewGroupManager viewManager = (ViewGroupManager) resolveViewManager(tag); + final ViewGroup viewToManage = (ViewGroup) mTagsToViews.get(tag); + final ViewGroupManager viewManager = (ViewGroupManager) resolveViewManager(tag); if (viewToManage == null) { throw new IllegalViewOperationException("Trying to manageChildren view with tag " + tag + " which doesn't exist\n detail: " + @@ -344,7 +345,17 @@ public void manageChildren( viewsToAdd, tagsToDelete)); } - viewManager.removeViewAt(viewToManage, indicesToRemove[i]); + + View viewToRemove = viewToManage.getChildAt(indexToRemove); + + if (mLayoutAnimator.shouldAnimateLayout(viewToRemove) && + arrayContains(tagsToDelete, viewToRemove.getId())) { + // The view will be removed and dropped by the 'delete' layout animation + // instead, so do nothing + } else { + viewManager.removeViewAt(viewToManage, indexToRemove); + } + lastIndexToRemove = indexToRemove; } } @@ -371,7 +382,7 @@ public void manageChildren( if (tagsToDelete != null) { for (int i = 0; i < tagsToDelete.length; i++) { int tagToDelete = tagsToDelete[i]; - View viewToDestroy = mTagsToViews.get(tagToDelete); + final View viewToDestroy = mTagsToViews.get(tagToDelete); if (viewToDestroy == null) { throw new IllegalViewOperationException( "Trying to destroy unknown view tag: " @@ -383,9 +394,29 @@ public void manageChildren( viewsToAdd, tagsToDelete)); } - dropView(viewToDestroy); + + if (mLayoutAnimator.shouldAnimateLayout(viewToDestroy)) { + mLayoutAnimator.deleteView(viewToDestroy, new LayoutAnimationListener() { + @Override + public void onAnimationEnd() { + viewManager.removeView(viewToManage, viewToDestroy); + dropView(viewToDestroy); + } + }); + } else { + dropView(viewToDestroy); + } + } + } + } + + private boolean arrayContains(int[] array, int ele) { + for (int curEle : array) { + if (curEle == ele) { + return true; } } + return false; } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java index 153f7b815f8275..e91be0e20f9ead 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java @@ -48,6 +48,15 @@ public void removeViewAt(T parent, int index) { parent.removeViewAt(index); } + public void removeView(T parent, View view) { + for (int i = 0; i < getChildCount(parent); i++) { + if (getChildAt(parent, i) == view) { + removeViewAt(parent, i); + break; + } + } + } + public void removeAllViews(T parent) { for (int i = getChildCount(parent) - 1; i >= 0; i--) { removeViewAt(parent, i); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java index d3522947fd36e1..d5a08e6b264ac5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java @@ -6,6 +6,7 @@ import javax.annotation.concurrent.NotThreadSafe; import android.view.View; +import android.view.ViewGroup; import android.view.animation.Animation; import com.facebook.react.bridge.ReadableMap; @@ -25,6 +26,7 @@ public class LayoutAnimationController { private final AbstractLayoutAnimation mLayoutCreateAnimation = new LayoutCreateAnimation(); private final AbstractLayoutAnimation mLayoutUpdateAnimation = new LayoutUpdateAnimation(); + private final AbstractLayoutAnimation mLayoutDeleteAnimation = new LayoutDeleteAnimation(); private boolean mShouldAnimateLayout; public void initializeFromConfig(final @Nullable ReadableMap config) { @@ -49,11 +51,17 @@ public void initializeFromConfig(final @Nullable ReadableMap config) { config.getMap(LayoutAnimationType.UPDATE.toString()), globalDuration); mShouldAnimateLayout = true; } + if (config.hasKey(LayoutAnimationType.DELETE.toString())) { + mLayoutDeleteAnimation.initializeFromConfig( + config.getMap(LayoutAnimationType.DELETE.toString()), globalDuration); + mShouldAnimateLayout = true; + } } public void reset() { mLayoutCreateAnimation.reset(); mLayoutUpdateAnimation.reset(); + mLayoutDeleteAnimation.reset(); mShouldAnimateLayout = false; } @@ -65,7 +73,8 @@ public boolean shouldAnimateLayout(View viewToAnimate) { /** * Update layout of given view, via immediate update or animation depending on the current batch - * layout animation configuration supplied during initialization. + * layout animation configuration supplied during initialization. Handles create and update + * animations. * * @param view the view to update layout of * @param x the new X position for the view @@ -76,9 +85,9 @@ public boolean shouldAnimateLayout(View viewToAnimate) { public void applyLayoutUpdate(View view, int x, int y, int width, int height) { UiThreadUtil.assertOnUiThread(); - // Determine which animation to use : if view is initially invisible, use create animation. - // If view is becoming invisible, use delete animation. Otherwise, use update animation. - // This approach is easier than maintaining a list of tags for recently created/deleted views. + // Determine which animation to use : if view is initially invisible, use create animation, + // otherwise use update animation. This approach is easier than maintaining a list of tags + // for recently created views. AbstractLayoutAnimation layoutAnimation = (view.getWidth() == 0 || view.getHeight() == 0) ? mLayoutCreateAnimation : mLayoutUpdateAnimation; @@ -91,4 +100,54 @@ public void applyLayoutUpdate(View view, int x, int y, int width, int height) { view.startAnimation(animation); } } + + /** + * Animate a view deletion using the layout animation configuration supplied during initialization. + * + * @param view The view to animate. + * @param listener Called once the animation is finished, should be used to + * completely remove the view. + */ + public void deleteView(final View view, final LayoutAnimationListener listener) { + UiThreadUtil.assertOnUiThread(); + + AbstractLayoutAnimation layoutAnimation = mLayoutDeleteAnimation; + + Animation animation = layoutAnimation.createAnimation( + view, view.getLeft(), view.getTop(), view.getWidth(), view.getHeight()); + + if (animation != null) { + disableUserInteractions(view); + + animation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation anim) {} + + @Override + public void onAnimationRepeat(Animation anim) {} + + @Override + public void onAnimationEnd(Animation anim) { + listener.onAnimationEnd(); + } + }); + + view.startAnimation(animation); + } else { + listener.onAnimationEnd(); + } + } + + /** + * Disables user interactions for a view and all it's subviews. + */ + private void disableUserInteractions(View view) { + view.setClickable(false); + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup)view; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + disableUserInteractions(viewGroup.getChildAt(i)); + } + } + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationListener.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationListener.java new file mode 100644 index 00000000000000..7edd4251d4cb46 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationListener.java @@ -0,0 +1,10 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.uimanager.layoutanimation; + +/** + * Listener invoked when a layout animation has completed. + */ +public interface LayoutAnimationListener { + void onAnimationEnd(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationType.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationType.java index 0f3d9d7fdb2b99..a21c0d49cd137e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationType.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationType.java @@ -7,7 +7,8 @@ */ /* package */ enum LayoutAnimationType { CREATE("create"), - UPDATE("update"); + UPDATE("update"), + DELETE("delete"); private final String mName; diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutDeleteAnimation.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutDeleteAnimation.java new file mode 100644 index 00000000000000..f2c3ccb2c2e387 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutDeleteAnimation.java @@ -0,0 +1,15 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.uimanager.layoutanimation; + +/** + * Class responsible for handling layout view deletion animation, applied to view whenever a + * valid config was supplied for the layout animation of DELETE type. + */ +/* package */ class LayoutDeleteAnimation extends BaseLayoutAnimation { + + @Override + boolean isReverse() { + return true; + } +} From 45636ed7f4bb584f650cd1a3c4d6c6463bcab633 Mon Sep 17 00:00:00 2001 From: Alexander Blom Date: Thu, 26 May 2016 06:23:17 -0700 Subject: [PATCH 122/843] Add more tracing to startup Reviewed By: astreet Differential Revision: D3352462 fbshipit-source-id: 9f10bb40eef9a262ae3ea6f8b2cd27b5774fc4da --- .../react/XReactInstanceManagerImpl.java | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java index 0cc29def3a9ca2..57601d12f5def0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java @@ -78,6 +78,7 @@ import static com.facebook.react.bridge.ReactMarkerConstants.PROCESS_PACKAGES_START; import static com.facebook.react.bridge.ReactMarkerConstants.RUN_JS_BUNDLE_END; import static com.facebook.react.bridge.ReactMarkerConstants.RUN_JS_BUNDLE_START; +import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; /** * This class is managing instances of {@link CatalystInstance}. It expose a way to configure @@ -612,7 +613,7 @@ public void detachRootView(ReactRootView rootView) { @Override public List createAllViewManagers( ReactApplicationContext catalystApplicationContext) { - Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "createAllViewManagers"); + Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "createAllViewManagers"); try { List allViewManagers = new ArrayList<>(); for (ReactPackage reactPackage : mPackages) { @@ -620,7 +621,7 @@ public List createAllViewManagers( } return allViewManagers; } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); } } @@ -681,6 +682,7 @@ private void recreateReactContextInBackground( } private void setupReactContext(ReactApplicationContext reactContext) { + Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "setupReactContext"); UiThreadUtil.assertOnUiThread(); Assertions.assertCondition(mCurrentReactContext == null); mCurrentReactContext = Assertions.assertNotNull(reactContext); @@ -703,11 +705,13 @@ private void setupReactContext(ReactApplicationContext reactContext) { for (ReactInstanceEventListener listener : listeners) { listener.onReactContextInitialized(reactContext); } + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); } private void attachMeasuredRootViewToInstance( ReactRootView rootView, CatalystInstance catalystInstance) { + Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "attachMeasuredRootViewToInstance"); UiThreadUtil.assertOnUiThread(); // Reset view content as it's going to be populated by the application content from JS @@ -724,6 +728,7 @@ private void attachMeasuredRootViewToInstance( appParams.putDouble("rootTag", rootTag); appParams.putMap("initialProps", initialProps); catalystInstance.getJSModule(AppRegistry.class).runApplication(jsAppModuleName, appParams); + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); } private void detachViewFromInstance( @@ -766,36 +771,36 @@ private ReactApplicationContext createReactContext( ReactMarker.logMarker(PROCESS_PACKAGES_START); Systrace.beginSection( - Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + TRACE_TAG_REACT_JAVA_BRIDGE, "createAndProcessCoreModulesPackage"); try { CoreModulesPackage coreModulesPackage = new CoreModulesPackage(this, mBackBtnHandler, mUIImplementationProvider); processPackage(coreModulesPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); } // TODO(6818138): Solve use-case of native/js modules overriding for (ReactPackage reactPackage : mPackages) { Systrace.beginSection( - Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, + TRACE_TAG_REACT_JAVA_BRIDGE, "createAndProcessCustomReactPackage"); try { processPackage(reactPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); } } ReactMarker.logMarker(PROCESS_PACKAGES_END); ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_START); - Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "buildNativeModuleRegistry"); + Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "buildNativeModuleRegistry"); NativeModuleRegistry nativeModuleRegistry; try { nativeModuleRegistry = nativeRegistryBuilder.build(); } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_END); } @@ -812,12 +817,12 @@ private ReactApplicationContext createReactContext( ReactMarker.logMarker(CREATE_CATALYST_INSTANCE_START); // CREATE_CATALYST_INSTANCE_END is in JSCExecutor.cpp - Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "createCatalystInstance"); + Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "createCatalystInstance"); final CatalystInstance catalystInstance; try { catalystInstance = catalystInstanceBuilder.build(); } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); ReactMarker.logMarker(CREATE_CATALYST_INSTANCE_END); } @@ -831,11 +836,11 @@ private ReactApplicationContext createReactContext( catalystInstance.getReactQueueConfiguration().getJSQueueThread().runOnQueue(new Runnable() { @Override public void run() { - Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "runJSBundle"); + Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "runJSBundle"); try { catalystInstance.runJSBundle(); } finally { - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); ReactMarker.logMarker(RUN_JS_BUNDLE_END); } } From 0bbfe79623c97d349586266885b5af58137d771f Mon Sep 17 00:00:00 2001 From: Emil Sjolander Date: Thu, 26 May 2016 06:57:35 -0700 Subject: [PATCH 123/843] Set a size of the root view before running test Reviewed By: astreet Differential Revision: D3352500 fbshipit-source-id: e6bb81601448a08fa216865b5b92f8739646fc05 --- .../java/com/facebook/react/views/textinput/TextInputTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/textinput/TextInputTest.java b/ReactAndroid/src/test/java/com/facebook/react/views/textinput/TextInputTest.java index cd5243beff7eb5..97fb0c3f570a1c 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/views/textinput/TextInputTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/views/textinput/TextInputTest.java @@ -90,6 +90,7 @@ public void testPropsApplied() { UIManagerModule uiManager = getUIManagerModule(); ReactRootView rootView = new ReactRootView(RuntimeEnvironment.application); + rootView.setLayoutParams(new ReactRootView.LayoutParams(100, 100)); int rootTag = uiManager.addMeasuredRootView(rootView); int textInputTag = rootTag + 1; final String hintStr = "placeholder text"; @@ -123,6 +124,7 @@ public void testPropsUpdate() { UIManagerModule uiManager = getUIManagerModule(); ReactRootView rootView = new ReactRootView(RuntimeEnvironment.application); + rootView.setLayoutParams(new ReactRootView.LayoutParams(100, 100)); int rootTag = uiManager.addMeasuredRootView(rootView); int textInputTag = rootTag + 1; final String hintStr = "placeholder text"; From 26aa27da630a8875dfe64f616b6aca59eaa89aaf Mon Sep 17 00:00:00 2001 From: Adam Comella Date: Thu, 26 May 2016 07:20:50 -0700 Subject: [PATCH 124/843] Fix TextInput autocorrect (#7496) Summary: Autocorrect was broken for controlled TextInput components by a change to batch event handling in React Native: https://github.com/facebook/react/commit/9f11f8c2634921e657df2691e6bfda18fead5bcc For example, a TextInput like this would be affected by this bug: ```javascript this.setState({ text })} value={this.state.text} /> ``` This fix uses the same approach as https://github.com/facebook/react-native/commit/0cd2904b235f53ed684bb9898280461e2cee0b5b The problem is that TextInput's _onChange handler relied on this.props.value being updated synchronously when calling this.props.onChangeText(text). However, this assumption was broken when React Native event handling started being batched. The fix is to move the code that relies on this.props.value being up-to-date to componentDidUpdate. **Test plan (required)** Tested autocorrect now works on iOS in a small app and a large app. Also tested t Closes https://github.com/facebook/react-native/pull/7676 Differential Revision: D3346221 Pulled By: nicklockwood fbshipit-source-id: 715df3e8a03aa58cb0a462de4add02289d42782f --- Libraries/Components/TextInput/TextInput.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index bbb665f60d4106..00e928bb9d02b2 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -366,8 +366,10 @@ var TextInput = React.createClass({ }, _focusSubscription: (undefined: ?Function), + _lastNativeText: (undefined: ?string), componentDidMount: function() { + this._lastNativeText = this.props.value; if (!this.context.focusEmitter) { if (this.props.autoFocus) { this.requestAnimationFrame(this.focus); @@ -613,10 +615,15 @@ var TextInput = React.createClass({ return; } + this._lastNativeText = text; + this.forceUpdate(); + }, + + componentDidUpdate: function () { // This is necessary in case native updates the text and JS decides // that the update should be ignored and we should stick with the value // that we have in JS. - if (text !== this.props.value && typeof this.props.value === 'string') { + if (this._lastNativeText !== this.props.value && typeof this.props.value === 'string') { this.refs.input.setNativeProps({ text: this.props.value, }); From be34e046c2ef70c1141f74693044b1bc0a5667e8 Mon Sep 17 00:00:00 2001 From: Nathan Azaria Date: Thu, 26 May 2016 09:27:18 -0700 Subject: [PATCH 125/843] Fixed broken link in Animations documentation Reviewed By: bestander Differential Revision: D3346075 fbshipit-source-id: 3dc066af8ea1ada180e7c7f33108b148f17d73f2 --- docs/Animations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Animations.md b/docs/Animations.md index eed529fef448d7..5eff3d3c53f6e8 100644 --- a/docs/Animations.md +++ b/docs/Animations.md @@ -323,7 +323,7 @@ animations that underlies all of the JavaScript-based animation APIs. In general, you shouldn't need to call this yourself - the animation APIs will manage frame updates for you. -### react-tween-state (Not recommended - use [Animated](#animated) instead) +### react-tween-state (Not recommended - use [Animated](docs/animations.html#animated) instead) [react-tween-state](https://github.com/chenglou/react-tween-state) is a minimal library that does exactly what its name suggests: it *tweens* a @@ -393,7 +393,7 @@ Here we animated the opacity, but as you might guess, we can animate any numeric value. Read more about react-tween-state in its [README](https://github.com/chenglou/react-tween-state). -### Rebound (Not recommended - use [Animated](docs/animation.html) instead) +### Rebound (Not recommended - use [Animated](docs/animations.html#animated) instead) [Rebound.js](https://github.com/facebook/rebound-js) is a JavaScript port of [Rebound for Android](https://github.com/facebook/rebound). It is From bdab83403683a2738af452c424d7d5f29acc97fe Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Thu, 26 May 2016 10:43:51 -0700 Subject: [PATCH 126/843] Improve how inspector handles native components Reviewed By: sebmarkbage Differential Revision: D3347768 fbshipit-source-id: 221ec54dc7bf9513a76578d90a272ed41fe189f9 --- Libraries/Inspector/ElementProperties.js | 12 +++++++++++- Libraries/Inspector/Inspector.js | 12 +++++++++--- Libraries/Inspector/InspectorUtils.js | 12 +++++++++++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Libraries/Inspector/ElementProperties.js b/Libraries/Inspector/ElementProperties.js index bbbf248407c304..c0b11a3882e62b 100644 --- a/Libraries/Inspector/ElementProperties.js +++ b/Libraries/Inspector/ElementProperties.js @@ -73,7 +73,7 @@ var ElementProperties = React.createClass({ style={[styles.breadItem, i === selection && styles.selected]} onPress={() => this.props.setSelection(i)}> - {item.getName ? item.getName() : 'Unknown'} + {getInstanceName(item)} ), @@ -107,6 +107,16 @@ var ElementProperties = React.createClass({ }, }); +function getInstanceName(instance) { + if (instance.getName) { + return instance.getName(); + } + if (instance.constructor && instance.constructor.displayName) { + return instance.constructor.displayName; + } + return 'Unknown'; +} + var styles = StyleSheet.create({ breadSep: { fontSize: 8, diff --git a/Libraries/Inspector/Inspector.js b/Libraries/Inspector/Inspector.js index 0f8276f5bc2268..3726597a9f050e 100644 --- a/Libraries/Inspector/Inspector.js +++ b/Libraries/Inspector/Inspector.js @@ -142,11 +142,17 @@ class Inspector extends React.Component { }); } - onTouchInstance(instance: Object, frame: Object, pointerY: number) { + onTouchInstance(touched: Object, frame: Object, pointerY: number) { + // Most likely the touched instance is a native wrapper (like RCTView) + // which is not very interesting. Most likely user wants a composite + // instance that contains it (like View) + var hierarchy = InspectorUtils.getOwnerHierarchy(touched); + var instance = InspectorUtils.lastNotNativeInstance(hierarchy); + if (this.state.devtoolsAgent) { this.state.devtoolsAgent.selectFromReactInstance(instance, true); } - var hierarchy = InspectorUtils.getOwnerHierarchy(instance); + // if we inspect a stateless component we can't use the getPublicInstance method // therefore we use the internal _instance property directly. var publicInstance = instance['_instance'] || {}; @@ -154,7 +160,7 @@ class Inspector extends React.Component { var source = instance['_currentElement'] && instance['_currentElement']['_source']; this.setState({ panelPos: pointerY > Dimensions.get('window').height / 2 ? 'top' : 'bottom', - selection: hierarchy.length - 1, + selection: hierarchy.indexOf(instance), hierarchy, inspected: { style: props.style || {}, diff --git a/Libraries/Inspector/InspectorUtils.js b/Libraries/Inspector/InspectorUtils.js index 17c03e30390062..75fb42bf4efab9 100644 --- a/Libraries/Inspector/InspectorUtils.js +++ b/Libraries/Inspector/InspectorUtils.js @@ -29,4 +29,14 @@ function getOwnerHierarchy(instance) { return hierarchy; } -module.exports = {findInstanceByNativeTag, getOwnerHierarchy}; +function lastNotNativeInstance(hierarchy) { + for (let i = hierarchy.length - 1; i > 1; i--) { + const instance = hierarchy[i]; + if (!instance.viewConfig) { + return instance; + } + } + return hierarchy[0]; +} + +module.exports = {findInstanceByNativeTag, getOwnerHierarchy, lastNotNativeInstance}; From f948662013bb96017b13cb4ae8bc480b2751f2ed Mon Sep 17 00:00:00 2001 From: Charles Dick Date: Thu, 26 May 2016 11:07:41 -0700 Subject: [PATCH 127/843] Drop JSC code on background Reviewed By: lexs Differential Revision: D3311037 fbshipit-source-id: e46559108c51f1cd163ed5c557d23c21f696ef88 --- .../facebook/react/MemoryPressureRouter.java | 10 +++++++++- .../facebook/react/bridge/MemoryPressure.java | 1 + .../com/facebook/react/bridge/ReactBridge.java | 4 ++++ .../react/cxxbridge/CatalystInstanceImpl.java | 18 ++++++++++++++++++ ReactAndroid/src/main/jni/react/Bridge.cpp | 6 ++++++ ReactAndroid/src/main/jni/react/Bridge.h | 1 + ReactAndroid/src/main/jni/react/Executor.h | 1 + .../src/main/jni/react/JSCExecutor.cpp | 6 ++++++ ReactAndroid/src/main/jni/react/JSCExecutor.h | 1 + ReactAndroid/src/main/jni/react/jni/OnLoad.cpp | 7 +++++++ .../jni/xreact/jni/CatalystInstanceImpl.cpp | 15 +++++++++++++++ .../main/jni/xreact/jni/CatalystInstanceImpl.h | 3 +++ ReactCommon/bridge/Executor.h | 1 + ReactCommon/bridge/Instance.cpp | 12 ++++++++++++ ReactCommon/bridge/Instance.h | 3 +++ ReactCommon/bridge/JSCExecutor.cpp | 6 ++++++ ReactCommon/bridge/JSCExecutor.h | 1 + ReactCommon/bridge/NativeToJsBridge.cpp | 6 ++++++ ReactCommon/bridge/NativeToJsBridge.h | 1 + 19 files changed, 102 insertions(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/MemoryPressureRouter.java b/ReactAndroid/src/main/java/com/facebook/react/MemoryPressureRouter.java index e9fd7574895f3b..7a17830a8c003e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/MemoryPressureRouter.java +++ b/ReactAndroid/src/main/java/com/facebook/react/MemoryPressureRouter.java @@ -20,6 +20,7 @@ import static android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE; import static android.content.ComponentCallbacks2.TRIM_MEMORY_MODERATE; import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL; +import static android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN; /** * Translates and routes memory pressure events to the current catalyst instance. @@ -27,6 +28,8 @@ public class MemoryPressureRouter { // Trigger this by sending an intent to your activity with adb shell: // am broadcast -a com.facebook.catalyst.ACTION_TRIM_MEMORY_MODERATE + private static final String ACTION_TRIM_MEMORY_UI_HIDDEN = + "com.facebook.rnfeed.ACTION_TRIM_MEMORY_UI_HIDDEN"; private static final String ACTION_TRIM_MEMORY_MODERATE = "com.facebook.rnfeed.ACTION_TRIM_MEMORY_MODERATE"; private static final String ACTION_TRIM_MEMORY_CRITICAL = @@ -52,6 +55,9 @@ public void onLowMemory() { @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public static boolean handleDebugIntent(Application application, String action) { switch (action) { + case ACTION_TRIM_MEMORY_UI_HIDDEN: + simulateTrimMemory(application, ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN); + break; case ACTION_TRIM_MEMORY_MODERATE: simulateTrimMemory(application, TRIM_MEMORY_MODERATE); break; @@ -87,10 +93,12 @@ public void destroy(Context context) { } private void trimMemory(int level) { - if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) { + if (level >= TRIM_MEMORY_COMPLETE) { dispatchMemoryPressure(MemoryPressure.CRITICAL); } else if (level >= TRIM_MEMORY_BACKGROUND || level == TRIM_MEMORY_RUNNING_CRITICAL) { dispatchMemoryPressure(MemoryPressure.MODERATE); + } else if (level == TRIM_MEMORY_UI_HIDDEN) { + dispatchMemoryPressure(MemoryPressure.UI_HIDDEN); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/MemoryPressure.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/MemoryPressure.java index c947782efe40c5..7412204e678baf 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/MemoryPressure.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/MemoryPressure.java @@ -3,6 +3,7 @@ package com.facebook.react.bridge; public enum MemoryPressure { + UI_HIDDEN, MODERATE, CRITICAL } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java index cbdebb4f38afd1..e4d43f90b6a4f9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java @@ -58,6 +58,9 @@ public void dispose() { public void handleMemoryPressure(MemoryPressure level) { switch (level) { + case UI_HIDDEN: + handleMemoryPressureUiHidden(); + break; case MODERATE: handleMemoryPressureModerate(); break; @@ -86,6 +89,7 @@ private native void initialize( public native void startProfiler(String title); public native void stopProfiler(String title, String filename); public native ExecutorToken getMainExecutorToken(); + private native void handleMemoryPressureUiHidden(); private native void handleMemoryPressureModerate(); private native void handleMemoryPressureCritical(); public native void destroy(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java index 3a5de280d1a071..753ef65a61f1dc 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java @@ -289,8 +289,26 @@ public Collection getNativeModules() { return mJavaRegistry.getAllModules(); } + private native void handleMemoryPressureUiHidden(); + private native void handleMemoryPressureModerate(); + private native void handleMemoryPressureCritical(); + @Override public void handleMemoryPressure(MemoryPressure level) { + if (mDestroyed) { + return; + } + switch(level) { + case UI_HIDDEN: + handleMemoryPressureUiHidden(); + break; + case MODERATE: + handleMemoryPressureModerate(); + break; + case CRITICAL: + handleMemoryPressureCritical(); + break; + } } /** diff --git a/ReactAndroid/src/main/jni/react/Bridge.cpp b/ReactAndroid/src/main/jni/react/Bridge.cpp index cf5dd566bc966d..ee5bcb16cc44f3 100644 --- a/ReactAndroid/src/main/jni/react/Bridge.cpp +++ b/ReactAndroid/src/main/jni/react/Bridge.cpp @@ -127,6 +127,12 @@ void Bridge::stopProfiler(const std::string& title, const std::string& filename) }); } +void Bridge::handleMemoryPressureUiHidden() { + runOnExecutorQueue(*m_mainExecutorToken, [=] (JSExecutor* executor) { + executor->handleMemoryPressureUiHidden(); + }); +} + void Bridge::handleMemoryPressureModerate() { runOnExecutorQueue(*m_mainExecutorToken, [=] (JSExecutor* executor) { executor->handleMemoryPressureModerate(); diff --git a/ReactAndroid/src/main/jni/react/Bridge.h b/ReactAndroid/src/main/jni/react/Bridge.h index 9fa3413ec5c9e0..b2c004a5855bb5 100644 --- a/ReactAndroid/src/main/jni/react/Bridge.h +++ b/ReactAndroid/src/main/jni/react/Bridge.h @@ -96,6 +96,7 @@ class Bridge { bool supportsProfiling(); void startProfiler(const std::string& title); void stopProfiler(const std::string& title, const std::string& filename); + void handleMemoryPressureUiHidden(); void handleMemoryPressureModerate(); void handleMemoryPressureCritical(); diff --git a/ReactAndroid/src/main/jni/react/Executor.h b/ReactAndroid/src/main/jni/react/Executor.h index 07aea900abe1c9..c161547b12fb8e 100644 --- a/ReactAndroid/src/main/jni/react/Executor.h +++ b/ReactAndroid/src/main/jni/react/Executor.h @@ -68,6 +68,7 @@ class JSExecutor { }; virtual void startProfiler(const std::string &titleString) {}; virtual void stopProfiler(const std::string &titleString, const std::string &filename) {}; + virtual void handleMemoryPressureUiHidden() {}; virtual void handleMemoryPressureModerate() {}; virtual void handleMemoryPressureCritical() { handleMemoryPressureModerate(); diff --git a/ReactAndroid/src/main/jni/react/JSCExecutor.cpp b/ReactAndroid/src/main/jni/react/JSCExecutor.cpp index e8027a064d55ec..a26a79742500e2 100644 --- a/ReactAndroid/src/main/jni/react/JSCExecutor.cpp +++ b/ReactAndroid/src/main/jni/react/JSCExecutor.cpp @@ -363,6 +363,12 @@ void JSCExecutor::stopProfiler(const std::string &titleString, const std::string #endif } +void JSCExecutor::handleMemoryPressureUiHidden() { + #ifdef WITH_JSC_MEMORY_PRESSURE + JSHandleMemoryPressure(this, m_context, JSMemoryPressure::UI_HIDDEN); + #endif +} + void JSCExecutor::handleMemoryPressureModerate() { #ifdef WITH_JSC_MEMORY_PRESSURE JSHandleMemoryPressure(this, m_context, JSMemoryPressure::MODERATE); diff --git a/ReactAndroid/src/main/jni/react/JSCExecutor.h b/ReactAndroid/src/main/jni/react/JSCExecutor.h index 3de1c088ec6023..acd6d14cc386f8 100644 --- a/ReactAndroid/src/main/jni/react/JSCExecutor.h +++ b/ReactAndroid/src/main/jni/react/JSCExecutor.h @@ -71,6 +71,7 @@ class JSCExecutor : public JSExecutor { virtual bool supportsProfiling() override; virtual void startProfiler(const std::string &titleString) override; virtual void stopProfiler(const std::string &titleString, const std::string &filename) override; + virtual void handleMemoryPressureUiHidden() override; virtual void handleMemoryPressureModerate() override; virtual void handleMemoryPressureCritical() override; virtual void destroy() override; diff --git a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp index 76a24fbb162f37..001c7237ea3dcf 100644 --- a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp +++ b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp @@ -839,6 +839,12 @@ static void stopProfiler(JNIEnv* env, jobject obj, jstring title, jstring filena bridge->stopProfiler(fromJString(env, title), fromJString(env, filename)); } +static void handleMemoryPressureUiHidden(JNIEnv* env, jobject obj) { + LOG(WARNING) << "handleMemoryPressureUiHidden"; + auto bridge = extractRefPtr(env, obj); + bridge->handleMemoryPressureUiHidden(); +} + static void handleMemoryPressureModerate(JNIEnv* env, jobject obj) { auto bridge = extractRefPtr(env, obj); bridge->handleMemoryPressureModerate(); @@ -1029,6 +1035,7 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { makeNativeMethod("supportsProfiling", bridge::supportsProfiling), makeNativeMethod("startProfiler", bridge::startProfiler), makeNativeMethod("stopProfiler", bridge::stopProfiler), + makeNativeMethod("handleMemoryPressureUiHidden", bridge::handleMemoryPressureUiHidden), makeNativeMethod("handleMemoryPressureModerate", bridge::handleMemoryPressureModerate), makeNativeMethod("handleMemoryPressureCritical", bridge::handleMemoryPressureCritical), makeNativeMethod("getJavaScriptContextNativePtrExperimental", bridge::getJavaScriptContext), diff --git a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp index 6bdeae504ca15f..c4c6faa74b2899 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp @@ -106,6 +106,9 @@ void CatalystInstanceImpl::registerNatives() { makeNativeMethod("callJSCallback", CatalystInstanceImpl::callJSCallback), makeNativeMethod("getMainExecutorToken", CatalystInstanceImpl::getMainExecutorToken), makeNativeMethod("setGlobalVariable", CatalystInstanceImpl::setGlobalVariable), + makeNativeMethod("handleMemoryPressureUiHidden", CatalystInstanceImpl::handleMemoryPressureUiHidden), + makeNativeMethod("handleMemoryPressureModerate", CatalystInstanceImpl::handleMemoryPressureModerate), + makeNativeMethod("handleMemoryPressureCritical", CatalystInstanceImpl::handleMemoryPressureCritical), makeNativeMethod("supportsProfiling", CatalystInstanceImpl::supportsProfiling), makeNativeMethod("startProfiler", CatalystInstanceImpl::startProfiler), makeNativeMethod("stopProfiler", CatalystInstanceImpl::stopProfiler), @@ -204,6 +207,18 @@ void CatalystInstanceImpl::setGlobalVariable(std::string propName, folly::make_unique(std::move(jsonValue))); } +void CatalystInstanceImpl::handleMemoryPressureUiHidden() { + instance_->handleMemoryPressureUiHidden(); +} + +void CatalystInstanceImpl::handleMemoryPressureModerate() { + instance_->handleMemoryPressureModerate(); +} + +void CatalystInstanceImpl::handleMemoryPressureCritical() { + instance_->handleMemoryPressureCritical(); +} + jboolean CatalystInstanceImpl::supportsProfiling() { if (!instance_) { return false; diff --git a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h index 0bb6302fca7cba..cddc4e03c12eb1 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h +++ b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.h @@ -55,6 +55,9 @@ class CatalystInstanceImpl : public jni::HybridClass { local_ref getMainExecutorToken(); void setGlobalVariable(std::string propName, std::string&& jsonValue); + void handleMemoryPressureUiHidden(); + void handleMemoryPressureModerate(); + void handleMemoryPressureCritical(); jboolean supportsProfiling(); void startProfiler(const std::string& title); void stopProfiler(const std::string& title, const std::string& filename); diff --git a/ReactCommon/bridge/Executor.h b/ReactCommon/bridge/Executor.h index 08af931d511cff..a27bf844fe6020 100644 --- a/ReactCommon/bridge/Executor.h +++ b/ReactCommon/bridge/Executor.h @@ -168,6 +168,7 @@ class JSExecutor { } virtual void startProfiler(const std::string &titleString) {} virtual void stopProfiler(const std::string &titleString, const std::string &filename) {} + virtual void handleMemoryPressureUiHidden() {} virtual void handleMemoryPressureModerate() {} virtual void handleMemoryPressureCritical() { handleMemoryPressureModerate(); diff --git a/ReactCommon/bridge/Instance.cpp b/ReactCommon/bridge/Instance.cpp index fa70b02abbb554..a179fa14f72432 100644 --- a/ReactCommon/bridge/Instance.cpp +++ b/ReactCommon/bridge/Instance.cpp @@ -118,5 +118,17 @@ ExecutorToken Instance::getMainExecutorToken() { return nativeToJsBridge_->getMainExecutorToken(); } +void Instance::handleMemoryPressureUiHidden() { + nativeToJsBridge_->handleMemoryPressureUiHidden(); +} + +void Instance::handleMemoryPressureModerate() { + nativeToJsBridge_->handleMemoryPressureModerate(); +} + +void Instance::handleMemoryPressureCritical() { + nativeToJsBridge_->handleMemoryPressureCritical(); +} + } // namespace react } // namespace facebook diff --git a/ReactCommon/bridge/Instance.h b/ReactCommon/bridge/Instance.h index 9aee9d238b2008..d5a3507eb80bc2 100644 --- a/ReactCommon/bridge/Instance.h +++ b/ReactCommon/bridge/Instance.h @@ -50,6 +50,9 @@ class Instance { MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int moduleId, unsigned int methodId, folly::dynamic&& args); ExecutorToken getMainExecutorToken(); + void handleMemoryPressureUiHidden(); + void handleMemoryPressureModerate(); + void handleMemoryPressureCritical(); private: void callNativeModules(ExecutorToken token, const std::string& calls, bool isEndOfBatch); diff --git a/ReactCommon/bridge/JSCExecutor.cpp b/ReactCommon/bridge/JSCExecutor.cpp index 4bc0d99a04af66..cde2a7715d5468 100644 --- a/ReactCommon/bridge/JSCExecutor.cpp +++ b/ReactCommon/bridge/JSCExecutor.cpp @@ -371,6 +371,12 @@ void JSCExecutor::stopProfiler(const std::string &titleString, const std::string #endif } +void JSCExecutor::handleMemoryPressureUiHidden() { + #ifdef WITH_JSC_MEMORY_PRESSURE + JSHandleMemoryPressure(this, m_context, JSMemoryPressure::UI_HIDDEN); + #endif +} + void JSCExecutor::handleMemoryPressureModerate() { #ifdef WITH_JSC_MEMORY_PRESSURE JSHandleMemoryPressure(this, m_context, JSMemoryPressure::MODERATE); diff --git a/ReactCommon/bridge/JSCExecutor.h b/ReactCommon/bridge/JSCExecutor.h index e869946d76d51d..735e1c45e627e6 100644 --- a/ReactCommon/bridge/JSCExecutor.h +++ b/ReactCommon/bridge/JSCExecutor.h @@ -74,6 +74,7 @@ class JSCExecutor : public JSExecutor { virtual bool supportsProfiling() override; virtual void startProfiler(const std::string &titleString) override; virtual void stopProfiler(const std::string &titleString, const std::string &filename) override; + virtual void handleMemoryPressureUiHidden() override; virtual void handleMemoryPressureModerate() override; virtual void handleMemoryPressureCritical() override; virtual void destroy() override; diff --git a/ReactCommon/bridge/NativeToJsBridge.cpp b/ReactCommon/bridge/NativeToJsBridge.cpp index eaa50d38f86259..8a90f7a0efb3e4 100644 --- a/ReactCommon/bridge/NativeToJsBridge.cpp +++ b/ReactCommon/bridge/NativeToJsBridge.cpp @@ -226,6 +226,12 @@ void NativeToJsBridge::stopProfiler(const std::string& title, const std::string& }); } +void NativeToJsBridge::handleMemoryPressureUiHidden() { + runOnExecutorQueue(m_mainExecutorToken, [=] (JSExecutor* executor) { + executor->handleMemoryPressureUiHidden(); + }); +} + void NativeToJsBridge::handleMemoryPressureModerate() { runOnExecutorQueue(m_mainExecutorToken, [=] (JSExecutor* executor) { executor->handleMemoryPressureModerate(); diff --git a/ReactCommon/bridge/NativeToJsBridge.h b/ReactCommon/bridge/NativeToJsBridge.h index 3f3230a6c980ad..942ee17c93607e 100644 --- a/ReactCommon/bridge/NativeToJsBridge.h +++ b/ReactCommon/bridge/NativeToJsBridge.h @@ -101,6 +101,7 @@ class NativeToJsBridge { bool supportsProfiling(); void startProfiler(const std::string& title); void stopProfiler(const std::string& title, const std::string& filename); + void handleMemoryPressureUiHidden(); void handleMemoryPressureModerate(); void handleMemoryPressureCritical(); From a96fcc7ce1cbdbc01923a52420af339011a4ea1a Mon Sep 17 00:00:00 2001 From: Sean Kinsey Date: Thu, 26 May 2016 12:22:30 -0700 Subject: [PATCH 128/843] Optimize and flowify mapWithSeparator Summary: Update mapWithSeparator so that Flow can reason about the arguments and return type. For simplicity, it is expected that the type of the separator will be the same as that of the mapped item. Reviewed By: vjeux Differential Revision: D3323557 fbshipit-source-id: 75b59e928d4e8c309b5933499a14744370ee5660 --- .../__tests__/mapWithSeparator-test.js | 57 +++++++++++++++++++ Libraries/Utilities/mapWithSeparator.js | 26 ++++++--- 2 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 Libraries/Utilities/__tests__/mapWithSeparator-test.js diff --git a/Libraries/Utilities/__tests__/mapWithSeparator-test.js b/Libraries/Utilities/__tests__/mapWithSeparator-test.js new file mode 100644 index 00000000000000..4dea1ec7bf2fa1 --- /dev/null +++ b/Libraries/Utilities/__tests__/mapWithSeparator-test.js @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.unmock('mapWithSeparator'); + +describe('mapWithSeparator', () => { + const mapWithSeparator = require('mapWithSeparator'); + + it('mapWithSeparator returns expected results', () => { + const array = [1, 2, 3]; + const result = mapWithSeparator( + array, + function(value) { + return value * 2; + }, + function() { + return 0; + } + ); + expect(result).toEqual([2, 0, 4, 0, 6]); + }); + + it('mapWithSeparator indices are correct', () => { + const array = [1, 2, 3]; + const result = mapWithSeparator( + array, + function(value, index) { + return index; + }, + function(index) { + return index; + } + ); + expect(result).toEqual([0, 0, 1, 1, 2]); + }); + + it('mapWithSeparator passes correct array and indices', () => { + const array = [3, 2, 1]; + const result = mapWithSeparator( + array, + function(value, index, arr) { + return arr[index]; + }, + function(index) { + return index; + } + ); + expect(result).toEqual([3, 0, 2, 1, 1]); + }); +}); diff --git a/Libraries/Utilities/mapWithSeparator.js b/Libraries/Utilities/mapWithSeparator.js index 4aa8665c3520b1..ff3048a2609df6 100644 --- a/Libraries/Utilities/mapWithSeparator.js +++ b/Libraries/Utilities/mapWithSeparator.js @@ -1,19 +1,29 @@ /** - * Copyright 2004-present Facebook. All Rights Reserved. + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule mapWithSeparator + * @flow */ 'use strict'; -function mapWithSeparator(array, valueFunction, separatorFunction) { - var results = []; - for (var i = 0; i < array.length; i++) { - results.push(valueFunction(array[i], i, array)); - if (i !== array.length - 1) { - results.push(separatorFunction(i)); +function mapWithSeparator( + items: Array, + itemRenderer: (item: TFrom, index: number, items: Array) => TTo, + spacerRenderer: (index: number) => TTo, +): Array { + const mapped = []; + if (items.length > 0) { + mapped.push(itemRenderer(items[0], 0, items)); + for (let ii = 1; ii < items.length; ii++) { + mapped.push(spacerRenderer(ii - 1), itemRenderer(items[ii], ii, items)); } } - return results; + return mapped; } module.exports = mapWithSeparator; From 7ca8e0e8b1c03e181eec2fd9a28b986baa65a501 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Thu, 26 May 2016 13:35:12 -0700 Subject: [PATCH 129/843] Stabilized TestIdTestApp integration test Summary: - TestIdTestModule instrumentation tests is responsible for too many occasional crashes, e.g. https://circleci.com/gh/facebook/react-native/7054. This should fix the problem (will monitor over next week) - Made file naming more consistent - 5 retries don't make e2e tests more stable, reduced back to 3 but I need to investigate how to make it more reliable Closes https://github.com/facebook/react-native/pull/7784 Differential Revision: D3354444 fbshipit-source-id: d058362edbec09522a4828998e01988a82a74487 --- .../java/com/facebook/react/tests/TestIdTestCase.java | 5 +++-- ReactAndroid/src/androidTest/js/TestIdTestModule.js | 8 ++++++-- circle.yml | 5 +++-- ...n-tests.js => run-android-ci-instrumentation-tests.js} | 4 +++- ...ion-test.sh => run-android-local-integration-tests.sh} | 0 ...ests.sh => run-instrumentation-tests-via-adb-shell.sh} | 0 6 files changed, 15 insertions(+), 7 deletions(-) rename scripts/{run-ci-android-instrumentation-tests.js => run-android-ci-instrumentation-tests.js} (90%) rename scripts/{run-android-local-integration-test.sh => run-android-local-integration-tests.sh} (100%) rename scripts/{run-android-instrumentation-tests.sh => run-instrumentation-tests-via-adb-shell.sh} (100%) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/TestIdTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/TestIdTestCase.java index 527e355c0e4a0c..90ac857c2827f5 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/TestIdTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/TestIdTestCase.java @@ -46,8 +46,9 @@ protected String getReactApplicationKeyUnderTest() { "Toolbar", "TextInput", "View", - "WebView", - "ScrollView Item (same id used for all items)"); + // "WebView", TODO t11449130 + "ScrollView Item (same id used for all items)" + ); public void testPropertyIsSetForViews() { for (String tag : viewTags) { diff --git a/ReactAndroid/src/androidTest/js/TestIdTestModule.js b/ReactAndroid/src/androidTest/js/TestIdTestModule.js index 8ad181d0925c73..6f11c1574fab20 100644 --- a/ReactAndroid/src/androidTest/js/TestIdTestModule.js +++ b/ReactAndroid/src/androidTest/js/TestIdTestModule.js @@ -111,12 +111,16 @@ var TestIdTestApp = React.createClass({ - } style={styles.base} - /> + />*/} ); diff --git a/circle.yml b/circle.yml index 8717c9f4139ff6..136d49b3ae218e 100644 --- a/circle.yml +++ b/circle.yml @@ -47,6 +47,7 @@ test: - source scripts/circle-ci-android-setup.sh && waitForAVD override: + # TODO flow is disabled see t11343811 and 0.25.1 # eslint bot # - cat <(echo eslint; npm run lint --silent -- --format=json; echo flow; npm run flow --silent -- check --json) | GITHUB_TOKEN="af6ef0d15709bc91d""06a6217a5a826a226fb57b7" CI_USER=$CIRCLE_PROJECT_USERNAME CI_REPO=$CIRCLE_PROJECT_REPONAME PULL_REQUEST_NUMBER=$CIRCLE_PR_NUMBER node bots/code-analysis-bot.js # JS tests for dependencies installed with npm3 @@ -69,14 +70,14 @@ test: # build test APK - buck install ReactAndroid/src/androidTest/buck-runner:instrumentation-tests --config build.threads=1 # run installed apk with tests - - node ./scripts/run-ci-android-instrumentation-tests.js --retries 3 --path ./ReactAndroid/src/androidTest/java/com/facebook/react/tests --package com.facebook.react.tests + - node ./scripts/run-android-ci-instrumentation-tests.js --retries 3 --path ./ReactAndroid/src/androidTest/java/com/facebook/react/tests --package com.facebook.react.tests - ./gradlew :ReactAndroid:testDebugUnitTest # Deprecated: these tests are executed using Buck above, while we support Gradle we just make sure the test code compiles - ./gradlew :ReactAndroid:assembleDebugAndroidTest # Android e2e test - - node ./scripts/run-ci-e2e-tests.js --android --js --retries 5 + - node ./scripts/run-ci-e2e-tests.js --android --js --retries 3 # testing docs generation is not broken - cd website && node ./server/generate.js diff --git a/scripts/run-ci-android-instrumentation-tests.js b/scripts/run-android-ci-instrumentation-tests.js similarity index 90% rename from scripts/run-ci-android-instrumentation-tests.js rename to scripts/run-android-ci-instrumentation-tests.js index 7bd04bb84f9c50..329d46070fa12b 100644 --- a/scripts/run-ci-android-instrumentation-tests.js +++ b/scripts/run-android-ci-instrumentation-tests.js @@ -42,8 +42,10 @@ let exitCode = 0; testClasses.forEach((testClass) => { if (tryExecNTimes( () => { + echo(`Starting ${testClass}`); + // any faster means Circle CI crashes exec('sleep 10s'); - return exec(`./scripts/run-android-instrumentation-tests.sh ${argv.package} ${testClass}`).code; + return exec(`./scripts/run-instrumentation-tests-via-adb-shell.sh ${argv.package} ${testClass}`).code; }, numberOfRetries)) { echo(`${testClass} failed ${numberOfRetries} times`); diff --git a/scripts/run-android-local-integration-test.sh b/scripts/run-android-local-integration-tests.sh similarity index 100% rename from scripts/run-android-local-integration-test.sh rename to scripts/run-android-local-integration-tests.sh diff --git a/scripts/run-android-instrumentation-tests.sh b/scripts/run-instrumentation-tests-via-adb-shell.sh similarity index 100% rename from scripts/run-android-instrumentation-tests.sh rename to scripts/run-instrumentation-tests-via-adb-shell.sh From 98dd91825f65e115cdf13c464689ee6c2bb8e27f Mon Sep 17 00:00:00 2001 From: Robert Rose Date: Thu, 26 May 2016 13:36:31 -0700 Subject: [PATCH 130/843] Fixing Issue #7526 Summary: Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: Fixing a bug detailed in Issue #7526 where Android app was crashing when using a binary number. **Test plan:** - Build & Run UIExplorer - Build & Run UIExplorer with `const binaryNumber = 0b101010` inserted into UIExplorerApp.android.js - Use `react-native init` to create a new blank project, then replace files in node_modules and insert `const binaryNumber = 0b101010` into index.android.js. Closes https://github.com/facebook/react-native/pull/7730 Reviewed By: avaly Differential Revision: D3353119 Pulled By: bestander fbshipit-source-id: 098442da32a29c369f5932b7a4004e8d7f4cb91f --- babel-preset/configs/main.js | 1 + babel-preset/package.json | 5 +++-- babel-preset/plugins.js | 1 + package.json | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/babel-preset/configs/main.js b/babel-preset/configs/main.js index f56d7ed689d391..2e809e2a579c03 100644 --- a/babel-preset/configs/main.js +++ b/babel-preset/configs/main.js @@ -30,6 +30,7 @@ module.exports = { 'transform-es2015-shorthand-properties', 'transform-es2015-spread', 'transform-es2015-template-literals', + 'transform-es2015-literals', 'transform-flow-strip-types', 'transform-object-assign', 'transform-object-rest-spread', diff --git a/babel-preset/package.json b/babel-preset/package.json index 6c296dc8a2b745..3a6a1c984cab9b 100644 --- a/babel-preset/package.json +++ b/babel-preset/package.json @@ -1,6 +1,6 @@ { "name": "babel-preset-react-native", - "version": "1.8.0", + "version": "1.9.0", "description": "Babel preset for React Native applications", "main": "index.js", "repository": "https://github.com/facebook/react-native/tree/master/babel-preset", @@ -23,13 +23,14 @@ "babel-plugin-syntax-jsx": "^6.5.0", "babel-plugin-syntax-trailing-function-commas": "^6.5.0", "babel-plugin-transform-class-properties": "^6.5.0", - "babel-plugin-transform-es2015-function-name": "^6.5.0", "babel-plugin-transform-es2015-arrow-functions": "^6.5.0", "babel-plugin-transform-es2015-block-scoping": "^6.5.0", "babel-plugin-transform-es2015-classes": "^6.5.0", "babel-plugin-transform-es2015-computed-properties": "^6.5.0", "babel-plugin-transform-es2015-destructuring": "^6.5.0", "babel-plugin-transform-es2015-for-of": "^6.5.0", + "babel-plugin-transform-es2015-function-name": "^6.5.0", + "babel-plugin-transform-es2015-literals": "^6.5.0", "babel-plugin-transform-es2015-modules-commonjs": "^6.5.0", "babel-plugin-transform-es2015-parameters": "^6.5.0", "babel-plugin-transform-es2015-shorthand-properties": "^6.5.0", diff --git a/babel-preset/plugins.js b/babel-preset/plugins.js index ceecb1889272a9..07e3ec3a6116a6 100644 --- a/babel-preset/plugins.js +++ b/babel-preset/plugins.js @@ -26,6 +26,7 @@ module.exports = { 'babel-plugin-transform-es2015-shorthand-properties': require('babel-plugin-transform-es2015-shorthand-properties'), 'babel-plugin-transform-es2015-spread': require('babel-plugin-transform-es2015-spread'), 'babel-plugin-transform-es2015-template-literals': require('babel-plugin-transform-es2015-template-literals'), + 'babel-plugin-transform-es2015-literals' : require('babel-plugin-transform-es2015-literals'), 'babel-plugin-transform-flow-strip-types': require('babel-plugin-transform-flow-strip-types'), 'babel-plugin-transform-object-assign': require('babel-plugin-transform-object-assign'), 'babel-plugin-transform-object-rest-spread': require('babel-plugin-transform-object-rest-spread'), diff --git a/package.json b/package.json index aa91a3b49f11ab..4adea31612791e 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "babel-plugin-transform-object-rest-spread": "^6.6.5", "babel-polyfill": "^6.6.1", "babel-preset-es2015-node": "^4.0.2", - "babel-preset-react-native": "^1.8.0", + "babel-preset-react-native": "^1.9.0", "babel-register": "^6.6.0", "babel-types": "^6.6.4", "babylon": "^6.6.4", From 26e84262484767dd9abdfd78ff16fa37fcb8e334 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 26 May 2016 13:46:58 -0700 Subject: [PATCH 131/843] Cross platform ActivityIndicator Summary: The API for `ActivityIndiatorIOS` and `ProgressBarAndroid` is very similar and can be merged in a cross platform component that displays a circular indeterminate loading indicator. This deprecates `ActivityIndiatorIOS` and non-horizontal `ProgressBarAndroid` in favor of this new component. **Test plan (required)** Tested with the ActivityIndicator example in UIExplorer on android and ios. Also made sure that `ActivityIndicatorIOS` still works and displays a deprecation warning. Also tested that `ProgressBarAndroid` with `indeterminate == true` and `styleAttr != 'Horizontal'` displays a deprecation warning. Closes https://github.com/facebook/react-native/pull/6897 Differential Revision: D3351607 Pulled By: dmmiller fbshipit-source-id: b107ce99d966359003e8b3118cd97b90fa1d3d7d --- ...Example.js => ActivityIndicatorExample.js} | 96 +++++++------ .../ProgressBarAndroidExample.android.js | 35 +---- Examples/UIExplorer/UIExplorerList.android.js | 4 + Examples/UIExplorer/UIExplorerList.ios.js | 4 +- .../ActivityIndicator/ActivityIndicator.js | 127 ++++++++++++++++++ .../ActivityIndicatorIOS.android.js | 0 .../ActivityIndicatorIOS.ios.js | 57 +------- .../ProgressBarAndroid.android.js | 16 ++- Libraries/react-native/react-native.js | 1 + Libraries/react-native/react-native.js.flow | 1 + .../progressbar/ProgressBarContainerView.java | 11 ++ .../ReactProgressBarViewManager.java | 6 + docs/AndroidBuildingFromSource.md | 2 +- website/server/extractDocs.js | 2 +- 14 files changed, 229 insertions(+), 133 deletions(-) rename Examples/UIExplorer/{ActivityIndicatorIOSExample.js => ActivityIndicatorExample.js} (61%) create mode 100644 Libraries/Components/ActivityIndicator/ActivityIndicator.js rename Libraries/Components/{ActivityIndicatorIOS => ActivityIndicator}/ActivityIndicatorIOS.android.js (100%) rename Libraries/Components/{ActivityIndicatorIOS => ActivityIndicator}/ActivityIndicatorIOS.ios.js (55%) diff --git a/Examples/UIExplorer/ActivityIndicatorIOSExample.js b/Examples/UIExplorer/ActivityIndicatorExample.js similarity index 61% rename from Examples/UIExplorer/ActivityIndicatorIOSExample.js rename to Examples/UIExplorer/ActivityIndicatorExample.js index 967f042755d9ed..3ebf8c109b9ca1 100644 --- a/Examples/UIExplorer/ActivityIndicatorIOSExample.js +++ b/Examples/UIExplorer/ActivityIndicatorExample.js @@ -15,41 +15,38 @@ */ 'use strict'; -var React = require('react'); -var ReactNative = require('react-native'); -var { - ActivityIndicatorIOS, +const React = require('react'); +const ReactNative = require('react-native'); +const { + ActivityIndicator, StyleSheet, View, } = ReactNative; -var TimerMixin = require('react-timer-mixin'); +const TimerMixin = require('react-timer-mixin'); -var ToggleAnimatingActivityIndicator = React.createClass({ +const ToggleAnimatingActivityIndicator = React.createClass({ mixins: [TimerMixin], - getInitialState: function() { + getInitialState() { return { animating: true, }; }, - setToggleTimeout: function() { - this.setTimeout( - () => { - this.setState({animating: !this.state.animating}); - this.setToggleTimeout(); - }, - 1200 - ); + setToggleTimeout() { + this.setTimeout(() => { + this.setState({animating: !this.state.animating}); + this.setToggleTimeout(); + }, 2000); }, - componentDidMount: function() { + componentDidMount() { this.setToggleTimeout(); }, - render: function() { + render() { return ( - + /> ); } }, { title: 'Gray', - render: function() { + render() { return ( - - ); @@ -92,23 +89,23 @@ exports.examples = [ }, { title: 'Custom colors', - render: function() { + render() { return ( - - - - + + + + ); } }, { title: 'Large', - render: function() { + render() { return ( - @@ -117,22 +114,22 @@ exports.examples = [ }, { title: 'Large, custom colors', - render: function() { + render() { return ( - - - - @@ -142,16 +139,28 @@ exports.examples = [ }, { title: 'Start/stop', - render: function(): ReactElement { + render() { return ; } }, + { + title: 'Custom size', + render() { + return ( + + ); + } + }, ]; -var styles = StyleSheet.create({ +const styles = StyleSheet.create({ centering: { alignItems: 'center', justifyContent: 'center', + padding: 8, }, gray: { backgroundColor: '#cccccc', @@ -159,5 +168,6 @@ var styles = StyleSheet.create({ horizontal: { flexDirection: 'row', justifyContent: 'space-around', + padding: 8, }, }); diff --git a/Examples/UIExplorer/ProgressBarAndroidExample.android.js b/Examples/UIExplorer/ProgressBarAndroidExample.android.js index 19b182bc769289..2e08ab73801072 100644 --- a/Examples/UIExplorer/ProgressBarAndroidExample.android.js +++ b/Examples/UIExplorer/ProgressBarAndroidExample.android.js @@ -49,41 +49,12 @@ var ProgressBarAndroidExample = React.createClass({ statics: { title: '', - description: 'Visual indicator of progress of some operation. ' + - 'Shows either a cyclic animation or a horizontal bar.', + description: 'Horizontal bar to show the progress of some operation.', }, render: function() { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -92,10 +63,6 @@ var ProgressBarAndroidExample = React.createClass({ - - - - diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js index ceb3ec84478175..25279bdf6f157d 100644 --- a/Examples/UIExplorer/UIExplorerList.android.js +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -23,6 +23,10 @@ export type UIExplorerExample = { }; var ComponentExamples: Array = [ + { + key: 'ActivityIndicatorExample', + module: require('./ActivityIndicatorExample'), + }, { key: 'SliderExample', module: require('./SliderExample'), diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index e9ddd367b7572d..b93518faa1d636 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -29,8 +29,8 @@ export type UIExplorerExample = { const ComponentExamples: Array = [ { - key: 'ActivityIndicatorIOSExample', - module: require('./ActivityIndicatorIOSExample'), + key: 'ActivityIndicatorExample', + module: require('./ActivityIndicatorExample'), }, { key: 'DatePickerIOSExample', diff --git a/Libraries/Components/ActivityIndicator/ActivityIndicator.js b/Libraries/Components/ActivityIndicator/ActivityIndicator.js new file mode 100644 index 00000000000000..636f68327b3d3a --- /dev/null +++ b/Libraries/Components/ActivityIndicator/ActivityIndicator.js @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ActivityIndicator + * @flow + */ +'use strict'; + +const ColorPropType = require('ColorPropType'); +const NativeMethodsMixin = require('NativeMethodsMixin'); +const Platform = require('Platform'); +const PropTypes = require('ReactPropTypes'); +const React = require('React'); +const StyleSheet = require('StyleSheet'); +const View = require('View'); + +const requireNativeComponent = require('requireNativeComponent'); + +const GRAY = '#999999'; + +/** + * Displays a circular loading indicator. + */ +const ActivityIndicator = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + ...View.propTypes, + /** + * Whether to show the indicator (true, the default) or hide it (false). + */ + animating: PropTypes.bool, + /** + * The foreground color of the spinner (default is gray). + */ + color: ColorPropType, + /** + * Size of the indicator. Small has a height of 20, large has a height of 36. + * Other sizes can be obtained using a scale transform. + */ + size: PropTypes.oneOf([ + 'small', + 'large', + ]), + /** + * Whether the indicator should hide when not animating (true by default). + * + * @platform ios + */ + hidesWhenStopped: PropTypes.bool, + }, + + getDefaultProps() { + return { + animating: true, + color: GRAY, + hidesWhenStopped: true, + size: 'small', + }; + }, + + render() { + const {onLayout, style, ...props} = this.props; + let sizeStyle; + switch (props.size) { + case 'small': + sizeStyle = styles.sizeSmall; + break; + case 'large': + sizeStyle = styles.sizeLarge; + break; + } + return ( + + + + ); + } +}); + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + justifyContent: 'center', + }, + sizeSmall: { + width: 20, + height: 20, + }, + sizeLarge: { + width: 36, + height: 36, + }, +}); + +if (Platform.OS === 'ios') { + var RCTActivityIndicator = requireNativeComponent( + 'RCTActivityIndicatorView', + ActivityIndicator, + {nativeOnly: {activityIndicatorViewStyle: true}}, + ); +} else if (Platform.OS === 'android') { + var RCTActivityIndicator = requireNativeComponent( + 'AndroidProgressBar', + ActivityIndicator, + // Ignore props that are specific to non inderterminate ProgressBar. + {nativeOnly: { + indeterminate: true, + progress: true, + styleAttr: true, + }}, + ); +} + +module.exports = ActivityIndicator; diff --git a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.android.js b/Libraries/Components/ActivityIndicator/ActivityIndicatorIOS.android.js similarity index 100% rename from Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.android.js rename to Libraries/Components/ActivityIndicator/ActivityIndicatorIOS.android.js diff --git a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js b/Libraries/Components/ActivityIndicator/ActivityIndicatorIOS.ios.js similarity index 55% rename from Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js rename to Libraries/Components/ActivityIndicator/ActivityIndicatorIOS.ios.js index fa97eb83ac00ce..869a62e5a30c5c 100644 --- a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js +++ b/Libraries/Components/ActivityIndicator/ActivityIndicatorIOS.ios.js @@ -7,27 +7,18 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ActivityIndicatorIOS - * @flow */ 'use strict'; +var ActivityIndicator = require('ActivityIndicator'); var NativeMethodsMixin = require('NativeMethodsMixin'); var PropTypes = require('ReactPropTypes'); var React = require('React'); -var StyleSheet = require('StyleSheet'); var View = require('View'); -var requireNativeComponent = require('requireNativeComponent'); - -var GRAY = '#999999'; - -type DefaultProps = { - animating: boolean; - color: string; - hidesWhenStopped: boolean; - size: 'small' | 'large'; -}; - +/** + * Deprecated, use ActivityIndicator instead. + */ var ActivityIndicatorIOS = React.createClass({ mixins: [NativeMethodsMixin], @@ -60,47 +51,13 @@ var ActivityIndicatorIOS = React.createClass({ onLayout: PropTypes.func, }, - getDefaultProps: function(): DefaultProps { - return { - animating: true, - color: GRAY, - hidesWhenStopped: true, - size: 'small', - }; + componentDidMount: function() { + console.warn('ActivityIndicatorIOS is deprecated. Use ActivityIndicator instead.'); }, render: function() { - var {onLayout, style, ...props} = this.props; - var sizeStyle = (this.props.size === 'large') ? styles.sizeLarge : styles.sizeSmall; - return ( - - - - ); + return ; } }); -var styles = StyleSheet.create({ - container: { - alignItems: 'center', - justifyContent: 'center', - }, - sizeSmall: { - width: 20, - height: 20, - }, - sizeLarge: { - width: 36, - height: 36, - } -}); - -var RCTActivityIndicatorView = requireNativeComponent( - 'RCTActivityIndicatorView', - ActivityIndicatorIOS, - {nativeOnly: {activityIndicatorViewStyle: true}}, -); - module.exports = ActivityIndicatorIOS; diff --git a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js index 95e5504a449954..fb6cb8255b070a 100644 --- a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js +++ b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js @@ -13,7 +13,6 @@ var NativeMethodsMixin = require('NativeMethodsMixin'); var React = require('React'); var ReactPropTypes = require('ReactPropTypes'); -var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); var View = require('View'); var ColorPropType = require('ColorPropType'); @@ -107,11 +106,24 @@ var ProgressBarAndroid = React.createClass({ mixins: [NativeMethodsMixin], + componentDidMount: function() { + if (this.props.indeterminate && this.props.styleAttr !== 'Horizontal') { + console.warn( + 'Circular indeterminate `ProgressBarAndroid`' + + 'is deprecated. Use `ActivityIndicator` instead.' + ); + } + }, + render: function() { return ; }, }); -var AndroidProgressBar = requireNativeComponent('AndroidProgressBar', ProgressBarAndroid); +var AndroidProgressBar = requireNativeComponent( + 'AndroidProgressBar', + ProgressBarAndroid, + {nativeOnly: {animating: true}}, +); module.exports = ProgressBarAndroid; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index bd5e082a010893..713becd5dfaabf 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -27,6 +27,7 @@ if (__DEV__) { // Export React, plus some native additions. const ReactNative = { // Components + get ActivityIndicator() { return require('ActivityIndicator'); }, get ActivityIndicatorIOS() { return require('ActivityIndicatorIOS'); }, get ART() { return require('ReactNativeART'); }, get DatePickerIOS() { return require('DatePickerIOS'); }, diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index 74c306f2cba2fe..191d3e22284171 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -25,6 +25,7 @@ // var ReactNative = Object.assign(Object.create(require('ReactNative')), { // Components + ActivityIndicator: require('ActivityIndicator'), ActivityIndicatorIOS: require('ActivityIndicatorIOS'), ART: require('ReactNativeART'), DatePickerIOS: require('DatePickerIOS'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarContainerView.java b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarContainerView.java index d71b66922345a2..9e29777d243d04 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarContainerView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ProgressBarContainerView.java @@ -7,6 +7,7 @@ import android.content.Context; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; +import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ProgressBar; @@ -22,6 +23,7 @@ private @Nullable Integer mColor; private boolean mIndeterminate = true; + private boolean mAnimating = true; private double mProgress; private @Nullable ProgressBar mProgressBar; @@ -53,6 +55,10 @@ public void setProgress(double progress) { mProgress = progress; } + public void setAnimating(boolean animating) { + mAnimating = animating; + } + public void apply() { if (mProgressBar == null) { throw new JSApplicationIllegalArgumentException("setStyle() not called"); @@ -61,6 +67,11 @@ public void apply() { mProgressBar.setIndeterminate(mIndeterminate); setColor(mProgressBar); mProgressBar.setProgress((int) (mProgress * MAX_PROGRESS)); + if (mAnimating) { + mProgressBar.setVisibility(View.VISIBLE); + } else { + mProgressBar.setVisibility(View.GONE); + } } private void setColor(ProgressBar progressBar) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java index 38454282efcfd2..969626489337d3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java @@ -32,6 +32,7 @@ public class ReactProgressBarViewManager extends /* package */ static final String PROP_STYLE = "styleAttr"; /* package */ static final String PROP_INDETERMINATE = "indeterminate"; /* package */ static final String PROP_PROGRESS = "progress"; + /* package */ static final String PROP_ANIMATING = "animating"; /* package */ static final String REACT_CLASS = "AndroidProgressBar"; /* package */ static final String DEFAULT_STYLE = "Normal"; @@ -79,6 +80,11 @@ public void setProgress(ProgressBarContainerView view, double progress) { view.setProgress(progress); } + @ReactProp(name = PROP_ANIMATING) + public void setAnimating(ProgressBarContainerView view, boolean animating) { + view.setAnimating(animating); + } + @Override public ProgressBarShadowNode createShadowNodeInstance() { return new ProgressBarShadowNode(); diff --git a/docs/AndroidBuildingFromSource.md b/docs/AndroidBuildingFromSource.md index 5213a161d571ac..5336f668ed7d26 100644 --- a/docs/AndroidBuildingFromSource.md +++ b/docs/AndroidBuildingFromSource.md @@ -4,7 +4,7 @@ title: Building React Native from source layout: docs category: Guides (Android) permalink: docs/android-building-from-source.html -next: activityindicatorios +next: activityindicator --- You will need to build React Native from source if you want to work on a new feature/bug fix, try out the latest features which are not released yet, or maintain your own fork with patches that cannot be merged to the core. diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index 77d153861d886c..8929271181c0d7 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -250,7 +250,7 @@ function renderStyle(filepath) { } const components = [ - '../Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js', + '../Libraries/Components/ActivityIndicator/ActivityIndicator.js', '../Libraries/Components/DatePicker/DatePickerIOS.ios.js', '../Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js', '../Libraries/Image/Image.ios.js', From f7279b407471bcc15ffe730e1049c4f7c6f0d108 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Thu, 26 May 2016 18:07:48 -0700 Subject: [PATCH 132/843] Remove `key` from `NavigationState`. Summary: NavigationState is composed of many things such as `index`, `routes`. The only way to effectively compare navigation states is to assume their immutability and simply compare their references. That said, `navigationState.key` does not really serves anything useful. The key does not make a navigation state more unique or comparable. This diff makes `navigationState.key` irrelevant thus we could make NavigationState simpler. Reviewed By: ericvicenti Differential Revision: D3351915 fbshipit-source-id: 75d5fa432d2a693f999a91b11e3bff1fdd42e61d --- .../NavigationPropTypes.js | 1 - .../NavigationStateUtils.js | 21 ++++++++----------- .../NavigationTypeDefinition.js | 1 - 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/Libraries/NavigationExperimental/NavigationPropTypes.js b/Libraries/NavigationExperimental/NavigationPropTypes.js index 6183078a3c1601..d5a4776cc8a708 100644 --- a/Libraries/NavigationExperimental/NavigationPropTypes.js +++ b/Libraries/NavigationExperimental/NavigationPropTypes.js @@ -43,7 +43,6 @@ const navigationRoute = PropTypes.shape({ /* navigationRoute */ const navigationState = PropTypes.shape({ index: PropTypes.number.isRequired, - key: PropTypes.string.isRequired, routes: PropTypes.arrayOf(navigationRoute), }); diff --git a/Libraries/NavigationExperimental/NavigationStateUtils.js b/Libraries/NavigationExperimental/NavigationStateUtils.js index aa7bb26109e46c..4ef94cbadf6b4c 100644 --- a/Libraries/NavigationExperimental/NavigationStateUtils.js +++ b/Libraries/NavigationExperimental/NavigationStateUtils.js @@ -95,7 +95,6 @@ function set(state: ?NavigationState, key: string, nextChildren: Array, }; From a0562c7ccffea7d001499ce8e431ede5249cab5e Mon Sep 17 00:00:00 2001 From: Dave Miller Date: Thu, 26 May 2016 20:12:58 -0700 Subject: [PATCH 133/843] Fix Modal when the Activity is paused or resumed Summary: When the activity hosting a Modal goes away, we should dismiss the dialog from the stack and then reconstitute it when the activity comes back. This means that if an activity is paused because another activity is placed on top of it but our ui operation was delayed, it will not blow up finding no window since it is gone. Also fixes a place where we should remove a listener for lifecycle events which we were not doing. Reviewed By: halfjuice Differential Revision: D3357286 fbshipit-source-id: c5c6dd8e5ef299762ed9aa15a6910ce9c0b111dc --- .../views/modal/ReactModalHostManager.java | 2 +- .../react/views/modal/ReactModalHostView.java | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java index 4af221dd4965e7..2fb42c594f6b27 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java @@ -62,7 +62,7 @@ public Class getShadowNodeClass() { @Override public void onDropViewInstance(ReactModalHostView view) { super.onDropViewInstance(view); - view.dismiss(); + view.onDropInstance(); } @ReactProp(name = "animationType") diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java index 24b33892a363cb..48b66db9f97f08 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java @@ -109,7 +109,12 @@ public void addChildrenForAccessibility(ArrayList outChildren) { // Those will be handled by the mHostView which lives in the dialog } - public void dismiss() { + public void onDropInstance() { + ((ReactContext) getContext()).removeLifecycleEventListener(this); + dismiss(); + } + + private void dismiss() { if (mDialog != null) { mDialog.dismiss(); mDialog = null; @@ -140,18 +145,20 @@ protected void setAnimationType(String animationType) { @Override public void onHostResume() { - // do nothing + // We show the dialog again when the host resumes + showOrUpdate(); } @Override public void onHostPause() { - // do nothing + // We dismiss the dialog and reconstitute it onHostResume + dismiss(); } @Override public void onHostDestroy() { - // Dismiss the dialog if it is present - dismiss(); + // Drop the instance if the host is destroyed which will dismiss the dialog + onDropInstance(); } @VisibleForTesting From fc14f85f73f0c6d475c15eab959b977b193e1924 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Fri, 27 May 2016 04:46:25 -0700 Subject: [PATCH 134/843] Reduce boxing overhead of arrays in uiBlockWithLayoutUpdateForRootView Summary: The view update cycle in UIManager was relying on a bunch of boolean values boxes as NSNumbers in parallel arrays. This diff packs those values into a struct, which is more efficient and easier to maintain. Reviewed By: javache Differential Revision: D3253073 fbshipit-source-id: 3e1520c27b88bc1b44ddffcaae3218d7681b2cd2 --- React/Modules/RCTUIManager.m | 217 +++++++++++++++++++---------------- 1 file changed, 119 insertions(+), 98 deletions(-) diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 11112f92a9dc6e..29ef7549d4ce8f 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -57,8 +57,6 @@ @interface RCTAnimation : NSObject @property (nonatomic, readonly) NSTimeInterval duration; @property (nonatomic, readonly) NSTimeInterval delay; @property (nonatomic, readonly, copy) NSString *property; -@property (nonatomic, readonly) id fromValue; -@property (nonatomic, readonly) id toValue; @property (nonatomic, readonly) CGFloat springDamping; @property (nonatomic, readonly) CGFloat initialVelocity; @property (nonatomic, readonly) RCTAnimationType animationType; @@ -136,8 +134,6 @@ - (instancetype)initWithDuration:(NSTimeInterval)duration dictionary:(NSDictiona _springDamping = [RCTConvert CGFloat:config[@"springDamping"]]; _initialVelocity = [RCTConvert CGFloat:config[@"initialVelocity"]]; } - _fromValue = config[@"fromValue"]; - _toValue = config[@"toValue"]; } return self; } @@ -552,42 +548,47 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * return nil; } - // Parallel arrays are built and then handed off to main thread - NSMutableArray *frameReactTags = - [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; - NSMutableArray *frames = - [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; - NSMutableArray *areNew = - [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; - NSMutableArray *parentsAreNew = - [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; - NSMutableDictionary *updateBlocks = - [NSMutableDictionary new]; - NSMutableArray *areHidden = - [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; - - for (RCTShadowView *shadowView in viewsWithNewFrames) { - [frameReactTags addObject:shadowView.reactTag]; - [frames addObject:[NSValue valueWithCGRect:shadowView.frame]]; - [areNew addObject:@(shadowView.isNewView)]; - [parentsAreNew addObject:@(shadowView.superview.isNewView)]; - [areHidden addObject:@(shadowView.isHidden)]; - } - - for (RCTShadowView *shadowView in viewsWithNewFrames) { - // We have to do this after we build the parentsAreNew array. - shadowView.newView = NO; + typedef struct { + CGRect frame; + BOOL isNew; + BOOL parentIsNew; + BOOL isHidden; + } RCTFrameData; + + // Construct arrays then hand off to main thread + NSInteger count = viewsWithNewFrames.count; + NSMutableArray *reactTags = [[NSMutableArray alloc] initWithCapacity:count]; + NSMutableData *framesData = [[NSMutableData alloc] initWithLength:sizeof(RCTFrameData) * count]; + { + NSInteger index = 0; + RCTFrameData *frameDataArray = (RCTFrameData *)framesData.mutableBytes; + for (RCTShadowView *shadowView in viewsWithNewFrames) { + reactTags[index] = shadowView.reactTag; + frameDataArray[index++] = (RCTFrameData){ + shadowView.frame, + shadowView.isNewView, + shadowView.superview.isNewView, + shadowView.isHidden, + }; + } } // These are blocks to be executed on each view, immediately after // reactSetFrame: has been called. Note that if reactSetFrame: is not called, // these won't be called either, so this is not a suitable place to update // properties that aren't related to layout. + NSMutableDictionary *updateBlocks = + [NSMutableDictionary new]; for (RCTShadowView *shadowView in viewsWithNewFrames) { + + // We have to do this after we build the parentsAreNew array. + shadowView.newView = NO; + + NSNumber *reactTag = shadowView.reactTag; RCTViewManager *manager = [_componentDataByName[shadowView.viewName] manager]; RCTViewManagerUIBlock block = [manager uiBlockToAmendWithShadowView:shadowView]; if (block) { - updateBlocks[shadowView.reactTag] = block; + updateBlocks[reactTag] = block; } if (shadowView.onLayout) { @@ -602,8 +603,7 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * }); } - if (RCTIsReactRootView(shadowView.reactTag)) { - NSNumber *reactTag = shadowView.reactTag; + if (RCTIsReactRootView(reactTag)) { CGSize contentSize = shadowView.frame.size; dispatch_async(dispatch_get_main_queue(), ^{ @@ -618,87 +618,107 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * // Perform layout (possibly animated) return ^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTLayoutAnimation *layoutAnimation = _layoutAnimation; - - __block NSUInteger completionsCalled = 0; - for (NSUInteger ii = 0; ii < frames.count; ii++) { - NSNumber *reactTag = frameReactTags[ii]; - UIView *view = viewRegistry[reactTag]; - CGRect frame = [frames[ii] CGRectValue]; - - BOOL isHidden = [areHidden[ii] boolValue]; - BOOL isNew = [areNew[ii] boolValue]; - RCTAnimation *updateAnimation = isNew ? nil : layoutAnimation.updateAnimation; - BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue]; - RCTAnimation *createAnimation = shouldAnimateCreation ? layoutAnimation.createAnimation : nil; - - void (^completion)(BOOL) = ^(BOOL finished) { - completionsCalled++; - if (layoutAnimation.callback && completionsCalled == frames.count) { - layoutAnimation.callback(@[@(finished)]); - - // It's unsafe to call this callback more than once, so we nil it out here - // to make sure that doesn't happen. - layoutAnimation.callback = nil; - } - }; - if (view.isHidden != isHidden) { - view.hidden = isHidden; - } + const RCTFrameData *frameDataArray = (const RCTFrameData *)framesData.bytes; - // Animate view creation - if (createAnimation) { + RCTLayoutAnimation *layoutAnimation = uiManager->_layoutAnimation; + if (!layoutAnimation.updateAnimation && !layoutAnimation.createAnimation) { + + // Fast path for common case + NSInteger index = 0; + for (NSNumber *reactTag in reactTags) { + RCTFrameData frameData = frameDataArray[index++]; + + UIView *view = viewRegistry[reactTag]; + CGRect frame = frameData.frame; [view reactSetFrame:frame]; - CATransform3D finalTransform = view.layer.transform; - CGFloat finalOpacity = view.layer.opacity; - if ([createAnimation.property isEqualToString:@"scaleXY"]) { - view.layer.transform = CATransform3DMakeScale(0, 0, 0); - } else if ([createAnimation.property isEqualToString:@"opacity"]) { - view.layer.opacity = 0.0; + RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag]; + if (updateBlock) { + updateBlock(self, viewRegistry); } + } + if (layoutAnimation.callback) { + layoutAnimation.callback(@[@YES]); + } - [createAnimation performAnimations:^{ - if ([createAnimation.property isEqual:@"scaleXY"]) { - view.layer.transform = finalTransform; - } else if ([createAnimation.property isEqual:@"opacity"]) { - view.layer.opacity = finalOpacity; - } else { - RCTLogError(@"Unsupported layout animation createConfig property %@", - createAnimation.property); - } + } else { + + __block NSUInteger completionsCalled = 0; + + NSInteger index = 0; + for (NSNumber *reactTag in reactTags) { + RCTFrameData frameData = frameDataArray[index++]; + + UIView *view = viewRegistry[reactTag]; + CGRect frame = frameData.frame; + + BOOL isNew = frameData.isNew; + RCTAnimation *updateAnimation = isNew ? nil : layoutAnimation.updateAnimation; + BOOL shouldAnimateCreation = isNew && !frameData.parentIsNew; + RCTAnimation *createAnimation = shouldAnimateCreation ? layoutAnimation.createAnimation : nil; + + BOOL isHidden = frameData.isHidden; + if (view.isHidden != isHidden) { + view.hidden = isHidden; + } - RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag]; - if (updateBlock) { - updateBlock(self, _viewRegistry); + void (^completion)(BOOL) = ^(BOOL finished) { + completionsCalled++; + if (layoutAnimation.callback && completionsCalled == count) { + layoutAnimation.callback(@[@(finished)]); + + // It's unsafe to call this callback more than once, so we nil it out here + // to make sure that doesn't happen. + layoutAnimation.callback = nil; } - } withCompletionBlock:completion]; + }; + + RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag]; + if (createAnimation) { - // Animate view update - } else if (updateAnimation) { - [updateAnimation performAnimations:^{ + // Animate view creation [view reactSetFrame:frame]; - RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag]; - if (updateBlock) { - updateBlock(self, _viewRegistry); - } - } withCompletionBlock:completion]; + CATransform3D finalTransform = view.layer.transform; + CGFloat finalOpacity = view.layer.opacity; - // Update without animation - } else { - [view reactSetFrame:frame]; + NSString *property = createAnimation.property; + if ([property isEqualToString:@"scaleXY"]) { + view.layer.transform = CATransform3DMakeScale(0, 0, 0); + } else if ([property isEqualToString:@"opacity"]) { + view.layer.opacity = 0.0; + } else { + RCTLogError(@"Unsupported layout animation createConfig property %@", + createAnimation.property); + } - RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag]; - if (updateBlock) { - updateBlock(self, _viewRegistry); + [createAnimation performAnimations:^{ + if ([property isEqualToString:@"scaleXY"]) { + view.layer.transform = finalTransform; + } else if ([property isEqualToString:@"opacity"]) { + view.layer.opacity = finalOpacity; + } + if (updateBlock) { + updateBlock(self, viewRegistry); + } + } withCompletionBlock:completion]; + + } else if (updateAnimation) { + + // Animate view update + [updateAnimation performAnimations:^{ + [view reactSetFrame:frame]; + if (updateBlock) { + updateBlock(self, viewRegistry); + } + } withCompletionBlock:completion]; } - completion(YES); } } - _layoutAnimation = nil; + // Clean up + uiManager->_layoutAnimation = nil; }; } @@ -802,10 +822,11 @@ - (void)_removeChildren:(NSArray> *)children // the view events anyway. view.userInteractionEnabled = NO; + NSString *property = deleteAnimation.property; [deleteAnimation performAnimations:^{ - if ([deleteAnimation.property isEqual:@"scaleXY"]) { + if ([property isEqualToString:@"scaleXY"]) { view.layer.transform = CATransform3DMakeScale(0, 0, 0); - } else if ([deleteAnimation.property isEqual:@"opacity"]) { + } else if ([property isEqualToString:@"opacity"]) { view.layer.opacity = 0.0; } else { RCTLogError(@"Unsupported layout animation createConfig property %@", From 3f2f7733882263766c58684d8b007bfe62e575bf Mon Sep 17 00:00:00 2001 From: kiran Date: Fri, 27 May 2016 04:47:43 -0700 Subject: [PATCH 135/843] Mentioned Google emulator instead of genymotion Summary: Closes https://github.com/facebook/react-native/pull/7793 Differential Revision: D3358150 fbshipit-source-id: 881ea80dfc9cfd28337995a8648f14c51b468b0a --- docs/QuickStart-GettingStarted.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/QuickStart-GettingStarted.md b/docs/QuickStart-GettingStarted.md index c78d7501ceef24..b63dc4f00bc983 100644 --- a/docs/QuickStart-GettingStarted.md +++ b/docs/QuickStart-GettingStarted.md @@ -219,7 +219,7 @@ Get started with Nuclide [here](http://nuclide.io/docs/quick-start/getting-start #### Genymotion Genymotion is an alternative to the stock Google emulator that comes with Android Studio. -However, it's only free for personal use. If you want to use the stock Google emulator, see below. +However, it's only free for personal use. If you want to use Genymotion, see below. 1. Download and install [Genymotion](https://www.genymotion.com/). 2. Open Genymotion. It might ask you to install VirtualBox unless you already have it. From c82856fb7a519edfd3d7b5c31e308e702fdb6237 Mon Sep 17 00:00:00 2001 From: Aaron Chiu Date: Fri, 27 May 2016 04:59:46 -0700 Subject: [PATCH 136/843] unbreak RN startup Reviewed By: lexs Differential Revision: D3352709 fbshipit-source-id: 56cdec2dee46ab1f011bed9aadd14ea464ec4163 --- .../react/XReactInstanceManagerImpl.java | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java index 57601d12f5def0..1b72f4d0a3f340 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java @@ -764,7 +764,7 @@ private ReactApplicationContext createReactContext( NativeModuleRegistry.Builder nativeRegistryBuilder = new NativeModuleRegistry.Builder(); JavaScriptModuleRegistry.Builder jsModulesBuilder = new JavaScriptModuleRegistry.Builder(); - ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext); + final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext); if (mUseDeveloperSupport) { reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager); } @@ -830,21 +830,28 @@ private ReactApplicationContext createReactContext( catalystInstance.addBridgeIdleDebugListener(mBridgeIdleDebugListener); } - reactContext.initializeWithInstance(catalystInstance); ReactMarker.logMarker(RUN_JS_BUNDLE_START); - catalystInstance.getReactQueueConfiguration().getJSQueueThread().runOnQueue(new Runnable() { - @Override - public void run() { - Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "runJSBundle"); - try { - catalystInstance.runJSBundle(); - } finally { - Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); - ReactMarker.logMarker(RUN_JS_BUNDLE_END); - } - } - }); + try { + catalystInstance.getReactQueueConfiguration().getJSQueueThread().callOnQueue( + new Callable() { + @Override + public Void call() throws Exception { + reactContext.initializeWithInstance(catalystInstance); + + Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "runJSBundle"); + try { + catalystInstance.runJSBundle(); + } finally { + Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); + ReactMarker.logMarker(RUN_JS_BUNDLE_END); + } + return null; + } + }).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } return reactContext; } From 26d3af3421f0768cc2d4aeb354068ace3f7581e3 Mon Sep 17 00:00:00 2001 From: Siqi Liu Date: Fri, 27 May 2016 07:43:22 -0700 Subject: [PATCH 137/843] Redbox: skip column number if it is 0 in Android. Summary: As for symbolicated stack trace in the red box in Android, make column number not shown if it's zero. Format Before: {F61180667} Format After: {F61180666} Reviewed By: mkonicek Differential Revision: D3358317 fbshipit-source-id: 87981e678e22ab9f483727002175c8835941ceee --- .../main/java/com/facebook/react/devsupport/RedBoxDialog.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java index eefb1d7359de21..914f8db575f80c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java @@ -124,7 +124,8 @@ public View getView(int position, View convertView, ViewGroup parent) { FrameViewHolder holder = (FrameViewHolder) convertView.getTag(); holder.mMethodView.setText(frame.getMethod()); final int column = frame.getColumn(); - final String columnString = column < 0 ? "" : ":" + column; + // If the column is 0, don't show it in red box. + final String columnString = column <= 0 ? "" : ":" + column; holder.mFileView.setText(frame.getFileName() + ":" + frame.getLine() + columnString); return convertView; } From 8d4b15d25388a63eee3530d704b26974e6ca4c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= Date: Fri, 27 May 2016 09:10:36 -0700 Subject: [PATCH 138/843] Add Ticketea Summary: Add Ticketea App Closes https://github.com/facebook/react-native/pull/7776 Differential Revision: D3358599 Pulled By: mkonicek fbshipit-source-id: 04d769ddbdbf10bb5da3817c5f0f3c1c6e23307f --- website/src/react-native/showcase.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/website/src/react-native/showcase.js b/website/src/react-native/showcase.js index 8ec9ea8614ec3f..892dc955fb972f 100644 --- a/website/src/react-native/showcase.js +++ b/website/src/react-native/showcase.js @@ -887,6 +887,13 @@ var apps = [ link: 'https://itunes.apple.com/us/app/thai-tone/id1064086189?mt=8', author: 'Alexey Ledak', }, + { + name: 'Ticketea', + icon: 'http://f.cl.ly/items/0n3g3x2t0W0a0d0b1F0C/tkt-icon.png', + linkAppStore: 'https://itunes.apple.com/es/app/entradas-teatro-y-conciertos/id1060067658?mt=8', + linkPlayStore: 'https://play.google.com/store/apps/details?id=com.ticketea.geminis', + author: 'Ticketea (@ticketeaeng)', + }, { name: 'Tong Xing Wang', icon: 'http://a3.mzstatic.com/us/r30/Purple1/v4/7d/52/a7/7d52a71f-9532-82a5-b92f-87076624fdb2/icon175x175.jpeg', From 60e0d2c676b91adf909a571f2fe0359e2b5ac02e Mon Sep 17 00:00:00 2001 From: Siqi Liu Date: Fri, 27 May 2016 09:45:02 -0700 Subject: [PATCH 139/843] Make "Debug JS" dev menu option persist across app restarts on RN Android Summary: 1. Make "Remote JS Debug" and "Start/Stop Profile" options persist across app restarts. 2. Check and confirm: - All options in the Android dev menu are persisted now. - The behavior is the same on Android and iOS now. Reviewed By: mkonicek Differential Revision: D3340097 fbshipit-source-id: 4087b6605031c650e164282244cedb006f8f6fd3 --- .../react/testing/ReactSettingsForTests.java | 10 ++++++++++ .../react/XReactInstanceManagerImpl.java | 12 ++++++++++-- .../react/devsupport/DevInternalSettings.java | 11 +++++++++++ .../react/devsupport/DevSupportManagerImpl.java | 17 ++++++++--------- .../react/modules/debug/DeveloperSettings.java | 11 +++++++++++ 5 files changed, 50 insertions(+), 11 deletions(-) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactSettingsForTests.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactSettingsForTests.java index 6d0946d8dd5a81..569f1c7bd27eae 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactSettingsForTests.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactSettingsForTests.java @@ -36,4 +36,14 @@ public boolean isJSMinifyEnabled() { public boolean isElementInspectorEnabled() { return false; } + + @Override + public boolean isRemoteJSDebugEnabled() { + return false; + } + + @Override + public void setRemoteJSDebugEnabled(boolean remoteJSDebugEnabled) { + + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java index 1b72f4d0a3f340..9f5ed6c1960d9a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java @@ -59,6 +59,7 @@ import com.facebook.react.devsupport.ReactInstanceDevCommandsHandler; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.modules.debug.DeveloperSettings; import com.facebook.react.uimanager.AppRegistry; import com.facebook.react.uimanager.DisplayMetricsHolder; import com.facebook.react.uimanager.UIImplementationProvider; @@ -362,8 +363,13 @@ private void recreateReactContextInBackgroundInner() { UiThreadUtil.assertOnUiThread(); if (mUseDeveloperSupport && mJSMainModuleName != null) { - if (mDevSupportManager.hasUpToDateJSBundleInCache()) { - // If there is a up-to-date bundle downloaded from server, always use that + final DeveloperSettings devSettings = mDevSupportManager.getDevSettings(); + + // If remote JS debugging is enabled, load from dev server. + if (mDevSupportManager.hasUpToDateJSBundleInCache() && + !devSettings.isRemoteJSDebugEnabled()) { + // If there is a up-to-date bundle downloaded from server, + // with remote JS debugging disabled, always use that. onJSBundleLoadedFromServer(); } else if (mJSBundleFile == null) { mDevSupportManager.handleReloadJS(); @@ -379,6 +385,8 @@ public void run() { if (packagerIsRunning) { mDevSupportManager.handleReloadJS(); } else { + // If dev server is down, disable the remote JS debugging. + devSettings.setRemoteJSDebugEnabled(false); recreateReactContextInBackgroundFromBundleFile(); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java index 349cd22dbbefe0..2899be2b8d5886 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java @@ -36,6 +36,7 @@ public class DevInternalSettings implements private static final String PREFS_RELOAD_ON_JS_CHANGE_KEY = "reload_on_js_change"; private static final String PREFS_INSPECTOR_DEBUG_KEY = "inspector_debug"; private static final String PREFS_HOT_MODULE_REPLACEMENT_KEY = "hot_module_replacement"; + private static final String PREFS_REMOTE_JS_DEBUG_KEY = "remote_js_debug"; private final SharedPreferences mPreferences; private final DevSupportManager mDebugManager; @@ -108,4 +109,14 @@ public boolean isElementInspectorEnabled() { public void setElementInspectorEnabled(boolean enabled) { mPreferences.edit().putBoolean(PREFS_INSPECTOR_DEBUG_KEY, enabled).apply(); } + + @Override + public boolean isRemoteJSDebugEnabled() { + return mPreferences.getBoolean(PREFS_REMOTE_JS_DEBUG_KEY, false); + } + + @Override + public void setRemoteJSDebugEnabled(boolean remoteJSDebugEnabled) { + mPreferences.edit().putBoolean(PREFS_REMOTE_JS_DEBUG_KEY, remoteJSDebugEnabled).apply(); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index 3925a0e8257356..55057a8eda0b6f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -106,7 +106,6 @@ public class DevSupportManagerImpl implements DevSupportManager { private @Nullable DebugOverlayController mDebugOverlayController; private @Nullable ReactContext mCurrentContext; private DevInternalSettings mDevSettings; - private boolean mIsUsingJSProxy = false; private boolean mIsReceiverRegistered = false; private boolean mIsShakeDetectorStarted = false; private boolean mIsDevSupportEnabled = false; @@ -139,10 +138,10 @@ public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (DevServerHelper.getReloadAppAction(context).equals(action)) { if (intent.getBooleanExtra(DevServerHelper.RELOAD_APP_EXTRA_JS_PROXY, false)) { - mIsUsingJSProxy = true; + mDevSettings.setRemoteJSDebugEnabled(true); mDevServerHelper.launchJSDevtools(); } else { - mIsUsingJSProxy = false; + mDevSettings.setRemoteJSDebugEnabled(false); } handleReloadJS(); } @@ -265,13 +264,13 @@ public void onOptionSelected() { } }); options.put( - mIsUsingJSProxy ? + mDevSettings.isRemoteJSDebugEnabled() ? mApplicationContext.getString(R.string.catalyst_debugjs_off) : mApplicationContext.getString(R.string.catalyst_debugjs), new DevOptionHandler() { @Override public void onOptionSelected() { - mIsUsingJSProxy = !mIsUsingJSProxy; + mDevSettings.setRemoteJSDebugEnabled(!mDevSettings.isRemoteJSDebugEnabled()); handleReloadJS(); } }); @@ -297,7 +296,7 @@ public void onOptionSelected() { } }); options.put( - mApplicationContext.getString(R.string.catalyst_element_inspector), + mApplicationContext.getString(R.string.catalyst_element_inspector), new DevOptionHandler() { @Override public void onOptionSelected() { @@ -340,7 +339,7 @@ public void onOptionSelected() { mCurrentContext.getCatalystInstance().supportsProfiling()) { options.put( mApplicationContext.getString( - mIsCurrentlyProfiling ? R.string.catalyst_stop_profile : + mIsCurrentlyProfiling ? R.string.catalyst_stop_profile : R.string.catalyst_start_profile), new DevOptionHandler() { @Override @@ -581,13 +580,13 @@ public void handleReloadJS() { ProgressDialog progressDialog = new ProgressDialog(mApplicationContext); progressDialog.setTitle(R.string.catalyst_jsload_title); progressDialog.setMessage(mApplicationContext.getString( - mIsUsingJSProxy ? R.string.catalyst_remotedbg_message : R.string.catalyst_jsload_message)); + mDevSettings.isRemoteJSDebugEnabled() ? R.string.catalyst_remotedbg_message : R.string.catalyst_jsload_message)); progressDialog.setIndeterminate(true); progressDialog.setCancelable(false); progressDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); progressDialog.show(); - if (mIsUsingJSProxy) { + if (mDevSettings.isRemoteJSDebugEnabled()) { reloadJSInProxyMode(progressDialog); } else { reloadJSFromServer(progressDialog); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DeveloperSettings.java b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DeveloperSettings.java index 5defb93865d605..afb72dd87c01fc 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DeveloperSettings.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/DeveloperSettings.java @@ -38,4 +38,15 @@ public interface DeveloperSettings { * @return Whether element inspector is enabled. */ boolean isElementInspectorEnabled(); + + /** + * @return Whether remote JS debugging is enabled. + */ + boolean isRemoteJSDebugEnabled(); + + /** + * Enable/Disable remote JS debugging. + */ + void setRemoteJSDebugEnabled(boolean remoteJSDebugEnabled); + } From a4b5f1bf10020ee7f46fc3629d1cbb0e6d6dca4b Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Fri, 27 May 2016 09:49:15 -0700 Subject: [PATCH 140/843] Test perf effect of reverting D3269333 Reviewed By: javache Differential Revision: D3346235 fbshipit-source-id: 2008f8fb9df5d61da59bb0067b25acd5a71f256f --- Examples/UIExplorer/TextExample.ios.js | 5 +- Libraries/Image/Image.ios.js | 12 ++ .../Image/RCTImage.xcodeproj/project.pbxproj | 10 ++ Libraries/Image/RCTImageView.h | 3 +- Libraries/Image/RCTShadowVirtualImage.h | 28 ++++ Libraries/Image/RCTShadowVirtualImage.m | 82 ++++++++++++ Libraries/Image/RCTVirtualImageManager.h | 14 ++ Libraries/Image/RCTVirtualImageManager.m | 25 ++++ Libraries/Text/RCTShadowText.m | 121 +++++------------- Libraries/Text/RCTText.m | 20 --- Libraries/Text/RCTTextManager.m | 13 -- React/React.xcodeproj/project.pbxproj | 2 + React/Views/RCTImageComponent.h | 19 +++ React/Views/RCTShadowView.h | 20 --- React/Views/RCTShadowView.m | 39 ------ docs/Text.md | 14 -- 16 files changed, 225 insertions(+), 202 deletions(-) create mode 100644 Libraries/Image/RCTShadowVirtualImage.h create mode 100644 Libraries/Image/RCTShadowVirtualImage.m create mode 100644 Libraries/Image/RCTVirtualImageManager.h create mode 100644 Libraries/Image/RCTVirtualImageManager.m create mode 100644 React/Views/RCTImageComponent.h diff --git a/Examples/UIExplorer/TextExample.ios.js b/Examples/UIExplorer/TextExample.ios.js index 8c7b4ff86ef006..e21b7c3bfd2e91 100644 --- a/Examples/UIExplorer/TextExample.ios.js +++ b/Examples/UIExplorer/TextExample.ios.js @@ -422,13 +422,12 @@ exports.examples = [ ); }, }, { - title: 'Inline views', + title: 'Inline images', render: function() { return ( - This text contains an inline blue view and - an inline image . Neat, huh? + This text contains an inline image . Neat, huh? ); diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index b2f4718f7f120d..7c496656869bf2 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -201,6 +201,10 @@ var Image = React.createClass({ validAttributes: ReactNativeViewAttributes.UIView }, + contextTypes: { + isInAParentText: React.PropTypes.bool + }, + render: function() { var source = resolveAssetSource(this.props.source) || {}; var {width, height, uri} = source; @@ -221,6 +225,13 @@ var Image = React.createClass({ console.warn('The component requires a `source` property rather than `src`.'); } + if (this.context.isInAParentText) { + RawImage = RCTVirtualImage; + if (!width || !height) { + console.warn('You must specify a width and height for the image %s', uri); + } + } + return ( +#import "RCTImageComponent.h" #import "RCTResizeMode.h" @class RCTBridge; @class RCTImageSource; -@interface RCTImageView : UIImageView +@interface RCTImageView : UIImageView - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; diff --git a/Libraries/Image/RCTShadowVirtualImage.h b/Libraries/Image/RCTShadowVirtualImage.h new file mode 100644 index 00000000000000..b9623f736f239e --- /dev/null +++ b/Libraries/Image/RCTShadowVirtualImage.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTShadowView.h" +#import "RCTImageComponent.h" +#import "RCTImageSource.h" +#import "RCTResizeMode.h" + +@class RCTBridge; + +/** + * Shadow image component, used for embedding images in non-view contexts such + * as text. This is NOT used for ordinary views. + */ +@interface RCTShadowVirtualImage : RCTShadowView + +- (instancetype)initWithBridge:(RCTBridge *)bridge; + +@property (nonatomic, strong) RCTImageSource *source; +@property (nonatomic, assign) RCTResizeMode resizeMode; + +@end diff --git a/Libraries/Image/RCTShadowVirtualImage.m b/Libraries/Image/RCTShadowVirtualImage.m new file mode 100644 index 00000000000000..757a48c24ea056 --- /dev/null +++ b/Libraries/Image/RCTShadowVirtualImage.m @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTShadowVirtualImage.h" +#import "RCTImageLoader.h" +#import "RCTImageUtils.h" +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTUIManager.h" +#import "RCTUtils.h" + +@implementation RCTShadowVirtualImage +{ + RCTBridge *_bridge; + RCTImageLoaderCancellationBlock _cancellationBlock; +} + +@synthesize image = _image; + +- (instancetype)initWithBridge:(RCTBridge *)bridge +{ + if ((self = [super init])) { + _bridge = bridge; + } + return self; +} + +RCT_NOT_IMPLEMENTED(-(instancetype)init) + +- (void)didSetProps:(NSArray *)changedProps +{ + [super didSetProps:changedProps]; + + if (changedProps.count == 0) { + // No need to reload image + return; + } + + // Cancel previous request + if (_cancellationBlock) { + _cancellationBlock(); + } + + CGSize imageSize = { + RCTZeroIfNaN(self.width), + RCTZeroIfNaN(self.height), + }; + + __weak RCTShadowVirtualImage *weakSelf = self; + _cancellationBlock = [_bridge.imageLoader loadImageWithTag:_source.imageURL.absoluteString + size:imageSize + scale:RCTScreenScale() + resizeMode:_resizeMode + progressBlock:nil + completionBlock:^(NSError *error, UIImage *image) { + + dispatch_async(_bridge.uiManager.methodQueue, ^{ + RCTShadowVirtualImage *strongSelf = weakSelf; + if (![_source isEqual:strongSelf.source]) { + // Bail out if source has changed since we started loading + return; + } + strongSelf->_image = image; + [strongSelf dirtyText]; + }); + }]; +} + +- (void)dealloc +{ + if (_cancellationBlock) { + _cancellationBlock(); + } +} + +@end diff --git a/Libraries/Image/RCTVirtualImageManager.h b/Libraries/Image/RCTVirtualImageManager.h new file mode 100644 index 00000000000000..b92896235d7f79 --- /dev/null +++ b/Libraries/Image/RCTVirtualImageManager.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTViewManager.h" + +@interface RCTVirtualImageManager : RCTViewManager + +@end diff --git a/Libraries/Image/RCTVirtualImageManager.m b/Libraries/Image/RCTVirtualImageManager.m new file mode 100644 index 00000000000000..6311010f4ce263 --- /dev/null +++ b/Libraries/Image/RCTVirtualImageManager.m @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTVirtualImageManager.h" +#import "RCTShadowVirtualImage.h" + +@implementation RCTVirtualImageManager + +RCT_EXPORT_MODULE() + +- (RCTShadowView *)shadowView +{ + return [[RCTShadowVirtualImage alloc] initWithBridge:self.bridge]; +} + +RCT_EXPORT_SHADOW_PROPERTY(source, RCTImageSource) +RCT_EXPORT_SHADOW_PROPERTY(resizeMode, UIViewContentMode) + +@end diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index 3220cca7615391..adf7be667d5520 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -13,12 +13,12 @@ #import "RCTUIManager.h" #import "RCTBridge.h" #import "RCTConvert.h" +#import "RCTImageComponent.h" #import "RCTLog.h" #import "RCTShadowRawText.h" #import "RCTText.h" #import "RCTUtils.h" -NSString *const RCTShadowViewAttributeName = @"RCTShadowViewAttributeName"; NSString *const RCTIsHighlightedAttributeName = @"IsHighlightedAttributeName"; NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName"; @@ -114,45 +114,6 @@ - (void)applyLayoutNode:(css_node_t *)node [self dirtyPropagation]; } -- (void)applyLayoutToChildren:(css_node_t *)node - viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame - absolutePosition:(CGPoint)absolutePosition -{ - // Run layout on subviews. - NSTextStorage *textStorage = [self buildTextStorageForWidth:self.frame.size.width widthMode:CSS_MEASURE_MODE_EXACTLY]; - NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; - NSTextContainer *textContainer = layoutManager.textContainers.firstObject; - NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; - NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL]; - [layoutManager.textStorage enumerateAttribute:RCTShadowViewAttributeName inRange:characterRange options:0 usingBlock:^(RCTShadowView *child, NSRange range, BOOL *_) { - if (child != nil) { - css_node_t *childNode = child.cssNode; - float width = childNode->style.dimensions[CSS_WIDTH]; - float height = childNode->style.dimensions[CSS_HEIGHT]; - if (isUndefined(width) || isUndefined(height)) { - RCTLogError(@"Views nested within a must have a width and height"); - } - UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:range.location effectiveRange:nil]; - CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range inTextContainer:textContainer]; - CGRect childFrame = {{ - RCTRoundPixelValue(glyphRect.origin.x), - RCTRoundPixelValue(glyphRect.origin.y + glyphRect.size.height - height + font.descender) - }, { - RCTRoundPixelValue(width), - RCTRoundPixelValue(height) - }}; - - NSRange truncatedGlyphRange = [layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:range.location]; - BOOL childIsTruncated = NSIntersectionRange(range, truncatedGlyphRange).length != 0; - - [child collectUpdatedFrames:viewsWithNewFrame - withFrame:childFrame - hidden:childIsTruncated - absolutePosition:absolutePosition]; - } - }]; -} - - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width widthMode:(css_measure_mode_t)widthMode { if (_cachedTextStorage && width == _cachedTextStorageWidth && widthMode == _cachedTextStorageWidthMode) { @@ -238,48 +199,33 @@ - (NSAttributedString *)_attributedStringWithFontFamily:(NSString *)fontFamily _effectiveLetterSpacing = letterSpacing.doubleValue; - UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily - size:fontSize weight:fontWeight style:fontStyle - scaleMultiplier:_allowFontScaling ? _fontSizeMultiplier : 1.0]; - - CGFloat heightOfTallestSubview = 0.0; NSMutableAttributedString *attributedString = [NSMutableAttributedString new]; for (RCTShadowView *child in [self reactSubviews]) { if ([child isKindOfClass:[RCTShadowText class]]) { RCTShadowText *shadowText = (RCTShadowText *)child; [attributedString appendAttributedString: - [shadowText _attributedStringWithFontFamily:fontFamily - fontSize:fontSize - fontWeight:fontWeight - fontStyle:fontStyle - letterSpacing:letterSpacing - useBackgroundColor:YES - foregroundColor:shadowText.color ?: foregroundColor - backgroundColor:shadowText.backgroundColor ?: backgroundColor - opacity:opacity * shadowText.opacity]]; - [child setTextComputed]; + [shadowText _attributedStringWithFontFamily:fontFamily + fontSize:fontSize + fontWeight:fontWeight + fontStyle:fontStyle + letterSpacing:letterSpacing + useBackgroundColor:YES + foregroundColor:shadowText.color ?: foregroundColor + backgroundColor:shadowText.backgroundColor ?: backgroundColor + opacity:opacity * shadowText.opacity]]; } else if ([child isKindOfClass:[RCTShadowRawText class]]) { RCTShadowRawText *shadowRawText = (RCTShadowRawText *)child; [attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:shadowRawText.text ?: @""]]; - [child setTextComputed]; + } else if ([child conformsToProtocol:@protocol(RCTImageComponent)]) { + NSTextAttachment *imageAttachment = [NSTextAttachment new]; + imageAttachment.image = ((id)child).image; + imageAttachment.bounds = (CGRect){CGPointZero, {RCTZeroIfNaN(child.width), RCTZeroIfNaN(child.height)}}; + [attributedString appendAttributedString:[NSAttributedString attributedStringWithAttachment:imageAttachment]]; } else { - float width = child.cssNode->style.dimensions[CSS_WIDTH]; - float height = child.cssNode->style.dimensions[CSS_HEIGHT]; - if (isUndefined(width) || isUndefined(height)) { - RCTLogError(@"Views nested within a must have a width and height"); - } - NSTextAttachment *attachment = [NSTextAttachment new]; - attachment.bounds = (CGRect){CGPointZero, {width, height}}; - NSMutableAttributedString *attachmentString = [NSMutableAttributedString new]; - [attachmentString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]]; - [attachmentString addAttribute:RCTShadowViewAttributeName value:child range:(NSRange){0, attachmentString.length}]; - [attributedString appendAttributedString:attachmentString]; - if (height > heightOfTallestSubview) { - heightOfTallestSubview = height; - } - // Don't call setTextComputed on this child. RCTTextManager takes care of - // processing inline UIViews. + RCTLogError(@" can't have any children except , or raw strings"); } + + [child setTextComputed]; } [self _addAttribute:NSForegroundColorAttributeName @@ -295,10 +241,13 @@ - (NSAttributedString *)_attributedStringWithFontFamily:(NSString *)fontFamily toAttributedString:attributedString]; } + UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily + size:fontSize weight:fontWeight style:fontStyle + scaleMultiplier:_allowFontScaling ? _fontSizeMultiplier : 1.0]; [self _addAttribute:NSFontAttributeName withValue:font toAttributedString:attributedString]; [self _addAttribute:NSKernAttributeName withValue:letterSpacing toAttributedString:attributedString]; [self _addAttribute:RCTReactTagAttributeName withValue:self.reactTag toAttributedString:attributedString]; - [self _setParagraphStyleOnAttributedString:attributedString heightOfTallestSubview:heightOfTallestSubview]; + [self _setParagraphStyleOnAttributedString:attributedString]; // create a non-mutable attributedString for use by the Text system which avoids copies down the line _cachedAttributedString = [[NSAttributedString alloc] initWithAttributedString:attributedString]; @@ -321,7 +270,6 @@ - (void)_addAttribute:(NSString *)attribute withValue:(id)attributeValue toAttri * varying lineHeights, we simply take the max. */ - (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attributedString - heightOfTallestSubview:(CGFloat)heightOfTallestSubview { // check if we have lineHeight set on self __block BOOL hasParagraphStyle = NO; @@ -329,7 +277,9 @@ - (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attrib hasParagraphStyle = YES; } - __block float newLineHeight = _lineHeight ?: 0.0; + if (!_lineHeight) { + self.lineHeight = 0.0; + } CGFloat fontSizeMultiplier = _allowFontScaling ? _fontSizeMultiplier : 1.0; @@ -338,25 +288,15 @@ - (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attrib if (value) { NSParagraphStyle *paragraphStyle = (NSParagraphStyle *)value; CGFloat maximumLineHeight = round(paragraphStyle.maximumLineHeight / fontSizeMultiplier); - if (maximumLineHeight > newLineHeight) { - newLineHeight = maximumLineHeight; + if (maximumLineHeight > self.lineHeight) { + self.lineHeight = maximumLineHeight; } hasParagraphStyle = YES; } }]; - if (self.lineHeight != newLineHeight) { - self.lineHeight = newLineHeight; - } - - NSTextAlignment newTextAlign = _textAlign ?: NSTextAlignmentNatural; - if (self.textAlign != newTextAlign) { - self.textAlign = newTextAlign; - } - NSWritingDirection newWritingDirection = _writingDirection ?: NSWritingDirectionNatural; - if (self.writingDirection != newWritingDirection) { - self.writingDirection = newWritingDirection; - } + self.textAlign = _textAlign ?: NSTextAlignmentNatural; + self.writingDirection = _writingDirection ?: NSWritingDirectionNatural; // if we found anything, set it :D if (hasParagraphStyle) { @@ -364,9 +304,6 @@ - (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attrib paragraphStyle.alignment = _textAlign; paragraphStyle.baseWritingDirection = _writingDirection; CGFloat lineHeight = round(_lineHeight * fontSizeMultiplier); - if (heightOfTallestSubview > lineHeight) { - lineHeight = ceilf(heightOfTallestSubview); - } paragraphStyle.minimumLineHeight = lineHeight; paragraphStyle.maximumLineHeight = lineHeight; [attributedString addAttribute:NSParagraphStyleAttributeName diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index fd37c4b75ff265..e57b93aa191f7a 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -13,17 +13,6 @@ #import "RCTUtils.h" #import "UIView+React.h" -static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDescendants) -{ - for (UIView *child in view.reactSubviews) { - if ([child isKindOfClass:[RCTText class]]) { - collectNonTextDescendants((RCTText *)child, nonTextDescendants); - } else { - [nonTextDescendants addObject:child]; - } - } -} - @implementation RCTText { NSTextStorage *_textStorage; @@ -130,15 +119,6 @@ - (void)drawRect:(CGRect)rect [_highlightLayer removeFromSuperlayer]; _highlightLayer = nil; } - - for (UIView *child in [self subviews]) { - [child removeFromSuperview]; - } - NSMutableArray *nonTextDescendants = [NSMutableArray new]; - collectNonTextDescendants(self, nonTextDescendants); - for (UIView *child in nonTextDescendants) { - [self addSubview:child]; - } } - (NSNumber *)reactTagAtPoint:(CGPoint)point diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index 9104ea72c073f6..92fa589496d2a0 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -20,18 +20,6 @@ #import "RCTTextView.h" #import "UIView+React.h" -static void collectDirtyNonTextDescendants(RCTShadowText *shadowView, NSMutableArray *nonTextDescendants) { - for (RCTShadowView *child in shadowView.reactSubviews) { - if ([child isKindOfClass:[RCTShadowText class]]) { - collectDirtyNonTextDescendants((RCTShadowText *)child, nonTextDescendants); - } else if ([child isKindOfClass:[RCTShadowRawText class]]) { - // no-op - } else if ([child isTextDirty]) { - [nonTextDescendants addObject:child]; - } - } -} - @interface RCTShadowText (Private) - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width widthMode:(css_measure_mode_t)widthMode; @@ -97,7 +85,6 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(NSDictionary tag. Not rendering string: '%@'", [(RCTShadowRawText *)shadowView text]); diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index c967aea8534d99..ed4eaa243233c6 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -216,6 +216,7 @@ 13E067501A70F44B002CDEE1 /* RCTView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTView.m; sourceTree = ""; }; 13E067531A70F44B002CDEE1 /* UIView+React.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+React.h"; sourceTree = ""; }; 13E067541A70F44B002CDEE1 /* UIView+React.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+React.m"; sourceTree = ""; }; + 13EF7F441BC69646003F47DD /* RCTImageComponent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTImageComponent.h; sourceTree = ""; }; 13F17A831B8493E5007D4C75 /* RCTRedBox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRedBox.h; sourceTree = ""; }; 13F17A841B8493E5007D4C75 /* RCTRedBox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRedBox.m; sourceTree = ""; }; 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptLoader.h; sourceTree = ""; }; @@ -384,6 +385,7 @@ 13AB90BF1B6FA36700713B4F /* RCTComponentData.h */, 13456E941ADAD482009F94A7 /* RCTConvert+MapKit.h */, 13AB90C01B6FA36700713B4F /* RCTComponentData.m */, + 13EF7F441BC69646003F47DD /* RCTImageComponent.h */, 13456E911ADAD2DE009F94A7 /* RCTConvert+CoreLocation.h */, 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */, 13456E951ADAD482009F94A7 /* RCTConvert+MapKit.m */, diff --git a/React/Views/RCTImageComponent.h b/React/Views/RCTImageComponent.h new file mode 100644 index 00000000000000..a6916c9aa63be0 --- /dev/null +++ b/React/Views/RCTImageComponent.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +/** + * Generic interface for components that contain an image. + */ +@protocol RCTImageComponent + +@property (nonatomic, strong, readonly) UIImage *image; + +@end diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index 95bae0e21b30dc..ad09ae9f061783 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -139,32 +139,12 @@ typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry - (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties NS_REQUIRES_SUPER; -/** - * Can be called by a parent on a child in order to calculate all views whose frame needs - * updating in that branch. Adds these frames to `viewsWithNewFrame`. Useful if layout - * enters a view where flex doesn't apply (e.g. Text) and then you want to resume flex - * layout on a subview. - */ -- (void)collectUpdatedFrames:(NSMutableSet *)viewsWithNewFrame - withFrame:(CGRect)frame - hidden:(BOOL)hidden - absolutePosition:(CGPoint)absolutePosition; - /** * Recursively apply layout to children. - * The job of applyLayoutNode:viewsWithNewFrame:absolutePosition is to apply the layout to - * this node. - * The job of applyLayoutToChildren:viewsWithNewFrame:absolutePosition is to enumerate the - * children and tell them to apply layout. - * The functionality is split into two methods so subclasses can override applyLayoutToChildren - * while using the default implementation of applyLayoutNode. */ - (void)applyLayoutNode:(css_node_t *)node viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame absolutePosition:(CGPoint)absolutePosition NS_REQUIRES_SUPER; -- (void)applyLayoutToChildren:(css_node_t *)node - viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame - absolutePosition:(CGPoint)absolutePosition; /** * The following are implementation details exposed to subclasses. Do not call them directly diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index 0472b272c7dd72..d55e80c624c96e 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -157,13 +157,6 @@ - (void)applyLayoutNode:(css_node_t *)node absolutePosition.x += node->layout.position[CSS_LEFT]; absolutePosition.y += node->layout.position[CSS_TOP]; - [self applyLayoutToChildren:node viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition]; -} - -- (void)applyLayoutToChildren:(css_node_t *)node - viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame - absolutePosition:(CGPoint)absolutePosition -{ for (int i = 0; i < node->children_count; ++i) { RCTShadowView *child = (RCTShadowView *)_reactSubviews[i]; [child applyLayoutNode:node->get_child(node->context, i) @@ -216,37 +209,6 @@ - (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks } } -- (void)collectUpdatedFrames:(NSMutableSet *)viewsWithNewFrame - withFrame:(CGRect)frame - hidden:(BOOL)hidden - absolutePosition:(CGPoint)absolutePosition -{ - if (_hidden != hidden) { - // The hidden state has changed. Even if the frame hasn't changed, add - // this ShadowView to viewsWithNewFrame so the UIManager will process - // this ShadowView's UIView and update its hidden state. - _hidden = hidden; - [viewsWithNewFrame addObject:self]; - } - - if (!CGRectEqualToRect(frame, _frame)) { - _cssNode->style.position_type = CSS_POSITION_ABSOLUTE; - _cssNode->style.dimensions[CSS_WIDTH] = frame.size.width; - _cssNode->style.dimensions[CSS_HEIGHT] = frame.size.height; - _cssNode->style.position[CSS_LEFT] = frame.origin.x; - _cssNode->style.position[CSS_TOP] = frame.origin.y; - // Our parent has asked us to change our cssNode->styles. Dirty the layout - // so that we can rerun layout on this node. The request came from our parent - // so there's no need to dirty our ancestors by calling dirtyLayout. - _layoutLifecycle = RCTUpdateLifecycleDirtied; - } - - [self fillCSSNode:_cssNode]; - resetNodeLayout(self.cssNode); - layoutNode(_cssNode, frame.size.width, frame.size.height, CSS_DIRECTION_INHERIT); - [self applyLayoutNode:_cssNode viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition]; -} - - (CGRect)measureLayoutRelativeToAncestor:(RCTShadowView *)ancestor { CGPoint offset = CGPointZero; @@ -497,7 +459,6 @@ - (void)set##setProp:(CGFloat)value \ { \ _cssNode->style.dimensions[CSS_##cssProp] = value; \ [self dirtyLayout]; \ - [self dirtyText]; \ } \ - (CGFloat)getProp \ { \ diff --git a/docs/Text.md b/docs/Text.md index 48853606e432fe..800137b03b0b71 100644 --- a/docs/Text.md +++ b/docs/Text.md @@ -21,20 +21,6 @@ Behind the scenes, React Native converts this to a flat `NSAttributedString` or 9-17: bold, red ``` -## Nested Views (iOS Only) - -On iOS, you can nest views within your Text component. Here's an example: - -```javascript - - There is a blue square - - in between my text. - -``` - -In order to use this feature, you must give the view a `width` and a `height`. - ## Containers The `` element is special relative to layout: everything inside is no longer using the flexbox layout but using text layout. This means that elements inside of a `` are no longer rectangles, but wrap when they see the end of the line. From 1623e34c5135fc50d67272c30c123426d4b0036d Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Fri, 27 May 2016 10:14:12 -0700 Subject: [PATCH 141/843] Updated Linking and PushNotificationIOS modules to use NativeEventEmitter Reviewed By: javache Differential Revision: D3352819 fbshipit-source-id: d218791a16aba597d2544691ef993711cf00522c --- Libraries/Linking/Linking.js | 71 +++------ Libraries/LinkingIOS/LinkingIOS.js | 145 +----------------- Libraries/LinkingIOS/RCTLinkingManager.h | 4 +- Libraries/LinkingIOS/RCTLinkingManager.m | 70 ++++----- .../PushNotificationIOS.js | 79 ++++++---- .../RCTPushNotificationManager.h | 6 +- .../RCTPushNotificationManager.m | 63 ++++---- docs/LinkingLibraries.md | 2 +- website/server/extractDocs.js | 1 - 9 files changed, 139 insertions(+), 302 deletions(-) diff --git a/Libraries/Linking/Linking.js b/Libraries/Linking/Linking.js index e2dfcd441e7adc..6bafe4ec9c0d9b 100644 --- a/Libraries/Linking/Linking.js +++ b/Libraries/Linking/Linking.js @@ -11,19 +11,14 @@ */ 'use strict'; +const NativeEventEmitter = require('NativeEventEmitter'); +const NativeModules = require('NativeModules'); const Platform = require('Platform'); -const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); -const { - IntentAndroid, - LinkingManager: LinkingManagerIOS -} = require('NativeModules'); -const LinkingManager = Platform.OS === 'android' ? IntentAndroid : LinkingManagerIOS; -const invariant = require('fbjs/lib/invariant'); -const Map = require('Map'); -const _notifHandlers = new Map(); +const invariant = require('fbjs/lib/invariant'); -const DEVICE_NOTIF_EVENT = 'openURL'; +const LinkingManager = Platform.OS === 'android' ? + NativeModules.IntentAndroid : NativeModules.LinkingManager; /** * `Linking` gives you a general interface to interact with both incoming @@ -110,26 +105,23 @@ const DEVICE_NOTIF_EVENT = 'openURL'; * }).catch(err => console.error('An error occurred', err)); * ``` */ -class Linking { +class Linking extends NativeEventEmitter { + + constructor() { + super(LinkingManager); + } + /** * Add a handler to Linking changes by listening to the `url` event type * and providing the handler * * @platform ios */ - static addEventListener(type: string, handler: Function) { + addEventListener(type: string, handler: Function) { if (Platform.OS === 'android') { - console.warn('Linking.addEventListener is not supported on Android'); + console.warn('Linking.addEventListener is not supported on Android'); } else { - invariant( - type === 'url', - 'Linking only supports `url` events' - ); - var listener = RCTDeviceEventEmitter.addListener( - DEVICE_NOTIF_EVENT, - handler - ); - _notifHandlers.set(handler, listener); + this.addListener(type, handler); } } @@ -138,20 +130,11 @@ class Linking { * * @platform ios */ - static removeEventListener(type: string, handler: Function ) { + removeEventListener(type: string, handler: Function ) { if (Platform.OS === 'android') { - console.warn('Linking.removeEventListener is not supported on Android'); + console.warn('Linking.removeEventListener is not supported on Android'); } else { - invariant( - type === 'url', - 'Linking only supports `url` events' - ); - var listener = _notifHandlers.get(handler); - if (!listener) { - return; - } - listener.remove(); - _notifHandlers.delete(handler); + this.removeListener(type, handler); } } @@ -166,7 +149,7 @@ class Linking { * * NOTE: For web URLs, the protocol ("http://", "https://") must be set accordingly! */ - static openURL(url: string): Promise { + openURL(url: string): Promise { this._validateURL(url); return LinkingManager.openURL(url); } @@ -177,30 +160,26 @@ class Linking { * NOTE: For web URLs, the protocol ("http://", "https://") must be set accordingly! * * NOTE: As of iOS 9, your app needs to provide the `LSApplicationQueriesSchemes` key - * inside `Info.plist`. + * inside `Info.plist` or canOpenURL will always return false. * * @param URL the URL to open */ - static canOpenURL(url: string): Promise { + canOpenURL(url: string): Promise { this._validateURL(url); return LinkingManager.canOpenURL(url); } /** - * If the app launch was triggered by an app link with, + * If the app launch was triggered by an app link, * it will give the link url, otherwise it will give `null` * * NOTE: To support deep linking on Android, refer http://developer.android.com/training/app-indexing/deep-linking.html#handling-intents */ - static getInitialURL(): Promise { - if (Platform.OS === 'android') { - return IntentAndroid.getInitialURL(); - } else { - return Promise.resolve(LinkingManagerIOS.initialURL); - } + getInitialURL(): Promise { + return LinkingManager.getInitialURL(); } - static _validateURL(url: string) { + _validateURL(url: string) { invariant( typeof url === 'string', 'Invalid URL: should be a string. Was: ' + url @@ -212,4 +191,4 @@ class Linking { } } -module.exports = Linking; +module.exports = new Linking(); diff --git a/Libraries/LinkingIOS/LinkingIOS.js b/Libraries/LinkingIOS/LinkingIOS.js index b5ec9f7a6c6068..19730186257719 100644 --- a/Libraries/LinkingIOS/LinkingIOS.js +++ b/Libraries/LinkingIOS/LinkingIOS.js @@ -12,148 +12,7 @@ 'use strict'; var Linking = require('Linking'); -var RCTLinkingManager = require('NativeModules').LinkingManager; -var invariant = require('fbjs/lib/invariant'); -var _initialURL = RCTLinkingManager && RCTLinkingManager.initialURL; +console.warn('LinkingIOS is deprecated. Use Linking instead'); -/** - * NOTE: `LinkingIOS` is being deprecated. Use `Linking` instead. - * - * `LinkingIOS` gives you a general interface to interact with both incoming - * and outgoing app links. - * - * ### Basic Usage - * - * #### Handling deep links - * - * If your app was launched from an external url registered to your app you can - * access and handle it from any component you want with - * - * ``` - * componentDidMount() { - * var url = LinkingIOS.popInitialURL(); - * } - * ``` - * - * In case you also want to listen to incoming app links during your app's - * execution you'll need to add the following lines to you `*AppDelegate.m`: - * - * ``` - * - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url - * sourceApplication:(NSString *)sourceApplication annotation:(id)annotation - * { - * return [RCTLinkingManager application:application openURL:url - * sourceApplication:sourceApplication annotation:annotation]; - * } - * - * // Only if your app is using [Universal Links](https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/AppSearch/UniversalLinks.html). - * - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity - * restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler - * { - * return [RCTLinkingManager application:application - * continueUserActivity:userActivity - * restorationHandler:restorationHandler]; - * } - * - * ``` - * - * And then on your React component you'll be able to listen to the events on - * `LinkingIOS` as follows - * - * ``` - * componentDidMount() { - * LinkingIOS.addEventListener('url', this._handleOpenURL); - * }, - * componentWillUnmount() { - * LinkingIOS.removeEventListener('url', this._handleOpenURL); - * }, - * _handleOpenURL(event) { - * console.log(event.url); - * } - * ``` - * - * #### Triggering App links - * - * To trigger an app link (browser, email or custom schemas), call - * - * ``` - * LinkingIOS.openURL(url) - * ``` - * - * If you want to check if any installed app can handle a given URL beforehand, call - * ``` - * LinkingIOS.canOpenURL(url, (supported) => { - * if (!supported) { - * AlertIOS.alert('Can\'t handle url: ' + url); - * } else { - * LinkingIOS.openURL(url); - * } - * }); - * ``` - */ -class LinkingIOS { - /** - * Add a handler to LinkingIOS changes by listening to the `url` event type - * and providing the handler - * - * @deprecated - */ - static addEventListener(type: string, handler: Function) { - console.warn('"LinkingIOS.addEventListener" is deprecated. Use "Linking.addEventListener" instead.'); - Linking.addEventListener(type, handler); - } - - /** - * Remove a handler by passing the `url` event type and the handler - * - * @deprecated - */ - static removeEventListener(type: string, handler: Function ) { - console.warn('"LinkingIOS.removeEventListener" is deprecated. Use "Linking.removeEventListener" instead.'); - Linking.removeEventListener(type, handler); - } - - /** - * Try to open the given `url` with any of the installed apps. - * - * @deprecated - */ - static openURL(url: string) { - console.warn('"LinkingIOS.openURL" is deprecated. Use the promise based "Linking.openURL" instead.'); - Linking.openURL(url); - } - - /** - * Determine whether or not an installed app can handle a given URL. - * The callback function will be called with `bool supported` as the only argument - * - * NOTE: As of iOS 9, your app needs to provide the `LSApplicationQueriesSchemes` key - * inside `Info.plist`. - * - * @deprecated - */ - static canOpenURL(url: string, callback: Function) { - console.warn('"LinkingIOS.canOpenURL" is deprecated. Use the promise based "Linking.canOpenURL" instead.'); - invariant( - typeof callback === 'function', - 'A valid callback function is required' - ); - Linking.canOpenURL(url).then(callback); - } - - /** - * If the app launch was triggered by an app link, it will pop the link url, - * otherwise it will return `null` - * - * @deprecated - */ - static popInitialURL(): ?string { - console.warn('"LinkingIOS.popInitialURL" is deprecated. Use the promise based "Linking.getInitialURL" instead.'); - var initialURL = _initialURL; - _initialURL = null; - return initialURL; - } -} - -module.exports = LinkingIOS; +module.exports = Linking; diff --git a/Libraries/LinkingIOS/RCTLinkingManager.h b/Libraries/LinkingIOS/RCTLinkingManager.h index 092ca990b8a19b..bea92bb7196009 100644 --- a/Libraries/LinkingIOS/RCTLinkingManager.h +++ b/Libraries/LinkingIOS/RCTLinkingManager.h @@ -9,9 +9,9 @@ #import -#import "RCTBridgeModule.h" +#import "RCTEventEmitter.h" -@interface RCTLinkingManager : NSObject +@interface RCTLinkingManager : RCTEventEmitter + (BOOL)application:(UIApplication *)application openURL:(NSURL *)URL diff --git a/Libraries/LinkingIOS/RCTLinkingManager.m b/Libraries/LinkingIOS/RCTLinkingManager.m index ca4112fc75c9fe..17de5406cd3362 100644 --- a/Libraries/LinkingIOS/RCTLinkingManager.m +++ b/Libraries/LinkingIOS/RCTLinkingManager.m @@ -17,48 +17,24 @@ @implementation RCTLinkingManager -@synthesize bridge = _bridge; - RCT_EXPORT_MODULE() -- (instancetype)init -{ - // We're only overriding this to ensure the module gets created at startup - // TODO (t11106126): Remove once we have more declarative control over module setup. - return [super init]; -} - -- (void)setBridge:(RCTBridge *)bridge +- (void)startObserving { - _bridge = bridge; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleOpenURLNotification:) name:RCTOpenURLNotification object:nil]; } -- (NSDictionary *)constantsToExport +- (void)stopObserving { - NSURL *initialURL; - - if (_bridge.launchOptions[UIApplicationLaunchOptionsURLKey]) { - initialURL = _bridge.launchOptions[UIApplicationLaunchOptionsURLKey]; - } else if (&UIApplicationLaunchOptionsUserActivityDictionaryKey && - _bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]) { - NSDictionary *userActivityDictionary = _bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; - - if ([userActivityDictionary[UIApplicationLaunchOptionsUserActivityTypeKey] isEqual:NSUserActivityTypeBrowsingWeb]) { - initialURL = ((NSUserActivity *)userActivityDictionary[@"UIApplicationLaunchOptionsUserActivityKey"]).webpageURL; - } - } - - return @{@"initialURL": RCTNullIfNil(initialURL.absoluteString)}; + [[NSNotificationCenter defaultCenter] removeObserver:self]; } -- (void)dealloc +- (NSArray *)supportedEvents { - [[NSNotificationCenter defaultCenter] removeObserver:self]; + return @[@"url"]; } + (BOOL)application:(UIApplication *)application @@ -88,19 +64,19 @@ + (BOOL)application:(UIApplication *)application - (void)handleOpenURLNotification:(NSNotification *)notification { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"openURL" - body:notification.userInfo]; -#pragma clang diagnostic pop + [self sendEventWithName:@"url" body:notification.userInfo]; } RCT_EXPORT_METHOD(openURL:(NSURL *)URL resolve:(RCTPromiseResolveBlock)resolve - reject:(__unused RCTPromiseRejectBlock)reject) + reject:(RCTPromiseRejectBlock)reject) { BOOL opened = [RCTSharedApplication() openURL:URL]; - resolve(@(opened)); + if (opened) { + resolve(nil); + } else { + reject(RCTErrorUnspecified, [NSString stringWithFormat:@"Unable to open URL: %@", URL], nil); + } } RCT_EXPORT_METHOD(canOpenURL:(NSURL *)URL @@ -114,9 +90,31 @@ - (void)handleOpenURLNotification:(NSNotification *)notification return; } + // TODO: on iOS9 this will fail if URL isn't included in the plist + // we should probably check for that and reject in that case instead of + // simply resolving with NO + // This can be expensive, so we deliberately don't call on main thread BOOL canOpen = [RCTSharedApplication() canOpenURL:URL]; resolve(@(canOpen)); } +RCT_EXPORT_METHOD(getInitialURL:(RCTPromiseResolveBlock)resolve + reject:(__unused RCTPromiseRejectBlock)reject) +{ + NSURL *initialURL = nil; + if (self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]) { + initialURL = self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]; + } else if (&UIApplicationLaunchOptionsUserActivityDictionaryKey && + self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]) { + NSDictionary *userActivityDictionary = + self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; + + if ([userActivityDictionary[UIApplicationLaunchOptionsUserActivityTypeKey] isEqual:NSUserActivityTypeBrowsingWeb]) { + initialURL = ((NSUserActivity *)userActivityDictionary[@"UIApplicationLaunchOptionsUserActivityKey"]).webpageURL; + } + } + resolve(RCTNullIfNil(initialURL.absoluteString)); +} + @end diff --git a/Libraries/PushNotificationIOS/PushNotificationIOS.js b/Libraries/PushNotificationIOS/PushNotificationIOS.js index 39fbca8c654876..89b8dc986aa17b 100644 --- a/Libraries/PushNotificationIOS/PushNotificationIOS.js +++ b/Libraries/PushNotificationIOS/PushNotificationIOS.js @@ -11,17 +11,21 @@ */ 'use strict'; -var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); -var RCTPushNotificationManager = require('NativeModules').PushNotificationManager; -var invariant = require('fbjs/lib/invariant'); +const NativeEventEmitter = require('NativeEventEmitter'); +const RCTPushNotificationManager = require('NativeModules').PushNotificationManager; +const invariant = require('fbjs/lib/invariant'); -var _notifHandlers = new Map(); -var _initialNotification = RCTPushNotificationManager && +const PushNotificationEmitter = new NativeEventEmitter(RCTPushNotificationManager); + +const _notifHandlers = new Map(); + +//TODO: remove this once all call sites for popInitialNotification() have been removed +let _initialNotification = RCTPushNotificationManager && RCTPushNotificationManager.initialNotification; -var DEVICE_NOTIF_EVENT = 'remoteNotificationReceived'; -var NOTIF_REGISTER_EVENT = 'remoteNotificationsRegistered'; -var DEVICE_LOCAL_NOTIF_EVENT = 'localNotificationReceived'; +const DEVICE_NOTIF_EVENT = 'remoteNotificationReceived'; +const NOTIF_REGISTER_EVENT = 'remoteNotificationsRegistered'; +const DEVICE_LOCAL_NOTIF_EVENT = 'localNotificationReceived'; /** * Handle push notifications for your app, including permission handling and @@ -158,21 +162,21 @@ class PushNotificationIOS { ); var listener; if (type === 'notification') { - listener = RCTDeviceEventEmitter.addListener( + listener = PushNotificationEmitter.addListener( DEVICE_NOTIF_EVENT, (notifData) => { handler(new PushNotificationIOS(notifData)); } ); } else if (type === 'localNotification') { - listener = RCTDeviceEventEmitter.addListener( + listener = PushNotificationEmitter.addListener( DEVICE_LOCAL_NOTIF_EVENT, (notifData) => { handler(new PushNotificationIOS(notifData)); } ); } else if (type === 'register') { - listener = RCTDeviceEventEmitter.addListener( + listener = PushNotificationEmitter.addListener( NOTIF_REGISTER_EVENT, (registrationInfo) => { handler(registrationInfo.deviceToken); @@ -182,6 +186,23 @@ class PushNotificationIOS { _notifHandlers.set(handler, listener); } + /** + * Removes the event listener. Do this in `componentWillUnmount` to prevent + * memory leaks + */ + static removeEventListener(type: string, handler: Function) { + invariant( + type === 'notification' || type === 'register' || type === 'localNotification', + 'PushNotificationIOS only supports `notification`, `register` and `localNotification` events' + ); + var listener = _notifHandlers.get(handler); + if (!listener) { + return; + } + listener.remove(); + _notifHandlers.delete(handler); + } + /** * Requests notification permissions from iOS, prompting the user's * dialog box. By default, it will request all notification permissions, but @@ -247,36 +268,29 @@ class PushNotificationIOS { } /** - * Removes the event listener. Do this in `componentWillUnmount` to prevent - * memory leaks - */ - static removeEventListener(type: string, handler: Function) { - invariant( - type === 'notification' || type === 'register' || type === 'localNotification', - 'PushNotificationIOS only supports `notification`, `register` and `localNotification` events' - ); - var listener = _notifHandlers.get(handler); - if (!listener) { - return; - } - listener.remove(); - _notifHandlers.delete(handler); - } - - - /** - * An initial notification will be available if the app was cold-launched - * from a notification. + * DEPRECATED: An initial notification will be available if the app was + * cold-launched from a notification. * * The first caller of `popInitialNotification` will get the initial * notification object, or `null`. Subsequent invocations will return null. */ static popInitialNotification() { + console.warn('PushNotificationIOS.popInitialNotification() is deprecated. Use getInitialNotification() instead.'); var initialNotification = _initialNotification && new PushNotificationIOS(_initialNotification); _initialNotification = null; return initialNotification; } + + /** + * If the app launch was triggered by a push notification, + * it will give the notification object, otherwise it will give `null` + */ + static getInitialNotification(): Promise { + return RCTPushNotificationManager.getInitialNotification().then(notification => { + return notification && new PushNotificationIOS(notification); + }); + } /** * You will never need to instantiate `PushNotificationIOS` yourself. @@ -286,8 +300,7 @@ class PushNotificationIOS { constructor(nativeNotif: Object) { this._data = {}; - // Extract data from Apple's `aps` dict as defined: - + // Extract data from Apple's `aps` dict as specified here: // https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html Object.keys(nativeNotif).forEach((notifKey) => { diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.h b/Libraries/PushNotificationIOS/RCTPushNotificationManager.h index b7ab540151b5ce..fe8158ce9a3c3a 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.h +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.h @@ -7,11 +7,9 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import +#import "RCTEventEmitter.h" -#import "RCTBridgeModule.h" - -@interface RCTPushNotificationManager : NSObject +@interface RCTPushNotificationManager : RCTEventEmitter + (void)didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings; + (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken; diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m index ac360e254b91a7..64f0e8de512680 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -52,27 +52,8 @@ @implementation RCTPushNotificationManager RCT_EXPORT_MODULE() -@synthesize bridge = _bridge; - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -- (instancetype)init +- (void)startObserving { - // We're only overriding this to ensure the module gets created at startup - // TODO (t11106126): Remove once we have more declarative control over module setup. - return [super init]; -} - -- (void)setBridge:(RCTBridge *)bridge -{ - _bridge = bridge; - - // TODO: if we add an explicit "startObserving" method, we can take this out - // of the application startup path - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleLocalNotificationReceived:) name:RCTLocalNotificationReceived @@ -87,10 +68,24 @@ - (void)setBridge:(RCTBridge *)bridge object:nil]; } +- (void)stopObserving +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (NSArray *)supportedEvents +{ + return @[@"localNotificationReceived", + @"remoteNotificationReceived", + @"remoteNotificationsRegistered"]; +} + +// TODO: Once all JS call sites for popInitialNotification have +// been removed we can get rid of this - (NSDictionary *)constantsToExport { NSDictionary *initialNotification = - [_bridge.launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] copy]; + [self.bridge.launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] copy]; return @{@"initialNotification": RCTNullIfNil(initialNotification)}; } @@ -137,29 +132,17 @@ + (void)didReceiveLocalNotification:(UILocalNotification *)notification - (void)handleLocalNotificationReceived:(NSNotification *)notification { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"localNotificationReceived" - body:notification.userInfo]; -#pragma clang diagnostic pop + [self sendEventWithName:@"localNotificationReceived" body:notification.userInfo]; } - (void)handleRemoteNotificationReceived:(NSNotification *)notification { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"remoteNotificationReceived" - body:notification.userInfo]; -#pragma clang diagnostic pop + [self sendEventWithName:@"remoteNotificationReceived" body:notification.userInfo]; } - (void)handleRemoteNotificationsRegistered:(NSNotification *)notification { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_bridge.eventDispatcher sendDeviceEventWithName:@"remoteNotificationsRegistered" - body:notification.userInfo]; -#pragma clang diagnostic pop + [self sendEventWithName:@"remoteNotificationsRegistered" body:notification.userInfo]; } /** @@ -273,4 +256,12 @@ - (void)handleRemoteNotificationsRegistered:(NSNotification *)notification } } +RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve + reject:(__unused RCTPromiseRejectBlock)reject) +{ + NSDictionary *initialNotification = + [self.bridge.launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] copy]; + resolve(RCTNullIfNil(initialNotification)); +} + @end diff --git a/docs/LinkingLibraries.md b/docs/LinkingLibraries.md index 0ce0849531a481..b32c9bbc85a2b1 100644 --- a/docs/LinkingLibraries.md +++ b/docs/LinkingLibraries.md @@ -85,7 +85,7 @@ What that means is, are you using this library on the native side or only in JavaScript? If you are only using it in JavaScript, you are good to go! This step is not necessary for libraries that we ship with React Native with the -exception of `PushNotificationIOS` and `LinkingIOS`. +exception of `PushNotificationIOS` and `Linking`. In the case of the `PushNotificationIOS` for example, you have to call a method on the library from your `AppDelegate` every time a new push notification is diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index 8929271181c0d7..e16dad530fb2de 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -301,7 +301,6 @@ const apis = [ '../Libraries/Interaction/InteractionManager.js', '../Libraries/LayoutAnimation/LayoutAnimation.js', '../Libraries/Linking/Linking.js', - '../Libraries/LinkingIOS/LinkingIOS.js', '../Libraries/CustomComponents/ListView/ListViewDataSource.js', '../node_modules/react/lib/NativeMethodsMixin.js', '../Libraries/Network/NetInfo.js', From 0656b96354f7e6fd0af30b1c4e485f1fc43b66be Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Fri, 27 May 2016 11:38:28 -0700 Subject: [PATCH 142/843] Redbox: skip column number if it is 0 Summary: Format before: ``` methodName file_name.js @ 42:0 ``` Format after ``` methodName file_name.js:42 ``` Reviewed By: javache Differential Revision: D3350320 fbshipit-source-id: 456deb66bd34deb24bf8b8aa958883bdf4f99129 --- React/Modules/RCTRedBox.m | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/React/Modules/RCTRedBox.m b/React/Modules/RCTRedBox.m index f732459e3a3686..b01ca785758083 100644 --- a/React/Modules/RCTRedBox.m +++ b/React/Modules/RCTRedBox.m @@ -89,7 +89,7 @@ - (instancetype)initWithFrame:(CGRect)frame [copyButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateNormal]; [copyButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted]; [copyButton addTarget:self action:@selector(copyStack) forControlEvents:UIControlEventTouchUpInside]; - + CGFloat buttonWidth = self.bounds.size.width / 3; dismissButton.frame = CGRectMake(0, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight); reloadButton.frame = CGRectMake(buttonWidth, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight); @@ -147,7 +147,7 @@ - (void)reload - (void)copyStack { NSMutableString *fullStackTrace; - + if (_lastErrorMessage != nil) { fullStackTrace = [_lastErrorMessage mutableCopy]; [fullStackTrace appendString:@"\n\n"]; @@ -155,22 +155,31 @@ - (void)copyStack else { fullStackTrace = [NSMutableString string]; } - + for (NSDictionary *stackFrame in _lastStackTrace) { [fullStackTrace appendString:[NSString stringWithFormat:@"%@\n", stackFrame[@"methodName"]]]; if (stackFrame[@"file"]) { - NSString *lineInfo = [NSString stringWithFormat:@" %@ @ %zd:%zd\n", - [stackFrame[@"file"] lastPathComponent], - [stackFrame[@"lineNumber"] integerValue], - [stackFrame[@"column"] integerValue]]; - [fullStackTrace appendString:lineInfo]; + [fullStackTrace appendFormat:@" %@\n", [self formatFrameSource:stackFrame]]; } } - + UIPasteboard *pb = [UIPasteboard generalPasteboard]; [pb setString:fullStackTrace]; } +- (NSString *)formatFrameSource:(NSDictionary *)stackFrame +{ + NSString *lineInfo = [NSString stringWithFormat:@"%@:%zd", + [stackFrame[@"file"] lastPathComponent], + [stackFrame[@"lineNumber"] integerValue]]; + + NSInteger column = [stackFrame[@"column"] integerValue]; + if (column != 0) { + lineInfo = [lineInfo stringByAppendingFormat:@":%zd", column]; + } + return lineInfo; +} + #pragma mark - TableView - (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView @@ -231,10 +240,7 @@ - (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(NSDictiona cell.textLabel.text = stackFrame[@"methodName"]; if (stackFrame[@"file"]) { - cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ @ %zd:%zd", - [stackFrame[@"file"] lastPathComponent], - [stackFrame[@"lineNumber"] integerValue], - [stackFrame[@"column"] integerValue]]; + cell.detailTextLabel.text = [self formatFrameSource:stackFrame]; } else { cell.detailTextLabel.text = @""; } From c780a717e5e146e1b0c16893d70c3bbb87ef5907 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Fri, 27 May 2016 12:02:55 -0700 Subject: [PATCH 143/843] Fix UIExplorer Search Summary: In UI explorer, the route is made of an object which look like this. ``` {key: 'AppList', filter: 'query string from the search box'} ``` When a new search query is enter, a new `filter` value is applied, and the key `AppList` remains the same. In NavigationScenesReducer, we should compare the routes with both their keys and references. The current implementation only compares the keys, which unfortunately depends on the a weak assumption that all routes immutable and keys are unique. In UI Explore, the route key is always 'AppList', which makes sense since we use the key to match the scene, and whenever a new search query is provides, a new route will be created. Reviewed By: nicklockwood Differential Revision: D3357023 fbshipit-source-id: a3c9e98092f5ce555e5dbb4cc806bab2e67d8014 --- .../Reducer/NavigationScenesReducer.js | 24 ++++++++++++++++++- .../__tests__/NavigationScenesReducer-test.js | 19 ++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js index a858a11e9791be..3e08b8727ad46d 100644 --- a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js +++ b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js @@ -12,6 +12,7 @@ 'use strict'; const invariant = require('fbjs/lib/invariant'); +const shallowEqual = require('fbjs/lib/shallowEqual'); import type { NavigationRoute, @@ -55,6 +56,9 @@ function compareScenes( ); } +/** + * Whether two routes are the same. + */ function areScenesShallowEqual( one: NavigationScene, two: NavigationScene, @@ -63,10 +67,28 @@ function areScenesShallowEqual( one.key === two.key && one.index === two.index && one.isStale === two.isStale && - one.route.key === two.route.key + areRoutesShallowEqual(one.route, two.route) ); } +/** + * Whether two routes are the same. + */ +function areRoutesShallowEqual( + one: ?NavigationRoute, + two: ?NavigationRoute, +): boolean { + if (!one || !two) { + return one === two; + } + + if (one.key !== two.key) { + return false; + } + + return shallowEqual(one, two); +} + function NavigationScenesReducer( scenes: Array, nextState: NavigationState, diff --git a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js index 66373efe68d182..413adff622f557 100644 --- a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js +++ b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js @@ -113,7 +113,7 @@ describe('NavigationScenesReducer', () => { expect(scenes1).toBe(scenes2); }); - it('gets different scenes', () => { + it('gets different scenes when keys are different', () => { const state1 = { index: 0, routes: [{key: '1'}, {key: '2'}], @@ -129,6 +129,23 @@ describe('NavigationScenesReducer', () => { expect(scenes1).not.toBe(scenes2); }); + it('gets different scenes when routes are different', () => { + const state1 = { + index: 0, + routes: [{key: '1', x: 1}, {key: '2', x: 2}], + }; + + const state2 = { + index: 0, + routes: [{key: '1', x: 3}, {key: '2', x: 4}], + }; + + const scenes1 = NavigationScenesReducer([], state1, null); + const scenes2 = NavigationScenesReducer(scenes1, state2, state1); + expect(scenes1).not.toBe(scenes2); + }); + + it('pops scenes', () => { // Transition from ['1', '2', '3'] to ['1', '2']. const scenes = testTransition([ From 3ba232f8fb35c58605ad47d7c25c2d94bc8742a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eloy=20Dur=C3=A1n?= Date: Fri, 27 May 2016 13:55:04 -0700 Subject: [PATCH 144/843] Add C++ and standard v14 settings. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: This PR adds the C++ stdlib to the linker flags and sets the C++ standard that’s used to v14. I have tested this with my app, without it any CP build would fail unless users add those flags to the generated projects themselves. /cc grabbou Closes https://github.com/facebook/react-native/pull/7800 Differential Revision: D3360421 fbshipit-source-id: 0a80030dd255f073a201acc6e1c846be114c2c2a --- React.podspec | 98 ++++++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/React.podspec b/React.podspec index 9b1559979bd8be..3f767c95b1963c 100644 --- a/React.podspec +++ b/React.podspec @@ -29,95 +29,97 @@ Pod::Spec.new do |s| s.preserve_paths = "cli.js", "Libraries/**/*.js", "lint", "linter.js", "node_modules", "package.json", "packager", "PATENTS", "react-native-cli" s.subspec 'Core' do |ss| - ss.source_files = "React/**/*.{c,h,m,mm,S}" - ss.exclude_files = "**/__tests__/*", "IntegrationTests/*" - ss.frameworks = "JavaScriptCore" + ss.source_files = "React/**/*.{c,h,m,mm,S}" + ss.exclude_files = "**/__tests__/*", "IntegrationTests/*" + ss.frameworks = "JavaScriptCore" + ss.libraries = "stdc++" + ss.pod_target_xcconfig = { "CLANG_CXX_LANGUAGE_STANDARD" => "c++14" } end s.subspec 'ART' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/ART/**/*.{h,m}" - ss.preserve_paths = "Libraries/ART/**/*.js" + ss.dependency 'React/Core' + ss.source_files = "Libraries/ART/**/*.{h,m}" + ss.preserve_paths = "Libraries/ART/**/*.js" end s.subspec 'RCTActionSheet' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/ActionSheetIOS/*.{h,m}" - ss.preserve_paths = "Libraries/ActionSheetIOS/*.js" + ss.dependency 'React/Core' + ss.source_files = "Libraries/ActionSheetIOS/*.{h,m}" + ss.preserve_paths = "Libraries/ActionSheetIOS/*.js" end s.subspec 'RCTAdSupport' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/AdSupport/*.{h,m}" - ss.preserve_paths = "Libraries/AdSupport/*.js" + ss.dependency 'React/Core' + ss.source_files = "Libraries/AdSupport/*.{h,m}" + ss.preserve_paths = "Libraries/AdSupport/*.js" end s.subspec 'RCTCameraRoll' do |ss| - ss.dependency 'React/Core' - ss.dependency 'React/RCTImage' - ss.source_files = "Libraries/CameraRoll/*.{h,m}" - ss.preserve_paths = "Libraries/CameraRoll/*.js" + ss.dependency 'React/Core' + ss.dependency 'React/RCTImage' + ss.source_files = "Libraries/CameraRoll/*.{h,m}" + ss.preserve_paths = "Libraries/CameraRoll/*.js" end s.subspec 'RCTGeolocation' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/Geolocation/*.{h,m}" - ss.preserve_paths = "Libraries/Geolocation/*.js" + ss.dependency 'React/Core' + ss.source_files = "Libraries/Geolocation/*.{h,m}" + ss.preserve_paths = "Libraries/Geolocation/*.js" end s.subspec 'RCTImage' do |ss| - ss.dependency 'React/Core' - ss.dependency 'React/RCTNetwork' - ss.source_files = "Libraries/Image/*.{h,m}" - ss.preserve_paths = "Libraries/Image/*.js" + ss.dependency 'React/Core' + ss.dependency 'React/RCTNetwork' + ss.source_files = "Libraries/Image/*.{h,m}" + ss.preserve_paths = "Libraries/Image/*.js" end s.subspec 'RCTNetwork' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/Network/*.{h,m}" - ss.preserve_paths = "Libraries/Network/*.js" + ss.dependency 'React/Core' + ss.source_files = "Libraries/Network/*.{h,m}" + ss.preserve_paths = "Libraries/Network/*.js" end s.subspec 'RCTPushNotification' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/PushNotificationIOS/*.{h,m}" - ss.preserve_paths = "Libraries/PushNotificationIOS/*.js" + ss.dependency 'React/Core' + ss.source_files = "Libraries/PushNotificationIOS/*.{h,m}" + ss.preserve_paths = "Libraries/PushNotificationIOS/*.js" end s.subspec 'RCTSettings' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/Settings/*.{h,m}" - ss.preserve_paths = "Libraries/Settings/*.js" + ss.dependency 'React/Core' + ss.source_files = "Libraries/Settings/*.{h,m}" + ss.preserve_paths = "Libraries/Settings/*.js" end s.subspec 'RCTText' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/Text/*.{h,m}" - ss.preserve_paths = "Libraries/Text/*.js" + ss.dependency 'React/Core' + ss.source_files = "Libraries/Text/*.{h,m}" + ss.preserve_paths = "Libraries/Text/*.js" end s.subspec 'RCTVibration' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/Vibration/*.{h,m}" - ss.preserve_paths = "Libraries/Vibration/*.js" + ss.dependency 'React/Core' + ss.source_files = "Libraries/Vibration/*.{h,m}" + ss.preserve_paths = "Libraries/Vibration/*.js" end s.subspec 'RCTWebSocket' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/WebSocket/*.{h,m}" - ss.preserve_paths = "Libraries/WebSocket/*.js" + ss.dependency 'React/Core' + ss.source_files = "Libraries/WebSocket/*.{h,m}" + ss.preserve_paths = "Libraries/WebSocket/*.js" end s.subspec 'RCTLinkingIOS' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/LinkingIOS/*.{h,m}" - ss.preserve_paths = "Libraries/LinkingIOS/*.js" + ss.dependency 'React/Core' + ss.source_files = "Libraries/LinkingIOS/*.{h,m}" + ss.preserve_paths = "Libraries/LinkingIOS/*.js" end s.subspec 'RCTTest' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/RCTTest/**/*.{h,m}" - ss.preserve_paths = "Libraries/RCTTest/**/*.js" - ss.frameworks = "XCTest" + ss.dependency 'React/Core' + ss.source_files = "Libraries/RCTTest/**/*.{h,m}" + ss.preserve_paths = "Libraries/RCTTest/**/*.js" + ss.frameworks = "XCTest" end end From 708530a9059876daa76c06b6bcc0b81b856432fd Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Fri, 27 May 2016 15:30:59 -0700 Subject: [PATCH 145/843] Handle old navigation state safely. Reviewed By: mkonicek Differential Revision: D3359593 fbshipit-source-id: ca6dbcf14a31491bc6a3730243f358f21971e9f3 --- Examples/UIExplorer/UIExplorerApp.ios.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Examples/UIExplorer/UIExplorerApp.ios.js b/Examples/UIExplorer/UIExplorerApp.ios.js index 1e05c795cbaab4..f87cfff9fe9fd7 100644 --- a/Examples/UIExplorer/UIExplorerApp.ios.js +++ b/Examples/UIExplorer/UIExplorerApp.ios.js @@ -60,6 +60,8 @@ type State = UIExplorerNavigationState & { externalExample?: string, }; +const APP_STATE_KEY = 'UIExplorerAppState.v1'; + class UIExplorerApp extends React.Component { _renderOverlay: Function; _renderScene: Function; @@ -81,7 +83,7 @@ class UIExplorerApp extends React.Component { componentDidMount() { Linking.getInitialURL().then((url) => { - AsyncStorage.getItem('UIExplorerAppState', (err, storedString) => { + AsyncStorage.getItem(APP_STATE_KEY, (err, storedString) => { const exampleAction = URIActionMap(this.props.exampleFromAppetizeParams); const urlAction = URIActionMap(url); const launchAction = exampleAction || urlAction; @@ -111,7 +113,7 @@ class UIExplorerApp extends React.Component { const newState = UIExplorerNavigationReducer(this.state, action); if (this.state !== newState) { this.setState(newState); - AsyncStorage.setItem('UIExplorerAppState', JSON.stringify(this.state)); + AsyncStorage.setItem(APP_STATE_KEY, JSON.stringify(this.state)); } } From 5e8f1716fca92a48ed06024e312e658b6c3c90e7 Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Fri, 27 May 2016 16:02:50 -0700 Subject: [PATCH 146/843] Build new bridge with gradle Reviewed By: bestander Differential Revision: D3324351 fbshipit-source-id: 41fa18a23c8661440a7deff244c93278f418e1d9 --- ReactAndroid/build.gradle | 7 ++-- ReactAndroid/src/main/jni/Application.mk | 2 +- .../src/main/jni/first-party/fb/Android.mk | 3 +- ReactAndroid/src/main/jni/react/Android.mk | 3 +- .../src/main/jni/react/jni/Android.mk | 5 ++- .../src/main/jni/react/test/Android.mk | 1 - .../src/main/jni/third-party/folly/Android.mk | 1 + ReactAndroid/src/main/jni/xreact/Android.mk | 40 ++++++++++++++++++ .../src/main/jni/xreact/jni/Android.mk | 40 ++++++++++++++++++ ReactAndroid/src/main/jni/xreact/jni/BUCK | 4 +- .../main/jni/xreact/jni/CxxModuleWrapper.cpp | 3 +- .../src/main/jni/xreact/jni/MethodInvoker.cpp | 2 +- .../jni/xreact/jni/ModuleRegistryHolder.h | 2 +- .../src/main/jni/xreact/jni/ProxyExecutor.cpp | 3 +- .../src/main/jni/xreact/perftests/BUCK | 4 +- ReactCommon/cxxreact/Android.mk | 41 +++++++++++++++++++ ReactCommon/{bridge => cxxreact}/BUCK | 3 +- ReactCommon/{bridge => cxxreact}/CxxModule.h | 0 ReactCommon/{bridge => cxxreact}/Executor.h | 0 .../{bridge => cxxreact}/ExecutorToken.h | 0 .../ExecutorTokenFactory.h | 0 ReactCommon/cxxreact/FollySupport.h | 21 ++++++++++ ReactCommon/{bridge => cxxreact}/Instance.cpp | 0 ReactCommon/{bridge => cxxreact}/Instance.h | 0 .../{bridge => cxxreact}/JSCExecutor.cpp | 3 +- .../{bridge => cxxreact}/JSCExecutor.h | 0 .../{bridge => cxxreact}/JSCHelpers.cpp | 0 ReactCommon/{bridge => cxxreact}/JSCHelpers.h | 0 .../JSCLegacyProfiler.cpp | 0 .../{bridge => cxxreact}/JSCLegacyProfiler.h | 0 .../{bridge => cxxreact}/JSCLegacyTracing.cpp | 0 .../{bridge => cxxreact}/JSCLegacyTracing.h | 0 .../{bridge => cxxreact}/JSCMemory.cpp | 0 ReactCommon/{bridge => cxxreact}/JSCMemory.h | 0 .../{bridge => cxxreact}/JSCPerfStats.cpp | 0 .../{bridge => cxxreact}/JSCPerfStats.h | 0 .../{bridge => cxxreact}/JSCTracing.cpp | 0 ReactCommon/{bridge => cxxreact}/JSCTracing.h | 0 .../{bridge => cxxreact}/JSCWebWorker.cpp | 5 ++- .../{bridge => cxxreact}/JSCWebWorker.h | 0 .../{bridge => cxxreact}/JSModulesUnbundle.h | 0 .../JsArgumentHelpers-inl.h | 0 .../{bridge => cxxreact}/JsArgumentHelpers.h | 5 ++- .../{bridge => cxxreact}/MessageQueueThread.h | 0 .../{bridge => cxxreact}/MethodCall.cpp | 0 ReactCommon/{bridge => cxxreact}/MethodCall.h | 0 .../{bridge => cxxreact}/ModuleRegistry.cpp | 0 .../{bridge => cxxreact}/ModuleRegistry.h | 0 .../{bridge => cxxreact}/NativeModule.h | 0 .../{bridge => cxxreact}/NativeToJsBridge.cpp | 6 ++- .../{bridge => cxxreact}/NativeToJsBridge.h | 0 ReactCommon/{bridge => cxxreact}/Platform.cpp | 0 ReactCommon/{bridge => cxxreact}/Platform.h | 0 .../{bridge => cxxreact}/SampleCxxModule.cpp | 0 .../{bridge => cxxreact}/SampleCxxModule.h | 0 .../{bridge => cxxreact}/SystraceSection.h | 0 ReactCommon/{bridge => cxxreact}/Value.cpp | 0 ReactCommon/{bridge => cxxreact}/Value.h | 0 .../{bridge => cxxreact}/noncopyable.h | 0 59 files changed, 179 insertions(+), 25 deletions(-) create mode 100644 ReactAndroid/src/main/jni/xreact/Android.mk create mode 100644 ReactAndroid/src/main/jni/xreact/jni/Android.mk create mode 100644 ReactCommon/cxxreact/Android.mk rename ReactCommon/{bridge => cxxreact}/BUCK (99%) rename ReactCommon/{bridge => cxxreact}/CxxModule.h (100%) rename ReactCommon/{bridge => cxxreact}/Executor.h (100%) rename ReactCommon/{bridge => cxxreact}/ExecutorToken.h (100%) rename ReactCommon/{bridge => cxxreact}/ExecutorTokenFactory.h (100%) create mode 100644 ReactCommon/cxxreact/FollySupport.h rename ReactCommon/{bridge => cxxreact}/Instance.cpp (100%) rename ReactCommon/{bridge => cxxreact}/Instance.h (100%) rename ReactCommon/{bridge => cxxreact}/JSCExecutor.cpp (99%) rename ReactCommon/{bridge => cxxreact}/JSCExecutor.h (100%) rename ReactCommon/{bridge => cxxreact}/JSCHelpers.cpp (100%) rename ReactCommon/{bridge => cxxreact}/JSCHelpers.h (100%) rename ReactCommon/{bridge => cxxreact}/JSCLegacyProfiler.cpp (100%) rename ReactCommon/{bridge => cxxreact}/JSCLegacyProfiler.h (100%) rename ReactCommon/{bridge => cxxreact}/JSCLegacyTracing.cpp (100%) rename ReactCommon/{bridge => cxxreact}/JSCLegacyTracing.h (100%) rename ReactCommon/{bridge => cxxreact}/JSCMemory.cpp (100%) rename ReactCommon/{bridge => cxxreact}/JSCMemory.h (100%) rename ReactCommon/{bridge => cxxreact}/JSCPerfStats.cpp (100%) rename ReactCommon/{bridge => cxxreact}/JSCPerfStats.h (100%) rename ReactCommon/{bridge => cxxreact}/JSCTracing.cpp (100%) rename ReactCommon/{bridge => cxxreact}/JSCTracing.h (100%) rename ReactCommon/{bridge => cxxreact}/JSCWebWorker.cpp (99%) rename ReactCommon/{bridge => cxxreact}/JSCWebWorker.h (100%) rename ReactCommon/{bridge => cxxreact}/JSModulesUnbundle.h (100%) rename ReactCommon/{bridge => cxxreact}/JsArgumentHelpers-inl.h (100%) rename ReactCommon/{bridge => cxxreact}/JsArgumentHelpers.h (96%) rename ReactCommon/{bridge => cxxreact}/MessageQueueThread.h (100%) rename ReactCommon/{bridge => cxxreact}/MethodCall.cpp (100%) rename ReactCommon/{bridge => cxxreact}/MethodCall.h (100%) rename ReactCommon/{bridge => cxxreact}/ModuleRegistry.cpp (100%) rename ReactCommon/{bridge => cxxreact}/ModuleRegistry.h (100%) rename ReactCommon/{bridge => cxxreact}/NativeModule.h (100%) rename ReactCommon/{bridge => cxxreact}/NativeToJsBridge.cpp (98%) rename ReactCommon/{bridge => cxxreact}/NativeToJsBridge.h (100%) rename ReactCommon/{bridge => cxxreact}/Platform.cpp (100%) rename ReactCommon/{bridge => cxxreact}/Platform.h (100%) rename ReactCommon/{bridge => cxxreact}/SampleCxxModule.cpp (100%) rename ReactCommon/{bridge => cxxreact}/SampleCxxModule.h (100%) rename ReactCommon/{bridge => cxxreact}/SystraceSection.h (100%) rename ReactCommon/{bridge => cxxreact}/Value.cpp (100%) rename ReactCommon/{bridge => cxxreact}/Value.h (100%) rename ReactCommon/{bridge => cxxreact}/noncopyable.h (100%) diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle index 5f5ba755ce07f5..361ce74ed4512e 100644 --- a/ReactAndroid/build.gradle +++ b/ReactAndroid/build.gradle @@ -114,7 +114,7 @@ task prepareGlog(dependsOn: downloadGlog, type: Copy) { task downloadJSCHeaders(type: Download) { def jscAPIBaseURL = 'https://svn.webkit.org/repository/webkit/!svn/bc/174650/trunk/Source/JavaScriptCore/API/' - def jscHeaderFiles = ['JSBase.h', 'JSContextRef.h', 'JSObjectRef.h', 'JSRetainPtr.h', 'JSStringRef.h', 'JSValueRef.h', 'WebKitAvailability.h'] + def jscHeaderFiles = ['JavaScript.h', 'JSBase.h', 'JSContextRef.h', 'JSObjectRef.h', 'JSRetainPtr.h', 'JSStringRef.h', 'JSValueRef.h', 'WebKitAvailability.h'] def output = new File(downloadsDir, 'jsc') output.mkdirs() src(jscHeaderFiles.collect { headerName -> "$jscAPIBaseURL$headerName" }) @@ -185,7 +185,7 @@ def getNdkBuildFullPath() { } task buildReactNdkLib(dependsOn: [prepareJSC, prepareBoost, prepareDoubleConversion, prepareFolly, prepareGlog], type: Exec) { - inputs.file('src/main/jni/react') + inputs.file('src/main/jni/xreact') outputs.dir("$buildDir/react-ndk/all") commandLine getNdkBuildFullPath(), 'NDK_PROJECT_PATH=null', @@ -193,7 +193,8 @@ task buildReactNdkLib(dependsOn: [prepareJSC, prepareBoost, prepareDoubleConvers 'NDK_OUT=' + temporaryDir, "NDK_LIBS_OUT=$buildDir/react-ndk/all", "THIRD_PARTY_NDK_DIR=$buildDir/third-party-ndk", - '-C', file('src/main/jni/react/jni').absolutePath, + "REACT_COMMON_DIR=$projectDir/../ReactCommon", + '-C', file('src/main/jni/xreact/jni').absolutePath, '--jobs', project.hasProperty("jobs") ? project.property("jobs") : Runtime.runtime.availableProcessors() } diff --git a/ReactAndroid/src/main/jni/Application.mk b/ReactAndroid/src/main/jni/Application.mk index f403e5ba18a26d..2a1e94dc049479 100644 --- a/ReactAndroid/src/main/jni/Application.mk +++ b/ReactAndroid/src/main/jni/Application.mk @@ -5,7 +5,7 @@ APP_PLATFORM := android-9 APP_MK_DIR := $(dir $(lastword $(MAKEFILE_LIST))) -NDK_MODULE_PATH := $(APP_MK_DIR)$(HOST_DIRSEP)$(THIRD_PARTY_NDK_DIR)$(HOST_DIRSEP)$(APP_MK_DIR)first-party +NDK_MODULE_PATH := $(APP_MK_DIR)$(HOST_DIRSEP)$(THIRD_PARTY_NDK_DIR)$(HOST_DIRSEP)$(REACT_COMMON_DIR)$(HOST_DIRSEP)$(APP_MK_DIR)first-party APP_STL := gnustl_shared diff --git a/ReactAndroid/src/main/jni/first-party/fb/Android.mk b/ReactAndroid/src/main/jni/first-party/fb/Android.mk index 510d07018d00eb..6eb6eeab9d7dd8 100644 --- a/ReactAndroid/src/main/jni/first-party/fb/Android.mk +++ b/ReactAndroid/src/main/jni/first-party/fb/Android.mk @@ -1,4 +1,5 @@ -LOCAL_PATH:= $(call my-dir) +LOCAL_PATH := $(call my-dir) + include $(CLEAR_VARS) LOCAL_SRC_FILES:= \ diff --git a/ReactAndroid/src/main/jni/react/Android.mk b/ReactAndroid/src/main/jni/react/Android.mk index 9c5498da9740d5..311dff6b38bdd9 100644 --- a/ReactAndroid/src/main/jni/react/Android.mk +++ b/ReactAndroid/src/main/jni/react/Android.mk @@ -1,4 +1,4 @@ -LOCAL_PATH:= $(call my-dir) +LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) @@ -18,7 +18,6 @@ LOCAL_EXPORT_C_INCLUDES := $(LOCAL_C_INCLUDES) LOCAL_CFLAGS := \ -DLOG_TAG=\"ReactNative\" -LOCAL_LDLIBS += -landroid LOCAL_CFLAGS += -Wall -Werror -fexceptions -frtti CXX11_FLAGS := -std=c++11 LOCAL_CFLAGS += $(CXX11_FLAGS) diff --git a/ReactAndroid/src/main/jni/react/jni/Android.mk b/ReactAndroid/src/main/jni/react/jni/Android.mk index 0fabec805d22c7..42e43e8ce81a93 100644 --- a/ReactAndroid/src/main/jni/react/jni/Android.mk +++ b/ReactAndroid/src/main/jni/react/jni/Android.mk @@ -2,20 +2,21 @@ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) -LOCAL_MODULE := reactnativejni +LOCAL_MODULE := libreactnativejni LOCAL_SRC_FILES := \ JExecutorToken.cpp \ JMessageQueueThread.cpp \ + JniJSModulesUnbundle.cpp \ JSCPerfLogging.cpp \ JSLoader.cpp \ JSLogging.cpp \ - JniJSModulesUnbundle.cpp \ NativeArray.cpp \ OnLoad.cpp \ ProxyExecutor.cpp \ LOCAL_C_INCLUDES := $(LOCAL_PATH) +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/../.. $(LOCAL_PATH)/.. LOCAL_CFLAGS += -Wall -Werror -fvisibility=hidden -fexceptions -frtti CXX11_FLAGS := -std=c++11 diff --git a/ReactAndroid/src/main/jni/react/test/Android.mk b/ReactAndroid/src/main/jni/react/test/Android.mk index 87d0480b3ce857..3bd10a2b002612 100644 --- a/ReactAndroid/src/main/jni/react/test/Android.mk +++ b/ReactAndroid/src/main/jni/react/test/Android.mk @@ -24,4 +24,3 @@ LOCAL_LDFLAGS += $(BUCK_DEP_LDFLAGS) include $(BUILD_EXECUTABLE) $(call import-module,react) -$(call import-module,google-test) diff --git a/ReactAndroid/src/main/jni/third-party/folly/Android.mk b/ReactAndroid/src/main/jni/third-party/folly/Android.mk index 4cabadccb83c66..dd3a3af807a412 100644 --- a/ReactAndroid/src/main/jni/third-party/folly/Android.mk +++ b/ReactAndroid/src/main/jni/third-party/folly/Android.mk @@ -5,6 +5,7 @@ LOCAL_SRC_FILES:= \ folly/json.cpp \ folly/Unicode.cpp \ folly/Conv.cpp \ + folly/Demangle.cpp \ folly/detail/FunctionalExcept.cpp \ folly/detail/MallocImpl.cpp \ folly/StringBase.cpp \ diff --git a/ReactAndroid/src/main/jni/xreact/Android.mk b/ReactAndroid/src/main/jni/xreact/Android.mk new file mode 100644 index 00000000000000..4491326e1c08e7 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/Android.mk @@ -0,0 +1,40 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := reactnativejnifb + +LOCAL_SRC_FILES := \ + CatalystInstanceImpl.cpp \ + CxxModuleWrapper.cpp \ + JExecutorToken.cpp \ + JMessageQueueThread.cpp \ + JniJSModulesUnbundle.cpp \ + JSCPerfLogging.cpp \ + JSLoader.cpp \ + JSLogging.cpp \ + MethodInvoker.cpp \ + ModuleRegistryHolder.cpp \ + OnLoad.cpp \ + ProxyExecutor.cpp \ + +LOCAL_C_INCLUDES := $(LOCAL_PATH) +LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../ + +LOCAL_CFLAGS += -Wall -Werror -fvisibility=hidden -fexceptions -frtti +CXX11_FLAGS := -std=c++11 +LOCAL_CFLAGS += $(CXX11_FLAGS) +LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) + +LOCAL_LDLIBS += -landroid +LOCAL_SHARED_LIBRARIES := libfolly_json libfbjni libjsc libglog_init libreactnativejni +LOCAL_STATIC_LIBRARIES := libreactnative libreactnativefb + +include $(BUILD_SHARED_LIBRARY) + +$(call import-module,cxxreact) +$(call import-module,jsc) +$(call import-module,folly) +$(call import-module,fbgloginit) +$(call import-module,jni) +$(call import-module,react) diff --git a/ReactAndroid/src/main/jni/xreact/jni/Android.mk b/ReactAndroid/src/main/jni/xreact/jni/Android.mk new file mode 100644 index 00000000000000..c0078c36a8a157 --- /dev/null +++ b/ReactAndroid/src/main/jni/xreact/jni/Android.mk @@ -0,0 +1,40 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := libreactnativejnifb + +LOCAL_SRC_FILES := \ + CatalystInstanceImpl.cpp \ + CxxModuleWrapper.cpp \ + JExecutorToken.cpp \ + JMessageQueueThread.cpp \ + JniJSModulesUnbundle.cpp \ + JSCPerfLogging.cpp \ + JSLoader.cpp \ + JSLogging.cpp \ + MethodInvoker.cpp \ + ModuleRegistryHolder.cpp \ + OnLoad.cpp \ + ProxyExecutor.cpp \ + +LOCAL_C_INCLUDES := $(LOCAL_PATH) +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/../.. + +LOCAL_CFLAGS += -Wall -Werror -fvisibility=hidden -fexceptions -frtti +CXX11_FLAGS := -std=c++11 +LOCAL_CFLAGS += $(CXX11_FLAGS) +LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) + +LOCAL_LDLIBS += -landroid +LOCAL_SHARED_LIBRARIES := libfolly_json libfbjni libjsc libglog_init libreactnativejni +LOCAL_STATIC_LIBRARIES := libreactnativefb + +include $(BUILD_SHARED_LIBRARY) + +$(call import-module,cxxreact) +$(call import-module,jsc) +$(call import-module,folly) +$(call import-module,fbgloginit) +$(call import-module,jsc) +$(call import-module,react/jni) diff --git a/ReactAndroid/src/main/jni/xreact/jni/BUCK b/ReactAndroid/src/main/jni/xreact/jni/BUCK index 57b172b4a9a2ee..ea88102d01c483 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/BUCK +++ b/ReactAndroid/src/main/jni/xreact/jni/BUCK @@ -18,8 +18,8 @@ cxx_library( '//xplat/folly:molly', '//xplat/fbsystrace:fbsystrace', react_native_target('jni/react/jni:jni'), - react_native_xplat_target('bridge:bridge'), - react_native_xplat_target('bridge:module'), + react_native_xplat_target('cxxreact:bridge'), + react_native_xplat_target('cxxreact:module'), ], srcs = glob(['*.cpp']), exported_headers = EXPORTED_HEADERS, diff --git a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp index f0c3e4c0eacdb8..d1532fd10dd2b9 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp @@ -10,6 +10,7 @@ #include #include +#include #include @@ -190,7 +191,7 @@ std::string CxxModuleWrapper::getConstantsJson() { constsobject.insert(std::move(c.first), std::move(c.second)); } - return folly::toJson(constsobject); + return facebook::react::detail::toStdString(folly::toJson(constsobject)); } jobject CxxModuleWrapper::getMethods() { diff --git a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp index 8c304378dc3cde..5dcfd3e62b3679 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp @@ -137,7 +137,7 @@ jvalue extract(std::weak_ptr& instance, ExecutorToken token, char type value.l = valueOf(extractDouble(arg)); break; case 'S': - value.l = jni::make_jstring(arg.getString()).release(); + value.l = jni::make_jstring(arg.getString().c_str()).release(); break; case 'A': value.l = ReadableNativeArray::newObjectCxxArgs(arg).release(); diff --git a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.h b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.h index 40a1143636a9ec..565d9e769418e3 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.h +++ b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.h @@ -5,7 +5,7 @@ #include #include -#include +#include "CxxModuleWrapper.h" namespace facebook { namespace react { diff --git a/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp b/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp index 659c9f84186c14..7846187260141d 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp @@ -10,6 +10,7 @@ #include #include +#include namespace facebook { namespace react { @@ -56,7 +57,7 @@ ProxyExecutor::ProxyExecutor(jni::global_ref&& executorInstance, SystraceSection t("setGlobalVariable"); setGlobalVariable( "__fbBatchedBridgeConfig", - folly::make_unique(folly::toJson(config))); + folly::make_unique(detail::toStdString(folly::toJson(config)))); } ProxyExecutor::~ProxyExecutor() { diff --git a/ReactAndroid/src/main/jni/xreact/perftests/BUCK b/ReactAndroid/src/main/jni/xreact/perftests/BUCK index d111fa394175a3..a80f52b229c90c 100644 --- a/ReactAndroid/src/main/jni/xreact/perftests/BUCK +++ b/ReactAndroid/src/main/jni/xreact/perftests/BUCK @@ -8,10 +8,10 @@ cxx_library( '-fexceptions', ], deps = [ - '//native:base', '//native/fb:fb', + '//native:base', '//xplat/folly:molly', - react_native_xplat_target('bridge:module'), + react_native_xplat_target('cxxreact:module'), ], visibility = [ '//instrumentation_tests/com/facebook/react/...', diff --git a/ReactCommon/cxxreact/Android.mk b/ReactCommon/cxxreact/Android.mk new file mode 100644 index 00000000000000..985cc035752b11 --- /dev/null +++ b/ReactCommon/cxxreact/Android.mk @@ -0,0 +1,41 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := libreactnativefb + +LOCAL_SRC_FILES := \ + Instance.cpp \ + JSCExecutor.cpp \ + JSCHelpers.cpp \ + JSCLegacyProfiler.cpp \ + JSCLegacyTracing.cpp \ + JSCMemory.cpp \ + JSCPerfStats.cpp \ + JSCTracing.cpp \ + JSCWebWorker.cpp \ + MethodCall.cpp \ + ModuleRegistry.cpp \ + NativeToJsBridge.cpp \ + Platform.cpp \ + Value.cpp \ + +LOCAL_C_INCLUDES := $(LOCAL_PATH)/.. +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_C_INCLUDES) + +LOCAL_CFLAGS := \ + -DLOG_TAG=\"ReactNative\" + +LOCAL_CFLAGS += -Wall -Werror -fexceptions -frtti +CXX11_FLAGS := -std=c++11 +LOCAL_CFLAGS += $(CXX11_FLAGS) +LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) + +LOCAL_SHARED_LIBRARIES := libfb libfolly_json libjsc libglog + +include $(BUILD_STATIC_LIBRARY) + +$(call import-module,fb) +$(call import-module,folly) +$(call import-module,jsc) +$(call import-module,glog) diff --git a/ReactCommon/bridge/BUCK b/ReactCommon/cxxreact/BUCK similarity index 99% rename from ReactCommon/bridge/BUCK rename to ReactCommon/cxxreact/BUCK index cff2c6c6962ed2..43e52bbdc186f3 100644 --- a/ReactCommon/bridge/BUCK +++ b/ReactCommon/cxxreact/BUCK @@ -56,13 +56,14 @@ elif THIS_IS_FBOBJC: cxx_library( name = 'module', - header_namespace = 'cxxreact', force_static = True, exported_headers = [ 'CxxModule.h', + 'FollySupport.h', 'JsArgumentHelpers.h', 'JsArgumentHelpers-inl.h', ], + header_namespace = 'cxxreact', deps = [ '//xplat/folly:molly', ], diff --git a/ReactCommon/bridge/CxxModule.h b/ReactCommon/cxxreact/CxxModule.h similarity index 100% rename from ReactCommon/bridge/CxxModule.h rename to ReactCommon/cxxreact/CxxModule.h diff --git a/ReactCommon/bridge/Executor.h b/ReactCommon/cxxreact/Executor.h similarity index 100% rename from ReactCommon/bridge/Executor.h rename to ReactCommon/cxxreact/Executor.h diff --git a/ReactCommon/bridge/ExecutorToken.h b/ReactCommon/cxxreact/ExecutorToken.h similarity index 100% rename from ReactCommon/bridge/ExecutorToken.h rename to ReactCommon/cxxreact/ExecutorToken.h diff --git a/ReactCommon/bridge/ExecutorTokenFactory.h b/ReactCommon/cxxreact/ExecutorTokenFactory.h similarity index 100% rename from ReactCommon/bridge/ExecutorTokenFactory.h rename to ReactCommon/cxxreact/ExecutorTokenFactory.h diff --git a/ReactCommon/cxxreact/FollySupport.h b/ReactCommon/cxxreact/FollySupport.h new file mode 100644 index 00000000000000..7da9e36ada60d6 --- /dev/null +++ b/ReactCommon/cxxreact/FollySupport.h @@ -0,0 +1,21 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include + +namespace facebook { +namespace react { +namespace detail { +// TODO(cjhopman): Once folly is updated, remove these. + +inline std::string toStdString(std::string&& str) { + return std::move(str); +} + +inline std::string toStdString(folly::fbstring&& str) { + return str.toStdString(); +} + +}}} diff --git a/ReactCommon/bridge/Instance.cpp b/ReactCommon/cxxreact/Instance.cpp similarity index 100% rename from ReactCommon/bridge/Instance.cpp rename to ReactCommon/cxxreact/Instance.cpp diff --git a/ReactCommon/bridge/Instance.h b/ReactCommon/cxxreact/Instance.h similarity index 100% rename from ReactCommon/bridge/Instance.h rename to ReactCommon/cxxreact/Instance.h diff --git a/ReactCommon/bridge/JSCExecutor.cpp b/ReactCommon/cxxreact/JSCExecutor.cpp similarity index 99% rename from ReactCommon/bridge/JSCExecutor.cpp rename to ReactCommon/cxxreact/JSCExecutor.cpp index cde2a7715d5468..536313da4b3e80 100644 --- a/ReactCommon/bridge/JSCExecutor.cpp +++ b/ReactCommon/cxxreact/JSCExecutor.cpp @@ -14,6 +14,7 @@ #include #include +#include "FollySupport.h" #include "JSCHelpers.h" #include "Platform.h" #include "SystraceSection.h" @@ -139,7 +140,7 @@ JSCExecutor::JSCExecutor(std::shared_ptr delegate, SystraceSection t("setGlobalVariable"); setGlobalVariable( "__fbBatchedBridgeConfig", - folly::make_unique(folly::toJson(config))); + folly::make_unique(detail::toStdString(folly::toJson(config)))); } JSCExecutor::JSCExecutor( diff --git a/ReactCommon/bridge/JSCExecutor.h b/ReactCommon/cxxreact/JSCExecutor.h similarity index 100% rename from ReactCommon/bridge/JSCExecutor.h rename to ReactCommon/cxxreact/JSCExecutor.h diff --git a/ReactCommon/bridge/JSCHelpers.cpp b/ReactCommon/cxxreact/JSCHelpers.cpp similarity index 100% rename from ReactCommon/bridge/JSCHelpers.cpp rename to ReactCommon/cxxreact/JSCHelpers.cpp diff --git a/ReactCommon/bridge/JSCHelpers.h b/ReactCommon/cxxreact/JSCHelpers.h similarity index 100% rename from ReactCommon/bridge/JSCHelpers.h rename to ReactCommon/cxxreact/JSCHelpers.h diff --git a/ReactCommon/bridge/JSCLegacyProfiler.cpp b/ReactCommon/cxxreact/JSCLegacyProfiler.cpp similarity index 100% rename from ReactCommon/bridge/JSCLegacyProfiler.cpp rename to ReactCommon/cxxreact/JSCLegacyProfiler.cpp diff --git a/ReactCommon/bridge/JSCLegacyProfiler.h b/ReactCommon/cxxreact/JSCLegacyProfiler.h similarity index 100% rename from ReactCommon/bridge/JSCLegacyProfiler.h rename to ReactCommon/cxxreact/JSCLegacyProfiler.h diff --git a/ReactCommon/bridge/JSCLegacyTracing.cpp b/ReactCommon/cxxreact/JSCLegacyTracing.cpp similarity index 100% rename from ReactCommon/bridge/JSCLegacyTracing.cpp rename to ReactCommon/cxxreact/JSCLegacyTracing.cpp diff --git a/ReactCommon/bridge/JSCLegacyTracing.h b/ReactCommon/cxxreact/JSCLegacyTracing.h similarity index 100% rename from ReactCommon/bridge/JSCLegacyTracing.h rename to ReactCommon/cxxreact/JSCLegacyTracing.h diff --git a/ReactCommon/bridge/JSCMemory.cpp b/ReactCommon/cxxreact/JSCMemory.cpp similarity index 100% rename from ReactCommon/bridge/JSCMemory.cpp rename to ReactCommon/cxxreact/JSCMemory.cpp diff --git a/ReactCommon/bridge/JSCMemory.h b/ReactCommon/cxxreact/JSCMemory.h similarity index 100% rename from ReactCommon/bridge/JSCMemory.h rename to ReactCommon/cxxreact/JSCMemory.h diff --git a/ReactCommon/bridge/JSCPerfStats.cpp b/ReactCommon/cxxreact/JSCPerfStats.cpp similarity index 100% rename from ReactCommon/bridge/JSCPerfStats.cpp rename to ReactCommon/cxxreact/JSCPerfStats.cpp diff --git a/ReactCommon/bridge/JSCPerfStats.h b/ReactCommon/cxxreact/JSCPerfStats.h similarity index 100% rename from ReactCommon/bridge/JSCPerfStats.h rename to ReactCommon/cxxreact/JSCPerfStats.h diff --git a/ReactCommon/bridge/JSCTracing.cpp b/ReactCommon/cxxreact/JSCTracing.cpp similarity index 100% rename from ReactCommon/bridge/JSCTracing.cpp rename to ReactCommon/cxxreact/JSCTracing.cpp diff --git a/ReactCommon/bridge/JSCTracing.h b/ReactCommon/cxxreact/JSCTracing.h similarity index 100% rename from ReactCommon/bridge/JSCTracing.h rename to ReactCommon/cxxreact/JSCTracing.h diff --git a/ReactCommon/bridge/JSCWebWorker.cpp b/ReactCommon/cxxreact/JSCWebWorker.cpp similarity index 99% rename from ReactCommon/bridge/JSCWebWorker.cpp rename to ReactCommon/cxxreact/JSCWebWorker.cpp index 7c56ba17b7961d..0a123b6456fdee 100644 --- a/ReactCommon/bridge/JSCWebWorker.cpp +++ b/ReactCommon/cxxreact/JSCWebWorker.cpp @@ -2,12 +2,11 @@ #include "JSCWebWorker.h" -#include + #include #include #include -#include #include #include "JSCHelpers.h" @@ -15,6 +14,8 @@ #include "Platform.h" #include "Value.h" +#include + #include namespace facebook { diff --git a/ReactCommon/bridge/JSCWebWorker.h b/ReactCommon/cxxreact/JSCWebWorker.h similarity index 100% rename from ReactCommon/bridge/JSCWebWorker.h rename to ReactCommon/cxxreact/JSCWebWorker.h diff --git a/ReactCommon/bridge/JSModulesUnbundle.h b/ReactCommon/cxxreact/JSModulesUnbundle.h similarity index 100% rename from ReactCommon/bridge/JSModulesUnbundle.h rename to ReactCommon/cxxreact/JSModulesUnbundle.h diff --git a/ReactCommon/bridge/JsArgumentHelpers-inl.h b/ReactCommon/cxxreact/JsArgumentHelpers-inl.h similarity index 100% rename from ReactCommon/bridge/JsArgumentHelpers-inl.h rename to ReactCommon/cxxreact/JsArgumentHelpers-inl.h diff --git a/ReactCommon/bridge/JsArgumentHelpers.h b/ReactCommon/cxxreact/JsArgumentHelpers.h similarity index 96% rename from ReactCommon/bridge/JsArgumentHelpers.h rename to ReactCommon/cxxreact/JsArgumentHelpers.h index 80e3e48bd950dd..5ad42eafc4cbd6 100644 --- a/ReactCommon/bridge/JsArgumentHelpers.h +++ b/ReactCommon/cxxreact/JsArgumentHelpers.h @@ -5,7 +5,10 @@ #include #include +#include "FollySupport.h" + #include +#include // When building a cross-platform module for React Native, arguments passed // from JS are represented as a folly::dynamic. This class provides helpers to @@ -97,7 +100,7 @@ inline double jsArgAsDouble(const folly::dynamic& args, size_t n) { // Extract the n'th arg from the given dynamic, as a string. Throws a // JsArgumentException if this fails for some reason. inline std::string jsArgAsString(const folly::dynamic& args, size_t n) { - return jsArgN(args, n, &folly::dynamic::asString); + return facebook::react::detail::toStdString(jsArgN(args, n, &folly::dynamic::asString)); } }} diff --git a/ReactCommon/bridge/MessageQueueThread.h b/ReactCommon/cxxreact/MessageQueueThread.h similarity index 100% rename from ReactCommon/bridge/MessageQueueThread.h rename to ReactCommon/cxxreact/MessageQueueThread.h diff --git a/ReactCommon/bridge/MethodCall.cpp b/ReactCommon/cxxreact/MethodCall.cpp similarity index 100% rename from ReactCommon/bridge/MethodCall.cpp rename to ReactCommon/cxxreact/MethodCall.cpp diff --git a/ReactCommon/bridge/MethodCall.h b/ReactCommon/cxxreact/MethodCall.h similarity index 100% rename from ReactCommon/bridge/MethodCall.h rename to ReactCommon/cxxreact/MethodCall.h diff --git a/ReactCommon/bridge/ModuleRegistry.cpp b/ReactCommon/cxxreact/ModuleRegistry.cpp similarity index 100% rename from ReactCommon/bridge/ModuleRegistry.cpp rename to ReactCommon/cxxreact/ModuleRegistry.cpp diff --git a/ReactCommon/bridge/ModuleRegistry.h b/ReactCommon/cxxreact/ModuleRegistry.h similarity index 100% rename from ReactCommon/bridge/ModuleRegistry.h rename to ReactCommon/cxxreact/ModuleRegistry.h diff --git a/ReactCommon/bridge/NativeModule.h b/ReactCommon/cxxreact/NativeModule.h similarity index 100% rename from ReactCommon/bridge/NativeModule.h rename to ReactCommon/cxxreact/NativeModule.h diff --git a/ReactCommon/bridge/NativeToJsBridge.cpp b/ReactCommon/cxxreact/NativeToJsBridge.cpp similarity index 98% rename from ReactCommon/bridge/NativeToJsBridge.cpp rename to ReactCommon/cxxreact/NativeToJsBridge.cpp index 8a90f7a0efb3e4..c7b665a551b4e9 100644 --- a/ReactCommon/bridge/NativeToJsBridge.cpp +++ b/ReactCommon/cxxreact/NativeToJsBridge.cpp @@ -147,8 +147,9 @@ void NativeToJsBridge::callFunction( const std::string& methodId, const folly::dynamic& arguments, const std::string& tracingName) { + int systraceCookie = -1; #ifdef WITH_FBSYSTRACE - int systraceCookie = m_systraceCookie++; + systraceCookie = m_systraceCookie++; FbSystraceAsyncFlow::begin( TRACE_TAG_REACT_CXX_BRIDGE, tracingName.c_str(), @@ -173,8 +174,9 @@ void NativeToJsBridge::callFunction( void NativeToJsBridge::invokeCallback(ExecutorToken executorToken, const double callbackId, const folly::dynamic& arguments) { + int systraceCookie = -1; #ifdef WITH_FBSYSTRACE - int systraceCookie = m_systraceCookie++; + systraceCookie = m_systraceCookie++; FbSystraceAsyncFlow::begin( TRACE_TAG_REACT_CXX_BRIDGE, "", diff --git a/ReactCommon/bridge/NativeToJsBridge.h b/ReactCommon/cxxreact/NativeToJsBridge.h similarity index 100% rename from ReactCommon/bridge/NativeToJsBridge.h rename to ReactCommon/cxxreact/NativeToJsBridge.h diff --git a/ReactCommon/bridge/Platform.cpp b/ReactCommon/cxxreact/Platform.cpp similarity index 100% rename from ReactCommon/bridge/Platform.cpp rename to ReactCommon/cxxreact/Platform.cpp diff --git a/ReactCommon/bridge/Platform.h b/ReactCommon/cxxreact/Platform.h similarity index 100% rename from ReactCommon/bridge/Platform.h rename to ReactCommon/cxxreact/Platform.h diff --git a/ReactCommon/bridge/SampleCxxModule.cpp b/ReactCommon/cxxreact/SampleCxxModule.cpp similarity index 100% rename from ReactCommon/bridge/SampleCxxModule.cpp rename to ReactCommon/cxxreact/SampleCxxModule.cpp diff --git a/ReactCommon/bridge/SampleCxxModule.h b/ReactCommon/cxxreact/SampleCxxModule.h similarity index 100% rename from ReactCommon/bridge/SampleCxxModule.h rename to ReactCommon/cxxreact/SampleCxxModule.h diff --git a/ReactCommon/bridge/SystraceSection.h b/ReactCommon/cxxreact/SystraceSection.h similarity index 100% rename from ReactCommon/bridge/SystraceSection.h rename to ReactCommon/cxxreact/SystraceSection.h diff --git a/ReactCommon/bridge/Value.cpp b/ReactCommon/cxxreact/Value.cpp similarity index 100% rename from ReactCommon/bridge/Value.cpp rename to ReactCommon/cxxreact/Value.cpp diff --git a/ReactCommon/bridge/Value.h b/ReactCommon/cxxreact/Value.h similarity index 100% rename from ReactCommon/bridge/Value.h rename to ReactCommon/cxxreact/Value.h diff --git a/ReactCommon/bridge/noncopyable.h b/ReactCommon/cxxreact/noncopyable.h similarity index 100% rename from ReactCommon/bridge/noncopyable.h rename to ReactCommon/cxxreact/noncopyable.h From d81cf658ef330c55e7871ad43add7596a29e864d Mon Sep 17 00:00:00 2001 From: Gabe Levi Date: Fri, 27 May 2016 17:47:01 -0700 Subject: [PATCH 147/843] Deploy v0.26.0 Reviewed By: bhosmer, samwgoldman Differential Revision: D3361342 fbshipit-source-id: c062656e3789784929102ba04c54be3be774d7dc --- .flowconfig | 6 +++--- .../NavigationHeaderTitle.js | 14 ++++++++++---- Libraries/ReactIOS/YellowBox.js | 2 ++ package.json | 2 +- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.flowconfig b/.flowconfig index dd29ffc7434af9..fe850cb6952416 100644 --- a/.flowconfig +++ b/.flowconfig @@ -88,9 +88,9 @@ suppress_type=$FlowIssue suppress_type=$FlowFixMe suppress_type=$FixMe -suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(2[0-5]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) -suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-5]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(2[0-6]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-6]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy [version] -^0.25.0 +^0.26.0 diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationHeaderTitle.js b/Libraries/CustomComponents/NavigationExperimental/NavigationHeaderTitle.js index 4ee95e0ae96a02..1d781ec35ceac8 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationHeaderTitle.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationHeaderTitle.js @@ -1,5 +1,10 @@ /** - * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. * * Facebook, Inc. ("Facebook") owns all right, title and interest, including * all intellectual property and other proprietary rights, in and to the React @@ -39,11 +44,12 @@ const { type Props = { children: ReactElement; - style: any; - textStyle: any; - viewProps: any; + style?: any; + textStyle?: any; + viewProps?: any; } +// $FlowIssue(>=0.26.0) #11432532 const NavigationHeaderTitle = ({ children, style, textStyle, viewProps }: Props) => ( {children} diff --git a/Libraries/ReactIOS/YellowBox.js b/Libraries/ReactIOS/YellowBox.js index 777a777581d44b..d334eab3661985 100644 --- a/Libraries/ReactIOS/YellowBox.js +++ b/Libraries/ReactIOS/YellowBox.js @@ -134,6 +134,8 @@ const WarningInspector = ({ const View = require('View'); const countSentence = + /* $FlowFixMe(>=0.26.0) - count can be undefined! Look at WarningInspector + * usage! */ 'Warning encountered ' + count + ' time' + (count - 1 ? 's' : '') + '.'; return ( diff --git a/package.json b/package.json index 4adea31612791e..3e5be91ab717e6 100644 --- a/package.json +++ b/package.json @@ -188,7 +188,7 @@ "eslint": "^2.5.3", "eslint-plugin-flow-vars": "^0.2.1", "eslint-plugin-react": "^4.2.1", - "flow-bin": "^0.25.0", + "flow-bin": "^0.26.0", "jest": "12.1.1", "portfinder": "0.4.0", "react": "15.1.0", From 44554262a4bd957f352b0291ad9acc8e465cb905 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Fri, 27 May 2016 18:30:49 -0700 Subject: [PATCH 148/843] Fix prop update perf issue with windowed list view Summary: Makes compute call async from willReceiveProps and fixes crashes with proper bounds in render function instead. This means that fast prop updates won't force rapid and synchronous row increments during initial render. Check `rowData` and `rowKey` explicitly so clients don't have to worry about preserving === rowData containers around Relay data to prevent re-renders. Also moves layout jump warning behind DEBUG since it's not a common issue any more. Reviewed By: devknoll Differential Revision: D3357710 fbshipit-source-id: ee2e5be04261d5722abd07a063b345960b0c5cbe --- Libraries/Experimental/WindowedListView.js | 32 +++++++++++++--------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/Libraries/Experimental/WindowedListView.js b/Libraries/Experimental/WindowedListView.js index 3e077414d24abf..92e7e0fc55652a 100644 --- a/Libraries/Experimental/WindowedListView.js +++ b/Libraries/Experimental/WindowedListView.js @@ -211,30 +211,35 @@ class WindowedListView extends React.Component { this._scrollRef.getScrollResponder(); } shouldComponentUpdate(newProps: Props, newState: State): boolean { + DEBUG && infoLog('WLV: shouldComponentUpdate...'); if (newState !== this.state) { + DEBUG && infoLog(' yes: ', {newState, oldState: this.state}); return true; } for (const key in newProps) { if (key !== 'data' && newProps[key] !== this.props[key]) { + DEBUG && infoLog(' yes, non-data prop change: ', {key}); return true; } } const newDataSubset = newProps.data.slice(newState.firstRow, newState.lastRow + 1); const prevDataSubset = this.props.data.slice(this.state.firstRow, this.state.lastRow + 1); if (newDataSubset.length !== prevDataSubset.length) { + DEBUG && infoLog(' yes, subset length: ', {newLen: newDataSubset.length, oldLen: prevDataSubset.length}); return true; } for (let idx = 0; idx < newDataSubset.length; idx++) { - if (newDataSubset[idx] !== prevDataSubset[idx]) { + if (newDataSubset[idx].rowData !== prevDataSubset[idx].rowData || + newDataSubset[idx].rowKey !== prevDataSubset[idx].rowKey) { + DEBUG && infoLog(' yes, data change: ', {idx, new: newDataSubset[idx], old: prevDataSubset[idx]}); return true; } } + DEBUG && infoLog(' knope'); return false; } - componentWillReceiveProps(newProps: Object) { - // This has to happen immediately otherwise we could crash, e.g. if the data - // array has gotten shorter. - this._computeRowsToRender(newProps); + componentWillReceiveProps() { + this._enqueueComputeRowsToRender(); } _onMomentumScrollEnd = (e: Object) => { this._onScroll(e); @@ -273,13 +278,13 @@ class WindowedListView extends React.Component { 'record layout for row: ', {k: rowKey, h: layout.height, y: layout.y, x: layout.x, hp: layoutPrev.height, yp: layoutPrev.y} ); - } - if (this._rowFrames[rowKey]) { - const deltaY = Math.abs(this._rowFrames[rowKey].y - layout.y); - const deltaH = Math.abs(this._rowFrames[rowKey].height - layout.height); - if (deltaY > 2 || deltaH > 2) { - const dataEntry = this.props.data.find((datum) => datum.rowKey === rowKey); - console.warn('layout jump: ', {dataEntry, prevLayout: this._rowFrames[rowKey], newLayout: layout}); + if (this._rowFrames[rowKey]) { + const deltaY = Math.abs(this._rowFrames[rowKey].y - layout.y); + const deltaH = Math.abs(this._rowFrames[rowKey].height - layout.height); + if (deltaY > 2 || deltaH > 2) { + const dataEntry = this.props.data.find((datum) => datum.rowKey === rowKey); + console.warn('layout jump: ', {dataEntry, prevLayout: this._rowFrames[rowKey], newLayout: layout}); + } } } this._rowFrames[rowKey] = {...layout, offscreenLayoutDone: true}; @@ -431,7 +436,8 @@ class WindowedListView extends React.Component { this._lastVisible = newLastVisible; } render(): ReactElement { - const {firstRow, lastRow} = this.state; + const {firstRow} = this.state; + const lastRow = clamp(0, this.state.lastRow, this.props.data.length - 1); const rowFrames = this._rowFrames; const rows = []; let spacerHeight = 0; From 72340161805da4a9c66a8667a68b7d65c8e806c5 Mon Sep 17 00:00:00 2001 From: bingo Date: Sat, 28 May 2016 05:53:43 -0700 Subject: [PATCH 149/843] =?UTF-8?q?add=20=E9=89=85=E4=BA=A8=E8=B2=A1?= =?UTF-8?q?=E7=B6=93=E6=96=B0=E8=81=9E=20showcase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Closes https://github.com/facebook/react-native/pull/7742 Differential Revision: D3362867 fbshipit-source-id: 7127715cfd5f0b03051a74e7b6c1099af9936585 --- website/src/react-native/showcase.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/website/src/react-native/showcase.js b/website/src/react-native/showcase.js index 892dc955fb972f..82b46c05fd13b5 100644 --- a/website/src/react-native/showcase.js +++ b/website/src/react-native/showcase.js @@ -1063,6 +1063,13 @@ var apps = [ link: 'https://emberall.com/', author: 'Kyle Corbitt', }, + { + name: '鉅亨財經新聞', + icon: 'http://a4.mzstatic.com/us/r30/Purple18/v4/1e/b1/dd/1eb1dd6e-0d9e-03cf-22f9-1b6fd816814d/icon175x175.jpeg', + linkAppStore: 'https://itunes.apple.com/us/app/ju-heng-cai-jing-xin-wen/id1071014509?mt=8', + linkPlayStore: 'https://play.google.com/store/apps/details?id=com.cnyes.android', + author: '鉅亨網', + }, ]; var AppList = React.createClass({ From 573dc858ad8e77f6f222f1a1c926b2243b93c5bd Mon Sep 17 00:00:00 2001 From: Blair Vanderhoof Date: Sat, 28 May 2016 12:12:25 -0700 Subject: [PATCH 150/843] Reverted commit D3253073 Summary: The view update cycle in UIManager was relying on a bunch of boolean values boxes as NSNumbers in parallel arrays. This diff packs those values into a struct, which is more efficient and easier to maintain. Reviewed By: javache Differential Revision: D3253073 fbshipit-source-id: abbf2a910aeb536050c3a83513fb542962ce71a5 --- React/Modules/RCTUIManager.m | 217 ++++++++++++++++------------------- 1 file changed, 98 insertions(+), 119 deletions(-) diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 29ef7549d4ce8f..11112f92a9dc6e 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -57,6 +57,8 @@ @interface RCTAnimation : NSObject @property (nonatomic, readonly) NSTimeInterval duration; @property (nonatomic, readonly) NSTimeInterval delay; @property (nonatomic, readonly, copy) NSString *property; +@property (nonatomic, readonly) id fromValue; +@property (nonatomic, readonly) id toValue; @property (nonatomic, readonly) CGFloat springDamping; @property (nonatomic, readonly) CGFloat initialVelocity; @property (nonatomic, readonly) RCTAnimationType animationType; @@ -134,6 +136,8 @@ - (instancetype)initWithDuration:(NSTimeInterval)duration dictionary:(NSDictiona _springDamping = [RCTConvert CGFloat:config[@"springDamping"]]; _initialVelocity = [RCTConvert CGFloat:config[@"initialVelocity"]]; } + _fromValue = config[@"fromValue"]; + _toValue = config[@"toValue"]; } return self; } @@ -548,47 +552,42 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * return nil; } - typedef struct { - CGRect frame; - BOOL isNew; - BOOL parentIsNew; - BOOL isHidden; - } RCTFrameData; - - // Construct arrays then hand off to main thread - NSInteger count = viewsWithNewFrames.count; - NSMutableArray *reactTags = [[NSMutableArray alloc] initWithCapacity:count]; - NSMutableData *framesData = [[NSMutableData alloc] initWithLength:sizeof(RCTFrameData) * count]; - { - NSInteger index = 0; - RCTFrameData *frameDataArray = (RCTFrameData *)framesData.mutableBytes; - for (RCTShadowView *shadowView in viewsWithNewFrames) { - reactTags[index] = shadowView.reactTag; - frameDataArray[index++] = (RCTFrameData){ - shadowView.frame, - shadowView.isNewView, - shadowView.superview.isNewView, - shadowView.isHidden, - }; - } + // Parallel arrays are built and then handed off to main thread + NSMutableArray *frameReactTags = + [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; + NSMutableArray *frames = + [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; + NSMutableArray *areNew = + [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; + NSMutableArray *parentsAreNew = + [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; + NSMutableDictionary *updateBlocks = + [NSMutableDictionary new]; + NSMutableArray *areHidden = + [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; + + for (RCTShadowView *shadowView in viewsWithNewFrames) { + [frameReactTags addObject:shadowView.reactTag]; + [frames addObject:[NSValue valueWithCGRect:shadowView.frame]]; + [areNew addObject:@(shadowView.isNewView)]; + [parentsAreNew addObject:@(shadowView.superview.isNewView)]; + [areHidden addObject:@(shadowView.isHidden)]; + } + + for (RCTShadowView *shadowView in viewsWithNewFrames) { + // We have to do this after we build the parentsAreNew array. + shadowView.newView = NO; } // These are blocks to be executed on each view, immediately after // reactSetFrame: has been called. Note that if reactSetFrame: is not called, // these won't be called either, so this is not a suitable place to update // properties that aren't related to layout. - NSMutableDictionary *updateBlocks = - [NSMutableDictionary new]; for (RCTShadowView *shadowView in viewsWithNewFrames) { - - // We have to do this after we build the parentsAreNew array. - shadowView.newView = NO; - - NSNumber *reactTag = shadowView.reactTag; RCTViewManager *manager = [_componentDataByName[shadowView.viewName] manager]; RCTViewManagerUIBlock block = [manager uiBlockToAmendWithShadowView:shadowView]; if (block) { - updateBlocks[reactTag] = block; + updateBlocks[shadowView.reactTag] = block; } if (shadowView.onLayout) { @@ -603,7 +602,8 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * }); } - if (RCTIsReactRootView(reactTag)) { + if (RCTIsReactRootView(shadowView.reactTag)) { + NSNumber *reactTag = shadowView.reactTag; CGSize contentSize = shadowView.frame.size; dispatch_async(dispatch_get_main_queue(), ^{ @@ -618,107 +618,87 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * // Perform layout (possibly animated) return ^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - - const RCTFrameData *frameDataArray = (const RCTFrameData *)framesData.bytes; - - RCTLayoutAnimation *layoutAnimation = uiManager->_layoutAnimation; - if (!layoutAnimation.updateAnimation && !layoutAnimation.createAnimation) { - - // Fast path for common case - NSInteger index = 0; - for (NSNumber *reactTag in reactTags) { - RCTFrameData frameData = frameDataArray[index++]; - - UIView *view = viewRegistry[reactTag]; - CGRect frame = frameData.frame; - [view reactSetFrame:frame]; - - RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag]; - if (updateBlock) { - updateBlock(self, viewRegistry); + RCTLayoutAnimation *layoutAnimation = _layoutAnimation; + + __block NSUInteger completionsCalled = 0; + for (NSUInteger ii = 0; ii < frames.count; ii++) { + NSNumber *reactTag = frameReactTags[ii]; + UIView *view = viewRegistry[reactTag]; + CGRect frame = [frames[ii] CGRectValue]; + + BOOL isHidden = [areHidden[ii] boolValue]; + BOOL isNew = [areNew[ii] boolValue]; + RCTAnimation *updateAnimation = isNew ? nil : layoutAnimation.updateAnimation; + BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue]; + RCTAnimation *createAnimation = shouldAnimateCreation ? layoutAnimation.createAnimation : nil; + + void (^completion)(BOOL) = ^(BOOL finished) { + completionsCalled++; + if (layoutAnimation.callback && completionsCalled == frames.count) { + layoutAnimation.callback(@[@(finished)]); + + // It's unsafe to call this callback more than once, so we nil it out here + // to make sure that doesn't happen. + layoutAnimation.callback = nil; } - } - if (layoutAnimation.callback) { - layoutAnimation.callback(@[@YES]); - } - - } else { - - __block NSUInteger completionsCalled = 0; - - NSInteger index = 0; - for (NSNumber *reactTag in reactTags) { - RCTFrameData frameData = frameDataArray[index++]; + }; - UIView *view = viewRegistry[reactTag]; - CGRect frame = frameData.frame; + if (view.isHidden != isHidden) { + view.hidden = isHidden; + } - BOOL isNew = frameData.isNew; - RCTAnimation *updateAnimation = isNew ? nil : layoutAnimation.updateAnimation; - BOOL shouldAnimateCreation = isNew && !frameData.parentIsNew; - RCTAnimation *createAnimation = shouldAnimateCreation ? layoutAnimation.createAnimation : nil; + // Animate view creation + if (createAnimation) { + [view reactSetFrame:frame]; - BOOL isHidden = frameData.isHidden; - if (view.isHidden != isHidden) { - view.hidden = isHidden; + CATransform3D finalTransform = view.layer.transform; + CGFloat finalOpacity = view.layer.opacity; + if ([createAnimation.property isEqualToString:@"scaleXY"]) { + view.layer.transform = CATransform3DMakeScale(0, 0, 0); + } else if ([createAnimation.property isEqualToString:@"opacity"]) { + view.layer.opacity = 0.0; } - void (^completion)(BOOL) = ^(BOOL finished) { - completionsCalled++; - if (layoutAnimation.callback && completionsCalled == count) { - layoutAnimation.callback(@[@(finished)]); - - // It's unsafe to call this callback more than once, so we nil it out here - // to make sure that doesn't happen. - layoutAnimation.callback = nil; + [createAnimation performAnimations:^{ + if ([createAnimation.property isEqual:@"scaleXY"]) { + view.layer.transform = finalTransform; + } else if ([createAnimation.property isEqual:@"opacity"]) { + view.layer.opacity = finalOpacity; + } else { + RCTLogError(@"Unsupported layout animation createConfig property %@", + createAnimation.property); } - }; - RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag]; - if (createAnimation) { + RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag]; + if (updateBlock) { + updateBlock(self, _viewRegistry); + } + } withCompletionBlock:completion]; - // Animate view creation + // Animate view update + } else if (updateAnimation) { + [updateAnimation performAnimations:^{ [view reactSetFrame:frame]; - CATransform3D finalTransform = view.layer.transform; - CGFloat finalOpacity = view.layer.opacity; - - NSString *property = createAnimation.property; - if ([property isEqualToString:@"scaleXY"]) { - view.layer.transform = CATransform3DMakeScale(0, 0, 0); - } else if ([property isEqualToString:@"opacity"]) { - view.layer.opacity = 0.0; - } else { - RCTLogError(@"Unsupported layout animation createConfig property %@", - createAnimation.property); + RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag]; + if (updateBlock) { + updateBlock(self, _viewRegistry); } + } withCompletionBlock:completion]; + + // Update without animation + } else { + [view reactSetFrame:frame]; - [createAnimation performAnimations:^{ - if ([property isEqualToString:@"scaleXY"]) { - view.layer.transform = finalTransform; - } else if ([property isEqualToString:@"opacity"]) { - view.layer.opacity = finalOpacity; - } - if (updateBlock) { - updateBlock(self, viewRegistry); - } - } withCompletionBlock:completion]; - - } else if (updateAnimation) { - - // Animate view update - [updateAnimation performAnimations:^{ - [view reactSetFrame:frame]; - if (updateBlock) { - updateBlock(self, viewRegistry); - } - } withCompletionBlock:completion]; + RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag]; + if (updateBlock) { + updateBlock(self, _viewRegistry); } + completion(YES); } } - // Clean up - uiManager->_layoutAnimation = nil; + _layoutAnimation = nil; }; } @@ -822,11 +802,10 @@ - (void)_removeChildren:(NSArray> *)children // the view events anyway. view.userInteractionEnabled = NO; - NSString *property = deleteAnimation.property; [deleteAnimation performAnimations:^{ - if ([property isEqualToString:@"scaleXY"]) { + if ([deleteAnimation.property isEqual:@"scaleXY"]) { view.layer.transform = CATransform3DMakeScale(0, 0, 0); - } else if ([property isEqualToString:@"opacity"]) { + } else if ([deleteAnimation.property isEqual:@"opacity"]) { view.layer.opacity = 0.0; } else { RCTLogError(@"Unsupported layout animation createConfig property %@", From 55717940357c294edf813b564686a24698397e2e Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Sat, 28 May 2016 12:42:23 -0700 Subject: [PATCH 151/843] Change survey image Reviewed By: vjeux Differential Revision: D3362968 fbshipit-source-id: f6de18a5f27e32e6bb64a65bc233d741cb276f41 --- website/src/react-native/css/react-native.css | 9 +++------ website/src/react-native/img/survey.png | Bin 131722 -> 20708 bytes 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/website/src/react-native/css/react-native.css b/website/src/react-native/css/react-native.css index 6209da6516f517..5217b1e62fcc13 100644 --- a/website/src/react-native/css/react-native.css +++ b/website/src/react-native/css/react-native.css @@ -1371,13 +1371,10 @@ div[data-twttr-id] iframe { } .survey-image { - position: absolute; - top: 10px; - height: 83px; - width: 120px; + float: left; + height: 128px; + width: 128px; background-image: url('../img/survey.png'); - background-size: 272px 198px; - background-position: -8px -8px; } .survey p { diff --git a/website/src/react-native/img/survey.png b/website/src/react-native/img/survey.png index 8baf2fa31b7b19ce740fe2f3bccde42a840df0de..af025067da23807e274489d036fcffedd21e5db2 100644 GIT binary patch literal 20708 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ℜdEq0*-0Txakh5^C{djUr?}g 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

    (67 z_hBLu9jm#VoTIzf@0i2F#r!<2-b4 z%MNqZ?sVWEDUPeeyHrl!iH^B0!Ebt3MVelf>T+us8qm70QS-}H{c5=xkTV;vCv6wJ zop94+yo;bSER&S_r`p^3+2tqx=NDlM&o-`bSqSx^NP9?v zjG_aYbhTWb_7HYX2{QvU`h{Y#-SkW>$mz#D68hk|+S5+xL1OO#jil zG3&QcpB-P;J6c!HbtskjX!hw}!9_jRb-OPU42*Y(hS=7~DUb7=V|ZSt?YrhP!fuFY z?HHbP!msblmu&D>WuDvb9p zpPNx5d^>T3mU_rixI;#ikz$xI^~^Vmek$d-*7lJwk)F4E{x7Qse{fM>f(7^R3ZEmH zPV0^Al%4g-O;-475H|mX5Bqm0n^Y?(qCd%^Oyb#D3~%rGyGop2vGwaBWYCO~4ez4f zlEvOGxDR#JDNGMg1dSyi@_q&45Uev(eqJlY<>qtaXen-l1K_7t4q)iIxyfCOha)qD zE2`n~iD(%ojd&jyTPBzfYN$A>Ht ztV@Ym))M z|0}@AIE^K|#RjcYYRxj<#(}R5QrBO3Me1AnZ4W5TY(B1pNzfHzzzK-Q^rL22-H@&%-&@;Mx=5?D8#fTTD2qM!${BZS|9`E^v*ikztZswk`9lEn6<;ZHg*x)?W94U-e7ruyCEJ{&L({);dNq$ZKT>7^O5I{ur5Ht2L$cqE=i(Ia!s& z`FMbMYk}X6^)aPAr{iVs6Zu$zN4!ngfZR;tupn^@Rho_%$%L936eVvIPGE8^is1ae z7-gdmi{+$J=Hd+lNOiV_9&&rM&7sH-r!0XF(7%dKbAHzgd$}bV6opYIqR<(11w$H7Q=cy; zWfRA2(vLtPMErfB2C*!S2#$~qedB_o`P=cO_2cVj?xUH>0-7d$Jq)}Ycjx1-tsI<}c;06H zPOfN%pudJIqNJw#s=#L#JDu_bzKcmqCM#EkA8{1yl)(MKKtoMl;wvKov)8uyCj!P+ z2T~QM8JYx|>YrC}Jg-F^RTGA-9`vWGvs8;0VFxWD&j?J9eoJkv0OJ9*eGlES^kOIz zxqYNqc>VrZjGu#Vd((EV^BN2^Y*We9QaZKkD1oXaD2)g3-01f=XrhD1FAr3qca0a% z5YzWGnVUvy^cwn}@JBbio^A({P|7Thr1jTsvQD? zj8qa0;~VTpP5*MZ;&3N$U5XFGm|b<#S^o5qZp+WKLzFjSXZ%prm;J&x1|Q zd$z+v|HK(Flmv6oq{*(6+d9)UF%}(EY`>oOX=V4Nk5ftp&DUDhKis=zy`3m7z-C3R zDS@^NA8QG}?A(Zq}!sK6)9M8EK<>+GS%Ik6zOdUBZILyz(Uk`VT&m3m5q~G)tQ(ecjJx9 z`0Kr2Q*L%!8gvi&um|n%2kp{V9KtmI=R5G?|AVje1xULhJdP^w94ZESkEHLcrRwpQ zN{X1(Og7nY@jm#(ko^vw{J7O!5Fx%~y3;EJhg)?k!*XOO(UI0ytDhiquc6QE@edq{ z58xF9^f_n7g#A~ked(!l*Mz3&dxC7sFPIy7XfCzXwdDD57^qOB zPwhCJI2NcpNzq8YLXf->Cm+e&O}8Kp`-jXyQLmV zpB>g13MpZjye8h-{9P1%_PF>8@fAn2{>T0dBSVwykI!^o$MD_yu{}<2SBa_TbA-eC;j`EFSX`k%5bG><}iXW$~+AF{fs+- zP|hv=R}wMpI08Q;MtlRM^<)v|$-ZQ@*r90GYG^47@_C*R>uj9j@IaOz66-ep=O1L? zsSn6QYqkt}ZN^pSLe(6y8q)7P?HZ56pb-@XP%NIdi`t;w(~Z>hFgU~e^(5lr$#GtC z;i}*5Qj^qRl?T}C#A<~0gku?@t}c zH9<`8V$A>CVomlQ^v54(!DN_+&o8x>_ovD~W3LU+Q}cXweJyg=eHr0C z3dVsvg(}6b2{9u=t_XV%;w90NsMI3-IMw!6A1Z?3NUG^s3rLV zB_nTy90p40Ep#kF56tXml*y}$HV8~8h~v}aJV%yaV(N(o6~kB}lwo)S<_x$NbZX4M z$kdTgNk1Q_PW9pqGLt|_B8c{DW@xxe;AZ}+c)r@pxSt}g$!hr-U1_%9eo1i<8>2~m z|BjA=j02Zaw?%iljb5DfCvB;VER~jcgR1}>q2=yg8b-P(LyX^aUU)Clvc?n**Bmu# zsf=2+k*qigrX(~qe-x5nz^CjU{GQ32Hk2Hmd?X2JNj}}pw2p_gl#DF0(3#^`@p6a& zilmQAYo`~m-sfJocWxN(h@@{B^Atg1q)08zAYypkM4?=U!R&i1QjFPmv_bKhT+5!< zQ&g@9h963i3B){sMLhWm;A%fL9txCdI|i0FPNv(JN$Udd-%iD<3WnM;+a60p?y*D3 z2lMZANnLeOEXuIVU#|_gU4+t8pDcXuQ$2_$aHSRmXyzt)?aHD3za<|N?UWvw%KId| z)29%Wh`*HALEdtOqZg(oI)a7U%L~j^0ApP>Hv}t|1zh<$h_XG1_FQ*+NV6Y?#Q3^r zK4s-3)B8#@%p2f*u?+8D#>gZK8!d_v7A28yivj#CF~gy$_7AUEHPxO z25Yd!ptT`MG)jV03NQ3g$|vwEh+K#Y3fY^V%L0WLAf$(oYP6Doad}lJfj){TlqwWU zWzr}iN#ksVGG@W18j=BE(dmH1f9_)lfwTelY@cX*&n(lSm#TpUR0&8o6mjE_8`%hw$k5PJ2VG(y3cE6vaHDaq8S+$YoRzZ z-|*fdg*+#PJ*kwPm4PI+&`(C?3}eyOfH9-<`F@N zQXx|L2+s%Uqol+NIf?<6XgK7=J@<0<-Zx=Pg0)#?)>wIdF=|D8#g%I-DYoVgW=I3{b2$I z{J=vgg*H+OAx>0s@`N?^zk6gcf5B+`for?z2M#0qiH(6i7`W}=FPg84WJC(^lz-^m`K)XS zSUcMC(H4uw>g>d=eO{iSV}rFZl~Re&7evtjou-Uyonz6^5afJ{VV-Scuv)*MnROKA zUcka}TM9xj81`wmTP!S{pi-;QsJGFlEk=V*Q+fwG>~3waxqOwC<#~8g&SA~>&D8zxU1;*zqs08y`kwxOpWKcw4PqSUH6fM2lVDRwZq+X_QVIlb znfCs^@AJbM6gq zrcf-fc<&pr+AugcWcOg3-HjWpUc18D@>O;>mgyh%0FWLi&&Tt8A%(b4dE9rp6TCxU z|KcFBzjI?Zecv;?>2m6}alYVQ_sNJ-@aDx z9)&`UN~=Sw)1f&rOS3yez1g8!Zx9xWNFhFw z6Arhx*tl_-)iu&y<`{^&Q?3hm- z#I91}ymFzh#9L>B|GpgZt&tYL`Ph2=D;u|oC3#;|@}c)F>1ffA_vN>o?S}vK(Uquw z%R9VG1y4}S3BOrV^Gh}V0b}gfSjC%*Qr@p5_1Hq90Y;Swouaiyrv_`Xo{D^-M72?; z)|{X*F-2oyhI+k4x!IspX^_tsxb*C0KKv&i!Dx;2yzF#cmWgqTcK8=al9XI7pkA+Y z>vM2oRBg3Xt93lj#~90KC*>|#u#oDQrP*mbWjWm620Ww^gq0H0)e6&dr#ScUHxLgG z*tl_>)vH&ye&qriE6eO{Zx9a?d=I=ZhmRnpfZw(dk{q5jyt{$bo&Fx>8p1*c6HXFe6ib(|+!HK3fQO=2n znmP5>N>7r)T)!TI@2_}leA|RskYiw%2oWJ zgr8H{c1xNP8^f^Q$6A9@igv5T^wcC*E?-3EipTqOHZ=7x=gAe7x?Hvp`@G&eEFki+QHlBtLFDtkMw(= z*^EErDkaX#0FJv&FMHA%iwr&unbl#ld%7(J(TnlkJi(P=g4HXB^Kb{(&nhr4)g16XT`r%960789dClO&5EGCL_G zgOIs}dzoE2%Y%b&U~hki_2p+-e&zxz&pyfC?lv|xc=-UW6w1okTwwp<8>WIEn6KM^ zzc{*J z<}J+E)LSFXH^$A6MJ8q!XmqA1 zSDVPtM;b^?icS)=PKgE@YX>i?NhC=^KA-2IhaTYQsK@&H8u@$GcLLnUG{BZnCdaES7omn;#|&eFlS<{yN5F z8)(7t#R#{$9dwhPjLO2Cm0+J@q9rTh6pL^d0)Q$WKI6f zxw`t*Yg_tV7k84!2C)k!&fN)pL8mD0ORVvX0ja21Vb24%Oh&3Xkq4@l?D+jrVwcwr z&FAk^yrgO<&{pKSRk66#@E`G|_`0Gm-y#I3QiDtr3nHt0=rkryG$OTV2?|BA+F){S zfzHeV6Z1>7yAxEZlZ1sl2(m5TBtfSdohHQ5@C9E#wU%5i$9?DSWqo6v;V>eX3-2OP z5F|;Alpbf#o+bza27|$EZ=I$orBa@yr4y{JZIGrZO8x)ry?K;e*L~;rx%a-U_Nw0Z z4f{fD1Ob8|NO6%!Q4*=uBUxUL?Zjs^cI-H3V&^0oXX4m1nb=MyW7!!`V%Z*B=2#LX zYnLTaq_{5tK@j_n2GARN@2Yz1z4@aW6iAV700ae~yFUNmpu66!?yh%#?|Xmi+uNy2 z@qHi5Qf%0;o?I@&^z`go+mZQJ1-cp#ivWbgvMkzKyXoi|V8gb18NYCzlLz*2VD~eO zoIXKN3BeYmToQ0hF_6&N+0C|XTPPIr3kngay-yT{^!9dg`|aC# z;f0qdm&-K~AmkfQv*C!^Q&#`024jLLtR4SDP-{*DaUza2aNm3>BD+sB`6^m@# zaUZKUZ)fPtNe=COhU5EQVPb3?$5m*@K}sntBY%Ebv;0tZ!TJ2jso+n~Oeg#2{Hr{$ z)>$%`TG&Hf;#@SwF!ffiwFRtcc5_mZPGV|FmVzNmV&2|CQ1gDIZADj6_PCbV*jsXT zNc0XV#9FCn(Hi{N%qM}?JQz_BVGP)gLt|rsVtX%b9ldn)EThoUMn2zwmrB)AI)osJ z0xGjpwQ!5pKW0bX@Dm9^97nXbcd%i@MxKBE1$^Jfah$i^uf`aXBtmQ0x^)wsogGxG z)qhgJ)NF`m`SLzSM~69h@CfO23d?$X4~dx%V<(Ab!-n;Ac6Kp0x3Il>-B21$9EC(t zfbbN3Z(kSXuwa zGZV>o2dA(2%v$G7EP64*e1|@$#ayzkXzIvYnbFy>yufu(6DDuD8-%PGoxTPq-&;_( zD@#0>cf|unurf}tY9*$I2u&Qu^QRC%f#-Sjbo9}^Y!#i$SJBzkkDDvu+BPv79frhl zL{#+&-?SOus>#pWanXg@*x1b7ci%-@+hL9$Kgsm;3^k{ZqNcYbxm=cI%LZ7zdM)X+ zL$w+#ys_ue+Ti;kD_5={m&>we&#Md%52JO65OUt2)Qqi^Hp`avaO$sJ{VNb6 zZ;8NYP2kr$M_ZQ5veg^uU$c=5cb#GHb3fp<=YGJ%q4uEdMd)OW)gs7WqrQJn~l0)r;l(?RnKd&}7}`N^wu# zlXnw^{ z{_IT6W8yii+OUn)o3=4_{xq*X`#5`_e}d`p8I%Gq<6@-BO87)uPJOJWU_biGIr+aG znhf`wdQQnzvlPTgh1z@r0JNb!N=y-WJ27(U*~OSQJ^EX6mP}iszq_bzFQ(PqQs^BP zVqJm|)d_ujjU zk&$tRhlisn;9Sz22gX>M-Awrzr-%4I97%Os8i=H^0#5EPo)xbMTCX2XuV zdF{C$vVZr}To@X|OIg^C&72mEX^;Q)-j%7J8l2F7I5-pk<>>6i5j%D2{1}&gvzpq- zh1XwiV8}=y0b2>$^Qy_S#p;y})_ag*mxRrh5Y0wFko+ht87DDulprKHZkkeKiRRt` zI(z!*>|IHrv6)OhkB9}4NYGKt+}wCAo#D-%k1O5OFmInCw61v|nwlCpbm%CfqoV{t zNF2xWBOe5*lt)ibH>*~ypuN3~Akaincv%ABVh)+>y6oDulZJ*yPMkQ->}(k+BeZ_q zrGZk4#>NI#ty;;dRVz>md_TFY_xUFgwF6UJt@SSvLQrUI!=V?R zVs!L8mIbzx!XRW6C!bo~F7NLvd7nKx5&!AnR6J520D9#t6)`e<$^Y}z<|zCaKX&^F zgVbWuRNHWGCgn0c_b2+D&x1$z1 zNw95)&dzo^J3B~{5D;i>a2)&foUb1*^SgFj>}3%86+$U#YU$)Xk9?AKx9#H9=f20Q z&p*lR#2ijq;y!E@! zoT+{-d`G1ZOOHh_MuZT)65@3Hqlc_zE-@6Bk{E*q|6*7;j1c(@zjEJ7uPH6neM-_2 z83>YEUyeA5NWzFj*Sh@WOO3R24$wWYg3kVxG`4h+&gT&rVjZKy1iw;7zgd9%1c{;_ z#WrizHP^m7&Fx*>|5Lxjs*QK>!V_QR=-xe4 zs}&r_#b6en}Q ztKavexJ?V}S_H{Yh}~u%VQqR4UTgSi0o9Vm^l>Aq;EfYF8peE>g=MiI7rKEaoVc3TRzx#0Fql z7E%hdzUZ+r*H(zU1zKzT>I_mUGPx#pJ@j!_ZQRbj=f2C{=YGiS_!M^9!m(W{m|L4N z>ap8@|mn31wP-<+|CY9%!pX++zbUnhXojSjXF?fT-} z1%Yb|@oP#*Vl!Vdmn6htL}Vc4Iy86o($+UXZ|^GF`d5-K6j5#pDQbzQer0YxS?tXL zpX<-NEezi6^)Hv&$u6cmH-oYrx(1fh()Vke+5Za9J^l?&?mvL#g*dKP)2!mRsO*)sSsjr*PffaQ1ucEoVhfFqyupJZ$Q556P&DK02Z}Qk(50i3Hl)RhN z!oD^8AurC&g880H*TZXB6I<2FEP*d6G_~-a5B?0RHtpo4AAFO;uRKGgQpQWA2(`8v zGwOfbk(OIdPRQSQ<$QQ_q1Edw0WlILhM9^PZOzvHA812QgK9fHAqWlRQ)JgRdbgxJ z9`J1WuujZ|OKx?c6OA8*wZ0byY}=x_vzM-wYv^CTp7!2l6dRfmQV=CEaTpN!l^+jx zTo0>J=M4xU&{0Swj&K~8RaAchg z#BogEM@VBZ3cO67p5?vtuDFGs6>Dkg?j-Hx5dy*_APz!wWsZ7FoEzs*12r3Bj3F$~ z5i3P!|8jbk{Wt7>W)n|+?QaVcXdp3y`Aqbh045;` zG`UiVj_zf2Enh`X|7vnAZMb%-_WdwK>zcGRX^Z=act|7>LT$Guy1YAlI6etWm;b{DOKL1OynzZ5x2=)@S;7=bp1 zBuR+E0Hq|hlSb=^B#zl|+g)tA`#~C8IeyuTD^Hva{-)mHXsIGt(`;wgw%E^y#;i4fB}i;bs5mhs zrk1!Ghf%GyxF>PkG*MW^OQqR<|3}%lbtg_fk0l-AI3S55qA09Cn-&8?kjZ4wQt{&B zU*~&Y_$#7Flgnj58eB_|B;rq=Jzf2^q1pOa;J4!P+T7iQPwn_|Ek|ab?Kp2D=G^JuIsZ53_3P{3Q&HAnneTr4fFO#nESt8TKAcRJ>g+5&z@Pg703ZNKL_t(>9FfWv zxaZ+dQtIf!pPnTQE5uQVt{-L=6;T)=w4r;&db;}t7#TXl*zg5h$09}`Fx$HF_L`ZP z@6Y=B?OW0=Q5KmP5ezh0sWmOm?R|~*zgXU2|Ifvg{7nOQ8}qslsku($grFRd&SmIZ zxt=X|KFE9j*)O8C=G4KxXaOP-4BWDrLUZez@<5h^LuttoMAD_SykuP1*zPs;l&tsnHK_l7d873^wkoNpJH}=jMGOCAf>=|T~=(^LfXp_Mges(yLd^G7?VV_ zb`8+GY7J9k=NUeG4j~eRWh13o*OjyGtc2!!(}A8|XuYMxqDWwEN=qx{vTk{!cYnd< z1CAwjMhQ-!4H#VPY;sHrHNxrYr*-qcSxb0J01K%J*Rc? zkZ}YVTNGOJYFmHNy5CXqA!My0CepQVUK~e+K};&;B9$Vjl<6E;$=3ILfYy$FicO8! zZW^T&^F7OususRUB8{mVl0}aYf+R_pnVF!esgqCq>c8YWkA9Bb&wLL|8R2D+%Iw_S zY5i?z_{i`~Ji5@DONE!>MXIY&?Hp*dKJFnuC?uU>Qk$G;jKL48B!NK!UN%G5vSqB^vW-=1 zZsW;s{XNfp|GVV!4r3$dIJSQ`o3`DL4g=);oP~5IQ)_@FH6m72rlv4LB9NFjbc(OD zco7Hzm6>Tu&22pVbDtp&|A;e(kC4u%i7ZWb!TuvD8GH70@Sp2i_2we$NHUJ>UDK*Q z)R0mivn+8(oZv(?BSIV}gh7N(G*UvTp_z_lD_DE$PTIN#_=nH^F{h3n!FCj<4)5j6 z>BBTPb>d%~HmfC;D{IY3EP;+|L)oZnktKyNP%W2n-8A=q^k+z=QvBnW{xjwB9O-lh zDb2<$oz7oqVpkw@z8wi)(rt2)dn=hVL*~VCYz;o zSsy)X*V8w!hQ`)z(&;o#Iz#Wub?pA(chTCQ6FB(Nv#i^62VN>gH3&dJDqFx-9&ua- zqnVk$fDWrjTaiR{F|w2}z|7=0nPQQT|LVWwi~s2lIl2EJnL-L9HSKx(Kiu9ePd;;| z`t(9;)w#Z8YSVC=JX_q`U2q;&QtwiTp4b?oz+liMK}f7MMhNo7M!Ne~(6f3iU43h4 zY;VVrDR2Zm%Wh%t#8E6uF*`NOzGr{PeINd5l%+5lgas)#jZl(Wm2XTKL|m-%th02e zwYaaKO~P&WeHeq}`;Ywp*KyH_O*;XW6<Ap!Q%2Zu;oLc@fltay)vG#}AVNy2 zvvcH&4SeKhe~m{!`}>?dbCyCWMV#nDTV8y&AuYB|RLr|KY^TmuW7~?=CAGY(L47Lg z%1>#-nnY`ifl%vODQ*y>HKcML%U7&n+4?Q?uiikhu?MrO z>3^hBnZa@u`=0*}>$luNV|x!#IEP6Lw(Fsc!5~PYkg7j}OiZnRNWBtoNg;$F3Ihb1 zJ0JKcD8WBI`j?Ps@KQKR|MYEL&fr`S{Py$`9zk)#RnrtRiscRVrmdaMFZVXsf0VY> zXEb7Ulo&hFnlK89DKed=!4f|E zE?q7rwt}H+`5KIlIey?EmMuUj6lN!elf$#g-i6j$tlThSB<;ec4rj~SCi@TCi`MUH z!v~}gZHdt&K|~Y>RLe1*=g_xqBirwJh&vzn7`JSD4^8deIF^N9^@*YYGk@{{-q^`d zDq4GbICE$pqvyvE7L+F@=~=m!rsht9(8tYWD9_Gvdf#jKRakz@8rE*U1C)hM>SGv} zGGVn!dv_n%LL*0BeUT`rA}xoME!Q;GuYTmOrc z+^UVtMM**w`S|4sO+r(97wfm(#U1ba2s`h6gx<9q$rXwi3}LlO6kgIx=n@-}#4(PS zBI9}-+V?C$Il}j$sX5PzP20c-v`NSmid5#N89jH3RU5Z*+pY((lp?O%kR?m4S}($~ z6x}P@XuE!?>Oc zMyHx{YRzn@AD!{_l5@jf_noo6!?}ND$@+IlHbokYHuGIhHK}Zh?iCwYwP7p0E7wve zG@&tsVSq_uwANR=xs?z|+u`)l{R|JDVqon$n%a7a=f_?fV^Fq@Yk3%D&~ZZOSKsJi zsk3A;#^9yWL{Y?-KlkrBx_39}Tm~r(0`c{|=Yx-&noOdFgmIl~N5PS7@9{oU%<#L_ zm?jXIBq0oa;uxBmTUfr~R+ewLm9E~^SdNVj0-_{-%SpHkQJY8Nf`aMcA%ta-Z)_ro zVt%xi!LnVH?V!VuD6WsZtMem_(d3FPoH_m~U-+|sM|p0RR3?M1U}84@*FQWR{Hvwr zWxcLX*bVLWhu1c{U#w~)wSg!I@GCKeQifHx?qcJ%U9@*Ct5veZ5n&Lb^*eKO8bBcN zav78nL^>v{RC%*Ol2TIZIk9vTit4;I#z3xEWN2uRzyIUkV`^rcbS6XEhI4cAZ#;E8 z_`eofqt3M<=Sg<Mp_>CYI7z{zVLKs79R~MUZ+sUeJchcC}fsW$&gHDWo$LT!= zr7RpbgCQpL{Tph!Y@Hh)AtaeXi5DOL2fq2}=dc}xV>x)Xte%@mc0PTwx@Vy^78BPk zOtZe#zHLRb^Ju6|CefOpT*bC+Hto2V`#bx@w zDY(5i^}pv_ek(|72Zx|iCD+izoew<1Jr941~u@#-wVK$Csz9{MU3EinjrN3d8tshBXw>} zgpfpGNWP&FS2-L#up21^NQtXh7A1V?LRrr(w9aDSx{Pj047V5%T5FO7Hf+D2fi>%y znwlU9!+Nf0otpw71!1{N_p)_tzWpxZNRuQn3C3+ni~p`EV_g@qzZ4K)A=oT|z^_tl zYUaL2ewtjN#LUEa{V-GKrootmBnnu!?M_zSvIV~q0+PhAdRc?@E2$-7MXrk&0W!6J zYO^2m%}vC9f{CMg)k>Y43Jrvj&&E66M?*_9VKqQ&fn|#adkfZw7P`L9Rbv@b>mY56 z!FJQw=?q~|LCEW6*6P%`8U%sw<7IN(cFzY$m zwH4#TLmc1t5?(4(KhM;;DXGp*)4SppR&LxzP&H_+(FrY`IrR%Uck#PNuFG%ORkW(j z8Tsi*lM=!(cJ3tUY$F4!Zy}08bduCvtaWZ0wANV4qM@aOGe`C?H93WC*;tn7&)Vwo z!KwJnLTl8yG7N%_oLzDq9yY?p537uxIz`W#bu_nj<5$ad8&c<{BGDSp^GKy~9DMnQ zfI%pQ>xkBBsJ}E`(X$J!aWivW#E6FRmW;~Ug1fLRk|^fPv4eQIJl*}P5TZ5+uWn50 z+zbdIFghkzY@%EZ7(DnY!cqt!RN5Aq$!hY2a#;8HT}!k9Bh2aEoVqtQbU3!n)WkFw zhECG6axK|H13C)d#VVRQb*?S7cJZ2GQv>6p=NLYF21nTlL1WIdj-DEiUtY|GHP=Oq z%ms!EbIDU(MeA-`(JL&6>Rg#)`*)MhX6aeE5dp+e_!AK%N-1pH0jV(cCTDf70+VQz z>yXXmIeuU_)05LUo=e7I-JEZ}GFH~Z3$0P-%9!?bHS1U>nlkFsu{P3mY=+K_aPGt* zR;}Aawy}{g@E2O<8X>VA8>K8TwGNy0DxNx5j81UUX;P^Shxffe62&N6VFbmY>G&^# zMf`ZWE@A|r8lz_d^K56{dan|u$#PtD9B^#kUaI9u+WVK2$rTC10Ab9vPi&oE)63)# zLJ@{lENS7UG9)I!+zg>2V`}!!2oSa2vURQ#LSW*URHne}>?DInj$qjmA*6^k7Y3)} z#}`_n&Xsd+COPR@3^t^#hqN%twiRQ;V+@@=N@wpX8XDUOf(r2N^eQqK9LL4;QY3MR zPBgaXp)BVoc9^QOh!6sdA>Y`-;E_X&j}4=22Um)=l(V?o1za~V0x%co@nTwhv*5{l zj4`d!anMQ3$pc527&}K-|0)`qT8WeB+UAiMgS0HtsXS*-9^{#Ce~G*%Qog=_4sFvj4y z8MM*te)=DI`a6$t=%uHa7#*R|)I_PN89xlK;XBq@3K(O^6iQTPr#Njp!AYjpZtaPY-h9;42hbT4?X=1EY!K!rWaP{sBWKQFxi(6Owu~iTIx`&~ ztQU*DJDi)1pU>D_D0<@k8X;`k;=+XqP9NS!V{0d^y#wedAxW;y0mB%B>v>cvvwY>R z|Ac41{VirLj4(Md#_>akICbb1+Pem5ZtElrs@LQ_>nshl*4UPV?KvFXyPLqDMOikE z5)G3T{pE?Oo>^#(o001-Mxf>)ne_Q`XTd62n5{+#Y}a9Wa)MI_cViMs=fG+l&wkrk zGFO7@dJGMow}2_*RzYhL#)|yJGSMJB#SuMU+0=4guo;k zFOy^R+zEyU&tlmMA!LJ;;@Pt^@qxv7sJS7;2tYa1{ikE-zO%CqeOSa%4gna#b%5@0K5lP~+X2VvL z>!G9T*Nn{=gKaxluER^;{U+b|+dt>z(G!G~8KT4|2?N5|CzzXKWN3sVdv`N6GDOen zHDohI!pN@+lAj<#7=(ayzR1Z#uP{A6iS4>LwrH1zFO8P<#6oM-xmpb8W|EilDS4tH zt=^j?!m(V3O1aF@J-dlQpT3praWWop6m#{1Hw~7Tr95?k??3h^k*<)=q+bt@3L&vP zn7J@Yd;bbrJ9>!1V1YG%@+f6tSvJbCkwVpe`o;K^y9r6OCY??ZRzpr6d=Z31DT!x` zvB9bM8_^=vwl|Cz0jPwU$#VQ`WO%lelG`OT+Kz)`OU@iS#^8wqlu9iWTiYB&j9U$Y6LllqABpCDR;tAHC$j9g@_tf#Ai`DCPnLF<@cE`r2x`}^L{wtGK-)(QGL8j(~w&H2I8 zJo(i}F@!`0nlmN-^~Zmkp5Ap(nT0rpB*sc**wD9;(djV`o;{3hTZFz(@4zaWI=bs( zzddyGZHaxV13<>?&F9j%24c8l`Mf4V{$zm{X@tGcq!Y=cOr? zis;LUcWOwIkoLY6wDt7iIu6-WXeIrpv{%%6&Ry&(<#PBhdFcP0JdWzH0eS{Jux^P?@KP< zkyqy(5+;VB*=TQ5Moy+}^@uhGg&|dH!nj3-hDI1Xa|Yk9QYw|OZF|975+MX}6p|?v zShe9+(%CeQZBuAzV#Ai(xbH)srhWNJs$@Fa6HqpVoJ1=~xZlX#)GT0$66?x)D)3N*BKkc3yBGh_fuS?I)YbpK2Evoi?Wq2LJ-Mf&p>Dtb{F^*3f+ z-JrxZePI~^291;!ZYqWHQmDvhczBHI>8H5st{rrC_7H{(hzJ339Fvr*46NQj&&o9< zeuT7a9LFXKL!zj1#SfrsiS2q+E7P1jyq_3w1f*S;C<-`n;3V1Ke`! zoov1LgVr(%95O6h?r;vTSxg`yHP8+Sic^j_rSi zhd=!LkNw zPGd`fqb#H{{N1x(=F#WAOh>7a{+8wRHutcurJwH3ej06?$=PWV1GX_>EHdd*-G~6T zV-qJKKlsw;dGV=d$Yv#v{K98gwP`1QWfmzVZZ^l>=O5>p?|zRYhRF+GpsRlcJ*zen z$5%cuGYk!-))vy~471}^a3RsGjxdr-T{3mvbqb!KzrpGX5<|@)tdYvD#nS-Wwn?Yc zoH%iczP^6?`nnbrBT%ywVYPyg66H7mBvDwiKiAQO0ALI$*Jb3~8TLK@1ZfZaL=zjs zZ#?*`+YXSnm*v*0+-)Z+!SIY(I}t<)Q+6e0^0&<=1sc5wgqt<5yuIhcV$h6 zk|ZX%f!vTn*$AOHd-e!^xlAhSk;E~fj!3jXNX^L$=Q%!lj>JIP_Gru$>22v|dTxT0 z;}Hh|>8wY-u^C|u#w5TcRYOY&V{qIILuZb0c>hbJQ{Y!MCl9^GijCWG9G5tbkXjQ4 z0j>*f(IyBZMn}%lwel8(5LY4UH2GX@MY3K103ZNKL_t)Ee5skK$qO~_j3s(|@^#*vcH58x`BV`-h>rE}yp)0aU6%0D_5{?-Ri688F1sFq+(*^JQs#fVOda~DYJ6(Hh%gyewUHK zGaPyONlqO*#^lf-Gt(o8z+eeTr(Kk-&{~ru(A?dN=cQ{0pE`g*2}xoS1ho=xA-L%J zL}F0NLtX7#QwcZj*VicY?QKyMZ&-R=6|H)nJ?1c-pjg< zRdh6V(c934U1&rEWx_B-3J|(BhC=?RVtI)ph~kJ`zC=?)3n#}Xaa_fP(eunqU7)q2 zmnig^o1LY9)mnBu@Ce`j+s~ntKv{6|)t6bl;dX4>Ccf+n8-WpuOs;_CNVG9%WAH4| zW~q7OwMe)r#E6ozBm|8ZLl_x~jZNHt|NUIJFv`@#1g`5+Dix7Z5{5VX*lvT8it}fW zGdnqsv}|k>Q=XdQ+>wLyuiwI!UGK$nJ*H=;IsV#9oIbpt6R+)M@Wji^OiZGb4GKED zda;#F#V=p?@b!&=;LYdVq?R)9`X6V0V~aO#@T=eM8Y!VrYC;HVuK(IBy_SWptaHt= zfdWDhMKO+>V(ZqexVao-LnFj-OrcOBmrLXO*SCu9THrV?zF+0Uf!)MOLQ18GN3~F(HgsL(8R>@QrVu0APyq7t`8#;&48j<4nKb!q24l?YuGX8~hzMa5 zV;DSgkjRfn=hEO*OnhAfB^G7Cl?$}$ZOTfY1&$E8(U3lpX)wvmhxQd(3(%RL>_}F=nf@)=!(4R%g&V}CQ z4VcK_q*6HP4E|*G;zPRtIKZO$U~URA0^|W@YRNkkiVYZ9OFWI^WWk;QmxU16mW_}# zqjbHRojIOOd3u_2NB5&eEnDCBW3&<2mPI9)--TlkyShkGe zb0_gK5b1=Lre&1cyK3>?IvBuF9**Y`#{p<4&&|}zpRtgZgOj$I9iJl6F|MshGa;QXV?iL{UJJymK33jKQ)j zlx1PGsWrR04#!s^Ei46RjvrxWW)jP`2!nw4fAYVkb6^#(KKCTY54^CRQDT@Y z2e`IEDVrpT-%*U$XpQTp@I#*@2+5X;wb>F^;!3*)7-O)M&Dh9UMn;B6yR}{i*{nwr zM@&psiR0SvJl7LQVW>`zbMWPHUVGtrvUwXflchR6N9VFt%8wfKW{nZwwXHxGr$Z6zzljP0fGdJAQq7#RV>j`EYmG^y!NhJoP880 z+3Y5dH`#r5pEw)udbP<$w%6;TM7AS&WlOR|i6SY95<3WjsDlBfVK4>LZawvV_m4B| zl1Nkz>G}Kx+&lN&bMHOp_kQ1BEq73#Uqrf+ixcOW7#(KQu6_7v%FOu*jz0fwv;-q1 zQc2p|R+CCDb5Y6im$aYDW*kK^k%cvRQR-_GjLl!pF;}=85^IYXT2X+6)F>9JRZ98d zQuUoANuo=t!h0=QLp=v0B(+Kv<+GCX*SMr{`Dx)=*-8dL7-qN95)#OdW-{_j!JC?r>E zWzX$*bAEIXtVKx8(0zY41>ij5=eDP1HSLWHi?=}wH`C+8vWGePL;W`e+Sk4ch#-#&zn=J`gfMK8^% zKw7XCrA3>kq-3R7^1891?6^XSytNiD@3L5_@y&-Hp`}=$v$KPqp4F^c)s5ETvckPu zI|wUy@w;E+>F<7nwsIG@f9S{Q+qi=`XuQ%`?|LAW$HnPUCdY=6TH$#<#=;kV|9|Dt zFZ?O%wrpklzV|V(Yd;_TncqPC{HFeFb8&nGow-7>7S?aQk$ka47+jh& zLn@2HL%ZH>6mxTPT$pHb;>0Q1+uJ#K@MhMpUq_>H zX`NH06rdz0pMQemhfm;paDMcQn@HjnA@T~=VlC`F zbRWkLKgnXXLa~%%=E67+|5ery)sf)la@1;dq>$`?{|70xuA(wO%jIltvxHfT(hB7W zOq9iKtfgcuD~->t6I#N$R^26q%_r7E48rkf)axwHFL3VM2%hJ#b?ZiMy6FJ!(!|2o#Z=T`4YLjNAJK^w%vG;yFc-B6mn%SDPWNjdin-%avo9Rof{UqTnVF) zVoM8mJ@_*$T%2I!)Hp5e9#+A`$Pm_KA$KV?i?a>-w{PU8Lmwd9QYXA7YyK@sV~gV` zwBu%t&oX@81>;;Uh08Fp*O#IvXmv$LwuiJ>92})^b2;+)9K~Xh`T2RCdFC1B=jZV} z@2w8`1_ag$TFa{_lpxpQ;Yb)ca~vfVTKinDHN#a7a~CI=pSXZ@A&xEEcCTS=cN@v7K~KgH%f2dUSqmnB4Ai?IS>)oU$>{X8A3y2%#=g?7!w$#G^b zjN|2t*DdElIv!yVaQ5U27z+poV^fqAL@}gD+=7c=a4|{9*%L4Fw}1Ls{)eBvoB#43 z{ztrg4wNEa%F#2h9f@4=iTnVB5L9c6w6}Hh;6L~$?7ib4X%rELAxeO9G7nqaY_fCT z9zOij|24TnD{?V=Y0W!p>XnBG=l^vyc8QBg3bOD-BQ**Mt78J2-gvN7%G$KT#Bt#^IIy z?w0KLk^=0@NvlHg9y28G0=faPAe4*NP8K#xvxUX;T$02vHa5!k?VE8N?-D}3mNZT2 z>{(CWrk#uo4wBDnf;eF4=<}=_*h!((LbF!A#tXZ^_k70Aonq|lIXqWUuZQf|+QoxE zd63beDV}`#4ChA|s8j=tRXAFp@`7}!^}SLqCJAU?x0$ZBeV3g>^LGJhCG~oh_UhO1&y*P`bHKq1;`Zw)m>%N=F7s{913jSp*21f|Ad|m3A=3TIUUkc0q z!~)P(zgx(<^ETPE^G4PUY+-Wj9J7;?2qkH4>!7P| z1I1DcL8C#EB$s78-iU-ZR^+^=+^g>rx*!uCMkK`6qT+1MgtZoHEomC#d3l7;Z%>E_ zi4>V<)i0Hp7(T`DsiP=Yg4THCc>2+=P???K{U3XXa{H>K1?g=XsY^sif!2!A!DBQV zHT;}MP!HLEaDcmhVjna@J~43<=SF5Z@zQxtoSI_j^m(S|8n{X$gv=WKrIvN;H&JS7 zrBPo=fd5el0rg51*Y()7{~(+9?4wbuBeD3!BFgh2P6(QpCq!PxDwXYJUh@#|u|%F_ zFGk49dOKg2JLwLklPL~7W7=zfA)<(e(W`b3fE$s00+-a_sKvXZJ1rq_vRg z=_(^b)BMr@`vfDy7m*~mo=fkhO;{r^@k%TBw?YU(97n`ygp>}YmKLmq)EMGM1N(A~ z|79Woo2Fzv6JKFPjJyMs$jX?A!-%fl4(_?{ZmQKflatd#QAkHeCtJ5}LMcrc{_v(n z)>?A;0t>SjIeGXAYLx{li?a+Kc@E`h9LFV$nG($)&1PLS-T#ah+S<~0fnw}Q^_>Z0-iVR8^R`slA)tUj)BwP_9mx2Lf zVxp_HrjC{p>0GSQB`WMkV$h$<02p^j%Loc7Bt7|oVeI6S&tjCxv)7V;mq!0mc zT%Tr8;~RhRN1Qx*7^$-8hwu3arKmRQ+%&M4pStzqeBs%z@$|W8aq~X)#c765A7l4{ zJK!3mVQB4;nv}7@6HCpP3|hk8+c%MpF7Pk@@n7=oZx2x{I&^gS*`TI0U0OZ`VKYEV zP}(60VeIs2YPAaO-90N}$XEc@r$zUc}}2zpzTh2EWI7VxQ5ZO41ABB_>M{ABe{T&|ui$5R=Qj`RxAXh9BB{7bY{NkaX z1F8A5Z~qyQwd6b)KJ^m&4t)SG-$L4`U)6@y4H%R^OKx7dKhbg%RBV`^naZjn#N~{V|4~@9c4^kDTo^fnu#lLP{tfL6tZ(IC z|MM^K@WVs&^yCmCrdADTEtlxsu$h+DR)o;hf&~^Pr^xsKo1nrKc$Uydyg630cXd=i>-P*a!*35UsQ3g4VwEmR4wuv6jIXpJDoZ zN_Ve9Y8NRraU76X%dg)3bA0q8zX{EGzVp&|Ieh*Ec`2yY7HBq__{H*Dz0D<~9FH&z z89wm>#w0{mQ*80LY2P|iKE?A#h8R9G&%#2Lg6C0dHnJ(!NMb43wChG( zuSjEY@ro?eFKwM^E$Uqi5*u_QAxYSPtHOA2;3cL5k%Lq^}Xi zBCMrWt1&k@N)*McS+^M@Ea^3_09xVgAi!ijPO=$!mc)vlHAQ`kvGRhism6&}7_FrC zz2$UWKE#GjW9gPy5|epC{Zb1GK@wkDqOwgA&@Q`fejk(LBUBe=5vGaf=Mhpu8ZtCF z4D(|g8#>DP!Z^Oxm^2~h70Bgs*d)D@6oMa!<2cM-7-f2L6h}#FjfCAhdl}fh7F^9! zUp~s`KKB%-Pfid;308tg1%?#I)s%7$@lr)iWnlpuHnY(ME5$VLJQ7Tt5Cu(yA*-k{ ztlih^{Py~?dU9+rT~Lw>LU2BbOm(F4?)=e~wHVP#(BI}2jI=osgX2i%s||)n$LQ{8 z!S{XAG=19*isB$ZdLH+F;^*0Q@DM{UKFdqbewSLUMxj__F{ttHzWjUiKe?XgM~_kP zJfdcR=NIW&+Yja{TM$5Li8X@BkuwC1I@)v5C^}XZ`PR2jqO?WIlrT!U`(r=Fp$|Vu zl0ej`VWR}c_fgWJ)p017+Yw3<1p#q%g(aygTrn0<2-4JIQv)`8EHU=ho>u*}b!FzU z%%;fUwe|2MR-SRBI5RvSpB`OE7hFkT?K#d9D@Vg^qR(%D_6wbTk& zq9}e_6*R^eqBLgno&$8P?PvV-F%CcV7#D^H@d_G8<@xHdZ!x_%!*p$y#h^}J`7Fc< zy*+(+xfW9Ua-P!FA@lAkE)1Wfwot+GT#_i_rkgk7^|bL9pZNy=&;Rup3zdYnvWxF# ziM*)ndXzS%8Gv>)CNMnv-LJ7>+YUOr*3oQMxSHp>!lh$Ui;|jLu>?zbf>zRI;h(H3 zD!N-#4a=;sOhw4kJ(cjNkn)sN=H%eC8J(%7O-DnREN>&Sti_0{q2$wTQ!CK|$4aKA zrg-#`M<}boIL^N~KAYfYISIq<}Ec*_mT@rpmEs^KpV^NZ4o~1UOEP*~tl-L7mRdH3&zO#z_`eToEGgI08sWGlA)AnoR@7AcRz@ zv1KDD1O1TRw5I3)SZi$9;=9A!yBHCIlUi|Px?%>$7NSuhy7kK`$ucM_w zX_xPvdjZ!`6djLBy+Kd8h20140wqZ5Z*8kC1t}zP(qMe}1lB?l8@jp+9JqZGfA!hN z_{_igCT>pA-CZDR*0IW=eRVhaTq}u5s4XrKG%7S34T1nVagf@AR zF>5~`G#a?R4_1%}i!h>ypa`rNq&N4Jou$aIy3n@Ho=a0s8OyV$W`h@+ksLC{OikC8 z-*3CD#Yk2jMVFL-0V@?sd)QPlF?pWpsVP>kKFh&_H&ZT`34$M9j`s>eki;=bynxbz zO?wU^9gi>k{=X$^*6?z9v-&+)5>~Qmo?NcL`H?}s^10tC2aDUS@!#AhmD`W{PTyC$>&ZswU1XzAj5f<`Ui=<$;b4vlcb4ZGOB zeH&?-zICH7k@W-$nl(fq$rXy+`H_dH&Chc5nJ4h`d0O&$qGrHCwaMzPPCojvzt8H; zn`wquzpPjJ99!Pa)G3i@_R; zv6QuqTu0$K9z8v+eCbO^ zs0AtIvQK5MPXEsBeE6q+jn!+{(`?q3N{bY&ZGx_K{p`BwHc}%%!~~5X+YMgDb6w#| zuq-aj;+1;n+O&$)1emBn8Z}6Q8YT%KiD}fD#El^9F{Q{VMwNqh9IT}sXcv-w{cY+Y zAyk+o_N9&O`mt(cA3Zsh9FDA+s5IXhBeJZ;h!h|No&~HiNY6t%E>?m~Ev_pu#%FHs zB9%&w_VzX|CF4rFnz-5E$;ZD!vr<9(E^*Z4;0HdzE%$u{&&?B}Ri4rvmv z*pFp|)|&bGdFnxvV(V&@&Q>i;iZKS$4CvdihQ2jjT$~!?;^Z{7g&7v+r-^FyrG?Rw zMcyQ_5X4yp*lJhF525+c-Zk={N}OjV8s;0-diu5LAU!@)d1nmDazr4yvQ{z{fkHbD z!l*1?LQ0Co0`+>0@$qqbdwVXaVonG_u8`y83(s=;g=g?wMHnWo!JPJT$6st=<7cNvecXozSUQ@5v*|1|15B}n(X(_d^I6H+g z2IV+-IfXTrs8#_aSyRBJLkO0D001BWNkl5mQioeft4Puq6DQPbU=|_u_8!>G<4+8c#0IaZxOjeq;j_oswf7K7F5u$iIN$ohpHf{|Bwxx?oo}#iU@I-< zRfI{%ifeWGAf=?)Y!WmBq?Fk7HM>nAkcA=x+jg^S=MLgH#n_Zay+X56XLja1Pd)J+ zX2#D!8jyswEXf17S;W_RsD7l*)PvF%pV+?ioaEDzI}22UZb3s z?Co|RDEZ>o!^9#GfFXXmAel$OpC>(*_eR4&uAaT``j(m0@5 zt221wMV|W3qckgX*r-YpG_eK*2|`G$6d)8*u-kFW?wwuE&l$_aK&Spsr>EmT9GXv0 zUXKj=Whq9st#USRU8R2wA%EGXHir<{G(kE!gyRte0a9cg4wEEh)26Mowzj#n%8cIX6L-Z zr6GhsN?2U1U~P&JOCxEPZtOIUDHJ?PIhV@9JaHT&q(TZwTh}W5QY)p?;{>`DEevTI zkkqRrjVf_-5ffI4f(A)qaXc3*6_Jr8DR`*6q&{_5UOYZtN&j?o(R|@zHL3iFQ+>~| zug|@8T}l5#p~NRrgJXd>36PGa)Y?uGN2E#C)GJ9++S)qUym>P(I4SZirCE3DtN zhr2)iDNY}Kjt!f4viJ56P+ge4rUNzw*@853<^+|+3a;-VwPoX`PDV!OF$oBp(mSw~ zHT?rr7w53n;QBdin(&P;{Ta_a{%w3$a@QvpIq?4bhy!y;pMQmGPqv|{GdEX3zhT7| zBHNI+x3wc>%HrHi_BaxyCBCZ;T-gnylWio(F#RH<2Pm zNk!18U-R`mbJq(dh6hO+4V;`yT2I-vYdyG{kAGx0U;pwEPP{ZjcTb+m;w(>o^RKZ6 z99QDz^Tbhrm4@3s@E{vD@1)+St_YE3L^iX+QmIt1_RXHVG)-u0D^qGIlcs655T*$R zP?2PQc8W?hbMH&fM>-xkzk{URAmWflut>W}a7>l+=g!cmG!Y6s-@|Anr4V=Zw>x*O zZMDBLQm%IszWY(02T@2Ezy1CtmxFRWqOi%> z&~a=6CP`>16xh3~my?6jOC_PuQf?<#D3XK`uIo`Qw{iH%$2s=wckz7)1Hn_QThB8@{_$AMe{ zZ4209CpNVtQIn*xNM}(qJ2}S8$`tD?CcC%?kmLP|-Jgv`$` z@Wx54#u(C6(ABjH-**UtR~Nx-5SwCha+*e?iR)_YQXD@f0V%PrgNO_4x&0RQ?AXlc z*+EVpeTKmopJQ=$8jZry9#H~PaG~dJZ1Cm+dznLOYt>rB*g?>L-dKaycKZB|#8j?WKP&S)N6L z#Uhm?jY5)Z7T#UL7+lX|YIKaL3+K=pVj~&Y)WPoEz5L~uo+nN$UIC`3#<+N4oZj96 z!ZhN{@uNKP)z3q0@$z}BXs}`P)~t(baP?z+^3nD+Kg zR;_A#Q=c5IHPu>!i!c*q!Jj3tR< z!bVI!m*d8JKFERFK0xQ{UV@-OWpNR$CHZ_Fu!P}Kg5QTH# z%D#gYt~x>pDwP`5Y7Hr0|JxX2$>lViogGUC=hf@a^IXC(V0?T6FmKLDdd>bMFN` z;=8b=-MzP`RsB}c;ois!;@FTx0YPZ#?CxUkt#@+6ZTCV(~5#_iACMAqB=@@go#25Zr$GP{LO|==f8bsH2CAI*{|ds6C*t(srL3b|0t*R{}f|< zYciD=G$WLNEjR4r_WM3Y-=>{l4E08BX%sU{!r|(D3D+BIv#OYhvu9YW%%hY-DA>Jc zJ+d_ql^`n$YJ_Y%u#w;1xtfdPRfZ?#`Rr#N;mp}-JPn0Xk-q)`VzQR-D=Z&g^=?ej zXasoP8+SAb<#IcvQp@Xvu(eqt<>=^n=H@P<_3K~v>g!WBZyKPzwT)(@N}5`vQgn2$ z<)-#kjLk>Hr(Ytej1$)@NG(W>gE8X9O`YynQo+X$k4I1bz=OX_*w*E&-q@l4X-><3 zWGsXR(lnx3jqwW(`wkuAwmUya_qr{lNkF4f$C`A>h+E7Xl33qONF@oQkkK>8NM(vh zA)j-(5Q_}qElHXeE81eJx5a@ps?@M*>dPa_owDe!Xz!YIIZ9d5t(LE1XHSgb6( z)!VLcol!~=1P$iqX5N%Tn#3_$LuY3vU|u)Yz;QepjV9ycL!_y}_vM>kpClBEMf&?U z5Jyqgrz@!AI63+^Zl@l?`PqQR>{;T;)HX9 zM@i!pp&SZ%&CcE3fCXc+t(CDbTLDMYxqB`3FlJ$)ff5c$9I;``COTI25v;gYmmOMb z(llXudKNH9rLiyPzo%K(%#Mx@JWrD(uP%y?;}C`sgM)*_vB}(|Z?Gak5K=C;vw8DI zq=YcE2mvNFBypXg!Lw9m7isC~$9DD6m^n*hdIVzt^GY)lh44^Br6G`Tyz;KKt({JvQBN z3zgbpW_MOtRw$)dT&z;B2RNRWH4a&_BSuPcu1{-gJ3`3UUD-lNrl+TwnwrIR-8Tsl zV+_U^Hf|iCy}e8rhOgWo)}~yXndbC~6UaiD&bC_!rpH*kcnX?zaC{fgATa%%_msDX`B zk|aTE&E(`XV`CF&t==R=j4>ohOkdwx%HJ0JKI zd*632+Ht5a&M&#H#Z|Q-kQsZV$h@`J*(+(SMQe}A(Lv@fo=0exv>CByXD?n?0cM(z zQ#|xbhgiF|ozH#lyNnFZfhWkuehT7dM9)A!HywHaFxWU*$v0bO2*7b%7HSO|K@&$i zD8L8>LSjkj=;~%|-&(ZefK((&f-xz=TF#w2M;J!gaK<-Wqe+rbDwXK(U;nzx_hpan z$jB(;;}bZJ#v}^AcLTcE#=`Jn;`%Jo&4Dmod)IpZHca@yx%n$^w|L#eNYO(d?DPM1 zDKGw6J<43AVY7j;mVLM0#?2r2F?u%i6Gbue7pJoZMOX9Yuol#NYjL^TYKs3Y=Q6xEDWw9oey~SnX|+OoHV7iT&8beJ8}H( z4q3ey$-3ma44yo~_>o6}6xa0-I=lXi()fOX#i>!wyB4inTt7#ym`6#?=-3!322c9D zv5DWyMr7m0btK8FxhB?TG2)@2A*QFNa2yA)*d!)R41Rkr?bfm|aujP5VkEwn4{qyJ zpLuF5J^ZE*u?*>IQQXq&{j!4py%uKnp;oO^@O*Ck&|Tbe=)<(H>LO^?X;zb^{o~cU zud@)EgO|%8WQs{FQYwr|3B%^)v?#eO9PJTD4bC2aku(D3S_*|cdv5FlBR~Q{42j`= zAKJ;m0~;B>xX9o9`7?a|t1ses0wV>RcHV&N`84ab6(O<=QI12SS?A=_k5d_Z8tvy% z+RM5!D~H$!8eYoK@ns)KT|i9O`I_D zL^}=x+x9ZBYahnMM9r%|GvWs!r3MM-htCj(1~2D;P1te68t`1GyumP7z?Tdjzrg8} zQ{;0Blf<-lucx!O4`VL96mf-Xi{ok%V>tHAx2R2BB;V=~gtaAs4G2jf6%UWTgwhUP zzDQfSjkcb3c)2|1MkjHl#Nwb-E?Zoz#n==JY~Q|>j*d=(;MKv6xtvQ7gq%8cnj}f^ z{nze>QcMz(?_5u_F-LRiEK-5Bs!d3_1$bA8k@aQwqd8yx$7X0S*3zsr=-IH2JAdqF z*}P*n#zL)H-UD}#Z|kC2nd6OBQ&$~h&_$1h znJLE4oj_`cBTLWP0s~unAT^mMRSHN9%r$U2TiD#?Q)@&dae`2SLZQHhO?ybK!Pv}u zw8ApNTD0SH=EY|iJp35K7(^P82t#Ny&z!P`v-4*;`TS$#wMV|SOnX;1oohF+I9I1$ zT_l%p0i~A`=mpjYf@Y1@)>gJ{+kmyN{_a8u^7%ZcPoHLLYI12q^YW8m;u@~sNxo|x zwXu@`#MY{wl6%weZ2GO{6%O#cE@I?hkNZGdLH&0@Y_Mrcy%EyCZ7cWw_}^#4_MJ;p zBLjr6S5<^$SRB8=$f;v|?eo9S#PE5f10sRL-}@E^?|Fbb9{77Wj+>3uzQzb8h=T?f zMg|cA;y9(FGtc(zyIiz2}@mf&%5?16`)!oVN zJ$rFnk2py&X|kl<6+=U(85^6#^PD%iWwDmjra0Osz-A5?{pLSgo(Q&fs?MIG_@Beb zAS~1un+)vO#z%hc*Xdf_OLbu}^LSo$`;pBteR?pd=i7`f)z= z&@ZuO!#0|=$|ZM#z7&K~XsJ0rbdtrzg{*wDny~xE0lIq&(2O$i`sypdrkp%H#uJa9 zK)Q+~P3Z2~KwD2ACW%-PBFhk^kiv3y@I~T=A=ly~Z73|S((GAYfZMzyxcKxL~qx}}NUqs~c2w?k;9jxl=B@CNcXJ}(kN>QyY^3qGkNmGOCz9C=D zW><>NwO{r-iEc}O||7Kyt7ClP)c&{ z_={Aki{x`Y7=t635JDJWH3)^4!dgg-1q;Snz+!E-mLw*CT7X!bBXBHM`AFfAPd9Mt za2snj^wZk58oyLPOUd~72uF?_M{9vr>djZN5Se$SS;N{Cpq`Vh+uPr~L^ z?oTSk?$`*L%{r@ldwAfX|Ax+0Ygn9{1-NEHL`sR9%kkvjevzlX^)Rjng;pQPs&Yn8 zpXZS;e4bDK>Zi%)@&w_l9;>U3lp1SNh6j&hQ$vD7J|{SE^9E1~!tnJQIxRq3o_=D8 z>S97ir$@b>(7$spxk8z+w)jKeYlU|RW5^W>%+Jj-di*7XlU;*SkZLK;0Y1u2V4}}N|;{xx_QmTNLya_d#>2qjI!yLUA^nL_me+MTe*wb`5CU= z9wNh0nzJWf;KIlRt?eGKwB!c7T#hKJ^ZfU}$<`Zh!poHigF4r0ryu~SG!tV()E4GZ zu0$oUdsi<`dn?p_Kvj(6Fgh{G$x|~pjvxwBdN%dZxvGaazIH}$h08;EKDB0r;ZsL4 zkA;*Vxj5UjKN%+Gload%HdsrSR%Uf)&fBD1(`T(H3oFX8vBfOND?x(MWG>n`0uwNH z`Uqnu4wG;1V9mNsY~H+=Reb}zaN;O5YV!!85dusq zM&<*1`Yk@vJCB4owf8tml7tanF2_9&{xn@Z>#0}f-s;s?jB+$Vqt59UonK@k`L zdM&Kh!mR7adt0?@2C%YhQaaPu{I14=z(Qg$)JZEVf-?)rsakO5*U>c>BZ;x?#-dVdu?E(x zAD~$5U~ysgnhOynC5cU_R~EDPdc)4iV(~mdy;7mkXy6sgFX#7M6QtviMj;bt21zgw zDe`%T8*c0eWwUtiYY`GWIQi0f7G`RAd50uQ8Q8Lqe6e&*cVH`AI@Vg0kc^x=!{q1? zj+4b|Q)8YyJP~}S(zF2IeEIKP1#8QxBdlyUmUhRnJ^k&@dS8ky# z;${1B<+k}vSSeA$VeHHa=I5tyTuIt6?73kRtz89(-VkP17PRE#kx|YLU&Qwuk|bvH z?wxe3UPBzL!WS>paF<(ln*nY~ac! zNn@6}Jd4sA?UoR(2Ud`V71FSZP3=;6#^SitkDO0Gdz~8(UoMe=1;{*2^@RoIW~Nxb zbrfZmePk-CI(?>I-DblEpie=fd<%a15Y;b^75+LM;B#@9_0=YL_a`Ovm z){DHaNZ zzI%l}tb#xWB(hnSt-FTNp;4q4pj##iP)d@U^QdcB!iu%m5JvtL+as8c#mvMQ!+j^v zZJk0PU};M|ZR_gesL1(_P{L%Qe})5l`mju$C|u}Ev3xb5_rhj*EplZsOaqDJ@b+yK z-6H8k0)aw>;>$&kXKQeO2ueZ0TX+xxa@+e#2ap`F4e|LE)n-)~sWsW8enWj)yH&sz z)6$y^BY~7Ej`~T33?)SK#OWS^R0>VRBNPBk9YVxcPE7}&2rh$O1kQ;QTp99S6jnGQ za4h|&g{Gq-Nnh6yg3zaVMLX$q8G-Mi&h7Md*-%PSSzS*enc&pXqs-@AkO9SFfLn^# zc>Q&}?(OdeEgtE9<(v_PY1G6DYbUYV6*gr4-D`BgLbs>684Qx|X>6+#!M69~kbTr*S({+TdA%q}d2qc`E zDTN={I}m=Mc)nozB}$|~)703KGULrssw&g6nHnDDboX%*<>fRswqQ68p&!5%up=sp z&@iZLY+`)yG(!WU$S5ER0*d(%+lDv(%=>6wy@67G{&HU)IJ0B6>Fqeo!KWU@Fm(z! z#jUrm<&L+kg&>Uc_-7CT5<0t{?Bd?Oll>$b4*rrQbp;OuyfMP3>xrAVa9 z80$aHw(tBWnv&?2jjpM|u1Wu+;{`c$v2#2p3y(!4VIm)uriyZ4EEhgAl#9MGS5gm@ zIUE5nO|8n-L{exPN(h9epZ(Ev9W)J7XqdXd57j?)j0Yb+IT2j8UGWu-A! z^(%~@)=_sU$HwNXKA%6vT3JS|k;TB7=@C;CaqNXy%%C!x# zAvyorrht;qp1pkpL4;{)*t$mZ@-+(~;!FIxEppjVN@18bV}t!1-2E6DoTFq6XNOCD&w!K5~5wA-BzF0uC)zTNks)vMyx!Vj}-lbx#+3h z$?z*9x$yKwF7c9MXg2CyULF2r+A#m!izJEiBom_p-1Bdr(ba(=XN(+)?7h5jLm)f=v-ZPg}RuZW=;*r^neA6;QbByKTbSvuom z<4lbWE|l~t)~~3hwXp)i^BfBaBoYjrp5WMl0d!qQ1pyUJEmYMv6Zn_kXe7zeP8}(A<;1jxX_B9vX1MnhLP8`ZEiDzSUQ-Ka zs;15$fuu!W$1sPF4PY1=DhjDs(nMp+3c|p>VyUD>UKj#!(rHFd_i}XK(^yU{wA6G; zLle;#^Uu|p`*NWm=b`oux`%b8vSM}jzmA~lU%-iu~upF{t4Ik+m$2*8n% zU_eH^V`;hm4@uK_7lwwDPLP|P=E1xFlj8?>bJJ_@pmps=(y0nc#bR71t<)v&HiQtd z&@Z3Iw2~xKarLBMD#m^MFY){rL)SIX42Dm2GMCF?n+7FMvVLevDf&qn_eT;;*A z@kno(173Dp-}zF+)2H2k+}L3BRND3@O(R-`a7Y=F-0UO|edjA2KfIGIx8KQ%O`Az) z$|<=8Lf?nWP$nmYAn*%Bp@C^9u!);a72Nr=zrPX)Auw%|k%4}ur-reUCWXADeSJL* zEtMDcC(<-#k?WNdgA!#0qSWaaAhq_Z^(|34OaDWTgIN-Cb&b|3lad6JnFnoyKH z`T6cif6v8^znplXVsJk6;78M_TrRGfGXijIJecg64BmZ1t9_`x%>1Yu$ckhpg$e?O zx;pqy-zi#FujiVZZl`tSdJ>r|q3;p;?n15fr6LIY;B= zNCYv|5f>3cX@62r|zy_q`;NDY638s)>=`35d5uq7`VeyI?zD0gKOw%HaLY{f( zUJ8XG>GC8Rid>++JTMpi$ime{el+G>$@WwJ{mZJ=?)5ds2Q6KFL~2?_q*K_DVx+H^ z`@Z~7EZ=<%n{Is_t?ipgq|5M%C6o#-LqtFzWrSZU#_r8kcn>vA5V#Qoorej%5UCqj zj>Zi)wt=ZZ$>SV5W@|9#v3GYbfdpGf^hBEF>o;LqdTax)g56u>M?(mKVYF!5kjF0Mmna$fBp%YV|htjp)!`|$Cb#P{oY>>&qX%_k0y<{|Av!Jf~M#^w4ZN$ z`u}kEXaAOtz1xA1^2$1rP6DA{IK}kGN5oFZRWKrFFf@btxoL(6PoZf789{ArmbL2^ zN_v0rRs^7%_!Dy+>lnr~b-W;=ysDPQWorp#cvTGGA{R#qL9#5%;ayL1;@~sbNfQ(p zh?$Z3=uZk)GPL>%r*|@#c&Nw!lfK#LW~t;ojt-g*iBu9(GdOwd5O;t2lYHlMpX7AM zK{A;#GTAB&(^@#gCE5{j`6mHCAG%?Y%gu25R0m2Zqz9FiDcaXJ$0?cce7%{$K=(Mu zjt=42dOUKXwt>dB_63pe)w%CQegu>vnaVIfKf_axe1o7QF)RyH13%)^r)PrBi=E=- z!c~oooFNEddvB?uI-}jyoHc%?F=KqdGPyw)CVIv|g#mqCCm8DMqOoNqYd3FY`MOPH z%WKgcf$J9IfqF{3R}gV*Q4CtcnyA&WnU81%}3vLqiM>%wSnM zDvD@XzJ_!~mY{T1BWH^|4@yar=`6#2-8}xCFQEg5X<=%LV#Hr`jD=knJI5lIhUZ7@ z9gL2iwDq5Bsnp)qT5kQeX>hx+EmEe5QW4$94l>Z!N!#jc*s%3hT2^f!namIbK0z4w zD7jir#S1|xh0txr2fCOZ8NxJ8a)p4+8f6i8jCTuD{HCpob)jaHvMjJzZyu5c~ zz4@+`WqcGK3|&X|vusT}n{Ig{ZELO}ohc^@eLUYo2H{K6gy@!q zu0l?Cb>RCUhHx;ofNQq2fNg^7a`wh0kg+LF=Gp#a2TFj9e3DL**467#QX=JIWaK4- zfK;-K1J69n{wE*7vW$2RlA(no^U-gO&4-g0JH^Y1E8b`1M`5xcy{_qS_f$cBqb8;I zr7h|tqQTHKOw&YYI;H#!{Uilm^dL>kCM*@$vF#r6 z^V5VOWXm%A!uxKdqACsEd16$@WPUo&7e4zWbMr-nkYuas+4{OWu$>f9c$F2Ai(G(V z7&yr!yB@orJ&!(slQGdWorIwk2IqqJ9vzGNUW(7+%blE0bb2N{C?meA8)71D(kv9! zLN_rj1EPT8p%HqI?WXT^2chR;J84p7S!HihCG z3PDwQf?s+6tI1T@P>i1SEi@pTVE?XOzW(KXSe7(hRZv`AvrTXaF2UU;cyM=j3BiK9 zy9al72=1ntzyu7%s^zw`1@4bdBkPg| zz9|~T6IFPJ{9~_%kDUIqL$Wli$bF{FFFSGm_Yeb$_)X1iNnk@xyR!ZAEu?(`r<4R( zS%vybyQE&)eBRP5ttG5=$e*E0btdw>Vv9Rl>YH0IV>0Z>+_m9W(+7uG(O&Aulj+eN zb_i60=`7PnM{+PX~^ zQeI(-Ac!*L6kK2Uw*=hVa}p!3_0Qp4cfV}cMp}*-bF|}rlBC-lEEfL9@#U?hK=1vk zlHs_EUblOiwOKPTjm$*0kZ$KDk@6;wNA5OBDy>Fuy3wY#te`ElSX~N1B2XGT*NW|8 zi_DdHk`<5i$k^W-6U0i__7QB*8WlFo^k=q>otO}BNgrHy36;_=@-le3QRAurq?<*#b6US#3 zKq+RVu#SZEzNfu=xO>Qb@cC#$?HnPvUMv(NvRM`n=EmK1W^GnPVo-=Xw9(%E_|h{EI=mxx8opGW^bzF)AY}j>#Y*GzDf) z(8|J6`Ys$_s<`KtyW^;DCuCDo$p_FO*9Ze2LdEnX2@4CzB}WgZl=SL;nh$yheQ9kG zT@H#ATZP*|PLnkcUjbC3)Uz)MZL6V z(dZuwO0i<6rQ2qIWqpC!RM@!wJ$&54NyzRiZaB@>Pd3=hqyXsc%2s4-kJgqQ@cjuN zKceQW`7UX*B9+FZt3;t2SL*6J4U41y553x)AjE%A=3TZBVTLL-6%}l2OSbOsR2yKr zh;8wSh;KFCOcXmL1)K(&X(Ymt938rwOpimyYey7A4}A-r4pwVG|HqXmC;dp>N%rC6 zm}}Z$B-`2hlSKv|K~blGXfd{x?y)f2-fLeWrL!9b!gt2o<(Qh^(n$nct%rZJccJae z>5wXDu4`YuFK-3fww!>I%>D(ie&v(F3&jhIv=hgJyU9B;KG490Zxxm>6aO@!-3n@S zr%e4FOErg&Kq&x;-T$C(&hIP*e-}JmWKXa|Q-*)UixFRiK$)e>*yb<(>Ccs(v;^G@ z4E)`%!d3^=kp;xp2s6fo#EP0>!(TH#IzZQF4cE_=nU_1Hw0uwsUC_fy%3uJr7UmRz z-7{U<%prWpE5nVLC;m77?n17lT4K^*1y^i)Dq&~S)gC4+#{kT>$|;%gxwh`vMZL_q z_qF*UDV7wOk7QF`ZAA#y9B?Q)5h;;T8cyZU5qmuJ$RHYIsI7z`F-wBd(;v)uDYJ<} zPu|aq$(|Wm?dEB92GdW3_GVVP{A7sz==gx~g$y1n%VN1@dz?{*WmsSVA#i=ULL+9S zhMA0$7{bE38i1q-Pfra@2pa7tlR`gV2s?_wehcawQ2%w5)6eQkf#1+<#k>18_QC!z z$miS6h&a6@MdBbDvk;PQV)X7D!3Nbe>CmTvy4vPx6bGsTEn(8|G(iG}S6p3O8 zfuw&V3iQzX-q2ITxRIG?1Cx@B{d3z6t@bQvN44fv*6Ljg;)LBDi9n@Q&vbxb0$!NX zSZcLCI3n>2^nE9$zCN^~#dNp&-+ei%98U(ziL6+j7RiD?c;;Q$DNu}?Gmy2uyYn^0 z4fxfoy;e|*Ic0b2MJp>ZCugqr#frdd*ZGDI>mqn^W&QR2W72c?4ZaeQkQkRu5g|cZ z>8vo$%NYmsdK~WF9P`yS#mt|Ie-F3A&2Nz^6_qk#LVC^Iq_g` zuVtafaoJIxWEfyaxBNt7Y9##$0a;lbV(Ij6@Rw+d*1Wui-wJRY(!7qAcoTDo0})4IQUvKW9q{@mdRD#55iOryPAt{8wKXpkd*U3~uyi>}wbTOL5jzPUq9e=*Lcv1e1h zpM{ozO@oClYe9h(L3?dMv*%nx zvZD%8N=!x$o|&m<2qix$xo&GpTI#Xo_wKB$Cz{68e~XbY0NjiTmqd5NEK`F-E>qn2 zgATwh8Umh!fAKWu`7(#2cHZUm3@(5>@yQjOU6OC^T?gCB)MO;x((dlULOEW0y+Yym zh*lvZqX)>DF|pb)mbUtml9FG$^%Q3H806$I&2U^vAtNUDe#F%I)qjnI_LJ%rXPS9J zP4GQK1;X70rP%fhselmP`{(d%7IzMry6=AB^_`|pR5C)4E#K*bhVElQ{VNT^?Fdij z*Pg_8a;^EtKBVl0*=bC|k+^sNwohvb@n*DV3)hYs$W_J04Eq?kzuo2~XCJw>%31Ov z5c)ziq!*}W6lY8>&6Zb|!Km^#_2LCH(2ycZPme_mD?yHS#uWwBz^0+MPzsf|bO}gb zg&zvkqcNrym)0f@?=o6_>&~J^nKZ4mmiZ=II75jnR;*GyXBKng(QsXGKIi6&mV0dU zp?{v0v+rO7n;71`!Fa&jRgeUdJFPqmalE^9QR7j>jrZp0(>D!^->((}({v%dp!ew4=w=ZP;q?Dswl7_5O3v$hPc}#Oe!us1Q}V zEv|ruW|YNePi}lVPR+?u7b131oeulMM#3w-tK(^_Iys_Voo_B3f3m+M`h0F+`2-ZcMdy#z$`+ zfetevgH8`zPe#7W9rU&)G)vW-=jNRK;|(3FXr}@GYD@ztV83uep2q`NIoJYIXB=&l zS&mx#Qf7FrFZ63#K`6|U;K$AxozPmSU-y$<@ck5-07;mLlMCl zZQ(j2PbnBLakbObBgbyGZ8*52cm~`+?Mg*=ir9(=m=w=DN1FPNn++@+KjsL(V*_2r zyLf!E946dH)eQ_!lJ5N>rRaT3Pv(O2#Jp^Cck7tTS=G{EX+Q!p|JyI~jd{eqveH>( z6zXjm1?=HWOe4e;6>OxY9T(8eo$JOoKBdy6$}DQm+VA4>$KM%_2}jewThFj}Lf*pD z0={VK>gtQ7WQk4}0_pWI@>$1=e|EoScnh3cp4=ct<%U}=XP6KE26GqD+GgD6euams zSA0@ZbUArIFD=6ICYR4zwzSk2#e+d;z|-u)2JHDxu=S19$R=MVl6&)^Zl)peRA7r~qepG?!*{jD|COelzGX6O z)5EbCWG$em=u-WdyPq_UU>|TiZFo|@FOi=tQz}EHu5$Tu>@NIBhURL{@J}BDH82iiWYv!e#PBJ0w+6h&5JVz561~MfP~-j; zv0X}4$;eXT(+$J?fJyCYyCV7_tdx!K_Z?eSyq%6c7L{Nq>0er<*c_BJ1t#QOCiS z78M;5gg)ZOo%=Anq3ggMDr6bcEA!>-E^~M0rXJUoRky=UDCFA!-9ggFp)KSr9m#}g za!PbdOTLFhQ+t}}MbkK}97@Zqu!3XLaiZ~Y%)gU6^ei3-a@1*@t*WZ6A-&h%BD#dm z1I_^~H_S;0ELW^DINRQPj~GR5JMmFu@U@Aa>F@c>6c4?~_#BVm_WM|F;KlZAp^N7~ zC(bUd*1_NG8V~-XNed;_OTGg`s2x;iZ#R&+m03wX8;-A9KmI-adpX?imTaj^OAJ*1 z8y9=s%Plmaeh-mm&jtXP3cP%(48(d@tia;;9jB*u<$NnBP=g~nwmiw0L>Qrcch0S@ zLs`2$J!Yu(XmVw>`Nce9DclpA9oBSu^>Xx{Ao3gR_{HIXNw zF^#aVHr$0FQrKbBM`oXks#p-nT|1MeS8u4YeyY%qGfY_NGyob;g`XLUm?q5ay_4S7 zl|nw03@H{*AzRF2hsd3*a&PrGSe~VFd^nfht>mGUpP1A|(f@)b7I@y@UelVS@SadV z>;>H6e|X>la!;`ueHx45Tc=H`+RN)mKy>alG?UH|E7w-zV_MD2SlQbrP#kHLf){mi zG9@zFjY%e7$)i9;l=He1c!kH^k!6i|B+)-4IY@&K5Ij-@(SN?8N;Z4Bc@9X|Jg%j^ z@mp0(B)ggUMa7Q12p|?bYhDq{B{dI^0+Edz`jar-=U5NyX)y>WqdCie)-Z7kas3-%Kx zcJc7>0T~&s>TiMDJkuR_fpvay4sXvE^b?j3l4$z9@WOBRoWFlVt7sXZ5N@ozKTbag zQH2`YPno^C={hQ855S|ba%=V~iIg%$Qml(e8jb2rne0BfuV2J@fa>HXwF1pvQ$ zhB{M!-wf%VTWa9id!#E|fu!qw%w58vx@UQ=cJ88HqzxY z7&AHDyKS~#m7~9qmC{L}QB9N>U-V~c33+G3z@*k+B@>DG1Bu=)9!*IvDi)K%h;+rT zkcb+|dtv~r0jZLwg>;=XaApQyJDyhWQ70wUr+sR)S+!U3E99)c@p(w7ZM4Cjqzt?1 z8u~R1(51>DqoHwda15-j5}BBoNT^CkO2QtxW-Tu-?+cvu)%nK}qX{TU#txrU%iHK; zk-lXI@E=EfK)$`Xczk~ihw+cFL+V5qy4V!nJw1O#Z^9~aC0d%ed{u5bzl1+L69_S( zz38HdG|l3Nyy{keb3<_ffkx>fG#AD=4ODg#>wE*D`Sbc4jm(XSKrw`?`D6$JT(!FL zUR(nk7j)_zO*_uioCPvezy2VJF!Zwyw%7_xm+X%kgjj7+ffmQ8S`vST#C@kDj|uvd z8l-Ww^hOhnk+M$}MPbV?s-sEW{y`B$H#NmJvmyy z%IKt$2_HAQZ_~WpG(mu|RBqfr$01*1G%*_Bg6Gp(bjZh!oLDez=g^(33wcpW8bbyS z?qVcS*`!SNAhq1*o+sFLR8m^?Oo<8v&|JpEv%s|m+O+NYa0`m{F{7oy_o?MOZ|`=R z3o%i4`UILEOGL|E&?RD+W7zH#p;r9g5aB^`Kk(rwE-oqCRHB&jev<=>^+`qS2cGO) zCbn9T-Qiyavi?_4d+I>G-6;;dYJsf{`i!Kj@Q?-Je0|? zq0Xgi$Dy1=YW$*ZCUOJa8(4e9th6sCOUskOHyN9oMsY;E{XnkCDg|0uup7|`V3HJf zc6PS!3W#it3L3t8a6)s6(>hZ3ymY)rxBNSv(zzeJ?;dxApf5QGXC5YScW)DSzIHWa zv~-#y>Kgjc{`ceF9#iClmTx6wFY7!|WiY*b+?tS=qnm+MBvZ;^*)qBPehGVb>Ko;n z8JR%PmL$F5lah)rA@O%I+;+V=%J6B|gI!Qy>S2(|We7@|-}}$`x*Z^>U`61BSV^`vNJVM5JrYF$}TQC(&%3;kREvm7Fa6{b6!B-327k<1^Jtj_XCKHL(D6y|b= z?~!w@1;>1WDJQ-c*&+TSekL5rCcy%)GlC(x(Wx>f)=7_KPk(EHYE^P5Jxawn`b+*d zPus|Mr-lOKo>F6MY&}=>?)r7vLveQ>{)EZr=nAF?80KTd<>UNm_in5FL zUTk1@32qDg?jqzUNLJEC_!y~?R2$sE+vdP8_U)iF)EIsIWHo=&2+{ zQ_G}m!chUf#A=k43@PXV5`C-Z4O`HU&SBP2>_rXVdM-lc+E2ET2$((=rk7O~=0YnA z4n~Umuqi^XG;GUMeOQ@~q8(W?6lSC8>Es!E4tD_pi&Z0AOtvb8T=c^!*ihQ!*riMd zxFd9Ub_V`H~0OY^~7Dm8r1bAFcHL)=%Y2k4x+G_!cnXty-KBsMG+%C zcf5CkuLu>cFX-O6(q>+QSru4bO%kfr{QOHCy$`(zsi`^)=^BBoJdO`kuq=b@z;sni zioCrRD-=Ub)>oPAikC(T?l29$0XZ;JCTUztMPE~1(vL&)_w76<6ScmM__(YQA8Hic z87E<=cCp{BGC+Rfq_=`N6ONz?3>{)g%hC|-Si#9<;PHHb0LYhQa@!ik+xHv+bcE zrw0b^2Y!kpB_-V2JohE0MeIIS)k)jJ6@Ezkv>@mN0plMLlvWR$db4lj^gLbb`E1*; zeDkP0Mz<&LR|qoGsfIXH_BYZg3=@7a$}0ZFc@4s{?bw$mT5iaZSRQ2EnkWBRajfy1 zQ|;Fmw!FLnG%?P}{6Y#?ISg6+GPKVp;YsXgVugAa`kWsh+vug9;Z?p9Sve<}nNBcP zlvfnBH)JkT15_Pw@|ad3KWZ2{oiMXonp$pGx;BLzy>DG_z2I%Q*GC`eutQg0ma4-sq4bfM_$B!$*_a4D z>~rnzeG@H6)}`A2QmB2r1|{Xa{=UDig*@U>zLq^!L40sS3oyo;5bsC@`oiDAka}k? ztYdH;Agh<;;D4{GpePN(9!d(2Ak&WWpms9h4z&*|7(tDlgYDMtC1hmf<%;k(77nvA z{%$osURiEqRoq^X`V_WndVDli%BVsBlPix6TleRYE334?3Qmbav0NZ38LJo*KLi~n zvA>z9kdDGpA6Z|#mFeaWeD6tv>N__-ZSZ=uzDXkEM5EmGQwnohYET4g&_A$VbVI%9 zqo}#L`C^9$i;0QT3LJn;AOB8SHuud z0x685w!SI7y^ck0)P=ijt`5~U0JJtYDI(~a)dh!`;{JH3%A@h6`rR~_1t75QPFF?_ zL1n6PMk`!KcoZS|4GoEk*<7<=@W|*Wf|^{?7}ezDB(9zfC+xAf?sIPhm{3@NH(o+%!EGQ%1nD9+Qd}6e|_L*xYu66#_ksD z`N(@`7m$GUe4j+mftYG)t>Fx=d!ZnG0tJY1+f`bpCDl}$R|mk%B&d#V%G`L!T<3@<~S@C4@X8 z0-+24%7x?R-ra;id4ozF$Yv#0X8d-HD;2iiMfp<2q>vztT_=*Z?M6Sob3vg%JPrlh z;^Vq}4tUye!8s#uA1@27ju1d1^sF#);AX!On$>tWB^b2#1bHTB9M-RaB`)$*=tkJa z8TG&w3vM*vFeDQ2Oz;(oH`4*y!t-aIxPdSf!t&Z$^ZluuhHZS9YMrJqTP~kIuiS_A zE*boEGa5C5*y9$*iy0?tYX&RZZ&1Cn7P-aGgCuHJbbHjghVR}B_FncV6P1d(tfxG& z)_8pUBrh~#!R^DlJA1STJgz^JpYw*l{3%vLrJk0=;*!_Z1vE@0B!qJJw|Gz$!PNy7 z6(2EC<5!H0$>Io-@33Lo9P+xzI*3F=~ z&WMVM5$gSv02GUz&yH@p7EsERb z^a{`kRWN%HYPj{3kB@#^1PF6-4i<=qXJlpd>mfLJb@e*>|7ytg9`u67E-q8`r z6Vzxtgv7wWTs??ShzD3hTEdkhEZ25r9*%iEJt>)F^VvA7s* z+Wxuhmzl(G>AqurJLY&-pf@b6258pPPH;*&gHCfHf#KQ=N#i^ZxzjJdc5==G(0ZQt z--GOj(W+`cSsw#m=L!?y#-{*>)XjW#xKILMAhDtzu4DU zSPC(BAP7h{K$-SGx)b4{srN?{JL~xBx+91ExHK%47?WcZw%~T*+rB`i~!r=L6LV^3a~gH z!Cg>>4#LdWP3b^yPV~xwc|@seXKhH#^TEa{qf5-aeC!?)rE2zq{WUd=y^N$Liegpn zzn@;YBMl}a2S-Q!*PTsU8YS3OS~Z1TT{#jIeaXr~V?bvmN8h-_#HdyMXXE>vMB-pvR`U4Y1QBVDUjgNmPNUtk)JB0%vc1m@F zK((aa`>6&C;f8Wgp)7PLMG)JUPvkJ6dbzWG%-(UNg>~C_$=d^XX>nG(7V6qzA4)3x zM9GAHJB+?B%P=$P?N~<(z-#}-pZ#h{&(UHTB*l^>g9u*rWNgO>C=VvfZ*5JFj6}3u z5$aGkY_Y}9=60Y-{crgDe1AT24G}d3}kk`K^E&+hjfQn+f8| z7(H29*2`m-y3ZDRC~yj|>2=ef)F@w!B5=T*>kj?rtHuIDuA%lpf=^gC_nBZa0W zfm(V>R1+DeS|+v&2-ds|xD@`TD0G9H$9E4SR#w{Z)jpyb{fz&*u6E?@X3#Rw#(w>; z^xKgkvm<~Rt`#?WBc}e7x76_nV3XHBfi(%d2T*r%7sipwV}yl9V92 zq`!)>tTTQ=$s?$<5U+$)UqlHaSeLo6rb3Yw^tt%?d)li_VPb;&9@2*&)+!g+EZ48= z%Gmz-S`3oeDM;YZ+bXTuPTSihNPJAp%*u+7mhPSxNrgsKEFpKU8xJa}kb;8)ON9m! z;3Q*Z9UX2Y=K&v-;R#hpyRb)np0GZf&yq>sHcOs|s~&n<<~3#5a6|YWMq!OyHVn`uPPk;g{lR?DcheGXn zfaZf-DEa`@N&_n=bYTCC2w1{{Jn4OviK3~s&(H}p5+a39HE!cDUYef6s!U(-q#g-T6CrdFCXMyRc zyWN*r;ebeuJ+U%6WO3x}ws(nkD^*qXX~nt#M((b+nxCe&<#~N)s7fR&(}{ur_s=(> zqCqs_2Nk)|%0mJ0rVi>1 zN`p%&#pC;51URq;kncS><{$%$y;kO>v?_^%Gj2&%7DlZ_L)$+?2vpQMS{=rFe0~_% zljg=#eL8CVv>OxMxyUqTZ9^#;oPwm-dxXOMgEr5uh#<^pRbYm7>PSSZ&cJU$NkK`; z`D$mZiQ++l#zhG>HYKT{felcV#3vvz+C>qTlpHwXnbd*ERj@{XU|(Vy3z@7fg3rXu z8f5Lg-Yn(rMw#S%G^*}8Gk=>-C)scbcn&98zlDM2rbtyZJ|O7UmS|;KzutrZ2iLpb zQF;p$y7xyhO9^%{_0NZ{z&3QvI}-yIX|`(!(`=!F4tW-tnGDPi zWSQ!w0v~Tkm5m_>WBT}MZC=1?duvsDWk)5vAT;x%ga#+g(fUcX7$kY0e_!zY@s7Lg z?zHt=R@;cwfD|z7oWWqwke~2Fi0gm-Tk*feHuf#q@IshVB`@CR~%i= z5nUBKq_PkZM^p3C&(fXCl6k73lXK$g#_zL5S$SHTCD-=@r1CJ)cG|eYpHY|+pCtXO zHb(=vb=}`yZ`%1Ghcx66wsT!}=ltmdMf`D~2GD{ZMpc#y6a8@M`3q z&3*9b?x2Hm`w?t3Ejc&U^Bx(A1UrP`M|y75dwOkzZ_?ZR4(;B0An!d7N2D+O0*7-} zeQ580AsgZTPYwci>%K)JVgm|U>EN{#2!v~w;HH-+$s`uxRvhOv{ z8@(TqLi_DmUJ;O92e59?lrQ>i@-~udCstpGYyG#YUM@-BNVp+Sd^Yl5w&)|uTm_g) z?F~(y?!HD*8tx6n`~AY&H>4~*a^;&mS7OD060QHLsj7)RDWV!Qh*DteMljEab96kx zG_Bm!$T~3of(`hHb*za!kfFeegH=qvs{~@^+bq^Hve>zHLb}_xUi~NT_K)0Cf?Xf6 z$Q`krtGFoI$`1A_@ty|Jv{*{_oM=zA{}cqp8=NBXFv^U z_Uo;8&O|y3009T5U`irjEAG!!pcQ1n-8v&qyIuI^%@&Y{{0yd@W_3M4Pw#QdbHNRJ z4D7k0zZICT! z*l)Qe_S_ixqoih0Xo;9=5MCkOL!4q&cb|2agqc2}jLTsijWURs?FV=}`V;s9B`^mMz;ac5rkYAD=H zf*fWfj@ZQ6Iq4fy%QorR>+`)65j=>~s&xgh@(lv2>hCq5)0y?R`hwws+b4=xBTj56 zImQlXvF?ObByn#HM1tNa8Th~SgkB9_{g5@uGY>aug|8U}fPp3{uEFz1xrtA{haLg^ zJQbht2(o>*{3ydw0U*b+RoAjTP^4bf;YT^#`>jPl@P7CHbBjLY(Dje&A?Bw~pAg9F znWA0!UL{&b*Hk!TFC(#Hlz(l&3qLmLz1?MZ-tG!-=o-EVxZFEz$wXvADS~w}^S18a z;@@+2mvp-CrTw!slv#35(HDv2i}xu*B}RKQzM$8h-jidKL7BJ}AV68MB4~e+) zk;SY8Zkrb0F1^d{KSpE1?Yd6cQdjg3`GlS4Tcp*B>b+`z_JhpF*Knx^6JT zj84iaYU}C%ecbp`t8ZzNOrN(O_o3s9Iy}yuulE#>ZoP}Oo9wc4U86h|%Cu@=2nPk@ zjT=T2i$rZKjjuT0H}c+Wn7eLq3_N#z@cGb*sP4Y2e#}3nnfBW;xrvXtl78QWn5W987Q|QuN z&5Z;kqzkRv70VKdi> z;>_Sx#pFT~Jk^3|xxq&=At?_+qh$30kCS8~7zp%1JQM&`sd32z2PSR-Gw{Yw4_pFj zXy89~T_)53%T?DgHyV5|ZD|n!G?>=bd#%0mg9{kT+arw-RIoLuEi0;!qPtbP1=WA| zZ1G)9pjp4AT30bD_z8O&CM7o0k5d2p)B?Z0C)~Kra-z{oI4rI`U&F-A?EL=b7nI15 z4Xm8YwBX3x9Hv9hE7w139DQ&wJjjXebM${WwRhLlTq+|1xg&8nAam|`b1?9Bb1TWioWELAfFberzK%1K7%Cnr2n@dyT!@Y$C}Y?MBD!^6#P(-XvgW`L>! z6|Vj4dAs+|RDKyOYt=t)XHI7^G`F%sM1az3(<{|uKDVA~HViR$JR3UST#bed#J`+a zcy2SkN7lUv_Y8D6_{N$KlyX~a;WMB6o<`G!ja_n(B59Qp5@Z>{8MepK%=e}!ehM+z z+~V!`Y}<7)pb&w1gQ?s%c@A7DI_G_;9dug$(-Lc=>$?0UE0!-dmLb+UGxmF|RE=0o zXVjXlk&o6!I@E#K&r0J##SoRCS$zyo+6S2xr?#*in`pQ5+tBuBsgXOasvLW$tFlB& zFB372K^;T6U#Kz4Lehp#mMdSy5HKSL<9r0Cx5rZ%RU0?GNB{{SnjEF;VL}JiuU|v= zz3{>UkJ|^0+2V-#r2+zkwQKbXOmwpSF&Jo-b7PECQygDjCWwUJ**`=_%dhqX#ILU# z03W?uY&p{UykBPdZ$b6L+RuW^L}GBc+~W78t3KLZH(P77F=^pPte2C3Ynws$*-2(5 zCdchzl;u+Z4+Ne8$P&sqF}5Wb zYy#7}H7o%bb)c=2@E$Ztbro!4MRipTVny%_C$Xd^dSzH;d3k$K`x0NGnmvgog7NsC z2_pu=Cb?kbzE|LV>jG&+g56I#EW_VdF3&>J_Uo%{DUwZ;g+XbkzIFdM>ue*@_EAH-}rT7O8c;-xj7||vKY_1$( z_B68^D4Hlx5fm_DFg+&pzFcc*7sY6~gV!%VuIueg__?}GjQEL9ln6(x;g3PYJ}yHY z(fhD$G7-JFqI3+EDl^7T8j>)n$CIBpLral9DS~?%{*XghaV{5ZhrE1h&%U;2$GzWx z^0W*!$Q?TRniqE>n|#(V*@T6s0hnTVPh)Urhsw{-uk-Ec`0wAp=(mk?i}>Fa6X z>E7NCKy(_JnZfFVf#t@cRkF1~3lBwt+_yRJBHf>@nfY~z0lnjxX%dhb1onspO*hZJ z@WsJGArpt83D_EjmxoNhq&@7)9v!1v2t8r=Z)(3udxKVzS?Q@;9Uyz_T|TaS#K;4^ z*PV1r-k%X*wv%b`^907w0*-JW7I1~8&tIzohJn~j7|o#KAkc<9!nM|Q$5!H7wucoS z9(W^9vIZ@dW|xae#%Gpi0oZ=xX9djxq=-g*4+c zmeUPhVxp0u`=B8=HOvD6PHGvh7ER@W@o*7QCYcv_M#XoO>22NGY3|w@S1nuR4L8OL zQoM6{b_xNrzrjTak;lD(>LWa!4@RL|veh*;s&!yhO&oF$Y{0825iM7O@2mIV#bf?=;#=Ed3jBn7B_bH zk6$s#Brs!3OO4(OcN<^s@t?C^{|GB0eM0!J(84abs_41Be(A4!F=?sPNl#C21J3_g zDN4*`L~?X7fa5T_o2%#N#gfs$L;6~v?0i>pzNsB>Cf#;5oNfAv9=Tw{2Q_Vf#{TPv z$Qx=kA;79>?88bGg>Is56je5uAzL=&Lx|XXC5D(}ga~e77LC!nJO&bGBm+zo)LeQu zt&Y2&xkg`2+_7O->z;vRq6hbPXZxCex8L8IBm(5fOQXO7h)N@UF*ZcP9{zPJ6lrN` z6tNO~Ii)>4dC|BmjrH{+?3pnkbpVF@-wwqK6aD>t7l?rWLA?L<#7{Se`i9SYli9%0 zR7i)kWcwNH*5-5147hEYUKRAZbDN(-`kF*%dt>g|S+7`^B4;!>#Rr;*RWyr_=gOOT zpSEEAAHnY^MFYVr%xG1WK>skSBEsoiX%)Y=dz*tUZ~nL#|3-z|eUJ50v_n_>_WxXf zp4;Tk%UzZPr4iO`HUS_wRu%JqVrD~xvPlZ%qM7KaN{HC)i8%J8oc}#Y0#!b ztsQxwnkRSE5ik(6Li^4An|P^dx%JZFF?nkccCUh?UhUfsAzCx^LGogrlMbOc*pWP+L<#rvFI~0A&d+ytTwGkN$iWg6q9tmZXKNiY^pQix zJaOX8>D!c~k1{C&z{E;gbnsf>>hUEGW5c{@4BUP9b#HG^h9+4x|4IODrNyH&eU0u5 zlZt_!BwMdjejSgJH|{hRLQ;_~xXuKRCb{nA!$7$5#Z^_E^I5;UVRA_c^}6@*7a$js z#7hUQDU}76S`qQNS}JcYd5AeUMf6>Y>%9$U*B|2KTlM9Ba>vKK*iD0<-)|TjEm9vJ zCwX-h-B|3vPhS>M#4s*69A&IY>nuRry^D=e2CIWh(;pt!`qu+`Z{7{JEL^$_8468&pI$NHH9v!e=M1{$aLUZVBP=O zE^E@fIc#e*k&>SQiQ9g4V-4`RlZqHX^98#VwM-jSGkw|OC8H|7=52gDjf54c?IG11 zwsd3STq|f?AQ$Euw0P@)nEJX}bnI>oMFXo?WJOSFCB=sicEiWV_X3Kf1P!i{!%w7} zx`t0sYEKB975Gmi%D%lVo=_blDPP`+k>otYF*9~5`r_KTW}woJ(mK}~c5(DK{U`Ru zI~a3x4AnqC!`5QS1JpeC#v&-lY8hCd``!7A*)LvfAU$9^5wh?4-@ZjGb7YnNmol)a zoy`ROUwD#}lL1oJ#^#P}MC~!qAaKrb**XI$!+!NopxsI_$O>CBGAQEwIis|#>b$q8 zI$ds_;VU*&Z2wH(TJZMEivX|~B49VBDse-@K+@-lJSrRrNjWgN`{RB}kY;9-Wv-#- zu8%v>$OANU!NL5{nh|C?+0;|_xzzwKdVevykX>X>Bw|{Y+|T#vCk#4nf&@A!#F(NO z(UR@}9Ut93S0@1`)}GXn%2Q_ZK}K4J2w!2Q1@X=Hqath?(ct)+nZ>``-F9@Q5As># za&64~@{^AI3K!b0wFHbA&s}-*KIH;B zGh=&uW;qGqv~eOL8tG$2MkNt=SiHwU@)(FZ@hJfv!R#6SME6vSnx@<#O7bU@bZnme zh!uetQIKyVHcPFB(|3;?gy@^P!Gkr7iCfRT9T7XY9z&@Z^v(EVRpt1L^O~-KmA9s% z(Z>uh&Un+;IyD=QPh|mfUO+&UG7wxCAuhHQ^RSK|?i(&0b7^U7w*l`6J*NA6nq(P> z{1=@pO>*Z&|0jO$M(sKSGbbkuMUW#=`0T7IDhcxZ;iP$Tm3E!;{h9vHa%W72`2ni` zx_hu7PP`gD%MV?yOP+`B{)-DGoR|K7D2MJxnp1Z|$UTrhd>;ntY~>~T^#!;c)+G=E zjqp@;zP_|!MZ#=vZ}S_FLivr2k0Tu)A5$gEMh#I689NznwXJ!1F0g)8&8uziaWxq; z;B+%Whd~>-X4?)c%sBxYw=*a}Xi*2b9JIK4UM5)_t|wG+_=s0LnCt$;-S|q=Wd~9t zuzsFV{V<|kuIp1%e4K0i*7?J7Dd~P3NZgxXGo=>95Qdhr~XuX z%+AKv_jtYE8ficn3kR;%k4}nqoMUMw0JvBGZCnk8RLdoU#XSr$fQAM1i1MZe2DxR; zj-+cprJ%i^q6U)%S86W>;wL=L95=CqB`aGaOqN~_44);t_y5J)ci6kRr7~*Qs?=y9 z;zA7@H&`O(Z}Y^&#@fkOis^yPjEn;R{nPY?&?b+u3J56BG;y#kqp87pvyh93E&^3) z&s#oK4<-oFrkbyBKNblOQ^Qx1pSRq~O8p==UKCxqKLc8n$?2o6s-;h@gT8?q@x+~- zX5U95AV8p7(CdvJdDaHT^W@f#%fKeQni^=dio3<9a(cMKaXDn#zAuL)L7=}t!Ag9Z zilB-es#Jy*`HfUjqVk{he1D?a90-IJdj<@TFQ@p=RqDV+)rP9r-rnBzcG>P7v34ti z+m|Cq`lM!eXLiM;zDL7JG>?kGg$d4WfBDZlv8=QKmJ#XOMR@BHSf1YNe$68@Gc%Hb z061E|F%1KGByKzZ*qf@{CxstayXU)8J3{I6wT=;BCr%5$ztkqfD)--0ce@tVMxmFd zQDS}3o^v*P$cI*G^7z+yvg_Wbg9NuF34EuG#`YW@3`{2`)cXJ~6 z-tTO^YobEEtZ-lzv;p{wSHP-clONd?wCcyVMDG~&UmyrUvfQB+7pEM~M9vuFTpvN> z>b}({@PVaO`-#|shp@ZgPuc;%v{0~ z?=F(AyzP~?qn#OO>c+;#hzUNgw6r;G!&OyPsfLa$ zxhkBx^7%fmK+FMNVcF2F;NU=vSmk*4RdoJMLp;xx+xBRlSFV+rDJLzSLPWY9y^}4P zc&Ld*u5Y1^uC9rRI)t&a|sxYV|MNTR4t7DBw^ne~Mnu{Umh3RTi=m9tRS!eWi&7O~b(432%a zhjc3z&60ig`P0i}>ygXn7PURn8jv3jymghTe}&fLKmi4GZFT;)RlpOVM-Msb!`14|$E9eCHi7dS+nKkIe4qfB2H?i?&O064z#g@sN z8_rO%&5Gf`P~5Qi4H0qfFd*D=t=J3YO0>(jpmGNr^pFO`V~Cb{#YurRz-2KoH8BbH z6sTYSZAYjG@_c*T2LkW1tROQJpCJ{gk^s*sHa^}3fd7~cyA^oE&z^;VWbq$DW!xb1 z`@0!X^<-Ds0HnYZNJFagZ-pApU?Z_IEQNvtU6KXIzbqQ_dBOqy*>tu5MFeyCsax65 zkd&q8g*|TD@xO|M!gZrJP+vO60a(@&wLVV85cFTXE;J0EJpQk?uWZVz3AV+8ySsbv z;O@cQ0|a-65Zv7*Sa5d;?iSpFLkJ!qSnvlAaOORy?oYTBU#Nm&KYM1nSFg2tS}>Qf zA7I9J+}zFQ;?y!p_m0W&?c!}GzxKOA)WmhY)e#>ZCS@14@7)RVyj1TU!ts{A8So1V zPJpvX@f6o)P+NgaS!!Wn0cW)0{d}{cZ)7ygt!%hLm#gwlKwx~X_&rcOfUv!AqDuam>q8GgB_*Yc+(mMgYKvM@L5= z`sN+DUjCN(FR^%@MdL8=yX_&y%Z7pt3vMFQ)&{O|PcqJ45L9Y)P_(rHpvb8#cuB6<`u_`j`-4`%Gt%yC- zH#D>e2oQao{bAMA)fz}XIt3vUtfkios{B5d99iGf+A3SU6oXv|#`EFI6s|gO0bU3w z8^t(lAoc%uIvVui>{^IhzT(Q5f|#K@SP~xoCWtXg*ykAFRZzosZ6uVzlUv%_Mz5~w z%f5f#_8dSb+Pi;sp@tK;cjgrg0vHs0qXKndEP*gm-J8M4_k@JMY%|C;YIPb;O%^=B z-&aL|$NipH1Oe|&Fc*E?0-Fl%`ue)rfc5t!f38X-SbSr@%VEaV3;+E8D8I9^n4-V- z$m}IL`sR+7SlU4+lCBm}y~+#Hqpn30vit_5$J07GI_;m&$^Q$?OLvQvn(9Iz>vVBv zD{E=fWlVB%HD2Fo*RSXs8z;#6A;80X(%Kz)5Hp4P^XIiSeFNsb&*z)N*WF~NG&AnW z7^zqpPT6IXnpaaPSW8zOkFRymV=cK#w&_J2&d=enl<4F-MXjwHFXgz%a*2 z-9s25_w@$|Jb$JMR9RJ3Kw-aHr!gk&0Z!i`r4IxD5VG{?07@_DA$mrfdNOvpAD}yT zU&4PaOI9dtwO#N#qI#1Zsd|KYALwz^=$xm|HJ`8ZX*i?26lyX(PL=%6#bvI}#D}cR z%&00NP~9Zov*0$0CfwZI$O*$^JWF~Q9sIXAg$@~9di)3cD$TP0Y%^q8*;LHCOqgfa zbPKFwWqiRQYCW}aJGk1PoZGS{lr8x#2H=l~NN{;Sh{P=hjvripvPOkziq)OSrX|<5 z*G)no9wo`@Uvj$~0B$<+5ZrSU0Qm|)mbstb&VAckW4#WeE#_+DJRaC~9$#FU{msH@vZvktOc8;)>%lKz#r1vzF+W zoM*_kfQy+WqWVeQY<}+qk2kV_CGR~iJ`vn^%j)RpaQtnctu5envCYfr)UJg?nK60F z9Dq<1({TPoiyciS8x2I?FAAkL3^T=?L$0KNp#u;^jwvfU+uYCZUtwy?zC-TJhSo56 zyMf^Cx;4*a;u2d6vwu^d5b}%J>}7b01x&0@9H?5_8`|9lwK_E6y(gV(+;qJM z+k)g~P0VlM5~@6gt&K#z=F3btYIEIby;>v7`tOr_GIsZ`AN*0~r2Y%eMrx*#fKdc& zOODWIrbgCgvDTZkUa53H2ZaR z#U*Zm1B_FX08q6lj{zp)_(rhz{Jo+S#T~iBS^|U;R5e2PU?(gtYH^q407;=aUy@+$ z8*}qgIg=OKllq4Kf8d81ywcwa-V$QC&TAcKr19$=XiDlw- zKYQFsuFVV>pn)FWJD|b&9ZFI7m{IC~$h0&)gRX-1MDT%s9J>`p4FJ5mTL&o}tts|A zFc|Oc?IqkQ0wDAs&B5mRHY);f>nxKo3S)_nw8CB&P+J(D6M#~pZ;je)?S;qGPHcTc zJu}>DPykP_)EaB#{In6^oPm+^g$eS7N~KejKTokN)5^gw@P$k@_6zm`Hw}9VFgSvj zAmI7;XWdNE7p|(YswxHEY*0B{=>@D8@KdQ`d3!@-k{jmc@TBEDbC zvj``@Rg@BuVZ6T*X4e=Nt6VoQ29v4LJ?AM+EFg$q$#Zf5xZ`&Pu0=gV!=XEWkd2E1 zu8I|WXRg~p%xQHju3{cGyNv$eEoFb;VqkI-?PzBg1M(o91Pu*M*9&R7CA5Ul+va%m zOHzbM*W&4Pl5}TP?F}cc`TfvQ%?ObJez_ccr5vGwi@lfhT@hATSXi)aKvE(kA=!l- zEvVua`xu+BQW(^&*ztg4=t9acpa1v2%C?3IF%Ifo7REdSleVDD1de z;`$`O*Zw&;D7N;l=UjKOlT-gPW{4d!aRO>(%%Mgv;ofR=`o*E}`i4&o=?C8J31pdh zZ}zbg|1Wzfr6h;;*wrLy{SX?^BOiQz#*MfWD}Lc-RN(j^#=#*@o&2+D7)JWL4pV5U znkk)x`j^pwAm`H7&bXy!`4X6wq2Pv7|JDD~(g+C&qZHZ`KM1$9w~vAU4(fO1g`$S_ zQ7n3lLZ=erda#|Pi|4k%gd=qd{?zfmRYe#rJfu8*G$MCl%mNssNM)ttQb7k6B>A$7+s8mm#9Nhc zd&2L7agJ+%!!!23SrT!CsVc?O$-q}P-{vhSpH0NtdML~vx41RbY2(R49KQ5D(*8a) za_0|1st03sUf#&3jWOg`SG@Gn(o)6<$nCAufAK8V-gn?f6{SMQYlCA$>&?X zRLf4l#sR8rpL(!DzHF@vpZ;@8A0IP!)@j!2bOb#6>=Dg6wH+NDty00EXXWOWF4+|_ zA~)zcjOu;!aAa9o^{J{9dn5)ZW{baf=*Ig89Lp$Gnu6>7dgN{ADXh7pBidwR5i*Uq zswxJM+&444JRNP54ZNwi6j)Gu+6Z9=e33-Le_!6`X%kUay@b^s0W*oLd{mij^O_@| zg4^w7+KlVp8@~sax0!yRK6sMI9*O+c{fkHC@73bpXZdb~)l8G__afW!Np+D$1D8#K zZXt3NYJ8}nz#wA#71W`A*;Kz$54lOSbnxf6T^tTiD9mll?l4|@K+mmKwYHy_OVCDb zZ6gV&T3OiHcfHBe6fCpF{u5CD%k!UD+BGVrW#us;;}lEf9)1`{4vMnG9!FQu?=lz7 zBIxNwz||X(5exzz7I*$!TWE$W@eYWf!xC^A*9SA_ot>t93X==XAF?$ah~whG2mbve z>hA77$M4plx2_w`kTID&a{yWolgY#xt+a8&0pTvfOCPa>^S|PYP*~%1%3E+>I(37Tbix{vEkEJg65F~iB z_dz00MmYvD`)w{9)M7Q>ayq^m-UZ#>W90a)u(8V{EvQ3V9nHVeUwZj{lzqK(LC6ieOBf+wo$hNcGnb z`Z-x6u7+VyM&U1WcgrJnOLku-_bZCJbQKw-7r24NVuY{nVcPMJbs?-HfA~QsTe>B1 zPNZ8lG&c`zbbT&nDgsYQhH~#P3aTv3WFD!N;f_hoYcjDVY^{%6;BGt~G~9f#``WGT z&#%L;3S5QBat!j8KJu2n@|Gx&o4eGvs0#}p3m6vOJDFq}HpmHQ(UB*b)2C!MJd5r5 zQiYiSYguqGH0q#N%{PdvL-TTFh61Bq(hWueDU_vi!JW@Z1uioe{Yajp-NXt{c#$IH>~shjlb#NpS9GF{2*-sXad$- zdFOw{-Sy5-C`^_f1xO*&&VHpdG+H_z#s6qcW?zUoxRsD<1K`yBQ>kl$x7lkG@t zE-Wr(a8iMKc|aMRbXeXu?%p9 z?7ED9mQFrRB4%I8DMVZA+uVC4ysvFmvHV2FG(a%msT$byy?}&SmPMH*or-)WWvpb3 zzETL@L{q5fW2Hti=uz;yiu}6RQ7j(wIu_FKw4| ze%^$Ahi6AYk8;~iVLH{j=w2b>q@V)i9VVEqH*|}dZwnmgDJ0>F8zia5vdQE}ZRaca z^s_(q(`24q(8*zFp?RQ9eNh1efV1Sl!)hK<(DQM~I)4=F@C#m_#T+hlMfQiai6-P^ zO?bg&h+yFSbRwuTy6VS{OoMP+&a)ctf=WP6oGcOzfQr*b=P)RlVv2xTQ&q(!C>VoD z{p$pWm^NHH^_zC8O1XgB-k3RS5wzfqV4ibvOvSGS#|4iECrpTFxVU;hj$oDfPFF$9 zwqi^_3WDW=vtfv{ce9|joiW}Pyegb1`AgYCcv%o}s5G-Ia~5s7!ra=5YH#2dNz*TR zlF6IDq5NT=ae;;Mbkgh?SZ$)q#In=f96x2v&!bimcON)3ByRSe)+Rh-i|W-LB#=HG9FT%B}Of_kAH3oR)8Y9fQE>%41@$doG zurqeMtGqFIe|@n=tWt`?7EED221!$kL|U~d6dPE22jmIqvLP(6!f-=J6k;ou$vFjuXfpvZBv^IAq8;XUKk=bTpniRwiHGM0xviNnlkL(xQq?>w}CNndell z4D8KwXT(M;z-zMecyj`QK%~1EBq#rXj|jBF8y_+tqD#{z%emZNn1V1E;kX9q1CWb> zvsL|j{OwdZpYVznp_-nzu%ih>;qL9e2fGfaZ{h=?hgw@}!*8SIudd_?5;%36Y2x1bF? z7iQ6@(H0odT3`JN6aQ^<_=gTc{ydH2!H?}BjNL+#e*BRuAtu+wJuIJmE&%(lqZV6p6zQ3jB{=I1pmFo`<(G0@OlA0f7& zC5wcFWKJaqB-$QhhAbt}O$7oKu{!W>X#C%pZA@qJ}ww zmgKnd7Yl}Ow*B3O-@7qj&v*F?->EvJM`xIsbxvP@gjxIBB;DT|ZNX|q{{5>zEo9Q8 z-#_!J3c&5_7Y1pCzN#%!X5Qz$FV-pT)Ir;fL#=x2pku)e8>a_DlT4i&mpqQg6c;zI zOGuT?&cPu(61>Ym^fi+<%g=_JRwQx$yP?akMyhNg+LhN$6>ca%;1?OPX|iPip%ZlU z6;#<-*x{$E3>Flf{8y9)(1I#A*=vd;&XFb@{BBIh^ulb-%7w5RE*jJ2q0`#ms|~6{ z5rtsQHhFTv?{ljEa7o&|xG#m2*R4?cT7r$%`s=C^YeDqt=c<~D1fz4VHa zRs|9XB5X0j*dKT%R8&s4ZOTD}gt-rYN4eW{E%}b=zAjm9?-%!P2Zu)alZJVko0tR* zMq;=F2L#Y#fm)jeq!yz@W_nXe?2(Z7)l?tuEJ_!SD#AsCg-PRIMA7OFehWAyK~Y1` z6}>e%z53GK#$bj5!z`&LDalf<9`Iyu`22Uv^q;0!q&`c54SZF)tRjRm2@#z-F^nQp z6q{npX}48~f0}r61YV^lSW|!4;NvLMo4{ZD*g73yMh3;2&XQfBTa~*)thl;Gk0uT; z+-C3k!-(;i20nlAU}pVlsh63x|Ep~aiJSnRY@uVf};Qft_{lmr}iiRR9nKHZtIZl0MDc zz)pBRRP`*Lt3U|ma}j9@njmijSa3=mj;{o2SW8Vg^D+Jv$=oDtYCt{;X;Cjmk0o|2 zF^L%M_1QisF){YM*+uA6jsa|2L$Kyr98oDx%?;NNAEat6WM-C9W|i7-XP%21`rZY2 z3@u&^{%qGF>m~yfuldr2syW+5#`*!sbeGqrZW8m57RLhpkydc;fGK*ue;$Va9!`$` zK8Qm;wmMbKr|=Q*l31t-q?->YUdaD-l2vMfBn9nIofqNL2uimD@60m56BpLkCthA& zio7jV&5nZds*68h2G!LxgCStM>-t_E zjgCg)Aw=Un&Y^filqd`Yqtg|NhOrs#V5Y|ki1kxeU#3gpv4ayMBa_%Pe7qb{W?C+? z9lNb?y1-(xn%A?zu@>C8CDnYb*nk)Uu}9eEV?1s=Vo_3lH&P=WD0$0IL~E@fG+&Ca6=|axQnHy} zMs*qf`_!Uaqf=Ad_V)G_Rm94Dz}pMj8zM}T#w`xd&lz%6oE1g$TCW-ibgDxk;_7ge zeB3gN`KMd6rIVZOd9R-~R-Aq^GC=ZZm);V1Ka~f@4Go0SM#yqJJ31tJ|2}f=Jo54}wMu1( zHq&lR{hYqha9!HvG0(azS=$aAF+Wn2cqBD$lAyZfu%mv>;vaqEuLs8@&Q}kw*L(u5 zJ3KFc>I2fVDoF^lF|>ur&HuS<=Qp-;D^@oId(io;_s{MQrEr>6vUJ{h71o#%cHhq{ zf|-j=-ObXY$ey|#b+J`d16oD4DRQ)92iK~Gs+TM-A{bk)>U_I-)HYQcl=J&c-tey$ zaJ$|-p99AJiKtHKiZES}A=I1hc2Lq*xT>Yn`*4M(%bQuD5I)l!cEDJP#?I;Xvp{fW z2-MvLnx$ce;|hnBa5vzL-eU-F}3%FBPs8`jW-p{vxQ%I6^v+%bIFF#-f4Qj&x1IUI|p$nlbQ7EJ3e!Q&_>09wP>J(Pw*4|0uJY|Sw z+THrun+Wk+_e7{i%NBS;SU_Ar=dox(%#%Fw_ifQZ%+MnO$GaXvDS@eVsOT4}#ZBWl zX>4rQMLn45xI#1PxmmSKOh!c7ZSx(oozKVoEoj}cWYyi|T4pQw4a>{0XzLSPx%1dR zd|=BrWLr<}b5*uQj}b0aO1K@?%9k$2F)ZXtH666xf^H7sto{A-C#+>f#i{k~h=ZT^ zF)?+aRipCdTo&%n|9vCBaeIInG>;_YcM0;~Ri)3S$y*mCVuT)bbtOXp{&2iExODf| zu-hB;eLvV=rveks`Q&JKy~y+Z?dz^5tK-jNs@a|nh-hh`X+(TpA53@iD495`e(`*U^JqaWud*5D`RBO04` zCZTAlra4aO8eOgyhFrlY=SNF@hz%>z_j5q1Ia}`(e^VBvO?{NPeqP_8ps7*X`V2Zv zQP+08H-SSd%+yTC5tSxF4L1yGw9(d?|DKpIQS<;?nCPF9Mo3{uvQmm1?h(n!U&4vrU8^ZZdzD9DpfY!k#R;9qQ4)0LxibG>SEjE6WnR zoGm$hAiwtnK*nK@f2RMNxL%lsUSWey0@hqv#WFQbTdhJGwd9JRSZPYkpvd#qOF^<6 z9h7?gQ6u&_J;Q!ugMyY;C2OCE_^!`3BJGI~I=lIPSECY4K-Cka7PQRuUxwZ>^HsBE z7c$4t$X)O_H*CY1g83+l=qCB zqiJ$P&3uyIv3UI7>0h4m8h3?jBMVZPX-dOq7rjFZD=f-{kFhN{@a1%und*f$dw}r~ zRjpAIXNQ>MKg~4}L9~!r(x!d zk2AY0Z{%k7APX+1+cV)Rs-0kHUW_ROUv1jB-|S5BHMpzq>aJpX8f#`$0=e6YWF?KT zESQsDsVESxG*fbP$TgdPpWdl9cuj9~vtpa?s~kZ|Cy@oNn%2PU)@IMBT>3^h*=wHr zR$EnPRdpx1k)(e9ylJ>JReIbv4t{V=+O~A?7-lMh z{4%_h5|sH{`rw~1llZr0F5UMX0X84sJ%7zRJ39LOS>#;gU@!55$UE6hBF~m|eLqnR z+Y)l^n_A|LkNMX^eb+(?aY z`!vz=l#l$+0iv4db%N0sG1_-egd9%jc)##o2Vv{8Fh?uY^dmivM_G^Ik9VIe8HuLj zPNEFTf3JvCl8kU5OhH*vWu?pFhMPQkJ_gH|Y1o>$q2ADDhs>>l;H{seo~maiPMici zMky`L>6AH`pGVKAx|{aas}x(JQen0R>x=beHQu@(R-az~+jQ6O!Z3tqj@L_{|9#*M zIO(~6gFpnI={n)XAwL+=icd`+74LXrHn^lFqjXI*BQLgf#y}ghIwy-3KONN zY=~;{&txzcOLodO6SfSk`6@3n)=$75^-}>{7~+u>ue*kzV$P<(t|$tZfn_CbZk*a6 zw9TQj(?)NV1K1TE{jSLYLAwu8-KuIi)NK%`x6VHAG}!~*#MVDheQzJL-nxfZ$rBuk zrE=Iie3?zu2sqSMVy(winW1#UqD(SKjT@&Y zQ${^PU4?HMXufVkS2dOjepjmkvZrv7PU3@fr1 zZhkG-s@wINIkguS5-9uZA#l z&tdaO46i;**4z7{(>INd+&q|caL}ol8H}^^nM(Qe^`5>*3^LW6q-gRt(4o`QEK^E# z5_0{}Op-a2{j6U657ZA%8T%AZNHO*IDj{xZZ~GuBQ;e23D}e-5Plyy%7)FIRuws%A zUA@`uJdqqQ!LiB2sc~vFZ+4-Jcg;th)=-@t7Pfi^OdNAl?NyWhI&ccFkN!=Z)jt(R zPi2Y0P_Rw7_2mD#u@gPo>-FSQTp?b_En{yRzWnQ#)%pDb%&o8T30epV{U6cwT> zn|!TN*O(*Zq~o9qO``TaT5IOjpGlTK&~)UkkSB|s*H4mfB4;X9MCpbYofqC0WthcT z+!a&&uKfIhEtj8`@AocVH}GS6I2Lv&oCy6AG81XZF10IIwa8dg>BPOinUKSzflcLz z6}}^9M~-#9;jva63lVY5`=&+oG z54b|TTTQWt+lV>Al}y(C%2XhW|DO1}nv;q^+D`&9%L*7xm>2`Kl&?%Q(ic3gL*jPy z#mVx#Fr`g#wHo@lddRy{72Q|ZTQ6r@RK|wVXtIYpK}%cr?4JttEaetoNDd*FQVDZXy#@!B%C)V7zh4S{5K@_f*$d#S;QF0`)3ElZ|CMv ztP|%hv%RD{)QKd7vM=;8{t5*4rVE^9b_LK=)HcNOA`F%?3RZC@3QAvLY#PPRk~UJQ zwINRE`d))4UPtw!amV>Z(4%M^vQ&l`oGNk=G&zP*9!BV5uL6(us=wg`x;2o>)%xR6 zyfokB@F$W)$ccSLRUrK#)e6mRzF07Gf0(p$OW$*2uaBzVGjjBcXQYcy&S}HU@YH+wj}(bXI~WN~*f|)q%XC7j>`w zfqrYM>dbGXgRA+_mlB0L1^yYaoalQwJv=+7>?ZGcSwKakjzaeve{2d!D(bx@Pp+h{ z82g}B5Tx7>a>xk67$`50Xei1JY53Mgs(Hp|+QwKtk+N$YNUoL8 zoU4Ww|Dq1BPV%$QBwP}?E4aD!-!HR0Pou$;fYq&=``{n5^C(!nfHOY`JHJC%DRJyj z$q*y%TQG`b&A=xh*gn3SD3V~_Sew*gDBjB<)6=75-m6&L)F1ot7IW>Sx`JDRpG}g` z@aI5d?;BDK+)@4dop(Pn@O-}cbKlp1_M413x%b4c8X{Bs!d7WzZ?swp@w>`&#)VkJ z^yVkJl{)4zm_uqYy7NJ^w8!h}2e}`&a*_U|Gc+Os@H2z#GaT!NyT^gP3@c*)5K0i< zZ+ErlCE`4_hYQ^}8dt{Yvf<3|M3cC~xhwxOBkD687{|LT#+J~sw7cc{V@W&+l?x%t zBTfG6;NI#J-_&BCQ!6^Xs;QcEb~-1vK6-S6NxVlJitSTC7OW$sg3)^a`wTzvxo^f& znTjUSkE%>`rEIf9$vq%BZ4zU?(<@w|b|N)VB<3J^%7>DPsr^P*%z3gipFqoM?_rVA z3$AX!HLP3B^F@1m;^U%53V9wDH*q&atS|eQRJ|-+`Rb1P4sO-wA>DwNTEpk*EgHt4 zxHALz4kMFl&R+!@xu3k2Qh)#YIUF4W_fDqBF7(?frNa-L&pJ_SjzxB4eFlT9_^wC{ zlWPTpDd?*5s^~|K&hLKenS1%?ZaxzCoYiaMNG;D?P_NsJ_w z=s)5^o9D~_49Ung7iwt8OLJZw7~A-xz*e7wAmdi7YXK%A1+7bn+gwwy2TusFGSUS zms_j%`F)AHZb_z5Z~H$Tnz~?yRSJH5?2sY}RY%TcR73U`rBn;bJ}q&Dp(rZXE_|@h`bvX*EyA=~A`6Fp z8hGzh75M5KP5chh_W9>S@$2-D*Jnz3)m>Etxfuxy?dFdey!6$Y>d2{fayPk}(RkZI zG;~g)v<}l)Z3N@2p5vdph7>2IZ}h*9ut?NJP`>?%7c^2pm{dh|?2@rit$$k*c*9(- z8jv;chXR%H$7tC%mCa)#Kf#(a3Iqxuud^9Ne*&3Rhs6C5}%S$5TZR<)MTYjRicQq81A4fCw z5Y-(Ca*zBd|D>5hS13woORj;o9O|cUnu8WyJG8Tm;ox)FOj*Phx(rMUwX2i&alN0- zq2beIX!EnhHu-~ijP|b*JAq&qlbR!r$V|384*izs{G7ABVcf(w`{j56vaCbobe>G^ z_)c(rlrR+Mo-ox&_b_Hg%#-E;A@Mw(FaH5dusq3Pnerkq*0_EZ=l>I|g*0U%EvR&P zO(hL&#c$y!C@G8XbVySoPOZ|ybl1IjukuRNs&E($#9kxvjnGLK5iZai<#KIq zS$dgA4%{rUp!nV}!LK-TtZz|m^vMenQuz_AV9;C76OuBCb++<74zN2gJbgUaZK&~W z3{6HF4ewh)GXC(xyD!y7Qz?Fc7Rio(wV!m%a$Xcsqf&h}7uyLOqb-8i5j6r#C3+!?RGQRM`?L|IAJqagMIc>c$5CJ} znH4>-b=*>S#IhH;6e_NtVY4?Qg%I`k)p%j;kW*GRq_rrWG zl&_Zj5F?M`N&t%xk%!)AE{@M=LcqPU2%Ux6``h-UjP5LF2=cZiE5_MSl0&&kj0ywn zm#%cb5{BdsVb>;=MLX-&mHpVg&PnNM{Ek~;6}^Z+G7s<5=5ponM+{`vFA~Ro9&V z_}pdG>*Z0)4m)Wg#9y^N&7wycmomD5fNF} z^o#In2bI61LsjM~WfLV(E`%{n#TP4ZxbdStI4Zm)K+4EKI%&GU(GSr;nwggl9-MjdDr{>Cw0il$PN?(f@Qc0;KyeNNXRtQU}on!KytU`y!8h!Uk{kBY5vFuuQ<+bN> z;TlxF`D*WXHX2A;j++!~MBbOx{h%nc`wEG39%(!qK$q=Ju-l9`<~;~Z#QoD1Z$*R% z_jfmn-Ed7&wAK4dKtsN;n08*n+sp~1xUwW0h5a$x8m%7m3B&o2$%GOd#SRl%G%A-b zR&Jg$=`wT7Bn2hWzddoRkW?}UtJB8#tfK`{a62=Q@VlfJ77$7E?pr>8yBk-<+;bzG zVi0TM37C%G_3)~o22H@GJ9v(2)KtPlMx?6WQf!#zs((xB|NXl?oV@GipY(zER{@L+BK|v?WOG{`K?h2LBl>W`EiNbe0n;5do zeHOB%3i9BT+(sd^54hDNDGqGgYU;A7IT%5gHQts~r9_QuNhYuh=+So38TqP+-rY?) zIjQl@0h7lvF4Zm+Gqrx5&`&q$&(`Zv%gfB)o8E>dp2@}+tC;Q@g5>IO-^!yuWw7Ph z#GUYL2mY5P0|b*a%8K^yhD+2>9^7Fg7hW2-oR$|~-%w2Y-2OG$b7mUFV^Q`l z+|7Jg%q*21x7UJg$g#3^ZBIyAan4B)YYC_jYxS*4sTZLeYh*#OlgY6CxX0YEP^&zS zkY*Mz}Vk* Date: Mon, 30 May 2016 05:43:45 -0700 Subject: [PATCH 152/843] Correctly cancel network calls in catalyst instance destroy Reviewed By: bestander Differential Revision: D3358458 fbshipit-source-id: 69ee83ba33b02d21310030a457c6378f67e168f9 --- .../react/common/network/OkHttpCallUtil.java | 4 - .../modules/network/NetworkingModule.java | 50 ++++++-- .../modules/network/NetworkingModuleTest.java | 112 +++++++++++++++++- 3 files changed, 145 insertions(+), 21 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java b/ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java index 7fa7d967ae30dc..4da3a5e9b80349 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java +++ b/ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java @@ -32,8 +32,4 @@ public static void cancelTag(OkHttpClient client, Object tag) { } } } - - public static void cancelAll(OkHttpClient client) { - client.dispatcher().cancelAll(); - } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java index 68e6e93b012ce3..1410f085effba1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java @@ -9,6 +9,17 @@ package com.facebook.react.modules.network; +import javax.annotation.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.net.SocketTimeoutException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ExecutorToken; import com.facebook.react.bridge.GuardedAsyncTask; @@ -22,15 +33,6 @@ import com.facebook.react.common.network.OkHttpCallUtil; import com.facebook.react.modules.core.DeviceEventManagerModule; -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.net.SocketTimeoutException; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import javax.annotation.Nullable; - import okhttp3.Call; import okhttp3.Callback; import okhttp3.Headers; @@ -61,6 +63,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule { private final ForwardingCookieHandler mCookieHandler; private final @Nullable String mDefaultUserAgent; private final CookieJarContainer mCookieJarContainer; + private final Set mRequestIds; private boolean mShuttingDown; /* package */ NetworkingModule( @@ -69,7 +72,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule { OkHttpClient client, @Nullable List networkInterceptorCreators) { super(reactContext); - + if (networkInterceptorCreators != null) { OkHttpClient.Builder clientBuilder = client.newBuilder(); for (NetworkInterceptorCreator networkInterceptorCreator : networkInterceptorCreators) { @@ -83,6 +86,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule { mCookieJarContainer = (CookieJarContainer) mClient.cookieJar(); mShuttingDown = false; mDefaultUserAgent = defaultUserAgent; + mRequestIds = new HashSet<>(); } /** @@ -138,7 +142,7 @@ public String getName() { @Override public void onCatalystInstanceDestroy() { mShuttingDown = true; - OkHttpCallUtil.cancelAll(mClient); + cancelAllRequests(); mCookieHandler.destroy(); mCookieJarContainer.removeCookieJar(); @@ -241,6 +245,7 @@ public void sendRequest( requestBuilder.method(method, RequestBodyUtil.getEmptyBody(method)); } + addRequest(requestId); client.newCall(requestBuilder.build()).enqueue( new Callback() { @Override @@ -248,6 +253,7 @@ public void onFailure(Call call, IOException e) { if (mShuttingDown) { return; } + removeRequest(requestId); onRequestError(executorToken, requestId, e.getMessage(), e); } @@ -256,7 +262,7 @@ public void onResponse(Call call, Response response) throws IOException { if (mShuttingDown) { return; } - + removeRequest(requestId); // Before we touch the body send headers to JS onResponseReceived(executorToken, requestId, response); @@ -335,6 +341,21 @@ private void onResponseReceived( getEventEmitter(ExecutorToken).emit("didReceiveNetworkResponse", args); } + private synchronized void addRequest(int requestId) { + mRequestIds.add(requestId); + } + + private synchronized void removeRequest(int requestId) { + mRequestIds.remove(requestId); + } + + private synchronized void cancelAllRequests() { + for (Integer requestId : mRequestIds) { + cancelRequest(requestId); + } + mRequestIds.clear(); + } + private static WritableMap translateHeaders(Headers headers) { WritableMap responseHeaders = Arguments.createMap(); for (int i = 0; i < headers.size(); i++) { @@ -353,6 +374,11 @@ private static WritableMap translateHeaders(Headers headers) { @ReactMethod public void abortRequest(ExecutorToken executorToken, final int requestId) { + cancelRequest(requestId); + removeRequest(requestId); + } + + private void cancelRequest(final int requestId) { // We have to use AsyncTask since this might trigger a NetworkOnMainThreadException, this is an // open issue on OkHttp: https://github.com/square/okhttp/issues/869 new GuardedAsyncTask(getReactApplicationContext()) { diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java index b805611834397b..ec04b5cbdd4067 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java @@ -15,12 +15,13 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ExecutorToken; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.JavaOnlyArray; import com.facebook.react.bridge.JavaOnlyMap; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.network.OkHttpCallUtil; import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; import okhttp3.Call; @@ -31,7 +32,6 @@ import okhttp3.Request; import okhttp3.RequestBody; import okio.Buffer; - import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,8 +40,8 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.rule.PowerMockRule; import org.robolectric.RobolectricTestRunner; @@ -63,7 +63,8 @@ MultipartBody.class, MultipartBody.Builder.class, NetworkingModule.class, - OkHttpClient.class}) + OkHttpClient.class, + OkHttpCallUtil.class}) @RunWith(RobolectricTestRunner.class) @PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) public class NetworkingModuleTest { @@ -476,4 +477,105 @@ public Object answer(InvocationOnMock invocation) throws Throwable { assertThat(bodyRequestBody.get(1).contentType()).isEqualTo(MediaType.parse("image/jpg")); assertThat(bodyRequestBody.get(1).contentLength()).isEqualTo("imageUri".getBytes().length); } + + @Test + public void testCancelAllCallsOnCatalystInstanceDestroy() throws Exception { + PowerMockito.mockStatic(OkHttpCallUtil.class); + OkHttpClient httpClient = mock(OkHttpClient.class); + final int requests = 3; + final Call[] calls = new Call[requests]; + for (int idx = 0; idx < requests; idx++) { + calls[idx] = mock(Call.class); + } + + when(httpClient.cookieJar()).thenReturn(mock(CookieJarContainer.class)); + when(httpClient.newCall(any(Request.class))).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Request request = (Request) invocation.getArguments()[0]; + return calls[(Integer) request.tag() - 1]; + } + }); + NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + networkingModule.initialize(); + + for (int idx = 0; idx < requests; idx++) { + networkingModule.sendRequest( + mock(ExecutorToken.class), + "GET", + "http://somedomain/foo", + idx + 1, + JavaOnlyArray.of(), + null, + true, + 0); + } + verify(httpClient, times(3)).newCall(any(Request.class)); + + networkingModule.onCatalystInstanceDestroy(); + PowerMockito.verifyStatic(times(3)); + ArgumentCaptor clientArguments = ArgumentCaptor.forClass(OkHttpClient.class); + ArgumentCaptor requestIdArguments = ArgumentCaptor.forClass(Integer.class); + OkHttpCallUtil.cancelTag(clientArguments.capture(), requestIdArguments.capture()); + + assertThat(requestIdArguments.getAllValues().size()).isEqualTo(requests); + for (int idx = 0; idx < requests; idx++) { + assertThat(requestIdArguments.getAllValues().contains(idx + 1)).isTrue(); + } + } + + @Test + public void testCancelSomeCallsOnCatalystInstanceDestroy() throws Exception { + PowerMockito.mockStatic(OkHttpCallUtil.class); + OkHttpClient httpClient = mock(OkHttpClient.class); + final int requests = 3; + final Call[] calls = new Call[requests]; + for (int idx = 0; idx < requests; idx++) { + calls[idx] = mock(Call.class); + } + + when(httpClient.cookieJar()).thenReturn(mock(CookieJarContainer.class)); + when(httpClient.newCall(any(Request.class))).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Request request = (Request) invocation.getArguments()[0]; + return calls[(Integer) request.tag() - 1]; + } + }); + NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + + for (int idx = 0; idx < requests; idx++) { + networkingModule.sendRequest( + mock(ExecutorToken.class), + "GET", + "http://somedomain/foo", + idx + 1, + JavaOnlyArray.of(), + null, + true, + 0); + } + verify(httpClient, times(3)).newCall(any(Request.class)); + + networkingModule.abortRequest(mock(ExecutorToken.class), requests); + PowerMockito.verifyStatic(times(1)); + ArgumentCaptor clientArguments = ArgumentCaptor.forClass(OkHttpClient.class); + ArgumentCaptor requestIdArguments = ArgumentCaptor.forClass(Integer.class); + OkHttpCallUtil.cancelTag(clientArguments.capture(), requestIdArguments.capture()); + assertThat(requestIdArguments.getAllValues().size()).isEqualTo(1); + assertThat(requestIdArguments.getAllValues().get(0)).isEqualTo(requests); + + // verifyStatic actually does not clear all calls so far, so we have to check for all of them. + // If `cancelTag` would've been called again for the aborted call, we would have had + // `requests + 1` calls. + networkingModule.onCatalystInstanceDestroy(); + PowerMockito.verifyStatic(times(requests)); + clientArguments = ArgumentCaptor.forClass(OkHttpClient.class); + requestIdArguments = ArgumentCaptor.forClass(Integer.class); + OkHttpCallUtil.cancelTag(clientArguments.capture(), requestIdArguments.capture()); + assertThat(requestIdArguments.getAllValues().size()).isEqualTo(requests); + for (int idx = 0; idx < requests; idx++) { + assertThat(requestIdArguments.getAllValues().contains(idx + 1)).isTrue(); + } + } } From d31a54a018953f07f74ed54027a0166db873e719 Mon Sep 17 00:00:00 2001 From: Andrej Badin Date: Mon, 30 May 2016 20:38:59 -0700 Subject: [PATCH 153/843] Fix scrolling issue on iOS devices. Summary: It was not possible to scroll due to overlaying (and visually not present) docs navigation layer. Fixes issue introduced in PR #7669, see this [comment](https://github.com/facebook/react-native/pull/7669#issuecomment-220784882) Additionally, script makes sure only single target is "in" and closes any other target with "in" state. Closes https://github.com/facebook/react-native/pull/7731 Differential Revision: D3344927 Pulled By: vjeux fbshipit-source-id: b2c64d90a2a6e531a9be79b936d6a5da61a69b22 --- website/src/react-native/css/react-native.css | 37 ++++++++++++------- website/src/react-native/js/scripts.js | 15 ++++++-- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/website/src/react-native/css/react-native.css b/website/src/react-native/css/react-native.css index 5217b1e62fcc13..7d506e34ec217c 100644 --- a/website/src/react-native/css/react-native.css +++ b/website/src/react-native/css/react-native.css @@ -482,41 +482,52 @@ h1:hover .hash-link, h2:hover .hash-link, h3:hover .hash-link, h4:hover .hash-li } @media only screen and (max-device-width: 1024px) { + @-webkit-keyframes slide-in { + 0% { top: -30px; opacity: 0; } + 100% { top: 0; opacity: 1; } + } + @-moz-keyframes slide-in { + 0% { top: -30px; opacity: 0; } + 100% { top: 0; opacity: 1; } + } + @-o-keyframes slide-in { + 0% { top: -30px; opacity: 0; } + 100% { top: 0; opacity: 1; } + } + @keyframes slide-in { + 0% { top: -30px; opacity: 0; } + 100% { top: 0; opacity: 1; } + } + .nav-docs { position: fixed; z-index: 90; - top: 0; + top: -100%; left: 0; width: 100%; height: 100%; margin: 0; padding: 53px 0 0 0; background: #3B3738; - /* Transition these properties */ - transition: opacity 0.3s, visibility 0.3s; - visibility: hidden; - opacity: 0; } .nav-docs-viewport { border-top: 1px solid rgb(5, 165, 209); - height: 100%; padding: 25px; overflow: scroll; -webkit-overflow-scrolling: touch; - top: -30px; position: relative; - transition: top 0.3s; + width: 100%; + height: 100%; } /* Active state */ .nav-docs.in { - visibility: visible; - opacity: 1; - } - - .nav-docs.in .nav-docs-viewport { top: 0; + -webkit-animation: slide-in 0.3s forwards; + -moz-animation: slide-in 0.3s forwards; + -o-animation: slide-in 0.3s forwards; + animation: slide-in 0.3s forwards; } .nav-docs * { diff --git a/website/src/react-native/js/scripts.js b/website/src/react-native/js/scripts.js index 0932c31ff18130..e59e8c1f4a66dd 100644 --- a/website/src/react-native/js/scripts.js +++ b/website/src/react-native/js/scripts.js @@ -8,7 +8,7 @@ function init() { if (isMobile()) { - document.querySelector('.nav-site-wrapper a[data-target]').addEventListener('click', toggleTargetNav); + document.querySelector('.nav-site-wrapper a[data-target]').addEventListener('click', toggleTarget); } var backdrop = document.querySelector('.modal-backdrop'); @@ -46,12 +46,21 @@ modal.classList.remove('modal-open'); } - function toggleTargetNav(event) { + var toggledTarget; + function toggleTarget(event) { var target = document.body.querySelector(event.target.getAttribute('data-target')); if (target) { event.preventDefault(); - target.classList.toggle('in'); + + if (toggledTarget === target) { + toggledTarget.classList.toggle('in'); + } else { + toggledTarget && toggledTarget.classList.remove('in'); + target.classList.add('in'); + } + + toggledTarget = target; } } From c2c370c8869281e13c2346077bb09874a00c1967 Mon Sep 17 00:00:00 2001 From: Emil Sjolander Date: Tue, 31 May 2016 04:11:41 -0700 Subject: [PATCH 154/843] import css-layout-185 Reviewed By: lucasr Differential Revision: D3312496 fbshipit-source-id: 259b6db2fc0166696eb171dc6e2974c81ec2133f --- React/Layout/Layout.c | 1733 ++++++++++------- React/Layout/Layout.h | 52 +- React/Views/RCTRootShadowView.m | 1 - .../csslayout/CSSCachedMeasurement.java | 22 + .../com/facebook/csslayout/CSSLayout.java | 48 +- .../facebook/csslayout/CSSLayoutContext.java | 3 +- .../java/com/facebook/csslayout/CSSNode.java | 8 +- .../com/facebook/csslayout/CSSOverflow.java | 17 + .../java/com/facebook/csslayout/CSSStyle.java | 4 +- .../com/facebook/csslayout/LayoutEngine.java | 1620 +++++++++------ .../main/java/com/facebook/csslayout/README | 2 +- .../com/facebook/csslayout/README.facebook | 2 +- 12 files changed, 2171 insertions(+), 1341 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSCachedMeasurement.java create mode 100644 ReactAndroid/src/main/java/com/facebook/csslayout/CSSOverflow.java diff --git a/React/Layout/Layout.c b/React/Layout/Layout.c index c8f086b0ef22a6..d94535b30b3d47 100644 --- a/React/Layout/Layout.c +++ b/React/Layout/Layout.c @@ -19,6 +19,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ + #include #include #include #include @@ -41,6 +42,13 @@ #endif #endif + #define POSITIVE_FLEX_IS_AUTO 0 + + int gCurrentGenerationCount = 0; + + bool layoutNodeInternal(css_node_t* node, float availableWidth, float availableHeight, css_direction_t parentDirection, + css_measure_mode_t widthMeasureMode, css_measure_mode_t heightMeasureMode, bool performLayout, char* reason); + bool isUndefined(float value) { return isnan(value); } @@ -52,13 +60,15 @@ return fabs(a - b) < 0.0001; } - void init_css_node(css_node_t *node) { + void init_css_node(css_node_t* node) { node->style.align_items = CSS_ALIGN_STRETCH; node->style.align_content = CSS_ALIGN_FLEX_START; node->style.direction = CSS_DIRECTION_INHERIT; node->style.flex_direction = CSS_FLEX_DIRECTION_COLUMN; + node->style.overflow = CSS_OVERFLOW_VISIBLE; + // Some of the fields default to undefined and not 0 node->style.dimensions[CSS_WIDTH] = CSS_UNDEFINED; node->style.dimensions[CSS_HEIGHT] = CSS_UNDEFINED; @@ -85,21 +95,23 @@ node->layout.dimensions[CSS_HEIGHT] = CSS_UNDEFINED; // Such that the comparison is always going to be false - node->layout.last_requested_dimensions[CSS_WIDTH] = -1; - node->layout.last_requested_dimensions[CSS_HEIGHT] = -1; - node->layout.last_parent_max_width = -1; - node->layout.last_parent_max_height = -1; - node->layout.last_direction = (css_direction_t)-1; + node->layout.last_parent_direction = (css_direction_t)-1; node->layout.should_update = true; + node->layout.next_cached_measurements_index = 0; + + node->layout.measured_dimensions[CSS_WIDTH] = CSS_UNDEFINED; + node->layout.measured_dimensions[CSS_HEIGHT] = CSS_UNDEFINED; + node->layout.cached_layout.width_measure_mode = (css_measure_mode_t)-1; + node->layout.cached_layout.height_measure_mode = (css_measure_mode_t)-1; } - css_node_t *new_css_node() { - css_node_t *node = (css_node_t *)calloc(1, sizeof(*node)); + css_node_t* new_css_node() { + css_node_t* node = (css_node_t*)calloc(1, sizeof(*node)); init_css_node(node); return node; } - void free_css_node(css_node_t *node) { + void free_css_node(css_node_t* node) { free(node); } @@ -109,13 +121,13 @@ } } - static void print_number_0(const char *str, float number) { + static void print_number_0(const char* str, float number) { if (!eq(number, 0)) { printf("%s: %g, ", str, number); } } - static void print_number_nan(const char *str, float number) { + static void print_number_nan(const char* str, float number) { if (!isnan(number)) { printf("%s: %g, ", str, number); } @@ -130,7 +142,7 @@ static void print_css_node_rec( - css_node_t *node, + css_node_t* node, css_print_options_t options, int level ) { @@ -154,11 +166,11 @@ if (node->style.flex_direction == CSS_FLEX_DIRECTION_COLUMN) { printf("flexDirection: 'column', "); } else if (node->style.flex_direction == CSS_FLEX_DIRECTION_COLUMN_REVERSE) { - printf("flexDirection: 'columnReverse', "); + printf("flexDirection: 'column-reverse', "); } else if (node->style.flex_direction == CSS_FLEX_DIRECTION_ROW) { printf("flexDirection: 'row', "); } else if (node->style.flex_direction == CSS_FLEX_DIRECTION_ROW_REVERSE) { - printf("flexDirection: 'rowReverse', "); + printf("flexDirection: 'row-reverse', "); } if (node->style.justify_content == CSS_JUSTIFY_CENTER) { @@ -199,6 +211,12 @@ print_number_nan("flex", node->style.flex); + if (node->style.overflow == CSS_OVERFLOW_HIDDEN) { + printf("overflow: 'hidden', "); + } else if (node->style.overflow == CSS_OVERFLOW_VISIBLE) { + printf("overflow: 'visible', "); + } + if (four_equal(node->style.margin)) { print_number_0("margin", node->style.margin[CSS_LEFT]); } else { @@ -211,7 +229,7 @@ } if (four_equal(node->style.padding)) { - print_number_0("padding", node->style.margin[CSS_LEFT]); + print_number_0("padding", node->style.padding[CSS_LEFT]); } else { print_number_0("paddingLeft", node->style.padding[CSS_LEFT]); print_number_0("paddingRight", node->style.padding[CSS_RIGHT]); @@ -234,6 +252,10 @@ print_number_nan("width", node->style.dimensions[CSS_WIDTH]); print_number_nan("height", node->style.dimensions[CSS_HEIGHT]); + print_number_nan("maxWidth", node->style.maxDimensions[CSS_WIDTH]); + print_number_nan("maxHeight", node->style.maxDimensions[CSS_HEIGHT]); + print_number_nan("minWidth", node->style.minDimensions[CSS_WIDTH]); + print_number_nan("minHeight", node->style.minDimensions[CSS_HEIGHT]); if (node->style.position_type == CSS_POSITION_ABSOLUTE) { printf("position: 'absolute', "); @@ -257,11 +279,10 @@ } } - void print_css_node(css_node_t *node, css_print_options_t options) { + void print_css_node(css_node_t* node, css_print_options_t options) { print_css_node_rec(node, options, 0); } - static css_position_t leading[4] = { /* CSS_FLEX_DIRECTION_COLUMN = */ CSS_TOP, /* CSS_FLEX_DIRECTION_COLUMN_REVERSE = */ CSS_BOTTOM, @@ -297,7 +318,41 @@ flex_direction == CSS_FLEX_DIRECTION_COLUMN_REVERSE; } - static float getLeadingMargin(css_node_t *node, css_flex_direction_t axis) { + static bool isFlexBasisAuto(css_node_t* node) { + #if POSITIVE_FLEX_IS_AUTO + // All flex values are auto. + (void) node; + return true; + #else + // A flex value > 0 implies a basis of zero. + return node->style.flex <= 0; + #endif + } + + static float getFlexGrowFactor(css_node_t* node) { + // Flex grow is implied by positive values for flex. + if (node->style.flex > 0) { + return node->style.flex; + } + return 0; + } + + static float getFlexShrinkFactor(css_node_t* node) { + #if POSITIVE_FLEX_IS_AUTO + // A flex shrink factor of 1 is implied by non-zero values for flex. + if (node->style.flex != 0) { + return 1; + } + #else + // A flex shrink factor of 1 is implied by negative values for flex. + if (node->style.flex < 0) { + return 1; + } + #endif + return 0; + } + + static float getLeadingMargin(css_node_t* node, css_flex_direction_t axis) { if (isRowDirection(axis) && !isUndefined(node->style.margin[CSS_START])) { return node->style.margin[CSS_START]; } @@ -305,7 +360,7 @@ return node->style.margin[leading[axis]]; } - static float getTrailingMargin(css_node_t *node, css_flex_direction_t axis) { + static float getTrailingMargin(css_node_t* node, css_flex_direction_t axis) { if (isRowDirection(axis) && !isUndefined(node->style.margin[CSS_END])) { return node->style.margin[CSS_END]; } @@ -313,7 +368,7 @@ return node->style.margin[trailing[axis]]; } - static float getLeadingPadding(css_node_t *node, css_flex_direction_t axis) { + static float getLeadingPadding(css_node_t* node, css_flex_direction_t axis) { if (isRowDirection(axis) && !isUndefined(node->style.padding[CSS_START]) && node->style.padding[CSS_START] >= 0) { @@ -327,7 +382,7 @@ return 0; } - static float getTrailingPadding(css_node_t *node, css_flex_direction_t axis) { + static float getTrailingPadding(css_node_t* node, css_flex_direction_t axis) { if (isRowDirection(axis) && !isUndefined(node->style.padding[CSS_END]) && node->style.padding[CSS_END] >= 0) { @@ -341,7 +396,7 @@ return 0; } - static float getLeadingBorder(css_node_t *node, css_flex_direction_t axis) { + static float getLeadingBorder(css_node_t* node, css_flex_direction_t axis) { if (isRowDirection(axis) && !isUndefined(node->style.border[CSS_START]) && node->style.border[CSS_START] >= 0) { @@ -355,7 +410,7 @@ return 0; } - static float getTrailingBorder(css_node_t *node, css_flex_direction_t axis) { + static float getTrailingBorder(css_node_t* node, css_flex_direction_t axis) { if (isRowDirection(axis) && !isUndefined(node->style.border[CSS_END]) && node->style.border[CSS_END] >= 0) { @@ -369,34 +424,30 @@ return 0; } - static float getLeadingPaddingAndBorder(css_node_t *node, css_flex_direction_t axis) { + static float getLeadingPaddingAndBorder(css_node_t* node, css_flex_direction_t axis) { return getLeadingPadding(node, axis) + getLeadingBorder(node, axis); } - static float getTrailingPaddingAndBorder(css_node_t *node, css_flex_direction_t axis) { + static float getTrailingPaddingAndBorder(css_node_t* node, css_flex_direction_t axis) { return getTrailingPadding(node, axis) + getTrailingBorder(node, axis); } - static float getBorderAxis(css_node_t *node, css_flex_direction_t axis) { - return getLeadingBorder(node, axis) + getTrailingBorder(node, axis); - } - - static float getMarginAxis(css_node_t *node, css_flex_direction_t axis) { + static float getMarginAxis(css_node_t* node, css_flex_direction_t axis) { return getLeadingMargin(node, axis) + getTrailingMargin(node, axis); } - static float getPaddingAndBorderAxis(css_node_t *node, css_flex_direction_t axis) { + static float getPaddingAndBorderAxis(css_node_t* node, css_flex_direction_t axis) { return getLeadingPaddingAndBorder(node, axis) + getTrailingPaddingAndBorder(node, axis); } - static css_align_t getAlignItem(css_node_t *node, css_node_t *child) { + static css_align_t getAlignItem(css_node_t* node, css_node_t* child) { if (child->style.align_self != CSS_ALIGN_AUTO) { return child->style.align_self; } return node->style.align_items; } - static css_direction_t resolveDirection(css_node_t *node, css_direction_t parentDirection) { + static css_direction_t resolveDirection(css_node_t* node, css_direction_t parentDirection) { css_direction_t direction = node->style.direction; if (direction == CSS_DIRECTION_INHERIT) { @@ -406,7 +457,7 @@ return direction; } - static css_flex_direction_t getFlexDirection(css_node_t *node) { + static css_flex_direction_t getFlexDirection(css_node_t* node) { return node->style.flex_direction; } @@ -430,46 +481,46 @@ } } - static float getFlex(css_node_t *node) { + static float getFlex(css_node_t* node) { return node->style.flex; } - static bool isFlex(css_node_t *node) { + static bool isFlex(css_node_t* node) { return ( node->style.position_type == CSS_POSITION_RELATIVE && - getFlex(node) > 0 + getFlex(node) != 0 ); } - static bool isFlexWrap(css_node_t *node) { + static bool isFlexWrap(css_node_t* node) { return node->style.flex_wrap == CSS_WRAP; } - static float getDimWithMargin(css_node_t *node, css_flex_direction_t axis) { - return node->layout.dimensions[dim[axis]] + + static float getDimWithMargin(css_node_t* node, css_flex_direction_t axis) { + return node->layout.measured_dimensions[dim[axis]] + getLeadingMargin(node, axis) + getTrailingMargin(node, axis); } - static bool isStyleDimDefined(css_node_t *node, css_flex_direction_t axis) { + static bool isStyleDimDefined(css_node_t* node, css_flex_direction_t axis) { float value = node->style.dimensions[dim[axis]]; return !isUndefined(value) && value >= 0.0; } - static bool isLayoutDimDefined(css_node_t *node, css_flex_direction_t axis) { - float value = node->layout.dimensions[dim[axis]]; + static bool isLayoutDimDefined(css_node_t* node, css_flex_direction_t axis) { + float value = node->layout.measured_dimensions[dim[axis]]; return !isUndefined(value) && value >= 0.0; } - static bool isPosDefined(css_node_t *node, css_position_t position) { + static bool isPosDefined(css_node_t* node, css_position_t position) { return !isUndefined(node->style.position[position]); } - static bool isMeasureDefined(css_node_t *node) { + static bool isMeasureDefined(css_node_t* node) { return node->measure; } - static float getPosition(css_node_t *node, css_position_t position) { + static float getPosition(css_node_t* node, css_position_t position) { float result = node->style.position[position]; if (!isUndefined(result)) { return result; @@ -477,7 +528,7 @@ return 0; } - static float boundAxis(css_node_t *node, css_flex_direction_t axis, float value) { + static float boundAxisWithinMinAndMax(css_node_t* node, css_flex_direction_t axis, float value) { float min = CSS_UNDEFINED; float max = CSS_UNDEFINED; @@ -501,32 +552,22 @@ return boundValue; } - // When the user specifically sets a value for width or height - static void setDimensionFromStyle(css_node_t *node, css_flex_direction_t axis) { - // The parent already computed us a width or height. We just skip it - if (isLayoutDimDefined(node, axis)) { - return; - } - // We only run if there's a width or height defined - if (!isStyleDimDefined(node, axis)) { - return; - } - - // The dimensions can never be smaller than the padding and border - node->layout.dimensions[dim[axis]] = fmaxf( - boundAxis(node, axis, node->style.dimensions[dim[axis]]), - getPaddingAndBorderAxis(node, axis) - ); + // Like boundAxisWithinMinAndMax but also ensures that the value doesn't go below the + // padding and border amount. + static float boundAxis(css_node_t* node, css_flex_direction_t axis, float value) { + return fmaxf(boundAxisWithinMinAndMax(node, axis, value), getPaddingAndBorderAxis(node, axis)); } - static void setTrailingPosition(css_node_t *node, css_node_t *child, css_flex_direction_t axis) { - child->layout.position[trailing[axis]] = node->layout.dimensions[dim[axis]] - - child->layout.dimensions[dim[axis]] - child->layout.position[pos[axis]]; - } + static void setTrailingPosition(css_node_t* node, css_node_t* child, css_flex_direction_t axis) { + float size = child->style.position_type == CSS_POSITION_ABSOLUTE ? + 0 : + child->layout.measured_dimensions[dim[axis]]; + child->layout.position[trailing[axis]] = node->layout.measured_dimensions[dim[axis]] - size - child->layout.position[pos[axis]]; + } // If both left and right are defined, then use left. Otherwise return // +left or -right depending on which is defined. - static float getRelativePosition(css_node_t *node, css_flex_direction_t axis) { + static float getRelativePosition(css_node_t* node, css_flex_direction_t axis) { float lead = node->style.position[leading[axis]]; if (!isUndefined(lead)) { return lead; @@ -534,352 +575,388 @@ return -getPosition(node, trailing[axis]); } - static void layoutNodeImpl(css_node_t *node, float parentMaxWidth, float parentMaxHeight, css_direction_t parentDirection) { - /** START_GENERATED **/ - css_direction_t direction = resolveDirection(node, parentDirection); + static void setPosition(css_node_t* node, css_direction_t direction) { css_flex_direction_t mainAxis = resolveAxis(getFlexDirection(node), direction); css_flex_direction_t crossAxis = getCrossFlexDirection(mainAxis, direction); - css_flex_direction_t resolvedRowAxis = resolveAxis(CSS_FLEX_DIRECTION_ROW, direction); - - // Handle width and height style attributes - setDimensionFromStyle(node, mainAxis); - setDimensionFromStyle(node, crossAxis); - - // Set the resolved resolution in the node's layout - node->layout.direction = direction; - // The position is set by the parent, but we need to complete it with a - // delta composed of the margin and left/top/right/bottom - node->layout.position[leading[mainAxis]] += getLeadingMargin(node, mainAxis) + + node->layout.position[leading[mainAxis]] = getLeadingMargin(node, mainAxis) + getRelativePosition(node, mainAxis); - node->layout.position[trailing[mainAxis]] += getTrailingMargin(node, mainAxis) + + node->layout.position[trailing[mainAxis]] = getTrailingMargin(node, mainAxis) + getRelativePosition(node, mainAxis); - node->layout.position[leading[crossAxis]] += getLeadingMargin(node, crossAxis) + + node->layout.position[leading[crossAxis]] = getLeadingMargin(node, crossAxis) + getRelativePosition(node, crossAxis); - node->layout.position[trailing[crossAxis]] += getTrailingMargin(node, crossAxis) + + node->layout.position[trailing[crossAxis]] = getTrailingMargin(node, crossAxis) + getRelativePosition(node, crossAxis); + } - // Inline immutable values from the target node to avoid excessive method - // invocations during the layout calculation. - int childCount = node->children_count; - float paddingAndBorderAxisResolvedRow = getPaddingAndBorderAxis(node, resolvedRowAxis); + // + // This is the main routine that implements a subset of the flexbox layout algorithm + // described in the W3C CSS documentation: https://www.w3.org/TR/css3-flexbox/. + // + // Limitations of this algorithm, compared to the full standard: + // * Display property is always assumed to be 'flex' except for Text nodes, which + // are assumed to be 'inline-flex'. + // * The 'zIndex' property (or any form of z ordering) is not supported. Nodes are + // stacked in document order. + // * The 'order' property is not supported. The order of flex items is always defined + // by document order. + // * The 'visibility' property is always assumed to be 'visible'. Values of 'collapse' + // and 'hidden' are not supported. + // * The 'wrap' property supports only 'nowrap' (which is the default) or 'wrap'. The + // rarely-used 'wrap-reverse' is not supported. + // * Rather than allowing arbitrary combinations of flexGrow, flexShrink and + // flexBasis, this algorithm supports only the three most common combinations: + // flex: 0 is equiavlent to flex: 0 0 auto + // flex: n (where n is a positive value) is equivalent to flex: n 1 auto + // If POSITIVE_FLEX_IS_AUTO is 0, then it is equivalent to flex: n 0 0 + // This is faster because the content doesn't need to be measured, but it's + // less flexible because the basis is always 0 and can't be overriden with + // the width/height attributes. + // flex: -1 (or any negative value) is equivalent to flex: 0 1 auto + // * Margins cannot be specified as 'auto'. They must be specified in terms of pixel + // values, and the default value is 0. + // * The 'baseline' value is not supported for alignItems and alignSelf properties. + // * Values of width, maxWidth, minWidth, height, maxHeight and minHeight must be + // specified as pixel values, not as percentages. + // * There is no support for calculation of dimensions based on intrinsic aspect ratios + // (e.g. images). + // * There is no support for forced breaks. + // * It does not support vertical inline directions (top-to-bottom or bottom-to-top text). + // + // Deviations from standard: + // * Section 4.5 of the spec indicates that all flex items have a default minimum + // main size. For text blocks, for example, this is the width of the widest word. + // Calculating the minimum width is expensive, so we forego it and assume a default + // minimum main size of 0. + // * Min/Max sizes in the main axis are not honored when resolving flexible lengths. + // * The spec indicates that the default value for 'flexDirection' is 'row', but + // the algorithm below assumes a default of 'column'. + // + // Input parameters: + // - node: current node to be sized and layed out + // - availableWidth & availableHeight: available size to be used for sizing the node + // or CSS_UNDEFINED if the size is not available; interpretation depends on layout + // flags + // - parentDirection: the inline (text) direction within the parent (left-to-right or + // right-to-left) + // - widthMeasureMode: indicates the sizing rules for the width (see below for explanation) + // - heightMeasureMode: indicates the sizing rules for the height (see below for explanation) + // - performLayout: specifies whether the caller is interested in just the dimensions + // of the node or it requires the entire node and its subtree to be layed out + // (with final positions) + // + // Details: + // This routine is called recursively to lay out subtrees of flexbox elements. It uses the + // information in node.style, which is treated as a read-only input. It is responsible for + // setting the layout.direction and layout.measured_dimensions fields for the input node as well + // as the layout.position and layout.line_index fields for its child nodes. The + // layout.measured_dimensions field includes any border or padding for the node but does + // not include margins. + // + // The spec describes four different layout modes: "fill available", "max content", "min content", + // and "fit content". Of these, we don't use "min content" because we don't support default + // minimum main sizes (see above for details). Each of our measure modes maps to a layout mode + // from the spec (https://www.w3.org/TR/css3-sizing/#terms): + // - CSS_MEASURE_MODE_UNDEFINED: max content + // - CSS_MEASURE_MODE_EXACTLY: fill available + // - CSS_MEASURE_MODE_AT_MOST: fit content + // + // When calling layoutNodeImpl and layoutNodeInternal, if the caller passes an available size of + // undefined then it must also pass a measure mode of CSS_MEASURE_MODE_UNDEFINED in that dimension. + // + static void layoutNodeImpl(css_node_t* node, float availableWidth, float availableHeight, + css_direction_t parentDirection, css_measure_mode_t widthMeasureMode, css_measure_mode_t heightMeasureMode, bool performLayout) { + /** START_GENERATED **/ + + assert(isUndefined(availableWidth) ? widthMeasureMode == CSS_MEASURE_MODE_UNDEFINED : true); // availableWidth is indefinite so widthMeasureMode must be CSS_MEASURE_MODE_UNDEFINED + assert(isUndefined(availableHeight) ? heightMeasureMode == CSS_MEASURE_MODE_UNDEFINED : true); // availableHeight is indefinite so heightMeasureMode must be CSS_MEASURE_MODE_UNDEFINED + + float paddingAndBorderAxisRow = getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); float paddingAndBorderAxisColumn = getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_COLUMN); + float marginAxisRow = getMarginAxis(node, CSS_FLEX_DIRECTION_ROW); + float marginAxisColumn = getMarginAxis(node, CSS_FLEX_DIRECTION_COLUMN); + // Set the resolved resolution in the node's layout. + css_direction_t direction = resolveDirection(node, parentDirection); + node->layout.direction = direction; + + // For content (text) nodes, determine the dimensions based on the text contents. if (isMeasureDefined(node)) { - bool isResolvedRowDimDefined = isLayoutDimDefined(node, resolvedRowAxis); - - float width = CSS_UNDEFINED; - css_measure_mode_t widthMode = CSS_MEASURE_MODE_UNDEFINED; - if (isStyleDimDefined(node, resolvedRowAxis)) { - width = node->style.dimensions[CSS_WIDTH]; - widthMode = CSS_MEASURE_MODE_EXACTLY; - } else if (isResolvedRowDimDefined) { - width = node->layout.dimensions[dim[resolvedRowAxis]]; - widthMode = CSS_MEASURE_MODE_EXACTLY; - } else { - width = parentMaxWidth - - getMarginAxis(node, resolvedRowAxis); - widthMode = CSS_MEASURE_MODE_AT_MOST; - } - width -= paddingAndBorderAxisResolvedRow; - if (isUndefined(width)) { - widthMode = CSS_MEASURE_MODE_UNDEFINED; - } + float innerWidth = availableWidth - marginAxisRow - paddingAndBorderAxisRow; + float innerHeight = availableHeight - marginAxisColumn - paddingAndBorderAxisColumn; - float height = CSS_UNDEFINED; - css_measure_mode_t heightMode = CSS_MEASURE_MODE_UNDEFINED; - if (isStyleDimDefined(node, CSS_FLEX_DIRECTION_COLUMN)) { - height = node->style.dimensions[CSS_HEIGHT]; - heightMode = CSS_MEASURE_MODE_EXACTLY; - } else if (isLayoutDimDefined(node, CSS_FLEX_DIRECTION_COLUMN)) { - height = node->layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]]; - heightMode = CSS_MEASURE_MODE_EXACTLY; - } else { - height = parentMaxHeight - - getMarginAxis(node, resolvedRowAxis); - heightMode = CSS_MEASURE_MODE_AT_MOST; - } - height -= getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_COLUMN); - if (isUndefined(height)) { - heightMode = CSS_MEASURE_MODE_UNDEFINED; - } + if (widthMeasureMode == CSS_MEASURE_MODE_EXACTLY && heightMeasureMode == CSS_MEASURE_MODE_EXACTLY) { - // We only need to give a dimension for the text if we haven't got any - // for it computed yet. It can either be from the style attribute or because - // the element is flexible. - bool isRowUndefined = !isStyleDimDefined(node, resolvedRowAxis) && !isResolvedRowDimDefined; - bool isColumnUndefined = !isStyleDimDefined(node, CSS_FLEX_DIRECTION_COLUMN) && - isUndefined(node->layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]]); + // Don't bother sizing the text if both dimensions are already defined. + node->layout.measured_dimensions[CSS_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, availableWidth - marginAxisRow); + node->layout.measured_dimensions[CSS_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, availableHeight - marginAxisColumn); + } else if (innerWidth <= 0 || innerHeight <= 0) { - // Let's not measure the text if we already know both dimensions - if (isRowUndefined || isColumnUndefined) { + // Don't bother sizing the text if there's no horizontal or vertical space. + node->layout.measured_dimensions[CSS_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, 0); + node->layout.measured_dimensions[CSS_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, 0); + } else { + + // Measure the text under the current constraints. css_dim_t measureDim = node->measure( node->context, - width, - widthMode, - height, - heightMode + innerWidth, + widthMeasureMode, + innerHeight, + heightMeasureMode ); - if (isRowUndefined) { - node->layout.dimensions[CSS_WIDTH] = measureDim.dimensions[CSS_WIDTH] + - paddingAndBorderAxisResolvedRow; - } - if (isColumnUndefined) { - node->layout.dimensions[CSS_HEIGHT] = measureDim.dimensions[CSS_HEIGHT] + - paddingAndBorderAxisColumn; - } + + node->layout.measured_dimensions[CSS_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, + (widthMeasureMode == CSS_MEASURE_MODE_UNDEFINED || widthMeasureMode == CSS_MEASURE_MODE_AT_MOST) ? + measureDim.dimensions[CSS_WIDTH] + paddingAndBorderAxisRow : + availableWidth - marginAxisRow); + node->layout.measured_dimensions[CSS_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, + (heightMeasureMode == CSS_MEASURE_MODE_UNDEFINED || heightMeasureMode == CSS_MEASURE_MODE_AT_MOST) ? + measureDim.dimensions[CSS_HEIGHT] + paddingAndBorderAxisColumn : + availableHeight - marginAxisColumn); + } + + return; + } + + // For nodes with no children, use the available values if they were provided, or + // the minimum size as indicated by the padding and border sizes. + int childCount = node->children_count; + if (childCount == 0) { + node->layout.measured_dimensions[CSS_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, + (widthMeasureMode == CSS_MEASURE_MODE_UNDEFINED || widthMeasureMode == CSS_MEASURE_MODE_AT_MOST) ? + paddingAndBorderAxisRow : + availableWidth - marginAxisRow); + node->layout.measured_dimensions[CSS_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, + (heightMeasureMode == CSS_MEASURE_MODE_UNDEFINED || heightMeasureMode == CSS_MEASURE_MODE_AT_MOST) ? + paddingAndBorderAxisColumn : + availableHeight - marginAxisColumn); + return; + } + + // If we're not being asked to perform a full layout, we can handle a number of common + // cases here without incurring the cost of the remaining function. + if (!performLayout) { + // If we're being asked to size the content with an at most constraint but there is no available width, + // the measurement will always be zero. + if (widthMeasureMode == CSS_MEASURE_MODE_AT_MOST && availableWidth <= 0 && + heightMeasureMode == CSS_MEASURE_MODE_AT_MOST && availableHeight <= 0) { + node->layout.measured_dimensions[CSS_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, 0); + node->layout.measured_dimensions[CSS_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, 0); + return; + } + + if (widthMeasureMode == CSS_MEASURE_MODE_AT_MOST && availableWidth <= 0) { + node->layout.measured_dimensions[CSS_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, 0); + node->layout.measured_dimensions[CSS_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, isUndefined(availableHeight) ? 0 : (availableHeight - marginAxisColumn)); + return; } - if (childCount == 0) { + + if (heightMeasureMode == CSS_MEASURE_MODE_AT_MOST && availableHeight <= 0) { + node->layout.measured_dimensions[CSS_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, isUndefined(availableWidth) ? 0 : (availableWidth - marginAxisRow)); + node->layout.measured_dimensions[CSS_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, 0); + return; + } + + // If we're being asked to use an exact width/height, there's no need to measure the children. + if (widthMeasureMode == CSS_MEASURE_MODE_EXACTLY && heightMeasureMode == CSS_MEASURE_MODE_EXACTLY) { + node->layout.measured_dimensions[CSS_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, availableWidth - marginAxisRow); + node->layout.measured_dimensions[CSS_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, availableHeight - marginAxisColumn); return; } } + // STEP 1: CALCULATE VALUES FOR REMAINDER OF ALGORITHM + css_flex_direction_t mainAxis = resolveAxis(getFlexDirection(node), direction); + css_flex_direction_t crossAxis = getCrossFlexDirection(mainAxis, direction); + bool isMainAxisRow = isRowDirection(mainAxis); + css_justify_t justifyContent = node->style.justify_content; bool isNodeFlexWrap = isFlexWrap(node); - css_justify_t justifyContent = node->style.justify_content; + css_node_t* firstAbsoluteChild = NULL; + css_node_t* currentAbsoluteChild = NULL; float leadingPaddingAndBorderMain = getLeadingPaddingAndBorder(node, mainAxis); + float trailingPaddingAndBorderMain = getTrailingPaddingAndBorder(node, mainAxis); float leadingPaddingAndBorderCross = getLeadingPaddingAndBorder(node, crossAxis); float paddingAndBorderAxisMain = getPaddingAndBorderAxis(node, mainAxis); float paddingAndBorderAxisCross = getPaddingAndBorderAxis(node, crossAxis); - bool isMainDimDefined = isLayoutDimDefined(node, mainAxis); - bool isCrossDimDefined = isLayoutDimDefined(node, crossAxis); - bool isMainRowDirection = isRowDirection(mainAxis); + css_measure_mode_t measureModeMainDim = isMainAxisRow ? widthMeasureMode : heightMeasureMode; + css_measure_mode_t measureModeCrossDim = isMainAxisRow ? heightMeasureMode : widthMeasureMode; - int i; - int ii; + // STEP 2: DETERMINE AVAILABLE SIZE IN MAIN AND CROSS DIRECTIONS + float availableInnerWidth = availableWidth - marginAxisRow - paddingAndBorderAxisRow; + float availableInnerHeight = availableHeight - marginAxisColumn - paddingAndBorderAxisColumn; + float availableInnerMainDim = isMainAxisRow ? availableInnerWidth : availableInnerHeight; + float availableInnerCrossDim = isMainAxisRow ? availableInnerHeight : availableInnerWidth; + + // STEP 3: DETERMINE FLEX BASIS FOR EACH ITEM css_node_t* child; - css_flex_direction_t axis; + int i; + float childWidth; + float childHeight; + css_measure_mode_t childWidthMeasureMode; + css_measure_mode_t childHeightMeasureMode; + for (i = 0; i < childCount; i++) { + child = node->get_child(node->context, i); + + if (performLayout) { + // Set the initial position (relative to the parent). + css_direction_t childDirection = resolveDirection(child, direction); + setPosition(child, childDirection); + } - css_node_t* firstAbsoluteChild = NULL; - css_node_t* currentAbsoluteChild = NULL; + // Absolute-positioned children don't participate in flex layout. Add them + // to a list that we can process later. + if (child->style.position_type == CSS_POSITION_ABSOLUTE) { - float definedMainDim = CSS_UNDEFINED; - if (isMainDimDefined) { - definedMainDim = node->layout.dimensions[dim[mainAxis]] - paddingAndBorderAxisMain; - } + // Store a private linked list of absolutely positioned children + // so that we can efficiently traverse them later. + if (firstAbsoluteChild == NULL) { + firstAbsoluteChild = child; + } + if (currentAbsoluteChild != NULL) { + currentAbsoluteChild->next_child = child; + } + currentAbsoluteChild = child; + child->next_child = NULL; + } else { - // We want to execute the next two loops one per line with flex-wrap - int startLine = 0; - int endLine = 0; - // int nextOffset = 0; - int alreadyComputedNextLayout = 0; - // We aggregate the total dimensions of the container in those two variables - float linesCrossDim = 0; - float linesMainDim = 0; - int linesCount = 0; - while (endLine < childCount) { - // Layout non flexible children and count children by type - - // mainContentDim is accumulation of the dimensions and margin of all the - // non flexible children. This will be used in order to either set the - // dimensions of the node if none already exist, or to compute the - // remaining space left for the flexible children. - float mainContentDim = 0; - - // There are three kind of children, non flexible, flexible and absolute. - // We need to know how many there are in order to distribute the space. - int flexibleChildrenCount = 0; - float totalFlexible = 0; - int nonFlexibleChildrenCount = 0; - - // Use the line loop to position children in the main axis for as long - // as they are using a simple stacking behaviour. Children that are - // immediately stacked in the initial loop will not be touched again - // in . - bool isSimpleStackMain = - (isMainDimDefined && justifyContent == CSS_JUSTIFY_FLEX_START) || - (!isMainDimDefined && justifyContent != CSS_JUSTIFY_CENTER); - int firstComplexMain = (isSimpleStackMain ? childCount : startLine); - - // Use the initial line loop to position children in the cross axis for - // as long as they are relatively positioned with alignment STRETCH or - // FLEX_START. Children that are immediately stacked in the initial loop - // will not be touched again in . - bool isSimpleStackCross = true; - int firstComplexCross = childCount; - - css_node_t* firstFlexChild = NULL; - css_node_t* currentFlexChild = NULL; - - float mainDim = leadingPaddingAndBorderMain; - float crossDim = 0; + if (isMainAxisRow && isStyleDimDefined(child, CSS_FLEX_DIRECTION_ROW)) { - float maxWidth = CSS_UNDEFINED; - float maxHeight = CSS_UNDEFINED; - for (i = startLine; i < childCount; ++i) { - child = node->get_child(node->context, i); - child->line_index = linesCount; - - child->next_absolute_child = NULL; - child->next_flex_child = NULL; - - css_align_t alignItem = getAlignItem(node, child); - - // Pre-fill cross axis dimensions when the child is using stretch before - // we call the recursive layout pass - if (alignItem == CSS_ALIGN_STRETCH && - child->style.position_type == CSS_POSITION_RELATIVE && - isCrossDimDefined && - !isStyleDimDefined(child, crossAxis)) { - child->layout.dimensions[dim[crossAxis]] = fmaxf( - boundAxis(child, crossAxis, node->layout.dimensions[dim[crossAxis]] - - paddingAndBorderAxisCross - getMarginAxis(child, crossAxis)), - // You never want to go smaller than padding - getPaddingAndBorderAxis(child, crossAxis) - ); - } else if (child->style.position_type == CSS_POSITION_ABSOLUTE) { - // Store a private linked list of absolutely positioned children - // so that we can efficiently traverse them later. - if (firstAbsoluteChild == NULL) { - firstAbsoluteChild = child; + // The width is definite, so use that as the flex basis. + child->layout.flex_basis = fmaxf(child->style.dimensions[CSS_WIDTH], getPaddingAndBorderAxis(child, CSS_FLEX_DIRECTION_ROW)); + } else if (!isMainAxisRow && isStyleDimDefined(child, CSS_FLEX_DIRECTION_COLUMN)) { + + // The height is definite, so use that as the flex basis. + child->layout.flex_basis = fmaxf(child->style.dimensions[CSS_HEIGHT], getPaddingAndBorderAxis(child, CSS_FLEX_DIRECTION_COLUMN)); + } else if (!isFlexBasisAuto(child) && !isUndefined(availableInnerMainDim)) { + + // If the basis isn't 'auto', it is assumed to be zero. + child->layout.flex_basis = fmaxf(0, getPaddingAndBorderAxis(child, mainAxis)); + } else { + + // Compute the flex basis and hypothetical main size (i.e. the clamped flex basis). + childWidth = CSS_UNDEFINED; + childHeight = CSS_UNDEFINED; + childWidthMeasureMode = CSS_MEASURE_MODE_UNDEFINED; + childHeightMeasureMode = CSS_MEASURE_MODE_UNDEFINED; + + if (isStyleDimDefined(child, CSS_FLEX_DIRECTION_ROW)) { + childWidth = child->style.dimensions[CSS_WIDTH] + getMarginAxis(child, CSS_FLEX_DIRECTION_ROW); + childWidthMeasureMode = CSS_MEASURE_MODE_EXACTLY; } - if (currentAbsoluteChild != NULL) { - currentAbsoluteChild->next_absolute_child = child; + if (isStyleDimDefined(child, CSS_FLEX_DIRECTION_COLUMN)) { + childHeight = child->style.dimensions[CSS_HEIGHT] + getMarginAxis(child, CSS_FLEX_DIRECTION_COLUMN); + childHeightMeasureMode = CSS_MEASURE_MODE_EXACTLY; } - currentAbsoluteChild = child; - - // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both - // left and right or top and bottom). - for (ii = 0; ii < 2; ii++) { - axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; - if (isLayoutDimDefined(node, axis) && - !isStyleDimDefined(child, axis) && - isPosDefined(child, leading[axis]) && - isPosDefined(child, trailing[axis])) { - child->layout.dimensions[dim[axis]] = fmaxf( - boundAxis(child, axis, node->layout.dimensions[dim[axis]] - - getPaddingAndBorderAxis(node, axis) - - getMarginAxis(child, axis) - - getPosition(child, leading[axis]) - - getPosition(child, trailing[axis])), - // You never want to go smaller than padding - getPaddingAndBorderAxis(child, axis) - ); + + // According to the spec, if the main size is not definite and the + // child's inline axis is parallel to the main axis (i.e. it's + // horizontal), the child should be sized using "UNDEFINED" in + // the main size. Otherwise use "AT_MOST" in the cross axis. + if (!isMainAxisRow && isUndefined(childWidth) && !isUndefined(availableInnerWidth)) { + childWidth = availableInnerWidth; + childWidthMeasureMode = CSS_MEASURE_MODE_AT_MOST; + } + + // The W3C spec doesn't say anything about the 'overflow' property, + // but all major browsers appear to implement the following logic. + if (node->style.overflow == CSS_OVERFLOW_HIDDEN) { + if (isMainAxisRow && isUndefined(childHeight) && !isUndefined(availableInnerHeight)) { + childHeight = availableInnerHeight; + childHeightMeasureMode = CSS_MEASURE_MODE_AT_MOST; } } + + // Measure the child + layoutNodeInternal(child, childWidth, childHeight, direction, childWidthMeasureMode, childHeightMeasureMode, false, "measure"); + + child->layout.flex_basis = fmaxf(isMainAxisRow ? child->layout.measured_dimensions[CSS_WIDTH] : child->layout.measured_dimensions[CSS_HEIGHT], getPaddingAndBorderAxis(child, mainAxis)); } + } + } - float nextContentDim = 0; + // STEP 4: COLLECT FLEX ITEMS INTO FLEX LINES - // It only makes sense to consider a child flexible if we have a computed - // dimension for the node-> - if (isMainDimDefined && isFlex(child)) { - flexibleChildrenCount++; - totalFlexible += child->style.flex; + // Indexes of children that represent the first and last items in the line. + int startOfLineIndex = 0; + int endOfLineIndex = 0; - // Store a private linked list of flexible children so that we can - // efficiently traverse them later. - if (firstFlexChild == NULL) { - firstFlexChild = child; - } - if (currentFlexChild != NULL) { - currentFlexChild->next_flex_child = child; - } - currentFlexChild = child; + // Number of lines. + int lineCount = 0; - // Even if we don't know its exact size yet, we already know the padding, - // border and margin. We'll use this partial information, which represents - // the smallest possible size for the child, to compute the remaining - // available space. - nextContentDim = getPaddingAndBorderAxis(child, mainAxis) + - getMarginAxis(child, mainAxis); + // Accumulated cross dimensions of all lines so far. + float totalLineCrossDim = 0; - } else { - maxWidth = CSS_UNDEFINED; - maxHeight = CSS_UNDEFINED; + // Max main dimension of all the lines. + float maxLineMainDim = 0; - if (!isMainRowDirection) { - if (isLayoutDimDefined(node, resolvedRowAxis)) { - maxWidth = node->layout.dimensions[dim[resolvedRowAxis]] - - paddingAndBorderAxisResolvedRow; - } else { - maxWidth = parentMaxWidth - - getMarginAxis(node, resolvedRowAxis) - - paddingAndBorderAxisResolvedRow; - } - } else { - if (isLayoutDimDefined(node, CSS_FLEX_DIRECTION_COLUMN)) { - maxHeight = node->layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] - - paddingAndBorderAxisColumn; - } else { - maxHeight = parentMaxHeight - - getMarginAxis(node, CSS_FLEX_DIRECTION_COLUMN) - - paddingAndBorderAxisColumn; - } - } + while (endOfLineIndex < childCount) { - // This is the main recursive call. We layout non flexible children. - if (alreadyComputedNextLayout == 0) { - layoutNode(child, maxWidth, maxHeight, direction); - } + // Number of items on the currently line. May be different than the difference + // between start and end indicates because we skip over absolute-positioned items. + int itemsOnLine = 0; - // Absolute positioned elements do not take part of the layout, so we - // don't use them to compute mainContentDim - if (child->style.position_type == CSS_POSITION_RELATIVE) { - nonFlexibleChildrenCount++; - // At this point we know the final size and margin of the element. - nextContentDim = getDimWithMargin(child, mainAxis); - } - } + // sizeConsumedOnCurrentLine is accumulation of the dimensions and margin + // of all the children on the current line. This will be used in order to + // either set the dimensions of the node if none already exist or to compute + // the remaining space left for the flexible children. + float sizeConsumedOnCurrentLine = 0; - // The element we are about to add would make us go to the next line - if (isNodeFlexWrap && - isMainDimDefined && - mainContentDim + nextContentDim > definedMainDim && - // If there's only one element, then it's bigger than the content - // and needs its own line - i != startLine) { - nonFlexibleChildrenCount--; - alreadyComputedNextLayout = 1; - break; - } + float totalFlexGrowFactors = 0; + float totalFlexShrinkScaledFactors = 0; - // Disable simple stacking in the main axis for the current line as - // we found a non-trivial child-> The remaining children will be laid out - // in . - if (isSimpleStackMain && - (child->style.position_type != CSS_POSITION_RELATIVE || isFlex(child))) { - isSimpleStackMain = false; - firstComplexMain = i; - } + i = startOfLineIndex; - // Disable simple stacking in the cross axis for the current line as - // we found a non-trivial child-> The remaining children will be laid out - // in . - if (isSimpleStackCross && - (child->style.position_type != CSS_POSITION_RELATIVE || - (alignItem != CSS_ALIGN_STRETCH && alignItem != CSS_ALIGN_FLEX_START) || - (alignItem == CSS_ALIGN_STRETCH && !isCrossDimDefined))) { - isSimpleStackCross = false; - firstComplexCross = i; - } + // Maintain a linked list of the child nodes that can shrink and/or grow. + css_node_t* firstRelativeChild = NULL; + css_node_t* currentRelativeChild = NULL; - if (isSimpleStackMain) { - child->layout.position[pos[mainAxis]] += mainDim; - if (isMainDimDefined) { - setTrailingPosition(node, child, mainAxis); + // Add items to the current line until it's full or we run out of items. + while (i < childCount) { + child = node->get_child(node->context, i); + child->line_index = lineCount; + + if (child->style.position_type != CSS_POSITION_ABSOLUTE) { + float outerFlexBasis = child->layout.flex_basis + getMarginAxis(child, mainAxis); + + // If this is a multi-line flow and this item pushes us over the available size, we've + // hit the end of the current line. Break out of the loop and lay out the current line. + if (sizeConsumedOnCurrentLine + outerFlexBasis > availableInnerMainDim && isNodeFlexWrap && itemsOnLine > 0) { + break; } - mainDim += getDimWithMargin(child, mainAxis); - crossDim = fmaxf(crossDim, boundAxis(child, crossAxis, getDimWithMargin(child, crossAxis))); - } + sizeConsumedOnCurrentLine += outerFlexBasis; + itemsOnLine++; - if (isSimpleStackCross) { - child->layout.position[pos[crossAxis]] += linesCrossDim + leadingPaddingAndBorderCross; - if (isCrossDimDefined) { - setTrailingPosition(node, child, crossAxis); + if (isFlex(child)) { + totalFlexGrowFactors += getFlexGrowFactor(child); + + // Unlike the grow factor, the shrink factor is scaled relative to the child + // dimension. + totalFlexShrinkScaledFactors += getFlexShrinkFactor(child) * child->layout.flex_basis; + } + + // Store a private linked list of children that need to be layed out. + if (firstRelativeChild == NULL) { + firstRelativeChild = child; } + if (currentRelativeChild != NULL) { + currentRelativeChild->next_child = child; + } + currentRelativeChild = child; + child->next_child = NULL; } - alreadyComputedNextLayout = 0; - mainContentDim += nextContentDim; - endLine = i + 1; + i++; + endOfLineIndex++; } - // Layout flexible children and allocate empty space + // If we don't need to measure the cross axis, we can skip the entire flex step. + bool canSkipFlex = !performLayout && measureModeCrossDim == CSS_MEASURE_MODE_EXACTLY; // In order to position the elements in the main axis, we have two // controls. The space between the beginning and the first element @@ -887,212 +964,300 @@ float leadingMainDim = 0; float betweenMainDim = 0; - // The remaining available space that needs to be allocated - float remainingMainDim = 0; - if (isMainDimDefined) { - remainingMainDim = definedMainDim - mainContentDim; - } else { - remainingMainDim = fmaxf(mainContentDim, 0) - mainContentDim; + // STEP 5: RESOLVING FLEXIBLE LENGTHS ON MAIN AXIS + // Calculate the remaining available space that needs to be allocated. + // If the main dimension size isn't known, it is computed based on + // the line length, so there's no more space left to distribute. + float remainingFreeSpace = 0; + if (!isUndefined(availableInnerMainDim)) { + remainingFreeSpace = availableInnerMainDim - sizeConsumedOnCurrentLine; + } else if (sizeConsumedOnCurrentLine < 0) { + // availableInnerMainDim is indefinite which means the node is being sized based on its content. + // sizeConsumedOnCurrentLine is negative which means the node will allocate 0 pixels for + // its content. Consequently, remainingFreeSpace is 0 - sizeConsumedOnCurrentLine. + remainingFreeSpace = -sizeConsumedOnCurrentLine; } - // If there are flexible children in the mix, they are going to fill the - // remaining space - if (flexibleChildrenCount != 0) { - float flexibleMainDim = remainingMainDim / totalFlexible; - float baseMainDim; - float boundMainDim; - - // If the flex share of remaining space doesn't meet min/max bounds, - // remove this child from flex calculations. - currentFlexChild = firstFlexChild; - while (currentFlexChild != NULL) { - baseMainDim = flexibleMainDim * currentFlexChild->style.flex + - getPaddingAndBorderAxis(currentFlexChild, mainAxis); - boundMainDim = boundAxis(currentFlexChild, mainAxis, baseMainDim); - - if (baseMainDim != boundMainDim) { - remainingMainDim -= boundMainDim; - totalFlexible -= currentFlexChild->style.flex; + float originalRemainingFreeSpace = remainingFreeSpace; + float deltaFreeSpace = 0; + + if (!canSkipFlex) { + float childFlexBasis; + float flexShrinkScaledFactor; + float flexGrowFactor; + float baseMainSize; + float boundMainSize; + + // Do two passes over the flex items to figure out how to distribute the remaining space. + // The first pass finds the items whose min/max constraints trigger, freezes them at those + // sizes, and excludes those sizes from the remaining space. The second pass sets the size + // of each flexible item. It distributes the remaining space amongst the items whose min/max + // constraints didn't trigger in pass 1. For the other items, it sets their sizes by forcing + // their min/max constraints to trigger again. + // + // This two pass approach for resolving min/max constraints deviates from the spec. The + // spec (https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths) describes a process + // that needs to be repeated a variable number of times. The algorithm implemented here + // won't handle all cases but it was simpler to implement and it mitigates performance + // concerns because we know exactly how many passes it'll do. + + // First pass: detect the flex items whose min/max constraints trigger + float deltaFlexShrinkScaledFactors = 0; + float deltaFlexGrowFactors = 0; + currentRelativeChild = firstRelativeChild; + while (currentRelativeChild != NULL) { + childFlexBasis = currentRelativeChild->layout.flex_basis; + + if (remainingFreeSpace < 0) { + flexShrinkScaledFactor = getFlexShrinkFactor(currentRelativeChild) * childFlexBasis; + + // Is this child able to shrink? + if (flexShrinkScaledFactor != 0) { + baseMainSize = childFlexBasis + + remainingFreeSpace / totalFlexShrinkScaledFactors * flexShrinkScaledFactor; + boundMainSize = boundAxis(currentRelativeChild, mainAxis, baseMainSize); + if (baseMainSize != boundMainSize) { + // By excluding this item's size and flex factor from remaining, this item's + // min/max constraints should also trigger in the second pass resulting in the + // item's size calculation being identical in the first and second passes. + deltaFreeSpace -= boundMainSize - childFlexBasis; + deltaFlexShrinkScaledFactors -= flexShrinkScaledFactor; + } + } + } else if (remainingFreeSpace > 0) { + flexGrowFactor = getFlexGrowFactor(currentRelativeChild); + + // Is this child able to grow? + if (flexGrowFactor != 0) { + baseMainSize = childFlexBasis + + remainingFreeSpace / totalFlexGrowFactors * flexGrowFactor; + boundMainSize = boundAxis(currentRelativeChild, mainAxis, baseMainSize); + if (baseMainSize != boundMainSize) { + // By excluding this item's size and flex factor from remaining, this item's + // min/max constraints should also trigger in the second pass resulting in the + // item's size calculation being identical in the first and second passes. + deltaFreeSpace -= boundMainSize - childFlexBasis; + deltaFlexGrowFactors -= flexGrowFactor; + } + } } - currentFlexChild = currentFlexChild->next_flex_child; + currentRelativeChild = currentRelativeChild->next_child; } - flexibleMainDim = remainingMainDim / totalFlexible; - // The non flexible children can overflow the container, in this case - // we should just assume that there is no space available. - if (flexibleMainDim < 0) { - flexibleMainDim = 0; - } + totalFlexShrinkScaledFactors += deltaFlexShrinkScaledFactors; + totalFlexGrowFactors += deltaFlexGrowFactors; + remainingFreeSpace += deltaFreeSpace; + + // Second pass: resolve the sizes of the flexible items + deltaFreeSpace = 0; + currentRelativeChild = firstRelativeChild; + while (currentRelativeChild != NULL) { + childFlexBasis = currentRelativeChild->layout.flex_basis; + float updatedMainSize = childFlexBasis; + + if (remainingFreeSpace < 0) { + flexShrinkScaledFactor = getFlexShrinkFactor(currentRelativeChild) * childFlexBasis; + + // Is this child able to shrink? + if (flexShrinkScaledFactor != 0) { + updatedMainSize = boundAxis(currentRelativeChild, mainAxis, childFlexBasis + + remainingFreeSpace / totalFlexShrinkScaledFactors * flexShrinkScaledFactor); + } + } else if (remainingFreeSpace > 0) { + flexGrowFactor = getFlexGrowFactor(currentRelativeChild); - currentFlexChild = firstFlexChild; - while (currentFlexChild != NULL) { - // At this point we know the final size of the element in the main - // dimension - currentFlexChild->layout.dimensions[dim[mainAxis]] = boundAxis(currentFlexChild, mainAxis, - flexibleMainDim * currentFlexChild->style.flex + - getPaddingAndBorderAxis(currentFlexChild, mainAxis) - ); - - maxWidth = CSS_UNDEFINED; - if (isLayoutDimDefined(node, resolvedRowAxis)) { - maxWidth = node->layout.dimensions[dim[resolvedRowAxis]] - - paddingAndBorderAxisResolvedRow; - } else if (!isMainRowDirection) { - maxWidth = parentMaxWidth - - getMarginAxis(node, resolvedRowAxis) - - paddingAndBorderAxisResolvedRow; + // Is this child able to grow? + if (flexGrowFactor != 0) { + updatedMainSize = boundAxis(currentRelativeChild, mainAxis, childFlexBasis + + remainingFreeSpace / totalFlexGrowFactors * flexGrowFactor); + } } - maxHeight = CSS_UNDEFINED; - if (isLayoutDimDefined(node, CSS_FLEX_DIRECTION_COLUMN)) { - maxHeight = node->layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] - - paddingAndBorderAxisColumn; - } else if (isMainRowDirection) { - maxHeight = parentMaxHeight - - getMarginAxis(node, CSS_FLEX_DIRECTION_COLUMN) - - paddingAndBorderAxisColumn; + + deltaFreeSpace -= updatedMainSize - childFlexBasis; + + if (isMainAxisRow) { + childWidth = updatedMainSize + getMarginAxis(currentRelativeChild, CSS_FLEX_DIRECTION_ROW); + childWidthMeasureMode = CSS_MEASURE_MODE_EXACTLY; + + if (!isStyleDimDefined(currentRelativeChild, CSS_FLEX_DIRECTION_COLUMN)) { + childHeight = availableInnerCrossDim; + childHeightMeasureMode = isUndefined(childHeight) ? CSS_MEASURE_MODE_UNDEFINED : CSS_MEASURE_MODE_AT_MOST; + } else { + childHeight = currentRelativeChild->style.dimensions[CSS_HEIGHT] + getMarginAxis(currentRelativeChild, CSS_FLEX_DIRECTION_COLUMN); + childHeightMeasureMode = CSS_MEASURE_MODE_EXACTLY; + } + } else { + childHeight = updatedMainSize + getMarginAxis(currentRelativeChild, CSS_FLEX_DIRECTION_COLUMN); + childHeightMeasureMode = CSS_MEASURE_MODE_EXACTLY; + + if (!isStyleDimDefined(currentRelativeChild, CSS_FLEX_DIRECTION_ROW)) { + childWidth = availableInnerCrossDim; + childWidthMeasureMode = isUndefined(childWidth) ? CSS_MEASURE_MODE_UNDEFINED : CSS_MEASURE_MODE_AT_MOST; + } else { + childWidth = currentRelativeChild->style.dimensions[CSS_WIDTH] + getMarginAxis(currentRelativeChild, CSS_FLEX_DIRECTION_ROW); + childWidthMeasureMode = CSS_MEASURE_MODE_EXACTLY; + } } - // And we recursively call the layout algorithm for this child - layoutNode(currentFlexChild, maxWidth, maxHeight, direction); + bool requiresStretchLayout = !isStyleDimDefined(currentRelativeChild, crossAxis) && + getAlignItem(node, currentRelativeChild) == CSS_ALIGN_STRETCH; + + // Recursively call the layout algorithm for this child with the updated main size. + layoutNodeInternal(currentRelativeChild, childWidth, childHeight, direction, childWidthMeasureMode, childHeightMeasureMode, performLayout && !requiresStretchLayout, "flex"); - child = currentFlexChild; - currentFlexChild = currentFlexChild->next_flex_child; - child->next_flex_child = NULL; + currentRelativeChild = currentRelativeChild->next_child; } + } + + remainingFreeSpace = originalRemainingFreeSpace + deltaFreeSpace; - // We use justifyContent to figure out how to allocate the remaining - // space available - } else if (justifyContent != CSS_JUSTIFY_FLEX_START) { + // STEP 6: MAIN-AXIS JUSTIFICATION & CROSS-AXIS SIZE DETERMINATION + + // At this point, all the children have their dimensions set in the main axis. + // Their dimensions are also set in the cross axis with the exception of items + // that are aligned "stretch". We need to compute these stretch values and + // set the final positions. + + // If we are using "at most" rules in the main axis, we won't distribute + // any remaining space at this point. + if (measureModeMainDim == CSS_MEASURE_MODE_AT_MOST) { + remainingFreeSpace = 0; + } + + // Use justifyContent to figure out how to allocate the remaining space + // available in the main axis. + if (justifyContent != CSS_JUSTIFY_FLEX_START) { if (justifyContent == CSS_JUSTIFY_CENTER) { - leadingMainDim = remainingMainDim / 2; + leadingMainDim = remainingFreeSpace / 2; } else if (justifyContent == CSS_JUSTIFY_FLEX_END) { - leadingMainDim = remainingMainDim; + leadingMainDim = remainingFreeSpace; } else if (justifyContent == CSS_JUSTIFY_SPACE_BETWEEN) { - remainingMainDim = fmaxf(remainingMainDim, 0); - if (flexibleChildrenCount + nonFlexibleChildrenCount - 1 != 0) { - betweenMainDim = remainingMainDim / - (flexibleChildrenCount + nonFlexibleChildrenCount - 1); + remainingFreeSpace = fmaxf(remainingFreeSpace, 0); + if (itemsOnLine > 1) { + betweenMainDim = remainingFreeSpace / (itemsOnLine - 1); } else { betweenMainDim = 0; } } else if (justifyContent == CSS_JUSTIFY_SPACE_AROUND) { // Space on the edges is half of the space between elements - betweenMainDim = remainingMainDim / - (flexibleChildrenCount + nonFlexibleChildrenCount); + betweenMainDim = remainingFreeSpace / itemsOnLine; leadingMainDim = betweenMainDim / 2; } } - // Position elements in the main axis and compute dimensions - - // At this point, all the children have their dimensions set. We need to - // find their position. In order to do that, we accumulate data in - // variables that are also useful to compute the total dimensions of the - // container! - mainDim += leadingMainDim; + float mainDim = leadingPaddingAndBorderMain + leadingMainDim; + float crossDim = 0; - for (i = firstComplexMain; i < endLine; ++i) { + for (i = startOfLineIndex; i < endOfLineIndex; ++i) { child = node->get_child(node->context, i); if (child->style.position_type == CSS_POSITION_ABSOLUTE && isPosDefined(child, leading[mainAxis])) { - // In case the child is position absolute and has left/top being - // defined, we override the position to whatever the user said - // (and margin/border). - child->layout.position[pos[mainAxis]] = getPosition(child, leading[mainAxis]) + - getLeadingBorder(node, mainAxis) + - getLeadingMargin(child, mainAxis); + if (performLayout) { + // In case the child is position absolute and has left/top being + // defined, we override the position to whatever the user said + // (and margin/border). + child->layout.position[pos[mainAxis]] = getPosition(child, leading[mainAxis]) + + getLeadingBorder(node, mainAxis) + + getLeadingMargin(child, mainAxis); + } } else { - // If the child is position absolute (without top/left) or relative, - // we put it at the current accumulated offset. - child->layout.position[pos[mainAxis]] += mainDim; - - // Define the trailing position accordingly. - if (isMainDimDefined) { - setTrailingPosition(node, child, mainAxis); + if (performLayout) { + // If the child is position absolute (without top/left) or relative, + // we put it at the current accumulated offset. + child->layout.position[pos[mainAxis]] += mainDim; } - // Now that we placed the element, we need to update the variables - // We only need to do that for relative elements. Absolute elements + // Now that we placed the element, we need to update the variables. + // We need to do that only for relative elements. Absolute elements // do not take part in that phase. if (child->style.position_type == CSS_POSITION_RELATIVE) { - // The main dimension is the sum of all the elements dimension plus - // the spacing. - mainDim += betweenMainDim + getDimWithMargin(child, mainAxis); - // The cross dimension is the max of the elements dimension since there - // can only be one element in that cross dimension. - crossDim = fmaxf(crossDim, boundAxis(child, crossAxis, getDimWithMargin(child, crossAxis))); + if (canSkipFlex) { + // If we skipped the flex step, then we can't rely on the measuredDims because + // they weren't computed. This means we can't call getDimWithMargin. + mainDim += betweenMainDim + getMarginAxis(child, mainAxis) + child->layout.flex_basis; + crossDim = availableInnerCrossDim; + } else { + // The main dimension is the sum of all the elements dimension plus + // the spacing. + mainDim += betweenMainDim + getDimWithMargin(child, mainAxis); + + // The cross dimension is the max of the elements dimension since there + // can only be one element in that cross dimension. + crossDim = fmaxf(crossDim, getDimWithMargin(child, crossAxis)); + } } } } - float containerCrossAxis = node->layout.dimensions[dim[crossAxis]]; - if (!isCrossDimDefined) { - containerCrossAxis = fmaxf( - // For the cross dim, we add both sides at the end because the value - // is aggregate via a max function. Intermediate negative values - // can mess this computation otherwise - boundAxis(node, crossAxis, crossDim + paddingAndBorderAxisCross), - paddingAndBorderAxisCross - ); - } + mainDim += trailingPaddingAndBorderMain; - // Position elements in the cross axis - for (i = firstComplexCross; i < endLine; ++i) { - child = node->get_child(node->context, i); + float containerCrossAxis = availableInnerCrossDim; + if (measureModeCrossDim == CSS_MEASURE_MODE_UNDEFINED || measureModeCrossDim == CSS_MEASURE_MODE_AT_MOST) { + // Compute the cross axis from the max cross dimension of the children. + containerCrossAxis = boundAxis(node, crossAxis, crossDim + paddingAndBorderAxisCross) - paddingAndBorderAxisCross; - if (child->style.position_type == CSS_POSITION_ABSOLUTE && - isPosDefined(child, leading[crossAxis])) { - // In case the child is absolutely positionned and has a - // top/left/bottom/right being set, we override all the previously - // computed positions to set it correctly. - child->layout.position[pos[crossAxis]] = getPosition(child, leading[crossAxis]) + - getLeadingBorder(node, crossAxis) + - getLeadingMargin(child, crossAxis); + if (measureModeCrossDim == CSS_MEASURE_MODE_AT_MOST) { + containerCrossAxis = fminf(containerCrossAxis, availableInnerCrossDim); + } + } - } else { - float leadingCrossDim = leadingPaddingAndBorderCross; + // If there's no flex wrap, the cross dimension is defined by the container. + if (!isNodeFlexWrap && measureModeCrossDim == CSS_MEASURE_MODE_EXACTLY) { + crossDim = availableInnerCrossDim; + } - // For a relative children, we're either using alignItems (parent) or - // alignSelf (child) in order to determine the position in the cross axis - if (child->style.position_type == CSS_POSITION_RELATIVE) { - /*eslint-disable */ - // This variable is intentionally re-defined as the code is transpiled to a block scope language + // Clamp to the min/max size specified on the container. + crossDim = boundAxis(node, crossAxis, crossDim + paddingAndBorderAxisCross) - paddingAndBorderAxisCross; + + // STEP 7: CROSS-AXIS ALIGNMENT + // We can skip child alignment if we're just measuring the container. + if (performLayout) { + for (i = startOfLineIndex; i < endOfLineIndex; ++i) { + child = node->get_child(node->context, i); + + if (child->style.position_type == CSS_POSITION_ABSOLUTE) { + // If the child is absolutely positioned and has a top/left/bottom/right + // set, override all the previously computed positions to set it correctly. + if (isPosDefined(child, leading[crossAxis])) { + child->layout.position[pos[crossAxis]] = getPosition(child, leading[crossAxis]) + + getLeadingBorder(node, crossAxis) + + getLeadingMargin(child, crossAxis); + } else { + child->layout.position[pos[crossAxis]] = leadingPaddingAndBorderCross + + getLeadingMargin(child, crossAxis); + } + } else { + float leadingCrossDim = leadingPaddingAndBorderCross; + + // For a relative children, we're either using alignItems (parent) or + // alignSelf (child) in order to determine the position in the cross axis css_align_t alignItem = getAlignItem(node, child); - /*eslint-enable */ + + // If the child uses align stretch, we need to lay it out one more time, this time + // forcing the cross-axis size to be the computed cross size for the current line. if (alignItem == CSS_ALIGN_STRETCH) { - // You can only stretch if the dimension has not already been defined - // previously. - if (!isStyleDimDefined(child, crossAxis)) { - float dimCrossAxis = child->layout.dimensions[dim[crossAxis]]; - child->layout.dimensions[dim[crossAxis]] = fmaxf( - boundAxis(child, crossAxis, containerCrossAxis - - paddingAndBorderAxisCross - getMarginAxis(child, crossAxis)), - // You never want to go smaller than padding - getPaddingAndBorderAxis(child, crossAxis) - ); - - // If the size has changed, and this child has children we need to re-layout this child - if (dimCrossAxis != child->layout.dimensions[dim[crossAxis]] && child->children_count > 0) { - // Reset child margins before re-layout as they are added back in layoutNode and would be doubled - child->layout.position[leading[mainAxis]] -= getLeadingMargin(child, mainAxis) + - getRelativePosition(child, mainAxis); - child->layout.position[trailing[mainAxis]] -= getTrailingMargin(child, mainAxis) + - getRelativePosition(child, mainAxis); - child->layout.position[leading[crossAxis]] -= getLeadingMargin(child, crossAxis) + - getRelativePosition(child, crossAxis); - child->layout.position[trailing[crossAxis]] -= getTrailingMargin(child, crossAxis) + - getRelativePosition(child, crossAxis); - - layoutNode(child, maxWidth, maxHeight, direction); - } + childWidth = child->layout.measured_dimensions[CSS_WIDTH] + getMarginAxis(child, CSS_FLEX_DIRECTION_ROW); + childHeight = child->layout.measured_dimensions[CSS_HEIGHT] + getMarginAxis(child, CSS_FLEX_DIRECTION_COLUMN); + bool isCrossSizeDefinite = false; + + if (isMainAxisRow) { + isCrossSizeDefinite = isStyleDimDefined(child, CSS_FLEX_DIRECTION_COLUMN); + childHeight = crossDim; + } else { + isCrossSizeDefinite = isStyleDimDefined(child, CSS_FLEX_DIRECTION_ROW); + childWidth = crossDim; + } + + // If the child defines a definite size for its cross axis, there's no need to stretch. + if (!isCrossSizeDefinite) { + childWidthMeasureMode = isUndefined(childWidth) ? CSS_MEASURE_MODE_UNDEFINED : CSS_MEASURE_MODE_EXACTLY; + childHeightMeasureMode = isUndefined(childHeight) ? CSS_MEASURE_MODE_UNDEFINED : CSS_MEASURE_MODE_EXACTLY; + layoutNodeInternal(child, childWidth, childHeight, direction, childWidthMeasureMode, childHeightMeasureMode, true, "stretch"); } } else if (alignItem != CSS_ALIGN_FLEX_START) { - // The remaining space between the parent dimensions+padding and child - // dimensions+margin. - float remainingCrossDim = containerCrossAxis - - paddingAndBorderAxisCross - getDimWithMargin(child, crossAxis); + float remainingCrossDim = containerCrossAxis - getDimWithMargin(child, crossAxis); if (alignItem == CSS_ALIGN_CENTER) { leadingCrossDim += remainingCrossDim / 2; @@ -1100,41 +1265,25 @@ leadingCrossDim += remainingCrossDim; } } - } - - // And we apply the position - child->layout.position[pos[crossAxis]] += linesCrossDim + leadingCrossDim; - // Define the trailing position accordingly. - if (isCrossDimDefined) { - setTrailingPosition(node, child, crossAxis); + // And we apply the position + child->layout.position[pos[crossAxis]] += totalLineCrossDim + leadingCrossDim; } } } - linesCrossDim += crossDim; - linesMainDim = fmaxf(linesMainDim, mainDim); - linesCount += 1; - startLine = endLine; + totalLineCrossDim += crossDim; + maxLineMainDim = fmaxf(maxLineMainDim, mainDim); + + // Reset variables for new line. + lineCount++; + startOfLineIndex = endOfLineIndex; + endOfLineIndex = startOfLineIndex; } - // - // - // Note(prenaux): More than one line, we need to layout the crossAxis - // according to alignContent. - // - // Note that we could probably remove and handle the one line case - // here too, but for the moment this is safer since it won't interfere with - // previously working code. - // - // See specs: - // http://www.w3.org/TR/2012/CR-css3-flexbox-20120918/#layout-algorithm - // section 9.4 - // - if (linesCount > 1 && isCrossDimDefined) { - float nodeCrossAxisInnerSize = node->layout.dimensions[dim[crossAxis]] - - paddingAndBorderAxisCross; - float remainingAlignContentDim = nodeCrossAxisInnerSize - linesCrossDim; + // STEP 8: MULTI-LINE CONTENT ALIGNMENT + if (lineCount > 1 && performLayout && !isUndefined(availableInnerCrossDim)) { + float remainingAlignContentDim = availableInnerCrossDim - totalLineCrossDim; float crossDimLead = 0; float currentLead = leadingPaddingAndBorderCross; @@ -1145,19 +1294,20 @@ } else if (alignContent == CSS_ALIGN_CENTER) { currentLead += remainingAlignContentDim / 2; } else if (alignContent == CSS_ALIGN_STRETCH) { - if (nodeCrossAxisInnerSize > linesCrossDim) { - crossDimLead = (remainingAlignContentDim / linesCount); + if (availableInnerCrossDim > totalLineCrossDim) { + crossDimLead = (remainingAlignContentDim / lineCount); } } int endIndex = 0; - for (i = 0; i < linesCount; ++i) { + for (i = 0; i < lineCount; ++i) { int startIndex = endIndex; + int j; // compute the line's height and find the endIndex float lineHeight = 0; - for (ii = startIndex; ii < childCount; ++ii) { - child = node->get_child(node->context, ii); + for (j = startIndex; j < childCount; ++j) { + child = node->get_child(node->context, j); if (child->style.position_type != CSS_POSITION_RELATIVE) { continue; } @@ -1165,33 +1315,33 @@ break; } if (isLayoutDimDefined(child, crossAxis)) { - lineHeight = fmaxf( - lineHeight, - child->layout.dimensions[dim[crossAxis]] + getMarginAxis(child, crossAxis) - ); + lineHeight = fmaxf(lineHeight, + child->layout.measured_dimensions[dim[crossAxis]] + getMarginAxis(child, crossAxis)); } } - endIndex = ii; + endIndex = j; lineHeight += crossDimLead; - for (ii = startIndex; ii < endIndex; ++ii) { - child = node->get_child(node->context, ii); - if (child->style.position_type != CSS_POSITION_RELATIVE) { - continue; - } + if (performLayout) { + for (j = startIndex; j < endIndex; ++j) { + child = node->get_child(node->context, j); + if (child->style.position_type != CSS_POSITION_RELATIVE) { + continue; + } - css_align_t alignContentAlignItem = getAlignItem(node, child); - if (alignContentAlignItem == CSS_ALIGN_FLEX_START) { - child->layout.position[pos[crossAxis]] = currentLead + getLeadingMargin(child, crossAxis); - } else if (alignContentAlignItem == CSS_ALIGN_FLEX_END) { - child->layout.position[pos[crossAxis]] = currentLead + lineHeight - getTrailingMargin(child, crossAxis) - child->layout.dimensions[dim[crossAxis]]; - } else if (alignContentAlignItem == CSS_ALIGN_CENTER) { - float childHeight = child->layout.dimensions[dim[crossAxis]]; - child->layout.position[pos[crossAxis]] = currentLead + (lineHeight - childHeight) / 2; - } else if (alignContentAlignItem == CSS_ALIGN_STRETCH) { - child->layout.position[pos[crossAxis]] = currentLead + getLeadingMargin(child, crossAxis); - // TODO(prenaux): Correctly set the height of items with undefined - // (auto) crossAxis dimension. + css_align_t alignContentAlignItem = getAlignItem(node, child); + if (alignContentAlignItem == CSS_ALIGN_FLEX_START) { + child->layout.position[pos[crossAxis]] = currentLead + getLeadingMargin(child, crossAxis); + } else if (alignContentAlignItem == CSS_ALIGN_FLEX_END) { + child->layout.position[pos[crossAxis]] = currentLead + lineHeight - getTrailingMargin(child, crossAxis) - child->layout.measured_dimensions[dim[crossAxis]]; + } else if (alignContentAlignItem == CSS_ALIGN_CENTER) { + childHeight = child->layout.measured_dimensions[dim[crossAxis]]; + child->layout.position[pos[crossAxis]] = currentLead + (lineHeight - childHeight) / 2; + } else if (alignContentAlignItem == CSS_ALIGN_STRETCH) { + child->layout.position[pos[crossAxis]] = currentLead + getLeadingMargin(child, crossAxis); + // TODO(prenaux): Correctly set the height of items with indefinite + // (auto) crossAxis dimension. + } } } @@ -1199,137 +1349,394 @@ } } - bool needsMainTrailingPos = false; - bool needsCrossTrailingPos = false; + // STEP 9: COMPUTING FINAL DIMENSIONS + node->layout.measured_dimensions[CSS_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, availableWidth - marginAxisRow); + node->layout.measured_dimensions[CSS_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, availableHeight - marginAxisColumn); + + // If the user didn't specify a width or height for the node, set the + // dimensions based on the children. + if (measureModeMainDim == CSS_MEASURE_MODE_UNDEFINED) { + // Clamp the size to the min/max size, if specified, and make sure it + // doesn't go below the padding and border amount. + node->layout.measured_dimensions[dim[mainAxis]] = boundAxis(node, mainAxis, maxLineMainDim); + } else if (measureModeMainDim == CSS_MEASURE_MODE_AT_MOST) { + node->layout.measured_dimensions[dim[mainAxis]] = fmaxf( + fminf(availableInnerMainDim + paddingAndBorderAxisMain, + boundAxisWithinMinAndMax(node, mainAxis, maxLineMainDim)), + paddingAndBorderAxisMain); + } - // If the user didn't specify a width or height, and it has not been set - // by the container, then we set it via the children. - if (!isMainDimDefined) { - node->layout.dimensions[dim[mainAxis]] = fmaxf( - // We're missing the last padding at this point to get the final - // dimension - boundAxis(node, mainAxis, linesMainDim + getTrailingPaddingAndBorder(node, mainAxis)), - // We can never assign a width smaller than the padding and borders - paddingAndBorderAxisMain - ); + if (measureModeCrossDim == CSS_MEASURE_MODE_UNDEFINED) { + // Clamp the size to the min/max size, if specified, and make sure it + // doesn't go below the padding and border amount. + node->layout.measured_dimensions[dim[crossAxis]] = boundAxis(node, crossAxis, totalLineCrossDim + paddingAndBorderAxisCross); + } else if (measureModeCrossDim == CSS_MEASURE_MODE_AT_MOST) { + node->layout.measured_dimensions[dim[crossAxis]] = fmaxf( + fminf(availableInnerCrossDim + paddingAndBorderAxisCross, + boundAxisWithinMinAndMax(node, crossAxis, totalLineCrossDim + paddingAndBorderAxisCross)), + paddingAndBorderAxisCross); + } + + // STEP 10: SETTING TRAILING POSITIONS FOR CHILDREN + if (performLayout) { + bool needsMainTrailingPos = false; + bool needsCrossTrailingPos = false; if (mainAxis == CSS_FLEX_DIRECTION_ROW_REVERSE || mainAxis == CSS_FLEX_DIRECTION_COLUMN_REVERSE) { needsMainTrailingPos = true; } - } - - if (!isCrossDimDefined) { - node->layout.dimensions[dim[crossAxis]] = fmaxf( - // For the cross dim, we add both sides at the end because the value - // is aggregate via a max function. Intermediate negative values - // can mess this computation otherwise - boundAxis(node, crossAxis, linesCrossDim + paddingAndBorderAxisCross), - paddingAndBorderAxisCross - ); if (crossAxis == CSS_FLEX_DIRECTION_ROW_REVERSE || crossAxis == CSS_FLEX_DIRECTION_COLUMN_REVERSE) { needsCrossTrailingPos = true; } - } - // Set trailing position if necessary - if (needsMainTrailingPos || needsCrossTrailingPos) { - for (i = 0; i < childCount; ++i) { - child = node->get_child(node->context, i); + // Set trailing position if necessary. + if (needsMainTrailingPos || needsCrossTrailingPos) { + for (i = 0; i < childCount; ++i) { + child = node->get_child(node->context, i); - if (needsMainTrailingPos) { - setTrailingPosition(node, child, mainAxis); - } + if (needsMainTrailingPos) { + setTrailingPosition(node, child, mainAxis); + } - if (needsCrossTrailingPos) { - setTrailingPosition(node, child, crossAxis); + if (needsCrossTrailingPos) { + setTrailingPosition(node, child, crossAxis); + } } } } - // Calculate dimensions for absolutely positioned elements + // STEP 11: SIZING AND POSITIONING ABSOLUTE CHILDREN currentAbsoluteChild = firstAbsoluteChild; while (currentAbsoluteChild != NULL) { - // Pre-fill dimensions when using absolute position and both offsets for - // the axis are defined (either both left and right or top and bottom). - for (ii = 0; ii < 2; ii++) { - axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; - - if (isLayoutDimDefined(node, axis) && - !isStyleDimDefined(currentAbsoluteChild, axis) && - isPosDefined(currentAbsoluteChild, leading[axis]) && - isPosDefined(currentAbsoluteChild, trailing[axis])) { - currentAbsoluteChild->layout.dimensions[dim[axis]] = fmaxf( - boundAxis(currentAbsoluteChild, axis, node->layout.dimensions[dim[axis]] - - getBorderAxis(node, axis) - - getMarginAxis(currentAbsoluteChild, axis) - - getPosition(currentAbsoluteChild, leading[axis]) - - getPosition(currentAbsoluteChild, trailing[axis]) - ), - // You never want to go smaller than padding - getPaddingAndBorderAxis(currentAbsoluteChild, axis) - ); + // Now that we know the bounds of the container, perform layout again on the + // absolutely-positioned children. + if (performLayout) { + + childWidth = CSS_UNDEFINED; + childHeight = CSS_UNDEFINED; + + if (isStyleDimDefined(currentAbsoluteChild, CSS_FLEX_DIRECTION_ROW)) { + childWidth = currentAbsoluteChild->style.dimensions[CSS_WIDTH] + getMarginAxis(currentAbsoluteChild, CSS_FLEX_DIRECTION_ROW); + } else { + // If the child doesn't have a specified width, compute the width based on the left/right offsets if they're defined. + if (isPosDefined(currentAbsoluteChild, CSS_LEFT) && isPosDefined(currentAbsoluteChild, CSS_RIGHT)) { + childWidth = node->layout.measured_dimensions[CSS_WIDTH] - + (getLeadingBorder(node, CSS_FLEX_DIRECTION_ROW) + getTrailingBorder(node, CSS_FLEX_DIRECTION_ROW)) - + (currentAbsoluteChild->style.position[CSS_LEFT] + currentAbsoluteChild->style.position[CSS_RIGHT]); + childWidth = boundAxis(currentAbsoluteChild, CSS_FLEX_DIRECTION_ROW, childWidth); + } + } + + if (isStyleDimDefined(currentAbsoluteChild, CSS_FLEX_DIRECTION_COLUMN)) { + childHeight = currentAbsoluteChild->style.dimensions[CSS_HEIGHT] + getMarginAxis(currentAbsoluteChild, CSS_FLEX_DIRECTION_COLUMN); + } else { + // If the child doesn't have a specified height, compute the height based on the top/bottom offsets if they're defined. + if (isPosDefined(currentAbsoluteChild, CSS_TOP) && isPosDefined(currentAbsoluteChild, CSS_BOTTOM)) { + childHeight = node->layout.measured_dimensions[CSS_HEIGHT] - + (getLeadingBorder(node, CSS_FLEX_DIRECTION_COLUMN) + getTrailingBorder(node, CSS_FLEX_DIRECTION_COLUMN)) - + (currentAbsoluteChild->style.position[CSS_TOP] + currentAbsoluteChild->style.position[CSS_BOTTOM]); + childHeight = boundAxis(currentAbsoluteChild, CSS_FLEX_DIRECTION_COLUMN, childHeight); + } } - if (isPosDefined(currentAbsoluteChild, trailing[axis]) && - !isPosDefined(currentAbsoluteChild, leading[axis])) { - currentAbsoluteChild->layout.position[leading[axis]] = - node->layout.dimensions[dim[axis]] - - currentAbsoluteChild->layout.dimensions[dim[axis]] - - getPosition(currentAbsoluteChild, trailing[axis]); + // If we're still missing one or the other dimension, measure the content. + if (isUndefined(childWidth) || isUndefined(childHeight)) { + childWidthMeasureMode = isUndefined(childWidth) ? CSS_MEASURE_MODE_UNDEFINED : CSS_MEASURE_MODE_EXACTLY; + childHeightMeasureMode = isUndefined(childHeight) ? CSS_MEASURE_MODE_UNDEFINED : CSS_MEASURE_MODE_EXACTLY; + + // According to the spec, if the main size is not definite and the + // child's inline axis is parallel to the main axis (i.e. it's + // horizontal), the child should be sized using "UNDEFINED" in + // the main size. Otherwise use "AT_MOST" in the cross axis. + if (!isMainAxisRow && isUndefined(childWidth) && !isUndefined(availableInnerWidth)) { + childWidth = availableInnerWidth; + childWidthMeasureMode = CSS_MEASURE_MODE_AT_MOST; + } + + // The W3C spec doesn't say anything about the 'overflow' property, + // but all major browsers appear to implement the following logic. + if (node->style.overflow == CSS_OVERFLOW_HIDDEN) { + if (isMainAxisRow && isUndefined(childHeight) && !isUndefined(availableInnerHeight)) { + childHeight = availableInnerHeight; + childHeightMeasureMode = CSS_MEASURE_MODE_AT_MOST; + } + } + + layoutNodeInternal(currentAbsoluteChild, childWidth, childHeight, direction, childWidthMeasureMode, childHeightMeasureMode, false, "abs-measure"); + childWidth = currentAbsoluteChild->layout.measured_dimensions[CSS_WIDTH] + getMarginAxis(currentAbsoluteChild, CSS_FLEX_DIRECTION_ROW); + childHeight = currentAbsoluteChild->layout.measured_dimensions[CSS_HEIGHT] + getMarginAxis(currentAbsoluteChild, CSS_FLEX_DIRECTION_COLUMN); + } + + layoutNodeInternal(currentAbsoluteChild, childWidth, childHeight, direction, CSS_MEASURE_MODE_EXACTLY, CSS_MEASURE_MODE_EXACTLY, true, "abs-layout"); + + if (isPosDefined(currentAbsoluteChild, trailing[CSS_FLEX_DIRECTION_ROW]) && + !isPosDefined(currentAbsoluteChild, leading[CSS_FLEX_DIRECTION_ROW])) { + currentAbsoluteChild->layout.position[leading[CSS_FLEX_DIRECTION_ROW]] = + node->layout.measured_dimensions[dim[CSS_FLEX_DIRECTION_ROW]] - + currentAbsoluteChild->layout.measured_dimensions[dim[CSS_FLEX_DIRECTION_ROW]] - + getPosition(currentAbsoluteChild, trailing[CSS_FLEX_DIRECTION_ROW]); + } + + if (isPosDefined(currentAbsoluteChild, trailing[CSS_FLEX_DIRECTION_COLUMN]) && + !isPosDefined(currentAbsoluteChild, leading[CSS_FLEX_DIRECTION_COLUMN])) { + currentAbsoluteChild->layout.position[leading[CSS_FLEX_DIRECTION_COLUMN]] = + node->layout.measured_dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] - + currentAbsoluteChild->layout.measured_dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] - + getPosition(currentAbsoluteChild, trailing[CSS_FLEX_DIRECTION_COLUMN]); } } - child = currentAbsoluteChild; - currentAbsoluteChild = currentAbsoluteChild->next_absolute_child; - child->next_absolute_child = NULL; + currentAbsoluteChild = currentAbsoluteChild->next_child; } /** END_GENERATED **/ } - void layoutNode(css_node_t *node, float parentMaxWidth, float parentMaxHeight, css_direction_t parentDirection) { - css_layout_t *layout = &node->layout; - css_direction_t direction = node->style.direction; - layout->should_update = true; - - bool skipLayout = - !node->is_dirty(node->context) && - eq(layout->last_requested_dimensions[CSS_WIDTH], layout->dimensions[CSS_WIDTH]) && - eq(layout->last_requested_dimensions[CSS_HEIGHT], layout->dimensions[CSS_HEIGHT]) && - eq(layout->last_parent_max_width, parentMaxWidth) && - eq(layout->last_parent_max_height, parentMaxHeight) && - eq(layout->last_direction, direction); - - if (skipLayout) { - layout->dimensions[CSS_WIDTH] = layout->last_dimensions[CSS_WIDTH]; - layout->dimensions[CSS_HEIGHT] = layout->last_dimensions[CSS_HEIGHT]; - layout->position[CSS_TOP] = layout->last_position[CSS_TOP]; - layout->position[CSS_LEFT] = layout->last_position[CSS_LEFT]; + int gDepth = 0; + bool gPrintTree = false; + bool gPrintChanges = false; + bool gPrintSkips = false; + + static const char* spacer = " "; + + static const char* getSpacer(unsigned long level) { + unsigned long spacerLen = strlen(spacer); + if (level > spacerLen) { + level = spacerLen; + } + return &spacer[spacerLen - level]; + } + + static const char* getModeName(css_measure_mode_t mode, bool performLayout) { + const char* kMeasureModeNames[CSS_MEASURE_MODE_COUNT] = { + "UNDEFINED", + "EXACTLY", + "AT_MOST" + }; + const char* kLayoutModeNames[CSS_MEASURE_MODE_COUNT] = { + "LAY_UNDEFINED", + "LAY_EXACTLY", + "LAY_AT_MOST" + }; + + if (mode >= CSS_MEASURE_MODE_COUNT) { + return ""; + } + + return performLayout? kLayoutModeNames[mode] : kMeasureModeNames[mode]; + } + + static bool canUseCachedMeasurement(float availableWidth, float availableHeight, + float marginRow, float marginColumn, + css_measure_mode_t widthMeasureMode, css_measure_mode_t heightMeasureMode, + css_cached_measurement_t cachedLayout) { + + // Is it an exact match? + if (eq(cachedLayout.available_width, availableWidth) && + eq(cachedLayout.available_height, availableHeight) && + cachedLayout.width_measure_mode == widthMeasureMode && + cachedLayout.height_measure_mode == heightMeasureMode) { + return true; + } + + // If the width is an exact match, try a fuzzy match on the height. + if (cachedLayout.width_measure_mode == widthMeasureMode && + eq(cachedLayout.available_width, availableWidth) && + heightMeasureMode == CSS_MEASURE_MODE_EXACTLY && + eq(availableHeight - marginColumn, cachedLayout.computed_height)) { + return true; + } + + // If the height is an exact match, try a fuzzy match on the width. + if (cachedLayout.height_measure_mode == heightMeasureMode && + eq(cachedLayout.available_height, availableHeight) && + widthMeasureMode == CSS_MEASURE_MODE_EXACTLY && + eq(availableWidth - marginRow, cachedLayout.computed_width)) { + return true; + } + + return false; + } + + // + // This is a wrapper around the layoutNodeImpl function. It determines + // whether the layout request is redundant and can be skipped. + // + // Parameters: + // Input parameters are the same as layoutNodeImpl (see above) + // Return parameter is true if layout was performed, false if skipped + // + bool layoutNodeInternal(css_node_t* node, float availableWidth, float availableHeight, + css_direction_t parentDirection, css_measure_mode_t widthMeasureMode, css_measure_mode_t heightMeasureMode, bool performLayout, char* reason) { + css_layout_t* layout = &node->layout; + + gDepth++; + + bool needToVisitNode = (node->is_dirty(node->context) && layout->generation_count != gCurrentGenerationCount) || + layout->last_parent_direction != parentDirection; + + if (needToVisitNode) { + // Invalidate the cached results. + layout->next_cached_measurements_index = 0; + layout->cached_layout.width_measure_mode = (css_measure_mode_t)-1; + layout->cached_layout.height_measure_mode = (css_measure_mode_t)-1; + } + + css_cached_measurement_t* cachedResults = NULL; + + // Determine whether the results are already cached. We maintain a separate + // cache for layouts and measurements. A layout operation modifies the positions + // and dimensions for nodes in the subtree. The algorithm assumes that each node + // gets layed out a maximum of one time per tree layout, but multiple measurements + // may be required to resolve all of the flex dimensions. + // We handle nodes with measure functions specially here because they are the most + // expensive to measure, so it's worth avoiding redundant measurements if at all possible. + if (isMeasureDefined(node)) { + float marginAxisRow = getMarginAxis(node, CSS_FLEX_DIRECTION_ROW); + float marginAxisColumn = getMarginAxis(node, CSS_FLEX_DIRECTION_COLUMN); + + // First, try to use the layout cache. + if (canUseCachedMeasurement(availableWidth, availableHeight, marginAxisRow, marginAxisColumn, + widthMeasureMode, heightMeasureMode, layout->cached_layout)) { + cachedResults = &layout->cached_layout; + } else { + // Try to use the measurement cache. + for (int i = 0; i < layout->next_cached_measurements_index; i++) { + if (canUseCachedMeasurement(availableWidth, availableHeight, marginAxisRow, marginAxisColumn, + widthMeasureMode, heightMeasureMode, layout->cached_measurements[i])) { + cachedResults = &layout->cached_measurements[i]; + break; + } + } + } + } else if (performLayout) { + if (eq(layout->cached_layout.available_width, availableWidth) && + eq(layout->cached_layout.available_height, availableHeight) && + layout->cached_layout.width_measure_mode == widthMeasureMode && + layout->cached_layout.height_measure_mode == heightMeasureMode) { + + cachedResults = &layout->cached_layout; + } } else { - layout->last_requested_dimensions[CSS_WIDTH] = layout->dimensions[CSS_WIDTH]; - layout->last_requested_dimensions[CSS_HEIGHT] = layout->dimensions[CSS_HEIGHT]; - layout->last_parent_max_width = parentMaxWidth; - layout->last_parent_max_height = parentMaxHeight; - layout->last_direction = direction; - - for (int i = 0, childCount = node->children_count; i < childCount; i++) { - resetNodeLayout(node->get_child(node->context, i)); + for (int i = 0; i < layout->next_cached_measurements_index; i++) { + if (eq(layout->cached_measurements[i].available_width, availableWidth) && + eq(layout->cached_measurements[i].available_height, availableHeight) && + layout->cached_measurements[i].width_measure_mode == widthMeasureMode && + layout->cached_measurements[i].height_measure_mode == heightMeasureMode) { + + cachedResults = &layout->cached_measurements[i]; + break; + } + } + } + + if (!needToVisitNode && cachedResults != NULL) { + layout->measured_dimensions[CSS_WIDTH] = cachedResults->computed_width; + layout->measured_dimensions[CSS_HEIGHT] = cachedResults->computed_height; + + if (gPrintChanges && gPrintSkips) { + printf("%s%d.{[skipped] ", getSpacer(gDepth), gDepth); + if (node->print) { + node->print(node->context); + } + printf("wm: %s, hm: %s, aw: %f ah: %f => d: (%f, %f) %s\n", + getModeName(widthMeasureMode, performLayout), + getModeName(heightMeasureMode, performLayout), + availableWidth, availableHeight, + cachedResults->computed_width, cachedResults->computed_height, reason); + } + } else { + + if (gPrintChanges) { + printf("%s%d.{%s", getSpacer(gDepth), gDepth, needToVisitNode ? "*" : ""); + if (node->print) { + node->print(node->context); + } + printf("wm: %s, hm: %s, aw: %f ah: %f %s\n", + getModeName(widthMeasureMode, performLayout), + getModeName(heightMeasureMode, performLayout), + availableWidth, availableHeight, reason); + } + + layoutNodeImpl(node, availableWidth, availableHeight, parentDirection, widthMeasureMode, heightMeasureMode, performLayout); + + if (gPrintChanges) { + printf("%s%d.}%s", getSpacer(gDepth), gDepth, needToVisitNode ? "*" : ""); + if (node->print) { + node->print(node->context); + } + printf("wm: %s, hm: %s, d: (%f, %f) %s\n", + getModeName(widthMeasureMode, performLayout), + getModeName(heightMeasureMode, performLayout), + layout->measured_dimensions[CSS_WIDTH], layout->measured_dimensions[CSS_HEIGHT], reason); } - layoutNodeImpl(node, parentMaxWidth, parentMaxHeight, parentDirection); + layout->last_parent_direction = parentDirection; - layout->last_dimensions[CSS_WIDTH] = layout->dimensions[CSS_WIDTH]; - layout->last_dimensions[CSS_HEIGHT] = layout->dimensions[CSS_HEIGHT]; - layout->last_position[CSS_TOP] = layout->position[CSS_TOP]; - layout->last_position[CSS_LEFT] = layout->position[CSS_LEFT]; + if (cachedResults == NULL) { + if (layout->next_cached_measurements_index == CSS_MAX_CACHED_RESULT_COUNT) { + if (gPrintChanges) { + printf("Out of cache entries!\n"); + } + layout->next_cached_measurements_index = 0; + } + + css_cached_measurement_t* newCacheEntry; + if (performLayout) { + // Use the single layout cache entry. + newCacheEntry = &layout->cached_layout; + } else { + // Allocate a new measurement cache entry. + newCacheEntry = &layout->cached_measurements[layout->next_cached_measurements_index]; + layout->next_cached_measurements_index++; + } + + newCacheEntry->available_width = availableWidth; + newCacheEntry->available_height = availableHeight; + newCacheEntry->width_measure_mode = widthMeasureMode; + newCacheEntry->height_measure_mode = heightMeasureMode; + newCacheEntry->computed_width = layout->measured_dimensions[CSS_WIDTH]; + newCacheEntry->computed_height = layout->measured_dimensions[CSS_HEIGHT]; + } + } + + if (performLayout) { + node->layout.dimensions[CSS_WIDTH] = node->layout.measured_dimensions[CSS_WIDTH]; + node->layout.dimensions[CSS_HEIGHT] = node->layout.measured_dimensions[CSS_HEIGHT]; + layout->should_update = true; } + + gDepth--; + layout->generation_count = gCurrentGenerationCount; + return (needToVisitNode || cachedResults == NULL); } - void resetNodeLayout(css_node_t *node) { - node->layout.dimensions[CSS_WIDTH] = CSS_UNDEFINED; - node->layout.dimensions[CSS_HEIGHT] = CSS_UNDEFINED; - node->layout.position[CSS_LEFT] = 0; - node->layout.position[CSS_TOP] = 0; + void layoutNode(css_node_t* node, float availableWidth, float availableHeight, css_direction_t parentDirection) { + // Increment the generation count. This will force the recursive routine to visit + // all dirty nodes at least once. Subsequent visits will be skipped if the input + // parameters don't change. + gCurrentGenerationCount++; + + // If the caller didn't specify a height/width, use the dimensions + // specified in the style. + if (isUndefined(availableWidth) && isStyleDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { + availableWidth = node->style.dimensions[CSS_WIDTH] + getMarginAxis(node, CSS_FLEX_DIRECTION_ROW); + } + if (isUndefined(availableHeight) && isStyleDimDefined(node, CSS_FLEX_DIRECTION_COLUMN)) { + availableHeight = node->style.dimensions[CSS_HEIGHT] + getMarginAxis(node, CSS_FLEX_DIRECTION_COLUMN); + } + + css_measure_mode_t widthMeasureMode = isUndefined(availableWidth) ? CSS_MEASURE_MODE_UNDEFINED : CSS_MEASURE_MODE_EXACTLY; + css_measure_mode_t heightMeasureMode = isUndefined(availableHeight) ? CSS_MEASURE_MODE_UNDEFINED : CSS_MEASURE_MODE_EXACTLY; + + if (layoutNodeInternal(node, availableWidth, availableHeight, parentDirection, widthMeasureMode, heightMeasureMode, true, "initial")) { + + setPosition(node, node->layout.direction); + + if (gPrintTree) { + print_css_node(node, CSS_PRINT_LAYOUT | CSS_PRINT_CHILDREN | CSS_PRINT_STYLE); + } + } } diff --git a/React/Layout/Layout.h b/React/Layout/Layout.h index b2de9cea7db836..59d702e0d18f0c 100644 --- a/React/Layout/Layout.h +++ b/React/Layout/Layout.h @@ -56,6 +56,11 @@ CSS_JUSTIFY_SPACE_AROUND } css_justify_t; + typedef enum { + CSS_OVERFLOW_VISIBLE = 0, + CSS_OVERFLOW_HIDDEN + } css_overflow_t; + // Note: auto is only a valid value for alignSelf. It is NOT a valid value for // alignItems. typedef enum { @@ -91,7 +96,8 @@ typedef enum { CSS_MEASURE_MODE_UNDEFINED = 0, CSS_MEASURE_MODE_EXACTLY, - CSS_MEASURE_MODE_AT_MOST + CSS_MEASURE_MODE_AT_MOST, + CSS_MEASURE_MODE_COUNT } css_measure_mode_t; typedef enum { @@ -99,20 +105,40 @@ CSS_HEIGHT } css_dimension_t; + typedef struct { + float available_width; + float available_height; + css_measure_mode_t width_measure_mode; + css_measure_mode_t height_measure_mode; + + float computed_width; + float computed_height; + } css_cached_measurement_t; + + enum { + // This value was chosen based on empiracle data. Even the most complicated + // layouts should not require more than 16 entries to fit within the cache. + CSS_MAX_CACHED_RESULT_COUNT = 16 + }; + typedef struct { float position[4]; float dimensions[2]; css_direction_t direction; + float flex_basis; + // Instead of recomputing the entire layout every single time, we // cache some information to break early when nothing changed bool should_update; - float last_requested_dimensions[2]; - float last_parent_max_width; - float last_parent_max_height; - float last_dimensions[2]; - float last_position[2]; - css_direction_t last_direction; + int generation_count; + css_direction_t last_parent_direction; + + int next_cached_measurements_index; + css_cached_measurement_t cached_measurements[CSS_MAX_CACHED_RESULT_COUNT]; + float measured_dimensions[2]; + + css_cached_measurement_t cached_layout; } css_layout_t; typedef struct { @@ -128,6 +154,7 @@ css_align_t align_self; css_position_type_t position_type; css_wrap_type_t flex_wrap; + css_overflow_t overflow; float flex; float margin[6]; float position[4]; @@ -155,8 +182,7 @@ int children_count; int line_index; - css_node_t *next_absolute_child; - css_node_t *next_flex_child; + css_node_t* next_child; css_dim_t (*measure)(void *context, float width, css_measure_mode_t widthMode, float height, css_measure_mode_t heightMode); void (*print)(void *context); @@ -178,12 +204,8 @@ } css_print_options_t; void print_css_node(css_node_t *node, css_print_options_t options); - bool isUndefined(float value); - // Function that computes the layout! - void layoutNode(css_node_t *node, float maxWidth, float maxHeight, css_direction_t parentDirection); - - // Reset the calculated layout values for a given node. You should call this before `layoutNode`. - void resetNodeLayout(css_node_t *node); + void layoutNode(css_node_t *node, float availableWidth, float availableHeight, css_direction_t parentDirection); + bool isUndefined(float value); #endif diff --git a/React/Views/RCTRootShadowView.m b/React/Views/RCTRootShadowView.m index 70f62be1705903..b23914d25cac4a 100644 --- a/React/Views/RCTRootShadowView.m +++ b/React/Views/RCTRootShadowView.m @@ -34,7 +34,6 @@ - (void)applySizeConstraints [self applySizeConstraints]; [self fillCSSNode:self.cssNode]; - resetNodeLayout(self.cssNode); layoutNode(self.cssNode, CSS_UNDEFINED, CSS_UNDEFINED, CSS_DIRECTION_INHERIT); NSMutableSet *viewsWithNewFrame = [NSMutableSet set]; diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSCachedMeasurement.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSCachedMeasurement.java new file mode 100644 index 00000000000000..11b188293e203a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSCachedMeasurement.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<> + +package com.facebook.csslayout; + +public class CSSCachedMeasurement { + public float availableWidth; + public float availableHeight; + public CSSMeasureMode widthMeasureMode = null; + public CSSMeasureMode heightMeasureMode = null; + + public float computedWidth; + public float computedHeight; +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayout.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayout.java index 165c5f96d8de32..60f33fe5b5b4ad 100644 --- a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayout.java +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayout.java @@ -7,7 +7,7 @@ */ // NOTE: this file is auto-copied from https://github.com/facebook/css-layout -// @generated SignedSource<<1e547b3af02a275fe73089e5a0a172c5>> +// @generated SignedSource<<4ed6ae4c11cdd41f5db380e3b9a06f23>> package com.facebook.csslayout; @@ -17,6 +17,10 @@ * Where the output of {@link LayoutEngine#layoutNode(CSSNode, float)} will go in the CSSNode. */ public class CSSLayout { + // This value was chosen based on empiracle data. Even the most complicated + // layouts should not require more than 16 entries to fit within the cache. + public static final int MAX_CACHED_RESULT_COUNT = 16; + public static final int POSITION_LEFT = 0; public static final int POSITION_TOP = 1; public static final int POSITION_RIGHT = 2; @@ -28,24 +32,38 @@ public class CSSLayout { public float[] position = new float[4]; public float[] dimensions = new float[2]; public CSSDirection direction = CSSDirection.LTR; - - /** - * This should always get called before calling {@link LayoutEngine#layoutNode(CSSNode, float)} - */ + + public float flexBasis; + + public int generationCount; + public CSSDirection lastParentDirection; + + public int nextCachedMeasurementsIndex; + public CSSCachedMeasurement[] cachedMeasurements = new CSSCachedMeasurement[MAX_CACHED_RESULT_COUNT]; + public float[] measuredDimensions = new float[2]; + + public CSSCachedMeasurement cachedLayout = new CSSCachedMeasurement(); + + CSSLayout() { + resetResult(); + } + public void resetResult() { Arrays.fill(position, 0); Arrays.fill(dimensions, CSSConstants.UNDEFINED); direction = CSSDirection.LTR; - } - - public void copy(CSSLayout layout) { - position[POSITION_LEFT] = layout.position[POSITION_LEFT]; - position[POSITION_TOP] = layout.position[POSITION_TOP]; - position[POSITION_RIGHT] = layout.position[POSITION_RIGHT]; - position[POSITION_BOTTOM] = layout.position[POSITION_BOTTOM]; - dimensions[DIMENSION_WIDTH] = layout.dimensions[DIMENSION_WIDTH]; - dimensions[DIMENSION_HEIGHT] = layout.dimensions[DIMENSION_HEIGHT]; - direction = layout.direction; + + flexBasis = 0; + + generationCount = 0; + lastParentDirection = null; + + nextCachedMeasurementsIndex = 0; + measuredDimensions[DIMENSION_WIDTH] = CSSConstants.UNDEFINED; + measuredDimensions[DIMENSION_HEIGHT] = CSSConstants.UNDEFINED; + + cachedLayout.widthMeasureMode = null; + cachedLayout.heightMeasureMode = null; } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayoutContext.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayoutContext.java index 574c0bdf5f7c56..a6562ff2b6dcac 100644 --- a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayoutContext.java +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSLayoutContext.java @@ -7,7 +7,7 @@ */ // NOTE: this file is auto-copied from https://github.com/facebook/css-layout -// @generated SignedSource<<9d48f3d4330e7b6cba0fff7d8f1e8b0c>> +// @generated SignedSource<> package com.facebook.csslayout; @@ -20,4 +20,5 @@ */ public class CSSLayoutContext { /*package*/ final MeasureOutput measureOutput = new MeasureOutput(); + int currentGenerationCount; } diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java index 348b695e02b776..6145219f70dbd3 100644 --- a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSNode.java @@ -7,7 +7,7 @@ */ // NOTE: this file is auto-copied from https://github.com/facebook/css-layout -// @generated SignedSource<> +// @generated SignedSource<<67fbba6df7c2472877c7b04327fb1863>> package com.facebook.csslayout; @@ -66,9 +66,8 @@ public static interface MeasureFunction { public int lineIndex = 0; - /*package*/ CSSNode nextAbsoluteChild; - /*package*/ CSSNode nextFlexChild; - + /*package*/ CSSNode nextChild; + private @Nullable ArrayList mChildren; private @Nullable CSSNode mParent; private @Nullable MeasureFunction mMeasureFunction = null; @@ -142,7 +141,6 @@ public boolean isMeasureDefined() { * Performs the actual layout and saves the results in {@link #layout} */ public void calculateLayout(CSSLayoutContext layoutContext) { - layout.resetResult(); LayoutEngine.layoutNode(layoutContext, this, CSSConstants.UNDEFINED, CSSConstants.UNDEFINED, null); } diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSOverflow.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSOverflow.java new file mode 100644 index 00000000000000..74e2efcb44d97f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSOverflow.java @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// NOTE: this file is auto-copied from https://github.com/facebook/css-layout +// @generated SignedSource<<3bbf86ec0e75cbdbc9c741e0b3922679>> + +package com.facebook.csslayout; + +public enum CSSOverflow { + VISIBLE, + HIDDEN, +} diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSStyle.java b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSStyle.java index 6170720ceb20c9..119761fb9c5a18 100644 --- a/ReactAndroid/src/main/java/com/facebook/csslayout/CSSStyle.java +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/CSSStyle.java @@ -7,7 +7,7 @@ */ // NOTE: this file is auto-copied from https://github.com/facebook/css-layout -// @generated SignedSource<<4c7c75ffd4800aee843a5f5828f3e3ab>> +// @generated SignedSource<> package com.facebook.csslayout; @@ -26,6 +26,7 @@ public class CSSStyle { public CSSAlign alignSelf; public CSSPositionType positionType; public CSSWrap flexWrap; + public CSSOverflow overflow; public float flex; public Spacing margin = new Spacing(); @@ -54,6 +55,7 @@ void reset() { alignSelf = CSSAlign.AUTO; positionType = CSSPositionType.RELATIVE; flexWrap = CSSWrap.NOWRAP; + overflow = CSSOverflow.VISIBLE; flex = 0f; margin.reset();; diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/LayoutEngine.java b/ReactAndroid/src/main/java/com/facebook/csslayout/LayoutEngine.java index 5b9cc60f1cd1bc..c8ef50114130ca 100644 --- a/ReactAndroid/src/main/java/com/facebook/csslayout/LayoutEngine.java +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/LayoutEngine.java @@ -7,10 +7,12 @@ */ // NOTE: this file is auto-copied from https://github.com/facebook/css-layout -// @generated SignedSource<<9224a1489ee541a447ede3657538f5bc>> +// @generated SignedSource<<9165e7546c964b47cb01be9ff8070aa8>> package com.facebook.csslayout; +import com.facebook.infer.annotation.Assertions; + import static com.facebook.csslayout.CSSLayout.DIMENSION_HEIGHT; import static com.facebook.csslayout.CSSLayout.DIMENSION_WIDTH; import static com.facebook.csslayout.CSSLayout.POSITION_BOTTOM; @@ -22,7 +24,9 @@ * Calculates layouts based on CSS style. See {@link #layoutNode(CSSNode, float, float)}. */ public class LayoutEngine { - + + private static final boolean POSITIVE_FLEX_IS_AUTO = false; + private static final int CSS_FLEX_DIRECTION_COLUMN = CSSFlexDirection.COLUMN.ordinal(); private static final int CSS_FLEX_DIRECTION_COLUMN_REVERSE = @@ -76,8 +80,42 @@ public class LayoutEngine { Spacing.END, Spacing.END }; + + private static boolean isFlexBasisAuto(CSSNode node) { + if (POSITIVE_FLEX_IS_AUTO) { + // All flex values are auto. + return true; + } else { + // A flex value > 0 implies a basis of zero. + return node.style.flex <= 0; + } + } + + private static float getFlexGrowFactor(CSSNode node) { + // Flex grow is implied by positive values for flex. + if (node.style.flex > 0) { + return node.style.flex; + } + return 0; + } + + private static float getFlexShrinkFactor(CSSNode node) { + if (POSITIVE_FLEX_IS_AUTO) { + // A flex shrink factor of 1 is implied by non-zero values for flex. + if (node.style.flex != 0) { + return 1; + } + } else { + // A flex shrink factor of 1 is implied by negative values for flex. + if (node.style.flex < 0) { + return 1; + } + } + return 0; + } - private static float boundAxis(CSSNode node, int axis, float value) { + + private static float boundAxisWithinMinAndMax(CSSNode node, int axis, float value) { float min = CSSConstants.UNDEFINED; float max = CSSConstants.UNDEFINED; @@ -102,26 +140,14 @@ private static float boundAxis(CSSNode node, int axis, float value) { return boundValue; } - - private static void setDimensionFromStyle(CSSNode node, int axis) { - // The parent already computed us a width or height. We just skip it - if (!Float.isNaN(node.layout.dimensions[dim[axis]])) { - return; - } - // We only run if there's a width or height defined - if (Float.isNaN(node.style.dimensions[dim[axis]]) || - node.style.dimensions[dim[axis]] <= 0.0) { - return; - } - - // The dimensions can never be smaller than the padding and border - float maxLayoutDimension = Math.max( - boundAxis(node, axis, node.style.dimensions[dim[axis]]), - node.style.padding.getWithFallback(leadingSpacing[axis], leading[axis]) + - node.style.padding.getWithFallback(trailingSpacing[axis], trailing[axis]) + - node.style.border.getWithFallback(leadingSpacing[axis], leading[axis]) + - node.style.border.getWithFallback(trailingSpacing[axis], trailing[axis])); - node.layout.dimensions[dim[axis]] = maxLayoutDimension; + + private static float boundAxis(CSSNode node, int axis, float value) { + float paddingAndBorderAxis = + node.style.padding.getWithFallback(leadingSpacing[axis], leading[axis]) + + node.style.border.getWithFallback(leadingSpacing[axis], leading[axis]) + + node.style.padding.getWithFallback(trailingSpacing[axis], trailing[axis]) + + node.style.border.getWithFallback(trailingSpacing[axis], trailing[axis]); + return Math.max(boundAxisWithinMinAndMax(node, axis, value), paddingAndBorderAxis); } private static float getRelativePosition(CSSNode node, int axis) { @@ -133,6 +159,20 @@ private static float getRelativePosition(CSSNode node, int axis) { float trailingPos = node.style.position[trailing[axis]]; return Float.isNaN(trailingPos) ? 0 : -trailingPos; } + + private static void setPosition(CSSNode node, CSSDirection direction) { + int mainAxis = resolveAxis(getFlexDirection(node), direction); + int crossAxis = getCrossFlexDirection(mainAxis, direction); + + node.layout.position[leading[mainAxis]] = node.style.margin.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + + getRelativePosition(node, mainAxis); + node.layout.position[trailing[mainAxis]] = node.style.margin.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]) + + getRelativePosition(node, mainAxis); + node.layout.position[leading[crossAxis]] = node.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + + getRelativePosition(node, crossAxis); + node.layout.position[trailing[crossAxis]] = node.style.margin.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis]) + + getRelativePosition(node, crossAxis); + } private static int resolveAxis( int axis, @@ -183,395 +223,571 @@ private static boolean isMeasureDefined(CSSNode node) { return node.isMeasureDefined(); } - static boolean needsRelayout(CSSNode node, float parentMaxWidth, float parentMaxHeight) { - return node.isDirty() || - !FloatUtil.floatsEqual( - node.lastLayout.requestedHeight, - node.layout.dimensions[DIMENSION_HEIGHT]) || - !FloatUtil.floatsEqual( - node.lastLayout.requestedWidth, - node.layout.dimensions[DIMENSION_WIDTH]) || - !FloatUtil.floatsEqual(node.lastLayout.parentMaxWidth, parentMaxWidth) || - !FloatUtil.floatsEqual(node.lastLayout.parentMaxHeight, parentMaxHeight); - } - /*package*/ static void layoutNode( CSSLayoutContext layoutContext, CSSNode node, - float parentMaxWidth, - float parentMaxHeight, + float availableWidth, + float availableHeight, CSSDirection parentDirection) { - if (needsRelayout(node, parentMaxWidth, parentMaxHeight)) { - node.lastLayout.requestedWidth = node.layout.dimensions[DIMENSION_WIDTH]; - node.lastLayout.requestedHeight = node.layout.dimensions[DIMENSION_HEIGHT]; - node.lastLayout.parentMaxWidth = parentMaxWidth; - node.lastLayout.parentMaxHeight = parentMaxHeight; - - for (int i = 0, childCount = node.getChildCount(); i < childCount; i++) { - node.getChildAt(i).layout.resetResult(); + // Increment the generation count. This will force the recursive routine to visit + // all dirty nodes at least once. Subsequent visits will be skipped if the input + // parameters don't change. + layoutContext.currentGenerationCount++; + + // If the caller didn't specify a height/width, use the dimensions + // specified in the style. + if (Float.isNaN(availableWidth) && node.style.dimensions[DIMENSION_WIDTH] >= 0.0) { + float marginAxisRow = (node.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_ROW], leading[CSS_FLEX_DIRECTION_ROW]) + node.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_ROW], trailing[CSS_FLEX_DIRECTION_ROW])); + availableWidth = node.style.dimensions[DIMENSION_WIDTH] + marginAxisRow; + } + if (Float.isNaN(availableHeight) && node.style.dimensions[DIMENSION_HEIGHT] >= 0.0) { + float marginAxisColumn = (node.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + node.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN])); + availableHeight = node.style.dimensions[DIMENSION_HEIGHT] + marginAxisColumn; + } + + CSSMeasureMode widthMeasureMode = Float.isNaN(availableWidth) ? CSSMeasureMode.UNDEFINED : CSSMeasureMode.EXACTLY; + CSSMeasureMode heightMeasureMode = Float.isNaN(availableHeight) ? CSSMeasureMode.UNDEFINED : CSSMeasureMode.EXACTLY; + + if (layoutNodeInternal(layoutContext, node, availableWidth, availableHeight, parentDirection, widthMeasureMode, heightMeasureMode, true, "initial")) { + setPosition(node, node.layout.direction); + } + } + + /*package*/ static boolean canUseCachedMeasurement(float availableWidth, float availableHeight, + float marginRow, float marginColumn, + CSSMeasureMode widthMeasureMode, CSSMeasureMode heightMeasureMode, + CSSCachedMeasurement cachedLayout) { + // Is it an exact match? + if (FloatUtil.floatsEqual(cachedLayout.availableWidth, availableWidth) && + FloatUtil.floatsEqual(cachedLayout.availableHeight, availableHeight) && + cachedLayout.widthMeasureMode == widthMeasureMode && + cachedLayout.heightMeasureMode == heightMeasureMode) { + return true; + } + + // If the width is an exact match, try a fuzzy match on the height. + if (FloatUtil.floatsEqual(cachedLayout.availableWidth, availableWidth) && + cachedLayout.widthMeasureMode == widthMeasureMode && + heightMeasureMode == CSSMeasureMode.EXACTLY && + FloatUtil.floatsEqual(availableHeight - marginColumn, cachedLayout.computedHeight)) { + return true; + } + + // If the height is an exact match, try a fuzzy match on the width. + if (FloatUtil.floatsEqual(cachedLayout.availableHeight, availableHeight) && + cachedLayout.heightMeasureMode == heightMeasureMode && + widthMeasureMode == CSSMeasureMode.EXACTLY && + FloatUtil.floatsEqual(availableWidth - marginRow, cachedLayout.computedWidth)) { + return true; + } + + return false; + } + + // + // This is a wrapper around the layoutNodeImpl function. It determines + // whether the layout request is redundant and can be skipped. + // + // Parameters: + // Input parameters are the same as layoutNodeImpl (see below) + // Return parameter is true if layout was performed, false if skipped + // + private static boolean layoutNodeInternal( + CSSLayoutContext layoutContext, + CSSNode node, + float availableWidth, + float availableHeight, + CSSDirection parentDirection, + CSSMeasureMode widthMeasureMode, + CSSMeasureMode heightMeasureMode, + boolean performLayout, + String reason) { + CSSLayout layout = node.layout; + + boolean needToVisitNode = (node.isDirty() && layout.generationCount != layoutContext.currentGenerationCount) || + layout.lastParentDirection != parentDirection; + + if (needToVisitNode) { + // Invalidate the cached results. + layout.nextCachedMeasurementsIndex = 0; + layout.cachedLayout.widthMeasureMode = null; + layout.cachedLayout.heightMeasureMode = null; + } + + CSSCachedMeasurement cachedResults = null; + + // Determine whether the results are already cached. We maintain a separate + // cache for layouts and measurements. A layout operation modifies the positions + // and dimensions for nodes in the subtree. The algorithm assumes that each node + // gets layed out a maximum of one time per tree layout, but multiple measurements + // may be required to resolve all of the flex dimensions. + // We handle nodes with measure functions specially here because they are the most + // expensive to measure, so it's worth avoiding redundant measurements if at all possible. + if (isMeasureDefined(node)) { + float marginAxisRow = + node.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_ROW], leading[CSS_FLEX_DIRECTION_ROW]) + + node.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_ROW], trailing[CSS_FLEX_DIRECTION_ROW]); + float marginAxisColumn = + node.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + + node.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN]); + + // First, try to use the layout cache. + if (canUseCachedMeasurement(availableWidth, availableHeight, marginAxisRow, marginAxisColumn, + widthMeasureMode, heightMeasureMode, layout.cachedLayout)) { + cachedResults = layout.cachedLayout; + } else { + // Try to use the measurement cache. + for (int i = 0; i < layout.nextCachedMeasurementsIndex; i++) { + if (canUseCachedMeasurement(availableWidth, availableHeight, marginAxisRow, marginAxisColumn, + widthMeasureMode, heightMeasureMode, layout.cachedMeasurements[i])) { + cachedResults = layout.cachedMeasurements[i]; + break; + } + } + } + } else if (performLayout) { + if (FloatUtil.floatsEqual(layout.cachedLayout.availableWidth, availableWidth) && + FloatUtil.floatsEqual(layout.cachedLayout.availableHeight, availableHeight) && + layout.cachedLayout.widthMeasureMode == widthMeasureMode && + layout.cachedLayout.heightMeasureMode == heightMeasureMode) { + + cachedResults = layout.cachedLayout; } - - layoutNodeImpl(layoutContext, node, parentMaxWidth, parentMaxHeight, parentDirection); - node.lastLayout.copy(node.layout); } else { - node.layout.copy(node.lastLayout); + for (int i = 0; i < layout.nextCachedMeasurementsIndex; i++) { + if (FloatUtil.floatsEqual(layout.cachedMeasurements[i].availableWidth, availableWidth) && + FloatUtil.floatsEqual(layout.cachedMeasurements[i].availableHeight, availableHeight) && + layout.cachedMeasurements[i].widthMeasureMode == widthMeasureMode && + layout.cachedMeasurements[i].heightMeasureMode == heightMeasureMode) { + + cachedResults = layout.cachedMeasurements[i]; + break; + } + } } + + if (!needToVisitNode && cachedResults != null) { + layout.measuredDimensions[DIMENSION_WIDTH] = cachedResults.computedWidth; + layout.measuredDimensions[DIMENSION_HEIGHT] = cachedResults.computedHeight; + } else { + layoutNodeImpl(layoutContext, node, availableWidth, availableHeight, parentDirection, widthMeasureMode, heightMeasureMode, performLayout); + + layout.lastParentDirection = parentDirection; + + if (cachedResults == null) { + if (layout.nextCachedMeasurementsIndex == CSSLayout.MAX_CACHED_RESULT_COUNT) { + layout.nextCachedMeasurementsIndex = 0; + } - node.markHasNewLayout(); + CSSCachedMeasurement newCacheEntry = null; + if (performLayout) { + // Use the single layout cache entry. + newCacheEntry = layout.cachedLayout; + } else { + // Allocate a new measurement cache entry. + newCacheEntry = layout.cachedMeasurements[layout.nextCachedMeasurementsIndex]; + if (newCacheEntry == null) { + newCacheEntry = new CSSCachedMeasurement(); + layout.cachedMeasurements[layout.nextCachedMeasurementsIndex] = newCacheEntry; + } + layout.nextCachedMeasurementsIndex++; + } + + newCacheEntry.availableWidth = availableWidth; + newCacheEntry.availableHeight = availableHeight; + newCacheEntry.widthMeasureMode = widthMeasureMode; + newCacheEntry.heightMeasureMode = heightMeasureMode; + newCacheEntry.computedWidth = layout.measuredDimensions[DIMENSION_WIDTH]; + newCacheEntry.computedHeight = layout.measuredDimensions[DIMENSION_HEIGHT]; + } + } + + if (performLayout) { + node.layout.dimensions[DIMENSION_WIDTH] = node.layout.measuredDimensions[DIMENSION_WIDTH]; + node.layout.dimensions[DIMENSION_HEIGHT] = node.layout.measuredDimensions[DIMENSION_HEIGHT]; + node.markHasNewLayout(); + } + + layout.generationCount = layoutContext.currentGenerationCount; + return (needToVisitNode || cachedResults == null); } - + + + // + // This is the main routine that implements a subset of the flexbox layout algorithm + // described in the W3C CSS documentation: https://www.w3.org/TR/css3-flexbox/. + // + // Limitations of this algorithm, compared to the full standard: + // * Display property is always assumed to be 'flex' except for Text nodes, which + // are assumed to be 'inline-flex'. + // * The 'zIndex' property (or any form of z ordering) is not supported. Nodes are + // stacked in document order. + // * The 'order' property is not supported. The order of flex items is always defined + // by document order. + // * The 'visibility' property is always assumed to be 'visible'. Values of 'collapse' + // and 'hidden' are not supported. + // * The 'wrap' property supports only 'nowrap' (which is the default) or 'wrap'. The + // rarely-used 'wrap-reverse' is not supported. + // * Rather than allowing arbitrary combinations of flexGrow, flexShrink and + // flexBasis, this algorithm supports only the three most common combinations: + // flex: 0 is equiavlent to flex: 0 0 auto + // flex: n (where n is a positive value) is equivalent to flex: n 1 auto + // If POSITIVE_FLEX_IS_AUTO is 0, then it is equivalent to flex: n 0 0 + // This is faster because the content doesn't need to be measured, but it's + // less flexible because the basis is always 0 and can't be overriden with + // the width/height attributes. + // flex: -1 (or any negative value) is equivalent to flex: 0 1 auto + // * Margins cannot be specified as 'auto'. They must be specified in terms of pixel + // values, and the default value is 0. + // * The 'baseline' value is not supported for alignItems and alignSelf properties. + // * Values of width, maxWidth, minWidth, height, maxHeight and minHeight must be + // specified as pixel values, not as percentages. + // * There is no support for calculation of dimensions based on intrinsic aspect ratios + // (e.g. images). + // * There is no support for forced breaks. + // * It does not support vertical inline directions (top-to-bottom or bottom-to-top text). + // + // Deviations from standard: + // * Section 4.5 of the spec indicates that all flex items have a default minimum + // main size. For text blocks, for example, this is the width of the widest word. + // Calculating the minimum width is expensive, so we forego it and assume a default + // minimum main size of 0. + // * Min/Max sizes in the main axis are not honored when resolving flexible lengths. + // * The spec indicates that the default value for 'flexDirection' is 'row', but + // the algorithm below assumes a default of 'column'. + // + // Input parameters: + // - node: current node to be sized and layed out + // - availableWidth & availableHeight: available size to be used for sizing the node + // or CSS_UNDEFINED if the size is not available; interpretation depends on layout + // flags + // - parentDirection: the inline (text) direction within the parent (left-to-right or + // right-to-left) + // - widthMeasureMode: indicates the sizing rules for the width (see below for explanation) + // - heightMeasureMode: indicates the sizing rules for the height (see below for explanation) + // - performLayout: specifies whether the caller is interested in just the dimensions + // of the node or it requires the entire node and its subtree to be layed out + // (with final positions) + // + // Details: + // This routine is called recursively to lay out subtrees of flexbox elements. It uses the + // information in node.style, which is treated as a read-only input. It is responsible for + // setting the layout.direction and layout.measured_dimensions fields for the input node as well + // as the layout.position and layout.line_index fields for its child nodes. The + // layout.measured_dimensions field includes any border or padding for the node but does + // not include margins. + // + // The spec describes four different layout modes: "fill available", "max content", "min content", + // and "fit content". Of these, we don't use "min content" because we don't support default + // minimum main sizes (see above for details). Each of our measure modes maps to a layout mode + // from the spec (https://www.w3.org/TR/css3-sizing/#terms): + // - CSS_MEASURE_MODE_UNDEFINED: max content + // - CSS_MEASURE_MODE_EXACTLY: fill available + // - CSS_MEASURE_MODE_AT_MOST: fit content + // + // When calling layoutNodeImpl and layoutNodeInternal, if the caller passes an available size of + // undefined then it must also pass a measure mode of CSS_MEASURE_MODE_UNDEFINED in that dimension. + // private static void layoutNodeImpl( CSSLayoutContext layoutContext, CSSNode node, - float parentMaxWidth, - float parentMaxHeight, - CSSDirection parentDirection) { + float availableWidth, + float availableHeight, + CSSDirection parentDirection, + CSSMeasureMode widthMeasureMode, + CSSMeasureMode heightMeasureMode, + boolean performLayout) { /** START_GENERATED **/ - CSSDirection direction = resolveDirection(node, parentDirection); - int mainAxis = resolveAxis(getFlexDirection(node), direction); - int crossAxis = getCrossFlexDirection(mainAxis, direction); - int resolvedRowAxis = resolveAxis(CSS_FLEX_DIRECTION_ROW, direction); - - // Handle width and height style attributes - setDimensionFromStyle(node, mainAxis); - setDimensionFromStyle(node, crossAxis); + Assertions.assertCondition(Float.isNaN(availableWidth) ? widthMeasureMode == CSSMeasureMode.UNDEFINED : true, "availableWidth is indefinite so widthMeasureMode must be CSSMeasureMode.UNDEFINED"); + Assertions.assertCondition(Float.isNaN(availableHeight) ? heightMeasureMode == CSSMeasureMode.UNDEFINED : true, "availableHeight is indefinite so heightMeasureMode must be CSSMeasureMode.UNDEFINED"); + + float paddingAndBorderAxisRow = ((node.style.padding.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_ROW], leading[CSS_FLEX_DIRECTION_ROW]) + node.style.border.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_ROW], leading[CSS_FLEX_DIRECTION_ROW])) + (node.style.padding.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_ROW], trailing[CSS_FLEX_DIRECTION_ROW]) + node.style.border.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_ROW], trailing[CSS_FLEX_DIRECTION_ROW]))); + float paddingAndBorderAxisColumn = ((node.style.padding.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + node.style.border.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN])) + (node.style.padding.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN]) + node.style.border.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN]))); + float marginAxisRow = (node.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_ROW], leading[CSS_FLEX_DIRECTION_ROW]) + node.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_ROW], trailing[CSS_FLEX_DIRECTION_ROW])); + float marginAxisColumn = (node.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + node.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN])); - // Set the resolved resolution in the node's layout + // Set the resolved resolution in the node's layout. + CSSDirection direction = resolveDirection(node, parentDirection); node.layout.direction = direction; - // The position is set by the parent, but we need to complete it with a - // delta composed of the margin and left/top/right/bottom - node.layout.position[leading[mainAxis]] += node.style.margin.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + - getRelativePosition(node, mainAxis); - node.layout.position[trailing[mainAxis]] += node.style.margin.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]) + - getRelativePosition(node, mainAxis); - node.layout.position[leading[crossAxis]] += node.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + - getRelativePosition(node, crossAxis); - node.layout.position[trailing[crossAxis]] += node.style.margin.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis]) + - getRelativePosition(node, crossAxis); - - // Inline immutable values from the target node to avoid excessive method - // invocations during the layout calculation. - int childCount = node.getChildCount(); - float paddingAndBorderAxisResolvedRow = ((node.style.padding.getWithFallback(leadingSpacing[resolvedRowAxis], leading[resolvedRowAxis]) + node.style.border.getWithFallback(leadingSpacing[resolvedRowAxis], leading[resolvedRowAxis])) + (node.style.padding.getWithFallback(trailingSpacing[resolvedRowAxis], trailing[resolvedRowAxis]) + node.style.border.getWithFallback(trailingSpacing[resolvedRowAxis], trailing[resolvedRowAxis]))); - float paddingAndBorderAxisColumn = ((node.style.padding.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + node.style.border.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN])) + (node.style.padding.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN]) + node.style.border.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN]))); - + // For content (text) nodes, determine the dimensions based on the text contents. if (isMeasureDefined(node)) { - boolean isResolvedRowDimDefined = (!Float.isNaN(node.layout.dimensions[dim[resolvedRowAxis]]) && node.layout.dimensions[dim[resolvedRowAxis]] >= 0.0); - - float width = CSSConstants.UNDEFINED; - CSSMeasureMode widthMode = CSSMeasureMode.UNDEFINED; - if ((!Float.isNaN(node.style.dimensions[dim[resolvedRowAxis]]) && node.style.dimensions[dim[resolvedRowAxis]] >= 0.0)) { - width = node.style.dimensions[DIMENSION_WIDTH]; - widthMode = CSSMeasureMode.EXACTLY; - } else if (isResolvedRowDimDefined) { - width = node.layout.dimensions[dim[resolvedRowAxis]]; - widthMode = CSSMeasureMode.EXACTLY; - } else { - width = parentMaxWidth - - (node.style.margin.getWithFallback(leadingSpacing[resolvedRowAxis], leading[resolvedRowAxis]) + node.style.margin.getWithFallback(trailingSpacing[resolvedRowAxis], trailing[resolvedRowAxis])); - widthMode = CSSMeasureMode.AT_MOST; - } - width -= paddingAndBorderAxisResolvedRow; - if (Float.isNaN(width)) { - widthMode = CSSMeasureMode.UNDEFINED; - } - - float height = CSSConstants.UNDEFINED; - CSSMeasureMode heightMode = CSSMeasureMode.UNDEFINED; - if ((!Float.isNaN(node.style.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]]) && node.style.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] >= 0.0)) { - height = node.style.dimensions[DIMENSION_HEIGHT]; - heightMode = CSSMeasureMode.EXACTLY; - } else if ((!Float.isNaN(node.layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]]) && node.layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] >= 0.0)) { - height = node.layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]]; - heightMode = CSSMeasureMode.EXACTLY; + float innerWidth = availableWidth - marginAxisRow - paddingAndBorderAxisRow; + float innerHeight = availableHeight - marginAxisColumn - paddingAndBorderAxisColumn; + + if (widthMeasureMode == CSSMeasureMode.EXACTLY && heightMeasureMode == CSSMeasureMode.EXACTLY) { + + // Don't bother sizing the text if both dimensions are already defined. + node.layout.measuredDimensions[DIMENSION_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, availableWidth - marginAxisRow); + node.layout.measuredDimensions[DIMENSION_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, availableHeight - marginAxisColumn); + } else if (innerWidth <= 0 || innerHeight <= 0) { + + // Don't bother sizing the text if there's no horizontal or vertical space. + node.layout.measuredDimensions[DIMENSION_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, 0); + node.layout.measuredDimensions[DIMENSION_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, 0); } else { - height = parentMaxHeight - - (node.style.margin.getWithFallback(leadingSpacing[resolvedRowAxis], leading[resolvedRowAxis]) + node.style.margin.getWithFallback(trailingSpacing[resolvedRowAxis], trailing[resolvedRowAxis])); - heightMode = CSSMeasureMode.AT_MOST; - } - height -= ((node.style.padding.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + node.style.border.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN])) + (node.style.padding.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN]) + node.style.border.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN]))); - if (Float.isNaN(height)) { - heightMode = CSSMeasureMode.UNDEFINED; - } - - // We only need to give a dimension for the text if we haven't got any - // for it computed yet. It can either be from the style attribute or because - // the element is flexible. - boolean isRowUndefined = !(!Float.isNaN(node.style.dimensions[dim[resolvedRowAxis]]) && node.style.dimensions[dim[resolvedRowAxis]] >= 0.0) && !isResolvedRowDimDefined; - boolean isColumnUndefined = !(!Float.isNaN(node.style.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]]) && node.style.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] >= 0.0) && - Float.isNaN(node.layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]]); - // Let's not measure the text if we already know both dimensions - if (isRowUndefined || isColumnUndefined) { + // Measure the text under the current constraints. MeasureOutput measureDim = node.measure( layoutContext.measureOutput, - width, - widthMode, - height, - heightMode + innerWidth, + widthMeasureMode, + innerHeight, + heightMeasureMode ); - if (isRowUndefined) { - node.layout.dimensions[DIMENSION_WIDTH] = measureDim.width + - paddingAndBorderAxisResolvedRow; - } - if (isColumnUndefined) { - node.layout.dimensions[DIMENSION_HEIGHT] = measureDim.height + - paddingAndBorderAxisColumn; - } + + node.layout.measuredDimensions[DIMENSION_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, + (widthMeasureMode == CSSMeasureMode.UNDEFINED || widthMeasureMode == CSSMeasureMode.AT_MOST) ? + measureDim.width + paddingAndBorderAxisRow : + availableWidth - marginAxisRow); + node.layout.measuredDimensions[DIMENSION_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, + (heightMeasureMode == CSSMeasureMode.UNDEFINED || heightMeasureMode == CSSMeasureMode.AT_MOST) ? + measureDim.height + paddingAndBorderAxisColumn : + availableHeight - marginAxisColumn); + } + + return; + } + + // For nodes with no children, use the available values if they were provided, or + // the minimum size as indicated by the padding and border sizes. + int childCount = node.getChildCount(); + if (childCount == 0) { + node.layout.measuredDimensions[DIMENSION_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, + (widthMeasureMode == CSSMeasureMode.UNDEFINED || widthMeasureMode == CSSMeasureMode.AT_MOST) ? + paddingAndBorderAxisRow : + availableWidth - marginAxisRow); + node.layout.measuredDimensions[DIMENSION_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, + (heightMeasureMode == CSSMeasureMode.UNDEFINED || heightMeasureMode == CSSMeasureMode.AT_MOST) ? + paddingAndBorderAxisColumn : + availableHeight - marginAxisColumn); + return; + } + + // If we're not being asked to perform a full layout, we can handle a number of common + // cases here without incurring the cost of the remaining function. + if (!performLayout) { + // If we're being asked to size the content with an at most constraint but there is no available width, + // the measurement will always be zero. + if (widthMeasureMode == CSSMeasureMode.AT_MOST && availableWidth <= 0 && + heightMeasureMode == CSSMeasureMode.AT_MOST && availableHeight <= 0) { + node.layout.measuredDimensions[DIMENSION_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, 0); + node.layout.measuredDimensions[DIMENSION_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, 0); + return; } - if (childCount == 0) { + + if (widthMeasureMode == CSSMeasureMode.AT_MOST && availableWidth <= 0) { + node.layout.measuredDimensions[DIMENSION_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, 0); + node.layout.measuredDimensions[DIMENSION_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, Float.isNaN(availableHeight) ? 0 : (availableHeight - marginAxisColumn)); + return; + } + + if (heightMeasureMode == CSSMeasureMode.AT_MOST && availableHeight <= 0) { + node.layout.measuredDimensions[DIMENSION_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, Float.isNaN(availableWidth) ? 0 : (availableWidth - marginAxisRow)); + node.layout.measuredDimensions[DIMENSION_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, 0); + return; + } + + // If we're being asked to use an exact width/height, there's no need to measure the children. + if (widthMeasureMode == CSSMeasureMode.EXACTLY && heightMeasureMode == CSSMeasureMode.EXACTLY) { + node.layout.measuredDimensions[DIMENSION_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, availableWidth - marginAxisRow); + node.layout.measuredDimensions[DIMENSION_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, availableHeight - marginAxisColumn); return; } } + // STEP 1: CALCULATE VALUES FOR REMAINDER OF ALGORITHM + int mainAxis = resolveAxis(getFlexDirection(node), direction); + int crossAxis = getCrossFlexDirection(mainAxis, direction); + boolean isMainAxisRow = (mainAxis == CSS_FLEX_DIRECTION_ROW || mainAxis == CSS_FLEX_DIRECTION_ROW_REVERSE); + CSSJustify justifyContent = node.style.justifyContent; boolean isNodeFlexWrap = (node.style.flexWrap == CSSWrap.WRAP); - CSSJustify justifyContent = node.style.justifyContent; + CSSNode firstAbsoluteChild = null; + CSSNode currentAbsoluteChild = null; float leadingPaddingAndBorderMain = (node.style.padding.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + node.style.border.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis])); + float trailingPaddingAndBorderMain = (node.style.padding.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]) + node.style.border.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis])); float leadingPaddingAndBorderCross = (node.style.padding.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + node.style.border.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis])); float paddingAndBorderAxisMain = ((node.style.padding.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + node.style.border.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis])) + (node.style.padding.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]) + node.style.border.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]))); float paddingAndBorderAxisCross = ((node.style.padding.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + node.style.border.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis])) + (node.style.padding.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis]) + node.style.border.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis]))); + + CSSMeasureMode measureModeMainDim = isMainAxisRow ? widthMeasureMode : heightMeasureMode; + CSSMeasureMode measureModeCrossDim = isMainAxisRow ? heightMeasureMode : widthMeasureMode; - boolean isMainDimDefined = (!Float.isNaN(node.layout.dimensions[dim[mainAxis]]) && node.layout.dimensions[dim[mainAxis]] >= 0.0); - boolean isCrossDimDefined = (!Float.isNaN(node.layout.dimensions[dim[crossAxis]]) && node.layout.dimensions[dim[crossAxis]] >= 0.0); - boolean isMainRowDirection = (mainAxis == CSS_FLEX_DIRECTION_ROW || mainAxis == CSS_FLEX_DIRECTION_ROW_REVERSE); + // STEP 2: DETERMINE AVAILABLE SIZE IN MAIN AND CROSS DIRECTIONS + float availableInnerWidth = availableWidth - marginAxisRow - paddingAndBorderAxisRow; + float availableInnerHeight = availableHeight - marginAxisColumn - paddingAndBorderAxisColumn; + float availableInnerMainDim = isMainAxisRow ? availableInnerWidth : availableInnerHeight; + float availableInnerCrossDim = isMainAxisRow ? availableInnerHeight : availableInnerWidth; - int i; - int ii; + // STEP 3: DETERMINE FLEX BASIS FOR EACH ITEM CSSNode child; - int axis; - - CSSNode firstAbsoluteChild = null; - CSSNode currentAbsoluteChild = null; - - float definedMainDim = CSSConstants.UNDEFINED; - if (isMainDimDefined) { - definedMainDim = node.layout.dimensions[dim[mainAxis]] - paddingAndBorderAxisMain; - } - - // We want to execute the next two loops one per line with flex-wrap - int startLine = 0; - int endLine = 0; - // int nextOffset = 0; - int alreadyComputedNextLayout = 0; - // We aggregate the total dimensions of the container in those two variables - float linesCrossDim = 0; - float linesMainDim = 0; - int linesCount = 0; - while (endLine < childCount) { - // Layout non flexible children and count children by type - - // mainContentDim is accumulation of the dimensions and margin of all the - // non flexible children. This will be used in order to either set the - // dimensions of the node if none already exist, or to compute the - // remaining space left for the flexible children. - float mainContentDim = 0; - - // There are three kind of children, non flexible, flexible and absolute. - // We need to know how many there are in order to distribute the space. - int flexibleChildrenCount = 0; - float totalFlexible = 0; - int nonFlexibleChildrenCount = 0; - - // Use the line loop to position children in the main axis for as long - // as they are using a simple stacking behaviour. Children that are - // immediately stacked in the initial loop will not be touched again - // in . - boolean isSimpleStackMain = - (isMainDimDefined && justifyContent == CSSJustify.FLEX_START) || - (!isMainDimDefined && justifyContent != CSSJustify.CENTER); - int firstComplexMain = (isSimpleStackMain ? childCount : startLine); - - // Use the initial line loop to position children in the cross axis for - // as long as they are relatively positioned with alignment STRETCH or - // FLEX_START. Children that are immediately stacked in the initial loop - // will not be touched again in . - boolean isSimpleStackCross = true; - int firstComplexCross = childCount; - - CSSNode firstFlexChild = null; - CSSNode currentFlexChild = null; - - float mainDim = leadingPaddingAndBorderMain; - float crossDim = 0; - - float maxWidth = CSSConstants.UNDEFINED; - float maxHeight = CSSConstants.UNDEFINED; - for (i = startLine; i < childCount; ++i) { - child = node.getChildAt(i); - child.lineIndex = linesCount; - - child.nextAbsoluteChild = null; - child.nextFlexChild = null; - - CSSAlign alignItem = getAlignItem(node, child); - - // Pre-fill cross axis dimensions when the child is using stretch before - // we call the recursive layout pass - if (alignItem == CSSAlign.STRETCH && - child.style.positionType == CSSPositionType.RELATIVE && - isCrossDimDefined && - !(!Float.isNaN(child.style.dimensions[dim[crossAxis]]) && child.style.dimensions[dim[crossAxis]] >= 0.0)) { - child.layout.dimensions[dim[crossAxis]] = Math.max( - boundAxis(child, crossAxis, node.layout.dimensions[dim[crossAxis]] - - paddingAndBorderAxisCross - (child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + child.style.margin.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis]))), - // You never want to go smaller than padding - ((child.style.padding.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + child.style.border.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis])) + (child.style.padding.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis]) + child.style.border.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis]))) - ); - } else if (child.style.positionType == CSSPositionType.ABSOLUTE) { - // Store a private linked list of absolutely positioned children - // so that we can efficiently traverse them later. - if (firstAbsoluteChild == null) { - firstAbsoluteChild = child; - } - if (currentAbsoluteChild != null) { - currentAbsoluteChild.nextAbsoluteChild = child; - } - currentAbsoluteChild = child; - - // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both - // left and right or top and bottom). - for (ii = 0; ii < 2; ii++) { - axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; - if ((!Float.isNaN(node.layout.dimensions[dim[axis]]) && node.layout.dimensions[dim[axis]] >= 0.0) && - !(!Float.isNaN(child.style.dimensions[dim[axis]]) && child.style.dimensions[dim[axis]] >= 0.0) && - !Float.isNaN(child.style.position[leading[axis]]) && - !Float.isNaN(child.style.position[trailing[axis]])) { - child.layout.dimensions[dim[axis]] = Math.max( - boundAxis(child, axis, node.layout.dimensions[dim[axis]] - - ((node.style.padding.getWithFallback(leadingSpacing[axis], leading[axis]) + node.style.border.getWithFallback(leadingSpacing[axis], leading[axis])) + (node.style.padding.getWithFallback(trailingSpacing[axis], trailing[axis]) + node.style.border.getWithFallback(trailingSpacing[axis], trailing[axis]))) - - (child.style.margin.getWithFallback(leadingSpacing[axis], leading[axis]) + child.style.margin.getWithFallback(trailingSpacing[axis], trailing[axis])) - - (Float.isNaN(child.style.position[leading[axis]]) ? 0 : child.style.position[leading[axis]]) - - (Float.isNaN(child.style.position[trailing[axis]]) ? 0 : child.style.position[trailing[axis]])), - // You never want to go smaller than padding - ((child.style.padding.getWithFallback(leadingSpacing[axis], leading[axis]) + child.style.border.getWithFallback(leadingSpacing[axis], leading[axis])) + (child.style.padding.getWithFallback(trailingSpacing[axis], trailing[axis]) + child.style.border.getWithFallback(trailingSpacing[axis], trailing[axis]))) - ); - } - } + int i; + float childWidth; + float childHeight; + CSSMeasureMode childWidthMeasureMode; + CSSMeasureMode childHeightMeasureMode; + for (i = 0; i < childCount; i++) { + child = node.getChildAt(i); + + if (performLayout) { + // Set the initial position (relative to the parent). + CSSDirection childDirection = resolveDirection(child, direction); + setPosition(child, childDirection); + } + + // Absolute-positioned children don't participate in flex layout. Add them + // to a list that we can process later. + if (child.style.positionType == CSSPositionType.ABSOLUTE) { + + // Store a private linked list of absolutely positioned children + // so that we can efficiently traverse them later. + if (firstAbsoluteChild == null) { + firstAbsoluteChild = child; } - - float nextContentDim = 0; - - // It only makes sense to consider a child flexible if we have a computed - // dimension for the node. - if (isMainDimDefined && (child.style.positionType == CSSPositionType.RELATIVE && child.style.flex > 0)) { - flexibleChildrenCount++; - totalFlexible += child.style.flex; - - // Store a private linked list of flexible children so that we can - // efficiently traverse them later. - if (firstFlexChild == null) { - firstFlexChild = child; + if (currentAbsoluteChild != null) { + currentAbsoluteChild.nextChild = child; + } + currentAbsoluteChild = child; + child.nextChild = null; + } else { + + if (isMainAxisRow && (child.style.dimensions[dim[CSS_FLEX_DIRECTION_ROW]] >= 0.0)) { + + // The width is definite, so use that as the flex basis. + child.layout.flexBasis = Math.max(child.style.dimensions[DIMENSION_WIDTH], ((child.style.padding.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_ROW], leading[CSS_FLEX_DIRECTION_ROW]) + child.style.border.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_ROW], leading[CSS_FLEX_DIRECTION_ROW])) + (child.style.padding.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_ROW], trailing[CSS_FLEX_DIRECTION_ROW]) + child.style.border.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_ROW], trailing[CSS_FLEX_DIRECTION_ROW])))); + } else if (!isMainAxisRow && (child.style.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] >= 0.0)) { + + // The height is definite, so use that as the flex basis. + child.layout.flexBasis = Math.max(child.style.dimensions[DIMENSION_HEIGHT], ((child.style.padding.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + child.style.border.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN])) + (child.style.padding.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN]) + child.style.border.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN])))); + } else if (!isFlexBasisAuto(child) && !Float.isNaN(availableInnerMainDim)) { + + // If the basis isn't 'auto', it is assumed to be zero. + child.layout.flexBasis = Math.max(0, ((child.style.padding.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + child.style.border.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis])) + (child.style.padding.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]) + child.style.border.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis])))); + } else { + + // Compute the flex basis and hypothetical main size (i.e. the clamped flex basis). + childWidth = CSSConstants.UNDEFINED; + childHeight = CSSConstants.UNDEFINED; + childWidthMeasureMode = CSSMeasureMode.UNDEFINED; + childHeightMeasureMode = CSSMeasureMode.UNDEFINED; + + if ((child.style.dimensions[dim[CSS_FLEX_DIRECTION_ROW]] >= 0.0)) { + childWidth = child.style.dimensions[DIMENSION_WIDTH] + (child.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_ROW], leading[CSS_FLEX_DIRECTION_ROW]) + child.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_ROW], trailing[CSS_FLEX_DIRECTION_ROW])); + childWidthMeasureMode = CSSMeasureMode.EXACTLY; } - if (currentFlexChild != null) { - currentFlexChild.nextFlexChild = child; + if ((child.style.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] >= 0.0)) { + childHeight = child.style.dimensions[DIMENSION_HEIGHT] + (child.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + child.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN])); + childHeightMeasureMode = CSSMeasureMode.EXACTLY; } - currentFlexChild = child; - - // Even if we don't know its exact size yet, we already know the padding, - // border and margin. We'll use this partial information, which represents - // the smallest possible size for the child, to compute the remaining - // available space. - nextContentDim = ((child.style.padding.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + child.style.border.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis])) + (child.style.padding.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]) + child.style.border.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]))) + - (child.style.margin.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + child.style.margin.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis])); - - } else { - maxWidth = CSSConstants.UNDEFINED; - maxHeight = CSSConstants.UNDEFINED; - - if (!isMainRowDirection) { - if ((!Float.isNaN(node.layout.dimensions[dim[resolvedRowAxis]]) && node.layout.dimensions[dim[resolvedRowAxis]] >= 0.0)) { - maxWidth = node.layout.dimensions[dim[resolvedRowAxis]] - - paddingAndBorderAxisResolvedRow; - } else { - maxWidth = parentMaxWidth - - (node.style.margin.getWithFallback(leadingSpacing[resolvedRowAxis], leading[resolvedRowAxis]) + node.style.margin.getWithFallback(trailingSpacing[resolvedRowAxis], trailing[resolvedRowAxis])) - - paddingAndBorderAxisResolvedRow; - } - } else { - if ((!Float.isNaN(node.layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]]) && node.layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] >= 0.0)) { - maxHeight = node.layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] - - paddingAndBorderAxisColumn; - } else { - maxHeight = parentMaxHeight - - (node.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + node.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN])) - - paddingAndBorderAxisColumn; - } + + // According to the spec, if the main size is not definite and the + // child's inline axis is parallel to the main axis (i.e. it's + // horizontal), the child should be sized using "UNDEFINED" in + // the main size. Otherwise use "AT_MOST" in the cross axis. + if (!isMainAxisRow && Float.isNaN(childWidth) && !Float.isNaN(availableInnerWidth)) { + childWidth = availableInnerWidth; + childWidthMeasureMode = CSSMeasureMode.AT_MOST; } - // This is the main recursive call. We layout non flexible children. - if (alreadyComputedNextLayout == 0) { - layoutNode(layoutContext, child, maxWidth, maxHeight, direction); + // The W3C spec doesn't say anything about the 'overflow' property, + // but all major browsers appear to implement the following logic. + if (node.style.overflow == CSSOverflow.HIDDEN) { + if (isMainAxisRow && Float.isNaN(childHeight) && !Float.isNaN(availableInnerHeight)) { + childHeight = availableInnerHeight; + childHeightMeasureMode = CSSMeasureMode.AT_MOST; + } } - // Absolute positioned elements do not take part of the layout, so we - // don't use them to compute mainContentDim - if (child.style.positionType == CSSPositionType.RELATIVE) { - nonFlexibleChildrenCount++; - // At this point we know the final size and margin of the element. - nextContentDim = (child.layout.dimensions[dim[mainAxis]] + child.style.margin.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + child.style.margin.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis])); - } + // Measure the child + layoutNodeInternal(layoutContext, child, childWidth, childHeight, direction, childWidthMeasureMode, childHeightMeasureMode, false, "measure"); + + child.layout.flexBasis = Math.max(isMainAxisRow ? child.layout.measuredDimensions[DIMENSION_WIDTH] : child.layout.measuredDimensions[DIMENSION_HEIGHT], ((child.style.padding.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + child.style.border.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis])) + (child.style.padding.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]) + child.style.border.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis])))); } + } + } - // The element we are about to add would make us go to the next line - if (isNodeFlexWrap && - isMainDimDefined && - mainContentDim + nextContentDim > definedMainDim && - // If there's only one element, then it's bigger than the content - // and needs its own line - i != startLine) { - nonFlexibleChildrenCount--; - alreadyComputedNextLayout = 1; - break; - } + // STEP 4: COLLECT FLEX ITEMS INTO FLEX LINES + + // Indexes of children that represent the first and last items in the line. + int startOfLineIndex = 0; + int endOfLineIndex = 0; + + // Number of lines. + int lineCount = 0; + + // Accumulated cross dimensions of all lines so far. + float totalLineCrossDim = 0; + + // Max main dimension of all the lines. + float maxLineMainDim = 0; + + while (endOfLineIndex < childCount) { + + // Number of items on the currently line. May be different than the difference + // between start and end indicates because we skip over absolute-positioned items. + int itemsOnLine = 0; + + // sizeConsumedOnCurrentLine is accumulation of the dimensions and margin + // of all the children on the current line. This will be used in order to + // either set the dimensions of the node if none already exist or to compute + // the remaining space left for the flexible children. + float sizeConsumedOnCurrentLine = 0; + + float totalFlexGrowFactors = 0; + float totalFlexShrinkScaledFactors = 0; + + i = startOfLineIndex; + + // Maintain a linked list of the child nodes that can shrink and/or grow. + CSSNode firstRelativeChild = null; + CSSNode currentRelativeChild = null; + + // Add items to the current line until it's full or we run out of items. + while (i < childCount) { + child = node.getChildAt(i); + child.lineIndex = lineCount; - // Disable simple stacking in the main axis for the current line as - // we found a non-trivial child. The remaining children will be laid out - // in . - if (isSimpleStackMain && - (child.style.positionType != CSSPositionType.RELATIVE || (child.style.positionType == CSSPositionType.RELATIVE && child.style.flex > 0))) { - isSimpleStackMain = false; - firstComplexMain = i; - } + if (child.style.positionType != CSSPositionType.ABSOLUTE) { + float outerFlexBasis = child.layout.flexBasis + (child.style.margin.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + child.style.margin.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis])); + + // If this is a multi-line flow and this item pushes us over the available size, we've + // hit the end of the current line. Break out of the loop and lay out the current line. + if (sizeConsumedOnCurrentLine + outerFlexBasis > availableInnerMainDim && isNodeFlexWrap && itemsOnLine > 0) { + break; + } - // Disable simple stacking in the cross axis for the current line as - // we found a non-trivial child. The remaining children will be laid out - // in . - if (isSimpleStackCross && - (child.style.positionType != CSSPositionType.RELATIVE || - (alignItem != CSSAlign.STRETCH && alignItem != CSSAlign.FLEX_START) || - (alignItem == CSSAlign.STRETCH && !isCrossDimDefined))) { - isSimpleStackCross = false; - firstComplexCross = i; - } + sizeConsumedOnCurrentLine += outerFlexBasis; + itemsOnLine++; - if (isSimpleStackMain) { - child.layout.position[pos[mainAxis]] += mainDim; - if (isMainDimDefined) { - child.layout.position[trailing[mainAxis]] = node.layout.dimensions[dim[mainAxis]] - child.layout.dimensions[dim[mainAxis]] - child.layout.position[pos[mainAxis]]; + if ((child.style.positionType == CSSPositionType.RELATIVE && child.style.flex != 0)) { + totalFlexGrowFactors += getFlexGrowFactor(child); + + // Unlike the grow factor, the shrink factor is scaled relative to the child + // dimension. + totalFlexShrinkScaledFactors += getFlexShrinkFactor(child) * child.layout.flexBasis; } - mainDim += (child.layout.dimensions[dim[mainAxis]] + child.style.margin.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + child.style.margin.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis])); - crossDim = Math.max(crossDim, boundAxis(child, crossAxis, (child.layout.dimensions[dim[crossAxis]] + child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + child.style.margin.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis])))); - } - - if (isSimpleStackCross) { - child.layout.position[pos[crossAxis]] += linesCrossDim + leadingPaddingAndBorderCross; - if (isCrossDimDefined) { - child.layout.position[trailing[crossAxis]] = node.layout.dimensions[dim[crossAxis]] - child.layout.dimensions[dim[crossAxis]] - child.layout.position[pos[crossAxis]]; + // Store a private linked list of children that need to be layed out. + if (firstRelativeChild == null) { + firstRelativeChild = child; } + if (currentRelativeChild != null) { + currentRelativeChild.nextChild = child; + } + currentRelativeChild = child; + child.nextChild = null; } - - alreadyComputedNextLayout = 0; - mainContentDim += nextContentDim; - endLine = i + 1; + + i++; + endOfLineIndex++; } - - // Layout flexible children and allocate empty space + + // If we don't need to measure the cross axis, we can skip the entire flex step. + boolean canSkipFlex = !performLayout && measureModeCrossDim == CSSMeasureMode.EXACTLY; // In order to position the elements in the main axis, we have two // controls. The space between the beginning and the first element @@ -579,212 +795,300 @@ private static void layoutNodeImpl( float leadingMainDim = 0; float betweenMainDim = 0; - // The remaining available space that needs to be allocated - float remainingMainDim = 0; - if (isMainDimDefined) { - remainingMainDim = definedMainDim - mainContentDim; - } else { - remainingMainDim = Math.max(mainContentDim, 0) - mainContentDim; + // STEP 5: RESOLVING FLEXIBLE LENGTHS ON MAIN AXIS + // Calculate the remaining available space that needs to be allocated. + // If the main dimension size isn't known, it is computed based on + // the line length, so there's no more space left to distribute. + float remainingFreeSpace = 0; + if (!Float.isNaN(availableInnerMainDim)) { + remainingFreeSpace = availableInnerMainDim - sizeConsumedOnCurrentLine; + } else if (sizeConsumedOnCurrentLine < 0) { + // availableInnerMainDim is indefinite which means the node is being sized based on its content. + // sizeConsumedOnCurrentLine is negative which means the node will allocate 0 pixels for + // its content. Consequently, remainingFreeSpace is 0 - sizeConsumedOnCurrentLine. + remainingFreeSpace = -sizeConsumedOnCurrentLine; } - - // If there are flexible children in the mix, they are going to fill the - // remaining space - if (flexibleChildrenCount != 0) { - float flexibleMainDim = remainingMainDim / totalFlexible; - float baseMainDim; - float boundMainDim; - - // If the flex share of remaining space doesn't meet min/max bounds, - // remove this child from flex calculations. - currentFlexChild = firstFlexChild; - while (currentFlexChild != null) { - baseMainDim = flexibleMainDim * currentFlexChild.style.flex + - ((currentFlexChild.style.padding.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + currentFlexChild.style.border.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis])) + (currentFlexChild.style.padding.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]) + currentFlexChild.style.border.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]))); - boundMainDim = boundAxis(currentFlexChild, mainAxis, baseMainDim); - - if (baseMainDim != boundMainDim) { - remainingMainDim -= boundMainDim; - totalFlexible -= currentFlexChild.style.flex; + + float originalRemainingFreeSpace = remainingFreeSpace; + float deltaFreeSpace = 0; + + if (!canSkipFlex) { + float childFlexBasis; + float flexShrinkScaledFactor; + float flexGrowFactor; + float baseMainSize; + float boundMainSize; + + // Do two passes over the flex items to figure out how to distribute the remaining space. + // The first pass finds the items whose min/max constraints trigger, freezes them at those + // sizes, and excludes those sizes from the remaining space. The second pass sets the size + // of each flexible item. It distributes the remaining space amongst the items whose min/max + // constraints didn't trigger in pass 1. For the other items, it sets their sizes by forcing + // their min/max constraints to trigger again. + // + // This two pass approach for resolving min/max constraints deviates from the spec. The + // spec (https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths) describes a process + // that needs to be repeated a variable number of times. The algorithm implemented here + // won't handle all cases but it was simpler to implement and it mitigates performance + // concerns because we know exactly how many passes it'll do. + + // First pass: detect the flex items whose min/max constraints trigger + float deltaFlexShrinkScaledFactors = 0; + float deltaFlexGrowFactors = 0; + currentRelativeChild = firstRelativeChild; + while (currentRelativeChild != null) { + childFlexBasis = currentRelativeChild.layout.flexBasis; + + if (remainingFreeSpace < 0) { + flexShrinkScaledFactor = getFlexShrinkFactor(currentRelativeChild) * childFlexBasis; + + // Is this child able to shrink? + if (flexShrinkScaledFactor != 0) { + baseMainSize = childFlexBasis + + remainingFreeSpace / totalFlexShrinkScaledFactors * flexShrinkScaledFactor; + boundMainSize = boundAxis(currentRelativeChild, mainAxis, baseMainSize); + if (baseMainSize != boundMainSize) { + // By excluding this item's size and flex factor from remaining, this item's + // min/max constraints should also trigger in the second pass resulting in the + // item's size calculation being identical in the first and second passes. + deltaFreeSpace -= boundMainSize - childFlexBasis; + deltaFlexShrinkScaledFactors -= flexShrinkScaledFactor; + } + } + } else if (remainingFreeSpace > 0) { + flexGrowFactor = getFlexGrowFactor(currentRelativeChild); + + // Is this child able to grow? + if (flexGrowFactor != 0) { + baseMainSize = childFlexBasis + + remainingFreeSpace / totalFlexGrowFactors * flexGrowFactor; + boundMainSize = boundAxis(currentRelativeChild, mainAxis, baseMainSize); + if (baseMainSize != boundMainSize) { + // By excluding this item's size and flex factor from remaining, this item's + // min/max constraints should also trigger in the second pass resulting in the + // item's size calculation being identical in the first and second passes. + deltaFreeSpace -= boundMainSize - childFlexBasis; + deltaFlexGrowFactors -= flexGrowFactor; + } + } } - - currentFlexChild = currentFlexChild.nextFlexChild; - } - flexibleMainDim = remainingMainDim / totalFlexible; - - // The non flexible children can overflow the container, in this case - // we should just assume that there is no space available. - if (flexibleMainDim < 0) { - flexibleMainDim = 0; + + currentRelativeChild = currentRelativeChild.nextChild; } + + totalFlexShrinkScaledFactors += deltaFlexShrinkScaledFactors; + totalFlexGrowFactors += deltaFlexGrowFactors; + remainingFreeSpace += deltaFreeSpace; + + // Second pass: resolve the sizes of the flexible items + deltaFreeSpace = 0; + currentRelativeChild = firstRelativeChild; + while (currentRelativeChild != null) { + childFlexBasis = currentRelativeChild.layout.flexBasis; + float updatedMainSize = childFlexBasis; + + if (remainingFreeSpace < 0) { + flexShrinkScaledFactor = getFlexShrinkFactor(currentRelativeChild) * childFlexBasis; + + // Is this child able to shrink? + if (flexShrinkScaledFactor != 0) { + updatedMainSize = boundAxis(currentRelativeChild, mainAxis, childFlexBasis + + remainingFreeSpace / totalFlexShrinkScaledFactors * flexShrinkScaledFactor); + } + } else if (remainingFreeSpace > 0) { + flexGrowFactor = getFlexGrowFactor(currentRelativeChild); - currentFlexChild = firstFlexChild; - while (currentFlexChild != null) { - // At this point we know the final size of the element in the main - // dimension - currentFlexChild.layout.dimensions[dim[mainAxis]] = boundAxis(currentFlexChild, mainAxis, - flexibleMainDim * currentFlexChild.style.flex + - ((currentFlexChild.style.padding.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + currentFlexChild.style.border.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis])) + (currentFlexChild.style.padding.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]) + currentFlexChild.style.border.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]))) - ); - - maxWidth = CSSConstants.UNDEFINED; - if ((!Float.isNaN(node.layout.dimensions[dim[resolvedRowAxis]]) && node.layout.dimensions[dim[resolvedRowAxis]] >= 0.0)) { - maxWidth = node.layout.dimensions[dim[resolvedRowAxis]] - - paddingAndBorderAxisResolvedRow; - } else if (!isMainRowDirection) { - maxWidth = parentMaxWidth - - (node.style.margin.getWithFallback(leadingSpacing[resolvedRowAxis], leading[resolvedRowAxis]) + node.style.margin.getWithFallback(trailingSpacing[resolvedRowAxis], trailing[resolvedRowAxis])) - - paddingAndBorderAxisResolvedRow; + // Is this child able to grow? + if (flexGrowFactor != 0) { + updatedMainSize = boundAxis(currentRelativeChild, mainAxis, childFlexBasis + + remainingFreeSpace / totalFlexGrowFactors * flexGrowFactor); + } } - maxHeight = CSSConstants.UNDEFINED; - if ((!Float.isNaN(node.layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]]) && node.layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] >= 0.0)) { - maxHeight = node.layout.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] - - paddingAndBorderAxisColumn; - } else if (isMainRowDirection) { - maxHeight = parentMaxHeight - - (node.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + node.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN])) - - paddingAndBorderAxisColumn; + + deltaFreeSpace -= updatedMainSize - childFlexBasis; + + if (isMainAxisRow) { + childWidth = updatedMainSize + (currentRelativeChild.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_ROW], leading[CSS_FLEX_DIRECTION_ROW]) + currentRelativeChild.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_ROW], trailing[CSS_FLEX_DIRECTION_ROW])); + childWidthMeasureMode = CSSMeasureMode.EXACTLY; + + if (!(currentRelativeChild.style.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] >= 0.0)) { + childHeight = availableInnerCrossDim; + childHeightMeasureMode = Float.isNaN(childHeight) ? CSSMeasureMode.UNDEFINED : CSSMeasureMode.AT_MOST; + } else { + childHeight = currentRelativeChild.style.dimensions[DIMENSION_HEIGHT] + (currentRelativeChild.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + currentRelativeChild.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN])); + childHeightMeasureMode = CSSMeasureMode.EXACTLY; + } + } else { + childHeight = updatedMainSize + (currentRelativeChild.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + currentRelativeChild.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN])); + childHeightMeasureMode = CSSMeasureMode.EXACTLY; + + if (!(currentRelativeChild.style.dimensions[dim[CSS_FLEX_DIRECTION_ROW]] >= 0.0)) { + childWidth = availableInnerCrossDim; + childWidthMeasureMode = Float.isNaN(childWidth) ? CSSMeasureMode.UNDEFINED : CSSMeasureMode.AT_MOST; + } else { + childWidth = currentRelativeChild.style.dimensions[DIMENSION_WIDTH] + (currentRelativeChild.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_ROW], leading[CSS_FLEX_DIRECTION_ROW]) + currentRelativeChild.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_ROW], trailing[CSS_FLEX_DIRECTION_ROW])); + childWidthMeasureMode = CSSMeasureMode.EXACTLY; + } } + + boolean requiresStretchLayout = !(currentRelativeChild.style.dimensions[dim[crossAxis]] >= 0.0) && + getAlignItem(node, currentRelativeChild) == CSSAlign.STRETCH; - // And we recursively call the layout algorithm for this child - layoutNode(layoutContext, currentFlexChild, maxWidth, maxHeight, direction); + // Recursively call the layout algorithm for this child with the updated main size. + layoutNodeInternal(layoutContext, currentRelativeChild, childWidth, childHeight, direction, childWidthMeasureMode, childHeightMeasureMode, performLayout && !requiresStretchLayout, "flex"); - child = currentFlexChild; - currentFlexChild = currentFlexChild.nextFlexChild; - child.nextFlexChild = null; + currentRelativeChild = currentRelativeChild.nextChild; } + } + + remainingFreeSpace = originalRemainingFreeSpace + deltaFreeSpace; + + // STEP 6: MAIN-AXIS JUSTIFICATION & CROSS-AXIS SIZE DETERMINATION + + // At this point, all the children have their dimensions set in the main axis. + // Their dimensions are also set in the cross axis with the exception of items + // that are aligned "stretch". We need to compute these stretch values and + // set the final positions. + + // If we are using "at most" rules in the main axis, we won't distribute + // any remaining space at this point. + if (measureModeMainDim == CSSMeasureMode.AT_MOST) { + remainingFreeSpace = 0; + } - // We use justifyContent to figure out how to allocate the remaining - // space available - } else if (justifyContent != CSSJustify.FLEX_START) { + // Use justifyContent to figure out how to allocate the remaining space + // available in the main axis. + if (justifyContent != CSSJustify.FLEX_START) { if (justifyContent == CSSJustify.CENTER) { - leadingMainDim = remainingMainDim / 2; + leadingMainDim = remainingFreeSpace / 2; } else if (justifyContent == CSSJustify.FLEX_END) { - leadingMainDim = remainingMainDim; + leadingMainDim = remainingFreeSpace; } else if (justifyContent == CSSJustify.SPACE_BETWEEN) { - remainingMainDim = Math.max(remainingMainDim, 0); - if (flexibleChildrenCount + nonFlexibleChildrenCount - 1 != 0) { - betweenMainDim = remainingMainDim / - (flexibleChildrenCount + nonFlexibleChildrenCount - 1); + remainingFreeSpace = Math.max(remainingFreeSpace, 0); + if (itemsOnLine > 1) { + betweenMainDim = remainingFreeSpace / (itemsOnLine - 1); } else { betweenMainDim = 0; } } else if (justifyContent == CSSJustify.SPACE_AROUND) { // Space on the edges is half of the space between elements - betweenMainDim = remainingMainDim / - (flexibleChildrenCount + nonFlexibleChildrenCount); + betweenMainDim = remainingFreeSpace / itemsOnLine; leadingMainDim = betweenMainDim / 2; } } - // Position elements in the main axis and compute dimensions - - // At this point, all the children have their dimensions set. We need to - // find their position. In order to do that, we accumulate data in - // variables that are also useful to compute the total dimensions of the - // container! - mainDim += leadingMainDim; + float mainDim = leadingPaddingAndBorderMain + leadingMainDim; + float crossDim = 0; - for (i = firstComplexMain; i < endLine; ++i) { + for (i = startOfLineIndex; i < endOfLineIndex; ++i) { child = node.getChildAt(i); if (child.style.positionType == CSSPositionType.ABSOLUTE && !Float.isNaN(child.style.position[leading[mainAxis]])) { - // In case the child is position absolute and has left/top being - // defined, we override the position to whatever the user said - // (and margin/border). - child.layout.position[pos[mainAxis]] = (Float.isNaN(child.style.position[leading[mainAxis]]) ? 0 : child.style.position[leading[mainAxis]]) + - node.style.border.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + - child.style.margin.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]); + if (performLayout) { + // In case the child is position absolute and has left/top being + // defined, we override the position to whatever the user said + // (and margin/border). + child.layout.position[pos[mainAxis]] = (Float.isNaN(child.style.position[leading[mainAxis]]) ? 0 : child.style.position[leading[mainAxis]]) + + node.style.border.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + + child.style.margin.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]); + } } else { - // If the child is position absolute (without top/left) or relative, - // we put it at the current accumulated offset. - child.layout.position[pos[mainAxis]] += mainDim; - - // Define the trailing position accordingly. - if (isMainDimDefined) { - child.layout.position[trailing[mainAxis]] = node.layout.dimensions[dim[mainAxis]] - child.layout.dimensions[dim[mainAxis]] - child.layout.position[pos[mainAxis]]; + if (performLayout) { + // If the child is position absolute (without top/left) or relative, + // we put it at the current accumulated offset. + child.layout.position[pos[mainAxis]] += mainDim; } - - // Now that we placed the element, we need to update the variables - // We only need to do that for relative elements. Absolute elements + + // Now that we placed the element, we need to update the variables. + // We need to do that only for relative elements. Absolute elements // do not take part in that phase. if (child.style.positionType == CSSPositionType.RELATIVE) { - // The main dimension is the sum of all the elements dimension plus - // the spacing. - mainDim += betweenMainDim + (child.layout.dimensions[dim[mainAxis]] + child.style.margin.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + child.style.margin.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis])); - // The cross dimension is the max of the elements dimension since there - // can only be one element in that cross dimension. - crossDim = Math.max(crossDim, boundAxis(child, crossAxis, (child.layout.dimensions[dim[crossAxis]] + child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + child.style.margin.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis])))); + if (canSkipFlex) { + // If we skipped the flex step, then we can't rely on the measuredDims because + // they weren't computed. This means we can't call getDimWithMargin. + mainDim += betweenMainDim + (child.style.margin.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + child.style.margin.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis])) + child.layout.flexBasis; + crossDim = availableInnerCrossDim; + } else { + // The main dimension is the sum of all the elements dimension plus + // the spacing. + mainDim += betweenMainDim + (child.layout.measuredDimensions[dim[mainAxis]] + child.style.margin.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + child.style.margin.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis])); + + // The cross dimension is the max of the elements dimension since there + // can only be one element in that cross dimension. + crossDim = Math.max(crossDim, (child.layout.measuredDimensions[dim[crossAxis]] + child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + child.style.margin.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis]))); + } } } } - float containerCrossAxis = node.layout.dimensions[dim[crossAxis]]; - if (!isCrossDimDefined) { - containerCrossAxis = Math.max( - // For the cross dim, we add both sides at the end because the value - // is aggregate via a max function. Intermediate negative values - // can mess this computation otherwise - boundAxis(node, crossAxis, crossDim + paddingAndBorderAxisCross), - paddingAndBorderAxisCross - ); + mainDim += trailingPaddingAndBorderMain; + + float containerCrossAxis = availableInnerCrossDim; + if (measureModeCrossDim == CSSMeasureMode.UNDEFINED || measureModeCrossDim == CSSMeasureMode.AT_MOST) { + // Compute the cross axis from the max cross dimension of the children. + containerCrossAxis = boundAxis(node, crossAxis, crossDim + paddingAndBorderAxisCross) - paddingAndBorderAxisCross; + + if (measureModeCrossDim == CSSMeasureMode.AT_MOST) { + containerCrossAxis = Math.min(containerCrossAxis, availableInnerCrossDim); + } } - // Position elements in the cross axis - for (i = firstComplexCross; i < endLine; ++i) { - child = node.getChildAt(i); - - if (child.style.positionType == CSSPositionType.ABSOLUTE && - !Float.isNaN(child.style.position[leading[crossAxis]])) { - // In case the child is absolutely positionned and has a - // top/left/bottom/right being set, we override all the previously - // computed positions to set it correctly. - child.layout.position[pos[crossAxis]] = (Float.isNaN(child.style.position[leading[crossAxis]]) ? 0 : child.style.position[leading[crossAxis]]) + - node.style.border.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + - child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]); + // If there's no flex wrap, the cross dimension is defined by the container. + if (!isNodeFlexWrap && measureModeCrossDim == CSSMeasureMode.EXACTLY) { + crossDim = availableInnerCrossDim; + } - } else { - float leadingCrossDim = leadingPaddingAndBorderCross; + // Clamp to the min/max size specified on the container. + crossDim = boundAxis(node, crossAxis, crossDim + paddingAndBorderAxisCross) - paddingAndBorderAxisCross; + + // STEP 7: CROSS-AXIS ALIGNMENT + // We can skip child alignment if we're just measuring the container. + if (performLayout) { + for (i = startOfLineIndex; i < endOfLineIndex; ++i) { + child = node.getChildAt(i); + + if (child.style.positionType == CSSPositionType.ABSOLUTE) { + // If the child is absolutely positioned and has a top/left/bottom/right + // set, override all the previously computed positions to set it correctly. + if (!Float.isNaN(child.style.position[leading[crossAxis]])) { + child.layout.position[pos[crossAxis]] = (Float.isNaN(child.style.position[leading[crossAxis]]) ? 0 : child.style.position[leading[crossAxis]]) + + node.style.border.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + + child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]); + } else { + child.layout.position[pos[crossAxis]] = leadingPaddingAndBorderCross + + child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]); + } + } else { + float leadingCrossDim = leadingPaddingAndBorderCross; - // For a relative children, we're either using alignItems (parent) or - // alignSelf (child) in order to determine the position in the cross axis - if (child.style.positionType == CSSPositionType.RELATIVE) { - /*eslint-disable */ - // This variable is intentionally re-defined as the code is transpiled to a block scope language + // For a relative children, we're either using alignItems (parent) or + // alignSelf (child) in order to determine the position in the cross axis CSSAlign alignItem = getAlignItem(node, child); - /*eslint-enable */ + + // If the child uses align stretch, we need to lay it out one more time, this time + // forcing the cross-axis size to be the computed cross size for the current line. if (alignItem == CSSAlign.STRETCH) { - // You can only stretch if the dimension has not already been defined - // previously. - if (!(!Float.isNaN(child.style.dimensions[dim[crossAxis]]) && child.style.dimensions[dim[crossAxis]] >= 0.0)) { - float dimCrossAxis = child.layout.dimensions[dim[crossAxis]]; - child.layout.dimensions[dim[crossAxis]] = Math.max( - boundAxis(child, crossAxis, containerCrossAxis - - paddingAndBorderAxisCross - (child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + child.style.margin.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis]))), - // You never want to go smaller than padding - ((child.style.padding.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + child.style.border.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis])) + (child.style.padding.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis]) + child.style.border.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis]))) - ); - - // If the size has changed, and this child has children we need to re-layout this child - if (dimCrossAxis != child.layout.dimensions[dim[crossAxis]] && child.getChildCount() > 0) { - // Reset child margins before re-layout as they are added back in layoutNode and would be doubled - child.layout.position[leading[mainAxis]] -= child.style.margin.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + - getRelativePosition(child, mainAxis); - child.layout.position[trailing[mainAxis]] -= child.style.margin.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]) + - getRelativePosition(child, mainAxis); - child.layout.position[leading[crossAxis]] -= child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + - getRelativePosition(child, crossAxis); - child.layout.position[trailing[crossAxis]] -= child.style.margin.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis]) + - getRelativePosition(child, crossAxis); - - layoutNode(layoutContext, child, maxWidth, maxHeight, direction); - } + childWidth = child.layout.measuredDimensions[DIMENSION_WIDTH] + (child.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_ROW], leading[CSS_FLEX_DIRECTION_ROW]) + child.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_ROW], trailing[CSS_FLEX_DIRECTION_ROW])); + childHeight = child.layout.measuredDimensions[DIMENSION_HEIGHT] + (child.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + child.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN])); + boolean isCrossSizeDefinite = false; + + if (isMainAxisRow) { + isCrossSizeDefinite = (child.style.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] >= 0.0); + childHeight = crossDim; + } else { + isCrossSizeDefinite = (child.style.dimensions[dim[CSS_FLEX_DIRECTION_ROW]] >= 0.0); + childWidth = crossDim; + } + + // If the child defines a definite size for its cross axis, there's no need to stretch. + if (!isCrossSizeDefinite) { + childWidthMeasureMode = Float.isNaN(childWidth) ? CSSMeasureMode.UNDEFINED : CSSMeasureMode.EXACTLY; + childHeightMeasureMode = Float.isNaN(childHeight) ? CSSMeasureMode.UNDEFINED : CSSMeasureMode.EXACTLY; + layoutNodeInternal(layoutContext, child, childWidth, childHeight, direction, childWidthMeasureMode, childHeightMeasureMode, true, "stretch"); } } else if (alignItem != CSSAlign.FLEX_START) { - // The remaining space between the parent dimensions+padding and child - // dimensions+margin. - float remainingCrossDim = containerCrossAxis - - paddingAndBorderAxisCross - (child.layout.dimensions[dim[crossAxis]] + child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + child.style.margin.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis])); + float remainingCrossDim = containerCrossAxis - (child.layout.measuredDimensions[dim[crossAxis]] + child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + child.style.margin.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis])); if (alignItem == CSSAlign.CENTER) { leadingCrossDim += remainingCrossDim / 2; @@ -792,41 +1096,25 @@ private static void layoutNodeImpl( leadingCrossDim += remainingCrossDim; } } - } - - // And we apply the position - child.layout.position[pos[crossAxis]] += linesCrossDim + leadingCrossDim; - // Define the trailing position accordingly. - if (isCrossDimDefined) { - child.layout.position[trailing[crossAxis]] = node.layout.dimensions[dim[crossAxis]] - child.layout.dimensions[dim[crossAxis]] - child.layout.position[pos[crossAxis]]; + // And we apply the position + child.layout.position[pos[crossAxis]] += totalLineCrossDim + leadingCrossDim; } } } - linesCrossDim += crossDim; - linesMainDim = Math.max(linesMainDim, mainDim); - linesCount += 1; - startLine = endLine; + totalLineCrossDim += crossDim; + maxLineMainDim = Math.max(maxLineMainDim, mainDim); + + // Reset variables for new line. + lineCount++; + startOfLineIndex = endOfLineIndex; + endOfLineIndex = startOfLineIndex; } - // - // - // Note(prenaux): More than one line, we need to layout the crossAxis - // according to alignContent. - // - // Note that we could probably remove and handle the one line case - // here too, but for the moment this is safer since it won't interfere with - // previously working code. - // - // See specs: - // http://www.w3.org/TR/2012/CR-css3-flexbox-20120918/#layout-algorithm - // section 9.4 - // - if (linesCount > 1 && isCrossDimDefined) { - float nodeCrossAxisInnerSize = node.layout.dimensions[dim[crossAxis]] - - paddingAndBorderAxisCross; - float remainingAlignContentDim = nodeCrossAxisInnerSize - linesCrossDim; + // STEP 8: MULTI-LINE CONTENT ALIGNMENT + if (lineCount > 1 && performLayout && !Float.isNaN(availableInnerCrossDim)) { + float remainingAlignContentDim = availableInnerCrossDim - totalLineCrossDim; float crossDimLead = 0; float currentLead = leadingPaddingAndBorderCross; @@ -837,53 +1125,54 @@ private static void layoutNodeImpl( } else if (alignContent == CSSAlign.CENTER) { currentLead += remainingAlignContentDim / 2; } else if (alignContent == CSSAlign.STRETCH) { - if (nodeCrossAxisInnerSize > linesCrossDim) { - crossDimLead = (remainingAlignContentDim / linesCount); + if (availableInnerCrossDim > totalLineCrossDim) { + crossDimLead = (remainingAlignContentDim / lineCount); } } int endIndex = 0; - for (i = 0; i < linesCount; ++i) { + for (i = 0; i < lineCount; ++i) { int startIndex = endIndex; + int j; // compute the line's height and find the endIndex float lineHeight = 0; - for (ii = startIndex; ii < childCount; ++ii) { - child = node.getChildAt(ii); + for (j = startIndex; j < childCount; ++j) { + child = node.getChildAt(j); if (child.style.positionType != CSSPositionType.RELATIVE) { continue; } if (child.lineIndex != i) { break; } - if ((!Float.isNaN(child.layout.dimensions[dim[crossAxis]]) && child.layout.dimensions[dim[crossAxis]] >= 0.0)) { - lineHeight = Math.max( - lineHeight, - child.layout.dimensions[dim[crossAxis]] + (child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + child.style.margin.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis])) - ); + if ((child.layout.measuredDimensions[dim[crossAxis]] >= 0.0)) { + lineHeight = Math.max(lineHeight, + child.layout.measuredDimensions[dim[crossAxis]] + (child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]) + child.style.margin.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis]))); } } - endIndex = ii; + endIndex = j; lineHeight += crossDimLead; - for (ii = startIndex; ii < endIndex; ++ii) { - child = node.getChildAt(ii); - if (child.style.positionType != CSSPositionType.RELATIVE) { - continue; - } + if (performLayout) { + for (j = startIndex; j < endIndex; ++j) { + child = node.getChildAt(j); + if (child.style.positionType != CSSPositionType.RELATIVE) { + continue; + } - CSSAlign alignContentAlignItem = getAlignItem(node, child); - if (alignContentAlignItem == CSSAlign.FLEX_START) { - child.layout.position[pos[crossAxis]] = currentLead + child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]); - } else if (alignContentAlignItem == CSSAlign.FLEX_END) { - child.layout.position[pos[crossAxis]] = currentLead + lineHeight - child.style.margin.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis]) - child.layout.dimensions[dim[crossAxis]]; - } else if (alignContentAlignItem == CSSAlign.CENTER) { - float childHeight = child.layout.dimensions[dim[crossAxis]]; - child.layout.position[pos[crossAxis]] = currentLead + (lineHeight - childHeight) / 2; - } else if (alignContentAlignItem == CSSAlign.STRETCH) { - child.layout.position[pos[crossAxis]] = currentLead + child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]); - // TODO(prenaux): Correctly set the height of items with undefined - // (auto) crossAxis dimension. + CSSAlign alignContentAlignItem = getAlignItem(node, child); + if (alignContentAlignItem == CSSAlign.FLEX_START) { + child.layout.position[pos[crossAxis]] = currentLead + child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]); + } else if (alignContentAlignItem == CSSAlign.FLEX_END) { + child.layout.position[pos[crossAxis]] = currentLead + lineHeight - child.style.margin.getWithFallback(trailingSpacing[crossAxis], trailing[crossAxis]) - child.layout.measuredDimensions[dim[crossAxis]]; + } else if (alignContentAlignItem == CSSAlign.CENTER) { + childHeight = child.layout.measuredDimensions[dim[crossAxis]]; + child.layout.position[pos[crossAxis]] = currentLead + (lineHeight - childHeight) / 2; + } else if (alignContentAlignItem == CSSAlign.STRETCH) { + child.layout.position[pos[crossAxis]] = currentLead + child.style.margin.getWithFallback(leadingSpacing[crossAxis], leading[crossAxis]); + // TODO(prenaux): Correctly set the height of items with indefinite + // (auto) crossAxis dimension. + } } } @@ -891,93 +1180,148 @@ private static void layoutNodeImpl( } } - boolean needsMainTrailingPos = false; - boolean needsCrossTrailingPos = false; + // STEP 9: COMPUTING FINAL DIMENSIONS + node.layout.measuredDimensions[DIMENSION_WIDTH] = boundAxis(node, CSS_FLEX_DIRECTION_ROW, availableWidth - marginAxisRow); + node.layout.measuredDimensions[DIMENSION_HEIGHT] = boundAxis(node, CSS_FLEX_DIRECTION_COLUMN, availableHeight - marginAxisColumn); + + // If the user didn't specify a width or height for the node, set the + // dimensions based on the children. + if (measureModeMainDim == CSSMeasureMode.UNDEFINED) { + // Clamp the size to the min/max size, if specified, and make sure it + // doesn't go below the padding and border amount. + node.layout.measuredDimensions[dim[mainAxis]] = boundAxis(node, mainAxis, maxLineMainDim); + } else if (measureModeMainDim == CSSMeasureMode.AT_MOST) { + node.layout.measuredDimensions[dim[mainAxis]] = Math.max( + Math.min(availableInnerMainDim + paddingAndBorderAxisMain, + boundAxisWithinMinAndMax(node, mainAxis, maxLineMainDim)), + paddingAndBorderAxisMain); + } - // If the user didn't specify a width or height, and it has not been set - // by the container, then we set it via the children. - if (!isMainDimDefined) { - node.layout.dimensions[dim[mainAxis]] = Math.max( - // We're missing the last padding at this point to get the final - // dimension - boundAxis(node, mainAxis, linesMainDim + (node.style.padding.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]) + node.style.border.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]))), - // We can never assign a width smaller than the padding and borders - paddingAndBorderAxisMain - ); + if (measureModeCrossDim == CSSMeasureMode.UNDEFINED) { + // Clamp the size to the min/max size, if specified, and make sure it + // doesn't go below the padding and border amount. + node.layout.measuredDimensions[dim[crossAxis]] = boundAxis(node, crossAxis, totalLineCrossDim + paddingAndBorderAxisCross); + } else if (measureModeCrossDim == CSSMeasureMode.AT_MOST) { + node.layout.measuredDimensions[dim[crossAxis]] = Math.max( + Math.min(availableInnerCrossDim + paddingAndBorderAxisCross, + boundAxisWithinMinAndMax(node, crossAxis, totalLineCrossDim + paddingAndBorderAxisCross)), + paddingAndBorderAxisCross); + } + + // STEP 10: SETTING TRAILING POSITIONS FOR CHILDREN + if (performLayout) { + boolean needsMainTrailingPos = false; + boolean needsCrossTrailingPos = false; if (mainAxis == CSS_FLEX_DIRECTION_ROW_REVERSE || mainAxis == CSS_FLEX_DIRECTION_COLUMN_REVERSE) { needsMainTrailingPos = true; } - } - - if (!isCrossDimDefined) { - node.layout.dimensions[dim[crossAxis]] = Math.max( - // For the cross dim, we add both sides at the end because the value - // is aggregate via a max function. Intermediate negative values - // can mess this computation otherwise - boundAxis(node, crossAxis, linesCrossDim + paddingAndBorderAxisCross), - paddingAndBorderAxisCross - ); if (crossAxis == CSS_FLEX_DIRECTION_ROW_REVERSE || crossAxis == CSS_FLEX_DIRECTION_COLUMN_REVERSE) { needsCrossTrailingPos = true; } - } - // Set trailing position if necessary - if (needsMainTrailingPos || needsCrossTrailingPos) { - for (i = 0; i < childCount; ++i) { - child = node.getChildAt(i); + // Set trailing position if necessary. + if (needsMainTrailingPos || needsCrossTrailingPos) { + for (i = 0; i < childCount; ++i) { + child = node.getChildAt(i); - if (needsMainTrailingPos) { - child.layout.position[trailing[mainAxis]] = node.layout.dimensions[dim[mainAxis]] - child.layout.dimensions[dim[mainAxis]] - child.layout.position[pos[mainAxis]]; - } + if (needsMainTrailingPos) { + child.layout.position[trailing[mainAxis]] = node.layout.measuredDimensions[dim[mainAxis]] - (child.style.positionType == CSSPositionType.ABSOLUTE ? 0 : child.layout.measuredDimensions[dim[mainAxis]]) - child.layout.position[pos[mainAxis]]; + } - if (needsCrossTrailingPos) { - child.layout.position[trailing[crossAxis]] = node.layout.dimensions[dim[crossAxis]] - child.layout.dimensions[dim[crossAxis]] - child.layout.position[pos[crossAxis]]; + if (needsCrossTrailingPos) { + child.layout.position[trailing[crossAxis]] = node.layout.measuredDimensions[dim[crossAxis]] - (child.style.positionType == CSSPositionType.ABSOLUTE ? 0 : child.layout.measuredDimensions[dim[crossAxis]]) - child.layout.position[pos[crossAxis]]; + } } } } - - // Calculate dimensions for absolutely positioned elements + + // STEP 11: SIZING AND POSITIONING ABSOLUTE CHILDREN currentAbsoluteChild = firstAbsoluteChild; while (currentAbsoluteChild != null) { - // Pre-fill dimensions when using absolute position and both offsets for - // the axis are defined (either both left and right or top and bottom). - for (ii = 0; ii < 2; ii++) { - axis = (ii != 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; - - if ((!Float.isNaN(node.layout.dimensions[dim[axis]]) && node.layout.dimensions[dim[axis]] >= 0.0) && - !(!Float.isNaN(currentAbsoluteChild.style.dimensions[dim[axis]]) && currentAbsoluteChild.style.dimensions[dim[axis]] >= 0.0) && - !Float.isNaN(currentAbsoluteChild.style.position[leading[axis]]) && - !Float.isNaN(currentAbsoluteChild.style.position[trailing[axis]])) { - currentAbsoluteChild.layout.dimensions[dim[axis]] = Math.max( - boundAxis(currentAbsoluteChild, axis, node.layout.dimensions[dim[axis]] - - (node.style.border.getWithFallback(leadingSpacing[axis], leading[axis]) + node.style.border.getWithFallback(trailingSpacing[axis], trailing[axis])) - - (currentAbsoluteChild.style.margin.getWithFallback(leadingSpacing[axis], leading[axis]) + currentAbsoluteChild.style.margin.getWithFallback(trailingSpacing[axis], trailing[axis])) - - (Float.isNaN(currentAbsoluteChild.style.position[leading[axis]]) ? 0 : currentAbsoluteChild.style.position[leading[axis]]) - - (Float.isNaN(currentAbsoluteChild.style.position[trailing[axis]]) ? 0 : currentAbsoluteChild.style.position[trailing[axis]]) - ), - // You never want to go smaller than padding - ((currentAbsoluteChild.style.padding.getWithFallback(leadingSpacing[axis], leading[axis]) + currentAbsoluteChild.style.border.getWithFallback(leadingSpacing[axis], leading[axis])) + (currentAbsoluteChild.style.padding.getWithFallback(trailingSpacing[axis], trailing[axis]) + currentAbsoluteChild.style.border.getWithFallback(trailingSpacing[axis], trailing[axis]))) - ); + // Now that we know the bounds of the container, perform layout again on the + // absolutely-positioned children. + if (performLayout) { + + childWidth = CSSConstants.UNDEFINED; + childHeight = CSSConstants.UNDEFINED; + + if ((currentAbsoluteChild.style.dimensions[dim[CSS_FLEX_DIRECTION_ROW]] >= 0.0)) { + childWidth = currentAbsoluteChild.style.dimensions[DIMENSION_WIDTH] + (currentAbsoluteChild.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_ROW], leading[CSS_FLEX_DIRECTION_ROW]) + currentAbsoluteChild.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_ROW], trailing[CSS_FLEX_DIRECTION_ROW])); + } else { + // If the child doesn't have a specified width, compute the width based on the left/right offsets if they're defined. + if (!Float.isNaN(currentAbsoluteChild.style.position[POSITION_LEFT]) && !Float.isNaN(currentAbsoluteChild.style.position[POSITION_RIGHT])) { + childWidth = node.layout.measuredDimensions[DIMENSION_WIDTH] - + (node.style.border.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_ROW], leading[CSS_FLEX_DIRECTION_ROW]) + node.style.border.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_ROW], trailing[CSS_FLEX_DIRECTION_ROW])) - + (currentAbsoluteChild.style.position[POSITION_LEFT] + currentAbsoluteChild.style.position[POSITION_RIGHT]); + childWidth = boundAxis(currentAbsoluteChild, CSS_FLEX_DIRECTION_ROW, childWidth); + } + } + + if ((currentAbsoluteChild.style.dimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] >= 0.0)) { + childHeight = currentAbsoluteChild.style.dimensions[DIMENSION_HEIGHT] + (currentAbsoluteChild.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + currentAbsoluteChild.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN])); + } else { + // If the child doesn't have a specified height, compute the height based on the top/bottom offsets if they're defined. + if (!Float.isNaN(currentAbsoluteChild.style.position[POSITION_TOP]) && !Float.isNaN(currentAbsoluteChild.style.position[POSITION_BOTTOM])) { + childHeight = node.layout.measuredDimensions[DIMENSION_HEIGHT] - + (node.style.border.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + node.style.border.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN])) - + (currentAbsoluteChild.style.position[POSITION_TOP] + currentAbsoluteChild.style.position[POSITION_BOTTOM]); + childHeight = boundAxis(currentAbsoluteChild, CSS_FLEX_DIRECTION_COLUMN, childHeight); + } } - if (!Float.isNaN(currentAbsoluteChild.style.position[trailing[axis]]) && - !!Float.isNaN(currentAbsoluteChild.style.position[leading[axis]])) { - currentAbsoluteChild.layout.position[leading[axis]] = - node.layout.dimensions[dim[axis]] - - currentAbsoluteChild.layout.dimensions[dim[axis]] - - (Float.isNaN(currentAbsoluteChild.style.position[trailing[axis]]) ? 0 : currentAbsoluteChild.style.position[trailing[axis]]); + // If we're still missing one or the other dimension, measure the content. + if (Float.isNaN(childWidth) || Float.isNaN(childHeight)) { + childWidthMeasureMode = Float.isNaN(childWidth) ? CSSMeasureMode.UNDEFINED : CSSMeasureMode.EXACTLY; + childHeightMeasureMode = Float.isNaN(childHeight) ? CSSMeasureMode.UNDEFINED : CSSMeasureMode.EXACTLY; + + // According to the spec, if the main size is not definite and the + // child's inline axis is parallel to the main axis (i.e. it's + // horizontal), the child should be sized using "UNDEFINED" in + // the main size. Otherwise use "AT_MOST" in the cross axis. + if (!isMainAxisRow && Float.isNaN(childWidth) && !Float.isNaN(availableInnerWidth)) { + childWidth = availableInnerWidth; + childWidthMeasureMode = CSSMeasureMode.AT_MOST; + } + + // The W3C spec doesn't say anything about the 'overflow' property, + // but all major browsers appear to implement the following logic. + if (node.style.overflow == CSSOverflow.HIDDEN) { + if (isMainAxisRow && Float.isNaN(childHeight) && !Float.isNaN(availableInnerHeight)) { + childHeight = availableInnerHeight; + childHeightMeasureMode = CSSMeasureMode.AT_MOST; + } + } + + layoutNodeInternal(layoutContext, currentAbsoluteChild, childWidth, childHeight, direction, childWidthMeasureMode, childHeightMeasureMode, false, "abs-measure"); + childWidth = currentAbsoluteChild.layout.measuredDimensions[DIMENSION_WIDTH] + (currentAbsoluteChild.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_ROW], leading[CSS_FLEX_DIRECTION_ROW]) + currentAbsoluteChild.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_ROW], trailing[CSS_FLEX_DIRECTION_ROW])); + childHeight = currentAbsoluteChild.layout.measuredDimensions[DIMENSION_HEIGHT] + (currentAbsoluteChild.style.margin.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + currentAbsoluteChild.style.margin.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN])); + } + + layoutNodeInternal(layoutContext, currentAbsoluteChild, childWidth, childHeight, direction, CSSMeasureMode.EXACTLY, CSSMeasureMode.EXACTLY, true, "abs-layout"); + + if (!Float.isNaN(currentAbsoluteChild.style.position[trailing[CSS_FLEX_DIRECTION_ROW]]) && + !!Float.isNaN(currentAbsoluteChild.style.position[leading[CSS_FLEX_DIRECTION_ROW]])) { + currentAbsoluteChild.layout.position[leading[CSS_FLEX_DIRECTION_ROW]] = + node.layout.measuredDimensions[dim[CSS_FLEX_DIRECTION_ROW]] - + currentAbsoluteChild.layout.measuredDimensions[dim[CSS_FLEX_DIRECTION_ROW]] - + (Float.isNaN(currentAbsoluteChild.style.position[trailing[CSS_FLEX_DIRECTION_ROW]]) ? 0 : currentAbsoluteChild.style.position[trailing[CSS_FLEX_DIRECTION_ROW]]); + } + + if (!Float.isNaN(currentAbsoluteChild.style.position[trailing[CSS_FLEX_DIRECTION_COLUMN]]) && + !!Float.isNaN(currentAbsoluteChild.style.position[leading[CSS_FLEX_DIRECTION_COLUMN]])) { + currentAbsoluteChild.layout.position[leading[CSS_FLEX_DIRECTION_COLUMN]] = + node.layout.measuredDimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] - + currentAbsoluteChild.layout.measuredDimensions[dim[CSS_FLEX_DIRECTION_COLUMN]] - + (Float.isNaN(currentAbsoluteChild.style.position[trailing[CSS_FLEX_DIRECTION_COLUMN]]) ? 0 : currentAbsoluteChild.style.position[trailing[CSS_FLEX_DIRECTION_COLUMN]]); } } - child = currentAbsoluteChild; - currentAbsoluteChild = currentAbsoluteChild.nextAbsoluteChild; - child.nextAbsoluteChild = null; + currentAbsoluteChild = currentAbsoluteChild.nextChild; } - } /** END_GENERATED **/ + } } diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/README b/ReactAndroid/src/main/java/com/facebook/csslayout/README index 92aafb8d060080..34a003a1b4551b 100644 --- a/ReactAndroid/src/main/java/com/facebook/csslayout/README +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/README @@ -1,7 +1,7 @@ The source of truth for css-layout is: https://github.com/facebook/css-layout The code here should be kept in sync with GitHub. -HEAD at the time this code was synced: https://github.com/facebook/css-layout/commit/b0d00ad33850d83450139d994bded89d20ddac32 +HEAD at the time this code was synced: https://github.com/facebook/css-layout/commit/16f43dac87ace8b60e0e4c07a798a558c22bd21b There is generated code in: - README (this file) diff --git a/ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook b/ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook index 637f6609e4e3d2..b4d26531d5369f 100644 --- a/ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook +++ b/ReactAndroid/src/main/java/com/facebook/csslayout/README.facebook @@ -1,7 +1,7 @@ The source of truth for css-layout is: https://github.com/facebook/css-layout The code here should be kept in sync with GitHub. -HEAD at the time this code was synced: https://github.com/facebook/css-layout/commit/b0d00ad33850d83450139d994bded89d20ddac32 +HEAD at the time this code was synced: https://github.com/facebook/css-layout/commit/16f43dac87ace8b60e0e4c07a798a558c22bd21b There is generated code in: - README.facebook (this file) From 3ccd99fb5399995e476f9b57802d7651179b5752 Mon Sep 17 00:00:00 2001 From: Nathan Azaria Date: Tue, 31 May 2016 04:56:49 -0700 Subject: [PATCH 155/843] Added RCTBundleURLProvider Reviewed By: javache Differential Revision: D3352568 fbshipit-source-id: fbba6771a1c581e2676bd0f81d3da62dbf21916b --- .../UIExplorer.xcodeproj/project.pbxproj | 4 + .../RCTBundleURLProviderTests.m | 103 +++++++++++ React/Base/RCTBundleURLProvider.h | 38 ++++ React/Base/RCTBundleURLProvider.m | 167 ++++++++++++++++++ React/React.xcodeproj/project.pbxproj | 6 + 5 files changed, 318 insertions(+) create mode 100644 Examples/UIExplorer/UIExplorerUnitTests/RCTBundleURLProviderTests.m create mode 100644 React/Base/RCTBundleURLProvider.h create mode 100644 React/Base/RCTBundleURLProvider.m diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index 7a307ded6b27fe..d2b3784bb88194 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -63,6 +63,7 @@ 27F441EC1BEBE5030039B79C /* FlexibleSizeExampleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F441E81BEBE5030039B79C /* FlexibleSizeExampleView.m */; }; 3578590A1B28D2CF00341EDB /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 357859011B28D2C500341EDB /* libRCTLinking.a */; }; 3DB99D0C1BA0340600302749 /* UIExplorerIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DB99D0B1BA0340600302749 /* UIExplorerIntegrationTests.m */; }; + 68FF44381CF6111500720EFD /* RCTBundleURLProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68FF44371CF6111500720EFD /* RCTBundleURLProviderTests.m */; }; 834C36EC1AF8DED70019C93C /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 834C36D21AF8DA610019C93C /* libRCTSettings.a */; }; 83636F8F1B53F22C009F943E /* RCTUIManagerScenarioTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */; }; 8385CEF51B873B5C00C6273E /* RCTImageLoaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */; }; @@ -248,6 +249,7 @@ 357858F81B28D2C400341EDB /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = ../../Libraries/LinkingIOS/RCTLinking.xcodeproj; sourceTree = ""; }; 3DB99D0B1BA0340600302749 /* UIExplorerIntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIExplorerIntegrationTests.m; sourceTree = ""; }; 58005BE41ABA80530062E044 /* RCTTest.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTTest.xcodeproj; path = ../../Libraries/RCTTest/RCTTest.xcodeproj; sourceTree = ""; }; + 68FF44371CF6111500720EFD /* RCTBundleURLProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBundleURLProviderTests.m; sourceTree = ""; }; 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIManagerScenarioTests.m; sourceTree = ""; }; 8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderTests.m; sourceTree = ""; }; 8385CF031B87479200C6273E /* RCTImageLoaderHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderHelpers.m; sourceTree = ""; }; @@ -415,6 +417,7 @@ isa = PBXGroup; children = ( 13B6C1A21C34225900D3FAF5 /* RCTURLUtilsTests.m */, + 68FF44371CF6111500720EFD /* RCTBundleURLProviderTests.m */, 1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */, 1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */, 1497CFA61B21F5E400C1F8F2 /* RCTJSCExecutorTests.m */, @@ -903,6 +906,7 @@ 138D6A181B53CD440074A87E /* RCTShadowViewTests.m in Sources */, 13B6C1A31C34225900D3FAF5 /* RCTURLUtilsTests.m in Sources */, 8385CF041B87479200C6273E /* RCTImageLoaderHelpers.m in Sources */, + 68FF44381CF6111500720EFD /* RCTBundleURLProviderTests.m in Sources */, 8385CEF51B873B5C00C6273E /* RCTImageLoaderTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTBundleURLProviderTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTBundleURLProviderTests.m new file mode 100644 index 00000000000000..bb044887c5e3f1 --- /dev/null +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTBundleURLProviderTests.m @@ -0,0 +1,103 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#import + +#import "RCTBundleURLProvider.h" +#import "RCTUtils.h" + + +static NSString *const testFile = @"test.jsbundle"; +static NSString *const mainBundle = @"main.jsbundle"; + +static NSURL *mainBundleURL() +{ + return [[[NSBundle mainBundle] bundleURL] URLByAppendingPathComponent:mainBundle]; +} + +static NSURL *localhostBundleURL() +{ + return [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.bundle?platform=ios&dev=true&minify=false", testFile]]; +} + +static NSURL *ipBundleURL() +{ + return [NSURL URLWithString:[NSString stringWithFormat:@"http://192.168.1.1:8081/%@.bundle?platform=ios&dev=true&minify=false", testFile]]; +} + +@implementation NSBundle (RCTBundleURLProviderTests) + +- (NSURL *)RCT_URLForResource:(NSString *)name withExtension:(NSString *)ext +{ + // Ensure that test files is always reported as existing + if ([[name stringByAppendingFormat:@".%@", ext] isEqualToString:mainBundle]) { + return [[self bundleURL] URLByAppendingPathComponent:mainBundle]; + } + return [self RCT_URLForResource:name withExtension:ext]; +} + +@end + +@interface RCTBundleURLProviderTests : XCTestCase +@end + +@implementation RCTBundleURLProviderTests + +- (void)setUp +{ + [super setUp]; + + RCTSwapInstanceMethods([NSBundle class], + @selector(URLForResource:withExtension:), + @selector(RCT_URLForResource:withExtension:)); + [[RCTBundleURLProvider sharedSettings] setDefaults]; +} + +- (void)tearDown +{ + RCTSwapInstanceMethods([NSBundle class], + @selector(URLForResource:withExtension:), + @selector(RCT_URLForResource:withExtension:)); + + [super tearDown]; +} + +- (void)testBundleURL +{ + RCTBundleURLProvider *settings = [RCTBundleURLProvider sharedSettings]; + settings.jsLocation = nil; + NSURL *URL = [settings jsBundleURLForBundleRoot:testFile fallbackResource:nil]; + if (!getenv("CI_USE_PACKAGER")) { + XCTAssertEqualObjects(URL, mainBundleURL()); + } else { + XCTAssertEqualObjects(URL, localhostBundleURL()); + } +} + +- (void)testLocalhostURL +{ + RCTBundleURLProvider *settings = [RCTBundleURLProvider sharedSettings]; + settings.jsLocation = @"localhost"; + NSURL *URL = [settings jsBundleURLForBundleRoot:testFile fallbackResource:nil]; + XCTAssertEqualObjects(URL, localhostBundleURL()); +} + +- (void)testIPURL +{ + RCTBundleURLProvider *settings = [RCTBundleURLProvider sharedSettings]; + settings.jsLocation = @"192.168.1.1"; + NSURL *URL = [settings jsBundleURLForBundleRoot:testFile fallbackResource:nil]; + XCTAssertEqualObjects(URL, ipBundleURL()); +} + +@end diff --git a/React/Base/RCTBundleURLProvider.h b/React/Base/RCTBundleURLProvider.h new file mode 100644 index 00000000000000..4ae517d6da606a --- /dev/null +++ b/React/Base/RCTBundleURLProvider.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface RCTBundleURLProvider : NSObject + +extern NSString *const RCTBundleURLProviderUpdatedNotification; + +/** + * Set default settings on NSUserDefaults. + */ +- (void)setDefaults; + +/** + * Returns the jsBundleURL for a given bundle entrypoint and + * the fallback offline JS bundle if the packager is not running. + */ +- (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot + fallbackResource:(NSString *)resourceName; + +/** + * The IP address or hostname of the packager. + */ +@property (nonatomic, copy) NSString *jsLocation; + +@property (nonatomic, assign) BOOL enableLiveReload; +@property (nonatomic, assign) BOOL enableMinification; +@property (nonatomic, assign) BOOL enableDev; + ++ (instancetype)sharedSettings; +@end diff --git a/React/Base/RCTBundleURLProvider.m b/React/Base/RCTBundleURLProvider.m new file mode 100644 index 00000000000000..f931b9a6280dfa --- /dev/null +++ b/React/Base/RCTBundleURLProvider.m @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTBundleURLProvider.h" +#import "RCTDefines.h" +#import "RCTConvert.h" + +NSString *const RCTBundleURLProviderUpdatedNotification = @"RCTBundleURLProviderUpdatedNotification"; + +static NSString *const kRCTJsLocationKey = @"RCT_jsLocation"; +static NSString *const kRCTEnableLiveReloadKey = @"RCT_enableLiveReload"; +static NSString *const kRCTEnableDevKey = @"RCT_enableDev"; +static NSString *const kRCTEnableMinificationKey = @"RCT_enableMinification"; + +static NSString *const kDefaultPort = @"8081"; + +@implementation RCTBundleURLProvider + +- (NSDictionary *)defaults +{ + static NSDictionary *defaults; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaults = @{ + kRCTEnableLiveReloadKey: @NO, + kRCTEnableDevKey: @YES, + kRCTEnableMinificationKey: @NO, + }; + }); + return defaults; +} + +- (void)settingsUpdated +{ + [[NSNotificationCenter defaultCenter] postNotificationName:RCTBundleURLProviderUpdatedNotification object:self]; +} + +- (void)setDefaults +{ + [[NSUserDefaults standardUserDefaults] registerDefaults:[self defaults]]; + [self settingsUpdated]; +} + +- (BOOL)isPackagerRunning:(NSString *)host +{ + if (RCT_DEV) { + NSURL *url = [[NSURL URLWithString:serverRootWithHost(host)] URLByAppendingPathComponent:@"status"]; + NSURLRequest *request = [NSURLRequest requestWithURL:url]; + NSURLResponse *response; + NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL]; + NSString *status = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + return [status isEqualToString:@"packager-status:running"]; + } + return NO; +} + +static NSString *serverRootWithHost(NSString *host) +{ + return [NSString stringWithFormat:@"http://%@:%@/", host, kDefaultPort]; +} + +- (NSString *)guessPackagerHost +{ + NSString *host = @"localhost"; + //TODO: Implement automatic IP address detection + if ([self isPackagerRunning:host]) { + return host; + } + return nil; +} + + +- (NSString *)packagerServerRoot +{ + NSString *location = [self jsLocation]; + if (location != nil) { + return serverRootWithHost(location); + } else { + NSString *host = [self guessPackagerHost]; + if (!host) { + return nil; + } else { + return serverRootWithHost(host); + } + } +} + +- (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot fallbackResource:(NSString *)resourceName +{ + resourceName = resourceName ?: @"main"; + NSString *serverRoot = [self packagerServerRoot]; + if (!serverRoot) { + return [[NSBundle mainBundle] URLForResource:resourceName withExtension:@"jsbundle"]; + } else { + NSString *fullBundlePath = [serverRoot stringByAppendingFormat:@"%@.bundle", bundleRoot]; + if ([fullBundlePath hasPrefix:@"http"]) { + NSString *dev = [self enableDev] ? @"true" : @"false"; + NSString *min = [self enableMinification] ? @"true": @"false"; + fullBundlePath = [fullBundlePath stringByAppendingFormat:@"?platform=ios&dev=%@&minify=%@", dev, min]; + } + return [NSURL URLWithString:fullBundlePath]; + } +} + +- (void)updateDefaults:(id)object forKey:(NSString *)key +{ + [[NSUserDefaults standardUserDefaults] setObject:object forKey:key]; + [[NSUserDefaults standardUserDefaults] synchronize]; + [self settingsUpdated]; +} + +- (BOOL)enableDev +{ + return [[NSUserDefaults standardUserDefaults] boolForKey:kRCTEnableDevKey]; +} + +- (BOOL)enableLiveReload +{ + return [[NSUserDefaults standardUserDefaults] boolForKey:kRCTEnableLiveReloadKey]; +} + +- (BOOL)enableMinification +{ + return [[NSUserDefaults standardUserDefaults] boolForKey:kRCTEnableMinificationKey]; +} + +- (NSString *)jsLocation +{ + return [[NSUserDefaults standardUserDefaults] stringForKey:kRCTJsLocationKey]; +} + +- (void)setEnableDev:(BOOL)enableDev +{ + [self updateDefaults:@(enableDev) forKey:kRCTEnableDevKey]; +} + +- (void)setEnableEnableLiveReload:(BOOL)enableLiveReload +{ + [self updateDefaults:@(enableLiveReload) forKey:kRCTEnableLiveReloadKey]; +} + +- (void)setJsLocation:(NSString *)jsLocation +{ + [self updateDefaults:jsLocation forKey:kRCTJsLocationKey]; +} + +- (void)setEnableMinification:(BOOL)enableMinification +{ + [self updateDefaults:@(enableMinification) forKey:kRCTEnableMinificationKey]; +} + ++ (instancetype)sharedSettings +{ + static RCTBundleURLProvider *sharedInstance; + static dispatch_once_t once_token; + dispatch_once(&once_token, ^{ + sharedInstance = [RCTBundleURLProvider new]; + }); + return sharedInstance; +} +@end diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index ed4eaa243233c6..c43d45fe5be9d8 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A151AAE854800E7D092 /* RCTPickerManager.m */; }; 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A4E1AAE93D500E7D092 /* RCTAsyncLocalStorage.m */; }; 58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */; }; + 68EFE4EE1CF6EB3900A1DE13 /* RCTBundleURLProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 68EFE4ED1CF6EB3900A1DE13 /* RCTBundleURLProvider.m */; }; 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; }; 832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; }; 83392EB31B6634E10013B15F /* RCTModalHostViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 83392EB21B6634E10013B15F /* RCTModalHostViewController.m */; }; @@ -272,6 +273,8 @@ 58114A4F1AAE93D500E7D092 /* RCTAsyncLocalStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAsyncLocalStorage.h; sourceTree = ""; }; 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDatePickerManager.m; sourceTree = ""; }; 58C571C01AA56C1900CDF9C8 /* RCTDatePickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDatePickerManager.h; sourceTree = ""; }; + 68EFE4EC1CF6EB3000A1DE13 /* RCTBundleURLProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBundleURLProvider.h; sourceTree = ""; }; + 68EFE4ED1CF6EB3900A1DE13 /* RCTBundleURLProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBundleURLProvider.m; sourceTree = ""; }; 6A15FB0C1BDF663500531DFB /* RCTRootViewInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootViewInternal.h; sourceTree = ""; }; 830213F31A654E0800B993E6 /* RCTBridgeModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTBridgeModule.h; sourceTree = ""; }; 830A229C1A66C68A008503DA /* RCTRootView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootView.h; sourceTree = ""; }; @@ -528,6 +531,8 @@ 83CBBA491A601E3B00E9B192 /* Base */ = { isa = PBXGroup; children = ( + 68EFE4ED1CF6EB3900A1DE13 /* RCTBundleURLProvider.m */, + 68EFE4EC1CF6EB3000A1DE13 /* RCTBundleURLProvider.h */, 83CBBA4A1A601E3B00E9B192 /* RCTAssert.h */, 83CBBA4B1A601E3B00E9B192 /* RCTAssert.m */, 14C2CA771B3ACB0400E6CBB2 /* RCTBatchedBridge.m */, @@ -746,6 +751,7 @@ 131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */, 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */, 191E3EBE1C29D9AF00C180A6 /* RCTRefreshControlManager.m in Sources */, + 68EFE4EE1CF6EB3900A1DE13 /* RCTBundleURLProvider.m in Sources */, 13B0801A1A69489C00A75B9A /* RCTNavigator.m in Sources */, 137327E71AA5CF210034F82E /* RCTTabBar.m in Sources */, 13F17A851B8493E5007D4C75 /* RCTRedBox.m in Sources */, From eb69d036d5a486fa915121877fa68ebf7d8fef58 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 31 May 2016 05:18:17 -0700 Subject: [PATCH 156/843] Log image url when image size doesn't match. Summary: Sometimes I got error message about image size doesn't match, but I don't know which image is wrong. Before: 2016-05-31 11 03 58 After this PR: 2016-05-31 10 59 21 **Test plan (required)** ``` ``` And don't add this image into project, it would report errors when run it. Closes https://github.com/facebook/react-native/pull/7840 Differential Revision: D3365467 Pulled By: nicklockwood fbshipit-source-id: de3d5989ef0a3c443cce557901486c4770d4e906 --- React/Base/RCTConvert.m | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index ebdbee02a5847d..f5c6924d6a0380 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -913,8 +913,10 @@ + (UIImage *)UIImage:(id)json if (!CGSizeEqualToSize(imageSource.size, CGSizeZero) && !CGSizeEqualToSize(imageSource.size, image.size)) { - RCTLogError(@"Image source size %@ does not match loaded image size %@.", - NSStringFromCGSize(imageSource.size), NSStringFromCGSize(image.size)); + RCTLogError(@"Image source %@ size %@ does not match loaded image size %@.", + imageSource.imageURL.path.lastPathComponent, + NSStringFromCGSize(imageSource.size), + NSStringFromCGSize(image.size)); } return image; From 31eea8eee3bd904065b06c639321dbc2cea1ca55 Mon Sep 17 00:00:00 2001 From: Siqi Liu Date: Tue, 31 May 2016 07:44:29 -0700 Subject: [PATCH 157/843] Android Dev Menu Options Reorder and Relabel Summary: Just rename and rearrange the dev menu options in Android, so as to be consistent with those in iOS. {F61192593} {F61192595} {F61192594} {F61192597} There are other issues to solve on the inspector and profiling in Android, so I just ignore them for now. Reviewed By: mkonicek Differential Revision: D3361415 fbshipit-source-id: ffa823a0c54a27f7918e4e43ecea3c845d2a2f90 --- .../devsupport/DevSupportManagerImpl.java | 40 +++++++++---------- .../main/res/devsupport/values/strings.xml | 8 ++-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index 55057a8eda0b6f..ee4a7f3a696a7d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -274,6 +274,16 @@ public void onOptionSelected() { handleReloadJS(); } }); + options.put( + mDevSettings.isReloadOnJSChangeEnabled() + ? mApplicationContext.getString(R.string.catalyst_live_reload_off) + : mApplicationContext.getString(R.string.catalyst_live_reload), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + mDevSettings.setReloadOnJSChangeEnabled(!mDevSettings.isReloadOnJSChangeEnabled()); + } + }); options.put( mDevSettings.isHotModuleReplacementEnabled() ? mApplicationContext.getString(R.string.catalyst_hot_module_replacement_off) @@ -285,16 +295,6 @@ public void onOptionSelected() { handleReloadJS(); } }); - options.put( - mDevSettings.isReloadOnJSChangeEnabled() - ? mApplicationContext.getString(R.string.catalyst_live_reload_off) - : mApplicationContext.getString(R.string.catalyst_live_reload), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - mDevSettings.setReloadOnJSChangeEnabled(!mDevSettings.isReloadOnJSChangeEnabled()); - } - }); options.put( mApplicationContext.getString(R.string.catalyst_element_inspector), new DevOptionHandler() { @@ -304,6 +304,16 @@ public void onOptionSelected() { mReactInstanceCommandsHandler.toggleElementInspector(); } }); + options.put( + mDevSettings.isFpsDebugEnabled() + ? mApplicationContext.getString(R.string.catalyst_perf_monitor_off) + : mApplicationContext.getString(R.string.catalyst_perf_monitor), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + mDevSettings.setFpsDebugEnabled(!mDevSettings.isFpsDebugEnabled()); + } + }); options.put( mApplicationContext.getString(R.string.catalyst_heap_capture), new DevOptionHandler() { @@ -323,16 +333,6 @@ public void onOptionSelected() { } } }); - options.put( - mDevSettings.isFpsDebugEnabled() - ? mApplicationContext.getString(R.string.catalyst_perf_monitor_off) - : mApplicationContext.getString(R.string.catalyst_perf_monitor), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - mDevSettings.setFpsDebugEnabled(!mDevSettings.isFpsDebugEnabled()); - } - }); if (mCurrentContext != null && mCurrentContext.getCatalystInstance() != null && !mCurrentContext.getCatalystInstance().isDestroyed() && diff --git a/ReactAndroid/src/main/res/devsupport/values/strings.xml b/ReactAndroid/src/main/res/devsupport/values/strings.xml index 9fe01dbda0ba34..dda73a8c506106 100644 --- a/ReactAndroid/src/main/res/devsupport/values/strings.xml +++ b/ReactAndroid/src/main/res/devsupport/values/strings.xml @@ -1,14 +1,14 @@ - Reload JS + Reload Debug JS Remotely - Stop Remote JS Debugging + Disable Remote JS Debugging Enable Hot Reloading Disable Hot Reloading Enable Live Reload Disable Live Reload - Enable Perf Monitor - Disable Perf Monitor + Show Perf Monitor + Hide Perf Monitor Dev Settings Catalyst Dev Settings Please wait… From 0b6764d18ea010bc87d7c7b5015b12eaa3c32b62 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Tue, 31 May 2016 08:17:36 -0700 Subject: [PATCH 158/843] Made React version not exact again Summary: React is a peer dependency and apps may depend on a wider range of versions of React, so strict dependency is not a good choice. Also `react-native init` does `npm install react` internally creating package.json with `react: ^0.15.x`, this change makes versions consistent as well. Reviewed By: matryoshcow Differential Revision: D3365433 fbshipit-source-id: d2810662c36129ff9af184c359ac190544db75da --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3e5be91ab717e6..49db927f4a3889 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "react-native": "local-cli/wrong-react-native.js" }, "peerDependencies": { - "react": "15.1.0" + "react": "^15.1.0" }, "dependencies": { "absolute-path": "^0.0.0", @@ -191,7 +191,7 @@ "flow-bin": "^0.26.0", "jest": "12.1.1", "portfinder": "0.4.0", - "react": "15.1.0", + "react": "^15.1.0", "shelljs": "0.6.0" } } From 5136d95f2cf442a68ca10d47d93708765ca3e2ac Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 31 May 2016 08:38:47 -0700 Subject: [PATCH 159/843] Reduce boxing overhead of arrays in uiBlockWithLayoutUpdateForRootView Summary: The view update cycle in UIManager was relying on a bunch of boolean values boxes as NSNumbers in parallel arrays. This diff packs those values into a struct, which is more efficient and easier to maintain. Reviewed By: javache Differential Revision: D3365346 fbshipit-source-id: d9cbf2865421f76772c1761b13992d40ec3675f0 --- .../UIExplorer.xcodeproj/project.pbxproj | 16 +-- React/Modules/RCTUIManager.m | 131 +++++++++--------- 2 files changed, 73 insertions(+), 74 deletions(-) diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index d2b3784bb88194..ba1a02b73a5a4d 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -1014,12 +1014,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.facebook.react.uiapp; PRODUCT_NAME = UIExplorer; TARGETED_DEVICE_FAMILY = "1,2"; - WARNING_CFLAGS = ( - "-Wextra", - "-Wall", - "-Wincompatible-pointer-types", - "-Wincompatible-pointer-types-discards-qualifiers", - ); }; name = Debug; }; @@ -1039,12 +1033,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.facebook.react.uiapp; PRODUCT_NAME = UIExplorer; TARGETED_DEVICE_FAMILY = "1,2"; - WARNING_CFLAGS = ( - "-Wextra", - "-Wall", - "-Wincompatible-pointer-types", - "-Wincompatible-pointer-types-discards-qualifiers", - ); }; name = Release; }; @@ -1149,6 +1137,8 @@ "-Wextra", "-Wall", "-Wincompatible-pointer-types", + "-Wincompatible-pointer-types-discards-qualifiers", + "-Wshadow", ); }; name = Debug; @@ -1212,6 +1202,8 @@ "-Wextra", "-Wall", "-Wincompatible-pointer-types", + "-Wincompatible-pointer-types-discards-qualifiers", + "-Wshadow", ); }; name = Release; diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 11112f92a9dc6e..033df4de6fddd1 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -57,8 +57,6 @@ @interface RCTAnimation : NSObject @property (nonatomic, readonly) NSTimeInterval duration; @property (nonatomic, readonly) NSTimeInterval delay; @property (nonatomic, readonly, copy) NSString *property; -@property (nonatomic, readonly) id fromValue; -@property (nonatomic, readonly) id toValue; @property (nonatomic, readonly) CGFloat springDamping; @property (nonatomic, readonly) CGFloat initialVelocity; @property (nonatomic, readonly) RCTAnimationType animationType; @@ -136,8 +134,6 @@ - (instancetype)initWithDuration:(NSTimeInterval)duration dictionary:(NSDictiona _springDamping = [RCTConvert CGFloat:config[@"springDamping"]]; _initialVelocity = [RCTConvert CGFloat:config[@"initialVelocity"]]; } - _fromValue = config[@"fromValue"]; - _toValue = config[@"toValue"]; } return self; } @@ -552,42 +548,47 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * return nil; } - // Parallel arrays are built and then handed off to main thread - NSMutableArray *frameReactTags = - [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; - NSMutableArray *frames = - [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; - NSMutableArray *areNew = - [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; - NSMutableArray *parentsAreNew = - [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; - NSMutableDictionary *updateBlocks = - [NSMutableDictionary new]; - NSMutableArray *areHidden = - [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; - - for (RCTShadowView *shadowView in viewsWithNewFrames) { - [frameReactTags addObject:shadowView.reactTag]; - [frames addObject:[NSValue valueWithCGRect:shadowView.frame]]; - [areNew addObject:@(shadowView.isNewView)]; - [parentsAreNew addObject:@(shadowView.superview.isNewView)]; - [areHidden addObject:@(shadowView.isHidden)]; - } - - for (RCTShadowView *shadowView in viewsWithNewFrames) { - // We have to do this after we build the parentsAreNew array. - shadowView.newView = NO; + typedef struct { + CGRect frame; + BOOL isNew; + BOOL parentIsNew; + BOOL isHidden; + } RCTFrameData; + + // Construct arrays then hand off to main thread + NSInteger count = viewsWithNewFrames.count; + NSMutableArray *reactTags = [[NSMutableArray alloc] initWithCapacity:count]; + NSMutableData *framesData = [[NSMutableData alloc] initWithLength:sizeof(RCTFrameData) * count]; + { + NSInteger index = 0; + RCTFrameData *frameDataArray = (RCTFrameData *)framesData.mutableBytes; + for (RCTShadowView *shadowView in viewsWithNewFrames) { + reactTags[index] = shadowView.reactTag; + frameDataArray[index++] = (RCTFrameData){ + shadowView.frame, + shadowView.isNewView, + shadowView.superview.isNewView, + shadowView.isHidden, + }; + } } // These are blocks to be executed on each view, immediately after // reactSetFrame: has been called. Note that if reactSetFrame: is not called, // these won't be called either, so this is not a suitable place to update // properties that aren't related to layout. + NSMutableDictionary *updateBlocks = + [NSMutableDictionary new]; for (RCTShadowView *shadowView in viewsWithNewFrames) { + + // We have to do this after we build the parentsAreNew array. + shadowView.newView = NO; + + NSNumber *reactTag = shadowView.reactTag; RCTViewManager *manager = [_componentDataByName[shadowView.viewName] manager]; RCTViewManagerUIBlock block = [manager uiBlockToAmendWithShadowView:shadowView]; if (block) { - updateBlocks[shadowView.reactTag] = block; + updateBlocks[reactTag] = block; } if (shadowView.onLayout) { @@ -602,8 +603,7 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * }); } - if (RCTIsReactRootView(shadowView.reactTag)) { - NSNumber *reactTag = shadowView.reactTag; + if (RCTIsReactRootView(reactTag)) { CGSize contentSize = shadowView.frame.size; dispatch_async(dispatch_get_main_queue(), ^{ @@ -618,23 +618,28 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * // Perform layout (possibly animated) return ^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTLayoutAnimation *layoutAnimation = _layoutAnimation; + + const RCTFrameData *frameDataArray = (const RCTFrameData *)framesData.bytes; + RCTLayoutAnimation *layoutAnimation = uiManager->_layoutAnimation; __block NSUInteger completionsCalled = 0; - for (NSUInteger ii = 0; ii < frames.count; ii++) { - NSNumber *reactTag = frameReactTags[ii]; + + NSInteger index = 0; + for (NSNumber *reactTag in reactTags) { + RCTFrameData frameData = frameDataArray[index++]; + UIView *view = viewRegistry[reactTag]; - CGRect frame = [frames[ii] CGRectValue]; + CGRect frame = frameData.frame; - BOOL isHidden = [areHidden[ii] boolValue]; - BOOL isNew = [areNew[ii] boolValue]; + BOOL isHidden = frameData.isHidden; + BOOL isNew = frameData.isNew; RCTAnimation *updateAnimation = isNew ? nil : layoutAnimation.updateAnimation; - BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue]; + BOOL shouldAnimateCreation = isNew && !frameData.parentIsNew; RCTAnimation *createAnimation = shouldAnimateCreation ? layoutAnimation.createAnimation : nil; void (^completion)(BOOL) = ^(BOOL finished) { completionsCalled++; - if (layoutAnimation.callback && completionsCalled == frames.count) { + if (layoutAnimation.callback && completionsCalled == count) { layoutAnimation.callback(@[@(finished)]); // It's unsafe to call this callback more than once, so we nil it out here @@ -647,58 +652,59 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView * view.hidden = isHidden; } - // Animate view creation + RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag]; if (createAnimation) { + + // Animate view creation [view reactSetFrame:frame]; CATransform3D finalTransform = view.layer.transform; CGFloat finalOpacity = view.layer.opacity; - if ([createAnimation.property isEqualToString:@"scaleXY"]) { + + NSString *property = createAnimation.property; + if ([property isEqualToString:@"scaleXY"]) { view.layer.transform = CATransform3DMakeScale(0, 0, 0); - } else if ([createAnimation.property isEqualToString:@"opacity"]) { + } else if ([property isEqualToString:@"opacity"]) { view.layer.opacity = 0.0; + } else { + RCTLogError(@"Unsupported layout animation createConfig property %@", + createAnimation.property); } [createAnimation performAnimations:^{ - if ([createAnimation.property isEqual:@"scaleXY"]) { + if ([property isEqualToString:@"scaleXY"]) { view.layer.transform = finalTransform; - } else if ([createAnimation.property isEqual:@"opacity"]) { + } else if ([property isEqualToString:@"opacity"]) { view.layer.opacity = finalOpacity; - } else { - RCTLogError(@"Unsupported layout animation createConfig property %@", - createAnimation.property); } - - RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag]; if (updateBlock) { - updateBlock(self, _viewRegistry); + updateBlock(self, viewRegistry); } } withCompletionBlock:completion]; - // Animate view update } else if (updateAnimation) { + + // Animate view update [updateAnimation performAnimations:^{ [view reactSetFrame:frame]; - - RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag]; if (updateBlock) { - updateBlock(self, _viewRegistry); + updateBlock(self, viewRegistry); } } withCompletionBlock:completion]; - // Update without animation } else { - [view reactSetFrame:frame]; - RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag]; + // Update without animation + [view reactSetFrame:frame]; if (updateBlock) { - updateBlock(self, _viewRegistry); + updateBlock(self, viewRegistry); } completion(YES); } } - _layoutAnimation = nil; + // Clean up + uiManager->_layoutAnimation = nil; }; } @@ -802,10 +808,11 @@ - (void)_removeChildren:(NSArray> *)children // the view events anyway. view.userInteractionEnabled = NO; + NSString *property = deleteAnimation.property; [deleteAnimation performAnimations:^{ - if ([deleteAnimation.property isEqual:@"scaleXY"]) { + if ([property isEqualToString:@"scaleXY"]) { view.layer.transform = CATransform3DMakeScale(0, 0, 0); - } else if ([deleteAnimation.property isEqual:@"opacity"]) { + } else if ([property isEqualToString:@"opacity"]) { view.layer.opacity = 0.0; } else { RCTLogError(@"Unsupported layout animation createConfig property %@", From 486dbe4e8f4e3710c258e22af00b2aa7155ef2f3 Mon Sep 17 00:00:00 2001 From: Adam Comella Date: Tue, 31 May 2016 10:18:37 -0700 Subject: [PATCH 160/843] iOS: Enable views to be nested within Summary: Previously, only Text and Image could be nested within Text. Now, any view can be nested within Text. One restriction of this feature is that developers must give inline views a width and a height via the style prop. Previously, inline Images were supported by using iOS's built-in support for rendering images with an NSAttributedString via NSTextAttachment. However, NSAttributedString doesn't support rendering arbitrary views. This change adds support for nesting views within Text by creating one NSTextAttachment per inline view. The NSTextAttachments act as placeholders. They are set to be the size of the corresponding view. After the text is laid out, we query the text system to find out where it has positioned each NSTextAttachment. We then position the views to be at those locations. This commit also contains a change in `RCTShadowText.m` `_setParagraphStyleOnAttributedString:heightOfTallestSubview:`. It now only sets `lineHeight`, `textAlign`, and `writingDirection` when they've actua Closes https://github.com/facebook/react-native/pull/7304 Reviewed By: javache Differential Revision: D3365373 Pulled By: nicklockwood fbshipit-source-id: 66d149eb80c5c6725311e1e46d7323eec086ce64 --- Examples/UIExplorer/TextExample.ios.js | 5 +- Libraries/Image/Image.ios.js | 12 -- .../Image/RCTImage.xcodeproj/project.pbxproj | 10 -- Libraries/Image/RCTImageView.h | 3 +- Libraries/Image/RCTShadowVirtualImage.h | 28 ---- Libraries/Image/RCTShadowVirtualImage.m | 82 ------------ Libraries/Image/RCTVirtualImageManager.h | 14 -- Libraries/Image/RCTVirtualImageManager.m | 25 ---- Libraries/Text/RCTShadowText.m | 121 +++++++++++++----- Libraries/Text/RCTText.m | 26 ++++ Libraries/Text/RCTTextManager.m | 13 ++ React/React.xcodeproj/project.pbxproj | 2 - React/Views/RCTImageComponent.h | 19 --- React/Views/RCTShadowView.h | 23 +++- React/Views/RCTShadowView.m | 38 ++++++ docs/Text.md | 14 ++ 16 files changed, 209 insertions(+), 226 deletions(-) delete mode 100644 Libraries/Image/RCTShadowVirtualImage.h delete mode 100644 Libraries/Image/RCTShadowVirtualImage.m delete mode 100644 Libraries/Image/RCTVirtualImageManager.h delete mode 100644 Libraries/Image/RCTVirtualImageManager.m delete mode 100644 React/Views/RCTImageComponent.h diff --git a/Examples/UIExplorer/TextExample.ios.js b/Examples/UIExplorer/TextExample.ios.js index e21b7c3bfd2e91..8c7b4ff86ef006 100644 --- a/Examples/UIExplorer/TextExample.ios.js +++ b/Examples/UIExplorer/TextExample.ios.js @@ -422,12 +422,13 @@ exports.examples = [ ); }, }, { - title: 'Inline images', + title: 'Inline views', render: function() { return ( - This text contains an inline image . Neat, huh? + This text contains an inline blue view and + an inline image . Neat, huh? ); diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 7c496656869bf2..b2f4718f7f120d 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -201,10 +201,6 @@ var Image = React.createClass({ validAttributes: ReactNativeViewAttributes.UIView }, - contextTypes: { - isInAParentText: React.PropTypes.bool - }, - render: function() { var source = resolveAssetSource(this.props.source) || {}; var {width, height, uri} = source; @@ -225,13 +221,6 @@ var Image = React.createClass({ console.warn('The component requires a `source` property rather than `src`.'); } - if (this.context.isInAParentText) { - RawImage = RCTVirtualImage; - if (!width || !height) { - console.warn('You must specify a width and height for the image %s', uri); - } - } - return ( -#import "RCTImageComponent.h" #import "RCTResizeMode.h" @class RCTBridge; @class RCTImageSource; -@interface RCTImageView : UIImageView +@interface RCTImageView : UIImageView - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; diff --git a/Libraries/Image/RCTShadowVirtualImage.h b/Libraries/Image/RCTShadowVirtualImage.h deleted file mode 100644 index b9623f736f239e..00000000000000 --- a/Libraries/Image/RCTShadowVirtualImage.h +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTShadowView.h" -#import "RCTImageComponent.h" -#import "RCTImageSource.h" -#import "RCTResizeMode.h" - -@class RCTBridge; - -/** - * Shadow image component, used for embedding images in non-view contexts such - * as text. This is NOT used for ordinary views. - */ -@interface RCTShadowVirtualImage : RCTShadowView - -- (instancetype)initWithBridge:(RCTBridge *)bridge; - -@property (nonatomic, strong) RCTImageSource *source; -@property (nonatomic, assign) RCTResizeMode resizeMode; - -@end diff --git a/Libraries/Image/RCTShadowVirtualImage.m b/Libraries/Image/RCTShadowVirtualImage.m deleted file mode 100644 index 757a48c24ea056..00000000000000 --- a/Libraries/Image/RCTShadowVirtualImage.m +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTShadowVirtualImage.h" -#import "RCTImageLoader.h" -#import "RCTImageUtils.h" -#import "RCTBridge.h" -#import "RCTConvert.h" -#import "RCTUIManager.h" -#import "RCTUtils.h" - -@implementation RCTShadowVirtualImage -{ - RCTBridge *_bridge; - RCTImageLoaderCancellationBlock _cancellationBlock; -} - -@synthesize image = _image; - -- (instancetype)initWithBridge:(RCTBridge *)bridge -{ - if ((self = [super init])) { - _bridge = bridge; - } - return self; -} - -RCT_NOT_IMPLEMENTED(-(instancetype)init) - -- (void)didSetProps:(NSArray *)changedProps -{ - [super didSetProps:changedProps]; - - if (changedProps.count == 0) { - // No need to reload image - return; - } - - // Cancel previous request - if (_cancellationBlock) { - _cancellationBlock(); - } - - CGSize imageSize = { - RCTZeroIfNaN(self.width), - RCTZeroIfNaN(self.height), - }; - - __weak RCTShadowVirtualImage *weakSelf = self; - _cancellationBlock = [_bridge.imageLoader loadImageWithTag:_source.imageURL.absoluteString - size:imageSize - scale:RCTScreenScale() - resizeMode:_resizeMode - progressBlock:nil - completionBlock:^(NSError *error, UIImage *image) { - - dispatch_async(_bridge.uiManager.methodQueue, ^{ - RCTShadowVirtualImage *strongSelf = weakSelf; - if (![_source isEqual:strongSelf.source]) { - // Bail out if source has changed since we started loading - return; - } - strongSelf->_image = image; - [strongSelf dirtyText]; - }); - }]; -} - -- (void)dealloc -{ - if (_cancellationBlock) { - _cancellationBlock(); - } -} - -@end diff --git a/Libraries/Image/RCTVirtualImageManager.h b/Libraries/Image/RCTVirtualImageManager.h deleted file mode 100644 index b92896235d7f79..00000000000000 --- a/Libraries/Image/RCTVirtualImageManager.h +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTViewManager.h" - -@interface RCTVirtualImageManager : RCTViewManager - -@end diff --git a/Libraries/Image/RCTVirtualImageManager.m b/Libraries/Image/RCTVirtualImageManager.m deleted file mode 100644 index 6311010f4ce263..00000000000000 --- a/Libraries/Image/RCTVirtualImageManager.m +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTVirtualImageManager.h" -#import "RCTShadowVirtualImage.h" - -@implementation RCTVirtualImageManager - -RCT_EXPORT_MODULE() - -- (RCTShadowView *)shadowView -{ - return [[RCTShadowVirtualImage alloc] initWithBridge:self.bridge]; -} - -RCT_EXPORT_SHADOW_PROPERTY(source, RCTImageSource) -RCT_EXPORT_SHADOW_PROPERTY(resizeMode, UIViewContentMode) - -@end diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index adf7be667d5520..eced9cd0cd827e 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -13,12 +13,12 @@ #import "RCTUIManager.h" #import "RCTBridge.h" #import "RCTConvert.h" -#import "RCTImageComponent.h" #import "RCTLog.h" #import "RCTShadowRawText.h" #import "RCTText.h" #import "RCTUtils.h" +NSString *const RCTShadowViewAttributeName = @"RCTShadowViewAttributeName"; NSString *const RCTIsHighlightedAttributeName = @"IsHighlightedAttributeName"; NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName"; @@ -114,6 +114,45 @@ - (void)applyLayoutNode:(css_node_t *)node [self dirtyPropagation]; } +- (void)applyLayoutToChildren:(css_node_t *)node + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + absolutePosition:(CGPoint)absolutePosition +{ + // Run layout on subviews. + NSTextStorage *textStorage = [self buildTextStorageForWidth:self.frame.size.width widthMode:CSS_MEASURE_MODE_EXACTLY]; + NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; + NSTextContainer *textContainer = layoutManager.textContainers.firstObject; + NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; + NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL]; + [layoutManager.textStorage enumerateAttribute:RCTShadowViewAttributeName inRange:characterRange options:0 usingBlock:^(RCTShadowView *child, NSRange range, BOOL *_) { + if (child) { + css_node_t *childNode = child.cssNode; + float width = childNode->style.dimensions[CSS_WIDTH]; + float height = childNode->style.dimensions[CSS_HEIGHT]; + if (isUndefined(width) || isUndefined(height)) { + RCTLogError(@"Views nested within a must have a width and height"); + } + UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:range.location effectiveRange:nil]; + CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range inTextContainer:textContainer]; + CGRect childFrame = {{ + RCTRoundPixelValue(glyphRect.origin.x), + RCTRoundPixelValue(glyphRect.origin.y + glyphRect.size.height - height + font.descender) + }, { + RCTRoundPixelValue(width), + RCTRoundPixelValue(height) + }}; + + NSRange truncatedGlyphRange = [layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:range.location]; + BOOL childIsTruncated = NSIntersectionRange(range, truncatedGlyphRange).length != 0; + + [child collectUpdatedFrames:viewsWithNewFrame + withFrame:childFrame + hidden:childIsTruncated + absolutePosition:absolutePosition]; + } + }]; +} + - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width widthMode:(css_measure_mode_t)widthMode { if (_cachedTextStorage && width == _cachedTextStorageWidth && widthMode == _cachedTextStorageWidthMode) { @@ -199,33 +238,48 @@ - (NSAttributedString *)_attributedStringWithFontFamily:(NSString *)fontFamily _effectiveLetterSpacing = letterSpacing.doubleValue; + UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily + size:fontSize weight:fontWeight style:fontStyle + scaleMultiplier:_allowFontScaling ? _fontSizeMultiplier : 1.0]; + + CGFloat heightOfTallestSubview = 0.0; NSMutableAttributedString *attributedString = [NSMutableAttributedString new]; for (RCTShadowView *child in [self reactSubviews]) { if ([child isKindOfClass:[RCTShadowText class]]) { RCTShadowText *shadowText = (RCTShadowText *)child; [attributedString appendAttributedString: - [shadowText _attributedStringWithFontFamily:fontFamily - fontSize:fontSize - fontWeight:fontWeight - fontStyle:fontStyle - letterSpacing:letterSpacing - useBackgroundColor:YES - foregroundColor:shadowText.color ?: foregroundColor - backgroundColor:shadowText.backgroundColor ?: backgroundColor - opacity:opacity * shadowText.opacity]]; + [shadowText _attributedStringWithFontFamily:fontFamily + fontSize:fontSize + fontWeight:fontWeight + fontStyle:fontStyle + letterSpacing:letterSpacing + useBackgroundColor:YES + foregroundColor:shadowText.color ?: foregroundColor + backgroundColor:shadowText.backgroundColor ?: backgroundColor + opacity:opacity * shadowText.opacity]]; + [child setTextComputed]; } else if ([child isKindOfClass:[RCTShadowRawText class]]) { RCTShadowRawText *shadowRawText = (RCTShadowRawText *)child; [attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:shadowRawText.text ?: @""]]; - } else if ([child conformsToProtocol:@protocol(RCTImageComponent)]) { - NSTextAttachment *imageAttachment = [NSTextAttachment new]; - imageAttachment.image = ((id)child).image; - imageAttachment.bounds = (CGRect){CGPointZero, {RCTZeroIfNaN(child.width), RCTZeroIfNaN(child.height)}}; - [attributedString appendAttributedString:[NSAttributedString attributedStringWithAttachment:imageAttachment]]; + [child setTextComputed]; } else { - RCTLogError(@" can't have any children except , or raw strings"); + float width = child.cssNode->style.dimensions[CSS_WIDTH]; + float height = child.cssNode->style.dimensions[CSS_HEIGHT]; + if (isUndefined(width) || isUndefined(height)) { + RCTLogError(@"Views nested within a must have a width and height"); + } + NSTextAttachment *attachment = [NSTextAttachment new]; + attachment.bounds = (CGRect){CGPointZero, {width, height}}; + NSMutableAttributedString *attachmentString = [NSMutableAttributedString new]; + [attachmentString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]]; + [attachmentString addAttribute:RCTShadowViewAttributeName value:child range:(NSRange){0, attachmentString.length}]; + [attributedString appendAttributedString:attachmentString]; + if (height > heightOfTallestSubview) { + heightOfTallestSubview = height; + } + // Don't call setTextComputed on this child. RCTTextManager takes care of + // processing inline UIViews. } - - [child setTextComputed]; } [self _addAttribute:NSForegroundColorAttributeName @@ -241,13 +295,10 @@ - (NSAttributedString *)_attributedStringWithFontFamily:(NSString *)fontFamily toAttributedString:attributedString]; } - UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily - size:fontSize weight:fontWeight style:fontStyle - scaleMultiplier:_allowFontScaling ? _fontSizeMultiplier : 1.0]; [self _addAttribute:NSFontAttributeName withValue:font toAttributedString:attributedString]; [self _addAttribute:NSKernAttributeName withValue:letterSpacing toAttributedString:attributedString]; [self _addAttribute:RCTReactTagAttributeName withValue:self.reactTag toAttributedString:attributedString]; - [self _setParagraphStyleOnAttributedString:attributedString]; + [self _setParagraphStyleOnAttributedString:attributedString heightOfTallestSubview:heightOfTallestSubview]; // create a non-mutable attributedString for use by the Text system which avoids copies down the line _cachedAttributedString = [[NSAttributedString alloc] initWithAttributedString:attributedString]; @@ -270,6 +321,7 @@ - (void)_addAttribute:(NSString *)attribute withValue:(id)attributeValue toAttri * varying lineHeights, we simply take the max. */ - (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attributedString + heightOfTallestSubview:(CGFloat)heightOfTallestSubview { // check if we have lineHeight set on self __block BOOL hasParagraphStyle = NO; @@ -277,9 +329,7 @@ - (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attrib hasParagraphStyle = YES; } - if (!_lineHeight) { - self.lineHeight = 0.0; - } + __block float newLineHeight = _lineHeight ?: 0.0; CGFloat fontSizeMultiplier = _allowFontScaling ? _fontSizeMultiplier : 1.0; @@ -288,15 +338,25 @@ - (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attrib if (value) { NSParagraphStyle *paragraphStyle = (NSParagraphStyle *)value; CGFloat maximumLineHeight = round(paragraphStyle.maximumLineHeight / fontSizeMultiplier); - if (maximumLineHeight > self.lineHeight) { - self.lineHeight = maximumLineHeight; + if (maximumLineHeight > newLineHeight) { + newLineHeight = maximumLineHeight; } hasParagraphStyle = YES; } }]; - self.textAlign = _textAlign ?: NSTextAlignmentNatural; - self.writingDirection = _writingDirection ?: NSWritingDirectionNatural; + if (self.lineHeight != newLineHeight) { + self.lineHeight = newLineHeight; + } + + NSTextAlignment newTextAlign = _textAlign ?: NSTextAlignmentNatural; + if (self.textAlign != newTextAlign) { + self.textAlign = newTextAlign; + } + NSWritingDirection newWritingDirection = _writingDirection ?: NSWritingDirectionNatural; + if (self.writingDirection != newWritingDirection) { + self.writingDirection = newWritingDirection; + } // if we found anything, set it :D if (hasParagraphStyle) { @@ -304,6 +364,9 @@ - (void)_setParagraphStyleOnAttributedString:(NSMutableAttributedString *)attrib paragraphStyle.alignment = _textAlign; paragraphStyle.baseWritingDirection = _writingDirection; CGFloat lineHeight = round(_lineHeight * fontSizeMultiplier); + if (heightOfTallestSubview > lineHeight) { + lineHeight = ceilf(heightOfTallestSubview); + } paragraphStyle.minimumLineHeight = lineHeight; paragraphStyle.maximumLineHeight = lineHeight; [attributedString addAttribute:NSParagraphStyleAttributeName diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index e57b93aa191f7a..864fa096b42dc0 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -13,6 +13,17 @@ #import "RCTUtils.h" #import "UIView+React.h" +static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDescendants) +{ + for (UIView *child in view.reactSubviews) { + if ([child isKindOfClass:[RCTText class]]) { + collectNonTextDescendants((RCTText *)child, nonTextDescendants); + } else if (!CGRectEqualToRect(child.frame, CGRectZero)) { + [nonTextDescendants addObject:child]; + } + } +} + @implementation RCTText { NSTextStorage *_textStorage; @@ -76,6 +87,21 @@ - (void)setTextStorage:(NSTextStorage *)textStorage { if (_textStorage != textStorage) { _textStorage = textStorage; + + NSMutableArray *nonTextDescendants = [NSMutableArray new]; + collectNonTextDescendants(self, nonTextDescendants); + NSArray *subviews = self.subviews; + if (![subviews isEqualToArray:nonTextDescendants]) { + for (UIView *child in subviews) { + if (![nonTextDescendants containsObject:child]) { + [child removeFromSuperview]; + } + } + for (UIView *child in nonTextDescendants) { + [self addSubview:child]; + } + } + [self setNeedsDisplay]; } } diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index 92fa589496d2a0..9104ea72c073f6 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -20,6 +20,18 @@ #import "RCTTextView.h" #import "UIView+React.h" +static void collectDirtyNonTextDescendants(RCTShadowText *shadowView, NSMutableArray *nonTextDescendants) { + for (RCTShadowView *child in shadowView.reactSubviews) { + if ([child isKindOfClass:[RCTShadowText class]]) { + collectDirtyNonTextDescendants((RCTShadowText *)child, nonTextDescendants); + } else if ([child isKindOfClass:[RCTShadowRawText class]]) { + // no-op + } else if ([child isTextDirty]) { + [nonTextDescendants addObject:child]; + } + } +} + @interface RCTShadowText (Private) - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width widthMode:(css_measure_mode_t)widthMode; @@ -85,6 +97,7 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(NSDictionary tag. Not rendering string: '%@'", [(RCTShadowRawText *)shadowView text]); diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index c43d45fe5be9d8..f103966c31b1ec 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -217,7 +217,6 @@ 13E067501A70F44B002CDEE1 /* RCTView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTView.m; sourceTree = ""; }; 13E067531A70F44B002CDEE1 /* UIView+React.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+React.h"; sourceTree = ""; }; 13E067541A70F44B002CDEE1 /* UIView+React.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+React.m"; sourceTree = ""; }; - 13EF7F441BC69646003F47DD /* RCTImageComponent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTImageComponent.h; sourceTree = ""; }; 13F17A831B8493E5007D4C75 /* RCTRedBox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRedBox.h; sourceTree = ""; }; 13F17A841B8493E5007D4C75 /* RCTRedBox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRedBox.m; sourceTree = ""; }; 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptLoader.h; sourceTree = ""; }; @@ -388,7 +387,6 @@ 13AB90BF1B6FA36700713B4F /* RCTComponentData.h */, 13456E941ADAD482009F94A7 /* RCTConvert+MapKit.h */, 13AB90C01B6FA36700713B4F /* RCTComponentData.m */, - 13EF7F441BC69646003F47DD /* RCTImageComponent.h */, 13456E911ADAD2DE009F94A7 /* RCTConvert+CoreLocation.h */, 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */, 13456E951ADAD482009F94A7 /* RCTConvert+MapKit.m */, diff --git a/React/Views/RCTImageComponent.h b/React/Views/RCTImageComponent.h deleted file mode 100644 index a6916c9aa63be0..00000000000000 --- a/React/Views/RCTImageComponent.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import - -/** - * Generic interface for components that contain an image. - */ -@protocol RCTImageComponent - -@property (nonatomic, strong, readonly) UIImage *image; - -@end diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index ad09ae9f061783..67955a9bc2d671 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -140,12 +140,33 @@ typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry parentProperties:(NSDictionary *)parentProperties NS_REQUIRES_SUPER; /** - * Recursively apply layout to children. + * Can be called by a parent on a child in order to calculate all views whose frame needs + * updating in that branch. Adds these frames to `viewsWithNewFrame`. Useful if layout + * enters a view where flex doesn't apply (e.g. Text) and then you want to resume flex + * layout on a subview. + */ +- (void)collectUpdatedFrames:(NSMutableSet *)viewsWithNewFrame + withFrame:(CGRect)frame + hidden:(BOOL)hidden + absolutePosition:(CGPoint)absolutePosition; + +/** + * Apply the CSS layout. + * This method also calls `applyLayoutToChildren:` internally. The functionality + * is split into two methods so subclasses can override `applyLayoutToChildren:` + * while using default implementation of `applyLayoutNode:`. */ - (void)applyLayoutNode:(css_node_t *)node viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame absolutePosition:(CGPoint)absolutePosition NS_REQUIRES_SUPER; +/** + * Enumerate the child nodes and tell them to apply layout. + */ +- (void)applyLayoutToChildren:(css_node_t *)node + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + absolutePosition:(CGPoint)absolutePosition; + /** * The following are implementation details exposed to subclasses. Do not call them directly */ diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index d55e80c624c96e..83ce22a5beb1fe 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -157,6 +157,13 @@ - (void)applyLayoutNode:(css_node_t *)node absolutePosition.x += node->layout.position[CSS_LEFT]; absolutePosition.y += node->layout.position[CSS_TOP]; + [self applyLayoutToChildren:node viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition]; +} + +- (void)applyLayoutToChildren:(css_node_t *)node + viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame + absolutePosition:(CGPoint)absolutePosition +{ for (int i = 0; i < node->children_count; ++i) { RCTShadowView *child = (RCTShadowView *)_reactSubviews[i]; [child applyLayoutNode:node->get_child(node->context, i) @@ -209,6 +216,36 @@ - (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks } } +- (void)collectUpdatedFrames:(NSMutableSet *)viewsWithNewFrame + withFrame:(CGRect)frame + hidden:(BOOL)hidden + absolutePosition:(CGPoint)absolutePosition +{ + if (_hidden != hidden) { + // The hidden state has changed. Even if the frame hasn't changed, add + // this ShadowView to viewsWithNewFrame so the UIManager will process + // this ShadowView's UIView and update its hidden state. + _hidden = hidden; + [viewsWithNewFrame addObject:self]; + } + + if (!CGRectEqualToRect(frame, _frame)) { + _cssNode->style.position_type = CSS_POSITION_ABSOLUTE; + _cssNode->style.dimensions[CSS_WIDTH] = frame.size.width; + _cssNode->style.dimensions[CSS_HEIGHT] = frame.size.height; + _cssNode->style.position[CSS_LEFT] = frame.origin.x; + _cssNode->style.position[CSS_TOP] = frame.origin.y; + // Our parent has asked us to change our cssNode->styles. Dirty the layout + // so that we can rerun layout on this node. The request came from our parent + // so there's no need to dirty our ancestors by calling dirtyLayout. + _layoutLifecycle = RCTUpdateLifecycleDirtied; + } + + [self fillCSSNode:_cssNode]; + layoutNode(_cssNode, frame.size.width, frame.size.height, CSS_DIRECTION_INHERIT); + [self applyLayoutNode:_cssNode viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition]; +} + - (CGRect)measureLayoutRelativeToAncestor:(RCTShadowView *)ancestor { CGPoint offset = CGPointZero; @@ -459,6 +496,7 @@ - (void)set##setProp:(CGFloat)value \ { \ _cssNode->style.dimensions[CSS_##cssProp] = value; \ [self dirtyLayout]; \ + [self dirtyText]; \ } \ - (CGFloat)getProp \ { \ diff --git a/docs/Text.md b/docs/Text.md index 800137b03b0b71..48853606e432fe 100644 --- a/docs/Text.md +++ b/docs/Text.md @@ -21,6 +21,20 @@ Behind the scenes, React Native converts this to a flat `NSAttributedString` or 9-17: bold, red ``` +## Nested Views (iOS Only) + +On iOS, you can nest views within your Text component. Here's an example: + +```javascript + + There is a blue square + + in between my text. + +``` + +In order to use this feature, you must give the view a `width` and a `height`. + ## Containers The `` element is special relative to layout: everything inside is no longer using the flexbox layout but using text layout. This means that elements inside of a `` are no longer rectangles, but wrap when they see the end of the line. From 69627bf91476274e92396370acff08fb20b8f3fc Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Tue, 31 May 2016 11:32:19 -0700 Subject: [PATCH 161/843] Clean up examples. Summary: The recent and upcoming API refactoring works makes maintaining the examples harder and harder. This removes the old examples that use too much deprecated APIs such as Reducers, Animated View. The new examples will be forcus on using new API with all-in-one sample codes and detailed documentation. Reviewed By: ericvicenti Differential Revision: D3354711 fbshipit-source-id: ac6360b1573989eb075d91cb9bf1ae8357dce7fa --- .../NavigationAnimatedExample.js | 164 --------- .../NavigationBasicExample.js | 110 ------ .../NavigationCardStack-example.js | 203 +++++++++++ .../NavigationCardStackExample.js | 165 --------- .../NavigationCompositionExample.js | 321 ----------------- .../NavigationExampleTabBar.js | 83 ----- .../NavigationExperimentalExample.js | 7 +- .../NavigationTabsExample.js | 120 ------- .../NavigationTicTacToeExample.js | 326 ------------------ .../NavigationStateUtils.js | 3 + 10 files changed, 207 insertions(+), 1295 deletions(-) delete mode 100644 Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js delete mode 100644 Examples/UIExplorer/NavigationExperimental/NavigationBasicExample.js create mode 100644 Examples/UIExplorer/NavigationExperimental/NavigationCardStack-example.js delete mode 100644 Examples/UIExplorer/NavigationExperimental/NavigationCardStackExample.js delete mode 100644 Examples/UIExplorer/NavigationExperimental/NavigationCompositionExample.js delete mode 100644 Examples/UIExplorer/NavigationExperimental/NavigationExampleTabBar.js delete mode 100644 Examples/UIExplorer/NavigationExperimental/NavigationTabsExample.js delete mode 100644 Examples/UIExplorer/NavigationExperimental/NavigationTicTacToeExample.js diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js deleted file mode 100644 index 020408a5d0fabe..00000000000000 --- a/Examples/UIExplorer/NavigationExperimental/NavigationAnimatedExample.js +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ -'use strict'; - -const React = require('react'); -const ReactNative = require('react-native'); - -const { - Animated, - NavigationExperimental, - StyleSheet, - ScrollView, -} = ReactNative; - -const NavigationExampleRow = require('./NavigationExampleRow'); - -const { - AnimatedView: NavigationAnimatedView, - Card: NavigationCard, - Header: NavigationHeader, - Reducer: NavigationReducer, -} = NavigationExperimental; - -const ExampleReducer = NavigationReducer.StackReducer({ - getPushedReducerForAction: (action) => { - if (action.type === 'push') { - return (state) => state || {key: action.key}; - } - return null; - }, - getReducerForState: (initialState) => (state) => state || initialState, - initialState: { - key: 'AnimatedExampleStackKey', - index: 0, - routes: [ - {key: 'First Route'}, - ], - }, -}); - -class NavigationAnimatedExample extends React.Component { - constructor(props, context) { - super(props, context); - this.state = ExampleReducer(); - } - - componentWillMount() { - this._renderCard = this._renderCard.bind(this); - this._renderHeader = this._renderHeader.bind(this); - this._renderScene = this._renderScene.bind(this); - this._renderTitleComponent = this._renderTitleComponent.bind(this); - this._handleAction = this._handleAction.bind(this); - } - - _handleAction(action): boolean { - if (!action) { - return false; - } - const newState = ExampleReducer(this.state, action); - if (newState === this.state) { - return false; - } - this.setState(newState); - return true; - } - - handleBackAction(): boolean { - return this._handleAction({ type: 'BackAction', }); - } - - render() { - return ( - { - Animated.timing(pos, {toValue: navState.index, duration: 500}).start(); - }} - renderScene={this._renderCard} - /> - ); - } - - _renderHeader(/*NavigationSceneRendererProps*/ props) { - return ( - - ); - } - - _renderTitleComponent(/*NavigationSceneRendererProps*/ props) { - return ( - - {props.scene.route.key} - - ); - } - - _renderCard(/*NavigationSceneRendererProps*/ props) { - return ( - - ); - } - - _renderScene(/*NavigationSceneRendererProps*/ props) { - return ( - - - { - props.onNavigate({ - type: 'push', - key: 'Route #' + props.scenes.length, - }); - }} - /> - - - ); - } -} - -const styles = StyleSheet.create({ - animatedView: { - flex: 1, - }, - scrollView: { - marginTop: NavigationHeader.HEIGHT, - }, -}); - -module.exports = NavigationAnimatedExample; diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationBasicExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationBasicExample.js deleted file mode 100644 index d296ebbd7715bb..00000000000000 --- a/Examples/UIExplorer/NavigationExperimental/NavigationBasicExample.js +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ -'use strict'; - -const React = require('react'); -const ReactNative = require('react-native'); -const { - NavigationExperimental, - ScrollView, - StyleSheet, -} = ReactNative; -const NavigationExampleRow = require('./NavigationExampleRow'); -const { - Reducer: NavigationReducer, -} = NavigationExperimental; - -const ExampleReducer = NavigationReducer.StackReducer({ - getPushedReducerForAction: (action) => { - if (action.type === 'push') { - return (state) => state || {key: action.key}; - } - return null; - }, - getReducerForState: (initialState) => (state) => state || initialState, - initialState: { - key: 'BasicExampleStackKey', - index: 0, - routes: [ - {key: 'First Route'}, - ], - }, -}); - -const NavigationBasicExample = React.createClass({ - - getInitialState: function() { - return ExampleReducer(); - }, - - render: function() { - return ( - - - { - this._handleAction({ type: 'push', key: 'page #' + this.state.routes.length }); - }} - /> - { - this._handleAction({ type: 'BackAction' }); - }} - /> - - - ); - }, - - _handleAction(action) { - if (!action) { - return false; - } - const newState = ExampleReducer(this.state, action); - if (newState === this.state) { - return false; - } - this.setState(newState); - return true; - }, - - handleBackAction() { - return this._handleAction({ type: 'BackAction' }); - }, - -}); - -const styles = StyleSheet.create({ - topView: { - backgroundColor: '#E9E9EF', - flex: 1, - paddingTop: 30, - }, -}); - -module.exports = NavigationBasicExample; diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationCardStack-example.js b/Examples/UIExplorer/NavigationExperimental/NavigationCardStack-example.js new file mode 100644 index 00000000000000..c54cc1ba9e9b6a --- /dev/null +++ b/Examples/UIExplorer/NavigationExperimental/NavigationCardStack-example.js @@ -0,0 +1,203 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +'use strict'; + +const NavigationExampleRow = require('./NavigationExampleRow'); +const React = require('react'); +const ReactNative = require('react-native'); + +const emptyFunction = require('fbjs/lib/emptyFunction'); + +/** + * Basic example that shows how to use to build + * an app with controlled navigation system. + */ +const { + NavigationExperimental, + ScrollView, + StyleSheet, +} = ReactNative; + +const { + CardStack: NavigationCardStack, + StateUtils: NavigationStateUtils, +} = NavigationExperimental; + +// Step 1: +// Define a component for your application. +class YourApplication extends React.Component { + + // This sets up the initial navigation state. + constructor(props, context) { + super(props, context); + + this.state = { + // This defines the initial navigation state. + navigationState: { + index: 0, // starts with first route focused. + routes: [{key: 'Welcome'}], // starts with only one route. + }, + }; + + this._exit = this._exit.bind(this); + this._onNavigationChange = this._onNavigationChange.bind(this); + } + + // User your own navigator (see Step 2). + render(): ReactElement { + return ( + + ); + } + + // This handles the navigation state changes. You're free and responsible + // to define the API that changes that navigation state. In this exmaple, + // we'd simply use a `function(type: string)` to update the navigation state. + _onNavigationChange(type: string): void { + let {navigationState} = this.state; + switch (type) { + case 'push': + // push a new route. + const route = {key: Date.now()}; + navigationState = NavigationStateUtils.push(navigationState, route); + break; + + case 'pop': + navigationState = NavigationStateUtils.pop(navigationState); + break; + } + + // NavigationStateUtils gives you back the same `navigationState` if nothing + // has changed. You could use that to avoid redundant re-rendering. + if (this.state.navigationState !== navigationState) { + this.setState({navigationState}); + } + } + + // Exits the example. `this.props.onExampleExit` is provided + // by the UI Explorer. + _exit(): void { + this.props.onExampleExit && this.props.onExampleExit(); + } + + // This public method is optional. If exists, the UI explorer will call it + // the "back button" is pressed. Normally this is the cases for Android only. + handleBackAction(): boolean { + return this._onNavigationChange('pop'); + } +} + +// Step 2: +// Define your own controlled navigator. +// +// +------------+ +// +-+ | +// +-+ | | +// | | | | +// | | | Active | +// | | | Scene | +// | | | | +// +-+ | | +// +-+ | +// +------------+ +// +class YourNavigator extends React.Component { + + // This sets up the methods (e.g. Pop, Push) for navigation. + constructor(props: any, context: any) { + super(props, context); + + this._onPushRoute = this.props.onNavigationChange.bind(null, 'push'); + this._onPopRoute = this.props.onNavigationChange.bind(null, 'pop'); + + this._renderScene = this._renderScene.bind(this); + } + + // Now use the `NavigationCardStack` to render the scenes. + render(): ReactElement { + // TODO(hedger): prop `onNavigate` will be deprecated soon. For now, + // use `emptyFunction` as a placeholder. + return ( + + ); + } + + // Render a scene for route. + // The detailed spec of `sceneProps` is defined at `NavigationTypeDefinition` + // as type `NavigationSceneRendererProps`. + _renderScene(sceneProps: Object): ReactElement { + return ( + + ); + } +} + +// Step 3: +// Define your own scene. +class YourScene extends React.Component { + render() { + return ( + + + + + + + ); + } +} + +const styles = StyleSheet.create({ + navigator: { + flex: 1, + }, + scrollView: { + marginTop: 64 + }, +}); + +module.exports = YourApplication; diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationCardStackExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationCardStackExample.js deleted file mode 100644 index bf00ad739baeb8..00000000000000 --- a/Examples/UIExplorer/NavigationExperimental/NavigationCardStackExample.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ -'use strict'; - -const NavigationExampleRow = require('./NavigationExampleRow'); -const React = require('react'); -const ReactNative = require('react-native'); - -const { - NavigationExperimental, - StyleSheet, - ScrollView, -} = ReactNative; - -const { - CardStack: NavigationCardStack, - StateUtils: NavigationStateUtils, -} = NavigationExperimental; - -function createReducer(initialState) { - return (currentState = initialState, action) => { - switch (action.type) { - case 'push': - return NavigationStateUtils.push(currentState, {key: action.key}); - - case 'BackAction': - case 'back': - case 'pop': - return currentState.index > 0 ? - NavigationStateUtils.pop(currentState) : - currentState; - - default: - return currentState; - } - }; -} - -const ExampleReducer = createReducer({ - index: 0, - key: 'exmaple', - routes: [{key: 'First Route'}], -}); - -class NavigationCardStackExample extends React.Component { - - constructor(props, context) { - super(props, context); - this.state = { - isHorizontal: true, - navState: ExampleReducer(undefined, {}), - }; - } - - componentWillMount() { - this._renderScene = this._renderScene.bind(this); - this._toggleDirection = this._toggleDirection.bind(this); - this._handleAction = this._handleAction.bind(this); - } - - render() { - return ( - - ); - } - - _handleAction(action): boolean { - if (!action) { - return false; - } - const newState = ExampleReducer(this.state.navState, action); - if (newState === this.state.navState) { - return false; - } - this.setState({ - navState: newState, - }); - return true; - } - - handleBackAction(): boolean { - return this._handleAction({ type: 'BackAction', }); - } - - _renderScene(/*NavigationSceneRendererProps*/ props) { - return ( - - - - { - props.onNavigate({ - type: 'push', - key: 'Route ' + props.scenes.length, - }); - }} - /> - { - props.onNavigate({ - type: 'pop', - }); - }} - /> - - - ); - } - - _toggleDirection() { - this.setState({ - isHorizontal: !this.state.isHorizontal, - }); - } - -} - -const styles = StyleSheet.create({ - main: { - flex: 1, - }, - scrollView: { - marginTop: 64 - }, -}); - -module.exports = NavigationCardStackExample; diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationCompositionExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationCompositionExample.js deleted file mode 100644 index 261e8060dff3be..00000000000000 --- a/Examples/UIExplorer/NavigationExperimental/NavigationCompositionExample.js +++ /dev/null @@ -1,321 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - * @flow - */ -'use strict'; - -const React = require('react'); -const ReactNative = require('react-native'); -const NavigationExampleRow = require('./NavigationExampleRow'); -const NavigationExampleTabBar = require('./NavigationExampleTabBar'); - -const { - NavigationExperimental, - ScrollView, - StyleSheet, - View, -} = ReactNative; - -const { - CardStack: NavigationCardStack, - Header: NavigationHeader, - Reducer: NavigationReducer, -} = NavigationExperimental; - - -import type { - NavigationState, - NavigationSceneRenderer, - NavigationSceneRendererProps, -} from 'NavigationTypeDefinition'; - -type Action = { - isExitAction?: boolean, -}; - -const ExampleExitAction = () => ({ - isExitAction: true, -}); - -ExampleExitAction.match = (action: Action) => ( - action && action.isExitAction === true -); - -const PageAction = (type) => ({ - type, - isPageAction: true, -}); - -PageAction.match = (action) => ( - action && action.isPageAction === true -); - -const ExampleProfilePageAction = (type) => ({ - ...PageAction(type), - isProfilePageAction: true, -}); - -ExampleProfilePageAction.match = (action) => ( - action && action.isProfilePageAction === true -); - -const ExampleInfoAction = () => PageAction('InfoPage'); - -const ExampleNotifProfileAction = () => ExampleProfilePageAction('NotifProfilePage'); - -const _jsInstanceUniqueId = '' + Date.now(); - -let _uniqueIdCount = 0; - -function pageStateActionMap(action) { - return { - key: 'page-' + _jsInstanceUniqueId + '-' + (_uniqueIdCount++), - type: action.type, - }; -} - -const ExampleAppReducer = NavigationReducer.TabsReducer({ - key: 'AppNavigationState', - initialIndex: 0, - tabReducers: [ - NavigationReducer.StackReducer({ - getPushedReducerForAction: (action) => { - if (PageAction.match(action) && !ExampleProfilePageAction.match(action)) { - return (state) => (state || pageStateActionMap(action)); - } - return null; - }, - initialState: { - key: 'notifs', - index: 0, - routes: [ - {key: 'base', type: 'NotifsPage'}, - ], - }, - }), - NavigationReducer.StackReducer({ - getPushedReducerForAction: (action) => { - if (PageAction.match(action) && !ExampleProfilePageAction.match(action)) { - return (state) => (state || pageStateActionMap(action)); - } - return null; - }, - initialState: { - key: 'settings', - index: 0, - routes: [ - {key: 'base', type: 'SettingsPage'}, - ], - }, - }), - NavigationReducer.StackReducer({ - getPushedReducerForAction: (action) => { - if (PageAction.match(action) || ExampleProfilePageAction.match(action)) { - return (state) => (state || pageStateActionMap(action)); - } - return null; - }, - initialState: { - key: 'profile', - index: 0, - routes: [ - {key: 'base', type: 'ProfilePage'}, - ], - }, - }), - ], -}); - -function stateTypeTitleMap(pageState: any) { - switch (pageState.type) { - case 'ProfilePage': - return 'Profile Page'; - case 'NotifsPage': - return 'Notifications'; - case 'SettingsPage': - return 'Settings'; - case 'InfoPage': - return 'Info Page'; - case 'NotifProfilePage': - return 'Page in Profile'; - } -} - -class ExampleTabScreen extends React.Component { - _renderCard: NavigationSceneRenderer; - _renderHeader: NavigationSceneRenderer; - _renderScene: NavigationSceneRenderer; - - componentWillMount() { - this._renderHeader = this._renderHeader.bind(this); - this._renderScene = this._renderScene.bind(this); - } - - render() { - return ( - - ); - } - _renderHeader(props: NavigationSceneRendererProps) { - return ( - - ); - } - - _renderTitleComponent(props: NavigationSceneRendererProps) { - return ( - - {stateTypeTitleMap(props.scene.route)} - - ); - } - - _renderScene(props: NavigationSceneRendererProps) { - const {onNavigate} = props; - return ( - - { - onNavigate(ExampleInfoAction()); - }} - /> - { - onNavigate(ExampleNotifProfileAction()); - }} - /> - { - onNavigate(ExampleExitAction()); - }} - /> - - ); - } -} - -class NavigationCompositionExample extends React.Component { - state: NavigationState; - constructor() { - super(); - this.state = ExampleAppReducer(undefined, {}); - } - handleAction(action: Object): boolean { - if (!action) { - return false; - } - const newState = ExampleAppReducer(this.state, action); - if (newState === this.state) { - return false; - } - this.setState(newState); - return true; - } - handleBackAction(): boolean { - return this.handleAction({ type: 'BackAction' }); - } - render() { - if (!this.state) { - return null; - } - return ( - - - - - ); - } -} - -class ExampleMainView extends React.Component { - _renderScene: Function; - _handleNavigation: Function; - - componentWillMount() { - this._renderScene = this._renderScene.bind(this); - this._handleNavigation = this._handleNavigation.bind(this); - } - - render() { - return ( - - {this._renderScene()} - - ); - } - - _renderScene(): ReactElement { - const {navigationState} = this.props; - const childState = navigationState.routes[navigationState.index]; - return ( - - ); - } - - _handleNavigation(action: Object) { - if (ExampleExitAction.match(action)) { - this.props.onExampleExit(); - return; - } - this.props.onNavigate(action); - } -} - -const styles = StyleSheet.create({ - topView: { - flex: 1, - }, - tabsContent: { - flex: 1, - }, - scrollView: { - marginTop: NavigationHeader.HEIGHT - }, - tabContent: { - flex: 1, - }, -}); - -module.exports = NavigationCompositionExample; diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationExampleTabBar.js b/Examples/UIExplorer/NavigationExperimental/NavigationExampleTabBar.js deleted file mode 100644 index 40f525466f9f9b..00000000000000 --- a/Examples/UIExplorer/NavigationExperimental/NavigationExampleTabBar.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ -'use strict'; - -const NavigationExperimental = require('NavigationExperimental'); -const React = require('react'); -const StyleSheet = require('StyleSheet'); -const Text = require('Text'); -const TouchableOpacity = require('TouchableOpacity'); -const View = require('View'); -const { - Reducer: NavigationReducer, -} = NavigationExperimental; -const { - JumpToAction, -} = NavigationReducer.TabsReducer; - -const NavigationExampleTabBar = React.createClass({ - render: function() { - return ( - - {this.props.tabs.map(this._renderTab)} - - ); - }, - _renderTab: function(tab, index) { - const textStyle = [styles.tabButtonText]; - if (this.props.index === index) { - textStyle.push(styles.selectedTabButtonText); - } - return ( - { - this.props.onNavigate(JumpToAction(index)); - }}> - - {tab.key} - - - ); - }, -}); - -const styles = StyleSheet.create({ - tabBar: { - height: 50, - flexDirection: 'row', - }, - tabButton: { - flex: 1, - }, - tabButtonText: { - paddingTop: 20, - textAlign: 'center', - fontSize: 17, - fontWeight: '500', - }, - selectedTabButtonText: { - color: 'blue', - }, -}); - -module.exports = NavigationExampleTabBar; diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationExperimentalExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationExperimentalExample.js index f0b0f922ed127a..12bb5664361c35 100644 --- a/Examples/UIExplorer/NavigationExperimental/NavigationExperimentalExample.js +++ b/Examples/UIExplorer/NavigationExperimental/NavigationExperimentalExample.js @@ -33,12 +33,7 @@ const View = require('View'); * To learn how to use the Navigation API, take a look at the following example files: */ const EXAMPLES = { - 'Tabs': require('./NavigationTabsExample'), - 'Basic': require('./NavigationBasicExample'), - 'Animated Example': require('./NavigationAnimatedExample'), - 'Composition': require('./NavigationCompositionExample'), - 'Card Stack Example': require('./NavigationCardStackExample'), - 'Tic Tac Toe': require('./NavigationTicTacToeExample'), + 'NavigationCardStack Example': require('./NavigationCardStack-example'), }; const EXAMPLE_STORAGE_KEY = 'NavigationExperimentalExample'; diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationTabsExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationTabsExample.js deleted file mode 100644 index f89e1ae5bc1275..00000000000000 --- a/Examples/UIExplorer/NavigationExperimental/NavigationTabsExample.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ -'use strict'; - -const React = require('react'); -const ReactNative = require('react-native'); -const { - NavigationExperimental, - ScrollView, - StyleSheet, - View, -} = ReactNative; -const { - Reducer: NavigationReducer, -} = NavigationExperimental; - -const NavigationExampleRow = require('./NavigationExampleRow'); -const NavigationExampleTabBar = require('./NavigationExampleTabBar'); - -class ExmpleTabPage extends React.Component { - render() { - const currentTabRoute = this.props.tabs[this.props.index]; - return ( - - - {this.props.tabs.map((tab, index) => ( - { - this.props.onNavigate(NavigationReducer.TabsReducer.JumpToAction(index)); - }} - /> - ))} - - - ); - } -} - -const ExampleTabsReducer = NavigationReducer.TabsReducer({ - tabReducers: [ - (lastRoute) => lastRoute || {key: 'one'}, - (lastRoute) => lastRoute || {key: 'two'}, - (lastRoute) => lastRoute || {key: 'three'}, - ], -}); - -class NavigationTabsExample extends React.Component { - constructor() { - super(); - this.state = ExampleTabsReducer(undefined, {}); - } - render() { - return ( - - - - - ); - } - handleAction(action) { - if (!action) { - return false; - } - const newState = ExampleTabsReducer(this.state, action); - if (newState === this.state) { - return false; - } - this.setState(newState); - return true; - } - handleBackAction() { - return this.handleAction({ type: 'BackAction' }); - } -} - -const styles = StyleSheet.create({ - topView: { - flex: 1, - paddingTop: 30, - }, - tabPage: { - backgroundColor: '#E9E9EF', - }, -}); - -module.exports = NavigationTabsExample; diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationTicTacToeExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationTicTacToeExample.js deleted file mode 100644 index 24ea3aee5ca39f..00000000000000 --- a/Examples/UIExplorer/NavigationExperimental/NavigationTicTacToeExample.js +++ /dev/null @@ -1,326 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - * @providesModule NavigationTicTacToeExample - * @flow - */ -'use strict'; - -const React = require('react'); -const StyleSheet = require('StyleSheet'); -const Text = require('Text'); -const TouchableHighlight = require('TouchableHighlight'); -const View = require('View'); - -type GameGrid = Array>; - -const evenOddPlayerMap = ['O', 'X']; -const rowLetterMap = ['a', 'b', 'c']; - -function parseGame(game: string): GameGrid { - const gameTurns = game ? game.split('-') : []; - const grid = Array(3); - for (let i = 0; i < 3; i++) { - const row = Array(3); - for (let j = 0; j < 3; j++) { - const turnIndex = gameTurns.indexOf(rowLetterMap[i] + j); - if (turnIndex === -1) { - row[j] = null; - } else { - row[j] = evenOddPlayerMap[turnIndex % 2]; - } - } - grid[i] = row; - } - return grid; -} - -function playTurn(game: string, row: number, col: number): string { - const turn = rowLetterMap[row] + col; - return game ? (game + '-' + turn) : turn; -} - -function getWinner(gameString: string): ?string { - const game = parseGame(gameString); - for (let i = 0; i < 3; i++) { - if (game[i][0] !== null && game[i][0] === game[i][1] && - game[i][0] === game[i][2]) { - return game[i][0]; - } - } - for (let i = 0; i < 3; i++) { - if (game[0][i] !== null && game[0][i] === game[1][i] && - game[0][i] === game[2][i]) { - return game[0][i]; - } - } - if (game[0][0] !== null && game[0][0] === game[1][1] && - game[0][0] === game[2][2]) { - return game[0][0]; - } - if (game[0][2] !== null && game[0][2] === game[1][1] && - game[0][2] === game[2][0]) { - return game[0][2]; - } - return null; -} - -function isGameOver(gameString: string): boolean { - if (getWinner(gameString)) { - return true; - } - const game = parseGame(gameString); - for (let i = 0; i < 3; i++) { - for (let j = 0; j < 3; j++) { - if (game[i][j] === null) { - return false; - } - } - } - return true; -} - -class Cell extends React.Component { - props: any; - cellStyle() { - switch (this.props.player) { - case 'X': - return styles.cellX; - case 'O': - return styles.cellO; - default: - return null; - } - } - textStyle() { - switch (this.props.player) { - case 'X': - return styles.cellTextX; - case 'O': - return styles.cellTextO; - default: - return {}; - } - } - render() { - return ( - - - - {this.props.player} - - - - ); - } -} - -function GameEndOverlay(props) { - if (!isGameOver(props.game)) { - return ; - } - const winner = getWinner(props.game); - return ( - - - {winner ? winner + ' wins!' : 'It\'s a tie!'} - - props.onNavigate(GameActions.Reset())} - underlayColor="transparent" - activeOpacity={0.5}> - - New Game - - - - ); -} - -function TicTacToeGame(props) { - const rows = parseGame(props.game).map((cells, row) => - - {cells.map((player, col) => - props.onNavigate(GameActions.Turn(row, col))} - /> - )} - - ); - return ( - - - Close - - EXTREME T3 - - {rows} - - - - ); -} - -const GameActions = { - Turn: (row, col) => ({type: 'TicTacToeTurnAction', row, col }), - Reset: (row, col) => ({type: 'TicTacToeResetAction' }), -}; - -function GameReducer(lastGame: ?string, action: Object): string { - if (!lastGame) { - lastGame = ''; - } - if (action.type === 'TicTacToeResetAction') { - return ''; - } - if (!isGameOver(lastGame) && action.type === 'TicTacToeTurnAction') { - return playTurn(lastGame, action.row, action.col); - } - return lastGame; -} - -type AppState = { - game: string, -}; - -class NavigationTicTacToeExample extends React.Component { - _handleAction: Function; - state: AppState; - constructor() { - super(); - this._handleAction = this._handleAction.bind(this); - this.state = { - game: '' - }; - } - _handleAction(action: Object) { - const newState = GameReducer(this.state.game, action); - if (newState !== this.state.game) { - this.setState({ - game: newState, - }); - } - } - render() { - return ( - - ); - } -} - -const styles = StyleSheet.create({ - closeButton: { - position: 'absolute', - left: 10, - top: 30, - fontSize: 14, - }, - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'white' - }, - title: { - fontFamily: 'Chalkduster', - fontSize: 39, - marginBottom: 20, - }, - board: { - padding: 5, - backgroundColor: '#47525d', - borderRadius: 10, - }, - row: { - flexDirection: 'row', - }, - cell: { - width: 80, - height: 80, - borderRadius: 5, - backgroundColor: '#7b8994', - margin: 5, - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - cellX: { - backgroundColor: '#72d0eb', - }, - cellO: { - backgroundColor: '#7ebd26', - }, - cellText: { - fontSize: 50, - fontFamily: 'AvenirNext-Bold', - }, - cellTextX: { - color: '#19a9e5', - }, - cellTextO: { - color: '#b9dc2f', - }, - overlay: { - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - backgroundColor: 'rgba(221, 221, 221, 0.5)', - flex: 1, - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - }, - overlayMessage: { - fontSize: 40, - marginBottom: 20, - marginLeft: 20, - marginRight: 20, - fontFamily: 'AvenirNext-DemiBold', - textAlign: 'center', - }, - newGame: { - backgroundColor: '#887766', - padding: 20, - borderRadius: 5, - }, - newGameText: { - color: 'white', - fontSize: 20, - fontFamily: 'AvenirNext-DemiBold', - }, -}); - -module.exports = NavigationTicTacToeExample; diff --git a/Libraries/NavigationExperimental/NavigationStateUtils.js b/Libraries/NavigationExperimental/NavigationStateUtils.js index 4ef94cbadf6b4c..8ff97e8447fba0 100644 --- a/Libraries/NavigationExperimental/NavigationStateUtils.js +++ b/Libraries/NavigationExperimental/NavigationStateUtils.js @@ -65,6 +65,9 @@ function push(state: NavigationState, newChildState: NavigationRoute): Navigatio } function pop(state: NavigationState): NavigationState { + if (state.index <= 0) { + return state; + } const lastChildren = state.routes; return { ...state, From 0f57702cd813cab7a68f66c583b5a5c934d579d5 Mon Sep 17 00:00:00 2001 From: Andrew Sardone Date: Tue, 31 May 2016 12:19:39 -0700 Subject: [PATCH 162/843] Enable ATS w/ localhost exception for generated iOS proj MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Building off of the work in https://github.com/facebook/react-native/pull/5290, this… - Remove the total disabling of ATS from the react-native generated iOS project in favor of a localhost exception Closes https://github.com/facebook/react-native/pull/5355 Differential Revision: D2837517 fbshipit-source-id: ba4b7bd2f6ba4359f5d45175944b990f9927db3b --- docs/KnownIssues.md | 14 ++++++++++++++ docs/RunningOnDeviceIOS.md | 6 +++++- .../generator-ios/templates/app/Info.plist | 18 ++++++++++++------ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/docs/KnownIssues.md b/docs/KnownIssues.md index c2ce26bd83b107..600eefb0a60c43 100644 --- a/docs/KnownIssues.md +++ b/docs/KnownIssues.md @@ -71,3 +71,17 @@ Try running `react-native init` with `--verbose` and see [#2797](https://github. ### Text Input Border The text input has by default a border at the bottom of its view. This border has its padding set by the background image provided by the system, and it cannot be changed. Solutions to avoid this is to either not set height explicitly, case in which the system will take care of displaying the border in the correct position, or to not display the border by setting underlineColorAndroid to transparent. + +### iOS App Transport Security and loading HTTP resources + +As of iOS 9, new Xcode projects enable App Transport Security by default, which rejects all HTTP requests that are not sent over HTTPS. This can result in HTTP traffic being blocked (including the developer React Native server) with the following console errors: + +``` +App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file. +``` + +``` +NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802) +``` + +See our guide for [running on an iOS device](RunningOnDeviceIOS.md) and working around the ATS issues, or [several](http://useyourloaf.com/blog/app-transport-security/) [community](https://www.hackingwithswift.com/example-code/system/how-to-handle-the-https-requirements-in-ios-9-with-app-transport-security) [posts](https://ste.vn/2015/06/10/configuring-app-transport-security-ios-9-osx-10-11/) on handling common cases like [whitelisting specific domains](http://stackoverflow.com/a/30732693) for non-HTTPS traffic. diff --git a/docs/RunningOnDeviceIOS.md b/docs/RunningOnDeviceIOS.md index 619b048fcea22d..5ff7548014b7b7 100644 --- a/docs/RunningOnDeviceIOS.md +++ b/docs/RunningOnDeviceIOS.md @@ -15,7 +15,11 @@ You can iterate quickly on device using development server. To do that, your lap 1. Open `AwesomeApp/ios/AwesomeApp/AppDelegate.m` 2. Change the IP in the URL from `localhost` to your laptop's IP. On Mac, you can find the IP address in System Preferences / Network. -3. In Xcode select your phone as build target and press "Build and run" +3. Temporarily disable App Transport Security (ATS) by [adding the `NSAllowsArbitraryLoads` entry to your `Info.plist` file][gpl]. Since ATS does not allow insecure HTTP requests to IP addresses, you must completely disable it to run on a device. This is only a requirement for development on a device, and unless you can't workaround an issue you should leave ATS enabled for production builds. For more information, see [this post on configuring ATS][bats]. +4. In Xcode select your phone as build target and press "Build and run" + +[gpl]: https://gist.github.com/andrewsardone/91797ff9923b9ac6ea64 +[bats]: http://ste.vn/2015/06/10/configuring-app-transport-security-ios-9-osx-10-11/ > Hint > diff --git a/local-cli/generator-ios/templates/app/Info.plist b/local-cli/generator-ios/templates/app/Info.plist index 91963b2670722d..e98ebb00425451 100644 --- a/local-cli/generator-ios/templates/app/Info.plist +++ b/local-cli/generator-ios/templates/app/Info.plist @@ -38,11 +38,17 @@ NSLocationWhenInUseUsageDescription - NSAppTransportSecurity - - - NSAllowsArbitraryLoads - - + NSAppTransportSecurity + + + NSExceptionDomains + + localhost + + NSTemporaryExceptionAllowsInsecureHTTPLoads + + + + From abe0b349bcdb59155445889d9615c13294a37c91 Mon Sep 17 00:00:00 2001 From: Alexey Lang Date: Tue, 31 May 2016 12:50:48 -0700 Subject: [PATCH 163/843] Implement RCTJSCWrapper Reviewed By: tadeuzagallo Differential Revision: D3258713 fbshipit-source-id: 418eb9d350bf3541c976b631bd9799a1c578f5e5 --- React/Base/RCTPerformanceLogger.h | 2 + React/Base/RCTPerformanceLogger.m | 2 + React/Executors/RCTJSCExecutor.h | 26 +++- React/Executors/RCTJSCExecutor.mm | 177 +++++++++++++++----------- React/Executors/RCTJSCWrapper.h | 58 +++++++++ React/Executors/RCTJSCWrapper.mm | 112 ++++++++++++++++ React/React.xcodeproj/project.pbxproj | 6 + 7 files changed, 306 insertions(+), 77 deletions(-) create mode 100644 React/Executors/RCTJSCWrapper.h create mode 100644 React/Executors/RCTJSCWrapper.mm diff --git a/React/Base/RCTPerformanceLogger.h b/React/Base/RCTPerformanceLogger.h index 051254fcc2d304..3cfd62b0f68947 100644 --- a/React/Base/RCTPerformanceLogger.h +++ b/React/Base/RCTPerformanceLogger.h @@ -24,6 +24,8 @@ typedef NS_ENUM(NSUInteger, RCTPLTag) { RCTPLNativeModulePrepareConfig, RCTPLNativeModuleInjectConfig, RCTPLNativeModuleMainThreadUsesCount, + RCTPLJSCWrapperOpenLibrary, + RCTPLJSCWrapperLoadFunctions, RCTPLJSCExecutorSetup, RCTPLBridgeStartup, RCTPLTTI, diff --git a/React/Base/RCTPerformanceLogger.m b/React/Base/RCTPerformanceLogger.m index 5223fc82393798..68e38f434fbab1 100644 --- a/React/Base/RCTPerformanceLogger.m +++ b/React/Base/RCTPerformanceLogger.m @@ -97,6 +97,8 @@ void RCTPerformanceLoggerAppendEnd(RCTPLTag tag) @"NativeModulePrepareConfig", @"NativeModuleInjectConfig", @"NativeModuleMainThreadUsesCount", + @"JSCWrapperOpenLibrary", + @"JSCWrapperLoadFunctions", @"JSCExecutorSetup", @"BridgeStartup", @"RootViewTTI", diff --git a/React/Executors/RCTJSCExecutor.h b/React/Executors/RCTJSCExecutor.h index 48fd16c399502a..f5b91bf8f06b74 100644 --- a/React/Executors/RCTJSCExecutor.h +++ b/React/Executors/RCTJSCExecutor.h @@ -25,17 +25,31 @@ RCT_EXTERN NSString *const RCTJSCThreadName; */ RCT_EXTERN NSString *const RCTJavaScriptContextCreatedNotification; +/** + * Uses a JavaScriptCore context as the execution engine. + */ +@interface RCTJSCExecutor : NSObject + +/** + * Sets a type of JSC library (system or custom) that's used + * to initialize RCTJSCWrapper. + * @default is NO. + */ ++ (void)setUseCustomJSCLibrary:(BOOL)useCustomLibrary; + +/** + * Gets a type of JSC library (system or custom) that's used + * to initialize RCTJSCWrapper. + * @default is NO. + */ ++ (BOOL)useCustomJSCLibrary; + /** * Create a NSError from a JSError object. * * If available, the error's userInfo property will contain the JS stacktrace under * the RCTJSStackTraceKey key. */ -RCT_EXTERN NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError); - -/** - * Uses a JavaScriptCore context as the execution engine. - */ -@interface RCTJSCExecutor : NSObject +- (NSError *)convertJSErrorToNSError:(JSValueRef)jsError context:(JSContextRef)context; @end diff --git a/React/Executors/RCTJSCExecutor.mm b/React/Executors/RCTJSCExecutor.mm index 7841834e8316ac..91e80d7b882bc8 100644 --- a/React/Executors/RCTJSCExecutor.mm +++ b/React/Executors/RCTJSCExecutor.mm @@ -12,13 +12,8 @@ #import #import #import +#import -#ifdef WITH_FB_JSC_TUNING -#include -#include -#endif - -#import #import #import "RCTAssert.h" @@ -33,6 +28,7 @@ #import "RCTJSCProfiler.h" #import "RCTRedBox.h" #import "RCTSourceCode.h" +#import "RCTJSCWrapper.h" NSString *const RCTJSCThreadName = @"com.facebook.react.JavaScript"; @@ -137,6 +133,8 @@ @implementation RCTJSCExecutor JSStringRef _bundleURL; RandomAccessBundleData _randomAccessBundle; + + RCTJSCWrapper *_jscWrapper; } @synthesize valid = _valid; @@ -144,36 +142,36 @@ @implementation RCTJSCExecutor RCT_EXPORT_MODULE() -static NSString *RCTJSValueToNSString(JSContextRef context, JSValueRef value, JSValueRef *exception) +static NSString *RCTJSValueToNSString(RCTJSCWrapper *jscWrapper, JSContextRef context, JSValueRef value, JSValueRef *exception) { - JSStringRef JSString = JSValueToStringCopy(context, value, exception); + JSStringRef JSString = jscWrapper->JSValueToStringCopy(context, value, exception); if (!JSString) { return nil; } - CFStringRef string = JSStringCopyCFString(kCFAllocatorDefault, JSString); - JSStringRelease(JSString); + CFStringRef string = jscWrapper->JSStringCopyCFString(kCFAllocatorDefault, JSString); + jscWrapper->JSStringRelease(JSString); return (__bridge_transfer NSString *)string; } -static NSString *RCTJSValueToJSONString(JSContextRef context, JSValueRef value, JSValueRef *exception, unsigned indent) +static NSString *RCTJSValueToJSONString(RCTJSCWrapper *jscWrapper, JSContextRef context, JSValueRef value, JSValueRef *exception, unsigned indent) { - JSStringRef JSString = JSValueCreateJSONString(context, value, indent, exception); - CFStringRef string = JSStringCopyCFString(kCFAllocatorDefault, JSString); - JSStringRelease(JSString); + JSStringRef jsString = jscWrapper->JSValueCreateJSONString(context, value, indent, exception); + CFStringRef string = jscWrapper->JSStringCopyCFString(kCFAllocatorDefault, jsString); + jscWrapper->JSStringRelease(jsString); return (__bridge_transfer NSString *)string; } -NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) +static NSError *RCTNSErrorFromJSError(RCTJSCWrapper *jscWrapper, JSContextRef context, JSValueRef jsError) { NSMutableDictionary *errorInfo = [NSMutableDictionary new]; - NSString *description = jsError ? RCTJSValueToNSString(context, jsError, NULL) : @"Unknown JS error"; + NSString *description = jsError ? RCTJSValueToNSString(jscWrapper, context, jsError, NULL) : @"Unknown JS error"; errorInfo[NSLocalizedDescriptionKey] = [@"Unhandled JS Exception: " stringByAppendingString:description]; - NSString *details = jsError ? RCTJSValueToJSONString(context, jsError, NULL, 0) : nil; + NSString *details = jsError ? RCTJSValueToJSONString(jscWrapper, context, jsError, NULL, 0) : nil; if (details) { errorInfo[NSLocalizedFailureReasonErrorKey] = details; @@ -219,6 +217,11 @@ @implementation RCTJSCExecutor return [NSError errorWithDomain:RCTErrorDomain code:1 userInfo:errorInfo]; } +- (NSError *)convertJSErrorToNSError:(JSValueRef)jsError context:(JSContextRef)context +{ + return RCTNSErrorFromJSError(_jscWrapper, context, jsError); +} + #if RCT_DEV static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) @@ -259,6 +262,18 @@ + (void)runRunLoopThread } } +static BOOL useCustomJSCLibrary = NO; + ++ (void)setUseCustomJSCLibrary:(BOOL)useCustomLibrary +{ + useCustomJSCLibrary = useCustomLibrary; +} + ++ (BOOL)useCustomJSCLibrary +{ + return useCustomJSCLibrary; +} + - (instancetype)init { if (self = [super init]) { @@ -290,7 +305,7 @@ - (RCTJavaScriptContext *)context } if (!_context) { - JSContext *context = [JSContext new]; + JSContext *context = [_jscWrapper->JSContext new]; _context = [[RCTJavaScriptContext alloc] initWithJSContext:context onThread:_javaScriptThread]; [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptContextCreatedNotification @@ -312,21 +327,33 @@ - (void)setUp { __weak RCTJSCExecutor *weakSelf = self; -#ifdef WITH_FB_JSC_TUNING [self executeBlockOnJavaScriptQueue:^{ RCTJSCExecutor *strongSelf = weakSelf; if (!strongSelf.valid) { return; } + strongSelf->_jscWrapper = RCTJSCWrapperCreate(useCustomJSCLibrary); + }]; + + + [self executeBlockOnJavaScriptQueue:^{ + RCTJSCExecutor *strongSelf = weakSelf; + if (!strongSelf.valid) { + return; + } + + if (strongSelf->_jscWrapper->configureJSContextForIOS == NULL) { + return; + } + NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]; RCTAssert(cachesPath != nil, @"cachesPath should not be nil"); if (cachesPath) { std::string path = std::string([cachesPath UTF8String]); - configureJSContextForIOS(strongSelf.context.ctx, path); + strongSelf->_jscWrapper->configureJSContextForIOS(strongSelf.context.ctx, path); } }]; -#endif [self addSynchronousHookWithName:@"noop" usingBlock:^{}]; @@ -454,11 +481,12 @@ - (void)setUp return; } - JSStringRef execJSString = JSStringCreateWithUTF8CString(sourceCode.UTF8String); - JSStringRef jsURL = JSStringCreateWithUTF8CString(sourceCodeURL.UTF8String); - JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, NULL); - JSStringRelease(jsURL); - JSStringRelease(execJSString); + RCTJSCWrapper *jscWrapper = strongSelf->_jscWrapper; + JSStringRef execJSString = jscWrapper->JSStringCreateWithUTF8CString(sourceCode.UTF8String); + JSStringRef jsURL = jscWrapper->JSStringCreateWithUTF8CString(sourceCodeURL.UTF8String); + jscWrapper->JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, NULL); + jscWrapper->JSStringRelease(jsURL); + jscWrapper->JSStringRelease(execJSString); }]; #endif } @@ -496,6 +524,12 @@ - (void)dealloc _randomAccessBundle.bundle.reset(); _randomAccessBundle.table.reset(); + + if (_jscWrapper) { + RCTJSCWrapperRelease(_jscWrapper); + _jscWrapper = NULL; + } + if (_cookieMap) { CFRelease(_cookieMap); } @@ -539,41 +573,42 @@ - (void)_executeJSCall:(NSString *)method JSValueRef errorJSRef = NULL; JSValueRef resultJSRef = NULL; - JSGlobalContextRef contextJSRef = JSContextGetGlobalContext(strongSelf->_context.ctx); + RCTJSCWrapper *jscWrapper = strongSelf->_jscWrapper; + JSGlobalContextRef contextJSRef = jscWrapper->JSContextGetGlobalContext(strongSelf->_context.ctx); JSContext *context = strongSelf->_context.context; - JSObjectRef globalObjectJSRef = JSContextGetGlobalObject(strongSelf->_context.ctx); + JSObjectRef globalObjectJSRef = jscWrapper->JSContextGetGlobalObject(strongSelf->_context.ctx); // get the BatchedBridge object - JSStringRef moduleNameJSStringRef = JSStringCreateWithUTF8CString("__fbBatchedBridge"); - JSValueRef moduleJSRef = JSObjectGetProperty(contextJSRef, globalObjectJSRef, moduleNameJSStringRef, &errorJSRef); - JSStringRelease(moduleNameJSStringRef); + JSStringRef moduleNameJSStringRef = jscWrapper->JSStringCreateWithUTF8CString("__fbBatchedBridge"); + JSValueRef moduleJSRef = jscWrapper->JSObjectGetProperty(contextJSRef, globalObjectJSRef, moduleNameJSStringRef, &errorJSRef); + jscWrapper->JSStringRelease(moduleNameJSStringRef); - if (moduleJSRef != NULL && errorJSRef == NULL && !JSValueIsUndefined(contextJSRef, moduleJSRef)) { + if (moduleJSRef != NULL && errorJSRef == NULL && !jscWrapper->JSValueIsUndefined(contextJSRef, moduleJSRef)) { // get method - JSStringRef methodNameJSStringRef = JSStringCreateWithCFString((__bridge CFStringRef)method); - JSValueRef methodJSRef = JSObjectGetProperty(contextJSRef, (JSObjectRef)moduleJSRef, methodNameJSStringRef, &errorJSRef); - JSStringRelease(methodNameJSStringRef); + JSStringRef methodNameJSStringRef = jscWrapper->JSStringCreateWithCFString((__bridge CFStringRef)method); + JSValueRef methodJSRef = jscWrapper->JSObjectGetProperty(contextJSRef, (JSObjectRef)moduleJSRef, methodNameJSStringRef, &errorJSRef); + jscWrapper->JSStringRelease(methodNameJSStringRef); - if (methodJSRef != NULL && errorJSRef == NULL && !JSValueIsUndefined(contextJSRef, methodJSRef)) { + if (methodJSRef != NULL && errorJSRef == NULL && !jscWrapper->JSValueIsUndefined(contextJSRef, methodJSRef)) { JSValueRef jsArgs[arguments.count]; for (NSUInteger i = 0; i < arguments.count; i++) { - jsArgs[i] = [JSValue valueWithObject:arguments[i] inContext:context].JSValueRef; + jsArgs[i] = [jscWrapper->JSValue valueWithObject:arguments[i] inContext:context].JSValueRef; } - resultJSRef = JSObjectCallAsFunction(contextJSRef, (JSObjectRef)methodJSRef, (JSObjectRef)moduleJSRef, arguments.count, jsArgs, &errorJSRef); + resultJSRef = jscWrapper->JSObjectCallAsFunction(contextJSRef, (JSObjectRef)methodJSRef, (JSObjectRef)moduleJSRef, arguments.count, jsArgs, &errorJSRef); } else { - if (!errorJSRef && JSValueIsUndefined(contextJSRef, methodJSRef)) { + if (!errorJSRef && jscWrapper->JSValueIsUndefined(contextJSRef, methodJSRef)) { error = RCTErrorWithMessage([NSString stringWithFormat:@"Unable to execute JS call: method %@ is undefined", method]); } } } else { - if (!errorJSRef && JSValueIsUndefined(contextJSRef, moduleJSRef)) { + if (!errorJSRef && jscWrapper->JSValueIsUndefined(contextJSRef, moduleJSRef)) { error = RCTErrorWithMessage(@"Unable to execute JS call: __fbBatchedBridge is undefined"); } } if (errorJSRef || error) { if (!error) { - error = RCTNSErrorFromJSError(contextJSRef, errorJSRef); + error = RCTNSErrorFromJSError(jscWrapper, contextJSRef, errorJSRef); } onComplete(nil, error); return; @@ -585,8 +620,8 @@ - (void)_executeJSCall:(NSString *)method id objcValue; // We often return `null` from JS when there is nothing for native side. JSONKit takes an extra hundred microseconds // to handle this simple case, so we are adding a shortcut to make executeJSCall method even faster - if (!JSValueIsNull(contextJSRef, resultJSRef)) { - objcValue = [[JSValue valueWithJSValueRef:resultJSRef inContext:context] toObject]; + if (!jscWrapper->JSValueIsNull(contextJSRef, resultJSRef)) { + objcValue = [[jscWrapper->JSValue valueWithJSValueRef:resultJSRef inContext:context] toObject]; } onComplete(objcValue, nil); @@ -624,8 +659,6 @@ - (void)executeApplicationScript:(NSData *)script script = nullTerminatedScript; } - _bundleURL = JSStringCreateWithUTF8CString(sourceURL.absoluteString.UTF8String); - __weak RCTJSCExecutor *weakSelf = self; [self executeBlockOnJavaScriptQueue:RCTProfileBlock((^{ @@ -637,15 +670,17 @@ - (void)executeApplicationScript:(NSData *)script RCTPerformanceLoggerStart(RCTPLScriptExecution); JSValueRef jsError = NULL; - JSStringRef execJSString = JSStringCreateWithUTF8CString((const char *)script.bytes); - JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, _bundleURL, 0, &jsError); - JSStringRelease(execJSString); + RCTJSCWrapper *jscWrapper = strongSelf->_jscWrapper; + JSStringRef execJSString = jscWrapper->JSStringCreateWithUTF8CString((const char *)script.bytes); + JSStringRef bundleURL = jscWrapper->JSStringCreateWithUTF8CString(sourceURL.absoluteString.UTF8String); + JSValueRef result = jscWrapper->JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, bundleURL, 0, &jsError); + jscWrapper->JSStringRelease(execJSString); RCTPerformanceLoggerEnd(RCTPLScriptExecution); if (onComplete) { NSError *error; if (!result) { - error = RCTNSErrorFromJSError(strongSelf->_context.ctx, jsError); + error = RCTNSErrorFromJSError(jscWrapper, strongSelf->_context.ctx, jsError); } onComplete(error); } @@ -684,9 +719,11 @@ - (void)injectJSONText:(NSString *)script if (!strongSelf || !strongSelf.isValid) { return; } - JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script); - JSValueRef valueToInject = JSValueMakeFromJSONString(strongSelf->_context.ctx, execJSString); - JSStringRelease(execJSString); + + RCTJSCWrapper *jscWrapper = strongSelf->_jscWrapper; + JSStringRef execJSString = jscWrapper->JSStringCreateWithCFString((__bridge CFStringRef)script); + JSValueRef valueToInject = jscWrapper->JSValueMakeFromJSONString(strongSelf->_context.ctx, execJSString); + jscWrapper->JSStringRelease(execJSString); if (!valueToInject) { NSString *errorDesc = [NSString stringWithFormat:@"Can't make JSON value from script '%@'", script]; @@ -699,17 +736,17 @@ - (void)injectJSONText:(NSString *)script return; } - JSObjectRef globalObject = JSContextGetGlobalObject(strongSelf->_context.ctx); - JSStringRef JSName = JSStringCreateWithCFString((__bridge CFStringRef)objectName); - JSObjectSetProperty(strongSelf->_context.ctx, globalObject, JSName, valueToInject, kJSPropertyAttributeNone, NULL); - JSStringRelease(JSName); + JSObjectRef globalObject = jscWrapper->JSContextGetGlobalObject(strongSelf->_context.ctx); + JSStringRef JSName = jscWrapper->JSStringCreateWithCFString((__bridge CFStringRef)objectName); + jscWrapper->JSObjectSetProperty(strongSelf->_context.ctx, globalObject, JSName, valueToInject, kJSPropertyAttributeNone, NULL); + jscWrapper->JSStringRelease(JSName); if (onComplete) { onComplete(nil); } }), 0, @"js_call,json_call", (@{@"objectName": objectName}))]; } -static bool readRandomAccessModule(const RandomAccessBundleData& bundleData, size_t offset, size_t size, char *data) +static bool readRandomAccessModule(const RandomAccessBundleData &bundleData, size_t offset, size_t size, char *data) { return fseek(bundleData.bundle.get(), offset + bundleData.baseOffset, SEEK_SET) == 0 && fread(data, 1, size, bundleData.bundle.get()) == size; @@ -726,17 +763,18 @@ static void executeRandomAccessModule(RCTJSCExecutor *executor, uint32_t moduleI char url[14]; // 10 = maximum decimal digits in a 32bit unsigned int + ".js" + null byte sprintf(url, "%" PRIu32 ".js", moduleID); - JSStringRef code = JSStringCreateWithUTF8CString(data.get()); + RCTJSCWrapper *jscWrapper = executor->_jscWrapper; + JSStringRef code = jscWrapper->JSStringCreateWithUTF8CString(data.get()); JSValueRef jsError = NULL; - JSStringRef sourceURL = JSStringCreateWithUTF8CString(url); - JSValueRef result = JSEvaluateScript(executor->_context.ctx, code, NULL, sourceURL, 0, &jsError); + JSStringRef sourceURL = jscWrapper->JSStringCreateWithUTF8CString(url); + JSValueRef result = jscWrapper->JSEvaluateScript(executor->_context.ctx, code, NULL, sourceURL, 0, &jsError); - JSStringRelease(code); - JSStringRelease(sourceURL); + jscWrapper->JSStringRelease(code); + jscWrapper->JSStringRelease(sourceURL); if (!result) { dispatch_async(dispatch_get_main_queue(), ^{ - RCTFatal(RCTNSErrorFromJSError(executor->_context.ctx, jsError)); + RCTFatal(RCTNSErrorFromJSError(jscWrapper, executor->_context.ctx, jsError)); [executor invalidate]; }); } @@ -780,7 +818,7 @@ - (void)registerNativeRequire }]; } -static RandomAccessBundleStartupCode readRAMBundle(file_ptr bundle, RandomAccessBundleData& randomAccessBundle) +static RandomAccessBundleStartupCode readRAMBundle(file_ptr bundle, RandomAccessBundleData &randomAccessBundle) { // read in magic header, number of entries, and length of the startup section uint32_t header[3]; @@ -846,13 +884,10 @@ - (NSData *)loadRAMBundle:(NSURL *)sourceURL error:(NSError **)error RCT_EXPORT_METHOD(setContextName:(nonnull NSString *)name) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wtautological-pointer-compare" - if (JSGlobalContextSetName != NULL) { -#pragma clang diagnostic pop - JSStringRef JSName = JSStringCreateWithCFString((__bridge CFStringRef)name); - JSGlobalContextSetName(_context.ctx, JSName); - JSStringRelease(JSName); + if (_jscWrapper->JSGlobalContextSetName != NULL) { + JSStringRef JSName = _jscWrapper->JSStringCreateWithCFString((__bridge CFStringRef)name); + _jscWrapper->JSGlobalContextSetName(_context.ctx, JSName); + _jscWrapper->JSStringRelease(JSName); } } diff --git a/React/Executors/RCTJSCWrapper.h b/React/Executors/RCTJSCWrapper.h new file mode 100644 index 00000000000000..fc0ca29a33b35f --- /dev/null +++ b/React/Executors/RCTJSCWrapper.h @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "RCTDefines.h" +#import + +typedef JSStringRef (*JSValueToStringCopyFuncType)(JSContextRef, JSValueRef, JSValueRef *); +typedef JSStringRef (*JSStringCreateWithCFStringFuncType)(CFStringRef); +typedef CFStringRef (*JSStringCopyCFStringFuncType)(CFAllocatorRef, JSStringRef); +typedef JSStringRef (*JSStringCreateWithUTF8CStringFuncType)(const char *); +typedef void (*JSStringReleaseFuncType)(JSStringRef); +typedef void (*JSGlobalContextSetNameFuncType)(JSGlobalContextRef, JSStringRef); +typedef JSGlobalContextRef (*JSContextGetGlobalContextFuncType)(JSContextRef); +typedef void (*JSObjectSetPropertyFuncType)(JSContextRef, JSObjectRef, JSStringRef, JSValueRef, JSPropertyAttributes, JSValueRef *); +typedef JSObjectRef (*JSContextGetGlobalObjectFuncType)(JSContextRef); +typedef JSValueRef (*JSObjectGetPropertyFuncType)(JSContextRef, JSObjectRef, JSStringRef, JSValueRef *); +typedef JSValueRef (*JSValueMakeFromJSONStringFuncType)(JSContextRef, JSStringRef); +typedef JSValueRef (*JSObjectCallAsFunctionFuncType)(JSContextRef, JSObjectRef, JSObjectRef, size_t, const JSValueRef *, JSValueRef *); +typedef JSValueRef (*JSValueMakeNullFuncType)(JSContextRef); +typedef JSStringRef (*JSValueCreateJSONStringFuncType)(JSContextRef, JSValueRef, unsigned, JSValueRef *); +typedef bool (*JSValueIsUndefinedFuncType)(JSContextRef, JSValueRef); +typedef bool (*JSValueIsNullFuncType)(JSContextRef, JSValueRef); +typedef JSValueRef (*JSEvaluateScriptFuncType)(JSContextRef, JSStringRef, JSObjectRef, JSStringRef, int, JSValueRef *); +typedef void (*configureJSContextForIOSFuncType)(JSContextRef ctx, const std::string &cacheDir); + +typedef struct RCTJSCWrapper { + JSValueToStringCopyFuncType JSValueToStringCopy; + JSStringCreateWithCFStringFuncType JSStringCreateWithCFString; + JSStringCopyCFStringFuncType JSStringCopyCFString; + JSStringCreateWithUTF8CStringFuncType JSStringCreateWithUTF8CString; + JSStringReleaseFuncType JSStringRelease; + JSGlobalContextSetNameFuncType JSGlobalContextSetName; + JSContextGetGlobalContextFuncType JSContextGetGlobalContext; + JSObjectSetPropertyFuncType JSObjectSetProperty; + JSContextGetGlobalObjectFuncType JSContextGetGlobalObject; + JSObjectGetPropertyFuncType JSObjectGetProperty; + JSValueMakeFromJSONStringFuncType JSValueMakeFromJSONString; + JSObjectCallAsFunctionFuncType JSObjectCallAsFunction; + JSValueMakeNullFuncType JSValueMakeNull; + JSValueCreateJSONStringFuncType JSValueCreateJSONString; + JSValueIsUndefinedFuncType JSValueIsUndefined; + JSValueIsNullFuncType JSValueIsNull; + JSEvaluateScriptFuncType JSEvaluateScript; + Class JSContext; + Class JSValue; + configureJSContextForIOSFuncType configureJSContextForIOS; +} RCTJSCWrapper; + +RCT_EXTERN RCTJSCWrapper *RCTJSCWrapperCreate(BOOL useCustomJSC); +RCT_EXTERN void RCTJSCWrapperRelease(RCTJSCWrapper *wrapper); diff --git a/React/Executors/RCTJSCWrapper.mm b/React/Executors/RCTJSCWrapper.mm new file mode 100644 index 00000000000000..4da53aeaa76cd7 --- /dev/null +++ b/React/Executors/RCTJSCWrapper.mm @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTJSCWrapper.h" + +#import +#import + +#import "RCTLog.h" +#import "RCTPerformanceLogger.h" + +#include + +static void *RCTCustomLibraryHandler(void) +{ + static dispatch_once_t token; + static void *handler; + dispatch_once(&token, ^{ + const char *path = [[[NSBundle mainBundle] pathForResource:@"JavaScriptCore" + ofType:nil + inDirectory:@"JavaScriptCore.framework"] UTF8String]; + if (path) { + RCTPerformanceLoggerStart(RCTPLJSCWrapperOpenLibrary); + handler = dlopen(path, RTLD_LAZY); + RCTPerformanceLoggerEnd(RCTPLJSCWrapperOpenLibrary); + if (!handler) { + RCTLogWarn(@"Can't load custome JSC library: %s", dlerror()); + } + } + }); + return handler; +} + +static void RCTSetUpSystemLibraryPointers(RCTJSCWrapper *wrapper) +{ + wrapper->JSValueToStringCopy = JSValueToStringCopy; + wrapper->JSStringCreateWithCFString = JSStringCreateWithCFString; + wrapper->JSStringCopyCFString = JSStringCopyCFString; + wrapper->JSStringCreateWithUTF8CString = JSStringCreateWithUTF8CString; + wrapper->JSStringRelease = JSStringRelease; + wrapper->JSGlobalContextSetName = JSGlobalContextSetName; + wrapper->JSContextGetGlobalContext = JSContextGetGlobalContext; + wrapper->JSObjectSetProperty = JSObjectSetProperty; + wrapper->JSContextGetGlobalObject = JSContextGetGlobalObject; + wrapper->JSObjectGetProperty = JSObjectGetProperty; + wrapper->JSValueMakeFromJSONString = JSValueMakeFromJSONString; + wrapper->JSObjectCallAsFunction = JSObjectCallAsFunction; + wrapper->JSValueMakeNull = JSValueMakeNull; + wrapper->JSValueCreateJSONString = JSValueCreateJSONString; + wrapper->JSValueIsUndefined = JSValueIsUndefined; + wrapper->JSValueIsNull = JSValueIsNull; + wrapper->JSEvaluateScript = JSEvaluateScript; + wrapper->JSContext = [JSContext class]; + wrapper->JSValue = [JSValue class]; + wrapper->configureJSContextForIOS = NULL; +} + +static void RCTSetUpCustomLibraryPointers(RCTJSCWrapper *wrapper) +{ + void *libraryHandle = RCTCustomLibraryHandler(); + if (!libraryHandle) { + RCTSetUpSystemLibraryPointers(wrapper); + return; + } + + RCTPerformanceLoggerStart(RCTPLJSCWrapperLoadFunctions); + wrapper->JSValueToStringCopy = (JSValueToStringCopyFuncType)dlsym(libraryHandle, "JSValueToStringCopy"); + wrapper->JSStringCreateWithCFString = (JSStringCreateWithCFStringFuncType)dlsym(libraryHandle, "JSStringCreateWithCFString"); + wrapper->JSStringCopyCFString = (JSStringCopyCFStringFuncType)dlsym(libraryHandle, "JSStringCopyCFString"); + wrapper->JSStringCreateWithUTF8CString = (JSStringCreateWithUTF8CStringFuncType)dlsym(libraryHandle, "JSStringCreateWithUTF8CString"); + wrapper->JSStringRelease = (JSStringReleaseFuncType)dlsym(libraryHandle, "JSStringRelease"); + wrapper->JSGlobalContextSetName = (JSGlobalContextSetNameFuncType)dlsym(libraryHandle, "JSGlobalContextSetName"); + wrapper->JSContextGetGlobalContext = (JSContextGetGlobalContextFuncType)dlsym(libraryHandle, "JSContextGetGlobalContext"); + wrapper->JSObjectSetProperty = (JSObjectSetPropertyFuncType)dlsym(libraryHandle, "JSObjectSetProperty"); + wrapper->JSContextGetGlobalObject = (JSContextGetGlobalObjectFuncType)dlsym(libraryHandle, "JSContextGetGlobalObject"); + wrapper->JSObjectGetProperty = (JSObjectGetPropertyFuncType)dlsym(libraryHandle, "JSObjectGetProperty"); + wrapper->JSValueMakeFromJSONString = (JSValueMakeFromJSONStringFuncType)dlsym(libraryHandle, "JSValueMakeFromJSONString"); + wrapper->JSObjectCallAsFunction = (JSObjectCallAsFunctionFuncType)dlsym(libraryHandle, "JSObjectCallAsFunction"); + wrapper->JSValueMakeNull = (JSValueMakeNullFuncType)dlsym(libraryHandle, "JSValueMakeNull"); + wrapper->JSValueCreateJSONString = (JSValueCreateJSONStringFuncType)dlsym(libraryHandle, "JSValueCreateJSONString"); + wrapper->JSValueIsUndefined = (JSValueIsUndefinedFuncType)dlsym(libraryHandle, "JSValueIsUndefined"); + wrapper->JSValueIsNull = (JSValueIsNullFuncType)dlsym(libraryHandle, "JSValueIsNull"); + wrapper->JSEvaluateScript = (JSEvaluateScriptFuncType)dlsym(libraryHandle, "JSEvaluateScript"); + wrapper->JSContext = (__bridge Class)dlsym(libraryHandle, "OBJC_CLASS_$_JSContext"); + wrapper->JSValue = (__bridge Class)dlsym(libraryHandle, "OBJC_CLASS_$_JSValue"); + wrapper->configureJSContextForIOS = (configureJSContextForIOSFuncType)dlsym(libraryHandle, "configureJSContextForIOS"); + RCTPerformanceLoggerEnd(RCTPLJSCWrapperLoadFunctions); +} + +RCTJSCWrapper *RCTJSCWrapperCreate(BOOL useCustomJSC) +{ + RCTJSCWrapper *wrapper = (RCTJSCWrapper *)malloc(sizeof(RCTJSCWrapper)); + if (useCustomJSC && [UIDevice currentDevice].systemVersion.floatValue >= 8) { + RCTSetUpCustomLibraryPointers(wrapper); + } else { + RCTSetUpSystemLibraryPointers(wrapper); + } + return wrapper; +} + +void RCTJSCWrapperRelease(RCTJSCWrapper *wrapper) +{ + if (wrapper) { + free(wrapper); + } +} diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index f103966c31b1ec..70d247c4664710 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -94,6 +94,7 @@ 83CBBA691A601EF300E9B192 /* RCTEventDispatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA661A601EF300E9B192 /* RCTEventDispatcher.m */; }; 83CBBA981A6020BB00E9B192 /* RCTTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */; }; 83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBACB1A6023D300E9B192 /* RCTConvert.m */; }; + 85C199EE1CD2407900DAD810 /* RCTJSCWrapper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 85C199ED1CD2407900DAD810 /* RCTJSCWrapper.mm */; }; E9B20B7B1B500126007A2DA7 /* RCTAccessibilityManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E9B20B7A1B500126007A2DA7 /* RCTAccessibilityManager.m */; }; /* End PBXBuildFile section */ @@ -302,6 +303,8 @@ 83CBBACA1A6023D300E9B192 /* RCTConvert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTConvert.h; sourceTree = ""; }; 83CBBACB1A6023D300E9B192 /* RCTConvert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert.m; sourceTree = ""; }; 83F15A171B7CC46900F10295 /* UIView+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIView+Private.h"; sourceTree = ""; }; + 85C199EC1CD2407900DAD810 /* RCTJSCWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJSCWrapper.h; sourceTree = ""; }; + 85C199ED1CD2407900DAD810 /* RCTJSCWrapper.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSCWrapper.mm; sourceTree = ""; }; ACDD3FDA1BC7430D00E7DE33 /* RCTBorderStyle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBorderStyle.h; sourceTree = ""; }; E3BBC8EB1ADE6F47001BBD81 /* RCTTextDecorationLineType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTTextDecorationLineType.h; sourceTree = ""; }; E9B20B791B500126007A2DA7 /* RCTAccessibilityManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAccessibilityManager.h; sourceTree = ""; }; @@ -324,6 +327,8 @@ children = ( 134FCB391A6E7F0800051CC8 /* RCTJSCExecutor.h */, 134FCB3A1A6E7F0800051CC8 /* RCTJSCExecutor.mm */, + 85C199EC1CD2407900DAD810 /* RCTJSCWrapper.h */, + 85C199ED1CD2407900DAD810 /* RCTJSCWrapper.mm */, ); path = Executors; sourceTree = ""; @@ -723,6 +728,7 @@ 13E067591A70F44B002CDEE1 /* UIView+React.m in Sources */, 14F484561AABFCE100FDF6B9 /* RCTSliderManager.m in Sources */, 13D033631C1837FE0021DC29 /* RCTClipboard.m in Sources */, + 85C199EE1CD2407900DAD810 /* RCTJSCWrapper.mm in Sources */, 14C2CA741B3AC64300E6CBB2 /* RCTModuleData.m in Sources */, 142014191B32094000CC17BA /* RCTPerformanceLogger.m in Sources */, 83CBBA981A6020BB00E9B192 /* RCTTouchHandler.m in Sources */, From d46ac110cd540090392adf30668fdc00a7001aea Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Tue, 31 May 2016 14:23:32 -0700 Subject: [PATCH 164/843] Use a dot before patch release number for RCs Summary: Using RC release versions like `0.22.0-rc.2` instead of `0.22.0.rc2` will allow NPM to pick up patch releases to the RC. Closes https://github.com/facebook/react-native/pull/7856 Differential Revision: D3367889 fbshipit-source-id: 43aacdc8f7d13353a353b6c553e8cbcd04c34a77 --- Releases.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Releases.md b/Releases.md index 6c43477d8f1a47..4f3de43fe04790 100644 --- a/Releases.md +++ b/Releases.md @@ -28,7 +28,7 @@ Before cutting a release branch, make sure CI systems [Travis](https://travis-ci Before executing the following script, make sure you have: - An Android emulator / Genymotion device running -- No packager running in any of the projects +- No packager running in any of the projects ```bash ./scripts/test-manual-e2e.sh @@ -43,10 +43,10 @@ After `npm install` completes, the script prints a set of manual checks you have Run: ```bash -git checkout -b -stable +git checkout -b -stable # e.g. git checkout -b 0.22-stable -node ./scripts/bump-oss-version.js +node ./scripts/bump-oss-version.js # e.g. node ./scripts/bump-oss-version.js 0.22.0-rc git push origin -stable --tags @@ -68,7 +68,7 @@ https://github.com/facebook/react-native/compare/0.21-stable...0.22-stable **Note**: This only shows **250** commits, if there are more use git. -When making a list of changes, ignore docs, showcase updates and minor typos. +When making a list of changes, ignore docs, showcase updates and minor typos. Sometimes commit messages might be really short / confusing - try rewording them where it makes sense. Below are few examples: - `Fix logging reported by RUN_JS_BUNDLE` -> `Fix systrace logging of RUN_JS_BUNDLE event` @@ -90,7 +90,7 @@ A good way to do this is to create a github issue and post about it so people ca ------------------- -## How to release an RC update (e.g. 0.22.0-rc1, 0.22.0-rc2) +## How to release an RC update (e.g. 0.28.0-rc.1, 0.28.0-rc.2) After cherry-picking 1-2 bug fixes, it is a good idea to do a new RC release so that people can test again. Having a few RC releases can also help people bisect in case we cherry-pick a bad commit by mistake. @@ -111,10 +111,10 @@ git cherry-pick commitHash1 If everything worked: ```bash -node ./scripts/bump-oss-version.js -# e.g. node ./scripts/bump-oss-version.js 0.22.0-rc1 +node ./scripts/bump-oss-version.js +# e.g. node ./scripts/bump-oss-version.js 0.28.0-rc.1 -git push origin version_you_are_releasing-stable --tags +git push origin version_you_are_releasing-stable --tags # e.g. git push origin 0.22-stable --tags ```` @@ -143,13 +143,13 @@ git cherry-pick commitHash1 If everything worked: ```bash -node ./scripts/bump-oss-version.js +node ./scripts/bump-oss-version.js # e.g. node ./scripts/bump-oss-version.js 0.22.0 git tag -d latest git push origin :latest -git tag latest +git tag latest # The latest tag marks when to regenerate the website. git push origin version_you_are_releasing-stable --tags From c1ab76609679f374623405d55059c4b28443dd5c Mon Sep 17 00:00:00 2001 From: Fred Liu Date: Tue, 31 May 2016 16:23:51 -0700 Subject: [PATCH 165/843] Disable right-swipe from closed position Summary: Disables right-swipe from closed position. Reviewed By: fkgozali Differential Revision: D3368525 fbshipit-source-id: b850458bd088dfda09bbbe4db71b33c3577a2167 --- Libraries/Experimental/SwipeableRow/SwipeableRow.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Libraries/Experimental/SwipeableRow/SwipeableRow.js b/Libraries/Experimental/SwipeableRow/SwipeableRow.js index ee71fa040fcf1c..169642ef39fe6e 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableRow.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableRow.js @@ -166,7 +166,7 @@ const SwipeableRow = React.createClass({ gestureState: Object, ): boolean { // Decides whether a swipe is responded to by this component or its child - return gestureState.dy < 10 && this._isValidSwipe(gestureState); + return gestureState.dy < 10 && this._isValidSwipe(gestureState); }, _handlePanResponderGrant(event: Object, gestureState: Object): void { @@ -175,7 +175,14 @@ const SwipeableRow = React.createClass({ _handlePanResponderMove(event: Object, gestureState: Object): void { this.props.onSwipeStart(); - this.state.currentLeft.setValue(this._previousLeft + gestureState.dx); + + if (!this._isSwipingRightFromClosedPosition(gestureState)) { + this.state.currentLeft.setValue(this._previousLeft + gestureState.dx); + } + }, + + _isSwipingRightFromClosedPosition(gestureState: Object): boolean { + return this._previousLeft === CLOSED_LEFT_POSITION && gestureState.dx > 0; }, _onPanResponderTerminationRequest(event: Object, gestureState: Object): boolean { From 6321ad486b0462544d85cc47290da938b1d7d1ba Mon Sep 17 00:00:00 2001 From: Alexey Lang Date: Tue, 31 May 2016 16:28:00 -0700 Subject: [PATCH 166/843] Fix OSS tests Reviewed By: bestander Differential Revision: D3368572 fbshipit-source-id: 11326c21767bf7851d06f019c6aa2f8c16408d74 --- React/React.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 70d247c4664710..7031098d3dbaf4 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -304,7 +304,7 @@ 83CBBACB1A6023D300E9B192 /* RCTConvert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert.m; sourceTree = ""; }; 83F15A171B7CC46900F10295 /* UIView+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIView+Private.h"; sourceTree = ""; }; 85C199EC1CD2407900DAD810 /* RCTJSCWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJSCWrapper.h; sourceTree = ""; }; - 85C199ED1CD2407900DAD810 /* RCTJSCWrapper.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSCWrapper.mm; sourceTree = ""; }; + 85C199ED1CD2407900DAD810 /* RCTJSCWrapper.mm */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; fileEncoding = 4; path = RCTJSCWrapper.mm; sourceTree = ""; }; ACDD3FDA1BC7430D00E7DE33 /* RCTBorderStyle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBorderStyle.h; sourceTree = ""; }; E3BBC8EB1ADE6F47001BBD81 /* RCTTextDecorationLineType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTTextDecorationLineType.h; sourceTree = ""; }; E9B20B791B500126007A2DA7 /* RCTAccessibilityManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAccessibilityManager.h; sourceTree = ""; }; From a49558def830a22a833363ac4e4a46b2d7fc0674 Mon Sep 17 00:00:00 2001 From: Jim Remsik Date: Wed, 1 Jun 2016 00:54:40 -0700 Subject: [PATCH 167/843] =?UTF-8?q?Documentation=20typo=20=E2=80=93=20miss?= =?UTF-8?q?ing=20word?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Adds a word to enhance readability. Closes https://github.com/facebook/react-native/pull/7867 Differential Revision: D3370881 Pulled By: nicklockwood fbshipit-source-id: e3555a859dd3a44f2b9d12a22d1a2366a6a8097f --- Libraries/Linking/Linking.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Linking/Linking.js b/Libraries/Linking/Linking.js index 6bafe4ec9c0d9b..2217941095875a 100644 --- a/Libraries/Linking/Linking.js +++ b/Libraries/Linking/Linking.js @@ -42,7 +42,7 @@ const LinkingManager = Platform.OS === 'android' ? * ``` * * NOTE: For instructions on how to add support for deep linking on Android, - * refer [Enabling Deep Links for App Content - Add Intent Filters for Your Deep Links](http://developer.android.com/training/app-indexing/deep-linking.html#adding-filters). + * refer to [Enabling Deep Links for App Content - Add Intent Filters for Your Deep Links](http://developer.android.com/training/app-indexing/deep-linking.html#adding-filters). * * NOTE: On iOS you'll need to link `RCTLinking` to your project by following * the steps described [here](docs/linking-libraries-ios.html#manual-linking). From 2d32a6df27dc60b0e965b638e8e45147f9d4b892 Mon Sep 17 00:00:00 2001 From: Siqi Liu Date: Wed, 1 Jun 2016 03:29:11 -0700 Subject: [PATCH 168/843] Add Shortcut "Double R" to Reload JS in iOS Summary: Enable double tap R on iOS, consistent with Android. Keep the existing Cmd+R on iOS because people are already used to it. Make Cmd+Key and Double Key both invalid when focus is in textview or textfield. Also try to add Cmd+R in Android, but seems no good. Reviewed By: nicklockwood Differential Revision: D3343907 fbshipit-source-id: 68f7e3b0393711c137e1d932db33e1b6a2a19e09 --- React/Base/RCTBridge.m | 9 ++ React/Base/RCTKeyCommands.h | 25 +++++- React/Base/RCTKeyCommands.m | 172 +++++++++++++++++++++++++++++++++++- 3 files changed, 200 insertions(+), 6 deletions(-) diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index dceaf4409c24c0..2ca2d7c06619d4 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -172,6 +172,15 @@ - (void)bindKeys userInfo:nil]; }]; + + [commands registerDoublePressKeyCommandWithInput:@"r" + modifierFlags:0 + action:^(__unused UIKeyCommand *command) { + [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification + object:nil + userInfo:nil]; + }]; + #endif } diff --git a/React/Base/RCTKeyCommands.h b/React/Base/RCTKeyCommands.h index 0a27ce493889fa..d93485749e2ea8 100644 --- a/React/Base/RCTKeyCommands.h +++ b/React/Base/RCTKeyCommands.h @@ -14,22 +14,41 @@ + (instancetype)sharedInstance; /** - * Register a keyboard command. + * Register a single-press keyboard command. */ - (void)registerKeyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)flags action:(void (^)(UIKeyCommand *command))block; /** - * Unregister a keyboard command. + * Unregister a single-press keyboard command. */ - (void)unregisterKeyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)flags; /** - * Check if a command is registered. + * Check if a single-press command is registered. */ - (BOOL)isKeyCommandRegisteredForInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)flags; +/** + * Register a double-press keyboard command. + */ +- (void)registerDoublePressKeyCommandWithInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags + action:(void (^)(UIKeyCommand *command))block; + +/** + * Unregister a double-press keyboard command. + */ +- (void)unregisterDoublePressKeyCommandWithInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags; + +/** + * Check if a double-press command is registered. + */ +- (BOOL)isDoublePressKeyCommandRegisteredForInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags; + @end diff --git a/React/Base/RCTKeyCommands.m b/React/Base/RCTKeyCommands.m index 92747519f2a9d0..77ba605457190e 100644 --- a/React/Base/RCTKeyCommands.m +++ b/React/Base/RCTKeyCommands.m @@ -83,17 +83,60 @@ @interface RCTKeyCommands () @implementation UIResponder (RCTKeyCommands) ++ (UIResponder *)RCT_getFirstResponder:(UIResponder *)view +{ + UIResponder *firstResponder = nil; + + if (view.isFirstResponder) { + return view; + } else if ([view isKindOfClass:[UIViewController class]]) { + if ([(UIViewController *)view parentViewController]) { + firstResponder = [UIResponder RCT_getFirstResponder: [(UIViewController *)view parentViewController]]; + } + return firstResponder ? firstResponder : [UIResponder RCT_getFirstResponder: [(UIViewController *)view view]]; + } else if ([view isKindOfClass:[UIView class]]) { + for (UIView *subview in [(UIView *)view subviews]) { + firstResponder = [UIResponder RCT_getFirstResponder: subview]; + if (firstResponder) { + return firstResponder; + } + } + } + + return firstResponder; +} + - (NSArray *)RCT_keyCommands { + /** + * If current first responder is UITextView or UITextField, disable the shortcut key commands. + * For example, double-pressing a key should type two characters into the text view, + * not invoke a predefined keyboard shortcut. + */ + NSString *name = NSStringFromClass(self.class); + if ([name isEqualToString : @"AppDelegate"]) { + return nil; + } + + UIResponder *firstResponder = [UIResponder RCT_getFirstResponder: self]; + if ([firstResponder isKindOfClass:[UITextView class]] || + [firstResponder isKindOfClass:[UITextField class]] || + [firstResponder.class conformsToProtocol:@protocol(UITextViewDelegate)]) { + return nil; + } + NSSet *commands = [RCTKeyCommands sharedInstance].commands; return [[commands valueForKeyPath:@"keyCommand"] allObjects]; } +/** + * Single Press Key Command Response + * Command + KeyEvent (Command + R/D, etc.) + */ - (void)RCT_handleKeyCommand:(UIKeyCommand *)key { // NOTE: throttle the key handler because on iOS 9 the handleKeyCommand: // method gets called repeatedly if the command key is held down. - static NSTimeInterval lastCommand = 0; if (RCTIsIOS8OrEarlier() || CACurrentMediaTime() - lastCommand > 0.5) { for (RCTKeyCommand *command in [RCTKeyCommands sharedInstance].commands) { @@ -108,6 +151,61 @@ - (void)RCT_handleKeyCommand:(UIKeyCommand *)key } } +/** + * Double Press Key Command Response + * Double KeyEvent (Double R, etc.) + */ +- (void)RCT_handleDoublePressKeyCommand:(UIKeyCommand *)key +{ + static BOOL firstPress = YES; + static NSTimeInterval lastCommand = 0; + static NSTimeInterval lastDoubleCommand = 0; + static NSString *lastInput = nil; + static UIKeyModifierFlags lastModifierFlags = nil; + + if (firstPress) { + for (RCTKeyCommand *command in [RCTKeyCommands sharedInstance].commands) { + if ([command.keyCommand.input isEqualToString:key.input] && + command.keyCommand.modifierFlags == key.modifierFlags && + command.block) { + + firstPress = NO; + lastCommand = CACurrentMediaTime(); + lastInput = key.input; + lastModifierFlags = key.modifierFlags; + return; + } + } + } else { + // Second keyevent within 0.2 second, + // with the same key as the first one. + if (CACurrentMediaTime() - lastCommand < 0.2 && + lastInput == key.input && + lastModifierFlags == key.modifierFlags) { + + for (RCTKeyCommand *command in [RCTKeyCommands sharedInstance].commands) { + if ([command.keyCommand.input isEqualToString:key.input] && + command.keyCommand.modifierFlags == key.modifierFlags && + command.block) { + + // NOTE: throttle the key handler because on iOS 9 the handleKeyCommand: + // method gets called repeatedly if the command key is held down. + if (RCTIsIOS8OrEarlier() || CACurrentMediaTime() - lastDoubleCommand > 0.5) { + command.block(key); + lastDoubleCommand = CACurrentMediaTime(); + } + firstPress = YES; + return; + } + } + } + + lastCommand = CACurrentMediaTime(); + lastInput = key.input; + lastModifierFlags = key.modifierFlags; + } +} + @end @implementation UIApplication (RCTKeyCommands) @@ -118,6 +216,9 @@ - (BOOL)RCT_sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEve if (action == @selector(RCT_handleKeyCommand:)) { [self RCT_handleKeyCommand:sender]; return YES; + } else if (action == @selector(RCT_handleDoublePressKeyCommand:)) { + [self RCT_handleDoublePressKeyCommand:sender]; + return YES; } return [self RCT_sendAction:action to:target from:sender forEvent:event]; } @@ -130,7 +231,7 @@ + (void)initialize { if (RCTIsIOS8OrEarlier()) { - //swizzle UIApplication + // swizzle UIApplication RCTSwapInstanceMethods([UIApplication class], @selector(keyCommands), @selector(RCT_keyCommands)); @@ -140,7 +241,7 @@ + (void)initialize @selector(RCT_sendAction:to:from:forEvent:)); } else { - //swizzle UIResponder + // swizzle UIResponder RCTSwapInstanceMethods([UIResponder class], @selector(keyCommands), @selector(RCT_keyCommands)); @@ -218,6 +319,58 @@ - (BOOL)isKeyCommandRegisteredForInput:(NSString *)input return NO; } +- (void)registerDoublePressKeyCommandWithInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags + action:(void (^)(UIKeyCommand *))block +{ + RCTAssertMainThread(); + + if (input.length && flags && RCTIsIOS8OrEarlier()) { + + // Workaround around the first cmd not working: http://openradar.appspot.com/19613391 + // You can register just the cmd key and do nothing. This ensures that + // command-key modified commands will work first time. Fixed in iOS 9. + + [self registerDoublePressKeyCommandWithInput:@"" + modifierFlags:flags + action:nil]; + } + + UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input + modifierFlags:flags + action:@selector(RCT_handleDoublePressKeyCommand:)]; + + RCTKeyCommand *keyCommand = [[RCTKeyCommand alloc] initWithKeyCommand:command block:block]; + [_commands removeObject:keyCommand]; + [_commands addObject:keyCommand]; +} + +- (void)unregisterDoublePressKeyCommandWithInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags +{ + RCTAssertMainThread(); + + for (RCTKeyCommand *command in _commands.allObjects) { + if ([command matchesInput:input flags:flags]) { + [_commands removeObject:command]; + break; + } + } +} + +- (BOOL)isDoublePressKeyCommandRegisteredForInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags +{ + RCTAssertMainThread(); + + for (RCTKeyCommand *command in _commands) { + if ([command matchesInput:input flags:flags]) { + return YES; + } + } + return NO; +} + @end #else @@ -242,6 +395,19 @@ - (BOOL)isKeyCommandRegisteredForInput:(NSString *)input return NO; } +- (void)registerDoublePressKeyCommandWithInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags + action:(void (^)(UIKeyCommand *))block {} + +- (void)unregisterDoublePressKeyCommandWithInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags {} + +- (BOOL)isDoublePressKeyCommandRegisteredForInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags +{ + return NO; +} + @end #endif From 4391ef218b5145fb04103e260f862f968f653063 Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Wed, 1 Jun 2016 04:01:45 -0700 Subject: [PATCH 169/843] Tweak dev menu label: Stop Remote JS Debugging Summary: "Stop" is a bit clearer - the debugger is started and stopped, rather than enabled / disabled. Small difference :) In contrast, live reloading and hot reloading are enabled and disabled. satya164 pointed it out in [this comment](https://github.com/facebook/react-native/commit/31eea8eee3bd904065b06c639321dbc2cea1ca55#commitcomment-17682563) and I agree. **Test Plan** Ran the UIExplorer: screen shot 2016-05-31 at 11 11 28 pm On Android the change is kind of obvious :) Closes https://github.com/facebook/react-native/pull/7861 Differential Revision: D3371207 Pulled By: mkonicek fbshipit-source-id: 769288f687a98d62cf5c1a22cbc857b7dc4acf00 --- React/Modules/RCTDevMenu.m | 2 +- ReactAndroid/src/main/res/devsupport/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m index f574f7b06c0a38..d7cbc5dc5724f1 100644 --- a/React/Modules/RCTDevMenu.m +++ b/React/Modules/RCTDevMenu.m @@ -466,7 +466,7 @@ - (void)addItem:(RCTDevMenuItem *)item } else { BOOL isDebuggingJS = _executorClass && _executorClass == jsDebuggingExecutorClass; NSString *debuggingDescription = [_defaults objectForKey:@"websocket-executor-name"] ?: @"Remote JS"; - NSString *debugTitleJS = isDebuggingJS ? [NSString stringWithFormat:@"Disable %@ Debugging", debuggingDescription] : [NSString stringWithFormat:@"Debug %@", _webSocketExecutorName]; + NSString *debugTitleJS = isDebuggingJS ? [NSString stringWithFormat:@"Stop %@ Debugging", debuggingDescription] : [NSString stringWithFormat:@"Debug %@", _webSocketExecutorName]; [items addObject:[RCTDevMenuItem buttonItemWithTitle:debugTitleJS handler:^{ weakSelf.executorClass = isDebuggingJS ? Nil : jsDebuggingExecutorClass; }]]; diff --git a/ReactAndroid/src/main/res/devsupport/values/strings.xml b/ReactAndroid/src/main/res/devsupport/values/strings.xml index dda73a8c506106..edf2669c1c1186 100644 --- a/ReactAndroid/src/main/res/devsupport/values/strings.xml +++ b/ReactAndroid/src/main/res/devsupport/values/strings.xml @@ -2,7 +2,7 @@ Reload Debug JS Remotely - Disable Remote JS Debugging + Stop Remote JS Debugging Enable Hot Reloading Disable Hot Reloading Enable Live Reload From 6603cef95cd30fcc05330707d4ce10a2c4e22046 Mon Sep 17 00:00:00 2001 From: Emil Sjolander Date: Wed, 1 Jun 2016 04:18:36 -0700 Subject: [PATCH 170/843] Fix ScrollView to work correctly for new css-layout Reviewed By: sahrens Differential Revision: D3367140 fbshipit-source-id: ea470f289c92ebca71543a9b9328a7a5ed6d572b --- Libraries/Components/ScrollView/ScrollView.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index ad5e9edbf96d61..52e20aff632daf 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -468,11 +468,13 @@ const ScrollView = React.createClass({ this.props.alwaysBounceVertical : !this.props.horizontal; + const baseStyle = this.props.horizontal ? styles.baseHorizontal : styles.baseVertical; + const props = { ...this.props, alwaysBounceHorizontal, alwaysBounceVertical, - style: ([styles.base, this.props.style]: ?Array), + style: ([baseStyle, this.props.style]: ?Array), onTouchStart: this.scrollResponderHandleTouchStart, onTouchMove: this.scrollResponderHandleTouchMove, onTouchEnd: this.scrollResponderHandleTouchEnd, @@ -529,7 +531,7 @@ const ScrollView = React.createClass({ return React.cloneElement( refreshControl, {style: props.style}, - + {contentContainer} ); @@ -544,11 +546,14 @@ const ScrollView = React.createClass({ }); const styles = StyleSheet.create({ - base: { + baseVertical: { + flex: 1, + }, + baseHorizontal: { flex: 1, + flexDirection: 'row', }, contentContainerHorizontal: { - alignSelf: 'flex-start', flexDirection: 'row', }, }); From 0e82eb3bed05c06677efb03e55361d04fbe7451f Mon Sep 17 00:00:00 2001 From: Alessandro Nadalin Date: Wed, 1 Jun 2016 05:43:13 -0700 Subject: [PATCH 171/843] Update Upgrading.md Summary: Just updating the docs as .18 is quite old :) Closes https://github.com/facebook/react-native/pull/7824 Differential Revision: D3371417 Pulled By: javache fbshipit-source-id: 91b8249f814eeef68cbeff4b481624644dc7be73 --- docs/Upgrading.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Upgrading.md b/docs/Upgrading.md index 4034b4f731cfbf..52865b00117dcd 100644 --- a/docs/Upgrading.md +++ b/docs/Upgrading.md @@ -19,10 +19,10 @@ Note the latest version of the `react-native` npm package from here (or use `npm * https://www.npmjs.com/package/react-native -Now install that version of `react-native` in your project with `npm install --save`. For example, to upgrade to the version `0.18`, in a terminal run: +Now install that version of `react-native` in your project with `npm install --save`. For example, to upgrade to the version `0.26`, in a terminal run: ```sh -$ npm install --save react-native@0.18 +$ npm install --save react-native@0.26 ``` ## 2. Upgrade your project templates From aa53770c253f8f222113ad4a8ce55dbd148dd484 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Wed, 1 Jun 2016 06:16:51 -0700 Subject: [PATCH 172/843] removed unused code: _numPrependedModules Summary: cc davidaurelio Closes https://github.com/facebook/react-native/pull/7873 Differential Revision: D3371406 Pulled By: javache fbshipit-source-id: 72ed3838a88022ae5c0832dcca5abda75f18dbe1 --- packager/react-packager/src/Bundler/Bundle.js | 7 ------- packager/react-packager/src/Bundler/index.js | 5 ----- 2 files changed, 12 deletions(-) diff --git a/packager/react-packager/src/Bundler/Bundle.js b/packager/react-packager/src/Bundler/Bundle.js index c3cdc8041a121f..ccaaffa88e663e 100644 --- a/packager/react-packager/src/Bundler/Bundle.js +++ b/packager/react-packager/src/Bundler/Bundle.js @@ -22,7 +22,6 @@ class Bundle extends BundleBase { this._sourceMap = false; this._sourceMapUrl = sourceMapUrl; this._shouldCombineSourceMaps = false; - this._numPrependedModules = 0; this._numRequireCalls = 0; this._dev = dev; this._minify = minify; @@ -53,10 +52,6 @@ class Bundle extends BundleBase { }); } - setNumPrependedModules(n) { - this._numPrependedModules = n; - } - finalize(options) { options = options || {}; if (options.runMainModule) { @@ -277,7 +272,6 @@ class Bundle extends BundleBase { return { ...super.toJSON(), sourceMapUrl: this._sourceMapUrl, - numPrependedModules: this._numPrependedModules, numRequireCalls: this._numRequireCalls, shouldCombineSourceMaps: this._shouldCombineSourceMaps, }; @@ -287,7 +281,6 @@ class Bundle extends BundleBase { const bundle = new Bundle({sourceMapUrl: json.sourceMapUrl}); bundle._sourceMapUrl = json.sourceMapUrl; - bundle._numPrependedModules = json.numPrependedModules; bundle._numRequireCalls = json.numRequireCalls; bundle._shouldCombineSourceMaps = json.shouldCombineSourceMaps; diff --git a/packager/react-packager/src/Bundler/index.js b/packager/react-packager/src/Bundler/index.js index 029846b197f683..8b411be9fddf57 100644 --- a/packager/react-packager/src/Bundler/index.js +++ b/packager/react-packager/src/Bundler/index.js @@ -250,11 +250,6 @@ class Bundler { }) { const onResolutionResponse = response => { bundle.setMainModuleId(response.getModuleId(getMainModule(response))); - if (bundle.setNumPrependedModules) { - bundle.setNumPrependedModules( - response.numPrependedDependencies + moduleSystemDeps.length - ); - } if (entryModuleOnly) { response.dependencies = response.dependencies.filter(module => module.path.endsWith(entryFile) From bdcdfb03d41f82e2c9cd94147fd5fafe89e1a339 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Wed, 1 Jun 2016 06:18:17 -0700 Subject: [PATCH 173/843] Fixed "Sending `didSendNetworkData` with no listeners registered" warning Summary: XMLHttpRequest was sending the request before registering any listeners, resulting in a warning from the native event emitter. Since we weren't seeing widespread problems with XHR missing data, this was probably working OK in practice because the queuing of events meant that the listener would have been registered before the message was actually delivered. Still, this was working more through luck than design. This diff fixes it by registering the listeners *before* sending the request. Reviewed By: lexs Differential Revision: D3371320 fbshipit-source-id: c688d4053a61f856eaacccd0106905edbefcc86a --- Libraries/Network/XMLHttpRequest.js | 32 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Libraries/Network/XMLHttpRequest.js b/Libraries/Network/XMLHttpRequest.js index 24c9445124fd15..cdd4073126ce61 100644 --- a/Libraries/Network/XMLHttpRequest.js +++ b/Libraries/Network/XMLHttpRequest.js @@ -212,22 +212,6 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { // exposed for testing __didCreateRequest(requestId: number): void { this._requestId = requestId; - this._subscriptions.push(RCTNetworking.addListener( - 'didSendNetworkData', - (args) => this.__didUploadProgress(...args) - )); - this._subscriptions.push(RCTNetworking.addListener( - 'didReceiveNetworkResponse', - (args) => this._didReceiveResponse(...args) - )); - this._subscriptions.push(RCTNetworking.addListener( - 'didReceiveNetworkData', - (args) => this._didReceiveData(...args) - )); - this._subscriptions.push(RCTNetworking.addListener( - 'didCompleteNetworkResponse', - (args) => this.__didCompleteResponse(...args) - )); } // exposed for testing @@ -340,6 +324,22 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { useIncrementalUpdates: boolean, timeout: number, ): void { + this._subscriptions.push(RCTNetworking.addListener( + 'didSendNetworkData', + (args) => this.__didUploadProgress(...args) + )); + this._subscriptions.push(RCTNetworking.addListener( + 'didReceiveNetworkResponse', + (args) => this._didReceiveResponse(...args) + )); + this._subscriptions.push(RCTNetworking.addListener( + 'didReceiveNetworkData', + (args) => this._didReceiveData(...args) + )); + this._subscriptions.push(RCTNetworking.addListener( + 'didCompleteNetworkResponse', + (args) => this.__didCompleteResponse(...args) + )); RCTNetworking.sendRequest( method, url, From 002024cc456246761421f6e5c354466baa819843 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Wed, 1 Jun 2016 06:48:29 -0700 Subject: [PATCH 174/843] Removed hard-coded DatePicker width Summary: The DatePicker had a hard-coded width of 320 for no reason I can think of. Removing it allows the DatePicker to naturally scale to fit the width of the container, which is how the regular Picker works already. Reviewed By: lexs Differential Revision: D3371355 fbshipit-source-id: e06d31f7275de41bb00226232cf47ad022d25b4d --- .../DatePicker/DatePickerIOS.ios.js | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/Libraries/Components/DatePicker/DatePickerIOS.ios.js b/Libraries/Components/DatePicker/DatePickerIOS.ios.js index fe7c05e70e6e8f..60382cc4a74c35 100644 --- a/Libraries/Components/DatePicker/DatePickerIOS.ios.js +++ b/Libraries/Components/DatePicker/DatePickerIOS.ios.js @@ -13,13 +13,13 @@ */ 'use strict'; -var NativeMethodsMixin = require('NativeMethodsMixin'); -var PropTypes = require('ReactPropTypes'); -var React = require('React'); -var StyleSheet = require('StyleSheet'); -var View = require('View'); +const NativeMethodsMixin = require('NativeMethodsMixin'); +const PropTypes = require('ReactPropTypes'); +const React = require('React'); +const StyleSheet = require('StyleSheet'); +const View = require('View'); -var requireNativeComponent = require('requireNativeComponent'); +const requireNativeComponent = require('requireNativeComponent'); type DefaultProps = { mode: 'date' | 'time' | 'datetime'; @@ -34,7 +34,7 @@ type Event = Object; * the user's change will be reverted immediately to reflect `props.date` as the * source of truth. */ -var DatePickerIOS = React.createClass({ +const DatePickerIOS = React.createClass({ // TOOD: Put a better type for _picker _picker: (undefined: ?$FlowFixMe), @@ -97,7 +97,7 @@ var DatePickerIOS = React.createClass({ }, _onChange: function(event: Event) { - var nativeTimeStamp = event.nativeEvent.timestamp; + const nativeTimeStamp = event.nativeEvent.timestamp; this.props.onDateChange && this.props.onDateChange( new Date(nativeTimeStamp) ); @@ -107,7 +107,7 @@ var DatePickerIOS = React.createClass({ // prop. That way they can also disallow/undo/mutate the selection of // certain values. In other words, the embedder of this component should // be the source of truth, not the native component. - var propsTimeStamp = this.props.date.getTime(); + const propsTimeStamp = this.props.date.getTime(); if (this._picker && nativeTimeStamp !== propsTimeStamp) { this._picker.setNativeProps({ date: propsTimeStamp, @@ -116,11 +116,11 @@ var DatePickerIOS = React.createClass({ }, render: function() { - var props = this.props; + const props = this.props; return ( this._picker = picker } + ref={ picker => { this._picker = picker; } } style={styles.datePickerIOS} date={props.date.getTime()} maximumDate={ @@ -139,10 +139,9 @@ var DatePickerIOS = React.createClass({ } }); -var styles = StyleSheet.create({ +const styles = StyleSheet.create({ datePickerIOS: { height: 216, - width: 320, }, }); From e87bea464b88ca11f9f02c31c5ed8df6f44abcb3 Mon Sep 17 00:00:00 2001 From: Emilio Rodriguez Date: Wed, 1 Jun 2016 06:57:43 -0700 Subject: [PATCH 175/843] Replaced old password prop with secureTextEntry Summary: Closes https://github.com/facebook/react-native/pull/7783 Differential Revision: D3371521 Pulled By: javache fbshipit-source-id: f442b549e6fc8c2c8e5ec12f32185bfabafbdd81 --- Examples/UIExplorer/TextInputExample.ios.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/UIExplorer/TextInputExample.ios.js b/Examples/UIExplorer/TextInputExample.ios.js index 1f2e83d2e6a815..a3bae1d4629641 100644 --- a/Examples/UIExplorer/TextInputExample.ios.js +++ b/Examples/UIExplorer/TextInputExample.ios.js @@ -524,7 +524,7 @@ exports.examples = [ return ( - + ); From 88190f7e9d873649f0cb8ef322c5802b155a47a0 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Wed, 1 Jun 2016 07:58:25 -0700 Subject: [PATCH 176/843] A few instrumentation tests were fixed and are supposed to be more stable Reviewed By: avaly Differential Revision: D3354433 fbshipit-source-id: 4e3f00fae94e5cb910e188b0e3fbe204361a4d01 --- .../src/androidTest/java/com/facebook/react/tests/BUCK | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK index 33759db559d37f..c14e4d035dd4cf 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK @@ -5,13 +5,12 @@ include_defs('//ReactAndroid/DEFS') SANDCASTLE_FLAKY = [ 'ReactHorizontalScrollViewTestCase.java', 'ReactScrollViewTestCase.java', - 'ReactSwipeRefreshLayoutTestCase.java', - 'TestIdTestCase.java', ] deps = [ + react_native_dep('third-party/android/support/v4:lib-support-v4'), + react_native_dep('third-party/java/junit:junit'), react_native_integration_tests_target('java/com/facebook/react/testing:testing'), - react_native_target('java/com/facebook/react:react'), react_native_target('java/com/facebook/react/bridge:bridge'), react_native_target('java/com/facebook/react/common:common'), react_native_target('java/com/facebook/react/modules/core:core'), @@ -19,8 +18,8 @@ deps = [ react_native_target('java/com/facebook/react/modules/systeminfo:systeminfo'), react_native_target('java/com/facebook/react/modules/timepicker:timepicker'), react_native_target('java/com/facebook/react/touch:touch'), - react_native_target('java/com/facebook/react/uimanager:uimanager'), react_native_target('java/com/facebook/react/uimanager/annotations:annotations'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), react_native_target('java/com/facebook/react/views/picker:picker'), react_native_target('java/com/facebook/react/views/progressbar:progressbar'), react_native_target('java/com/facebook/react/views/recyclerview:recyclerview'), @@ -30,8 +29,7 @@ deps = [ react_native_target('java/com/facebook/react/views/text:text'), react_native_target('java/com/facebook/react/views/textinput:textinput'), react_native_target('java/com/facebook/react/views/view:view'), - react_native_dep('third-party/android/support/v4:lib-support-v4'), - react_native_dep('third-party/java/junit:junit'), + react_native_target('java/com/facebook/react:react'), ] android_library( From 572a85fa24f2d2b4d5e2b05cdbfc88f3a4bbc2e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Wed, 1 Jun 2016 08:02:56 -0700 Subject: [PATCH 177/843] Fix multiline TextField initial render Summary: The TextInput when configured with `multiline=true` has a minor issue due to which if the initial text doesn't fit in one line, the input won't span multiple lines. The root cause of the problem is that the `onChange` event is not fired for the initial render. This issue has been reported on open-source: https://github.com/facebook/react-native/commit/481f560f64806ba3324cf722d6bf8c3f36ac74a5 This is an attempt to fix this problem by moving the logic to fire the event to the method that updates the content size. This way we can guarantee that anytime the content size changes we'll trigger the JS event. The downside of this approach is that it's possible that multiple events get fired for a single character change. As per the comment that was removed, this was already happening when adding a character that when rendered, would increase the content size. By moving the code to the new place, this can happen more often (twice per character tapped). Let me know if you think this is an issue. I don't know this code much, so please be careful reviewing. I'm happy to add more test cases I may have missed to the test plan :). Reviewed By: nicklockwood Differential Revision: D3348218 fbshipit-source-id: d3da3c0da1a0da9b9960625441191497e91d322e --- Libraries/Text/RCTTextView.m | 41 +++++++++++++++--------------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index d32463333bfa7b..a04bad4579a302 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -251,6 +251,23 @@ - (void)updateContentSize size.height = [_textView sizeThatFits:size].height; _scrollView.contentSize = size; _textView.frame = (CGRect){CGPointZero, size}; + + NSUInteger textLength = _textView.text.length; + CGFloat contentHeight = _textView.contentSize.height; + if (textLength >= _previousTextLength) { + contentHeight = MAX(contentHeight, _previousContentHeight); + } + _previousTextLength = textLength; + _previousContentHeight = contentHeight; + _onChange(@{ + @"text": self.text, + @"contentSize": @{ + @"height": @(contentHeight), + @"width": @(_textView.contentSize.width) + }, + @"target": self.reactTag, + @"eventCount": @(_nativeEventCount), + }); } - (void)updatePlaceholder @@ -475,30 +492,6 @@ - (void)textViewDidChange:(UITextView *)textView if (!self.reactTag || !_onChange) { return; } - - // When the context size increases, iOS updates the contentSize twice; once - // with a lower height, then again with the correct height. To prevent a - // spurious event from being sent, we track the previous, and only send the - // update event if it matches our expectation that greater text length - // should result in increased height. This assumption is, of course, not - // necessarily true because shorter text might include more linebreaks, but - // in practice this works well enough. - NSUInteger textLength = textView.text.length; - CGFloat contentHeight = textView.contentSize.height; - if (textLength >= _previousTextLength) { - contentHeight = MAX(contentHeight, _previousContentHeight); - } - _previousTextLength = textLength; - _previousContentHeight = contentHeight; - _onChange(@{ - @"text": self.text, - @"contentSize": @{ - @"height": @(contentHeight), - @"width": @(textView.contentSize.width) - }, - @"target": self.reactTag, - @"eventCount": @(_nativeEventCount), - }); } - (void)textViewDidEndEditing:(UITextView *)textView From 0cc0aaecbb39db0e1d48081109d80dede6869744 Mon Sep 17 00:00:00 2001 From: Siqi Liu Date: Wed, 1 Jun 2016 08:39:58 -0700 Subject: [PATCH 178/843] Followup for Add Shortcut "Double R" to Reload JS in iOS Summary: Change nit codes in comments. Reviewed By: mkonicek Differential Revision: D3371536 fbshipit-source-id: 4133af88568f15410ff90c945695f82ffd94eb95 --- React/Base/RCTKeyCommands.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/React/Base/RCTKeyCommands.m b/React/Base/RCTKeyCommands.m index 77ba605457190e..be59c984b54ef7 100644 --- a/React/Base/RCTKeyCommands.m +++ b/React/Base/RCTKeyCommands.m @@ -108,7 +108,7 @@ + (UIResponder *)RCT_getFirstResponder:(UIResponder *)view - (NSArray *)RCT_keyCommands { - /** + /* * If current first responder is UITextView or UITextField, disable the shortcut key commands. * For example, double-pressing a key should type two characters into the text view, * not invoke a predefined keyboard shortcut. @@ -121,7 +121,7 @@ + (UIResponder *)RCT_getFirstResponder:(UIResponder *)view UIResponder *firstResponder = [UIResponder RCT_getFirstResponder: self]; if ([firstResponder isKindOfClass:[UITextView class]] || [firstResponder isKindOfClass:[UITextField class]] || - [firstResponder.class conformsToProtocol:@protocol(UITextViewDelegate)]) { + [firstResponder conformsToProtocol:@protocol(UITextViewDelegate)]) { return nil; } From cec913e7ce05d26181ab4d46e2e41d72acdfb87d Mon Sep 17 00:00:00 2001 From: Jakob Kerkhove Date: Wed, 1 Jun 2016 08:44:03 -0700 Subject: [PATCH 179/843] Native propTypes RCTDatePickerIOS !== propTypes DatePickerIOS Summary: The propTypes of RCTDatePickerIOS do not fit with the propTypes of DatePickerIOS. All dates (date, minimumDate, maximumDate) are a timestamp (check line 126), so they should have propType number. OnDateChange function should not be required since it is called onChange in the iOS implementation. The problem currently causes warnings that the given types are wrong, while they were added correctly. Closes https://github.com/facebook/react-native/pull/7833 Differential Revision: D3371324 Pulled By: nicklockwood fbshipit-source-id: bca5b2bbe7e9dd2e045288bfbd268578848c7bff --- Libraries/Components/DatePicker/DatePickerIOS.ios.js | 11 +++++++++-- Libraries/ReactIOS/verifyPropTypes.js | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Libraries/Components/DatePicker/DatePickerIOS.ios.js b/Libraries/Components/DatePicker/DatePickerIOS.ios.js index 60382cc4a74c35..00ff474590d324 100644 --- a/Libraries/Components/DatePicker/DatePickerIOS.ios.js +++ b/Libraries/Components/DatePicker/DatePickerIOS.ios.js @@ -145,8 +145,15 @@ const styles = StyleSheet.create({ }, }); -var RCTDatePickerIOS = requireNativeComponent('RCTDatePicker', DatePickerIOS, { - nativeOnly: { onChange: true }, +const RCTDatePickerIOS = requireNativeComponent('RCTDatePicker', { + propTypes: { + ...DatePickerIOS.propTypes, + date: PropTypes.number, + minimumDate: PropTypes.number, + maximumDate: PropTypes.number, + onDateChange: () => null, + onChange: PropTypes.func, + } }); module.exports = DatePickerIOS; diff --git a/Libraries/ReactIOS/verifyPropTypes.js b/Libraries/ReactIOS/verifyPropTypes.js index d284d60ec0aec4..b93f4afc659073 100644 --- a/Libraries/ReactIOS/verifyPropTypes.js +++ b/Libraries/ReactIOS/verifyPropTypes.js @@ -49,7 +49,7 @@ function verifyPropTypes( message = '`' + componentName + '` has no propType for native prop `' + viewConfig.uiViewClassName + '.' + prop + '` of native type `' + nativeProps[prop] + '`'; - }; + } message += '\nIf you haven\'t changed this prop yourself, this usually means that ' + 'your versions of the native code and JavaScript code are out of sync. Updating both ' + 'should make this error go away.'; From ee8496f364070f1b3629b94b347a0e11b2895dec Mon Sep 17 00:00:00 2001 From: Adam Comella Date: Wed, 1 Jun 2016 10:32:20 -0700 Subject: [PATCH 180/843] iOS: Support HTTP headers for source prop on components Summary: Allows developers to specify headers to include in the HTTP request when fetching a remote image. For example, one might leverage this when fetching an image from an endpoint that requires authentication: ``` ``` Note that the header values must be strings. Works on iOS and Android. **Test plan (required)** - Ran a small example like the one above on iOS and Android and ensured the headers were sent to the server. - Ran a small example to ensure that \ components without headers still work. - Currently using this code in our app. Adam Comella Microsoft Corp. Closes https://github.com/facebook/react-native/pull/7338 Reviewed By: javache Differential Revision: D3371458 Pulled By: nicklockwood fbshipit-source-id: cdb24fe2572c3ae3ba82c86ad383af6d85157e20 --- .../UIExplorerUnitTests/RCTImageLoaderTests.m | 10 +- Libraries/CameraRoll/RCTCameraRollManager.m | 5 +- Libraries/Image/Image.ios.js | 37 ++-- Libraries/Image/ImageSourcePropType.js | 56 +++++ Libraries/Image/RCTImageEditingManager.m | 6 +- Libraries/Image/RCTImageLoader.h | 77 ++++--- Libraries/Image/RCTImageLoader.m | 197 +++++++++++------- Libraries/Image/RCTImageView.m | 17 +- Libraries/Image/RCTImageViewManager.m | 2 +- React/Base/RCTConvert.m | 21 +- React/Base/RCTImageSource.h | 15 +- React/Base/RCTImageSource.m | 30 ++- 12 files changed, 331 insertions(+), 142 deletions(-) create mode 100644 Libraries/Image/ImageSourcePropType.js diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderTests.m index e67edb0b40481a..8de60a1992cc0c 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderTests.m @@ -49,7 +49,8 @@ - (void)testImageLoading NS_VALID_UNTIL_END_OF_SCOPE RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[loader]; } launchOptions:nil]; - [bridge.imageLoader loadImageWithTag:@"http://facebook.github.io/react/img/logo_og.png" size:CGSizeMake(100, 100) scale:1.0 resizeMode:RCTResizeModeContain progressBlock:^(int64_t progress, int64_t total) { + NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://facebook.github.io/react/img/logo_og.png"]]; + [bridge.imageLoader loadImageWithURLRequest:urlRequest size:CGSizeMake(100, 100) scale:1.0 clipped:YES resizeMode:RCTResizeModeContain progressBlock:^(int64_t progress, int64_t total) { XCTAssertEqual(progress, 1); XCTAssertEqual(total, 1); } completionBlock:^(NSError *loadError, id loadedImage) { @@ -79,7 +80,8 @@ - (void)testImageLoaderUsesImageURLLoaderWithHighestPriority NS_VALID_UNTIL_END_OF_SCOPE RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[loader1, loader2]; } launchOptions:nil]; - [bridge.imageLoader loadImageWithTag:@"http://facebook.github.io/react/img/logo_og.png" size:CGSizeMake(100, 100) scale:1.0 resizeMode:RCTResizeModeContain progressBlock:^(int64_t progress, int64_t total) { + NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://facebook.github.io/react/img/logo_og.png"]]; + [bridge.imageLoader loadImageWithURLRequest:urlRequest size:CGSizeMake(100, 100) scale:1.0 clipped:YES resizeMode:RCTResizeModeContain progressBlock:^(int64_t progress, int64_t total) { XCTAssertEqual(progress, 1); XCTAssertEqual(total, 1); } completionBlock:^(NSError *loadError, id loadedImage) { @@ -103,7 +105,7 @@ - (void)testImageDecoding NS_VALID_UNTIL_END_OF_SCOPE RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[decoder]; } launchOptions:nil]; - RCTImageLoaderCancellationBlock cancelBlock = [bridge.imageLoader decodeImageDataWithoutClipping:data size:CGSizeMake(1, 1) scale:1.0 resizeMode:RCTResizeModeStretch completionBlock:^(NSError *decodeError, id decodedImage) { + RCTImageLoaderCancellationBlock cancelBlock = [bridge.imageLoader decodeImageData:data size:CGSizeMake(1, 1) scale:1.0 clipped:NO resizeMode:RCTResizeModeStretch completionBlock:^(NSError *decodeError, id decodedImage) { XCTAssertEqualObjects(decodedImage, image); XCTAssertNil(decodeError); }]; @@ -132,7 +134,7 @@ - (void)testImageLoaderUsesImageDecoderWithHighestPriority NS_VALID_UNTIL_END_OF_SCOPE RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[decoder1, decoder2]; } launchOptions:nil]; - RCTImageLoaderCancellationBlock cancelBlock = [bridge.imageLoader decodeImageDataWithoutClipping:data size:CGSizeMake(1, 1) scale:1.0 resizeMode:RCTResizeModeStretch completionBlock:^(NSError *decodeError, id decodedImage) { + RCTImageLoaderCancellationBlock cancelBlock = [bridge.imageLoader decodeImageData:data size:CGSizeMake(1, 1) scale:1.0 clipped:NO resizeMode:RCTResizeModeStretch completionBlock:^(NSError *decodeError, id decodedImage) { XCTAssertEqualObjects(decodedImage, image); XCTAssertNil(decodeError); }]; diff --git a/Libraries/CameraRoll/RCTCameraRollManager.m b/Libraries/CameraRoll/RCTCameraRollManager.m index d060224ccc7afa..7d9e82aa254fdd 100644 --- a/Libraries/CameraRoll/RCTCameraRollManager.m +++ b/Libraries/CameraRoll/RCTCameraRollManager.m @@ -82,11 +82,12 @@ @implementation RCTCameraRollManager NSString *const RCTErrorUnableToLoad = @"E_UNABLE_TO_LOAD"; NSString *const RCTErrorUnableToSave = @"E_UNABLE_TO_SAVE"; -RCT_EXPORT_METHOD(saveImageWithTag:(NSString *)imageTag +RCT_EXPORT_METHOD(saveImageWithTag:(NSURLRequest *)imageRequest resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - [_bridge.imageLoader loadImageWithTag:imageTag callback:^(NSError *loadError, UIImage *loadedImage) { + [_bridge.imageLoader loadImageWithURLRequest:imageRequest + callback:^(NSError *loadError, UIImage *loadedImage) { if (loadError) { reject(RCTErrorUnableToLoad, nil, loadError); return; diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index b2f4718f7f120d..7afec06e8760df 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -13,6 +13,7 @@ var EdgeInsetsPropType = require('EdgeInsetsPropType'); var ImageResizeMode = require('ImageResizeMode'); +var ImageSourcePropType = require('ImageSourcePropType'); var ImageStylePropTypes = require('ImageStylePropTypes'); var NativeMethodsMixin = require('NativeMethodsMixin'); var NativeModules = require('NativeModules'); @@ -60,24 +61,32 @@ var Image = React.createClass({ propTypes: { style: StyleSheetPropType(ImageStylePropTypes), /** - * `uri` is a string representing the resource identifier for the image, which - * could be an http address, a local file path, or the name of a static image - * resource (which should be wrapped in the `require('./path/to/image.png')` function). + * The image source (either a remote URL or a local file resource). */ - source: PropTypes.oneOfType([ - PropTypes.shape({ - uri: PropTypes.string, - }), - // Opaque type returned by require('./image.jpg') - PropTypes.number, - ]), + source: ImageSourcePropType, /** * A static image to display while loading the image source. * @platform ios */ defaultSource: PropTypes.oneOfType([ PropTypes.shape({ + /** + * `uri` is a string representing the resource identifier for the image, which + * should be either a local file path or the name of a static image resource + * (which should be wrapped in the `require('./path/to/image.png')` function). + */ uri: PropTypes.string, + /** + * `width` and `height` can be specified if known at build time, in which case + * these will be used to set the default `` component dimensions. + */ + width: PropTypes.number, + height: PropTypes.number, + /** + * `scale` is used to indicate the scale factor of the image. Defaults to 1.0 if + * unspecified, meaning that one image pixel equates to one display point / DIP. + */ + scale: PropTypes.number, }), // Opaque type returned by require('./image.jpg') PropTypes.number, @@ -202,7 +211,7 @@ var Image = React.createClass({ }, render: function() { - var source = resolveAssetSource(this.props.source) || {}; + var source = resolveAssetSource(this.props.source) || { uri: null, width: undefined, height: undefined }; var {width, height, uri} = source; var style = flattenStyle([{width, height}, styles.base, this.props.style]) || {}; @@ -216,7 +225,11 @@ var Image = React.createClass({ if (isNetwork && (tintColor || this.props.blurRadius)) { RawImage = RCTImageView; } - + + if (uri === '') { + console.warn('source.uri should not be an empty string'); + } + if (this.props.src) { console.warn('The component requires a `source` property rather than `src`.'); } diff --git a/Libraries/Image/ImageSourcePropType.js b/Libraries/Image/ImageSourcePropType.js new file mode 100644 index 00000000000000..6f6c7bf8f4d0da --- /dev/null +++ b/Libraries/Image/ImageSourcePropType.js @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ImageSourcePropType + * @no-flow + */ +'use strict'; + +const PropTypes = require('ReactPropTypes'); + +const ImageSourcePropType = PropTypes.oneOfType([ + PropTypes.shape({ + /** + * `uri` is a string representing the resource identifier for the image, which + * could be an http address, a local file path, or the name of a static image + * resource (which should be wrapped in the `require('./path/to/image.png')` + * function). + */ + uri: PropTypes.string, + /** + * `method` is the HTTP Method to use. Defaults to GET if not specified. + */ + method: PropTypes.string, + /** + * `headers` is an object representing the HTTP headers to send along with the + * request for a remote image. + */ + headers: PropTypes.objectOf(PropTypes.string), + /** + * `body` is the HTTP body to send with the request. This must be a valid + * UTF-8 string, and will be sent exactly as specified, with no + * additional encoding (e.g. URL-escaping or base64) applied. + */ + body: PropTypes.string, + /** + * `width` and `height` can be specified if known at build time, in which case + * these will be used to set the default `` component dimensions. + */ + width: PropTypes.number, + height: PropTypes.number, + /** + * `scale` is used to indicate the scale factor of the image. Defaults to 1.0 if + * unspecified, meaning that one image pixel equates to one display point / DIP. + */ + scale: PropTypes.number, + }), + // Opaque type returned by require('./image.jpg') + PropTypes.number, +]); + +module.exports = ImageSourcePropType; diff --git a/Libraries/Image/RCTImageEditingManager.m b/Libraries/Image/RCTImageEditingManager.m index 9ff231d64f7f1a..cc8dfd0faf27f6 100644 --- a/Libraries/Image/RCTImageEditingManager.m +++ b/Libraries/Image/RCTImageEditingManager.m @@ -28,14 +28,14 @@ @implementation RCTImageEditingManager /** * Crops an image and adds the result to the image store. * - * @param imageTag A URL, a string identifying an asset etc. + * @param imageRequest An image URL * @param cropData Dictionary with `offset`, `size` and `displaySize`. * `offset` and `size` are relative to the full-resolution image size. * `displaySize` is an optimization - if specified, the image will * be scaled down to `displaySize` rather than `size`. * All units are in px (not points). */ -RCT_EXPORT_METHOD(cropImage:(NSString *)imageTag +RCT_EXPORT_METHOD(cropImage:(NSURLRequest *)imageRequest cropData:(NSDictionary *)cropData successCallback:(RCTResponseSenderBlock)successCallback errorCallback:(RCTResponseErrorBlock)errorCallback) @@ -45,7 +45,7 @@ @implementation RCTImageEditingManager [RCTConvert CGSize:cropData[@"size"]] }; - [_bridge.imageLoader loadImageWithTag:imageTag callback:^(NSError *error, UIImage *image) { + [_bridge.imageLoader loadImageWithURLRequest:imageRequest callback:^(NSError *error, UIImage *image) { if (error) { errorCallback(error); return; diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h index d11c424103d3ea..02d358404fcc0a 100644 --- a/Libraries/Image/RCTImageLoader.h +++ b/Libraries/Image/RCTImageLoader.h @@ -56,56 +56,85 @@ typedef void (^RCTImageLoaderCancellationBlock)(void); * Loads the specified image at the highest available resolution. * Can be called from any thread, will call back on an unspecified thread. */ -- (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag - callback:(RCTImageLoaderCompletionBlock)callback; +- (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest + callback:(RCTImageLoaderCompletionBlock)callback; + +/** + * As above, but includes target `size`, `scale` and `resizeMode`, which are used to + * select the optimal dimensions for the loaded image. The `clipped` option + * controls whether the image will be clipped to fit the specified size exactly, + * or if the original aspect ratio should be retained. + */ +- (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest + size:(CGSize)size + scale:(CGFloat)scale + clipped:(BOOL)clipped + resizeMode:(RCTResizeMode)resizeMode + progressBlock:(RCTImageLoaderProgressBlock)progressBlock + completionBlock:(RCTImageLoaderCompletionBlock)completionBlock; + +/** + * Finds an appropriate image decoder and passes the target `size`, `scale` and + * `resizeMode` for optimal image decoding. The `clipped` option controls + * whether the image will be clipped to fit the specified size exactly, or + * if the original aspect ratio should be retained. Can be called from any + * thread, will call callback on an unspecified thread. + */ +- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData + size:(CGSize)size + scale:(CGFloat)scale + clipped:(BOOL)clipped + resizeMode:(RCTResizeMode)resizeMode + completionBlock:(RCTImageLoaderCompletionBlock)completionBlock; /** - * As above, but includes target size, scale and resizeMode, which are used to - * select the optimal dimensions for the loaded image. + * Get image size, in pixels. This method will do the least work possible to get + * the information, and won't decode the image if it doesn't have to. */ +- (RCTImageLoaderCancellationBlock)getImageSizeForURLRequest:(NSURLRequest *)imageURLRequest + block:(void(^)(NSError *error, CGSize size))completionBlock; + +@end + +@interface RCTImageLoader (Deprecated) + +- (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag + callback:(RCTImageLoaderCompletionBlock)callback +__deprecated_msg("Use loadImageWithURLRequest:callback: instead"); + - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag size:(CGSize)size scale:(CGFloat)scale resizeMode:(RCTResizeMode)resizeMode progressBlock:(RCTImageLoaderProgressBlock)progressBlock - completionBlock:(RCTImageLoaderCompletionBlock)completionBlock; + completionBlock:(RCTImageLoaderCompletionBlock)completionBlock +__deprecated_msg("Use loadImageWithURLRequest:size:scale:clipped:resizeMode:progressBlock:completionBlock: instead"); -/** - * Loads an image without clipping the result to fit - used by RCTImageView. - */ - (RCTImageLoaderCancellationBlock)loadImageWithoutClipping:(NSString *)imageTag size:(CGSize)size scale:(CGFloat)scale resizeMode:(RCTResizeMode)resizeMode progressBlock:(RCTImageLoaderProgressBlock)progressBlock - completionBlock:(RCTImageLoaderCompletionBlock)completionBlock; + completionBlock:(RCTImageLoaderCompletionBlock)completionBlock +__deprecated_msg("Use loadImageWithURLRequest:size:scale:clipped:resizeMode:progressBlock:completionBlock: instead"); -/** - * Finds an appropriate image decoder and passes the target size, scale and - * resizeMode for optimal image decoding. Can be called from any thread, - * will call callback on an unspecified thread. - */ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData size:(CGSize)size scale:(CGFloat)scale resizeMode:(RCTResizeMode)resizeMode - completionBlock:(RCTImageLoaderCompletionBlock)completionBlock; + completionBlock:(RCTImageLoaderCompletionBlock)completionBlock +__deprecated_msg("Use decodeImageData:size:scale:clipped:resizeMode:completionBlock: instead"); -/** - * Decodes an image without clipping the result to fit. - */ - (RCTImageLoaderCancellationBlock)decodeImageDataWithoutClipping:(NSData *)data size:(CGSize)size scale:(CGFloat)scale resizeMode:(RCTResizeMode)resizeMode - completionBlock:(RCTImageLoaderCompletionBlock)completionBlock; + completionBlock:(RCTImageLoaderCompletionBlock)completionBlock +__deprecated_msg("Use decodeImageData:size:scale:clipped:resizeMode:completionBlock: instead"); -/** - * Get image size, in pixels. This method will do the least work possible to get - * the information, and won't decode the image if it doesn't have to. - */ - (RCTImageLoaderCancellationBlock)getImageSize:(NSString *)imageTag - block:(void(^)(NSError *error, CGSize size))completionBlock; + block:(void(^)(NSError *error, CGSize size))completionBlock +__deprecated_msg("Use getImageSizeWithURLRequest:callback: instead"); @end diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index c6f4e2ec871f9c..5059b2c1c932e7 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -220,15 +220,16 @@ - (void)dealloc return image; } -- (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag +- (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest callback:(RCTImageLoaderCompletionBlock)callback { - return [self loadImageWithTag:imageTag - size:CGSizeZero - scale:1 - resizeMode:RCTResizeModeStretch - progressBlock:nil - completionBlock:callback]; + return [self loadImageWithURLRequest:imageURLRequest + size:CGSizeZero + scale:1 + clipped:YES + resizeMode:RCTResizeModeStretch + progressBlock:nil + completionBlock:callback]; } - (void)dequeueTasks @@ -280,12 +281,12 @@ - (void)dequeueTasks * path taken. This is useful if you want to skip decoding, e.g. when preloading * the image, or retrieving metadata. */ -- (RCTImageLoaderCancellationBlock)loadImageOrDataWithTag:(NSString *)imageTag - size:(CGSize)size - scale:(CGFloat)scale - resizeMode:(RCTResizeMode)resizeMode - progressBlock:(RCTImageLoaderProgressBlock)progressHandler - completionBlock:(void (^)(NSError *error, id imageOrData))completionBlock +- (RCTImageLoaderCancellationBlock)loadImageOrDataWithURLRequest:(NSURLRequest *)imageURLRequest + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(RCTResizeMode)resizeMode + progressBlock:(RCTImageLoaderProgressBlock)progressHandler + completionBlock:(void (^)(NSError *error, id imageOrData))completionBlock { __block volatile uint32_t cancelled = 0; __block void(^cancelLoad)(void) = nil; @@ -306,11 +307,6 @@ - (RCTImageLoaderCancellationBlock)loadImageOrDataWithTag:(NSString *)imageTag } }; - if (imageTag.length == 0) { - completionHandler(RCTErrorWithMessage(@"source.uri should not be an empty string"), nil); - return ^{}; - } - // All access to URL cache must be serialized if (!_URLCacheQueue) { [self setUp]; @@ -329,7 +325,7 @@ - (RCTImageLoaderCancellationBlock)loadImageOrDataWithTag:(NSString *)imageTag } // Find suitable image URL loader - NSURLRequest *request = [RCTConvert NSURLRequest:imageTag]; + NSURLRequest *request = imageURLRequest; // Use a local variable so we can reassign it in this block id loadHandler = [strongSelf imageURLLoaderForURL:request.URL]; if (loadHandler) { cancelLoad = [loadHandler loadImageForURL:request.URL @@ -345,13 +341,13 @@ - (RCTImageLoaderCancellationBlock)loadImageOrDataWithTag:(NSString *)imageTag if (RCT_DEBUG && ![_bridge respondsToSelector:@selector(networking)]) { RCTLogError(@"No suitable image URL loader found for %@. You may need to " " import the RCTNetwork library in order to load images.", - imageTag); + request.URL.absoluteString); return; } // Check if networking module can load image if (RCT_DEBUG && ![_bridge.networking canHandleRequest:request]) { - RCTLogError(@"No suitable image URL loader found for %@", imageTag); + RCTLogError(@"No suitable image URL loader found for %@", request.URL.absoluteString); return; } @@ -405,7 +401,7 @@ - (RCTImageLoaderCancellationBlock)loadImageOrDataWithTag:(NSString *)imageTag } NSURL *redirectURL = [NSURL URLWithString: location]; - request = [NSURLRequest requestWithURL: redirectURL]; + request = [NSURLRequest requestWithURL:redirectURL]; cachedResponse = [_URLCache cachedResponseForRequest:request]; continue; } @@ -470,36 +466,20 @@ - (RCTImageLoaderCancellationBlock)loadImageOrDataWithTag:(NSString *)imageTag }; } -- (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag - size:(CGSize)size - scale:(CGFloat)scale - resizeMode:(RCTResizeMode)resizeMode - progressBlock:(RCTImageLoaderProgressBlock)progressHandler - completionBlock:(RCTImageLoaderCompletionBlock)completionBlock -{ - return [self loadImageWithoutClipping:imageTag - size:size - scale:scale - resizeMode:resizeMode - progressBlock:progressHandler - completionBlock:^(NSError *error, UIImage *image) { - completionBlock(error, RCTResizeImageIfNeeded(image, size, scale, resizeMode)); - }]; -} - -- (RCTImageLoaderCancellationBlock)loadImageWithoutClipping:(NSString *)imageTag - size:(CGSize)size - scale:(CGFloat)scale - resizeMode:(RCTResizeMode)resizeMode - progressBlock:(RCTImageLoaderProgressBlock)progressHandler - completionBlock:(RCTImageLoaderCompletionBlock)completionBlock +- (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest + size:(CGSize)size + scale:(CGFloat)scale + clipped:(BOOL)clipped + resizeMode:(RCTResizeMode)resizeMode + progressBlock:(RCTImageLoaderProgressBlock)progressHandler + completionBlock:(RCTImageLoaderCompletionBlock)completionBlock { __block volatile uint32_t cancelled = 0; __block void(^cancelLoad)(void) = nil; __weak RCTImageLoader *weakSelf = self; // Check decoded image cache - NSString *cacheKey = RCTCacheKeyForImage(imageTag, size, scale, resizeMode); + NSString *cacheKey = RCTCacheKeyForImage(imageURLRequest.URL.absoluteString, size, scale, resizeMode); { UIImage *image = [_decodedImageCache objectForKey:cacheKey]; if (image) { @@ -527,16 +507,17 @@ - (RCTImageLoaderCancellationBlock)loadImageWithoutClipping:(NSString *)imageTag if (!imageOrData || [imageOrData isKindOfClass:[UIImage class]]) { cacheResultHandler(error, imageOrData); } else { - cancelLoad = [weakSelf decodeImageDataWithoutClipping:imageOrData - size:size - scale:scale - resizeMode:resizeMode - completionBlock:cacheResultHandler]; + cancelLoad = [weakSelf decodeImageData:imageOrData + size:size + scale:scale + clipped:clipped + resizeMode:resizeMode + completionBlock:cacheResultHandler]; } } }; - cancelLoad = [self loadImageOrDataWithTag:imageTag + cancelLoad = [self loadImageOrDataWithURLRequest:imageURLRequest size:size scale:scale resizeMode:resizeMode @@ -553,23 +534,9 @@ - (RCTImageLoaderCancellationBlock)loadImageWithoutClipping:(NSString *)imageTag - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data size:(CGSize)size scale:(CGFloat)scale + clipped:(BOOL)clipped resizeMode:(RCTResizeMode)resizeMode completionBlock:(RCTImageLoaderCompletionBlock)completionBlock -{ - return [self decodeImageDataWithoutClipping:data - size:size - scale:scale - resizeMode:resizeMode - completionBlock:^(NSError *error, UIImage *image) { - completionBlock(error, RCTResizeImageIfNeeded(image, size, scale, resizeMode)); - }]; -} - -- (RCTImageLoaderCancellationBlock)decodeImageDataWithoutClipping:(NSData *)data - size:(CGSize)size - scale:(CGFloat)scale - resizeMode:(RCTResizeMode)resizeMode - completionBlock:(RCTImageLoaderCompletionBlock)completionBlock { if (data.length == 0) { completionBlock(RCTErrorWithMessage(@"No image data"), nil); @@ -583,11 +550,11 @@ - (RCTImageLoaderCancellationBlock)decodeImageDataWithoutClipping:(NSData *)data // expecting it, and may do expensive post-processing in the callback dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ if (!cancelled) { - completionBlock(error, image); + completionBlock(error, clipped ? RCTResizeImageIfNeeded(image, size, scale, resizeMode) : image); } }); } else if (!cancelled) { - completionBlock(error, image); + completionBlock(error, clipped ? RCTResizeImageIfNeeded(image, size, scale, resizeMode) : image); } }; @@ -675,10 +642,10 @@ - (RCTImageLoaderCancellationBlock)decodeImageDataWithoutClipping:(NSData *)data } } -- (RCTImageLoaderCancellationBlock)getImageSize:(NSString *)imageTag +- (RCTImageLoaderCancellationBlock)getImageSizeForURLRequest:(NSURLRequest *)imageURLRequest block:(void(^)(NSError *error, CGSize size))completionBlock { - return [self loadImageOrDataWithTag:imageTag + return [self loadImageOrDataWithURLRequest:imageURLRequest size:CGSizeZero scale:1 resizeMode:RCTResizeModeStretch @@ -713,7 +680,7 @@ - (RCTImageLoaderCancellationBlock)getImageSize:(NSString *)imageTag return; } - [_bridge.imageLoader loadImageWithTag:uri callback:^(NSError *error, UIImage *image) { + [_bridge.imageLoader loadImageWithURLRequest:[RCTConvert NSURLRequest:uri] callback:^(NSError *error, UIImage *image) { if (error) { reject(RCTErrorPrefetchFailure, nil, error); return; @@ -743,7 +710,7 @@ - (BOOL)canHandleRequest:(NSURLRequest *)request - (id)sendRequest:(NSURLRequest *)request withDelegate:(id)delegate { __block RCTImageLoaderCancellationBlock requestToken; - requestToken = [self loadImageWithTag:request.URL.absoluteString callback:^(NSError *error, UIImage *image) { + requestToken = [self loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) { if (error) { [delegate URLRequest:requestToken didCompleteWithError:error]; return; @@ -781,6 +748,90 @@ - (void)cancelRequest:(id)requestToken @end +@implementation RCTImageLoader (Deprecated) + +- (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag + callback:(RCTImageLoaderCompletionBlock)callback +{ + RCTLogWarn(@"[RCTImageLoader loadImageWithTag:callback:] is deprecated. Instead use [RCTImageLoader loadImageWithURLRequest:callback:]"); + return [self loadImageWithURLRequest:[RCTConvert NSURLRequest:imageTag] + callback:callback]; +} + +- (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(RCTResizeMode)resizeMode + progressBlock:(RCTImageLoaderProgressBlock)progressBlock + completionBlock:(RCTImageLoaderCompletionBlock)completionBlock +{ + RCTLogWarn(@"[RCTImageLoader loadImageWithTag:size:scale:resizeMode:progressBlock:completionBlock:] is deprecated. Instead use [RCTImageLoader loadImageWithURLRequest:size:scale:clipped:resizeMode:progressBlock:completionBlock:]"); + return [self loadImageWithURLRequest:[RCTConvert NSURLRequest:imageTag] + size:size + scale:scale + clipped:YES + resizeMode:resizeMode + progressBlock:progressBlock + completionBlock:completionBlock]; +} + +- (RCTImageLoaderCancellationBlock)loadImageWithoutClipping:(NSString *)imageTag + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(RCTResizeMode)resizeMode + progressBlock:(RCTImageLoaderProgressBlock)progressBlock + completionBlock:(RCTImageLoaderCompletionBlock)completionBlock +{ + RCTLogWarn(@"[RCTImageLoader loadImageWithoutClipping:size:scale:resizeMode:progressBlock:completionBlock:] is deprecated. Instead use [RCTImageLoader loadImageWithURLRequest:size:scale:clipped:resizeMode:progressBlock:completionBlock:]"); + return [self loadImageWithURLRequest:[RCTConvert NSURLRequest:imageTag] + size:size + scale:scale + clipped:NO + resizeMode:resizeMode + progressBlock:progressBlock + completionBlock:completionBlock]; +} + +- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(RCTResizeMode)resizeMode + completionBlock:(RCTImageLoaderCompletionBlock)completionBlock +{ + RCTLogWarn(@"[RCTImageLoader decodeImageData:size:scale:resizeMode:completionBlock:] is deprecated. Instead use [RCTImageLoader decodeImageData:size:scale:clipped:resizeMode:completionBlock:]"); + return [self decodeImageData:imageData + size:size + scale:scale + clipped:NO + resizeMode:resizeMode + completionBlock:completionBlock]; +} + +- (RCTImageLoaderCancellationBlock)decodeImageDataWithoutClipping:(NSData *)imageData + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(RCTResizeMode)resizeMode + completionBlock:(RCTImageLoaderCompletionBlock)completionBlock +{ + RCTLogWarn(@"[RCTImageLoader decodeImageDataWithoutClipping:size:scale:resizeMode:completionBlock:] is deprecated. Instead use [RCTImageLoader decodeImageData:size:scale:clipped:resizeMode:completionBlock:]"); + return [self decodeImageData:imageData + size:size + scale:scale + clipped:NO + resizeMode:resizeMode + completionBlock:completionBlock]; +} + +- (RCTImageLoaderCancellationBlock)getImageSize:(NSString *)imageTag + block:(void(^)(NSError *error, CGSize size))completionBlock +{ + RCTLogWarn(@"[RCTImageLoader getImageSize:block:] is deprecated. Instead use [RCTImageLoader getImageSizeForURLRequest:block:]"); + return [self getImageSizeForURLRequest:[RCTConvert NSURLRequest:imageTag] + block:completionBlock]; +} + +@end + @implementation RCTBridge (RCTImageLoader) - (RCTImageLoader *)imageLoader diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index 1376fb79649487..a0d63d35f0622c 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -224,12 +224,15 @@ - (void)reloadImage RCTImageSource *source = _source; CGFloat blurRadius = _blurRadius; __weak RCTImageView *weakSelf = self; - _reloadImageCancellationBlock = [_bridge.imageLoader loadImageWithoutClipping:_source.imageURL.absoluteString - size:imageSize - scale:imageScale - resizeMode:(RCTResizeMode)self.contentMode - progressBlock:progressHandler - completionBlock:^(NSError *error, UIImage *loadedImage) { + _reloadImageCancellationBlock = + [_bridge.imageLoader loadImageWithURLRequest:_source.request + size:imageSize + scale:imageScale + clipped:NO + resizeMode:(RCTResizeMode)self.contentMode + progressBlock:progressHandler + completionBlock:^(NSError *error, UIImage *loadedImage) { + RCTImageView *strongSelf = weakSelf; void (^setImageBlock)(UIImage *) = ^(UIImage *image) { if (![source isEqual:strongSelf.source]) { @@ -297,7 +300,7 @@ - (void)reactSetFrame:(CGRect)frame if (RCTShouldReloadImageForSizeChange(imageSize, idealSize)) { if (RCTShouldReloadImageForSizeChange(_targetSize, idealSize)) { - RCTLogInfo(@"[PERF IMAGEVIEW] Reloading image %@ as size %@", _source.imageURL, NSStringFromCGSize(idealSize)); + RCTLogInfo(@"[PERF IMAGEVIEW] Reloading image %@ as size %@", _source.request.URL.absoluteString, NSStringFromCGSize(idealSize)); // If the existing image or an image being loaded are not the right // size, reload the asset in case there is a better size available. diff --git a/Libraries/Image/RCTImageViewManager.m b/Libraries/Image/RCTImageViewManager.m index 2bf7a0fe3369ee..5da98b5cdd536c 100644 --- a/Libraries/Image/RCTImageViewManager.m +++ b/Libraries/Image/RCTImageViewManager.m @@ -48,7 +48,7 @@ - (UIView *)view successBlock:(RCTResponseSenderBlock)successBlock errorBlock:(RCTResponseErrorBlock)errorBlock) { - [self.bridge.imageLoader getImageSize:imageURL.absoluteString + [self.bridge.imageLoader getImageSizeForURLRequest:[NSURLRequest requestWithURL:imageURL] block:^(NSError *error, CGSize size) { if (error) { errorBlock(error); diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index f5c6924d6a0380..8c6816e9bd6bc2 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -133,6 +133,23 @@ + (NSURLRequest *)NSURLRequest:(id)json if ([method isEqualToString:@"GET"] && headers == nil && body == nil) { return [NSURLRequest requestWithURL:URL]; } + + if (headers) { + __block BOOL allHeadersAreStrings = YES; + [headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, id header, BOOL *stop) { + if (![header isKindOfClass:[NSString class]]) { + RCTLogError(@"Values of HTTP headers passed must be of type string. " + "Value of header '%@' is not a string.", key); + allHeadersAreStrings = NO; + *stop = YES; + } + }]; + if (!allHeadersAreStrings) { + // Set headers to nil here to avoid crashing later. + headers = nil; + } + } + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; request.HTTPBody = body; request.HTTPMethod = method; @@ -878,7 +895,7 @@ + (UIImage *)UIImage:(id)json return image; } - NSURL *URL = imageSource.imageURL; + NSURL *URL = imageSource.request.URL; NSString *scheme = URL.scheme.lowercaseString; if ([scheme isEqualToString:@"file"]) { NSString *assetName = RCTBundlePathForURL(URL); @@ -914,7 +931,7 @@ + (UIImage *)UIImage:(id)json if (!CGSizeEqualToSize(imageSource.size, CGSizeZero) && !CGSizeEqualToSize(imageSource.size, image.size)) { RCTLogError(@"Image source %@ size %@ does not match loaded image size %@.", - imageSource.imageURL.path.lastPathComponent, + URL.path.lastPathComponent, NSStringFromCGSize(imageSource.size), NSStringFromCGSize(image.size)); } diff --git a/React/Base/RCTImageSource.h b/React/Base/RCTImageSource.h index efcefaa2b89fba..e5b52c50216451 100644 --- a/React/Base/RCTImageSource.h +++ b/React/Base/RCTImageSource.h @@ -16,7 +16,7 @@ */ @interface RCTImageSource : NSObject -@property (nonatomic, strong, readonly) NSURL *imageURL; +@property (nonatomic, copy, readonly) NSURLRequest *request; @property (nonatomic, assign, readonly) CGSize size; @property (nonatomic, assign, readonly) CGFloat scale; @@ -25,9 +25,9 @@ * Pass a size of CGSizeZero if you do not know or wish to specify the image * size. Pass a scale of zero if you do not know or wish to specify the scale. */ -- (instancetype)initWithURL:(NSURL *)url - size:(CGSize)size - scale:(CGFloat)scale; +- (instancetype)initWithURLRequest:(NSURLRequest *)request + size:(CGSize)size + scale:(CGFloat)scale; /** * Create a copy of the image source with the specified size and scale. @@ -36,6 +36,13 @@ @end +@interface RCTImageSource (Deprecated) + +@property (nonatomic, strong, readonly) NSURL *imageURL +__deprecated_msg("Use request.URL instead."); + +@end + @interface RCTConvert (ImageSource) + (RCTImageSource *)RCTImageSource:(id)json; diff --git a/React/Base/RCTImageSource.m b/React/Base/RCTImageSource.m index 07518edf928a4d..173bad517ce5de 100644 --- a/React/Base/RCTImageSource.m +++ b/React/Base/RCTImageSource.m @@ -18,10 +18,10 @@ @interface RCTImageSource () @implementation RCTImageSource -- (instancetype)initWithURL:(NSURL *)url size:(CGSize)size scale:(CGFloat)scale +- (instancetype)initWithURLRequest:(NSURLRequest *)request size:(CGSize)size scale:(CGFloat)scale { if ((self = [super init])) { - _imageURL = url; + _request = [request copy]; _size = size; _scale = scale; } @@ -30,9 +30,9 @@ - (instancetype)initWithURL:(NSURL *)url size:(CGSize)size scale:(CGFloat)scale - (instancetype)imageSourceWithSize:(CGSize)size scale:(CGFloat)scale { - RCTImageSource *imageSource = [[RCTImageSource alloc] initWithURL:_imageURL - size:size - scale:scale]; + RCTImageSource *imageSource = [[RCTImageSource alloc] initWithURLRequest:_request + size:size + scale:scale]; imageSource.packagerAsset = _packagerAsset; return imageSource; } @@ -42,12 +42,22 @@ - (BOOL)isEqual:(RCTImageSource *)object if (![object isKindOfClass:[RCTImageSource class]]) { return NO; } - return [_imageURL isEqual:object.imageURL] && _scale == object.scale && + return [_request isEqual:object.request] && _scale == object.scale && (CGSizeEqualToSize(_size, object.size) || CGSizeEqualToSize(object.size, CGSizeZero)); } @end + +@implementation RCTImageSource (Deprecated) + +- (NSURL *)imageURL +{ + return self.request.URL; +} + +@end + @implementation RCTConvert (ImageSource) + (RCTImageSource *)RCTImageSource:(id)json @@ -56,25 +66,25 @@ + (RCTImageSource *)RCTImageSource:(id)json return nil; } - NSURL *imageURL; + NSURLRequest *request; CGSize size = CGSizeZero; CGFloat scale = 1.0; BOOL packagerAsset = NO; if ([json isKindOfClass:[NSDictionary class]]) { - if (!(imageURL = [self NSURL:RCTNilIfNull(json[@"uri"])])) { + if (!(request = [self NSURLRequest:json])) { return nil; } size = [self CGSize:json]; scale = [self CGFloat:json[@"scale"]] ?: [self BOOL:json[@"deprecated"]] ? 0.0 : 1.0; packagerAsset = [self BOOL:json[@"__packager_asset"]]; } else if ([json isKindOfClass:[NSString class]]) { - imageURL = [self NSURL:json]; + request = [self NSURLRequest:json]; } else { RCTLogConvertError(json, @"an image. Did you forget to call resolveAssetSource() on the JS side?"); return nil; } - RCTImageSource *imageSource = [[RCTImageSource alloc] initWithURL:imageURL + RCTImageSource *imageSource = [[RCTImageSource alloc] initWithURLRequest:request size:size scale:scale]; imageSource.packagerAsset = packagerAsset; From bbc6139baf215da40da125a92965da03d7627cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Wed, 1 Jun 2016 13:33:48 -0700 Subject: [PATCH 181/843] Reverted commit D3348218 Summary: The TextInput when configured with `multiline=true` has a minor issue due to which if the initial text doesn't fit in one line, the input won't span multiple lines. The root cause of the problem is that the `onChange` event is not fired for the initial render. This issue has been reported on open-source: https://github.com/facebook/react-native/commit/481f560f64806ba3324cf722d6bf8c3f36ac74a5 This is an attempt to fix this problem by moving the logic to fire the event to the method that updates the content size. This way we can guarantee that anytime the content size changes we'll trigger the JS event. The downside of this approach is that it's possible that multiple events get fired for a single character change. As per the comment that was removed, this was already happening when adding a character that when rendered, would increase the content size. By moving the code to the new place, this can happen more often (twice per character tapped). Let me know if you think this is an issue. I don't know this code much, so please be careful reviewing. I'm happy to add more test cases I may have missed to the test plan :). Reviewed By: nicklockwood Differential Revision: D3348218 fbshipit-source-id: 6b457624c9126e771c326eac61cd1cdd6496671d --- Libraries/Text/RCTTextView.m | 41 +++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index a04bad4579a302..d32463333bfa7b 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -251,23 +251,6 @@ - (void)updateContentSize size.height = [_textView sizeThatFits:size].height; _scrollView.contentSize = size; _textView.frame = (CGRect){CGPointZero, size}; - - NSUInteger textLength = _textView.text.length; - CGFloat contentHeight = _textView.contentSize.height; - if (textLength >= _previousTextLength) { - contentHeight = MAX(contentHeight, _previousContentHeight); - } - _previousTextLength = textLength; - _previousContentHeight = contentHeight; - _onChange(@{ - @"text": self.text, - @"contentSize": @{ - @"height": @(contentHeight), - @"width": @(_textView.contentSize.width) - }, - @"target": self.reactTag, - @"eventCount": @(_nativeEventCount), - }); } - (void)updatePlaceholder @@ -492,6 +475,30 @@ - (void)textViewDidChange:(UITextView *)textView if (!self.reactTag || !_onChange) { return; } + + // When the context size increases, iOS updates the contentSize twice; once + // with a lower height, then again with the correct height. To prevent a + // spurious event from being sent, we track the previous, and only send the + // update event if it matches our expectation that greater text length + // should result in increased height. This assumption is, of course, not + // necessarily true because shorter text might include more linebreaks, but + // in practice this works well enough. + NSUInteger textLength = textView.text.length; + CGFloat contentHeight = textView.contentSize.height; + if (textLength >= _previousTextLength) { + contentHeight = MAX(contentHeight, _previousContentHeight); + } + _previousTextLength = textLength; + _previousContentHeight = contentHeight; + _onChange(@{ + @"text": self.text, + @"contentSize": @{ + @"height": @(contentHeight), + @"width": @(textView.contentSize.width) + }, + @"target": self.reactTag, + @"eventCount": @(_nativeEventCount), + }); } - (void)textViewDidEndEditing:(UITextView *)textView From 7e100ac7a225bd5f03dc97a828f414b9f862e1ff Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Wed, 1 Jun 2016 13:50:30 -0700 Subject: [PATCH 182/843] Extract getDevServerURL into reusable module Summary: Many RN devtools (inspector, source maps, etc.) rely on packager. This refactors individual SourceCode.scriptURL parsing into one function that will be easier to change in the future. Reviewed By: javache Differential Revision: D3348465 fbshipit-source-id: 5a55939ea59e1517cb63bcbe4963f57f02ab15f3 --- Libraries/Inspector/ElementProperties.js | 7 +--- .../Initialization/ExceptionsManager.js | 11 +++-- .../Initialization/getDevServer.js | 40 +++++++++++++++++++ 3 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 Libraries/JavaScriptAppEngine/Initialization/getDevServer.js diff --git a/Libraries/Inspector/ElementProperties.js b/Libraries/Inspector/ElementProperties.js index c0b11a3882e62b..f7698cdefc2668 100644 --- a/Libraries/Inspector/ElementProperties.js +++ b/Libraries/Inspector/ElementProperties.js @@ -20,11 +20,11 @@ var Text = require('Text'); var TouchableHighlight = require('TouchableHighlight'); var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); var View = require('View'); -var {SourceCode} = require('NativeModules'); var {fetch} = require('fetch'); var flattenStyle = require('flattenStyle'); var mapWithSeparator = require('mapWithSeparator'); +var getDevServer = require('getDevServer'); var ElementProperties = React.createClass({ propTypes: { @@ -97,10 +97,7 @@ var ElementProperties = React.createClass({ }, _openFile: function(fileName: string, lineNumber: number) { - var match = SourceCode.scriptURL && SourceCode.scriptURL.match(/^https?:\/\/.*?\//); - var baseURL = match ? match[0] : 'http://localhost:8081/'; - - fetch(baseURL + 'open-stack-frame', { + fetch(getDevServer().url + 'open-stack-frame', { method: 'POST', body: JSON.stringify({file: fileName, lineNumber}), }); diff --git a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js index 19d651336d6846..42c8e9d1d39a59 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js +++ b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js @@ -35,12 +35,15 @@ function reportException(e: Error, isFatal: bool) { } function symbolicateAndUpdateStack(id, message, stack) { + const getDevServer = require('getDevServer'); const {fetch} = require('fetch'); - const {SourceCode, ExceptionsManager} = require('NativeModules'); - const match = SourceCode.scriptURL && SourceCode.scriptURL.match(/^https?:\/\/.*?\//); - const endpoint = (match && match[0] : 'http://localhost:8081/') + 'symbolicate'; + const {ExceptionsManager} = require('NativeModules'); + const devServer = getDevServer(); + if (!devServer.bundleLoadedFromServer) { + return; + } - fetch(endpoint, { method: 'POST', body: JSON.stringify({stack}) }) + fetch(devServer.url + 'symbolicate', { method: 'POST', body: JSON.stringify({stack}) }) .then(response => response.json()) .then(response => ExceptionsManager.updateExceptionMessage(message, response.stack, id)) diff --git a/Libraries/JavaScriptAppEngine/Initialization/getDevServer.js b/Libraries/JavaScriptAppEngine/Initialization/getDevServer.js new file mode 100644 index 00000000000000..b733c2f5c1ad15 --- /dev/null +++ b/Libraries/JavaScriptAppEngine/Initialization/getDevServer.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule getDevServer + * @flow + */ +'use strict'; + +const {SourceCode} = require('NativeModules'); + +let _cachedDevServerURL: ?string; +const FALLBACK = 'http://localhost:8081/'; + +type DevServerInfo = { + url: string; + bundleLoadedFromServer: boolean; +}; + +/** + * Many RN development tools rely on the development server (packager) running + * @return URL to packager with trailing slash + */ +function getDevServer(): DevServerInfo { + if (_cachedDevServerURL === undefined) { + const match = SourceCode.scriptURL && SourceCode.scriptURL.match(/^https?:\/\/.*?\//); + _cachedDevServerURL = match ? match[0] : null; + } + + return { + url: _cachedDevServerURL || FALLBACK, + bundleLoadedFromServer: _cachedDevServerURL !== null, + }; +} + +module.exports = getDevServer; From 2ef533352fcccf82c6bd82ce7facb2c92403ba22 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Wed, 1 Jun 2016 13:50:34 -0700 Subject: [PATCH 183/843] Extract symbolicateStackTrace into its own module Summary: Having a function that symbolicates any given stack trace is convenient, e.g. in #7459 More cleanup to follow. Reviewed By: davidaurelio Differential Revision: D3348616 fbshipit-source-id: 6313ec837869c6080829c811345a06aa1b2dcd21 --- .../Initialization/ExceptionsManager.js | 28 ++++--------- .../Initialization/parseErrorStack.js | 41 ++++--------------- .../Initialization/symbolicateStackTrace.js | 32 +++++++++++++++ 3 files changed, 47 insertions(+), 54 deletions(-) create mode 100644 Libraries/JavaScriptAppEngine/Initialization/symbolicateStackTrace.js diff --git a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js index 42c8e9d1d39a59..e5eae65b47a6be 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js +++ b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js @@ -18,6 +18,7 @@ let exceptionID = 0; */ function reportException(e: Error, isFatal: bool) { const parseErrorStack = require('parseErrorStack'); + const symbolicateStackTrace = require('symbolicateStackTrace'); const RCTExceptionsManager = require('NativeModules').ExceptionsManager; const currentExceptionID = ++exceptionID; @@ -29,31 +30,16 @@ function reportException(e: Error, isFatal: bool) { RCTExceptionsManager.reportSoftException(e.message, stack, currentExceptionID); } if (__DEV__) { - symbolicateAndUpdateStack(currentExceptionID, e.message, stack); + symbolicateStackTrace(stack).then( + (prettyStack) => + RCTExceptionsManager.updateExceptionMessage(e.message, prettyStack, currentExceptionID), + (error) => + console.warn('Unable to symbolicate stack trace: ' + error.message) + ); } } } -function symbolicateAndUpdateStack(id, message, stack) { - const getDevServer = require('getDevServer'); - const {fetch} = require('fetch'); - const {ExceptionsManager} = require('NativeModules'); - const devServer = getDevServer(); - if (!devServer.bundleLoadedFromServer) { - return; - } - - fetch(devServer.url + 'symbolicate', { method: 'POST', body: JSON.stringify({stack}) }) - .then(response => response.json()) - .then(response => - ExceptionsManager.updateExceptionMessage(message, response.stack, id)) - .catch(error => { - // This can happen in a variety of normal situations, such as - // Network module not being available, or when running locally - console.warn('Unable to symbolicate stack trace: ' + error.message); - }); -} - /** * Logs exceptions to the (native) console and displays them */ diff --git a/Libraries/JavaScriptAppEngine/Initialization/parseErrorStack.js b/Libraries/JavaScriptAppEngine/Initialization/parseErrorStack.js index 5fec943152b409..1b0d6633161d4d 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/parseErrorStack.js +++ b/Libraries/JavaScriptAppEngine/Initialization/parseErrorStack.js @@ -7,31 +7,19 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule parseErrorStack + * @flow */ 'use strict'; -var stacktraceParser = require('stacktrace-parser'); +export type StackFrame = { + file: string; + lineNumber: number; + column: number; +}; -function resolveSourceMaps(sourceMapInstance, stackFrame) { - try { - var orig = sourceMapInstance.originalPositionFor({ - line: stackFrame.lineNumber, - column: stackFrame.column, - }); - if (orig) { - // remove query string if any - const queryStringStartIndex = orig.source.indexOf('?'); - stackFrame.file = queryStringStartIndex === -1 - ? orig.source - : orig.source.substring(0, queryStringStartIndex); - stackFrame.lineNumber = orig.line; - stackFrame.column = orig.column; - } - } catch (innerEx) { - } -} +var stacktraceParser = require('stacktrace-parser'); -function parseErrorStack(e, sourceMaps) { +function parseErrorStack(e: Error): Array { if (!e || !e.stack) { return []; } @@ -43,19 +31,6 @@ function parseErrorStack(e, sourceMaps) { stack.shift(); } - if (sourceMaps) { - sourceMaps.forEach((sourceMap, index) => { - stack.forEach(frame => { - if (frame.file.indexOf(sourceMap.file) !== -1 || - frame.file.replace('.map', '.bundle').indexOf( - sourceMap.file - ) !== -1) { - resolveSourceMaps(sourceMap, frame); - } - }); - }); - } - return stack; } diff --git a/Libraries/JavaScriptAppEngine/Initialization/symbolicateStackTrace.js b/Libraries/JavaScriptAppEngine/Initialization/symbolicateStackTrace.js new file mode 100644 index 00000000000000..122837dad37049 --- /dev/null +++ b/Libraries/JavaScriptAppEngine/Initialization/symbolicateStackTrace.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule symbolicateStackTrace + * @flow + */ +'use strict'; + +const {fetch} = require('fetch'); +const getDevServer = require('getDevServer'); + +import type {StackFrame} from 'parseErrorStack'; + +async function symbolicateStackTrace(stack: Array): Promise> { + const devServer = getDevServer(); + if (!devServer.bundleLoadedFromServer) { + throw new Error('Bundle was not loaded from the packager'); + } + const response = await fetch(devServer.url + 'symbolicate', { + method: 'POST', + body: JSON.stringify({stack}), + }); + const json = await response.json(); + return json.stack; +} + +module.exports = symbolicateStackTrace; From 0d1d7ba2b7e142e9a3b41dcdaa3b1ca52177f5a5 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Wed, 1 Jun 2016 13:50:36 -0700 Subject: [PATCH 184/843] Remove dead SourceMap code Summary: In several pervious diffs we have moved symbolication of JS stack traces to the packeger. This lets us save a bunch of memory (~80MB) and CPU on device, reduces dependency on JS after exception occured. This diff cleans up a bunch of code that was used to do symbolication on client side. Reviewed By: javache Differential Revision: D3348676 fbshipit-source-id: 88baa5c502836c9ca892896e1ee5d83db37486d3 --- .../Initialization/SourceMap.js | 1966 ----------------- .../Initialization/SourceMapsCache.js | 44 - .../Initialization/SourceMapsUtils.js | 88 - Libraries/Utilities/HMRClient.js | 14 - 4 files changed, 2112 deletions(-) delete mode 100644 Libraries/JavaScriptAppEngine/Initialization/SourceMap.js delete mode 100644 Libraries/JavaScriptAppEngine/Initialization/SourceMapsCache.js delete mode 100644 Libraries/JavaScriptAppEngine/Initialization/SourceMapsUtils.js diff --git a/Libraries/JavaScriptAppEngine/Initialization/SourceMap.js b/Libraries/JavaScriptAppEngine/Initialization/SourceMap.js deleted file mode 100644 index ce42bb6ced30c1..00000000000000 --- a/Libraries/JavaScriptAppEngine/Initialization/SourceMap.js +++ /dev/null @@ -1,1966 +0,0 @@ - -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule SourceMap - * @generated - * @extern - * - * This module was generated from `node_modules/source-map` by running - * - * $ npm install dryice - * $ node Makefile.dryice.js - * $ cat dist/source-map.js - * - * and wrapping resulting file into `wrapper` function. - * - */ -/*eslint-disable */ - -var scope = {}; -wrapper.call(scope); - -module.exports = scope.sourceMap; - -function wrapper() { - -/* -*- Mode: js; js-indent-level: 2; -*- */ -/* - * Copyright 2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE or: - * http://opensource.org/licenses/BSD-3-Clause - */ - -/** - * Define a module along with a payload. - * @param {string} moduleName Name for the payload - * @param {ignored} deps Ignored. For compatibility with CommonJS AMD Spec - * @param {function} payload Function with (require, exports, module) params - */ -function define(moduleName, deps, payload) { - if (typeof moduleName != "string") { - throw new TypeError('Expected string, got: ' + moduleName); - } - - if (arguments.length == 2) { - payload = deps; - } - - if (moduleName in define.modules) { - throw new Error("Module already defined: " + moduleName); - } - define.modules[moduleName] = payload; -}; - -/** - * The global store of un-instantiated modules - */ -define.modules = {}; - - -/** - * We invoke require() in the context of a Domain so we can have multiple - * sets of modules running separate from each other. - * This contrasts with JSMs which are singletons, Domains allows us to - * optionally load a CommonJS module twice with separate data each time. - * Perhaps you want 2 command lines with a different set of commands in each, - * for example. - */ -function Domain() { - this.modules = {}; - this._currentModule = null; -} - -(function () { - - /** - * Lookup module names and resolve them by calling the definition function if - * needed. - * There are 2 ways to call this, either with an array of dependencies and a - * callback to call when the dependencies are found (which can happen - * asynchronously in an in-page context) or with a single string an no callback - * where the dependency is resolved synchronously and returned. - * The API is designed to be compatible with the CommonJS AMD spec and - * RequireJS. - * @param {string[]|string} deps A name, or names for the payload - * @param {function|undefined} callback Function to call when the dependencies - * are resolved - * @return {undefined|object} The module required or undefined for - * array/callback method - */ - Domain.prototype.require = function(deps, callback) { - if (Array.isArray(deps)) { - var params = deps.map(function(dep) { - return this.lookup(dep); - }, this); - if (callback) { - callback.apply(null, params); - } - return undefined; - } - else { - return this.lookup(deps); - } - }; - - function normalize(path) { - var bits = path.split('/'); - var i = 1; - while (i < bits.length) { - if (bits[i] === '..') { - bits.splice(i-1, 1); - } else if (bits[i] === '.') { - bits.splice(i, 1); - } else { - i++; - } - } - return bits.join('/'); - } - - function join(a, b) { - a = a.trim(); - b = b.trim(); - if (/^\//.test(b)) { - return b; - } else { - return a.replace(/\/*$/, '/') + b; - } - } - - function dirname(path) { - var bits = path.split('/'); - bits.pop(); - return bits.join('/'); - } - - /** - * Lookup module names and resolve them by calling the definition function if - * needed. - * @param {string} moduleName A name for the payload to lookup - * @return {object} The module specified by aModuleName or null if not found. - */ - Domain.prototype.lookup = function(moduleName) { - if (/^\./.test(moduleName)) { - moduleName = normalize(join(dirname(this._currentModule), moduleName)); - } - - if (moduleName in this.modules) { - var module = this.modules[moduleName]; - return module; - } - - if (!(moduleName in define.modules)) { - throw new Error("Module not defined: " + moduleName); - } - - var module = define.modules[moduleName]; - - if (typeof module == "function") { - var exports = {}; - var previousModule = this._currentModule; - this._currentModule = moduleName; - module(this.require.bind(this), exports, { id: moduleName, uri: "" }); - this._currentModule = previousModule; - module = exports; - } - - // cache the resulting module object for next time - this.modules[moduleName] = module; - - return module; - }; - -}()); - -define.Domain = Domain; -define.globalDomain = new Domain(); -var require = define.globalDomain.require.bind(define.globalDomain); -/* -*- Mode: js; js-indent-level: 2; -*- */ -/* - * Copyright 2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE or: - * http://opensource.org/licenses/BSD-3-Clause - */ -define('source-map/source-map-generator', ['require', 'exports', 'module' , 'source-map/base64-vlq', 'source-map/util', 'source-map/array-set'], function(require, exports, module) { - - var base64VLQ = require('./base64-vlq'); - var util = require('./util'); - var ArraySet = require('./array-set').ArraySet; - - /** - * An instance of the SourceMapGenerator represents a source map which is - * being built incrementally. To create a new one, you must pass an object - * with the following properties: - * - * - file: The filename of the generated source. - * - sourceRoot: An optional root for all URLs in this source map. - */ - function SourceMapGenerator(aArgs) { - this._file = util.getArg(aArgs, 'file'); - this._sourceRoot = util.getArg(aArgs, 'sourceRoot', null); - this._sources = new ArraySet(); - this._names = new ArraySet(); - this._mappings = []; - this._sourcesContents = null; - } - - SourceMapGenerator.prototype._version = 3; - - /** - * Creates a new SourceMapGenerator based on a SourceMapConsumer - * - * @param aSourceMapConsumer The SourceMap. - */ - SourceMapGenerator.fromSourceMap = - function SourceMapGenerator_fromSourceMap(aSourceMapConsumer) { - var sourceRoot = aSourceMapConsumer.sourceRoot; - var generator = new SourceMapGenerator({ - file: aSourceMapConsumer.file, - sourceRoot: sourceRoot - }); - aSourceMapConsumer.eachMapping(function (mapping) { - var newMapping = { - generated: { - line: mapping.generatedLine, - column: mapping.generatedColumn - } - }; - - if (mapping.source) { - newMapping.source = mapping.source; - if (sourceRoot) { - newMapping.source = util.relative(sourceRoot, newMapping.source); - } - - newMapping.original = { - line: mapping.originalLine, - column: mapping.originalColumn - }; - - if (mapping.name) { - newMapping.name = mapping.name; - } - } - - generator.addMapping(newMapping); - }); - aSourceMapConsumer.sources.forEach(function (sourceFile) { - var content = aSourceMapConsumer.sourceContentFor(sourceFile); - if (content) { - generator.setSourceContent(sourceFile, content); - } - }); - return generator; - }; - - /** - * Add a single mapping from original source line and column to the generated - * source's line and column for this source map being created. The mapping - * object should have the following properties: - * - * - generated: An object with the generated line and column positions. - * - original: An object with the original line and column positions. - * - source: The original source file (relative to the sourceRoot). - * - name: An optional original token name for this mapping. - */ - SourceMapGenerator.prototype.addMapping = - function SourceMapGenerator_addMapping(aArgs) { - var generated = util.getArg(aArgs, 'generated'); - var original = util.getArg(aArgs, 'original', null); - var source = util.getArg(aArgs, 'source', null); - var name = util.getArg(aArgs, 'name', null); - - this._validateMapping(generated, original, source, name); - - if (source && !this._sources.has(source)) { - this._sources.add(source); - } - - if (name && !this._names.has(name)) { - this._names.add(name); - } - - this._mappings.push({ - generatedLine: generated.line, - generatedColumn: generated.column, - originalLine: original != null && original.line, - originalColumn: original != null && original.column, - source: source, - name: name - }); - }; - - /** - * Set the source content for a source file. - */ - SourceMapGenerator.prototype.setSourceContent = - function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) { - var source = aSourceFile; - if (this._sourceRoot) { - source = util.relative(this._sourceRoot, source); - } - - if (aSourceContent !== null) { - // Add the source content to the _sourcesContents map. - // Create a new _sourcesContents map if the property is null. - if (!this._sourcesContents) { - this._sourcesContents = {}; - } - this._sourcesContents[util.toSetString(source)] = aSourceContent; - } else { - // Remove the source file from the _sourcesContents map. - // If the _sourcesContents map is empty, set the property to null. - delete this._sourcesContents[util.toSetString(source)]; - if (Object.keys(this._sourcesContents).length === 0) { - this._sourcesContents = null; - } - } - }; - - /** - * Applies the mappings of a sub-source-map for a specific source file to the - * source map being generated. Each mapping to the supplied source file is - * rewritten using the supplied source map. Note: The resolution for the - * resulting mappings is the minimum of this map and the supplied map. - * - * @param aSourceMapConsumer The source map to be applied. - * @param aSourceFile Optional. The filename of the source file. - * If omitted, SourceMapConsumer's file property will be used. - */ - SourceMapGenerator.prototype.applySourceMap = - function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile) { - // If aSourceFile is omitted, we will use the file property of the SourceMap - if (!aSourceFile) { - aSourceFile = aSourceMapConsumer.file; - } - var sourceRoot = this._sourceRoot; - // Make "aSourceFile" relative if an absolute Url is passed. - if (sourceRoot) { - aSourceFile = util.relative(sourceRoot, aSourceFile); - } - // Applying the SourceMap can add and remove items from the sources and - // the names array. - var newSources = new ArraySet(); - var newNames = new ArraySet(); - - // Find mappings for the "aSourceFile" - this._mappings.forEach(function (mapping) { - if (mapping.source === aSourceFile && mapping.originalLine) { - // Check if it can be mapped by the source map, then update the mapping. - var original = aSourceMapConsumer.originalPositionFor({ - line: mapping.originalLine, - column: mapping.originalColumn - }); - if (original.source !== null) { - // Copy mapping - if (sourceRoot) { - mapping.source = util.relative(sourceRoot, original.source); - } else { - mapping.source = original.source; - } - mapping.originalLine = original.line; - mapping.originalColumn = original.column; - if (original.name !== null && mapping.name !== null) { - // Only use the identifier name if it's an identifier - // in both SourceMaps - mapping.name = original.name; - } - } - } - - var source = mapping.source; - if (source && !newSources.has(source)) { - newSources.add(source); - } - - var name = mapping.name; - if (name && !newNames.has(name)) { - newNames.add(name); - } - - }, this); - this._sources = newSources; - this._names = newNames; - - // Copy sourcesContents of applied map. - aSourceMapConsumer.sources.forEach(function (sourceFile) { - var content = aSourceMapConsumer.sourceContentFor(sourceFile); - if (content) { - if (sourceRoot) { - sourceFile = util.relative(sourceRoot, sourceFile); - } - this.setSourceContent(sourceFile, content); - } - }, this); - }; - - /** - * A mapping can have one of the three levels of data: - * - * 1. Just the generated position. - * 2. The Generated position, original position, and original source. - * 3. Generated and original position, original source, as well as a name - * token. - * - * To maintain consistency, we validate that any new mapping being added falls - * in to one of these categories. - */ - SourceMapGenerator.prototype._validateMapping = - function SourceMapGenerator_validateMapping(aGenerated, aOriginal, aSource, - aName) { - if (aGenerated && 'line' in aGenerated && 'column' in aGenerated - && aGenerated.line > 0 && aGenerated.column >= 0 - && !aOriginal && !aSource && !aName) { - // Case 1. - return; - } - else if (aGenerated && 'line' in aGenerated && 'column' in aGenerated - && aOriginal && 'line' in aOriginal && 'column' in aOriginal - && aGenerated.line > 0 && aGenerated.column >= 0 - && aOriginal.line > 0 && aOriginal.column >= 0 - && aSource) { - // Cases 2 and 3. - return; - } - else { - throw new Error('Invalid mapping: ' + JSON.stringify({ - generated: aGenerated, - source: aSource, - orginal: aOriginal, - name: aName - })); - } - }; - - /** - * Serialize the accumulated mappings in to the stream of base 64 VLQs - * specified by the source map format. - */ - SourceMapGenerator.prototype._serializeMappings = - function SourceMapGenerator_serializeMappings() { - var previousGeneratedColumn = 0; - var previousGeneratedLine = 1; - var previousOriginalColumn = 0; - var previousOriginalLine = 0; - var previousName = 0; - var previousSource = 0; - var result = ''; - var mapping; - - // The mappings must be guaranteed to be in sorted order before we start - // serializing them or else the generated line numbers (which are defined - // via the ';' separators) will be all messed up. Note: it might be more - // performant to maintain the sorting as we insert them, rather than as we - // serialize them, but the big O is the same either way. - this._mappings.sort(util.compareByGeneratedPositions); - - for (var i = 0, len = this._mappings.length; i < len; i++) { - mapping = this._mappings[i]; - - if (mapping.generatedLine !== previousGeneratedLine) { - previousGeneratedColumn = 0; - while (mapping.generatedLine !== previousGeneratedLine) { - result += ';'; - previousGeneratedLine++; - } - } - else { - if (i > 0) { - if (!util.compareByGeneratedPositions(mapping, this._mappings[i - 1])) { - continue; - } - result += ','; - } - } - - result += base64VLQ.encode(mapping.generatedColumn - - previousGeneratedColumn); - previousGeneratedColumn = mapping.generatedColumn; - - if (mapping.source) { - result += base64VLQ.encode(this._sources.indexOf(mapping.source) - - previousSource); - previousSource = this._sources.indexOf(mapping.source); - - // lines are stored 0-based in SourceMap spec version 3 - result += base64VLQ.encode(mapping.originalLine - 1 - - previousOriginalLine); - previousOriginalLine = mapping.originalLine - 1; - - result += base64VLQ.encode(mapping.originalColumn - - previousOriginalColumn); - previousOriginalColumn = mapping.originalColumn; - - if (mapping.name) { - result += base64VLQ.encode(this._names.indexOf(mapping.name) - - previousName); - previousName = this._names.indexOf(mapping.name); - } - } - } - - return result; - }; - - SourceMapGenerator.prototype._generateSourcesContent = - function SourceMapGenerator_generateSourcesContent(aSources, aSourceRoot) { - return aSources.map(function (source) { - if (!this._sourcesContents) { - return null; - } - if (aSourceRoot) { - source = util.relative(aSourceRoot, source); - } - var key = util.toSetString(source); - return Object.prototype.hasOwnProperty.call(this._sourcesContents, - key) - ? this._sourcesContents[key] - : null; - }, this); - }; - - /** - * Externalize the source map. - */ - SourceMapGenerator.prototype.toJSON = - function SourceMapGenerator_toJSON() { - var map = { - version: this._version, - file: this._file, - sources: this._sources.toArray(), - names: this._names.toArray(), - mappings: this._serializeMappings() - }; - if (this._sourceRoot) { - map.sourceRoot = this._sourceRoot; - } - if (this._sourcesContents) { - map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot); - } - - return map; - }; - - /** - * Render the source map being generated to a string. - */ - SourceMapGenerator.prototype.toString = - function SourceMapGenerator_toString() { - return JSON.stringify(this); - }; - - exports.SourceMapGenerator = SourceMapGenerator; - -}); -/* -*- Mode: js; js-indent-level: 2; -*- */ -/* - * Copyright 2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE or: - * http://opensource.org/licenses/BSD-3-Clause - * - * Based on the Base 64 VLQ implementation in Closure Compiler: - * https://code.google.com/p/closure-compiler/source/browse/trunk/src/com/google/debugging/sourcemap/Base64VLQ.java - * - * Copyright 2011 The Closure Compiler Authors. All rights reserved. - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * * Neither the name of Google Inc. nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -define('source-map/base64-vlq', ['require', 'exports', 'module' , 'source-map/base64'], function(require, exports, module) { - - var base64 = require('./base64'); - - // A single base 64 digit can contain 6 bits of data. For the base 64 variable - // length quantities we use in the source map spec, the first bit is the sign, - // the next four bits are the actual value, and the 6th bit is the - // continuation bit. The continuation bit tells us whether there are more - // digits in this value following this digit. - // - // Continuation - // | Sign - // | | - // V V - // 101011 - - var VLQ_BASE_SHIFT = 5; - - // binary: 100000 - var VLQ_BASE = 1 << VLQ_BASE_SHIFT; - - // binary: 011111 - var VLQ_BASE_MASK = VLQ_BASE - 1; - - // binary: 100000 - var VLQ_CONTINUATION_BIT = VLQ_BASE; - - /** - * Converts from a two-complement value to a value where the sign bit is - * is placed in the least significant bit. For example, as decimals: - * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary) - * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary) - */ - function toVLQSigned(aValue) { - return aValue < 0 - ? ((-aValue) << 1) + 1 - : (aValue << 1) + 0; - } - - /** - * Converts to a two-complement value from a value where the sign bit is - * is placed in the least significant bit. For example, as decimals: - * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1 - * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2 - */ - function fromVLQSigned(aValue) { - var isNegative = (aValue & 1) === 1; - var shifted = aValue >> 1; - return isNegative - ? -shifted - : shifted; - } - - /** - * Returns the base 64 VLQ encoded value. - */ - exports.encode = function base64VLQ_encode(aValue) { - var encoded = ""; - var digit; - - var vlq = toVLQSigned(aValue); - - do { - digit = vlq & VLQ_BASE_MASK; - vlq >>>= VLQ_BASE_SHIFT; - if (vlq > 0) { - // There are still more digits in this value, so we must make sure the - // continuation bit is marked. - digit |= VLQ_CONTINUATION_BIT; - } - encoded += base64.encode(digit); - } while (vlq > 0); - - return encoded; - }; - - /** - * Decodes the next base 64 VLQ value from the given string and returns the - * value and the rest of the string. - */ - exports.decode = function base64VLQ_decode(aStr) { - var i = 0; - var strLen = aStr.length; - var result = 0; - var shift = 0; - var continuation, digit; - - do { - if (i >= strLen) { - throw new Error("Expected more digits in base 64 VLQ value."); - } - digit = base64.decode(aStr.charAt(i++)); - continuation = !!(digit & VLQ_CONTINUATION_BIT); - digit &= VLQ_BASE_MASK; - result = result + (digit << shift); - shift += VLQ_BASE_SHIFT; - } while (continuation); - - return { - value: fromVLQSigned(result), - rest: aStr.slice(i) - }; - }; - -}); -/* -*- Mode: js; js-indent-level: 2; -*- */ -/* - * Copyright 2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE or: - * http://opensource.org/licenses/BSD-3-Clause - */ -define('source-map/base64', ['require', 'exports', 'module' , ], function(require, exports, module) { - - var charToIntMap = {}; - var intToCharMap = {}; - - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' - .split('') - .forEach(function (ch, index) { - charToIntMap[ch] = index; - intToCharMap[index] = ch; - }); - - /** - * Encode an integer in the range of 0 to 63 to a single base 64 digit. - */ - exports.encode = function base64_encode(aNumber) { - if (aNumber in intToCharMap) { - return intToCharMap[aNumber]; - } - throw new TypeError("Must be between 0 and 63: " + aNumber); - }; - - /** - * Decode a single base 64 digit to an integer. - */ - exports.decode = function base64_decode(aChar) { - if (aChar in charToIntMap) { - return charToIntMap[aChar]; - } - throw new TypeError("Not a valid base 64 digit: " + aChar); - }; - -}); -/* -*- Mode: js; js-indent-level: 2; -*- */ -/* - * Copyright 2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE or: - * http://opensource.org/licenses/BSD-3-Clause - */ -define('source-map/util', ['require', 'exports', 'module' , ], function(require, exports, module) { - - /** - * This is a helper function for getting values from parameter/options - * objects. - * - * @param args The object we are extracting values from - * @param name The name of the property we are getting. - * @param defaultValue An optional value to return if the property is missing - * from the object. If this is not specified and the property is missing, an - * error will be thrown. - */ - function getArg(aArgs, aName, aDefaultValue) { - if (aName in aArgs) { - return aArgs[aName]; - } else if (arguments.length === 3) { - return aDefaultValue; - } else { - throw new Error('"' + aName + '" is a required argument.'); - } - } - exports.getArg = getArg; - - var urlRegexp = /([\w+\-.]+):\/\/((\w+:\w+)@)?([\w.]+)?(:(\d+))?(\S+)?/; - var dataUrlRegexp = /^data:.+\,.+/; - - function urlParse(aUrl) { - var match = aUrl.match(urlRegexp); - if (!match) { - return null; - } - return { - scheme: match[1], - auth: match[3], - host: match[4], - port: match[6], - path: match[7] - }; - } - exports.urlParse = urlParse; - - function urlGenerate(aParsedUrl) { - var url = aParsedUrl.scheme + "://"; - if (aParsedUrl.auth) { - url += aParsedUrl.auth + "@" - } - if (aParsedUrl.host) { - url += aParsedUrl.host; - } - if (aParsedUrl.port) { - url += ":" + aParsedUrl.port - } - if (aParsedUrl.path) { - url += aParsedUrl.path; - } - return url; - } - exports.urlGenerate = urlGenerate; - - function join(aRoot, aPath) { - var url; - - if (aPath.match(urlRegexp) || aPath.match(dataUrlRegexp)) { - return aPath; - } - - if (aPath.charAt(0) === '/' && (url = urlParse(aRoot))) { - url.path = aPath; - return urlGenerate(url); - } - - return aRoot.replace(/\/$/, '') + '/' + aPath; - } - exports.join = join; - - /** - * Because behavior goes wacky when you set `__proto__` on objects, we - * have to prefix all the strings in our set with an arbitrary character. - * - * See https://github.com/mozilla/source-map/pull/31 and - * https://github.com/mozilla/source-map/issues/30 - * - * @param String aStr - */ - function toSetString(aStr) { - return '$' + aStr; - } - exports.toSetString = toSetString; - - function fromSetString(aStr) { - return aStr.substr(1); - } - exports.fromSetString = fromSetString; - - function relative(aRoot, aPath) { - aRoot = aRoot.replace(/\/$/, ''); - - var url = urlParse(aRoot); - if (aPath.charAt(0) == "/" && url && url.path == "/") { - return aPath.slice(1); - } - - return aPath.indexOf(aRoot + '/') === 0 - ? aPath.substr(aRoot.length + 1) - : aPath; - } - exports.relative = relative; - - function strcmp(aStr1, aStr2) { - var s1 = aStr1 || ""; - var s2 = aStr2 || ""; - return (s1 > s2) - (s1 < s2); - } - - /** - * Comparator between two mappings where the original positions are compared. - * - * Optionally pass in `true` as `onlyCompareGenerated` to consider two - * mappings with the same original source/line/column, but different generated - * line and column the same. Useful when searching for a mapping with a - * stubbed out mapping. - */ - function compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) { - var cmp; - - cmp = strcmp(mappingA.source, mappingB.source); - if (cmp) { - return cmp; - } - - cmp = mappingA.originalLine - mappingB.originalLine; - if (cmp) { - return cmp; - } - - cmp = mappingA.originalColumn - mappingB.originalColumn; - if (cmp || onlyCompareOriginal) { - return cmp; - } - - cmp = strcmp(mappingA.name, mappingB.name); - if (cmp) { - return cmp; - } - - cmp = mappingA.generatedLine - mappingB.generatedLine; - if (cmp) { - return cmp; - } - - return mappingA.generatedColumn - mappingB.generatedColumn; - }; - exports.compareByOriginalPositions = compareByOriginalPositions; - - /** - * Comparator between two mappings where the generated positions are - * compared. - * - * Optionally pass in `true` as `onlyCompareGenerated` to consider two - * mappings with the same generated line and column, but different - * source/name/original line and column the same. Useful when searching for a - * mapping with a stubbed out mapping. - */ - function compareByGeneratedPositions(mappingA, mappingB, onlyCompareGenerated) { - var cmp; - - cmp = mappingA.generatedLine - mappingB.generatedLine; - if (cmp) { - return cmp; - } - - cmp = mappingA.generatedColumn - mappingB.generatedColumn; - if (cmp || onlyCompareGenerated) { - return cmp; - } - - cmp = strcmp(mappingA.source, mappingB.source); - if (cmp) { - return cmp; - } - - cmp = mappingA.originalLine - mappingB.originalLine; - if (cmp) { - return cmp; - } - - cmp = mappingA.originalColumn - mappingB.originalColumn; - if (cmp) { - return cmp; - } - - return strcmp(mappingA.name, mappingB.name); - }; - exports.compareByGeneratedPositions = compareByGeneratedPositions; - -}); -/* -*- Mode: js; js-indent-level: 2; -*- */ -/* - * Copyright 2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE or: - * http://opensource.org/licenses/BSD-3-Clause - */ -define('source-map/array-set', ['require', 'exports', 'module' , 'source-map/util'], function(require, exports, module) { - - var util = require('./util'); - - /** - * A data structure which is a combination of an array and a set. Adding a new - * member is O(1), testing for membership is O(1), and finding the index of an - * element is O(1). Removing elements from the set is not supported. Only - * strings are supported for membership. - */ - function ArraySet() { - this._array = []; - this._set = {}; - } - - /** - * Static method for creating ArraySet instances from an existing array. - */ - ArraySet.fromArray = function ArraySet_fromArray(aArray, aAllowDuplicates) { - var set = new ArraySet(); - for (var i = 0, len = aArray.length; i < len; i++) { - set.add(aArray[i], aAllowDuplicates); - } - return set; - }; - - /** - * Add the given string to this set. - * - * @param String aStr - */ - ArraySet.prototype.add = function ArraySet_add(aStr, aAllowDuplicates) { - var isDuplicate = this.has(aStr); - var idx = this._array.length; - if (!isDuplicate || aAllowDuplicates) { - this._array.push(aStr); - } - if (!isDuplicate) { - this._set[util.toSetString(aStr)] = idx; - } - }; - - /** - * Is the given string a member of this set? - * - * @param String aStr - */ - ArraySet.prototype.has = function ArraySet_has(aStr) { - return Object.prototype.hasOwnProperty.call(this._set, - util.toSetString(aStr)); - }; - - /** - * What is the index of the given string in the array? - * - * @param String aStr - */ - ArraySet.prototype.indexOf = function ArraySet_indexOf(aStr) { - if (this.has(aStr)) { - return this._set[util.toSetString(aStr)]; - } - throw new Error('"' + aStr + '" is not in the set.'); - }; - - /** - * What is the element at the given index? - * - * @param Number aIdx - */ - ArraySet.prototype.at = function ArraySet_at(aIdx) { - if (aIdx >= 0 && aIdx < this._array.length) { - return this._array[aIdx]; - } - throw new Error('No element indexed by ' + aIdx); - }; - - /** - * Returns the array representation of this set (which has the proper indices - * indicated by indexOf). Note that this is a copy of the internal array used - * for storing the members so that no one can mess with internal state. - */ - ArraySet.prototype.toArray = function ArraySet_toArray() { - return this._array.slice(); - }; - - exports.ArraySet = ArraySet; - -}); -/* -*- Mode: js; js-indent-level: 2; -*- */ -/* - * Copyright 2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE or: - * http://opensource.org/licenses/BSD-3-Clause - */ -define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'source-map/util', 'source-map/binary-search', 'source-map/array-set', 'source-map/base64-vlq'], function(require, exports, module) { - - var util = require('./util'); - var binarySearch = require('./binary-search'); - var ArraySet = require('./array-set').ArraySet; - var base64VLQ = require('./base64-vlq'); - - /** - * A SourceMapConsumer instance represents a parsed source map which we can - * query for information about the original file positions by giving it a file - * position in the generated source. - * - * The only parameter is the raw source map (either as a JSON string, or - * already parsed to an object). According to the spec, source maps have the - * following attributes: - * - * - version: Which version of the source map spec this map is following. - * - sources: An array of URLs to the original source files. - * - names: An array of identifiers which can be referenced by individual mappings. - * - sourceRoot: Optional. The URL root from which all sources are relative. - * - sourcesContent: Optional. An array of contents of the original source files. - * - mappings: A string of base64 VLQs which contain the actual mappings. - * - file: The generated file this source map is associated with. - * - * Here is an example source map, taken from the source map spec[0]: - * - * { - * version : 3, - * file: "out.js", - * sourceRoot : "", - * sources: ["foo.js", "bar.js"], - * names: ["src", "maps", "are", "fun"], - * mappings: "AA,AB;;ABCDE;" - * } - * - * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1# - */ - function SourceMapConsumer(aSourceMap) { - var sourceMap = aSourceMap; - if (typeof aSourceMap === 'string') { - sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, '')); - } - - var version = util.getArg(sourceMap, 'version'); - var sources = util.getArg(sourceMap, 'sources'); - // Sass 3.3 leaves out the 'names' array, so we deviate from the spec (which - // requires the array) to play nice here. - var names = util.getArg(sourceMap, 'names', []); - var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null); - var sourcesContent = util.getArg(sourceMap, 'sourcesContent', null); - var mappings = util.getArg(sourceMap, 'mappings'); - var file = util.getArg(sourceMap, 'file', null); - - // Once again, Sass deviates from the spec and supplies the version as a - // string rather than a number, so we use loose equality checking here. - if (version != this._version) { - throw new Error('Unsupported version: ' + version); - } - - // Pass `true` below to allow duplicate names and sources. While source maps - // are intended to be compressed and deduplicated, the TypeScript compiler - // sometimes generates source maps with duplicates in them. See Github issue - // #72 and bugzil.la/889492. - this._names = ArraySet.fromArray(names, true); - this._sources = ArraySet.fromArray(sources, true); - - this.sourceRoot = sourceRoot; - this.sourcesContent = sourcesContent; - this._mappings = mappings; - this.file = file; - } - - /** - * Create a SourceMapConsumer from a SourceMapGenerator. - * - * @param SourceMapGenerator aSourceMap - * The source map that will be consumed. - * @returns SourceMapConsumer - */ - SourceMapConsumer.fromSourceMap = - function SourceMapConsumer_fromSourceMap(aSourceMap) { - var smc = Object.create(SourceMapConsumer.prototype); - - smc._names = ArraySet.fromArray(aSourceMap._names.toArray(), true); - smc._sources = ArraySet.fromArray(aSourceMap._sources.toArray(), true); - smc.sourceRoot = aSourceMap._sourceRoot; - smc.sourcesContent = aSourceMap._generateSourcesContent(smc._sources.toArray(), - smc.sourceRoot); - smc.file = aSourceMap._file; - - smc.__generatedMappings = aSourceMap._mappings.slice() - .sort(util.compareByGeneratedPositions); - smc.__originalMappings = aSourceMap._mappings.slice() - .sort(util.compareByOriginalPositions); - - return smc; - }; - - /** - * The version of the source mapping spec that we are consuming. - */ - SourceMapConsumer.prototype._version = 3; - - /** - * The list of original sources. - */ - Object.defineProperty(SourceMapConsumer.prototype, 'sources', { - get: function () { - return this._sources.toArray().map(function (s) { - return this.sourceRoot ? util.join(this.sourceRoot, s) : s; - }, this); - } - }); - - // `__generatedMappings` and `__originalMappings` are arrays that hold the - // parsed mapping coordinates from the source map's "mappings" attribute. They - // are lazily instantiated, accessed via the `_generatedMappings` and - // `_originalMappings` getters respectively, and we only parse the mappings - // and create these arrays once queried for a source location. We jump through - // these hoops because there can be many thousands of mappings, and parsing - // them is expensive, so we only want to do it if we must. - // - // Each object in the arrays is of the form: - // - // { - // generatedLine: The line number in the generated code, - // generatedColumn: The column number in the generated code, - // source: The path to the original source file that generated this - // chunk of code, - // originalLine: The line number in the original source that - // corresponds to this chunk of generated code, - // originalColumn: The column number in the original source that - // corresponds to this chunk of generated code, - // name: The name of the original symbol which generated this chunk of - // code. - // } - // - // All properties except for `generatedLine` and `generatedColumn` can be - // `null`. - // - // `_generatedMappings` is ordered by the generated positions. - // - // `_originalMappings` is ordered by the original positions. - - SourceMapConsumer.prototype.__generatedMappings = null; - Object.defineProperty(SourceMapConsumer.prototype, '_generatedMappings', { - get: function () { - if (!this.__generatedMappings) { - this.__generatedMappings = []; - this.__originalMappings = []; - this._parseMappings(this._mappings, this.sourceRoot); - } - - return this.__generatedMappings; - } - }); - - SourceMapConsumer.prototype.__originalMappings = null; - Object.defineProperty(SourceMapConsumer.prototype, '_originalMappings', { - get: function () { - if (!this.__originalMappings) { - this.__generatedMappings = []; - this.__originalMappings = []; - this._parseMappings(this._mappings, this.sourceRoot); - } - - return this.__originalMappings; - } - }); - - /** - * Parse the mappings in a string in to a data structure which we can easily - * query (the ordered arrays in the `this.__generatedMappings` and - * `this.__originalMappings` properties). - */ - SourceMapConsumer.prototype._parseMappings = - function SourceMapConsumer_parseMappings(aStr, aSourceRoot) { - var generatedLine = 1; - var previousGeneratedColumn = 0; - var previousOriginalLine = 0; - var previousOriginalColumn = 0; - var previousSource = 0; - var previousName = 0; - var mappingSeparator = /^[,;]/; - var str = aStr; - var mapping; - var temp; - - while (str.length > 0) { - if (str.charAt(0) === ';') { - generatedLine++; - str = str.slice(1); - previousGeneratedColumn = 0; - } - else if (str.charAt(0) === ',') { - str = str.slice(1); - } - else { - mapping = {}; - mapping.generatedLine = generatedLine; - - // Generated column. - temp = base64VLQ.decode(str); - mapping.generatedColumn = previousGeneratedColumn + temp.value; - previousGeneratedColumn = mapping.generatedColumn; - str = temp.rest; - - if (str.length > 0 && !mappingSeparator.test(str.charAt(0))) { - // Original source. - temp = base64VLQ.decode(str); - mapping.source = this._sources.at(previousSource + temp.value); - previousSource += temp.value; - str = temp.rest; - if (str.length === 0 || mappingSeparator.test(str.charAt(0))) { - throw new Error('Found a source, but no line and column'); - } - - // Original line. - temp = base64VLQ.decode(str); - mapping.originalLine = previousOriginalLine + temp.value; - previousOriginalLine = mapping.originalLine; - // Lines are stored 0-based - mapping.originalLine += 1; - str = temp.rest; - if (str.length === 0 || mappingSeparator.test(str.charAt(0))) { - throw new Error('Found a source and line, but no column'); - } - - // Original column. - temp = base64VLQ.decode(str); - mapping.originalColumn = previousOriginalColumn + temp.value; - previousOriginalColumn = mapping.originalColumn; - str = temp.rest; - - if (str.length > 0 && !mappingSeparator.test(str.charAt(0))) { - // Original name. - temp = base64VLQ.decode(str); - mapping.name = this._names.at(previousName + temp.value); - previousName += temp.value; - str = temp.rest; - } - } - - this.__generatedMappings.push(mapping); - if (typeof mapping.originalLine === 'number') { - this.__originalMappings.push(mapping); - } - } - } - - this.__originalMappings.sort(util.compareByOriginalPositions); - }; - - /** - * Find the mapping that best matches the hypothetical "needle" mapping that - * we are searching for in the given "haystack" of mappings. - */ - SourceMapConsumer.prototype._findMapping = - function SourceMapConsumer_findMapping(aNeedle, aMappings, aLineName, - aColumnName, aComparator) { - // To return the position we are searching for, we must first find the - // mapping for the given position and then return the opposite position it - // points to. Because the mappings are sorted, we can use binary search to - // find the best mapping. - - if (aNeedle[aLineName] <= 0) { - throw new TypeError('Line must be greater than or equal to 1, got ' - + aNeedle[aLineName]); - } - if (aNeedle[aColumnName] < 0) { - throw new TypeError('Column must be greater than or equal to 0, got ' - + aNeedle[aColumnName]); - } - - return binarySearch.search(aNeedle, aMappings, aComparator); - }; - - /** - * Returns the original source, line, and column information for the generated - * source's line and column positions provided. The only argument is an object - * with the following properties: - * - * - line: The line number in the generated source. - * - column: The column number in the generated source. - * - * and an object is returned with the following properties: - * - * - source: The original source file, or null. - * - line: The line number in the original source, or null. - * - column: The column number in the original source, or null. - * - name: The original identifier, or null. - */ - SourceMapConsumer.prototype.originalPositionFor = - function SourceMapConsumer_originalPositionFor(aArgs) { - var needle = { - generatedLine: util.getArg(aArgs, 'line'), - generatedColumn: util.getArg(aArgs, 'column') - }; - - var mapping = this._findMapping(needle, - this._generatedMappings, - "generatedLine", - "generatedColumn", - util.compareByGeneratedPositions); - - if (mapping) { - var source = util.getArg(mapping, 'source', null); - if (source && this.sourceRoot) { - source = util.join(this.sourceRoot, source); - } - return { - source: source, - line: util.getArg(mapping, 'originalLine', null), - column: util.getArg(mapping, 'originalColumn', null), - name: util.getArg(mapping, 'name', null) - }; - } - - return { - source: null, - line: null, - column: null, - name: null - }; - }; - - /** - * Returns the original source content. The only argument is the url of the - * original source file. Returns null if no original source content is - * available. - */ - SourceMapConsumer.prototype.sourceContentFor = - function SourceMapConsumer_sourceContentFor(aSource) { - if (!this.sourcesContent) { - return null; - } - - if (this.sourceRoot) { - aSource = util.relative(this.sourceRoot, aSource); - } - - if (this._sources.has(aSource)) { - return this.sourcesContent[this._sources.indexOf(aSource)]; - } - - var url; - if (this.sourceRoot - && (url = util.urlParse(this.sourceRoot))) { - // XXX: file:// URIs and absolute paths lead to unexpected behavior for - // many users. We can help them out when they expect file:// URIs to - // behave like it would if they were running a local HTTP server. See - // https://bugzilla.mozilla.org/show_bug.cgi?id=885597. - var fileUriAbsPath = aSource.replace(/^file:\/\//, ""); - if (url.scheme == "file" - && this._sources.has(fileUriAbsPath)) { - return this.sourcesContent[this._sources.indexOf(fileUriAbsPath)] - } - - if ((!url.path || url.path == "/") - && this._sources.has("/" + aSource)) { - return this.sourcesContent[this._sources.indexOf("/" + aSource)]; - } - } - - throw new Error('"' + aSource + '" is not in the SourceMap.'); - }; - - /** - * Returns the generated line and column information for the original source, - * line, and column positions provided. The only argument is an object with - * the following properties: - * - * - source: The filename of the original source. - * - line: The line number in the original source. - * - column: The column number in the original source. - * - * and an object is returned with the following properties: - * - * - line: The line number in the generated source, or null. - * - column: The column number in the generated source, or null. - */ - SourceMapConsumer.prototype.generatedPositionFor = - function SourceMapConsumer_generatedPositionFor(aArgs) { - var needle = { - source: util.getArg(aArgs, 'source'), - originalLine: util.getArg(aArgs, 'line'), - originalColumn: util.getArg(aArgs, 'column') - }; - - if (this.sourceRoot) { - needle.source = util.relative(this.sourceRoot, needle.source); - } - - var mapping = this._findMapping(needle, - this._originalMappings, - "originalLine", - "originalColumn", - util.compareByOriginalPositions); - - if (mapping) { - return { - line: util.getArg(mapping, 'generatedLine', null), - column: util.getArg(mapping, 'generatedColumn', null) - }; - } - - return { - line: null, - column: null - }; - }; - - SourceMapConsumer.GENERATED_ORDER = 1; - SourceMapConsumer.ORIGINAL_ORDER = 2; - - /** - * Iterate over each mapping between an original source/line/column and a - * generated line/column in this source map. - * - * @param Function aCallback - * The function that is called with each mapping. - * @param Object aContext - * Optional. If specified, this object will be the value of `this` every - * time that `aCallback` is called. - * @param aOrder - * Either `SourceMapConsumer.GENERATED_ORDER` or - * `SourceMapConsumer.ORIGINAL_ORDER`. Specifies whether you want to - * iterate over the mappings sorted by the generated file's line/column - * order or the original's source/line/column order, respectively. Defaults to - * `SourceMapConsumer.GENERATED_ORDER`. - */ - SourceMapConsumer.prototype.eachMapping = - function SourceMapConsumer_eachMapping(aCallback, aContext, aOrder) { - var context = aContext || null; - var order = aOrder || SourceMapConsumer.GENERATED_ORDER; - - var mappings; - switch (order) { - case SourceMapConsumer.GENERATED_ORDER: - mappings = this._generatedMappings; - break; - case SourceMapConsumer.ORIGINAL_ORDER: - mappings = this._originalMappings; - break; - default: - throw new Error("Unknown order of iteration."); - } - - var sourceRoot = this.sourceRoot; - mappings.map(function (mapping) { - var source = mapping.source; - if (source && sourceRoot) { - source = util.join(sourceRoot, source); - } - return { - source: source, - generatedLine: mapping.generatedLine, - generatedColumn: mapping.generatedColumn, - originalLine: mapping.originalLine, - originalColumn: mapping.originalColumn, - name: mapping.name - }; - }).forEach(aCallback, context); - }; - - exports.SourceMapConsumer = SourceMapConsumer; - -}); -/* -*- Mode: js; js-indent-level: 2; -*- */ -/* - * Copyright 2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE or: - * http://opensource.org/licenses/BSD-3-Clause - */ -define('source-map/binary-search', ['require', 'exports', 'module' , ], function(require, exports, module) { - - /** - * Recursive implementation of binary search. - * - * @param aLow Indices here and lower do not contain the needle. - * @param aHigh Indices here and higher do not contain the needle. - * @param aNeedle The element being searched for. - * @param aHaystack The non-empty array being searched. - * @param aCompare Function which takes two elements and returns -1, 0, or 1. - */ - function recursiveSearch(aLow, aHigh, aNeedle, aHaystack, aCompare) { - // This function terminates when one of the following is true: - // - // 1. We find the exact element we are looking for. - // - // 2. We did not find the exact element, but we can return the next - // closest element that is less than that element. - // - // 3. We did not find the exact element, and there is no next-closest - // element which is less than the one we are searching for, so we - // return null. - var mid = Math.floor((aHigh - aLow) / 2) + aLow; - var cmp = aCompare(aNeedle, aHaystack[mid], true); - if (cmp === 0) { - // Found the element we are looking for. - return aHaystack[mid]; - } - else if (cmp > 0) { - // aHaystack[mid] is greater than our needle. - if (aHigh - mid > 1) { - // The element is in the upper half. - return recursiveSearch(mid, aHigh, aNeedle, aHaystack, aCompare); - } - // We did not find an exact match, return the next closest one - // (termination case 2). - return aHaystack[mid]; - } - else { - // aHaystack[mid] is less than our needle. - if (mid - aLow > 1) { - // The element is in the lower half. - return recursiveSearch(aLow, mid, aNeedle, aHaystack, aCompare); - } - // The exact needle element was not found in this haystack. Determine if - // we are in termination case (2) or (3) and return the appropriate thing. - return aLow < 0 - ? null - : aHaystack[aLow]; - } - } - - /** - * This is an implementation of binary search which will always try and return - * the next lowest value checked if there is no exact hit. This is because - * mappings between original and generated line/col pairs are single points, - * and there is an implicit region between each of them, so a miss just means - * that you aren't on the very start of a region. - * - * @param aNeedle The element you are looking for. - * @param aHaystack The array that is being searched. - * @param aCompare A function which takes the needle and an element in the - * array and returns -1, 0, or 1 depending on whether the needle is less - * than, equal to, or greater than the element, respectively. - */ - exports.search = function search(aNeedle, aHaystack, aCompare) { - return aHaystack.length > 0 - ? recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack, aCompare) - : null; - }; - -}); -/* -*- Mode: js; js-indent-level: 2; -*- */ -/* - * Copyright 2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE or: - * http://opensource.org/licenses/BSD-3-Clause - */ -define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/source-map-generator', 'source-map/util'], function(require, exports, module) { - - var SourceMapGenerator = require('./source-map-generator').SourceMapGenerator; - var util = require('./util'); - - /** - * SourceNodes provide a way to abstract over interpolating/concatenating - * snippets of generated JavaScript source code while maintaining the line and - * column information associated with the original source code. - * - * @param aLine The original line number. - * @param aColumn The original column number. - * @param aSource The original source's filename. - * @param aChunks Optional. An array of strings which are snippets of - * generated JS, or other SourceNodes. - * @param aName The original identifier. - */ - function SourceNode(aLine, aColumn, aSource, aChunks, aName) { - this.children = []; - this.sourceContents = {}; - this.line = aLine === undefined ? null : aLine; - this.column = aColumn === undefined ? null : aColumn; - this.source = aSource === undefined ? null : aSource; - this.name = aName === undefined ? null : aName; - if (aChunks != null) this.add(aChunks); - } - - /** - * Creates a SourceNode from generated code and a SourceMapConsumer. - * - * @param aGeneratedCode The generated code - * @param aSourceMapConsumer The SourceMap for the generated code - */ - SourceNode.fromStringWithSourceMap = - function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer) { - // The SourceNode we want to fill with the generated code - // and the SourceMap - var node = new SourceNode(); - - // The generated code - // Processed fragments are removed from this array. - var remainingLines = aGeneratedCode.split('\n'); - - // We need to remember the position of "remainingLines" - var lastGeneratedLine = 1, lastGeneratedColumn = 0; - - // The generate SourceNodes we need a code range. - // To extract it current and last mapping is used. - // Here we store the last mapping. - var lastMapping = null; - - aSourceMapConsumer.eachMapping(function (mapping) { - if (lastMapping === null) { - // We add the generated code until the first mapping - // to the SourceNode without any mapping. - // Each line is added as separate string. - while (lastGeneratedLine < mapping.generatedLine) { - node.add(remainingLines.shift() + "\n"); - lastGeneratedLine++; - } - if (lastGeneratedColumn < mapping.generatedColumn) { - var nextLine = remainingLines[0]; - node.add(nextLine.substr(0, mapping.generatedColumn)); - remainingLines[0] = nextLine.substr(mapping.generatedColumn); - lastGeneratedColumn = mapping.generatedColumn; - } - } else { - // We add the code from "lastMapping" to "mapping": - // First check if there is a new line in between. - if (lastGeneratedLine < mapping.generatedLine) { - var code = ""; - // Associate full lines with "lastMapping" - do { - code += remainingLines.shift() + "\n"; - lastGeneratedLine++; - lastGeneratedColumn = 0; - } while (lastGeneratedLine < mapping.generatedLine); - // When we reached the correct line, we add code until we - // reach the correct column too. - if (lastGeneratedColumn < mapping.generatedColumn) { - var nextLine = remainingLines[0]; - code += nextLine.substr(0, mapping.generatedColumn); - remainingLines[0] = nextLine.substr(mapping.generatedColumn); - lastGeneratedColumn = mapping.generatedColumn; - } - // Create the SourceNode. - addMappingWithCode(lastMapping, code); - } else { - // There is no new line in between. - // Associate the code between "lastGeneratedColumn" and - // "mapping.generatedColumn" with "lastMapping" - var nextLine = remainingLines[0]; - var code = nextLine.substr(0, mapping.generatedColumn - - lastGeneratedColumn); - remainingLines[0] = nextLine.substr(mapping.generatedColumn - - lastGeneratedColumn); - lastGeneratedColumn = mapping.generatedColumn; - addMappingWithCode(lastMapping, code); - } - } - lastMapping = mapping; - }, this); - // We have processed all mappings. - // Associate the remaining code in the current line with "lastMapping" - // and add the remaining lines without any mapping - addMappingWithCode(lastMapping, remainingLines.join("\n")); - - // Copy sourcesContent into SourceNode - aSourceMapConsumer.sources.forEach(function (sourceFile) { - var content = aSourceMapConsumer.sourceContentFor(sourceFile); - if (content) { - node.setSourceContent(sourceFile, content); - } - }); - - return node; - - function addMappingWithCode(mapping, code) { - if (mapping === null || mapping.source === undefined) { - node.add(code); - } else { - node.add(new SourceNode(mapping.originalLine, - mapping.originalColumn, - mapping.source, - code, - mapping.name)); - } - } - }; - - /** - * Add a chunk of generated JS to this source node. - * - * @param aChunk A string snippet of generated JS code, another instance of - * SourceNode, or an array where each member is one of those things. - */ - SourceNode.prototype.add = function SourceNode_add(aChunk) { - if (Array.isArray(aChunk)) { - aChunk.forEach(function (chunk) { - this.add(chunk); - }, this); - } - else if (aChunk instanceof SourceNode || typeof aChunk === "string") { - if (aChunk) { - this.children.push(aChunk); - } - } - else { - throw new TypeError( - "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk - ); - } - return this; - }; - - /** - * Add a chunk of generated JS to the beginning of this source node. - * - * @param aChunk A string snippet of generated JS code, another instance of - * SourceNode, or an array where each member is one of those things. - */ - SourceNode.prototype.prepend = function SourceNode_prepend(aChunk) { - if (Array.isArray(aChunk)) { - for (var i = aChunk.length-1; i >= 0; i--) { - this.prepend(aChunk[i]); - } - } - else if (aChunk instanceof SourceNode || typeof aChunk === "string") { - this.children.unshift(aChunk); - } - else { - throw new TypeError( - "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk - ); - } - return this; - }; - - /** - * Walk over the tree of JS snippets in this node and its children. The - * walking function is called once for each snippet of JS and is passed that - * snippet and the its original associated source's line/column location. - * - * @param aFn The traversal function. - */ - SourceNode.prototype.walk = function SourceNode_walk(aFn) { - var chunk; - for (var i = 0, len = this.children.length; i < len; i++) { - chunk = this.children[i]; - if (chunk instanceof SourceNode) { - chunk.walk(aFn); - } - else { - if (chunk !== '') { - aFn(chunk, { source: this.source, - line: this.line, - column: this.column, - name: this.name }); - } - } - } - }; - - /** - * Like `String.prototype.join` except for SourceNodes. Inserts `aStr` between - * each of `this.children`. - * - * @param aSep The separator. - */ - SourceNode.prototype.join = function SourceNode_join(aSep) { - var newChildren; - var i; - var len = this.children.length; - if (len > 0) { - newChildren = []; - for (i = 0; i < len-1; i++) { - newChildren.push(this.children[i]); - newChildren.push(aSep); - } - newChildren.push(this.children[i]); - this.children = newChildren; - } - return this; - }; - - /** - * Call String.prototype.replace on the very right-most source snippet. Useful - * for trimming whitespace from the end of a source node, etc. - * - * @param aPattern The pattern to replace. - * @param aReplacement The thing to replace the pattern with. - */ - SourceNode.prototype.replaceRight = function SourceNode_replaceRight(aPattern, aReplacement) { - var lastChild = this.children[this.children.length - 1]; - if (lastChild instanceof SourceNode) { - lastChild.replaceRight(aPattern, aReplacement); - } - else if (typeof lastChild === 'string') { - this.children[this.children.length - 1] = lastChild.replace(aPattern, aReplacement); - } - else { - this.children.push(''.replace(aPattern, aReplacement)); - } - return this; - }; - - /** - * Set the source content for a source file. This will be added to the SourceMapGenerator - * in the sourcesContent field. - * - * @param aSourceFile The filename of the source file - * @param aSourceContent The content of the source file - */ - SourceNode.prototype.setSourceContent = - function SourceNode_setSourceContent(aSourceFile, aSourceContent) { - this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent; - }; - - /** - * Walk over the tree of SourceNodes. The walking function is called for each - * source file content and is passed the filename and source content. - * - * @param aFn The traversal function. - */ - SourceNode.prototype.walkSourceContents = - function SourceNode_walkSourceContents(aFn) { - for (var i = 0, len = this.children.length; i < len; i++) { - if (this.children[i] instanceof SourceNode) { - this.children[i].walkSourceContents(aFn); - } - } - - var sources = Object.keys(this.sourceContents); - for (var i = 0, len = sources.length; i < len; i++) { - aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]); - } - }; - - /** - * Return the string representation of this source node. Walks over the tree - * and concatenates all the various snippets together to one string. - */ - SourceNode.prototype.toString = function SourceNode_toString() { - var str = ""; - this.walk(function (chunk) { - str += chunk; - }); - return str; - }; - - /** - * Returns the string representation of this source node along with a source - * map. - */ - SourceNode.prototype.toStringWithSourceMap = function SourceNode_toStringWithSourceMap(aArgs) { - var generated = { - code: "", - line: 1, - column: 0 - }; - var map = new SourceMapGenerator(aArgs); - var sourceMappingActive = false; - var lastOriginalSource = null; - var lastOriginalLine = null; - var lastOriginalColumn = null; - var lastOriginalName = null; - this.walk(function (chunk, original) { - generated.code += chunk; - if (original.source !== null - && original.line !== null - && original.column !== null) { - if(lastOriginalSource !== original.source - || lastOriginalLine !== original.line - || lastOriginalColumn !== original.column - || lastOriginalName !== original.name) { - map.addMapping({ - source: original.source, - original: { - line: original.line, - column: original.column - }, - generated: { - line: generated.line, - column: generated.column - }, - name: original.name - }); - } - lastOriginalSource = original.source; - lastOriginalLine = original.line; - lastOriginalColumn = original.column; - lastOriginalName = original.name; - sourceMappingActive = true; - } else if (sourceMappingActive) { - map.addMapping({ - generated: { - line: generated.line, - column: generated.column - } - }); - lastOriginalSource = null; - sourceMappingActive = false; - } - chunk.split('').forEach(function (ch) { - if (ch === '\n') { - generated.line++; - generated.column = 0; - } else { - generated.column++; - } - }); - }); - this.walkSourceContents(function (sourceFile, sourceContent) { - map.setSourceContent(sourceFile, sourceContent); - }); - - return { code: generated.code, map: map }; - }; - - exports.SourceNode = SourceNode; - -}); -/* -*- Mode: js; js-indent-level: 2; -*- */ -/////////////////////////////////////////////////////////////////////////////// - -this.sourceMap = { - SourceMapConsumer: require('source-map/source-map-consumer').SourceMapConsumer, - SourceMapGenerator: require('source-map/source-map-generator').SourceMapGenerator, - SourceNode: require('source-map/source-node').SourceNode -}; - -} diff --git a/Libraries/JavaScriptAppEngine/Initialization/SourceMapsCache.js b/Libraries/JavaScriptAppEngine/Initialization/SourceMapsCache.js deleted file mode 100644 index 63ab9c642c119f..00000000000000 --- a/Libraries/JavaScriptAppEngine/Initialization/SourceMapsCache.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule SourceMapsCache - */ -'use strict'; - -const getObjectValues = require('getObjectValues'); -const SourceMapsUtils = require('SourceMapsUtils'); - -const sourceMapsCache = {}; - -const SourceMapsCache = { - mainSourceMapID: 'main', - - fetch({text, url, fullSourceMappingURL}) { - const sourceMappingURL = fullSourceMappingURL - ? fullSourceMappingURL - : SourceMapsUtils.extractSourceMapURL({text, url}); - - sourceMapsCache[sourceMappingURL] = SourceMapsUtils.fetchSourceMap( - sourceMappingURL - ); - }, - - getSourceMaps() { - fetchMainSourceMap(); - return Promise.all(getObjectValues(sourceMapsCache)); - }, -}; - -function fetchMainSourceMap() { - if (!sourceMapsCache[SourceMapsCache.mainSourceMapID]) { - sourceMapsCache[SourceMapsCache.mainSourceMapID] = - SourceMapsUtils.fetchMainSourceMap(); - } -} - -module.exports = SourceMapsCache; diff --git a/Libraries/JavaScriptAppEngine/Initialization/SourceMapsUtils.js b/Libraries/JavaScriptAppEngine/Initialization/SourceMapsUtils.js deleted file mode 100644 index 9e545ba8f53cca..00000000000000 --- a/Libraries/JavaScriptAppEngine/Initialization/SourceMapsUtils.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule SourceMapsUtils - * @flow - */ - -'use strict'; - -const Promise = require('Promise'); -const NativeModules = require('NativeModules'); -const SourceMapConsumer = require('SourceMap').SourceMapConsumer; -const SourceMapURL = require('./source-map-url'); - -const RCTSourceCode = NativeModules.SourceCode; -const RCTNetworking = NativeModules.Networking; - -const SourceMapsUtils = { - fetchMainSourceMap(): Promise { - return SourceMapsUtils._getMainSourceMapURL().then(url => - SourceMapsUtils.fetchSourceMap(url) - ); - }, - - fetchSourceMap(sourceMappingURL: string): Promise { - return fetch(sourceMappingURL) - .then(response => response.text()) - .then(map => new SourceMapConsumer(map)); - }, - - extractSourceMapURL(data: ({url?:string, text?:string, fullSourceMappingURL?:string})): ?string { - const url = data.url; - const text = data.text; - const fullSourceMappingURL = data.fullSourceMappingURL; - if (fullSourceMappingURL) { - return fullSourceMappingURL; - } - const mapURL = SourceMapURL.getFrom(text); - if (!mapURL) { - return null; - } - if (!url) { - return null; - } - const baseURLs = url.match(/(.+:\/\/.*?)\//); - if (!baseURLs || baseURLs.length < 2) { - return null; - } - return baseURLs[1] + mapURL; - }, - - _getMainSourceMapURL(): Promise { - if (global.RAW_SOURCE_MAP) { - return Promise.resolve(global.RAW_SOURCE_MAP); - } - - if (!RCTSourceCode) { - return Promise.reject(new Error('RCTSourceCode module is not available')); - } - - if (!RCTNetworking) { - // Used internally by fetch - return Promise.reject(new Error('RCTNetworking module is not available')); - } - - const scriptText = RCTSourceCode.getScriptText(); - if (scriptText) { - return scriptText - .then(SourceMapsUtils.extractSourceMapURL) - .then((url) => { - if (url === null) { - return Promise.reject(new Error('No source map URL found. May be running from bundled file.')); - } - return Promise.resolve(url); - }); - } else { - // Running in mock-config mode - return Promise.reject(new Error('Couldn\'t fetch script text')); - } - }, -}; - -module.exports = SourceMapsUtils; diff --git a/Libraries/Utilities/HMRClient.js b/Libraries/Utilities/HMRClient.js index c8d68597398a50..a7987f22e647c8 100644 --- a/Libraries/Utilities/HMRClient.js +++ b/Libraries/Utilities/HMRClient.js @@ -98,23 +98,9 @@ Error: ${e.message}` RCTExceptionsManager && RCTExceptionsManager.dismissRedbox && RCTExceptionsManager.dismissRedbox(); } - let serverHost; - - if (Platform.OS === 'android') { - serverHost = require('NativeModules').AndroidConstants.ServerHost; - } else { - serverHost = port ? `${host}:${port}` : host; - } - modules.forEach(({id, code}, i) => { code = code + '\n\n' + sourceMappingURLs[i]; - require('SourceMapsCache').fetch({ - text: code, - url: `http://${serverHost}${sourceURLs[i]}`, - sourceMappingURL: sourceMappingURLs[i], - }); - // on JSC we need to inject from native for sourcemaps to work // (Safari doesn't support `sourceMappingURL` nor any variant when // evaluating code) but on Chrome we can simply use eval From 886a558f75dfa5871b9efe40df91d330df6c0792 Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Wed, 1 Jun 2016 16:16:15 -0700 Subject: [PATCH 185/843] Extract all NativeArray/NativeMap from OnLoad.cpp Summary: These will, eventually, need to be moved to the new bridge and so must become standalone things. For *NativeArray, this is almost just moving them out into their own .h/.cpp files. The *NativeMaps are updated to be hybrids instead of countables (in addition to getting their own .h/.cpp). Reviewed By: mhorowitz Differential Revision: D3325169 fbshipit-source-id: 40cfcab92b3fb2310bcd4de8f39e82f85d404abd --- .../com/facebook/react/bridge/NativeMap.java | 12 +- .../react/bridge/ReadableNativeMap.java | 23 +- .../react/bridge/WritableNativeArray.java | 2 +- .../react/bridge/WritableNativeMap.java | 8 +- .../src/main/jni/react/jni/Android.mk | 8 +- ReactAndroid/src/main/jni/react/jni/BUCK | 13 +- .../src/main/jni/react/jni/NativeArray.cpp | 15 +- .../src/main/jni/react/jni/NativeArray.h | 2 +- .../src/main/jni/react/jni/NativeCommon.cpp | 76 +++ .../src/main/jni/react/jni/NativeCommon.h | 31 + .../src/main/jni/react/jni/NativeMap.cpp | 28 + .../src/main/jni/react/jni/NativeMap.h | 33 + .../src/main/jni/react/jni/OnLoad.cpp | 565 +----------------- .../jni/react/jni/ReadableNativeArray.cpp | 102 ++++ .../main/jni/react/jni/ReadableNativeArray.h | 9 +- .../main/jni/react/jni/ReadableNativeMap.cpp | 150 +++++ .../main/jni/react/jni/ReadableNativeMap.h | 59 ++ .../jni/react/jni/WritableNativeArray.cpp | 83 +++ .../main/jni/react/jni/WritableNativeArray.h | 35 ++ .../main/jni/react/jni/WritableNativeMap.cpp | 105 ++++ .../main/jni/react/jni/WritableNativeMap.h | 40 ++ .../src/main/jni/xreact/jni/MethodInvoker.cpp | 2 +- 22 files changed, 823 insertions(+), 578 deletions(-) create mode 100644 ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp create mode 100644 ReactAndroid/src/main/jni/react/jni/NativeCommon.h create mode 100644 ReactAndroid/src/main/jni/react/jni/NativeMap.cpp create mode 100644 ReactAndroid/src/main/jni/react/jni/NativeMap.h create mode 100644 ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp create mode 100644 ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.cpp create mode 100644 ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.h create mode 100644 ReactAndroid/src/main/jni/react/jni/WritableNativeArray.cpp create mode 100644 ReactAndroid/src/main/jni/react/jni/WritableNativeArray.h create mode 100644 ReactAndroid/src/main/jni/react/jni/WritableNativeMap.cpp create mode 100644 ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java index 9b5ded014c87a4..2c683f9451130e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java @@ -9,7 +9,7 @@ package com.facebook.react.bridge; -import com.facebook.jni.Countable; +import com.facebook.jni.HybridData; import com.facebook.proguard.annotations.DoNotStrip; import com.facebook.soloader.SoLoader; @@ -17,18 +17,18 @@ * Base class for a Map whose keys and values are stored in native code (C++). */ @DoNotStrip -public abstract class NativeMap extends Countable { - +public abstract class NativeMap { static { SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); } - public NativeMap() { - initialize(); + public NativeMap(HybridData hybridData) { + mHybridData = hybridData; } @Override public native String toString(); - private native void initialize(); + @DoNotStrip + private HybridData mHybridData; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java index 9d1fec6313432c..f3782ca0896f75 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java @@ -9,7 +9,7 @@ package com.facebook.react.bridge; -import com.facebook.jni.Countable; +import com.facebook.jni.HybridData; import com.facebook.proguard.annotations.DoNotStrip; import com.facebook.soloader.SoLoader; @@ -27,6 +27,10 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap { SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); } + protected ReadableNativeMap(HybridData hybridData) { + super(hybridData); + } + @Override public native boolean hasKey(String name); @Override @@ -51,7 +55,7 @@ public ReadableMapKeySetIterator keySetIterator() { return new ReadableNativeMapKeySetIterator(this); } - public HashMaptoHashMap() { + public HashMap toHashMap() { ReadableMapKeySetIterator iterator = keySetIterator(); HashMap hashMap = new HashMap<>(); @@ -87,14 +91,17 @@ public ReadableMapKeySetIterator keySetIterator() { * Implementation of a {@link ReadableNativeMap} iterator in native memory. */ @DoNotStrip - private static class ReadableNativeMapKeySetIterator extends Countable - implements ReadableMapKeySetIterator { + private static class ReadableNativeMapKeySetIterator implements ReadableMapKeySetIterator { + @DoNotStrip + private final HybridData mHybridData; - private final ReadableNativeMap mReadableNativeMap; + // Need to hold a strong ref to the map so that our native references remain valid. + @DoNotStrip + private final ReadableNativeMap mMap; public ReadableNativeMapKeySetIterator(ReadableNativeMap readableNativeMap) { - mReadableNativeMap = readableNativeMap; - initialize(mReadableNativeMap); + mMap = readableNativeMap; + mHybridData = initHybrid(readableNativeMap); } @Override @@ -102,6 +109,6 @@ public ReadableNativeMapKeySetIterator(ReadableNativeMap readableNativeMap) { @Override public native String nextKey(); - private native void initialize(ReadableNativeMap readableNativeMap); + private static native HybridData initHybrid(ReadableNativeMap readableNativeMap); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java index c0f65d0723e150..26fe2dd11f6228 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java @@ -57,7 +57,7 @@ public void pushMap(WritableMap map) { pushNativeMap((WritableNativeMap) map); } - private native static HybridData initHybrid(); + private static native HybridData initHybrid(); private native void pushNativeArray(WritableNativeArray array); private native void pushNativeMap(WritableNativeMap map); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java index 1388be0ce2fb18..d30827ade5bf12 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java @@ -9,6 +9,7 @@ package com.facebook.react.bridge; +import com.facebook.jni.HybridData; import com.facebook.infer.annotation.Assertions; import com.facebook.proguard.annotations.DoNotStrip; import com.facebook.soloader.SoLoader; @@ -20,7 +21,6 @@ */ @DoNotStrip public class WritableNativeMap extends ReadableNativeMap implements WritableMap { - static { SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); } @@ -59,6 +59,12 @@ public void merge(ReadableMap source) { mergeNativeMap((ReadableNativeMap) source); } + public WritableNativeMap() { + super(initHybrid()); + } + + private static native HybridData initHybrid(); + private native void putNativeMap(String key, WritableNativeMap value); private native void putNativeArray(String key, WritableNativeArray value); private native void mergeNativeMap(ReadableNativeMap source); diff --git a/ReactAndroid/src/main/jni/react/jni/Android.mk b/ReactAndroid/src/main/jni/react/jni/Android.mk index 42e43e8ce81a93..742e2161d1a0b6 100644 --- a/ReactAndroid/src/main/jni/react/jni/Android.mk +++ b/ReactAndroid/src/main/jni/react/jni/Android.mk @@ -7,13 +7,19 @@ LOCAL_MODULE := libreactnativejni LOCAL_SRC_FILES := \ JExecutorToken.cpp \ JMessageQueueThread.cpp \ - JniJSModulesUnbundle.cpp \ JSCPerfLogging.cpp \ JSLoader.cpp \ JSLogging.cpp \ + JniJSModulesUnbundle.cpp \ NativeArray.cpp \ + NativeCommon.cpp \ + NativeMap.cpp \ OnLoad.cpp \ ProxyExecutor.cpp \ + ReadableNativeArray.cpp \ + ReadableNativeMap.cpp \ + WritableNativeArray.cpp \ + WritableNativeMap.cpp \ LOCAL_C_INCLUDES := $(LOCAL_PATH) LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/../.. $(LOCAL_PATH)/.. diff --git a/ReactAndroid/src/main/jni/react/jni/BUCK b/ReactAndroid/src/main/jni/react/jni/BUCK index 0941c5f7cc8111..9bd754fb70e92f 100644 --- a/ReactAndroid/src/main/jni/react/jni/BUCK +++ b/ReactAndroid/src/main/jni/react/jni/BUCK @@ -31,11 +31,17 @@ jni_library( 'JMessageQueueThread.cpp', 'JSCPerfLogging.cpp', 'JSLoader.cpp', + 'JSLogging.cpp', 'JniJSModulesUnbundle.cpp', 'NativeArray.cpp', + 'NativeCommon.cpp', + 'NativeMap.cpp', 'OnLoad.cpp', 'ProxyExecutor.cpp', - 'JSLogging.cpp', + 'ReadableNativeArray.cpp', + 'ReadableNativeMap.cpp', + 'WritableNativeArray.cpp', + 'WritableNativeMap.cpp', ], headers = [ 'JSLoader.h', @@ -50,8 +56,13 @@ jni_library( 'WebWorkers.h', ], exported_headers = [ + 'NativeCommon.h', 'NativeArray.h', + 'NativeMap.h', 'ReadableNativeArray.h', + 'ReadableNativeMap.h', + 'WritableNativeArray.h', + 'WritableNativeMap.h', ], preprocessor_flags = [ '-DLOG_TAG="ReactNativeJNI"', diff --git a/ReactAndroid/src/main/jni/react/jni/NativeArray.cpp b/ReactAndroid/src/main/jni/react/jni/NativeArray.cpp index 1bf9034699c447..5d01bf7a657fcb 100644 --- a/ReactAndroid/src/main/jni/react/jni/NativeArray.cpp +++ b/ReactAndroid/src/main/jni/react/jni/NativeArray.cpp @@ -5,23 +5,24 @@ #include #include +#include "NativeCommon.h" + +using namespace facebook::jni; + namespace facebook { namespace react { NativeArray::NativeArray(folly::dynamic a) : array(std::move(a)) { if (!array.isArray()) { - jni::throwNewJavaException("com/facebook/react/bridge/UnexpectedNativeTypeException", + throwNewJavaException(exceptions::gUnexpectedNativeTypeExceptionClass, "expected Array, got a %s", array.typeName()); } } -jstring NativeArray::toString() { - if (isConsumed) { - jni::throwNewJavaException("com/facebook/react/bridge/ObjectAlreadyConsumedException", - "Array already consumed"); - } - return jni::make_jstring(folly::toJson(array).c_str()).release(); +local_ref NativeArray::toString() { + exceptions::throwIfObjectAlreadyConsumed(this, "Array already consumed"); + return make_jstring(folly::toJson(array).c_str()); } void NativeArray::registerNatives() { diff --git a/ReactAndroid/src/main/jni/react/jni/NativeArray.h b/ReactAndroid/src/main/jni/react/jni/NativeArray.h index 0fc69d5ac74829..ef69e17249c356 100644 --- a/ReactAndroid/src/main/jni/react/jni/NativeArray.h +++ b/ReactAndroid/src/main/jni/react/jni/NativeArray.h @@ -17,7 +17,7 @@ class NativeArray : public jni::HybridClass { bool isConsumed = false; folly::dynamic array; - jstring toString(); + jni::local_ref toString(); static void registerNatives(); diff --git a/ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp b/ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp new file mode 100644 index 00000000000000..0f500735055f20 --- /dev/null +++ b/ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp @@ -0,0 +1,76 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "NativeCommon.h" + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +namespace exceptions { +const char *gUnexpectedNativeTypeExceptionClass = + "com/facebook/react/bridge/UnexpectedNativeTypeException"; +} + +namespace { + +local_ref getTypeField(const char* fieldName) { + static auto cls = ReadableType::javaClassStatic(); + auto field = cls->getStaticField(fieldName); + return cls->getStaticFieldValue(field); +} + +alias_ref getNullValue() { + static alias_ref val = make_global(getTypeField("Null")).release(); + return val; +} + +alias_ref getBooleanValue() { + static alias_ref val = make_global(getTypeField("Boolean")).release(); + return val; +} + +alias_ref getNumberValue() { + static alias_ref val = make_global(getTypeField("Number")).release(); + return val; +} + +alias_ref getStringValue() { + static alias_ref val = make_global(getTypeField("String")).release(); + return val; +} + +alias_ref getMapValue() { + static alias_ref val = make_global(getTypeField("Map")).release(); + return val; +} + +alias_ref getArrayValue() { + static alias_ref val = make_global(getTypeField("Array")).release(); + return val; +} + +} // namespace + +local_ref ReadableType::getType(folly::dynamic::Type type) { + switch (type) { + case folly::dynamic::Type::NULLT: + return make_local(getNullValue()); + case folly::dynamic::Type::BOOL: + return make_local(getBooleanValue()); + case folly::dynamic::Type::DOUBLE: + case folly::dynamic::Type::INT64: + return make_local(getNumberValue()); + case folly::dynamic::Type::STRING: + return make_local(getStringValue()); + case folly::dynamic::Type::OBJECT: + return make_local(getMapValue()); + case folly::dynamic::Type::ARRAY: + return make_local(getArrayValue()); + default: + throwNewJavaException(exceptions::gUnexpectedNativeTypeExceptionClass, "Unknown type"); + } +} + +} // namespace react +} // namespace facebook diff --git a/ReactAndroid/src/main/jni/react/jni/NativeCommon.h b/ReactAndroid/src/main/jni/react/jni/NativeCommon.h new file mode 100644 index 00000000000000..2f9a4a4d609072 --- /dev/null +++ b/ReactAndroid/src/main/jni/react/jni/NativeCommon.h @@ -0,0 +1,31 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include + +namespace facebook { +namespace react { + +struct ReadableType : public jni::JavaClass { + static auto constexpr kJavaDescriptor = "Lcom/facebook/react/bridge/ReadableType;"; + + static jni::local_ref getType(folly::dynamic::Type type); +}; + +namespace exceptions { + +extern const char *gUnexpectedNativeTypeExceptionClass; + +template +void throwIfObjectAlreadyConsumed(const T& t, const char* msg) { + if (t->isConsumed) { + jni::throwNewJavaException("com/facebook/react/bridge/ObjectAlreadyConsumedException", msg); + } +} + +} // namespace exceptions + +} // namespace react +} // namespace facebook diff --git a/ReactAndroid/src/main/jni/react/jni/NativeMap.cpp b/ReactAndroid/src/main/jni/react/jni/NativeMap.cpp new file mode 100644 index 00000000000000..92af29e67ddb79 --- /dev/null +++ b/ReactAndroid/src/main/jni/react/jni/NativeMap.cpp @@ -0,0 +1,28 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "NativeMap.h" + +#include "NativeCommon.h" + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +std::string NativeMap::toString() { + throwIfConsumed(); + return ("{ NativeMap: " + folly::toJson(map_) + " }").c_str(); +} + +void NativeMap::registerNatives() { + registerHybrid({ + makeNativeMethod("toString", NativeMap::toString), + }); +} + +void NativeMap::throwIfConsumed() { + exceptions::throwIfObjectAlreadyConsumed(this, "Map already consumed"); +} + +} // namespace react +} // namespace facebook diff --git a/ReactAndroid/src/main/jni/react/jni/NativeMap.h b/ReactAndroid/src/main/jni/react/jni/NativeMap.h new file mode 100644 index 00000000000000..4246195ba77bf0 --- /dev/null +++ b/ReactAndroid/src/main/jni/react/jni/NativeMap.h @@ -0,0 +1,33 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include + +namespace facebook { +namespace react { + +class NativeMap : public jni::HybridClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/facebook/react/bridge/NativeMap;"; + + explicit NativeMap(folly::dynamic s) : isConsumed(false), map_(s) {} + + std::string toString(); + + bool isConsumed; + void throwIfConsumed(); + + static void registerNatives(); + protected: + + folly::dynamic map_; + + friend HybridBase; + friend class ReadableNativeMapKeySetIterator; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp index 001c7237ea3dcf..8d60719c4e9975 100644 --- a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp +++ b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp @@ -22,6 +22,7 @@ #include "JExecutorTokenFactory.h" #include "JNativeRunnable.h" #include "JSLoader.h" +#include "NativeCommon.h" #include "ReadableNativeArray.h" #include "ProxyExecutor.h" #include "OnLoad.h" @@ -30,6 +31,7 @@ #include "JSLogging.h" #include "JSCPerfLogging.h" #include "WebWorkers.h" +#include "WritableNativeMap.h" #include #ifdef WITH_FBSYSTRACE @@ -42,492 +44,6 @@ using namespace facebook::jni; namespace facebook { namespace react { -static jclass gReadableNativeMapClass; -static jmethodID gReadableNativeMapCtor; - -namespace exceptions { - -static const char *gUnexpectedNativeTypeExceptionClass = - "com/facebook/react/bridge/UnexpectedNativeTypeException"; - -template -void throwIfObjectAlreadyConsumed(const T& t, const char* msg) { - if (t->isConsumed) { - throwNewJavaException("com/facebook/react/bridge/ObjectAlreadyConsumedException", msg); - } -} - -} - -struct NativeMap : public Countable { - // Whether this map has been added to another array or map and no longer has a valid map value - bool isConsumed = false; - folly::dynamic map = folly::dynamic::object; -}; - -struct ReadableNativeMapKeySetIterator : public Countable { - folly::dynamic::const_item_iterator iterator; - RefPtr mapRef; - - ReadableNativeMapKeySetIterator(folly::dynamic::const_item_iterator&& it, - const RefPtr& mapRef_) - : iterator(std::move(it)) - , mapRef(mapRef_) {} -}; - -static jobject createReadableNativeMapWithContents(JNIEnv* env, folly::dynamic map) { - if (map.isNull()) { - return nullptr; - } - - if (!map.isObject()) { - throwNewJavaException(exceptions::gUnexpectedNativeTypeExceptionClass, - "expected Map, got a %s", map.typeName()); - } - - jobject jnewMap = env->NewObject(gReadableNativeMapClass, gReadableNativeMapCtor); - if (env->ExceptionCheck()) { - return nullptr; - } - auto nativeMap = extractRefPtr(env, jnewMap); - nativeMap->map = std::move(map); - return jnewMap; -} - -namespace type { - -static jclass gReadableReactType; -static jobject gTypeNullValue; -static jobject gTypeBooleanValue; -static jobject gTypeNumberValue; -static jobject gTypeStringValue; -static jobject gTypeMapValue; -static jobject gTypeArrayValue; - -static jobject getTypeValue(JNIEnv* env, const char* fieldName) { - jfieldID fieldID = env->GetStaticFieldID( - gReadableReactType, fieldName, "Lcom/facebook/react/bridge/ReadableType;"); - jobject typeValue = env->GetStaticObjectField(gReadableReactType, fieldID); - return env->NewGlobalRef(typeValue); -} - -static void initialize(JNIEnv* env) { - gTypeNullValue = getTypeValue(env, "Null"); - gTypeBooleanValue = getTypeValue(env, "Boolean"); - gTypeNumberValue = getTypeValue(env, "Number"); - gTypeStringValue = getTypeValue(env, "String"); - gTypeMapValue = getTypeValue(env, "Map"); - gTypeArrayValue = getTypeValue(env, "Array"); -} - -static jobject getType(folly::dynamic::Type type) { - switch (type) { - case folly::dynamic::Type::NULLT: - return type::gTypeNullValue; - case folly::dynamic::Type::BOOL: - return type::gTypeBooleanValue; - case folly::dynamic::Type::DOUBLE: - case folly::dynamic::Type::INT64: - return type::gTypeNumberValue; - case folly::dynamic::Type::STRING: - return type::gTypeStringValue; - case folly::dynamic::Type::OBJECT: - return type::gTypeMapValue; - case folly::dynamic::Type::ARRAY: - return type::gTypeArrayValue; - default: - throwNewJavaException(exceptions::gUnexpectedNativeTypeExceptionClass, "Unknown type"); - } -} - -} - -// This attribute exports the ctor symbol, so ReadableNativeArray to be -// constructed from other DSOs. -__attribute__((visibility("default"))) -ReadableNativeArray::ReadableNativeArray(folly::dynamic array) - : HybridBase(std::move(array)) {} - -void ReadableNativeArray::mapException(const std::exception& ex) { - if (dynamic_cast(&ex) != nullptr) { - throwNewJavaException(exceptions::gUnexpectedNativeTypeExceptionClass, ex.what()); - } -} - -jint ReadableNativeArray::getSize() { - return array.size(); -} - -jboolean ReadableNativeArray::isNull(jint index) { - return array.at(index).isNull() ? JNI_TRUE : JNI_FALSE; -} - -jboolean ReadableNativeArray::getBoolean(jint index) { - return array.at(index).getBool() ? JNI_TRUE : JNI_FALSE; -} - -jdouble ReadableNativeArray::getDouble(jint index) { - const folly::dynamic& val = array.at(index); - if (val.isInt()) { - return val.getInt(); - } - return val.getDouble(); -} - -jint ReadableNativeArray::getInt(jint index) { - auto integer = array.at(index).getInt(); - static_assert(std::is_same::value, - "folly::dynamic int is not int64_t"); - jint javaint = static_cast(integer); - if (integer != javaint) { - throwNewJavaException( - exceptions::gUnexpectedNativeTypeExceptionClass, - "Value '%lld' doesn't fit into a 32 bit signed int", integer); - } - return javaint; -} - -const char* ReadableNativeArray::getString(jint index) { - const folly::dynamic& dyn = array.at(index); - if (dyn.isNull()) { - return nullptr; - } - return dyn.getString().c_str(); -} - -jni::local_ref ReadableNativeArray::getArray(jint index) { - auto& elem = array.at(index); - if (elem.isNull()) { - return jni::local_ref(nullptr); - } else { - return ReadableNativeArray::newObjectCxxArgs(elem); - } -} - -// Export getMap() so we can workaround constructing ReadableNativeMap -__attribute__((visibility("default"))) -jobject ReadableNativeArray::getMap(jint index) { - return createReadableNativeMapWithContents(Environment::current(), array.at(index)); -} - -jobject ReadableNativeArray::getType(jint index) { - return type::getType(array.at(index).type()); -} - -void ReadableNativeArray::registerNatives() { - jni::registerNatives("com/facebook/react/bridge/ReadableNativeArray", { - makeNativeMethod("size", ReadableNativeArray::getSize), - makeNativeMethod("isNull", ReadableNativeArray::isNull), - makeNativeMethod("getBoolean", ReadableNativeArray::getBoolean), - makeNativeMethod("getDouble", ReadableNativeArray::getDouble), - makeNativeMethod("getInt", ReadableNativeArray::getInt), - makeNativeMethod("getString", ReadableNativeArray::getString), - makeNativeMethod("getArray", ReadableNativeArray::getArray), - makeNativeMethod("getMap", "(I)Lcom/facebook/react/bridge/ReadableNativeMap;", - ReadableNativeArray::getMap), - makeNativeMethod("getType", "(I)Lcom/facebook/react/bridge/ReadableType;", - ReadableNativeArray::getType), - }); -} - -namespace { - -struct WritableNativeArray - : public jni::HybridClass { - static constexpr const char* kJavaDescriptor = "Lcom/facebook/react/bridge/WritableNativeArray;"; - - WritableNativeArray() - : HybridBase(folly::dynamic::array()) {} - - static local_ref initHybrid(alias_ref) { - return makeCxxInstance(); - } - - void pushNull() { - exceptions::throwIfObjectAlreadyConsumed(this, "Array already consumed"); - array.push_back(nullptr); - } - - void pushBoolean(jboolean value) { - exceptions::throwIfObjectAlreadyConsumed(this, "Array already consumed"); - array.push_back(value == JNI_TRUE); - } - - void pushDouble(jdouble value) { - exceptions::throwIfObjectAlreadyConsumed(this, "Receiving array already consumed"); - array.push_back(value); - } - - void pushInt(jint value) { - exceptions::throwIfObjectAlreadyConsumed(this, "Receiving array already consumed"); - array.push_back(value); - } - - void pushString(jstring value) { - if (value == NULL) { - pushNull(); - return; - } - exceptions::throwIfObjectAlreadyConsumed(this, "Receiving array already consumed"); - array.push_back(wrap_alias(value)->toStdString()); - } - - void pushNativeArray(WritableNativeArray* otherArray) { - if (otherArray == NULL) { - pushNull(); - return; - } - exceptions::throwIfObjectAlreadyConsumed(this, "Receiving array already consumed"); - exceptions::throwIfObjectAlreadyConsumed(otherArray, "Array to push already consumed"); - array.push_back(std::move(otherArray->array)); - otherArray->isConsumed = true; - } - - void pushNativeMap(jobject jmap) { - if (jmap == NULL) { - pushNull(); - return; - } - exceptions::throwIfObjectAlreadyConsumed(this, "Receiving array already consumed"); - auto map = extractRefPtr(Environment::current(), jmap); - exceptions::throwIfObjectAlreadyConsumed(map, "Map to push already consumed"); - array.push_back(std::move(map->map)); - map->isConsumed = true; - } - - static void registerNatives() { - jni::registerNatives("com/facebook/react/bridge/WritableNativeArray", { - makeNativeMethod("initHybrid", WritableNativeArray::initHybrid), - makeNativeMethod("pushNull", WritableNativeArray::pushNull), - makeNativeMethod("pushBoolean", WritableNativeArray::pushBoolean), - makeNativeMethod("pushDouble", WritableNativeArray::pushDouble), - makeNativeMethod("pushInt", WritableNativeArray::pushInt), - makeNativeMethod("pushString", WritableNativeArray::pushString), - makeNativeMethod("pushNativeArray", WritableNativeArray::pushNativeArray), - makeNativeMethod("pushNativeMap", "(Lcom/facebook/react/bridge/WritableNativeMap;)V", - WritableNativeArray::pushNativeMap), - }); - } -}; - -} - -namespace map { - -static void initialize(JNIEnv* env, jobject obj) { - auto map = createNew(); - setCountableForJava(env, obj, std::move(map)); -} - -static jstring toString(JNIEnv* env, jobject obj) { - auto nativeMap = extractRefPtr(env, obj); - exceptions::throwIfObjectAlreadyConsumed(nativeMap, "Map already consumed"); - LocalString string( - ("{ NativeMap: " + folly::toJson(nativeMap->map) + " }").c_str()); - return static_cast(env->NewLocalRef(string.string())); -} - -namespace writable { - -static void putNull(JNIEnv* env, jobject obj, jstring key) { - auto map = extractRefPtr(env, obj); - exceptions::throwIfObjectAlreadyConsumed(map, "Receiving map already consumed"); - map->map.insert(fromJString(env, key), nullptr); -} - -static void putBoolean(JNIEnv* env, jobject obj, jstring key, jboolean value) { - auto map = extractRefPtr(env, obj); - exceptions::throwIfObjectAlreadyConsumed(map, "Receiving map already consumed"); - map->map.insert(fromJString(env, key), value == JNI_TRUE); -} - -static void putDouble(JNIEnv* env, jobject obj, jstring key, jdouble value) { - auto map = extractRefPtr(env, obj); - exceptions::throwIfObjectAlreadyConsumed(map, "Receiving map already consumed"); - map->map.insert(fromJString(env, key), value); -} - -static void putInt(JNIEnv* env, jobject obj, jstring key, jint value) { - auto map = extractRefPtr(env, obj); - exceptions::throwIfObjectAlreadyConsumed(map, "Receiving map already consumed"); - map->map.insert(fromJString(env, key), value); -} - -static void putString(JNIEnv* env, jobject obj, jstring key, jstring value) { - if (value == NULL) { - putNull(env, obj, key); - return; - } - auto map = extractRefPtr(env, obj); - exceptions::throwIfObjectAlreadyConsumed(map, "Receiving map already consumed"); - map->map.insert(fromJString(env, key), fromJString(env, value)); -} - -static void putArray(JNIEnv* env, jobject obj, jstring key, - WritableNativeArray::jhybridobject value) { - if (value == NULL) { - putNull(env, obj, key); - return; - } - auto parentMap = extractRefPtr(env, obj); - exceptions::throwIfObjectAlreadyConsumed(parentMap, "Receiving map already consumed"); - auto arrayValue = cthis(wrap_alias(value)); - exceptions::throwIfObjectAlreadyConsumed(arrayValue, "Array to put already consumed"); - parentMap->map.insert(fromJString(env, key), std::move(arrayValue->array)); - arrayValue->isConsumed = true; -} - -static void putMap(JNIEnv* env, jobject obj, jstring key, jobject value) { - if (value == NULL) { - putNull(env, obj, key); - return; - } - auto parentMap = extractRefPtr(env, obj); - exceptions::throwIfObjectAlreadyConsumed(parentMap, "Receiving map already consumed"); - auto mapValue = extractRefPtr(env, value); - exceptions::throwIfObjectAlreadyConsumed(mapValue, "Map to put already consumed"); - parentMap->map.insert(fromJString(env, key), std::move(mapValue->map)); - mapValue->isConsumed = true; -} - -static void mergeMap(JNIEnv* env, jobject obj, jobject source) { - auto sourceMap = extractRefPtr(env, source); - exceptions::throwIfObjectAlreadyConsumed(sourceMap, "Source map already consumed"); - auto destMap = extractRefPtr(env, obj); - exceptions::throwIfObjectAlreadyConsumed(destMap, "Destination map already consumed"); - - // std::map#insert doesn't overwrite the value, therefore we need to clean values for keys - // that already exists before merging dest map into source map - for (auto sourceIt : sourceMap->map.items()) { - destMap->map.erase(sourceIt.first); - destMap->map.insert(std::move(sourceIt.first), std::move(sourceIt.second)); - } -} - -} // namespace writable - -namespace readable { - -static const char *gNoSuchKeyExceptionClass = "com/facebook/react/bridge/NoSuchKeyException"; - -static jboolean hasKey(JNIEnv* env, jobject obj, jstring keyName) { - auto nativeMap = extractRefPtr(env, obj); - auto& map = nativeMap->map; - bool found = map.find(fromJString(env, keyName)) != map.items().end(); - return found ? JNI_TRUE : JNI_FALSE; -} - -static const folly::dynamic& getMapValue(JNIEnv* env, jobject obj, jstring keyName) { - auto nativeMap = extractRefPtr(env, obj); - std::string key = fromJString(env, keyName); - try { - return nativeMap->map.at(key); - } catch (const std::out_of_range& ex) { - throwNewJavaException(gNoSuchKeyExceptionClass, ex.what()); - } -} - -static jboolean isNull(JNIEnv* env, jobject obj, jstring keyName) { - return getMapValue(env, obj, keyName).isNull() ? JNI_TRUE : JNI_FALSE; -} - -static jboolean getBooleanKey(JNIEnv* env, jobject obj, jstring keyName) { - try { - return getMapValue(env, obj, keyName).getBool() ? JNI_TRUE : JNI_FALSE; - } catch (const folly::TypeError& ex) { - throwNewJavaException(exceptions::gUnexpectedNativeTypeExceptionClass, ex.what()); - } -} - -static jdouble getDoubleKey(JNIEnv* env, jobject obj, jstring keyName) { - const folly::dynamic& val = getMapValue(env, obj, keyName); - if (val.isInt()) { - return val.getInt(); - } - try { - return val.getDouble(); - } catch (const folly::TypeError& ex) { - throwNewJavaException(exceptions::gUnexpectedNativeTypeExceptionClass, ex.what()); - } -} - -static jint getIntKey(JNIEnv* env, jobject obj, jstring keyName) { - try { - auto integer = getMapValue(env, obj, keyName).getInt(); - jint javaint = static_cast(integer); - if (integer != javaint) { - throwNewJavaException( - exceptions::gUnexpectedNativeTypeExceptionClass, - "Value '%lld' doesn't fit into a 32 bit signed int", integer); - } - return javaint; - } catch (const folly::TypeError& ex) { - throwNewJavaException(exceptions::gUnexpectedNativeTypeExceptionClass, ex.what()); - } -} - -static jstring getStringKey(JNIEnv* env, jobject obj, jstring keyName) { - const folly::dynamic& val = getMapValue(env, obj, keyName); - if (val.isNull()) { - return nullptr; - } - try { - LocalString value(val.getString().c_str()); - return static_cast(env->NewLocalRef(value.string())); - } catch (const folly::TypeError& ex) { - throwNewJavaException(exceptions::gUnexpectedNativeTypeExceptionClass, ex.what()); - } -} - -static jni::local_ref getArrayKey( - jni::alias_ref obj, jstring keyName) { - auto& value = getMapValue(Environment::current(), obj.get(), keyName); - if (value.isNull()) { - return jni::local_ref(nullptr); - } else { - return ReadableNativeArray::newObjectCxxArgs(value); - } -} - -static jobject getMapKey(JNIEnv* env, jobject obj, jstring keyName) { - return createReadableNativeMapWithContents(env, getMapValue(env, obj, keyName)); -} - -static jobject getValueType(JNIEnv* env, jobject obj, jstring keyName) { - return type::getType(getMapValue(env, obj, keyName).type()); -} - -} // namespace readable - -namespace iterator { - -static void initialize(JNIEnv* env, jobject obj, jobject nativeMapObj) { - auto nativeMap = extractRefPtr(env, nativeMapObj); - auto mapIterator = createNew( - nativeMap->map.items().begin(), nativeMap); - setCountableForJava(env, obj, std::move(mapIterator)); -} - -static jboolean hasNextKey(JNIEnv* env, jobject obj) { - auto nativeIterator = extractRefPtr(env, obj); - return ((nativeIterator->iterator != nativeIterator->mapRef.get()->map.items().end()) - ? JNI_TRUE : JNI_FALSE); -} - -static jstring getNextKey(JNIEnv* env, jobject obj) { - auto nativeIterator = extractRefPtr(env, obj); - if (JNI_FALSE == hasNextKey(env, obj)) { - throwNewJavaException("com/facebook/react/bridge/InvalidIteratorException", - "No such element exists"); - } - LocalString value(nativeIterator->iterator->first.c_str()); - ++nativeIterator->iterator; - return static_cast(env->NewLocalRef(value.string())); -} - -} // namespace iterator -} // namespace map - namespace { namespace runnable { @@ -758,7 +274,7 @@ static void loadScriptFromAssets(JNIEnv* env, jobject obj, jobject assetManager, static void loadScriptFromFile(JNIEnv* env, jobject obj, jstring fileName, jstring sourceURL) { jclass markerClass = env->FindClass("com/facebook/react/bridge/ReactMarker"); - auto bridge = jni::extractRefPtr(env, obj); + auto bridge = extractRefPtr(env, obj); auto fileNameStr = fileName == NULL ? "" : fromJString(env, fileName); env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromFile_start")); auto script = fileName == NULL ? "" : react::loadScriptFromFile(fileNameStr); @@ -769,7 +285,7 @@ static void loadScriptFromFile(JNIEnv* env, jobject obj, jstring fileName, jstri "sourceURL", sourceURLStr); #endif env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromFile_read")); - loadApplicationScript(bridge, script, jni::fromJString(env, sourceURL)); + loadApplicationScript(bridge, script, fromJString(env, sourceURL)); if (env->ExceptionCheck()) { return; } @@ -902,17 +418,14 @@ struct CountableJSCExecutorFactory : CountableJSExecutorFactory { folly::dynamic m_jscConfig; }; -static void createJSCExecutor(JNIEnv *env, jobject obj, jobject jscConfig) { - auto nativeMap = extractRefPtr(env, jscConfig); - exceptions::throwIfObjectAlreadyConsumed(nativeMap, "Map to push already consumed"); - auto executor = createNew(std::move(nativeMap->map)); - nativeMap->isConsumed = true; - setCountableForJava(env, obj, std::move(executor)); +static void createJSCExecutor(alias_ref obj, WritableNativeMap* jscConfig) { + auto executor = createNew(jscConfig->consume()); + setCountableForJava(Environment::current(), obj.get(), std::move(executor)); } static void createProxyExecutor(JNIEnv *env, jobject obj, jobject executorInstance) { auto executor = - createNew(jni::make_global(jni::adopt_local(executorInstance))); + createNew(make_global(adopt_local(executorInstance))); setCountableForJava(env, obj, std::move(executor)); } @@ -943,68 +456,19 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { PerfLogging::installNativeHooks = addNativePerfLoggingHooks; JSLogging::nativeHook = nativeLoggingHook; - // get the current env - JNIEnv* env = Environment::current(); - - auto readableTypeClass = findClassLocal("com/facebook/react/bridge/ReadableType"); - type::gReadableReactType = (jclass)env->NewGlobalRef(readableTypeClass.get()); - type::initialize(env); - NativeArray::registerNatives(); ReadableNativeArray::registerNatives(); WritableNativeArray::registerNatives(); JNativeRunnable::registerNatives(); registerJSLoaderNatives(); - registerNatives("com/facebook/react/bridge/NativeMap", { - makeNativeMethod("initialize", map::initialize), - makeNativeMethod("toString", map::toString), - }); - - jclass readableMapClass = env->FindClass("com/facebook/react/bridge/ReadableNativeMap"); - gReadableNativeMapClass = (jclass)env->NewGlobalRef(readableMapClass); - gReadableNativeMapCtor = env->GetMethodID(readableMapClass, "", "()V"); - wrap_alias(readableMapClass)->registerNatives({ - makeNativeMethod("hasKey", map::readable::hasKey), - makeNativeMethod("isNull", map::readable::isNull), - makeNativeMethod("getBoolean", map::readable::getBooleanKey), - makeNativeMethod("getDouble", map::readable::getDoubleKey), - makeNativeMethod("getInt", map::readable::getIntKey), - makeNativeMethod("getString", map::readable::getStringKey), - makeNativeMethod("getArray", map::readable::getArrayKey), - makeNativeMethod( - "getMap", "(Ljava/lang/String;)Lcom/facebook/react/bridge/ReadableNativeMap;", - map::readable::getMapKey), - makeNativeMethod( - "getType", "(Ljava/lang/String;)Lcom/facebook/react/bridge/ReadableType;", - map::readable::getValueType), - }); - - registerNatives("com/facebook/react/bridge/WritableNativeMap", { - makeNativeMethod("putNull", map::writable::putNull), - makeNativeMethod("putBoolean", map::writable::putBoolean), - makeNativeMethod("putDouble", map::writable::putDouble), - makeNativeMethod("putInt", map::writable::putInt), - makeNativeMethod("putString", map::writable::putString), - makeNativeMethod("putNativeArray", map::writable::putArray), - makeNativeMethod( - "putNativeMap", "(Ljava/lang/String;Lcom/facebook/react/bridge/WritableNativeMap;)V", - map::writable::putMap), - makeNativeMethod( - "mergeNativeMap", "(Lcom/facebook/react/bridge/ReadableNativeMap;)V", - map::writable::mergeMap) - }); - - registerNatives("com/facebook/react/bridge/ReadableNativeMap$ReadableNativeMapKeySetIterator", { - makeNativeMethod("initialize", "(Lcom/facebook/react/bridge/ReadableNativeMap;)V", - map::iterator::initialize), - makeNativeMethod("hasNextKey", map::iterator::hasNextKey), - makeNativeMethod("nextKey", map::iterator::getNextKey), - }); + NativeMap::registerNatives(); + ReadableNativeMap::registerNatives(); + WritableNativeMap::registerNatives(); + ReadableNativeMapKeySetIterator::registerNatives(); registerNatives("com/facebook/react/bridge/JSCJavaScriptExecutor", { - makeNativeMethod("initialize", "(Lcom/facebook/react/bridge/WritableNativeMap;)V", - executors::createJSCExecutor), + makeNativeMethod("initialize", executors::createJSCExecutor), }); registerNatives("com/facebook/react/bridge/ProxyJavaScriptExecutor", { @@ -1013,6 +477,9 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { executors::createProxyExecutor), }); + // get the current env + JNIEnv* env = Environment::current(); + jclass callbackClass = env->FindClass("com/facebook/react/bridge/ReactCallback"); bridge::gCallbackMethod = env->GetMethodID(callbackClass, "call", "(Lcom/facebook/react/bridge/ExecutorToken;IILcom/facebook/react/bridge/ReadableNativeArray;)V"); bridge::gOnBatchCompleteMethod = env->GetMethodID(callbackClass, "onBatchComplete", "()V"); diff --git a/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp b/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp new file mode 100644 index 00000000000000..d4e9faa3f17773 --- /dev/null +++ b/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp @@ -0,0 +1,102 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "ReadableNativeArray.h" + +#include "ReadableNativeMap.h" + +using namespace facebook::jni; + +namespace facebook { +namespace react { + + +// This attribute exports the ctor symbol, so ReadableNativeArray to be +// constructed from other DSOs. +__attribute__((visibility("default"))) +ReadableNativeArray::ReadableNativeArray(folly::dynamic array) + : HybridBase(std::move(array)) {} + +void ReadableNativeArray::mapException(const std::exception& ex) { + if (dynamic_cast(&ex) != nullptr) { + throwNewJavaException(exceptions::gUnexpectedNativeTypeExceptionClass, ex.what()); + } +} + +jint ReadableNativeArray::getSize() { + return array.size(); +} + +jboolean ReadableNativeArray::isNull(jint index) { + return array.at(index).isNull() ? JNI_TRUE : JNI_FALSE; +} + +jboolean ReadableNativeArray::getBoolean(jint index) { + return array.at(index).getBool() ? JNI_TRUE : JNI_FALSE; +} + +jdouble ReadableNativeArray::getDouble(jint index) { + const folly::dynamic& val = array.at(index); + if (val.isInt()) { + return val.getInt(); + } + return val.getDouble(); +} + +jint ReadableNativeArray::getInt(jint index) { + auto integer = array.at(index).getInt(); + static_assert(std::is_same::value, + "folly::dynamic int is not int64_t"); + jint javaint = static_cast(integer); + if (integer != javaint) { + throwNewJavaException( + exceptions::gUnexpectedNativeTypeExceptionClass, + "Value '%lld' doesn't fit into a 32 bit signed int", integer); + } + return javaint; +} + +const char* ReadableNativeArray::getString(jint index) { + const folly::dynamic& dyn = array.at(index); + if (dyn.isNull()) { + return nullptr; + } + return dyn.getString().c_str(); +} + +local_ref ReadableNativeArray::getArray(jint index) { + auto& elem = array.at(index); + if (elem.isNull()) { + return local_ref(nullptr); + } else { + return ReadableNativeArray::newObjectCxxArgs(elem); + } +} + +local_ref ReadableNativeArray::getType(jint index) { + return ReadableType::getType(array.at(index).type()); +} + +// Export getMap() so we can workaround constructing ReadableNativeMap +__attribute__((visibility("default"))) +local_ref ReadableNativeArray::getMap(jint index) { + // TODO(cjhopman): ... this moves the map?!? + return ReadableNativeMap::createWithContents(std::move(array.at(index))); +} + +void ReadableNativeArray::registerNatives() { + registerHybrid({ + makeNativeMethod("size", ReadableNativeArray::getSize), + makeNativeMethod("isNull", ReadableNativeArray::isNull), + makeNativeMethod("getBoolean", ReadableNativeArray::getBoolean), + makeNativeMethod("getDouble", ReadableNativeArray::getDouble), + makeNativeMethod("getInt", ReadableNativeArray::getInt), + makeNativeMethod("getString", ReadableNativeArray::getString), + makeNativeMethod("getArray", ReadableNativeArray::getArray), + makeNativeMethod("getMap", jmethod_traits::descriptor(), + ReadableNativeArray::getMap), + makeNativeMethod("getType", ReadableNativeArray::getType), + }); +} + +} // namespace react +} // namespace facebook diff --git a/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.h b/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.h index 5407d0e8a65d15..f8d4b5023898db 100644 --- a/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.h +++ b/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.h @@ -4,6 +4,9 @@ #include "NativeArray.h" +#include "NativeCommon.h" +#include "NativeMap.h" + namespace facebook { namespace react { @@ -26,8 +29,10 @@ class ReadableNativeArray : public jni::HybridClass getArray(jint index); - jobject getMap(jint index); - jobject getType(jint index); + // This actually returns a ReadableNativeMap::JavaPart, but due to + // limitations of fbjni, we can't specify that here. + jni::local_ref getMap(jint index); + jni::local_ref getType(jint index); static void registerNatives(); }; diff --git a/ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.cpp b/ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.cpp new file mode 100644 index 00000000000000..7b8d4a1fabb343 --- /dev/null +++ b/ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.cpp @@ -0,0 +1,150 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "ReadableNativeMap.h" + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +namespace { +const char *gNoSuchKeyExceptionClass = "com/facebook/react/bridge/NoSuchKeyException"; +} // namespace + +void ReadableNativeMap::mapException(const std::exception& ex) { + if (dynamic_cast(&ex) != nullptr) { + throwNewJavaException(exceptions::gUnexpectedNativeTypeExceptionClass, ex.what()); + } +} + +bool ReadableNativeMap::hasKey(const std::string& key) { + return map_.find(key) != map_.items().end(); +} + +const folly::dynamic& ReadableNativeMap::getMapValue(const std::string& key) { + try { + return map_.at(key); + } catch (const std::out_of_range& ex) { + throwNewJavaException(gNoSuchKeyExceptionClass, ex.what()); + } +} + +bool ReadableNativeMap::isNull(const std::string& key) { + return getMapValue(key).isNull(); +} + +bool ReadableNativeMap::getBooleanKey(const std::string& key) { + return getMapValue(key).getBool(); +} + +double ReadableNativeMap::getDoubleKey(const std::string& key) { + const folly::dynamic& val = getMapValue(key); + if (val.isInt()) { + return val.getInt(); + } + return val.getDouble(); +} + +jint ReadableNativeMap::getIntKey(const std::string& key) { + auto integer = getMapValue(key).getInt(); + jint javaint = static_cast(integer); + if (integer != javaint) { + throwNewJavaException( + exceptions::gUnexpectedNativeTypeExceptionClass, + "Value '%lld' doesn't fit into a 32 bit signed int", integer); + } + return javaint; +} + +local_ref ReadableNativeMap::getStringKey(const std::string& key) { + const folly::dynamic& val = getMapValue(key); + if (val.isNull()) { + return local_ref(nullptr); + } + return make_jstring(val.getString().c_str()); +} + +local_ref ReadableNativeMap::getArrayKey(const std::string& key) { + auto& value = getMapValue(key); + if (value.isNull()) { + return local_ref(nullptr); + } else { + return ReadableNativeArray::newObjectCxxArgs(value); + } +} + +local_ref ReadableNativeMap::getMapKey(const std::string& key) { + auto& value = getMapValue(key); + if (value.isNull()) { + return local_ref(nullptr); + } else if (!value.isObject()) { + throwNewJavaException(exceptions::gUnexpectedNativeTypeExceptionClass, + "expected Map, got a %s", value.typeName()); + } else { + return ReadableNativeMap::newObjectCxxArgs(value); + } +} + +local_ref ReadableNativeMap::getValueType(const std::string& key) { + return ReadableType::getType(getMapValue(key).type()); +} + +local_ref ReadableNativeMap::createWithContents(folly::dynamic&& map) { + if (map.isNull()) { + return local_ref(nullptr); + } + + if (!map.isObject()) { + throwNewJavaException(exceptions::gUnexpectedNativeTypeExceptionClass, + "expected Map, got a %s", map.typeName()); + } + + return newObjectCxxArgs(std::move(map)); +} + +void ReadableNativeMap::registerNatives() { + registerHybrid({ + makeNativeMethod("hasKey", ReadableNativeMap::hasKey), + makeNativeMethod("isNull", ReadableNativeMap::isNull), + makeNativeMethod("getBoolean", ReadableNativeMap::getBooleanKey), + makeNativeMethod("getDouble", ReadableNativeMap::getDoubleKey), + makeNativeMethod("getInt", ReadableNativeMap::getIntKey), + makeNativeMethod("getString", ReadableNativeMap::getStringKey), + makeNativeMethod("getArray", ReadableNativeMap::getArrayKey), + makeNativeMethod("getMap", ReadableNativeMap::getMapKey), + makeNativeMethod("getType", ReadableNativeMap::getValueType), + }); +} + +ReadableNativeMapKeySetIterator::ReadableNativeMapKeySetIterator(const folly::dynamic& map) + : iter_(map.items().begin()) + , map_(map) {} + +local_ref ReadableNativeMapKeySetIterator::initHybrid(alias_ref, ReadableNativeMap* nativeMap) { + return makeCxxInstance(nativeMap->map_); +} + +bool ReadableNativeMapKeySetIterator::hasNextKey() { + return iter_ != map_.items().end(); +} + +local_ref ReadableNativeMapKeySetIterator::nextKey() { + if (!hasNextKey()) { + throwNewJavaException("com/facebook/react/bridge/InvalidIteratorException", + "No such element exists"); + } + auto ret = make_jstring(iter_->first.c_str()); + ++iter_; + return ret; +} + +void ReadableNativeMapKeySetIterator::registerNatives() { + registerHybrid({ + makeNativeMethod("hasNextKey", ReadableNativeMapKeySetIterator::hasNextKey), + makeNativeMethod("nextKey", ReadableNativeMapKeySetIterator::nextKey), + makeNativeMethod("initHybrid", ReadableNativeMapKeySetIterator::initHybrid), + }); +} + +} // namespace react +} // namespace facebook diff --git a/ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.h b/ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.h new file mode 100644 index 00000000000000..8741dff67a406a --- /dev/null +++ b/ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.h @@ -0,0 +1,59 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include + +#include "NativeCommon.h" +#include "NativeMap.h" +#include "ReadableNativeArray.h" + +namespace facebook { +namespace react { + +struct WritableNativeMap; + +struct ReadableNativeMap : jni::HybridClass { + static auto constexpr kJavaDescriptor = "Lcom/facebook/react/bridge/ReadableNativeMap;"; + + bool hasKey(const std::string& key); + const folly::dynamic& getMapValue(const std::string& key); + bool isNull(const std::string& key); + bool getBooleanKey(const std::string& key); + double getDoubleKey(const std::string& key); + jint getIntKey(const std::string& key); + jni::local_ref getStringKey(const std::string& key); + jni::local_ref getArrayKey(const std::string& key); + jni::local_ref getMapKey(const std::string& key); + jni::local_ref getValueType(const std::string& key); + static jni::local_ref createWithContents(folly::dynamic&& map); + + static void mapException(const std::exception& ex); + + static void registerNatives(); + + using HybridBase::HybridBase; + friend HybridBase; + friend class WritableNativeMap; +}; + +struct ReadableNativeMapKeySetIterator : jni::HybridClass { + static auto constexpr kJavaDescriptor = "Lcom/facebook/react/bridge/ReadableNativeMap$ReadableNativeMapKeySetIterator;"; + + ReadableNativeMapKeySetIterator(const folly::dynamic& map); + + bool hasNextKey(); + jni::local_ref nextKey(); + + static jni::local_ref initHybrid(jni::alias_ref, ReadableNativeMap* nativeMap); + static void registerNatives(); + + folly::dynamic::const_item_iterator iter_; + // The Java side holds a strong ref to the Java ReadableNativeMap. + const folly::dynamic& map_; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactAndroid/src/main/jni/react/jni/WritableNativeArray.cpp b/ReactAndroid/src/main/jni/react/jni/WritableNativeArray.cpp new file mode 100644 index 00000000000000..1ff8fececb5483 --- /dev/null +++ b/ReactAndroid/src/main/jni/react/jni/WritableNativeArray.cpp @@ -0,0 +1,83 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "WritableNativeArray.h" + +#include "WritableNativeMap.h" + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +WritableNativeArray::WritableNativeArray() + : HybridBase(folly::dynamic::array()) {} + +local_ref WritableNativeArray::initHybrid(alias_ref) { + return makeCxxInstance(); +} + +void WritableNativeArray::pushNull() { + exceptions::throwIfObjectAlreadyConsumed(this, "Array already consumed"); + array.push_back(nullptr); +} + +void WritableNativeArray::pushBoolean(jboolean value) { + exceptions::throwIfObjectAlreadyConsumed(this, "Array already consumed"); + array.push_back(value == JNI_TRUE); +} + +void WritableNativeArray::pushDouble(jdouble value) { + exceptions::throwIfObjectAlreadyConsumed(this, "Receiving array already consumed"); + array.push_back(value); +} + +void WritableNativeArray::pushInt(jint value) { + exceptions::throwIfObjectAlreadyConsumed(this, "Receiving array already consumed"); + array.push_back(value); +} + +void WritableNativeArray::pushString(jstring value) { + if (value == NULL) { + pushNull(); + return; + } + exceptions::throwIfObjectAlreadyConsumed(this, "Receiving array already consumed"); + array.push_back(wrap_alias(value)->toStdString()); +} + +void WritableNativeArray::pushNativeArray(WritableNativeArray* otherArray) { + if (otherArray == NULL) { + pushNull(); + return; + } + exceptions::throwIfObjectAlreadyConsumed(this, "Receiving array already consumed"); + exceptions::throwIfObjectAlreadyConsumed(otherArray, "Array to push already consumed"); + array.push_back(std::move(otherArray->array)); + otherArray->isConsumed = true; +} + +void WritableNativeArray::pushNativeMap(WritableNativeMap* map) { + if (map == NULL) { + pushNull(); + return; + } + exceptions::throwIfObjectAlreadyConsumed(this, "Receiving array already consumed"); + map->throwIfConsumed(); + array.push_back(map->consume()); +} + +void WritableNativeArray::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", WritableNativeArray::initHybrid), + makeNativeMethod("pushNull", WritableNativeArray::pushNull), + makeNativeMethod("pushBoolean", WritableNativeArray::pushBoolean), + makeNativeMethod("pushDouble", WritableNativeArray::pushDouble), + makeNativeMethod("pushInt", WritableNativeArray::pushInt), + makeNativeMethod("pushString", WritableNativeArray::pushString), + makeNativeMethod("pushNativeArray", WritableNativeArray::pushNativeArray), + makeNativeMethod("pushNativeMap", WritableNativeArray::pushNativeMap), + }); +} + +} // namespace react +} // namespace facebook diff --git a/ReactAndroid/src/main/jni/react/jni/WritableNativeArray.h b/ReactAndroid/src/main/jni/react/jni/WritableNativeArray.h new file mode 100644 index 00000000000000..e47769517f005e --- /dev/null +++ b/ReactAndroid/src/main/jni/react/jni/WritableNativeArray.h @@ -0,0 +1,35 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include + +#include "ReadableNativeArray.h" + +namespace facebook { +namespace react { + +struct WritableNativeMap; + +struct WritableNativeArray + : public jni::HybridClass { + static constexpr const char* kJavaDescriptor = "Lcom/facebook/react/bridge/WritableNativeArray;"; + + WritableNativeArray(); + static jni::local_ref initHybrid(jni::alias_ref); + + void pushNull(); + void pushBoolean(jboolean value); + void pushDouble(jdouble value); + void pushInt(jint value); + void pushString(jstring value); + void pushNativeArray(WritableNativeArray* otherArray); + void pushNativeMap(WritableNativeMap* map); + + static void registerNatives(); +}; + +} +} diff --git a/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.cpp b/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.cpp new file mode 100644 index 00000000000000..1a911b5159ce91 --- /dev/null +++ b/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.cpp @@ -0,0 +1,105 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "WritableNativeMap.h" + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +WritableNativeMap::WritableNativeMap() + : HybridBase(folly::dynamic::object()) {} + +WritableNativeMap::WritableNativeMap(folly::dynamic&& val) + : HybridBase(std::move(val)) { + if (!map_.isObject()) { + throw std::runtime_error("WritableNativeMap value must be an object."); + } +} + +local_ref WritableNativeMap::initHybrid(alias_ref) { + return makeCxxInstance(); +} + +folly::dynamic WritableNativeMap::consume() { + throwIfConsumed(); + isConsumed = true; + return std::move(map_); +} + +void WritableNativeMap::putNull(std::string key) { + throwIfConsumed(); + map_.insert(std::move(key), nullptr); +} + +void WritableNativeMap::putBoolean(std::string key, bool val) { + throwIfConsumed(); + map_.insert(std::move(key), val); +} + +void WritableNativeMap::putDouble(std::string key, double val) { + throwIfConsumed(); + map_.insert(std::move(key), val); +} + +void WritableNativeMap::putInt(std::string key, int val) { + throwIfConsumed(); + map_.insert(std::move(key), val); +} + +void WritableNativeMap::putString(std::string key, alias_ref val) { + if (!val) { + putNull(std::move(key)); + return; + } + throwIfConsumed(); + map_.insert(std::move(key), val->toString()); +} + +void WritableNativeMap::putNativeArray(std::string key, alias_ref val) { + if (!val) { + putNull(std::move(key)); + return; + } + throwIfConsumed(); + auto array = val->cthis(); + exceptions::throwIfObjectAlreadyConsumed(array, "Array to put already consumed"); + map_.insert(key, std::move(array->array)); + array->isConsumed = true; +} + +void WritableNativeMap::putNativeMap(std::string key, alias_ref val) { + if (!val) { + putNull(std::move(key)); + return; + } + throwIfConsumed(); + auto other = val->cthis(); + map_.insert(std::move(key), other->consume()); +} + +void WritableNativeMap::mergeNativeMap(ReadableNativeMap* other) { + throwIfConsumed(); + other->throwIfConsumed(); + + for (auto sourceIt : other->map_.items()) { + map_[sourceIt.first] = sourceIt.second; + } +} + +void WritableNativeMap::registerNatives() { + registerHybrid({ + makeNativeMethod("putNull", WritableNativeMap::putNull), + makeNativeMethod("putBoolean", WritableNativeMap::putBoolean), + makeNativeMethod("putDouble", WritableNativeMap::putDouble), + makeNativeMethod("putInt", WritableNativeMap::putInt), + makeNativeMethod("putString", WritableNativeMap::putString), + makeNativeMethod("putNativeArray", WritableNativeMap::putNativeArray), + makeNativeMethod("putNativeMap", WritableNativeMap::putNativeMap), + makeNativeMethod("mergeNativeMap", WritableNativeMap::mergeNativeMap), + makeNativeMethod("initHybrid", WritableNativeMap::initHybrid), + }); +} + +} // namespace react +} // namespace facebook diff --git a/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h b/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h new file mode 100644 index 00000000000000..cf9cd95a000d88 --- /dev/null +++ b/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h @@ -0,0 +1,40 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include + +#include "ReadableNativeMap.h" +#include "WritableNativeArray.h" + +namespace facebook { +namespace react { + +struct WritableNativeMap : jni::HybridClass { + static auto constexpr kJavaDescriptor = "Lcom/facebook/react/bridge/WritableNativeMap;"; + + WritableNativeMap(); + WritableNativeMap(folly::dynamic&& val); + + static jni::local_ref initHybrid(jni::alias_ref); + + folly::dynamic consume(); + + void putNull(std::string key); + void putBoolean(std::string key, bool val); + void putDouble(std::string key, double val); + void putInt(std::string key, int val); + void putString(std::string key, jni::alias_ref val); + void putNativeArray(std::string key, jni::alias_ref val); + void putNativeMap(std::string key, jni::alias_ref val); + void mergeNativeMap(ReadableNativeMap* other); + + static void registerNatives(); + + friend HybridBase; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp index 5dcfd3e62b3679..218f6c9d0ac2d3 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp @@ -144,7 +144,7 @@ jvalue extract(std::weak_ptr& instance, ExecutorToken token, char type break; case 'M': // HACK: Workaround for constructing ReadableNativeMap - value.l = ExposedReadableNativeArray(folly::dynamic::array(arg)).getMap(0); + value.l = ExposedReadableNativeArray(folly::dynamic::array(arg)).getMap(0).release(); break; case 'X': value.l = extractCallback(instance, token, arg).release(); From aaf557da52869d2728bce716d34b748af07a7ebd Mon Sep 17 00:00:00 2001 From: Christine Abernathy Date: Wed, 1 Jun 2016 17:52:21 -0700 Subject: [PATCH 186/843] Fix instructions so react-native packages are installed first Summary: Post React 15.x one of the files pulled in for documentation reside in that repo: NativeMethodsMixin.js This ensures that developers install react-native packages first (which includes React) so the file can be found when it's referenced in server/extractDocs.js **Test plan (required)** 1. Ran through the instructions. 2. Made sure http://localhost:8079/react-native/docs/nativemethodsmixin.html exists in the nav and is shows correctly. Closes https://github.com/facebook/react-native/pull/7882 Differential Revision: D3376395 Pulled By: caabernathy fbshipit-source-id: bfcef271516ed2b21f04d86114dfceb2ff35138c --- website/README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/website/README.md b/website/README.md index 3c633b91216b5e..d58fb8d6813544 100644 --- a/website/README.md +++ b/website/README.md @@ -1,8 +1,19 @@ +# Install prerequisites + +Before running the website, make sure you've run the following: + +```sh +git clone https://github.com/facebook/react-native.git +cd react-native +npm install +``` + # Run the website server -The first time, get all the dependencies loaded via +The first time, get all the website dependencies loaded via ```sh +cd website npm install ``` @@ -21,4 +32,3 @@ Anytime you change the contents, just refresh the page and it's going to be upda cd website npm run publish-website ``` - From 75c6bf5723581a74c780cb1b5681d9f552fe0a32 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Thu, 2 Jun 2016 07:05:39 -0700 Subject: [PATCH 187/843] Open sourced more instrumentation tests Reviewed By: avaly Differential Revision: D3373926 fbshipit-source-id: da520b61c5e74d515b853cf1d9548ed3e671aa50 --- .../CatalystMultitouchHandlingTestCase.java | 656 ++++++++++++++++ ...alystNativeJSToJavaParametersTestCase.java | 709 ++++++++++++++++++ ...talystNativeJavaToJSArgumentsTestCase.java | 379 ++++++++++ ReactAndroid/src/androidTest/js/Asserts.js | 32 + .../js/MultitouchHandlingTestAppModule.js | 64 ++ ReactAndroid/src/androidTest/js/TestBundle.js | 6 + .../js/TestJSToJavaParametersModule.js | 117 +++ .../js/TestJavaToJSArgumentsModule.js | 116 +++ 8 files changed, 2079 insertions(+) create mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystMultitouchHandlingTestCase.java create mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java create mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java create mode 100644 ReactAndroid/src/androidTest/js/Asserts.js create mode 100644 ReactAndroid/src/androidTest/js/MultitouchHandlingTestAppModule.js create mode 100644 ReactAndroid/src/androidTest/js/TestJSToJavaParametersModule.js create mode 100644 ReactAndroid/src/androidTest/js/TestJavaToJSArgumentsModule.js diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystMultitouchHandlingTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystMultitouchHandlingTestCase.java new file mode 100644 index 00000000000000..ec21f0c2f066da --- /dev/null +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystMultitouchHandlingTestCase.java @@ -0,0 +1,656 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.tests; + +import java.util.List; + +import android.view.MotionEvent; + +import com.facebook.react.testing.ReactInstanceSpecForTest; +import com.facebook.react.testing.ReactAppInstrumentationTestCase; +import com.facebook.react.testing.StringRecordingModule; + +/** + * Test case for verifying that multitouch events are directed to the React's view touch handlers + * properly + */ +public class CatalystMultitouchHandlingTestCase extends ReactAppInstrumentationTestCase { + + private final StringRecordingModule mRecordingModule = new StringRecordingModule(); + + @Override + protected String getReactApplicationKeyUnderTest() { + return "MultitouchHandlingTestAppModule"; + } + + @Override + protected ReactInstanceSpecForTest createReactInstanceSpecForTest() { + return new ReactInstanceSpecForTest() + .addNativeModule(mRecordingModule); + } + + /** + * In this test case we send pre-recorded stream of pinch out gesture and verify that we have + * recorded important touch events in JS module + */ + public void testMultitouchEvents() throws InterruptedException { + generateRecordedPinchTouchEvents(); + waitForBridgeAndUIIdle(); + + // Expect to receive at least 5 events (DOWN for each pointer, UP for each pointer and at least + // one MOVE event with both pointers down) + List calls = mRecordingModule.getCalls(); + + int moveWithBothPointersEventIndex = -1; + int startEventIndex = -1; + int startExtraPointerEventIndex = -1; + int endEventIndex = -1; + int endExtraPointerEventIndex = -1; + + for (int i = 0; i < calls.size(); i++) { + String call = calls.get(i); + if (call.equals("start;ExtraPointer")) { + assertEquals(-1, startExtraPointerEventIndex); + startExtraPointerEventIndex = i; + } else if (call.equals("end;ExtraPointer")) { + assertEquals(-1, endExtraPointerEventIndex); + endExtraPointerEventIndex = i; + } else if (call.equals("start;1")) { + assertEquals(-1, startEventIndex); + startEventIndex = i; + } else if (call.equals("end;0")) { + assertEquals(-1, endEventIndex); + endEventIndex = i; + } else if (call.equals("move;2")) { + // this will happen more than once, let's just capture the last occurence + moveWithBothPointersEventIndex = i; + } + } + + assertEquals(0, startEventIndex); + assertTrue(-1 != startExtraPointerEventIndex); + assertTrue(-1 != moveWithBothPointersEventIndex); + assertTrue(-1 != endExtraPointerEventIndex); + assertTrue(startExtraPointerEventIndex < moveWithBothPointersEventIndex); + assertTrue(endExtraPointerEventIndex > moveWithBothPointersEventIndex); + assertEquals(calls.size() - 1, endEventIndex); + } + + private MotionEvent.PointerProperties createPointerProps(int id, int toolType) { + MotionEvent.PointerProperties pointerProps = new MotionEvent.PointerProperties(); + pointerProps.id = id; + pointerProps.toolType = toolType; + return pointerProps; + } + + private MotionEvent.PointerCoords createPointerCoords(float x, float y) { + MotionEvent.PointerCoords pointerCoords = new MotionEvent.PointerCoords(); + pointerCoords.x = x; + pointerCoords.y = y; + return pointerCoords; + } + + private void dispatchEvent( + final int action, + final long start, + final long when, + final int pointerCount, + final MotionEvent.PointerProperties[] pointerProps, + final MotionEvent.PointerCoords[] pointerCoords) { + getRootView().post( + new Runnable() { + @Override + public void run() { + MotionEvent event = + MotionEvent.obtain(start, when, action, pointerCount, pointerProps, pointerCoords, 0, 0, 1.0f, 1.0f, 0, 0, 0, 0); + getRootView().dispatchTouchEvent(event); + event.recycle(); + } + }); + getInstrumentation().waitForIdleSync(); + } + + /** + * This method "replay" multi-touch gesture recorded with modified TouchesHelper class that + * generated this piece of code (see https://phabricator.fb.com/P19756940). + * This is not intended to be copied/reused and once we need to have more multitouch gestures + * in instrumentation tests we should either: + * - implement nice generator similar to {@link SingleTouchGestureGenerator} + * - implement gesture recorded that will record touch data using arbitrary format and then read + * this recorded touch sequence during tests instead of generating code like this + */ + private void generateRecordedPinchTouchEvents() { + // START OF GENERATED CODE + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[1]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[1]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(268.0f, 347.0f); + dispatchEvent(MotionEvent.ACTION_DOWN, 446560605, 446560605, 1, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[1]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[1]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(267.0f, 346.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560630, 1, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(267.0f, 346.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(225.0f, 542.0f); + dispatchEvent(MotionEvent.ACTION_POINTER_DOWN | (1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 446560605, 446560630, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(266.0f, 345.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(225.0f, 542.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560647, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(265.0f, 344.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(224.0f, 541.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560664, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(264.0f, 342.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(223.0f, 540.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560681, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(263.0f, 340.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(222.0f, 539.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560698, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(262.0f, 337.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(221.0f, 538.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560714, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(262.0f, 333.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(220.0f, 537.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560731, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(261.0f, 328.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(219.0f, 536.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560748, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(260.0f, 321.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(218.0f, 536.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560765, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(260.0f, 313.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(216.0f, 536.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560781, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(260.0f, 304.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(214.0f, 537.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560798, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(260.0f, 295.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(211.0f, 539.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560815, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(261.0f, 285.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(208.0f, 542.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560832, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(264.0f, 274.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(203.0f, 547.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560849, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(266.0f, 264.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(199.0f, 551.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560865, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(269.0f, 254.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(194.0f, 556.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560882, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(273.0f, 245.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(190.0f, 561.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560899, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(276.0f, 236.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(186.0f, 567.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560916, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(280.0f, 227.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(183.0f, 573.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560933, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(283.0f, 219.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(181.0f, 579.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560949, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(287.0f, 211.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(179.0f, 584.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560966, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(291.0f, 202.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(177.0f, 589.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446560983, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(296.0f, 193.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(175.0f, 593.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561000, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(301.0f, 184.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(174.0f, 598.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561016, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(307.0f, 176.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(173.0f, 603.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561033, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(313.0f, 168.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(172.0f, 608.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561050, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(317.0f, 160.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(171.0f, 613.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561067, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(320.0f, 154.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(170.0f, 619.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561084, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(323.0f, 149.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(169.0f, 624.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561100, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(325.0f, 145.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(168.0f, 628.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561117, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(328.0f, 141.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(167.0f, 632.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561134, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(331.0f, 137.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(166.0f, 636.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561151, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(334.0f, 134.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(165.0f, 639.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561167, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(337.0f, 131.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(164.0f, 643.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561184, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(338.0f, 128.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(164.0f, 646.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561201, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(340.0f, 126.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(164.0f, 649.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561218, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(341.0f, 124.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(163.0f, 652.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561234, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(342.0f, 122.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(163.0f, 655.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561251, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(343.0f, 120.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(162.0f, 659.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561268, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(344.0f, 118.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(161.0f, 664.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561285, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(345.0f, 116.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(160.0f, 667.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561302, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(346.0f, 115.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(158.0f, 670.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561318, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(347.0f, 114.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(157.0f, 673.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561335, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(348.0f, 113.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(156.0f, 676.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561352, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(348.0f, 112.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(155.0f, 677.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561369, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(349.0f, 111.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(154.0f, 678.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561386, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(349.0f, 110.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(153.0f, 679.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561402, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(349.0f, 109.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(152.0f, 680.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561419, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(349.0f, 110.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(151.0f, 680.0f); + dispatchEvent(MotionEvent.ACTION_MOVE, 446560605, 446561435, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[2]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(349.0f, 110.0f); + pointerProps[1] = createPointerProps(1, 1); + pointerCoords[1] = createPointerCoords(151.0f, 680.0f); + dispatchEvent(MotionEvent.ACTION_POINTER_UP | (0 << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 446560605, 446561443, 2, pointerProps, pointerCoords); + } + + { + MotionEvent.PointerProperties[] pointerProps = new MotionEvent.PointerProperties[1]; + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[1]; + pointerProps[0] = createPointerProps(0, 1); + pointerCoords[0] = createPointerCoords(151.0f, 680.0f); + dispatchEvent(MotionEvent.ACTION_UP, 446560605, 446561451, 1, pointerProps, pointerCoords); + } + // END OF GENERATED CODE + } + +} diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java new file mode 100644 index 00000000000000..f6130be3aaec7f --- /dev/null +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java @@ -0,0 +1,709 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.tests; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.InvalidIteratorException; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NoSuchKeyException; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableType; +import com.facebook.react.bridge.ReadableNativeMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.UnexpectedNativeTypeException; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.modules.systeminfo.AndroidInfoModule; +import com.facebook.react.testing.ReactIntegrationTestCase; +import com.facebook.react.testing.ReactTestHelper; +import com.facebook.react.uimanager.UIImplementation; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.views.view.ReactViewManager; + +import org.junit.Ignore; + +/** + * Integration test to verify passing various types of parameters from JS to Java works + */ +public class CatalystNativeJSToJavaParametersTestCase extends ReactIntegrationTestCase { + + private interface TestJSToJavaParametersModule extends JavaScriptModule { + void returnBasicTypes(); + + void returnArrayWithBasicTypes(); + void returnNestedArray(); + void returnArrayWithMaps(); + + void returnMapWithBasicTypes(); + void returnNestedMap(); + void returnMapWithArrays(); + + void returnArrayWithStringDoubleIntMapArrayBooleanNull(); + void returnMapWithStringDoubleIntMapArrayBooleanNull(); + + void returnMapForMerge1(); + void returnMapForMerge2(); + + void returnMapWithMultibyteUTF8CharacterString(); + void returnArrayWithMultibyteUTF8CharacterString(); + + void returnArrayWithLargeInts(); + void returnMapWithLargeInts(); + } + + private RecordingTestModule mRecordingTestModule; + private CatalystInstance mCatalystInstance; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + List viewManagers = Arrays.asList( + new ReactViewManager()); + final UIManagerModule mUIManager = new UIManagerModule( + getContext(), + viewManagers, + new UIImplementation(getContext(), viewManagers)); + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + mUIManager.onHostResume(); + } + }); + waitForIdleSync(); + + mRecordingTestModule = new RecordingTestModule(); + mCatalystInstance = ReactTestHelper.catalystInstanceBuilder(this) + .addNativeModule(mRecordingTestModule) + .addNativeModule(new AndroidInfoModule()) + .addNativeModule(mUIManager) + .addJSModule(TestJSToJavaParametersModule.class) + .build(); + } + + public void testBasicTypes() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnBasicTypes(); + waitForBridgeAndUIIdle(); + + List basicTypesCalls = mRecordingTestModule.getBasicTypesCalls(); + assertEquals(1, basicTypesCalls.size()); + + Object[] args = basicTypesCalls.get(0); + assertEquals("foo", args[0]); + assertEquals(3.14, args[1]); + assertEquals(true, args[2]); + assertNull(args[3]); + } + + public void testArrayWithBasicTypes() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnArrayWithBasicTypes(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getArrayCalls(); + assertEquals(1, calls.size()); + ReadableArray array = calls.get(0); + assertNotNull(array); + assertEquals(5, array.size()); + assertFalse(array.isNull(0)); + assertEquals("foo", array.getString(0)); + assertFalse(array.isNull(1)); + assertEquals(3.14, array.getDouble(1)); + assertFalse(array.isNull(2)); + assertEquals(-111, array.getInt(2)); + assertFalse(array.isNull(3)); + assertTrue(array.getBoolean(3)); + assertTrue(array.isNull(4)); + assertEquals(null, array.getString(4)); + assertEquals(null, array.getMap(4)); + assertEquals(null, array.getArray(4)); + } + + public void testNestedArray() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnNestedArray(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getArrayCalls(); + assertEquals(1, calls.size()); + ReadableArray array = calls.get(0); + assertNotNull(array); + assertEquals(2, array.size()); + assertEquals("we", array.getString(0)); + + assertFalse(array.isNull(1)); + ReadableArray subArray = array.getArray(1); + assertEquals(2, subArray.size()); + assertEquals("have", subArray.getString(0)); + + subArray = subArray.getArray(1); + assertEquals(2, subArray.size()); + assertEquals("to", subArray.getString(0)); + + subArray = subArray.getArray(1); + assertEquals(2, subArray.size()); + assertEquals("go", subArray.getString(0)); + + subArray = subArray.getArray(1); + assertEquals(1, subArray.size()); + assertEquals("deeper", subArray.getString(0)); + } + + public void testArrayWithMaps() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnArrayWithMaps(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getArrayCalls(); + assertEquals(1, calls.size()); + ReadableArray array = calls.get(0); + assertEquals(2, array.size()); + + assertFalse(array.isNull(0)); + ReadableMap m1 = array.getMap(0); + ReadableMap m2 = array.getMap(1); + + assertEquals("m1v1", m1.getString("m1k1")); + assertEquals("m1v2", m1.getString("m1k2")); + assertEquals("m2v1", m2.getString("m2k1")); + } + + public void testMapWithBasicTypes() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnMapWithBasicTypes(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getMapCalls(); + assertEquals(1, calls.size()); + ReadableMap map = calls.get(0); + assertNotNull(map); + + assertTrue(map.hasKey("stringKey")); + assertFalse(map.isNull("stringKey")); + assertEquals("stringValue", map.getString("stringKey")); + + assertTrue(map.hasKey("doubleKey")); + assertFalse(map.isNull("doubleKey")); + assertTrue(Math.abs(3.14 - map.getDouble("doubleKey")) < .0001); + + assertTrue(map.hasKey("intKey")); + assertFalse(map.isNull("intKey")); + assertEquals(-11, map.getInt("intKey")); + + assertTrue(map.hasKey("booleanKey")); + assertFalse(map.isNull("booleanKey")); + assertTrue(map.getBoolean("booleanKey")); + + assertTrue(map.hasKey("nullKey")); + assertTrue(map.isNull("nullKey")); + assertNull(map.getString("nullKey")); + assertNull(map.getMap("nullKey")); + assertNull(map.getArray("nullKey")); + + assertFalse(map.hasKey("nonExistentKey")); + } + + public void testNestedMap() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnNestedMap(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getMapCalls(); + assertEquals(1, calls.size()); + ReadableMap map = calls.get(0); + assertNotNull(map); + + assertTrue(map.hasKey("weHaveToGoDeeper")); + assertFalse(map.isNull("weHaveToGoDeeper")); + ReadableMap nestedMap = map.getMap("weHaveToGoDeeper"); + assertTrue(nestedMap.hasKey("inception")); + assertTrue(nestedMap.getBoolean("inception")); + } + + public void testMapParameterWithArrays() throws InterruptedException { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnMapWithArrays(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getMapCalls(); + assertEquals(1, calls.size()); + ReadableMap map = calls.get(0); + assertNotNull(map); + + ReadableArray arrayParameter; + assertTrue(map.hasKey("empty")); + arrayParameter = map.getArray("empty"); + assertNotNull(arrayParameter); + assertEquals(0, arrayParameter.size()); + + assertTrue(map.hasKey("ints")); + assertFalse(map.isNull("ints")); + arrayParameter = map.getArray("ints"); + assertNotNull(arrayParameter); + assertEquals(2, arrayParameter.size()); + assertEquals(43, arrayParameter.getInt(0)); + assertEquals(44, arrayParameter.getInt(1)); + + assertTrue(map.hasKey("mixed")); + arrayParameter = map.getArray("mixed"); + assertNotNull(arrayParameter); + assertEquals(3, arrayParameter.size()); + assertEquals(77, arrayParameter.getInt(0)); + assertEquals("string", arrayParameter.getString(1)); + ReadableArray nestedArray = arrayParameter.getArray(2); + assertEquals(2, nestedArray.size()); + } + + public void testMapParameterDump() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnMapWithBasicTypes(); + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnNestedMap(); + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnMapWithArrays(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getMapCalls(); + assertEquals(3, calls.size()); + + // App should not crash while generating debug string representation of arguments + assertNotNull(calls.get(0).toString()); + assertNotNull(calls.get(1).toString()); + assertNotNull(calls.get(2).toString()); + } + + public void testGetTypeFromArray() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class) + .returnArrayWithStringDoubleIntMapArrayBooleanNull(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getArrayCalls(); + assertEquals(1, calls.size()); + ReadableArray array = calls.get(0); + + assertEquals(ReadableType.String, array.getType(0)); + assertEquals(ReadableType.Number, array.getType(1)); + assertEquals(ReadableType.Number, array.getType(2)); + assertEquals(ReadableType.Map, array.getType(3)); + assertEquals(ReadableType.Array, array.getType(4)); + assertEquals(ReadableType.Boolean, array.getType(5)); + assertEquals(ReadableType.Null, array.getType(6)); + } + + public void testGetTypeFromMap() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class) + .returnMapWithStringDoubleIntMapArrayBooleanNull(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getMapCalls(); + assertEquals(1, calls.size()); + ReadableMap map = calls.get(0); + + assertEquals(ReadableType.String, map.getType("string")); + assertEquals(ReadableType.Number, map.getType("double")); + assertEquals(ReadableType.Number, map.getType("int")); + assertEquals(ReadableType.Map, map.getType("map")); + assertEquals(ReadableType.Array, map.getType("array")); + assertEquals(ReadableType.Boolean, map.getType("boolean")); + assertEquals(ReadableType.Null, map.getType("null")); + } + + public void testGetWrongTypeFromArray() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class) + .returnArrayWithStringDoubleIntMapArrayBooleanNull(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getArrayCalls(); + assertEquals(1, calls.size()); + ReadableArray array = calls.get(0); + + assertUnexpectedTypeExceptionThrown(array, 0, "boolean"); + assertUnexpectedTypeExceptionThrown(array, 1, "string"); + assertUnexpectedTypeExceptionThrown(array, 2, "array"); + assertUnexpectedTypeExceptionThrown(array, 3, "double"); + assertUnexpectedTypeExceptionThrown(array, 4, "map"); + assertUnexpectedTypeExceptionThrown(array, 5, "array"); + } + + public void testGetWrongTypeFromMap() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class) + .returnMapWithStringDoubleIntMapArrayBooleanNull(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getMapCalls(); + assertEquals(1, calls.size()); + ReadableMap map = calls.get(0); + + assertUnexpectedTypeExceptionThrown(map, "string", "double"); + assertUnexpectedTypeExceptionThrown(map, "double", "map"); + assertUnexpectedTypeExceptionThrown(map, "int", "boolean"); + assertUnexpectedTypeExceptionThrown(map, "map", "array"); + assertUnexpectedTypeExceptionThrown(map, "array", "boolean"); + assertUnexpectedTypeExceptionThrown(map, "boolean", "string"); + } + + public void testArrayOutOfBoundsExceptionThrown() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnArrayWithBasicTypes(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getArrayCalls(); + assertEquals(1, calls.size()); + ReadableArray array = calls.get(0); + assertNotNull(array); + + assertArrayOutOfBoundsExceptionThrown(array, -1, "boolean"); + assertArrayOutOfBoundsExceptionThrown(array, -1, "string"); + assertArrayOutOfBoundsExceptionThrown(array, -1, "double"); + assertArrayOutOfBoundsExceptionThrown(array, -1, "int"); + assertArrayOutOfBoundsExceptionThrown(array, -1, "map"); + assertArrayOutOfBoundsExceptionThrown(array, -1, "array"); + + assertArrayOutOfBoundsExceptionThrown(array, 10, "boolean"); + assertArrayOutOfBoundsExceptionThrown(array, 10, "string"); + assertArrayOutOfBoundsExceptionThrown(array, 10, "double"); + assertArrayOutOfBoundsExceptionThrown(array, 10, "int"); + assertArrayOutOfBoundsExceptionThrown(array, 10, "map"); + assertArrayOutOfBoundsExceptionThrown(array, 10, "array"); + } + + public void testNoSuchKeyExceptionThrown() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnMapWithBasicTypes(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getMapCalls(); + assertEquals(1, calls.size()); + ReadableMap map = calls.get(0); + assertNotNull(map); + + assertNoSuchKeyExceptionThrown(map, "noSuchKey", "double"); + assertNoSuchKeyExceptionThrown(map, "noSuchKey", "int"); + assertNoSuchKeyExceptionThrown(map, "noSuchKey", "map"); + assertNoSuchKeyExceptionThrown(map, "noSuchKey", "array"); + assertNoSuchKeyExceptionThrown(map, "noSuchKey", "boolean"); + assertNoSuchKeyExceptionThrown(map, "noSuchKey", "string"); + } + + public void testIntOutOfRangeThrown() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnArrayWithLargeInts(); + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnMapWithLargeInts(); + waitForBridgeAndUIIdle(); + + assertEquals(1, mRecordingTestModule.getArrayCalls().size()); + assertEquals(1, mRecordingTestModule.getMapCalls().size()); + + ReadableArray array = mRecordingTestModule.getArrayCalls().get(0); + assertNotNull(array); + + ReadableMap map = mRecordingTestModule.getMapCalls().get(0); + assertNotNull(map); + + assertEquals(ReadableType.Number, array.getType(0)); + assertUnexpectedTypeExceptionThrown(array, 0, "int"); + assertEquals(ReadableType.Number, array.getType(1)); + assertUnexpectedTypeExceptionThrown(array, 1, "int"); + + assertEquals(ReadableType.Number, map.getType("first")); + assertUnexpectedTypeExceptionThrown(map, "first", "int"); + assertEquals(ReadableType.Number, map.getType("second")); + assertUnexpectedTypeExceptionThrown(map, "second", "int"); + } + + public void testMapMerging() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnMapForMerge1(); + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnMapForMerge2(); + waitForBridgeAndUIIdle(); + + List maps = mRecordingTestModule.getMapCalls(); + assertEquals(2, maps.size()); + + WritableMap dest = new WritableNativeMap(); + dest.merge(maps.get(0)); + dest.merge(maps.get(1)); + + assertTrue(dest.hasKey("a")); + assertTrue(dest.hasKey("b")); + assertTrue(dest.hasKey("c")); + assertTrue(dest.hasKey("d")); + assertTrue(dest.hasKey("e")); + assertTrue(dest.hasKey("f")); + assertTrue(dest.hasKey("newkey")); + + assertEquals("overwrite", dest.getString("a")); + assertEquals(41, dest.getInt("b")); + assertEquals("string", dest.getString("c")); + assertEquals(77, dest.getInt("d")); + assertTrue(dest.isNull("e")); + assertEquals(3, dest.getArray("f").size()); + assertEquals("newvalue", dest.getString("newkey")); + } + + public void testMapAccessibleAfterMerge() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnMapForMerge1(); + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnMapForMerge2(); + waitForBridgeAndUIIdle(); + + List maps = mRecordingTestModule.getMapCalls(); + assertEquals(2, maps.size()); + + WritableMap dest = new WritableNativeMap(); + dest.merge(maps.get(0)); + dest.merge(maps.get(1)); + + ReadableMap source = maps.get(1); + + assertTrue(source.hasKey("a")); + assertTrue(source.hasKey("d")); + assertTrue(source.hasKey("e")); + assertTrue(source.hasKey("f")); + assertTrue(source.hasKey("newkey")); + + assertFalse(source.hasKey("b")); + assertFalse(source.hasKey("c")); + + assertEquals("overwrite", source.getString("a")); + assertEquals(77, source.getInt("d")); + assertTrue(source.isNull("e")); + assertEquals(3, source.getArray("f").size()); + assertEquals("newvalue", source.getString("newkey")); + } + + public void testMapIterateOverMapWithBasicTypes() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnMapWithBasicTypes(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getMapCalls(); + assertEquals(1, calls.size()); + ReadableNativeMap map = (ReadableNativeMap) calls.get(0); + assertNotNull(map); + + ReadableMapKeySetIterator mapIterator = map.keySetIterator(); + Set keys = new HashSet(); + while (mapIterator.hasNextKey()) { + keys.add(mapIterator.nextKey()); + } + + Set expectedKeys = new HashSet( + Arrays.asList("stringKey", "doubleKey", "intKey", "booleanKey", "nullKey")); + assertEquals(keys, expectedKeys); + } + + public void testMapIterateOverNestedMaps() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnNestedMap(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getMapCalls(); + assertEquals(1, calls.size()); + ReadableNativeMap map = (ReadableNativeMap) calls.get(0); + assertNotNull(map); + + ReadableMapKeySetIterator firstLevelIterator = map.keySetIterator(); + String firstLevelKey = firstLevelIterator.nextKey(); + assertEquals(firstLevelKey, "weHaveToGoDeeper"); + + ReadableNativeMap secondMap = map.getMap("weHaveToGoDeeper"); + ReadableMapKeySetIterator secondLevelIterator = secondMap.keySetIterator(); + String secondLevelKey = secondLevelIterator.nextKey(); + assertEquals(secondLevelKey, "inception"); + assertTrue(secondMap.getBoolean(secondLevelKey)); + } + + public void testInvalidIteratorExceptionThrown() { + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class).returnMapWithBasicTypes(); + waitForBridgeAndUIIdle(); + + List calls = mRecordingTestModule.getMapCalls(); + assertEquals(1, calls.size()); + ReadableNativeMap map = (ReadableNativeMap) calls.get(0); + assertNotNull(map); + + ReadableMapKeySetIterator mapIterator = map.keySetIterator(); + while (mapIterator.hasNextKey()) { + mapIterator.nextKey(); + } + assertInvalidIteratorExceptionThrown(mapIterator); + } + + public void testStringWithMultibyteUTF8Characters() { + TestJSToJavaParametersModule jsModule = + mCatalystInstance.getJSModule(TestJSToJavaParametersModule.class); + jsModule.returnMapWithMultibyteUTF8CharacterString(); + jsModule.returnArrayWithMultibyteUTF8CharacterString(); + waitForBridgeAndUIIdle(); + + List maps = mRecordingTestModule.getMapCalls(); + assertEquals(1, maps.size()); + + ReadableMap map = maps.get(0); + assertEquals("a", map.getString("one-byte")); + assertEquals("\u00A2", map.getString("two-bytes")); + assertEquals("\u20AC", map.getString("three-bytes")); + assertEquals("\uD83D\uDE1C", map.getString("four-bytes")); + assertEquals( + "\u017C\u00F3\u0142\u0107 g\u0119\u015Bl\u0105 \u6211 \uD83D\uDE0E ja\u017A\u0107", + map.getString("mixed")); + + List arrays = mRecordingTestModule.getArrayCalls(); + assertEquals(1, arrays.size()); + + ReadableArray array = arrays.get(0); + assertEquals("a", array.getString(0)); + assertEquals("\u00A2", array.getString(1)); + assertEquals("\u20AC", array.getString(2)); + assertEquals("\uD83D\uDE1C", array.getString(3)); + assertEquals( + "\u017C\u00F3\u0142\u0107 g\u0119\u015Bl\u0105 \u6211 \uD83D\uDE0E ja\u017A\u0107", + array.getString(4)); + } + + private void assertUnexpectedTypeExceptionThrown( + ReadableArray array, + int index, + String typeToAskFor) { + boolean gotException = false; + try { + arrayGetByType(array, index, typeToAskFor); + } catch (UnexpectedNativeTypeException expected) { + gotException = true; + } + + assertTrue(gotException); + } + + private void assertUnexpectedTypeExceptionThrown( + ReadableMap map, + String key, + String typeToAskFor) { + boolean gotException = false; + try { + mapGetByType(map, key, typeToAskFor); + } catch (UnexpectedNativeTypeException expected) { + gotException = true; + } + + assertTrue(gotException); + } + + private void assertArrayOutOfBoundsExceptionThrown( + ReadableArray array, + int index, + String typeToAskFor) { + boolean gotException = false; + try { + arrayGetByType(array, index, typeToAskFor); + } catch (ArrayIndexOutOfBoundsException expected) { + gotException = true; + } + + assertTrue(gotException); + } + + private void assertNoSuchKeyExceptionThrown( + ReadableMap map, + String key, + String typeToAskFor) { + boolean gotException = false; + try { + mapGetByType(map, key, typeToAskFor); + } catch (NoSuchKeyException expected) { + gotException = true; + } + + assertTrue(gotException); + } + + private static void assertInvalidIteratorExceptionThrown( + ReadableMapKeySetIterator iterator) { + boolean gotException = false; + try { + iterator.nextKey(); + } catch (InvalidIteratorException expected) { + gotException = true; + } + + assertTrue(gotException); + } + + private void arrayGetByType(ReadableArray array, int index, String typeToAskFor) { + if (typeToAskFor.equals("double")) { + array.getDouble(index); + } else if (typeToAskFor.equals("int")) { + array.getInt(index); + } else if (typeToAskFor.equals("string")) { + array.getString(index); + } else if (typeToAskFor.equals("array")) { + array.getArray(index); + } else if (typeToAskFor.equals("map")) { + array.getMap(index); + } else if (typeToAskFor.equals("boolean")) { + array.getBoolean(index); + } else { + throw new RuntimeException("Unknown type: " + typeToAskFor); + } + } + + private void mapGetByType(ReadableMap map, String key, String typeToAskFor) { + if (typeToAskFor.equals("double")) { + map.getDouble(key); + } else if (typeToAskFor.equals("int")) { + map.getInt(key); + } else if (typeToAskFor.equals("string")) { + map.getString(key); + } else if (typeToAskFor.equals("array")) { + map.getArray(key); + } else if (typeToAskFor.equals("map")) { + map.getMap(key); + } else if (typeToAskFor.equals("boolean")) { + map.getBoolean(key); + } else { + throw new RuntimeException("Unknown type: " + typeToAskFor); + } + } + + private class RecordingTestModule extends BaseJavaModule { + + private final List mBasicTypesCalls = new ArrayList(); + private final List mArrayCalls = new ArrayList(); + private final List mMapCalls = new ArrayList(); + + @Override + public String getName() { + return "Recording"; + } + + @ReactMethod + public void receiveBasicTypes(String s, double d, boolean b, String nullableString) { + mBasicTypesCalls.add(new Object[]{s, d, b, nullableString}); + } + + @ReactMethod + public void receiveArray(ReadableArray array) { + mArrayCalls.add(array); + } + + @ReactMethod + public void receiveMap(ReadableMap map) { + mMapCalls.add(map); + } + + public List getBasicTypesCalls() { + return mBasicTypesCalls; + } + + public List getArrayCalls() { + return mArrayCalls; + } + + public List getMapCalls() { + return mMapCalls; + } + } +} diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java new file mode 100644 index 00000000000000..fd6b1d779b2d68 --- /dev/null +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java @@ -0,0 +1,379 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.tests; + +import java.util.Arrays; +import java.util.List; + +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.ObjectAlreadyConsumedException; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.testing.AssertModule; +import com.facebook.react.testing.ReactIntegrationTestCase; +import com.facebook.react.testing.ReactTestHelper; +import com.facebook.react.uimanager.UIImplementation; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.views.view.ReactViewManager; + +/** + * Test marshalling arguments from Java to JS to appropriate native classes. + */ +public class CatalystNativeJavaToJSArgumentsTestCase extends ReactIntegrationTestCase { + + private interface TestJavaToJSArgumentsModule extends JavaScriptModule { + void receiveBasicTypes(String s, double d, boolean b, String nullString); + + void receiveArrayWithBasicTypes(WritableArray array); + void receiveNestedArray(WritableArray nestedArray); + void receiveArrayWithMaps(WritableArray arrayWithMaps); + + void receiveMapWithBasicTypes(WritableMap map); + void receiveNestedMap(WritableMap nestedMap); + void receiveMapWithArrays(WritableMap mapWithArrays); + void receiveMapAndArrayWithNullValues( + WritableMap map, + WritableArray array); + void receiveMapWithMultibyteUTF8CharacterString(WritableMap map); + void receiveArrayWithMultibyteUTF8CharacterString(WritableArray array); + } + + private AssertModule mAssertModule; + private CatalystInstance mInstance; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + List viewManagers = Arrays.asList( + new ReactViewManager()); + final UIManagerModule mUIManager = new UIManagerModule( + getContext(), + viewManagers, + new UIImplementation(getContext(), viewManagers)); + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + mUIManager.onHostResume(); + } + }); + waitForIdleSync(); + + mAssertModule = new AssertModule(); + + mInstance = ReactTestHelper.catalystInstanceBuilder(this) + .addNativeModule(mAssertModule) + .addJSModule(TestJavaToJSArgumentsModule.class) + .addNativeModule(mUIManager) + .build(); + } + + public void testBasicTypes() { + mInstance.getJSModule(TestJavaToJSArgumentsModule.class) + .receiveBasicTypes("foo", 3.14, true, null); + waitForBridgeAndUIIdle(); + mAssertModule.verifyAssertsAndReset(); + } + + public void testArrayWithBasicTypes() { + WritableNativeArray array = new WritableNativeArray(); + array.pushString("red panda"); + array.pushDouble(1.19); + array.pushBoolean(true); + array.pushNull(); + + mInstance.getJSModule(TestJavaToJSArgumentsModule.class).receiveArrayWithBasicTypes(array); + waitForBridgeAndUIIdle(); + mAssertModule.verifyAssertsAndReset(); + } + + public void testNestedArray() { + WritableNativeArray level1 = new WritableNativeArray(); + WritableNativeArray level2 = new WritableNativeArray(); + WritableNativeArray level3 = new WritableNativeArray(); + level3.pushString("level3"); + level2.pushString("level2"); + level2.pushArray(level3); + level1.pushString("level1"); + level1.pushArray(level2); + + mInstance.getJSModule(TestJavaToJSArgumentsModule.class).receiveNestedArray(level1); + waitForBridgeAndUIIdle(); + mAssertModule.verifyAssertsAndReset(); + } + + public void testArrayWithMaps() { + WritableNativeMap m1 = new WritableNativeMap(); + WritableNativeMap m2 = new WritableNativeMap(); + m1.putString("m1k1", "m1v1"); + m1.putString("m1k2", "m1v2"); + m2.putString("m2k1", "m2v1"); + + WritableNativeArray array = new WritableNativeArray(); + array.pushMap(m1); + array.pushMap(m2); + mInstance.getJSModule(TestJavaToJSArgumentsModule.class).receiveArrayWithMaps(array); + waitForBridgeAndUIIdle(); + mAssertModule.verifyAssertsAndReset(); + } + + public void testMapWithBasicTypes() { + WritableNativeMap map = new WritableNativeMap(); + map.putString("stringKey", "stringValue"); + map.putDouble("doubleKey", 3.14); + map.putBoolean("booleanKey", true); + map.putNull("nullKey"); + + mInstance.getJSModule(TestJavaToJSArgumentsModule.class).receiveMapWithBasicTypes(map); + waitForBridgeAndUIIdle(); + mAssertModule.verifyAssertsAndReset(); + } + + public void testNestedMap() { + WritableNativeMap map = new WritableNativeMap(); + WritableNativeMap nestedMap = new WritableNativeMap(); + nestedMap.putString("animals", "foxes"); + map.putMap("nestedMap", nestedMap); + + mInstance.getJSModule(TestJavaToJSArgumentsModule.class).receiveNestedMap(map); + waitForBridgeAndUIIdle(); + mAssertModule.verifyAssertsAndReset(); + } + + public void testMapWithArrays() { + WritableNativeMap map = new WritableNativeMap(); + WritableNativeArray a1 = new WritableNativeArray(); + WritableNativeArray a2 = new WritableNativeArray(); + a1.pushDouble(3); + a1.pushDouble(1); + a1.pushDouble(4); + a2.pushDouble(1); + a2.pushDouble(9); + map.putArray("array1", a1); + map.putArray("array2", a2); + + mInstance.getJSModule(TestJavaToJSArgumentsModule.class).receiveMapWithArrays(map); + waitForBridgeAndUIIdle(); + mAssertModule.verifyAssertsAndReset(); + } + + public void testMapWithNullStringValue() { + WritableNativeMap map = new WritableNativeMap(); + map.putString("string", null); + map.putArray("array", null); + map.putMap("map", null); + + WritableNativeArray array = new WritableNativeArray(); + array.pushString(null); + array.pushArray(null); + array.pushMap(null); + + mInstance.getJSModule(TestJavaToJSArgumentsModule.class) + .receiveMapAndArrayWithNullValues(map, array); + waitForBridgeAndUIIdle(); + mAssertModule.verifyAssertsAndReset(); + } + + public void testStringWithMultibyteUTF8Characters() { + TestJavaToJSArgumentsModule jsModule = mInstance.getJSModule(TestJavaToJSArgumentsModule.class); + + WritableNativeMap map = new WritableNativeMap(); + map.putString("two-bytes", "\u00A2"); + map.putString("three-bytes", "\u20AC"); + map.putString("four-bytes", "\uD83D\uDE1C"); + map.putString( + "mixed", + "\u017C\u00F3\u0142\u0107 g\u0119\u015Bl\u0105 \u6211 \uD83D\uDE0E ja\u017A\u0107"); + + jsModule.receiveMapWithMultibyteUTF8CharacterString(map); + waitForBridgeAndUIIdle(); + mAssertModule.verifyAssertsAndReset(); + + WritableArray array = new WritableNativeArray(); + array.pushString("\u00A2"); + array.pushString("\u20AC"); + array.pushString("\uD83D\uDE1C"); + array.pushString( + "\u017C\u00F3\u0142\u0107 g\u0119\u015Bl\u0105 \u6211 \uD83D\uDE0E ja\u017A\u0107"); + + jsModule.receiveArrayWithMultibyteUTF8CharacterString(array); + waitForBridgeAndUIIdle(); + mAssertModule.verifyAssertsAndReset(); + } + + public void testThrowWhenArrayReusedInArray() { + boolean gotException = false; + try { + WritableNativeArray array1 = new WritableNativeArray(); + WritableNativeArray array2 = new WritableNativeArray(); + WritableNativeArray child = new WritableNativeArray(); + array1.pushArray(child); + array2.pushArray(child); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + } + + public void testThrowWhenArrayReusedInMap() { + boolean gotException = false; + try { + WritableNativeMap map1 = new WritableNativeMap(); + WritableNativeMap map2 = new WritableNativeMap(); + WritableNativeArray child = new WritableNativeArray(); + map1.putArray("child", child); + map2.putArray("child", child); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + } + + public void testThrowWhenMapReusedInArray() { + boolean gotException = false; + try { + WritableNativeArray array1 = new WritableNativeArray(); + WritableNativeArray array2 = new WritableNativeArray(); + WritableNativeMap child = new WritableNativeMap(); + array1.pushMap(child); + array2.pushMap(child); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + } + + public void testThrowWhenMapReusedInMap() { + boolean gotException = false; + try { + WritableNativeMap map1 = new WritableNativeMap(); + WritableNativeMap map2 = new WritableNativeMap(); + WritableNativeMap child = new WritableNativeMap(); + map1.putMap("child", child); + map2.putMap("child", child); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + } + + public void testThrowWhenAddToConsumedArray() { + WritableNativeArray array = new WritableNativeArray(); + WritableNativeArray parent = new WritableNativeArray(); + parent.pushArray(array); + + boolean gotException = false; + try { + array.pushNull(); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + + gotException = false; + try { + array.pushBoolean(true); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + + gotException = false; + try { + array.pushDouble(1); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + + gotException = false; + try { + array.pushString("foo"); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + + gotException = false; + try { + array.pushArray(new WritableNativeArray()); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + + gotException = false; + try { + array.pushMap(new WritableNativeMap()); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + } + + public void testThrowWhenAddToConsumedMap() { + WritableNativeMap map = new WritableNativeMap(); + WritableNativeArray parent = new WritableNativeArray(); + parent.pushMap(map); + + boolean gotException = false; + try { + map.putNull("key"); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + + gotException = false; + try { + map.putBoolean("key", true); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + + gotException = false; + try { + map.putDouble("key", 1); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + + gotException = false; + try { + map.putString("key", "foo"); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + + gotException = false; + try { + map.putArray("key", new WritableNativeArray()); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + + gotException = false; + try { + map.putMap("key", new WritableNativeMap()); + } catch (ObjectAlreadyConsumedException e) { + gotException = true; + } + assertTrue(gotException); + } +} diff --git a/ReactAndroid/src/androidTest/js/Asserts.js b/ReactAndroid/src/androidTest/js/Asserts.js new file mode 100644 index 00000000000000..9a4e9654ad550c --- /dev/null +++ b/ReactAndroid/src/androidTest/js/Asserts.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule Asserts + */ + +'use strict'; + +var Assert = require('NativeModules').Assert; + +var Asserts = { + assertEquals: function(expected, actual, msg) { + if (expected !== actual) { + Assert.fail( + msg || + 'Expected: ' + expected + ', received: ' + actual + '\n' + + 'at ' + (new Error()).stack); + } else { + Assert.success(); + } + }, + assertTrue: function(expr, msg) { + Asserts.assertEquals(true, expr, msg); + }, +}; + +module.exports = Asserts; diff --git a/ReactAndroid/src/androidTest/js/MultitouchHandlingTestAppModule.js b/ReactAndroid/src/androidTest/js/MultitouchHandlingTestAppModule.js new file mode 100644 index 00000000000000..7c51afbee37c4c --- /dev/null +++ b/ReactAndroid/src/androidTest/js/MultitouchHandlingTestAppModule.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule MultitouchHandlingTestAppModule + */ + +'use strict'; + +var React = require('React'); +var Recording = require('NativeModules').Recording; +var StyleSheet = require('StyleSheet'); +var TouchEventUtils = require('fbjs/lib/TouchEventUtils'); +var View = require('View'); + +var TouchTestApp = React.createClass({ + handleStartShouldSetResponder: function(e) { + return true; + }, + handleOnResponderMove: function(e) { + e = TouchEventUtils.extractSingleTouch(e.nativeEvent); + Recording.record('move;' + e.touches.length); + }, + handleResponderStart: function(e) { + e = TouchEventUtils.extractSingleTouch(e.nativeEvent); + if (e.touches) { + Recording.record('start;' + e.touches.length); + } else { + Recording.record('start;ExtraPointer'); + } + }, + handleResponderEnd: function(e) { + e = TouchEventUtils.extractSingleTouch(e.nativeEvent); + if (e.touches) { + Recording.record('end;' + e.touches.length); + } else { + Recording.record('end;ExtraPointer'); + } + }, + render: function() { + return ( + + ); + }, +}); + +var styles = StyleSheet.create({ + container: { + flex: 1, + }, +}); + +module.exports = TouchTestApp; diff --git a/ReactAndroid/src/androidTest/js/TestBundle.js b/ReactAndroid/src/androidTest/js/TestBundle.js index 9af72339c2d606..39eb37c7bcdc24 100644 --- a/ReactAndroid/src/androidTest/js/TestBundle.js +++ b/ReactAndroid/src/androidTest/js/TestBundle.js @@ -15,6 +15,8 @@ console.disableYellowBox = true; // Include callable JS modules first, in case one of the other ones below throws require('ProgressBarTestModule'); require('ViewRenderingTestModule'); +require('TestJavaToJSArgumentsModule'); +require('TestJSToJavaParametersModule'); require('PickerAndroidTestModule'); require('CatalystRootViewTestModule'); @@ -40,6 +42,10 @@ var apps = [ appKey: 'HorizontalScrollViewTestApp', component: () => require('ScrollViewTestModule').HorizontalScrollViewTestApp, }, +{ + appKey: 'MultitouchHandlingTestAppModule', + component: () => require('MultitouchHandlingTestAppModule') +}, { appKey: 'PickerAndroidTestApp', component: () => require('PickerAndroidTestModule').PickerAndroidTestApp, diff --git a/ReactAndroid/src/androidTest/js/TestJSToJavaParametersModule.js b/ReactAndroid/src/androidTest/js/TestJSToJavaParametersModule.js new file mode 100644 index 00000000000000..57380f3bcba5d7 --- /dev/null +++ b/ReactAndroid/src/androidTest/js/TestJSToJavaParametersModule.js @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule TestJSToJavaParametersModule + */ + +'use strict'; + +var BatchedBridge = require('BatchedBridge'); +var Recording = require('NativeModules').Recording; + +var TestJSToJavaParametersModule = { + returnBasicTypes: function() { + Recording.receiveBasicTypes('foo', 3.14, true, null); + }, + returnArrayWithBasicTypes: function() { + Recording.receiveArray(['foo', 3.14, -111, true, null]); + }, + returnNestedArray: function() { + Recording.receiveArray(['we', ['have', ['to', ['go', ['deeper']]]]]); + }, + returnArrayWithMaps: function() { + Recording.receiveArray([{m1k1: 'm1v1', m1k2: 'm1v2'}, {m2k1: 'm2v1'}]); + }, + returnMapWithBasicTypes: function() { + Recording.receiveMap({ + stringKey: 'stringValue', + doubleKey: 3.14, + intKey: -11, + booleanKey: true, + nullKey: null, + }); + }, + returnNestedMap: function() { + Recording.receiveMap({ + weHaveToGoDeeper: { + inception: true, + }, + }); + }, + returnMapWithArrays: function() { + Recording.receiveMap({ + 'empty': [], + 'ints': [43, 44], + 'mixed': [77, 'string', ['another', 'array']], + }); + }, + returnArrayWithStringDoubleIntMapArrayBooleanNull: function() { + Recording.receiveArray(['string', 3.14, 555, {}, [], true, null]); + }, + returnMapWithStringDoubleIntMapArrayBooleanNull: function() { + Recording.receiveMap({ + string: 'string', + double: 3, + map: {}, + int: -55, + array: [], + boolean: true, + null: null + }); + }, + returnArrayWithLargeInts: function() { + Recording.receiveArray([2147483648, -5555555555]); + }, + returnMapWithLargeInts: function() { + Recording.receiveMap({first: -2147483649, second: 5551231231}); + }, + returnMapForMerge1: function() { + Recording.receiveMap({ + a: 1, + b: 41, + c: 'string', + d: 'other string', + e: [1,'foo','bar'], + f: null, + }); + }, + returnMapForMerge2: function() { + Recording.receiveMap({ + a: 'overwrite', + d: 77, + e: null, + f: ['array', 'with', 'stuff'], + newkey: 'newvalue', + }); + }, + returnMapWithMultibyteUTF8CharacterString: function() { + Recording.receiveMap({ + 'one-byte': 'a', + 'two-bytes': '\u00A2', + 'three-bytes': '\u20AC', + 'four-bytes': '\uD83D\uDE1C', + 'mixed': '\u017C\u00F3\u0142\u0107 g\u0119\u015Bl\u0105 \u6211 \uD83D\uDE0E ja\u017A\u0107' + }); + }, + returnArrayWithMultibyteUTF8CharacterString: function() { + Recording.receiveArray([ + 'a', + '\u00A2', + '\u20AC', + '\uD83D\uDE1C', + '\u017C\u00F3\u0142\u0107 g\u0119\u015Bl\u0105 \u6211 \uD83D\uDE0E ja\u017A\u0107' + ]); + }, +}; + +BatchedBridge.registerCallableModule( + 'TestJSToJavaParametersModule', + TestJSToJavaParametersModule +); + +module.exports = TestJSToJavaParametersModule; diff --git a/ReactAndroid/src/androidTest/js/TestJavaToJSArgumentsModule.js b/ReactAndroid/src/androidTest/js/TestJavaToJSArgumentsModule.js new file mode 100644 index 00000000000000..4073d4d6a3fd09 --- /dev/null +++ b/ReactAndroid/src/androidTest/js/TestJavaToJSArgumentsModule.js @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule TestJavaToJSArgumentsModule + */ + +'use strict'; + +var BatchedBridge = require('BatchedBridge'); +var {assertEquals, assertTrue} = require('Asserts'); + +function strictStringCompare(a, b) { + if (typeof a !== 'string' || typeof b !== 'string' || a.length !== b.length) { + return false; + } + for (var i = 0; i < a.length; i++) { + if (a.charCodeAt(i) !== b.charCodeAt(i)) { + return false; + } + } + return true; +} + +function assertStrictStringEquals(a, b) { + assertTrue( + strictStringCompare(a,b), + 'Expected: ' + a + ', received: ' + b); +} + +var TestJavaToJSArgumentsModule = { + receiveBasicTypes: function(str, dbl, bool, null_arg) { + assertEquals("foo", str); + assertEquals(3.14, dbl); + assertEquals(true, bool); + assertEquals(null, null_arg); + }, + receiveArrayWithBasicTypes: function(arr) { + assertEquals(4, arr.length); + assertEquals("red panda", arr[0]); + assertEquals(1.19, arr[1]); + assertEquals(true, arr[2]); + assertEquals(null, arr[3]); + }, + receiveNestedArray: function(arr) { + assertEquals(2, arr.length); + assertEquals("level1", arr[0]); + var arr2 = arr[1]; + assertEquals("level2", arr2[0]); + var arr3 = arr2[1]; + assertEquals("level3", arr3[0]); + }, + receiveArrayWithMaps: function(arr) { + assertEquals(2, arr.length); + var m1 = arr[0]; + var m2 = arr[1]; + assertEquals("m1v1", m1["m1k1"]); + assertEquals("m1v2", m1["m1k2"]); + assertEquals("m2v1", m2["m2k1"]); + }, + receiveMapWithBasicTypes: function(map) { + assertEquals("stringValue", map["stringKey"]); + assertEquals(3.14, map["doubleKey"]); + assertEquals(true, map["booleanKey"]); + assertEquals(null, map["nullKey"]); + }, + receiveNestedMap: function(map) { + var nestedMap = map["nestedMap"]; + assertEquals("foxes", nestedMap["animals"]); + }, + receiveMapWithArrays: function(map) { + var a1 = map["array1"]; + var a2 = map["array2"]; + assertEquals(3, a1.length); + assertEquals(2, a2.length); + assertEquals(3, a1[0]); + assertEquals(9, a2[1]); + }, + receiveMapAndArrayWithNullValues: function(map, array) { + assertEquals(null, map.string); + assertEquals(null, map.array); + assertEquals(null, map.map); + + assertEquals(null, array[0]); + assertEquals(null, array[1]); + assertEquals(null, array[2]); + }, + receiveMapWithMultibyteUTF8CharacterString: function(map) { + assertStrictStringEquals('\u00A2', map['two-bytes']); + assertStrictStringEquals('\u20AC', map['three-bytes']); + assertStrictStringEquals('\uD83D\uDE1C', map['four-bytes']); + assertStrictStringEquals( + '\u017C\u00F3\u0142\u0107 g\u0119\u015Bl\u0105 \u6211 \uD83D\uDE0E ja\u017A\u0107', + map.mixed); + }, + receiveArrayWithMultibyteUTF8CharacterString: function(array) { + assertTrue(true); + assertStrictStringEquals('\u00A2', array[0]); + assertStrictStringEquals('\u20AC', array[1]); + assertStrictStringEquals('\uD83D\uDE1C', array[2]); + assertStrictStringEquals( + '\u017C\u00F3\u0142\u0107 g\u0119\u015Bl\u0105 \u6211 \uD83D\uDE0E ja\u017A\u0107', + array[3]); + }, +}; + +BatchedBridge.registerCallableModule( + 'TestJavaToJSArgumentsModule', + TestJavaToJSArgumentsModule +); + +module.exports = TestJavaToJSArgumentsModule; From 9172597019e07d786290ace91078af990f5bcc27 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Thu, 2 Jun 2016 10:46:35 -0700 Subject: [PATCH 188/843] Returned flow checks to CI Summary: Closes https://github.com/facebook/react-native/pull/7894 Differential Revision: D3379513 fbshipit-source-id: 49290b3582ae66ea99d19ec3f490bdb4436536dd --- circle.yml | 5 ++--- scripts/run-ci-e2e-tests.js | 11 +++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/circle.yml b/circle.yml index 136d49b3ae218e..642c4e8a84c51e 100644 --- a/circle.yml +++ b/circle.yml @@ -47,11 +47,10 @@ test: - source scripts/circle-ci-android-setup.sh && waitForAVD override: - # TODO flow is disabled see t11343811 and 0.25.1 # eslint bot - # - cat <(echo eslint; npm run lint --silent -- --format=json; echo flow; npm run flow --silent -- check --json) | GITHUB_TOKEN="af6ef0d15709bc91d""06a6217a5a826a226fb57b7" CI_USER=$CIRCLE_PROJECT_USERNAME CI_REPO=$CIRCLE_PROJECT_REPONAME PULL_REQUEST_NUMBER=$CIRCLE_PR_NUMBER node bots/code-analysis-bot.js + - cat <(echo eslint; npm run lint --silent -- --format=json; echo flow; npm run flow --silent -- check --json) | GITHUB_TOKEN="af6ef0d15709bc91d""06a6217a5a826a226fb57b7" CI_USER=$CIRCLE_PROJECT_USERNAME CI_REPO=$CIRCLE_PROJECT_REPONAME PULL_REQUEST_NUMBER=$CIRCLE_PR_NUMBER node bots/code-analysis-bot.js # JS tests for dependencies installed with npm3 - # - npm run flow check + - npm run flow -- check - npm test -- --maxWorkers=1 # build app diff --git a/scripts/run-ci-e2e-tests.js b/scripts/run-ci-e2e-tests.js index 8633be8b7425e4..cf3534b6408d6e 100644 --- a/scripts/run-ci-e2e-tests.js +++ b/scripts/run-ci-e2e-tests.js @@ -195,12 +195,11 @@ try { exitCode = 1; throw Error(exitCode); } -// TODO disabled while flow 0.25.0 is crashing - // if (exec(`${ROOT}/node_modules/.bin/flow check`).code) { - // echo('Flow check does not pass'); - // exitCode = 1; - // throw Error(exitCode); - // } + if (exec(`${ROOT}/node_modules/.bin/flow check`).code) { + echo('Flow check does not pass'); + exitCode = 1; + throw Error(exitCode); + } } exitCode = 0; From 1b2d4266b95408236556b5226300707c0a32442a Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Thu, 2 Jun 2016 11:38:01 -0700 Subject: [PATCH 189/843] Update `fbjs-scripts` to ^0.7.0 Summary: `fbjs-scripts` 0.4.0 has Babel 5 as a dependency, which causes some amount of havoc when you're trying to use `react-native` in a project that's otherwise dependent on Babel 6 and using the flat-installing npm >=3. Closes https://github.com/facebook/react-native/pull/7855 Reviewed By: frantic Differential Revision: D3371679 Pulled By: steveluscher fbshipit-source-id: 9f7643171d89da0de0492e7e97875f472725e990 --- package.json | 3 ++- packager/transformer.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 49db927f4a3889..2ce89688d5fa33 100644 --- a/package.json +++ b/package.json @@ -136,6 +136,7 @@ "babel-plugin-transform-object-rest-spread": "^6.6.5", "babel-polyfill": "^6.6.1", "babel-preset-es2015-node": "^4.0.2", + "babel-preset-fbjs": "^2.0.0", "babel-preset-react-native": "^1.9.0", "babel-register": "^6.6.0", "babel-types": "^6.6.4", @@ -148,7 +149,7 @@ "debug": "^2.2.0", "event-target-shim": "^1.0.5", "fbjs": "^0.8.0", - "fbjs-scripts": "^0.4.0", + "fbjs-scripts": "^0.7.0", "graceful-fs": "^4.1.3", "image-size": "^0.3.5", "immutable": "~3.7.6", diff --git a/packager/transformer.js b/packager/transformer.js index faf01669776c7c..0a972982e3a8d0 100644 --- a/packager/transformer.js +++ b/packager/transformer.js @@ -15,7 +15,7 @@ const externalHelpersPlugin = require('babel-plugin-external-helpers'); const fs = require('fs'); const makeHMRConfig = require('babel-preset-react-native/configs/hmr'); const resolvePlugins = require('babel-preset-react-native/lib/resolvePlugins'); -const inlineRequiresPlugin = require('fbjs-scripts/babel-6/inline-requires'); +const inlineRequiresPlugin = require('babel-preset-fbjs/plugins/inline-requires'); const json5 = require('json5'); const path = require('path'); From 9a6e61426d85462ef8b18630ec5e72fb3d86304d Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Thu, 2 Jun 2016 13:37:57 -0700 Subject: [PATCH 190/843] Fixes to NativeMap/Array Reviewed By: mhorowitz Differential Revision: D3376409 fbshipit-source-id: 4efd1a8567358baf2634008af77f7e743ace849e --- .../src/main/jni/react/jni/NativeCommon.cpp | 71 +++++++------------ .../jni/react/jni/ReadableNativeArray.cpp | 10 ++- 2 files changed, 35 insertions(+), 46 deletions(-) diff --git a/ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp b/ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp index 0f500735055f20..633792a7d2e53c 100644 --- a/ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp +++ b/ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp @@ -14,59 +14,42 @@ const char *gUnexpectedNativeTypeExceptionClass = namespace { -local_ref getTypeField(const char* fieldName) { +// Returns a leaked global_ref. +alias_ref getTypeField(const char* fieldName) { static auto cls = ReadableType::javaClassStatic(); auto field = cls->getStaticField(fieldName); - return cls->getStaticFieldValue(field); -} - -alias_ref getNullValue() { - static alias_ref val = make_global(getTypeField("Null")).release(); - return val; -} - -alias_ref getBooleanValue() { - static alias_ref val = make_global(getTypeField("Boolean")).release(); - return val; -} - -alias_ref getNumberValue() { - static alias_ref val = make_global(getTypeField("Number")).release(); - return val; -} - -alias_ref getStringValue() { - static alias_ref val = make_global(getTypeField("String")).release(); - return val; -} - -alias_ref getMapValue() { - static alias_ref val = make_global(getTypeField("Map")).release(); - return val; -} - -alias_ref getArrayValue() { - static alias_ref val = make_global(getTypeField("Array")).release(); - return val; + return make_global(cls->getStaticFieldValue(field)).release(); } } // namespace local_ref ReadableType::getType(folly::dynamic::Type type) { switch (type) { - case folly::dynamic::Type::NULLT: - return make_local(getNullValue()); - case folly::dynamic::Type::BOOL: - return make_local(getBooleanValue()); + case folly::dynamic::Type::NULLT: { + static alias_ref val = getTypeField("Null"); + return make_local(val); + } + case folly::dynamic::Type::BOOL: { + static alias_ref val = getTypeField("Boolean"); + return make_local(val); + } case folly::dynamic::Type::DOUBLE: - case folly::dynamic::Type::INT64: - return make_local(getNumberValue()); - case folly::dynamic::Type::STRING: - return make_local(getStringValue()); - case folly::dynamic::Type::OBJECT: - return make_local(getMapValue()); - case folly::dynamic::Type::ARRAY: - return make_local(getArrayValue()); + case folly::dynamic::Type::INT64: { + static alias_ref val = getTypeField("Number"); + return make_local(val); + } + case folly::dynamic::Type::STRING: { + static alias_ref val = getTypeField("String"); + return make_local(val); + } + case folly::dynamic::Type::OBJECT: { + static alias_ref val = getTypeField("Map"); + return make_local(val); + } + case folly::dynamic::Type::ARRAY: { + static alias_ref val = getTypeField("Array"); + return make_local(val); + } default: throwNewJavaException(exceptions::gUnexpectedNativeTypeExceptionClass, "Unknown type"); } diff --git a/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp b/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp index d4e9faa3f17773..58df6c88b39a26 100644 --- a/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp +++ b/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp @@ -83,6 +83,13 @@ local_ref ReadableNativeArray::getMap(jint index) { return ReadableNativeMap::createWithContents(std::move(array.at(index))); } +namespace { +// This is just to allow signature deduction below. +local_ref getMapFixed(alias_ref array, jint index) { + return static_ref_cast(array->cthis()->getMap(index)); +} +} + void ReadableNativeArray::registerNatives() { registerHybrid({ makeNativeMethod("size", ReadableNativeArray::getSize), @@ -92,8 +99,7 @@ void ReadableNativeArray::registerNatives() { makeNativeMethod("getInt", ReadableNativeArray::getInt), makeNativeMethod("getString", ReadableNativeArray::getString), makeNativeMethod("getArray", ReadableNativeArray::getArray), - makeNativeMethod("getMap", jmethod_traits::descriptor(), - ReadableNativeArray::getMap), + makeNativeMethod("getMap", getMapFixed), makeNativeMethod("getType", ReadableNativeArray::getType), }); } From 68c11e55ee5f5c2d2f4f232a6e195e4f089bdb81 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Thu, 2 Jun 2016 14:02:50 -0700 Subject: [PATCH 191/843] Promote grandchildren of certain views are Views Reviewed By: astreet Differential Revision: D3161232 fbshipit-source-id: ea313d513bd7567e32f18765d479ba0538f2efbf --- .../com/facebook/react/uimanager/ViewGroupManager.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java index e91be0e20f9ead..7b72448abfb909 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java @@ -77,4 +77,13 @@ public boolean needsCustomLayoutForChildren() { return false; } + /** + * Returns whether or not this View type should promote its grandchildren as Views. This is an + * optimization for Scrollable containers when using Nodes, where instead of having one ViewGroup + * containing a large number of draw commands (and thus being more expensive in the case of + * an invalidate or re-draw), we split them up into several draw commands. + */ + public boolean shouldPromoteGrandchildren() { + return false; + } } From db3adb4445785936501b5908e4bf7e60f10466f3 Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Thu, 2 Jun 2016 17:05:09 -0700 Subject: [PATCH 192/843] Cancel pending bridge work during shutdown Reviewed By: mhorowitz Differential Revision: D3381541 fbshipit-source-id: 51a3f766509ec78262792817dee3d5e6969ff05f --- ReactCommon/cxxreact/NativeToJsBridge.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ReactCommon/cxxreact/NativeToJsBridge.cpp b/ReactCommon/cxxreact/NativeToJsBridge.cpp index c7b665a551b4e9..4a935cf5b25dca 100644 --- a/ReactCommon/cxxreact/NativeToJsBridge.cpp +++ b/ReactCommon/cxxreact/NativeToJsBridge.cpp @@ -312,12 +312,15 @@ ExecutorToken NativeToJsBridge::getTokenForExecutor(JSExecutor& executor) { void NativeToJsBridge::destroy() { m_delegate->quitQueueSynchronous(); auto* executorMessageQueueThread = getMessageQueueThread(m_mainExecutorToken); + // All calls made through runOnExecutorQueue have an early exit if + // m_destroyed is true. Setting this before the runOnQueueSync will cause + // pending work to be cancelled and we won't have to wait for it. + *m_destroyed = true; executorMessageQueueThread->runOnQueueSync([this, executorMessageQueueThread] { m_mainExecutor->destroy(); executorMessageQueueThread->quitSynchronous(); unregisterExecutor(*m_mainExecutor); m_mainExecutor = nullptr; - *m_destroyed = true; }); } From 1f2027a1fe740b159f120663d6ddb9ca381d48af Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Thu, 2 Jun 2016 17:27:24 -0700 Subject: [PATCH 193/843] Android: allow app/build.gradle to specify additional packager arguments Summary: If for instance you're using a custom transformer, having this hook will let you specify it without forking all of react.gradle **Test plan:** Specified some additional packager args in my app's `android/app/build.gradle`: ```groovy project.ext.react = [ bundleInDebug: true, extraPackagerArgs: ["--transformer", "path/to/my/transformer.js"] ] ``` and ensured they show up when gradle invokes the bundler. Closes https://github.com/facebook/react-native/pull/7858 Differential Revision: D3382996 Pulled By: mkonicek fbshipit-source-id: 437b2e6c902931d45b9d2f7ec97c833ba0cd3217 --- .../generator-android/templates/src/app/build.gradle | 5 ++++- react.gradle | 11 +++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/local-cli/generator-android/templates/src/app/build.gradle b/local-cli/generator-android/templates/src/app/build.gradle index fe553085a01b6c..11380b0062f60f 100644 --- a/local-cli/generator-android/templates/src/app/build.gradle +++ b/local-cli/generator-android/templates/src/app/build.gradle @@ -55,7 +55,10 @@ import com.android.build.OutputFile * // date; if you have any other folders that you want to ignore for performance reasons (gradle * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ * // for example, you might want to remove it from here. - * inputExcludes: ["android/**", "ios/**"] + * inputExcludes: ["android/**", "ios/**"], + * + * // supply additional arguments to the packager + * extraPackagerArgs: [] * ] */ diff --git a/react.gradle b/react.gradle index 64263a3c58c0fa..f52b30c3b284d2 100644 --- a/react.gradle +++ b/react.gradle @@ -49,6 +49,9 @@ gradle.projectsEvaluated { // Bundle task name for variant def bundleJsAndAssetsTaskName = "bundle${targetName}JsAndAssets" + // Additional packager commandline arguments + def extraPackagerArgs = config.extraPackagerArgs ?: [] + def currentBundleTask = tasks.create( name: bundleJsAndAssetsTaskName, type: Exec) { @@ -72,11 +75,11 @@ gradle.projectsEvaluated { // Set up dev mode def devEnabled = !targetName.toLowerCase().contains("release") if (Os.isFamily(Os.FAMILY_WINDOWS)) { - commandLine "cmd", "/c", "node", "node_modules/react-native/local-cli/cli.js", "bundle", "--platform", "android", "--dev", "${devEnabled}", - "--reset-cache", "true", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir + commandLine("cmd", "/c", "node", "node_modules/react-native/local-cli/cli.js", "bundle", "--platform", "android", "--dev", "${devEnabled}", + "--reset-cache", "true", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir, *extraPackagerArgs) } else { - commandLine "node", "node_modules/react-native/local-cli/cli.js", "bundle", "--platform", "android", "--dev", "${devEnabled}", - "--reset-cache", "true", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir + commandLine("node", "node_modules/react-native/local-cli/cli.js", "bundle", "--platform", "android", "--dev", "${devEnabled}", + "--reset-cache", "true", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir, *extraPackagerArgs) } enabled config."bundleIn${targetName}" || From c55e3649dd317f14cfcce0ea18d9aa142de2c485 Mon Sep 17 00:00:00 2001 From: Igor Avramovic Date: Thu, 2 Jun 2016 17:33:28 -0700 Subject: [PATCH 194/843] Lazy load platform specific code Differential Revision: D3369528 fbshipit-source-id: 0fbc63fbac5e96468598fb3d0a86c6b20f96f39d --- Libraries/Utilities/Platform.android.js | 9 ++++++++- Libraries/Utilities/Platform.ios.js | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Libraries/Utilities/Platform.android.js b/Libraries/Utilities/Platform.android.js index 86861e4c76d0dd..c9ba2c0bc5349e 100644 --- a/Libraries/Utilities/Platform.android.js +++ b/Libraries/Utilities/Platform.android.js @@ -12,10 +12,17 @@ 'use strict'; -var Platform = { +const Platform = { OS: 'android', get Version() { return require('NativeModules').AndroidConstants.Version; }, select: (obj: Object) => obj.android, + lazySelect(obj: ?Object): ?Object { + if (!obj || !obj.android) { + return null; + } + + return obj.android(); + }, }; module.exports = Platform; diff --git a/Libraries/Utilities/Platform.ios.js b/Libraries/Utilities/Platform.ios.js index 070bd28747cdc1..99eb5d1c106e72 100644 --- a/Libraries/Utilities/Platform.ios.js +++ b/Libraries/Utilities/Platform.ios.js @@ -12,9 +12,16 @@ 'use strict'; -var Platform = { +const Platform = { OS: 'ios', select: (obj: Object) => obj.ios, + lazySelect(obj: ?Object): ?Object { + if (!obj || !obj.ios) { + return null; + } + + return obj.ios(); + }, }; module.exports = Platform; From 16a97c80274ef50493b30e1b4871ba4bbae1f447 Mon Sep 17 00:00:00 2001 From: Kevin Lacker Date: Thu, 2 Jun 2016 17:43:00 -0700 Subject: [PATCH 195/843] Update error message on unrecognized commands Summary: Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: (You can skip this if you're fixing a typo or adding an app to the Showcase.) Kevin: This isn't quite a typo, but it's a change to an error message. In my experience, frequently when this error message is displayed, the command is unrecognized because I haven't run npm install, I just checked out a repo of someone else's. We could just update this error message a bit, and I think it would improve the developer experience. Closes https://github.com/facebook/react-native/pull/7859 Differential Revision: D3379234 fbshipit-source-id: 4fb6e5bae20904871c9c4f7504013dc18b1fe707 --- react-native-cli/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react-native-cli/index.js b/react-native-cli/index.js index 56ab767a8c7444..c9cd36c27f5232 100755 --- a/react-native-cli/index.js +++ b/react-native-cli/index.js @@ -108,7 +108,7 @@ if (cli) { default: console.error( 'Command `%s` unrecognized. ' + - 'Did you mean to run this inside a react-native project?', + 'Make sure that you have run `npm install` and that you are inside a react-native project.', commands[0] ); process.exit(1); From e29350214a960a1ccda5187242e1ed0518526984 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Thu, 2 Jun 2016 18:07:36 -0700 Subject: [PATCH 196/843] Correct semantics for XMLHttpRequest.responseText Summary: Accessing the `responseText` property when `responseType` is not `''` or `'text'` should throw. Also, the property is read-only. **Test Plan:** UIExplorer example, unit tests Closes https://github.com/facebook/react-native/pull/7284 Differential Revision: D3366893 fbshipit-source-id: a4cf5ebabcd1e03d6e2dc9d51230982922746c11 --- Libraries/Network/XMLHttpRequest.js | 59 +++++++++++++------ .../Network/__tests__/XMLHttpRequest-test.js | 44 +++++++++++++- 2 files changed, 83 insertions(+), 20 deletions(-) diff --git a/Libraries/Network/XMLHttpRequest.js b/Libraries/Network/XMLHttpRequest.js index cdd4073126ce61..ed39d9fe450070 100644 --- a/Libraries/Network/XMLHttpRequest.js +++ b/Libraries/Network/XMLHttpRequest.js @@ -85,7 +85,6 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { readyState: number = UNSENT; responseHeaders: ?Object; - responseText: string = ''; status: number = 0; timeout: number = 0; responseURL: ?string; @@ -103,6 +102,7 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { _method: ?string = null; _response: string | ?Object; _responseType: ResponseType; + _responseText: string = ''; _sent: boolean; _url: ?string = null; _timedOut: boolean = false; @@ -116,7 +116,6 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { _reset(): void { this.readyState = this.UNSENT; this.responseHeaders = undefined; - this.responseText = ''; this.status = 0; delete this.responseURL; @@ -125,6 +124,7 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { this._cachedResponse = undefined; this._hasError = false; this._headers = {}; + this._responseText = ''; this._responseType = ''; this._sent = false; this._lowerCaseResponseHeaders = {}; @@ -148,7 +148,9 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { } if (!SUPPORTED_RESPONSE_TYPES.hasOwnProperty(responseType)) { warning( - `The provided value '${responseType}' is not a valid 'responseType'.`); + false, + `The provided value '${responseType}' is not a valid 'responseType'.` + ); return; } @@ -160,13 +162,27 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { this._responseType = responseType; } + // $FlowIssue #10784535 + get responseText(): string { + if (this._responseType !== '' && this._responseType !== 'text') { + throw new Error( + `The 'responseText' property is only available if 'responseType' ` + + `is set to '' or 'text', but it is '${this._responseType}'.` + ); + } + if (this.readyState < LOADING) { + return ''; + } + return this._responseText; + } + // $FlowIssue #10784535 get response(): Response { const {responseType} = this; if (responseType === '' || responseType === 'text') { return this.readyState < LOADING || this._hasError ? '' - : this.responseText; + : this._responseText; } if (this.readyState !== DONE) { @@ -177,26 +193,26 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { return this._cachedResponse; } - switch (this.responseType) { + switch (this._responseType) { case 'document': this._cachedResponse = null; break; case 'arraybuffer': this._cachedResponse = toArrayBuffer( - this.responseText, this.getResponseHeader('content-type') || ''); + this._responseText, this.getResponseHeader('content-type') || ''); break; case 'blob': this._cachedResponse = new global.Blob( - [this.responseText], + [this._responseText], {type: this.getResponseHeader('content-type') || ''} ); break; case 'json': try { - this._cachedResponse = JSON.parse(this.responseText); + this._cachedResponse = JSON.parse(this._responseText); } catch (_) { this._cachedResponse = null; } @@ -226,7 +242,12 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { } } - _didReceiveResponse(requestId: number, status: number, responseHeaders: ?Object, responseURL: ?string): void { + __didReceiveResponse( + requestId: number, + status: number, + responseHeaders: ?Object, + responseURL: ?string + ): void { if (requestId === this._requestId) { this.status = status; this.setResponseHeaders(responseHeaders); @@ -239,12 +260,12 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { } } - _didReceiveData(requestId: number, responseText: string): void { + __didReceiveData(requestId: number, responseText: string): void { if (requestId === this._requestId) { - if (!this.responseText) { - this.responseText = responseText; + if (!this._responseText) { + this._responseText = responseText; } else { - this.responseText += responseText; + this._responseText += responseText; } this._cachedResponse = undefined; // force lazy recomputation this.setReadyState(this.LOADING); @@ -252,10 +273,14 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { } // exposed for testing - __didCompleteResponse(requestId: number, error: string, timeOutError: boolean): void { + __didCompleteResponse( + requestId: number, + error: string, + timeOutError: boolean + ): void { if (requestId === this._requestId) { if (error) { - this.responseText = error; + this._responseText = error; this._hasError = true; if (timeOutError) { this._timedOut = true; @@ -330,11 +355,11 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) { )); this._subscriptions.push(RCTNetworking.addListener( 'didReceiveNetworkResponse', - (args) => this._didReceiveResponse(...args) + (args) => this.__didReceiveResponse(...args) )); this._subscriptions.push(RCTNetworking.addListener( 'didReceiveNetworkData', - (args) => this._didReceiveData(...args) + (args) => this.__didReceiveData(...args) )); this._subscriptions.push(RCTNetworking.addListener( 'didCompleteNetworkResponse', diff --git a/Libraries/Network/__tests__/XMLHttpRequest-test.js b/Libraries/Network/__tests__/XMLHttpRequest-test.js index a7c1a995638626..afbae4780b080f 100644 --- a/Libraries/Network/__tests__/XMLHttpRequest-test.js +++ b/Libraries/Network/__tests__/XMLHttpRequest-test.js @@ -56,15 +56,53 @@ describe('XMLHttpRequest', function(){ handleLoad = null; }); - it('should transition readyState correctly', function() { - expect(xhr.readyState).toBe(xhr.UNSENT); + it('should transition readyState correctly', function() { + + expect(xhr.readyState).toBe(xhr.UNSENT); xhr.open('GET', 'blabla'); expect(xhr.onreadystatechange.mock.calls.length).toBe(1); expect(handleReadyStateChange.mock.calls.length).toBe(1); expect(xhr.readyState).toBe(xhr.OPENED); - }); + }); + + it('should expose responseType correctly', function() { + expect(xhr.responseType).toBe(''); + + // Setting responseType to an unsupported value has no effect. + xhr.responseType = 'arrayblobbuffertextfile'; + expect(xhr.responseType).toBe(''); + + xhr.responseType = 'arraybuffer'; + expect(xhr.responseType).toBe('arraybuffer'); + + // Can't change responseType after first data has been received. + xhr.__didReceiveData(1, 'Some data'); + expect(() => { xhr.responseType = 'text'; }).toThrow(); + }); + + it('should expose responseText correctly', function() { + xhr.responseType = ''; + expect(xhr.responseText).toBe(''); + expect(xhr.response).toBe(''); + + xhr.responseType = 'arraybuffer'; + expect(() => xhr.responseText).toThrow(); + expect(xhr.response).toBe(null); + + xhr.responseType = 'text'; + expect(xhr.responseText).toBe(''); + expect(xhr.response).toBe(''); + + // responseText is read-only. + expect(() => { xhr.responseText = 'hi'; }).toThrow(); + expect(xhr.responseText).toBe(''); + expect(xhr.response).toBe(''); + + xhr.__didReceiveData(1, 'Some data'); + expect(xhr.responseText).toBe('Some data'); + }); it('should call ontimeout function when the request times out', function(){ xhr.__didCompleteResponse(1, 'Timeout', true); From a7f1428d4abe13fcf9fab3e2d7db6546db5eefce Mon Sep 17 00:00:00 2001 From: Kasim Tan Date: Fri, 3 Jun 2016 02:32:30 -0700 Subject: [PATCH 197/843] Fix comment typos Summary: Closes https://github.com/facebook/react-native/pull/7901 Differential Revision: D3384624 Pulled By: javache fbshipit-source-id: 741343dec5406af1855624069dd8dc426a93bdb3 --- local-cli/bundle/output/unbundle/util.js | 2 +- local-cli/rnpm/link/src/groupFilesByType.js | 2 +- local-cli/upgrade/upgrade.js | 2 +- local-cli/util/isPackagerRunning.js | 4 ++-- local-cli/util/parseCommandLine.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/local-cli/bundle/output/unbundle/util.js b/local-cli/bundle/output/unbundle/util.js index ce4697eb9ef4e1..d4504dfbd1d384 100644 --- a/local-cli/bundle/output/unbundle/util.js +++ b/local-cli/bundle/output/unbundle/util.js @@ -17,7 +17,7 @@ function lineToLineSourceMap(source, filename) { const firstLine = 'AAAA;'; // Most other lines in our mappings are all zeros (for module, column etc) - // except for the lineno mappinp: curLineno - prevLineno = 1; Which is C. + // except for the lineno mapping: curLineno - prevLineno = 1; Which is C. const line = 'AACA;'; return { diff --git a/local-cli/rnpm/link/src/groupFilesByType.js b/local-cli/rnpm/link/src/groupFilesByType.js index ddf0df6dbe00fa..5dc692e1c21e1a 100644 --- a/local-cli/rnpm/link/src/groupFilesByType.js +++ b/local-cli/rnpm/link/src/groupFilesByType.js @@ -2,7 +2,7 @@ const groupBy = require('lodash').groupBy; const mime = require('mime'); /** - * Since there are no officialy registered MIME types + * Since there are no officially registered MIME types * for ttf/otf yet http://www.iana.org/assignments/media-types/media-types.xhtml, * we define two non-standard ones for the sake of parsing */ diff --git a/local-cli/upgrade/upgrade.js b/local-cli/upgrade/upgrade.js index 03d17d7fba4cd2..aaf6c01ce2b905 100644 --- a/local-cli/upgrade/upgrade.js +++ b/local-cli/upgrade/upgrade.js @@ -45,7 +45,7 @@ module.exports = function upgrade(args, config) { ) ); - // >= v0.21.0, we require react to be a peer depdendency + // >= v0.21.0, we require react to be a peer dependency if (semver.gte(v, '0.21.0') && !pak.dependencies['react']) { console.log( chalk.yellow( diff --git a/local-cli/util/isPackagerRunning.js b/local-cli/util/isPackagerRunning.js index ae8950a8b89da8..82828c1afd5f89 100644 --- a/local-cli/util/isPackagerRunning.js +++ b/local-cli/util/isPackagerRunning.js @@ -11,12 +11,12 @@ const fetch = require('node-fetch'); /** - * Indicates whether or not the packager is running. It ruturns a promise that + * Indicates whether or not the packager is running. It returns a promise that * when fulfilled can returns one out of these possible values: * - `running`: the packager is running * - `not_running`: the packager nor any process is running on the expected * port. - * - `unrecognized`: one other process is running on the port ew expect the + * - `unrecognized`: one other process is running on the port we expect the * packager to be running. */ function isPackagerRunning() { diff --git a/local-cli/util/parseCommandLine.js b/local-cli/util/parseCommandLine.js index 04490f67ac4913..b373ddad428b70 100644 --- a/local-cli/util/parseCommandLine.js +++ b/local-cli/util/parseCommandLine.js @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * Wrapper on-top of `optimist` in order to properly support boolean flags - * and have a slightly less akward API. + * and have a slightly less awkward API. * * Usage example: * var argv = parseCommandLine([{ From fe5c2615ddeb2f8ddf37635d2294cc102862fea5 Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Fri, 3 Jun 2016 02:37:43 -0700 Subject: [PATCH 198/843] react-native-github: turn on strict type args in Flow, codemod stragglers Reviewed By: frantic Differential Revision: D3374980 fbshipit-source-id: 6dbefeffa588f676c8e162ba7ac7912b61a7711a --- .flowconfig | 2 ++ Examples/UIExplorer/AppStateExample.js | 2 +- Libraries/Linking/Linking.js | 2 +- package.json | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.flowconfig b/.flowconfig index fe850cb6952416..bb444475193b11 100644 --- a/.flowconfig +++ b/.flowconfig @@ -79,6 +79,8 @@ module.system=haste esproposal.class_static_fields=enable esproposal.class_instance_fields=enable +experimental.strict_type_args=true + munge_underscores=true module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' diff --git a/Examples/UIExplorer/AppStateExample.js b/Examples/UIExplorer/AppStateExample.js index 0f9c3c321a3da2..8cac00b5414ef6 100644 --- a/Examples/UIExplorer/AppStateExample.js +++ b/Examples/UIExplorer/AppStateExample.js @@ -102,6 +102,6 @@ exports.examples = [ platform: 'ios', title: 'Memory Warnings', description: 'In the IOS simulator, hit Shift+Command+M to simulate a memory warning.', - render(): ReactElement { return ; } + render(): ReactElement { return ; } }, ]; diff --git a/Libraries/Linking/Linking.js b/Libraries/Linking/Linking.js index 2217941095875a..455de7ec75a2d4 100644 --- a/Libraries/Linking/Linking.js +++ b/Libraries/Linking/Linking.js @@ -149,7 +149,7 @@ class Linking extends NativeEventEmitter { * * NOTE: For web URLs, the protocol ("http://", "https://") must be set accordingly! */ - openURL(url: string): Promise { + openURL(url: string): Promise { this._validateURL(url); return LinkingManager.openURL(url); } diff --git a/package.json b/package.json index 2ce89688d5fa33..bca743d1e27c91 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "core-js": "^2.2.2", "debug": "^2.2.0", "event-target-shim": "^1.0.5", - "fbjs": "^0.8.0", + "fbjs": "^0.8.3", "fbjs-scripts": "^0.7.0", "graceful-fs": "^4.1.3", "image-size": "^0.3.5", From 8fe58849a5148ce87aaa6d9498607462ce72216f Mon Sep 17 00:00:00 2001 From: Christian Ost Date: Fri, 3 Jun 2016 02:49:21 -0700 Subject: [PATCH 199/843] Update the documenation of ActionSheetIOS to include all possible options Summary: As it is shown in the examples, `subject` and `excludedActivityTypes` should be part of the documentation. `excludedActivityTypes` could be completed by a list of all possible options (see [UIActivity](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIActivity_Class/#//apple_ref/doc/constant_group/Built_in_Activity_Types) documentation of Apple). Screenshots of updated documentation: ![actionsheetios](https://cloud.githubusercontent.com/assets/12033337/15772879/0e54ca98-2974-11e6-9af5-df76c093ec5a.png) Closes https://github.com/facebook/react-native/pull/7908 Differential Revision: D3384642 Pulled By: javache fbshipit-source-id: 837e50eeabbcd532baf5ea1d4f8a64267c93c6ce --- Libraries/ActionSheetIOS/ActionSheetIOS.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Libraries/ActionSheetIOS/ActionSheetIOS.js b/Libraries/ActionSheetIOS/ActionSheetIOS.js index 2321814fae695e..1e3ab80e1ec7f4 100644 --- a/Libraries/ActionSheetIOS/ActionSheetIOS.js +++ b/Libraries/ActionSheetIOS/ActionSheetIOS.js @@ -44,10 +44,13 @@ var ActionSheetIOS = { /** * Display the iOS share sheet. The `options` object should contain - * one or both of: + * one or both of `message` and `url` and can additionally have + * a `subject` or `excludedActivityTypes`: * - * - `message` (string) - a message to share * - `url` (string) - a URL to share + * - `message` (string) - a message to share + * - `subject` (string) - a subject for the message + * - `excludedActivityTypes` (array) - the activites to exclude from the ActionSheet * * NOTE: if `url` points to a local file, or is a base64-encoded * uri, the file it points to will be loaded and shared directly. From 5de1151bdd48a9bc6ec6dae325c9a260121b6e04 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Fri, 3 Jun 2016 02:58:36 -0700 Subject: [PATCH 200/843] Fix NPE in cancelling network calls Summary: Issue reported here https://github.com/facebook/react-native/issues/7755#issuecomment-222950463 Converting int to Integer explicitly and not cancelling the call more than once should fix it. Reviewed By: bestander Differential Revision: D3371238 fbshipit-source-id: cb00663b4ca19a788bd27b971b6447cc0788a818 --- .../java/com/facebook/react/common/network/OkHttpCallUtil.java | 2 ++ .../com/facebook/react/modules/network/NetworkingModule.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java b/ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java index 4da3a5e9b80349..1d199203c423d1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java +++ b/ReactAndroid/src/main/java/com/facebook/react/common/network/OkHttpCallUtil.java @@ -24,11 +24,13 @@ public static void cancelTag(OkHttpClient client, Object tag) { for (Call call : client.dispatcher().queuedCalls()) { if (tag.equals(call.request().tag())) { call.cancel(); + return; } } for (Call call : client.dispatcher().runningCalls()) { if (tag.equals(call.request().tag())) { call.cancel(); + return; } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java index 1410f085effba1..97a8039658e068 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java @@ -384,7 +384,7 @@ private void cancelRequest(final int requestId) { new GuardedAsyncTask(getReactApplicationContext()) { @Override protected void doInBackgroundGuarded(Void... params) { - OkHttpCallUtil.cancelTag(mClient, requestId); + OkHttpCallUtil.cancelTag(mClient, Integer.valueOf(requestId)); } }.execute(); } From f22e9353a7fd18eb88218000d0debecae3140d19 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Fri, 3 Jun 2016 03:34:54 -0700 Subject: [PATCH 201/843] Made react installed via reac-native init strict to unbreak 15.1.0 Summary: React@15.1.0 is incompatible with React-Native@0.26. This PR was cherry-picked to 0.26-stable branch now. What this change does: - react-native-cli (major release bump) saves exact versions of react-native and react (only in npm2) dependencies into package.json - local-cli saves exact version of react (only npm3) dependency into package.json Closes https://github.com/facebook/react-native/pull/7879 Differential Revision: D3384705 Pulled By: davidaurelio fbshipit-source-id: d4dff418f9659bd083ae8826433a4e7c0355d03b --- local-cli/generator/index.js | 2 +- react-native-cli/index.js | 8 ++++---- react-native-cli/package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/local-cli/generator/index.js b/local-cli/generator/index.js index 43c327c5c02b43..5c5faf821bc7f6 100644 --- a/local-cli/generator/index.js +++ b/local-cli/generator/index.js @@ -107,6 +107,6 @@ module.exports = yeoman.generators.NamedBase.extend({ return; } - this.npmInstall(`react@${reactVersion}`, { '--save': true }); + this.npmInstall(`react@${reactVersion}`, { '--save': true, '--save-exact': true }); } }); diff --git a/react-native-cli/index.js b/react-native-cli/index.js index c9cd36c27f5232..5b923568016325 100755 --- a/react-native-cli/index.js +++ b/react-native-cli/index.js @@ -213,11 +213,11 @@ function getInstallPackage(rnPackage) { } function run(root, projectName, rnPackage) { - exec('npm install --save ' + getInstallPackage(rnPackage), function(e, stdout, stderr) { + exec('npm install --save --save-exact ' + getInstallPackage(rnPackage), function(e, stdout, stderr) { if (e) { console.log(stdout); console.error(stderr); - console.error('`npm install --save react-native` failed'); + console.error('`npm install --save --save-exact react-native` failed'); process.exit(1); } @@ -229,10 +229,10 @@ function run(root, projectName, rnPackage) { } function runVerbose(root, projectName, rnPackage) { - var proc = spawn('npm', ['install', '--verbose', '--save', getInstallPackage(rnPackage)], {stdio: 'inherit'}); + var proc = spawn('npm', ['install', '--verbose', '--save', '--save-exact', getInstallPackage(rnPackage)], {stdio: 'inherit'}); proc.on('close', function (code) { if (code !== 0) { - console.error('`npm install --save react-native` failed'); + console.error('`npm install --save --save-exact react-native` failed'); return; } diff --git a/react-native-cli/package.json b/react-native-cli/package.json index e0cf94a5d819c8..894186f842b90e 100644 --- a/react-native-cli/package.json +++ b/react-native-cli/package.json @@ -1,6 +1,6 @@ { "name": "react-native-cli", - "version": "0.2.0", + "version": "1.0.0", "license": "BSD-3-Clause", "description": "The React Native CLI tools", "main": "index.js", From 3ed00f140dc0af091eb0f208e705db42b92222da Mon Sep 17 00:00:00 2001 From: Jake Murzy Date: Fri, 3 Jun 2016 04:23:33 -0700 Subject: [PATCH 202/843] Fix typo in NavigationTransitioner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Typo. 🍺 Closes https://github.com/facebook/react-native/pull/7809 Differential Revision: D3384782 Pulled By: javache fbshipit-source-id: edfd2c561dc19ac8ec8c526860b934bc19429c6a --- Libraries/NavigationExperimental/NavigationTransitioner.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Libraries/NavigationExperimental/NavigationTransitioner.js b/Libraries/NavigationExperimental/NavigationTransitioner.js index 49a999978049a6..dc616a1588a0df 100644 --- a/Libraries/NavigationExperimental/NavigationTransitioner.js +++ b/Libraries/NavigationExperimental/NavigationTransitioner.js @@ -130,7 +130,7 @@ class NavigationTransitioner extends React.Component { nextProps.configureTransition() : null; - const transtionSpec = { + const transitionSpec = { ...DefaultTransitionSpec, ...transitionUserSpec, }; @@ -141,7 +141,7 @@ class NavigationTransitioner extends React.Component { Animated.timing( progress, { - ...transtionSpec, + ...transitionSpec, toValue: 1, }, ), @@ -152,7 +152,7 @@ class NavigationTransitioner extends React.Component { Animated.timing( position, { - ...transtionSpec, + ...transitionSpec, toValue: nextProps.navigationState.index, }, ), From 7c3364196a2a791a01e2a2d9d167bffb6f184ff6 Mon Sep 17 00:00:00 2001 From: Igor Avramovic Date: Fri, 3 Jun 2016 05:42:22 -0700 Subject: [PATCH 203/843] Reverted commit D3369528 Differential Revision: D3369528 fbshipit-source-id: 4400411f43bc8396b3692d80b797eceed8899452 --- Libraries/Utilities/Platform.android.js | 9 +-------- Libraries/Utilities/Platform.ios.js | 9 +-------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/Libraries/Utilities/Platform.android.js b/Libraries/Utilities/Platform.android.js index c9ba2c0bc5349e..86861e4c76d0dd 100644 --- a/Libraries/Utilities/Platform.android.js +++ b/Libraries/Utilities/Platform.android.js @@ -12,17 +12,10 @@ 'use strict'; -const Platform = { +var Platform = { OS: 'android', get Version() { return require('NativeModules').AndroidConstants.Version; }, select: (obj: Object) => obj.android, - lazySelect(obj: ?Object): ?Object { - if (!obj || !obj.android) { - return null; - } - - return obj.android(); - }, }; module.exports = Platform; diff --git a/Libraries/Utilities/Platform.ios.js b/Libraries/Utilities/Platform.ios.js index 99eb5d1c106e72..070bd28747cdc1 100644 --- a/Libraries/Utilities/Platform.ios.js +++ b/Libraries/Utilities/Platform.ios.js @@ -12,16 +12,9 @@ 'use strict'; -const Platform = { +var Platform = { OS: 'ios', select: (obj: Object) => obj.ios, - lazySelect(obj: ?Object): ?Object { - if (!obj || !obj.ios) { - return null; - } - - return obj.ios(); - }, }; module.exports = Platform; From d16c6e926c02358cbd51a585daa413cd36099e27 Mon Sep 17 00:00:00 2001 From: sheparddw Date: Fri, 3 Jun 2016 06:20:18 -0700 Subject: [PATCH 204/843] Fix typo in ListViewDataSource documentation. Summary: Closes https://github.com/facebook/react-native/pull/7860 Differential Revision: D3384941 Pulled By: javache fbshipit-source-id: 8a9a9c3dbf8f921ee3f6597154347ec102783415 --- Libraries/CustomComponents/ListView/ListViewDataSource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/CustomComponents/ListView/ListViewDataSource.js b/Libraries/CustomComponents/ListView/ListViewDataSource.js index 0f65b1cceead45..6fdbee7a6ab170 100644 --- a/Libraries/CustomComponents/ListView/ListViewDataSource.js +++ b/Libraries/CustomComponents/ListView/ListViewDataSource.js @@ -145,7 +145,7 @@ class ListViewDataSource { * construction an extractor to get the interesting information was defined * (or the default was used). * - * The `rowIdentities` is is a 2D array of identifiers for rows. + * The `rowIdentities` is a 2D array of identifiers for rows. * ie. [['a1', 'a2'], ['b1', 'b2', 'b3'], ...]. If not provided, it's * assumed that the keys of the section data are the row identities. * From 7028929d8f44872e790376875c7971155bb99f81 Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Fri, 3 Jun 2016 09:05:14 -0700 Subject: [PATCH 205/843] react-native link calls into rnpm Summary: An initial integration of rnpm into 'react-native link'. **Test plan (required)** react-native init MyApp cd MyApp # copied local-cli into MyApp's node_modules npm install --save react-native-vector-icons react-native link react-native-vector-icons The link code ran, Android build files were modified. Closes https://github.com/facebook/react-native/pull/7895 Reviewed By: bestander Differential Revision: D3379197 Pulled By: mkonicek fbshipit-source-id: 597158623ed072d7a7fc55a01624233a0f79de23 --- local-cli/cliEntry.js | 12 +++++++++++- .../rnpm/link/src/android/patches/applyParams.js | 2 +- .../link/src/android/patches/makeStringsPatch.js | 2 +- .../rnpm/link/src/android/unregisterNativeModule.js | 2 +- package.json | 8 ++++++++ 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/local-cli/cliEntry.js b/local-cli/cliEntry.js index 6ec0f794fe569c..3e726c5a4f28fe 100644 --- a/local-cli/cliEntry.js +++ b/local-cli/cliEntry.js @@ -15,6 +15,7 @@ const defaultConfig = require('./default.config'); const dependencies = require('./dependencies/dependencies'); const generate = require('./generate/generate'); const library = require('./library/library'); +const link = require('./rnpm/link/src/link'); const path = require('path'); const Promise = require('promise'); const runAndroid = require('./runAndroid/runAndroid'); @@ -29,6 +30,14 @@ const version = require('./version/version'); const fs = require('fs'); const gracefulFs = require('graceful-fs'); +// Just a helper to proxy 'react-native link' to rnpm +const linkWrapper = (args, config) => { + const rnpmConfig = require('./rnpm/core/src/config'); + return new Promise((resolve, reject) => { + link(rnpmConfig, args.slice(1)).then(resolve, reject); + }); +} + // graceful-fs helps on getting an error when we run out of file // descriptors. When that happens it will enqueue the operation and retry it. gracefulFs.gracefulify(fs); @@ -42,7 +51,8 @@ const documentedCommands = { 'run-android': [runAndroid, 'builds your app and starts it on a connected Android emulator or device'], 'run-ios': [runIOS, 'builds your app and starts it on iOS simulator'], 'upgrade': [upgrade, 'upgrade your app\'s template files to the latest version; run this after ' + - 'updating the react-native version in your package.json and running npm install'] + 'updating the react-native version in your package.json and running npm install'], + 'link': [linkWrapper, 'link a library'], }; const exportedCommands = {dependencies: dependencies}; diff --git a/local-cli/rnpm/link/src/android/patches/applyParams.js b/local-cli/rnpm/link/src/android/patches/applyParams.js index 90973c2f589a91..d91bbb0d526c0d 100644 --- a/local-cli/rnpm/link/src/android/patches/applyParams.js +++ b/local-cli/rnpm/link/src/android/patches/applyParams.js @@ -1,4 +1,4 @@ -const toCamelCase = require('to-camel-case'); +const toCamelCase = require('lodash').camelCase; module.exports = function applyParams(str, params, prefix) { return str.replace( diff --git a/local-cli/rnpm/link/src/android/patches/makeStringsPatch.js b/local-cli/rnpm/link/src/android/patches/makeStringsPatch.js index 06300085dad429..b71bc6c00b7cb1 100644 --- a/local-cli/rnpm/link/src/android/patches/makeStringsPatch.js +++ b/local-cli/rnpm/link/src/android/patches/makeStringsPatch.js @@ -1,4 +1,4 @@ -const toCamelCase = require('to-camel-case'); +const toCamelCase = require('lodash').camelCase; module.exports = function makeStringsPatch(params, prefix) { const patch = Object.keys(params).map(param => { diff --git a/local-cli/rnpm/link/src/android/unregisterNativeModule.js b/local-cli/rnpm/link/src/android/unregisterNativeModule.js index 64db4a0fb6ce36..1c5eaa2bf4ae25 100644 --- a/local-cli/rnpm/link/src/android/unregisterNativeModule.js +++ b/local-cli/rnpm/link/src/android/unregisterNativeModule.js @@ -1,7 +1,7 @@ const fs = require('fs'); const getReactVersion = require('../getReactNativeVersion'); const getPrefix = require('./getPrefix'); -const toCamelCase = require('to-camel-case'); +const toCamelCase = require('lodash').camelCase; const revokePatch = require('./patches/revokePatch'); const makeSettingsPatch = require('./patches/makeSettingsPatch'); diff --git a/package.json b/package.json index bca743d1e27c91..5d2cd61f2843e9 100644 --- a/package.json +++ b/package.json @@ -150,20 +150,26 @@ "event-target-shim": "^1.0.5", "fbjs": "^0.8.3", "fbjs-scripts": "^0.7.0", + "fs-extra": "^0.26.2", + "glob": "^5.0.15", "graceful-fs": "^4.1.3", "image-size": "^0.3.5", "immutable": "~3.7.6", + "inquirer": "^0.12.0", "joi": "^6.6.1", "json-stable-stringify": "^1.0.1", "json5": "^0.4.0", "jstransform": "^11.0.3", "lodash": "^3.10.1", + "mime": "^1.3.4", "mkdirp": "^0.5.1", "module-deps": "^3.9.1", "node-fetch": "^1.3.3", "node-haste": "~2.12.0", + "npmlog": "^2.0.4", "opn": "^3.0.2", "optimist": "^0.6.1", + "plist": "^1.2.0", "progress": "^1.1.8", "promise": "^7.1.1", "react-clone-referenced-element": "^1.0.1", @@ -180,6 +186,8 @@ "wordwrap": "^1.0.0", "worker-farm": "^1.3.1", "ws": "^0.8.0", + "xcode": "^0.8.2", + "xmldoc": "^0.4.0", "yargs": "^3.24.0", "yeoman-environment": "^1.2.7", "yeoman-generator": "^0.20.3" From 03512fb7214abcc2de1d9ccc85c9f0fd48c14506 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Fri, 3 Jun 2016 15:07:10 -0700 Subject: [PATCH 206/843] Fix the Origin: header used in WebSocket requests. Summary: RFC 6454 section 7 defines the Origin: header syntax, and it's a scheme, host, and optional port, not a URL. Section 6 defines serialization of the header, including omission of the port. Therefore, we need to omit the trailing slash in all cases, and omit the port if it matches the default port for the protocol. Closes https://github.com/facebook/react-native/pull/7920 Differential Revision: D3387619 fbshipit-source-id: 552756e63ad41463af357a5073fae56c96e58958 --- Libraries/WebSocket/RCTSRWebSocket.m | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Libraries/WebSocket/RCTSRWebSocket.m b/Libraries/WebSocket/RCTSRWebSocket.m index 54ebbc552c9a0e..e5ed0adb99ef25 100644 --- a/Libraries/WebSocket/RCTSRWebSocket.m +++ b/Libraries/WebSocket/RCTSRWebSocket.m @@ -1565,10 +1565,14 @@ - (NSString *)RCTSR_origin; scheme = @"http"; } - if (self.port) { - return [NSString stringWithFormat:@"%@://%@:%@/", scheme, self.host, self.port]; + int defaultPort = ([scheme isEqualToString:@"https"] ? 443 : + [scheme isEqualToString:@"http"] ? 80 : + -1); + int port = self.port.intValue; + if (port > 0 && port != defaultPort) { + return [NSString stringWithFormat:@"%@://%@:%d", scheme, self.host, port]; } else { - return [NSString stringWithFormat:@"%@://%@/", scheme, self.host]; + return [NSString stringWithFormat:@"%@://%@", scheme, self.host]; } } From 724134746bb1887ecf18fd93a420f6bbfbaaf2af Mon Sep 17 00:00:00 2001 From: Nathan Azaria Date: Fri, 3 Jun 2016 16:08:34 -0700 Subject: [PATCH 207/843] Changed RCTTextView to check for failed focus Reviewed By: nicklockwood Differential Revision: D3378236 fbshipit-source-id: b4a33f7808ffe116b51631cde35f5cd0042caee9 --- Libraries/Text/RCTTextView.m | 8 ++++++++ React/Modules/RCTUIManager.m | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index d32463333bfa7b..84e83214f265b0 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -48,6 +48,14 @@ - (void)reactDidMakeFirstResponder _jsRequestingFirstResponder = NO; } +- (void)didMoveToWindow +{ + if (_jsRequestingFirstResponder) { + [self becomeFirstResponder]; + [self reactDidMakeFirstResponder]; + } +} + @end @implementation RCTTextView diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 033df4de6fddd1..10ee2152d6c256 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -1032,8 +1032,9 @@ - (void)_manageChildren:(NSNumber *)containerReactTag [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { UIView *newResponder = viewRegistry[reactTag]; [newResponder reactWillMakeFirstResponder]; - [newResponder becomeFirstResponder]; - [newResponder reactDidMakeFirstResponder]; + if ([newResponder becomeFirstResponder]) { + [newResponder reactDidMakeFirstResponder]; + } }]; } From f4dbf37ba85acb988bbb26e339ea7f7536b1f931 Mon Sep 17 00:00:00 2001 From: Jonathan Stanton Date: Fri, 3 Jun 2016 16:14:44 -0700 Subject: [PATCH 208/843] PushNotificationIOS requestPermission promisified Summary: **Motivation** Today it's hard to build a good flow around requesting permissions if we don't know when the user rejects the push notification permission. With this PR I wrap `PushNotificationIOS#requestPermission` in a promise. The promise will return with the updated permissions when the user accepts, rejects or has previously rejected the permission. An example flow of how an app should handle push notifications with the change proposed: 1) Show user an explanation of push notification permissions with a button to enable, 2) User presses the enable push notifications button, 3) If the user accepts -> take them into the app, 4) if the user rejects -> explain how to enable permission in the settings app. 5) My app will now store some state about how it has asked permissions for push notifications so that the next time the user is taken through this flow they will go straight to step 4. Without this change we could listen to the `register` event that PushNotificationIOS fires on the success sc Closes https://github.com/facebook/react-native/pull/7900 Differential Revision: D3387424 Pulled By: nicklockwood fbshipit-source-id: e27df41e83216e4e2a14d1e42c6b26e72236f48c --- .../PushNotificationIOS.js | 12 +++++- .../RCTPushNotificationManager.m | 43 ++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/Libraries/PushNotificationIOS/PushNotificationIOS.js b/Libraries/PushNotificationIOS/PushNotificationIOS.js index 89b8dc986aa17b..0fd8260625412d 100644 --- a/Libraries/PushNotificationIOS/PushNotificationIOS.js +++ b/Libraries/PushNotificationIOS/PushNotificationIOS.js @@ -216,12 +216,20 @@ class PushNotificationIOS { * * If a map is provided to the method, only the permissions with truthy values * will be requested. + + * This method returns a promise that will resolve when the user accepts, + * rejects, or if the permissions were previously rejected. The promise + * resolves to the current state of the permission. */ static requestPermissions(permissions?: { alert?: boolean, badge?: boolean, sound?: boolean - }) { + }): Promise<{ + alert: boolean, + badge: boolean, + sound: boolean + }> { var requestedPermissions = {}; if (permissions) { requestedPermissions = { @@ -236,7 +244,7 @@ class PushNotificationIOS { sound: true }; } - RCTPushNotificationManager.requestPermissions(requestedPermissions); + return RCTPushNotificationManager.requestPermissions(requestedPermissions); } /** diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m index 64f0e8de512680..329f7644d3f91a 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -27,6 +27,13 @@ NSString *const RCTLocalNotificationReceived = @"LocalNotificationReceived"; NSString *const RCTRemoteNotificationReceived = @"RemoteNotificationReceived"; NSString *const RCTRemoteNotificationsRegistered = @"RemoteNotificationsRegistered"; +NSString *const RCTRegisterUserNotificationSettings = @"RegisterUserNotificationSettings"; + +NSString *const RCTErrorUnableToRequestPermissions = @"E_UNABLE_TO_REQUEST_PERMISSIONS"; + +@interface RCTPushNotificationManager () +@property (nonatomic, copy) RCTPromiseResolveBlock requestPermissionsResolveBlock; +@end @implementation RCTConvert (UILocalNotification) @@ -66,6 +73,10 @@ - (void)startObserving selector:@selector(handleRemoteNotificationsRegistered:) name:RCTRemoteNotificationsRegistered object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleRegisterUserNotificationSettings:) + name:RCTRegisterUserNotificationSettings + object:nil]; } - (void)stopObserving @@ -93,6 +104,9 @@ + (void)didRegisterUserNotificationSettings:(__unused UIUserNotificationSettings { if ([UIApplication instancesRespondToSelector:@selector(registerForRemoteNotifications)]) { [[UIApplication sharedApplication] registerForRemoteNotifications]; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTRegisterUserNotificationSettings + object:self + userInfo:@{@"notificationSettings": notificationSettings}]; } } @@ -145,6 +159,23 @@ - (void)handleRemoteNotificationsRegistered:(NSNotification *)notification [self sendEventWithName:@"remoteNotificationsRegistered" body:notification.userInfo]; } +- (void)handleRegisterUserNotificationSettings:(NSNotification *)notification +{ + if (self.requestPermissionsResolveBlock == nil) { + return; + } + + UIUserNotificationSettings *notificationSettings = notification.userInfo[@"notificationSettings"]; + NSDictionary *notificationTypes = @{ + @"alert": @((notificationSettings.types & UIUserNotificationTypeAlert) > 0), + @"sound": @((notificationSettings.types & UIUserNotificationTypeSound) > 0), + @"badge": @((notificationSettings.types & UIUserNotificationTypeBadge) > 0), + }; + + self.requestPermissionsResolveBlock(notificationTypes); + self.requestPermissionsResolveBlock = nil; +} + /** * Update the application icon badge number on the home screen */ @@ -161,12 +192,22 @@ - (void)handleRemoteNotificationsRegistered:(NSNotification *)notification callback(@[@(RCTSharedApplication().applicationIconBadgeNumber)]); } -RCT_EXPORT_METHOD(requestPermissions:(NSDictionary *)permissions) +RCT_EXPORT_METHOD(requestPermissions:(NSDictionary *)permissions + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { if (RCTRunningInAppExtension()) { + reject(RCTErrorUnableToRequestPermissions, nil, RCTErrorWithMessage(@"Requesting push notifications is currently unavailable in an app extension")); return; } + if (self.requestPermissionsResolveBlock != nil) { + RCTLogError(@"Cannot call requestPermissions twice before the first has returned."); + return; + } + + self.requestPermissionsResolveBlock = resolve; + UIUserNotificationType types = UIUserNotificationTypeNone; if (permissions) { if ([RCTConvert BOOL:permissions[@"alert"]]) { From 59617646681f54759931a9af661497e5b2ffbf00 Mon Sep 17 00:00:00 2001 From: Gerald Monaco Date: Fri, 3 Jun 2016 16:52:17 -0700 Subject: [PATCH 209/843] Recenter RCTScrollView when width or height are equal Reviewed By: sahrens Differential Revision: D3375944 fbshipit-source-id: 74f1c1f98364604a9be786ff233f230799d9b75d --- React/Views/RCTScrollView.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index 18aa3c952c98de..a90de0d0436e6e 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -259,10 +259,10 @@ - (void)setContentOffset:(CGPoint)contentOffset if (contentView && _centerContent) { CGSize subviewSize = contentView.frame.size; CGSize scrollViewSize = self.bounds.size; - if (subviewSize.width < scrollViewSize.width) { + if (subviewSize.width <= scrollViewSize.width) { contentOffset.x = -(scrollViewSize.width - subviewSize.width) / 2.0; } - if (subviewSize.height < scrollViewSize.height) { + if (subviewSize.height <= scrollViewSize.height) { contentOffset.y = -(scrollViewSize.height - subviewSize.height) / 2.0; } } From 5aa0e098b48e521f6ffe799143b1a4254b0dd4a3 Mon Sep 17 00:00:00 2001 From: Siqi Liu Date: Fri, 3 Jun 2016 17:11:51 -0700 Subject: [PATCH 210/843] Intercepting all redboxes in Android Ads Manager Summary: Implement a handler to allow intercepting all RN redboxes in Android, including exceptions in both JS and Java. The handler is not open sourced, so there is only an open-source interface called **RedBoxHandler** in //fbandroid/java/com/facebook/catalyst/js/react-native-github/ReactAndroid/src/main/java/com/facebook/react/devsupport//, meantime there is an internal class called **FBRedBoxHandler**, which implements **RedBoxHandler** and is located in //fbandroid/java/com/facebook/fbreact/redboxhandler//, actually handles the exception information. The code structure is as follows: - **AdsManagerActivity** has a member variable of **FBRedBoxHandler**. - **AdsManagerActivity** passes this handler all the way down to the **DevSupportManagerImpl**, through** ReactInstanceManager**, **ReactInstanceManagerImpl**, **DevSupportManagerFactory**. - **DevSupportManagerImpl** intercepts the exceptions just before showing the redboxes, like this: mRedBoxDialog.setExceptionDetails(message, stack); mRedBoxDialog.setErrorCookie(errorCookie); if (mRedBoxHandler != null) { mRedBoxHandler.handleRedbox(message, stack); } mRedBoxDialog.show(); By now, the internal class just prints information for each redbox to logcat, including exception message and stack trace. Reviewed By: mkonicek Differential Revision: D3369064 fbshipit-source-id: 199012c4b6ecf4b3d3aff51a26c9c9901847b6fc --- .../facebook/react/ReactInstanceManager.java | 10 ++- .../react/ReactInstanceManagerImpl.java | 38 ++++++++++- .../facebook/react/XReactInstanceManager.java | 3 +- .../react/XReactInstanceManagerImpl.java | 37 ++++++++++- .../devsupport/DevSupportManagerFactory.java | 64 +++++++++++++------ .../devsupport/DevSupportManagerImpl.java | 36 +++++++++-- .../react/devsupport/RedBoxHandler.java | 21 ++++++ .../react/devsupport/StackTraceHelper.java | 2 +- 8 files changed, 177 insertions(+), 34 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxHandler.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index 0a82ddd5438286..f6db4ae3510838 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -25,6 +25,7 @@ import com.facebook.react.bridge.ReactContext; import com.facebook.react.common.annotations.VisibleForTesting; import com.facebook.react.devsupport.DevSupportManager; +import com.facebook.react.devsupport.RedBoxHandler; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.uimanager.UIImplementationProvider; import com.facebook.react.uimanager.ViewManager; @@ -188,6 +189,7 @@ public static class Builder { protected @Nullable JSCConfig mJSCConfig; protected @Nullable Activity mCurrentActivity; protected @Nullable DefaultHardwareBackBtnHandler mDefaultHardwareBackBtnHandler; + protected @Nullable RedBoxHandler mRedBoxHandler; protected Builder() { } @@ -297,6 +299,11 @@ public Builder setJSCConfig(JSCConfig jscConfig) { return this; } + public Builder setRedBoxHandler(RedBoxHandler redBoxHandler) { + mRedBoxHandler = redBoxHandler; + return this; + } + /** * Instantiates a new {@link ReactInstanceManagerImpl}. * Before calling {@code build}, the following must be called: @@ -335,7 +342,8 @@ public ReactInstanceManager build() { Assertions.assertNotNull(mInitialLifecycleState, "Initial lifecycle state was not set"), mUIImplementationProvider, mNativeModuleCallExceptionHandler, - mJSCConfig); + mJSCConfig, + mRedBoxHandler); } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java index bfaf7e6bab1ced..b347a81bfb58ef 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java @@ -57,6 +57,7 @@ import com.facebook.react.devsupport.DevSupportManager; import com.facebook.react.devsupport.DevSupportManagerFactory; import com.facebook.react.devsupport.ReactInstanceDevCommandsHandler; +import com.facebook.react.devsupport.RedBoxHandler; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.uimanager.AppRegistry; @@ -122,6 +123,7 @@ private final MemoryPressureRouter mMemoryPressureRouter; private final @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; private final @Nullable JSCConfig mJSCConfig; + private @Nullable RedBoxHandler mRedBoxHandler; private final ReactInstanceDevCommandsHandler mDevInterface = new ReactInstanceDevCommandsHandler() { @@ -262,6 +264,35 @@ public T get() throws Exception { } } + /* package */ ReactInstanceManagerImpl( + Context applicationContext, + @Nullable Activity currentActivity, + @Nullable DefaultHardwareBackBtnHandler defaultHardwareBackBtnHandler, + @Nullable String jsBundleFile, + @Nullable String jsMainModuleName, + List packages, + boolean useDeveloperSupport, + @Nullable NotThreadSafeBridgeIdleDebugListener bridgeIdleDebugListener, + LifecycleState initialLifecycleState, + UIImplementationProvider uiImplementationProvider, + NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler, + @Nullable JSCConfig jscConfig) { + + this(applicationContext, + currentActivity, + defaultHardwareBackBtnHandler, + jsBundleFile, + jsMainModuleName, + packages, + useDeveloperSupport, + bridgeIdleDebugListener, + initialLifecycleState, + uiImplementationProvider, + nativeModuleCallExceptionHandler, + jscConfig, + null); + } + /* package */ ReactInstanceManagerImpl( Context applicationContext, @Nullable Activity currentActivity, @@ -274,7 +305,8 @@ public T get() throws Exception { LifecycleState initialLifecycleState, UIImplementationProvider uiImplementationProvider, NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler, - @Nullable JSCConfig jscConfig) { + @Nullable JSCConfig jscConfig, + @Nullable RedBoxHandler redBoxHandler) { initializeSoLoaderIfNecessary(applicationContext); // TODO(9577825): remove this @@ -288,11 +320,13 @@ public T get() throws Exception { mJSMainModuleName = jsMainModuleName; mPackages = packages; mUseDeveloperSupport = useDeveloperSupport; + mRedBoxHandler = redBoxHandler; mDevSupportManager = DevSupportManagerFactory.create( applicationContext, mDevInterface, mJSMainModuleName, - useDeveloperSupport); + useDeveloperSupport, + mRedBoxHandler); mBridgeIdleDebugListener = bridgeIdleDebugListener; mLifecycleState = initialLifecycleState; mUIImplementationProvider = uiImplementationProvider; diff --git a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java index 9ac350808b87c5..b3ba638c518279 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java @@ -62,7 +62,8 @@ public ReactInstanceManager build() { Assertions.assertNotNull(mInitialLifecycleState, "Initial lifecycle state was not set"), mUIImplementationProvider, mNativeModuleCallExceptionHandler, - mJSCConfig); + mJSCConfig, + mRedBoxHandler); } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java index 9f5ed6c1960d9a..cdc96efede6c99 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java @@ -67,6 +67,7 @@ import com.facebook.react.uimanager.ViewManager; import com.facebook.soloader.SoLoader; import com.facebook.systrace.Systrace; +import com.facebook.react.devsupport.RedBoxHandler; import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_JS_MODULE_CONFIG_END; import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_JS_MODULE_CONFIG_START; @@ -125,6 +126,7 @@ private final MemoryPressureRouter mMemoryPressureRouter; private final @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; private final @Nullable JSCConfig mJSCConfig; + private @Nullable RedBoxHandler mRedBoxHandler; private final ReactInstanceDevCommandsHandler mDevInterface = new ReactInstanceDevCommandsHandler() { @@ -276,6 +278,37 @@ public T get() throws Exception { UIImplementationProvider uiImplementationProvider, NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler, @Nullable JSCConfig jscConfig) { + + this(applicationContext, + currentActivity, + defaultHardwareBackBtnHandler, + jsBundleFile, + jsMainModuleName, + packages, + useDeveloperSupport, + bridgeIdleDebugListener, + initialLifecycleState, + uiImplementationProvider, + nativeModuleCallExceptionHandler, + jscConfig, + null); + } + + /* package */ XReactInstanceManagerImpl( + Context applicationContext, + @Nullable Activity currentActivity, + @Nullable DefaultHardwareBackBtnHandler defaultHardwareBackBtnHandler, + @Nullable String jsBundleFile, + @Nullable String jsMainModuleName, + List packages, + boolean useDeveloperSupport, + @Nullable NotThreadSafeBridgeIdleDebugListener bridgeIdleDebugListener, + LifecycleState initialLifecycleState, + UIImplementationProvider uiImplementationProvider, + NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler, + @Nullable JSCConfig jscConfig, + @Nullable RedBoxHandler redBoxHandler) { + initializeSoLoaderIfNecessary(applicationContext); // TODO(9577825): remove this @@ -289,11 +322,13 @@ public T get() throws Exception { mJSMainModuleName = jsMainModuleName; mPackages = packages; mUseDeveloperSupport = useDeveloperSupport; + mRedBoxHandler = redBoxHandler; mDevSupportManager = DevSupportManagerFactory.create( applicationContext, mDevInterface, mJSMainModuleName, - useDeveloperSupport); + useDeveloperSupport, + mRedBoxHandler); mBridgeIdleDebugListener = bridgeIdleDebugListener; mLifecycleState = initialLifecycleState; mUIImplementationProvider = uiImplementationProvider; diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.java index 22d925672598bd..d41db6f5cf547b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerFactory.java @@ -1,3 +1,12 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + package com.facebook.react.devsupport; import javax.annotation.Nullable; @@ -5,10 +14,6 @@ import java.lang.reflect.Constructor; import android.content.Context; -import android.util.Log; - -import com.facebook.react.common.ReactConstants; -import com.facebook.react.common.build.ReactBuildConfig; /** * A simple factory that creates instances of {@link DevSupportManager} implementations. Uses @@ -26,6 +31,21 @@ public static DevSupportManager create( ReactInstanceDevCommandsHandler reactInstanceCommandsHandler, @Nullable String packagerPathForJSBundleName, boolean enableOnCreate) { + + return create( + applicationContext, + reactInstanceCommandsHandler, + packagerPathForJSBundleName, + enableOnCreate, + null); + } + + public static DevSupportManager create( + Context applicationContext, + ReactInstanceDevCommandsHandler reactInstanceCommandsHandler, + @Nullable String packagerPathForJSBundleName, + boolean enableOnCreate, + @Nullable RedBoxHandler redBoxHandler) { if (!enableOnCreate) { return new DisabledDevSupportManager(); } @@ -34,28 +54,30 @@ public static DevSupportManager create( // Class.forName() with a static string. So instead we generate a quasi-dynamic string to // confuse it. String className = - new StringBuilder(DEVSUPPORT_IMPL_PACKAGE) - .append(".") - .append(DEVSUPPORT_IMPL_CLASS) - .toString(); + new StringBuilder(DEVSUPPORT_IMPL_PACKAGE) + .append(".") + .append(DEVSUPPORT_IMPL_CLASS) + .toString(); Class devSupportManagerClass = - Class.forName(className); + Class.forName(className); Constructor constructor = - devSupportManagerClass.getConstructor( - Context.class, - ReactInstanceDevCommandsHandler.class, - String.class, - boolean.class); + devSupportManagerClass.getConstructor( + Context.class, + ReactInstanceDevCommandsHandler.class, + String.class, + boolean.class, + RedBoxHandler.class); return (DevSupportManager) constructor.newInstance( - applicationContext, - reactInstanceCommandsHandler, - packagerPathForJSBundleName, - true); + applicationContext, + reactInstanceCommandsHandler, + packagerPathForJSBundleName, + true, + redBoxHandler); } catch (Exception e) { throw new RuntimeException( - "Requested enabled DevSupportManager, but DevSupportManagerImpl class was not found" + - " or could not be created", - e); + "Requested enabled DevSupportManager, but DevSupportManagerImpl class was not found" + + " or could not be created", + e); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index ee4a7f3a696a7d..35d7471fefd45d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -111,12 +111,13 @@ public class DevSupportManagerImpl implements DevSupportManager { private boolean mIsDevSupportEnabled = false; private boolean mIsCurrentlyProfiling = false; private int mProfileIndex = 0; + private @Nullable RedBoxHandler mRedBoxHandler; public DevSupportManagerImpl( - Context applicationContext, - ReactInstanceDevCommandsHandler reactInstanceCommandsHandler, - @Nullable String packagerPathForJSBundleName, - boolean enableOnCreate) { + Context applicationContext, + ReactInstanceDevCommandsHandler reactInstanceCommandsHandler, + @Nullable String packagerPathForJSBundleName, + boolean enableOnCreate) { mReactInstanceCommandsHandler = reactInstanceCommandsHandler; mApplicationContext = applicationContext; mJSAppBundleName = packagerPathForJSBundleName; @@ -160,6 +161,21 @@ public void onReceive(Context context, Intent intent) { setDevSupportEnabled(enableOnCreate); } + public DevSupportManagerImpl( + Context applicationContext, + ReactInstanceDevCommandsHandler reactInstanceCommandsHandler, + @Nullable String packagerPathForJSBundleName, + boolean enableOnCreate, + @Nullable RedBoxHandler redBoxHandler) { + + this(applicationContext, + reactInstanceCommandsHandler, + packagerPathForJSBundleName, + enableOnCreate); + + mRedBoxHandler = redBoxHandler; + } + @Override public void handleException(Exception e) { if (mIsDevSupportEnabled) { @@ -209,9 +225,12 @@ public void run() { errorCookie != mRedBoxDialog.getErrorCookie()) { return; } - mRedBoxDialog.setExceptionDetails( - message, - StackTraceHelper.convertJsStackTrace(details)); + StackFrame[] stack = StackTraceHelper.convertJsStackTrace(details); + mRedBoxDialog.setExceptionDetails(message, stack); + mRedBoxDialog.setErrorCookie(errorCookie); + if (mRedBoxHandler != null) { + mRedBoxHandler.handleRedbox(message, stack); + } mRedBoxDialog.show(); } }); @@ -244,6 +263,9 @@ public void run() { } mRedBoxDialog.setExceptionDetails(message, stack); mRedBoxDialog.setErrorCookie(errorCookie); + if (mRedBoxHandler != null) { + mRedBoxHandler.handleRedbox(message, stack); + } mRedBoxDialog.show(); } }); diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxHandler.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxHandler.java new file mode 100644 index 00000000000000..61b9bce9006b74 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxHandler.java @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.devsupport; + +import com.facebook.react.devsupport.StackTraceHelper.StackFrame; + +/** + * Interface used by {@link DevSupportManagerImpl} to allow interception on any redboxes + * during development and handling the information from the redbox. + * The implementation should be passed by {@link #setRedBoxHandler} in {@link ReactInstanceManager}. + */ +public interface RedBoxHandler { + void handleRedbox(String title, StackFrame[] stack); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/StackTraceHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/StackTraceHelper.java index 179c0c35d96aac..aaec579198cf7b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/StackTraceHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/StackTraceHelper.java @@ -17,7 +17,7 @@ /** * Helper class converting JS and Java stack traces into arrays of {@link StackFrame} objects. */ -/* package */ class StackTraceHelper { +public class StackTraceHelper { /** * Represents a generic entry in a stack trace, be it originally from JS or Java. From e62a6a71c95e4b1ff8ebf42491ddd6f88e584d7f Mon Sep 17 00:00:00 2001 From: Paul Shen Date: Fri, 3 Jun 2016 17:43:26 -0700 Subject: [PATCH 211/843] Typo fix in Touchable example Summary: **Test plan** Run UIExplorer example and see that enabled TouchableHighlight example has correct text. ![image](https://cloud.githubusercontent.com/assets/2266187/15682555/2117ec9a-2713-11e6-9272-b868a8d8d705.png) Closes https://github.com/facebook/react-native/pull/7853 Differential Revision: D3387862 fbshipit-source-id: c37bd1e5b01b778371978ac69a7cef43effb149f --- Examples/UIExplorer/TouchableExample.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/UIExplorer/TouchableExample.js b/Examples/UIExplorer/TouchableExample.js index a78d27e001a5ba..12f417b2d4e2fd 100644 --- a/Examples/UIExplorer/TouchableExample.js +++ b/Examples/UIExplorer/TouchableExample.js @@ -333,7 +333,7 @@ var TouchableDisabled = React.createClass({ style={[styles.row, styles.block]} onPress={() => console.log('custom THW text - highlight')}> - Disabled TouchableHighlight + Enabled TouchableHighlight From 0f35f7c6d543da742622be5d3ad1884cfbf5d720 Mon Sep 17 00:00:00 2001 From: Sean Kozer Date: Fri, 3 Jun 2016 21:38:08 -0700 Subject: [PATCH 212/843] Link React library to Tests target Summary: Fixes https://github.com/facebook/react-native/issues/2685 Closes https://github.com/facebook/react-native/pull/7918 Differential Revision: D3389814 fbshipit-source-id: d5054dae386d66e8055c883581f142ec24e60e18 --- local-cli/generator-ios/templates/xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/local-cli/generator-ios/templates/xcodeproj/project.pbxproj b/local-cli/generator-ios/templates/xcodeproj/project.pbxproj index 4eb04cda13421b..2aff4338b436b0 100644 --- a/local-cli/generator-ios/templates/xcodeproj/project.pbxproj +++ b/local-cli/generator-ios/templates/xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 140ED2AC1D01E1AD002B40FF /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; /* End PBXBuildFile section */ @@ -133,6 +134,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 140ED2AC1D01E1AD002B40FF /* libReact.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; From e63ea3acc4e6b0cf4efbe6c186c212c54e1d64e7 Mon Sep 17 00:00:00 2001 From: tantan Date: Sat, 4 Jun 2016 08:42:37 -0700 Subject: [PATCH 213/843] add progressListener for android when using FormData to upload files Summary: When using FormData upload images or files, in Android version, network module cannot send an event for showing progress. This PR will solve this issue. I changed example in XHRExample for Android, you can see uploading progress in warning yellow bar. Closes https://github.com/facebook/react-native/pull/7256 Differential Revision: D3390087 fbshipit-source-id: 7f3e53c80072fff397afd6f5fe17bf0f2ecd83b2 --- Examples/UIExplorer/XHRExample.android.js | 62 ++++++++++++++-- Examples/UIExplorer/XHRExample.ios.js | 15 ++-- .../modules/network/NetworkingModule.java | 28 +++++++- .../modules/network/ProgressRequestBody.java | 70 +++++++++++++++++++ .../network/ProgressRequestListener.java | 15 ++++ .../modules/network/RequestBodyUtil.java | 7 ++ .../modules/network/NetworkingModuleTest.java | 5 ++ 7 files changed, 185 insertions(+), 17 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestBody.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestListener.java diff --git a/Examples/UIExplorer/XHRExample.android.js b/Examples/UIExplorer/XHRExample.android.js index 7cadaa4ffe6e38..991f88d4528671 100644 --- a/Examples/UIExplorer/XHRExample.android.js +++ b/Examples/UIExplorer/XHRExample.android.js @@ -24,6 +24,8 @@ var { TextInput, TouchableHighlight, View, + Image, + CameraRoll } = ReactNative; var XHRExampleHeaders = require('./XHRExampleHeaders'); @@ -127,6 +129,8 @@ class Downloader extends React.Component { } } +var PAGE_SIZE = 20; + class FormUploader extends React.Component { _isMounted: boolean; @@ -143,6 +147,8 @@ class FormUploader extends React.Component { this._isMounted = true; this._addTextParam = this._addTextParam.bind(this); this._upload = this._upload.bind(this); + this._fetchRandomPhoto = this._fetchRandomPhoto.bind(this); + this._fetchRandomPhoto(); } _addTextParam() { @@ -151,6 +157,25 @@ class FormUploader extends React.Component { this.setState({textParams}); } + _fetchRandomPhoto() { + CameraRoll.getPhotos( + {first: PAGE_SIZE} + ).then( + (data) => { + if (!this._isMounted) { + return; + } + var edges = data.edges; + var edge = edges[Math.floor(Math.random() * edges.length)]; + var randomPhoto = edge && edge.node && edge.node.image; + if (randomPhoto) { + this.setState({randomPhoto}); + } + }, + (error) => undefined + ); + } + componentWillUnmount() { this._isMounted = false; } @@ -201,19 +226,29 @@ class FormUploader extends React.Component { this.state.textParams.forEach( (param) => formdata.append(param.name, param.value) ); - if (xhr.upload) { - xhr.upload.onprogress = (event) => { - console.log('upload onprogress', event); - if (event.lengthComputable) { - this.setState({uploadProgress: event.loaded / event.total}); - } - }; + if (this.state.randomPhoto) { + formdata.append('image', {...this.state.randomPhoto, type:'image/jpg', name: 'image.jpg'}); } + xhr.upload.onprogress = (event) => { + console.log('upload onprogress', event); + if (event.lengthComputable) { + this.setState({uploadProgress: event.loaded / event.total}); + } + }; xhr.send(formdata); this.setState({isUploading: true}); } render() { + var image = null; + if (this.state.randomPhoto) { + image = ( + + ); + } var textItems = this.state.textParams.map((item, index) => ( + + + Random photo from your library + ( + update + ) + + {image} + {textItems} formdata.append(param.name, param.value) ); - if (xhr.upload) { - xhr.upload.onprogress = (event) => { - console.log('upload onprogress', event); - if (event.lengthComputable) { - this.setState({uploadProgress: event.loaded / event.total}); - } - }; - } + xhr.upload.onprogress = (event) => { + console.log('upload onprogress', event); + if (event.lengthComputable) { + this.setState({uploadProgress: event.loaded / event.total}); + } + }; + xhr.send(formdata); this.setState({isUploading: true}); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java index 97a8039658e068..91afa78622ea95 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java @@ -56,7 +56,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule { private static final String REQUEST_BODY_KEY_URI = "uri"; private static final String REQUEST_BODY_KEY_FORMDATA = "formData"; private static final String USER_AGENT_HEADER_NAME = "user-agent"; - + private static final int CHUNK_TIMEOUT_NS = 100 * 1000000; // 100ms private static final int MAX_CHUNK_SIZE_BETWEEN_FLUSHES = 8 * 1024; // 8K private final OkHttpClient mClient; @@ -239,7 +239,19 @@ public void sendRequest( if (multipartBuilder == null) { return; } - requestBuilder.method(method, multipartBuilder.build()); + + requestBuilder.method(method, RequestBodyUtil.createProgressRequest(multipartBuilder.build(), new ProgressRequestListener() { + long last = System.nanoTime(); + + @Override + public void onRequestProgress(long bytesWritten, long contentLength, boolean done) { + long now = System.nanoTime(); + if (done || shouldDispatch(now, last)) { + onDataSend(executorToken, requestId, bytesWritten,contentLength); + last = now; + } + } + })); } else { // Nothing in data payload, at least nothing we could understand anyway. requestBuilder.method(method, RequestBodyUtil.getEmptyBody(method)); @@ -298,6 +310,18 @@ private void readWithProgress( } } + private static boolean shouldDispatch(long now, long last) { + return last + CHUNK_TIMEOUT_NS < now; + } + + private void onDataSend(ExecutorToken ExecutorToken, int requestId, long progress, long total) { + WritableArray args = Arguments.createArray(); + args.pushInt(requestId); + args.pushInt((int) progress); + args.pushInt((int) total); + getEventEmitter(ExecutorToken).emit("didSendNetworkData", args); + } + private void onDataReceived(ExecutorToken ExecutorToken, int requestId, String data) { WritableArray args = Arguments.createArray(); args.pushInt(requestId); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestBody.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestBody.java new file mode 100644 index 00000000000000..0e511f926dda19 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestBody.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.network; + +import java.io.IOException; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okhttp3.internal.Util; +import okio.BufferedSink; +import okio.Buffer; +import okio.Sink; +import okio.ForwardingSink; +import okio.ByteString; +import okio.Okio; +import okio.Source; + +public class ProgressRequestBody extends RequestBody { + + private final RequestBody mRequestBody; + private final ProgressRequestListener mProgressListener; + private BufferedSink mBufferedSink; + + public ProgressRequestBody(RequestBody requestBody, ProgressRequestListener progressListener) { + mRequestBody = requestBody; + mProgressListener = progressListener; + } + + @Override + public MediaType contentType() { + return mRequestBody.contentType(); + } + + @Override + public long contentLength() throws IOException { + return mRequestBody.contentLength(); + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + if (mBufferedSink == null) { + mBufferedSink = Okio.buffer(sink(sink)); + } + mRequestBody.writeTo(mBufferedSink); + mBufferedSink.flush(); + } + + private Sink sink(Sink sink) { + return new ForwardingSink(sink) { + long bytesWritten = 0L; + long contentLength = 0L; + + @Override + public void write(Buffer source, long byteCount) throws IOException { + super.write(source, byteCount); + if (contentLength == 0) { + contentLength = contentLength(); + } + bytesWritten += byteCount; + mProgressListener.onRequestProgress(bytesWritten, contentLength, bytesWritten == contentLength); + } + }; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestListener.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestListener.java new file mode 100644 index 00000000000000..10230e6dcb9c56 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ProgressRequestListener.java @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.network; + + +public interface ProgressRequestListener { + void onRequestProgress(long bytesWritten, long contentLength, boolean done); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java index 0290a23fd2670f..1d5a5e1d916a05 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java @@ -114,6 +114,13 @@ public void writeTo(BufferedSink sink) throws IOException { }; } + /** + * Creates a ProgressRequestBody that can be used for showing uploading progress + */ + public static ProgressRequestBody createProgressRequest(RequestBody requestBody, ProgressRequestListener listener) { + return new ProgressRequestBody(requestBody, listener); + } + /** * Creates a empty RequestBody if required by the http method spec, otherwise use null */ diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java index ec04b5cbdd4067..8e5f40cb78030f 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java @@ -60,6 +60,8 @@ Arguments.class, Call.class, RequestBodyUtil.class, + ProgressRequestBody.class, + ProgressRequestListener.class, MultipartBody.class, MultipartBody.Builder.class, NetworkingModule.class, @@ -262,6 +264,7 @@ public void testMultipartPostRequestSimple() throws Exception { .thenReturn(mock(InputStream.class)); when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))) .thenReturn(mock(RequestBody.class)); + when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressRequestListener.class))).thenCallRealMethod(); JavaOnlyMap body = new JavaOnlyMap(); JavaOnlyArray formData = new JavaOnlyArray(); @@ -316,6 +319,7 @@ public void testMultipartPostRequestHeaders() throws Exception { .thenReturn(mock(InputStream.class)); when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))) .thenReturn(mock(RequestBody.class)); + when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressRequestListener.class))).thenCallRealMethod(); List headers = Arrays.asList( JavaOnlyArray.of("Accept", "text/plain"), @@ -378,6 +382,7 @@ public void testMultipartPostRequestBody() throws Exception { when(RequestBodyUtil.getFileInputStream(any(ReactContext.class), any(String.class))) .thenReturn(inputStream); when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))).thenCallRealMethod(); + when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressRequestListener.class))).thenCallRealMethod(); when(inputStream.available()).thenReturn("imageUri".length()); final MultipartBody.Builder multipartBuilder = mock(MultipartBody.Builder.class); From 3f92e09787c0be0b5906f361ed8e5b3f17b2abea Mon Sep 17 00:00:00 2001 From: Marco Bonaldo Date: Sat, 4 Jun 2016 12:36:51 -0700 Subject: [PATCH 214/843] Fix header elevation of NavigationExperimental Summary: Updated according to the documentation: http://www.google.com/design/spec/what-is-material/elevation-shadows.html#elevation-shadows-elevation-android- Closes https://github.com/facebook/react-native/pull/7289 Differential Revision: D3390296 Pulled By: ericvicenti fbshipit-source-id: 4c475a6c4b6cd1c40b4d995ed18d31d9e179a69a --- .../CustomComponents/NavigationExperimental/NavigationHeader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js b/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js index 0140f3b50a1bc3..6bde840730b4b0 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationHeader.js @@ -220,7 +220,7 @@ const styles = StyleSheet.create({ backgroundColor: Platform.OS === 'ios' ? '#EFEFF2' : '#FFF', borderBottomColor: 'rgba(0, 0, 0, .15)', borderBottomWidth: Platform.OS === 'ios' ? StyleSheet.hairlineWidth : 0, - elevation: 2, + elevation: 4, flexDirection: 'row', height: APPBAR_HEIGHT + STATUSBAR_HEIGHT, justifyContent: 'flex-start', From 28b16dd294776deb4c06e65453277707645da9ed Mon Sep 17 00:00:00 2001 From: Mohit Natoo Date: Sat, 4 Jun 2016 15:48:42 -0700 Subject: [PATCH 215/843] use 'a uri' instead of 'an uri' as uri is pronounced as 'You R I' Summary: Closes https://github.com/facebook/react-native/pull/7912 Differential Revision: D3390536 fbshipit-source-id: b0ca315b1e8b4b5ab0c7933cf995c9982a074afc --- docs/Images.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Images.md b/docs/Images.md index 89c8d44638b5a4..f00363c1695b36 100644 --- a/docs/Images.md +++ b/docs/Images.md @@ -113,7 +113,7 @@ For example, the result of `require('./my-icon.png')` might be: ## Source as an object -In React Native, one interesting decision is that the `src` attribute is named `source` and doesn't take a string but an object with an `uri` attribute. +In React Native, one interesting decision is that the `src` attribute is named `source` and doesn't take a string but an object with a `uri` attribute. ```javascript From 58fb91e62bbab1db648725367f5b1af3350a1954 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Sat, 4 Jun 2016 15:57:01 -0700 Subject: [PATCH 216/843] Android: allow app/build.gradle to specify how node gets invoked for the packager Summary: **Test plan:** With the given patch applied to `react.gradle`, I specified the following in my `android/app/build.gradle`: ``` project.ext.react = [ nodeExecutableAndArgs: ["node", "--max_old_space_size=4096"] ] ``` and ensured in a `./gradlew installDebug --debug` run that the packager gets indeed invoked with these parameters. Closes https://github.com/facebook/react-native/pull/7903 Differential Revision: D3390543 fbshipit-source-id: cf440b36633420b8f67070f47dfabf4c84cb28a7 --- local-cli/generator-android/templates/src/app/build.gradle | 3 +++ react.gradle | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/local-cli/generator-android/templates/src/app/build.gradle b/local-cli/generator-android/templates/src/app/build.gradle index 11380b0062f60f..63ab2dcbf9c37f 100644 --- a/local-cli/generator-android/templates/src/app/build.gradle +++ b/local-cli/generator-android/templates/src/app/build.gradle @@ -57,6 +57,9 @@ import com.android.build.OutputFile * // for example, you might want to remove it from here. * inputExcludes: ["android/**", "ios/**"], * + * // override which node gets called and with what additional arguments + * nodeExecutableAndArgs: ["node"] + * * // supply additional arguments to the packager * extraPackagerArgs: [] * ] diff --git a/react.gradle b/react.gradle index f52b30c3b284d2..ffa9e48c6d37ed 100644 --- a/react.gradle +++ b/react.gradle @@ -49,7 +49,8 @@ gradle.projectsEvaluated { // Bundle task name for variant def bundleJsAndAssetsTaskName = "bundle${targetName}JsAndAssets" - // Additional packager commandline arguments + // Additional node and packager commandline arguments + def nodeExecutableAndArgs = config.nodeExecutableAndArgs ?: ["node"] def extraPackagerArgs = config.extraPackagerArgs ?: [] def currentBundleTask = tasks.create( @@ -75,10 +76,10 @@ gradle.projectsEvaluated { // Set up dev mode def devEnabled = !targetName.toLowerCase().contains("release") if (Os.isFamily(Os.FAMILY_WINDOWS)) { - commandLine("cmd", "/c", "node", "node_modules/react-native/local-cli/cli.js", "bundle", "--platform", "android", "--dev", "${devEnabled}", + commandLine("cmd", "/c", *nodeExecutableAndArgs, "node_modules/react-native/local-cli/cli.js", "bundle", "--platform", "android", "--dev", "${devEnabled}", "--reset-cache", "true", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir, *extraPackagerArgs) } else { - commandLine("node", "node_modules/react-native/local-cli/cli.js", "bundle", "--platform", "android", "--dev", "${devEnabled}", + commandLine(*nodeExecutableAndArgs, "node_modules/react-native/local-cli/cli.js", "bundle", "--platform", "android", "--dev", "${devEnabled}", "--reset-cache", "true", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir, *extraPackagerArgs) } From b91bf8eeacae4c873348cfbfb77bce2a6ca27c64 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Sat, 4 Jun 2016 16:25:55 -0700 Subject: [PATCH 217/843] Update RCTActionSheetManager.m Summary: We use [self bridge] in this class but weren't synthesizing the bridge getters/setters. Fixes https://github.com/facebook/react-native/issues/7890 Closes https://github.com/facebook/react-native/pull/7909 Differential Revision: D3390538 fbshipit-source-id: d75a1976af01b21887ddad5060ce635cf84abadd --- Libraries/ActionSheetIOS/RCTActionSheetManager.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Libraries/ActionSheetIOS/RCTActionSheetManager.m b/Libraries/ActionSheetIOS/RCTActionSheetManager.m index d38058fc493478..483781d52bc48a 100644 --- a/Libraries/ActionSheetIOS/RCTActionSheetManager.m +++ b/Libraries/ActionSheetIOS/RCTActionSheetManager.m @@ -27,6 +27,8 @@ @implementation RCTActionSheetManager RCT_EXPORT_MODULE() +@synthesize bridge = _bridge; + - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); From f49f28ce1567460ec40fc2625ca332fc24e7d150 Mon Sep 17 00:00:00 2001 From: Kasim Tan Date: Sat, 4 Jun 2016 16:30:19 -0700 Subject: [PATCH 218/843] Fix typo (shoud -> should) Summary: Closes https://github.com/facebook/react-native/pull/7928 Differential Revision: D3390537 fbshipit-source-id: 97ce2331d0942ffe21078b3aec27daed20cd8af5 --- .../Navigator/Navigation/NavigationRouteStack.js | 2 +- local-cli/rnpm/core/test/getCommands.spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationRouteStack.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationRouteStack.js index 486285177418e6..f73ab10f01e2af 100644 --- a/Libraries/CustomComponents/Navigator/Navigation/NavigationRouteStack.js +++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationRouteStack.js @@ -149,7 +149,7 @@ class RouteStack { * excluding the last index in this stack. */ pop(): RouteStack { - invariant(this._routeNodes.size > 1, 'shoud not pop routeNodes stack to empty'); + invariant(this._routeNodes.size > 1, 'should not pop routeNodes stack to empty'); // When popping, removes the rest of the routes past the current index. var routeNodes = this._routeNodes.slice(0, this._index); diff --git a/local-cli/rnpm/core/test/getCommands.spec.js b/local-cli/rnpm/core/test/getCommands.spec.js index 58c25446fb9200..4389b064da5969 100644 --- a/local-cli/rnpm/core/test/getCommands.spec.js +++ b/local-cli/rnpm/core/test/getCommands.spec.js @@ -111,7 +111,7 @@ describe('getCommands', () => { afterEach(() => revert()); - it('shoud load when installed locally', () => { + it('should load when installed locally', () => { revert = getCommands.__set__({ __dirname: path.join(LOCAL_NODE_MODULES, 'rnpm/src'), }); From 1facfb77da8f5c70674e023b68ec84226b4948d9 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 5 Jun 2016 03:06:24 -0700 Subject: [PATCH 219/843] Fix for "Unable to resolve module nullthrows" Summary: Recreated due to mistake with https://github.com/facebook/react-native/pull/7594. Closes https://github.com/facebook/react-native/pull/7934 Differential Revision: D3391010 fbshipit-source-id: 7aad253d29f2272cfdaace1c1bb4f54a63faa353 --- Libraries/Experimental/WindowedListView.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/Experimental/WindowedListView.js b/Libraries/Experimental/WindowedListView.js index 92e7e0fc55652a..911b0507d46a31 100644 --- a/Libraries/Experimental/WindowedListView.js +++ b/Libraries/Experimental/WindowedListView.js @@ -44,8 +44,8 @@ const ViewabilityHelper = require('ViewabilityHelper'); const clamp = require('clamp'); const deepDiffer = require('deepDiffer'); const infoLog = require('infoLog'); -const invariant = require('invariant'); -const nullthrows = require('nullthrows'); +const invariant = require('fbjs/lib/invariant'); +const nullthrows = require('fbjs/lib/nullthrows'); import type ReactComponent from 'ReactComponent'; From 8c3db9782e3c6b37b08c05d8dc1666adb843d255 Mon Sep 17 00:00:00 2001 From: David Aurelio Date: Sun, 5 Jun 2016 05:47:26 -0700 Subject: [PATCH 220/843] inline `Platform.select` Summary: We are already inlining `Platform.OS`. This diff adds support to inline calls to `Platform.select` with an object literal as first argument. The transform will replace the call with the property value corresponding to the platform, or `undefined` if it does not exist. Reviewed By: frantic Differential Revision: D3385391 fbshipit-source-id: bb068d17948ed84e381707faeaa0450399c2f306 --- .../worker/__tests__/inline-test.js | 107 ++++++++++++++++++ .../src/JSTransformer/worker/inline.js | 32 ++++++ 2 files changed, 139 insertions(+) diff --git a/packager/react-packager/src/JSTransformer/worker/__tests__/inline-test.js b/packager/react-packager/src/JSTransformer/worker/__tests__/inline-test.js index 102e39ab7ac8b9..b52e07c05fd9bb 100644 --- a/packager/react-packager/src/JSTransformer/worker/__tests__/inline-test.js +++ b/packager/react-packager/src/JSTransformer/worker/__tests__/inline-test.js @@ -141,6 +141,113 @@ describe('inline constants', () => { normalize(code.replace(/require\('react-native'\)\.Platform\.OS/, '"android"'))); }); + it('inlines Platform.select in the code if Platform is a global and the argument is an object literal', () => { + const code = `function a() { + var a = Platform.select({ios: 1, android: 2}); + var b = a.Platform.select({ios: 1, android: 2}); + }`; + const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'}); + expect(toString(ast)).toEqual(normalize(code.replace(/Platform\.select[^;]+/, '1'))); + }); + + it('replaces Platform.select in the code if Platform is a top level import', () => { + const code = ` + var Platform = require('Platform'); + function a() { + Platform.select({ios: 1, android: 2}); + var b = a.Platform.select({}); + }`; + const {ast} = inline('arbitrary.js', {code}, {platform: 'android'}); + expect(toString(ast)).toEqual(normalize(code.replace(/Platform\.select[^;]+/, '2'))); + }); + + it('replaces Platform.select in the code if Platform is a top level import from react-native', () => { + const code = ` + var Platform = require('react-native').Platform; + function a() { + Platform.select({ios: 1, android: 2}); + var b = a.Platform.select({}); + }`; + const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'}); + expect(toString(ast)).toEqual(normalize(code.replace(/Platform\.select[^;]+/, '1'))); + }); + + it('replaces require("Platform").select in the code', () => { + const code = `function a() { + var a = require('Platform').select({ios: 1, android: 2}); + var b = a.require('Platform').select({}); + }`; + const {ast} = inline('arbitrary.js', {code}, {platform: 'android'}); + expect(toString(ast)).toEqual(normalize(code.replace(/Platform\.select[^;]+/, '2'))); + }); + + it('replaces React.Platform.select in the code if React is a global', () => { + const code = `function a() { + var a = React.Platform.select({ios: 1, android: 2}); + var b = a.React.Platform.select({}); + }`; + const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'}); + expect(toString(ast)).toEqual(normalize(code.replace(/React\.Platform\.select[^;]+/, '1'))); + }); + + it('replaces ReactNative.Platform.select in the code if ReactNative is a global', () => { + const code = `function a() { + var a = ReactNative.Platform.select({ios: 1, android: 2}); + var b = a.ReactNative.Platform.select({}); + }`; + const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'}); + expect(toString(ast)).toEqual(normalize(code.replace(/ReactNative\.Platform\.select[^;]+/, '1'))); + }); + + it('replaces React.Platform.select in the code if React is a top level import', () => { + const code = ` + var React = require('React'); + function a() { + var a = React.Platform.select({ios: 1, android: 2}); + var b = a.React.Platform.select({}); + }`; + const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'}); + expect(toString(ast)).toEqual(normalize(code.replace(/React\.Platform\.select[^;]+/, '1'))); + }); + + it('replaces require("React").Platform.select in the code', () => { + const code = `function a() { + var a = require('React').Platform.select({ios: 1, android: 2}); + var b = a.require('React').Platform.select({}); + }`; + const {ast} = inline('arbitrary.js', {code}, {platform: 'android'}); + expect(toString(ast)).toEqual( + normalize(code.replace(/require\('React'\)\.Platform\.select[^;]+/, '2'))); + }); + + it('replaces ReactNative.Platform.select in the code if ReactNative is a top level import', () => { + const code = ` + var ReactNative = require('react-native'); + function a() { + var a = ReactNative.Plaftform.select({ios: 1, android: 2}); + var b = a.ReactNative.Platform.select; + }`; + const {ast} = inline('arbitrary.js', {code}, {platform: 'android'}); + expect(toString(ast)).toEqual(normalize(code.replace(/ReactNative.Platform\.select[^;]+/, '2'))); + }); + + it('replaces require("react-native").Platform.select in the code', () => { + const code = ` + var a = require('react-native').Platform.select({ios: 1, android: 2}); + var b = a.require('react-native').Platform.select({}); + `; + const {ast} = inline('arbitrary.js', {code}, {platform: 'android'}); + expect(toString(ast)).toEqual( + normalize(code.replace(/require\('react-native'\)\.Platform\.select[^;]+/, '2'))); + }); + + it('replaces non-existing properties with `undefined`', () => { + const code = 'var a = Platform.select({ios: 1, android: 2})'; + const {ast} = inline('arbitrary.js', {code}, {platform: 'doesnotexist'}); + expect(toString(ast)).toEqual( + normalize(code.replace(/Platform\.select[^;]+/, 'undefined'))); + }); + it('replaces process.env.NODE_ENV in the code', () => { const code = `function a() { if (process.env.NODE_ENV === 'production') { diff --git a/packager/react-packager/src/JSTransformer/worker/inline.js b/packager/react-packager/src/JSTransformer/worker/inline.js index 13e20b0e297b91..adc1f71d191de0 100644 --- a/packager/react-packager/src/JSTransformer/worker/inline.js +++ b/packager/react-packager/src/JSTransformer/worker/inline.js @@ -15,6 +15,7 @@ const React = {name: 'React'}; const ReactNative = {name: 'ReactNative'}; const platform = {name: 'Platform'}; const os = {name: 'OS'}; +const select = {name: 'select'}; const requirePattern = {name: 'require'}; const env = {name: 'env'}; @@ -63,11 +64,29 @@ const isProcessEnvNodeEnv = (node, scope) => t.isIdentifier(node.object.object, processId) && isGlobal(scope.getBinding(processId.name)); +const isPlatformSelect = (node, scope) => + t.isMemberExpression(node.callee) && + t.isIdentifier(node.callee.object, platform) && + t.isIdentifier(node.callee.property, select) && + isImportOrGlobal(node.callee.object, scope, [platform]); + +const isReactPlatformSelect = (node, scope) => + t.isMemberExpression(node.callee) && + t.isIdentifier(node.callee.property, select) && + t.isMemberExpression(node.callee.object) && + t.isIdentifier(node.callee.object.property, platform) && + isImportOrGlobal(node.callee.object.object, scope, [React, ReactNative]); + const isDev = (node, parent, scope) => t.isIdentifier(node, dev) && isGlobal(scope.getBinding(dev.name)) && !(t.isMemberExpression(parent)); +function findProperty(objectExpression, key) { + const property = objectExpression.properties.find(p => p.key.name === key); + return property ? property.value : t.identifier('undefined'); +} + const inlinePlugin = { visitor: { Identifier(path, state) { @@ -86,6 +105,19 @@ const inlinePlugin = { t.stringLiteral(state.opts.dev ? 'development' : 'production')); } }, + CallExpression(path, state) { + const node = path.node; + const scope = path.scope; + const arg = node.arguments[0]; + + if (isPlatformSelect(node, scope) || isReactPlatformSelect(node, scope)) { + const replacement = t.isObjectExpression(arg) + ? findProperty(arg, state.opts.platform) + : node; + + path.replaceWith(replacement); + } + } }, }; From a966c85fcec298c6f44e08cd653f473b70593659 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 5 Jun 2016 14:06:10 -0700 Subject: [PATCH 221/843] Added ReactCommon to package.json to allow for building locally Summary: This pull request solves an error building from source as noted in https://github.com/facebook/react-native/issues/7911 and https://github.com/facebook/react-native/pull/7118. The error says BUILD FAILED due to cxxreact module not found. Previously the ReactCommon folder was excluded when installed from npm. There may be a better way to include this folder, but I'm unfamiliar with FB's build process. **Test plan (required)** Create a new sample project by npm init, npm install --save https://github.com/opensports/react-native.git. Then run react-native run-android and the project should build successfully. Closes https://github.com/facebook/react-native/pull/7929 Differential Revision: D3391332 fbshipit-source-id: 710cb741cea2e212767b21d606e3a459347b0fb9 --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 5d2cd61f2843e9..ef34429ccd699c 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "React", "React.podspec", "ReactAndroid", + "ReactCommon", "react.gradle", "android", "Libraries", From c36430a0d6b0197f35dcab2caf4dfb4bba699e43 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Sun, 5 Jun 2016 18:46:32 -0700 Subject: [PATCH 222/843] Hook up Android intent to Linking.js #7079 Summary: Fixed #7118 by rreusser Closes https://github.com/facebook/react-native/pull/7940 Differential Revision: D3391586 fbshipit-source-id: f7e572a91347fb0629602374cb6944eabf5b0e8f --- Libraries/Linking/Linking.js | 31 +++++++++---------- .../com/facebook/react/ReactActivity.java | 9 ++++++ .../facebook/react/ReactInstanceManager.java | 5 +++ .../react/ReactInstanceManagerImpl.java | 17 ++++++++++ .../react/XReactInstanceManagerImpl.java | 21 +++++++++++-- .../core/DeviceEventManagerModule.java | 11 +++++++ 6 files changed, 74 insertions(+), 20 deletions(-) diff --git a/Libraries/Linking/Linking.js b/Libraries/Linking/Linking.js index 455de7ec75a2d4..0060e3ce26db11 100644 --- a/Libraries/Linking/Linking.js +++ b/Libraries/Linking/Linking.js @@ -44,13 +44,24 @@ const LinkingManager = Platform.OS === 'android' ? * NOTE: For instructions on how to add support for deep linking on Android, * refer to [Enabling Deep Links for App Content - Add Intent Filters for Your Deep Links](http://developer.android.com/training/app-indexing/deep-linking.html#adding-filters). * + * If you wish to receive the intent in an existing instance of MainActivity, + * you may set the `launchMode` of MainActivity to `singleTask` in + * `AndroidManifest.xml`. See [``](http://developer.android.com/guide/topics/manifest/activity-element.html) + * documentation for more information. + * + * ``` + * + * ``` + * * NOTE: On iOS you'll need to link `RCTLinking` to your project by following * the steps described [here](docs/linking-libraries-ios.html#manual-linking). * In case you also want to listen to incoming app links during your app's * execution you'll need to add the following lines to you `*AppDelegate.m`: * * ``` - *#import "RCTLinkingManager.h" + * #import "RCTLinkingManager.h" * * - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url * sourceApplication:(NSString *)sourceApplication annotation:(id)annotation @@ -84,8 +95,6 @@ const LinkingManager = Platform.OS === 'android' ? * console.log(event.url); * } * ``` - * Note that this is only supported on iOS. - * * #### Opening external links * * To start the corresponding activity for a link (web URL, email, contact etc.), call @@ -114,28 +123,16 @@ class Linking extends NativeEventEmitter { /** * Add a handler to Linking changes by listening to the `url` event type * and providing the handler - * - * @platform ios */ addEventListener(type: string, handler: Function) { - if (Platform.OS === 'android') { - console.warn('Linking.addEventListener is not supported on Android'); - } else { - this.addListener(type, handler); - } + this.addListener(type, handler); } /** * Remove a handler by passing the `url` event type and the handler - * - * @platform ios */ removeEventListener(type: string, handler: Function ) { - if (Platform.OS === 'android') { - console.warn('Linking.removeEventListener is not supported on Android'); - } else { - this.removeListener(type, handler); - } + this.removeListener(type, handler); } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java index b6bdfb0b5b5213..4befbd3df758ac 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java @@ -227,4 +227,13 @@ public void onBackPressed() { public void invokeDefaultOnBackPressed() { super.onBackPressed(); } + + @Override + public void onNewIntent(Intent intent) { + if (mReactInstanceManager != null) { + mReactInstanceManager.onNewIntent(intent); + } else { + super.onNewIntent(intent); + } + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index f6db4ae3510838..a000a7ebd83ec2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -88,6 +88,11 @@ public interface ReactInstanceEventListener { */ public abstract void onBackPressed(); + /** + * This method will give JS the opportunity to receive intents via Linking. + */ + public abstract void onNewIntent(Intent intent); + /** * Call this from {@link Activity#onPause()}. This notifies any listening modules so they can do * any necessary cleanup. diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java index b347a81bfb58ef..0f5aa1af34f868 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java @@ -26,6 +26,7 @@ import android.os.AsyncTask; import android.os.Bundle; import android.view.View; +import android.net.Uri; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; @@ -466,6 +467,22 @@ private void invokeDefaultOnBackPressed() { } } + @Override + public void onNewIntent(Intent intent) { + if (mCurrentReactContext == null) { + FLog.w(ReactConstants.TAG, "Instance detached from instance manager"); + } else { + String action = intent.getAction(); + Uri uri = intent.getData(); + + if (Intent.ACTION_VIEW.equals(action) && uri != null) { + DeviceEventManagerModule deviceEventManagerModule = + Assertions.assertNotNull(mCurrentReactContext).getNativeModule(DeviceEventManagerModule.class); + deviceEventManagerModule.emitNewIntentReceived(uri); + } + } + } + private void toggleElementInspector() { if (mCurrentReactContext != null) { mCurrentReactContext diff --git a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java index cdc96efede6c99..b0afbd5366f8b5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManagerImpl.java @@ -23,6 +23,7 @@ import android.app.Application; import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.view.View; @@ -57,6 +58,7 @@ import com.facebook.react.devsupport.DevSupportManager; import com.facebook.react.devsupport.DevSupportManagerFactory; import com.facebook.react.devsupport.ReactInstanceDevCommandsHandler; +import com.facebook.react.devsupport.RedBoxHandler; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.debug.DeveloperSettings; @@ -67,10 +69,7 @@ import com.facebook.react.uimanager.ViewManager; import com.facebook.soloader.SoLoader; import com.facebook.systrace.Systrace; -import com.facebook.react.devsupport.RedBoxHandler; -import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_JS_MODULE_CONFIG_END; -import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_JS_MODULE_CONFIG_START; import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_NATIVE_MODULE_REGISTRY_END; import static com.facebook.react.bridge.ReactMarkerConstants.BUILD_NATIVE_MODULE_REGISTRY_START; import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_CATALYST_INSTANCE_END; @@ -476,6 +475,22 @@ private void invokeDefaultOnBackPressed() { } } + @Override + public void onNewIntent(Intent intent) { + if (mCurrentReactContext == null) { + FLog.w(ReactConstants.TAG, "Instance detached from instance manager"); + } else { + String action = intent.getAction(); + Uri uri = intent.getData(); + + if (Intent.ACTION_VIEW.equals(action) && uri != null) { + DeviceEventManagerModule deviceEventManagerModule = + Assertions.assertNotNull(mCurrentReactContext).getNativeModule(DeviceEventManagerModule.class); + deviceEventManagerModule.emitNewIntentReceived(uri); + } + } + } + private void toggleElementInspector() { if (mCurrentReactContext != null) { mCurrentReactContext diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java index 37a34283c8139e..2ac4f45024ff8a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java @@ -11,6 +11,8 @@ import javax.annotation.Nullable; +import android.net.Uri; + import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; @@ -52,6 +54,15 @@ public void emitHardwareBackPressed() { .emit("hardwareBackPress", null); } + /** + * Sends an event to the JS instance that a new intent was received. + */ + public void emitNewIntentReceived(Uri uri) { + getReactApplicationContext() + .getJSModule(RCTDeviceEventEmitter.class) + .emit("url", uri.toString()); + } + /** * Invokes the default back handler for the host of this catalyst instance. This should be invoked * if JS does not want to handle the back press itself. From 4acf009284bfbcd3408b0f198358112de1bcba28 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Mon, 6 Jun 2016 04:23:50 -0700 Subject: [PATCH 223/843] Pass resource drawable id to constructor directly Summary: Instead of passing the helper to each method that uses it, just pass it to the image constructor. Reviewed By: dmmiller Differential Revision: D3364532 fbshipit-source-id: 949bdbf951875c9b8cd05d028a2c329e12d72042 --- .../react/views/image/ReactImageManager.java | 7 +++--- .../react/views/image/ReactImageView.java | 24 +++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java index eaee5053a74d07..fbd98355c4ebbb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java @@ -71,19 +71,20 @@ public ReactImageView createViewInstance(ThemedReactContext context) { return new ReactImageView( context, getDraweeControllerBuilder(), - getCallerContext()); + getCallerContext(), + mResourceDrawableIdHelper); } // In JS this is Image.props.source.uri @ReactProp(name = "src") public void setSource(ReactImageView view, @Nullable String source) { - view.setSource(source, mResourceDrawableIdHelper); + view.setSource(source); } // In JS this is Image.props.loadingIndicatorSource.uri @ReactProp(name = "loadingIndicatorSrc") public void setLoadingIndicatorSource(ReactImageView view, @Nullable String source) { - view.setLoadingIndicatorSource(source, mResourceDrawableIdHelper); + view.setLoadingIndicatorSource(source); } @ReactProp(name = "borderColor", customType = "Color") diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java index 10a474420ed4d6..d7d093868ce282 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java @@ -11,6 +11,8 @@ import javax.annotation.Nullable; +import java.util.Arrays; + import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapShader; @@ -51,8 +53,6 @@ import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.events.EventDispatcher; -import java.util.Arrays; - /** * Wrapper class around Fresco's GenericDraweeView, enabling persisting props across multiple view * update and consistent processing of both static and network images. @@ -135,6 +135,8 @@ public void process(Bitmap output, Bitmap source) { } } + private final ResourceDrawableIdHelper mResourceDrawableIdHelper; + private @Nullable Uri mUri; private @Nullable Drawable mLoadingImageDrawable; private int mBorderColor; @@ -163,12 +165,14 @@ private static GenericDraweeHierarchy buildHierarchy(Context context) { public ReactImageView( Context context, AbstractDraweeControllerBuilder draweeControllerBuilder, - @Nullable Object callerContext) { + @Nullable Object callerContext, + ResourceDrawableIdHelper resourceDrawableIdHelper) { super(context, buildHierarchy(context)); mScaleType = ImageResizeMode.defaultValue(); mDraweeControllerBuilder = draweeControllerBuilder; mRoundedCornerPostprocessor = new RoundedCornerPostprocessor(); mCallerContext = callerContext; + mResourceDrawableIdHelper = resourceDrawableIdHelper; } public void setShouldNotifyLoadEvents(boolean shouldNotify) { @@ -255,9 +259,7 @@ public void setScaleType(ScalingUtils.ScaleType scaleType) { mIsDirty = true; } - public void setSource( - @Nullable String source, - ResourceDrawableIdHelper resourceDrawableIdHelper) { + public void setSource(@Nullable String source) { mUri = null; if (source != null) { try { @@ -270,7 +272,7 @@ public void setSource( // ignore malformed uri, then attempt to extract resource ID. } if (mUri == null) { - mUri = resourceDrawableIdHelper.getResourceDrawableUri(getContext(), source); + mUri = mResourceDrawableIdHelper.getResourceDrawableUri(getContext(), source); mIsLocalImage = true; } else { mIsLocalImage = false; @@ -279,10 +281,8 @@ public void setSource( mIsDirty = true; } - public void setLoadingIndicatorSource( - @Nullable String name, - ResourceDrawableIdHelper resourceDrawableIdHelper) { - Drawable drawable = resourceDrawableIdHelper.getResourceDrawable(getContext(), name); + public void setLoadingIndicatorSource(@Nullable String name) { + Drawable drawable = mResourceDrawableIdHelper.getResourceDrawable(getContext(), name); mLoadingImageDrawable = drawable != null ? (Drawable) new AutoRotateDrawable(drawable, 1000) : null; mIsDirty = true; @@ -313,7 +313,7 @@ public void maybeUpdateView() { } boolean doResize = shouldResize(mUri); - if (doResize && (getWidth() <= 0 || getHeight() <=0)) { + if (doResize && (getWidth() <= 0 || getHeight() <= 0)) { // If need a resize and the size is not yet set, wait until the layout pass provides one return; } From 2aad61650e2e9c47de0ac692ea08148da5ad552d Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Mon, 6 Jun 2016 04:24:47 -0700 Subject: [PATCH 224/843] Open sourced more instrumentation tests #1 Reviewed By: avaly Differential Revision: D3366104 fbshipit-source-id: 1c77b29e28726a6a105317d9f6944bbf78b707d7 --- .../tests/CatalystMeasureLayoutTest.java | 131 +++++++++++ .../androidTest/js/MeasureLayoutTestModule.js | 205 ++++++++++++++++++ ReactAndroid/src/androidTest/js/TestBundle.js | 8 +- 3 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystMeasureLayoutTest.java create mode 100644 ReactAndroid/src/androidTest/js/MeasureLayoutTestModule.js diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystMeasureLayoutTest.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystMeasureLayoutTest.java new file mode 100644 index 00000000000000..ba34dda0858e42 --- /dev/null +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystMeasureLayoutTest.java @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.tests; + +import com.facebook.react.testing.ReactInstanceSpecForTest; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.testing.AssertModule; +import com.facebook.react.testing.ReactAppInstrumentationTestCase; + +/** + * Tests for {@link UIManagerModule#measure}, {@link UIManagerModule#measureLayout}, and + * {@link UIManagerModule#measureLayoutRelativeToParent}. Tests measurement for views in the + * following hierarchy: + * + * +---------------------------------------------+ + * | A | + * | | + * | +-----------+ +---------+ | + * | | B | | D | | + * | | +---+ | | | | + * | | | C | | | | | + * | | | | | +---------+ | + * | | +---+ | | + * | +-----------+ | + * | | + * | | + * | | + * +---------------------------------------------+ + * + * View locations and dimensions: + * A - (0,0) to (500, 500) (500x500) + * B - (50,80) to (250, 380) (200x300) + * C - (150,150) to (200, 300) (50x150) + * D - (400,100) to (450, 300) (50x200) + */ +public class CatalystMeasureLayoutTest extends ReactAppInstrumentationTestCase { + + private static interface MeasureLayoutTestModule extends JavaScriptModule { + public void verifyMeasureOnViewA(); + public void verifyMeasureOnViewC(); + public void verifyMeasureLayoutCRelativeToA(); + public void verifyMeasureLayoutCRelativeToB(); + public void verifyMeasureLayoutCRelativeToSelf(); + public void verifyMeasureLayoutRelativeToParentOnViewA(); + public void verifyMeasureLayoutRelativeToParentOnViewB(); + public void verifyMeasureLayoutRelativeToParentOnViewC(); + public void verifyMeasureLayoutDRelativeToB(); + public void verifyMeasureLayoutNonExistentTag(); + public void verifyMeasureLayoutNonExistentAncestor(); + public void verifyMeasureLayoutRelativeToParentNonExistentTag(); + } + + private MeasureLayoutTestModule mTestJSModule; + private AssertModule mAssertModule; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mTestJSModule = getReactContext().getJSModule(MeasureLayoutTestModule.class); + } + + @Override + protected String getReactApplicationKeyUnderTest() { + return "MeasureLayoutTestApp"; + } + + @Override + protected ReactInstanceSpecForTest createReactInstanceSpecForTest() { + mAssertModule = new AssertModule(); + return new ReactInstanceSpecForTest() + .addNativeModule(mAssertModule) + .addJSModule(MeasureLayoutTestModule.class); + } + + private void waitForBridgeIdleAndVerifyAsserts() { + waitForBridgeAndUIIdle(); + mAssertModule.verifyAssertsAndReset(); + } + + public void testMeasure() { + mTestJSModule.verifyMeasureOnViewA(); + waitForBridgeIdleAndVerifyAsserts(); + mTestJSModule.verifyMeasureOnViewC(); + waitForBridgeIdleAndVerifyAsserts(); + } + + public void testMeasureLayout() { + mTestJSModule.verifyMeasureLayoutCRelativeToA(); + waitForBridgeIdleAndVerifyAsserts(); + mTestJSModule.verifyMeasureLayoutCRelativeToB(); + waitForBridgeIdleAndVerifyAsserts(); + mTestJSModule.verifyMeasureLayoutCRelativeToSelf(); + waitForBridgeIdleAndVerifyAsserts(); + } + + public void testMeasureLayoutRelativeToParent() { + mTestJSModule.verifyMeasureLayoutRelativeToParentOnViewA(); + waitForBridgeIdleAndVerifyAsserts(); + mTestJSModule.verifyMeasureLayoutRelativeToParentOnViewB(); + waitForBridgeIdleAndVerifyAsserts(); + mTestJSModule.verifyMeasureLayoutRelativeToParentOnViewC(); + waitForBridgeIdleAndVerifyAsserts(); + } + + public void testMeasureLayoutCallsErrorCallbackWhenViewIsNotChildOfAncestor() { + mTestJSModule.verifyMeasureLayoutDRelativeToB(); + waitForBridgeIdleAndVerifyAsserts(); + } + + public void testMeasureLayoutCallsErrorCallbackWhenViewDoesNotExist() { + mTestJSModule.verifyMeasureLayoutNonExistentTag(); + waitForBridgeIdleAndVerifyAsserts(); + } + + public void testMeasureLayoutCallsErrorCallbackWhenAncestorDoesNotExist() { + mTestJSModule.verifyMeasureLayoutNonExistentAncestor(); + waitForBridgeIdleAndVerifyAsserts(); + } + + public void testMeasureLayoutRelativeToParentCallsErrorCallbackWhenViewDoesNotExist() { + mTestJSModule.verifyMeasureLayoutRelativeToParentNonExistentTag(); + waitForBridgeIdleAndVerifyAsserts(); + } +} diff --git a/ReactAndroid/src/androidTest/js/MeasureLayoutTestModule.js b/ReactAndroid/src/androidTest/js/MeasureLayoutTestModule.js new file mode 100644 index 00000000000000..2bec7735212492 --- /dev/null +++ b/ReactAndroid/src/androidTest/js/MeasureLayoutTestModule.js @@ -0,0 +1,205 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule MeasureLayoutTestModule + */ + +'use strict'; + +var BatchedBridge = require('BatchedBridge'); +var React = require('React'); +var ReactNative = require('ReactNative'); +var View = require('View'); +var StyleSheet = require('StyleSheet'); +var UIManager = require('UIManager'); + +var assertEquals = require('Asserts').assertEquals; + +var styles = StyleSheet.create({ + A: { + "width": 500, + "height": 500, + }, + B: { + backgroundColor: "rgb(255, 0, 0)", + "left": 50, + "top": 80, + "width": 200, + "height": 300, + }, + C: { + backgroundColor: "rgb(0, 255, 0)", + "left": 100, + "top": 70, + "width": 50, + "height": 150, + }, + D: { + backgroundColor: "rgb(0, 0, 255)", + "left": 400, + "top": 100, + "width": 50, + "height": 200, + }, +}); + +var A, B, C, D; + +var MeasureLayoutTestApp = React.createClass({ + componentDidMount: function() { + A = ReactNative.findNodeHandle(this.refs.A); + B = ReactNative.findNodeHandle(this.refs.B); + C = ReactNative.findNodeHandle(this.refs.C); + D = ReactNative.findNodeHandle(this.refs.D); + }, + render: function() { + return ( + + + + + + + ); + }, +}); + +function shouldNotCallThisCallback() { + assertEquals(false, true); +} + +var MeasureLayoutTestModule = { + MeasureLayoutTestApp: MeasureLayoutTestApp, + verifyMeasureOnViewA: function() { + UIManager.measure(A, function(a, b, width, height, x, y) { + assertEquals(500, width); + assertEquals(500, height); + assertEquals(0, x); + assertEquals(0, y); + }); + }, + verifyMeasureOnViewC: function() { + UIManager.measure(C, function(a, b, width, height, x, y) { + assertEquals(50, width); + assertEquals(150, height); + assertEquals(150, x); + assertEquals(150, y); + }); + }, + verifyMeasureLayoutCRelativeToA: function() { + UIManager.measureLayout( + C, + A, + shouldNotCallThisCallback, + function (x, y, width, height) { + assertEquals(50, width); + assertEquals(150, height); + assertEquals(150, x); + assertEquals(150, y); + }); + }, + verifyMeasureLayoutCRelativeToB: function() { + UIManager.measureLayout( + C, + B, + shouldNotCallThisCallback, + function (x, y, width, height) { + assertEquals(50, width); + assertEquals(150, height); + assertEquals(100, x); + assertEquals(70, y); + }); + }, + verifyMeasureLayoutCRelativeToSelf: function() { + UIManager.measureLayout( + C, + C, + shouldNotCallThisCallback, + function (x, y, width, height) { + assertEquals(50, width); + assertEquals(150, height); + assertEquals(0, x); + assertEquals(0, y); + }); + }, + verifyMeasureLayoutRelativeToParentOnViewA: function() { + UIManager.measureLayoutRelativeToParent( + A, + shouldNotCallThisCallback, + function (x, y, width, height) { + assertEquals(500, width); + assertEquals(500, height); + assertEquals(0, x); + assertEquals(0, y); + }); + }, + verifyMeasureLayoutRelativeToParentOnViewB: function() { + UIManager.measureLayoutRelativeToParent( + B, + shouldNotCallThisCallback, + function (x, y, width, height) { + assertEquals(200, width); + assertEquals(300, height); + assertEquals(50, x); + assertEquals(80, y); + }); + }, + verifyMeasureLayoutRelativeToParentOnViewC: function() { + UIManager.measureLayoutRelativeToParent( + C, + shouldNotCallThisCallback, + function (x, y, width, height) { + assertEquals(50, width); + assertEquals(150, height); + assertEquals(100, x); + assertEquals(70, y); + }); + }, + verifyMeasureLayoutDRelativeToB: function() { + UIManager.measureLayout( + D, + B, + function () { + assertEquals(true, true); + }, + shouldNotCallThisCallback); + }, + verifyMeasureLayoutNonExistentTag: function() { + UIManager.measureLayout( + 192, + A, + function () { + assertEquals(true, true); + }, + shouldNotCallThisCallback); + }, + verifyMeasureLayoutNonExistentAncestor: function() { + UIManager.measureLayout( + B, + 192, + function () { + assertEquals(true, true); + }, + shouldNotCallThisCallback); + }, + verifyMeasureLayoutRelativeToParentNonExistentTag: function() { + UIManager.measureLayoutRelativeToParent( + 192, + function () { + assertEquals(true, true); + }, + shouldNotCallThisCallback); + }, +}; + +BatchedBridge.registerCallableModule( + 'MeasureLayoutTestModule', + MeasureLayoutTestModule +); + +module.exports = MeasureLayoutTestModule; diff --git a/ReactAndroid/src/androidTest/js/TestBundle.js b/ReactAndroid/src/androidTest/js/TestBundle.js index 39eb37c7bcdc24..8dcae7d0bfb193 100644 --- a/ReactAndroid/src/androidTest/js/TestBundle.js +++ b/ReactAndroid/src/androidTest/js/TestBundle.js @@ -18,14 +18,16 @@ require('ViewRenderingTestModule'); require('TestJavaToJSArgumentsModule'); require('TestJSToJavaParametersModule'); -require('PickerAndroidTestModule'); require('CatalystRootViewTestModule'); require('DatePickerDialogTestModule'); +require('MeasureLayoutTestModule'); +require('PickerAndroidTestModule'); require('ScrollViewTestModule'); require('SwipeRefreshLayoutTestModule'); require('TextInputTestModule'); require('TimePickerDialogTestModule'); + // Define catalyst test apps used in integration tests var AppRegistry = require('AppRegistry'); @@ -42,6 +44,10 @@ var apps = [ appKey: 'HorizontalScrollViewTestApp', component: () => require('ScrollViewTestModule').HorizontalScrollViewTestApp, }, +{ + appKey: 'MeasureLayoutTestApp', + component: () => require('MeasureLayoutTestModule').MeasureLayoutTestApp +}, { appKey: 'MultitouchHandlingTestAppModule', component: () => require('MultitouchHandlingTestAppModule') From a8d4fbb5b2ed57e077f96340c926e57f0633955c Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Mon, 6 Jun 2016 04:36:57 -0700 Subject: [PATCH 225/843] Add upcoming releases to the schedule Summary: Closes https://github.com/facebook/react-native/pull/7947 Differential Revision: D3392139 fbshipit-source-id: aa7c0dd760d4922e44eb1b36e8d4f599d81ba680 --- Releases.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Releases.md b/Releases.md index 4f3de43fe04790..84413931461532 100644 --- a/Releases.md +++ b/Releases.md @@ -5,12 +5,12 @@ https://github.com/facebook/react-native/releases | Version | RC release | Stable release | | ------- | ---------------- | -------------- | -| 0.22.0 | week of Mar 7 | Mar 21 | -| 0.23.0 | week of Mar 21 | Apr 4 | -| 0.24.0 | week of Apr 4 | Apr 18 | -| 0.25.0 | week of Apr 18 | May 2 | -| 0.26.0 | week of May 2 | May 16 | -| 0.27.0 | week of May 16 | May 30 | +| 0.27.0 | week of May 16 | June 6 | +| 0.28.0 | week of June 6 | June 20 | +| 0.29.0 | week of June 20 | July 4 | +| 0.30.0 | week of July 4 | July 18 | +| 0.31.0 | week of July 18 | Aug 1 | +| 0.32.0 | week of Aug 1 | Aug 15 | | ... | ... | ... | ------------------- From 329c7168975a508de535ea7809ddd1d8c6ab5455 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 6 Jun 2016 05:20:51 -0700 Subject: [PATCH 226/843] Fixed bug where layoutSubviews was called continuously for scrollview Summary: RCTScrollView was calling `dockClosestSectionHeader` in `reactBridgeDidFinishTransaction`, which triggers a layout update. The reason for this was in case the `stickyHeaderIndexes` property was updated, which would require the headers to be adjusted. However, doing this in `reactBridgeDidFinishTransaction` had the affect of causing `layoutSubviews` to be called repeatedly every frame even if nothing had changed and the scrollview wasn't moving, which was especially expensive when combined with the `removeClippedSubviews` logic, that loops through every view to calculate if it needs to be clipped. This fix moves the `dockClosestSectionHeader` call into `didUpdateProps`, and checks that the stickyHeaderIndexes have actually changed before calling it. Reviewed By: javache Differential Revision: D3387607 fbshipit-source-id: c71e00c6fac48337a63d7fee7c7c23e016acf24e --- React/Views/RCTScrollView.m | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index a90de0d0436e6e..a2d1e5d859cf58 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -894,8 +894,13 @@ - (void)reactBridgeDidFinishTransaction lastIndex, subviewCount); } } +} - [_scrollView dockClosestSectionHeader]; +- (void)didSetProps:(NSArray *)changedProps +{ + if ([changedProps containsObject:@"stickyHeaderIndices"]) { + [_scrollView dockClosestSectionHeader]; + } } // Note: setting several properties of UIScrollView has the effect of From 873149499088d25c130742fc7b3d90e7ef280e6f Mon Sep 17 00:00:00 2001 From: dragonwong <814465104@qq.com> Date: Mon, 6 Jun 2016 07:27:01 -0700 Subject: [PATCH 227/843] change leftButton init position Summary: In navigationBar, when back to the previous view, the left button of the previous view should fade in like iOS native navBar, instead of move in. It's interesting that the right button acts correct. React Native: ![clear](https://cloud.githubusercontent.com/assets/2622602/15535443/fa5e77bc-229f-11e6-8632-0dd9f5f2fb26.gif) iOS Native: ![clear2](https://cloud.githubusercontent.com/assets/2622602/15535452/05999918-22a0-11e6-877c-63590b7bb9ef.gif) Closes https://github.com/facebook/react-native/pull/7745 Differential Revision: D3392307 Pulled By: nicklockwood fbshipit-source-id: e5384140049bf7b6f78fc093209ee48814a2ffdc --- .../Navigator/NavigatorNavigationBarStylesIOS.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/CustomComponents/Navigator/NavigatorNavigationBarStylesIOS.js b/Libraries/CustomComponents/Navigator/NavigatorNavigationBarStylesIOS.js index 1ae2a94eaa0e57..8163d49674a7f0 100644 --- a/Libraries/CustomComponents/Navigator/NavigatorNavigationBarStylesIOS.js +++ b/Libraries/CustomComponents/Navigator/NavigatorNavigationBarStylesIOS.js @@ -77,7 +77,7 @@ var BASE_STYLES = { var Stages = { Left: { Title: merge(BASE_STYLES.Title, { left: -SCREEN_WIDTH / 2, opacity: 0 }), - LeftButton: merge(BASE_STYLES.LeftButton, { left: -SCREEN_WIDTH / 3, opacity: 0 }), + LeftButton: merge(BASE_STYLES.LeftButton, { left: 0, opacity: 0 }), RightButton: merge(BASE_STYLES.RightButton, { opacity: 0 }), }, Center: { From 1048e5d3445094393298d4e818ff04c41f4e56a7 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 6 Jun 2016 07:56:32 -0700 Subject: [PATCH 228/843] Fixed removeClippedSubviews Summary: The `removeClippedSubviews` feature works by umounting views from the hierarchy if they move outside the bounds of their parent. This was previously restricted to clipping views which had `overflow: hidden`, since we cannot efficiently check whether the subviews of a view go outside its bounds, and so clipping a view that has potentially overflowing children becomes an expensive recursive operation. The problem with this is that `overflow: visible` is the default, and it's not well documented nor easy to tell that `removeClippedSubviews` has been set up correctly (i.e. with all children having `overflow: hidden`). When I checked, I found that `removeClippedSubviews` was not working on any of the examples in UIExplorer, nor in several of our internal apps, because the views inside the ListView has `overflow: visible`. This was probably caused by an infra change at some point, but I'm not sure how long it's been broken. It's vanishingly unlikely that anyone would ever deliberately want subviews to overflow their bounds in this scenario, so I've updated the logic to simply ignore the `overflow` property and assume that views should be clipped if you are using the `removeClippedSubviews` property on the parent. Cons / Breaking changes: in some rare circumstances, a view might get clipped prematurely if its parent is outside the scrollview bounds, but it itself is inside. This doesn't occur in practice in any of our products, and could be worked around with additional wrapper views if it did. Pros: removeClippedSubviews is now much easier to use, and much more likely to work as intended, so most list-based apps should see a performance improvement. Reviewed By: javache Differential Revision: D3385316 fbshipit-source-id: 1c0064a4c21340a971ba80d794062a356ae6cfb3 --- React/Views/RCTView.m | 49 +++++++++++++------------------------------ 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index c73f0167e3e9da..ad2ea1424bbdaf 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -312,43 +312,24 @@ - (void)remountSubview:(UIView *)view - (void)mountOrUnmountSubview:(UIView *)view withClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView { - if (view.clipsToBounds) { - - // View has cliping enabled, so we can easily test if it is partially - // or completely within the clipRect, and mount or unmount it accordingly - - if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) { - - // View is at least partially visible, so remount it if unmounted - if (view.superview == nil) { - [self remountSubview:view]; - } - - // Then test its subviews - if (CGRectContainsRect(clipRect, view.frame)) { - [view react_remountAllSubviews]; - } else { - [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; - } - - } else if (view.superview) { - - // View is completely outside the clipRect, so unmount it - [view removeFromSuperview]; - } - - } else { - - // View has clipping disabled, so there's no way to tell if it has - // any visible subviews without an expensive recursive test, so we'll - // just add it. + if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) { + // View is at least partially visible, so remount it if unmounted if (view.superview == nil) { [self remountSubview:view]; } - // Check if subviews need to be mounted/unmounted - [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; + // Then test its subviews + if (CGRectContainsRect(clipRect, view.frame)) { + [view react_remountAllSubviews]; + } else { + [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; + } + + } else if (view.superview) { + + // View is completely outside the clipRect, so unmount it + [view removeFromSuperview]; } } @@ -375,10 +356,8 @@ - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView: // Convert clipping rect to local coordinates clipRect = [clipView convertRect:clipRect toView:self]; + clipRect = CGRectIntersection(clipRect, self.bounds); clipView = self; - if (self.clipsToBounds) { - clipRect = CGRectIntersection(clipRect, self.bounds); - } // Mount / unmount views for (UIView *view in _reactSubviews) { From 72b363d7fc1bf0b9fe5226d5e1a31141ea144614 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 6 Jun 2016 07:57:55 -0700 Subject: [PATCH 229/843] Replaced isMainThread checks with a proper test for main queue Summary: As per https://twitter.com/olebegemann/status/738656134731599872, our use of "main thread" to mean "main queue" seems to be unsafe. This diff replaces the `NSThread.isMainQueue` checks with dispatch_get_specific(), which is the recommended approach. I've also replaced all use of "MainThread" terminology with "MainQueue", and taken the opportunity to deprecate the "sync" param of `RCTExecuteOnMainThread()`, which, while we do still use it in a few places, is incredibly unsafe and shouldn't be encouraged. Reviewed By: javache Differential Revision: D3384910 fbshipit-source-id: ea7c216013372267b82eb25a38db5eb4cd46a089 --- .../UIExplorerUnitTests/RCTModuleInitTests.m | 24 +++++++++--------- Libraries/Image/RCTImageLoader.m | 4 +-- Libraries/Image/RCTImageStoreManager.m | 4 +-- Libraries/Image/RCTImageView.m | 12 +++------ Libraries/WebSocket/RCTWebSocketExecutor.m | 2 +- React/Base/RCTAssert.h | 25 +++++++++++++------ React/Base/RCTAssert.m | 2 +- React/Base/RCTBatchedBridge.m | 20 +++++++-------- React/Base/RCTBridge.m | 12 ++++----- React/Base/RCTConvert.m | 2 +- React/Base/RCTKeyCommands.m | 12 ++++----- React/Base/RCTModuleData.h | 2 +- React/Base/RCTModuleData.m | 12 ++++----- React/Base/RCTRootView.m | 10 ++++---- React/Base/RCTUtils.h | 16 +++++++++--- React/Base/RCTUtils.m | 25 ++++++++++++++++++- React/Modules/RCTAppState.m | 2 +- React/Modules/RCTUIManager.m | 16 ++++++------ React/Views/RCTComponentData.m | 2 +- 19 files changed, 121 insertions(+), 83 deletions(-) diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitTests.m index a1056a81e35dd6..bf05e72bad620e 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitTests.m @@ -48,7 +48,7 @@ @implementation RCTTestInjectedModule @interface RCTTestCustomInitModule : NSObject -@property (nonatomic, assign) BOOL initializedOnMainThread; +@property (nonatomic, assign) BOOL initializedOnMainQueue; @end @@ -62,7 +62,7 @@ @implementation RCTTestCustomInitModule - (id)init { if ((self = [super init])) { - _initializedOnMainThread = [NSThread isMainThread]; + _initializedOnMainQueue = RCTIsMainQueue(); } return self; } @@ -72,7 +72,7 @@ - (id)init @interface RCTTestCustomSetBridgeModule : NSObject -@property (nonatomic, assign) BOOL setBridgeOnMainThread; +@property (nonatomic, assign) BOOL setBridgeOnMainQueue; @end @@ -86,7 +86,7 @@ @implementation RCTTestCustomSetBridgeModule - (void)setBridge:(RCTBridge *)bridge { _bridge = bridge; - _setBridgeOnMainThread = [NSThread isMainThread]; + _setBridgeOnMainQueue = RCTIsMainQueue(); } @end @@ -95,7 +95,7 @@ - (void)setBridge:(RCTBridge *)bridge @interface RCTTestExportConstantsModule : NSObject @property (nonatomic, assign) BOOL exportedConstants; -@property (nonatomic, assign) BOOL exportedConstantsOnMainThread; +@property (nonatomic, assign) BOOL exportedConstantsOnMainQueue; @end @@ -109,7 +109,7 @@ @implementation RCTTestExportConstantsModule - (NSDictionary *)constantsToExport { _exportedConstants = YES; - _exportedConstantsOnMainThread = [NSThread isMainThread]; + _exportedConstantsOnMainQueue = RCTIsMainQueue(); return @{ @"foo": @"bar" }; } @@ -137,7 +137,7 @@ @interface RCTModuleInitTests : XCTestCase BOOL _customSetBridgeModuleNotificationSent; BOOL _exportConstantsModuleNotificationSent; BOOL _lazyInitModuleNotificationSent; - BOOL _lazyInitModuleNotificationSentOnMainThread; + BOOL _lazyInitModuleNotificationSentOnMainQueue; BOOL _viewManagerModuleNotificationSent; RCTTestInjectedModule *_injectedModule; } @@ -196,7 +196,7 @@ - (void)moduleDidInit:(NSNotification *)note _exportConstantsModuleNotificationSent = YES; } else if ([module isKindOfClass:[RCTLazyInitModule class]]) { _lazyInitModuleNotificationSent = YES; - _lazyInitModuleNotificationSentOnMainThread = [NSThread isMainThread]; + _lazyInitModuleNotificationSentOnMainQueue = RCTIsMainQueue(); } } @@ -214,7 +214,7 @@ - (void)testCustomInitModuleInitializedAtBridgeStartup RUN_RUNLOOP_WHILE(!_customInitModuleNotificationSent); XCTAssertTrue(_customInitModuleNotificationSent); RCTTestCustomInitModule *module = [_bridge moduleForClass:[RCTTestCustomInitModule class]]; - XCTAssertTrue(module.initializedOnMainThread); + XCTAssertTrue(module.initializedOnMainQueue); XCTAssertEqual(module.bridge, _bridge.batchedBridge); XCTAssertNotNil(module.methodQueue); } @@ -230,7 +230,7 @@ - (void)testCustomSetBridgeModuleInitializedAtBridgeStartup RUN_RUNLOOP_WHILE(!module); XCTAssertTrue(_customSetBridgeModuleNotificationSent); - XCTAssertFalse(module.setBridgeOnMainThread); + XCTAssertFalse(module.setBridgeOnMainQueue); XCTAssertEqual(module.bridge, _bridge.batchedBridge); XCTAssertNotNil(module.methodQueue); } @@ -242,7 +242,7 @@ - (void)testExportConstantsModuleInitializedAtBridgeStartup RCTTestExportConstantsModule *module = [_bridge moduleForClass:[RCTTestExportConstantsModule class]]; RUN_RUNLOOP_WHILE(!module.exportedConstants); XCTAssertTrue(module.exportedConstants); - XCTAssertTrue(module.exportedConstantsOnMainThread); + XCTAssertTrue(module.exportedConstantsOnMainQueue); XCTAssertEqual(module.bridge, _bridge.batchedBridge); XCTAssertNotNil(module.methodQueue); } @@ -258,7 +258,7 @@ - (void)testLazyInitModuleNotInitializedDuringBridgeInit RUN_RUNLOOP_WHILE(!module); XCTAssertTrue(_lazyInitModuleNotificationSent); - XCTAssertFalse(_lazyInitModuleNotificationSentOnMainThread); + XCTAssertFalse(_lazyInitModuleNotificationSentOnMainQueue); XCTAssertNotNil(module); XCTAssertEqual(module.bridge, _bridge.batchedBridge); XCTAssertNotNil(module.methodQueue); diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 5059b2c1c932e7..f0ea4f04699567 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -293,7 +293,7 @@ - (RCTImageLoaderCancellationBlock)loadImageOrDataWithURLRequest:(NSURLRequest * __weak RCTImageLoader *weakSelf = self; void (^completionHandler)(NSError *error, id imageOrData) = ^(NSError *error, id imageOrData) { - if ([NSThread isMainThread]) { + if (RCTIsMainQueue()) { // Most loaders do not return on the main thread, so caller is probably not // expecting it, and may do expensive post-processing in the callback @@ -545,7 +545,7 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data __block volatile uint32_t cancelled = 0; void (^completionHandler)(NSError *, UIImage *) = ^(NSError *error, UIImage *image) { - if ([NSThread isMainThread]) { + if (RCTIsMainQueue()) { // Most loaders do not return on the main thread, so caller is probably not // expecting it, and may do expensive post-processing in the callback dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ diff --git a/Libraries/Image/RCTImageStoreManager.m b/Libraries/Image/RCTImageStoreManager.m index 4c6441a64cda2f..32afcee20f8647 100644 --- a/Libraries/Image/RCTImageStoreManager.m +++ b/Libraries/Image/RCTImageStoreManager.m @@ -191,7 +191,7 @@ @implementation RCTImageStoreManager (Deprecated) - (NSString *)storeImage:(UIImage *)image { - RCTAssertMainThread(); + RCTAssertMainQueue(); RCTLogWarn(@"RCTImageStoreManager.storeImage() is deprecated and has poor performance. Use an alternative method instead."); __block NSString *imageTag; dispatch_sync(_methodQueue, ^{ @@ -202,7 +202,7 @@ - (NSString *)storeImage:(UIImage *)image - (UIImage *)imageForTag:(NSString *)imageTag { - RCTAssertMainThread(); + RCTAssertMainQueue(); RCTLogWarn(@"RCTImageStoreManager.imageForTag() is deprecated and has poor performance. Use an alternative method instead."); __block NSData *imageData; dispatch_sync(_methodQueue, ^{ diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index a0d63d35f0622c..3a5864c793cd57 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -263,22 +263,18 @@ - (void)reloadImage // Blur on a background thread to avoid blocking interaction dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ UIImage *image = RCTBlurredImageWithRadius(loadedImage, blurRadius); - RCTExecuteOnMainThread(^{ + RCTExecuteOnMainQueue(^{ setImageBlock(image); - }, NO); + }); }); } else { // No blur, so try to set the image on the main thread synchronously to minimize image // flashing. (For instance, if this view gets attached to a window, then -didMoveToWindow // calls -reloadImage, and we want to set the image synchronously if possible so that the // image property is set in the same CATransaction that attaches this view to the window.) - if ([NSThread isMainThread]) { + RCTExecuteOnMainQueue(^{ setImageBlock(loadedImage); - } else { - RCTExecuteOnMainThread(^{ - setImageBlock(loadedImage); - }, NO); - } + }); } }]; } else { diff --git a/Libraries/WebSocket/RCTWebSocketExecutor.m b/Libraries/WebSocket/RCTWebSocketExecutor.m index 11adfd8f3da973..d11505974c0ab3 100644 --- a/Libraries/WebSocket/RCTWebSocketExecutor.m +++ b/Libraries/WebSocket/RCTWebSocketExecutor.m @@ -215,7 +215,7 @@ - (void)injectJSONText:(NSString *)script asGlobalObjectNamed:(NSString *)object - (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block { - RCTExecuteOnMainThread(block, NO); + RCTExecuteOnMainQueue(block); } - (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block diff --git a/React/Base/RCTAssert.h b/React/Base/RCTAssert.h index 345a3254164431..9777203773f7c4 100644 --- a/React/Base/RCTAssert.h +++ b/React/Base/RCTAssert.h @@ -11,6 +11,11 @@ #import "RCTDefines.h" +/* + * Defined in RCTUtils.m + */ +RCT_EXTERN BOOL RCTIsMainQueue(void); + /** * This is the main assert macro that you should use. Asserts should be compiled out * in production builds. You can customize the assert behaviour by setting a custom @@ -58,13 +63,11 @@ RCT_EXTERN NSString *const RCTFatalExceptionName; /** * A block signature to be used for custom assertion handling. */ -typedef void (^RCTAssertFunction)( - NSString *condition, +typedef void (^RCTAssertFunction)(NSString *condition, NSString *fileName, NSNumber *lineNumber, NSString *function, - NSString *message - ); + NSString *message); typedef void (^RCTFatalHandler)(NSError *error); @@ -74,17 +77,23 @@ typedef void (^RCTFatalHandler)(NSError *error); #define RCTAssertParam(name) RCTAssert(name, @"'%s' is a required parameter", #name) /** - * Convenience macro for asserting that we're running on main thread. + * Convenience macro for asserting that we're running on main queue. */ -#define RCTAssertMainThread() RCTAssert([NSThread isMainThread], \ +#define RCTAssertMainQueue() RCTAssert(RCTIsMainQueue(), \ @"This function must be called on the main thread") /** - * Convenience macro for asserting that we're running off the main thread. + * Convenience macro for asserting that we're running off the main queue. */ -#define RCTAssertNotMainThread() RCTAssert(![NSThread isMainThread], \ +#define RCTAssertNotMainQueue() RCTAssert(!RCTIsMainQueue(), \ @"This function must not be called on the main thread") +/** + * Deprecated, do not use + */ +#define RCTAssertMainThread() RCTAssertMainQueue() +#define RCTAssertNotMainThread() RCTAssertNotMainQueue() + /** * These methods get and set the current assert function called by the RCTAssert * macros. You can use these to replace the standard behavior with custom assert diff --git a/React/Base/RCTAssert.m b/React/Base/RCTAssert.m index e08e34cc7462a6..d31f85b3fe1ad2 100644 --- a/React/Base/RCTAssert.m +++ b/React/Base/RCTAssert.m @@ -87,7 +87,7 @@ void RCTPerformBlockWithAssertFunction(void (^block)(void), RCTAssertFunction as NSString *RCTCurrentThreadName(void) { NSThread *thread = [NSThread currentThread]; - NSString *threadName = thread.isMainThread ? @"main" : thread.name; + NSString *threadName = RCTIsMainQueue() || thread.isMainThread ? @"main" : thread.name; if (threadName.length == 0) { const char *label = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL); if (label && strlen(label) > 0) { diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index ac3cd7e80c8315..3314a2d071e902 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -366,7 +366,7 @@ - (void)initModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup // Synchronously set up the pre-initialized modules for (RCTModuleData *moduleData in _moduleDataByID) { if (moduleData.hasInstance && - (!moduleData.requiresMainThreadSetup || [NSThread isMainThread])) { + (!moduleData.requiresMainQueueSetup || RCTIsMainQueue())) { // Modules that were pre-initialized should ideally be set up before // bridge init has finished, otherwise the caller may try to access the // module directly rather than via `[bridge moduleForClass:]`, which won't @@ -383,10 +383,10 @@ - (void)initModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup // Set up modules that require main thread init or constants export RCTPerformanceLoggerSet(RCTPLNativeModuleMainThread, 0); - NSUInteger modulesOnMainThreadCount = 0; + NSUInteger modulesOnMainQueueCount = 0; for (RCTModuleData *moduleData in _moduleDataByID) { __weak RCTBatchedBridge *weakSelf = self; - if (moduleData.requiresMainThreadSetup || moduleData.hasConstantsToExport) { + if (moduleData.requiresMainQueueSetup || moduleData.hasConstantsToExport) { // Modules that need to be set up on the main thread cannot be initialized // lazily when required without doing a dispatch_sync to the main thread, // which can result in deadlock. To avoid this, we initialize all of these @@ -400,12 +400,12 @@ - (void)initModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup RCTPerformanceLoggerAppendEnd(RCTPLNativeModuleMainThread); } }); - modulesOnMainThreadCount++; + modulesOnMainQueueCount++; } } RCTPerformanceLoggerEnd(RCTPLNativeModuleInit); - RCTPerformanceLoggerSet(RCTPLNativeModuleMainThreadUsesCount, modulesOnMainThreadCount); + RCTPerformanceLoggerSet(RCTPLNativeModuleMainThreadUsesCount, modulesOnMainQueueCount); } - (void)setUpExecutor @@ -509,7 +509,7 @@ - (void)didFinishLoading - (void)stopLoadingWithError:(NSError *)error { - RCTAssertMainThread(); + RCTAssertMainQueue(); if (!_valid || !_loading) { return; @@ -551,7 +551,7 @@ - (Class)executorClass - (void)setExecutorClass:(Class)executorClass { - RCTAssertMainThread(); + RCTAssertMainQueue(); _parentBridge.executorClass = executorClass; } @@ -599,7 +599,7 @@ - (void)invalidate return; } - RCTAssertMainThread(); + RCTAssertMainQueue(); RCTAssert(_javaScriptExecutor != nil, @"Can't complete invalidation without a JS executor"); _loading = NO; @@ -1004,7 +1004,7 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i - (void)startProfiling { - RCTAssertMainThread(); + RCTAssertMainQueue(); [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ RCTProfileInit(self); @@ -1013,7 +1013,7 @@ - (void)startProfiling - (void)stopProfiling:(void (^)(NSData *))callback { - RCTAssertMainThread(); + RCTAssertMainQueue(); [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ RCTProfileEnd(self, ^(NSString *log) { diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 2ca2d7c06619d4..6f3d05b6ff1cbc 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -117,7 +117,7 @@ - (instancetype)initWithDelegate:(id)delegate _delegate = delegate; _launchOptions = [launchOptions copy]; [self setUp]; - RCTExecuteOnMainThread(^{ [self bindKeys]; }, NO); + RCTExecuteOnMainQueue(^{ [self bindKeys]; }); } return self; } @@ -134,7 +134,7 @@ - (instancetype)initWithBundleURL:(NSURL *)bundleURL _moduleProvider = block; _launchOptions = [launchOptions copy]; [self setUp]; - RCTExecuteOnMainThread(^{ [self bindKeys]; }, NO); + RCTExecuteOnMainQueue(^{ [self bindKeys]; }); } return self; } @@ -145,7 +145,7 @@ - (void)dealloc { /** * This runs only on the main thread, but crashes the subclass - * RCTAssertMainThread(); + * RCTAssertMainQueue(); */ [[NSNotificationCenter defaultCenter] removeObserver:self]; [self invalidate]; @@ -153,7 +153,7 @@ - (void)dealloc - (void)bindKeys { - RCTAssertMainThread(); + RCTAssertMainQueue(); [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reload) @@ -270,9 +270,9 @@ - (void)invalidate self.batchedBridge = nil; if (batchedBridge) { - RCTExecuteOnMainThread(^{ + RCTExecuteOnMainQueue(^{ [batchedBridge invalidate]; - }, NO); + }); } } diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 8c6816e9bd6bc2..73f41000f08314 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -884,7 +884,7 @@ + (UIImage *)UIImage:(id)json } __block UIImage *image; - if (![NSThread isMainThread]) { + if (!RCTIsMainQueue()) { // It seems that none of the UIImage loading methods can be guaranteed // thread safe, so we'll pick the lesser of two evils here and block rather // than run the risk of crashing diff --git a/React/Base/RCTKeyCommands.m b/React/Base/RCTKeyCommands.m index be59c984b54ef7..ddfc5d8665fff5 100644 --- a/React/Base/RCTKeyCommands.m +++ b/React/Base/RCTKeyCommands.m @@ -271,7 +271,7 @@ - (void)registerKeyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)flags action:(void (^)(UIKeyCommand *))block { - RCTAssertMainThread(); + RCTAssertMainQueue(); if (input.length && flags && RCTIsIOS8OrEarlier()) { @@ -296,7 +296,7 @@ - (void)registerKeyCommandWithInput:(NSString *)input - (void)unregisterKeyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)flags { - RCTAssertMainThread(); + RCTAssertMainQueue(); for (RCTKeyCommand *command in _commands.allObjects) { if ([command matchesInput:input flags:flags]) { @@ -309,7 +309,7 @@ - (void)unregisterKeyCommandWithInput:(NSString *)input - (BOOL)isKeyCommandRegisteredForInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)flags { - RCTAssertMainThread(); + RCTAssertMainQueue(); for (RCTKeyCommand *command in _commands) { if ([command matchesInput:input flags:flags]) { @@ -323,7 +323,7 @@ - (void)registerDoublePressKeyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)flags action:(void (^)(UIKeyCommand *))block { - RCTAssertMainThread(); + RCTAssertMainQueue(); if (input.length && flags && RCTIsIOS8OrEarlier()) { @@ -348,7 +348,7 @@ - (void)registerDoublePressKeyCommandWithInput:(NSString *)input - (void)unregisterDoublePressKeyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)flags { - RCTAssertMainThread(); + RCTAssertMainQueue(); for (RCTKeyCommand *command in _commands.allObjects) { if ([command matchesInput:input flags:flags]) { @@ -361,7 +361,7 @@ - (void)unregisterDoublePressKeyCommandWithInput:(NSString *)input - (BOOL)isDoublePressKeyCommandRegisteredForInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)flags { - RCTAssertMainThread(); + RCTAssertMainQueue(); for (RCTKeyCommand *command in _commands) { if ([command matchesInput:input flags:flags]) { diff --git a/React/Base/RCTModuleData.h b/React/Base/RCTModuleData.h index 7bd297739f6886..c04f7164498840 100644 --- a/React/Base/RCTModuleData.h +++ b/React/Base/RCTModuleData.h @@ -48,7 +48,7 @@ /** * Returns YES if module instance must be created on the main thread. */ -@property (nonatomic, assign, readonly) BOOL requiresMainThreadSetup; +@property (nonatomic, assign, readonly) BOOL requiresMainQueueSetup; /** * Returns YES if module has constants to export. diff --git a/React/Base/RCTModuleData.m b/React/Base/RCTModuleData.m index 09d70b4472cc42..c1509bbe6eb129 100644 --- a/React/Base/RCTModuleData.m +++ b/React/Base/RCTModuleData.m @@ -44,7 +44,7 @@ - (void)setUp // If a module overrides `init` then we must assume that it expects to be // initialized on the main thread, because it may need to access UIKit. - _requiresMainThreadSetup = !_instance && + _requiresMainQueueSetup = !_instance && [_moduleClass instanceMethodForSelector:@selector(init)] != objectInitMethod; // If a module overrides `constantsToExport` then we must assume that it @@ -85,8 +85,8 @@ - (void)setUpInstanceAndBridge [_instanceLock lock]; if (!_setupComplete && _bridge.valid) { if (!_instance) { - if (RCT_DEBUG && _requiresMainThreadSetup) { - RCTAssertMainThread(); + if (RCT_DEBUG && _requiresMainQueueSetup) { + RCTAssertMainQueue(); } RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"[RCTModuleData setUpInstanceAndBridge] [_moduleClass new]", nil); _instance = [_moduleClass new]; @@ -128,13 +128,13 @@ - (void)setUpInstanceAndBridge // If we're here, then the module is completely initialized, // except for what finishSetupForInstance does. When the instance // method is called after moduleSetupComplete, - // finishSetupForInstance will run. If _requiresMainThreadSetup + // finishSetupForInstance will run. If _requiresMainQueueSetup // is true, getting the instance will block waiting for the main // thread, which could take a while if the main thread is busy // (I've seen 50ms in testing). So we clear that flag, since // nothing in finishSetupForInstance needs to be run on the main // thread. - _requiresMainThreadSetup = NO; + _requiresMainQueueSetup = NO; } } @@ -208,7 +208,7 @@ - (BOOL)hasInstance { if (!_setupComplete) { RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, [NSString stringWithFormat:@"[RCTModuleData instanceForClass:%@]", _moduleClass], nil); - if (_requiresMainThreadSetup) { + if (_requiresMainQueueSetup) { // The chances of deadlock here are low, because module init very rarely // calls out to other threads, however we can't control when a module might // get accessed by client code during bridge setup, and a very low risk of diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index 30084e4eeda0ed..cff7861a1b9833 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -58,7 +58,7 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName initialProperties:(NSDictionary *)initialProperties { - RCTAssertMainThread(); + RCTAssertMainQueue(); RCTAssert(bridge, @"A bridge instance is required to create an RCTRootView"); RCTAssert(moduleName, @"A moduleName is required to create an RCTRootView"); @@ -175,7 +175,7 @@ - (void)hideLoadingView - (NSNumber *)reactTag { - RCTAssertMainThread(); + RCTAssertMainQueue(); if (!super.reactTag) { /** * Every root view that is created must have a unique react tag. @@ -191,14 +191,14 @@ - (NSNumber *)reactTag - (void)bridgeDidReload { - RCTAssertMainThread(); + RCTAssertMainQueue(); // Clear the reactTag so it can be re-assigned self.reactTag = nil; } - (void)javaScriptDidLoad:(NSNotification *)notification { - RCTAssertMainThread(); + RCTAssertMainQueue(); RCTBridge *bridge = notification.userInfo[@"bridge"]; [self bundleFinishedLoading:bridge]; } @@ -250,7 +250,7 @@ - (void)layoutSubviews - (void)setAppProperties:(NSDictionary *)appProperties { - RCTAssertMainThread(); + RCTAssertMainQueue(); if ([_appProperties isEqualToDictionary:appProperties]) { return; diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index d004648297172b..04705d383287b8 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -29,9 +29,19 @@ RCT_EXTERN id RCTJSONClean(id object); // Get MD5 hash of a string RCT_EXTERN NSString *RCTMD5Hash(NSString *string); -// Execute the specified block on the main thread. Unlike dispatch_sync/async -// this will not context-switch if we're already running on the main thread. -RCT_EXTERN void RCTExecuteOnMainThread(dispatch_block_t block, BOOL sync); +// Check is we are currently on the main queue (not to be confused with +// the main thread, which is not neccesarily the same thing) +// https://twitter.com/olebegemann/status/738656134731599872 +RCT_EXTERN BOOL RCTIsMainQueue(void); + +// Execute the specified block on the main queue. Unlike dispatch_async() +// this will execute immediately if we're already on the main queue. +RCT_EXTERN void RCTExecuteOnMainQueue(dispatch_block_t block); + +// Deprecated - do not use. +RCT_EXTERN void RCTExecuteOnMainThread(dispatch_block_t block, BOOL sync) +__deprecated_msg("Use RCTExecuteOnMainQueue instead. RCTExecuteOnMainQueue is " + "async. If you need to use the `sync` option... please don't."); // Get screen metrics in a thread-safe way RCT_EXTERN CGFloat RCTScreenScale(void); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index a477c394c24ffe..398f137b0b09af 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -19,6 +19,7 @@ #import #import +#import "RCTAssert.h" #import "RCTLog.h" NSString *const RCTErrorUnspecified = @"EUNSPECIFIED"; @@ -228,9 +229,31 @@ id RCTJSONClean(id object) ]; } +BOOL RCTIsMainQueue() +{ + static void *mainQueueKey = &mainQueueKey; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dispatch_queue_set_specific(dispatch_get_main_queue(), + mainQueueKey, mainQueueKey, NULL); + }); + return dispatch_get_specific(mainQueueKey) == mainQueueKey; +} + +void RCTExecuteOnMainQueue(dispatch_block_t block) +{ + if (RCTIsMainQueue()) { + block(); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + block(); + }); + } +} + void RCTExecuteOnMainThread(dispatch_block_t block, BOOL sync) { - if ([NSThread isMainThread]) { + if (RCTIsMainQueue()) { block(); } else if (sync) { dispatch_sync(dispatch_get_main_queue(), ^{ diff --git a/React/Modules/RCTAppState.m b/React/Modules/RCTAppState.m index 3f4c178b35901b..db9716a7eff2b5 100644 --- a/React/Modules/RCTAppState.m +++ b/React/Modules/RCTAppState.m @@ -16,7 +16,7 @@ static NSString *RCTCurrentAppBackgroundState() { - RCTAssertMainThread(); + RCTAssertMainQueue(); static NSDictionary *states; static dispatch_once_t onceToken; diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 10ee2152d6c256..168e115dc82f3d 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -366,7 +366,7 @@ - (dispatch_queue_t)methodQueue - (void)registerRootView:(UIView *)rootView withSizeFlexibility:(RCTRootViewSizeFlexibility)sizeFlexibility { - RCTAssertMainThread(); + RCTAssertMainQueue(); NSNumber *reactTag = rootView.reactTag; RCTAssert(RCTIsReactRootView(reactTag), @@ -404,13 +404,13 @@ - (void)registerRootView:(UIView *)rootView withSizeFlexibility:(RCTRootViewSize - (UIView *)viewForReactTag:(NSNumber *)reactTag { - RCTAssertMainThread(); + RCTAssertMainQueue(); return _viewRegistry[reactTag]; } - (void)setFrame:(CGRect)frame forView:(UIView *)view { - RCTAssertMainThread(); + RCTAssertMainQueue(); // The following variable has no meaning if the view is not a react root view RCTRootViewSizeFlexibility sizeFlexibility = RCTRootViewSizeFlexibilityNone; @@ -453,7 +453,7 @@ - (void)setFrame:(CGRect)frame forView:(UIView *)view - (void)setIntrinsicContentSize:(CGSize)size forView:(UIView *)view { - RCTAssertMainThread(); + RCTAssertMainQueue(); NSNumber *reactTag = view.reactTag; dispatch_async(RCTGetUIManagerQueue(), ^{ @@ -468,7 +468,7 @@ - (void)setIntrinsicContentSize:(CGSize)size forView:(UIView *)view - (void)setBackgroundColor:(UIColor *)color forView:(UIView *)view { - RCTAssertMainThread(); + RCTAssertMainQueue(); NSNumber *reactTag = view.reactTag; @@ -534,7 +534,7 @@ - (void)addUIBlock:(RCTViewManagerUIBlock)block - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView *)rootShadowView { - RCTAssert(![NSThread isMainThread], @"Should be called on shadow thread"); + RCTAssert(!RCTIsMainQueue(), @"Should be called on shadow queue"); // This is nuanced. In the JS thread, we create a new update buffer // `frameTags`/`frames` that is created/mutated in the JS thread. We access @@ -835,7 +835,7 @@ - (void)_removeChildren:(NSArray> *)children [_rootViewTags removeObject:rootReactTag]; [self addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry){ - RCTAssertMainThread(); + RCTAssertMainQueue(); UIView *rootView = viewRegistry[rootReactTag]; [uiManager _purgeChildren:(NSArray> *)rootView.reactSubviews fromRegistry:(NSMutableDictionary> *)viewRegistry]; @@ -1488,7 +1488,7 @@ static void RCTMeasureLayout(RCTShadowView *view, static NSDictionary *RCTExportedDimensions(BOOL rotateBounds) { - RCTAssertMainThread(); + RCTAssertMainQueue(); // Don't use RCTScreenSize since it the interface orientation doesn't apply to it CGRect screenSize = [[UIScreen mainScreen] bounds]; diff --git a/React/Views/RCTComponentData.m b/React/Views/RCTComponentData.m index 9259540c19801e..d22b41e33c68e5 100644 --- a/React/Views/RCTComponentData.m +++ b/React/Views/RCTComponentData.m @@ -87,7 +87,7 @@ - (RCTViewManager *)manager - (UIView *)createViewWithTag:(NSNumber *)tag { - RCTAssertMainThread(); + RCTAssertMainQueue(); UIView *view = [self.manager view]; view.reactTag = tag; From 4de616b4c1a9d3556632a93504828f0539fa4fa5 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Mon, 6 Jun 2016 08:58:35 -0700 Subject: [PATCH 230/843] Remove deprecated calls from StatusBarIOS Reviewed By: nicklockwood Differential Revision: D3346701 fbshipit-source-id: 17809a4cd686f3b431257e85d85770aee640bbc9 --- Examples/UIExplorer/StatusBarIOSExample.js | 118 ------------------ Examples/UIExplorer/UIExplorerList.ios.js | 4 - .../Components/StatusBar/StatusBarIOS.ios.js | 28 +---- 3 files changed, 3 insertions(+), 147 deletions(-) delete mode 100644 Examples/UIExplorer/StatusBarIOSExample.js diff --git a/Examples/UIExplorer/StatusBarIOSExample.js b/Examples/UIExplorer/StatusBarIOSExample.js deleted file mode 100644 index 27a00de4993084..00000000000000 --- a/Examples/UIExplorer/StatusBarIOSExample.js +++ /dev/null @@ -1,118 +0,0 @@ -/** - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - * @flow - */ -'use strict'; - -var React = require('react'); -var ReactNative = require('react-native'); -var { - StyleSheet, - View, - Text, - TouchableHighlight, - StatusBarIOS, -} = ReactNative; - -exports.framework = 'React'; -exports.title = 'StatusBarIOS'; -exports.description = 'Module for controlling iOS status bar'; -exports.examples = [{ - title: 'Status Bar Style', - render() { - return ( - - {['default', 'light-content'].map((style) => - StatusBarIOS.setStyle(style)}> - - setStyle('{style}') - - - )} - - ); - }, -}, { - title: 'Status Bar Style Animated', - render() { - return ( - - {['default', 'light-content'].map((style) => - StatusBarIOS.setStyle(style, true)}> - - setStyle('{style}', true) - - - )} - - ); - }, -}, { - title: 'Status Bar Hidden', - render() { - return ( - - {['none', 'fade', 'slide'].map((animation) => - - StatusBarIOS.setHidden(true, animation)}> - - setHidden(true, '{animation}') - - - StatusBarIOS.setHidden(false, animation)}> - - setHidden(false, '{animation}') - - - - )} - - ); - }, -}, { - title: 'Status Bar Network Activity Indicator', - render() { - return ( - - StatusBarIOS.setNetworkActivityIndicatorVisible(true)}> - - setNetworkActivityIndicatorVisible(true) - - - StatusBarIOS.setNetworkActivityIndicatorVisible(false)}> - - setNetworkActivityIndicatorVisible(false) - - - - ); - }, -}]; - -var styles = StyleSheet.create({ - wrapper: { - borderRadius: 5, - marginBottom: 5, - }, - button: { - backgroundColor: '#eeeeee', - padding: 10, - }, -}); diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index b93518faa1d636..5aefa3288d2b1e 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -243,10 +243,6 @@ const APIExamples: Array = [ key: 'SnapshotExample', module: require('./SnapshotExample'), }, - { - key: 'StatusBarIOSExample', - module: require('./StatusBarIOSExample'), - }, { key: 'TimerExample', module: require('./TimerExample'), diff --git a/Libraries/Components/StatusBar/StatusBarIOS.ios.js b/Libraries/Components/StatusBar/StatusBarIOS.ios.js index 92e75b1d1762c8..a3aceaac7e0988 100644 --- a/Libraries/Components/StatusBar/StatusBarIOS.ios.js +++ b/Libraries/Components/StatusBar/StatusBarIOS.ios.js @@ -12,33 +12,11 @@ 'use strict'; const NativeEventEmitter = require('NativeEventEmitter'); -const StatusBar = require('StatusBar'); -const StatusBarManager = require('NativeModules').StatusBarManager; - -import type {StatusBarStyle, StatusBarAnimation} from 'StatusBar'; +const { StatusBarManager } = require('NativeModules'); /** - * Deprecated. Use `StatusBar` instead. + * Use `StatusBar` for mutating the status bar. */ -class StatusBarIOS extends NativeEventEmitter { - - setStyle(style: StatusBarStyle, animated?: boolean) { - console.warn('`StatusBarIOS.setStyle` is deprecated. Use `StatusBar.setBarStyle` instead.'); - StatusBar.setBarStyle(style, animated); - } - - setHidden(hidden: boolean, animation?: StatusBarAnimation) { - console.warn('`StatusBarIOS.setHidden` is deprecated. Use `StatusBar.setHidden` instead.'); - StatusBar.setHidden(hidden, animation); - } - - setNetworkActivityIndicatorVisible(visible: boolean) { - console.warn( - '`StatusBarIOS.setNetworkActivityIndicatorVisible` is deprecated. ' + - 'Use `StatusBar.setNetworkActivityIndicatorVisible` instead.' - ); - StatusBar.setNetworkActivityIndicatorVisible(visible); - } -} +class StatusBarIOS extends NativeEventEmitter {} module.exports = new StatusBarIOS(StatusBarManager); From 5b4b02747792c77b51c64449bc18b9a0ba81f867 Mon Sep 17 00:00:00 2001 From: Joshua Pinter Date: Mon, 6 Jun 2016 09:18:59 -0700 Subject: [PATCH 231/843] Add get scheduled local notifications Summary: _(This is a remake of #6907, which I botched pretty good with a rebase.)_ This returns an `Array` of Local Notifications that have been scheduled to be delivered. Available attributes on return Notification object (if set in the local notification itself): `alertAction` `alertBody` `applicationIconBadgeNumber` `category` `fireDate` `soundName` `userInfo` More could be added to this but I just matched what was available in the `Object` passed to `presentLocalNotification` and `scheduleLocalNotification`. **Motivation** I needed to determine the number and other details about local notifications that were already scheduled. There is an API for this in iOS but one hadn't been built yet for React Native. I created the Obj-C method and updated the documentation in `PushNotificationIOS.js` as well. **Usage:** ```js PushNotificationIOS.getScheduledLocalNotifications( (notifications) => { console.log(notifications); }); ``` **Sample Output:** ```js [ Object { aler Closes https://github.com/facebook/react-native/pull/7937 Differential Revision: D3392476 Pulled By: javache fbshipit-source-id: d83f419bfcfbdaf9b955724dcf5cfe26834895fb --- .../PushNotificationIOS.js | 7 +++++ .../RCTPushNotificationManager.m | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/Libraries/PushNotificationIOS/PushNotificationIOS.js b/Libraries/PushNotificationIOS/PushNotificationIOS.js index 0fd8260625412d..4c89295ed7b266 100644 --- a/Libraries/PushNotificationIOS/PushNotificationIOS.js +++ b/Libraries/PushNotificationIOS/PushNotificationIOS.js @@ -142,6 +142,13 @@ class PushNotificationIOS { RCTPushNotificationManager.cancelLocalNotifications(userInfo); } + /** + * Gets the local notifications that are currently scheduled. + */ + static getScheduledLocalNotifications(callback: Function) { + RCTPushNotificationManager.getScheduledLocalNotifications(callback); + } + /** * Attaches a listener to remote or local notification events while the app is running * in the foreground or the background. diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m index 329f7644d3f91a..98f991825abf3b 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -57,6 +57,24 @@ + (UILocalNotification *)UILocalNotification:(id)json @implementation RCTPushNotificationManager +static NSDictionary *formatLocalNotification(UILocalNotification *notification) +{ + NSMutableDictionary *formattedLocalNotification = [NSMutableDictionary dictionary]; + if (notification.fireDate) { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"]; + NSString *fireDateString = [formatter stringFromDate:notification.fireDate]; + formattedLocalNotification[@"fireDate"] = fireDateString; + } + formattedLocalNotification[@"alertAction"] = RCTNullIfNil(notification.alertAction); + formattedLocalNotification[@"alertBody"] = RCTNullIfNil(notification.alertBody); + formattedLocalNotification[@"applicationIconBadgeNumber"] = @(notification.applicationIconBadgeNumber); + formattedLocalNotification[@"category"] = RCTNullIfNil(notification.category); + formattedLocalNotification[@"soundName"] = RCTNullIfNil(notification.soundName); + formattedLocalNotification[@"userInfo"] = RCTNullIfNil(RCTJSONClean(notification.userInfo)); + return formattedLocalNotification; +} + RCT_EXPORT_MODULE() - (void)startObserving @@ -305,4 +323,14 @@ - (void)handleRegisterUserNotificationSettings:(NSNotification *)notification resolve(RCTNullIfNil(initialNotification)); } +RCT_EXPORT_METHOD(getScheduledLocalNotifications:(RCTResponseSenderBlock)callback) +{ + NSArray *scheduledLocalNotifications = [UIApplication sharedApplication].scheduledLocalNotifications; + NSMutableArray *formattedScheduledLocalNotifications = [[NSMutableArray alloc] init]; + for (UILocalNotification *notification in scheduledLocalNotifications) { + [formattedScheduledLocalNotifications addObject:formatLocalNotification(notification)]; + } + callback(@[formattedScheduledLocalNotifications]); +} + @end From 61046c3195da29296f540255bd602bdb2742181f Mon Sep 17 00:00:00 2001 From: Siqi Liu Date: Mon, 6 Jun 2016 09:30:56 -0700 Subject: [PATCH 232/843] Fix the bug where key shortcuts are invoked in TextInputs in iOS Summary: This is a followup for "Add Shortcut "Double R" to Reload JS in iOS". Please see the previous two revisions:[[ D3371536 | D3371536 ]], [[ D3343907 | D3343907 ]] In previous revisions, we only tested with the iOS UIExplorer app, without testing in the iOS Catalyst app, where the key shortcuts we added are always invoked in TextInput components. It's due to a bug with the `UIApplicationDelegate`. Just fix this bug in this revision and successfully tested in the Catalyst app. Reviewed By: mkonicek Differential Revision: D3391045 fbshipit-source-id: 8b76fbfe7592218b02dd22502d25eebbc59f3cbc --- React/Base/RCTKeyCommands.m | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/React/Base/RCTKeyCommands.m b/React/Base/RCTKeyCommands.m index ddfc5d8665fff5..d97b8d4f0bc413 100644 --- a/React/Base/RCTKeyCommands.m +++ b/React/Base/RCTKeyCommands.m @@ -113,15 +113,11 @@ + (UIResponder *)RCT_getFirstResponder:(UIResponder *)view * For example, double-pressing a key should type two characters into the text view, * not invoke a predefined keyboard shortcut. */ - NSString *name = NSStringFromClass(self.class); - if ([name isEqualToString : @"AppDelegate"]) { - return nil; - } - UIResponder *firstResponder = [UIResponder RCT_getFirstResponder: self]; if ([firstResponder isKindOfClass:[UITextView class]] || [firstResponder isKindOfClass:[UITextField class]] || - [firstResponder conformsToProtocol:@protocol(UITextViewDelegate)]) { + [firstResponder conformsToProtocol:@protocol(UITextViewDelegate)] || + [self conformsToProtocol:@protocol(UIApplicationDelegate)]) { return nil; } From f3507f99f18f8a26d67289fbad4d52e1f15a294c Mon Sep 17 00:00:00 2001 From: Dave Miller Date: Mon, 6 Jun 2016 10:07:28 -0700 Subject: [PATCH 233/843] Remove support for password property and only use secureTextEntry Summary: Reduce the public surface area of TextInput. It only exposes a secureTextEntry property, but on Android was also accepting password as a prop. This removes that. Reviewed By: javache Differential Revision: D3392223 fbshipit-source-id: 67c36fbe16fe493e2841d5d9deb78e3be2209ebd --- Libraries/Components/TextInput/TextInput.js | 2 +- ReactAndroid/src/androidTest/js/TextInputTestModule.js | 4 ++-- .../react/views/textinput/ReactTextInputManager.java | 4 ++-- .../react/views/textinput/ReactTextInputPropertyTest.java | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 00e928bb9d02b2..7845ddd8e80466 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -557,9 +557,9 @@ var TextInput = React.createClass({ onSubmitEditing={this.props.onSubmitEditing} blurOnSubmit={this.props.blurOnSubmit} onLayout={this.props.onLayout} - password={this.props.password || this.props.secureTextEntry} placeholder={this.props.placeholder} placeholderTextColor={this.props.placeholderTextColor} + secureTextEntry={this.props.secureTextEntry} selectionColor={this.props.selectionColor} text={this._getText()} underlineColorAndroid={this.props.underlineColorAndroid} diff --git a/ReactAndroid/src/androidTest/js/TextInputTestModule.js b/ReactAndroid/src/androidTest/js/TextInputTestModule.js index d4e7cc0da36c46..9ce28448674f45 100644 --- a/ReactAndroid/src/androidTest/js/TextInputTestModule.js +++ b/ReactAndroid/src/androidTest/js/TextInputTestModule.js @@ -90,7 +90,7 @@ var TextInputTestApp = React.createClass({ autoFocus={true} keyboardType='numeric' multiline={true} - password={true} + secureTextEntry={true} defaultValue="This is text" testID="textInput1" /> @@ -101,7 +101,7 @@ var TextInputTestApp = React.createClass({ autoFocus={false} keyboardType='default' multiline={false} - password={false} + secureTextEntry={false} placeholder='1234' testID="textInput2" /> diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index 5564580edce9a6..d8d835dab33b81 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -394,8 +394,8 @@ public void setMultiline(ReactEditText view, boolean multiline) { multiline ? InputType.TYPE_TEXT_FLAG_MULTI_LINE : 0); } - @ReactProp(name = "password", defaultBoolean = false) - public void setPassword(ReactEditText view, boolean password) { + @ReactProp(name = "secureTextEntry", defaultBoolean = false) + public void setSecureTextEntry(ReactEditText view, boolean password) { updateStagedInputTypeFlag( view, password ? 0 : diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.java b/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.java index ca6d9a30429ab1..3e7c53d10525a7 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.java @@ -242,13 +242,13 @@ public void testPasswordInput() { mManager.updateProperties(view, buildStyles()); assertThat(view.getInputType() & InputType.TYPE_TEXT_VARIATION_PASSWORD).isZero(); - mManager.updateProperties(view, buildStyles("password", false)); + mManager.updateProperties(view, buildStyles("secureTextEntry", false)); assertThat(view.getInputType() & InputType.TYPE_TEXT_VARIATION_PASSWORD).isZero(); - mManager.updateProperties(view, buildStyles("password", true)); + mManager.updateProperties(view, buildStyles("secureTextEntry", true)); assertThat(view.getInputType() & InputType.TYPE_TEXT_VARIATION_PASSWORD).isNotZero(); - mManager.updateProperties(view, buildStyles("password", null)); + mManager.updateProperties(view, buildStyles("secureTextEntry", null)); assertThat(view.getInputType() & InputType.TYPE_TEXT_VARIATION_PASSWORD).isZero(); } From 4e3a8343b353b51b716dcebbbdb14bc2f92a89c8 Mon Sep 17 00:00:00 2001 From: Tom Walters Date: Mon, 6 Jun 2016 10:20:35 -0700 Subject: [PATCH 234/843] Convert warning about keystore into warning block in SignedAPKAndroid.md Summary: The current docs page [Generating a Signed APK](https://facebook.github.io/react-native/docs/signed-apk-android.html) contains a note about keeping your generated keystore safe, but this isn't well highlighted. This commit highlights the notice in a warning blockquote to ensure people following the guide don't miss the importance of keeping the keystore safe as shown below: screen shot 2016-06-06 at 10 01 25 Closes https://github.com/facebook/react-native/pull/7948 Differential Revision: D3393005 fbshipit-source-id: e9c2666a79134eccc1b1868fe850ee896e63266e --- docs/SignedAPKAndroid.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/SignedAPKAndroid.md b/docs/SignedAPKAndroid.md index 555e4cc347e4eb..c271d4e7c9497c 100644 --- a/docs/SignedAPKAndroid.md +++ b/docs/SignedAPKAndroid.md @@ -35,9 +35,11 @@ MYAPP_RELEASE_KEY_PASSWORD=***** These are going to be global gradle variables, which we can later use in our gradle config to sign our app. -_Note about saving the keystore: Once you publish the app on the Play Store, you will need to republish your app under a different package name (losing all downloads and ratings) if you want to change the signing key at any point. So backup your keystore and don't forget the passwords._ +> __Note about saving the keystore:__ -_Note about security: If you are not keen on storing your passwords in plaintext and you are running OSX, you can also [store your credentials in the Keychain Access app](https://pilloxa.gitlab.io/posts/safer-passwords-in-gradle/). Then you can skip the two last rows in `~/.gradle/gradle.properties`._ +> Once you publish the app on the Play Store, you will need to republish your app under a different package name (losing all downloads and ratings) if you want to change the signing key at any point. So backup your keystore and don't forget the passwords. + +_Note about security: If you are not keen on storing your passwords in plaintext and you are running OSX, you can also [store your credentials in the Keychain Access app](https://pilloxa.gitlab.io/posts/safer-passwords-in-gradle/). Then you can skip the two last rows in `~/.gradle/gradle.properties`._ ### Adding signing config to your app's gradle config From 44c9cf3a917cd7efd3e5569f47fb1aa8b53587b7 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 6 Jun 2016 10:27:36 -0700 Subject: [PATCH 235/843] Refactored subview management Reviewed By: javache Differential Revision: D3392214 fbshipit-source-id: 6f16841df5cf866dda5ac27dd244e266ec85a86e --- .../UIExplorerUnitTests/RCTUIManagerTests.m | 10 +- Libraries/Text/RCTText.m | 17 +-- Libraries/Text/RCTTextField.m | 26 ---- Libraries/Text/RCTTextView.m | 22 +-- React/Base/RCTRootView.m | 2 +- React/Modules/RCTUIManager.m | 14 +- React/Views/RCTComponent.h | 5 + React/Views/RCTMap.m | 16 +-- React/Views/RCTModalHostView.m | 15 +- React/Views/RCTNavigator.m | 33 +++-- React/Views/RCTScrollView.m | 12 +- React/Views/RCTShadowView.m | 5 + React/Views/RCTTabBar.m | 29 ++-- React/Views/RCTView.m | 129 +++++------------- React/Views/UIView+React.h | 15 +- React/Views/UIView+React.m | 30 ++-- 16 files changed, 147 insertions(+), 233 deletions(-) diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m index 4ae4b9f5ca8027..c3bd09ff1313ef 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m @@ -77,8 +77,8 @@ - (void)testManagingChildrenToAddViews @"Expect to have 5 react subviews after calling manage children \ with 5 tags to add, instead have %lu", (unsigned long)[[containerView reactSubviews] count]); for (UIView *view in addedViews) { - XCTAssertTrue([view superview] == containerView, - @"Expected to have manage children successfully add children"); + XCTAssertTrue([view reactSuperview] == containerView, + @"Expected to have manage children successfully add children"); [view removeFromSuperview]; } } @@ -95,7 +95,7 @@ - (void)testManagingChildrenToRemoveViews } for (NSInteger i = 2; i < 20; i++) { UIView *view = _uiManager.viewRegistry[@(i)]; - [containerView addSubview:view]; + [containerView insertReactSubview:view atIndex:containerView.reactSubviews.count]; } // Remove views 1-5 from view 20 @@ -112,7 +112,7 @@ - (void)testManagingChildrenToRemoveViews with 5 tags to remove and 18 prior children, instead have %zd", containerView.reactSubviews.count); for (UIView *view in removedViews) { - XCTAssertTrue([view superview] == nil, + XCTAssertTrue([view reactSuperview] == nil, @"Expected to have manage children successfully remove children"); // After removing views are unregistered - we need to reregister _uiManager.viewRegistry[view.reactTag] = view; @@ -155,7 +155,7 @@ - (void)testManagingChildrenToAddRemoveAndMove for (NSInteger i = 1; i < 11; i++) { UIView *view = _uiManager.viewRegistry[@(i)]; - [containerView addSubview:view]; + [containerView insertReactSubview:view atIndex:containerView.reactSubviews.count]; } [_uiManager _manageChildren:@20 diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index 864fa096b42dc0..fc5f60bdcea0d2 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -27,7 +27,6 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc @implementation RCTText { NSTextStorage *_textStorage; - NSMutableArray *_reactSubviews; CAShapeLayer *_highlightLayer; } @@ -35,7 +34,6 @@ - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { _textStorage = [NSTextStorage new]; - _reactSubviews = [NSMutableArray array]; self.isAccessibilityElement = YES; self.accessibilityTraits |= UIAccessibilityTraitStaticText; @@ -68,19 +66,9 @@ - (void)reactSetInheritedBackgroundColor:(UIColor *)inheritedBackgroundColor self.backgroundColor = inheritedBackgroundColor; } -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +- (void)reactUpdateSubviews { - [_reactSubviews insertObject:subview atIndex:atIndex]; -} - -- (void)removeReactSubview:(UIView *)subview -{ - [_reactSubviews removeObject:subview]; -} - -- (NSArray *)reactSubviews -{ - return _reactSubviews; + // Do nothing, as subviews are managed by `setTextStorage:` method } - (void)setTextStorage:(NSTextStorage *)textStorage @@ -88,6 +76,7 @@ - (void)setTextStorage:(NSTextStorage *)textStorage if (_textStorage != textStorage) { _textStorage = textStorage; + // Update subviews NSMutableArray *nonTextDescendants = [NSMutableArray new]; collectNonTextDescendants(self, nonTextDescendants); NSArray *subviews = self.subviews; diff --git a/Libraries/Text/RCTTextField.m b/Libraries/Text/RCTTextField.m index 24b077baff106b..71859ad4d77443 100644 --- a/Libraries/Text/RCTTextField.m +++ b/Libraries/Text/RCTTextField.m @@ -17,7 +17,6 @@ @implementation RCTTextField { RCTEventDispatcher *_eventDispatcher; - NSMutableArray *_reactSubviews; BOOL _jsRequestingFirstResponder; NSInteger _nativeEventCount; BOOL _submitted; @@ -35,7 +34,6 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher [self addTarget:self action:@selector(textFieldEndEditing) forControlEvents:UIControlEventEditingDidEnd]; [self addTarget:self action:@selector(textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit]; [self addObserver:self forKeyPath:@"selectedTextRange" options:0 context:nil]; - _reactSubviews = [NSMutableArray new]; _blurOnSubmit = YES; } return self; @@ -112,30 +110,6 @@ - (void)setPlaceholder:(NSString *)placeholder RCTUpdatePlaceholder(self); } -- (NSArray *)reactSubviews -{ - // TODO: do we support subviews of textfield in React? - // In any case, we should have a better approach than manually - // maintaining array in each view subclass like this - return _reactSubviews; -} - -- (void)removeReactSubview:(UIView *)subview -{ - // TODO: this is a bit broken - if the TextField inserts any of - // its own views below or between React's, the indices won't match - [_reactSubviews removeObject:subview]; - [subview removeFromSuperview]; -} - -- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex -{ - // TODO: this is a bit broken - if the TextField inserts any of - // its own views below or between React's, the indices won't match - [_reactSubviews insertObject:view atIndex:atIndex]; - [super insertSubview:view atIndex:atIndex]; -} - - (CGRect)caretRectForPosition:(UITextPosition *)position { if (_caretHidden) { diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index 84e83214f265b0..2d4c0a7d905f1a 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -67,7 +67,6 @@ @implementation RCTTextView NSInteger _nativeEventCount; RCTText *_richTextView; NSAttributedString *_pendingAttributedText; - NSMutableArray *_subviews; BOOL _blockTextShouldChange; UITextRange *_previousSelectionRange; NSUInteger _previousTextLength; @@ -97,7 +96,6 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher _previousSelectionRange = _textView.selectedTextRange; - _subviews = [NSMutableArray new]; [self addSubview:_scrollView]; } return self; @@ -106,19 +104,14 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) -- (NSArray *)reactSubviews -{ - return _subviews; -} - - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index { + [super insertReactSubview:subview atIndex:index]; if ([subview isKindOfClass:[RCTText class]]) { if (_richTextView) { RCTLogError(@"Tried to insert a second into - there can only be one."); } _richTextView = (RCTText *)subview; - [_subviews insertObject:_richTextView atIndex:index]; // If this is in rich text editing mode, and the child node providing rich text // styling has a backgroundColor, then the attributedText produced by the child node will have an @@ -131,23 +124,22 @@ - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index attrs[NSBackgroundColorAttributeName] = subview.backgroundColor; _textView.typingAttributes = attrs; } - } else { - [_subviews insertObject:subview atIndex:index]; - [self insertSubview:subview atIndex:index]; } } - (void)removeReactSubview:(UIView *)subview { + [super removeReactSubview:subview]; if (_richTextView == subview) { - [_subviews removeObject:_richTextView]; _richTextView = nil; - } else { - [_subviews removeObject:subview]; - [subview removeFromSuperview]; } } +- (void)reactUpdateSubviews +{ + // Do nothing, as we don't allow non-text subviews +} + - (void)setMostRecentEventCount:(NSInteger)mostRecentEventCount { _mostRecentEventCount = mostRecentEventCount; diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index cff7861a1b9833..78896f5fe482e1 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -337,7 +337,7 @@ - (instancetype)initWithFrame:(CGRect)frame RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder:(nonnull NSCoder *)aDecoder) -- (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { [super insertReactSubview:subview atIndex:atIndex]; RCTPerformanceLoggerEnd(RCTPLTTI); diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 168e115dc82f3d..4ab30f9918b723 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -893,16 +893,18 @@ static void RCTSetChildren(NSNumber *containerTag, [container insertReactSubview:view atIndex:index++]; } } + + [container didUpdateReactSubviews]; } -RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerReactTag +RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag moveFromIndices:(NSArray *)moveFromIndices moveToIndices:(NSArray *)moveToIndices addChildReactTags:(NSArray *)addChildReactTags addAtIndices:(NSArray *)addAtIndices removeAtIndices:(NSArray *)removeAtIndices) { - [self _manageChildren:containerReactTag + [self _manageChildren:containerTag moveFromIndices:moveFromIndices moveToIndices:moveToIndices addChildReactTags:addChildReactTags @@ -911,7 +913,7 @@ static void RCTSetChildren(NSNumber *containerTag, registry:(NSMutableDictionary> *)_shadowViewRegistry]; [self addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry){ - [uiManager _manageChildren:containerReactTag + [uiManager _manageChildren:containerTag moveFromIndices:moveFromIndices moveToIndices:moveToIndices addChildReactTags:addChildReactTags @@ -921,7 +923,7 @@ static void RCTSetChildren(NSNumber *containerTag, }]; } -- (void)_manageChildren:(NSNumber *)containerReactTag +- (void)_manageChildren:(NSNumber *)containerTag moveFromIndices:(NSArray *)moveFromIndices moveToIndices:(NSArray *)moveToIndices addChildReactTags:(NSArray *)addChildReactTags @@ -929,7 +931,7 @@ - (void)_manageChildren:(NSNumber *)containerReactTag removeAtIndices:(NSArray *)removeAtIndices registry:(NSMutableDictionary> *)registry { - id container = registry[containerReactTag]; + id container = registry[containerTag]; RCTAssert(moveFromIndices.count == moveToIndices.count, @"moveFromIndices had size %tu, moveToIndices had size %tu", moveFromIndices.count, moveToIndices.count); RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one React child to add"); @@ -963,6 +965,8 @@ - (void)_manageChildren:(NSNumber *)containerReactTag [container insertReactSubview:destinationsToChildrenToAdd[reactIndex] atIndex:reactIndex.integerValue]; } + + [container didUpdateReactSubviews]; } RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag diff --git a/React/Views/RCTComponent.h b/React/Views/RCTComponent.h index 5236f850b7993e..5c9beab09ba96c 100644 --- a/React/Views/RCTComponent.h +++ b/React/Views/RCTComponent.h @@ -43,6 +43,11 @@ typedef void (^RCTBubblingEventBlock)(NSDictionary *body); */ - (void)didSetProps:(NSArray *)changedProps; +/** + * Called each time subviews have been updated + */ +- (void)didUpdateReactSubviews; + // TODO: Deprecate this // This method is called after layout has been performed for all views known // to the RCTViewManager. It is only called on UIViews, not shadow views. diff --git a/React/Views/RCTMap.m b/React/Views/RCTMap.m index 1b3472530155d0..7a1334668761ca 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -23,7 +23,6 @@ @implementation RCTMap { UIView *_legalLabel; CLLocationManager *_locationManager; - NSMutableArray *_reactSubviews; } - (instancetype)init @@ -31,7 +30,6 @@ - (instancetype)init if ((self = [super init])) { _hasStartedRendering = NO; - _reactSubviews = [NSMutableArray new]; // Find Apple link label for (UIView *subview in self.subviews) { @@ -51,19 +49,9 @@ - (void)dealloc [_regionChangeObserveTimer invalidate]; } -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +- (void)reactUpdateSubviews { - [_reactSubviews insertObject:subview atIndex:atIndex]; -} - -- (void)removeReactSubview:(UIView *)subview -{ - [_reactSubviews removeObject:subview]; -} - -- (NSArray *)reactSubviews -{ - return _reactSubviews; + // Do nothing, as annotation views are managed by `setAnnotations:` method } - (void)layoutSubviews diff --git a/React/Views/RCTModalHostView.m b/React/Views/RCTModalHostView.m index c3140249305873..3dce5ed759910b 100644 --- a/React/Views/RCTModalHostView.m +++ b/React/Views/RCTModalHostView.m @@ -55,14 +55,10 @@ - (void)notifyForBoundsChange:(CGRect)newBounds } } -- (NSArray *)reactSubviews -{ - return _reactSubview ? @[_reactSubview] : @[]; -} - -- (void)insertReactSubview:(UIView *)subview atIndex:(__unused NSInteger)atIndex +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { RCTAssert(_reactSubview == nil, @"Modal view can only have one subview"); + [super insertReactSubview:subview atIndex:atIndex]; [subview addGestureRecognizer:_touchHandler]; subview.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; @@ -74,11 +70,16 @@ - (void)insertReactSubview:(UIView *)subview atIndex:(__unused NSInteger)atIndex - (void)removeReactSubview:(UIView *)subview { RCTAssert(subview == _reactSubview, @"Cannot remove view other than modal view"); + [super removeReactSubview:subview]; [subview removeGestureRecognizer:_touchHandler]; - [subview removeFromSuperview]; _reactSubview = nil; } +- (void)didUpdateReactSubviews +{ + // Do nothing, as subview (singular) is managed by `insertReactSubview:atIndex:` +} + - (void)dismissModalViewController { if (_isPresented) { diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index d95089ee461559..eb19b1f6c36579 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -217,7 +217,6 @@ @interface RCTNavigator() *previousViews; -@property (nonatomic, readwrite, strong) NSMutableArray *currentViews; @property (nonatomic, readwrite, strong) RCTNavigationController *navigationController; /** * Display link is used to get high frequency sample rate during @@ -299,7 +298,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _dummyView = [[UIView alloc] initWithFrame:CGRectZero]; _previousRequestedTopOfStack = kNeverRequested; // So that we initialize with a push. _previousViews = @[]; - _currentViews = [[NSMutableArray alloc] initWithCapacity:0]; __weak RCTNavigator *weakSelf = self; _navigationController = [[RCTNavigationController alloc] initWithScrollCallback:^{ [weakSelf dispatchFakeScrollEvent]; @@ -351,7 +349,7 @@ - (void)setPaused:(BOOL)paused - (void)setInteractivePopGestureEnabled:(BOOL)interactivePopGestureEnabled { _interactivePopGestureEnabled = interactivePopGestureEnabled; - + _navigationController.interactivePopGestureRecognizer.delegate = self; _navigationController.interactivePopGestureRecognizer.enabled = interactivePopGestureEnabled; @@ -402,8 +400,8 @@ - (void)navigationController:(UINavigationController *)navigationController return; } - NSUInteger indexOfFrom = [_currentViews indexOfObject:fromController.navItem]; - NSUInteger indexOfTo = [_currentViews indexOfObject:toController.navItem]; + NSUInteger indexOfFrom = [self.reactSubviews indexOfObject:fromController.navItem]; + NSUInteger indexOfTo = [self.reactSubviews indexOfObject:toController.navItem]; CGFloat destination = indexOfFrom < indexOfTo ? 1.0 : -1.0; _dummyView.frame = (CGRect){{destination, 0}, CGSizeZero}; _currentlyTransitioningFrom = indexOfFrom; @@ -433,7 +431,7 @@ - (BOOL)requestSchedulingJavaScriptNavigation - (void)freeLock { _navigationController.navigationLock = RCTNavigationLockNone; - + // Unless the pop gesture has been explicitly disabled (RCTPopGestureStateDisabled), // Set interactivePopGestureRecognizer.enabled to YES // If the popGestureState is RCTPopGestureStateDefault the default behavior will be maintained @@ -452,12 +450,12 @@ - (void)insertReactSubview:(RCTNavItem *)view atIndex:(NSInteger)atIndex _navigationController.navigationLock == RCTNavigationLockJavaScript, @"Cannot change subviews from JS without first locking." ); - [_currentViews insertObject:view atIndex:atIndex]; + [super insertReactSubview:view atIndex:atIndex]; } -- (NSArray *)reactSubviews +- (void)didUpdateReactSubviews { - return _currentViews; + // Do nothing, as subviews are managed by `reactBridgeDidFinishTransaction` } - (void)layoutSubviews @@ -469,11 +467,11 @@ - (void)layoutSubviews - (void)removeReactSubview:(RCTNavItem *)subview { - if (_currentViews.count <= 0 || subview == _currentViews[0]) { + if (self.reactSubviews.count <= 0 || subview == self.reactSubviews[0]) { RCTLogError(@"Attempting to remove invalid RCT subview of RCTNavigator"); return; } - [_currentViews removeObject:subview]; + [super removeReactSubview:subview]; } - (void)handleTopOfStackChanged @@ -497,7 +495,8 @@ - (void)dispatchFakeScrollEvent - (UIView *)reactSuperview { RCTAssert(!_bridge.isValid || self.superview != nil, @"put reactNavSuperviewLink back"); - return self.superview ? self.superview : self.reactNavSuperviewLink; + UIView *superview = [super reactSuperview]; + return superview ?: self.reactNavSuperviewLink; } - (void)reactBridgeDidFinishTransaction @@ -545,14 +544,14 @@ - (void)reactBridgeDidFinishTransaction jsGettingtooSlow)) { RCTLogError(@"JS has only made partial progress to catch up to UIKit"); } - if (currentReactCount > _currentViews.count) { + if (currentReactCount > self.reactSubviews.count) { RCTLogError(@"Cannot adjust current top of stack beyond available views"); } // Views before the previous React count must not have changed. Views greater than previousReactCount // up to currentReactCount may have changed. - for (NSUInteger i = 0; i < MIN(_currentViews.count, MIN(_previousViews.count, previousReactCount)); i++) { - if (_currentViews[i] != _previousViews[i]) { + for (NSUInteger i = 0; i < MIN(self.reactSubviews.count, MIN(_previousViews.count, previousReactCount)); i++) { + if (self.reactSubviews[i] != _previousViews[i]) { RCTLogError(@"current view should equal previous view"); } } @@ -561,7 +560,7 @@ - (void)reactBridgeDidFinishTransaction } if (jsGettingAhead) { if (reactPushOne) { - UIView *lastView = _currentViews.lastObject; + UIView *lastView = self.reactSubviews.lastObject; RCTWrapperViewController *vc = [[RCTWrapperViewController alloc] initWithNavItem:(RCTNavItem *)lastView]; vc.navigationListener = self; _numberOfViewControllerMovesToIgnore = 1; @@ -580,7 +579,7 @@ - (void)reactBridgeDidFinishTransaction return; } - _previousViews = [_currentViews copy]; + _previousViews = [self.reactSubviews copy]; _previousRequestedTopOfStack = _requestedTopOfStack; } diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index a2d1e5d859cf58..3f2cd82efc057b 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -418,8 +418,9 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews // Does nothing } -- (void)insertReactSubview:(UIView *)view atIndex:(__unused NSInteger)atIndex +- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex { + [super insertReactSubview:view atIndex:atIndex]; if ([view isKindOfClass:[RCTRefreshControl class]]) { _scrollView.refreshControl = (RCTRefreshControl*)view; } else { @@ -431,21 +432,18 @@ - (void)insertReactSubview:(UIView *)view atIndex:(__unused NSInteger)atIndex - (void)removeReactSubview:(UIView *)subview { + [super removeReactSubview:subview]; if ([subview isKindOfClass:[RCTRefreshControl class]]) { _scrollView.refreshControl = nil; } else { RCTAssert(_contentView == subview, @"Attempted to remove non-existent subview"); _contentView = nil; - [subview removeFromSuperview]; } } -- (NSArray *)reactSubviews +- (void)didUpdateReactSubviews { - if (_contentView && _scrollView.refreshControl) { - return @[_contentView, _scrollView.refreshControl]; - } - return _contentView ? @[_contentView] : @[]; + // Do nothing, as subviews are managed by `insertReactSubview:atIndex:` } - (BOOL)centerContent diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index 83ce22a5beb1fe..0d31e150734ee0 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -297,6 +297,11 @@ - (BOOL)isReactRootView return RCTIsReactRootView(self.reactTag); } +- (void)didUpdateReactSubviews +{ + // Does nothing by default +} + - (void)dealloc { free_css_node(_cssNode); diff --git a/React/Views/RCTTabBar.m b/React/Views/RCTTabBar.m index 6f31f69985e20c..d544d75b2efa48 100644 --- a/React/Views/RCTTabBar.m +++ b/React/Views/RCTTabBar.m @@ -26,13 +26,11 @@ @implementation RCTTabBar { BOOL _tabsChanged; UITabBarController *_tabController; - NSMutableArray *_tabViews; } - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { - _tabViews = [NSMutableArray new]; _tabController = [UITabBarController new]; _tabController.delegate = self; [self addSubview:_tabController.view]; @@ -53,31 +51,31 @@ - (void)dealloc [_tabController removeFromParentViewController]; } -- (NSArray *)reactSubviews +- (void)insertReactSubview:(RCTTabBarItem *)subview atIndex:(NSInteger)atIndex { - return _tabViews; -} - -- (void)insertReactSubview:(RCTTabBarItem *)view atIndex:(NSInteger)atIndex -{ - if (![view isKindOfClass:[RCTTabBarItem class]]) { + if (![subview isKindOfClass:[RCTTabBarItem class]]) { RCTLogError(@"subview should be of type RCTTabBarItem"); return; } - [_tabViews insertObject:view atIndex:atIndex]; + [super insertReactSubview:subview atIndex:atIndex]; _tabsChanged = YES; } - (void)removeReactSubview:(RCTTabBarItem *)subview { - if (_tabViews.count == 0) { + if (self.reactSubviews.count == 0) { RCTLogError(@"should have at least one view to remove a subview"); return; } - [_tabViews removeObject:subview]; + [super removeReactSubview:subview]; _tabsChanged = YES; } +- (void)didUpdateReactSubviews +{ + // Do nothing, as subviews are managed by `reactBridgeDidFinishTransaction` +} + - (void)layoutSubviews { [super layoutSubviews]; @@ -106,8 +104,9 @@ - (void)reactBridgeDidFinishTransaction _tabsChanged = NO; } - [_tabViews enumerateObjectsUsingBlock: - ^(RCTTabBarItem *tab, NSUInteger index, __unused BOOL *stop) { + [self.reactSubviews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger index, __unused BOOL *stop) { + + RCTTabBarItem *tab = (RCTTabBarItem *)view; UIViewController *controller = _tabController.viewControllers[index]; if (_unselectedTintColor) { [tab.barItem setTitleTextAttributes:@{NSForegroundColorAttributeName: _unselectedTintColor} forState:UIControlStateNormal]; @@ -165,7 +164,7 @@ - (void)setItemPositioning:(UITabBarItemPositioning)itemPositioning - (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController { NSUInteger index = [tabBarController.viewControllers indexOfObject:viewController]; - RCTTabBarItem *tab = _tabViews[index]; + RCTTabBarItem *tab = (RCTTabBarItem *)self.reactSubviews[index]; if (tab.onPress) tab.onPress(nil); return NO; } diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index ad2ea1424bbdaf..ffce4b7fd07864 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -97,7 +97,6 @@ - (UIView *)react_findClipView @implementation RCTView { - NSMutableArray *_reactSubviews; UIColor *_backgroundColor; } @@ -275,76 +274,31 @@ + (UIEdgeInsets)contentInsetsForView:(UIView *)view - (void)react_remountAllSubviews { - if (_reactSubviews) { - NSUInteger index = 0; - for (UIView *view in _reactSubviews) { + if (_removeClippedSubviews) { + for (UIView *view in self.reactSubviews) { if (view.superview != self) { - if (index < self.subviews.count) { - [self insertSubview:view atIndex:index]; - } else { - [self addSubview:view]; - } + [self addSubview:view]; [view react_remountAllSubviews]; } - index++; } } else { - // If react_subviews is nil, we must already be showing all subviews + // If _removeClippedSubviews is false, we must already be showing all subviews [super react_remountAllSubviews]; } } -- (void)remountSubview:(UIView *)view -{ - // Calculate insertion index for view - NSInteger index = 0; - for (UIView *subview in _reactSubviews) { - if (subview == view) { - [self insertSubview:view atIndex:index]; - break; - } - if (subview.superview) { - // View is mounted, so bump the index - index++; - } - } -} - -- (void)mountOrUnmountSubview:(UIView *)view withClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView -{ - if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) { - - // View is at least partially visible, so remount it if unmounted - if (view.superview == nil) { - [self remountSubview:view]; - } - - // Then test its subviews - if (CGRectContainsRect(clipRect, view.frame)) { - [view react_remountAllSubviews]; - } else { - [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; - } - - } else if (view.superview) { - - // View is completely outside the clipRect, so unmount it - [view removeFromSuperview]; - } -} - - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView { // TODO (#5906496): for scrollviews (the primary use-case) we could // optimize this by only doing a range check along the scroll axis, // instead of comparing the whole frame - if (_reactSubviews == nil) { + if (!_removeClippedSubviews) { // Use default behavior if unmounting is disabled return [super react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; } - if (_reactSubviews.count == 0) { + if (self.reactSubviews.count == 0) { // Do nothing if we have no subviews return; } @@ -360,61 +314,44 @@ - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView: clipView = self; // Mount / unmount views - for (UIView *view in _reactSubviews) { - [self mountOrUnmountSubview:view withClipRect:clipRect relativeToView:clipView]; - } -} + for (UIView *view in self.reactSubviews) { + if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) { -- (void)setRemoveClippedSubviews:(BOOL)removeClippedSubviews -{ - if (removeClippedSubviews && !_reactSubviews) { - _reactSubviews = [self.subviews mutableCopy]; - } else if (!removeClippedSubviews && _reactSubviews) { - [self react_remountAllSubviews]; - _reactSubviews = nil; - } -} - -- (BOOL)removeClippedSubviews -{ - return _reactSubviews != nil; -} + // View is at least partially visible, so remount it if unmounted + [self addSubview:view]; -- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex -{ - if (_reactSubviews == nil) { - [self insertSubview:view atIndex:atIndex]; - } else { - [_reactSubviews insertObject:view atIndex:atIndex]; - - // Find a suitable view to use for clipping - UIView *clipView = [self react_findClipView]; - if (clipView) { - - // If possible, don't add subviews if they are clipped - [self mountOrUnmountSubview:view withClipRect:clipView.bounds relativeToView:clipView]; + // Then test its subviews + if (CGRectContainsRect(clipRect, view.frame)) { + // View is fully visible, so remount all subviews + [view react_remountAllSubviews]; + } else { + // View is partially visible, so update clipped subviews + [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; + } - } else { + } else if (view.superview) { - // Fallback if we can't find a suitable clipView - [self remountSubview:view]; + // View is completely outside the clipRect, so unmount it + [view removeFromSuperview]; } } } -- (void)removeReactSubview:(UIView *)subview +- (void)setRemoveClippedSubviews:(BOOL)removeClippedSubviews { - [_reactSubviews removeObject:subview]; - [subview removeFromSuperview]; + if (!removeClippedSubviews && _removeClippedSubviews) { + [self react_remountAllSubviews]; + } + _removeClippedSubviews = removeClippedSubviews; } -- (NSArray *)reactSubviews +- (void)didUpdateReactSubviews { - // The _reactSubviews array is only used when we have hidden - // offscreen views. If _reactSubviews is nil, we can assume - // that [self reactSubviews] and [self subviews] are the same - - return _reactSubviews ?: self.subviews; + if (_removeClippedSubviews) { + [self updateClippedSubviews]; + } else { + [super didUpdateReactSubviews]; + } } - (void)updateClippedSubviews @@ -435,7 +372,7 @@ - (void)layoutSubviews [super layoutSubviews]; - if (_reactSubviews) { + if (_removeClippedSubviews) { [self updateClippedSubviews]; } } diff --git a/React/Views/UIView+React.h b/React/Views/UIView+React.h index 794c977e9e7413..fa779e5208a821 100644 --- a/React/Views/UIView+React.h +++ b/React/Views/UIView+React.h @@ -17,8 +17,19 @@ @interface UIView (React) -- (NSArray *)reactSubviews; -- (UIView *)reactSuperview; +/** + * RCTComponent interface. + */ +- (NSArray *)reactSubviews NS_REQUIRES_SUPER; +- (UIView *)reactSuperview NS_REQUIRES_SUPER; +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex NS_REQUIRES_SUPER; +- (void)removeReactSubview:(UIView *)subview NS_REQUIRES_SUPER; + +/** + * Updates the subviews array based on the reactSubviews. Default behavior is + * to insert the reactSubviews into the UIView. + */ +- (void)didUpdateReactSubviews; /** * Used by the UIIManager to set the view frame. diff --git a/React/Views/UIView+React.m b/React/Views/UIView+React.m index e7736f3a125651..69658d132f807a 100644 --- a/React/Views/UIView+React.m +++ b/React/Views/UIView+React.m @@ -56,25 +56,37 @@ - (NSNumber *)reactTagAtPoint:(CGPoint)point return view.reactTag; } -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +- (NSArray *)reactSubviews { - [self insertSubview:subview atIndex:atIndex]; + return objc_getAssociatedObject(self, _cmd); } -- (void)removeReactSubview:(UIView *)subview +- (UIView *)reactSuperview { - RCTAssert(subview.superview == self, @"%@ is a not a subview of %@", subview, self); - [subview removeFromSuperview]; + return self.superview; } -- (NSArray *)reactSubviews +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { - return self.subviews; + NSMutableArray *reactSubviews = (NSMutableArray *)self.reactSubviews; + if (!reactSubviews) { + reactSubviews = [NSMutableArray new]; + objc_setAssociatedObject(self, @selector(reactSubviews), reactSubviews, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + [reactSubviews insertObject:subview atIndex:atIndex]; } -- (UIView *)reactSuperview +- (void)removeReactSubview:(UIView *)subview { - return self.superview; + [(NSMutableArray *)self.reactSubviews removeObject:subview]; + [subview removeFromSuperview]; +} + +- (void)didUpdateReactSubviews +{ + for (UIView *subview in self.reactSubviews) { + [self addSubview:subview]; + } } - (void)reactSetFrame:(CGRect)frame From 42a5568419fb1fea6ffdd59725f065949d799a7f Mon Sep 17 00:00:00 2001 From: Alexey Lang Date: Mon, 6 Jun 2016 11:10:24 -0700 Subject: [PATCH 236/843] Change API for using custom JSC Reviewed By: javache Differential Revision: D3392219 fbshipit-source-id: ae372dfe9ceab7f7c2da69c731515a05eb3b020a --- React/Executors/RCTJSCExecutor.h | 11 +++++------ React/Executors/RCTJSCExecutor.mm | 17 ++++++----------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/React/Executors/RCTJSCExecutor.h b/React/Executors/RCTJSCExecutor.h index f5b91bf8f06b74..db690042e4d94c 100644 --- a/React/Executors/RCTJSCExecutor.h +++ b/React/Executors/RCTJSCExecutor.h @@ -31,18 +31,17 @@ RCT_EXTERN NSString *const RCTJavaScriptContextCreatedNotification; @interface RCTJSCExecutor : NSObject /** - * Sets a type of JSC library (system or custom) that's used - * to initialize RCTJSCWrapper. + * Returns whether executor uses custom JSC library. + * This value is used to initialize RCTJSCWrapper. * @default is NO. */ -+ (void)setUseCustomJSCLibrary:(BOOL)useCustomLibrary; +@property (nonatomic, readonly, assign) BOOL useCustomJSCLibrary; /** - * Gets a type of JSC library (system or custom) that's used + * Inits a new executor instance with given flag that's used * to initialize RCTJSCWrapper. - * @default is NO. */ -+ (BOOL)useCustomJSCLibrary; +- (instancetype)initWithUseCustomJSCLibrary:(BOOL)useCustomJSCLibrary; /** * Create a NSError from a JSError object. diff --git a/React/Executors/RCTJSCExecutor.mm b/React/Executors/RCTJSCExecutor.mm index 91e80d7b882bc8..a65cd36c4890cd 100644 --- a/React/Executors/RCTJSCExecutor.mm +++ b/React/Executors/RCTJSCExecutor.mm @@ -135,6 +135,7 @@ @implementation RCTJSCExecutor RandomAccessBundleData _randomAccessBundle; RCTJSCWrapper *_jscWrapper; + BOOL _useCustomJSCLibrary; } @synthesize valid = _valid; @@ -262,21 +263,15 @@ + (void)runRunLoopThread } } -static BOOL useCustomJSCLibrary = NO; - -+ (void)setUseCustomJSCLibrary:(BOOL)useCustomLibrary -{ - useCustomJSCLibrary = useCustomLibrary; -} - -+ (BOOL)useCustomJSCLibrary +- (instancetype)init { - return useCustomJSCLibrary; + return [self initWithUseCustomJSCLibrary:NO]; } -- (instancetype)init +- (instancetype)initWithUseCustomJSCLibrary:(BOOL)useCustomJSCLibrary { if (self = [super init]) { + _useCustomJSCLibrary = useCustomJSCLibrary; _valid = YES; _javaScriptThread = [[NSThread alloc] initWithTarget:[self class] @@ -333,7 +328,7 @@ - (void)setUp return; } - strongSelf->_jscWrapper = RCTJSCWrapperCreate(useCustomJSCLibrary); + strongSelf->_jscWrapper = RCTJSCWrapperCreate(strongSelf->_useCustomJSCLibrary); }]; From 1dc33b5f23640a60682ac879b9a3e94a4aa519d9 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Mon, 6 Jun 2016 11:15:38 -0700 Subject: [PATCH 237/843] Add example for composite navigation. Reviewed By: hramos Differential Revision: D3388314 fbshipit-source-id: 7066363e03534e75ccdf8806db9ed395ee2163a4 --- .../NavigationExperimentalExample.js | 1 + .../NavigationHeaderScenesTabs-example.js | 478 ++++++++++++++++++ 2 files changed, 479 insertions(+) create mode 100644 Examples/UIExplorer/NavigationExperimental/NavigationHeaderScenesTabs-example.js diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationExperimentalExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationExperimentalExample.js index 12bb5664361c35..d264ff6fc2af65 100644 --- a/Examples/UIExplorer/NavigationExperimental/NavigationExperimentalExample.js +++ b/Examples/UIExplorer/NavigationExperimental/NavigationExperimentalExample.js @@ -34,6 +34,7 @@ const View = require('View'); */ const EXAMPLES = { 'NavigationCardStack Example': require('./NavigationCardStack-example'), + 'Header + Scenes + Tabs Example': require('./NavigationHeaderScenesTabs-example'), }; const EXAMPLE_STORAGE_KEY = 'NavigationExperimentalExample'; diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationHeaderScenesTabs-example.js b/Examples/UIExplorer/NavigationExperimental/NavigationHeaderScenesTabs-example.js new file mode 100644 index 00000000000000..7d8efd0c950917 --- /dev/null +++ b/Examples/UIExplorer/NavigationExperimental/NavigationHeaderScenesTabs-example.js @@ -0,0 +1,478 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +'use strict'; + +const NavigationExampleRow = require('./NavigationExampleRow'); +const React = require('react'); +const ReactNative = require('react-native'); + +/** + * Basic example that shows how to use to build + * an app with composite navigation system. + */ + +const { + Component, + PropTypes, +} = React; + +const { + NavigationExperimental, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} = ReactNative; + +const { + CardStack: NavigationCardStack, + Header: NavigationHeader, + PropTypes: NavigationPropTypes, + StateUtils: NavigationStateUtils, +} = NavigationExperimental; + +// First Step. +// Define what app navigation state will look like. +function createAppNavigationState(): Object { + return { + // Three tabs. + tabs: { + index: 0, + routes: [ + {key: 'apple'}, + {key: 'banana'}, + {key: 'orange'}, + ], + }, + // Scenes for the `apple` tab. + apple: { + index: 0, + routes: [{key: 'Apple Home'}], + }, + // Scenes for the `banana` tab. + banana: { + index: 0, + routes: [{key: 'Banana Home'}], + }, + // Scenes for the `orange` tab. + orange: { + index: 0, + routes: [{key: 'Orange Home'}], + }, + }; +} + +// Next step. +// Define what app navigation state shall be updated. +function updateAppNavigationState( + state: Object, + action: Object, +): Object { + let {type} = action; + if (type === 'BackAction') { + type = 'pop'; + } + + switch (type) { + case 'push': { + // Push a route into the scenes stack. + const route: Object = action.route; + const {tabs} = state; + const tabKey = tabs.routes[tabs.index].key; + const scenes = state[tabKey]; + const nextScenes = NavigationStateUtils.push(scenes, route); + if (scenes !== nextScenes) { + return { + ...state, + [tabKey]: nextScenes, + }; + } + break; + } + + case 'pop': { + // Pops a route from the scenes stack. + const {tabs} = state; + const tabKey = tabs.routes[tabs.index].key; + const scenes = state[tabKey]; + const nextScenes = NavigationStateUtils.pop(scenes); + if (scenes !== nextScenes) { + return { + ...state, + [tabKey]: nextScenes, + }; + } + break; + } + + case 'selectTab': { + // Switches the tab. + const tabKey: string = action.tabKey; + const tabs = NavigationStateUtils.jumpTo(state.tabs, tabKey); + if (tabs !== state.tabs) { + return { + ...state, + tabs, + }; + } + } + } + return state; +} + +// Next step. +// Defines a helper function that creates a HOC (higher-order-component) +// which provides a function `navigate` through component props. The +// `navigate` function will be used to invoke navigation changes. +// This serves a convenient way for a component to navigate. +function createAppNavigationContainer(ComponentClass) { + const key = '_yourAppNavigationContainerNavigateCall'; + + class Container extends Component { + static contextTypes = { + [key]: PropTypes.func, + }; + + static childContextTypes = { + [key]: PropTypes.func.isRequired, + }; + + static propTypes = { + navigate: PropTypes.func, + }; + + getChildContext(): Object { + return { + [key]: this.context[key] || this.props.navigate, + }; + } + + render(): ReactElement { + const navigate = this.context[key] || this.props.navigate; + return ; + } + } + + return Container; +} + +// Next step. +// Define a component for your application that owns the navigation state. +class YourApplication extends Component { + + static propTypes = { + onExampleExit: PropTypes.func, + }; + + // This sets up the initial navigation state. + constructor(props, context) { + super(props, context); + // This sets up the initial navigation state. + this.state = createAppNavigationState(); + this._navigate = this._navigate.bind(this); + } + + render(): ReactElement { + // User your own navigator (see next step). + return ( + + ); + } + + // This public method is optional. If exists, the UI explorer will call it + // the "back button" is pressed. Normally this is the cases for Android only. + handleBackAction(): boolean { + return this._onNavigate({type: 'pop'}); + } + + // This handles the navigation state changes. You're free and responsible + // to define the API that changes that navigation state. In this exmaple, + // we'd simply use a `updateAppNavigationState` to update the navigation + // state. + _navigate(action: Object): void { + if (action.type === 'exit') { + // Exits the example. `this.props.onExampleExit` is provided + // by the UI Explorer. + this.props.onExampleExit && this.props.onExampleExit(); + return; + } + + const state = updateAppNavigationState( + this.state, + action, + ); + + // `updateAppNavigationState` (which uses NavigationStateUtils) gives you + // back the same `state` if nothing has changed. You could use + // that to avoid redundant re-rendering. + if (this.state !== state) { + this.setState(state); + } + } +} + +// Next step. +// Define your own controlled navigator. +const YourNavigator = createAppNavigationContainer(class extends Component { + static propTypes = { + appNavigationState: PropTypes.shape({ + apple: NavigationPropTypes.navigationState.isRequired, + banana: NavigationPropTypes.navigationState.isRequired, + orange: NavigationPropTypes.navigationState.isRequired, + tabs: NavigationPropTypes.navigationState.isRequired, + }), + navigate: PropTypes.func.isRequired, + }; + + // This sets up the methods (e.g. Pop, Push) for navigation. + constructor(props: any, context: any) { + super(props, context); + this._renderHeader = this._renderHeader.bind(this); + this._renderScene = this._renderScene.bind(this); + } + + // Now use the `NavigationCardStack` to render the scenes. + render(): ReactElement { + const {appNavigationState} = this.props; + const {tabs} = appNavigationState; + const tabKey = tabs.routes[tabs.index].key; + const scenes = appNavigationState[tabKey]; + + return ( + + + + + ); + } + + // Render the header. + // The detailed spec of `sceneProps` is defined at `NavigationTypeDefinition` + // as type `NavigationSceneRendererProps`. + _renderHeader(sceneProps: Object): ReactElement { + return ( + + ); + } + + // Render a scene for route. + // The detailed spec of `sceneProps` is defined at `NavigationTypeDefinition` + // as type `NavigationSceneRendererProps`. + _renderScene(sceneProps: Object): ReactElement { + return ( + + ); + } +}); + +// Next step. +// Define your own header. +const YourHeader = createAppNavigationContainer(class extends Component { + static propTypes = { + ...NavigationPropTypes.SceneRendererProps, + navigate: PropTypes.func.isRequired, + }; + + constructor(props: Object, context: any) { + super(props, context); + this._renderTitleComponent = this._renderTitleComponent.bind(this); + } + + render(): ReactElement { + return ( + + ); + } + + _renderTitleComponent(): ReactElement { + return ( + + {this.props.scene.route.key} + + ); + } +}); + +// Next step. +// Define your own scene. +const YourScene = createAppNavigationContainer(class extends Component { + static propTypes = { + ...NavigationPropTypes.SceneRendererProps, + navigate: PropTypes.func.isRequired, + }; + + constructor(props: Object, context: any) { + super(props, context); + this._exit = this._exit.bind(this); + this._popRoute = this._popRoute.bind(this); + this._pushRoute = this._pushRoute.bind(this); + } + + render(): ReactElement { + return ( + + + + + + ); + } + + _pushRoute(): void { + // Just push a route with a new unique key. + const route = {key: '[' + this.props.scenes.length + ']-' + Date.now()}; + this.props.navigate({type: 'push', route}); + } + + _popRoute(): void { + this.props.navigate({type: 'pop'}); + } + + _exit(): void { + this.props.navigate({type: 'exit'}); + } +}); + +// Next step. +// Define your own tabs. +const YourTabs = createAppNavigationContainer(class extends Component { + static propTypes = { + navigationState: NavigationPropTypes.navigationState.isRequired, + navigate: PropTypes.func.isRequired, + }; + + constructor(props: Object, context: any) { + super(props, context); + } + + render(): ReactElement { + return ( + + {this.props.navigationState.routes.map(this._renderTab, this)} + + ); + } + + _renderTab(route: Object, index: number): ReactElement { + return ( + + ); + } +}); + +// Next step. +// Define your own Tab +const YourTab = createAppNavigationContainer(class extends Component { + + static propTypes = { + navigate: PropTypes.func.isRequired, + route: NavigationPropTypes.navigationRoute.isRequired, + selected: PropTypes.bool.isRequired, + }; + + constructor(props: Object, context: any) { + super(props, context); + this._onPress = this._onPress.bind(this); + } + + render(): ReactElement { + const style = [styles.tabText]; + if (this.props.selected) { + style.push(styles.tabSelected); + } + return ( + + + {this.props.route.key} + + + ); + } + + _onPress() { + this.props.navigate({type: 'selectTab', tabKey: this.props.route.key}); + } +}); + +const styles = StyleSheet.create({ + navigator: { + flex: 1, + }, + navigatorCardStack: { + flex: 20, + }, + scrollView: { + marginTop: 64 + }, + tabs: { + flex: 1, + flexDirection: 'row', + }, + tab: { + alignItems: 'center', + backgroundColor: '#fff', + flex: 1, + justifyContent: 'center', + }, + tabText: { + color: '#222', + fontWeight: '500', + }, + tabSelected: { + color: 'blue', + }, +}); + +module.exports = YourApplication; From bc7ec03670c0b61cf667f67bdeebf6877fa87c5a Mon Sep 17 00:00:00 2001 From: Alexander Blom Date: Mon, 6 Jun 2016 11:49:07 -0700 Subject: [PATCH 238/843] Add better markers for getConfig Reviewed By: astreet Differential Revision: D3358278 fbshipit-source-id: 16668cd33a42a120c2de88be4cb4a1c8d0d5a13c --- ReactCommon/cxxreact/ModuleRegistry.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ReactCommon/cxxreact/ModuleRegistry.cpp b/ReactCommon/cxxreact/ModuleRegistry.cpp index adfdf1dbce5eb4..a7c09bca368ff9 100644 --- a/ReactCommon/cxxreact/ModuleRegistry.cpp +++ b/ReactCommon/cxxreact/ModuleRegistry.cpp @@ -39,6 +39,7 @@ std::vector ModuleRegistry::moduleNames() { } folly::dynamic ModuleRegistry::getConfig(const std::string& name) { + SystraceSection s("getConfig", "module", name); auto it = modulesByName_.find(name); if (it == modulesByName_.end()) { return nullptr; @@ -51,8 +52,7 @@ folly::dynamic ModuleRegistry::getConfig(const std::string& name) { folly::dynamic config = folly::dynamic::array(name); { - SystraceSection s("getConfig constants", - "module", name); + SystraceSection s("getConstants"); folly::dynamic constants = module->getConstants(); if (constants.isObject() && constants.size() > 0) { config.push_back(std::move(constants)); @@ -60,8 +60,7 @@ folly::dynamic ModuleRegistry::getConfig(const std::string& name) { } { - SystraceSection s("getConfig methods", - "module", name); + SystraceSection s("getMethods"); std::vector methods = module->getMethods(); folly::dynamic methodNames = folly::dynamic::array; From 1fce89176b2b87ea1914f94cf036b06a3176c4c1 Mon Sep 17 00:00:00 2001 From: Ben Nham Date: Mon, 6 Jun 2016 12:54:37 -0700 Subject: [PATCH 239/843] Explicitly set default background color in RCTTextView Summary: RCTShadowText currently explicitly defaults to black text color: https://github.com/facebook/react-native/blob/d46ac11/Libraries/Text/RCTShadowText.m#L204 However, the UITextView used by RCTTextInput doesn't explicitly default to black text color. This causes issues when RCTTextInput is in rich text editing mode (i.e. when the element uses child nodes to provide extra styling info) and the client doesn't provide us with any explicit color info. In this case, the following series of badness occurs: 1. -[UITextView attributedText] will return an attributed string without the NSColor property set. 2. -[RCTShadowText attributedString] will return an attributed string with NSColor equal to blackColor. 3. The `[_textView.attributedText isEqualToAttributedString:_pendingAttributedText]` check in -performPendingTextUpdate will fail and causes -[UITextView setText:] to be called. 4. -setText: clears the marked text range in the text view, which breaks multi-character autocomplete (e.g. QWERTY input methods for CJK languages). The easiest fix for now is to just make sure RCTUITextView and RCTShadowText default to the same text color. Reviewed By: nicklockwood Differential Revision: D3368726 fbshipit-source-id: a92cb188c60bac80d963af6b1f2a09f27ae084f5 --- Libraries/Text/RCTTextView.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index 2d4c0a7d905f1a..9b0a4d737cd4eb 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -86,6 +86,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher _textView = [[RCTUITextView alloc] initWithFrame:CGRectZero]; _textView.backgroundColor = [UIColor clearColor]; + _textView.textColor = [UIColor blackColor]; _textView.scrollsToTop = NO; _textView.scrollEnabled = NO; _textView.delegate = self; From 8d038572f3c42033cfcc7a7901eea0ff75bbc32a Mon Sep 17 00:00:00 2001 From: Sokovikov Date: Mon, 6 Jun 2016 12:55:07 -0700 Subject: [PATCH 240/843] show stacktrace in yellowbox Summary: instead of #7030 Closes https://github.com/facebook/react-native/pull/7459 Differential Revision: D3282147 Pulled By: frantic fbshipit-source-id: cd79b729685047c303945e83d0672ca1671e2cca --- Libraries/ReactIOS/YellowBox.js | 96 +++++++++++++++++++++++++++------ 1 file changed, 81 insertions(+), 15 deletions(-) diff --git a/Libraries/ReactIOS/YellowBox.js b/Libraries/ReactIOS/YellowBox.js index d334eab3661985..dbc15dc9e94d34 100644 --- a/Libraries/ReactIOS/YellowBox.js +++ b/Libraries/ReactIOS/YellowBox.js @@ -13,13 +13,22 @@ 'use strict'; const EventEmitter = require('EventEmitter'); -import type EmitterSubscription from 'EmitterSubscription'; const Platform = require('Platform'); const React = require('React'); const StyleSheet = require('StyleSheet'); +const symbolicateStackTrace = require('symbolicateStackTrace'); +const parseErrorStack = require('parseErrorStack'); + +import type EmitterSubscription from 'EmitterSubscription'; +import type {StackFrame} from 'parseErrorStack'; + +type WarningInfo = { + count: number; + stacktrace: Array; +}; const _warningEmitter = new EventEmitter(); -const _warningMap = new Map(); +const _warningMap: Map = new Map(); /** * YellowBox renders warnings at the bottom of the app being developed. @@ -82,9 +91,28 @@ function updateWarningMap(format, ...args): void { ...args.slice(argCount).map(stringifySafe), ].join(' '); - const count = _warningMap.has(warning) ? _warningMap.get(warning) : 0; - _warningMap.set(warning, count + 1); + var warningInfo = _warningMap.get(warning); + if (warningInfo) { + warningInfo.count += 1; + } else { + warningInfo = {count: 1, stacktrace: []}; + } + + _warningMap.set(warning, warningInfo); _warningEmitter.emit('warning', _warningMap); + + var error : any = new Error(); + error.framesToPop = 2; + + symbolicateStackTrace(parseErrorStack(error)).then(stack => { + warningInfo = _warningMap.get(warning); + if (warningInfo) { + warningInfo.stacktrace = stack; + + _warningMap.set(warning, warningInfo); + _warningEmitter.emit('warning', _warningMap); + } + }, () => { /* Do nothing when can't load source map */ }); } function isWarningIgnored(warning: string): boolean { @@ -121,23 +149,45 @@ const WarningRow = ({count, warning, onPress}) => { ); }; +type StackRowProps = { frame: StackFrame }; +const StackRow = ({frame}: StackRowProps) => { + const Text = require('Text'); + const fileParts = frame.file.split('/'); + const fileName = fileParts[fileParts.length - 1]; + return ( + + {`${fileName}:${frame.lineNumber}`} + + ); +}; + const WarningInspector = ({ - count, + warningInfo, warning, + stacktraceVisible, onClose, onDismiss, onDismissAll, + toggleStacktrace, }) => { const ScrollView = require('ScrollView'); const Text = require('Text'); const TouchableHighlight = require('TouchableHighlight'); const View = require('View'); + const {count, stacktrace} = warningInfo || {}; const countSentence = - /* $FlowFixMe(>=0.26.0) - count can be undefined! Look at WarningInspector - * usage! */ 'Warning encountered ' + count + ' time' + (count - 1 ? 's' : '') + '.'; + let stacktraceList; + if (stacktraceVisible && stacktrace) { + stacktraceList = ( + + {stacktrace.map((frame, ii) => )} + + ); + } + return ( {countSentence} + + + {stacktraceVisible ? 'Hide' : 'Show'} stacktrace + + + {stacktraceList} {warning} @@ -178,6 +238,7 @@ const WarningInspector = ({ class YellowBox extends React.Component { state: { + stacktraceVisible: boolean; inspecting: ?string; warningMap: Map; }; @@ -188,6 +249,7 @@ class YellowBox extends React.Component { super(props, context); this.state = { inspecting: null, + stacktraceVisible: false, warningMap: _warningMap, }; this.dismissWarning = warning => { @@ -231,24 +293,26 @@ class YellowBox extends React.Component { const ScrollView = require('ScrollView'); const View = require('View'); - const inspecting = this.state.inspecting; + const {inspecting, stacktraceVisible} = this.state; const inspector = inspecting !== null ? this.setState({inspecting: null})} onDismiss={() => this.dismissWarning(inspecting)} onDismissAll={() => this.dismissWarning(null)} + toggleStacktrace={() => this.setState({stacktraceVisible: !stacktraceVisible})} /> : null; const rows = []; - this.state.warningMap.forEach((count, warning) => { + this.state.warningMap.forEach((warningInfo, warning) => { if (!isWarningIgnored(warning)) { rows.push( this.setState({inspecting: warning})} onDismiss={() => this.dismissWarning(warning)} @@ -304,6 +368,11 @@ var styles = StyleSheet.create({ inspectorButton: { flex: 1, padding: 22, + backgroundColor: backgroundColor(1), + }, + stacktraceButton: { + flex: 1, + padding: 5, }, inspectorButtonText: { color: textColor, @@ -324,10 +393,7 @@ var styles = StyleSheet.create({ fontSize: 14, }, inspectorWarning: { - padding: 15, - position: 'absolute', - top: 39, - bottom: 60, + paddingHorizontal: 15, }, inspectorWarningText: { color: textColor, From 2e7a536aba17c49974d98ce2aeea274047ab24c2 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Mon, 6 Jun 2016 13:20:09 -0700 Subject: [PATCH 241/843] Unbreak Image.ios.js Summary: Don't pass `null` as default empty uri. Use `undefined` instead. Differential Revision: D3393211 fbshipit-source-id: ac7cd385aa7f2196bc0743b8679dd2c94be4108b --- Libraries/Image/Image.ios.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 7afec06e8760df..13eea466027c7e 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -211,7 +211,7 @@ var Image = React.createClass({ }, render: function() { - var source = resolveAssetSource(this.props.source) || { uri: null, width: undefined, height: undefined }; + var source = resolveAssetSource(this.props.source) || { uri: undefined, width: undefined, height: undefined }; var {width, height, uri} = source; var style = flattenStyle([{width, height}, styles.base, this.props.style]) || {}; @@ -225,11 +225,11 @@ var Image = React.createClass({ if (isNetwork && (tintColor || this.props.blurRadius)) { RawImage = RCTImageView; } - + if (uri === '') { console.warn('source.uri should not be an empty string'); } - + if (this.props.src) { console.warn('The component requires a `source` property rather than `src`.'); } From 2fd537c55dddd2a7e9906add5f6281b0444cc9b6 Mon Sep 17 00:00:00 2001 From: Geoff Lawson Date: Mon, 6 Jun 2016 13:47:21 -0700 Subject: [PATCH 242/843] =?UTF-8?q?Incorrect=20import=20reference=20of=20e?= =?UTF-8?q?mptyFunction=20causes=20error=20when=20importi=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Experimental component SwipeableListView fails to import due to incorrect import reference. Followed example use of import of emptyFunction found in https://github.com/facebook/react-native/blob/master/Libraries/Components/TextInput/TextInput.js#L31 Closes https://github.com/facebook/react-native/pull/7954 Differential Revision: D3394202 fbshipit-source-id: db8ddc9d9635878f0ce1fb969ef6c6e3c27d63f3 --- Libraries/Experimental/SwipeableRow/SwipeableRow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Experimental/SwipeableRow/SwipeableRow.js b/Libraries/Experimental/SwipeableRow/SwipeableRow.js index 169642ef39fe6e..8c2e43d0e50e9e 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableRow.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableRow.js @@ -31,7 +31,7 @@ const View = require('View'); const {PropTypes} = React; -const emptyFunction = require('emptyFunction'); +const emptyFunction = require('fbjs/lib/emptyFunction'); // Position of the left of the swipable item when closed const CLOSED_LEFT_POSITION = 0; From 7cd8591734e6dc48a44a7e17ba6752fe9c98cd76 Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Mon, 6 Jun 2016 14:15:48 -0700 Subject: [PATCH 243/843] Fixed Typo: "Termimal" => "Terminal" Summary: Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: (You can skip this if you're fixing a typo or adding an app to the Showcase.) Explain the **motivation** for making this change. What existing problem does the pull request solve? Prefer **small pull requests**. These are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise please split it. **Test plan (required)** Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. Make sure tests pass on both Travis and Circle CI. **Code formatting** Look around. Match the style of the rest of the codebase. See also the simple [style guide](https://github.com/facebook/react-native/blob/master/CONTRIBUTING.md#style-guide). For more info, see the ["Pull Requests" section of our "Contributing" guidelines](https://github.com/facebook/react-native/blob/mas Closes https://github.com/facebook/react-native/pull/7951 Differential Revision: D3394509 Pulled By: JoelMarcey fbshipit-source-id: 0ae23950c69ae06a4b85d2e4b577a71e9aa2b7f4 --- docs/QuickStart-GettingStarted.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/QuickStart-GettingStarted.md b/docs/QuickStart-GettingStarted.md index b63dc4f00bc983..cd30ef889ea67d 100644 --- a/docs/QuickStart-GettingStarted.md +++ b/docs/QuickStart-GettingStarted.md @@ -282,7 +282,7 @@ the Terminal should be something like: #### Python 2 -Fire up the Termimal and use Chocolatey to install Python 2. +Fire up the Terminal and use Chocolatey to install Python 2. > Python 3 will currently not work when initializing a React Native project. From 26a92220c2a6355afc0a54be4a7266da50f7c692 Mon Sep 17 00:00:00 2001 From: Fred Liu Date: Mon, 6 Jun 2016 14:49:23 -0700 Subject: [PATCH 244/843] Allow some right swipe Summary: Allow some right swipe so users know swiping is a possibility, with some bounceback. Reviewed By: fkgozali Differential Revision: D3388836 fbshipit-source-id: 596a6be3c641ff0ee6ac32d7c0d798478edef72b --- .../Experimental/SwipeableRow/SwipeableRow.js | 77 +++++++++++++++++-- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/Libraries/Experimental/SwipeableRow/SwipeableRow.js b/Libraries/Experimental/SwipeableRow/SwipeableRow.js index 8c2e43d0e50e9e..09838bfe63805d 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableRow.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableRow.js @@ -37,6 +37,16 @@ const emptyFunction = require('fbjs/lib/emptyFunction'); const CLOSED_LEFT_POSITION = 0; // Minimum swipe distance before we recognize it as such const HORIZONTAL_SWIPE_DISTANCE_THRESHOLD = 15; +// Distance left of closed position to bounce back when right-swiping from closed +const RIGHT_SWIPE_BOUNCE_BACK_DISTANCE = 30; +// Factor to divide by to get slow speed; i.e. 4 means 1/4 of full speed +const SLOW_SPEED_SWIPE_FACTOR = 4; +/** + * Max distance of right swipe to allow (right swipes do functionally nothing). + * Must be multiplied by SLOW_SPEED_SWIPE_FACTOR because gestureState.dx tracks + * how far the finger swipes, and not the actual animation distance. +*/ +const RIGHT_SWIPE_THRESHOLD = 30 * SLOW_SPEED_SWIPE_FACTOR; // Time, in milliseconds, of how long the animated swipe should be const SWIPE_DURATION = 200; @@ -51,7 +61,7 @@ const SwipeableRow = React.createClass({ propTypes: { isOpen: PropTypes.bool, maxSwipeDistance: PropTypes.number.isRequired, - onOpen: PropTypes.func, + onOpen: PropTypes.func.isRequired, onSwipeEnd: PropTypes.func.isRequired, onSwipeStart: PropTypes.func.isRequired, /** @@ -84,6 +94,7 @@ const SwipeableRow = React.createClass({ return { isOpen: false, maxSwipeDistance: 0, + onOpen: emptyFunction, onSwipeEnd: emptyFunction, onSwipeStart: emptyFunction, swipeThreshold: 30, @@ -174,22 +185,53 @@ const SwipeableRow = React.createClass({ }, _handlePanResponderMove(event: Object, gestureState: Object): void { + if (this._isSwipingExcessivelyRightFromClosedPosition(gestureState)) { + return; + } + this.props.onSwipeStart(); - if (!this._isSwipingRightFromClosedPosition(gestureState)) { - this.state.currentLeft.setValue(this._previousLeft + gestureState.dx); + if (this._isSwipingRightFromClosed(gestureState)) { + this._swipeSlowSpeed(gestureState); + } else { + this._swipeFullSpeed(gestureState); } }, - _isSwipingRightFromClosedPosition(gestureState: Object): boolean { + _isSwipingRightFromClosed(gestureState: Object): boolean { return this._previousLeft === CLOSED_LEFT_POSITION && gestureState.dx > 0; }, - _onPanResponderTerminationRequest(event: Object, gestureState: Object): boolean { + _swipeFullSpeed(gestureState: Object): void { + this.state.currentLeft.setValue(this._previousLeft + gestureState.dx); + }, + + _swipeSlowSpeed(gestureState: Object): void { + this.state.currentLeft.setValue( + this._previousLeft + gestureState.dx / SLOW_SPEED_SWIPE_FACTOR, + ); + }, + + _isSwipingExcessivelyRightFromClosedPosition(gestureState: Object): boolean { + /** + * We want to allow a BIT of right swipe, to allow users to know that + * swiping is available, but swiping right does not do anything + * functionally. + */ + return ( + this._isSwipingRightFromClosed(gestureState) && + gestureState.dx > RIGHT_SWIPE_THRESHOLD + ); + }, + + _onPanResponderTerminationRequest( + event: Object, + gestureState: Object, + ): boolean { return false; }, - _animateTo(toValue: number): void { + _animateTo(toValue: number, callback: Function = emptyFunction): void { Animated.timing( this.state.currentLeft, { @@ -198,6 +240,7 @@ const SwipeableRow = React.createClass({ }, ).start(() => { this._previousLeft = toValue; + callback(); }); }, @@ -209,6 +252,17 @@ const SwipeableRow = React.createClass({ this._animateTo(CLOSED_LEFT_POSITION); }, + _animateRightSwipeBounceBack(): void { + /** + * When swiping right, we want to bounce back past closed position on release + * so users know they should swipe right to get content. + */ + this._animateTo( + -RIGHT_SWIPE_BOUNCE_BACK_DISTANCE, + this._animateToClosedPosition, + ); + }, + // Ignore swipes due to user's finger moving slightly when tapping _isValidSwipe(gestureState: Object): boolean { return Math.abs(gestureState.dx) > HORIZONTAL_SWIPE_DISTANCE_THRESHOLD; @@ -217,16 +271,23 @@ const SwipeableRow = React.createClass({ _handlePanResponderEnd(event: Object, gestureState: Object): void { const horizontalDistance = gestureState.dx; - if (Math.abs(horizontalDistance) > this.props.swipeThreshold) { + if (this._isSwipingRightFromClosed(gestureState)) { + this.props.onOpen(); + this._animateRightSwipeBounceBack(); + } else if (Math.abs(horizontalDistance) > this.props.swipeThreshold) { + // Overswiped + if (horizontalDistance < 0) { // Swiped left - this.props.onOpen && this.props.onOpen(); + this.props.onOpen(); this._animateToOpenPosition(); } else { // Swiped right this._animateToClosedPosition(); } } else { + // Swiping from closed but let go before fully + if (this._previousLeft === CLOSED_LEFT_POSITION) { this._animateToClosedPosition(); } else { From bf010a4c171ec621b9925563e92a3f32cda976a3 Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Mon, 6 Jun 2016 16:01:52 -0700 Subject: [PATCH 245/843] Invert native dependency of new bridge -> old bridge Reviewed By: mhorowitz Differential Revision: D3374937 fbshipit-source-id: 58b082bba727e33a3b08361be3630d669fc8dc5b --- .../react/bridge/JSCJavaScriptExecutor.java | 8 ++++---- .../com/facebook/react/bridge/NativeArray.java | 2 +- .../java/com/facebook/react/bridge/NativeMap.java | 2 +- .../react/bridge/ProxyJavaScriptExecutor.java | 8 ++++---- .../com/facebook/react/bridge/ReactBridge.java | 8 +++++++- .../react/bridge/ReadableNativeArray.java | 3 +-- .../facebook/react/bridge/ReadableNativeMap.java | 3 +-- .../react/bridge/WritableNativeArray.java | 3 +-- .../facebook/react/bridge/WritableNativeMap.java | 2 +- ReactAndroid/src/main/jni/react/jni/Android.mk | 10 ++-------- ReactAndroid/src/main/jni/react/jni/BUCK | 15 +-------------- ReactAndroid/src/main/jni/react/jni/OnLoad.cpp | 14 +++----------- ReactAndroid/src/main/jni/xreact/jni/Android.mk | 12 +++++++++--- ReactAndroid/src/main/jni/xreact/jni/BUCK | 10 ++++++++-- .../main/jni/xreact/jni/CatalystInstanceImpl.cpp | 3 +-- .../src/main/jni/xreact/jni/CxxModuleWrapper.cpp | 11 ++++++----- ReactAndroid/src/main/jni/xreact/jni/JCallback.h | 2 +- .../src/main/jni/xreact/jni/JSCPerfLogging.cpp | 3 ++- .../src/main/jni/xreact/jni/JSLogging.cpp | 3 ++- .../src/main/jni/xreact/jni/MethodInvoker.cpp | 2 +- .../main/jni/xreact/jni/ModuleRegistryHolder.cpp | 5 ++--- .../jni/{react => xreact}/jni/NativeArray.cpp | 0 .../main/jni/{react => xreact}/jni/NativeArray.h | 0 .../jni/{react => xreact}/jni/NativeCommon.cpp | 0 .../main/jni/{react => xreact}/jni/NativeCommon.h | 0 .../main/jni/{react => xreact}/jni/NativeMap.cpp | 0 .../main/jni/{react => xreact}/jni/NativeMap.h | 0 ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp | 12 +++++++++++- .../{react => xreact}/jni/ReadableNativeArray.cpp | 0 .../{react => xreact}/jni/ReadableNativeArray.h | 0 .../{react => xreact}/jni/ReadableNativeMap.cpp | 0 .../jni/{react => xreact}/jni/ReadableNativeMap.h | 0 .../{react => xreact}/jni/WritableNativeArray.cpp | 0 .../{react => xreact}/jni/WritableNativeArray.h | 0 .../{react => xreact}/jni/WritableNativeMap.cpp | 0 .../jni/{react => xreact}/jni/WritableNativeMap.h | 1 + 36 files changed, 71 insertions(+), 71 deletions(-) rename ReactAndroid/src/main/jni/{react => xreact}/jni/NativeArray.cpp (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/NativeArray.h (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/NativeCommon.cpp (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/NativeCommon.h (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/NativeMap.cpp (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/NativeMap.h (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/ReadableNativeArray.cpp (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/ReadableNativeArray.h (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/ReadableNativeMap.cpp (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/ReadableNativeMap.h (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/WritableNativeArray.cpp (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/WritableNativeArray.h (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/WritableNativeMap.cpp (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/WritableNativeMap.h (96%) diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java index b4431c0d66d9cb..4f895abf31aa26 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java @@ -14,6 +14,10 @@ @DoNotStrip public class JSCJavaScriptExecutor extends JavaScriptExecutor { + static { + ReactBridge.staticInit(); + } + public static class Factory implements JavaScriptExecutor.Factory { @Override public JavaScriptExecutor create(WritableNativeMap jscConfig) throws Exception { @@ -21,10 +25,6 @@ public JavaScriptExecutor create(WritableNativeMap jscConfig) throws Exception { } } - static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); - } - public JSCJavaScriptExecutor(WritableNativeMap jscConfig) { initialize(jscConfig); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java index 2045a4b23e0016..d4c46f72302c53 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java @@ -19,7 +19,7 @@ @DoNotStrip public abstract class NativeArray { static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + ReactBridge.staticInit(); } protected NativeArray(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java index 2c683f9451130e..9e192dc4ea6d8d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java @@ -19,7 +19,7 @@ @DoNotStrip public abstract class NativeMap { static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + ReactBridge.staticInit(); } public NativeMap(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java index 37d83b3386174c..6ebf4dc0df6e01 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java @@ -24,6 +24,10 @@ */ @DoNotStrip public class ProxyJavaScriptExecutor extends JavaScriptExecutor { + static { + ReactBridge.staticInit(); + } + public static class Factory implements JavaScriptExecutor.Factory { private final JavaJSExecutor.Factory mJavaJSExecutorFactory; @@ -37,10 +41,6 @@ public JavaScriptExecutor create(WritableNativeMap jscConfig) throws Exception { } } - static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); - } - private @Nullable JavaJSExecutor mJavaJSExecutor; /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java index e4d43f90b6a4f9..6cf4170d463176 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java @@ -24,16 +24,22 @@ @DoNotStrip public class ReactBridge extends Countable { - /* package */ static final String REACT_NATIVE_LIB = "reactnativejni"; + private static final String REACT_NATIVE_LIB = "reactnativejni"; + private static final String XREACT_NATIVE_LIB = "reactnativejnifb"; static { SoLoader.loadLibrary(REACT_NATIVE_LIB); + SoLoader.loadLibrary(XREACT_NATIVE_LIB); } private final ReactCallback mCallback; private final JavaScriptExecutor mJSExecutor; private final MessageQueueThread mNativeModulesQueueThread; + public static void staticInit() { + // This is just called to ensure that ReactBridge's static initialization has taken place. + } + /** * @param jsExecutor the JS executor to use to run JS * @param callback the callback class used to invoke native modules diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java index 4cd1b75814e80b..2f81d301b60bf9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java @@ -21,9 +21,8 @@ */ @DoNotStrip public class ReadableNativeArray extends NativeArray implements ReadableArray { - static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + ReactBridge.staticInit(); } protected ReadableNativeArray(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java index f3782ca0896f75..ea289ac668f2e0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java @@ -22,9 +22,8 @@ */ @DoNotStrip public class ReadableNativeMap extends NativeMap implements ReadableMap { - static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + ReactBridge.staticInit(); } protected ReadableNativeMap(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java index 26fe2dd11f6228..e6c343264a8d2c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java @@ -21,9 +21,8 @@ */ @DoNotStrip public class WritableNativeArray extends ReadableNativeArray implements WritableArray { - static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + ReactBridge.staticInit(); } public WritableNativeArray() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java index d30827ade5bf12..6b6c639d2e032b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java @@ -22,7 +22,7 @@ @DoNotStrip public class WritableNativeMap extends ReadableNativeMap implements WritableMap { static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + ReactBridge.staticInit(); } @Override diff --git a/ReactAndroid/src/main/jni/react/jni/Android.mk b/ReactAndroid/src/main/jni/react/jni/Android.mk index 742e2161d1a0b6..06266de376fc6e 100644 --- a/ReactAndroid/src/main/jni/react/jni/Android.mk +++ b/ReactAndroid/src/main/jni/react/jni/Android.mk @@ -11,15 +11,8 @@ LOCAL_SRC_FILES := \ JSLoader.cpp \ JSLogging.cpp \ JniJSModulesUnbundle.cpp \ - NativeArray.cpp \ - NativeCommon.cpp \ - NativeMap.cpp \ OnLoad.cpp \ ProxyExecutor.cpp \ - ReadableNativeArray.cpp \ - ReadableNativeMap.cpp \ - WritableNativeArray.cpp \ - WritableNativeMap.cpp \ LOCAL_C_INCLUDES := $(LOCAL_PATH) LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/../.. $(LOCAL_PATH)/.. @@ -30,7 +23,7 @@ LOCAL_CFLAGS += $(CXX11_FLAGS) LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) LOCAL_LDLIBS += -landroid -LOCAL_SHARED_LIBRARIES := libfolly_json libfb libjsc libglog_init +LOCAL_SHARED_LIBRARIES := libfolly_json libfb libjsc libglog_init libreactnativejnifb LOCAL_STATIC_LIBRARIES := libreactnative include $(BUILD_SHARED_LIBRARY) @@ -41,3 +34,4 @@ $(call import-module,folly) $(call import-module,fbgloginit) $(call import-module,fb) $(call import-module,jsc) +$(call import-module,xreact/jni) diff --git a/ReactAndroid/src/main/jni/react/jni/BUCK b/ReactAndroid/src/main/jni/react/jni/BUCK index 9bd754fb70e92f..e210b9dd9575eb 100644 --- a/ReactAndroid/src/main/jni/react/jni/BUCK +++ b/ReactAndroid/src/main/jni/react/jni/BUCK @@ -18,6 +18,7 @@ def jni_library(**kwargs): ], deps = DEPS + JSC_DEPS + [ react_native_target('jni/react:react'), + react_native_target('jni/xreact/jni:jni'), ], **kwargs ) @@ -33,15 +34,8 @@ jni_library( 'JSLoader.cpp', 'JSLogging.cpp', 'JniJSModulesUnbundle.cpp', - 'NativeArray.cpp', - 'NativeCommon.cpp', - 'NativeMap.cpp', 'OnLoad.cpp', 'ProxyExecutor.cpp', - 'ReadableNativeArray.cpp', - 'ReadableNativeMap.cpp', - 'WritableNativeArray.cpp', - 'WritableNativeMap.cpp', ], headers = [ 'JSLoader.h', @@ -56,13 +50,6 @@ jni_library( 'WebWorkers.h', ], exported_headers = [ - 'NativeCommon.h', - 'NativeArray.h', - 'NativeMap.h', - 'ReadableNativeArray.h', - 'ReadableNativeMap.h', - 'WritableNativeArray.h', - 'WritableNativeMap.h', ], preprocessor_flags = [ '-DLOG_TAG="ReactNativeJNI"', diff --git a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp index 8d60719c4e9975..63fee95574aaca 100644 --- a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp +++ b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp @@ -22,8 +22,6 @@ #include "JExecutorTokenFactory.h" #include "JNativeRunnable.h" #include "JSLoader.h" -#include "NativeCommon.h" -#include "ReadableNativeArray.h" #include "ProxyExecutor.h" #include "OnLoad.h" #include "JMessageQueueThread.h" @@ -31,7 +29,9 @@ #include "JSLogging.h" #include "JSCPerfLogging.h" #include "WebWorkers.h" -#include "WritableNativeMap.h" + +#include +#include #include #ifdef WITH_FBSYSTRACE @@ -456,17 +456,9 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { PerfLogging::installNativeHooks = addNativePerfLoggingHooks; JSLogging::nativeHook = nativeLoggingHook; - NativeArray::registerNatives(); - ReadableNativeArray::registerNatives(); - WritableNativeArray::registerNatives(); JNativeRunnable::registerNatives(); registerJSLoaderNatives(); - NativeMap::registerNatives(); - ReadableNativeMap::registerNatives(); - WritableNativeMap::registerNatives(); - ReadableNativeMapKeySetIterator::registerNatives(); - registerNatives("com/facebook/react/bridge/JSCJavaScriptExecutor", { makeNativeMethod("initialize", executors::createJSCExecutor), }); diff --git a/ReactAndroid/src/main/jni/xreact/jni/Android.mk b/ReactAndroid/src/main/jni/xreact/jni/Android.mk index c0078c36a8a157..4eb04b448854f4 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/Android.mk +++ b/ReactAndroid/src/main/jni/xreact/jni/Android.mk @@ -9,14 +9,21 @@ LOCAL_SRC_FILES := \ CxxModuleWrapper.cpp \ JExecutorToken.cpp \ JMessageQueueThread.cpp \ - JniJSModulesUnbundle.cpp \ JSCPerfLogging.cpp \ JSLoader.cpp \ JSLogging.cpp \ + JniJSModulesUnbundle.cpp \ MethodInvoker.cpp \ ModuleRegistryHolder.cpp \ + NativeArray.cpp \ + NativeCommon.cpp \ + NativeMap.cpp \ OnLoad.cpp \ ProxyExecutor.cpp \ + ReadableNativeArray.cpp \ + ReadableNativeMap.cpp \ + WritableNativeArray.cpp \ + WritableNativeMap.cpp \ LOCAL_C_INCLUDES := $(LOCAL_PATH) LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/../.. @@ -27,7 +34,7 @@ LOCAL_CFLAGS += $(CXX11_FLAGS) LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) LOCAL_LDLIBS += -landroid -LOCAL_SHARED_LIBRARIES := libfolly_json libfbjni libjsc libglog_init libreactnativejni +LOCAL_SHARED_LIBRARIES := libfolly_json libfbjni libjsc libglog_init LOCAL_STATIC_LIBRARIES := libreactnativefb include $(BUILD_SHARED_LIBRARY) @@ -37,4 +44,3 @@ $(call import-module,jsc) $(call import-module,folly) $(call import-module,fbgloginit) $(call import-module,jsc) -$(call import-module,react/jni) diff --git a/ReactAndroid/src/main/jni/xreact/jni/BUCK b/ReactAndroid/src/main/jni/xreact/jni/BUCK index ea88102d01c483..cb47fa436a1b1c 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/BUCK +++ b/ReactAndroid/src/main/jni/xreact/jni/BUCK @@ -5,19 +5,25 @@ SUPPORTED_PLATFORMS = '^android-(armv7|x86)$' EXPORTED_HEADERS = [ 'CxxModuleWrapper.h', + 'NativeArray.h', + 'NativeCommon.h', + 'NativeMap.h', + 'ReadableNativeArray.h', + 'ReadableNativeMap.h', + 'WritableNativeArray.h', + 'WritableNativeMap.h', ] cxx_library( name='jni', soname = 'libreactnativejnifb.so', - header_namespace = 'react/jni', + header_namespace = 'xreact/jni', supported_platforms_regex = SUPPORTED_PLATFORMS, deps = JSC_DEPS + [ '//native/fb:fb', '//native/third-party/android-ndk:android', '//xplat/folly:molly', '//xplat/fbsystrace:fbsystrace', - react_native_target('jni/react/jni:jni'), react_native_xplat_target('cxxreact:bridge'), react_native_xplat_target('cxxreact:module'), ], diff --git a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp index c4c6faa74b2899..64fa80f98100f2 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp @@ -13,8 +13,6 @@ #include #include -#include - #include #include #include @@ -23,6 +21,7 @@ #include "JavaScriptExecutorHolder.h" #include "JniJSModulesUnbundle.h" #include "ModuleRegistryHolder.h" +#include "NativeArray.h" #include "JNativeRunnable.h" using namespace facebook::jni; diff --git a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp index d1532fd10dd2b9..9267d876e963c5 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp @@ -2,16 +2,11 @@ #include "CxxModuleWrapper.h" -#include - #include #include #include #include -#include -#include - #include #include @@ -20,6 +15,12 @@ #include #include +#include +#include + +#include "ReadableNativeArray.h" + + using namespace facebook::jni; using namespace facebook::xplat::module; using namespace facebook::react; diff --git a/ReactAndroid/src/main/jni/xreact/jni/JCallback.h b/ReactAndroid/src/main/jni/xreact/jni/JCallback.h index 539273104dced2..1d1393eb95bca9 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/JCallback.h +++ b/ReactAndroid/src/main/jni/xreact/jni/JCallback.h @@ -7,7 +7,7 @@ #include #include -#include +#include "NativeArray.h" namespace facebook { namespace react { diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp index 1b7b5e6887b6fa..1dd837db87bf61 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp @@ -2,9 +2,10 @@ #include "JSCPerfLogging.h" +#include + #include #include -#include using namespace facebook::jni; diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp index 7690a5656ebd8d..a27d3f5d35e732 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp @@ -4,9 +4,10 @@ #include #include -#include #include +#include + namespace facebook { namespace react { diff --git a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp index 218f6c9d0ac2d3..e7d3d2c2d0718b 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp @@ -2,7 +2,6 @@ #include "MethodInvoker.h" -#include #ifdef WITH_FBSYSTRACE #include #endif @@ -10,6 +9,7 @@ #include "ModuleRegistryHolder.h" #include "JCallback.h" #include "JExecutorToken.h" +#include "ReadableNativeArray.h" namespace facebook { namespace react { diff --git a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp index 29349f741af1dc..eebed41df0c624 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp @@ -10,11 +10,10 @@ #include #include #include -#include - -#include "MethodInvoker.h" #include "CatalystInstanceImpl.h" +#include "MethodInvoker.h" +#include "ReadableNativeArray.h" using facebook::xplat::module::CxxModule; diff --git a/ReactAndroid/src/main/jni/react/jni/NativeArray.cpp b/ReactAndroid/src/main/jni/xreact/jni/NativeArray.cpp similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/NativeArray.cpp rename to ReactAndroid/src/main/jni/xreact/jni/NativeArray.cpp diff --git a/ReactAndroid/src/main/jni/react/jni/NativeArray.h b/ReactAndroid/src/main/jni/xreact/jni/NativeArray.h similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/NativeArray.h rename to ReactAndroid/src/main/jni/xreact/jni/NativeArray.h diff --git a/ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp b/ReactAndroid/src/main/jni/xreact/jni/NativeCommon.cpp similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp rename to ReactAndroid/src/main/jni/xreact/jni/NativeCommon.cpp diff --git a/ReactAndroid/src/main/jni/react/jni/NativeCommon.h b/ReactAndroid/src/main/jni/xreact/jni/NativeCommon.h similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/NativeCommon.h rename to ReactAndroid/src/main/jni/xreact/jni/NativeCommon.h diff --git a/ReactAndroid/src/main/jni/react/jni/NativeMap.cpp b/ReactAndroid/src/main/jni/xreact/jni/NativeMap.cpp similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/NativeMap.cpp rename to ReactAndroid/src/main/jni/xreact/jni/NativeMap.cpp diff --git a/ReactAndroid/src/main/jni/react/jni/NativeMap.h b/ReactAndroid/src/main/jni/xreact/jni/NativeMap.h similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/NativeMap.h rename to ReactAndroid/src/main/jni/xreact/jni/NativeMap.h diff --git a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp index 3242c59ca2bd63..952a6b46c4c4ff 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include "CatalystInstanceImpl.h" #include "JavaScriptExecutorHolder.h" #include "JSCPerfLogging.h" @@ -17,6 +16,9 @@ #include "WebWorkers.h" #include "JCallback.h" +#include "WritableNativeMap.h" +#include "WritableNativeArray.h" + #include using namespace facebook::jni; @@ -180,6 +182,14 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { CxxModuleWrapper::registerNatives(); JCallbackImpl::registerNatives(); registerJSLoaderNatives(); + + NativeArray::registerNatives(); + ReadableNativeArray::registerNatives(); + WritableNativeArray::registerNatives(); + NativeMap::registerNatives(); + ReadableNativeMap::registerNatives(); + WritableNativeMap::registerNatives(); + ReadableNativeMapKeySetIterator::registerNatives(); }); } diff --git a/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp b/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.cpp similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp rename to ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.cpp diff --git a/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.h b/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.h similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.h rename to ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.h diff --git a/ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.cpp b/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.cpp similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.cpp rename to ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.cpp diff --git a/ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.h b/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.h similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.h rename to ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.h diff --git a/ReactAndroid/src/main/jni/react/jni/WritableNativeArray.cpp b/ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.cpp similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/WritableNativeArray.cpp rename to ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.cpp diff --git a/ReactAndroid/src/main/jni/react/jni/WritableNativeArray.h b/ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.h similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/WritableNativeArray.h rename to ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.h diff --git a/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.cpp b/ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.cpp similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/WritableNativeMap.cpp rename to ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.cpp diff --git a/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h b/ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.h similarity index 96% rename from ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h rename to ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.h index cf9cd95a000d88..1fc942a025e5e1 100644 --- a/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h +++ b/ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.h @@ -20,6 +20,7 @@ struct WritableNativeMap : jni::HybridClass initHybrid(jni::alias_ref); + __attribute__((visibility("default"))) folly::dynamic consume(); void putNull(std::string key); From e79f5d7e7a129f30a8817c0676c98990fbe01686 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Mon, 6 Jun 2016 16:08:20 -0700 Subject: [PATCH 246/843] add StyleSheet.absoluteFill convenience constant Summary: It's annoying and inefficient to create styles like ``` wrapper: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, ``` all the time, so this makes a handy constant for reuse and a helper method to create customized styles. Reviewed By: devknoll Differential Revision: D3389612 fbshipit-source-id: 88fbe9e8ca32a0bc937bf275cf5ae0739ee21302 --- .../ReactIOS/renderApplication.android.js | 54 +++++++++---------- Libraries/StyleSheet/StyleSheet.js | 30 +++++++++++ 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/Libraries/ReactIOS/renderApplication.android.js b/Libraries/ReactIOS/renderApplication.android.js index 994eb52cfefb49..5b5ec232e44284 100644 --- a/Libraries/ReactIOS/renderApplication.android.js +++ b/Libraries/ReactIOS/renderApplication.android.js @@ -11,22 +11,22 @@ 'use strict'; -var Inspector = require('Inspector'); -var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); -var React = require('React'); -var ReactNative = require('ReactNative'); -var StyleSheet = require('StyleSheet'); -var Subscribable = require('Subscribable'); -var View = require('View'); +const Inspector = require('Inspector'); +const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +const React = require('React'); +const ReactNative = require('ReactNative'); +const StyleSheet = require('StyleSheet'); +const Subscribable = require('Subscribable'); +const View = require('View'); -var invariant = require('fbjs/lib/invariant'); +const invariant = require('fbjs/lib/invariant'); -var YellowBox = __DEV__ ? require('YellowBox') : null; +const YellowBox = __DEV__ ? require('YellowBox') : null; // require BackAndroid so it sets the default handler that exits the app if no listeners respond require('BackAndroid'); -var AppContainer = React.createClass({ +const AppContainer = React.createClass({ mixins: [Subscribable.Mixin], getInitialState: function() { @@ -41,7 +41,7 @@ var AppContainer = React.createClass({ toggleElementInspector: function() { this.setState({ inspectorVisible: !this.state.inspectorVisible, - rootNodeHandle: ReactNative.findNodeHandle(this.refs.main), + rootNodeHandle: ReactNative.findNodeHandle(this._mainRef), }); }, @@ -63,7 +63,7 @@ var AppContainer = React.createClass({ onRequestRerenderApp={(updateInspectedViewTag) => { this.setState( (s) => ({mainKey: s.mainKey + 1}), - () => updateInspectedViewTag(ReactNative.findNodeHandle(this.refs.main)) + () => updateInspectedViewTag(ReactNative.findNodeHandle(this._mainRef)) ); }} /> : @@ -74,21 +74,26 @@ var AppContainer = React.createClass({ this._unmounted = true; }, + _setMainRef: function(ref) { + this._mainRef = ref; + }, + render: function() { - var RootComponent = this.props.rootComponent; - var appView = + const RootComponent = this.props.rootComponent; + const appView = + style={StyleSheet.absoluteFill}> + importantForAccessibility={this.state.rootImportanceForAccessibility} + /> ; return __DEV__ ? - + {appView} {this.renderInspector()} @@ -110,19 +115,10 @@ function renderApplication( , + rootTag={rootTag} + />, rootTag ); } -var styles = StyleSheet.create({ - appContainer: { - position: 'absolute', - left: 0, - top: 0, - right: 0, - bottom: 0, - }, -}); - module.exports = renderApplication; diff --git a/Libraries/StyleSheet/StyleSheet.js b/Libraries/StyleSheet/StyleSheet.js index 1fc7376abf4ea1..3c12f5f05cb7de 100644 --- a/Libraries/StyleSheet/StyleSheet.js +++ b/Libraries/StyleSheet/StyleSheet.js @@ -22,6 +22,15 @@ if (hairlineWidth === 0) { hairlineWidth = 1 / PixelRatio.get(); } +const absoluteFillObject = { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, +}; +const absoluteFill = ReactNativePropRegistry.register(absoluteFillObject); // This also freezes it + /** * A StyleSheet is an abstraction similar to CSS StyleSheets * @@ -86,6 +95,27 @@ module.exports = { */ hairlineWidth, + /** + * A very common pattern is to create overlays with position absolute and zero positioning, + * so `absoluteFill` can be used for convenience and to reduce duplication of these repeated + * styles. + */ + absoluteFill, + + /** + * Sometimes you may want `absoluteFill` but with a couple tweaks - `absoluteFillObject` can be + * used to create a customized entry in a `StyleSheet`, e.g.: + * + * const styles = StyleSheet.create({ + * wrapper: { + * ...StyleSheet.absoluteFillObject, + * top: 10, + * backgroundColor: 'transparent', + * }, + * }); + */ + absoluteFillObject, + /** * Flattens an array of style objects, into one aggregated style object. * Alternatively, this method can be used to lookup IDs, returned by From 93c7a93de98d84e796c5b87d7b8f6c35f7e8003b Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 6 Jun 2016 16:23:32 -0700 Subject: [PATCH 247/843] Reverted commit D3392214 Reviewed By: javache Differential Revision: D3392214 fbshipit-source-id: 4136c8b0eb160f4b245df2e4b5d67d00efd7b1a7 --- .../UIExplorerUnitTests/RCTUIManagerTests.m | 10 +- Libraries/Text/RCTText.m | 17 ++- Libraries/Text/RCTTextField.m | 26 ++++ Libraries/Text/RCTTextView.m | 22 ++- React/Base/RCTRootView.m | 2 +- React/Modules/RCTUIManager.m | 14 +- React/Views/RCTComponent.h | 5 - React/Views/RCTMap.m | 16 ++- React/Views/RCTModalHostView.m | 15 +- React/Views/RCTNavigator.m | 33 ++--- React/Views/RCTScrollView.m | 12 +- React/Views/RCTShadowView.m | 5 - React/Views/RCTTabBar.m | 29 ++-- React/Views/RCTView.m | 129 +++++++++++++----- React/Views/UIView+React.h | 15 +- React/Views/UIView+React.m | 30 ++-- 16 files changed, 233 insertions(+), 147 deletions(-) diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m index c3bd09ff1313ef..4ae4b9f5ca8027 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m @@ -77,8 +77,8 @@ - (void)testManagingChildrenToAddViews @"Expect to have 5 react subviews after calling manage children \ with 5 tags to add, instead have %lu", (unsigned long)[[containerView reactSubviews] count]); for (UIView *view in addedViews) { - XCTAssertTrue([view reactSuperview] == containerView, - @"Expected to have manage children successfully add children"); + XCTAssertTrue([view superview] == containerView, + @"Expected to have manage children successfully add children"); [view removeFromSuperview]; } } @@ -95,7 +95,7 @@ - (void)testManagingChildrenToRemoveViews } for (NSInteger i = 2; i < 20; i++) { UIView *view = _uiManager.viewRegistry[@(i)]; - [containerView insertReactSubview:view atIndex:containerView.reactSubviews.count]; + [containerView addSubview:view]; } // Remove views 1-5 from view 20 @@ -112,7 +112,7 @@ - (void)testManagingChildrenToRemoveViews with 5 tags to remove and 18 prior children, instead have %zd", containerView.reactSubviews.count); for (UIView *view in removedViews) { - XCTAssertTrue([view reactSuperview] == nil, + XCTAssertTrue([view superview] == nil, @"Expected to have manage children successfully remove children"); // After removing views are unregistered - we need to reregister _uiManager.viewRegistry[view.reactTag] = view; @@ -155,7 +155,7 @@ - (void)testManagingChildrenToAddRemoveAndMove for (NSInteger i = 1; i < 11; i++) { UIView *view = _uiManager.viewRegistry[@(i)]; - [containerView insertReactSubview:view atIndex:containerView.reactSubviews.count]; + [containerView addSubview:view]; } [_uiManager _manageChildren:@20 diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index fc5f60bdcea0d2..864fa096b42dc0 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -27,6 +27,7 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc @implementation RCTText { NSTextStorage *_textStorage; + NSMutableArray *_reactSubviews; CAShapeLayer *_highlightLayer; } @@ -34,6 +35,7 @@ - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { _textStorage = [NSTextStorage new]; + _reactSubviews = [NSMutableArray array]; self.isAccessibilityElement = YES; self.accessibilityTraits |= UIAccessibilityTraitStaticText; @@ -66,9 +68,19 @@ - (void)reactSetInheritedBackgroundColor:(UIColor *)inheritedBackgroundColor self.backgroundColor = inheritedBackgroundColor; } -- (void)reactUpdateSubviews +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { - // Do nothing, as subviews are managed by `setTextStorage:` method + [_reactSubviews insertObject:subview atIndex:atIndex]; +} + +- (void)removeReactSubview:(UIView *)subview +{ + [_reactSubviews removeObject:subview]; +} + +- (NSArray *)reactSubviews +{ + return _reactSubviews; } - (void)setTextStorage:(NSTextStorage *)textStorage @@ -76,7 +88,6 @@ - (void)setTextStorage:(NSTextStorage *)textStorage if (_textStorage != textStorage) { _textStorage = textStorage; - // Update subviews NSMutableArray *nonTextDescendants = [NSMutableArray new]; collectNonTextDescendants(self, nonTextDescendants); NSArray *subviews = self.subviews; diff --git a/Libraries/Text/RCTTextField.m b/Libraries/Text/RCTTextField.m index 71859ad4d77443..24b077baff106b 100644 --- a/Libraries/Text/RCTTextField.m +++ b/Libraries/Text/RCTTextField.m @@ -17,6 +17,7 @@ @implementation RCTTextField { RCTEventDispatcher *_eventDispatcher; + NSMutableArray *_reactSubviews; BOOL _jsRequestingFirstResponder; NSInteger _nativeEventCount; BOOL _submitted; @@ -34,6 +35,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher [self addTarget:self action:@selector(textFieldEndEditing) forControlEvents:UIControlEventEditingDidEnd]; [self addTarget:self action:@selector(textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit]; [self addObserver:self forKeyPath:@"selectedTextRange" options:0 context:nil]; + _reactSubviews = [NSMutableArray new]; _blurOnSubmit = YES; } return self; @@ -110,6 +112,30 @@ - (void)setPlaceholder:(NSString *)placeholder RCTUpdatePlaceholder(self); } +- (NSArray *)reactSubviews +{ + // TODO: do we support subviews of textfield in React? + // In any case, we should have a better approach than manually + // maintaining array in each view subclass like this + return _reactSubviews; +} + +- (void)removeReactSubview:(UIView *)subview +{ + // TODO: this is a bit broken - if the TextField inserts any of + // its own views below or between React's, the indices won't match + [_reactSubviews removeObject:subview]; + [subview removeFromSuperview]; +} + +- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex +{ + // TODO: this is a bit broken - if the TextField inserts any of + // its own views below or between React's, the indices won't match + [_reactSubviews insertObject:view atIndex:atIndex]; + [super insertSubview:view atIndex:atIndex]; +} + - (CGRect)caretRectForPosition:(UITextPosition *)position { if (_caretHidden) { diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index 9b0a4d737cd4eb..627fd7ae47c7e9 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -67,6 +67,7 @@ @implementation RCTTextView NSInteger _nativeEventCount; RCTText *_richTextView; NSAttributedString *_pendingAttributedText; + NSMutableArray *_subviews; BOOL _blockTextShouldChange; UITextRange *_previousSelectionRange; NSUInteger _previousTextLength; @@ -97,6 +98,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher _previousSelectionRange = _textView.selectedTextRange; + _subviews = [NSMutableArray new]; [self addSubview:_scrollView]; } return self; @@ -105,14 +107,19 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) +- (NSArray *)reactSubviews +{ + return _subviews; +} + - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index { - [super insertReactSubview:subview atIndex:index]; if ([subview isKindOfClass:[RCTText class]]) { if (_richTextView) { RCTLogError(@"Tried to insert a second into - there can only be one."); } _richTextView = (RCTText *)subview; + [_subviews insertObject:_richTextView atIndex:index]; // If this is in rich text editing mode, and the child node providing rich text // styling has a backgroundColor, then the attributedText produced by the child node will have an @@ -125,22 +132,23 @@ - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index attrs[NSBackgroundColorAttributeName] = subview.backgroundColor; _textView.typingAttributes = attrs; } + } else { + [_subviews insertObject:subview atIndex:index]; + [self insertSubview:subview atIndex:index]; } } - (void)removeReactSubview:(UIView *)subview { - [super removeReactSubview:subview]; if (_richTextView == subview) { + [_subviews removeObject:_richTextView]; _richTextView = nil; + } else { + [_subviews removeObject:subview]; + [subview removeFromSuperview]; } } -- (void)reactUpdateSubviews -{ - // Do nothing, as we don't allow non-text subviews -} - - (void)setMostRecentEventCount:(NSInteger)mostRecentEventCount { _mostRecentEventCount = mostRecentEventCount; diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index 78896f5fe482e1..cff7861a1b9833 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -337,7 +337,7 @@ - (instancetype)initWithFrame:(CGRect)frame RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder:(nonnull NSCoder *)aDecoder) -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +- (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex { [super insertReactSubview:subview atIndex:atIndex]; RCTPerformanceLoggerEnd(RCTPLTTI); diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 4ab30f9918b723..168e115dc82f3d 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -893,18 +893,16 @@ static void RCTSetChildren(NSNumber *containerTag, [container insertReactSubview:view atIndex:index++]; } } - - [container didUpdateReactSubviews]; } -RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag +RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerReactTag moveFromIndices:(NSArray *)moveFromIndices moveToIndices:(NSArray *)moveToIndices addChildReactTags:(NSArray *)addChildReactTags addAtIndices:(NSArray *)addAtIndices removeAtIndices:(NSArray *)removeAtIndices) { - [self _manageChildren:containerTag + [self _manageChildren:containerReactTag moveFromIndices:moveFromIndices moveToIndices:moveToIndices addChildReactTags:addChildReactTags @@ -913,7 +911,7 @@ static void RCTSetChildren(NSNumber *containerTag, registry:(NSMutableDictionary> *)_shadowViewRegistry]; [self addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry){ - [uiManager _manageChildren:containerTag + [uiManager _manageChildren:containerReactTag moveFromIndices:moveFromIndices moveToIndices:moveToIndices addChildReactTags:addChildReactTags @@ -923,7 +921,7 @@ static void RCTSetChildren(NSNumber *containerTag, }]; } -- (void)_manageChildren:(NSNumber *)containerTag +- (void)_manageChildren:(NSNumber *)containerReactTag moveFromIndices:(NSArray *)moveFromIndices moveToIndices:(NSArray *)moveToIndices addChildReactTags:(NSArray *)addChildReactTags @@ -931,7 +929,7 @@ - (void)_manageChildren:(NSNumber *)containerTag removeAtIndices:(NSArray *)removeAtIndices registry:(NSMutableDictionary> *)registry { - id container = registry[containerTag]; + id container = registry[containerReactTag]; RCTAssert(moveFromIndices.count == moveToIndices.count, @"moveFromIndices had size %tu, moveToIndices had size %tu", moveFromIndices.count, moveToIndices.count); RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one React child to add"); @@ -965,8 +963,6 @@ - (void)_manageChildren:(NSNumber *)containerTag [container insertReactSubview:destinationsToChildrenToAdd[reactIndex] atIndex:reactIndex.integerValue]; } - - [container didUpdateReactSubviews]; } RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag diff --git a/React/Views/RCTComponent.h b/React/Views/RCTComponent.h index 5c9beab09ba96c..5236f850b7993e 100644 --- a/React/Views/RCTComponent.h +++ b/React/Views/RCTComponent.h @@ -43,11 +43,6 @@ typedef void (^RCTBubblingEventBlock)(NSDictionary *body); */ - (void)didSetProps:(NSArray *)changedProps; -/** - * Called each time subviews have been updated - */ -- (void)didUpdateReactSubviews; - // TODO: Deprecate this // This method is called after layout has been performed for all views known // to the RCTViewManager. It is only called on UIViews, not shadow views. diff --git a/React/Views/RCTMap.m b/React/Views/RCTMap.m index 7a1334668761ca..1b3472530155d0 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -23,6 +23,7 @@ @implementation RCTMap { UIView *_legalLabel; CLLocationManager *_locationManager; + NSMutableArray *_reactSubviews; } - (instancetype)init @@ -30,6 +31,7 @@ - (instancetype)init if ((self = [super init])) { _hasStartedRendering = NO; + _reactSubviews = [NSMutableArray new]; // Find Apple link label for (UIView *subview in self.subviews) { @@ -49,9 +51,19 @@ - (void)dealloc [_regionChangeObserveTimer invalidate]; } -- (void)reactUpdateSubviews +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { - // Do nothing, as annotation views are managed by `setAnnotations:` method + [_reactSubviews insertObject:subview atIndex:atIndex]; +} + +- (void)removeReactSubview:(UIView *)subview +{ + [_reactSubviews removeObject:subview]; +} + +- (NSArray *)reactSubviews +{ + return _reactSubviews; } - (void)layoutSubviews diff --git a/React/Views/RCTModalHostView.m b/React/Views/RCTModalHostView.m index 3dce5ed759910b..c3140249305873 100644 --- a/React/Views/RCTModalHostView.m +++ b/React/Views/RCTModalHostView.m @@ -55,10 +55,14 @@ - (void)notifyForBoundsChange:(CGRect)newBounds } } -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +- (NSArray *)reactSubviews +{ + return _reactSubview ? @[_reactSubview] : @[]; +} + +- (void)insertReactSubview:(UIView *)subview atIndex:(__unused NSInteger)atIndex { RCTAssert(_reactSubview == nil, @"Modal view can only have one subview"); - [super insertReactSubview:subview atIndex:atIndex]; [subview addGestureRecognizer:_touchHandler]; subview.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; @@ -70,16 +74,11 @@ - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex - (void)removeReactSubview:(UIView *)subview { RCTAssert(subview == _reactSubview, @"Cannot remove view other than modal view"); - [super removeReactSubview:subview]; [subview removeGestureRecognizer:_touchHandler]; + [subview removeFromSuperview]; _reactSubview = nil; } -- (void)didUpdateReactSubviews -{ - // Do nothing, as subview (singular) is managed by `insertReactSubview:atIndex:` -} - - (void)dismissModalViewController { if (_isPresented) { diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index eb19b1f6c36579..d95089ee461559 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -217,6 +217,7 @@ @interface RCTNavigator() *previousViews; +@property (nonatomic, readwrite, strong) NSMutableArray *currentViews; @property (nonatomic, readwrite, strong) RCTNavigationController *navigationController; /** * Display link is used to get high frequency sample rate during @@ -298,6 +299,7 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _dummyView = [[UIView alloc] initWithFrame:CGRectZero]; _previousRequestedTopOfStack = kNeverRequested; // So that we initialize with a push. _previousViews = @[]; + _currentViews = [[NSMutableArray alloc] initWithCapacity:0]; __weak RCTNavigator *weakSelf = self; _navigationController = [[RCTNavigationController alloc] initWithScrollCallback:^{ [weakSelf dispatchFakeScrollEvent]; @@ -349,7 +351,7 @@ - (void)setPaused:(BOOL)paused - (void)setInteractivePopGestureEnabled:(BOOL)interactivePopGestureEnabled { _interactivePopGestureEnabled = interactivePopGestureEnabled; - + _navigationController.interactivePopGestureRecognizer.delegate = self; _navigationController.interactivePopGestureRecognizer.enabled = interactivePopGestureEnabled; @@ -400,8 +402,8 @@ - (void)navigationController:(UINavigationController *)navigationController return; } - NSUInteger indexOfFrom = [self.reactSubviews indexOfObject:fromController.navItem]; - NSUInteger indexOfTo = [self.reactSubviews indexOfObject:toController.navItem]; + NSUInteger indexOfFrom = [_currentViews indexOfObject:fromController.navItem]; + NSUInteger indexOfTo = [_currentViews indexOfObject:toController.navItem]; CGFloat destination = indexOfFrom < indexOfTo ? 1.0 : -1.0; _dummyView.frame = (CGRect){{destination, 0}, CGSizeZero}; _currentlyTransitioningFrom = indexOfFrom; @@ -431,7 +433,7 @@ - (BOOL)requestSchedulingJavaScriptNavigation - (void)freeLock { _navigationController.navigationLock = RCTNavigationLockNone; - + // Unless the pop gesture has been explicitly disabled (RCTPopGestureStateDisabled), // Set interactivePopGestureRecognizer.enabled to YES // If the popGestureState is RCTPopGestureStateDefault the default behavior will be maintained @@ -450,12 +452,12 @@ - (void)insertReactSubview:(RCTNavItem *)view atIndex:(NSInteger)atIndex _navigationController.navigationLock == RCTNavigationLockJavaScript, @"Cannot change subviews from JS without first locking." ); - [super insertReactSubview:view atIndex:atIndex]; + [_currentViews insertObject:view atIndex:atIndex]; } -- (void)didUpdateReactSubviews +- (NSArray *)reactSubviews { - // Do nothing, as subviews are managed by `reactBridgeDidFinishTransaction` + return _currentViews; } - (void)layoutSubviews @@ -467,11 +469,11 @@ - (void)layoutSubviews - (void)removeReactSubview:(RCTNavItem *)subview { - if (self.reactSubviews.count <= 0 || subview == self.reactSubviews[0]) { + if (_currentViews.count <= 0 || subview == _currentViews[0]) { RCTLogError(@"Attempting to remove invalid RCT subview of RCTNavigator"); return; } - [super removeReactSubview:subview]; + [_currentViews removeObject:subview]; } - (void)handleTopOfStackChanged @@ -495,8 +497,7 @@ - (void)dispatchFakeScrollEvent - (UIView *)reactSuperview { RCTAssert(!_bridge.isValid || self.superview != nil, @"put reactNavSuperviewLink back"); - UIView *superview = [super reactSuperview]; - return superview ?: self.reactNavSuperviewLink; + return self.superview ? self.superview : self.reactNavSuperviewLink; } - (void)reactBridgeDidFinishTransaction @@ -544,14 +545,14 @@ - (void)reactBridgeDidFinishTransaction jsGettingtooSlow)) { RCTLogError(@"JS has only made partial progress to catch up to UIKit"); } - if (currentReactCount > self.reactSubviews.count) { + if (currentReactCount > _currentViews.count) { RCTLogError(@"Cannot adjust current top of stack beyond available views"); } // Views before the previous React count must not have changed. Views greater than previousReactCount // up to currentReactCount may have changed. - for (NSUInteger i = 0; i < MIN(self.reactSubviews.count, MIN(_previousViews.count, previousReactCount)); i++) { - if (self.reactSubviews[i] != _previousViews[i]) { + for (NSUInteger i = 0; i < MIN(_currentViews.count, MIN(_previousViews.count, previousReactCount)); i++) { + if (_currentViews[i] != _previousViews[i]) { RCTLogError(@"current view should equal previous view"); } } @@ -560,7 +561,7 @@ - (void)reactBridgeDidFinishTransaction } if (jsGettingAhead) { if (reactPushOne) { - UIView *lastView = self.reactSubviews.lastObject; + UIView *lastView = _currentViews.lastObject; RCTWrapperViewController *vc = [[RCTWrapperViewController alloc] initWithNavItem:(RCTNavItem *)lastView]; vc.navigationListener = self; _numberOfViewControllerMovesToIgnore = 1; @@ -579,7 +580,7 @@ - (void)reactBridgeDidFinishTransaction return; } - _previousViews = [self.reactSubviews copy]; + _previousViews = [_currentViews copy]; _previousRequestedTopOfStack = _requestedTopOfStack; } diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index 3f2cd82efc057b..a2d1e5d859cf58 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -418,9 +418,8 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews // Does nothing } -- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex +- (void)insertReactSubview:(UIView *)view atIndex:(__unused NSInteger)atIndex { - [super insertReactSubview:view atIndex:atIndex]; if ([view isKindOfClass:[RCTRefreshControl class]]) { _scrollView.refreshControl = (RCTRefreshControl*)view; } else { @@ -432,18 +431,21 @@ - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex - (void)removeReactSubview:(UIView *)subview { - [super removeReactSubview:subview]; if ([subview isKindOfClass:[RCTRefreshControl class]]) { _scrollView.refreshControl = nil; } else { RCTAssert(_contentView == subview, @"Attempted to remove non-existent subview"); _contentView = nil; + [subview removeFromSuperview]; } } -- (void)didUpdateReactSubviews +- (NSArray *)reactSubviews { - // Do nothing, as subviews are managed by `insertReactSubview:atIndex:` + if (_contentView && _scrollView.refreshControl) { + return @[_contentView, _scrollView.refreshControl]; + } + return _contentView ? @[_contentView] : @[]; } - (BOOL)centerContent diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index 0d31e150734ee0..83ce22a5beb1fe 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -297,11 +297,6 @@ - (BOOL)isReactRootView return RCTIsReactRootView(self.reactTag); } -- (void)didUpdateReactSubviews -{ - // Does nothing by default -} - - (void)dealloc { free_css_node(_cssNode); diff --git a/React/Views/RCTTabBar.m b/React/Views/RCTTabBar.m index d544d75b2efa48..6f31f69985e20c 100644 --- a/React/Views/RCTTabBar.m +++ b/React/Views/RCTTabBar.m @@ -26,11 +26,13 @@ @implementation RCTTabBar { BOOL _tabsChanged; UITabBarController *_tabController; + NSMutableArray *_tabViews; } - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { + _tabViews = [NSMutableArray new]; _tabController = [UITabBarController new]; _tabController.delegate = self; [self addSubview:_tabController.view]; @@ -51,31 +53,31 @@ - (void)dealloc [_tabController removeFromParentViewController]; } -- (void)insertReactSubview:(RCTTabBarItem *)subview atIndex:(NSInteger)atIndex +- (NSArray *)reactSubviews { - if (![subview isKindOfClass:[RCTTabBarItem class]]) { + return _tabViews; +} + +- (void)insertReactSubview:(RCTTabBarItem *)view atIndex:(NSInteger)atIndex +{ + if (![view isKindOfClass:[RCTTabBarItem class]]) { RCTLogError(@"subview should be of type RCTTabBarItem"); return; } - [super insertReactSubview:subview atIndex:atIndex]; + [_tabViews insertObject:view atIndex:atIndex]; _tabsChanged = YES; } - (void)removeReactSubview:(RCTTabBarItem *)subview { - if (self.reactSubviews.count == 0) { + if (_tabViews.count == 0) { RCTLogError(@"should have at least one view to remove a subview"); return; } - [super removeReactSubview:subview]; + [_tabViews removeObject:subview]; _tabsChanged = YES; } -- (void)didUpdateReactSubviews -{ - // Do nothing, as subviews are managed by `reactBridgeDidFinishTransaction` -} - - (void)layoutSubviews { [super layoutSubviews]; @@ -104,9 +106,8 @@ - (void)reactBridgeDidFinishTransaction _tabsChanged = NO; } - [self.reactSubviews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger index, __unused BOOL *stop) { - - RCTTabBarItem *tab = (RCTTabBarItem *)view; + [_tabViews enumerateObjectsUsingBlock: + ^(RCTTabBarItem *tab, NSUInteger index, __unused BOOL *stop) { UIViewController *controller = _tabController.viewControllers[index]; if (_unselectedTintColor) { [tab.barItem setTitleTextAttributes:@{NSForegroundColorAttributeName: _unselectedTintColor} forState:UIControlStateNormal]; @@ -164,7 +165,7 @@ - (void)setItemPositioning:(UITabBarItemPositioning)itemPositioning - (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController { NSUInteger index = [tabBarController.viewControllers indexOfObject:viewController]; - RCTTabBarItem *tab = (RCTTabBarItem *)self.reactSubviews[index]; + RCTTabBarItem *tab = _tabViews[index]; if (tab.onPress) tab.onPress(nil); return NO; } diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index ffce4b7fd07864..ad2ea1424bbdaf 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -97,6 +97,7 @@ - (UIView *)react_findClipView @implementation RCTView { + NSMutableArray *_reactSubviews; UIColor *_backgroundColor; } @@ -274,31 +275,76 @@ + (UIEdgeInsets)contentInsetsForView:(UIView *)view - (void)react_remountAllSubviews { - if (_removeClippedSubviews) { - for (UIView *view in self.reactSubviews) { + if (_reactSubviews) { + NSUInteger index = 0; + for (UIView *view in _reactSubviews) { if (view.superview != self) { - [self addSubview:view]; + if (index < self.subviews.count) { + [self insertSubview:view atIndex:index]; + } else { + [self addSubview:view]; + } [view react_remountAllSubviews]; } + index++; } } else { - // If _removeClippedSubviews is false, we must already be showing all subviews + // If react_subviews is nil, we must already be showing all subviews [super react_remountAllSubviews]; } } +- (void)remountSubview:(UIView *)view +{ + // Calculate insertion index for view + NSInteger index = 0; + for (UIView *subview in _reactSubviews) { + if (subview == view) { + [self insertSubview:view atIndex:index]; + break; + } + if (subview.superview) { + // View is mounted, so bump the index + index++; + } + } +} + +- (void)mountOrUnmountSubview:(UIView *)view withClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView +{ + if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) { + + // View is at least partially visible, so remount it if unmounted + if (view.superview == nil) { + [self remountSubview:view]; + } + + // Then test its subviews + if (CGRectContainsRect(clipRect, view.frame)) { + [view react_remountAllSubviews]; + } else { + [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; + } + + } else if (view.superview) { + + // View is completely outside the clipRect, so unmount it + [view removeFromSuperview]; + } +} + - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView { // TODO (#5906496): for scrollviews (the primary use-case) we could // optimize this by only doing a range check along the scroll axis, // instead of comparing the whole frame - if (!_removeClippedSubviews) { + if (_reactSubviews == nil) { // Use default behavior if unmounting is disabled return [super react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; } - if (self.reactSubviews.count == 0) { + if (_reactSubviews.count == 0) { // Do nothing if we have no subviews return; } @@ -314,46 +360,63 @@ - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView: clipView = self; // Mount / unmount views - for (UIView *view in self.reactSubviews) { - if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) { - - // View is at least partially visible, so remount it if unmounted - [self addSubview:view]; - - // Then test its subviews - if (CGRectContainsRect(clipRect, view.frame)) { - // View is fully visible, so remount all subviews - [view react_remountAllSubviews]; - } else { - // View is partially visible, so update clipped subviews - [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; - } - - } else if (view.superview) { - - // View is completely outside the clipRect, so unmount it - [view removeFromSuperview]; - } + for (UIView *view in _reactSubviews) { + [self mountOrUnmountSubview:view withClipRect:clipRect relativeToView:clipView]; } } - (void)setRemoveClippedSubviews:(BOOL)removeClippedSubviews { - if (!removeClippedSubviews && _removeClippedSubviews) { + if (removeClippedSubviews && !_reactSubviews) { + _reactSubviews = [self.subviews mutableCopy]; + } else if (!removeClippedSubviews && _reactSubviews) { [self react_remountAllSubviews]; + _reactSubviews = nil; } - _removeClippedSubviews = removeClippedSubviews; } -- (void)didUpdateReactSubviews +- (BOOL)removeClippedSubviews { - if (_removeClippedSubviews) { - [self updateClippedSubviews]; + return _reactSubviews != nil; +} + +- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex +{ + if (_reactSubviews == nil) { + [self insertSubview:view atIndex:atIndex]; } else { - [super didUpdateReactSubviews]; + [_reactSubviews insertObject:view atIndex:atIndex]; + + // Find a suitable view to use for clipping + UIView *clipView = [self react_findClipView]; + if (clipView) { + + // If possible, don't add subviews if they are clipped + [self mountOrUnmountSubview:view withClipRect:clipView.bounds relativeToView:clipView]; + + } else { + + // Fallback if we can't find a suitable clipView + [self remountSubview:view]; + } } } +- (void)removeReactSubview:(UIView *)subview +{ + [_reactSubviews removeObject:subview]; + [subview removeFromSuperview]; +} + +- (NSArray *)reactSubviews +{ + // The _reactSubviews array is only used when we have hidden + // offscreen views. If _reactSubviews is nil, we can assume + // that [self reactSubviews] and [self subviews] are the same + + return _reactSubviews ?: self.subviews; +} + - (void)updateClippedSubviews { // Find a suitable view to use for clipping @@ -372,7 +435,7 @@ - (void)layoutSubviews [super layoutSubviews]; - if (_removeClippedSubviews) { + if (_reactSubviews) { [self updateClippedSubviews]; } } diff --git a/React/Views/UIView+React.h b/React/Views/UIView+React.h index fa779e5208a821..794c977e9e7413 100644 --- a/React/Views/UIView+React.h +++ b/React/Views/UIView+React.h @@ -17,19 +17,8 @@ @interface UIView (React) -/** - * RCTComponent interface. - */ -- (NSArray *)reactSubviews NS_REQUIRES_SUPER; -- (UIView *)reactSuperview NS_REQUIRES_SUPER; -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex NS_REQUIRES_SUPER; -- (void)removeReactSubview:(UIView *)subview NS_REQUIRES_SUPER; - -/** - * Updates the subviews array based on the reactSubviews. Default behavior is - * to insert the reactSubviews into the UIView. - */ -- (void)didUpdateReactSubviews; +- (NSArray *)reactSubviews; +- (UIView *)reactSuperview; /** * Used by the UIIManager to set the view frame. diff --git a/React/Views/UIView+React.m b/React/Views/UIView+React.m index 69658d132f807a..e7736f3a125651 100644 --- a/React/Views/UIView+React.m +++ b/React/Views/UIView+React.m @@ -56,37 +56,25 @@ - (NSNumber *)reactTagAtPoint:(CGPoint)point return view.reactTag; } -- (NSArray *)reactSubviews -{ - return objc_getAssociatedObject(self, _cmd); -} - -- (UIView *)reactSuperview -{ - return self.superview; -} - - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { - NSMutableArray *reactSubviews = (NSMutableArray *)self.reactSubviews; - if (!reactSubviews) { - reactSubviews = [NSMutableArray new]; - objc_setAssociatedObject(self, @selector(reactSubviews), reactSubviews, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - [reactSubviews insertObject:subview atIndex:atIndex]; + [self insertSubview:subview atIndex:atIndex]; } - (void)removeReactSubview:(UIView *)subview { - [(NSMutableArray *)self.reactSubviews removeObject:subview]; + RCTAssert(subview.superview == self, @"%@ is a not a subview of %@", subview, self); [subview removeFromSuperview]; } -- (void)didUpdateReactSubviews +- (NSArray *)reactSubviews { - for (UIView *subview in self.reactSubviews) { - [self addSubview:subview]; - } + return self.subviews; +} + +- (UIView *)reactSuperview +{ + return self.superview; } - (void)reactSetFrame:(CGRect)frame From 588f01e9982775f0699c7bfd56623d4ed3949810 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Mon, 6 Jun 2016 17:45:30 -0700 Subject: [PATCH 248/843] Packager: accept relative --transformer paths everywhere. Summary: `react-native start` already ensures that the `--transformer` path is understood to be relative to CWD, not to the module that ends up importing that file. `react-native bundle` and `react-native dependencies` didn't up until this point. **Test plan:** Ensured that `react-native bundle ... --transformer ./relative/path` works with this patch applied. Closes https://github.com/facebook/react-native/pull/7857 Differential Revision: D3393777 fbshipit-source-id: 303a226fae9c8087c3dd3b2e8d004462ca66665e --- local-cli/bundle/buildBundle.js | 3 ++- local-cli/bundle/bundleCommandLineArgs.js | 2 +- local-cli/dependencies/dependencies.js | 4 ++-- local-cli/server/runServer.js | 8 +------- local-cli/server/server.js | 2 +- 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/local-cli/bundle/buildBundle.js b/local-cli/bundle/buildBundle.js index 7c9b3f4d053abd..d71b7515d27e98 100644 --- a/local-cli/bundle/buildBundle.js +++ b/local-cli/bundle/buildBundle.js @@ -9,6 +9,7 @@ const log = require('../util/log').out('bundle'); const outputBundle = require('./output/bundle'); +const path = require('path'); const Promise = require('promise'); const saveAssets = require('./saveAssets'); const Server = require('../../packager/react-packager/src/Server'); @@ -31,7 +32,7 @@ function buildBundle(args, config, output = outputBundle, packagerInstance) { assetRoots: config.getAssetRoots(), blacklistRE: config.getBlacklistRE(args.platform), getTransformOptionsModulePath: config.getTransformOptionsModulePath, - transformModulePath: args.transformer, + transformModulePath: path.resolve(args.transformer), extraNodeModules: config.extraNodeModules, nonPersistent: true, resetCache: args['reset-cache'], diff --git a/local-cli/bundle/bundleCommandLineArgs.js b/local-cli/bundle/bundleCommandLineArgs.js index c22aa3946f0f53..b3b30dd5ba65cc 100644 --- a/local-cli/bundle/bundleCommandLineArgs.js +++ b/local-cli/bundle/bundleCommandLineArgs.js @@ -20,7 +20,7 @@ module.exports = [ type: 'string', }, { command: 'transformer', - description: 'Specify a custom transformer to be used (absolute path)', + description: 'Specify a custom transformer to be used', type: 'string', default: require.resolve('../../packager/transformer'), }, { diff --git a/local-cli/dependencies/dependencies.js b/local-cli/dependencies/dependencies.js index d9ad3da1d5b71c..0010e11c9eb266 100644 --- a/local-cli/dependencies/dependencies.js +++ b/local-cli/dependencies/dependencies.js @@ -41,7 +41,7 @@ function _dependencies(argv, config, resolve, reject, packagerInstance) { command: 'transformer', type: 'string', default: require.resolve('../../packager/transformer'), - description: 'Specify a custom transformer to be used (absolute path)' + description: 'Specify a custom transformer to be used' }, { command: 'verbose', description: 'Enables logging', @@ -59,7 +59,7 @@ function _dependencies(argv, config, resolve, reject, packagerInstance) { assetRoots: config.getAssetRoots(), blacklistRE: config.getBlacklistRE(args.platform), getTransformOptionsModulePath: config.getTransformOptionsModulePath, - transformModulePath: args.transformer, + transformModulePath: path.resolve(args.transformer), extraNodeModules: config.extraNodeModules, verbose: config.verbose, }; diff --git a/local-cli/server/runServer.js b/local-cli/server/runServer.js index 362a09ca4a88d0..4106102b2a069b 100644 --- a/local-cli/server/runServer.js +++ b/local-cli/server/runServer.js @@ -13,7 +13,6 @@ const connect = require('connect'); const cpuProfilerMiddleware = require('./middleware/cpuProfilerMiddleware'); const getDevToolsMiddleware = require('./middleware/getDevToolsMiddleware'); const http = require('http'); -const isAbsolutePath = require('absolute-path'); const loadRawBodyMiddleware = require('./middleware/loadRawBodyMiddleware'); const messageSocket = require('./util/messageSocket.js'); const openStackFrameInEditorMiddleware = require('./middleware/openStackFrameInEditorMiddleware'); @@ -68,18 +67,13 @@ function runServer(args, config, readyCallback) { } function getPackagerServer(args, config) { - let transformerPath = args.transformer; - if (!isAbsolutePath(transformerPath)) { - transformerPath = path.resolve(process.cwd(), transformerPath); - } - return ReactPackager.createServer({ nonPersistent: args.nonPersistent, projectRoots: args.projectRoots, blacklistRE: config.getBlacklistRE(), cacheVersion: '3', getTransformOptionsModulePath: config.getTransformOptionsModulePath, - transformModulePath: transformerPath, + transformModulePath: path.resolve(args.transformer), extraNodeModules: config.extraNodeModules, assetRoots: args.assetRoots, assetExts: [ diff --git a/local-cli/server/server.js b/local-cli/server/server.js index 2b7efe8038e0d8..f2766bc462a0b1 100644 --- a/local-cli/server/server.js +++ b/local-cli/server/server.js @@ -55,7 +55,7 @@ function _server(argv, config, resolve, reject) { command: 'transformer', type: 'string', default: require.resolve('../../packager/transformer'), - description: 'Specify a custom transformer to be used (absolute path)' + description: 'Specify a custom transformer to be used' }, { command: 'resetCache', description: 'Removes cached files', From edfe2a3a0654e9f427e3bc70c8dc1c177729d76e Mon Sep 17 00:00:00 2001 From: Steve Mao Date: Mon, 6 Jun 2016 19:22:30 -0700 Subject: [PATCH 249/843] Fix how react is imported Summary: Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: (You can skip this if you're fixing a typo or adding an app to the Showcase.) Explain the **motivation** for making this change. What existing problem does the pull request solve? Prefer **small pull requests**. These are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise please split it. **Test plan (required)** Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. Make sure tests pass on both Travis and Circle CI. **Code formatting** Look around. Match the style of the rest of the codebase. See also the simple [style guide](https://github.com/facebook/react-native/blob/master/CONTRIBUTING.md#style-guide). For more info, see the ["Pull Requests" section of our "Contributing" guidelines](https://github.com/facebook/react-native/blob/mas Closes https://github.com/facebook/react-native/pull/7962 Differential Revision: D3397289 fbshipit-source-id: 21adf955af4a623f1ce71e7a5e412020ceaad12a --- docs/Tutorial-CoreComponents.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Tutorial-CoreComponents.md b/docs/Tutorial-CoreComponents.md index a117ce9fdca611..0cac871ac14f27 100644 --- a/docs/Tutorial-CoreComponents.md +++ b/docs/Tutorial-CoreComponents.md @@ -88,7 +88,8 @@ Direct text-based user input is a foundation for many apps. Writing a post or co This example creates a simple `TextInput` box with the `string` `Hello` as the placeholder when the `TextInput` is empty. ```JavaScript -import React, { AppRegistry, TextInput, View } from 'react-native' +import React from 'react'; +import { AppRegistry, TextInput, View } from 'react-native'; const App = () => { return ( From 2a92b52ac8b177021b082375d73eaaef31a0f37a Mon Sep 17 00:00:00 2001 From: Nicolas Charpentier Date: Mon, 6 Jun 2016 22:45:12 -0700 Subject: [PATCH 250/843] Add Fresco to ProGuard Summary: Motivation #7760 Closes https://github.com/facebook/react-native/pull/7781 Differential Revision: D3397772 fbshipit-source-id: 02b6fd4a403da590fd1c55c554eca00e15899a03 --- .../generator-android/templates/src/app/proguard-rules.pro | 3 +++ 1 file changed, 3 insertions(+) diff --git a/local-cli/generator-android/templates/src/app/proguard-rules.pro b/local-cli/generator-android/templates/src/app/proguard-rules.pro index 9852871bd0de35..48361a9015dc50 100644 --- a/local-cli/generator-android/templates/src/app/proguard-rules.pro +++ b/local-cli/generator-android/templates/src/app/proguard-rules.pro @@ -26,11 +26,14 @@ # See http://sourceforge.net/p/proguard/bugs/466/ -keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip -keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters +-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip # Do not strip any method/class that is annotated with @DoNotStrip -keep @com.facebook.proguard.annotations.DoNotStrip class * +-keep @com.facebook.common.internal.DoNotStrip class * -keepclassmembers class * { @com.facebook.proguard.annotations.DoNotStrip *; + @com.facebook.common.internal.DoNotStrip *; } -keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { From 46c02b6ae58f9be60affe1e2b3f0fc19646fb817 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 7 Jun 2016 00:08:16 -0700 Subject: [PATCH 251/843] Refactored subview management Summary: This diff refactors the view update process into two stages: 1. The `reactSubviews` array is set, whose order matches the order of the JS components and shadowView components, as specified by the UIManager. 2. The `didUpdateReactSubviews` method is called, which actually inserts the reactSubviews into the view hierarchy. This simplifies a lot of the hacks we had for special-case treatment of subviews: In many cases we don't want to actually insert `reactSubviews` into the parentView, and we had a bunch of component-specific solutions for that (typically overriding all of the reactSubviews methods to store views in an array). Now, we can simply override the `didUpdateReactSubviews` method for those views to do nothing, or do something different. Reviewed By: wwjholmes Differential Revision: D3396594 fbshipit-source-id: 92fc56fd31db0cfc66aac3d1634a4d4ae3903085 --- .../FlexibleSizeExampleView.m | 1 + .../UpdatePropertiesExampleView.m | 1 + .../UIExplorerUnitTests/RCTUIManagerTests.m | 10 +- Libraries/Text/RCTText.m | 17 +-- Libraries/Text/RCTTextField.m | 26 ---- Libraries/Text/RCTTextView.m | 22 +-- React/Base/RCTRootView.m | 2 +- React/Modules/RCTUIManager.m | 14 +- React/Views/RCTComponent.h | 5 + React/Views/RCTMap.m | 16 +-- React/Views/RCTModalHostView.m | 15 +- React/Views/RCTNavigator.m | 33 +++-- React/Views/RCTScrollView.m | 12 +- React/Views/RCTShadowView.h | 13 +- React/Views/RCTShadowView.m | 5 + React/Views/RCTTabBar.m | 29 ++-- React/Views/RCTView.m | 134 +++++------------- React/Views/RCTViewManager.h | 2 +- React/Views/RCTViewManager.m | 4 +- React/Views/UIView+React.h | 15 +- React/Views/UIView+React.m | 35 +++-- 21 files changed, 170 insertions(+), 241 deletions(-) diff --git a/Examples/UIExplorer/UIExplorer/NativeExampleViews/FlexibleSizeExampleView.m b/Examples/UIExplorer/UIExplorer/NativeExampleViews/FlexibleSizeExampleView.m index ce899207027a2e..0465a14f6c155c 100644 --- a/Examples/UIExplorer/UIExplorer/NativeExampleViews/FlexibleSizeExampleView.m +++ b/Examples/UIExplorer/UIExplorer/NativeExampleViews/FlexibleSizeExampleView.m @@ -90,6 +90,7 @@ - (void)layoutSubviews - (NSArray *> *)reactSubviews { // this is to avoid unregistering our RCTRootView when the component is removed from RN hierarchy + (void)[super reactSubviews]; return @[]; } diff --git a/Examples/UIExplorer/UIExplorer/NativeExampleViews/UpdatePropertiesExampleView.m b/Examples/UIExplorer/UIExplorer/NativeExampleViews/UpdatePropertiesExampleView.m index cc78f6cff84024..0416024a8b7da3 100644 --- a/Examples/UIExplorer/UIExplorer/NativeExampleViews/UpdatePropertiesExampleView.m +++ b/Examples/UIExplorer/UIExplorer/NativeExampleViews/UpdatePropertiesExampleView.m @@ -89,6 +89,7 @@ - (void)changeColor - (NSArray *> *)reactSubviews { // this is to avoid unregistering our RCTRootView when the component is removed from RN hierarchy + (void)[super reactSubviews]; return @[]; } diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m index 4ae4b9f5ca8027..c3bd09ff1313ef 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m @@ -77,8 +77,8 @@ - (void)testManagingChildrenToAddViews @"Expect to have 5 react subviews after calling manage children \ with 5 tags to add, instead have %lu", (unsigned long)[[containerView reactSubviews] count]); for (UIView *view in addedViews) { - XCTAssertTrue([view superview] == containerView, - @"Expected to have manage children successfully add children"); + XCTAssertTrue([view reactSuperview] == containerView, + @"Expected to have manage children successfully add children"); [view removeFromSuperview]; } } @@ -95,7 +95,7 @@ - (void)testManagingChildrenToRemoveViews } for (NSInteger i = 2; i < 20; i++) { UIView *view = _uiManager.viewRegistry[@(i)]; - [containerView addSubview:view]; + [containerView insertReactSubview:view atIndex:containerView.reactSubviews.count]; } // Remove views 1-5 from view 20 @@ -112,7 +112,7 @@ - (void)testManagingChildrenToRemoveViews with 5 tags to remove and 18 prior children, instead have %zd", containerView.reactSubviews.count); for (UIView *view in removedViews) { - XCTAssertTrue([view superview] == nil, + XCTAssertTrue([view reactSuperview] == nil, @"Expected to have manage children successfully remove children"); // After removing views are unregistered - we need to reregister _uiManager.viewRegistry[view.reactTag] = view; @@ -155,7 +155,7 @@ - (void)testManagingChildrenToAddRemoveAndMove for (NSInteger i = 1; i < 11; i++) { UIView *view = _uiManager.viewRegistry[@(i)]; - [containerView addSubview:view]; + [containerView insertReactSubview:view atIndex:containerView.reactSubviews.count]; } [_uiManager _manageChildren:@20 diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index 864fa096b42dc0..fc5f60bdcea0d2 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -27,7 +27,6 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc @implementation RCTText { NSTextStorage *_textStorage; - NSMutableArray *_reactSubviews; CAShapeLayer *_highlightLayer; } @@ -35,7 +34,6 @@ - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { _textStorage = [NSTextStorage new]; - _reactSubviews = [NSMutableArray array]; self.isAccessibilityElement = YES; self.accessibilityTraits |= UIAccessibilityTraitStaticText; @@ -68,19 +66,9 @@ - (void)reactSetInheritedBackgroundColor:(UIColor *)inheritedBackgroundColor self.backgroundColor = inheritedBackgroundColor; } -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +- (void)reactUpdateSubviews { - [_reactSubviews insertObject:subview atIndex:atIndex]; -} - -- (void)removeReactSubview:(UIView *)subview -{ - [_reactSubviews removeObject:subview]; -} - -- (NSArray *)reactSubviews -{ - return _reactSubviews; + // Do nothing, as subviews are managed by `setTextStorage:` method } - (void)setTextStorage:(NSTextStorage *)textStorage @@ -88,6 +76,7 @@ - (void)setTextStorage:(NSTextStorage *)textStorage if (_textStorage != textStorage) { _textStorage = textStorage; + // Update subviews NSMutableArray *nonTextDescendants = [NSMutableArray new]; collectNonTextDescendants(self, nonTextDescendants); NSArray *subviews = self.subviews; diff --git a/Libraries/Text/RCTTextField.m b/Libraries/Text/RCTTextField.m index 24b077baff106b..71859ad4d77443 100644 --- a/Libraries/Text/RCTTextField.m +++ b/Libraries/Text/RCTTextField.m @@ -17,7 +17,6 @@ @implementation RCTTextField { RCTEventDispatcher *_eventDispatcher; - NSMutableArray *_reactSubviews; BOOL _jsRequestingFirstResponder; NSInteger _nativeEventCount; BOOL _submitted; @@ -35,7 +34,6 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher [self addTarget:self action:@selector(textFieldEndEditing) forControlEvents:UIControlEventEditingDidEnd]; [self addTarget:self action:@selector(textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit]; [self addObserver:self forKeyPath:@"selectedTextRange" options:0 context:nil]; - _reactSubviews = [NSMutableArray new]; _blurOnSubmit = YES; } return self; @@ -112,30 +110,6 @@ - (void)setPlaceholder:(NSString *)placeholder RCTUpdatePlaceholder(self); } -- (NSArray *)reactSubviews -{ - // TODO: do we support subviews of textfield in React? - // In any case, we should have a better approach than manually - // maintaining array in each view subclass like this - return _reactSubviews; -} - -- (void)removeReactSubview:(UIView *)subview -{ - // TODO: this is a bit broken - if the TextField inserts any of - // its own views below or between React's, the indices won't match - [_reactSubviews removeObject:subview]; - [subview removeFromSuperview]; -} - -- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex -{ - // TODO: this is a bit broken - if the TextField inserts any of - // its own views below or between React's, the indices won't match - [_reactSubviews insertObject:view atIndex:atIndex]; - [super insertSubview:view atIndex:atIndex]; -} - - (CGRect)caretRectForPosition:(UITextPosition *)position { if (_caretHidden) { diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index 627fd7ae47c7e9..9b0a4d737cd4eb 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -67,7 +67,6 @@ @implementation RCTTextView NSInteger _nativeEventCount; RCTText *_richTextView; NSAttributedString *_pendingAttributedText; - NSMutableArray *_subviews; BOOL _blockTextShouldChange; UITextRange *_previousSelectionRange; NSUInteger _previousTextLength; @@ -98,7 +97,6 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher _previousSelectionRange = _textView.selectedTextRange; - _subviews = [NSMutableArray new]; [self addSubview:_scrollView]; } return self; @@ -107,19 +105,14 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) -- (NSArray *)reactSubviews -{ - return _subviews; -} - - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index { + [super insertReactSubview:subview atIndex:index]; if ([subview isKindOfClass:[RCTText class]]) { if (_richTextView) { RCTLogError(@"Tried to insert a second into - there can only be one."); } _richTextView = (RCTText *)subview; - [_subviews insertObject:_richTextView atIndex:index]; // If this is in rich text editing mode, and the child node providing rich text // styling has a backgroundColor, then the attributedText produced by the child node will have an @@ -132,23 +125,22 @@ - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index attrs[NSBackgroundColorAttributeName] = subview.backgroundColor; _textView.typingAttributes = attrs; } - } else { - [_subviews insertObject:subview atIndex:index]; - [self insertSubview:subview atIndex:index]; } } - (void)removeReactSubview:(UIView *)subview { + [super removeReactSubview:subview]; if (_richTextView == subview) { - [_subviews removeObject:_richTextView]; _richTextView = nil; - } else { - [_subviews removeObject:subview]; - [subview removeFromSuperview]; } } +- (void)reactUpdateSubviews +{ + // Do nothing, as we don't allow non-text subviews +} + - (void)setMostRecentEventCount:(NSInteger)mostRecentEventCount { _mostRecentEventCount = mostRecentEventCount; diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index cff7861a1b9833..78896f5fe482e1 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -337,7 +337,7 @@ - (instancetype)initWithFrame:(CGRect)frame RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder:(nonnull NSCoder *)aDecoder) -- (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { [super insertReactSubview:subview atIndex:atIndex]; RCTPerformanceLoggerEnd(RCTPLTTI); diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 168e115dc82f3d..4ab30f9918b723 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -893,16 +893,18 @@ static void RCTSetChildren(NSNumber *containerTag, [container insertReactSubview:view atIndex:index++]; } } + + [container didUpdateReactSubviews]; } -RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerReactTag +RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag moveFromIndices:(NSArray *)moveFromIndices moveToIndices:(NSArray *)moveToIndices addChildReactTags:(NSArray *)addChildReactTags addAtIndices:(NSArray *)addAtIndices removeAtIndices:(NSArray *)removeAtIndices) { - [self _manageChildren:containerReactTag + [self _manageChildren:containerTag moveFromIndices:moveFromIndices moveToIndices:moveToIndices addChildReactTags:addChildReactTags @@ -911,7 +913,7 @@ static void RCTSetChildren(NSNumber *containerTag, registry:(NSMutableDictionary> *)_shadowViewRegistry]; [self addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry){ - [uiManager _manageChildren:containerReactTag + [uiManager _manageChildren:containerTag moveFromIndices:moveFromIndices moveToIndices:moveToIndices addChildReactTags:addChildReactTags @@ -921,7 +923,7 @@ static void RCTSetChildren(NSNumber *containerTag, }]; } -- (void)_manageChildren:(NSNumber *)containerReactTag +- (void)_manageChildren:(NSNumber *)containerTag moveFromIndices:(NSArray *)moveFromIndices moveToIndices:(NSArray *)moveToIndices addChildReactTags:(NSArray *)addChildReactTags @@ -929,7 +931,7 @@ - (void)_manageChildren:(NSNumber *)containerReactTag removeAtIndices:(NSArray *)removeAtIndices registry:(NSMutableDictionary> *)registry { - id container = registry[containerReactTag]; + id container = registry[containerTag]; RCTAssert(moveFromIndices.count == moveToIndices.count, @"moveFromIndices had size %tu, moveToIndices had size %tu", moveFromIndices.count, moveToIndices.count); RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one React child to add"); @@ -963,6 +965,8 @@ - (void)_manageChildren:(NSNumber *)containerReactTag [container insertReactSubview:destinationsToChildrenToAdd[reactIndex] atIndex:reactIndex.integerValue]; } + + [container didUpdateReactSubviews]; } RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag diff --git a/React/Views/RCTComponent.h b/React/Views/RCTComponent.h index 5236f850b7993e..5c9beab09ba96c 100644 --- a/React/Views/RCTComponent.h +++ b/React/Views/RCTComponent.h @@ -43,6 +43,11 @@ typedef void (^RCTBubblingEventBlock)(NSDictionary *body); */ - (void)didSetProps:(NSArray *)changedProps; +/** + * Called each time subviews have been updated + */ +- (void)didUpdateReactSubviews; + // TODO: Deprecate this // This method is called after layout has been performed for all views known // to the RCTViewManager. It is only called on UIViews, not shadow views. diff --git a/React/Views/RCTMap.m b/React/Views/RCTMap.m index 1b3472530155d0..7a1334668761ca 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -23,7 +23,6 @@ @implementation RCTMap { UIView *_legalLabel; CLLocationManager *_locationManager; - NSMutableArray *_reactSubviews; } - (instancetype)init @@ -31,7 +30,6 @@ - (instancetype)init if ((self = [super init])) { _hasStartedRendering = NO; - _reactSubviews = [NSMutableArray new]; // Find Apple link label for (UIView *subview in self.subviews) { @@ -51,19 +49,9 @@ - (void)dealloc [_regionChangeObserveTimer invalidate]; } -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +- (void)reactUpdateSubviews { - [_reactSubviews insertObject:subview atIndex:atIndex]; -} - -- (void)removeReactSubview:(UIView *)subview -{ - [_reactSubviews removeObject:subview]; -} - -- (NSArray *)reactSubviews -{ - return _reactSubviews; + // Do nothing, as annotation views are managed by `setAnnotations:` method } - (void)layoutSubviews diff --git a/React/Views/RCTModalHostView.m b/React/Views/RCTModalHostView.m index c3140249305873..3dce5ed759910b 100644 --- a/React/Views/RCTModalHostView.m +++ b/React/Views/RCTModalHostView.m @@ -55,14 +55,10 @@ - (void)notifyForBoundsChange:(CGRect)newBounds } } -- (NSArray *)reactSubviews -{ - return _reactSubview ? @[_reactSubview] : @[]; -} - -- (void)insertReactSubview:(UIView *)subview atIndex:(__unused NSInteger)atIndex +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { RCTAssert(_reactSubview == nil, @"Modal view can only have one subview"); + [super insertReactSubview:subview atIndex:atIndex]; [subview addGestureRecognizer:_touchHandler]; subview.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; @@ -74,11 +70,16 @@ - (void)insertReactSubview:(UIView *)subview atIndex:(__unused NSInteger)atIndex - (void)removeReactSubview:(UIView *)subview { RCTAssert(subview == _reactSubview, @"Cannot remove view other than modal view"); + [super removeReactSubview:subview]; [subview removeGestureRecognizer:_touchHandler]; - [subview removeFromSuperview]; _reactSubview = nil; } +- (void)didUpdateReactSubviews +{ + // Do nothing, as subview (singular) is managed by `insertReactSubview:atIndex:` +} + - (void)dismissModalViewController { if (_isPresented) { diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index d95089ee461559..eb19b1f6c36579 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -217,7 +217,6 @@ @interface RCTNavigator() *previousViews; -@property (nonatomic, readwrite, strong) NSMutableArray *currentViews; @property (nonatomic, readwrite, strong) RCTNavigationController *navigationController; /** * Display link is used to get high frequency sample rate during @@ -299,7 +298,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _dummyView = [[UIView alloc] initWithFrame:CGRectZero]; _previousRequestedTopOfStack = kNeverRequested; // So that we initialize with a push. _previousViews = @[]; - _currentViews = [[NSMutableArray alloc] initWithCapacity:0]; __weak RCTNavigator *weakSelf = self; _navigationController = [[RCTNavigationController alloc] initWithScrollCallback:^{ [weakSelf dispatchFakeScrollEvent]; @@ -351,7 +349,7 @@ - (void)setPaused:(BOOL)paused - (void)setInteractivePopGestureEnabled:(BOOL)interactivePopGestureEnabled { _interactivePopGestureEnabled = interactivePopGestureEnabled; - + _navigationController.interactivePopGestureRecognizer.delegate = self; _navigationController.interactivePopGestureRecognizer.enabled = interactivePopGestureEnabled; @@ -402,8 +400,8 @@ - (void)navigationController:(UINavigationController *)navigationController return; } - NSUInteger indexOfFrom = [_currentViews indexOfObject:fromController.navItem]; - NSUInteger indexOfTo = [_currentViews indexOfObject:toController.navItem]; + NSUInteger indexOfFrom = [self.reactSubviews indexOfObject:fromController.navItem]; + NSUInteger indexOfTo = [self.reactSubviews indexOfObject:toController.navItem]; CGFloat destination = indexOfFrom < indexOfTo ? 1.0 : -1.0; _dummyView.frame = (CGRect){{destination, 0}, CGSizeZero}; _currentlyTransitioningFrom = indexOfFrom; @@ -433,7 +431,7 @@ - (BOOL)requestSchedulingJavaScriptNavigation - (void)freeLock { _navigationController.navigationLock = RCTNavigationLockNone; - + // Unless the pop gesture has been explicitly disabled (RCTPopGestureStateDisabled), // Set interactivePopGestureRecognizer.enabled to YES // If the popGestureState is RCTPopGestureStateDefault the default behavior will be maintained @@ -452,12 +450,12 @@ - (void)insertReactSubview:(RCTNavItem *)view atIndex:(NSInteger)atIndex _navigationController.navigationLock == RCTNavigationLockJavaScript, @"Cannot change subviews from JS without first locking." ); - [_currentViews insertObject:view atIndex:atIndex]; + [super insertReactSubview:view atIndex:atIndex]; } -- (NSArray *)reactSubviews +- (void)didUpdateReactSubviews { - return _currentViews; + // Do nothing, as subviews are managed by `reactBridgeDidFinishTransaction` } - (void)layoutSubviews @@ -469,11 +467,11 @@ - (void)layoutSubviews - (void)removeReactSubview:(RCTNavItem *)subview { - if (_currentViews.count <= 0 || subview == _currentViews[0]) { + if (self.reactSubviews.count <= 0 || subview == self.reactSubviews[0]) { RCTLogError(@"Attempting to remove invalid RCT subview of RCTNavigator"); return; } - [_currentViews removeObject:subview]; + [super removeReactSubview:subview]; } - (void)handleTopOfStackChanged @@ -497,7 +495,8 @@ - (void)dispatchFakeScrollEvent - (UIView *)reactSuperview { RCTAssert(!_bridge.isValid || self.superview != nil, @"put reactNavSuperviewLink back"); - return self.superview ? self.superview : self.reactNavSuperviewLink; + UIView *superview = [super reactSuperview]; + return superview ?: self.reactNavSuperviewLink; } - (void)reactBridgeDidFinishTransaction @@ -545,14 +544,14 @@ - (void)reactBridgeDidFinishTransaction jsGettingtooSlow)) { RCTLogError(@"JS has only made partial progress to catch up to UIKit"); } - if (currentReactCount > _currentViews.count) { + if (currentReactCount > self.reactSubviews.count) { RCTLogError(@"Cannot adjust current top of stack beyond available views"); } // Views before the previous React count must not have changed. Views greater than previousReactCount // up to currentReactCount may have changed. - for (NSUInteger i = 0; i < MIN(_currentViews.count, MIN(_previousViews.count, previousReactCount)); i++) { - if (_currentViews[i] != _previousViews[i]) { + for (NSUInteger i = 0; i < MIN(self.reactSubviews.count, MIN(_previousViews.count, previousReactCount)); i++) { + if (self.reactSubviews[i] != _previousViews[i]) { RCTLogError(@"current view should equal previous view"); } } @@ -561,7 +560,7 @@ - (void)reactBridgeDidFinishTransaction } if (jsGettingAhead) { if (reactPushOne) { - UIView *lastView = _currentViews.lastObject; + UIView *lastView = self.reactSubviews.lastObject; RCTWrapperViewController *vc = [[RCTWrapperViewController alloc] initWithNavItem:(RCTNavItem *)lastView]; vc.navigationListener = self; _numberOfViewControllerMovesToIgnore = 1; @@ -580,7 +579,7 @@ - (void)reactBridgeDidFinishTransaction return; } - _previousViews = [_currentViews copy]; + _previousViews = [self.reactSubviews copy]; _previousRequestedTopOfStack = _requestedTopOfStack; } diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index a2d1e5d859cf58..3f2cd82efc057b 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -418,8 +418,9 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews // Does nothing } -- (void)insertReactSubview:(UIView *)view atIndex:(__unused NSInteger)atIndex +- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex { + [super insertReactSubview:view atIndex:atIndex]; if ([view isKindOfClass:[RCTRefreshControl class]]) { _scrollView.refreshControl = (RCTRefreshControl*)view; } else { @@ -431,21 +432,18 @@ - (void)insertReactSubview:(UIView *)view atIndex:(__unused NSInteger)atIndex - (void)removeReactSubview:(UIView *)subview { + [super removeReactSubview:subview]; if ([subview isKindOfClass:[RCTRefreshControl class]]) { _scrollView.refreshControl = nil; } else { RCTAssert(_contentView == subview, @"Attempted to remove non-existent subview"); _contentView = nil; - [subview removeFromSuperview]; } } -- (NSArray *)reactSubviews +- (void)didUpdateReactSubviews { - if (_contentView && _scrollView.refreshControl) { - return @[_contentView, _scrollView.refreshControl]; - } - return _contentView ? @[_contentView] : @[]; + // Do nothing, as subviews are managed by `insertReactSubview:atIndex:` } - (BOOL)centerContent diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index 67955a9bc2d671..ed3e4298890ec8 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -35,8 +35,13 @@ typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry */ @interface RCTShadowView : NSObject -- (NSArray *)reactSubviews; -- (RCTShadowView *)reactSuperview; +/** + * RCTComponent interface. + */ +- (NSArray *)reactSubviews NS_REQUIRES_SUPER; +- (RCTShadowView *)reactSuperview NS_REQUIRES_SUPER; +- (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex NS_REQUIRES_SUPER; +- (void)removeReactSubview:(RCTShadowView *)subview NS_REQUIRES_SUPER; @property (nonatomic, weak, readonly) RCTShadowView *superview; @property (nonatomic, assign, readonly) css_node_t *cssNode; @@ -181,6 +186,10 @@ typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry - (void)setTextComputed NS_REQUIRES_SUPER; - (BOOL)isTextDirty; +/** + * As described in RCTComponent protocol. + */ +- (void)didUpdateReactSubviews NS_REQUIRES_SUPER; - (void)didSetProps:(NSArray *)changedProps NS_REQUIRES_SUPER; /** diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index 83ce22a5beb1fe..d257e96594e93b 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -596,6 +596,11 @@ - (void)setBackgroundColor:(UIColor *)color [self dirtyPropagation]; } +- (void)didUpdateReactSubviews +{ + // Does nothing by default +} + - (void)didSetProps:(__unused NSArray *)changedProps { if (_recomputePadding) { diff --git a/React/Views/RCTTabBar.m b/React/Views/RCTTabBar.m index 6f31f69985e20c..d544d75b2efa48 100644 --- a/React/Views/RCTTabBar.m +++ b/React/Views/RCTTabBar.m @@ -26,13 +26,11 @@ @implementation RCTTabBar { BOOL _tabsChanged; UITabBarController *_tabController; - NSMutableArray *_tabViews; } - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { - _tabViews = [NSMutableArray new]; _tabController = [UITabBarController new]; _tabController.delegate = self; [self addSubview:_tabController.view]; @@ -53,31 +51,31 @@ - (void)dealloc [_tabController removeFromParentViewController]; } -- (NSArray *)reactSubviews +- (void)insertReactSubview:(RCTTabBarItem *)subview atIndex:(NSInteger)atIndex { - return _tabViews; -} - -- (void)insertReactSubview:(RCTTabBarItem *)view atIndex:(NSInteger)atIndex -{ - if (![view isKindOfClass:[RCTTabBarItem class]]) { + if (![subview isKindOfClass:[RCTTabBarItem class]]) { RCTLogError(@"subview should be of type RCTTabBarItem"); return; } - [_tabViews insertObject:view atIndex:atIndex]; + [super insertReactSubview:subview atIndex:atIndex]; _tabsChanged = YES; } - (void)removeReactSubview:(RCTTabBarItem *)subview { - if (_tabViews.count == 0) { + if (self.reactSubviews.count == 0) { RCTLogError(@"should have at least one view to remove a subview"); return; } - [_tabViews removeObject:subview]; + [super removeReactSubview:subview]; _tabsChanged = YES; } +- (void)didUpdateReactSubviews +{ + // Do nothing, as subviews are managed by `reactBridgeDidFinishTransaction` +} + - (void)layoutSubviews { [super layoutSubviews]; @@ -106,8 +104,9 @@ - (void)reactBridgeDidFinishTransaction _tabsChanged = NO; } - [_tabViews enumerateObjectsUsingBlock: - ^(RCTTabBarItem *tab, NSUInteger index, __unused BOOL *stop) { + [self.reactSubviews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger index, __unused BOOL *stop) { + + RCTTabBarItem *tab = (RCTTabBarItem *)view; UIViewController *controller = _tabController.viewControllers[index]; if (_unselectedTintColor) { [tab.barItem setTitleTextAttributes:@{NSForegroundColorAttributeName: _unselectedTintColor} forState:UIControlStateNormal]; @@ -165,7 +164,7 @@ - (void)setItemPositioning:(UITabBarItemPositioning)itemPositioning - (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController { NSUInteger index = [tabBarController.viewControllers indexOfObject:viewController]; - RCTTabBarItem *tab = _tabViews[index]; + RCTTabBarItem *tab = (RCTTabBarItem *)self.reactSubviews[index]; if (tab.onPress) tab.onPress(nil); return NO; } diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index ad2ea1424bbdaf..4d1b362ae4ff31 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -56,8 +56,7 @@ - (UIView *)react_findClipView UIView *clipView = nil; CGRect clipRect = self.bounds; // We will only look for a clipping view up the view hierarchy until we hit the root view. - BOOL passedRootView = NO; - while (testView && !passedRootView) { + while (testView) { if (testView.clipsToBounds) { if (clipView) { CGRect testRect = [clipView convertRect:clipRect toView:testView]; @@ -71,7 +70,7 @@ - (UIView *)react_findClipView } } if ([testView isReactRootView]) { - passedRootView = YES; + break; } testView = testView.superview; } @@ -97,7 +96,6 @@ - (UIView *)react_findClipView @implementation RCTView { - NSMutableArray *_reactSubviews; UIColor *_backgroundColor; } @@ -275,76 +273,31 @@ + (UIEdgeInsets)contentInsetsForView:(UIView *)view - (void)react_remountAllSubviews { - if (_reactSubviews) { - NSUInteger index = 0; - for (UIView *view in _reactSubviews) { + if (_removeClippedSubviews) { + for (UIView *view in self.reactSubviews) { if (view.superview != self) { - if (index < self.subviews.count) { - [self insertSubview:view atIndex:index]; - } else { - [self addSubview:view]; - } + [self addSubview:view]; [view react_remountAllSubviews]; } - index++; } } else { - // If react_subviews is nil, we must already be showing all subviews + // If _removeClippedSubviews is false, we must already be showing all subviews [super react_remountAllSubviews]; } } -- (void)remountSubview:(UIView *)view -{ - // Calculate insertion index for view - NSInteger index = 0; - for (UIView *subview in _reactSubviews) { - if (subview == view) { - [self insertSubview:view atIndex:index]; - break; - } - if (subview.superview) { - // View is mounted, so bump the index - index++; - } - } -} - -- (void)mountOrUnmountSubview:(UIView *)view withClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView -{ - if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) { - - // View is at least partially visible, so remount it if unmounted - if (view.superview == nil) { - [self remountSubview:view]; - } - - // Then test its subviews - if (CGRectContainsRect(clipRect, view.frame)) { - [view react_remountAllSubviews]; - } else { - [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; - } - - } else if (view.superview) { - - // View is completely outside the clipRect, so unmount it - [view removeFromSuperview]; - } -} - - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView { // TODO (#5906496): for scrollviews (the primary use-case) we could // optimize this by only doing a range check along the scroll axis, // instead of comparing the whole frame - if (_reactSubviews == nil) { + if (!_removeClippedSubviews) { // Use default behavior if unmounting is disabled return [super react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; } - if (_reactSubviews.count == 0) { + if (self.reactSubviews.count == 0) { // Do nothing if we have no subviews return; } @@ -360,61 +313,44 @@ - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView: clipView = self; // Mount / unmount views - for (UIView *view in _reactSubviews) { - [self mountOrUnmountSubview:view withClipRect:clipRect relativeToView:clipView]; - } -} + for (UIView *view in self.reactSubviews) { + if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) { -- (void)setRemoveClippedSubviews:(BOOL)removeClippedSubviews -{ - if (removeClippedSubviews && !_reactSubviews) { - _reactSubviews = [self.subviews mutableCopy]; - } else if (!removeClippedSubviews && _reactSubviews) { - [self react_remountAllSubviews]; - _reactSubviews = nil; - } -} - -- (BOOL)removeClippedSubviews -{ - return _reactSubviews != nil; -} - -- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex -{ - if (_reactSubviews == nil) { - [self insertSubview:view atIndex:atIndex]; - } else { - [_reactSubviews insertObject:view atIndex:atIndex]; - - // Find a suitable view to use for clipping - UIView *clipView = [self react_findClipView]; - if (clipView) { + // View is at least partially visible, so remount it if unmounted + [self addSubview:view]; - // If possible, don't add subviews if they are clipped - [self mountOrUnmountSubview:view withClipRect:clipView.bounds relativeToView:clipView]; + // Then test its subviews + if (CGRectContainsRect(clipRect, view.frame)) { + // View is fully visible, so remount all subviews + [view react_remountAllSubviews]; + } else { + // View is partially visible, so update clipped subviews + [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; + } - } else { + } else if (view.superview) { - // Fallback if we can't find a suitable clipView - [self remountSubview:view]; + // View is completely outside the clipRect, so unmount it + [view removeFromSuperview]; } } } -- (void)removeReactSubview:(UIView *)subview +- (void)setRemoveClippedSubviews:(BOOL)removeClippedSubviews { - [_reactSubviews removeObject:subview]; - [subview removeFromSuperview]; + if (!removeClippedSubviews && _removeClippedSubviews) { + [self react_remountAllSubviews]; + } + _removeClippedSubviews = removeClippedSubviews; } -- (NSArray *)reactSubviews +- (void)didUpdateReactSubviews { - // The _reactSubviews array is only used when we have hidden - // offscreen views. If _reactSubviews is nil, we can assume - // that [self reactSubviews] and [self subviews] are the same - - return _reactSubviews ?: self.subviews; + if (_removeClippedSubviews) { + [self updateClippedSubviews]; + } else { + [super didUpdateReactSubviews]; + } } - (void)updateClippedSubviews @@ -435,7 +371,7 @@ - (void)layoutSubviews [super layoutSubviews]; - if (_reactSubviews) { + if (_removeClippedSubviews) { [self updateClippedSubviews]; } } diff --git a/React/Views/RCTViewManager.h b/React/Views/RCTViewManager.h index df532c1eb7aac7..d963e2b4b03217 100644 --- a/React/Views/RCTViewManager.h +++ b/React/Views/RCTViewManager.h @@ -11,10 +11,10 @@ #import "RCTBridgeModule.h" #import "RCTConvert.h" -#import "RCTComponent.h" #import "RCTDefines.h" #import "RCTEventDispatcher.h" #import "RCTLog.h" +#import "UIView+React.h" @class RCTBridge; @class RCTShadowView; diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index eeea7cbce0d211..899b3f0317801e 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -115,8 +115,8 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(__unused NSDictio RCT_REMAP_VIEW_PROPERTY(testID, accessibilityIdentifier, NSString) RCT_REMAP_VIEW_PROPERTY(backfaceVisibility, layer.doubleSided, css_backface_visibility_t) RCT_REMAP_VIEW_PROPERTY(opacity, alpha, CGFloat) -RCT_REMAP_VIEW_PROPERTY(shadowColor, layer.shadowColor, CGColor); -RCT_REMAP_VIEW_PROPERTY(shadowOffset, layer.shadowOffset, CGSize); +RCT_REMAP_VIEW_PROPERTY(shadowColor, layer.shadowColor, CGColor) +RCT_REMAP_VIEW_PROPERTY(shadowOffset, layer.shadowOffset, CGSize) RCT_REMAP_VIEW_PROPERTY(shadowOpacity, layer.shadowOpacity, float) RCT_REMAP_VIEW_PROPERTY(shadowRadius, layer.shadowRadius, CGFloat) RCT_REMAP_VIEW_PROPERTY(overflow, clipsToBounds, css_clip_t) diff --git a/React/Views/UIView+React.h b/React/Views/UIView+React.h index 794c977e9e7413..fa779e5208a821 100644 --- a/React/Views/UIView+React.h +++ b/React/Views/UIView+React.h @@ -17,8 +17,19 @@ @interface UIView (React) -- (NSArray *)reactSubviews; -- (UIView *)reactSuperview; +/** + * RCTComponent interface. + */ +- (NSArray *)reactSubviews NS_REQUIRES_SUPER; +- (UIView *)reactSuperview NS_REQUIRES_SUPER; +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex NS_REQUIRES_SUPER; +- (void)removeReactSubview:(UIView *)subview NS_REQUIRES_SUPER; + +/** + * Updates the subviews array based on the reactSubviews. Default behavior is + * to insert the reactSubviews into the UIView. + */ +- (void)didUpdateReactSubviews; /** * Used by the UIIManager to set the view frame. diff --git a/React/Views/UIView+React.m b/React/Views/UIView+React.m index e7736f3a125651..f25c716447741a 100644 --- a/React/Views/UIView+React.m +++ b/React/Views/UIView+React.m @@ -56,25 +56,42 @@ - (NSNumber *)reactTagAtPoint:(CGPoint)point return view.reactTag; } -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +- (NSArray *)reactSubviews { - [self insertSubview:subview atIndex:atIndex]; + return objc_getAssociatedObject(self, _cmd); } -- (void)removeReactSubview:(UIView *)subview +- (UIView *)reactSuperview { - RCTAssert(subview.superview == self, @"%@ is a not a subview of %@", subview, self); - [subview removeFromSuperview]; + return self.superview; } -- (NSArray *)reactSubviews +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { - return self.subviews; + // We access the associated object directly here in case someone overrides + // the `reactSubviews` getter method and returns an immutable array. + NSMutableArray *subviews = objc_getAssociatedObject(self, @selector(reactSubviews)); + if (!subviews) { + subviews = [NSMutableArray new]; + objc_setAssociatedObject(self, @selector(reactSubviews), subviews, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + [subviews insertObject:subview atIndex:atIndex]; } -- (UIView *)reactSuperview +- (void)removeReactSubview:(UIView *)subview { - return self.superview; + // We access the associated object directly here in case someone overrides + // the `reactSubviews` getter method and returns an immutable array. + NSMutableArray *subviews = objc_getAssociatedObject(self, @selector(reactSubviews)); + [subviews removeObject:subview]; + [subview removeFromSuperview]; +} + +- (void)didUpdateReactSubviews +{ + for (UIView *subview in self.reactSubviews) { + [self addSubview:subview]; + } } - (void)reactSetFrame:(CGRect)frame From 774290093139f7bfa2670717de7ddf878f3a8316 Mon Sep 17 00:00:00 2001 From: Sreejumon Date: Tue, 7 Jun 2016 01:50:57 -0700 Subject: [PATCH 252/843] Fix the WebSocket sendBinary error Summary: Fixing the WebSocket SendBinary method error. This is a regression caused during recent change. Closes https://github.com/facebook/react-native/pull/7956 Differential Revision: D3398065 fbshipit-source-id: 5d56eba807b59d1f3265cba5d5f501d610afebf2 --- .../com/facebook/react/modules/websocket/WebSocketModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java index e7eae279bc14b0..5f318cab69c06b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java @@ -223,7 +223,7 @@ public void sendBinary(String base64String, int id) { throw new RuntimeException("Cannot send a message. Unknown WebSocket id " + id); } try { - client.sendMessage(RequestBody.create(WebSocket.TEXT, ByteString.decodeBase64(base64String))); + client.sendMessage(RequestBody.create(WebSocket.BINARY, ByteString.decodeBase64(base64String))); } catch (IOException | IllegalStateException e) { notifyWebSocketFailed(id, e.getMessage()); } From 2c59dca7b674656f002c0e306d81c91914e95427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=93=E5=9B=BD=E6=A2=81?= Date: Tue, 7 Jun 2016 01:58:19 -0700 Subject: [PATCH 253/843] no need both iOSLink and AndroidLink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: **motivation** if users showcase data only have `linkAppStore` or `linkPlayStore` , not both. They will be render as a not link block. like `D.I.T.` or `烘焙帮` **Test plan** Befor: ![2016-06-07 2 03 27](https://cloud.githubusercontent.com/assets/73235/15847463/a9c5922c-2cb8-11e6-902e-32a942d4f1bd.png) After: ![2016-06-07 2 03 15](https://cloud.githubusercontent.com/assets/73235/15847467/b039df50-2cb8-11e6-893c-7cc25d876a5c.png) Closes https://github.com/facebook/react-native/pull/7966 Differential Revision: D3398002 fbshipit-source-id: 4c9cbbe34fbda5d84a7165415c67559191b08b25 --- website/src/react-native/showcase.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/website/src/react-native/showcase.js b/website/src/react-native/showcase.js index 82b46c05fd13b5..52c9575016e0ad 100644 --- a/website/src/react-native/showcase.js +++ b/website/src/react-native/showcase.js @@ -1087,7 +1087,7 @@ var AppList = React.createClass({
    {app.name}

    {app.name}

    - {app.linkAppStore && app.linkPlayStore ? this._renderLinks(app) : null} + {app.linkAppStore || app.linkPlayStore ? this._renderLinks(app) : null}

    By {app.author}

    {this._renderBlogPosts(app)} {this._renderSourceLink(app)} @@ -1167,11 +1167,14 @@ var AppList = React.createClass({ }, _renderLinks: function(app) { + var linkAppStore = app.linkAppStore ? iOS : ''; + var linkPlayStore = app.linkPlayStore ? Android : ''; + return (

    - iOS - {' '}·{' '} - Android + {linkAppStore} + {linkAppStore && linkPlayStore ? ' · ' : ''} + {linkPlayStore}

    ); }, From d836e8c0ef97d6e83fd12fe7b0734e4122826835 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Tue, 7 Jun 2016 02:47:52 -0700 Subject: [PATCH 254/843] Reverted commit D3374937 Reviewed By: mhorowitz Differential Revision: D3374937 fbshipit-source-id: e794fb9b8cb1194133da64dcb38861477406c307 --- .../react/bridge/JSCJavaScriptExecutor.java | 8 ++++---- .../com/facebook/react/bridge/NativeArray.java | 2 +- .../java/com/facebook/react/bridge/NativeMap.java | 2 +- .../react/bridge/ProxyJavaScriptExecutor.java | 8 ++++---- .../com/facebook/react/bridge/ReactBridge.java | 8 +------- .../react/bridge/ReadableNativeArray.java | 3 ++- .../facebook/react/bridge/ReadableNativeMap.java | 3 ++- .../react/bridge/WritableNativeArray.java | 3 ++- .../facebook/react/bridge/WritableNativeMap.java | 2 +- ReactAndroid/src/main/jni/react/jni/Android.mk | 10 ++++++++-- ReactAndroid/src/main/jni/react/jni/BUCK | 15 ++++++++++++++- .../jni/{xreact => react}/jni/NativeArray.cpp | 0 .../main/jni/{xreact => react}/jni/NativeArray.h | 0 .../jni/{xreact => react}/jni/NativeCommon.cpp | 0 .../main/jni/{xreact => react}/jni/NativeCommon.h | 0 .../main/jni/{xreact => react}/jni/NativeMap.cpp | 0 .../main/jni/{xreact => react}/jni/NativeMap.h | 0 ReactAndroid/src/main/jni/react/jni/OnLoad.cpp | 14 +++++++++++--- .../{xreact => react}/jni/ReadableNativeArray.cpp | 0 .../{xreact => react}/jni/ReadableNativeArray.h | 0 .../{xreact => react}/jni/ReadableNativeMap.cpp | 0 .../jni/{xreact => react}/jni/ReadableNativeMap.h | 0 .../{xreact => react}/jni/WritableNativeArray.cpp | 0 .../{xreact => react}/jni/WritableNativeArray.h | 0 .../{xreact => react}/jni/WritableNativeMap.cpp | 0 .../jni/{xreact => react}/jni/WritableNativeMap.h | 1 - ReactAndroid/src/main/jni/xreact/jni/Android.mk | 12 +++--------- ReactAndroid/src/main/jni/xreact/jni/BUCK | 10 ++-------- .../main/jni/xreact/jni/CatalystInstanceImpl.cpp | 3 ++- .../src/main/jni/xreact/jni/CxxModuleWrapper.cpp | 11 +++++------ ReactAndroid/src/main/jni/xreact/jni/JCallback.h | 2 +- .../src/main/jni/xreact/jni/JSCPerfLogging.cpp | 3 +-- .../src/main/jni/xreact/jni/JSLogging.cpp | 3 +-- .../src/main/jni/xreact/jni/MethodInvoker.cpp | 2 +- .../main/jni/xreact/jni/ModuleRegistryHolder.cpp | 5 +++-- ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp | 12 +----------- 36 files changed, 71 insertions(+), 71 deletions(-) rename ReactAndroid/src/main/jni/{xreact => react}/jni/NativeArray.cpp (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/NativeArray.h (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/NativeCommon.cpp (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/NativeCommon.h (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/NativeMap.cpp (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/NativeMap.h (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/ReadableNativeArray.cpp (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/ReadableNativeArray.h (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/ReadableNativeMap.cpp (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/ReadableNativeMap.h (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/WritableNativeArray.cpp (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/WritableNativeArray.h (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/WritableNativeMap.cpp (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/WritableNativeMap.h (96%) diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java index 4f895abf31aa26..b4431c0d66d9cb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java @@ -14,10 +14,6 @@ @DoNotStrip public class JSCJavaScriptExecutor extends JavaScriptExecutor { - static { - ReactBridge.staticInit(); - } - public static class Factory implements JavaScriptExecutor.Factory { @Override public JavaScriptExecutor create(WritableNativeMap jscConfig) throws Exception { @@ -25,6 +21,10 @@ public JavaScriptExecutor create(WritableNativeMap jscConfig) throws Exception { } } + static { + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + } + public JSCJavaScriptExecutor(WritableNativeMap jscConfig) { initialize(jscConfig); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java index d4c46f72302c53..2045a4b23e0016 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java @@ -19,7 +19,7 @@ @DoNotStrip public abstract class NativeArray { static { - ReactBridge.staticInit(); + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); } protected NativeArray(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java index 9e192dc4ea6d8d..2c683f9451130e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java @@ -19,7 +19,7 @@ @DoNotStrip public abstract class NativeMap { static { - ReactBridge.staticInit(); + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); } public NativeMap(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java index 6ebf4dc0df6e01..37d83b3386174c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java @@ -24,10 +24,6 @@ */ @DoNotStrip public class ProxyJavaScriptExecutor extends JavaScriptExecutor { - static { - ReactBridge.staticInit(); - } - public static class Factory implements JavaScriptExecutor.Factory { private final JavaJSExecutor.Factory mJavaJSExecutorFactory; @@ -41,6 +37,10 @@ public JavaScriptExecutor create(WritableNativeMap jscConfig) throws Exception { } } + static { + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + } + private @Nullable JavaJSExecutor mJavaJSExecutor; /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java index 6cf4170d463176..e4d43f90b6a4f9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java @@ -24,22 +24,16 @@ @DoNotStrip public class ReactBridge extends Countable { - private static final String REACT_NATIVE_LIB = "reactnativejni"; - private static final String XREACT_NATIVE_LIB = "reactnativejnifb"; + /* package */ static final String REACT_NATIVE_LIB = "reactnativejni"; static { SoLoader.loadLibrary(REACT_NATIVE_LIB); - SoLoader.loadLibrary(XREACT_NATIVE_LIB); } private final ReactCallback mCallback; private final JavaScriptExecutor mJSExecutor; private final MessageQueueThread mNativeModulesQueueThread; - public static void staticInit() { - // This is just called to ensure that ReactBridge's static initialization has taken place. - } - /** * @param jsExecutor the JS executor to use to run JS * @param callback the callback class used to invoke native modules diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java index 2f81d301b60bf9..4cd1b75814e80b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java @@ -21,8 +21,9 @@ */ @DoNotStrip public class ReadableNativeArray extends NativeArray implements ReadableArray { + static { - ReactBridge.staticInit(); + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); } protected ReadableNativeArray(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java index ea289ac668f2e0..f3782ca0896f75 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java @@ -22,8 +22,9 @@ */ @DoNotStrip public class ReadableNativeMap extends NativeMap implements ReadableMap { + static { - ReactBridge.staticInit(); + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); } protected ReadableNativeMap(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java index e6c343264a8d2c..26fe2dd11f6228 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java @@ -21,8 +21,9 @@ */ @DoNotStrip public class WritableNativeArray extends ReadableNativeArray implements WritableArray { + static { - ReactBridge.staticInit(); + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); } public WritableNativeArray() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java index 6b6c639d2e032b..d30827ade5bf12 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java @@ -22,7 +22,7 @@ @DoNotStrip public class WritableNativeMap extends ReadableNativeMap implements WritableMap { static { - ReactBridge.staticInit(); + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); } @Override diff --git a/ReactAndroid/src/main/jni/react/jni/Android.mk b/ReactAndroid/src/main/jni/react/jni/Android.mk index 06266de376fc6e..742e2161d1a0b6 100644 --- a/ReactAndroid/src/main/jni/react/jni/Android.mk +++ b/ReactAndroid/src/main/jni/react/jni/Android.mk @@ -11,8 +11,15 @@ LOCAL_SRC_FILES := \ JSLoader.cpp \ JSLogging.cpp \ JniJSModulesUnbundle.cpp \ + NativeArray.cpp \ + NativeCommon.cpp \ + NativeMap.cpp \ OnLoad.cpp \ ProxyExecutor.cpp \ + ReadableNativeArray.cpp \ + ReadableNativeMap.cpp \ + WritableNativeArray.cpp \ + WritableNativeMap.cpp \ LOCAL_C_INCLUDES := $(LOCAL_PATH) LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/../.. $(LOCAL_PATH)/.. @@ -23,7 +30,7 @@ LOCAL_CFLAGS += $(CXX11_FLAGS) LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) LOCAL_LDLIBS += -landroid -LOCAL_SHARED_LIBRARIES := libfolly_json libfb libjsc libglog_init libreactnativejnifb +LOCAL_SHARED_LIBRARIES := libfolly_json libfb libjsc libglog_init LOCAL_STATIC_LIBRARIES := libreactnative include $(BUILD_SHARED_LIBRARY) @@ -34,4 +41,3 @@ $(call import-module,folly) $(call import-module,fbgloginit) $(call import-module,fb) $(call import-module,jsc) -$(call import-module,xreact/jni) diff --git a/ReactAndroid/src/main/jni/react/jni/BUCK b/ReactAndroid/src/main/jni/react/jni/BUCK index e210b9dd9575eb..9bd754fb70e92f 100644 --- a/ReactAndroid/src/main/jni/react/jni/BUCK +++ b/ReactAndroid/src/main/jni/react/jni/BUCK @@ -18,7 +18,6 @@ def jni_library(**kwargs): ], deps = DEPS + JSC_DEPS + [ react_native_target('jni/react:react'), - react_native_target('jni/xreact/jni:jni'), ], **kwargs ) @@ -34,8 +33,15 @@ jni_library( 'JSLoader.cpp', 'JSLogging.cpp', 'JniJSModulesUnbundle.cpp', + 'NativeArray.cpp', + 'NativeCommon.cpp', + 'NativeMap.cpp', 'OnLoad.cpp', 'ProxyExecutor.cpp', + 'ReadableNativeArray.cpp', + 'ReadableNativeMap.cpp', + 'WritableNativeArray.cpp', + 'WritableNativeMap.cpp', ], headers = [ 'JSLoader.h', @@ -50,6 +56,13 @@ jni_library( 'WebWorkers.h', ], exported_headers = [ + 'NativeCommon.h', + 'NativeArray.h', + 'NativeMap.h', + 'ReadableNativeArray.h', + 'ReadableNativeMap.h', + 'WritableNativeArray.h', + 'WritableNativeMap.h', ], preprocessor_flags = [ '-DLOG_TAG="ReactNativeJNI"', diff --git a/ReactAndroid/src/main/jni/xreact/jni/NativeArray.cpp b/ReactAndroid/src/main/jni/react/jni/NativeArray.cpp similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/NativeArray.cpp rename to ReactAndroid/src/main/jni/react/jni/NativeArray.cpp diff --git a/ReactAndroid/src/main/jni/xreact/jni/NativeArray.h b/ReactAndroid/src/main/jni/react/jni/NativeArray.h similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/NativeArray.h rename to ReactAndroid/src/main/jni/react/jni/NativeArray.h diff --git a/ReactAndroid/src/main/jni/xreact/jni/NativeCommon.cpp b/ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/NativeCommon.cpp rename to ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp diff --git a/ReactAndroid/src/main/jni/xreact/jni/NativeCommon.h b/ReactAndroid/src/main/jni/react/jni/NativeCommon.h similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/NativeCommon.h rename to ReactAndroid/src/main/jni/react/jni/NativeCommon.h diff --git a/ReactAndroid/src/main/jni/xreact/jni/NativeMap.cpp b/ReactAndroid/src/main/jni/react/jni/NativeMap.cpp similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/NativeMap.cpp rename to ReactAndroid/src/main/jni/react/jni/NativeMap.cpp diff --git a/ReactAndroid/src/main/jni/xreact/jni/NativeMap.h b/ReactAndroid/src/main/jni/react/jni/NativeMap.h similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/NativeMap.h rename to ReactAndroid/src/main/jni/react/jni/NativeMap.h diff --git a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp index 63fee95574aaca..8d60719c4e9975 100644 --- a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp +++ b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp @@ -22,6 +22,8 @@ #include "JExecutorTokenFactory.h" #include "JNativeRunnable.h" #include "JSLoader.h" +#include "NativeCommon.h" +#include "ReadableNativeArray.h" #include "ProxyExecutor.h" #include "OnLoad.h" #include "JMessageQueueThread.h" @@ -29,9 +31,7 @@ #include "JSLogging.h" #include "JSCPerfLogging.h" #include "WebWorkers.h" - -#include -#include +#include "WritableNativeMap.h" #include #ifdef WITH_FBSYSTRACE @@ -456,9 +456,17 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { PerfLogging::installNativeHooks = addNativePerfLoggingHooks; JSLogging::nativeHook = nativeLoggingHook; + NativeArray::registerNatives(); + ReadableNativeArray::registerNatives(); + WritableNativeArray::registerNatives(); JNativeRunnable::registerNatives(); registerJSLoaderNatives(); + NativeMap::registerNatives(); + ReadableNativeMap::registerNatives(); + WritableNativeMap::registerNatives(); + ReadableNativeMapKeySetIterator::registerNatives(); + registerNatives("com/facebook/react/bridge/JSCJavaScriptExecutor", { makeNativeMethod("initialize", executors::createJSCExecutor), }); diff --git a/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.cpp b/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.cpp rename to ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp diff --git a/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.h b/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.h similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.h rename to ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.h diff --git a/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.cpp b/ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.cpp similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.cpp rename to ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.cpp diff --git a/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.h b/ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.h similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.h rename to ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.h diff --git a/ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.cpp b/ReactAndroid/src/main/jni/react/jni/WritableNativeArray.cpp similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.cpp rename to ReactAndroid/src/main/jni/react/jni/WritableNativeArray.cpp diff --git a/ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.h b/ReactAndroid/src/main/jni/react/jni/WritableNativeArray.h similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.h rename to ReactAndroid/src/main/jni/react/jni/WritableNativeArray.h diff --git a/ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.cpp b/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.cpp similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.cpp rename to ReactAndroid/src/main/jni/react/jni/WritableNativeMap.cpp diff --git a/ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.h b/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h similarity index 96% rename from ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.h rename to ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h index 1fc942a025e5e1..cf9cd95a000d88 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.h +++ b/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h @@ -20,7 +20,6 @@ struct WritableNativeMap : jni::HybridClass initHybrid(jni::alias_ref); - __attribute__((visibility("default"))) folly::dynamic consume(); void putNull(std::string key); diff --git a/ReactAndroid/src/main/jni/xreact/jni/Android.mk b/ReactAndroid/src/main/jni/xreact/jni/Android.mk index 4eb04b448854f4..c0078c36a8a157 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/Android.mk +++ b/ReactAndroid/src/main/jni/xreact/jni/Android.mk @@ -9,21 +9,14 @@ LOCAL_SRC_FILES := \ CxxModuleWrapper.cpp \ JExecutorToken.cpp \ JMessageQueueThread.cpp \ + JniJSModulesUnbundle.cpp \ JSCPerfLogging.cpp \ JSLoader.cpp \ JSLogging.cpp \ - JniJSModulesUnbundle.cpp \ MethodInvoker.cpp \ ModuleRegistryHolder.cpp \ - NativeArray.cpp \ - NativeCommon.cpp \ - NativeMap.cpp \ OnLoad.cpp \ ProxyExecutor.cpp \ - ReadableNativeArray.cpp \ - ReadableNativeMap.cpp \ - WritableNativeArray.cpp \ - WritableNativeMap.cpp \ LOCAL_C_INCLUDES := $(LOCAL_PATH) LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/../.. @@ -34,7 +27,7 @@ LOCAL_CFLAGS += $(CXX11_FLAGS) LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) LOCAL_LDLIBS += -landroid -LOCAL_SHARED_LIBRARIES := libfolly_json libfbjni libjsc libglog_init +LOCAL_SHARED_LIBRARIES := libfolly_json libfbjni libjsc libglog_init libreactnativejni LOCAL_STATIC_LIBRARIES := libreactnativefb include $(BUILD_SHARED_LIBRARY) @@ -44,3 +37,4 @@ $(call import-module,jsc) $(call import-module,folly) $(call import-module,fbgloginit) $(call import-module,jsc) +$(call import-module,react/jni) diff --git a/ReactAndroid/src/main/jni/xreact/jni/BUCK b/ReactAndroid/src/main/jni/xreact/jni/BUCK index cb47fa436a1b1c..ea88102d01c483 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/BUCK +++ b/ReactAndroid/src/main/jni/xreact/jni/BUCK @@ -5,25 +5,19 @@ SUPPORTED_PLATFORMS = '^android-(armv7|x86)$' EXPORTED_HEADERS = [ 'CxxModuleWrapper.h', - 'NativeArray.h', - 'NativeCommon.h', - 'NativeMap.h', - 'ReadableNativeArray.h', - 'ReadableNativeMap.h', - 'WritableNativeArray.h', - 'WritableNativeMap.h', ] cxx_library( name='jni', soname = 'libreactnativejnifb.so', - header_namespace = 'xreact/jni', + header_namespace = 'react/jni', supported_platforms_regex = SUPPORTED_PLATFORMS, deps = JSC_DEPS + [ '//native/fb:fb', '//native/third-party/android-ndk:android', '//xplat/folly:molly', '//xplat/fbsystrace:fbsystrace', + react_native_target('jni/react/jni:jni'), react_native_xplat_target('cxxreact:bridge'), react_native_xplat_target('cxxreact:module'), ], diff --git a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp index 64fa80f98100f2..c4c6faa74b2899 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp @@ -13,6 +13,8 @@ #include #include +#include + #include #include #include @@ -21,7 +23,6 @@ #include "JavaScriptExecutorHolder.h" #include "JniJSModulesUnbundle.h" #include "ModuleRegistryHolder.h" -#include "NativeArray.h" #include "JNativeRunnable.h" using namespace facebook::jni; diff --git a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp index 9267d876e963c5..d1532fd10dd2b9 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp @@ -2,11 +2,16 @@ #include "CxxModuleWrapper.h" +#include + #include #include #include #include +#include +#include + #include #include @@ -15,12 +20,6 @@ #include #include -#include -#include - -#include "ReadableNativeArray.h" - - using namespace facebook::jni; using namespace facebook::xplat::module; using namespace facebook::react; diff --git a/ReactAndroid/src/main/jni/xreact/jni/JCallback.h b/ReactAndroid/src/main/jni/xreact/jni/JCallback.h index 1d1393eb95bca9..539273104dced2 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/JCallback.h +++ b/ReactAndroid/src/main/jni/xreact/jni/JCallback.h @@ -7,7 +7,7 @@ #include #include -#include "NativeArray.h" +#include namespace facebook { namespace react { diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp index 1dd837db87bf61..1b7b5e6887b6fa 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp @@ -2,10 +2,9 @@ #include "JSCPerfLogging.h" -#include - #include #include +#include using namespace facebook::jni; diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp index a27d3f5d35e732..7690a5656ebd8d 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp @@ -4,10 +4,9 @@ #include #include +#include #include -#include - namespace facebook { namespace react { diff --git a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp index e7d3d2c2d0718b..218f6c9d0ac2d3 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp @@ -2,6 +2,7 @@ #include "MethodInvoker.h" +#include #ifdef WITH_FBSYSTRACE #include #endif @@ -9,7 +10,6 @@ #include "ModuleRegistryHolder.h" #include "JCallback.h" #include "JExecutorToken.h" -#include "ReadableNativeArray.h" namespace facebook { namespace react { diff --git a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp index eebed41df0c624..29349f741af1dc 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp @@ -10,10 +10,11 @@ #include #include #include +#include -#include "CatalystInstanceImpl.h" #include "MethodInvoker.h" -#include "ReadableNativeArray.h" + +#include "CatalystInstanceImpl.h" using facebook::xplat::module::CxxModule; diff --git a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp index 952a6b46c4c4ff..3242c59ca2bd63 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "CatalystInstanceImpl.h" #include "JavaScriptExecutorHolder.h" #include "JSCPerfLogging.h" @@ -16,9 +17,6 @@ #include "WebWorkers.h" #include "JCallback.h" -#include "WritableNativeMap.h" -#include "WritableNativeArray.h" - #include using namespace facebook::jni; @@ -182,14 +180,6 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { CxxModuleWrapper::registerNatives(); JCallbackImpl::registerNatives(); registerJSLoaderNatives(); - - NativeArray::registerNatives(); - ReadableNativeArray::registerNatives(); - WritableNativeArray::registerNatives(); - NativeMap::registerNatives(); - ReadableNativeMap::registerNatives(); - WritableNativeMap::registerNatives(); - ReadableNativeMapKeySetIterator::registerNatives(); }); } From 46c3af994121be073cb38c191bfae3bb1f6d10be Mon Sep 17 00:00:00 2001 From: hlynurf Date: Tue, 7 Jun 2016 02:48:19 -0700 Subject: [PATCH 255/843] Add Dohop to showcase Summary: Just released on Play Store and iOS has been in production since November. Closes https://github.com/facebook/react-native/pull/7874 Differential Revision: D3397811 fbshipit-source-id: 3ed293c1baff59c9171814ed7d34402beb30b383 --- website/src/react-native/showcase.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/website/src/react-native/showcase.js b/website/src/react-native/showcase.js index 52c9575016e0ad..52006ea0f268f6 100644 --- a/website/src/react-native/showcase.js +++ b/website/src/react-native/showcase.js @@ -395,6 +395,13 @@ var apps = [ ], author: 'Genki Takiuchi (s21g Inc.)', }, + { + name: 'Dohop Flights', + icon: 'http://a5.mzstatic.com/us/r30/Purple60/v4/3e/94/e9/3e94e9b3-f9a0-7b27-1824-b3da732ec967/icon175x175.jpeg', + linkAppStore: 'https://itunes.apple.com/us/app/dohop-flights-your-new-flight/id964170399', + linkPlayStore: 'https://play.google.com/store/apps/details?id=com.dohop', + author: 'Dohop', + }, { name: 'DONUT chatrooms for communities', icon: 'http://a2.mzstatic.com/eu/r30/Purple49/v4/d4/2d/e5/d42de510-6802-2694-1b60-ca80ffa1e2cb/icon175x175.png', From 592d5fb8f37fd9a250554cd1485bf477a262b19e Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Tue, 7 Jun 2016 04:39:47 -0700 Subject: [PATCH 256/843] Changed version of React dependency to ~ Summary: React has some internal code related only to React Native. Some times React needs to update them which will result in a breaking change for React Native but it would not be a breaking change for React public API, so no major version bump will happen. It means that a non breaking API bump may result in a break for React Native. That is why React Native should not depend on a ^ of React version. However React is a peer dependency to React Native and we want to give some flexibility to users to update React independently, so we don't fix the version strict and use ~ to allow patches. This will be fixed once we extract RN specific code from React into an independent dependency. Reviewed By: avaly Differential Revision: D3398202 fbshipit-source-id: cca520f4b80c9ed5ae6fb1444f3d0bf7ffb9c9dd --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ef34429ccd699c..8420977d49b4cd 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "react-native": "local-cli/wrong-react-native.js" }, "peerDependencies": { - "react": "^15.1.0" + "react": "~15.1.0" }, "dependencies": { "absolute-path": "^0.0.0", @@ -201,7 +201,7 @@ "flow-bin": "^0.26.0", "jest": "12.1.1", "portfinder": "0.4.0", - "react": "^15.1.0", + "react": "~15.1.0", "shelljs": "0.6.0" } } From 748a5078619340cf441594e66af727664823cc3d Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 7 Jun 2016 05:29:13 -0700 Subject: [PATCH 257/843] Simplified Image.js Summary: Now that we no longer have a separate NetworkImageView implementation, we can remove that code path from Image.js I've also moved the prefetch method into RCTImageViewManager for consistency with the getImageSize method, which means we no longer need to export the RCTImageLoader module to js. Reviewed By: javache Differential Revision: D3398157 fbshipit-source-id: fbbcf90a61549831ad28bad0cb3b50c375aae32c --- Libraries/Image/Image.ios.js | 69 +++++++++++---------------- Libraries/Image/RCTImageLoader.h | 2 +- Libraries/Image/RCTImageLoader.m | 24 ---------- Libraries/Image/RCTImageViewManager.m | 23 ++++++++- 4 files changed, 49 insertions(+), 69 deletions(-) diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 13eea466027c7e..86ae5e1920c2a9 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -11,27 +11,23 @@ */ 'use strict'; -var EdgeInsetsPropType = require('EdgeInsetsPropType'); -var ImageResizeMode = require('ImageResizeMode'); -var ImageSourcePropType = require('ImageSourcePropType'); -var ImageStylePropTypes = require('ImageStylePropTypes'); -var NativeMethodsMixin = require('NativeMethodsMixin'); -var NativeModules = require('NativeModules'); -var PropTypes = require('ReactPropTypes'); -var React = require('React'); -var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); -var StyleSheet = require('StyleSheet'); -var StyleSheetPropType = require('StyleSheetPropType'); - -var flattenStyle = require('flattenStyle'); -var requireNativeComponent = require('requireNativeComponent'); -var resolveAssetSource = require('resolveAssetSource'); - -var { - ImageLoader, - ImageViewManager, - NetworkImageViewManager, -} = NativeModules; +const EdgeInsetsPropType = require('EdgeInsetsPropType'); +const ImageResizeMode = require('ImageResizeMode'); +const ImageSourcePropType = require('ImageSourcePropType'); +const ImageStylePropTypes = require('ImageStylePropTypes'); +const NativeMethodsMixin = require('NativeMethodsMixin'); +const NativeModules = require('NativeModules'); +const PropTypes = require('ReactPropTypes'); +const React = require('React'); +const ReactNativeViewAttributes = require('ReactNativeViewAttributes'); +const StyleSheet = require('StyleSheet'); +const StyleSheetPropType = require('StyleSheetPropType'); + +const flattenStyle = require('flattenStyle'); +const requireNativeComponent = require('requireNativeComponent'); +const resolveAssetSource = require('resolveAssetSource'); + +const ImageViewManager = NativeModules.ImageViewManager; /** * A React component for displaying different types of images, @@ -57,7 +53,7 @@ var { * }, * ``` */ -var Image = React.createClass({ +const Image = React.createClass({ propTypes: { style: StyleSheetPropType(ImageStylePropTypes), /** @@ -195,7 +191,7 @@ var Image = React.createClass({ * cache */ prefetch(url: string) { - return ImageLoader.prefetchImage(url); + return ImageViewManager.prefetchImage(url); }, }, @@ -211,20 +207,11 @@ var Image = React.createClass({ }, render: function() { - var source = resolveAssetSource(this.props.source) || { uri: undefined, width: undefined, height: undefined }; - var {width, height, uri} = source; - var style = flattenStyle([{width, height}, styles.base, this.props.style]) || {}; - - var isNetwork = uri && uri.match(/^https?:/); - var RawImage = isNetwork ? RCTNetworkImageView : RCTImageView; - var resizeMode = this.props.resizeMode || (style || {}).resizeMode || 'cover'; // Workaround for flow bug t7737108 - var tintColor = (style || {}).tintColor; // Workaround for flow bug t7737108 - - // This is a workaround for #8243665. RCTNetworkImageView does not support tintColor - // TODO: Remove this hack once we have one image implementation #8389274 - if (isNetwork && (tintColor || this.props.blurRadius)) { - RawImage = RCTImageView; - } + const source = resolveAssetSource(this.props.source) || { uri: undefined, width: undefined, height: undefined }; + const {width, height, uri} = source; + const style = flattenStyle([{width, height}, styles.base, this.props.style]) || {}; + const resizeMode = this.props.resizeMode || (style || {}).resizeMode || 'cover'; // Workaround for flow bug t7737108 + const tintColor = (style || {}).tintColor; // Workaround for flow bug t7737108 if (uri === '') { console.warn('source.uri should not be an empty string'); @@ -235,7 +222,7 @@ var Image = React.createClass({ } return ( - Date: Tue, 7 Jun 2016 06:56:52 -0700 Subject: [PATCH 258/843] Enable Double R shortcut to reload JS when redbox is shown on Android Summary: Make "double tap R" shortcut enabled when redbox is shown in RN Android, consistent with that in iOS. Reviewed By: mkonicek Differential Revision: D3390132 fbshipit-source-id: 48fc40c2ba371a34abcac42a077359d11e907dfc --- .../com/facebook/react/ReactActivity.java | 31 ++++++------- .../devsupport/DoubleTapReloadRecognizer.java | 44 +++++++++++++++++++ .../react/devsupport/RedBoxDialog.java | 6 ++- 3 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/DoubleTapReloadRecognizer.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java index 4befbd3df758ac..80513012047061 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java @@ -1,3 +1,12 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + package com.facebook.react; import android.app.Activity; @@ -12,6 +21,7 @@ import com.facebook.common.logging.FLog; import com.facebook.react.common.ReactConstants; +import com.facebook.react.devsupport.DoubleTapReloadRecognizer; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import java.util.List; @@ -29,7 +39,7 @@ public abstract class ReactActivity extends Activity implements DefaultHardwareB private @Nullable ReactInstanceManager mReactInstanceManager; private @Nullable ReactRootView mReactRootView; private LifecycleState mLifecycleState = LifecycleState.BEFORE_RESUME; - private boolean mDoRefresh = false; + private DoubleTapReloadRecognizer mDoubleTapReloadRecognizer; /** * Returns the name of the bundle in assets. If this is null, and no file path is specified for @@ -142,6 +152,7 @@ protected void onCreate(Bundle savedInstanceState) { mReactRootView = createRootView(); mReactRootView.startReactApplication(mReactInstanceManager, getMainComponentName(), getLaunchOptions()); setContentView(mReactRootView); + mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer(); } @Override @@ -193,22 +204,8 @@ public boolean onKeyUp(int keyCode, KeyEvent event) { mReactInstanceManager.showDevOptionsDialog(); return true; } - if (keyCode == KeyEvent.KEYCODE_R && !(getCurrentFocus() instanceof EditText)) { - // Enable double-tap-R-to-reload - if (mDoRefresh) { - mReactInstanceManager.getDevSupportManager().handleReloadJS(); - mDoRefresh = false; - } else { - mDoRefresh = true; - new Handler().postDelayed( - new Runnable() { - @Override - public void run() { - mDoRefresh = false; - } - }, - 200); - } + if (mDoubleTapReloadRecognizer.didDoubleTapR(keyCode, getCurrentFocus())) { + mReactInstanceManager.getDevSupportManager().handleReloadJS(); } } return super.onKeyUp(keyCode, event); diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DoubleTapReloadRecognizer.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DoubleTapReloadRecognizer.java new file mode 100644 index 00000000000000..71742026b370c3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DoubleTapReloadRecognizer.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.devsupport; + +import android.os.Handler; +import android.view.KeyEvent; +import android.view.View; +import android.widget.EditText; + +/** + * A class allows recognizing double key tap of "R", used to reload JS in + * {@link AbstractReactActivity}, {@link RedBoxDialog} and {@link ReactActivity}. + */ +public class DoubleTapReloadRecognizer { + private boolean mDoRefresh = false; + private static final long DOUBLE_TAP_DELAY = 200; + + public boolean didDoubleTapR(int keyCode, View view) { + if (keyCode == KeyEvent.KEYCODE_R && !(view instanceof EditText)) { + if (mDoRefresh) { + mDoRefresh = false; + return true; + } else { + mDoRefresh = true; + new Handler().postDelayed( + new Runnable() { + @Override + public void run() { + mDoRefresh = false; + } + }, + DOUBLE_TAP_DELAY); + } + } + return false; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java index 914f8db575f80c..7b9cc1eb45274e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java @@ -42,6 +42,7 @@ /* package */ class RedBoxDialog extends Dialog implements AdapterView.OnItemClickListener { private final DevSupportManager mDevSupportManager; + private final DoubleTapReloadRecognizer mDoubleTapReloadRecognizer; private ListView mStackView; private Button mReloadJs; @@ -182,6 +183,7 @@ protected RedBoxDialog(Context context, DevSupportManager devSupportManager) { setContentView(R.layout.redbox_view); mDevSupportManager = devSupportManager; + mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer(); mStackView = (ListView) findViewById(R.id.rn_redbox_stack); mStackView.setOnItemClickListener(this); @@ -219,7 +221,9 @@ public boolean onKeyUp(int keyCode, KeyEvent event) { mDevSupportManager.showDevOptionsDialog(); return true; } - + if (mDoubleTapReloadRecognizer.didDoubleTapR(keyCode, getCurrentFocus())) { + mDevSupportManager.handleReloadJS(); + } return super.onKeyUp(keyCode, event); } } From 993a928833ec2480d280eccbabbb335a357c1fa2 Mon Sep 17 00:00:00 2001 From: Nathan Azaria Date: Tue, 7 Jun 2016 07:21:52 -0700 Subject: [PATCH 259/843] Fixed MapView's draggable annotation Reviewed By: javache Differential Revision: D3384947 fbshipit-source-id: 801a0998c8db788a731d27ae5956193ff23aa198 --- Examples/UIExplorer/MapViewExample.js | 57 ++++++++++++++++++++++--- Libraries/Components/MapView/MapView.js | 3 -- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/Examples/UIExplorer/MapViewExample.js b/Examples/UIExplorer/MapViewExample.js index c236a7781b8d30..d393702280a7ec 100644 --- a/Examples/UIExplorer/MapViewExample.js +++ b/Examples/UIExplorer/MapViewExample.js @@ -252,6 +252,56 @@ var AnnotationExample = React.createClass({ }); +var DraggableAnnotationExample = React.createClass({ + + createAnnotation(longitude, latitude) { + return { + longitude, + latitude, + draggable: true, + onDragStateChange: (event) => { + if (event.state === 'idle') { + this.setState({ + annotations: [this.createAnnotation(event.longitude, event.latitude)], + }); + } + console.log('Drag state: ' + event.state); + }, + }; + }, + + getInitialState() { + return { + isFirstLoad: true, + annotations: [], + mapRegion: undefined, + }; + }, + + render() { + if (this.state.isFirstLoad) { + var onRegionChangeComplete = (region) => { + //When the MapView loads for the first time, we can create the annotation at the + //region that was loaded. + this.setState({ + isFirstLoad: false, + annotations: [this.createAnnotation(region.longitude, region.latitude)], + }); + }; + } + + return ( + + ); + }, + +}); + var styles = StyleSheet.create({ map: { height: 150, @@ -338,12 +388,7 @@ exports.examples = [ { title: 'Draggable pin', render() { - return { - console.log('Drag state: ' + event.state); - }, - }}/>; + return ; } }, { diff --git a/Libraries/Components/MapView/MapView.js b/Libraries/Components/MapView/MapView.js index 60c7515272f8d2..be15531cedd292 100644 --- a/Libraries/Components/MapView/MapView.js +++ b/Libraries/Components/MapView/MapView.js @@ -437,9 +437,6 @@ const MapView = React.createClass({ onAnnotationDragStateChange = (event: Event) => { const annotation = findByAnnotationId(event.nativeEvent.annotationId); if (annotation) { - // Update location - annotation.latitude = event.nativeEvent.latitude; - annotation.longitude = event.nativeEvent.longitude; // Call callback annotation.onDragStateChange && annotation.onDragStateChange(event.nativeEvent); From d64368b9e239b574039f4a6508bf2aeb0806121b Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 7 Jun 2016 07:40:25 -0700 Subject: [PATCH 260/843] Implement CSS z-index for iOS Summary: This diff implement the CSS z-index for React Native iOS views. We've had numerous pull request for this feature, but they've all attempted to use the `layer.zPosition` property, which is problematic for two reasons: 1. zPosition only affects rendering order, not event processing order. Views with a higher zPosition will appear in front of others in the hierarchy, but won't be the first to receive touch events, and may be blocked by views that are visually behind them. 2. when using a perspective transform matrix, views with a nonzero zPosition will be rendered in a different position due to parallax, which probably isn't desirable. See https://github.com/facebook/react-native/pull/7825 for further discussion of this problem. So instead of using `layer.zPosition`, I've implemented this by actually adjusting the order of the subviews within their parent based on the zIndex. This can't be done on the JS side because it would affect layout, which is order-dependent, so I'm doing it inside the view itself. It works as follows: 1. The `reactSubviews` array is set, whose order matches the order of the JS components and shadowView components, as specified by the UIManager. 2. `didUpdateReactSubviews` is called, which in turn calls `sortedSubviews` (which lazily generates a sorted array of `reactSubviews` by zIndex) and inserts the result into the view. 3. If a subview is added or removed, or the zIndex of any subview is changed, the previous `sortedSubviews` array is cleared and `didUpdateReactSubviews` is called again. To demonstrate it working, I've modified the UIExplorer example from https://github.com/facebook/react-native/pull/7825 Reviewed By: javache Differential Revision: D3365717 fbshipit-source-id: b34aa8bfad577bce023f8af5414f9b974aafd8aa --- Examples/UIExplorer/ViewExample.js | 67 ++++++++++++++++++++++++- Libraries/StyleSheet/LayoutPropTypes.js | 3 ++ React/Modules/RCTUIManager.m | 4 -- React/Views/RCTShadowView.h | 5 ++ React/Views/RCTShadowView.m | 24 +++++++++ React/Views/RCTView.h | 7 +++ React/Views/RCTView.m | 6 ++- React/Views/RCTViewManager.m | 4 ++ React/Views/UIView+Private.h | 6 ++- React/Views/UIView+React.h | 13 ++++- React/Views/UIView+React.m | 44 +++++++++++++++- 11 files changed, 173 insertions(+), 10 deletions(-) diff --git a/Examples/UIExplorer/ViewExample.js b/Examples/UIExplorer/ViewExample.js index c5cb7cae73b7d3..7dc42925b4237e 100644 --- a/Examples/UIExplorer/ViewExample.js +++ b/Examples/UIExplorer/ViewExample.js @@ -1,4 +1,11 @@ /** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * * The examples provided by Facebook are for non-commercial testing and * evaluation purposes only. * @@ -30,7 +37,13 @@ var styles = StyleSheet.create({ backgroundColor: '#527FE4', borderColor: '#000033', borderWidth: 1, - } + }, + zIndex: { + justifyContent: 'space-around', + width: 100, + height: 50, + marginTop: -10, + }, }); var ViewBorderStyleExample = React.createClass({ @@ -74,6 +87,53 @@ var ViewBorderStyleExample = React.createClass({ } }); +var ZIndexExample = React.createClass({ + getInitialState() { + return { + flipped: false + }; + }, + + render() { + const indices = this.state.flipped ? [-1, 0, 1, 2] : [2, 1, 0, -1]; + return ( + + + Tap to flip sorting order + + ZIndex {indices[0]} + + + ZIndex {indices[1]} + + + ZIndex {indices[2]} + + + ZIndex {indices[3]} + + + + ); + }, + + _handlePress() { + this.setState({flipped: !this.state.flipped}); + } +}); + exports.title = ''; exports.description = 'Basic building block of all UI, examples that ' + 'demonstrate some of the many styles available.'; @@ -188,5 +248,10 @@ exports.examples = [ ); }, + }, { + title: 'ZIndex', + render: function() { + return ; + }, }, ]; diff --git a/Libraries/StyleSheet/LayoutPropTypes.js b/Libraries/StyleSheet/LayoutPropTypes.js index 95414b33929143..a07e16fe2f953c 100644 --- a/Libraries/StyleSheet/LayoutPropTypes.js +++ b/Libraries/StyleSheet/LayoutPropTypes.js @@ -100,6 +100,9 @@ var LayoutPropTypes = { // https://developer.mozilla.org/en-US/docs/Web/CSS/flex flex: ReactPropTypes.number, + + // https://developer.mozilla.org/en-US/docs/Web/CSS/z-index + zIndex: ReactPropTypes.number, }; module.exports = LayoutPropTypes; diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 4ab30f9918b723..62539be9df5e60 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -893,8 +893,6 @@ static void RCTSetChildren(NSNumber *containerTag, [container insertReactSubview:view atIndex:index++]; } } - - [container didUpdateReactSubviews]; } RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag @@ -965,8 +963,6 @@ - (void)_manageChildren:(NSNumber *)containerTag [container insertReactSubview:destinationsToChildrenToAdd[reactIndex] atIndex:reactIndex.integerValue]; } - - [container didUpdateReactSubviews]; } RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index ed3e4298890ec8..d32ba136527644 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -129,6 +129,11 @@ typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry @property (nonatomic, assign) css_wrap_type_t flexWrap; @property (nonatomic, assign) CGFloat flex; +/** + * z-index, used to override sibling order in the view + */ +@property (nonatomic, assign) double zIndex; + /** * Calculate property changes that need to be propagated to the view. * The applierBlocks set contains RCTApplierBlock functions that must be applied diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index d257e96594e93b..b518929dd75958 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -13,6 +13,7 @@ #import "RCTLog.h" #import "RCTUtils.h" #import "UIView+React.h" +#import "UIView+Private.h" typedef void (^RCTActionBlock)(RCTShadowView *shadowViewSelf, id value); typedef void (^RCTResetActionBlock)(RCTShadowView *shadowViewSelf); @@ -39,6 +40,7 @@ @implementation RCTShadowView BOOL _recomputePadding; BOOL _recomputeMargin; BOOL _recomputeBorder; + BOOL _didUpdateSubviews; float _paddingMetaProps[META_PROP_COUNT]; float _marginMetaProps[META_PROP_COUNT]; float _borderMetaProps[META_PROP_COUNT]; @@ -179,6 +181,16 @@ - (void)applyLayoutToChildren:(css_node_t *)node // dirtied, but really we should track which properties have changed and // only update those. + if (_didUpdateSubviews) { + _didUpdateSubviews = NO; + [self didUpdateReactSubviews]; + [applierBlocks addObject:^(NSDictionary *viewRegistry) { + UIView *view = viewRegistry[_reactTag]; + [view clearSortedSubviews]; + [view didUpdateReactSubviews]; + }]; + } + if (!_backgroundColor) { UIColor *parentBackgroundColor = parentProperties[RCTBackgroundColorProp]; if (parentBackgroundColor) { @@ -351,6 +363,7 @@ - (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex [_reactSubviews insertObject:subview atIndex:atIndex]; _cssNode->children_count = (int)_reactSubviews.count; subview->_superview = self; + _didUpdateSubviews = YES; [self dirtyText]; [self dirtyLayout]; [self dirtyPropagation]; @@ -361,6 +374,7 @@ - (void)removeReactSubview:(RCTShadowView *)subview [subview dirtyText]; [subview dirtyLayout]; [subview dirtyPropagation]; + _didUpdateSubviews = YES; subview->_superview = nil; [_reactSubviews removeObject:subview]; _cssNode->children_count = (int)_reactSubviews.count; @@ -596,6 +610,16 @@ - (void)setBackgroundColor:(UIColor *)color [self dirtyPropagation]; } +- (void)setZIndex:(double)zIndex +{ + _zIndex = zIndex; + if (_superview) { + // Changing zIndex means the subview order of the parent needs updating + _superview->_didUpdateSubviews = YES; + [_superview dirtyPropagation]; + } +} + - (void)didUpdateReactSubviews { // Does nothing by default diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index 7dc5bf3187ad7d..ea17f568abb437 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -41,6 +41,13 @@ */ + (UIEdgeInsets)contentInsetsForView:(UIView *)curView; +/** + * z-index, used to override sibling order in didUpdateReactSubviews. This is + * inherited from UIView+React, but we override it here to reduce the boxing + * and associated object overheads. + */ +@property (nonatomic, assign) double reactZIndex; + /** * This is an optimization used to improve performance * for large scrolling views with many subviews, such as a diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index 4d1b362ae4ff31..ac8718eedd31f4 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -99,6 +99,8 @@ @implementation RCTView UIColor *_backgroundColor; } +@synthesize reactZIndex = _reactZIndex; + - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { @@ -274,7 +276,7 @@ + (UIEdgeInsets)contentInsetsForView:(UIView *)view - (void)react_remountAllSubviews { if (_removeClippedSubviews) { - for (UIView *view in self.reactSubviews) { + for (UIView *view in self.sortedReactSubviews) { if (view.superview != self) { [self addSubview:view]; [view react_remountAllSubviews]; @@ -313,7 +315,7 @@ - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView: clipView = self; // Mount / unmount views - for (UIView *view in self.reactSubviews) { + for (UIView *view in self.sortedReactSubviews) { if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) { // View is at least partially visible, so remount it if unmounted diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 899b3f0317801e..c54e726c31a3a3 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -246,6 +246,8 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(__unused NSDictio RCT_VIEW_BORDER_RADIUS_PROPERTY(BottomLeft) RCT_VIEW_BORDER_RADIUS_PROPERTY(BottomRight) +RCT_REMAP_VIEW_PROPERTY(zIndex, reactZIndex, double) + #pragma mark - ShadowView properties RCT_EXPORT_SHADOW_PROPERTY(backgroundColor, UIColor) @@ -290,4 +292,6 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(__unused NSDictio RCT_EXPORT_SHADOW_PROPERTY(onLayout, RCTDirectEventBlock) +RCT_EXPORT_SHADOW_PROPERTY(zIndex, double) + @end diff --git a/React/Views/UIView+Private.h b/React/Views/UIView+Private.h index 14e6fcd2b4f246..057c46293b712c 100644 --- a/React/Views/UIView+Private.h +++ b/React/Views/UIView+Private.h @@ -9,10 +9,14 @@ #import -@interface UIView (RCTViewUnmounting) +@interface UIView (Private) +// remove clipped subviews implementation - (void)react_remountAllSubviews; - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView; - (UIView *)react_findClipView; +// zIndex sorting +- (void)clearSortedSubviews; + @end diff --git a/React/Views/UIView+React.h b/React/Views/UIView+React.h index fa779e5208a821..61484740b7f662 100644 --- a/React/Views/UIView+React.h +++ b/React/Views/UIView+React.h @@ -25,9 +25,20 @@ - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex NS_REQUIRES_SUPER; - (void)removeReactSubview:(UIView *)subview NS_REQUIRES_SUPER; +/** + * z-index, used to override sibling order in didUpdateReactSubviews. + */ +@property (nonatomic, assign) double reactZIndex; + +/** + * The reactSubviews array, sorted by zIndex. This value is cached and + * automatically recalculated if views are added or removed. + */ +@property (nonatomic, copy, readonly) NSArray *sortedReactSubviews; + /** * Updates the subviews array based on the reactSubviews. Default behavior is - * to insert the reactSubviews into the UIView. + * to insert the sortedReactSubviews into the UIView. */ - (void)didUpdateReactSubviews; diff --git a/React/Views/UIView+React.m b/React/Views/UIView+React.m index f25c716447741a..7004493d81cdd5 100644 --- a/React/Views/UIView+React.m +++ b/React/Views/UIView+React.m @@ -87,9 +87,51 @@ - (void)removeReactSubview:(UIView *)subview [subview removeFromSuperview]; } +- (double)reactZIndex +{ + return [objc_getAssociatedObject(self, _cmd) doubleValue]; +} + +- (void)setReactZIndex:(double)reactZIndex +{ + objc_setAssociatedObject(self, @selector(reactZIndex), @(reactZIndex), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSArray *)sortedReactSubviews +{ + NSArray *subviews = objc_getAssociatedObject(self, _cmd); + if (!subviews) { + // Check if sorting is required - in most cases it won't be + BOOL sortingRequired = NO; + for (UIView *subview in self.reactSubviews) { + if (subview.reactZIndex != 0) { + sortingRequired = YES; + break; + } + } + subviews = sortingRequired ? [self.reactSubviews sortedArrayUsingComparator:^NSComparisonResult(UIView *a, UIView *b) { + if (a.reactZIndex > b.reactZIndex) { + return NSOrderedDescending; + } else { + // ensure sorting is stable by treating equal zIndex as ascending so + // that original order is preserved + return NSOrderedAscending; + } + }] : self.reactSubviews; + objc_setAssociatedObject(self, _cmd, subviews, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return subviews; +} + +// private method, used to reset sort +- (void)clearSortedSubviews +{ + objc_setAssociatedObject(self, @selector(sortedReactSubviews), nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + - (void)didUpdateReactSubviews { - for (UIView *subview in self.reactSubviews) { + for (UIView *subview in self.sortedReactSubviews) { [self addSubview:subview]; } } From 8b78846a9501ef9c5ce9d1e18ee104bfae76af2e Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 7 Jun 2016 07:42:50 -0700 Subject: [PATCH 261/843] Open sourced KeyboardAvoidingView Summary: KeyboardAvoidingView is a component we built internally to solve the common problem of views that need to move out of the way of the virtual keyboard. KeyboardAvoidingView can automatically adjust either its position or bottom padding based on the position of the keyboard. Reviewed By: javache Differential Revision: D3398238 fbshipit-source-id: 493f2d2dec76667996250c011a1c5b7a14f245eb --- .../UIExplorer/KeyboardAvoidingViewExample.js | 111 ++++++++++ Examples/UIExplorer/UIExplorerList.android.js | 7 + Examples/UIExplorer/UIExplorerList.ios.js | 4 + .../Keyboard/KeyboardAvoidingView.js | 189 ++++++++++++++++++ Libraries/react-native/react-native.js | 1 + Libraries/react-native/react-native.js.flow | 1 + 6 files changed, 313 insertions(+) create mode 100644 Examples/UIExplorer/KeyboardAvoidingViewExample.js create mode 100644 Libraries/Components/Keyboard/KeyboardAvoidingView.js diff --git a/Examples/UIExplorer/KeyboardAvoidingViewExample.js b/Examples/UIExplorer/KeyboardAvoidingViewExample.js new file mode 100644 index 00000000000000..e23a984f19c371 --- /dev/null +++ b/Examples/UIExplorer/KeyboardAvoidingViewExample.js @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule KeyboardAvoidingViewExample + */ +'use strict'; + +const React = require('React'); +const ReactNative = require('react-native'); +const { + KeyboardAvoidingView, + Modal, + SegmentedControlIOS, + StyleSheet, + Text, + TextInput, + TouchableHighlight, + View, +} = ReactNative; + +const UIExplorerBlock = require('./UIExplorerBlock'); +const UIExplorerPage = require('./UIExplorerPage'); + +const KeyboardAvoidingViewExample = React.createClass({ + statics: { + title: '', + description: 'Base component for views that automatically adjust their height or position to move out of the way of the keyboard.', + }, + + getInitialState() { + return { + behavior: 'padding', + modalOpen: false, + }; + }, + + onSegmentChange(segment: String) { + this.setState({behavior: segment.toLowerCase()}); + }, + + renderExample() { + return ( + + + + + + + this.setState({modalOpen: false})} + style={styles.closeButton}> + Close + + + + this.setState({modalOpen: true})}> + Open Example + + + ); + }, + + render() { + return ( + + + {this.renderExample()} + + + ); + }, +}); + +const styles = StyleSheet.create({ + outerContainer: { + flex: 1, + }, + container: { + flex: 1, + justifyContent: 'center', + paddingHorizontal: 20, + paddingTop: 20, + }, + textInput: { + borderRadius: 5, + borderWidth: 1, + height: 44, + paddingHorizontal: 10, + }, + segment: { + marginBottom: 10, + }, + closeButton: { + position: 'absolute', + top: 30, + left: 10, + } +}); + +module.exports = KeyboardAvoidingViewExample; diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js index 25279bdf6f157d..c4c7aecc95d7bc 100644 --- a/Examples/UIExplorer/UIExplorerList.android.js +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -1,4 +1,11 @@ /** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * * The examples provided by Facebook are for non-commercial testing and * evaluation purposes only. * diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index 5aefa3288d2b1e..3ab425b24f6b1b 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -40,6 +40,10 @@ const ComponentExamples: Array = [ key: 'ImageExample', module: require('./ImageExample'), }, + { + key: 'KeyboardAvoidingViewExample', + module: require('./KeyboardAvoidingViewExample'), + }, { key: 'LayoutEventsExample', module: require('./LayoutEventsExample'), diff --git a/Libraries/Components/Keyboard/KeyboardAvoidingView.js b/Libraries/Components/Keyboard/KeyboardAvoidingView.js new file mode 100644 index 00000000000000..c5c1d9ba0b9b34 --- /dev/null +++ b/Libraries/Components/Keyboard/KeyboardAvoidingView.js @@ -0,0 +1,189 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule KeyboardAvoidingView + * @flow + */ +'use strict'; + +const Keyboard = require('Keyboard'); +const LayoutAnimation = require('LayoutAnimation'); +const Platform = require('Platform'); +const PropTypes = require('ReactPropTypes'); +const React = require('React'); +const TimerMixin = require('react-timer-mixin'); +const View = require('View'); + +import type EmitterSubscription from 'EmitterSubscription'; + +type Rect = { + x: number; + y: number; + width: number; + height: number; +}; +type ScreenRect = { + screenX: number; + screenY: number; + width: number; + height: number; +}; +type KeyboardChangeEvent = { + startCoordinates?: ScreenRect; + endCoordinates: ScreenRect; + duration?: number; + easing?: string; +}; +type LayoutEvent = { + nativeEvent: { + layout: Rect; + } +}; + +const viewRef = 'VIEW'; + +const KeyboardAvoidingView = React.createClass({ + mixins: [TimerMixin], + + propTypes: { + ...View.propTypes, + behavior: PropTypes.oneOf(['height', 'position', 'padding']), + + /** + * This is the distance between the top of the user screen and the react native view, + * may be non-zero in some use cases. + */ + keyboardVerticalOffset: PropTypes.number.isRequired, + }, + + getDefaultProps() { + return { + keyboardVerticalOffset: 0, + }; + }, + + getInitialState() { + return { + bottom: 0, + }; + }, + + subscriptions: ([]: Array), + frame: (null: ?Rect), + + relativeKeyboardHeight(keyboardFrame: ScreenRect): number { + const frame = this.frame; + if (!frame) { + return 0; + } + + const y1 = Math.max(frame.y, keyboardFrame.screenY - this.props.keyboardVerticalOffset); + const y2 = Math.min(frame.y + frame.height, keyboardFrame.screenY + keyboardFrame.height - this.props.keyboardVerticalOffset); + return Math.max(y2 - y1, 0); + }, + + onKeyboardChange(event: ?KeyboardChangeEvent) { + if (!event) { + this.setState({bottom: 0}); + return; + } + + const {duration, easing, endCoordinates} = event; + const height = this.relativeKeyboardHeight(endCoordinates); + + if (duration && easing) { + LayoutAnimation.configureNext({ + duration: duration, + update: { + duration: duration, + type: LayoutAnimation.Types[easing] || 'keyboard', + }, + }); + } + this.setState({bottom: height}); + }, + + onLayout(event: LayoutEvent) { + this.frame = event.nativeEvent.layout; + }, + + componentWillUpdate(nextProps: Object, nextState: Object, nextContext?: Object): void { + if (nextState.bottom === this.state.bottom && + this.props.behavior === 'height' && + nextProps.behavior === 'height') { + // If the component rerenders without an internal state change, e.g. + // triggered by parent component re-rendering, no need for bottom to change. + nextState.bottom = 0; + } + }, + + componentWillMount() { + if (Platform.OS === 'ios') { + this.subscriptions = [ + Keyboard.addListener('keyboardWillChangeFrame', this.onKeyboardChange), + ]; + } else { + this.subscriptions = [ + Keyboard.addListener('keyboardDidHide', this.onKeyboardChange), + Keyboard.addListener('keyboardDidShow', this.onKeyboardChange), + ]; + } + }, + + componentWillUnmount() { + this.subscriptions.forEach((sub) => sub.remove()); + }, + + render(): ReactElement { + const {behavior, children, style, ...props} = this.props; + + switch (behavior) { + case 'height': + let heightStyle; + if (this.frame) { + // Note that we only apply a height change when there is keyboard present, + // i.e. this.state.bottom is greater than 0. If we remove that condition, + // this.frame.height will never go back to its original value. + // When height changes, we need to disable flex. + heightStyle = {height: this.frame.height - this.state.bottom, flex: 0}; + } + return ( + + {children} + + ); + + case 'position': + const positionStyle = {bottom: this.state.bottom}; + return ( + + + {children} + + + ); + + case 'padding': + const paddingStyle = {paddingBottom: this.state.bottom}; + return ( + + {children} + + ); + + default: + return ( + + {children} + + ); + } + }, +}); + +module.exports = KeyboardAvoidingView; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 713becd5dfaabf..c9129314cdc365 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -35,6 +35,7 @@ const ReactNative = { get Image() { return require('Image'); }, get ImageEditor() { return require('ImageEditor'); }, get ImageStore() { return require('ImageStore'); }, + get KeyboardAvoidingView() { return require('KeyboardAvoidingView'); }, get ListView() { return require('ListView'); }, get MapView() { return require('MapView'); }, get Modal() { return require('Modal'); }, diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index 191d3e22284171..b63a57cb5e2878 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -33,6 +33,7 @@ var ReactNative = Object.assign(Object.create(require('ReactNative')), { Image: require('Image'), ImageEditor: require('ImageEditor'), ImageStore: require('ImageStore'), + KeyboardAvoidingView: require('KeyboardAvoidingView'), ListView: require('ListView'), MapView: require('MapView'), Modal: require('Modal'), From 282df0ed071015a136b7b9901430a429da698b4b Mon Sep 17 00:00:00 2001 From: Valentin Agachi Date: Tue, 7 Jun 2016 08:05:17 -0700 Subject: [PATCH 262/843] Remove extra packager call from objc-test Reviewed By: bestander Differential Revision: D3398448 fbshipit-source-id: dd0676021eac7169e55e7049d3edac6ee0c8bf45 --- scripts/objc-test.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/objc-test.sh b/scripts/objc-test.sh index 3ebce52e9eaeae..83835ce44effd9 100755 --- a/scripts/objc-test.sh +++ b/scripts/objc-test.sh @@ -20,12 +20,9 @@ function cleanup { [ -f $REACT_PACKAGER_LOG ] && cat $REACT_PACKAGER_LOG fi - [ $SERVER_PID ] && kill -9 $SERVER_PID } trap cleanup EXIT -./packager/packager.sh --nonPersistent & -SERVER_PID=$! # TODO: We use xcodebuild because xctool would stall when collecting info about # the tests before running them. Switch back when this issue with xctool has # been resolved. From 79dcbc7b29fb3a06d40a26ac9ca0a1cf299b1831 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 7 Jun 2016 08:36:07 -0700 Subject: [PATCH 263/843] Fix unit tests Reviewed By: bestander Differential Revision: D3398431 fbshipit-source-id: 37561bea78c933673595625530cf083c85c3fbbd --- .../UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m | 4 ++-- Libraries/Text/RCTText.m | 2 +- Libraries/Text/RCTTextView.m | 2 +- React/Views/RCTMap.m | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m index 520c9bedaef690..ba0bbf508aab02 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m @@ -95,7 +95,7 @@ - (void)testManagingChildrenToRemoveViews } for (NSInteger i = 2; i < 20; i++) { UIView *view = _uiManager.viewRegistry[@(i)]; - [containerView addSubview:view]; + [containerView insertReactSubview:view atIndex:containerView.reactSubviews.count]; } // Remove views 1-5 from view 20 @@ -155,7 +155,7 @@ - (void)testManagingChildrenToAddRemoveAndMove for (NSInteger i = 1; i < 11; i++) { UIView *view = _uiManager.viewRegistry[@(i)]; - [containerView addSubview:view]; + [containerView insertReactSubview:view atIndex:containerView.reactSubviews.count]; } [_uiManager _manageChildren:@20 diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index fc5f60bdcea0d2..4a926b246bb7fd 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -66,7 +66,7 @@ - (void)reactSetInheritedBackgroundColor:(UIColor *)inheritedBackgroundColor self.backgroundColor = inheritedBackgroundColor; } -- (void)reactUpdateSubviews +- (void)didUpdateReactSubviews { // Do nothing, as subviews are managed by `setTextStorage:` method } diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index 9b0a4d737cd4eb..f10cc2605eee50 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -136,7 +136,7 @@ - (void)removeReactSubview:(UIView *)subview } } -- (void)reactUpdateSubviews +- (void)didUpdateReactSubviews { // Do nothing, as we don't allow non-text subviews } diff --git a/React/Views/RCTMap.m b/React/Views/RCTMap.m index 7a1334668761ca..fc071ec024f4a6 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -14,6 +14,7 @@ #import "RCTMapAnnotation.h" #import "RCTMapOverlay.h" #import "RCTUtils.h" +#import "UIView+React.h" const CLLocationDegrees RCTMapDefaultSpan = 0.005; const NSTimeInterval RCTMapRegionChangeObserveInterval = 0.1; @@ -49,7 +50,7 @@ - (void)dealloc [_regionChangeObserveTimer invalidate]; } -- (void)reactUpdateSubviews +- (void)didUpdateReactSubviews { // Do nothing, as annotation views are managed by `setAnnotations:` method } From 48689481753af22986851c908f6d7b6d8f0a9567 Mon Sep 17 00:00:00 2001 From: Fred Liu Date: Tue, 7 Jun 2016 09:07:30 -0700 Subject: [PATCH 264/843] NUX-y bounce Summary: On mount, bounce the 1st row so users know it's swipeable. Reviewed By: fkgozali Differential Revision: D3395214 fbshipit-source-id: 6d391209014a6a7957a2160734d8ef6548b7693b --- .../SwipeableRow/SwipeableListView.js | 22 +++++- .../SwipeableListViewDataSource.js | 12 +++ .../Experimental/SwipeableRow/SwipeableRow.js | 78 +++++++++++++++---- 3 files changed, 96 insertions(+), 16 deletions(-) diff --git a/Libraries/Experimental/SwipeableRow/SwipeableListView.js b/Libraries/Experimental/SwipeableRow/SwipeableListView.js index 1572d471076aa1..4600483d47ca74 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableListView.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableListView.js @@ -47,8 +47,10 @@ const SwipeableListView = React.createClass({ }, _listViewRef: (null: ?string), + _shouldBounceFirstRowOnMount: false, propTypes: { + bounceFirstRowOnMount: PropTypes.bool.isRequired, dataSource: PropTypes.instanceOf(SwipeableListViewDataSource).isRequired, maxSwipeDistance: PropTypes.number, // Callback method to render the swipeable view @@ -59,6 +61,7 @@ const SwipeableListView = React.createClass({ getDefaultProps(): Object { return { + bounceFirstRowOnMount: false, renderQuickActions: () => null, }; }, @@ -69,6 +72,10 @@ const SwipeableListView = React.createClass({ }; }, + componentWillMount(): void { + this._shouldBounceFirstRowOnMount = this.props.bounceFirstRowOnMount; + }, + componentWillReceiveProps(nextProps: Object): void { if ( this.state.dataSource.getDataSource() !== nextProps.dataSource.getDataSource() @@ -114,7 +121,11 @@ const SwipeableListView = React.createClass({ } }, - _renderRow(rowData: Object, sectionID: string, rowID: string): ReactElement { + _renderRow( + rowData: Object, + sectionID: string, + rowID: string, + ): ReactElement { const slideoutView = this.props.renderQuickActions(rowData, sectionID, rowID); // If renderRowSlideout is unspecified or returns falsey, don't allow swipe @@ -122,6 +133,12 @@ const SwipeableListView = React.createClass({ return this.props.renderRow(rowData, sectionID, rowID); } + let shouldBounceOnMount = false; + if (this._shouldBounceFirstRowOnMount) { + this._shouldBounceFirstRowOnMount = false; + shouldBounceOnMount = rowID === this.props.dataSource.getFirstRowID(); + } + return ( this._onOpen(rowData.id)} onSwipeEnd={() => this._setListViewScrollable(true)} - onSwipeStart={() => this._setListViewScrollable(false)}> + onSwipeStart={() => this._setListViewScrollable(false)} + shouldBounceOnMount={shouldBounceOnMount}> {this.props.renderRow(rowData, sectionID, rowID)} ); diff --git a/Libraries/Experimental/SwipeableRow/SwipeableListViewDataSource.js b/Libraries/Experimental/SwipeableRow/SwipeableListViewDataSource.js index b39a671f5b40a4..4c01bf1a0f9a4d 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableListViewDataSource.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableListViewDataSource.js @@ -88,6 +88,18 @@ class SwipeableListViewDataSource { return this._openRowID; } + getFirstRowID(): ?string { + /** + * If rowIdentities is specified, find the first data row from there since + * we don't want to attempt to bounce section headers. If unspecified, find + * the first data row from _dataBlob. + */ + if (this.rowIdentities) { + return this.rowIdentities[0] && this.rowIdentities[0][0]; + } + return Object.keys(this._dataBlob)[0]; + } + setOpenRowID(rowID: string): SwipeableListViewDataSource { this._previousOpenRowID = this._openRowID; this._openRowID = rowID; diff --git a/Libraries/Experimental/SwipeableRow/SwipeableRow.js b/Libraries/Experimental/SwipeableRow/SwipeableRow.js index 09838bfe63805d..e615fc80485b54 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableRow.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableRow.js @@ -27,28 +27,41 @@ const Animated = require('Animated'); const PanResponder = require('PanResponder'); const React = require('React'); const StyleSheet = require('StyleSheet'); +const TimerMixin = require('react-timer-mixin'); const View = require('View'); const {PropTypes} = React; const emptyFunction = require('fbjs/lib/emptyFunction'); +// NOTE: Eventually convert these consts to an input object of configurations + // Position of the left of the swipable item when closed const CLOSED_LEFT_POSITION = 0; // Minimum swipe distance before we recognize it as such const HORIZONTAL_SWIPE_DISTANCE_THRESHOLD = 15; -// Distance left of closed position to bounce back when right-swiping from closed -const RIGHT_SWIPE_BOUNCE_BACK_DISTANCE = 30; +// Minimum swipe speed before we fully animate the user's action (open/close) +const HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD = 0.5; // Factor to divide by to get slow speed; i.e. 4 means 1/4 of full speed const SLOW_SPEED_SWIPE_FACTOR = 4; +// Time, in milliseconds, of how long the animated swipe should be +const SWIPE_DURATION = 200; + +/** + * On SwipeableListView mount, the 1st item will bounce to show users it's + * possible to swipe + */ +const ON_MOUNT_BOUNCE_DELAY = 700; +const ON_MOUNT_BOUNCE_DURATION = 200; + +// Distance left of closed position to bounce back when right-swiping from closed +const RIGHT_SWIPE_BOUNCE_BACK_DISTANCE = 30; /** * Max distance of right swipe to allow (right swipes do functionally nothing). * Must be multiplied by SLOW_SPEED_SWIPE_FACTOR because gestureState.dx tracks * how far the finger swipes, and not the actual animation distance. */ const RIGHT_SWIPE_THRESHOLD = 30 * SLOW_SPEED_SWIPE_FACTOR; -// Time, in milliseconds, of how long the animated swipe should be -const SWIPE_DURATION = 200; /** * Creates a swipable row that allows taps on the main item and a custom View @@ -58,12 +71,16 @@ const SwipeableRow = React.createClass({ _panResponder: {}, _previousLeft: CLOSED_LEFT_POSITION, + mixins: [TimerMixin], + propTypes: { isOpen: PropTypes.bool, maxSwipeDistance: PropTypes.number.isRequired, onOpen: PropTypes.func.isRequired, onSwipeEnd: PropTypes.func.isRequired, onSwipeStart: PropTypes.func.isRequired, + // Should bounce the row on mount + shouldBounceOnMount: PropTypes.bool, /** * A ReactElement that is unveiled when the user swipes */ @@ -116,6 +133,18 @@ const SwipeableRow = React.createClass({ }); }, + componentDidMount(): void { + if (this.props.shouldBounceOnMount) { + /** + * Do the on mount bounce after a delay because if we animate when other + * components are loading, the animation will be laggy + */ + this.setTimeout(() => { + this._animateBounceBack(ON_MOUNT_BOUNCE_DURATION); + }, ON_MOUNT_BOUNCE_DELAY); + } + }, + componentWillReceiveProps(nextProps: Object): void { /** * We do not need an "animateOpen(noCallback)" because this animation is @@ -126,6 +155,15 @@ const SwipeableRow = React.createClass({ } }, + shouldComponentUpdate(nextProps: Object, nextState: Object): boolean { + if (this.props.shouldBounceOnMount && !nextProps.shouldBounceOnMount) { + // No need to rerender if SwipeableListView is disabling the bounce flag + return false; + } + + return true; + }, + render(): ReactElement { // The view hidden behind the main view let slideOutView; @@ -231,12 +269,16 @@ const SwipeableRow = React.createClass({ return false; }, - _animateTo(toValue: number, callback: Function = emptyFunction): void { + _animateTo( + toValue: number, + duration: number = SWIPE_DURATION, + callback: Function = emptyFunction, + ): void { Animated.timing( this.state.currentLeft, { - duration: SWIPE_DURATION, - toValue: toValue, + duration, + toValue, }, ).start(() => { this._previousLeft = toValue; @@ -252,13 +294,14 @@ const SwipeableRow = React.createClass({ this._animateTo(CLOSED_LEFT_POSITION); }, - _animateRightSwipeBounceBack(): void { + _animateBounceBack(duration: number = SWIPE_DURATION): void { /** * When swiping right, we want to bounce back past closed position on release * so users know they should swipe right to get content. */ this._animateTo( -RIGHT_SWIPE_BOUNCE_BACK_DISTANCE, + duration, this._animateToClosedPosition, ); }, @@ -268,15 +311,24 @@ const SwipeableRow = React.createClass({ return Math.abs(gestureState.dx) > HORIZONTAL_SWIPE_DISTANCE_THRESHOLD; }, + _shouldAnimateRemainder(gestureState: Object): boolean { + /** + * If user has swiped past a certain distance, animate the rest of the way + * if they let go + */ + return ( + Math.abs(gestureState.dx) > this.props.swipeThreshold || + gestureState.vx > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD + ); + }, + _handlePanResponderEnd(event: Object, gestureState: Object): void { const horizontalDistance = gestureState.dx; if (this._isSwipingRightFromClosed(gestureState)) { this.props.onOpen(); - this._animateRightSwipeBounceBack(); - } else if (Math.abs(horizontalDistance) > this.props.swipeThreshold) { - // Overswiped - + this._animateBounceBack(); + } else if (this._shouldAnimateRemainder(gestureState)) { if (horizontalDistance < 0) { // Swiped left this.props.onOpen(); @@ -286,8 +338,6 @@ const SwipeableRow = React.createClass({ this._animateToClosedPosition(); } } else { - // Swiping from closed but let go before fully - if (this._previousLeft === CLOSED_LEFT_POSITION) { this._animateToClosedPosition(); } else { From 49a5fe493e830e5610eedfe5414b37c759b61491 Mon Sep 17 00:00:00 2001 From: Andy Street Date: Tue, 7 Jun 2016 09:32:35 -0700 Subject: [PATCH 265/843] Fix deletion layout animations to work with custom view managers Summary: D3352450 didn't handle the case where removeClippedSubviews is on and the indices within the parent view don't match the indices JS is working on Differential Revision: D3398675 fbshipit-source-id: 0a1b9cf41b02f71f6585db92474e4699b944d273 --- .../facebook/react/uimanager/NativeViewHierarchyManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index 36ab83c8fa871f..f6e13761a1c397 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -346,7 +346,7 @@ public void manageChildren( tagsToDelete)); } - View viewToRemove = viewToManage.getChildAt(indexToRemove); + View viewToRemove = viewManager.getChildAt(viewToManage, indexToRemove); if (mLayoutAnimator.shouldAnimateLayout(viewToRemove) && arrayContains(tagsToDelete, viewToRemove.getId())) { From 7e7abff55ae671442502574ad26e46a5001bea09 Mon Sep 17 00:00:00 2001 From: Saurabh Aggarwal Date: Tue, 7 Jun 2016 09:32:47 -0700 Subject: [PATCH 266/843] 4.5 / N [RNFeed] Isolate reusable util methods out of NetworkingModule Reviewed By: lexs Differential Revision: D3394808 fbshipit-source-id: 19c916be693377651f2c8e21eb3dd490ec68f74c --- .../modules/network/NetworkingModule.java | 154 +++++++----------- .../react/modules/network/ResponseUtil.java | 85 ++++++++++ .../modules/network/NetworkingModuleTest.java | 28 ++-- 3 files changed, 159 insertions(+), 108 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/network/ResponseUtil.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java index 91afa78622ea95..9c42dfcd59130e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java @@ -14,7 +14,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.Reader; -import java.net.SocketTimeoutException; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -28,10 +27,9 @@ import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.network.OkHttpCallUtil; -import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; import okhttp3.Call; import okhttp3.Callback; @@ -178,9 +176,10 @@ public void sendRequest( .build(); } + final RCTDeviceEventEmitter eventEmitter = getEventEmitter(executorToken); Headers requestHeaders = extractHeaders(headers, data); if (requestHeaders == null) { - onRequestError(executorToken, requestId, "Unrecognized headers format", null); + ResponseUtil.onRequestError(eventEmitter, requestId, "Unrecognized headers format", null); return; } String contentType = requestHeaders.get(CONTENT_TYPE_HEADER_NAME); @@ -191,11 +190,11 @@ public void sendRequest( requestBuilder.method(method, RequestBodyUtil.getEmptyBody(method)); } else if (data.hasKey(REQUEST_BODY_KEY_STRING)) { if (contentType == null) { - onRequestError( - executorToken, - requestId, - "Payload is set but no content-type header specified", - null); + ResponseUtil.onRequestError( + eventEmitter, + requestId, + "Payload is set but no content-type header specified", + null); return; } String body = data.getString(REQUEST_BODY_KEY_STRING); @@ -203,7 +202,7 @@ public void sendRequest( if (RequestBodyUtil.isGzipEncoding(contentEncoding)) { RequestBody requestBody = RequestBodyUtil.createGzip(contentMediaType, body); if (requestBody == null) { - onRequestError(executorToken, requestId, "Failed to gzip request body", null); + ResponseUtil.onRequestError(eventEmitter, requestId, "Failed to gzip request body", null); return; } requestBuilder.method(method, requestBody); @@ -212,18 +211,22 @@ public void sendRequest( } } else if (data.hasKey(REQUEST_BODY_KEY_URI)) { if (contentType == null) { - onRequestError( - executorToken, - requestId, - "Payload is set but no content-type header specified", - null); + ResponseUtil.onRequestError( + eventEmitter, + requestId, + "Payload is set but no content-type header specified", + null); return; } String uri = data.getString(REQUEST_BODY_KEY_URI); InputStream fileInputStream = RequestBodyUtil.getFileInputStream(getReactApplicationContext(), uri); if (fileInputStream == null) { - onRequestError(executorToken, requestId, "Could not retrieve file for uri " + uri, null); + ResponseUtil.onRequestError( + eventEmitter, + requestId, + "Could not retrieve file for uri " + uri, + null); return; } requestBuilder.method( @@ -240,14 +243,18 @@ public void sendRequest( return; } - requestBuilder.method(method, RequestBodyUtil.createProgressRequest(multipartBuilder.build(), new ProgressRequestListener() { + requestBuilder.method( + method, + RequestBodyUtil.createProgressRequest( + multipartBuilder.build(), + new ProgressRequestListener() { long last = System.nanoTime(); @Override public void onRequestProgress(long bytesWritten, long contentLength, boolean done) { long now = System.nanoTime(); if (done || shouldDispatch(now, last)) { - onDataSend(executorToken, requestId, bytesWritten,contentLength); + ResponseUtil.onDataSend(eventEmitter, requestId, bytesWritten, contentLength); last = now; } } @@ -266,7 +273,7 @@ public void onFailure(Call call, IOException e) { return; } removeRequest(requestId); - onRequestError(executorToken, requestId, e.getMessage(), e); + ResponseUtil.onRequestError(eventEmitter, requestId, e.getMessage(), e); } @Override @@ -276,26 +283,31 @@ public void onResponse(Call call, Response response) throws IOException { } removeRequest(requestId); // Before we touch the body send headers to JS - onResponseReceived(executorToken, requestId, response); + ResponseUtil.onResponseReceived( + eventEmitter, + requestId, + response.code(), + translateHeaders(response.headers()), + response.request().url().toString()); ResponseBody responseBody = response.body(); try { if (useIncrementalUpdates) { - readWithProgress(executorToken, requestId, responseBody); - onRequestSuccess(executorToken, requestId); + readWithProgress(eventEmitter, requestId, responseBody); + ResponseUtil.onRequestSuccess(eventEmitter, requestId); } else { - onDataReceived(executorToken, requestId, responseBody.string()); - onRequestSuccess(executorToken, requestId); + ResponseUtil.onDataReceived(eventEmitter, requestId, responseBody.string()); + ResponseUtil.onRequestSuccess(eventEmitter, requestId); } } catch (IOException e) { - onRequestError(executorToken, requestId, e.getMessage(), e); + ResponseUtil.onRequestError(eventEmitter, requestId, e.getMessage(), e); } } }); } private void readWithProgress( - ExecutorToken executorToken, + RCTDeviceEventEmitter eventEmitter, int requestId, ResponseBody responseBody) throws IOException { Reader reader = responseBody.charStream(); @@ -303,7 +315,7 @@ private void readWithProgress( char[] buffer = new char[MAX_CHUNK_SIZE_BETWEEN_FLUSHES]; int read; while ((read = reader.read(buffer)) != -1) { - onDataReceived(executorToken, requestId, new String(buffer, 0, read)); + ResponseUtil.onDataReceived(eventEmitter, requestId, new String(buffer, 0, read)); } } finally { reader.close(); @@ -314,57 +326,6 @@ private static boolean shouldDispatch(long now, long last) { return last + CHUNK_TIMEOUT_NS < now; } - private void onDataSend(ExecutorToken ExecutorToken, int requestId, long progress, long total) { - WritableArray args = Arguments.createArray(); - args.pushInt(requestId); - args.pushInt((int) progress); - args.pushInt((int) total); - getEventEmitter(ExecutorToken).emit("didSendNetworkData", args); - } - - private void onDataReceived(ExecutorToken ExecutorToken, int requestId, String data) { - WritableArray args = Arguments.createArray(); - args.pushInt(requestId); - args.pushString(data); - - getEventEmitter(ExecutorToken).emit("didReceiveNetworkData", args); - } - - private void onRequestError(ExecutorToken ExecutorToken, int requestId, String error, IOException e) { - WritableArray args = Arguments.createArray(); - args.pushInt(requestId); - args.pushString(error); - - if ((e != null) && (e.getClass() == SocketTimeoutException.class)) { - args.pushBoolean(true); // last argument is a time out boolean - } - - getEventEmitter(ExecutorToken).emit("didCompleteNetworkResponse", args); - } - - private void onRequestSuccess(ExecutorToken ExecutorToken, int requestId) { - WritableArray args = Arguments.createArray(); - args.pushInt(requestId); - args.pushNull(); - - getEventEmitter(ExecutorToken).emit("didCompleteNetworkResponse", args); - } - - private void onResponseReceived( - ExecutorToken ExecutorToken, - int requestId, - Response response) { - WritableMap headers = translateHeaders(response.headers()); - - WritableArray args = Arguments.createArray(); - args.pushInt(requestId); - args.pushInt(response.code()); - args.pushMap(headers); - args.pushString(response.request().url().toString()); - - getEventEmitter(ExecutorToken).emit("didReceiveNetworkResponse", args); - } - private synchronized void addRequest(int requestId) { mRequestIds.add(requestId); } @@ -430,6 +391,7 @@ public boolean supportsWebWorkers() { ReadableArray body, String contentType, int requestId) { + RCTDeviceEventEmitter eventEmitter = getEventEmitter(ExecutorToken); MultipartBody.Builder multipartBuilder = new MultipartBody.Builder(); multipartBuilder.setType(MediaType.parse(contentType)); @@ -440,11 +402,11 @@ public boolean supportsWebWorkers() { ReadableArray headersArray = bodyPart.getArray("headers"); Headers headers = extractHeaders(headersArray, null); if (headers == null) { - onRequestError( - ExecutorToken, - requestId, - "Missing or invalid header format for FormData part.", - null); + ResponseUtil.onRequestError( + eventEmitter, + requestId, + "Missing or invalid header format for FormData part.", + null); return null; } MediaType partContentType = null; @@ -461,27 +423,27 @@ public boolean supportsWebWorkers() { multipartBuilder.addPart(headers, RequestBody.create(partContentType, bodyValue)); } else if (bodyPart.hasKey(REQUEST_BODY_KEY_URI)) { if (partContentType == null) { - onRequestError( - ExecutorToken, - requestId, - "Binary FormData part needs a content-type header.", - null); + ResponseUtil.onRequestError( + eventEmitter, + requestId, + "Binary FormData part needs a content-type header.", + null); return null; } String fileContentUriStr = bodyPart.getString(REQUEST_BODY_KEY_URI); InputStream fileInputStream = RequestBodyUtil.getFileInputStream(getReactApplicationContext(), fileContentUriStr); if (fileInputStream == null) { - onRequestError( - ExecutorToken, - requestId, - "Could not retrieve file for uri " + fileContentUriStr, - null); + ResponseUtil.onRequestError( + eventEmitter, + requestId, + "Could not retrieve file for uri " + fileContentUriStr, + null); return null; } multipartBuilder.addPart(headers, RequestBodyUtil.create(partContentType, fileInputStream)); } else { - onRequestError(ExecutorToken, requestId, "Unrecognized FormData part.", null); + ResponseUtil.onRequestError(eventEmitter, requestId, "Unrecognized FormData part.", null); } } return multipartBuilder; @@ -519,8 +481,8 @@ public boolean supportsWebWorkers() { return headersBuilder.build(); } - private DeviceEventManagerModule.RCTDeviceEventEmitter getEventEmitter(ExecutorToken ExecutorToken) { + private RCTDeviceEventEmitter getEventEmitter(ExecutorToken ExecutorToken) { return getReactApplicationContext() - .getJSModule(ExecutorToken, DeviceEventManagerModule.RCTDeviceEventEmitter.class); + .getJSModule(ExecutorToken, RCTDeviceEventEmitter.class); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/network/ResponseUtil.java b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ResponseUtil.java new file mode 100644 index 00000000000000..eb2b6e2bbb6a5d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/network/ResponseUtil.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.network; + +import java.io.IOException; +import java.net.SocketTimeoutException; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; + +/** + * Util methods to send network responses to JS. + */ +public class ResponseUtil { + public static void onDataSend( + RCTDeviceEventEmitter eventEmitter, + int requestId, + long progress, + long total) { + WritableArray args = Arguments.createArray(); + args.pushInt(requestId); + args.pushInt((int) progress); + args.pushInt((int) total); + eventEmitter.emit("didSendNetworkData", args); + } + + public static void onDataReceived( + RCTDeviceEventEmitter eventEmitter, + int requestId, + String data) { + WritableArray args = Arguments.createArray(); + args.pushInt(requestId); + args.pushString(data); + + eventEmitter.emit("didReceiveNetworkData", args); + } + + public static void onRequestError( + RCTDeviceEventEmitter eventEmitter, + int requestId, + String error, + IOException e) { + WritableArray args = Arguments.createArray(); + args.pushInt(requestId); + args.pushString(error); + + if ((e != null) && (e.getClass() == SocketTimeoutException.class)) { + args.pushBoolean(true); // last argument is a time out boolean + } + + eventEmitter.emit("didCompleteNetworkResponse", args); + } + + public static void onRequestSuccess(RCTDeviceEventEmitter eventEmitter, int requestId) { + WritableArray args = Arguments.createArray(); + args.pushInt(requestId); + args.pushNull(); + + eventEmitter.emit("didCompleteNetworkResponse", args); + } + + public static void onResponseReceived( + RCTDeviceEventEmitter eventEmitter, + int requestId, + int statusCode, + WritableMap headers, + String url) { + WritableArray args = Arguments.createArray(); + args.pushInt(requestId); + args.pushInt(statusCode); + args.pushMap(headers); + args.pushString(url); + + eventEmitter.emit("didReceiveNetworkResponse", args); + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java index 8e5f40cb78030f..c06a99f9adef47 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java @@ -84,8 +84,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return callMock; } }); - - NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + NetworkingModule networkingModule = + new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); networkingModule.sendRequest( mock(ExecutorToken.class), @@ -196,8 +196,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return callMock; } }); - - NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + NetworkingModule networkingModule = + new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); JavaOnlyMap body = new JavaOnlyMap(); body.putString("string", "This is request body"); @@ -234,7 +234,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return callMock; } }); - NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + NetworkingModule networkingModule = + new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); List headers = Arrays.asList( JavaOnlyArray.of("Accept", "text/plain"), @@ -287,8 +288,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return callMock; } }); - - NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + NetworkingModule networkingModule = + new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); networkingModule.sendRequest( mock(ExecutorToken.class), "POST", @@ -347,8 +348,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return callMock; } }); - - NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + NetworkingModule networkingModule = + new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); networkingModule.sendRequest( mock(ExecutorToken.class), "POST", @@ -445,7 +446,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable { } }); - NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + NetworkingModule networkingModule = + new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); networkingModule.sendRequest( mock(ExecutorToken.class), "POST", @@ -501,7 +503,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return calls[(Integer) request.tag() - 1]; } }); - NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + NetworkingModule networkingModule = + new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); networkingModule.initialize(); for (int idx = 0; idx < requests; idx++) { @@ -547,7 +550,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return calls[(Integer) request.tag() - 1]; } }); - NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + NetworkingModule networkingModule = + new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient); for (int idx = 0; idx < requests; idx++) { networkingModule.sendRequest( From 4b19db31591ae52938eefcd5fe3cd9d622be3021 Mon Sep 17 00:00:00 2001 From: Siqi Liu Date: Tue, 7 Jun 2016 09:43:33 -0700 Subject: [PATCH 267/843] Remove the Profiler Option in RN Android Dev Menu Summary: Remove the "Start/Stop" option in RN Android dev menu. mkonicek talked to Mike Armstrong who originally added this option and he said Traceview didn't end up being used much and Systrace was the preferred way to do profiling, which is used from the command line. Haven't removed all the codes behind because there are some codes in JNI, including native methods in C++ through the ReactBridge. Dev menu before: {F61306550} Dev menu after: {F61306553} Reviewed By: mkonicek Differential Revision: D3391092 fbshipit-source-id: c400d8bb3c196afa9ef53cda13476e1fec6c2384 --- .../devsupport/DevSupportManagerImpl.java | 51 ------------------- .../main/res/devsupport/values/strings.xml | 2 - 2 files changed, 53 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index 35d7471fefd45d..21a025c573507f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -87,9 +87,6 @@ public class DevSupportManagerImpl implements DevSupportManager { private static final String EXOPACKAGE_LOCATION_FORMAT = "/data/local/tmp/exopackage/%s//secondary-dex"; - private static final int JAVA_SAMPLING_PROFILE_MEMORY_BYTES = 8 * 1024 * 1024; - private static final int JAVA_SAMPLING_PROFILE_DELTA_US = 100; - private final Context mApplicationContext; private final ShakeDetector mShakeDetector; private final BroadcastReceiver mReloadAppBroadcastReceiver; @@ -109,8 +106,6 @@ public class DevSupportManagerImpl implements DevSupportManager { private boolean mIsReceiverRegistered = false; private boolean mIsShakeDetectorStarted = false; private boolean mIsDevSupportEnabled = false; - private boolean mIsCurrentlyProfiling = false; - private int mProfileIndex = 0; private @Nullable RedBoxHandler mRedBoxHandler; public DevSupportManagerImpl( @@ -355,42 +350,6 @@ public void onOptionSelected() { } } }); - if (mCurrentContext != null && - mCurrentContext.getCatalystInstance() != null && - !mCurrentContext.getCatalystInstance().isDestroyed() && - mCurrentContext.getCatalystInstance().supportsProfiling()) { - options.put( - mApplicationContext.getString( - mIsCurrentlyProfiling ? R.string.catalyst_stop_profile : - R.string.catalyst_start_profile), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - if (mCurrentContext != null && mCurrentContext.hasActiveCatalystInstance()) { - String profileName = (Environment.getExternalStorageDirectory().getPath() + - "/profile_" + mProfileIndex + ".json"); - if (mIsCurrentlyProfiling) { - mIsCurrentlyProfiling = false; - mProfileIndex++; - Debug.stopMethodTracing(); - mCurrentContext.getCatalystInstance() - .stopProfiler("profile", profileName); - Toast.makeText( - mCurrentContext, - "Profile output to " + profileName, - Toast.LENGTH_LONG).show(); - } else { - mIsCurrentlyProfiling = true; - mCurrentContext.getCatalystInstance().startProfiler("profile"); - Debug.startMethodTracingSampling( - profileName, - JAVA_SAMPLING_PROFILE_MEMORY_BYTES, - JAVA_SAMPLING_PROFILE_DELTA_US); - } - } - } - }); - } options.put( mApplicationContext.getString(R.string.catalyst_settings), new DevOptionHandler() { @Override @@ -550,16 +509,6 @@ private void resetCurrentContext(@Nullable ReactContext reactContext) { return; } - // if currently profiling stop and write the profile file - if (mIsCurrentlyProfiling) { - mIsCurrentlyProfiling = false; - String profileName = (Environment.getExternalStorageDirectory().getPath() + - "/profile_" + mProfileIndex + ".json"); - mProfileIndex++; - Debug.stopMethodTracing(); - mCurrentContext.getCatalystInstance().stopProfiler("profile", profileName); - } - mCurrentContext = reactContext; // Recreate debug overlay controller with new CatalystInstance object diff --git a/ReactAndroid/src/main/res/devsupport/values/strings.xml b/ReactAndroid/src/main/res/devsupport/values/strings.xml index edf2669c1c1186..fc84ce1d28338c 100644 --- a/ReactAndroid/src/main/res/devsupport/values/strings.xml +++ b/ReactAndroid/src/main/res/devsupport/values/strings.xml @@ -17,7 +17,5 @@ Connecting to remote debugger Unable to connect with remote debugger Toggle Inspector - Start Profile - Stop Profile Capture Heap From a7404713a49b8db76fb4d1cf45bf8ddcf4121081 Mon Sep 17 00:00:00 2001 From: Siqi Liu Date: Tue, 7 Jun 2016 10:46:49 -0700 Subject: [PATCH 268/843] Followup for Intercepting all redboxes in Android Ads Manager Reviewed By: mkonicek Differential Revision: D3398979 fbshipit-source-id: 9ea9ba6d45daf01284717a9eb889c16607109683 --- .../facebook/react/ReactInstanceManager.java | 2 +- .../devsupport/DevSupportManagerImpl.java | 28 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index a000a7ebd83ec2..0784a6e940935d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -304,7 +304,7 @@ public Builder setJSCConfig(JSCConfig jscConfig) { return this; } - public Builder setRedBoxHandler(RedBoxHandler redBoxHandler) { + public Builder setRedBoxHandler(@Nullable RedBoxHandler redBoxHandler) { mRedBoxHandler = redBoxHandler; return this; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index 21a025c573507f..f48d6320025a43 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -113,6 +113,21 @@ public DevSupportManagerImpl( ReactInstanceDevCommandsHandler reactInstanceCommandsHandler, @Nullable String packagerPathForJSBundleName, boolean enableOnCreate) { + + this(applicationContext, + reactInstanceCommandsHandler, + packagerPathForJSBundleName, + enableOnCreate, + null); + } + + public DevSupportManagerImpl( + Context applicationContext, + ReactInstanceDevCommandsHandler reactInstanceCommandsHandler, + @Nullable String packagerPathForJSBundleName, + boolean enableOnCreate, + @Nullable RedBoxHandler redBoxHandler) { + mReactInstanceCommandsHandler = reactInstanceCommandsHandler; mApplicationContext = applicationContext; mJSAppBundleName = packagerPathForJSBundleName; @@ -154,19 +169,6 @@ public void onReceive(Context context, Intent intent) { mDefaultNativeModuleCallExceptionHandler = new DefaultNativeModuleCallExceptionHandler(); setDevSupportEnabled(enableOnCreate); - } - - public DevSupportManagerImpl( - Context applicationContext, - ReactInstanceDevCommandsHandler reactInstanceCommandsHandler, - @Nullable String packagerPathForJSBundleName, - boolean enableOnCreate, - @Nullable RedBoxHandler redBoxHandler) { - - this(applicationContext, - reactInstanceCommandsHandler, - packagerPathForJSBundleName, - enableOnCreate); mRedBoxHandler = redBoxHandler; } From f3fab5184ea5e2a366e016e7b34479779f59a9b1 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Tue, 7 Jun 2016 11:07:53 -0700 Subject: [PATCH 269/843] Remove SourceCode.getScriptText Summary: After cleaning up JS SourceMap code, these native methods are not needed anymore. On iOS it saves another 30+ Mb during development. Reviewed By: javache, astreet Differential Revision: D3348975 fbshipit-source-id: a68ae9b00b4dbaa374b421029ae676fc69ae5a75 --- React/Base/RCTBatchedBridge.m | 1 - React/Modules/RCTSourceCode.h | 1 - React/Modules/RCTSourceCode.m | 17 ----------------- .../com/facebook/react/CoreModulesPackage.java | 4 +--- .../react/modules/debug/SourceCodeModule.java | 11 +---------- 5 files changed, 2 insertions(+), 32 deletions(-) diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index 3314a2d071e902..98d0615db4cb76 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -455,7 +455,6 @@ - (void)executeSourceCode:(NSData *)sourceCode RCTSourceCode *sourceCodeModule = [self moduleForClass:[RCTSourceCode class]]; sourceCodeModule.scriptURL = self.bundleURL; - sourceCodeModule.scriptData = sourceCode; [self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:^(NSError *loadError) { if (!_valid) { diff --git a/React/Modules/RCTSourceCode.h b/React/Modules/RCTSourceCode.h index 1acf6f1e03139d..4e2ef845cdd790 100644 --- a/React/Modules/RCTSourceCode.h +++ b/React/Modules/RCTSourceCode.h @@ -13,7 +13,6 @@ @interface RCTSourceCode : NSObject -@property (nonatomic, copy) NSData *scriptData; @property (nonatomic, copy) NSURL *scriptURL; @end diff --git a/React/Modules/RCTSourceCode.m b/React/Modules/RCTSourceCode.m index 1608aca706163c..df81bb89aa7d45 100644 --- a/React/Modules/RCTSourceCode.m +++ b/React/Modules/RCTSourceCode.m @@ -20,23 +20,6 @@ @implementation RCTSourceCode @synthesize bridge = _bridge; -#if !RCT_DEV -- (void)setScriptText:(NSString *)scriptText {} -#endif - -NSString *const RCTErrorUnavailable = @"E_SOURCE_CODE_UNAVAILABLE"; - -RCT_EXPORT_METHOD(getScriptText:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject) -{ - if (RCT_DEV && self.scriptData && self.scriptURL) { - NSString *scriptText = [[NSString alloc] initWithData:self.scriptData encoding:NSUTF8StringEncoding]; - resolve(@{@"text": RCTNullIfNil(scriptText), @"url": self.scriptURL.absoluteString}); - } else { - reject(RCTErrorUnavailable, nil, RCTErrorWithMessage(@"Source code is not available")); - } -} - - (NSDictionary *)constantsToExport { NSString *URL = self.bridge.bundleURL.absoluteString ?: @""; diff --git a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java index 9b975bf552d23f..8be15fb6d2812e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java @@ -81,9 +81,7 @@ public List createNativeModules( new DeviceEventManagerModule(catalystApplicationContext, mHardwareBackBtnHandler), new ExceptionsManagerModule(mReactInstanceManager.getDevSupportManager()), new Timing(catalystApplicationContext), - new SourceCodeModule( - mReactInstanceManager.getSourceUrl(), - mReactInstanceManager.getDevSupportManager().getSourceMapUrl()), + new SourceCodeModule(mReactInstanceManager.getSourceUrl()), uiManagerModule, new JSCHeapCapture(catalystApplicationContext), new DebugComponentOwnershipModule(catalystApplicationContext)); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java index 3417c81946af87..71aba3511c488b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/debug/SourceCodeModule.java @@ -25,11 +25,9 @@ */ public class SourceCodeModule extends BaseJavaModule { - private final String mSourceMapUrl; private final String mSourceUrl; - public SourceCodeModule(String sourceUrl, String sourceMapUrl) { - mSourceMapUrl = sourceMapUrl; + public SourceCodeModule(String sourceUrl) { mSourceUrl = sourceUrl; } @@ -38,13 +36,6 @@ public String getName() { return "RCTSourceCode"; } - @ReactMethod - public void getScriptText(final Promise promise) { - WritableMap map = new WritableNativeMap(); - map.putString("fullSourceMappingURL", mSourceMapUrl); - promise.resolve(map); - } - @Override public @Nullable Map getConstants() { HashMap constants = new HashMap(); From 837aafde72fb43836aa7c36cf66db4e9abae0e17 Mon Sep 17 00:00:00 2001 From: Olivier Notteghem Date: Tue, 7 Jun 2016 11:53:28 -0700 Subject: [PATCH 270/843] Fix touch handling crash that can when Modal is displayed as a result of touch handling code Reviewed By: dmmiller Differential Revision: D3399325 fbshipit-source-id: 0a0bb179ef524bf35cc137d94ea3d5680a68aab2 --- .../react/uimanager/JSTouchDispatcher.java | 22 ++++++++++------ .../react/uimanager/events/TouchEvent.java | 25 +++++++++++++------ .../events/TouchEventCoalescingKeyHelper.java | 24 +++++++++--------- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java index a5d7056ce86599..0fba856555339f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java @@ -32,6 +32,8 @@ public class JSTouchDispatcher { private final float[] mTargetCoordinates = new float[2]; private boolean mChildIsHandlingNativeGesture = false; private final ViewGroup mRootViewGroup; + private final TouchEventCoalescingKeyHelper mTouchEventCoalescingKeyHelper = + new TouchEventCoalescingKeyHelper(); public JSTouchDispatcher(ViewGroup viewGroup) { mRootViewGroup = viewGroup; @@ -83,7 +85,8 @@ public void handleTouchEvent(MotionEvent ev, EventDispatcher eventDispatcher) { TouchEventType.START, ev, mTargetCoordinates[0], - mTargetCoordinates[1])); + mTargetCoordinates[1], + mTouchEventCoalescingKeyHelper)); } else if (mChildIsHandlingNativeGesture) { // If the touch was intercepted by a child, we've already sent a cancel event to JS for this // gesture, so we shouldn't send any more touches related to it. @@ -105,7 +108,8 @@ public void handleTouchEvent(MotionEvent ev, EventDispatcher eventDispatcher) { TouchEventType.END, ev, mTargetCoordinates[0], - mTargetCoordinates[1])); + mTargetCoordinates[1], + mTouchEventCoalescingKeyHelper)); mTargetTag = -1; } else if (action == MotionEvent.ACTION_MOVE) { // Update pointer position for current gesture @@ -116,7 +120,8 @@ public void handleTouchEvent(MotionEvent ev, EventDispatcher eventDispatcher) { TouchEventType.MOVE, ev, mTargetCoordinates[0], - mTargetCoordinates[1])); + mTargetCoordinates[1], + mTouchEventCoalescingKeyHelper)); } else if (action == MotionEvent.ACTION_POINTER_DOWN) { // New pointer goes down, this can only happen after ACTION_DOWN is sent for the first pointer eventDispatcher.dispatchEvent( @@ -126,7 +131,8 @@ public void handleTouchEvent(MotionEvent ev, EventDispatcher eventDispatcher) { TouchEventType.START, ev, mTargetCoordinates[0], - mTargetCoordinates[1])); + mTargetCoordinates[1], + mTouchEventCoalescingKeyHelper)); } else if (action == MotionEvent.ACTION_POINTER_UP) { // Exactly onw of the pointers goes up eventDispatcher.dispatchEvent( @@ -136,9 +142,10 @@ public void handleTouchEvent(MotionEvent ev, EventDispatcher eventDispatcher) { TouchEventType.END, ev, mTargetCoordinates[0], - mTargetCoordinates[1])); + mTargetCoordinates[1], + mTouchEventCoalescingKeyHelper)); } else if (action == MotionEvent.ACTION_CANCEL) { - if (TouchEventCoalescingKeyHelper.hasCoalescingKey(ev.getDownTime())) { + if (mTouchEventCoalescingKeyHelper.hasCoalescingKey(ev.getDownTime())) { dispatchCancelEvent(ev, eventDispatcher); } else { FLog.e( @@ -176,6 +183,7 @@ private void dispatchCancelEvent(MotionEvent androidEvent, EventDispatcher event TouchEventType.CANCEL, androidEvent, mTargetCoordinates[0], - mTargetCoordinates[1])); + mTargetCoordinates[1], + mTouchEventCoalescingKeyHelper)); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java index 1600b74783d047..a7df8fb3eab968 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java @@ -37,12 +37,20 @@ public static TouchEvent obtain( TouchEventType touchEventType, MotionEvent motionEventToCopy, float viewX, - float viewY) { + float viewY, + TouchEventCoalescingKeyHelper touchEventCoalescingKeyHelper) { TouchEvent event = EVENTS_POOL.acquire(); if (event == null) { event = new TouchEvent(); } - event.init(viewTag, timestampMs, touchEventType, motionEventToCopy, viewX, viewY); + event.init( + viewTag, + timestampMs, + touchEventType, + motionEventToCopy, + viewX, + viewY, + touchEventCoalescingKeyHelper); return event; } @@ -63,28 +71,29 @@ private void init( TouchEventType touchEventType, MotionEvent motionEventToCopy, float viewX, - float viewY) { + float viewY, + TouchEventCoalescingKeyHelper touchEventCoalescingKeyHelper) { super.init(viewTag, timestampMs); short coalescingKey = 0; int action = (motionEventToCopy.getAction() & MotionEvent.ACTION_MASK); switch (action) { case MotionEvent.ACTION_DOWN: - TouchEventCoalescingKeyHelper.addCoalescingKey(motionEventToCopy.getDownTime()); + touchEventCoalescingKeyHelper.addCoalescingKey(motionEventToCopy.getDownTime()); break; case MotionEvent.ACTION_UP: - TouchEventCoalescingKeyHelper.removeCoalescingKey(motionEventToCopy.getDownTime()); + touchEventCoalescingKeyHelper.removeCoalescingKey(motionEventToCopy.getDownTime()); break; case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_POINTER_UP: - TouchEventCoalescingKeyHelper.incrementCoalescingKey(motionEventToCopy.getDownTime()); + touchEventCoalescingKeyHelper.incrementCoalescingKey(motionEventToCopy.getDownTime()); break; case MotionEvent.ACTION_MOVE: coalescingKey = - TouchEventCoalescingKeyHelper.getCoalescingKey(motionEventToCopy.getDownTime()); + touchEventCoalescingKeyHelper.getCoalescingKey(motionEventToCopy.getDownTime()); break; case MotionEvent.ACTION_CANCEL: - TouchEventCoalescingKeyHelper.removeCoalescingKey(motionEventToCopy.getDownTime()); + touchEventCoalescingKeyHelper.removeCoalescingKey(motionEventToCopy.getDownTime()); break; default: throw new RuntimeException("Unhandled MotionEvent action: " + action); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper.java index acc2d3afa38615..3003dedb4c5d32 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper.java @@ -46,31 +46,31 @@ */ public class TouchEventCoalescingKeyHelper { - private static final SparseIntArray sDownTimeToCoalescingKey = new SparseIntArray(); + private final SparseIntArray mDownTimeToCoalescingKey = new SparseIntArray(); /** * Starts tracking a new coalescing key corresponding to the gesture with this down time. */ - public static void addCoalescingKey(long downTime) { - sDownTimeToCoalescingKey.put((int) downTime, 0); + public void addCoalescingKey(long downTime) { + mDownTimeToCoalescingKey.put((int) downTime, 0); } /** * Increments the coalescing key corresponding to the gesture with this down time. */ - public static void incrementCoalescingKey(long downTime) { - int currentValue = sDownTimeToCoalescingKey.get((int) downTime, -1); + public void incrementCoalescingKey(long downTime) { + int currentValue = mDownTimeToCoalescingKey.get((int) downTime, -1); if (currentValue == -1) { throw new RuntimeException("Tried to increment non-existent cookie"); } - sDownTimeToCoalescingKey.put((int) downTime, currentValue + 1); + mDownTimeToCoalescingKey.put((int) downTime, currentValue + 1); } /** * Gets the coalescing key corresponding to the gesture with this down time. */ - public static short getCoalescingKey(long downTime) { - int currentValue = sDownTimeToCoalescingKey.get((int) downTime, -1); + public short getCoalescingKey(long downTime) { + int currentValue = mDownTimeToCoalescingKey.get((int) downTime, -1); if (currentValue == -1) { throw new RuntimeException("Tried to get non-existent cookie"); } @@ -80,12 +80,12 @@ public static short getCoalescingKey(long downTime) { /** * Stops tracking a new coalescing key corresponding to the gesture with this down time. */ - public static void removeCoalescingKey(long downTime) { - sDownTimeToCoalescingKey.delete((int) downTime); + public void removeCoalescingKey(long downTime) { + mDownTimeToCoalescingKey.delete((int) downTime); } - public static boolean hasCoalescingKey(long downTime) { - int currentValue = sDownTimeToCoalescingKey.get((int) downTime, -1); + public boolean hasCoalescingKey(long downTime) { + int currentValue = mDownTimeToCoalescingKey.get((int) downTime, -1); if (currentValue == -1) { return false; } From 381a0051c7ec5bde4c35967e336050c8dca6a677 Mon Sep 17 00:00:00 2001 From: Fred Liu Date: Tue, 7 Jun 2016 11:55:38 -0700 Subject: [PATCH 271/843] Swipeables documentation Summary: So users can know how to use Reviewed By: hedgerwang Differential Revision: D3399028 fbshipit-source-id: 5ce97c5464f1975145aba1bcf4b79550394859ae --- .../SwipeableRow/SwipeableListView.js | 25 ++++++++++++++++++- .../SwipeableQuickActionButton.js | 5 ++++ .../SwipeableRow/SwipeableQuickActions.js | 10 ++++++++ .../Experimental/SwipeableRow/SwipeableRow.js | 5 +++- 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/Libraries/Experimental/SwipeableRow/SwipeableListView.js b/Libraries/Experimental/SwipeableRow/SwipeableListView.js index 4600483d47ca74..ea0e5e22241976 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableListView.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableListView.js @@ -32,7 +32,21 @@ const {PropTypes} = React; /** * A container component that renders multiple SwipeableRow's in a ListView - * implementation. + * implementation. This is designed to be a drop-in replacement for the + * standard React Native `ListView`, so use it as if it were a ListView, but + * with extra props, i.e. + * + * let ds = SwipeableListView.getNewDataSource(); + * ds.cloneWithRowsAndSections(dataBlob, ?sectionIDs, ?rowIDs); + * // .. + * + * + * SwipeableRow can be used independently of this component, but the main + * benefit of using this component is + * + * - It ensures that at most 1 row is swiped open (auto closes others) + * - It can bounce the 1st row of the list so users know it's swipeable + * - More to come */ const SwipeableListView = React.createClass({ statics: { @@ -50,8 +64,17 @@ const SwipeableListView = React.createClass({ _shouldBounceFirstRowOnMount: false, propTypes: { + /** + * To alert the user that swiping is possible, the first row can bounce + * on component mount. + */ bounceFirstRowOnMount: PropTypes.bool.isRequired, + /** + * Use `SwipeableListView.getNewDataSource()` to get a data source to use, + * then use it just like you would a normal ListView data source + */ dataSource: PropTypes.instanceOf(SwipeableListViewDataSource).isRequired, + // Maximum distance to open to after a swipe maxSwipeDistance: PropTypes.number, // Callback method to render the swipeable view renderRow: PropTypes.func.isRequired, diff --git a/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js b/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js index de7f27ea268256..10dc7a6c5326b7 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js @@ -31,6 +31,11 @@ const View = require('View'); const {PropTypes} = React; +/** + * Standard set of quick action buttons that can, if the user chooses, be used + * with SwipeableListView. Each button takes an image and text with optional + * formatting. + */ const SwipeableQuickActionButton = React.createClass({ propTypes: { accessibilityLabel: PropTypes.string, diff --git a/Libraries/Experimental/SwipeableRow/SwipeableQuickActions.js b/Libraries/Experimental/SwipeableRow/SwipeableQuickActions.js index 13e97ab5c28b50..d5b77585ab2138 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableQuickActions.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableQuickActions.js @@ -29,6 +29,16 @@ const View = require('View'); const MAX_QUICK_ACTIONS = 2; +/** + * A thin wrapper around standard quick action buttons that can, if the user + * chooses, be used with SwipeableListView. Sample usage is as follows, in the + * renderQuickActions callback: + * + * + * + * + * + */ const SwipeableQuickActions = React.createClass({ propTypes: { style: View.propTypes.style, diff --git a/Libraries/Experimental/SwipeableRow/SwipeableRow.js b/Libraries/Experimental/SwipeableRow/SwipeableRow.js index e615fc80485b54..11fe84d4ecc2e4 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableRow.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableRow.js @@ -65,7 +65,10 @@ const RIGHT_SWIPE_THRESHOLD = 30 * SLOW_SPEED_SWIPE_FACTOR; /** * Creates a swipable row that allows taps on the main item and a custom View - * on the item hidden behind the row + * on the item hidden behind the row. Typically this should be used in + * conjunction with SwipeableListView for additional functionality, but can be + * used in a normal ListView. See the renderRow for SwipeableListView to see how + * to use this component separately. */ const SwipeableRow = React.createClass({ _panResponder: {}, From 39eca05b91d0ca344db755dfefb590472485cbf7 Mon Sep 17 00:00:00 2001 From: Maarten Schumacher Date: Tue, 7 Jun 2016 12:00:29 -0700 Subject: [PATCH 272/843] Handle the case where redirect URL is relative MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: While trying to download an image I encountered an error: ![screen shot 2016-05-31 at 13 58 54](https://cloud.githubusercontent.com/assets/10407025/15799726/a7bcbf1c-2a5d-11e6-926a-b2f84e011258.png) As you can see the domain name is missing from the URL. I traced it back to a method which handles redirects. It would retrieve the redirect URL from the `Location` header field, but it didn’t handle the case where `Location` contains a relative URL (which [according to the RFC spec](https://tools.ietf.org/html/rfc7231#section-7.1.2), is allowed). Closes https://github.com/facebook/react-native/pull/7926 Differential Revision: D3399531 Pulled By: nicklockwood fbshipit-source-id: ffbd5e9fc55b1737a8ff6a9bcc06fb1f9f19d093 --- Libraries/Image/RCTImageLoader.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 3aefdb0c65ecfe..661f6bf119e47c 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -397,7 +397,7 @@ - (RCTImageLoaderCancellationBlock)loadImageOrDataWithURLRequest:(NSURLRequest * return; } - NSURL *redirectURL = [NSURL URLWithString: location]; + NSURL *redirectURL = [NSURL URLWithString: location relativeToURL: request.URL]; request = [NSURLRequest requestWithURL:redirectURL]; cachedResponse = [_URLCache cachedResponseForRequest:request]; continue; From 26a5b033f88f66e56df5f968960ed672a705a5ef Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 7 Jun 2016 12:33:58 -0700 Subject: [PATCH 273/843] Fixed unit tests Reviewed By: bestander Differential Revision: D3399766 fbshipit-source-id: ebc72a9d02ff812eccae13efc1492d2fec3f95c3 --- .../RCTUIManagerScenarioTests.m | 4 ++++ .../testViewExample_1@2x.png | Bin 156461 -> 113288 bytes .../UIExplorerUnitTests/RCTUIManagerTests.m | 6 ++++++ 3 files changed, 10 insertions(+) diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m index ba0bbf508aab02..d36a112775b395 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m @@ -73,6 +73,8 @@ - (void)testManagingChildrenToAddViews removeAtIndices:nil registry:(NSMutableDictionary> *)_uiManager.viewRegistry]; + [_uiManager.viewRegistry[@20] didUpdateReactSubviews]; + XCTAssertTrue([[containerView reactSubviews] count] == 5, @"Expect to have 5 react subviews after calling manage children \ with 5 tags to add, instead have %lu", (unsigned long)[[containerView reactSubviews] count]); @@ -107,6 +109,8 @@ - (void)testManagingChildrenToRemoveViews removeAtIndices:removeAtIndices registry:(NSMutableDictionary> *)_uiManager.viewRegistry]; + [_uiManager.viewRegistry[@20] didUpdateReactSubviews]; + XCTAssertEqual(containerView.reactSubviews.count, (NSUInteger)13, @"Expect to have 13 react subviews after calling manage children\ with 5 tags to remove and 18 prior children, instead have %zd", diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExample_1@2x.png b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExample_1@2x.png index 45bdb2631f1b63e2ce2b3a4fa974a2dfaf3e997b..0da195f5c3fedea7a5ad4d2a711e4e45c9f1ee2e 100644 GIT binary patch literal 113288 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Ν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 literal 156461 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 Date: Tue, 7 Jun 2016 13:25:20 -0700 Subject: [PATCH 274/843] Begin inverting dependency between new+old bridge Reviewed By: mhorowitz Differential Revision: D3306510 fbshipit-source-id: 312a498a4461e980f1f749fe7858a13be14dfa2f --- .../src/main/java/com/facebook/react/BUCK | 20 +------ .../facebook/react/ReactInstanceManager.java | 57 +++++++++++++------ .../facebook/react/XReactInstanceManager.java | 53 +---------------- .../java/com/facebook/react/cxxbridge/BUCK | 2 +- 4 files changed, 47 insertions(+), 85 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/BUCK b/ReactAndroid/src/main/java/com/facebook/react/BUCK index 6fcd3551cfe4a5..1c925ed23091c6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/BUCK @@ -1,11 +1,7 @@ include_defs('//ReactAndroid/DEFS') -XREACT_SRCS = [ - 'XReactInstanceManager.java', - 'XReactInstanceManagerImpl.java', -] - DEPS = [ + react_native_target('java/com/facebook/react/cxxbridge:bridge'), react_native_target('java/com/facebook/react/bridge:bridge'), react_native_target('java/com/facebook/react/common:common'), react_native_target('java/com/facebook/react/devsupport:devsupport'), @@ -23,25 +19,13 @@ DEPS = [ android_library( name = 'react', - srcs = glob(['*.java'], excludes=XREACT_SRCS), + srcs = glob(['*.java']), deps = DEPS, visibility = [ 'PUBLIC', ], ) -android_library( - name = 'xreact', - srcs = XREACT_SRCS, - deps = DEPS + [ - ':react', - react_native_target('java/com/facebook/react/cxxbridge:bridge'), - ], - visibility = [ - 'PUBLIC', - ], -) - project_config( src_target = ':react', ) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index 0784a6e940935d..51994024170129 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -195,6 +195,7 @@ public static class Builder { protected @Nullable Activity mCurrentActivity; protected @Nullable DefaultHardwareBackBtnHandler mDefaultHardwareBackBtnHandler; protected @Nullable RedBoxHandler mRedBoxHandler; + protected boolean mUseNewBridge; protected Builder() { } @@ -309,6 +310,11 @@ public Builder setRedBoxHandler(@Nullable RedBoxHandler redBoxHandler) { return this; } + public Builder setUseNewBridge() { + mUseNewBridge = true; + return this; + } + /** * Instantiates a new {@link ReactInstanceManagerImpl}. * Before calling {@code build}, the following must be called: @@ -333,22 +339,41 @@ public ReactInstanceManager build() { mUIImplementationProvider = new UIImplementationProvider(); } - return new ReactInstanceManagerImpl( - Assertions.assertNotNull( - mApplication, - "Application property has not been set with this builder"), - mCurrentActivity, - mDefaultHardwareBackBtnHandler, - mJSBundleFile, - mJSMainModuleName, - mPackages, - mUseDeveloperSupport, - mBridgeIdleDebugListener, - Assertions.assertNotNull(mInitialLifecycleState, "Initial lifecycle state was not set"), - mUIImplementationProvider, - mNativeModuleCallExceptionHandler, - mJSCConfig, - mRedBoxHandler); + if (mUseNewBridge) { + return new XReactInstanceManagerImpl( + Assertions.assertNotNull( + mApplication, + "Application property has not been set with this builder"), + mCurrentActivity, + mDefaultHardwareBackBtnHandler, + mJSBundleFile, + mJSMainModuleName, + mPackages, + mUseDeveloperSupport, + mBridgeIdleDebugListener, + Assertions.assertNotNull(mInitialLifecycleState, "Initial lifecycle state was not set"), + mUIImplementationProvider, + mNativeModuleCallExceptionHandler, + mJSCConfig, + mRedBoxHandler); + } else { + return new ReactInstanceManagerImpl( + Assertions.assertNotNull( + mApplication, + "Application property has not been set with this builder"), + mCurrentActivity, + mDefaultHardwareBackBtnHandler, + mJSBundleFile, + mJSMainModuleName, + mPackages, + mUseDeveloperSupport, + mBridgeIdleDebugListener, + Assertions.assertNotNull(mInitialLifecycleState, "Initial lifecycle state was not set"), + mUIImplementationProvider, + mNativeModuleCallExceptionHandler, + mJSCConfig, + mRedBoxHandler); + } } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java index b3ba638c518279..548343fc6a9f3f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/XReactInstanceManager.java @@ -14,56 +14,9 @@ public abstract class XReactInstanceManager { /** - * Creates a builder that is capable of creating an instance of {@link XReactInstanceManagerImpl}. + * Creates a builder that is defaulted to using the new bridge. */ - public static Builder builder() { - return new Builder(); - } - - /** - * Builder class for {@link XReactInstanceManagerImpl} - */ - public static class Builder extends ReactInstanceManager.Builder { - /** - * Instantiates a new {@link ReactInstanceManagerImpl}. - * Before calling {@code build}, the following must be called: - *
      - *
    • {@link #setApplication} - *
    • {@link #setCurrentActivity} if the activity has already resumed - *
    • {@link #setDefaultHardwareBackBtnHandler} if the activity has already resumed - *
    • {@link #setJSBundleFile} or {@link #setJSMainModuleName} - *
    - */ - public ReactInstanceManager build() { - Assertions.assertCondition( - mUseDeveloperSupport || mJSBundleFile != null, - "JS Bundle File has to be provided when dev support is disabled"); - - Assertions.assertCondition( - mJSMainModuleName != null || mJSBundleFile != null, - "Either MainModuleName or JS Bundle File needs to be provided"); - - if (mUIImplementationProvider == null) { - // create default UIImplementationProvider if the provided one is null. - mUIImplementationProvider = new UIImplementationProvider(); - } - - return new XReactInstanceManagerImpl( - Assertions.assertNotNull( - mApplication, - "Application property has not been set with this builder"), - mCurrentActivity, - mDefaultHardwareBackBtnHandler, - mJSBundleFile, - mJSMainModuleName, - mPackages, - mUseDeveloperSupport, - mBridgeIdleDebugListener, - Assertions.assertNotNull(mInitialLifecycleState, "Initial lifecycle state was not set"), - mUIImplementationProvider, - mNativeModuleCallExceptionHandler, - mJSCConfig, - mRedBoxHandler); - } + public static ReactInstanceManager.Builder builder() { + return new ReactInstanceManager.Builder().setUseNewBridge(); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK index feed577072e8bb..898715d8955d87 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/BUCK @@ -9,8 +9,8 @@ android_library( ], proguard_config = 'bridge.pro', deps = [ - '//libraries/fbcore/src/main/java/com/facebook/common/logging:logging', react_native_dep('java/com/facebook/systrace:systrace'), + react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'), react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'), # TODO mhorowitz: # java/com/facebook/catalyst/js/react-native-github/ReactAndroid/src/main/java/com/facebook/react/bridge/ From f8cbd9f4f4b483886c7648d431fa9de8df06dbb4 Mon Sep 17 00:00:00 2001 From: Jon Lebensold Date: Tue, 7 Jun 2016 15:25:40 -0700 Subject: [PATCH 275/843] typo in the error message (missing a space due to newline) Summary: Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: (You can skip this if you're fixing a typo or adding an app to the Showcase.) Explain the **motivation** for making this change. What existing problem does the pull request solve? Prefer **small pull requests**. These are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise please split it. **Test plan (required)** Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. Make sure tests pass on both Travis and Circle CI. **Code formatting** Look around. Match the style of the rest of the codebase. See also the simple [style guide](https://github.com/facebook/react-native/blob/master/CONTRIBUTING.md#style-guide). For more info, see the ["Pull Requests" section of our "Contributing" guidelines](https://github.com/facebook/react-native/blob/mas Closes https://github.com/facebook/react-native/pull/7974 Differential Revision: D3401208 Pulled By: nicklockwood fbshipit-source-id: 7c807339ba512e05f848036fb40840cf3fee8df6 --- .../NavigationExperimental/Reducer/NavigationScenesReducer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js index 3e08b8727ad46d..75f3bd330b7f72 100644 --- a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js +++ b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js @@ -122,7 +122,7 @@ function NavigationScenesReducer( }; invariant( !nextKeys.has(key), - `navigationState.routes[${index}].key "${key}" conflicts with` + + `navigationState.routes[${index}].key "${key}" conflicts with ` + 'another route!' ); nextKeys.add(key); From 0e50373b606282ef26350119131077b10336c82c Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Tue, 7 Jun 2016 16:19:23 -0700 Subject: [PATCH 276/843] Invert native dependency of new bridge -> old bridge Reviewed By: mhorowitz Differential Revision: D3400379 fbshipit-source-id: d641846261977b4f77c2d215bef94e8e676e2c02 --- .../react/bridge/JSCJavaScriptExecutor.java | 8 ++++---- .../com/facebook/react/bridge/NativeArray.java | 2 +- .../java/com/facebook/react/bridge/NativeMap.java | 2 +- .../react/bridge/ProxyJavaScriptExecutor.java | 8 ++++---- .../com/facebook/react/bridge/ReactBridge.java | 10 ++++++++-- .../react/bridge/ReadableNativeArray.java | 3 +-- .../facebook/react/bridge/ReadableNativeMap.java | 3 +-- .../react/bridge/WritableNativeArray.java | 3 +-- .../facebook/react/bridge/WritableNativeMap.java | 2 +- ReactAndroid/src/main/jni/react/jni/Android.mk | 10 ++-------- ReactAndroid/src/main/jni/react/jni/BUCK | 15 +-------------- ReactAndroid/src/main/jni/react/jni/OnLoad.cpp | 14 +++----------- ReactAndroid/src/main/jni/xreact/jni/Android.mk | 12 +++++++++--- ReactAndroid/src/main/jni/xreact/jni/BUCK | 10 ++++++++-- .../main/jni/xreact/jni/CatalystInstanceImpl.cpp | 3 +-- .../src/main/jni/xreact/jni/CxxModuleWrapper.cpp | 11 ++++++----- ReactAndroid/src/main/jni/xreact/jni/JCallback.h | 2 +- .../src/main/jni/xreact/jni/JSCPerfLogging.cpp | 3 ++- .../src/main/jni/xreact/jni/JSLogging.cpp | 3 ++- .../src/main/jni/xreact/jni/MethodInvoker.cpp | 2 +- .../main/jni/xreact/jni/ModuleRegistryHolder.cpp | 5 ++--- .../jni/{react => xreact}/jni/NativeArray.cpp | 0 .../main/jni/{react => xreact}/jni/NativeArray.h | 0 .../jni/{react => xreact}/jni/NativeCommon.cpp | 0 .../main/jni/{react => xreact}/jni/NativeCommon.h | 0 .../main/jni/{react => xreact}/jni/NativeMap.cpp | 0 .../main/jni/{react => xreact}/jni/NativeMap.h | 0 ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp | 12 +++++++++++- .../{react => xreact}/jni/ReadableNativeArray.cpp | 0 .../{react => xreact}/jni/ReadableNativeArray.h | 0 .../{react => xreact}/jni/ReadableNativeMap.cpp | 0 .../jni/{react => xreact}/jni/ReadableNativeMap.h | 0 .../{react => xreact}/jni/WritableNativeArray.cpp | 0 .../{react => xreact}/jni/WritableNativeArray.h | 0 .../{react => xreact}/jni/WritableNativeMap.cpp | 0 .../jni/{react => xreact}/jni/WritableNativeMap.h | 1 + 36 files changed, 72 insertions(+), 72 deletions(-) rename ReactAndroid/src/main/jni/{react => xreact}/jni/NativeArray.cpp (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/NativeArray.h (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/NativeCommon.cpp (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/NativeCommon.h (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/NativeMap.cpp (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/NativeMap.h (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/ReadableNativeArray.cpp (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/ReadableNativeArray.h (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/ReadableNativeMap.cpp (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/ReadableNativeMap.h (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/WritableNativeArray.cpp (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/WritableNativeArray.h (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/WritableNativeMap.cpp (100%) rename ReactAndroid/src/main/jni/{react => xreact}/jni/WritableNativeMap.h (96%) diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java index b4431c0d66d9cb..4f895abf31aa26 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java @@ -14,6 +14,10 @@ @DoNotStrip public class JSCJavaScriptExecutor extends JavaScriptExecutor { + static { + ReactBridge.staticInit(); + } + public static class Factory implements JavaScriptExecutor.Factory { @Override public JavaScriptExecutor create(WritableNativeMap jscConfig) throws Exception { @@ -21,10 +25,6 @@ public JavaScriptExecutor create(WritableNativeMap jscConfig) throws Exception { } } - static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); - } - public JSCJavaScriptExecutor(WritableNativeMap jscConfig) { initialize(jscConfig); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java index 2045a4b23e0016..d4c46f72302c53 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java @@ -19,7 +19,7 @@ @DoNotStrip public abstract class NativeArray { static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + ReactBridge.staticInit(); } protected NativeArray(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java index 2c683f9451130e..9e192dc4ea6d8d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java @@ -19,7 +19,7 @@ @DoNotStrip public abstract class NativeMap { static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + ReactBridge.staticInit(); } public NativeMap(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java index 37d83b3386174c..6ebf4dc0df6e01 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java @@ -24,6 +24,10 @@ */ @DoNotStrip public class ProxyJavaScriptExecutor extends JavaScriptExecutor { + static { + ReactBridge.staticInit(); + } + public static class Factory implements JavaScriptExecutor.Factory { private final JavaJSExecutor.Factory mJavaJSExecutorFactory; @@ -37,10 +41,6 @@ public JavaScriptExecutor create(WritableNativeMap jscConfig) throws Exception { } } - static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); - } - private @Nullable JavaJSExecutor mJavaJSExecutor; /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java index e4d43f90b6a4f9..2e69e78f94075b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java @@ -24,16 +24,22 @@ @DoNotStrip public class ReactBridge extends Countable { - /* package */ static final String REACT_NATIVE_LIB = "reactnativejni"; + private static final String REACT_NATIVE_LIB = "reactnativejni"; + private static final String XREACT_NATIVE_LIB = "reactnativejnifb"; static { - SoLoader.loadLibrary(REACT_NATIVE_LIB); + staticInit(); } private final ReactCallback mCallback; private final JavaScriptExecutor mJSExecutor; private final MessageQueueThread mNativeModulesQueueThread; + public static void staticInit() { + SoLoader.loadLibrary(REACT_NATIVE_LIB); + SoLoader.loadLibrary(XREACT_NATIVE_LIB); + } + /** * @param jsExecutor the JS executor to use to run JS * @param callback the callback class used to invoke native modules diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java index 4cd1b75814e80b..2f81d301b60bf9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java @@ -21,9 +21,8 @@ */ @DoNotStrip public class ReadableNativeArray extends NativeArray implements ReadableArray { - static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + ReactBridge.staticInit(); } protected ReadableNativeArray(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java index f3782ca0896f75..ea289ac668f2e0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java @@ -22,9 +22,8 @@ */ @DoNotStrip public class ReadableNativeMap extends NativeMap implements ReadableMap { - static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + ReactBridge.staticInit(); } protected ReadableNativeMap(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java index 26fe2dd11f6228..e6c343264a8d2c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java @@ -21,9 +21,8 @@ */ @DoNotStrip public class WritableNativeArray extends ReadableNativeArray implements WritableArray { - static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + ReactBridge.staticInit(); } public WritableNativeArray() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java index d30827ade5bf12..6b6c639d2e032b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java @@ -22,7 +22,7 @@ @DoNotStrip public class WritableNativeMap extends ReadableNativeMap implements WritableMap { static { - SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + ReactBridge.staticInit(); } @Override diff --git a/ReactAndroid/src/main/jni/react/jni/Android.mk b/ReactAndroid/src/main/jni/react/jni/Android.mk index 742e2161d1a0b6..06266de376fc6e 100644 --- a/ReactAndroid/src/main/jni/react/jni/Android.mk +++ b/ReactAndroid/src/main/jni/react/jni/Android.mk @@ -11,15 +11,8 @@ LOCAL_SRC_FILES := \ JSLoader.cpp \ JSLogging.cpp \ JniJSModulesUnbundle.cpp \ - NativeArray.cpp \ - NativeCommon.cpp \ - NativeMap.cpp \ OnLoad.cpp \ ProxyExecutor.cpp \ - ReadableNativeArray.cpp \ - ReadableNativeMap.cpp \ - WritableNativeArray.cpp \ - WritableNativeMap.cpp \ LOCAL_C_INCLUDES := $(LOCAL_PATH) LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/../.. $(LOCAL_PATH)/.. @@ -30,7 +23,7 @@ LOCAL_CFLAGS += $(CXX11_FLAGS) LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) LOCAL_LDLIBS += -landroid -LOCAL_SHARED_LIBRARIES := libfolly_json libfb libjsc libglog_init +LOCAL_SHARED_LIBRARIES := libfolly_json libfb libjsc libglog_init libreactnativejnifb LOCAL_STATIC_LIBRARIES := libreactnative include $(BUILD_SHARED_LIBRARY) @@ -41,3 +34,4 @@ $(call import-module,folly) $(call import-module,fbgloginit) $(call import-module,fb) $(call import-module,jsc) +$(call import-module,xreact/jni) diff --git a/ReactAndroid/src/main/jni/react/jni/BUCK b/ReactAndroid/src/main/jni/react/jni/BUCK index 9bd754fb70e92f..e210b9dd9575eb 100644 --- a/ReactAndroid/src/main/jni/react/jni/BUCK +++ b/ReactAndroid/src/main/jni/react/jni/BUCK @@ -18,6 +18,7 @@ def jni_library(**kwargs): ], deps = DEPS + JSC_DEPS + [ react_native_target('jni/react:react'), + react_native_target('jni/xreact/jni:jni'), ], **kwargs ) @@ -33,15 +34,8 @@ jni_library( 'JSLoader.cpp', 'JSLogging.cpp', 'JniJSModulesUnbundle.cpp', - 'NativeArray.cpp', - 'NativeCommon.cpp', - 'NativeMap.cpp', 'OnLoad.cpp', 'ProxyExecutor.cpp', - 'ReadableNativeArray.cpp', - 'ReadableNativeMap.cpp', - 'WritableNativeArray.cpp', - 'WritableNativeMap.cpp', ], headers = [ 'JSLoader.h', @@ -56,13 +50,6 @@ jni_library( 'WebWorkers.h', ], exported_headers = [ - 'NativeCommon.h', - 'NativeArray.h', - 'NativeMap.h', - 'ReadableNativeArray.h', - 'ReadableNativeMap.h', - 'WritableNativeArray.h', - 'WritableNativeMap.h', ], preprocessor_flags = [ '-DLOG_TAG="ReactNativeJNI"', diff --git a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp index 8d60719c4e9975..63fee95574aaca 100644 --- a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp +++ b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp @@ -22,8 +22,6 @@ #include "JExecutorTokenFactory.h" #include "JNativeRunnable.h" #include "JSLoader.h" -#include "NativeCommon.h" -#include "ReadableNativeArray.h" #include "ProxyExecutor.h" #include "OnLoad.h" #include "JMessageQueueThread.h" @@ -31,7 +29,9 @@ #include "JSLogging.h" #include "JSCPerfLogging.h" #include "WebWorkers.h" -#include "WritableNativeMap.h" + +#include +#include #include #ifdef WITH_FBSYSTRACE @@ -456,17 +456,9 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { PerfLogging::installNativeHooks = addNativePerfLoggingHooks; JSLogging::nativeHook = nativeLoggingHook; - NativeArray::registerNatives(); - ReadableNativeArray::registerNatives(); - WritableNativeArray::registerNatives(); JNativeRunnable::registerNatives(); registerJSLoaderNatives(); - NativeMap::registerNatives(); - ReadableNativeMap::registerNatives(); - WritableNativeMap::registerNatives(); - ReadableNativeMapKeySetIterator::registerNatives(); - registerNatives("com/facebook/react/bridge/JSCJavaScriptExecutor", { makeNativeMethod("initialize", executors::createJSCExecutor), }); diff --git a/ReactAndroid/src/main/jni/xreact/jni/Android.mk b/ReactAndroid/src/main/jni/xreact/jni/Android.mk index c0078c36a8a157..4eb04b448854f4 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/Android.mk +++ b/ReactAndroid/src/main/jni/xreact/jni/Android.mk @@ -9,14 +9,21 @@ LOCAL_SRC_FILES := \ CxxModuleWrapper.cpp \ JExecutorToken.cpp \ JMessageQueueThread.cpp \ - JniJSModulesUnbundle.cpp \ JSCPerfLogging.cpp \ JSLoader.cpp \ JSLogging.cpp \ + JniJSModulesUnbundle.cpp \ MethodInvoker.cpp \ ModuleRegistryHolder.cpp \ + NativeArray.cpp \ + NativeCommon.cpp \ + NativeMap.cpp \ OnLoad.cpp \ ProxyExecutor.cpp \ + ReadableNativeArray.cpp \ + ReadableNativeMap.cpp \ + WritableNativeArray.cpp \ + WritableNativeMap.cpp \ LOCAL_C_INCLUDES := $(LOCAL_PATH) LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/../.. @@ -27,7 +34,7 @@ LOCAL_CFLAGS += $(CXX11_FLAGS) LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) LOCAL_LDLIBS += -landroid -LOCAL_SHARED_LIBRARIES := libfolly_json libfbjni libjsc libglog_init libreactnativejni +LOCAL_SHARED_LIBRARIES := libfolly_json libfbjni libjsc libglog_init LOCAL_STATIC_LIBRARIES := libreactnativefb include $(BUILD_SHARED_LIBRARY) @@ -37,4 +44,3 @@ $(call import-module,jsc) $(call import-module,folly) $(call import-module,fbgloginit) $(call import-module,jsc) -$(call import-module,react/jni) diff --git a/ReactAndroid/src/main/jni/xreact/jni/BUCK b/ReactAndroid/src/main/jni/xreact/jni/BUCK index ea88102d01c483..cb47fa436a1b1c 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/BUCK +++ b/ReactAndroid/src/main/jni/xreact/jni/BUCK @@ -5,19 +5,25 @@ SUPPORTED_PLATFORMS = '^android-(armv7|x86)$' EXPORTED_HEADERS = [ 'CxxModuleWrapper.h', + 'NativeArray.h', + 'NativeCommon.h', + 'NativeMap.h', + 'ReadableNativeArray.h', + 'ReadableNativeMap.h', + 'WritableNativeArray.h', + 'WritableNativeMap.h', ] cxx_library( name='jni', soname = 'libreactnativejnifb.so', - header_namespace = 'react/jni', + header_namespace = 'xreact/jni', supported_platforms_regex = SUPPORTED_PLATFORMS, deps = JSC_DEPS + [ '//native/fb:fb', '//native/third-party/android-ndk:android', '//xplat/folly:molly', '//xplat/fbsystrace:fbsystrace', - react_native_target('jni/react/jni:jni'), react_native_xplat_target('cxxreact:bridge'), react_native_xplat_target('cxxreact:module'), ], diff --git a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp index c4c6faa74b2899..64fa80f98100f2 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp @@ -13,8 +13,6 @@ #include #include -#include - #include #include #include @@ -23,6 +21,7 @@ #include "JavaScriptExecutorHolder.h" #include "JniJSModulesUnbundle.h" #include "ModuleRegistryHolder.h" +#include "NativeArray.h" #include "JNativeRunnable.h" using namespace facebook::jni; diff --git a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp index d1532fd10dd2b9..9267d876e963c5 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp @@ -2,16 +2,11 @@ #include "CxxModuleWrapper.h" -#include - #include #include #include #include -#include -#include - #include #include @@ -20,6 +15,12 @@ #include #include +#include +#include + +#include "ReadableNativeArray.h" + + using namespace facebook::jni; using namespace facebook::xplat::module; using namespace facebook::react; diff --git a/ReactAndroid/src/main/jni/xreact/jni/JCallback.h b/ReactAndroid/src/main/jni/xreact/jni/JCallback.h index 539273104dced2..1d1393eb95bca9 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/JCallback.h +++ b/ReactAndroid/src/main/jni/xreact/jni/JCallback.h @@ -7,7 +7,7 @@ #include #include -#include +#include "NativeArray.h" namespace facebook { namespace react { diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp index 1b7b5e6887b6fa..1dd837db87bf61 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp @@ -2,9 +2,10 @@ #include "JSCPerfLogging.h" +#include + #include #include -#include using namespace facebook::jni; diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp index 7690a5656ebd8d..a27d3f5d35e732 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp @@ -4,9 +4,10 @@ #include #include -#include #include +#include + namespace facebook { namespace react { diff --git a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp index 218f6c9d0ac2d3..e7d3d2c2d0718b 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp @@ -2,7 +2,6 @@ #include "MethodInvoker.h" -#include #ifdef WITH_FBSYSTRACE #include #endif @@ -10,6 +9,7 @@ #include "ModuleRegistryHolder.h" #include "JCallback.h" #include "JExecutorToken.h" +#include "ReadableNativeArray.h" namespace facebook { namespace react { diff --git a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp index 29349f741af1dc..eebed41df0c624 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp @@ -10,11 +10,10 @@ #include #include #include -#include - -#include "MethodInvoker.h" #include "CatalystInstanceImpl.h" +#include "MethodInvoker.h" +#include "ReadableNativeArray.h" using facebook::xplat::module::CxxModule; diff --git a/ReactAndroid/src/main/jni/react/jni/NativeArray.cpp b/ReactAndroid/src/main/jni/xreact/jni/NativeArray.cpp similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/NativeArray.cpp rename to ReactAndroid/src/main/jni/xreact/jni/NativeArray.cpp diff --git a/ReactAndroid/src/main/jni/react/jni/NativeArray.h b/ReactAndroid/src/main/jni/xreact/jni/NativeArray.h similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/NativeArray.h rename to ReactAndroid/src/main/jni/xreact/jni/NativeArray.h diff --git a/ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp b/ReactAndroid/src/main/jni/xreact/jni/NativeCommon.cpp similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp rename to ReactAndroid/src/main/jni/xreact/jni/NativeCommon.cpp diff --git a/ReactAndroid/src/main/jni/react/jni/NativeCommon.h b/ReactAndroid/src/main/jni/xreact/jni/NativeCommon.h similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/NativeCommon.h rename to ReactAndroid/src/main/jni/xreact/jni/NativeCommon.h diff --git a/ReactAndroid/src/main/jni/react/jni/NativeMap.cpp b/ReactAndroid/src/main/jni/xreact/jni/NativeMap.cpp similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/NativeMap.cpp rename to ReactAndroid/src/main/jni/xreact/jni/NativeMap.cpp diff --git a/ReactAndroid/src/main/jni/react/jni/NativeMap.h b/ReactAndroid/src/main/jni/xreact/jni/NativeMap.h similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/NativeMap.h rename to ReactAndroid/src/main/jni/xreact/jni/NativeMap.h diff --git a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp index 3242c59ca2bd63..952a6b46c4c4ff 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include "CatalystInstanceImpl.h" #include "JavaScriptExecutorHolder.h" #include "JSCPerfLogging.h" @@ -17,6 +16,9 @@ #include "WebWorkers.h" #include "JCallback.h" +#include "WritableNativeMap.h" +#include "WritableNativeArray.h" + #include using namespace facebook::jni; @@ -180,6 +182,14 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { CxxModuleWrapper::registerNatives(); JCallbackImpl::registerNatives(); registerJSLoaderNatives(); + + NativeArray::registerNatives(); + ReadableNativeArray::registerNatives(); + WritableNativeArray::registerNatives(); + NativeMap::registerNatives(); + ReadableNativeMap::registerNatives(); + WritableNativeMap::registerNatives(); + ReadableNativeMapKeySetIterator::registerNatives(); }); } diff --git a/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp b/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.cpp similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp rename to ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.cpp diff --git a/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.h b/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.h similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.h rename to ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.h diff --git a/ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.cpp b/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.cpp similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.cpp rename to ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.cpp diff --git a/ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.h b/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.h similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.h rename to ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.h diff --git a/ReactAndroid/src/main/jni/react/jni/WritableNativeArray.cpp b/ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.cpp similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/WritableNativeArray.cpp rename to ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.cpp diff --git a/ReactAndroid/src/main/jni/react/jni/WritableNativeArray.h b/ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.h similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/WritableNativeArray.h rename to ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.h diff --git a/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.cpp b/ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.cpp similarity index 100% rename from ReactAndroid/src/main/jni/react/jni/WritableNativeMap.cpp rename to ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.cpp diff --git a/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h b/ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.h similarity index 96% rename from ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h rename to ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.h index cf9cd95a000d88..1fc942a025e5e1 100644 --- a/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h +++ b/ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.h @@ -20,6 +20,7 @@ struct WritableNativeMap : jni::HybridClass initHybrid(jni::alias_ref); + __attribute__((visibility("default"))) folly::dynamic consume(); void putNull(std::string key); From d4e7c8a0550891208284bd1d900bd9721d899f8f Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Tue, 7 Jun 2016 16:22:56 -0700 Subject: [PATCH 277/843] Properly validate layout animation config Summary: The type key for a layout animation config must be supplied otherwise the app crashes (on Android). I added a isRequired check in JS as well as an explicit exception in java otherwise it crashed at a very hard to debug place. The crash happens when passing null to `Animation.setInterpolator` so this makes sure it never happens. **Test plan (required)** Tested that the error is caught properly in JS when passing an invalid animation config like ``` LayoutAnimation.configureNext({ duration: 250, update: { type: undefined }, // or LayoutAnimation.Types.easeInEastOut in my case haha :) }); ``` Also tested the java exception. Closes https://github.com/facebook/react-native/pull/7958 Differential Revision: D3401760 Pulled By: nicklockwood fbshipit-source-id: 83c019d863c2b2294405b60e87297c562add0f49 --- Libraries/LayoutAnimation/LayoutAnimation.js | 2 +- .../uimanager/layoutanimation/AbstractLayoutAnimation.java | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Libraries/LayoutAnimation/LayoutAnimation.js b/Libraries/LayoutAnimation/LayoutAnimation.js index 8a8f221ce02676..6f28182873d0fc 100644 --- a/Libraries/LayoutAnimation/LayoutAnimation.js +++ b/Libraries/LayoutAnimation/LayoutAnimation.js @@ -40,7 +40,7 @@ var animChecker = createStrictShapeTypeChecker({ initialVelocity: PropTypes.number, type: PropTypes.oneOf( Object.keys(Types) - ), + ).isRequired, property: PropTypes.oneOf( // Only applies to create/delete Object.keys(Properties) ), diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimation.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimation.java index c29d7ae0bef963..8d234269a9e432 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimation.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimation.java @@ -61,8 +61,10 @@ public void initializeFromConfig(ReadableMap data, int globalDuration) { AnimatedPropertyType.fromString(data.getString("property")) : null; mDurationMs = data.hasKey("duration") ? data.getInt("duration") : globalDuration; mDelayMs = data.hasKey("delay") ? data.getInt("delay") : 0; - mInterpolator = data.hasKey("type") ? - getInterpolator(InterpolatorType.fromString(data.getString("type"))) : null; + if (!data.hasKey("type")) { + throw new IllegalArgumentException("Missing interpolation type."); + } + mInterpolator = getInterpolator(InterpolatorType.fromString(data.getString("type"))); if (!isValid()) { throw new IllegalViewOperationException("Invalid layout animation : " + data); From 7357ccc37067133859c663f0b907c58801ac25c9 Mon Sep 17 00:00:00 2001 From: Kyle Corbitt Date: Tue, 7 Jun 2016 16:37:48 -0700 Subject: [PATCH 278/843] Allow CameraRoll to export videos Summary: This PR adds the ability to export videos to the CameraRoll on both Android and iOS (previously only photos were possible, at least on iOS). The API has changed as follows: ``` // old saveImageWithTag(tag: string): Promise // new saveToCameraRoll(tag: string, type?: 'photo' | 'video'): Promise ``` if no `type` parameter is passed, `video` is inferred if the tag ends with ".mov" or ".mp4", otherwise `photo` is assumed. I've left in the `saveImageWithTag` method for now with a deprecation warning. **Test plan (required)** I created the following very simple app to test exporting photos and videos to the CameraRoll, and ran it on both iOS and Android. The functionality works as intended on both platforms. ```js // index.js /** * Sample React Native App * https://github.com/facebook/react-native * flow */ import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, CameraRoll, } from 'react-native'; import FS fro Closes https://github.com/facebook/react-native/pull/7988 Differential Revision: D3401251 Pulled By: nicklockwood fbshipit-source-id: af3fc24e6fa5b84ac377e9173f3709c6f9795f20 --- Libraries/CameraRoll/CameraRoll.js | 46 ++++++++++++------- Libraries/CameraRoll/RCTCameraRollManager.m | 36 ++++++++++----- .../modules/camera/CameraRollManager.java | 27 ++++++----- 3 files changed, 70 insertions(+), 39 deletions(-) diff --git a/Libraries/CameraRoll/CameraRoll.js b/Libraries/CameraRoll/CameraRoll.js index a4f0d7fd67a52b..4b7f23f3e8bdab 100644 --- a/Libraries/CameraRoll/CameraRoll.js +++ b/Libraries/CameraRoll/CameraRoll.js @@ -115,33 +115,45 @@ class CameraRoll { static GroupTypesOptions: Array; static AssetTypeOptions: Array; + + static saveImageWithTag(tag: string):Promise<*> { + console.warn('CameraRoll.saveImageWithTag is deprecated. Use CameraRoll.saveToCameraRoll instead'); + return this.saveToCameraRoll(tag, 'photo'); + } + /** - * Saves the image to the camera roll / gallery. + * Saves the photo or video to the camera roll / gallery. * - * On Android, the tag is a local URI, such as `"file:///sdcard/img.png"`. + * On Android, the tag must be a local image or video URI, such as `"file:///sdcard/img.png"`. * - * On iOS, the tag can be one of the following: + * On iOS, the tag can be any image URI (including local, remote asset-library and base64 data URIs) + * or a local video file URI (remote or data URIs are not supported for saving video at this time). * - * - local URI - * - assets-library tag - * - a tag not matching any of the above, which means the image data will - * be stored in memory (and consume memory as long as the process is alive) + * If the tag has a file extension of .mov or .mp4, it will be inferred as a video. Otherwise + * it will be treated as a photo. To override the automatic choice, you can pass an optional + * `type` parameter that must be one of 'photo' or 'video'. * - * Returns a Promise which when resolved will be passed the new URI. + * Returns a Promise which will resolve with the new URI. */ - static saveImageWithTag(tag) { + static saveToCameraRoll(tag: string, type?: 'photo' | 'video'): Promise<*> { invariant( typeof tag === 'string', - 'CameraRoll.saveImageWithTag tag must be a valid string.' + 'CameraRoll.saveToCameraRoll must be a valid string.' ); - if (arguments.length > 1) { - console.warn('CameraRoll.saveImageWithTag(tag, success, error) is deprecated. Use the returned Promise instead'); - const successCallback = arguments[1]; - const errorCallback = arguments[2] || ( () => {} ); - RCTCameraRollManager.saveImageWithTag(tag).then(successCallback, errorCallback); - return; + + invariant( + type === 'photo' || type === 'video' || type === undefined, + `The second argument to saveToCameraRoll must be 'photo' or 'video'. You passed ${type}` + ); + + let mediaType = 'photo'; + if (type) { + mediaType = type; + } else if (['mov', 'mp4'].indexOf(tag.split('.').slice(-1)[0]) >= 0) { + mediaType = 'video'; } - return RCTCameraRollManager.saveImageWithTag(tag); + + return RCTCameraRollManager.saveToCameraRoll(tag, mediaType); } /** diff --git a/Libraries/CameraRoll/RCTCameraRollManager.m b/Libraries/CameraRoll/RCTCameraRollManager.m index 7d9e82aa254fdd..8512cf64e743d6 100644 --- a/Libraries/CameraRoll/RCTCameraRollManager.m +++ b/Libraries/CameraRoll/RCTCameraRollManager.m @@ -82,28 +82,42 @@ @implementation RCTCameraRollManager NSString *const RCTErrorUnableToLoad = @"E_UNABLE_TO_LOAD"; NSString *const RCTErrorUnableToSave = @"E_UNABLE_TO_SAVE"; -RCT_EXPORT_METHOD(saveImageWithTag:(NSURLRequest *)imageRequest +RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request + type:(NSString *)type resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - [_bridge.imageLoader loadImageWithURLRequest:imageRequest - callback:^(NSError *loadError, UIImage *loadedImage) { - if (loadError) { - reject(RCTErrorUnableToLoad, nil, loadError); - return; - } - // It's unclear if writeImageToSavedPhotosAlbum is thread-safe + if ([type isEqualToString:@"video"]) { + // It's unclear if writeVideoAtPathToSavedPhotosAlbum is thread-safe dispatch_async(dispatch_get_main_queue(), ^{ - [_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) { + [_bridge.assetsLibrary writeVideoAtPathToSavedPhotosAlbum:request.URL completionBlock:^(NSURL *assetURL, NSError *saveError) { if (saveError) { - RCTLogWarn(@"Error saving cropped image: %@", saveError); reject(RCTErrorUnableToSave, nil, saveError); } else { resolve(assetURL.absoluteString); } }]; }); - }]; + } else { + [_bridge.imageLoader loadImageWithURLRequest:request + callback:^(NSError *loadError, UIImage *loadedImage) { + if (loadError) { + reject(RCTErrorUnableToLoad, nil, loadError); + return; + } + // It's unclear if writeImageToSavedPhotosAlbum is thread-safe + dispatch_async(dispatch_get_main_queue(), ^{ + [_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) { + if (saveError) { + RCTLogWarn(@"Error saving cropped image: %@", saveError); + reject(RCTErrorUnableToSave, nil, saveError); + } else { + resolve(assetURL.absoluteString); + } + }]; + }); + }]; + } } static void RCTResolvePromise(RCTPromiseResolveBlock resolve, diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java b/ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java index e5302355c7e4a7..c103f445ae4879 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java @@ -117,22 +117,26 @@ public Map getConstants() { * @param promise to be resolved or rejected */ @ReactMethod - public void saveImageWithTag(String uri, Promise promise) { - new SaveImageTag(getReactApplicationContext(), Uri.parse(uri), promise) + public void saveToCameraRoll(String uri, String type, Promise promise) { + MediaType parsedType = type.equals("video") ? MediaType.VIDEO : MediaType.PHOTO; + new SaveToCameraRoll(getReactApplicationContext(), Uri.parse(uri), parsedType, promise) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - private static class SaveImageTag extends GuardedAsyncTask { + private enum MediaType { PHOTO, VIDEO }; + private static class SaveToCameraRoll extends GuardedAsyncTask { private final Context mContext; private final Uri mUri; private final Promise mPromise; + private final MediaType mType; - public SaveImageTag(ReactContext context, Uri uri, Promise promise) { + public SaveToCameraRoll(ReactContext context, Uri uri, MediaType type, Promise promise) { super(context); mContext = context; mUri = uri; mPromise = promise; + mType = type; } @Override @@ -140,14 +144,15 @@ protected void doInBackgroundGuarded(Void... params) { File source = new File(mUri.getPath()); FileChannel input = null, output = null; try { - File pictures = - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); - pictures.mkdirs(); - if (!pictures.isDirectory()) { - mPromise.reject(ERROR_UNABLE_TO_LOAD, "External storage pictures directory not available"); + File exportDir = (mType == MediaType.PHOTO) + ? Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + : Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES); + exportDir.mkdirs(); + if (!exportDir.isDirectory()) { + mPromise.reject(ERROR_UNABLE_TO_LOAD, "External media storage directory not available"); return; } - File dest = new File(pictures, source.getName()); + File dest = new File(exportDir, source.getName()); int n = 0; String fullSourceName = source.getName(); String sourceName, sourceExt; @@ -159,7 +164,7 @@ protected void doInBackgroundGuarded(Void... params) { sourceExt = ""; } while (!dest.createNewFile()) { - dest = new File(pictures, sourceName + "_" + (n++) + sourceExt); + dest = new File(exportDir, sourceName + "_" + (n++) + sourceExt); } input = new FileInputStream(source).getChannel(); output = new FileOutputStream(dest).getChannel(); From 5ecdb252c3cc0ca15a70efa574f9a3ba974a3307 Mon Sep 17 00:00:00 2001 From: Brandon Withrow Date: Tue, 7 Jun 2016 20:40:54 -0700 Subject: [PATCH 279/843] Add support for native animations on iOS Summary: Currently on iOS animations are being performed on the JS thread. This ports animations over to the native thread and performs them natively. A lot of this work has already been done on Android, but this PR enables a few animation nodes that Android doesn't yet support such as Transform, Multiplication, and Addition nodes. Also there is a demo of the native animations added to the UIExplorer app. Closes https://github.com/facebook/react-native/pull/7884 Differential Revision: D3401811 Pulled By: nicklockwood fbshipit-source-id: c8d750b75e4410923e17eaeb6dcaf079a09942e2 --- .../UIExplorer/NativeAnimationsExample.js | 308 ++++++++++++++++ .../UIExplorer.xcodeproj/project.pbxproj | 30 ++ .../xcschemes/UIExplorer.xcscheme | 2 +- Examples/UIExplorer/UIExplorerList.ios.js | 4 + .../Animated/src/AnimatedImplementation.js | 34 +- .../Animated/src/NativeAnimatedHelper.js | 19 +- .../Nodes/RCTAdditionAnimatedNode.h | 14 + .../Nodes/RCTAdditionAnimatedNode.m | 27 ++ .../NativeAnimation/Nodes/RCTAnimatedNode.h | 54 +++ .../NativeAnimation/Nodes/RCTAnimatedNode.m | 135 +++++++ .../Nodes/RCTAnimationDriverNode.h | 40 +++ .../Nodes/RCTAnimationDriverNode.m | 138 +++++++ .../Nodes/RCTInterpolationAnimatedNode.h | 14 + .../Nodes/RCTInterpolationAnimatedNode.m | 97 +++++ .../Nodes/RCTMultiplicationAnimatedNode.h | 14 + .../Nodes/RCTMultiplicationAnimatedNode.m | 29 ++ .../Nodes/RCTPropsAnimatedNode.h | 24 ++ .../Nodes/RCTPropsAnimatedNode.m | 61 ++++ .../Nodes/RCTStyleAnimatedNode.h | 16 + .../Nodes/RCTStyleAnimatedNode.m | 61 ++++ .../Nodes/RCTTransformAnimatedNode.h | 16 + .../Nodes/RCTTransformAnimatedNode.m | 52 +++ .../Nodes/RCTValueAnimatedNode.h | 17 + .../Nodes/RCTValueAnimatedNode.m | 14 + .../RCTAnimation.xcodeproj/project.pbxproj | 337 ++++++++++++++++++ Libraries/NativeAnimation/RCTAnimationUtils.h | 22 ++ Libraries/NativeAnimation/RCTAnimationUtils.m | 32 ++ .../NativeAnimation/RCTNativeAnimatedModule.h | 13 + .../NativeAnimation/RCTNativeAnimatedModule.m | 247 +++++++++++++ .../NativeAnimation/RCTViewPropertyMapper.h | 22 ++ .../NativeAnimation/RCTViewPropertyMapper.m | 94 +++++ 31 files changed, 1983 insertions(+), 4 deletions(-) create mode 100644 Examples/UIExplorer/NativeAnimationsExample.js create mode 100644 Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.h create mode 100644 Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.m create mode 100644 Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h create mode 100644 Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m create mode 100644 Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.h create mode 100644 Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.m create mode 100644 Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.h create mode 100644 Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m create mode 100644 Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.h create mode 100644 Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.m create mode 100644 Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.h create mode 100644 Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m create mode 100644 Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.h create mode 100644 Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.m create mode 100644 Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.h create mode 100644 Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.m create mode 100644 Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h create mode 100644 Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.m create mode 100644 Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj create mode 100644 Libraries/NativeAnimation/RCTAnimationUtils.h create mode 100644 Libraries/NativeAnimation/RCTAnimationUtils.m create mode 100644 Libraries/NativeAnimation/RCTNativeAnimatedModule.h create mode 100644 Libraries/NativeAnimation/RCTNativeAnimatedModule.m create mode 100644 Libraries/NativeAnimation/RCTViewPropertyMapper.h create mode 100644 Libraries/NativeAnimation/RCTViewPropertyMapper.m diff --git a/Examples/UIExplorer/NativeAnimationsExample.js b/Examples/UIExplorer/NativeAnimationsExample.js new file mode 100644 index 00000000000000..67757f56eaf0d6 --- /dev/null +++ b/Examples/UIExplorer/NativeAnimationsExample.js @@ -0,0 +1,308 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + View, + Text, + Animated, + StyleSheet, + TouchableWithoutFeedback, +} = ReactNative; +var UIExplorerButton = require('./UIExplorerButton'); + +var Tester = React.createClass({ + current: 0, + getInitialState() { + return { + native: new Animated.Value(0), + js: new Animated.Value(0), + }; + }, + + onPress() { + this.current = this.current ? 0 : 1; + const config = { + ...this.props.config, + toValue: this.current, + }; + try { + Animated[this.props.type](this.state.native, { ...config, useNativeDriver: true }).start(); + } catch (e) { + // uncomment this if you want to get the redbox errors! + throw e; + } + Animated[this.props.type](this.state.js, { ...config, useNativeDriver: false }).start(); + }, + + render() { + return ( + + + + Native: + + + {this.props.children(this.state.native)} + + + JavaScript: + + + {this.props.children(this.state.js)} + + + + ); + }, +}); + +const styles = StyleSheet.create({ + row: { + padding: 10, + zIndex: 1, + }, + block: { + width: 50, + height: 50, + backgroundColor: 'blue', + }, +}); + +exports.framework = 'React'; +exports.title = 'Native Animated Example'; +exports.description = 'Test out Native Animations'; + +exports.examples = [ +{ + title: 'Multistage With Multiply and rotation', + description: 'description', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'Multistage With Multiply', + description: 'description', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'Scale interpolation', + description: 'description', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'Opacity without interpolation', + description: 'description', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'Rotate interpolation', + description: 'description', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'translateX => Animated.spring', + description: 'description', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, +]; diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index ba1a02b73a5a4d..f73a4214d02fc8 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ 13BCE84F1C9C209600DD7AAD /* RCTComponentPropsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13BCE84E1C9C209600DD7AAD /* RCTComponentPropsTests.m */; }; 13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */; }; 13DF61B61B67A45000EDB188 /* RCTMethodArgumentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */; }; + 13E501F11D07A84A005F35D8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13E501A31D07A502005F35D8 /* libRCTAnimation.a */; }; 143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */; }; 144D21241B2204C5006DB32B /* RCTImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 144D21231B2204C5006DB32B /* RCTImageUtilTests.m */; }; 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; }; @@ -121,6 +122,13 @@ remoteGlobalIDString = 3C86DF461ADF2C930047B81A; remoteInfo = RCTWebSocket; }; + 13E501A21D07A502005F35D8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 13E5019C1D07A502005F35D8 /* RCTAnimation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTAnimation; + }; 143BC59B1B21E3E100462512 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; @@ -210,6 +218,7 @@ 13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../../Libraries/Settings/RCTSettings.xcodeproj; sourceTree = ""; }; 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSONTests.m; sourceTree = ""; }; 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMethodArgumentTests.m; sourceTree = ""; }; + 13E5019C1D07A502005F35D8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = ../../Libraries/NativeAnimation/RCTAnimation.xcodeproj; sourceTree = ""; }; 143BC57E1B21E18100462512 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 143BC5811B21E18100462512 /* testLayoutExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testLayoutExampleSnapshot_1@2x.png"; sourceTree = ""; }; 143BC5821B21E18100462512 /* testSliderExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testSliderExampleSnapshot_1@2x.png"; sourceTree = ""; }; @@ -285,6 +294,7 @@ 14AADF051AC3DBB1002390C9 /* libReact.a in Frameworks */, 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */, 134454601AAFCABD003F0779 /* libRCTAdSupport.a in Frameworks */, + 13E501F11D07A84A005F35D8 /* libRCTAnimation.a in Frameworks */, 138DEE241B9EDFB6007F4EA5 /* libRCTCameraRoll.a in Frameworks */, 134A8A2A1AACED7A00945AAE /* libRCTGeolocation.a in Frameworks */, 13417FE91AA91432003F314A /* libRCTImage.a in Frameworks */, @@ -315,6 +325,7 @@ 14AADEFF1AC3DB95002390C9 /* React.xcodeproj */, 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */, 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */, + 13E5019C1D07A502005F35D8 /* RCTAnimation.xcodeproj */, 138DEE021B9EDDDB007F4EA5 /* RCTCameraRoll.xcodeproj */, 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */, 13417FE31AA91428003F314A /* RCTImage.xcodeproj */, @@ -413,6 +424,14 @@ name = UIExplorer; sourceTree = ""; }; + 13E5019D1D07A502005F35D8 /* Products */ = { + isa = PBXGroup; + children = ( + 13E501A31D07A502005F35D8 /* libRCTAnimation.a */, + ); + name = Products; + sourceTree = ""; + }; 143BC57C1B21E18100462512 /* UIExplorerUnitTests */ = { isa = PBXGroup; children = ( @@ -693,6 +712,10 @@ ProductGroup = 134454561AAFCAAE003F0779 /* Products */; ProjectRef = 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */; }, + { + ProductGroup = 13E5019D1D07A502005F35D8 /* Products */; + ProjectRef = 13E5019C1D07A502005F35D8 /* RCTAnimation.xcodeproj */; + }, { ProductGroup = 138DEE031B9EDDDB007F4EA5 /* Products */; ProjectRef = 138DEE021B9EDDDB007F4EA5 /* RCTCameraRoll.xcodeproj */; @@ -801,6 +824,13 @@ remoteRef = 139FDED81B0651EA00C62182 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 13E501A31D07A502005F35D8 /* libRCTAnimation.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTAnimation.a; + remoteRef = 13E501A21D07A502005F35D8 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme index 833aaaa4a65686..547d76ac50989f 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme @@ -1,6 +1,6 @@ = [ key: 'ModalExample', module: require('./ModalExample'), }, + { + key: 'NativeAnimationsExample', + module: require('./NativeAnimationsExample'), + }, { key: 'NavigatorExample', module: require('./Navigator/NavigatorExample'), diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js index a9188050513529..9b767c649ce7b1 100644 --- a/Libraries/Animated/src/AnimatedImplementation.js +++ b/Libraries/Animated/src/AnimatedImplementation.js @@ -243,7 +243,7 @@ class TimingAnimation extends Animation { this._duration = config.duration !== undefined ? config.duration : 500; this._delay = config.delay !== undefined ? config.delay : 0; this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true; - this._useNativeDriver = !!config.useNativeDriver; + this._useNativeDriver = config.useNativeDriver !== undefined ? config.useNativeDriver : false; } _getNativeAnimationConfig(): any { @@ -256,6 +256,7 @@ class TimingAnimation extends Animation { type: 'frames', frames, toValue: this._toValue, + delay: this._delay }; } @@ -1086,6 +1087,18 @@ class AnimatedTransform extends AnimatedWithChildren { this._transforms = transforms; } + __makeNative() { + super.__makeNative(); + this._transforms.forEach(transform => { + for (var key in transform) { + var value = transform[key]; + if (value instanceof Animated) { + value.__makeNative(); + } + } + }); + } + __getValue(): Array { return this._transforms.map(transform => { var result = {}; @@ -1138,6 +1151,25 @@ class AnimatedTransform extends AnimatedWithChildren { } }); } + + __getNativeConfig(): any { + var transConfig = {}; + + this._transforms.forEach(transform => { + for (var key in transform) { + var value = transform[key]; + if (value instanceof Animated) { + transConfig[key] = value.__getNativeTag(); + } + } + }); + + NativeAnimatedHelper.validateTransform(transConfig); + return { + type: 'transform', + transform: transConfig, + }; + } } class AnimatedStyle extends AnimatedWithChildren { diff --git a/Libraries/Animated/src/NativeAnimatedHelper.js b/Libraries/Animated/src/NativeAnimatedHelper.js index 2e5cc68b1737b9..befbad13148217 100644 --- a/Libraries/Animated/src/NativeAnimatedHelper.js +++ b/Libraries/Animated/src/NativeAnimatedHelper.js @@ -74,16 +74,22 @@ var API = { var PROPS_WHITELIST = { style: { opacity: true, - + transform: true, /* legacy android transform properties */ scaleX: true, scaleY: true, - rotation: true, translateX: true, translateY: true, }, }; +var TRANSFORM_WHITELIST = { + translateX: true, + translateY: true, + scale: true, + rotate: true, +}; + function validateProps(params: Object): void { for (var key in params) { if (!PROPS_WHITELIST.hasOwnProperty(key)) { @@ -92,6 +98,14 @@ function validateProps(params: Object): void { } } +function validateTransform(config: Object): void { + for (var key in config) { + if (!TRANSFORM_WHITELIST.hasOwnProperty(key)) { + throw new Error(`Property '${key}' is not supported by native animated module`); + } + } +} + function validateStyles(styles: Object): void { var STYLES_WHITELIST = PROPS_WHITELIST.style || {}; for (var key in styles) { @@ -129,6 +143,7 @@ module.exports = { API, validateProps, validateStyles, + validateTransform, validateInterpolation, generateNewNodeTag, generateNewAnimationId, diff --git a/Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.h new file mode 100644 index 00000000000000..83641afead8442 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTValueAnimatedNode.h" + +@interface RCTAdditionAnimatedNode : RCTValueAnimatedNode + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.m new file mode 100644 index 00000000000000..4007809552e131 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.m @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTAdditionAnimatedNode.h" + +@implementation RCTAdditionAnimatedNode + +- (void)performUpdate +{ + [super performUpdate]; + NSArray *inputNodes = self.config[@"input"]; + if (inputNodes.count > 1) { + RCTValueAnimatedNode *parent1 = (RCTValueAnimatedNode *)self.parentNodes[inputNodes[0]]; + RCTValueAnimatedNode *parent2 = (RCTValueAnimatedNode *)self.parentNodes[inputNodes[1]]; + if (parent1 && parent2) { + self.value = parent1.value + parent2.value; + } + } +} + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h new file mode 100644 index 00000000000000..8e06e90629fdd6 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface RCTAnimatedNode : NSObject + +- (instancetype)initWithTag:(NSNumber *)tag + config:(NSDictionary *)config NS_DESIGNATED_INITIALIZER; + +@property (nonatomic, readonly) NSNumber *nodeTag; +@property (nonatomic, copy, readonly) NSDictionary *config; + +@property (nonatomic, copy, readonly) NSDictionary *childNodes; +@property (nonatomic, copy, readonly) NSDictionary *parentNodes; + +@property (nonatomic, readonly) BOOL needsUpdate; +@property (nonatomic, readonly) BOOL hasUpdated; + +/** + * Marks a node and its children as needing update. + */ +- (void)setNeedsUpdate NS_REQUIRES_SUPER; + +/** + * The node will update its value if necesarry and only after its parents have updated. + */ +- (void)updateNodeIfNecessary NS_REQUIRES_SUPER; + +/** + * Where the actual update code lives. Called internally from updateNodeIfNecessary + */ +- (void)performUpdate NS_REQUIRES_SUPER; + +/** + * Cleans up after a round of updates. + */ +- (void)cleanupAnimationUpdate NS_REQUIRES_SUPER; + +- (void)addChild:(RCTAnimatedNode *)child NS_REQUIRES_SUPER; +- (void)removeChild:(RCTAnimatedNode *)child NS_REQUIRES_SUPER; + +- (void)onAttachedToNode:(RCTAnimatedNode *)parent NS_REQUIRES_SUPER; +- (void)onDetachedFromNode:(RCTAnimatedNode *)parent NS_REQUIRES_SUPER; + +- (void)detachNode NS_REQUIRES_SUPER; + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m new file mode 100644 index 00000000000000..9f5075b6222140 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTAnimatedNode.h" + +#import "RCTDefines.h" + +@implementation RCTAnimatedNode +{ + NSMutableDictionary *_childNodes; + NSMutableDictionary *_parentNodes; +} + +- (instancetype)initWithTag:(NSNumber *)tag + config:(NSDictionary *)config +{ + if ((self = [super init])) { + _nodeTag = tag; + _config = [config copy]; + } + return self; +} + +RCT_NOT_IMPLEMENTED(- (instancetype)init) + +- (NSDictionary *)childNodes +{ + return _childNodes; +} + +- (NSDictionary *)parentNodes +{ + return _parentNodes; +} + +- (void)addChild:(RCTAnimatedNode *)child +{ + if (!_childNodes) { + _childNodes = [NSMutableDictionary new]; + } + if (child) { + _childNodes[child.nodeTag] = child; + [child onAttachedToNode:self]; + } +} + +- (void)removeChild:(RCTAnimatedNode *)child +{ + if (!_childNodes) { + return; + } + if (child) { + [_childNodes removeObjectForKey:child.nodeTag]; + [child onDetachedFromNode:self]; + } +} + +- (void)onAttachedToNode:(RCTAnimatedNode *)parent +{ + if (!_parentNodes) { + _parentNodes = [NSMutableDictionary new]; + } + if (parent) { + _parentNodes[parent.nodeTag] = parent; + } +} + +- (void)onDetachedFromNode:(RCTAnimatedNode *)parent +{ + if (!_parentNodes) { + return; + } + if (parent) { + [_parentNodes removeObjectForKey:parent.nodeTag]; + } +} + +- (void)detachNode +{ + for (RCTAnimatedNode *parent in _parentNodes.allValues) { + [parent removeChild:self]; + } + for (RCTAnimatedNode *child in _childNodes.allValues) { + [self removeChild:child]; + } +} + +- (void)setNeedsUpdate +{ + if (_needsUpdate) { + // Has already been marked. Stop branch. + return; + } + _needsUpdate = YES; + for (RCTAnimatedNode *child in _childNodes.allValues) { + [child setNeedsUpdate]; + } +} + +- (void)cleanupAnimationUpdate +{ + if (_hasUpdated) { + _needsUpdate = NO; + _hasUpdated = NO; + for (RCTAnimatedNode *child in _childNodes.allValues) { + [child cleanupAnimationUpdate]; + } + } +} + +- (void)updateNodeIfNecessary +{ + if (_needsUpdate && !_hasUpdated) { + for (RCTAnimatedNode *parent in _parentNodes.allValues) { + [parent updateNodeIfNecessary]; + } + [self performUpdate]; + } +} + +- (void)performUpdate +{ + _hasUpdated = YES; + // To be overidden by subclasses + // This method is called on a node only if it has been marked for update + // during the current update loop +} + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.h b/Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.h new file mode 100644 index 00000000000000..5f39ea4656f8f6 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import "RCTBridgeModule.h" + +@class RCTValueAnimatedNode; + +NS_ASSUME_NONNULL_BEGIN + +@interface RCTAnimationDriverNode : NSObject + +@property (nonatomic, readonly) NSNumber *animationId; +@property (nonatomic, readonly) NSNumber *outputValue; + +@property (nonatomic, readonly) BOOL animationHasBegun; +@property (nonatomic, readonly) BOOL animationHasFinished; + +- (instancetype)initWithId:(NSNumber *)animationId + delay:(NSTimeInterval)delay + toValue:(CGFloat)toValue + frames:(NSArray *)frames + forNode:(RCTValueAnimatedNode *)valueNode + callBack:(nullable RCTResponseSenderBlock)callback NS_DESIGNATED_INITIALIZER; + +- (void)startAnimation; +- (void)stopAnimation; +- (void)stepAnimation; +- (void)removeAnimation; +- (void)cleanupAnimationUpdate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.m b/Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.m new file mode 100644 index 00000000000000..7da52da9c18921 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.m @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTAnimationDriverNode.h" + +#import + +#import "RCTAnimationUtils.h" +#import "RCTDefines.h" +#import "RCTValueAnimatedNode.h" + +const double SINGLE_FRAME_INTERVAL = 1.0 / 60.0; + +@implementation RCTAnimationDriverNode +{ + NSArray *_frames; + CGFloat _toValue; + CGFloat _fromValue; + NSTimeInterval _delay; + NSTimeInterval _animationStartTime; + NSTimeInterval _animationCurrentTime; + RCTValueAnimatedNode *_valueNode; + RCTResponseSenderBlock _callback; +} + +- (instancetype)initWithId:(nonnull NSNumber *)animationId + delay:(NSTimeInterval)delay + toValue:(CGFloat)toValue + frames:(nonnull NSArray *)frames + forNode:(nonnull RCTValueAnimatedNode *)valueNode + callBack:(nullable RCTResponseSenderBlock)callback +{ + if ((self = [super init])) { + _animationId = animationId; + _toValue = toValue; + _fromValue = valueNode.value; + _valueNode = valueNode; + _delay = delay; + _frames = [frames copy]; + _outputValue = @0; + _callback = [callback copy]; + } + return self; +} + +RCT_NOT_IMPLEMENTED(- (instancetype)init) + +- (void)startAnimation +{ + _animationStartTime = CACurrentMediaTime(); + _animationCurrentTime = _animationStartTime; + _animationHasBegun = YES; +} + +- (void)stopAnimation +{ + _animationHasFinished = YES; +} + +- (void)removeAnimation +{ + [self stopAnimation]; + _valueNode = nil; + if (_callback) { + _callback(@[(id)kCFNull]); + } +} + +- (void)stepAnimation +{ + if (!_animationHasBegun || + _animationHasFinished || + _frames.count == 0) { + // Animation has not begun or animation has already finished. + return; + } + + NSTimeInterval currentTime = CACurrentMediaTime(); + NSTimeInterval stepInterval = currentTime - _animationCurrentTime; + _animationCurrentTime = currentTime; + NSTimeInterval currentDuration = _animationCurrentTime - _animationStartTime; + + if (_delay > 0) { + // Decrement delay + _delay -= stepInterval; + return; + } + + // Determine how many frames have passed since last update. + // Get index of frames that surround the current interval + NSUInteger startIndex = floor(currentDuration / SINGLE_FRAME_INTERVAL); + NSUInteger nextIndex = startIndex + 1; + + if (nextIndex >= _frames.count) { + // We are at the end of the animation + // Update value and flag animation has ended. + NSNumber *finalValue = _frames.lastObject; + [self updateOutputWithFrameOutput:finalValue.doubleValue]; + [self stopAnimation]; + return; + } + + // Do a linear remap of the two frames to safegaurd against variable framerates + NSNumber *fromFrameValue = _frames[startIndex]; + NSNumber *toFrameValue = _frames[nextIndex]; + NSTimeInterval fromInterval = startIndex * SINGLE_FRAME_INTERVAL; + NSTimeInterval toInterval = nextIndex * SINGLE_FRAME_INTERVAL; + + // Interpolate between the individual frames to ensure the animations are + //smooth and of the proper duration regardless of the framerate. + CGFloat frameOutput = RCTInterpolateValue(currentDuration, + fromInterval, + toInterval, + fromFrameValue.doubleValue, + toFrameValue.doubleValue); + [self updateOutputWithFrameOutput:frameOutput]; +} + +- (void)updateOutputWithFrameOutput:(CGFloat)frameOutput +{ + CGFloat outputValue = RCTInterpolateValue(frameOutput, 0, 1, _fromValue, _toValue); + _outputValue = @(outputValue); + _valueNode.value = outputValue; + [_valueNode setNeedsUpdate]; +} + +- (void)cleanupAnimationUpdate +{ + [_valueNode cleanupAnimationUpdate]; +} + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.h new file mode 100644 index 00000000000000..2257e49646dc20 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTValueAnimatedNode.h" + +@interface RCTInterpolationAnimatedNode : RCTValueAnimatedNode + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m new file mode 100644 index 00000000000000..4c160ec24e7ef0 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTInterpolationAnimatedNode.h" +#import "RCTAnimationUtils.h" + +@implementation RCTInterpolationAnimatedNode +{ + __weak RCTValueAnimatedNode *_parentNode; + NSArray *_inputRange; + NSArray *_outputRange; +} + +- (instancetype)initWithTag:(NSNumber *)tag + config:(NSDictionary *)config +{ + if ((self = [super initWithTag:tag config:config])) { + _inputRange = [config[@"inputRange"] copy]; + NSMutableArray *outputRange = [NSMutableArray array]; + for (id value in config[@"outputRange"]) { + if ([value isKindOfClass:[NSNumber class]]) { + [outputRange addObject:value]; + } else if ([value isKindOfClass:[NSString class]]) { + NSString *str = (NSString *)value; + if ([str hasSuffix:@"deg"]) { + double degrees = str.doubleValue; + [outputRange addObject:@(RCTDegreesToRadians(degrees))]; + } else { + // Assume radians + [outputRange addObject:@(str.doubleValue)]; + } + } + } + _outputRange = [outputRange copy]; + } + return self; +} + +- (void)onAttachedToNode:(RCTAnimatedNode *)parent +{ + [super onAttachedToNode:parent]; + if ([parent isKindOfClass:[RCTValueAnimatedNode class]]) { + _parentNode = (RCTValueAnimatedNode *)parent; + } +} + +- (void)onDetachedFromNode:(RCTAnimatedNode *)parent +{ + [super onDetachedFromNode:parent]; + if (_parentNode == parent) { + _parentNode = nil; + } +} + +- (NSUInteger)findIndexOfNearestValue:(CGFloat)value + inRange:(NSArray *)range +{ + NSUInteger index; + NSUInteger rangeCount = range.count; + for (index = 1; index < rangeCount - 1; index++) { + NSNumber *inputValue = range[index]; + if (inputValue.doubleValue >= value) { + break; + } + } + return index - 1; +} + +- (void)performUpdate +{ + [super performUpdate]; + if (!_parentNode) { + return; + } + + NSUInteger rangeIndex = [self findIndexOfNearestValue:_parentNode.value + inRange:_inputRange]; + NSNumber *inputMin = _inputRange[rangeIndex]; + NSNumber *inputMax = _inputRange[rangeIndex + 1]; + NSNumber *outputMin = _outputRange[rangeIndex]; + NSNumber *outputMax = _outputRange[rangeIndex + 1]; + + CGFloat outputValue = RCTInterpolateValue(_parentNode.value, + inputMin.doubleValue, + inputMax.doubleValue, + outputMin.doubleValue, + outputMax.doubleValue); + self.value = outputValue; +} + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.h new file mode 100644 index 00000000000000..43a1ff9b957ca9 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTValueAnimatedNode.h" + +@interface RCTMultiplicationAnimatedNode : RCTValueAnimatedNode + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.m new file mode 100644 index 00000000000000..5a8117bdb24d91 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.m @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTMultiplicationAnimatedNode.h" + +@implementation RCTMultiplicationAnimatedNode + +- (void)performUpdate +{ + [super performUpdate]; + + NSArray *inputNodes = self.config[@"input"]; + if (inputNodes.count > 1) { + RCTValueAnimatedNode *parent1 = (RCTValueAnimatedNode *)self.parentNodes[inputNodes[0]]; + RCTValueAnimatedNode *parent2 = (RCTValueAnimatedNode *)self.parentNodes[inputNodes[1]]; + if (!parent1 || !parent2) { + return; + } + self.value = parent1.value * parent2.value; + } +} + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.h new file mode 100644 index 00000000000000..08debc6c794b74 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTAnimatedNode.h" + +@class RCTNativeAnimatedModule; +@class RCTViewPropertyMapper; + +@interface RCTPropsAnimatedNode : RCTAnimatedNode + +@property (nonatomic, readonly) RCTViewPropertyMapper *propertyMapper; + +- (void)connectToView:(NSNumber *)viewTag animatedModule:(RCTNativeAnimatedModule *)animationModule; +- (void)disconnectFromView:(NSNumber *)viewTag; + +- (void)performViewUpdatesIfNecessary; + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m new file mode 100644 index 00000000000000..fcd5c34d6643b4 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTPropsAnimatedNode.h" +#import "RCTAnimationUtils.h" +#import "RCTNativeAnimatedModule.h" +#import "RCTStyleAnimatedNode.h" +#import "RCTViewPropertyMapper.h" + +@implementation RCTPropsAnimatedNode +{ + RCTStyleAnimatedNode *_parentNode; +} + +- (void)onAttachedToNode:(RCTAnimatedNode *)parent +{ + [super onAttachedToNode:parent]; + if ([parent isKindOfClass:[RCTStyleAnimatedNode class]]) { + _parentNode = (RCTStyleAnimatedNode *)parent; + } +} + +- (void)onDetachedFromNode:(RCTAnimatedNode *)parent +{ + [super onDetachedFromNode:parent]; + if (_parentNode == parent) { + _parentNode = nil; + } +} + +- (void)connectToView:(NSNumber *)viewTag animatedModule:(RCTNativeAnimatedModule *)animationModule +{ + _propertyMapper = [[RCTViewPropertyMapper alloc] initWithViewTag:viewTag animationModule:animationModule]; +} + +- (void)disconnectFromView:(NSNumber *)viewTag +{ + _propertyMapper = nil; +} + +- (void)performUpdate +{ + [super performUpdate]; + [self performViewUpdatesIfNecessary]; +} + +- (void)performViewUpdatesIfNecessary +{ + NSDictionary *updates = [_parentNode updatedPropsDictionary]; + if (updates.count) { + [_propertyMapper updateViewWithDictionary:updates]; + } +} + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.h new file mode 100644 index 00000000000000..f3a6cd5dfe44b6 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTAnimatedNode.h" + +@interface RCTStyleAnimatedNode : RCTAnimatedNode + +- (NSDictionary *)updatedPropsDictionary; + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.m new file mode 100644 index 00000000000000..1910a133314bcc --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.m @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTStyleAnimatedNode.h" +#import "RCTAnimationUtils.h" +#import "RCTValueAnimatedNode.h" +#import "RCTTransformAnimatedNode.h" + +@implementation RCTStyleAnimatedNode +{ + NSMutableDictionary *_updatedPropsDictionary; +} + +- (instancetype)initWithTag:(NSNumber *)tag + config:(NSDictionary *)config; +{ + if ((self = [super initWithTag:tag config:config])) { + _updatedPropsDictionary = [NSMutableDictionary new]; + } + return self; +} + +- (NSDictionary *)updatedPropsDictionary +{ + return _updatedPropsDictionary; +} + +- (void)performUpdate +{ + [super performUpdate]; + + NSDictionary *style = self.config[@"style"]; + [style enumerateKeysAndObjectsUsingBlock:^(NSString *property, NSNumber *nodeTag, __unused BOOL *stop) { + RCTAnimatedNode *node = self.parentNodes[nodeTag]; + if (node && node.hasUpdated) { + if ([node isKindOfClass:[RCTValueAnimatedNode class]]) { + RCTValueAnimatedNode *parentNode = (RCTValueAnimatedNode *)node; + [_updatedPropsDictionary setObject:@(parentNode.value) forKey:property]; + } else if ([node isKindOfClass:[RCTTransformAnimatedNode class]]) { + RCTTransformAnimatedNode *parentNode = (RCTTransformAnimatedNode *)node; + if (parentNode.updatedPropsDictionary.count) { + [_updatedPropsDictionary addEntriesFromDictionary:parentNode.updatedPropsDictionary]; + } + } + } + }]; +} + +- (void)cleanupAnimationUpdate +{ + [super cleanupAnimationUpdate]; + [_updatedPropsDictionary removeAllObjects]; +} + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.h new file mode 100644 index 00000000000000..6d1cfc840af0f4 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTAnimatedNode.h" + +@interface RCTTransformAnimatedNode : RCTAnimatedNode + +- (NSDictionary *)updatedPropsDictionary; + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.m new file mode 100644 index 00000000000000..c6a356ff5ed8a2 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.m @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTTransformAnimatedNode.h" +#import "RCTValueAnimatedNode.h" + +@implementation RCTTransformAnimatedNode +{ + NSMutableDictionary *_updatedPropsDictionary; +} + +- (instancetype)initWithTag:(NSNumber *)tag + config:(NSDictionary *)config; +{ + if ((self = [super initWithTag:tag config:config])) { + _updatedPropsDictionary = [NSMutableDictionary new]; + } + return self; +} + +- (NSDictionary *)updatedPropsDictionary +{ + return _updatedPropsDictionary; +} + +- (void)performUpdate +{ + [super performUpdate]; + + NSDictionary *transforms = self.config[@"transform"]; + [transforms enumerateKeysAndObjectsUsingBlock:^(NSString *property, NSNumber *nodeTag, __unused BOOL *stop) { + RCTAnimatedNode *node = self.parentNodes[nodeTag]; + if (node.hasUpdated && [node isKindOfClass:[RCTValueAnimatedNode class]]) { + RCTValueAnimatedNode *parentNode = (RCTValueAnimatedNode *)node; + _updatedPropsDictionary[property] = @(parentNode.value); + } + }]; +} + +- (void)cleanupAnimationUpdate +{ + [super cleanupAnimationUpdate]; + [_updatedPropsDictionary removeAllObjects]; +} + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h new file mode 100644 index 00000000000000..af183243534cdd --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTAnimatedNode.h" +#import + +@interface RCTValueAnimatedNode : RCTAnimatedNode + +@property (nonatomic, assign) CGFloat value; + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.m new file mode 100644 index 00000000000000..cffa66681d7b17 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.m @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTValueAnimatedNode.h" + +@implementation RCTValueAnimatedNode + +@end diff --git a/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj b/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj new file mode 100644 index 00000000000000..3f0c751b22370a --- /dev/null +++ b/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj @@ -0,0 +1,337 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 13E501CC1D07A644005F35D8 /* RCTAnimationUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501B81D07A644005F35D8 /* RCTAnimationUtils.m */; }; + 13E501CF1D07A644005F35D8 /* RCTNativeAnimatedModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501BE1D07A644005F35D8 /* RCTNativeAnimatedModule.m */; }; + 13E501D41D07A644005F35D8 /* RCTViewPropertyMapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501C81D07A644005F35D8 /* RCTViewPropertyMapper.m */; }; + 13E501E81D07A6C9005F35D8 /* RCTAdditionAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501D71D07A6C9005F35D8 /* RCTAdditionAnimatedNode.m */; }; + 13E501E91D07A6C9005F35D8 /* RCTAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501D91D07A6C9005F35D8 /* RCTAnimatedNode.m */; }; + 13E501EA1D07A6C9005F35D8 /* RCTAnimationDriverNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501DB1D07A6C9005F35D8 /* RCTAnimationDriverNode.m */; }; + 13E501EB1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501DD1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.m */; }; + 13E501EC1D07A6C9005F35D8 /* RCTMultiplicationAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501DF1D07A6C9005F35D8 /* RCTMultiplicationAnimatedNode.m */; }; + 13E501ED1D07A6C9005F35D8 /* RCTPropsAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E11D07A6C9005F35D8 /* RCTPropsAnimatedNode.m */; }; + 13E501EE1D07A6C9005F35D8 /* RCTStyleAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E31D07A6C9005F35D8 /* RCTStyleAnimatedNode.m */; }; + 13E501EF1D07A6C9005F35D8 /* RCTTransformAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E51D07A6C9005F35D8 /* RCTTransformAnimatedNode.m */; }; + 13E501F01D07A6C9005F35D8 /* RCTValueAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E71D07A6C9005F35D8 /* RCTValueAnimatedNode.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 58B511D91A9E6C8500147676 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 134814201AA4EA6300B7C361 /* libRCTAnimation.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTAnimation.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 13E501B71D07A644005F35D8 /* RCTAnimationUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationUtils.h; sourceTree = ""; }; + 13E501B81D07A644005F35D8 /* RCTAnimationUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimationUtils.m; sourceTree = ""; }; + 13E501BD1D07A644005F35D8 /* RCTNativeAnimatedModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNativeAnimatedModule.h; sourceTree = ""; }; + 13E501BE1D07A644005F35D8 /* RCTNativeAnimatedModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNativeAnimatedModule.m; sourceTree = ""; }; + 13E501C71D07A644005F35D8 /* RCTViewPropertyMapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTViewPropertyMapper.h; sourceTree = ""; }; + 13E501C81D07A644005F35D8 /* RCTViewPropertyMapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTViewPropertyMapper.m; sourceTree = ""; }; + 13E501D61D07A6C9005F35D8 /* RCTAdditionAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAdditionAnimatedNode.h; sourceTree = ""; }; + 13E501D71D07A6C9005F35D8 /* RCTAdditionAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAdditionAnimatedNode.m; sourceTree = ""; }; + 13E501D81D07A6C9005F35D8 /* RCTAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimatedNode.h; sourceTree = ""; }; + 13E501D91D07A6C9005F35D8 /* RCTAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimatedNode.m; sourceTree = ""; }; + 13E501DA1D07A6C9005F35D8 /* RCTAnimationDriverNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationDriverNode.h; sourceTree = ""; }; + 13E501DB1D07A6C9005F35D8 /* RCTAnimationDriverNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimationDriverNode.m; sourceTree = ""; }; + 13E501DC1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTInterpolationAnimatedNode.h; sourceTree = ""; }; + 13E501DD1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTInterpolationAnimatedNode.m; sourceTree = ""; }; + 13E501DE1D07A6C9005F35D8 /* RCTMultiplicationAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMultiplicationAnimatedNode.h; sourceTree = ""; }; + 13E501DF1D07A6C9005F35D8 /* RCTMultiplicationAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMultiplicationAnimatedNode.m; sourceTree = ""; }; + 13E501E01D07A6C9005F35D8 /* RCTPropsAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPropsAnimatedNode.h; sourceTree = ""; }; + 13E501E11D07A6C9005F35D8 /* RCTPropsAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPropsAnimatedNode.m; sourceTree = ""; }; + 13E501E21D07A6C9005F35D8 /* RCTStyleAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStyleAnimatedNode.h; sourceTree = ""; }; + 13E501E31D07A6C9005F35D8 /* RCTStyleAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStyleAnimatedNode.m; sourceTree = ""; }; + 13E501E41D07A6C9005F35D8 /* RCTTransformAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTransformAnimatedNode.h; sourceTree = ""; }; + 13E501E51D07A6C9005F35D8 /* RCTTransformAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTransformAnimatedNode.m; sourceTree = ""; }; + 13E501E61D07A6C9005F35D8 /* RCTValueAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTValueAnimatedNode.h; sourceTree = ""; }; + 13E501E71D07A6C9005F35D8 /* RCTValueAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTValueAnimatedNode.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 58B511D81A9E6C8500147676 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 134814211AA4EA7D00B7C361 /* Products */ = { + isa = PBXGroup; + children = ( + 134814201AA4EA6300B7C361 /* libRCTAnimation.a */, + ); + name = Products; + sourceTree = ""; + }; + 13E501D51D07A6C9005F35D8 /* Nodes */ = { + isa = PBXGroup; + children = ( + 13E501D61D07A6C9005F35D8 /* RCTAdditionAnimatedNode.h */, + 13E501D71D07A6C9005F35D8 /* RCTAdditionAnimatedNode.m */, + 13E501D81D07A6C9005F35D8 /* RCTAnimatedNode.h */, + 13E501D91D07A6C9005F35D8 /* RCTAnimatedNode.m */, + 13E501DA1D07A6C9005F35D8 /* RCTAnimationDriverNode.h */, + 13E501DB1D07A6C9005F35D8 /* RCTAnimationDriverNode.m */, + 13E501DC1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.h */, + 13E501DD1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.m */, + 13E501DE1D07A6C9005F35D8 /* RCTMultiplicationAnimatedNode.h */, + 13E501DF1D07A6C9005F35D8 /* RCTMultiplicationAnimatedNode.m */, + 13E501E01D07A6C9005F35D8 /* RCTPropsAnimatedNode.h */, + 13E501E11D07A6C9005F35D8 /* RCTPropsAnimatedNode.m */, + 13E501E21D07A6C9005F35D8 /* RCTStyleAnimatedNode.h */, + 13E501E31D07A6C9005F35D8 /* RCTStyleAnimatedNode.m */, + 13E501E41D07A6C9005F35D8 /* RCTTransformAnimatedNode.h */, + 13E501E51D07A6C9005F35D8 /* RCTTransformAnimatedNode.m */, + 13E501E61D07A6C9005F35D8 /* RCTValueAnimatedNode.h */, + 13E501E71D07A6C9005F35D8 /* RCTValueAnimatedNode.m */, + ); + path = Nodes; + sourceTree = ""; + }; + 58B511D21A9E6C8500147676 = { + isa = PBXGroup; + children = ( + 13E501B71D07A644005F35D8 /* RCTAnimationUtils.h */, + 13E501B81D07A644005F35D8 /* RCTAnimationUtils.m */, + 13E501C71D07A644005F35D8 /* RCTViewPropertyMapper.h */, + 13E501C81D07A644005F35D8 /* RCTViewPropertyMapper.m */, + 13E501BD1D07A644005F35D8 /* RCTNativeAnimatedModule.h */, + 13E501BE1D07A644005F35D8 /* RCTNativeAnimatedModule.m */, + 13E501D51D07A6C9005F35D8 /* Nodes */, + 134814211AA4EA7D00B7C361 /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 58B511DA1A9E6C8500147676 /* RCTAnimation */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTAnimation" */; + buildPhases = ( + 58B511D71A9E6C8500147676 /* Sources */, + 58B511D81A9E6C8500147676 /* Frameworks */, + 58B511D91A9E6C8500147676 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTAnimation; + productName = RCTDataManager; + productReference = 134814201AA4EA6300B7C361 /* libRCTAnimation.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 58B511D31A9E6C8500147676 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0730; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 58B511DA1A9E6C8500147676 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTAnimation" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 58B511D21A9E6C8500147676; + productRefGroup = 58B511D21A9E6C8500147676; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 58B511DA1A9E6C8500147676 /* RCTAnimation */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 58B511D71A9E6C8500147676 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13E501F01D07A6C9005F35D8 /* RCTValueAnimatedNode.m in Sources */, + 13E501EE1D07A6C9005F35D8 /* RCTStyleAnimatedNode.m in Sources */, + 13E501CC1D07A644005F35D8 /* RCTAnimationUtils.m in Sources */, + 13E501CF1D07A644005F35D8 /* RCTNativeAnimatedModule.m in Sources */, + 13E501EC1D07A6C9005F35D8 /* RCTMultiplicationAnimatedNode.m in Sources */, + 13E501ED1D07A6C9005F35D8 /* RCTPropsAnimatedNode.m in Sources */, + 13E501E91D07A6C9005F35D8 /* RCTAnimatedNode.m in Sources */, + 13E501EB1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.m in Sources */, + 13E501E81D07A6C9005F35D8 /* RCTAdditionAnimatedNode.m in Sources */, + 13E501EA1D07A6C9005F35D8 /* RCTAnimationDriverNode.m in Sources */, + 13E501EF1D07A6C9005F35D8 /* RCTTransformAnimatedNode.m in Sources */, + 13E501D41D07A644005F35D8 /* RCTViewPropertyMapper.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 58B511ED1A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_SHADOW = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + WARNING_CFLAGS = ( + "-Werror", + "-Wall", + ); + }; + name = Debug; + }; + 58B511EE1A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_SHADOW = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + WARNING_CFLAGS = ( + "-Werror", + "-Wall", + ); + }; + name = Release; + }; + 58B511F01A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTAnimation; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 58B511F11A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTAnimation; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTAnimation" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511ED1A9E6C8500147676 /* Debug */, + 58B511EE1A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTAnimation" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511F01A9E6C8500147676 /* Debug */, + 58B511F11A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 58B511D31A9E6C8500147676 /* Project object */; +} diff --git a/Libraries/NativeAnimation/RCTAnimationUtils.h b/Libraries/NativeAnimation/RCTAnimationUtils.h new file mode 100644 index 00000000000000..f34a9c6ee8f8a9 --- /dev/null +++ b/Libraries/NativeAnimation/RCTAnimationUtils.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +#import "RCTDefines.h" + +RCT_EXTERN CGFloat RCTInterpolateValue(CGFloat value, + CGFloat fromMin, + CGFloat fromMax, + CGFloat toMin, + CGFloat toMax); + +RCT_EXTERN CGFloat RCTRadiansToDegrees(CGFloat radians); +RCT_EXTERN CGFloat RCTDegreesToRadians(CGFloat degrees); diff --git a/Libraries/NativeAnimation/RCTAnimationUtils.m b/Libraries/NativeAnimation/RCTAnimationUtils.m new file mode 100644 index 00000000000000..720edddbd47396 --- /dev/null +++ b/Libraries/NativeAnimation/RCTAnimationUtils.m @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTAnimationUtils.h" + +/** + * Interpolates value by remapping it linearly fromMin->fromMax to toMin->toMax + */ +CGFloat RCTInterpolateValue(CGFloat value, + CGFloat fromMin, + CGFloat fromMax, + CGFloat toMin, + CGFloat toMax) +{ + return toMin + (value - fromMin) * (toMax - toMin) / (fromMax - fromMin); +} + +CGFloat RCTRadiansToDegrees(CGFloat radians) +{ + return radians * 180.0 / M_PI; +} + +CGFloat RCTDegreesToRadians(CGFloat degrees) +{ + return degrees / 180.0 * M_PI; +} diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedModule.h b/Libraries/NativeAnimation/RCTNativeAnimatedModule.h new file mode 100644 index 00000000000000..d099ac53bfacf9 --- /dev/null +++ b/Libraries/NativeAnimation/RCTNativeAnimatedModule.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#import "RCTBridgeModule.h" + +@interface RCTNativeAnimatedModule : NSObject + +@end diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedModule.m b/Libraries/NativeAnimation/RCTNativeAnimatedModule.m new file mode 100644 index 00000000000000..74e157fc17fb6b --- /dev/null +++ b/Libraries/NativeAnimation/RCTNativeAnimatedModule.m @@ -0,0 +1,247 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#import "RCTNativeAnimatedModule.h" + +#import "RCTAdditionAnimatedNode.h" +#import "RCTAnimationDriverNode.h" +#import "RCTAnimationUtils.h" +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTInterpolationAnimatedNode.h" +#import "RCTLog.h" +#import "RCTMultiplicationAnimatedNode.h" +#import "RCTPropsAnimatedNode.h" +#import "RCTStyleAnimatedNode.h" +#import "RCTTransformAnimatedNode.h" +#import "RCTValueAnimatedNode.h" + +@implementation RCTNativeAnimatedModule +{ + NSMutableDictionary *_animationNodes; + NSMutableDictionary *_animationDrivers; + NSMutableSet *_activeAnimations; + NSMutableSet *_finishedAnimations; + NSMutableSet *_updatedValueNodes; + NSMutableSet *_propAnimationNodes; + CADisplayLink *_displayLink; +} + +@synthesize bridge = _bridge; + +RCT_EXPORT_MODULE() + +- (void)setBridge:(RCTBridge *)bridge +{ + _bridge = bridge; + _animationNodes = [NSMutableDictionary new]; + _animationDrivers = [NSMutableDictionary new]; + _activeAnimations = [NSMutableSet new]; + _finishedAnimations = [NSMutableSet new]; + _updatedValueNodes = [NSMutableSet new]; + _propAnimationNodes = [NSMutableSet new]; +} + +RCT_EXPORT_METHOD(createAnimatedNode:(nonnull NSNumber *)tag + config:(NSDictionary *)config) +{ + static NSDictionary *map; + static dispatch_once_t mapToken; + dispatch_once(&mapToken, ^{ + map = @{@"style" : [RCTStyleAnimatedNode class], + @"value" : [RCTValueAnimatedNode class], + @"props" : [RCTPropsAnimatedNode class], + @"interpolation" : [RCTInterpolationAnimatedNode class], + @"addition" : [RCTAdditionAnimatedNode class], + @"multiplication" : [RCTMultiplicationAnimatedNode class], + @"transform" : [RCTTransformAnimatedNode class]}; + }); + + NSString *nodeType = [RCTConvert NSString:config[@"type"]]; + + Class nodeClass = map[nodeType]; + if (!nodeClass) { + RCTLogError(@"Animated node type %@ not supported natively", nodeType); + return; + } + + RCTAnimatedNode *node = [[nodeClass alloc] initWithTag:tag config:config]; + _animationNodes[tag] = node; + + if ([node isKindOfClass:[RCTPropsAnimatedNode class]]) { + [_propAnimationNodes addObject:(RCTPropsAnimatedNode *)node]; + } +} + +RCT_EXPORT_METHOD(connectAnimatedNodes:(nonnull NSNumber *)parentTag + childTag:(nonnull NSNumber *)childTag) +{ + RCTAssertParam(parentTag); + RCTAssertParam(childTag); + + RCTAnimatedNode *parentNode = _animationNodes[parentTag]; + RCTAnimatedNode *childNode = _animationNodes[childTag]; + + RCTAssertParam(parentNode); + RCTAssertParam(childNode); + + [parentNode addChild:childNode]; +} + +RCT_EXPORT_METHOD(disconnectAnimatedNodes:(nonnull NSNumber *)parentTag + childTag:(nonnull NSNumber *)childTag) +{ + RCTAssertParam(parentTag); + RCTAssertParam(childTag); + + RCTAnimatedNode *parentNode = _animationNodes[parentTag]; + RCTAnimatedNode *childNode = _animationNodes[childTag]; + + RCTAssertParam(parentNode); + RCTAssertParam(childNode); + + [parentNode removeChild:childNode]; +} + +RCT_EXPORT_METHOD(startAnimatingNode:(nonnull NSNumber *)animationId + nodeTag:(nonnull NSNumber *)nodeTag + config:(NSDictionary *)config + endCallback:(RCTResponseSenderBlock)callBack) +{ + if (RCT_DEBUG && ![config[@"type"] isEqual:@"frames"]) { + RCTLogError(@"Unsupported animation type: %@", config[@"type"]); + return; + } + + NSTimeInterval delay = [RCTConvert double:config[@"delay"]]; + NSNumber *toValue = [RCTConvert NSNumber:config[@"toValue"]] ?: @1; + NSArray *frames = [RCTConvert NSNumberArray:config[@"frames"]]; + + RCTValueAnimatedNode *valueNode = (RCTValueAnimatedNode *)_animationNodes[nodeTag]; + + RCTAnimationDriverNode *animationDriver = + [[RCTAnimationDriverNode alloc] initWithId:animationId + delay:delay + toValue:toValue.doubleValue + frames:frames + forNode:valueNode + callBack:callBack]; + [_activeAnimations addObject:animationDriver]; + _animationDrivers[animationId] = animationDriver; + [animationDriver startAnimation]; + [self startAnimation]; +} + +RCT_EXPORT_METHOD(stopAnimation:(nonnull NSNumber *)animationId) +{ + RCTAnimationDriverNode *driver = _animationDrivers[animationId]; + if (driver) { + [driver removeAnimation]; + [_animationDrivers removeObjectForKey:animationId]; + [_activeAnimations removeObject:driver]; + [_finishedAnimations removeObject:driver]; + } +} + +RCT_EXPORT_METHOD(setAnimatedNodeValue:(nonnull NSNumber *)nodeTag + value:(nonnull NSNumber *)value) +{ + RCTAnimatedNode *node = _animationNodes[nodeTag]; + if (![node isKindOfClass:[RCTValueAnimatedNode class]]) { + RCTLogError(@"Not a value node."); + return; + } + RCTValueAnimatedNode *valueNode = (RCTValueAnimatedNode *)node; + valueNode.value = value.floatValue; + [valueNode setNeedsUpdate]; +} + +RCT_EXPORT_METHOD(connectAnimatedNodeToView:(nonnull NSNumber *)nodeTag + viewTag:(nonnull NSNumber *)viewTag) +{ + RCTAnimatedNode *node = _animationNodes[nodeTag]; + if (viewTag && [node isKindOfClass:[RCTPropsAnimatedNode class]]) { + [(RCTPropsAnimatedNode *)node connectToView:viewTag animatedModule:self]; + } +} + +RCT_EXPORT_METHOD(disconnectAnimatedNodeFromView:(nonnull NSNumber *)nodeTag + viewTag:(nonnull NSNumber *)viewTag) +{ + RCTAnimatedNode *node = _animationNodes[nodeTag]; + if (viewTag && node && [node isKindOfClass:[RCTPropsAnimatedNode class]]) { + [(RCTPropsAnimatedNode *)node disconnectFromView:viewTag]; + } +} + +RCT_EXPORT_METHOD(dropAnimatedNode:(nonnull NSNumber *)tag) +{ + RCTAnimatedNode *node = _animationNodes[tag]; + if (node) { + [node detachNode]; + [_animationNodes removeObjectForKey:tag]; + if ([node isKindOfClass:[RCTValueAnimatedNode class]]) { + [_updatedValueNodes removeObject:(RCTValueAnimatedNode *)node]; + } else if ([node isKindOfClass:[RCTPropsAnimatedNode class]]) { + [_propAnimationNodes removeObject:(RCTPropsAnimatedNode *)node]; + } + } +} + +#pragma mark -- Animation Loop + +- (void)startAnimation +{ + if (!_displayLink && _activeAnimations.count > 0) { + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateAnimations)]; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } +} + +- (void)updateAnimations +{ + // Step Current active animations + // This also recursively marks children nodes as needing update + for (RCTAnimationDriverNode *animationDriver in _activeAnimations) { + [animationDriver stepAnimation]; + } + + // Perform node updates for marked nodes. + // At this point all nodes that are in need of an update are properly marked as such. + for (RCTPropsAnimatedNode *propsNode in _propAnimationNodes) { + [propsNode updateNodeIfNecessary]; + } + + // Cleanup nodes and prepare for next cycle. Remove updated nodes from bucket. + for (RCTAnimationDriverNode *driverNode in _activeAnimations) { + [driverNode cleanupAnimationUpdate]; + } + for (RCTValueAnimatedNode *valueNode in _updatedValueNodes) { + [valueNode cleanupAnimationUpdate]; + } + [_updatedValueNodes removeAllObjects]; + + for (RCTAnimationDriverNode *driverNode in _activeAnimations) { + if (driverNode.animationHasFinished) { + [driverNode removeAnimation]; + [_finishedAnimations addObject:driverNode]; + } + } + for (RCTAnimationDriverNode *driverNode in _finishedAnimations) { + [_activeAnimations removeObject:driverNode]; + [_animationDrivers removeObjectForKey:driverNode.animationId]; + } + [_finishedAnimations removeAllObjects]; + + if (_activeAnimations.count == 0) { + [_displayLink invalidate]; + _displayLink = nil; + } +} + +@end diff --git a/Libraries/NativeAnimation/RCTViewPropertyMapper.h b/Libraries/NativeAnimation/RCTViewPropertyMapper.h new file mode 100644 index 00000000000000..29bbee754a7c7c --- /dev/null +++ b/Libraries/NativeAnimation/RCTViewPropertyMapper.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#import + +@class RCTNativeAnimatedModule; + +@interface RCTViewPropertyMapper : NSObject + +@property (nonatomic, readonly) NSNumber *viewTag; + +- (instancetype)initWithViewTag:(NSNumber *)viewTag + animationModule:(RCTNativeAnimatedModule *)animationModule NS_DESIGNATED_INITIALIZER; + +- (void)updateViewWithDictionary:(NSDictionary *)updates; + +@end diff --git a/Libraries/NativeAnimation/RCTViewPropertyMapper.m b/Libraries/NativeAnimation/RCTViewPropertyMapper.m new file mode 100644 index 00000000000000..bfa9803a65e0d6 --- /dev/null +++ b/Libraries/NativeAnimation/RCTViewPropertyMapper.m @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTViewPropertyMapper.h" + +#import + +#import "RCTBridge.h" +#import "RCTUIManager.h" +#import "RCTNativeAnimatedModule.h" + +@implementation RCTViewPropertyMapper +{ + CGFloat _translateX; + CGFloat _translateY; + CGFloat _scaleX; + CGFloat _scaleY; + CGFloat _rotation; + RCTNativeAnimatedModule *_animationModule; +} + +- (instancetype)initWithViewTag:(NSNumber *)viewTag + animationModule:(RCTNativeAnimatedModule *)animationModule +{ + if ((self = [super init])) { + _animationModule = animationModule; + _viewTag = viewTag; + _translateX = 0; + _translateY = 0; + _scaleX = 1; + _scaleY = 1; + _rotation = 0; + } + return self; +} + +RCT_NOT_IMPLEMENTED(- (instancetype)init) + +- (void)updateViewWithDictionary:(NSDictionary *)updates +{ + if (updates.count) { + UIView *view = [_animationModule.bridge.uiManager viewForReactTag:_viewTag]; + if (!view) { + return; + } + + NSNumber *opacity = updates[@"opacity"]; + if (opacity) { + view.alpha = opacity.doubleValue; + } + + NSNumber *scale = updates[@"scale"]; + if (scale) { + _scaleX = scale.doubleValue; + _scaleY = scale.doubleValue; + } + NSNumber *scaleX = updates[@"scaleX"]; + if (scaleX) { + _scaleX = scaleX.doubleValue; + } + NSNumber *scaleY = updates[@"scaleY"]; + if (scaleY) { + _scaleY = scaleY.doubleValue; + } + NSNumber *translateX = updates[@"translateX"]; + if (translateX) { + _translateX = translateX.doubleValue; + } + NSNumber *translateY = updates[@"translateY"]; + if (translateY) { + _translateY = translateY.doubleValue; + } + NSNumber *rotation = updates[@"rotate"]; + if (rotation) { + _rotation = rotation.doubleValue; + } + + if (translateX || translateY || scale || scaleX || scaleY || rotation) { + CATransform3D xform = CATransform3DMakeScale(_scaleX, _scaleY, 0); + xform = CATransform3DTranslate(xform, _translateX, _translateY, 0); + xform = CATransform3DRotate(xform, _rotation, 0, 0, 1); + view.layer.allowsEdgeAntialiasing = YES; + view.layer.transform = xform; + } + } +} + +@end From b29c938312d6f717c56453f83c9f94c10e2767bd Mon Sep 17 00:00:00 2001 From: Brandon Withrow Date: Tue, 7 Jun 2016 23:39:09 -0700 Subject: [PATCH 280/843] Reverted commit D3401811 Summary: Currently on iOS animations are being performed on the JS thread. This ports animations over to the native thread and performs them natively. A lot of this work has already been done on Android, but this PR enables a few animation nodes that Android doesn't yet support such as Transform, Multiplication, and Addition nodes. Also there is a demo of the native animations added to the UIExplorer app. Closes https://github.com/facebook/react-native/pull/7884 Differential Revision: D3401811 Pulled By: nicklockwood fbshipit-source-id: 709e533243130153febef03ddd60d39e9fe70e3e --- .../UIExplorer/NativeAnimationsExample.js | 308 ---------------- .../UIExplorer.xcodeproj/project.pbxproj | 30 -- .../xcschemes/UIExplorer.xcscheme | 2 +- Examples/UIExplorer/UIExplorerList.ios.js | 4 - .../Animated/src/AnimatedImplementation.js | 34 +- .../Animated/src/NativeAnimatedHelper.js | 19 +- .../Nodes/RCTAdditionAnimatedNode.h | 14 - .../Nodes/RCTAdditionAnimatedNode.m | 27 -- .../NativeAnimation/Nodes/RCTAnimatedNode.h | 54 --- .../NativeAnimation/Nodes/RCTAnimatedNode.m | 135 ------- .../Nodes/RCTAnimationDriverNode.h | 40 --- .../Nodes/RCTAnimationDriverNode.m | 138 ------- .../Nodes/RCTInterpolationAnimatedNode.h | 14 - .../Nodes/RCTInterpolationAnimatedNode.m | 97 ----- .../Nodes/RCTMultiplicationAnimatedNode.h | 14 - .../Nodes/RCTMultiplicationAnimatedNode.m | 29 -- .../Nodes/RCTPropsAnimatedNode.h | 24 -- .../Nodes/RCTPropsAnimatedNode.m | 61 ---- .../Nodes/RCTStyleAnimatedNode.h | 16 - .../Nodes/RCTStyleAnimatedNode.m | 61 ---- .../Nodes/RCTTransformAnimatedNode.h | 16 - .../Nodes/RCTTransformAnimatedNode.m | 52 --- .../Nodes/RCTValueAnimatedNode.h | 17 - .../Nodes/RCTValueAnimatedNode.m | 14 - .../RCTAnimation.xcodeproj/project.pbxproj | 337 ------------------ Libraries/NativeAnimation/RCTAnimationUtils.h | 22 -- Libraries/NativeAnimation/RCTAnimationUtils.m | 32 -- .../NativeAnimation/RCTNativeAnimatedModule.h | 13 - .../NativeAnimation/RCTNativeAnimatedModule.m | 247 ------------- .../NativeAnimation/RCTViewPropertyMapper.h | 22 -- .../NativeAnimation/RCTViewPropertyMapper.m | 94 ----- 31 files changed, 4 insertions(+), 1983 deletions(-) delete mode 100644 Examples/UIExplorer/NativeAnimationsExample.js delete mode 100644 Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.h delete mode 100644 Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.m delete mode 100644 Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h delete mode 100644 Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m delete mode 100644 Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.h delete mode 100644 Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.m delete mode 100644 Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.h delete mode 100644 Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m delete mode 100644 Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.h delete mode 100644 Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.m delete mode 100644 Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.h delete mode 100644 Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m delete mode 100644 Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.h delete mode 100644 Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.m delete mode 100644 Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.h delete mode 100644 Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.m delete mode 100644 Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h delete mode 100644 Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.m delete mode 100644 Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj delete mode 100644 Libraries/NativeAnimation/RCTAnimationUtils.h delete mode 100644 Libraries/NativeAnimation/RCTAnimationUtils.m delete mode 100644 Libraries/NativeAnimation/RCTNativeAnimatedModule.h delete mode 100644 Libraries/NativeAnimation/RCTNativeAnimatedModule.m delete mode 100644 Libraries/NativeAnimation/RCTViewPropertyMapper.h delete mode 100644 Libraries/NativeAnimation/RCTViewPropertyMapper.m diff --git a/Examples/UIExplorer/NativeAnimationsExample.js b/Examples/UIExplorer/NativeAnimationsExample.js deleted file mode 100644 index 67757f56eaf0d6..00000000000000 --- a/Examples/UIExplorer/NativeAnimationsExample.js +++ /dev/null @@ -1,308 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - * @flow - */ -'use strict'; - -var React = require('react'); -var ReactNative = require('react-native'); -var { - View, - Text, - Animated, - StyleSheet, - TouchableWithoutFeedback, -} = ReactNative; -var UIExplorerButton = require('./UIExplorerButton'); - -var Tester = React.createClass({ - current: 0, - getInitialState() { - return { - native: new Animated.Value(0), - js: new Animated.Value(0), - }; - }, - - onPress() { - this.current = this.current ? 0 : 1; - const config = { - ...this.props.config, - toValue: this.current, - }; - try { - Animated[this.props.type](this.state.native, { ...config, useNativeDriver: true }).start(); - } catch (e) { - // uncomment this if you want to get the redbox errors! - throw e; - } - Animated[this.props.type](this.state.js, { ...config, useNativeDriver: false }).start(); - }, - - render() { - return ( - - - - Native: - - - {this.props.children(this.state.native)} - - - JavaScript: - - - {this.props.children(this.state.js)} - - - - ); - }, -}); - -const styles = StyleSheet.create({ - row: { - padding: 10, - zIndex: 1, - }, - block: { - width: 50, - height: 50, - backgroundColor: 'blue', - }, -}); - -exports.framework = 'React'; -exports.title = 'Native Animated Example'; -exports.description = 'Test out Native Animations'; - -exports.examples = [ -{ - title: 'Multistage With Multiply and rotation', - description: 'description', - render: function() { - return ( - - {anim => ( - - )} - - ); - }, - }, - { - title: 'Multistage With Multiply', - description: 'description', - render: function() { - return ( - - {anim => ( - - )} - - ); - }, - }, - { - title: 'Scale interpolation', - description: 'description', - render: function() { - return ( - - {anim => ( - - )} - - ); - }, - }, - { - title: 'Opacity without interpolation', - description: 'description', - render: function() { - return ( - - {anim => ( - - )} - - ); - }, - }, - { - title: 'Rotate interpolation', - description: 'description', - render: function() { - return ( - - {anim => ( - - )} - - ); - }, - }, - { - title: 'translateX => Animated.spring', - description: 'description', - render: function() { - return ( - - {anim => ( - - )} - - ); - }, - }, -]; diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index f73a4214d02fc8..ba1a02b73a5a4d 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -32,7 +32,6 @@ 13BCE84F1C9C209600DD7AAD /* RCTComponentPropsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13BCE84E1C9C209600DD7AAD /* RCTComponentPropsTests.m */; }; 13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */; }; 13DF61B61B67A45000EDB188 /* RCTMethodArgumentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */; }; - 13E501F11D07A84A005F35D8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13E501A31D07A502005F35D8 /* libRCTAnimation.a */; }; 143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */; }; 144D21241B2204C5006DB32B /* RCTImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 144D21231B2204C5006DB32B /* RCTImageUtilTests.m */; }; 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; }; @@ -122,13 +121,6 @@ remoteGlobalIDString = 3C86DF461ADF2C930047B81A; remoteInfo = RCTWebSocket; }; - 13E501A21D07A502005F35D8 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 13E5019C1D07A502005F35D8 /* RCTAnimation.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 134814201AA4EA6300B7C361; - remoteInfo = RCTAnimation; - }; 143BC59B1B21E3E100462512 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; @@ -218,7 +210,6 @@ 13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../../Libraries/Settings/RCTSettings.xcodeproj; sourceTree = ""; }; 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSONTests.m; sourceTree = ""; }; 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMethodArgumentTests.m; sourceTree = ""; }; - 13E5019C1D07A502005F35D8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = ../../Libraries/NativeAnimation/RCTAnimation.xcodeproj; sourceTree = ""; }; 143BC57E1B21E18100462512 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 143BC5811B21E18100462512 /* testLayoutExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testLayoutExampleSnapshot_1@2x.png"; sourceTree = ""; }; 143BC5821B21E18100462512 /* testSliderExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testSliderExampleSnapshot_1@2x.png"; sourceTree = ""; }; @@ -294,7 +285,6 @@ 14AADF051AC3DBB1002390C9 /* libReact.a in Frameworks */, 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */, 134454601AAFCABD003F0779 /* libRCTAdSupport.a in Frameworks */, - 13E501F11D07A84A005F35D8 /* libRCTAnimation.a in Frameworks */, 138DEE241B9EDFB6007F4EA5 /* libRCTCameraRoll.a in Frameworks */, 134A8A2A1AACED7A00945AAE /* libRCTGeolocation.a in Frameworks */, 13417FE91AA91432003F314A /* libRCTImage.a in Frameworks */, @@ -325,7 +315,6 @@ 14AADEFF1AC3DB95002390C9 /* React.xcodeproj */, 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */, 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */, - 13E5019C1D07A502005F35D8 /* RCTAnimation.xcodeproj */, 138DEE021B9EDDDB007F4EA5 /* RCTCameraRoll.xcodeproj */, 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */, 13417FE31AA91428003F314A /* RCTImage.xcodeproj */, @@ -424,14 +413,6 @@ name = UIExplorer; sourceTree = ""; }; - 13E5019D1D07A502005F35D8 /* Products */ = { - isa = PBXGroup; - children = ( - 13E501A31D07A502005F35D8 /* libRCTAnimation.a */, - ); - name = Products; - sourceTree = ""; - }; 143BC57C1B21E18100462512 /* UIExplorerUnitTests */ = { isa = PBXGroup; children = ( @@ -712,10 +693,6 @@ ProductGroup = 134454561AAFCAAE003F0779 /* Products */; ProjectRef = 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */; }, - { - ProductGroup = 13E5019D1D07A502005F35D8 /* Products */; - ProjectRef = 13E5019C1D07A502005F35D8 /* RCTAnimation.xcodeproj */; - }, { ProductGroup = 138DEE031B9EDDDB007F4EA5 /* Products */; ProjectRef = 138DEE021B9EDDDB007F4EA5 /* RCTCameraRoll.xcodeproj */; @@ -824,13 +801,6 @@ remoteRef = 139FDED81B0651EA00C62182 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 13E501A31D07A502005F35D8 /* libRCTAnimation.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTAnimation.a; - remoteRef = 13E501A21D07A502005F35D8 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme index 547d76ac50989f..833aaaa4a65686 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme @@ -1,6 +1,6 @@ = [ key: 'ModalExample', module: require('./ModalExample'), }, - { - key: 'NativeAnimationsExample', - module: require('./NativeAnimationsExample'), - }, { key: 'NavigatorExample', module: require('./Navigator/NavigatorExample'), diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js index 9b767c649ce7b1..a9188050513529 100644 --- a/Libraries/Animated/src/AnimatedImplementation.js +++ b/Libraries/Animated/src/AnimatedImplementation.js @@ -243,7 +243,7 @@ class TimingAnimation extends Animation { this._duration = config.duration !== undefined ? config.duration : 500; this._delay = config.delay !== undefined ? config.delay : 0; this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true; - this._useNativeDriver = config.useNativeDriver !== undefined ? config.useNativeDriver : false; + this._useNativeDriver = !!config.useNativeDriver; } _getNativeAnimationConfig(): any { @@ -256,7 +256,6 @@ class TimingAnimation extends Animation { type: 'frames', frames, toValue: this._toValue, - delay: this._delay }; } @@ -1087,18 +1086,6 @@ class AnimatedTransform extends AnimatedWithChildren { this._transforms = transforms; } - __makeNative() { - super.__makeNative(); - this._transforms.forEach(transform => { - for (var key in transform) { - var value = transform[key]; - if (value instanceof Animated) { - value.__makeNative(); - } - } - }); - } - __getValue(): Array { return this._transforms.map(transform => { var result = {}; @@ -1151,25 +1138,6 @@ class AnimatedTransform extends AnimatedWithChildren { } }); } - - __getNativeConfig(): any { - var transConfig = {}; - - this._transforms.forEach(transform => { - for (var key in transform) { - var value = transform[key]; - if (value instanceof Animated) { - transConfig[key] = value.__getNativeTag(); - } - } - }); - - NativeAnimatedHelper.validateTransform(transConfig); - return { - type: 'transform', - transform: transConfig, - }; - } } class AnimatedStyle extends AnimatedWithChildren { diff --git a/Libraries/Animated/src/NativeAnimatedHelper.js b/Libraries/Animated/src/NativeAnimatedHelper.js index befbad13148217..2e5cc68b1737b9 100644 --- a/Libraries/Animated/src/NativeAnimatedHelper.js +++ b/Libraries/Animated/src/NativeAnimatedHelper.js @@ -74,22 +74,16 @@ var API = { var PROPS_WHITELIST = { style: { opacity: true, - transform: true, + /* legacy android transform properties */ scaleX: true, scaleY: true, + rotation: true, translateX: true, translateY: true, }, }; -var TRANSFORM_WHITELIST = { - translateX: true, - translateY: true, - scale: true, - rotate: true, -}; - function validateProps(params: Object): void { for (var key in params) { if (!PROPS_WHITELIST.hasOwnProperty(key)) { @@ -98,14 +92,6 @@ function validateProps(params: Object): void { } } -function validateTransform(config: Object): void { - for (var key in config) { - if (!TRANSFORM_WHITELIST.hasOwnProperty(key)) { - throw new Error(`Property '${key}' is not supported by native animated module`); - } - } -} - function validateStyles(styles: Object): void { var STYLES_WHITELIST = PROPS_WHITELIST.style || {}; for (var key in styles) { @@ -143,7 +129,6 @@ module.exports = { API, validateProps, validateStyles, - validateTransform, validateInterpolation, generateNewNodeTag, generateNewAnimationId, diff --git a/Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.h deleted file mode 100644 index 83641afead8442..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.h +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTValueAnimatedNode.h" - -@interface RCTAdditionAnimatedNode : RCTValueAnimatedNode - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.m deleted file mode 100644 index 4007809552e131..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTAdditionAnimatedNode.m +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTAdditionAnimatedNode.h" - -@implementation RCTAdditionAnimatedNode - -- (void)performUpdate -{ - [super performUpdate]; - NSArray *inputNodes = self.config[@"input"]; - if (inputNodes.count > 1) { - RCTValueAnimatedNode *parent1 = (RCTValueAnimatedNode *)self.parentNodes[inputNodes[0]]; - RCTValueAnimatedNode *parent2 = (RCTValueAnimatedNode *)self.parentNodes[inputNodes[1]]; - if (parent1 && parent2) { - self.value = parent1.value + parent2.value; - } - } -} - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h deleted file mode 100644 index 8e06e90629fdd6..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import - -@interface RCTAnimatedNode : NSObject - -- (instancetype)initWithTag:(NSNumber *)tag - config:(NSDictionary *)config NS_DESIGNATED_INITIALIZER; - -@property (nonatomic, readonly) NSNumber *nodeTag; -@property (nonatomic, copy, readonly) NSDictionary *config; - -@property (nonatomic, copy, readonly) NSDictionary *childNodes; -@property (nonatomic, copy, readonly) NSDictionary *parentNodes; - -@property (nonatomic, readonly) BOOL needsUpdate; -@property (nonatomic, readonly) BOOL hasUpdated; - -/** - * Marks a node and its children as needing update. - */ -- (void)setNeedsUpdate NS_REQUIRES_SUPER; - -/** - * The node will update its value if necesarry and only after its parents have updated. - */ -- (void)updateNodeIfNecessary NS_REQUIRES_SUPER; - -/** - * Where the actual update code lives. Called internally from updateNodeIfNecessary - */ -- (void)performUpdate NS_REQUIRES_SUPER; - -/** - * Cleans up after a round of updates. - */ -- (void)cleanupAnimationUpdate NS_REQUIRES_SUPER; - -- (void)addChild:(RCTAnimatedNode *)child NS_REQUIRES_SUPER; -- (void)removeChild:(RCTAnimatedNode *)child NS_REQUIRES_SUPER; - -- (void)onAttachedToNode:(RCTAnimatedNode *)parent NS_REQUIRES_SUPER; -- (void)onDetachedFromNode:(RCTAnimatedNode *)parent NS_REQUIRES_SUPER; - -- (void)detachNode NS_REQUIRES_SUPER; - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m deleted file mode 100644 index 9f5075b6222140..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTAnimatedNode.h" - -#import "RCTDefines.h" - -@implementation RCTAnimatedNode -{ - NSMutableDictionary *_childNodes; - NSMutableDictionary *_parentNodes; -} - -- (instancetype)initWithTag:(NSNumber *)tag - config:(NSDictionary *)config -{ - if ((self = [super init])) { - _nodeTag = tag; - _config = [config copy]; - } - return self; -} - -RCT_NOT_IMPLEMENTED(- (instancetype)init) - -- (NSDictionary *)childNodes -{ - return _childNodes; -} - -- (NSDictionary *)parentNodes -{ - return _parentNodes; -} - -- (void)addChild:(RCTAnimatedNode *)child -{ - if (!_childNodes) { - _childNodes = [NSMutableDictionary new]; - } - if (child) { - _childNodes[child.nodeTag] = child; - [child onAttachedToNode:self]; - } -} - -- (void)removeChild:(RCTAnimatedNode *)child -{ - if (!_childNodes) { - return; - } - if (child) { - [_childNodes removeObjectForKey:child.nodeTag]; - [child onDetachedFromNode:self]; - } -} - -- (void)onAttachedToNode:(RCTAnimatedNode *)parent -{ - if (!_parentNodes) { - _parentNodes = [NSMutableDictionary new]; - } - if (parent) { - _parentNodes[parent.nodeTag] = parent; - } -} - -- (void)onDetachedFromNode:(RCTAnimatedNode *)parent -{ - if (!_parentNodes) { - return; - } - if (parent) { - [_parentNodes removeObjectForKey:parent.nodeTag]; - } -} - -- (void)detachNode -{ - for (RCTAnimatedNode *parent in _parentNodes.allValues) { - [parent removeChild:self]; - } - for (RCTAnimatedNode *child in _childNodes.allValues) { - [self removeChild:child]; - } -} - -- (void)setNeedsUpdate -{ - if (_needsUpdate) { - // Has already been marked. Stop branch. - return; - } - _needsUpdate = YES; - for (RCTAnimatedNode *child in _childNodes.allValues) { - [child setNeedsUpdate]; - } -} - -- (void)cleanupAnimationUpdate -{ - if (_hasUpdated) { - _needsUpdate = NO; - _hasUpdated = NO; - for (RCTAnimatedNode *child in _childNodes.allValues) { - [child cleanupAnimationUpdate]; - } - } -} - -- (void)updateNodeIfNecessary -{ - if (_needsUpdate && !_hasUpdated) { - for (RCTAnimatedNode *parent in _parentNodes.allValues) { - [parent updateNodeIfNecessary]; - } - [self performUpdate]; - } -} - -- (void)performUpdate -{ - _hasUpdated = YES; - // To be overidden by subclasses - // This method is called on a node only if it has been marked for update - // during the current update loop -} - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.h b/Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.h deleted file mode 100644 index 5f39ea4656f8f6..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.h +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import -#import "RCTBridgeModule.h" - -@class RCTValueAnimatedNode; - -NS_ASSUME_NONNULL_BEGIN - -@interface RCTAnimationDriverNode : NSObject - -@property (nonatomic, readonly) NSNumber *animationId; -@property (nonatomic, readonly) NSNumber *outputValue; - -@property (nonatomic, readonly) BOOL animationHasBegun; -@property (nonatomic, readonly) BOOL animationHasFinished; - -- (instancetype)initWithId:(NSNumber *)animationId - delay:(NSTimeInterval)delay - toValue:(CGFloat)toValue - frames:(NSArray *)frames - forNode:(RCTValueAnimatedNode *)valueNode - callBack:(nullable RCTResponseSenderBlock)callback NS_DESIGNATED_INITIALIZER; - -- (void)startAnimation; -- (void)stopAnimation; -- (void)stepAnimation; -- (void)removeAnimation; -- (void)cleanupAnimationUpdate; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.m b/Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.m deleted file mode 100644 index 7da52da9c18921..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTAnimationDriverNode.m +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTAnimationDriverNode.h" - -#import - -#import "RCTAnimationUtils.h" -#import "RCTDefines.h" -#import "RCTValueAnimatedNode.h" - -const double SINGLE_FRAME_INTERVAL = 1.0 / 60.0; - -@implementation RCTAnimationDriverNode -{ - NSArray *_frames; - CGFloat _toValue; - CGFloat _fromValue; - NSTimeInterval _delay; - NSTimeInterval _animationStartTime; - NSTimeInterval _animationCurrentTime; - RCTValueAnimatedNode *_valueNode; - RCTResponseSenderBlock _callback; -} - -- (instancetype)initWithId:(nonnull NSNumber *)animationId - delay:(NSTimeInterval)delay - toValue:(CGFloat)toValue - frames:(nonnull NSArray *)frames - forNode:(nonnull RCTValueAnimatedNode *)valueNode - callBack:(nullable RCTResponseSenderBlock)callback -{ - if ((self = [super init])) { - _animationId = animationId; - _toValue = toValue; - _fromValue = valueNode.value; - _valueNode = valueNode; - _delay = delay; - _frames = [frames copy]; - _outputValue = @0; - _callback = [callback copy]; - } - return self; -} - -RCT_NOT_IMPLEMENTED(- (instancetype)init) - -- (void)startAnimation -{ - _animationStartTime = CACurrentMediaTime(); - _animationCurrentTime = _animationStartTime; - _animationHasBegun = YES; -} - -- (void)stopAnimation -{ - _animationHasFinished = YES; -} - -- (void)removeAnimation -{ - [self stopAnimation]; - _valueNode = nil; - if (_callback) { - _callback(@[(id)kCFNull]); - } -} - -- (void)stepAnimation -{ - if (!_animationHasBegun || - _animationHasFinished || - _frames.count == 0) { - // Animation has not begun or animation has already finished. - return; - } - - NSTimeInterval currentTime = CACurrentMediaTime(); - NSTimeInterval stepInterval = currentTime - _animationCurrentTime; - _animationCurrentTime = currentTime; - NSTimeInterval currentDuration = _animationCurrentTime - _animationStartTime; - - if (_delay > 0) { - // Decrement delay - _delay -= stepInterval; - return; - } - - // Determine how many frames have passed since last update. - // Get index of frames that surround the current interval - NSUInteger startIndex = floor(currentDuration / SINGLE_FRAME_INTERVAL); - NSUInteger nextIndex = startIndex + 1; - - if (nextIndex >= _frames.count) { - // We are at the end of the animation - // Update value and flag animation has ended. - NSNumber *finalValue = _frames.lastObject; - [self updateOutputWithFrameOutput:finalValue.doubleValue]; - [self stopAnimation]; - return; - } - - // Do a linear remap of the two frames to safegaurd against variable framerates - NSNumber *fromFrameValue = _frames[startIndex]; - NSNumber *toFrameValue = _frames[nextIndex]; - NSTimeInterval fromInterval = startIndex * SINGLE_FRAME_INTERVAL; - NSTimeInterval toInterval = nextIndex * SINGLE_FRAME_INTERVAL; - - // Interpolate between the individual frames to ensure the animations are - //smooth and of the proper duration regardless of the framerate. - CGFloat frameOutput = RCTInterpolateValue(currentDuration, - fromInterval, - toInterval, - fromFrameValue.doubleValue, - toFrameValue.doubleValue); - [self updateOutputWithFrameOutput:frameOutput]; -} - -- (void)updateOutputWithFrameOutput:(CGFloat)frameOutput -{ - CGFloat outputValue = RCTInterpolateValue(frameOutput, 0, 1, _fromValue, _toValue); - _outputValue = @(outputValue); - _valueNode.value = outputValue; - [_valueNode setNeedsUpdate]; -} - -- (void)cleanupAnimationUpdate -{ - [_valueNode cleanupAnimationUpdate]; -} - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.h deleted file mode 100644 index 2257e49646dc20..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.h +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTValueAnimatedNode.h" - -@interface RCTInterpolationAnimatedNode : RCTValueAnimatedNode - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m deleted file mode 100644 index 4c160ec24e7ef0..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTInterpolationAnimatedNode.m +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTInterpolationAnimatedNode.h" -#import "RCTAnimationUtils.h" - -@implementation RCTInterpolationAnimatedNode -{ - __weak RCTValueAnimatedNode *_parentNode; - NSArray *_inputRange; - NSArray *_outputRange; -} - -- (instancetype)initWithTag:(NSNumber *)tag - config:(NSDictionary *)config -{ - if ((self = [super initWithTag:tag config:config])) { - _inputRange = [config[@"inputRange"] copy]; - NSMutableArray *outputRange = [NSMutableArray array]; - for (id value in config[@"outputRange"]) { - if ([value isKindOfClass:[NSNumber class]]) { - [outputRange addObject:value]; - } else if ([value isKindOfClass:[NSString class]]) { - NSString *str = (NSString *)value; - if ([str hasSuffix:@"deg"]) { - double degrees = str.doubleValue; - [outputRange addObject:@(RCTDegreesToRadians(degrees))]; - } else { - // Assume radians - [outputRange addObject:@(str.doubleValue)]; - } - } - } - _outputRange = [outputRange copy]; - } - return self; -} - -- (void)onAttachedToNode:(RCTAnimatedNode *)parent -{ - [super onAttachedToNode:parent]; - if ([parent isKindOfClass:[RCTValueAnimatedNode class]]) { - _parentNode = (RCTValueAnimatedNode *)parent; - } -} - -- (void)onDetachedFromNode:(RCTAnimatedNode *)parent -{ - [super onDetachedFromNode:parent]; - if (_parentNode == parent) { - _parentNode = nil; - } -} - -- (NSUInteger)findIndexOfNearestValue:(CGFloat)value - inRange:(NSArray *)range -{ - NSUInteger index; - NSUInteger rangeCount = range.count; - for (index = 1; index < rangeCount - 1; index++) { - NSNumber *inputValue = range[index]; - if (inputValue.doubleValue >= value) { - break; - } - } - return index - 1; -} - -- (void)performUpdate -{ - [super performUpdate]; - if (!_parentNode) { - return; - } - - NSUInteger rangeIndex = [self findIndexOfNearestValue:_parentNode.value - inRange:_inputRange]; - NSNumber *inputMin = _inputRange[rangeIndex]; - NSNumber *inputMax = _inputRange[rangeIndex + 1]; - NSNumber *outputMin = _outputRange[rangeIndex]; - NSNumber *outputMax = _outputRange[rangeIndex + 1]; - - CGFloat outputValue = RCTInterpolateValue(_parentNode.value, - inputMin.doubleValue, - inputMax.doubleValue, - outputMin.doubleValue, - outputMax.doubleValue); - self.value = outputValue; -} - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.h deleted file mode 100644 index 43a1ff9b957ca9..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.h +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTValueAnimatedNode.h" - -@interface RCTMultiplicationAnimatedNode : RCTValueAnimatedNode - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.m deleted file mode 100644 index 5a8117bdb24d91..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTMultiplicationAnimatedNode.m +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTMultiplicationAnimatedNode.h" - -@implementation RCTMultiplicationAnimatedNode - -- (void)performUpdate -{ - [super performUpdate]; - - NSArray *inputNodes = self.config[@"input"]; - if (inputNodes.count > 1) { - RCTValueAnimatedNode *parent1 = (RCTValueAnimatedNode *)self.parentNodes[inputNodes[0]]; - RCTValueAnimatedNode *parent2 = (RCTValueAnimatedNode *)self.parentNodes[inputNodes[1]]; - if (!parent1 || !parent2) { - return; - } - self.value = parent1.value * parent2.value; - } -} - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.h deleted file mode 100644 index 08debc6c794b74..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.h +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTAnimatedNode.h" - -@class RCTNativeAnimatedModule; -@class RCTViewPropertyMapper; - -@interface RCTPropsAnimatedNode : RCTAnimatedNode - -@property (nonatomic, readonly) RCTViewPropertyMapper *propertyMapper; - -- (void)connectToView:(NSNumber *)viewTag animatedModule:(RCTNativeAnimatedModule *)animationModule; -- (void)disconnectFromView:(NSNumber *)viewTag; - -- (void)performViewUpdatesIfNecessary; - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m deleted file mode 100644 index fcd5c34d6643b4..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTPropsAnimatedNode.h" -#import "RCTAnimationUtils.h" -#import "RCTNativeAnimatedModule.h" -#import "RCTStyleAnimatedNode.h" -#import "RCTViewPropertyMapper.h" - -@implementation RCTPropsAnimatedNode -{ - RCTStyleAnimatedNode *_parentNode; -} - -- (void)onAttachedToNode:(RCTAnimatedNode *)parent -{ - [super onAttachedToNode:parent]; - if ([parent isKindOfClass:[RCTStyleAnimatedNode class]]) { - _parentNode = (RCTStyleAnimatedNode *)parent; - } -} - -- (void)onDetachedFromNode:(RCTAnimatedNode *)parent -{ - [super onDetachedFromNode:parent]; - if (_parentNode == parent) { - _parentNode = nil; - } -} - -- (void)connectToView:(NSNumber *)viewTag animatedModule:(RCTNativeAnimatedModule *)animationModule -{ - _propertyMapper = [[RCTViewPropertyMapper alloc] initWithViewTag:viewTag animationModule:animationModule]; -} - -- (void)disconnectFromView:(NSNumber *)viewTag -{ - _propertyMapper = nil; -} - -- (void)performUpdate -{ - [super performUpdate]; - [self performViewUpdatesIfNecessary]; -} - -- (void)performViewUpdatesIfNecessary -{ - NSDictionary *updates = [_parentNode updatedPropsDictionary]; - if (updates.count) { - [_propertyMapper updateViewWithDictionary:updates]; - } -} - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.h deleted file mode 100644 index f3a6cd5dfe44b6..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTAnimatedNode.h" - -@interface RCTStyleAnimatedNode : RCTAnimatedNode - -- (NSDictionary *)updatedPropsDictionary; - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.m deleted file mode 100644 index 1910a133314bcc..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTStyleAnimatedNode.m +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTStyleAnimatedNode.h" -#import "RCTAnimationUtils.h" -#import "RCTValueAnimatedNode.h" -#import "RCTTransformAnimatedNode.h" - -@implementation RCTStyleAnimatedNode -{ - NSMutableDictionary *_updatedPropsDictionary; -} - -- (instancetype)initWithTag:(NSNumber *)tag - config:(NSDictionary *)config; -{ - if ((self = [super initWithTag:tag config:config])) { - _updatedPropsDictionary = [NSMutableDictionary new]; - } - return self; -} - -- (NSDictionary *)updatedPropsDictionary -{ - return _updatedPropsDictionary; -} - -- (void)performUpdate -{ - [super performUpdate]; - - NSDictionary *style = self.config[@"style"]; - [style enumerateKeysAndObjectsUsingBlock:^(NSString *property, NSNumber *nodeTag, __unused BOOL *stop) { - RCTAnimatedNode *node = self.parentNodes[nodeTag]; - if (node && node.hasUpdated) { - if ([node isKindOfClass:[RCTValueAnimatedNode class]]) { - RCTValueAnimatedNode *parentNode = (RCTValueAnimatedNode *)node; - [_updatedPropsDictionary setObject:@(parentNode.value) forKey:property]; - } else if ([node isKindOfClass:[RCTTransformAnimatedNode class]]) { - RCTTransformAnimatedNode *parentNode = (RCTTransformAnimatedNode *)node; - if (parentNode.updatedPropsDictionary.count) { - [_updatedPropsDictionary addEntriesFromDictionary:parentNode.updatedPropsDictionary]; - } - } - } - }]; -} - -- (void)cleanupAnimationUpdate -{ - [super cleanupAnimationUpdate]; - [_updatedPropsDictionary removeAllObjects]; -} - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.h deleted file mode 100644 index 6d1cfc840af0f4..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTAnimatedNode.h" - -@interface RCTTransformAnimatedNode : RCTAnimatedNode - -- (NSDictionary *)updatedPropsDictionary; - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.m deleted file mode 100644 index c6a356ff5ed8a2..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTTransformAnimatedNode.m +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTTransformAnimatedNode.h" -#import "RCTValueAnimatedNode.h" - -@implementation RCTTransformAnimatedNode -{ - NSMutableDictionary *_updatedPropsDictionary; -} - -- (instancetype)initWithTag:(NSNumber *)tag - config:(NSDictionary *)config; -{ - if ((self = [super initWithTag:tag config:config])) { - _updatedPropsDictionary = [NSMutableDictionary new]; - } - return self; -} - -- (NSDictionary *)updatedPropsDictionary -{ - return _updatedPropsDictionary; -} - -- (void)performUpdate -{ - [super performUpdate]; - - NSDictionary *transforms = self.config[@"transform"]; - [transforms enumerateKeysAndObjectsUsingBlock:^(NSString *property, NSNumber *nodeTag, __unused BOOL *stop) { - RCTAnimatedNode *node = self.parentNodes[nodeTag]; - if (node.hasUpdated && [node isKindOfClass:[RCTValueAnimatedNode class]]) { - RCTValueAnimatedNode *parentNode = (RCTValueAnimatedNode *)node; - _updatedPropsDictionary[property] = @(parentNode.value); - } - }]; -} - -- (void)cleanupAnimationUpdate -{ - [super cleanupAnimationUpdate]; - [_updatedPropsDictionary removeAllObjects]; -} - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h deleted file mode 100644 index af183243534cdd..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.h +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTAnimatedNode.h" -#import - -@interface RCTValueAnimatedNode : RCTAnimatedNode - -@property (nonatomic, assign) CGFloat value; - -@end diff --git a/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.m deleted file mode 100644 index cffa66681d7b17..00000000000000 --- a/Libraries/NativeAnimation/Nodes/RCTValueAnimatedNode.m +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTValueAnimatedNode.h" - -@implementation RCTValueAnimatedNode - -@end diff --git a/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj b/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj deleted file mode 100644 index 3f0c751b22370a..00000000000000 --- a/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj +++ /dev/null @@ -1,337 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 13E501CC1D07A644005F35D8 /* RCTAnimationUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501B81D07A644005F35D8 /* RCTAnimationUtils.m */; }; - 13E501CF1D07A644005F35D8 /* RCTNativeAnimatedModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501BE1D07A644005F35D8 /* RCTNativeAnimatedModule.m */; }; - 13E501D41D07A644005F35D8 /* RCTViewPropertyMapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501C81D07A644005F35D8 /* RCTViewPropertyMapper.m */; }; - 13E501E81D07A6C9005F35D8 /* RCTAdditionAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501D71D07A6C9005F35D8 /* RCTAdditionAnimatedNode.m */; }; - 13E501E91D07A6C9005F35D8 /* RCTAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501D91D07A6C9005F35D8 /* RCTAnimatedNode.m */; }; - 13E501EA1D07A6C9005F35D8 /* RCTAnimationDriverNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501DB1D07A6C9005F35D8 /* RCTAnimationDriverNode.m */; }; - 13E501EB1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501DD1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.m */; }; - 13E501EC1D07A6C9005F35D8 /* RCTMultiplicationAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501DF1D07A6C9005F35D8 /* RCTMultiplicationAnimatedNode.m */; }; - 13E501ED1D07A6C9005F35D8 /* RCTPropsAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E11D07A6C9005F35D8 /* RCTPropsAnimatedNode.m */; }; - 13E501EE1D07A6C9005F35D8 /* RCTStyleAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E31D07A6C9005F35D8 /* RCTStyleAnimatedNode.m */; }; - 13E501EF1D07A6C9005F35D8 /* RCTTransformAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E51D07A6C9005F35D8 /* RCTTransformAnimatedNode.m */; }; - 13E501F01D07A6C9005F35D8 /* RCTValueAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E71D07A6C9005F35D8 /* RCTValueAnimatedNode.m */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 58B511D91A9E6C8500147676 /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "include/$(PRODUCT_NAME)"; - dstSubfolderSpec = 16; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 134814201AA4EA6300B7C361 /* libRCTAnimation.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTAnimation.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 13E501B71D07A644005F35D8 /* RCTAnimationUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationUtils.h; sourceTree = ""; }; - 13E501B81D07A644005F35D8 /* RCTAnimationUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimationUtils.m; sourceTree = ""; }; - 13E501BD1D07A644005F35D8 /* RCTNativeAnimatedModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNativeAnimatedModule.h; sourceTree = ""; }; - 13E501BE1D07A644005F35D8 /* RCTNativeAnimatedModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNativeAnimatedModule.m; sourceTree = ""; }; - 13E501C71D07A644005F35D8 /* RCTViewPropertyMapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTViewPropertyMapper.h; sourceTree = ""; }; - 13E501C81D07A644005F35D8 /* RCTViewPropertyMapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTViewPropertyMapper.m; sourceTree = ""; }; - 13E501D61D07A6C9005F35D8 /* RCTAdditionAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAdditionAnimatedNode.h; sourceTree = ""; }; - 13E501D71D07A6C9005F35D8 /* RCTAdditionAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAdditionAnimatedNode.m; sourceTree = ""; }; - 13E501D81D07A6C9005F35D8 /* RCTAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimatedNode.h; sourceTree = ""; }; - 13E501D91D07A6C9005F35D8 /* RCTAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimatedNode.m; sourceTree = ""; }; - 13E501DA1D07A6C9005F35D8 /* RCTAnimationDriverNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationDriverNode.h; sourceTree = ""; }; - 13E501DB1D07A6C9005F35D8 /* RCTAnimationDriverNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimationDriverNode.m; sourceTree = ""; }; - 13E501DC1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTInterpolationAnimatedNode.h; sourceTree = ""; }; - 13E501DD1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTInterpolationAnimatedNode.m; sourceTree = ""; }; - 13E501DE1D07A6C9005F35D8 /* RCTMultiplicationAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMultiplicationAnimatedNode.h; sourceTree = ""; }; - 13E501DF1D07A6C9005F35D8 /* RCTMultiplicationAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMultiplicationAnimatedNode.m; sourceTree = ""; }; - 13E501E01D07A6C9005F35D8 /* RCTPropsAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPropsAnimatedNode.h; sourceTree = ""; }; - 13E501E11D07A6C9005F35D8 /* RCTPropsAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPropsAnimatedNode.m; sourceTree = ""; }; - 13E501E21D07A6C9005F35D8 /* RCTStyleAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStyleAnimatedNode.h; sourceTree = ""; }; - 13E501E31D07A6C9005F35D8 /* RCTStyleAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStyleAnimatedNode.m; sourceTree = ""; }; - 13E501E41D07A6C9005F35D8 /* RCTTransformAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTransformAnimatedNode.h; sourceTree = ""; }; - 13E501E51D07A6C9005F35D8 /* RCTTransformAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTransformAnimatedNode.m; sourceTree = ""; }; - 13E501E61D07A6C9005F35D8 /* RCTValueAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTValueAnimatedNode.h; sourceTree = ""; }; - 13E501E71D07A6C9005F35D8 /* RCTValueAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTValueAnimatedNode.m; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 58B511D81A9E6C8500147676 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 134814211AA4EA7D00B7C361 /* Products */ = { - isa = PBXGroup; - children = ( - 134814201AA4EA6300B7C361 /* libRCTAnimation.a */, - ); - name = Products; - sourceTree = ""; - }; - 13E501D51D07A6C9005F35D8 /* Nodes */ = { - isa = PBXGroup; - children = ( - 13E501D61D07A6C9005F35D8 /* RCTAdditionAnimatedNode.h */, - 13E501D71D07A6C9005F35D8 /* RCTAdditionAnimatedNode.m */, - 13E501D81D07A6C9005F35D8 /* RCTAnimatedNode.h */, - 13E501D91D07A6C9005F35D8 /* RCTAnimatedNode.m */, - 13E501DA1D07A6C9005F35D8 /* RCTAnimationDriverNode.h */, - 13E501DB1D07A6C9005F35D8 /* RCTAnimationDriverNode.m */, - 13E501DC1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.h */, - 13E501DD1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.m */, - 13E501DE1D07A6C9005F35D8 /* RCTMultiplicationAnimatedNode.h */, - 13E501DF1D07A6C9005F35D8 /* RCTMultiplicationAnimatedNode.m */, - 13E501E01D07A6C9005F35D8 /* RCTPropsAnimatedNode.h */, - 13E501E11D07A6C9005F35D8 /* RCTPropsAnimatedNode.m */, - 13E501E21D07A6C9005F35D8 /* RCTStyleAnimatedNode.h */, - 13E501E31D07A6C9005F35D8 /* RCTStyleAnimatedNode.m */, - 13E501E41D07A6C9005F35D8 /* RCTTransformAnimatedNode.h */, - 13E501E51D07A6C9005F35D8 /* RCTTransformAnimatedNode.m */, - 13E501E61D07A6C9005F35D8 /* RCTValueAnimatedNode.h */, - 13E501E71D07A6C9005F35D8 /* RCTValueAnimatedNode.m */, - ); - path = Nodes; - sourceTree = ""; - }; - 58B511D21A9E6C8500147676 = { - isa = PBXGroup; - children = ( - 13E501B71D07A644005F35D8 /* RCTAnimationUtils.h */, - 13E501B81D07A644005F35D8 /* RCTAnimationUtils.m */, - 13E501C71D07A644005F35D8 /* RCTViewPropertyMapper.h */, - 13E501C81D07A644005F35D8 /* RCTViewPropertyMapper.m */, - 13E501BD1D07A644005F35D8 /* RCTNativeAnimatedModule.h */, - 13E501BE1D07A644005F35D8 /* RCTNativeAnimatedModule.m */, - 13E501D51D07A6C9005F35D8 /* Nodes */, - 134814211AA4EA7D00B7C361 /* Products */, - ); - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 58B511DA1A9E6C8500147676 /* RCTAnimation */ = { - isa = PBXNativeTarget; - buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTAnimation" */; - buildPhases = ( - 58B511D71A9E6C8500147676 /* Sources */, - 58B511D81A9E6C8500147676 /* Frameworks */, - 58B511D91A9E6C8500147676 /* CopyFiles */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = RCTAnimation; - productName = RCTDataManager; - productReference = 134814201AA4EA6300B7C361 /* libRCTAnimation.a */; - productType = "com.apple.product-type.library.static"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 58B511D31A9E6C8500147676 /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 0730; - ORGANIZATIONNAME = Facebook; - TargetAttributes = { - 58B511DA1A9E6C8500147676 = { - CreatedOnToolsVersion = 6.1.1; - }; - }; - }; - buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTAnimation" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; - hasScannedForEncodings = 0; - knownRegions = ( - en, - ); - mainGroup = 58B511D21A9E6C8500147676; - productRefGroup = 58B511D21A9E6C8500147676; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 58B511DA1A9E6C8500147676 /* RCTAnimation */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXSourcesBuildPhase section */ - 58B511D71A9E6C8500147676 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 13E501F01D07A6C9005F35D8 /* RCTValueAnimatedNode.m in Sources */, - 13E501EE1D07A6C9005F35D8 /* RCTStyleAnimatedNode.m in Sources */, - 13E501CC1D07A644005F35D8 /* RCTAnimationUtils.m in Sources */, - 13E501CF1D07A644005F35D8 /* RCTNativeAnimatedModule.m in Sources */, - 13E501EC1D07A6C9005F35D8 /* RCTMultiplicationAnimatedNode.m in Sources */, - 13E501ED1D07A6C9005F35D8 /* RCTPropsAnimatedNode.m in Sources */, - 13E501E91D07A6C9005F35D8 /* RCTAnimatedNode.m in Sources */, - 13E501EB1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.m in Sources */, - 13E501E81D07A6C9005F35D8 /* RCTAdditionAnimatedNode.m in Sources */, - 13E501EA1D07A6C9005F35D8 /* RCTAnimationDriverNode.m in Sources */, - 13E501EF1D07A6C9005F35D8 /* RCTTransformAnimatedNode.m in Sources */, - 13E501D41D07A644005F35D8 /* RCTViewPropertyMapper.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 58B511ED1A9E6C8500147676 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_SYMBOLS_PRIVATE_EXTERN = NO; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; - GCC_WARN_SHADOW = YES; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - WARNING_CFLAGS = ( - "-Werror", - "-Wall", - ); - }; - name = Debug; - }; - 58B511EE1A9E6C8500147676 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = YES; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; - GCC_WARN_SHADOW = YES; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - VALIDATE_PRODUCT = YES; - WARNING_CFLAGS = ( - "-Werror", - "-Wall", - ); - }; - name = Release; - }; - 58B511F01A9E6C8500147676 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - HEADER_SEARCH_PATHS = ( - "$(inherited)", - /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, - "$(SRCROOT)/../../React/**", - ); - LIBRARY_SEARCH_PATHS = "$(inherited)"; - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = RCTAnimation; - SKIP_INSTALL = YES; - }; - name = Debug; - }; - 58B511F11A9E6C8500147676 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - HEADER_SEARCH_PATHS = ( - "$(inherited)", - /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, - "$(SRCROOT)/../../React/**", - ); - LIBRARY_SEARCH_PATHS = "$(inherited)"; - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = RCTAnimation; - SKIP_INSTALL = YES; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTAnimation" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 58B511ED1A9E6C8500147676 /* Debug */, - 58B511EE1A9E6C8500147676 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTAnimation" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 58B511F01A9E6C8500147676 /* Debug */, - 58B511F11A9E6C8500147676 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 58B511D31A9E6C8500147676 /* Project object */; -} diff --git a/Libraries/NativeAnimation/RCTAnimationUtils.h b/Libraries/NativeAnimation/RCTAnimationUtils.h deleted file mode 100644 index f34a9c6ee8f8a9..00000000000000 --- a/Libraries/NativeAnimation/RCTAnimationUtils.h +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import -#import - -#import "RCTDefines.h" - -RCT_EXTERN CGFloat RCTInterpolateValue(CGFloat value, - CGFloat fromMin, - CGFloat fromMax, - CGFloat toMin, - CGFloat toMax); - -RCT_EXTERN CGFloat RCTRadiansToDegrees(CGFloat radians); -RCT_EXTERN CGFloat RCTDegreesToRadians(CGFloat degrees); diff --git a/Libraries/NativeAnimation/RCTAnimationUtils.m b/Libraries/NativeAnimation/RCTAnimationUtils.m deleted file mode 100644 index 720edddbd47396..00000000000000 --- a/Libraries/NativeAnimation/RCTAnimationUtils.m +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTAnimationUtils.h" - -/** - * Interpolates value by remapping it linearly fromMin->fromMax to toMin->toMax - */ -CGFloat RCTInterpolateValue(CGFloat value, - CGFloat fromMin, - CGFloat fromMax, - CGFloat toMin, - CGFloat toMax) -{ - return toMin + (value - fromMin) * (toMax - toMin) / (fromMax - fromMin); -} - -CGFloat RCTRadiansToDegrees(CGFloat radians) -{ - return radians * 180.0 / M_PI; -} - -CGFloat RCTDegreesToRadians(CGFloat degrees) -{ - return degrees / 180.0 * M_PI; -} diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedModule.h b/Libraries/NativeAnimation/RCTNativeAnimatedModule.h deleted file mode 100644 index d099ac53bfacf9..00000000000000 --- a/Libraries/NativeAnimation/RCTNativeAnimatedModule.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -#import "RCTBridgeModule.h" - -@interface RCTNativeAnimatedModule : NSObject - -@end diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedModule.m b/Libraries/NativeAnimation/RCTNativeAnimatedModule.m deleted file mode 100644 index 74e157fc17fb6b..00000000000000 --- a/Libraries/NativeAnimation/RCTNativeAnimatedModule.m +++ /dev/null @@ -1,247 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -#import "RCTNativeAnimatedModule.h" - -#import "RCTAdditionAnimatedNode.h" -#import "RCTAnimationDriverNode.h" -#import "RCTAnimationUtils.h" -#import "RCTBridge.h" -#import "RCTConvert.h" -#import "RCTInterpolationAnimatedNode.h" -#import "RCTLog.h" -#import "RCTMultiplicationAnimatedNode.h" -#import "RCTPropsAnimatedNode.h" -#import "RCTStyleAnimatedNode.h" -#import "RCTTransformAnimatedNode.h" -#import "RCTValueAnimatedNode.h" - -@implementation RCTNativeAnimatedModule -{ - NSMutableDictionary *_animationNodes; - NSMutableDictionary *_animationDrivers; - NSMutableSet *_activeAnimations; - NSMutableSet *_finishedAnimations; - NSMutableSet *_updatedValueNodes; - NSMutableSet *_propAnimationNodes; - CADisplayLink *_displayLink; -} - -@synthesize bridge = _bridge; - -RCT_EXPORT_MODULE() - -- (void)setBridge:(RCTBridge *)bridge -{ - _bridge = bridge; - _animationNodes = [NSMutableDictionary new]; - _animationDrivers = [NSMutableDictionary new]; - _activeAnimations = [NSMutableSet new]; - _finishedAnimations = [NSMutableSet new]; - _updatedValueNodes = [NSMutableSet new]; - _propAnimationNodes = [NSMutableSet new]; -} - -RCT_EXPORT_METHOD(createAnimatedNode:(nonnull NSNumber *)tag - config:(NSDictionary *)config) -{ - static NSDictionary *map; - static dispatch_once_t mapToken; - dispatch_once(&mapToken, ^{ - map = @{@"style" : [RCTStyleAnimatedNode class], - @"value" : [RCTValueAnimatedNode class], - @"props" : [RCTPropsAnimatedNode class], - @"interpolation" : [RCTInterpolationAnimatedNode class], - @"addition" : [RCTAdditionAnimatedNode class], - @"multiplication" : [RCTMultiplicationAnimatedNode class], - @"transform" : [RCTTransformAnimatedNode class]}; - }); - - NSString *nodeType = [RCTConvert NSString:config[@"type"]]; - - Class nodeClass = map[nodeType]; - if (!nodeClass) { - RCTLogError(@"Animated node type %@ not supported natively", nodeType); - return; - } - - RCTAnimatedNode *node = [[nodeClass alloc] initWithTag:tag config:config]; - _animationNodes[tag] = node; - - if ([node isKindOfClass:[RCTPropsAnimatedNode class]]) { - [_propAnimationNodes addObject:(RCTPropsAnimatedNode *)node]; - } -} - -RCT_EXPORT_METHOD(connectAnimatedNodes:(nonnull NSNumber *)parentTag - childTag:(nonnull NSNumber *)childTag) -{ - RCTAssertParam(parentTag); - RCTAssertParam(childTag); - - RCTAnimatedNode *parentNode = _animationNodes[parentTag]; - RCTAnimatedNode *childNode = _animationNodes[childTag]; - - RCTAssertParam(parentNode); - RCTAssertParam(childNode); - - [parentNode addChild:childNode]; -} - -RCT_EXPORT_METHOD(disconnectAnimatedNodes:(nonnull NSNumber *)parentTag - childTag:(nonnull NSNumber *)childTag) -{ - RCTAssertParam(parentTag); - RCTAssertParam(childTag); - - RCTAnimatedNode *parentNode = _animationNodes[parentTag]; - RCTAnimatedNode *childNode = _animationNodes[childTag]; - - RCTAssertParam(parentNode); - RCTAssertParam(childNode); - - [parentNode removeChild:childNode]; -} - -RCT_EXPORT_METHOD(startAnimatingNode:(nonnull NSNumber *)animationId - nodeTag:(nonnull NSNumber *)nodeTag - config:(NSDictionary *)config - endCallback:(RCTResponseSenderBlock)callBack) -{ - if (RCT_DEBUG && ![config[@"type"] isEqual:@"frames"]) { - RCTLogError(@"Unsupported animation type: %@", config[@"type"]); - return; - } - - NSTimeInterval delay = [RCTConvert double:config[@"delay"]]; - NSNumber *toValue = [RCTConvert NSNumber:config[@"toValue"]] ?: @1; - NSArray *frames = [RCTConvert NSNumberArray:config[@"frames"]]; - - RCTValueAnimatedNode *valueNode = (RCTValueAnimatedNode *)_animationNodes[nodeTag]; - - RCTAnimationDriverNode *animationDriver = - [[RCTAnimationDriverNode alloc] initWithId:animationId - delay:delay - toValue:toValue.doubleValue - frames:frames - forNode:valueNode - callBack:callBack]; - [_activeAnimations addObject:animationDriver]; - _animationDrivers[animationId] = animationDriver; - [animationDriver startAnimation]; - [self startAnimation]; -} - -RCT_EXPORT_METHOD(stopAnimation:(nonnull NSNumber *)animationId) -{ - RCTAnimationDriverNode *driver = _animationDrivers[animationId]; - if (driver) { - [driver removeAnimation]; - [_animationDrivers removeObjectForKey:animationId]; - [_activeAnimations removeObject:driver]; - [_finishedAnimations removeObject:driver]; - } -} - -RCT_EXPORT_METHOD(setAnimatedNodeValue:(nonnull NSNumber *)nodeTag - value:(nonnull NSNumber *)value) -{ - RCTAnimatedNode *node = _animationNodes[nodeTag]; - if (![node isKindOfClass:[RCTValueAnimatedNode class]]) { - RCTLogError(@"Not a value node."); - return; - } - RCTValueAnimatedNode *valueNode = (RCTValueAnimatedNode *)node; - valueNode.value = value.floatValue; - [valueNode setNeedsUpdate]; -} - -RCT_EXPORT_METHOD(connectAnimatedNodeToView:(nonnull NSNumber *)nodeTag - viewTag:(nonnull NSNumber *)viewTag) -{ - RCTAnimatedNode *node = _animationNodes[nodeTag]; - if (viewTag && [node isKindOfClass:[RCTPropsAnimatedNode class]]) { - [(RCTPropsAnimatedNode *)node connectToView:viewTag animatedModule:self]; - } -} - -RCT_EXPORT_METHOD(disconnectAnimatedNodeFromView:(nonnull NSNumber *)nodeTag - viewTag:(nonnull NSNumber *)viewTag) -{ - RCTAnimatedNode *node = _animationNodes[nodeTag]; - if (viewTag && node && [node isKindOfClass:[RCTPropsAnimatedNode class]]) { - [(RCTPropsAnimatedNode *)node disconnectFromView:viewTag]; - } -} - -RCT_EXPORT_METHOD(dropAnimatedNode:(nonnull NSNumber *)tag) -{ - RCTAnimatedNode *node = _animationNodes[tag]; - if (node) { - [node detachNode]; - [_animationNodes removeObjectForKey:tag]; - if ([node isKindOfClass:[RCTValueAnimatedNode class]]) { - [_updatedValueNodes removeObject:(RCTValueAnimatedNode *)node]; - } else if ([node isKindOfClass:[RCTPropsAnimatedNode class]]) { - [_propAnimationNodes removeObject:(RCTPropsAnimatedNode *)node]; - } - } -} - -#pragma mark -- Animation Loop - -- (void)startAnimation -{ - if (!_displayLink && _activeAnimations.count > 0) { - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateAnimations)]; - [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; - } -} - -- (void)updateAnimations -{ - // Step Current active animations - // This also recursively marks children nodes as needing update - for (RCTAnimationDriverNode *animationDriver in _activeAnimations) { - [animationDriver stepAnimation]; - } - - // Perform node updates for marked nodes. - // At this point all nodes that are in need of an update are properly marked as such. - for (RCTPropsAnimatedNode *propsNode in _propAnimationNodes) { - [propsNode updateNodeIfNecessary]; - } - - // Cleanup nodes and prepare for next cycle. Remove updated nodes from bucket. - for (RCTAnimationDriverNode *driverNode in _activeAnimations) { - [driverNode cleanupAnimationUpdate]; - } - for (RCTValueAnimatedNode *valueNode in _updatedValueNodes) { - [valueNode cleanupAnimationUpdate]; - } - [_updatedValueNodes removeAllObjects]; - - for (RCTAnimationDriverNode *driverNode in _activeAnimations) { - if (driverNode.animationHasFinished) { - [driverNode removeAnimation]; - [_finishedAnimations addObject:driverNode]; - } - } - for (RCTAnimationDriverNode *driverNode in _finishedAnimations) { - [_activeAnimations removeObject:driverNode]; - [_animationDrivers removeObjectForKey:driverNode.animationId]; - } - [_finishedAnimations removeAllObjects]; - - if (_activeAnimations.count == 0) { - [_displayLink invalidate]; - _displayLink = nil; - } -} - -@end diff --git a/Libraries/NativeAnimation/RCTViewPropertyMapper.h b/Libraries/NativeAnimation/RCTViewPropertyMapper.h deleted file mode 100644 index 29bbee754a7c7c..00000000000000 --- a/Libraries/NativeAnimation/RCTViewPropertyMapper.h +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -#import - -@class RCTNativeAnimatedModule; - -@interface RCTViewPropertyMapper : NSObject - -@property (nonatomic, readonly) NSNumber *viewTag; - -- (instancetype)initWithViewTag:(NSNumber *)viewTag - animationModule:(RCTNativeAnimatedModule *)animationModule NS_DESIGNATED_INITIALIZER; - -- (void)updateViewWithDictionary:(NSDictionary *)updates; - -@end diff --git a/Libraries/NativeAnimation/RCTViewPropertyMapper.m b/Libraries/NativeAnimation/RCTViewPropertyMapper.m deleted file mode 100644 index bfa9803a65e0d6..00000000000000 --- a/Libraries/NativeAnimation/RCTViewPropertyMapper.m +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "RCTViewPropertyMapper.h" - -#import - -#import "RCTBridge.h" -#import "RCTUIManager.h" -#import "RCTNativeAnimatedModule.h" - -@implementation RCTViewPropertyMapper -{ - CGFloat _translateX; - CGFloat _translateY; - CGFloat _scaleX; - CGFloat _scaleY; - CGFloat _rotation; - RCTNativeAnimatedModule *_animationModule; -} - -- (instancetype)initWithViewTag:(NSNumber *)viewTag - animationModule:(RCTNativeAnimatedModule *)animationModule -{ - if ((self = [super init])) { - _animationModule = animationModule; - _viewTag = viewTag; - _translateX = 0; - _translateY = 0; - _scaleX = 1; - _scaleY = 1; - _rotation = 0; - } - return self; -} - -RCT_NOT_IMPLEMENTED(- (instancetype)init) - -- (void)updateViewWithDictionary:(NSDictionary *)updates -{ - if (updates.count) { - UIView *view = [_animationModule.bridge.uiManager viewForReactTag:_viewTag]; - if (!view) { - return; - } - - NSNumber *opacity = updates[@"opacity"]; - if (opacity) { - view.alpha = opacity.doubleValue; - } - - NSNumber *scale = updates[@"scale"]; - if (scale) { - _scaleX = scale.doubleValue; - _scaleY = scale.doubleValue; - } - NSNumber *scaleX = updates[@"scaleX"]; - if (scaleX) { - _scaleX = scaleX.doubleValue; - } - NSNumber *scaleY = updates[@"scaleY"]; - if (scaleY) { - _scaleY = scaleY.doubleValue; - } - NSNumber *translateX = updates[@"translateX"]; - if (translateX) { - _translateX = translateX.doubleValue; - } - NSNumber *translateY = updates[@"translateY"]; - if (translateY) { - _translateY = translateY.doubleValue; - } - NSNumber *rotation = updates[@"rotate"]; - if (rotation) { - _rotation = rotation.doubleValue; - } - - if (translateX || translateY || scale || scaleX || scaleY || rotation) { - CATransform3D xform = CATransform3DMakeScale(_scaleX, _scaleY, 0); - xform = CATransform3DTranslate(xform, _translateX, _translateY, 0); - xform = CATransform3DRotate(xform, _rotation, 0, 0, 1); - view.layer.allowsEdgeAntialiasing = YES; - view.layer.transform = xform; - } - } -} - -@end From b0fed416cbb510009bef1ae721e564d04d053cc5 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Wed, 8 Jun 2016 00:06:08 -0700 Subject: [PATCH 281/843] Fixed ART views Summary: Fixed ART views, which were broken by the zIndex diff Reviewed By: wwjholmes Differential Revision: D3403679 fbshipit-source-id: cc3cdccd19c21223ce6bddeda3d914937ecb73b6 --- Libraries/ART/ARTNode.h | 3 +-- Libraries/ART/ARTNode.m | 14 ++++++++++---- Libraries/ART/ARTSurfaceView.m | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/Libraries/ART/ARTNode.h b/Libraries/ART/ARTNode.h index 511c09a5a6aae2..b918fad254418d 100644 --- a/Libraries/ART/ARTNode.h +++ b/Libraries/ART/ARTNode.h @@ -7,8 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import -#import +#import "UIView+React.h" /** * ART nodes are implemented as empty UIViews but this is just an implementation detail to fit diff --git a/Libraries/ART/ARTNode.m b/Libraries/ART/ARTNode.m index d23d5880a95677..38b01a0c56c449 100644 --- a/Libraries/ART/ARTNode.m +++ b/Libraries/ART/ARTNode.m @@ -13,16 +13,22 @@ @implementation ARTNode -- (void)insertSubview:(UIView *)subview atIndex:(NSInteger)index +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { + [super insertReactSubview:subview atIndex:atIndex]; + [self insertSubview:subview atIndex:atIndex]; [self invalidate]; - [super insertSubview:subview atIndex:index]; } -- (void)removeFromSuperview +- (void)removeReactSubview:(UIView *)subview { + [super removeReactSubview:subview]; [self invalidate]; - [super removeFromSuperview]; +} + +- (void)didUpdateReactSubviews +{ + // Do nothing, as subviews are inserted by insertReactSubview: } - (void)setOpacity:(CGFloat)opacity diff --git a/Libraries/ART/ARTSurfaceView.m b/Libraries/ART/ARTSurfaceView.m index f5c13651bf029a..0bb3cc84665642 100644 --- a/Libraries/ART/ARTSurfaceView.m +++ b/Libraries/ART/ARTSurfaceView.m @@ -14,6 +14,24 @@ @implementation ARTSurfaceView +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +{ + [super insertReactSubview:subview atIndex:atIndex]; + [self insertSubview:subview atIndex:atIndex]; + [self invalidate]; +} + +- (void)removeReactSubview:(UIView *)subview +{ + [super removeReactSubview:subview]; + [self invalidate]; +} + +- (void)didUpdateReactSubviews +{ + // Do nothing, as subviews are inserted by insertReactSubview: +} + - (void)invalidate { [self setNeedsDisplay]; From 525d35ba67d9ce9603678cf3cda419e0e7a0f3e8 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Wed, 8 Jun 2016 02:47:45 -0700 Subject: [PATCH 282/843] Reverted commit D3400379 Reviewed By: mhorowitz Differential Revision: D3400379 fbshipit-source-id: b7e733e74bb0f4fbd597fd073826c399ece2c47f --- .../react/bridge/JSCJavaScriptExecutor.java | 8 ++++---- .../com/facebook/react/bridge/NativeArray.java | 2 +- .../java/com/facebook/react/bridge/NativeMap.java | 2 +- .../react/bridge/ProxyJavaScriptExecutor.java | 8 ++++---- .../com/facebook/react/bridge/ReactBridge.java | 10 ++-------- .../react/bridge/ReadableNativeArray.java | 3 ++- .../facebook/react/bridge/ReadableNativeMap.java | 3 ++- .../react/bridge/WritableNativeArray.java | 3 ++- .../facebook/react/bridge/WritableNativeMap.java | 2 +- ReactAndroid/src/main/jni/react/jni/Android.mk | 10 ++++++++-- ReactAndroid/src/main/jni/react/jni/BUCK | 15 ++++++++++++++- .../jni/{xreact => react}/jni/NativeArray.cpp | 0 .../main/jni/{xreact => react}/jni/NativeArray.h | 0 .../jni/{xreact => react}/jni/NativeCommon.cpp | 0 .../main/jni/{xreact => react}/jni/NativeCommon.h | 0 .../main/jni/{xreact => react}/jni/NativeMap.cpp | 0 .../main/jni/{xreact => react}/jni/NativeMap.h | 0 ReactAndroid/src/main/jni/react/jni/OnLoad.cpp | 14 +++++++++++--- .../{xreact => react}/jni/ReadableNativeArray.cpp | 0 .../{xreact => react}/jni/ReadableNativeArray.h | 0 .../{xreact => react}/jni/ReadableNativeMap.cpp | 0 .../jni/{xreact => react}/jni/ReadableNativeMap.h | 0 .../{xreact => react}/jni/WritableNativeArray.cpp | 0 .../{xreact => react}/jni/WritableNativeArray.h | 0 .../{xreact => react}/jni/WritableNativeMap.cpp | 0 .../jni/{xreact => react}/jni/WritableNativeMap.h | 1 - ReactAndroid/src/main/jni/xreact/jni/Android.mk | 12 +++--------- ReactAndroid/src/main/jni/xreact/jni/BUCK | 10 ++-------- .../main/jni/xreact/jni/CatalystInstanceImpl.cpp | 3 ++- .../src/main/jni/xreact/jni/CxxModuleWrapper.cpp | 11 +++++------ ReactAndroid/src/main/jni/xreact/jni/JCallback.h | 2 +- .../src/main/jni/xreact/jni/JSCPerfLogging.cpp | 3 +-- .../src/main/jni/xreact/jni/JSLogging.cpp | 3 +-- .../src/main/jni/xreact/jni/MethodInvoker.cpp | 2 +- .../main/jni/xreact/jni/ModuleRegistryHolder.cpp | 5 +++-- ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp | 12 +----------- 36 files changed, 72 insertions(+), 72 deletions(-) rename ReactAndroid/src/main/jni/{xreact => react}/jni/NativeArray.cpp (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/NativeArray.h (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/NativeCommon.cpp (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/NativeCommon.h (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/NativeMap.cpp (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/NativeMap.h (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/ReadableNativeArray.cpp (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/ReadableNativeArray.h (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/ReadableNativeMap.cpp (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/ReadableNativeMap.h (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/WritableNativeArray.cpp (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/WritableNativeArray.h (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/WritableNativeMap.cpp (100%) rename ReactAndroid/src/main/jni/{xreact => react}/jni/WritableNativeMap.h (96%) diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java index 4f895abf31aa26..b4431c0d66d9cb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSCJavaScriptExecutor.java @@ -14,10 +14,6 @@ @DoNotStrip public class JSCJavaScriptExecutor extends JavaScriptExecutor { - static { - ReactBridge.staticInit(); - } - public static class Factory implements JavaScriptExecutor.Factory { @Override public JavaScriptExecutor create(WritableNativeMap jscConfig) throws Exception { @@ -25,6 +21,10 @@ public JavaScriptExecutor create(WritableNativeMap jscConfig) throws Exception { } } + static { + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + } + public JSCJavaScriptExecutor(WritableNativeMap jscConfig) { initialize(jscConfig); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java index d4c46f72302c53..2045a4b23e0016 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeArray.java @@ -19,7 +19,7 @@ @DoNotStrip public abstract class NativeArray { static { - ReactBridge.staticInit(); + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); } protected NativeArray(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java index 9e192dc4ea6d8d..2c683f9451130e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeMap.java @@ -19,7 +19,7 @@ @DoNotStrip public abstract class NativeMap { static { - ReactBridge.staticInit(); + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); } public NativeMap(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java index 6ebf4dc0df6e01..37d83b3386174c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ProxyJavaScriptExecutor.java @@ -24,10 +24,6 @@ */ @DoNotStrip public class ProxyJavaScriptExecutor extends JavaScriptExecutor { - static { - ReactBridge.staticInit(); - } - public static class Factory implements JavaScriptExecutor.Factory { private final JavaJSExecutor.Factory mJavaJSExecutorFactory; @@ -41,6 +37,10 @@ public JavaScriptExecutor create(WritableNativeMap jscConfig) throws Exception { } } + static { + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); + } + private @Nullable JavaJSExecutor mJavaJSExecutor; /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java index 2e69e78f94075b..e4d43f90b6a4f9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactBridge.java @@ -24,22 +24,16 @@ @DoNotStrip public class ReactBridge extends Countable { - private static final String REACT_NATIVE_LIB = "reactnativejni"; - private static final String XREACT_NATIVE_LIB = "reactnativejnifb"; + /* package */ static final String REACT_NATIVE_LIB = "reactnativejni"; static { - staticInit(); + SoLoader.loadLibrary(REACT_NATIVE_LIB); } private final ReactCallback mCallback; private final JavaScriptExecutor mJSExecutor; private final MessageQueueThread mNativeModulesQueueThread; - public static void staticInit() { - SoLoader.loadLibrary(REACT_NATIVE_LIB); - SoLoader.loadLibrary(XREACT_NATIVE_LIB); - } - /** * @param jsExecutor the JS executor to use to run JS * @param callback the callback class used to invoke native modules diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java index 2f81d301b60bf9..4cd1b75814e80b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java @@ -21,8 +21,9 @@ */ @DoNotStrip public class ReadableNativeArray extends NativeArray implements ReadableArray { + static { - ReactBridge.staticInit(); + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); } protected ReadableNativeArray(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java index ea289ac668f2e0..f3782ca0896f75 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java @@ -22,8 +22,9 @@ */ @DoNotStrip public class ReadableNativeMap extends NativeMap implements ReadableMap { + static { - ReactBridge.staticInit(); + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); } protected ReadableNativeMap(HybridData hybridData) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java index e6c343264a8d2c..26fe2dd11f6228 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java @@ -21,8 +21,9 @@ */ @DoNotStrip public class WritableNativeArray extends ReadableNativeArray implements WritableArray { + static { - ReactBridge.staticInit(); + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); } public WritableNativeArray() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java index 6b6c639d2e032b..d30827ade5bf12 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java @@ -22,7 +22,7 @@ @DoNotStrip public class WritableNativeMap extends ReadableNativeMap implements WritableMap { static { - ReactBridge.staticInit(); + SoLoader.loadLibrary(ReactBridge.REACT_NATIVE_LIB); } @Override diff --git a/ReactAndroid/src/main/jni/react/jni/Android.mk b/ReactAndroid/src/main/jni/react/jni/Android.mk index 06266de376fc6e..742e2161d1a0b6 100644 --- a/ReactAndroid/src/main/jni/react/jni/Android.mk +++ b/ReactAndroid/src/main/jni/react/jni/Android.mk @@ -11,8 +11,15 @@ LOCAL_SRC_FILES := \ JSLoader.cpp \ JSLogging.cpp \ JniJSModulesUnbundle.cpp \ + NativeArray.cpp \ + NativeCommon.cpp \ + NativeMap.cpp \ OnLoad.cpp \ ProxyExecutor.cpp \ + ReadableNativeArray.cpp \ + ReadableNativeMap.cpp \ + WritableNativeArray.cpp \ + WritableNativeMap.cpp \ LOCAL_C_INCLUDES := $(LOCAL_PATH) LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/../.. $(LOCAL_PATH)/.. @@ -23,7 +30,7 @@ LOCAL_CFLAGS += $(CXX11_FLAGS) LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) LOCAL_LDLIBS += -landroid -LOCAL_SHARED_LIBRARIES := libfolly_json libfb libjsc libglog_init libreactnativejnifb +LOCAL_SHARED_LIBRARIES := libfolly_json libfb libjsc libglog_init LOCAL_STATIC_LIBRARIES := libreactnative include $(BUILD_SHARED_LIBRARY) @@ -34,4 +41,3 @@ $(call import-module,folly) $(call import-module,fbgloginit) $(call import-module,fb) $(call import-module,jsc) -$(call import-module,xreact/jni) diff --git a/ReactAndroid/src/main/jni/react/jni/BUCK b/ReactAndroid/src/main/jni/react/jni/BUCK index e210b9dd9575eb..9bd754fb70e92f 100644 --- a/ReactAndroid/src/main/jni/react/jni/BUCK +++ b/ReactAndroid/src/main/jni/react/jni/BUCK @@ -18,7 +18,6 @@ def jni_library(**kwargs): ], deps = DEPS + JSC_DEPS + [ react_native_target('jni/react:react'), - react_native_target('jni/xreact/jni:jni'), ], **kwargs ) @@ -34,8 +33,15 @@ jni_library( 'JSLoader.cpp', 'JSLogging.cpp', 'JniJSModulesUnbundle.cpp', + 'NativeArray.cpp', + 'NativeCommon.cpp', + 'NativeMap.cpp', 'OnLoad.cpp', 'ProxyExecutor.cpp', + 'ReadableNativeArray.cpp', + 'ReadableNativeMap.cpp', + 'WritableNativeArray.cpp', + 'WritableNativeMap.cpp', ], headers = [ 'JSLoader.h', @@ -50,6 +56,13 @@ jni_library( 'WebWorkers.h', ], exported_headers = [ + 'NativeCommon.h', + 'NativeArray.h', + 'NativeMap.h', + 'ReadableNativeArray.h', + 'ReadableNativeMap.h', + 'WritableNativeArray.h', + 'WritableNativeMap.h', ], preprocessor_flags = [ '-DLOG_TAG="ReactNativeJNI"', diff --git a/ReactAndroid/src/main/jni/xreact/jni/NativeArray.cpp b/ReactAndroid/src/main/jni/react/jni/NativeArray.cpp similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/NativeArray.cpp rename to ReactAndroid/src/main/jni/react/jni/NativeArray.cpp diff --git a/ReactAndroid/src/main/jni/xreact/jni/NativeArray.h b/ReactAndroid/src/main/jni/react/jni/NativeArray.h similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/NativeArray.h rename to ReactAndroid/src/main/jni/react/jni/NativeArray.h diff --git a/ReactAndroid/src/main/jni/xreact/jni/NativeCommon.cpp b/ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/NativeCommon.cpp rename to ReactAndroid/src/main/jni/react/jni/NativeCommon.cpp diff --git a/ReactAndroid/src/main/jni/xreact/jni/NativeCommon.h b/ReactAndroid/src/main/jni/react/jni/NativeCommon.h similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/NativeCommon.h rename to ReactAndroid/src/main/jni/react/jni/NativeCommon.h diff --git a/ReactAndroid/src/main/jni/xreact/jni/NativeMap.cpp b/ReactAndroid/src/main/jni/react/jni/NativeMap.cpp similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/NativeMap.cpp rename to ReactAndroid/src/main/jni/react/jni/NativeMap.cpp diff --git a/ReactAndroid/src/main/jni/xreact/jni/NativeMap.h b/ReactAndroid/src/main/jni/react/jni/NativeMap.h similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/NativeMap.h rename to ReactAndroid/src/main/jni/react/jni/NativeMap.h diff --git a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp index 63fee95574aaca..8d60719c4e9975 100644 --- a/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp +++ b/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp @@ -22,6 +22,8 @@ #include "JExecutorTokenFactory.h" #include "JNativeRunnable.h" #include "JSLoader.h" +#include "NativeCommon.h" +#include "ReadableNativeArray.h" #include "ProxyExecutor.h" #include "OnLoad.h" #include "JMessageQueueThread.h" @@ -29,9 +31,7 @@ #include "JSLogging.h" #include "JSCPerfLogging.h" #include "WebWorkers.h" - -#include -#include +#include "WritableNativeMap.h" #include #ifdef WITH_FBSYSTRACE @@ -456,9 +456,17 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { PerfLogging::installNativeHooks = addNativePerfLoggingHooks; JSLogging::nativeHook = nativeLoggingHook; + NativeArray::registerNatives(); + ReadableNativeArray::registerNatives(); + WritableNativeArray::registerNatives(); JNativeRunnable::registerNatives(); registerJSLoaderNatives(); + NativeMap::registerNatives(); + ReadableNativeMap::registerNatives(); + WritableNativeMap::registerNatives(); + ReadableNativeMapKeySetIterator::registerNatives(); + registerNatives("com/facebook/react/bridge/JSCJavaScriptExecutor", { makeNativeMethod("initialize", executors::createJSCExecutor), }); diff --git a/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.cpp b/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.cpp rename to ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.cpp diff --git a/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.h b/ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.h similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/ReadableNativeArray.h rename to ReactAndroid/src/main/jni/react/jni/ReadableNativeArray.h diff --git a/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.cpp b/ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.cpp similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.cpp rename to ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.cpp diff --git a/ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.h b/ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.h similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/ReadableNativeMap.h rename to ReactAndroid/src/main/jni/react/jni/ReadableNativeMap.h diff --git a/ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.cpp b/ReactAndroid/src/main/jni/react/jni/WritableNativeArray.cpp similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.cpp rename to ReactAndroid/src/main/jni/react/jni/WritableNativeArray.cpp diff --git a/ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.h b/ReactAndroid/src/main/jni/react/jni/WritableNativeArray.h similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/WritableNativeArray.h rename to ReactAndroid/src/main/jni/react/jni/WritableNativeArray.h diff --git a/ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.cpp b/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.cpp similarity index 100% rename from ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.cpp rename to ReactAndroid/src/main/jni/react/jni/WritableNativeMap.cpp diff --git a/ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.h b/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h similarity index 96% rename from ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.h rename to ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h index 1fc942a025e5e1..cf9cd95a000d88 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/WritableNativeMap.h +++ b/ReactAndroid/src/main/jni/react/jni/WritableNativeMap.h @@ -20,7 +20,6 @@ struct WritableNativeMap : jni::HybridClass initHybrid(jni::alias_ref); - __attribute__((visibility("default"))) folly::dynamic consume(); void putNull(std::string key); diff --git a/ReactAndroid/src/main/jni/xreact/jni/Android.mk b/ReactAndroid/src/main/jni/xreact/jni/Android.mk index 4eb04b448854f4..c0078c36a8a157 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/Android.mk +++ b/ReactAndroid/src/main/jni/xreact/jni/Android.mk @@ -9,21 +9,14 @@ LOCAL_SRC_FILES := \ CxxModuleWrapper.cpp \ JExecutorToken.cpp \ JMessageQueueThread.cpp \ + JniJSModulesUnbundle.cpp \ JSCPerfLogging.cpp \ JSLoader.cpp \ JSLogging.cpp \ - JniJSModulesUnbundle.cpp \ MethodInvoker.cpp \ ModuleRegistryHolder.cpp \ - NativeArray.cpp \ - NativeCommon.cpp \ - NativeMap.cpp \ OnLoad.cpp \ ProxyExecutor.cpp \ - ReadableNativeArray.cpp \ - ReadableNativeMap.cpp \ - WritableNativeArray.cpp \ - WritableNativeMap.cpp \ LOCAL_C_INCLUDES := $(LOCAL_PATH) LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/../.. @@ -34,7 +27,7 @@ LOCAL_CFLAGS += $(CXX11_FLAGS) LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) LOCAL_LDLIBS += -landroid -LOCAL_SHARED_LIBRARIES := libfolly_json libfbjni libjsc libglog_init +LOCAL_SHARED_LIBRARIES := libfolly_json libfbjni libjsc libglog_init libreactnativejni LOCAL_STATIC_LIBRARIES := libreactnativefb include $(BUILD_SHARED_LIBRARY) @@ -44,3 +37,4 @@ $(call import-module,jsc) $(call import-module,folly) $(call import-module,fbgloginit) $(call import-module,jsc) +$(call import-module,react/jni) diff --git a/ReactAndroid/src/main/jni/xreact/jni/BUCK b/ReactAndroid/src/main/jni/xreact/jni/BUCK index cb47fa436a1b1c..ea88102d01c483 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/BUCK +++ b/ReactAndroid/src/main/jni/xreact/jni/BUCK @@ -5,25 +5,19 @@ SUPPORTED_PLATFORMS = '^android-(armv7|x86)$' EXPORTED_HEADERS = [ 'CxxModuleWrapper.h', - 'NativeArray.h', - 'NativeCommon.h', - 'NativeMap.h', - 'ReadableNativeArray.h', - 'ReadableNativeMap.h', - 'WritableNativeArray.h', - 'WritableNativeMap.h', ] cxx_library( name='jni', soname = 'libreactnativejnifb.so', - header_namespace = 'xreact/jni', + header_namespace = 'react/jni', supported_platforms_regex = SUPPORTED_PLATFORMS, deps = JSC_DEPS + [ '//native/fb:fb', '//native/third-party/android-ndk:android', '//xplat/folly:molly', '//xplat/fbsystrace:fbsystrace', + react_native_target('jni/react/jni:jni'), react_native_xplat_target('cxxreact:bridge'), react_native_xplat_target('cxxreact:module'), ], diff --git a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp index 64fa80f98100f2..c4c6faa74b2899 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/CatalystInstanceImpl.cpp @@ -13,6 +13,8 @@ #include #include +#include + #include #include #include @@ -21,7 +23,6 @@ #include "JavaScriptExecutorHolder.h" #include "JniJSModulesUnbundle.h" #include "ModuleRegistryHolder.h" -#include "NativeArray.h" #include "JNativeRunnable.h" using namespace facebook::jni; diff --git a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp index 9267d876e963c5..d1532fd10dd2b9 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/CxxModuleWrapper.cpp @@ -2,11 +2,16 @@ #include "CxxModuleWrapper.h" +#include + #include #include #include #include +#include +#include + #include #include @@ -15,12 +20,6 @@ #include #include -#include -#include - -#include "ReadableNativeArray.h" - - using namespace facebook::jni; using namespace facebook::xplat::module; using namespace facebook::react; diff --git a/ReactAndroid/src/main/jni/xreact/jni/JCallback.h b/ReactAndroid/src/main/jni/xreact/jni/JCallback.h index 1d1393eb95bca9..539273104dced2 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/JCallback.h +++ b/ReactAndroid/src/main/jni/xreact/jni/JCallback.h @@ -7,7 +7,7 @@ #include #include -#include "NativeArray.h" +#include namespace facebook { namespace react { diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp index 1dd837db87bf61..1b7b5e6887b6fa 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/JSCPerfLogging.cpp @@ -2,10 +2,9 @@ #include "JSCPerfLogging.h" -#include - #include #include +#include using namespace facebook::jni; diff --git a/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp index a27d3f5d35e732..7690a5656ebd8d 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/JSLogging.cpp @@ -4,10 +4,9 @@ #include #include +#include #include -#include - namespace facebook { namespace react { diff --git a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp index e7d3d2c2d0718b..218f6c9d0ac2d3 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/MethodInvoker.cpp @@ -2,6 +2,7 @@ #include "MethodInvoker.h" +#include #ifdef WITH_FBSYSTRACE #include #endif @@ -9,7 +10,6 @@ #include "ModuleRegistryHolder.h" #include "JCallback.h" #include "JExecutorToken.h" -#include "ReadableNativeArray.h" namespace facebook { namespace react { diff --git a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp index eebed41df0c624..29349f741af1dc 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/ModuleRegistryHolder.cpp @@ -10,10 +10,11 @@ #include #include #include +#include -#include "CatalystInstanceImpl.h" #include "MethodInvoker.h" -#include "ReadableNativeArray.h" + +#include "CatalystInstanceImpl.h" using facebook::xplat::module::CxxModule; diff --git a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp index 952a6b46c4c4ff..3242c59ca2bd63 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/OnLoad.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "CatalystInstanceImpl.h" #include "JavaScriptExecutorHolder.h" #include "JSCPerfLogging.h" @@ -16,9 +17,6 @@ #include "WebWorkers.h" #include "JCallback.h" -#include "WritableNativeMap.h" -#include "WritableNativeArray.h" - #include using namespace facebook::jni; @@ -182,14 +180,6 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { CxxModuleWrapper::registerNatives(); JCallbackImpl::registerNatives(); registerJSLoaderNatives(); - - NativeArray::registerNatives(); - ReadableNativeArray::registerNatives(); - WritableNativeArray::registerNatives(); - NativeMap::registerNatives(); - ReadableNativeMap::registerNatives(); - WritableNativeMap::registerNatives(); - ReadableNativeMapKeySetIterator::registerNatives(); }); } From 91e504b0c817d69492920cbaf0f16e6a03dd9b0b Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Wed, 8 Jun 2016 03:58:57 -0700 Subject: [PATCH 283/843] Fixed website generation for CameraRoll.js Summary: Fixes whate was broken in #7988 Website generation does not understand Promise<*>. Closes https://github.com/facebook/react-native/pull/8002 Differential Revision: D3404468 Pulled By: avaly fbshipit-source-id: 9070ce038431795b0195f9eb0366aab5f3fb4cb0 --- Libraries/CameraRoll/CameraRoll.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/CameraRoll/CameraRoll.js b/Libraries/CameraRoll/CameraRoll.js index 4b7f23f3e8bdab..140adc25f72815 100644 --- a/Libraries/CameraRoll/CameraRoll.js +++ b/Libraries/CameraRoll/CameraRoll.js @@ -116,7 +116,7 @@ class CameraRoll { static GroupTypesOptions: Array; static AssetTypeOptions: Array; - static saveImageWithTag(tag: string):Promise<*> { + static saveImageWithTag(tag: string): Promise { console.warn('CameraRoll.saveImageWithTag is deprecated. Use CameraRoll.saveToCameraRoll instead'); return this.saveToCameraRoll(tag, 'photo'); } @@ -135,7 +135,7 @@ class CameraRoll { * * Returns a Promise which will resolve with the new URI. */ - static saveToCameraRoll(tag: string, type?: 'photo' | 'video'): Promise<*> { + static saveToCameraRoll(tag: string, type?: 'photo' | 'video'): Promise { invariant( typeof tag === 'string', 'CameraRoll.saveToCameraRoll must be a valid string.' From 66a13d548be40e58e2f55c64bef35aad0c2beb9d Mon Sep 17 00:00:00 2001 From: Valentin Agachi Date: Wed, 8 Jun 2016 04:41:15 -0700 Subject: [PATCH 284/843] Add flexibility to the iOS UIExplorer test script Summary: Add more flexibility to the iOS UIExplorer test scripts: - support environments without `xcpretty` installed - support custom xcode destination argument Initial PR is based on the last git commit which passed on Travis today (2a92b52) in order to test Travis build. I'll rebase to latest, in order to fix merge conflicts, before merging. **Test plan (required)** Make sure tests pass on Travis. Closes https://github.com/facebook/react-native/pull/7982 Differential Revision: D3404574 Pulled By: avaly fbshipit-source-id: 48aabd81fba67d482af46728a9c3975842f03060 --- scripts/objc-test.sh | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/scripts/objc-test.sh b/scripts/objc-test.sh index 83835ce44effd9..c164f825c19d3d 100755 --- a/scripts/objc-test.sh +++ b/scripts/objc-test.sh @@ -5,8 +5,6 @@ set -e SCRIPTS=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) ROOT=$(dirname $SCRIPTS) -export REACT_PACKAGER_LOG="$ROOT/server.log" - cd $ROOT function cleanup { @@ -17,17 +15,29 @@ function cleanup { then WATCHMAN_LOGS=/usr/local/Cellar/watchman/3.1/var/run/watchman/$USER.log [ -f $WATCHMAN_LOGS ] && cat $WATCHMAN_LOGS - - [ -f $REACT_PACKAGER_LOG ] && cat $REACT_PACKAGER_LOG fi } trap cleanup EXIT +if [ -z "$XCODE_DESTINATION" ]; then + XCODE_DESTINATION="platform=iOS Simulator,name=iPhone 5,OS=9.3" +fi + +# Support for environments without xcpretty installed +set +e +OUTPUT_TOOL=$(which xcpretty) +set -e +if [ -z "$OUTPUT_TOOL" ]; then + OUTPUT_TOOL="sed" +fi + # TODO: We use xcodebuild because xctool would stall when collecting info about # the tests before running them. Switch back when this issue with xctool has # been resolved. xcodebuild \ -project Examples/UIExplorer/UIExplorer.xcodeproj \ - -scheme UIExplorer -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 5,OS=9.3' \ + -scheme UIExplorer \ + -sdk iphonesimulator \ + -destination "$XCODE_DESTINATION" \ test \ -| xcpretty && exit ${PIPESTATUS[0]} + | $OUTPUT_TOOL && exit ${PIPESTATUS[0]} From c8101607d8a38e3bde3daa58e80eed174d8c78f6 Mon Sep 17 00:00:00 2001 From: Yann Pringault Date: Wed, 8 Jun 2016 06:11:31 -0700 Subject: [PATCH 285/843] Minor typo in Error message Summary: Closes https://github.com/facebook/react-native/pull/8001 Differential Revision: D3404663 fbshipit-source-id: 0d0af84e4f6d31e6ddf79ef4e263737542b81361 --- packager/react-packager/src/Bundler/BundleBase.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packager/react-packager/src/Bundler/BundleBase.js b/packager/react-packager/src/Bundler/BundleBase.js index 53fa9c3438407a..838ffa1d6b5ac5 100644 --- a/packager/react-packager/src/Bundler/BundleBase.js +++ b/packager/react-packager/src/Bundler/BundleBase.js @@ -32,7 +32,7 @@ class BundleBase { addModule(module) { if (!(module instanceof ModuleTransport)) { - throw new Error('Expeceted a ModuleTransport object'); + throw new Error('Expected a ModuleTransport object'); } return this._modules.push(module) - 1; From c0ac0d5071351bdd613222cbff41041746420870 Mon Sep 17 00:00:00 2001 From: Olivier Notteghem Date: Wed, 8 Jun 2016 08:09:31 -0700 Subject: [PATCH 286/843] Fix missing check causing layout some animation code to be executed when it shouldn't Reviewed By: dmmiller Differential Revision: D3404149 fbshipit-source-id: 8663325fd3a4b9257d84075e22bd9cc59d6414ac --- .../react/uimanager/NativeViewHierarchyManager.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index f6e13761a1c397..7ffe35967226c7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -348,7 +348,8 @@ public void manageChildren( View viewToRemove = viewManager.getChildAt(viewToManage, indexToRemove); - if (mLayoutAnimator.shouldAnimateLayout(viewToRemove) && + if (mLayoutAnimationEnabled && + mLayoutAnimator.shouldAnimateLayout(viewToRemove) && arrayContains(tagsToDelete, viewToRemove.getId())) { // The view will be removed and dropped by the 'delete' layout animation // instead, so do nothing @@ -395,7 +396,8 @@ public void manageChildren( tagsToDelete)); } - if (mLayoutAnimator.shouldAnimateLayout(viewToDestroy)) { + if (mLayoutAnimationEnabled && + mLayoutAnimator.shouldAnimateLayout(viewToDestroy)) { mLayoutAnimator.deleteView(viewToDestroy, new LayoutAnimationListener() { @Override public void onAnimationEnd() { From bb9ed2d24c63b56aedb254c3758b3fb0560339b7 Mon Sep 17 00:00:00 2001 From: Olivier Notteghem Date: Wed, 8 Jun 2016 08:11:56 -0700 Subject: [PATCH 287/843] Fix potential NPE is layout animation deletion code Reviewed By: dmmiller Differential Revision: D3403930 fbshipit-source-id: 20067542d06396997719bbe963ce87403fae5ac3 --- .../facebook/react/uimanager/NativeViewHierarchyManager.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index 7ffe35967226c7..00b00d89c8e424 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -412,7 +412,10 @@ public void onAnimationEnd() { } } - private boolean arrayContains(int[] array, int ele) { + private boolean arrayContains(@Nullable int[] array, int ele) { + if (array == null) { + return false; + } for (int curEle : array) { if (curEle == ele) { return true; From 3a8b50ad550b176bbce5713c5c74e1318c10f2a0 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Wed, 8 Jun 2016 11:33:45 -0700 Subject: [PATCH 288/843] Kill NavigationReducers Summary: For navigation actions at high level, reducers from NavigationReducers does not know anything about the app-specific state thus people won't use these reducers. Instead, people should build their own reducers. There are a lot of good libraries available that help people to reducing things if that's what they really need. At the low level, for navigation state changes that don't involve app-specific state, `NavigationStateUtils` should server that kind of need. `NavigationReducers` serves little benefit cause it does not know the app state, it does not know how to traverse the navigation states which can be a tree, a list or a map. That said, we hold no interest in owning in the core navigation library. Reviewed By: ericvicenti Differential Revision: D3372910 fbshipit-source-id: 797382b46e7d64b7ad578b51dd37e2b941faa83d --- .../NavigationCardStack-example.js | 2 +- .../UIExplorer/UIExplorerNavigationReducer.js | 57 ++++++- .../NavigationExperimental.js | 2 - .../Reducer/NavigationFindReducer.js | 41 ----- .../Reducer/NavigationReducer.js | 24 --- .../Reducer/NavigationStackReducer.js | 102 ----------- .../Reducer/NavigationTabsReducer.js | 103 ------------ .../__tests__/NavigationFindReducer-test.js | 45 ----- .../__tests__/NavigationStackReducer-test.js | 158 ------------------ .../__tests__/NavigationTabsReducer-test.js | 56 ------- 10 files changed, 52 insertions(+), 538 deletions(-) delete mode 100644 Libraries/NavigationExperimental/Reducer/NavigationFindReducer.js delete mode 100644 Libraries/NavigationExperimental/Reducer/NavigationReducer.js delete mode 100644 Libraries/NavigationExperimental/Reducer/NavigationStackReducer.js delete mode 100644 Libraries/NavigationExperimental/Reducer/NavigationTabsReducer.js delete mode 100644 Libraries/NavigationExperimental/Reducer/__tests__/NavigationFindReducer-test.js delete mode 100644 Libraries/NavigationExperimental/Reducer/__tests__/NavigationStackReducer-test.js delete mode 100644 Libraries/NavigationExperimental/Reducer/__tests__/NavigationTabsReducer-test.js diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationCardStack-example.js b/Examples/UIExplorer/NavigationExperimental/NavigationCardStack-example.js index c54cc1ba9e9b6a..6116f04df3aa36 100644 --- a/Examples/UIExplorer/NavigationExperimental/NavigationCardStack-example.js +++ b/Examples/UIExplorer/NavigationExperimental/NavigationCardStack-example.js @@ -80,7 +80,7 @@ class YourApplication extends React.Component { switch (type) { case 'push': // push a new route. - const route = {key: Date.now()}; + const route = {key: 'route-' + Date.now()}; navigationState = NavigationStateUtils.push(navigationState, route); break; diff --git a/Examples/UIExplorer/UIExplorerNavigationReducer.js b/Examples/UIExplorer/UIExplorerNavigationReducer.js index cec32ee91ba6ec..741c6abb78ca50 100644 --- a/Examples/UIExplorer/UIExplorerNavigationReducer.js +++ b/Examples/UIExplorer/UIExplorerNavigationReducer.js @@ -22,27 +22,72 @@ */ 'use strict'; -const React = require('react'); const ReactNative = require('react-native'); // $FlowFixMe : This is a platform-forked component, and flow seems to only run on iOS? const UIExplorerList = require('./UIExplorerList'); + const { NavigationExperimental, } = ReactNative; + + const { - Reducer: NavigationReducer, + StateUtils: NavigationStateUtils, } = NavigationExperimental; -const StackReducer = NavigationReducer.StackReducer; import type {NavigationState} from 'NavigationTypeDefinition'; -import type {UIExplorerAction} from './UIExplorerActions'; - export type UIExplorerNavigationState = { externalExample: ?string; stack: NavigationState; }; +const defaultGetReducerForState = (initialState) => (state) => state || initialState; + +function StackReducer({initialState, getReducerForState, getPushedReducerForAction}: any): Function { + const getReducerForStateWithDefault = getReducerForState || defaultGetReducerForState; + return function (lastState: ?NavigationState, action: any): NavigationState { + if (!lastState) { + return initialState; + } + const lastParentState = NavigationStateUtils.getParent(lastState); + if (!lastParentState) { + return lastState; + } + + const activeSubState = lastParentState.routes[lastParentState.index]; + const activeSubReducer = getReducerForStateWithDefault(activeSubState); + const nextActiveState = activeSubReducer(activeSubState, action); + if (nextActiveState !== activeSubState) { + const nextChildren = [...lastParentState.routes]; + nextChildren[lastParentState.index] = nextActiveState; + return { + ...lastParentState, + routes: nextChildren, + }; + } + + const subReducerToPush = getPushedReducerForAction(action, lastParentState); + if (subReducerToPush) { + return NavigationStateUtils.push( + lastParentState, + subReducerToPush(null, action) + ); + } + + switch (action.type) { + case 'back': + case 'BackAction': + if (lastParentState.index === 0 || lastParentState.routes.length === 1) { + return lastParentState; + } + return NavigationStateUtils.pop(lastParentState); + } + + return lastParentState; + }; +} + const UIExplorerStackReducer = StackReducer({ getPushedReducerForAction: (action, lastState) => { if (action.type === 'UIExplorerExampleAction' && UIExplorerList.Modules[action.openExample]) { @@ -106,7 +151,7 @@ function UIExplorerNavigationReducer(lastState: ?UIExplorerNavigationState, acti return { externalExample: null, stack: newStack, - } + }; } return lastState; } diff --git a/Libraries/NavigationExperimental/NavigationExperimental.js b/Libraries/NavigationExperimental/NavigationExperimental.js index ff8179189fd474..4548b33609802e 100644 --- a/Libraries/NavigationExperimental/NavigationExperimental.js +++ b/Libraries/NavigationExperimental/NavigationExperimental.js @@ -16,14 +16,12 @@ const NavigationCard = require('NavigationCard'); const NavigationCardStack = require('NavigationCardStack'); const NavigationHeader = require('NavigationHeader'); const NavigationPropTypes = require('NavigationPropTypes'); -const NavigationReducer = require('NavigationReducer'); const NavigationStateUtils = require('NavigationStateUtils'); const NavigationTransitioner = require('NavigationTransitioner'); const NavigationExperimental = { // Core StateUtils: NavigationStateUtils, - Reducer: NavigationReducer, // Views AnimatedView: NavigationAnimatedView, diff --git a/Libraries/NavigationExperimental/Reducer/NavigationFindReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationFindReducer.js deleted file mode 100644 index fd9e74bda9a89c..00000000000000 --- a/Libraries/NavigationExperimental/Reducer/NavigationFindReducer.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule NavigationFindReducer - * @flow - */ -'use strict'; - -/* - * NavigationFindReducer takes an array of reducers, and returns a reducer that - * iterates through all of the reducers and the result of the first reducer - * that modifies the input - */ - -import type { - NavigationRoute, - NavigationReducer -} from 'NavigationTypeDefinition'; - -function NavigationFindReducer( - reducers: Array, - defaultState: NavigationRoute, -): NavigationReducer { - return function(lastState: ?NavigationRoute, action: ?any): NavigationRoute { - for (let i = 0; i < reducers.length; i++) { - let reducer = reducers[i]; - let newState = reducer(lastState, action); - if (newState !== lastState) { - return newState || defaultState; - } - } - return lastState || defaultState; - }; -} - -module.exports = NavigationFindReducer; diff --git a/Libraries/NavigationExperimental/Reducer/NavigationReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationReducer.js deleted file mode 100644 index 4fb52297daaa7e..00000000000000 --- a/Libraries/NavigationExperimental/Reducer/NavigationReducer.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule NavigationReducer - * @flow - */ -'use strict'; - -var NavigationFindReducer = require('NavigationFindReducer'); -var NavigationStackReducer = require('NavigationStackReducer'); -var NavigationTabsReducer = require('NavigationTabsReducer'); - -const NavigationReducer = { - FindReducer: NavigationFindReducer, - StackReducer: NavigationStackReducer, - TabsReducer: NavigationTabsReducer, -}; - -module.exports = NavigationReducer; diff --git a/Libraries/NavigationExperimental/Reducer/NavigationStackReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationStackReducer.js deleted file mode 100644 index 9c884d55eabbd2..00000000000000 --- a/Libraries/NavigationExperimental/Reducer/NavigationStackReducer.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule NavigationStackReducer - * @flow-broken - */ -'use strict'; - -const NavigationStateUtils = require('NavigationStateUtils'); - -import type { - NavigationRoute, - NavigationState, - NavigationReducer, -} from 'NavigationTypeDefinition'; - -export type ReducerForStateHandler = (state: NavigationRoute) => NavigationReducer; - -export type PushedReducerForActionHandler = (action: any, lastState: NavigationState) => ?NavigationReducer; - -export type StackReducerConfig = { - /* - * The initialState is that the reducer will use when there is no previous state. - * Must be a NavigationState: - * - * { - * routes: [ - * {key: 'subState0'}, - * {key: 'subState1'}, - * ], - * index: 0, - * key: 'navStackKey' - * } - */ - initialState: NavigationState; - - /* - * Returns the sub-reducer for a particular state to handle. This will be called - * when we need to handle an action on a sub-state. If no reducer is returned, - * no action will be taken - */ - getReducerForState?: ReducerForStateHandler; - - /* - * Returns a sub-reducer that will be used when pushing a new route. If a reducer - * is returned, it be called to get the new state that will be pushed - */ - getPushedReducerForAction: PushedReducerForActionHandler; -}; - -const defaultGetReducerForState = (initialState) => (state) => state || initialState; - -function NavigationStackReducer({initialState, getReducerForState, getPushedReducerForAction}: StackReducerConfig): NavigationReducer { - const getReducerForStateWithDefault = getReducerForState || defaultGetReducerForState; - return function (lastState: ?NavigationRoute, action: any): NavigationRoute { - if (!lastState) { - return initialState; - } - const lastParentState = NavigationStateUtils.getParent(lastState); - if (!lastParentState) { - return lastState; - } - - const activeSubState = lastParentState.routes[lastParentState.index]; - const activeSubReducer = getReducerForStateWithDefault(activeSubState); - const nextActiveState = activeSubReducer(activeSubState, action); - if (nextActiveState !== activeSubState) { - const nextChildren = [...lastParentState.routes]; - nextChildren[lastParentState.index] = nextActiveState; - return { - ...lastParentState, - routes: nextChildren, - }; - } - - const subReducerToPush = getPushedReducerForAction(action, lastParentState); - if (subReducerToPush) { - return NavigationStateUtils.push( - lastParentState, - subReducerToPush(null, action) - ); - } - - switch (action.type) { - case 'back': - case 'BackAction': - if (lastParentState.index === 0 || lastParentState.routes.length === 1) { - return lastParentState; - } - return NavigationStateUtils.pop(lastParentState); - } - - return lastParentState; - }; -} - -module.exports = NavigationStackReducer; diff --git a/Libraries/NavigationExperimental/Reducer/NavigationTabsReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationTabsReducer.js deleted file mode 100644 index 5e60cf8660479b..00000000000000 --- a/Libraries/NavigationExperimental/Reducer/NavigationTabsReducer.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule NavigationTabsReducer - * @flow-broken - */ -'use strict'; - -const NavigationFindReducer = require('NavigationFindReducer'); -const NavigationStateUtils = require('NavigationStateUtils'); - -import type { - NavigationReducer, - NavigationRoute, -} from 'NavigationTypeDefinition'; - -const ActionTypes = { - JUMP_TO: 'react-native/NavigationExperimental/tabs-jumpTo', -}; - -export type JumpToAction = { - type: typeof ActionTypes.JUMP_TO, - index: number, -}; -function NavigationTabsJumpToAction(index: number): JumpToAction { - return { - type: ActionTypes.JUMP_TO, - index, - }; -} - -type TabsReducerConfig = { - key: string; - initialIndex: number; - tabReducers: Array; -}; - -function NavigationTabsReducer({key, initialIndex, tabReducers}: TabsReducerConfig): NavigationReducer { - return function(lastNavState: ?NavigationRoute, action: ?any): NavigationRoute { - if (!lastNavState) { - lastNavState = { - routes: tabReducers.map(reducer => reducer(null, null)), - index: initialIndex || 0, - key, - }; - } - const lastParentNavState = NavigationStateUtils.getParent(lastNavState); - if (!action || !lastParentNavState) { - return lastNavState; - } - if ( - action.type === ActionTypes.JUMP_TO && - action.index !== lastParentNavState.index - ) { - return NavigationStateUtils.jumpToIndex( - lastParentNavState, - action.index, - ); - } - const subReducers = tabReducers.map((tabReducer, tabIndex) => { - return function(navState: ?NavigationRoute, tabAction: any): NavigationRoute { - if (!navState) { - return lastParentNavState; - } - const parentState = NavigationStateUtils.getParent(navState); - const tabState = parentState && parentState.routes[tabIndex]; - const nextTabState = tabReducer(tabState, tabAction); - if (nextTabState && tabState !== nextTabState) { - const tabs = parentState && parentState.routes || []; - tabs[tabIndex] = nextTabState; - return { - ...lastParentNavState, - tabs, - index: tabIndex, - }; - } - return lastParentNavState; - }; - }); - let selectedTabReducer = subReducers.splice(lastParentNavState.index, 1)[0]; - subReducers.unshift(function(navState: ?NavigationRoute, action: any): NavigationRoute { - if (navState && action.type === 'BackAction') { - return NavigationStateUtils.jumpToIndex( - lastParentNavState, - initialIndex || 0 - ); - } - return lastParentNavState; - }); - subReducers.unshift(selectedTabReducer); - const findReducer = NavigationFindReducer(subReducers, lastParentNavState); - return findReducer(lastParentNavState, action); - }; -} - -NavigationTabsReducer.JumpToAction = NavigationTabsJumpToAction; - -module.exports = NavigationTabsReducer; diff --git a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationFindReducer-test.js b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationFindReducer-test.js deleted file mode 100644 index d57afdfb0bd42e..00000000000000 --- a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationFindReducer-test.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @flow-broken - */ -'use strict'; - -jest - .unmock('NavigationFindReducer'); - -const NavigationFindReducer = require('NavigationFindReducer'); - -describe('NavigationFindReducer', () => { - - it('handles basic find reducing with strings', () => { - let reducer = NavigationFindReducer([ - s => s, - s => s + '_yes', - s => 'nope', - ]); - let route = reducer('input'); - expect(route).toBe('input_yes'); - - reducer = NavigationFindReducer([ - (s, action) => s, - (s, action) => 'origRoute', - (s, action) => 'firstChangedState', - ]); - route = reducer('origRoute', 'action1'); - expect(route).toBe('firstChangedState'); - - reducer = NavigationFindReducer([ - (s, action) => s, - (s, action) => action, - ]); - route = reducer('inputState', 'action2'); - expect(route).toBe('action2'); - }); - -}); diff --git a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationStackReducer-test.js b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationStackReducer-test.js deleted file mode 100644 index 23828086f61495..00000000000000 --- a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationStackReducer-test.js +++ /dev/null @@ -1,158 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @flow-broken - */ -'use strict'; - -jest - .unmock('NavigationStackReducer') - .unmock('NavigationStateUtils'); - -jest.setMock('React', {Component() {}, PropTypes: {}}); - -const NavigationStackReducer = require('NavigationStackReducer'); -const NavigationRootContainer = require('NavigationRootContainer'); - -describe('NavigationStackReducer', () => { - - it('provides default/initial state', () => { - const initialState = { - routes: [ - {key: 'a'}, - ], - index: 0, - key: 'myStack', - }; - const reducer = NavigationStackReducer({ - getPushedReducerForAction: (action) => null, - getReducerForState: (state) => () => state, - initialState, - }); - const dummyAction = {type: 'dummyAction'}; - expect(reducer(null, dummyAction)).toBe(initialState); - }); - - it('handles basic reducer pushing', () => { - const reducer = NavigationStackReducer({ - getPushedReducerForAction: (action) => { - if (action.type === 'TestPushAction') { - return (state) => state || {key: action.testValue}; - } - return null; - }, - getReducerForState: (state) => () => state, - initialState: { - routes: [ - {key: 'first'}, - ], - index: 0, - key: 'myStack' - } - }); - const state1 = reducer(null, {type: 'default'}); - expect(state1.routes.length).toBe(1); - expect(state1.routes[0].key).toBe('first'); - expect(state1.index).toBe(0); - - const action = {type: 'TestPushAction', testValue: 'second'}; - const state2 = reducer(state1, action); - expect(state2.routes.length).toBe(2); - expect(state2.routes[0].key).toBe('first'); - expect(state2.routes[1].key).toBe('second'); - expect(state2.index).toBe(1); - }); - - it('handles BackAction', () => { - const reducer = NavigationStackReducer({ - getPushedReducerForAction: (action) => { - if (action.type === 'TestPushAction') { - return (state) => state || {key: action.testValue}; - } - return null; - }, - getReducerForState: (state) => () => state, - initialState: { - routes: [ - {key: 'a'}, - {key: 'b'}, - ], - index: 1, - key: 'myStack', - }, - }); - - const state1 = reducer(null, {type: 'MyDefaultAction'}); - expect(state1.routes[0].key).toBe('a'); - expect(state1.routes[1].key).toBe('b'); - expect(state1.routes.length).toBe(2); - expect(state1.index).toBe(1); - expect(state1.key).toBe('myStack'); - - const state2 = reducer(state1, NavigationRootContainer.getBackAction()); - expect(state2.routes[0].key).toBe('a'); - expect(state2.routes.length).toBe(1); - expect(state2.index).toBe(0); - - const state3 = reducer(state2, NavigationRootContainer.getBackAction()); - expect(state3).toBe(state2); - }); - - it('allows inner reducers to handle back actions', () => { - const subReducer = NavigationStackReducer({ - getPushedReducerForAction: () => {}, - initialState: { - routes: [ - {key: 'first'}, - {key: 'second'}, - ], - index: 1, - key: 'myInnerStack' - }, - }); - - const reducer = NavigationStackReducer({ - getPushedReducerForAction: (action) => { - if (action.type === 'TestPushAction') { - return subReducer; - } - - return null; - }, - getReducerForState: (state) => { - if (state.key === 'myInnerStack') { - return subReducer; - } - return () => state; - }, - initialState: { - routes: [ - {key: 'a'}, - ], - index: 0, - key: 'myStack' - } - }); - - const state1 = reducer(null, {type: 'MyDefaultAction'}); - const state2 = reducer(state1, {type: 'TestPushAction'}); - expect(state2.routes.length).toBe(2); - expect(state2.routes[0].key).toBe('a'); - expect(state2.routes[1].key).toBe('myInnerStack'); - expect(state2.routes[1].routes.length).toBe(2); - expect(state2.routes[1].routes[0].key).toBe('first'); - expect(state2.routes[1].routes[1].key).toBe('second'); - - const state3 = reducer(state2, NavigationRootContainer.getBackAction()); - expect(state3.routes.length).toBe(2); - expect(state3.routes[0].key).toBe('a'); - expect(state3.routes[1].key).toBe('myInnerStack'); - expect(state3.routes[1].routes.length).toBe(1); - expect(state3.routes[1].routes[0].key).toBe('first'); - }); -}); diff --git a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationTabsReducer-test.js b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationTabsReducer-test.js deleted file mode 100644 index f97379cb738b2d..00000000000000 --- a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationTabsReducer-test.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @flow-broken - */ -'use strict'; - -jest - .unmock('NavigationTabsReducer') - .unmock('NavigationFindReducer') - .unmock('NavigationStateUtils'); - -const NavigationTabsReducer = require('NavigationTabsReducer'); - -const { - JumpToAction, -} = NavigationTabsReducer; - -describe('NavigationTabsReducer', () => { - - it('handles JumpTo with index', () => { - let reducer = NavigationTabsReducer({ - tabReducers: [ - (tabState, action) => tabState || 'a', - (tabState, action) => tabState || 'b', - (tabState, action) => tabState || 'c', - ], - initialIndex: 1, - }); - - let navState = reducer(); - - expect(navState.routes[0]).toBe('a'); - expect(navState.routes[1]).toBe('b'); - expect(navState.routes[2]).toBe('c'); - expect(navState.routes.length).toBe(3); - expect(navState.index).toBe(1); - - navState = reducer( - navState, - JumpToAction(2) - ); - - expect(navState.routes[0]).toEqual('a'); - expect(navState.routes[1]).toEqual('b'); - expect(navState.routes[2]).toEqual('c'); - expect(navState.routes.length).toBe(3); - expect(navState.index).toBe(2); - }); - -}); From 8324e92f76674bded61478fa33263b82341eabfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Wed, 8 Jun 2016 13:17:19 -0700 Subject: [PATCH 289/843] Streamline Getting Started Instructions Summary: The Getting Started guide is one of the first documents a new user will encounter. This pull request aims to minimize the amount of time it takes to get a React Native app up and running. * The original section title, "Required prerequisites", is redundant. "Installing React Native" is a better description of what this section is about. * Detailed installation instructions for each of the required tools are delegated to the first party instructions where available. * If the installation instructions already take care of installing the latest version, there's no need to warn the user about the minimum required version. * Assume the user is familiar with Homebrew or Chocolatey, and defer installation instructions to the package manager's website. The installation and explanation of what a package manager is is out of scope within this document. * Link to Node.js package manager instructions and let savvy Linux users use the pack Closes https://github.com/facebook/react-native/pull/8010 Differential Revision: D3407029 Pulled By: JoelMarcey fbshipit-source-id: c8b25d5b176c40eb58e5d7d3c6f13d43cde65166 --- docs/Debugging.md | 94 +++-- docs/QuickStart-GettingStarted.md | 613 ++++-------------------------- docs/Troubleshooting.md | 15 +- 3 files changed, 134 insertions(+), 588 deletions(-) diff --git a/docs/Debugging.md b/docs/Debugging.md index e9378c63ca8d5d..a3143a77811157 100644 --- a/docs/Debugging.md +++ b/docs/Debugging.md @@ -7,59 +7,73 @@ permalink: docs/debugging.html next: testing --- -## Debugging React Native Apps -To access the in-app developer menu: +## In-app Errors and Warnings -1. On iOS shake the device or press `control + ⌘ + z` in the simulator. -2. On Android shake the device or press hardware menu button (available on older devices and in most of the emulators, e.g. in [genymotion](https://www.genymotion.com) you can press `⌘ + m` or `F2` to simulate hardware menu button click). You can also install [Frappé](http://getfrappe.com), a tool for OS X, which allows you to emulate shaking of devices remotely. You can use `⌘ + Shift + R` as a shortcut to trigger a **shake** from Frappé. +Errors and warnings are displayed inside your app in development builds. -> Hint +### Errors -> To disable the developer menu for production builds: -> -> 1. For iOS open your project in Xcode and select `Product` → `Scheme` → `Edit Scheme...` (or press `⌘ + <`). Next, select `Run` from the menu on the left and change the Build Configuration to `Release`. -> 2. For Android, by default, developer menu will be disabled in release builds done by gradle (e.g with gradle `assembleRelease` task). Although this behavior can be customized by passing proper value to `ReactInstanceManager#setUseDeveloperSupport`. +In-app errors are displayed in a full screen alert with a red background inside your app. This screen is known as a RedBox. You can use `console.error()` to manually trigger one. -### Android logging -Run `adb logcat *:S ReactNative:V ReactNativeJS:V` in a terminal to see your Android app's logs. +### Warnings -### Reload -Selecting `Reload` (or pressing `⌘ + r` in the iOS simulator) will reload the JavaScript that powers your application. If you have added new resources (such as an image to `Images.xcassets` on iOS or to `res/drawable` folder on Android) or modified any native code (Objective-C/Swift code on iOS or Java/C++ code on Android), you will need to re-build the app for the changes to take effect. +Warnings will be displayed on screen with a yellow background. These alerts are known as YellowBoxes. Click on the alerts to show more information or to dismiss them. -### YellowBox/RedBox -Using `console.warn` will display an on-screen log on a yellow background. Click on this warning to show more information about it full screen and/or dismiss the warning. +As with a RedBox, you can use `console.warn()` to trigger a YellowBox. -You can use `console.error` to display a full screen error on a red background. +YellowBoxes can be disabled during development by using `console.disableYellowBox = true;`. Specific warnings can be ignored programmatically by setting an array of prefixes that should be ignored: `console.ignoredYellowBox = ['Warning: ...'];` -By default, the warning box is enabled in `__DEV__`. Set the following flag to disable it: -```js -console.disableYellowBox = true; -console.warn('YellowBox is disabled.'); -``` -Specific warnings can be ignored programmatically by setting the array: -```js -console.ignoredYellowBox = ['Warning: ...']; -``` -Strings in `console.ignoredYellowBox` can be a prefix of the warning that should be ignored. +> RedBoxes and YellowBoxes are automatically disabled in release (production) builds. -### Chrome Developer Tools -To debug the JavaScript code in Chrome, select `Debug JS Remotely` from the developer menu. This will open a new tab at [http://localhost:8081/debugger-ui](http://localhost:8081/debugger-ui). +## Accessing the In-App Developer Menu -In Chrome, press `⌘ + option + i` or select `View` → `Developer` → `Developer Tools` to toggle the developer tools console. Enable [Pause On Caught Exceptions](http://stackoverflow.com/questions/2233339/javascript-is-there-a-way-to-get-chrome-to-break-on-all-errors/17324511#17324511) for a better debugging experience. +You can access the developer menu by shaking your device. You can also use the `Command⌘ + D` keyboard shortcut when your app is running in the iPhone Simulator, or `Command⌘ + M` when running in an Android emulator. -To debug on a real device: +> The Developer Menu is disabled in release (production) builds. -1. On iOS - open the file [`RCTWebSocketExecutor.m`](https://github.com/facebook/react-native/blob/master/Libraries/WebSocket/RCTWebSocketExecutor.m) and change `localhost` to the IP address of your computer. Shake the device to open the development menu with the option to start debugging. -2. On Android, if you're running Android 5.0+ device connected via USB you can use `adb` command line tool to setup port forwarding from the device to your computer. For that run: `adb reverse tcp:8081 tcp:8081` (see [this link](http://developer.android.com/tools/help/adb.html) for help on `adb` command). Alternatively, you can [open dev menu](#debugging-react-native-apps) on the device and select `Dev Settings`, then update `Debug server host for device` setting to the IP address of your computer. +## Reloading JavaScript -### Custom JavaScript debugger -To use a custom JavaScript debugger define the `REACT_DEBUGGER` environment variable to a command that will start your custom debugger. That variable will be read from the Packager process. If that environment variable is set, selecting `Debug JS Remotely` from the developer menu will execute that command instead of opening Chrome. The exact command to be executed is the contents of the REACT_DEBUGGER environment variable followed by the space separated paths of all project roots (e.g. If you set REACT_DEBUGGER="node /path/to/launchDebugger.js --port 2345 --type ReactNative" then the command "node /path/to/launchDebugger.js --port 2345 --type ReactNative /path/to/reactNative/app" will end up being executed). Custom debugger commands executed this way should be short-lived processes, and they shouldn't produce more than 200 kilobytes of output. +Selecting `Reload` from the Developer Menu will reload the JavaScript that powers your application. You can also press `Command⌘ + R` in the iOS Simulator, or press `R` twice on Android emulators. -### Live Reload -This option allows for your JS changes to trigger automatic reload on the connected device/emulator. To enable this option: +You will need to rebuild your app for changes to take effect in certain situations: -1. On iOS, select `Enable Live Reload` via the developer menu to have the application automatically reload when changes are made to the JavaScript. -2. On Android, [launch dev menu](#debugging-react-native-apps), go to `Dev Settings` and select `Auto reload on JS change` option +* You have added new resources to your native app's bundle, such as an image in `Images.xcassets` on iOS or in `res/drawable` folder on Android. +* You have modified native code (Objective-C/Swift on iOS or Java/C++ on Android). -### FPS (Frames per Second) Monitor -On `0.5.0-rc` and higher versions, you can enable a FPS graph overlay in the developers menu in order to help you debug performance problems. +### Automatic reloading + +You may enable Live Reload to automatically trigger a reload whenever your JavaScript code changes. + +Live Reload is available on iOS via the Developer Menu. On Android, select "Dev Settings" from the Developer Menu and enable "Auto reload on JS change". + +## Accessing logs + +To view detailed logs on iOS, open your app in Xcode, then Build and Run your app on a device or the iPhone Simulator. The console should appear automatically after the app launches. + +Run `adb logcat *:S ReactNative:V ReactNativeJS:V` in a terminal to display the logs for an Android app running on a device or an emulator. + +## Chrome Developer Tools + +To debug the JavaScript code in Chrome, select `Debug JS Remotely` from the Developer Menu. This will open a new tab at [http://localhost:8081/debugger-ui](http://localhost:8081/debugger-ui). + +In Chrome, press `Command⌘ + Option⌥ + I` or select `View` → `Developer` → `Developer Tools` to toggle the developer tools console. Enable [Pause On Caught Exceptions](http://stackoverflow.com/questions/2233339/javascript-is-there-a-way-to-get-chrome-to-break-on-all-errors/17324511#17324511) for a better debugging experience. + +### Debugging on a device with Chrome Developer Tools + +On iOS devices, open the file [`RCTWebSocketExecutor.m`](https://github.com/facebook/react-native/blob/master/Libraries/WebSocket/RCTWebSocketExecutor.m) and change `localhost` to the IP address of your computer, then select `Debug JS Remotely` from the Developer Menu. + +On Android 5.0+ devices connected via USB, you can use the [`adb` command line tool](http://developer.android.com/tools/help/adb.html) to setup port forwarding from the device to your computer: + +`adb reverse tcp:8081 tcp:8081` + +Alternatively, select `Dev Settings` from the Developer Menu, then update the `Debug server host for device` setting to match the IP address of your computer. + +### Debugging using a custom JavaScript debugger + +To use a custom JavaScript debugger in place of Chrome Developer Tools, set the `REACT_DEBUGGER` environment variable to a command that will start your custom debugger. You can then select `Debug JS Remotely` from the Developer Menu to start debugging. + +> The debugger will receive a list of all project roots, separated by a space. For example, if you set `REACT_DEBUGGER="node /path/to/launchDebugger.js --port 2345 --type ReactNative"`, then the command `node /path/to/launchDebugger.js --port 2345 --type ReactNative /path/to/reactNative/app` will be used to start your debugger. Custom debugger commands executed this way should be short-lived processes, and they shouldn't produce more than 200 kilobytes of output. + +## FPS (Frames per Second) Monitor + +You can enable a FPS graph overlay in the Developer Menu in order to help you debug performance problems. diff --git a/docs/QuickStart-GettingStarted.md b/docs/QuickStart-GettingStarted.md index cd30ef889ea67d..249296abf0e91f 100644 --- a/docs/QuickStart-GettingStarted.md +++ b/docs/QuickStart-GettingStarted.md @@ -56,35 +56,31 @@ block { display: none; } - + -## Installation +## Installing React Native -### Required Prerequisites +There's a few things you need to install first. You will need Node.js, the React Native command line tools, Watchman, and Xcode. -#### Homebrew + -[Homebrew](http://brew.sh/), in order to install the required NodeJS, in addition to some -recommended installs. +## Installing React Native -``` -/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" -``` +There's a few things you need to install first. You will need Node.js, the React Native command line tools, Watchman, and Android Studio. -#### Node + -Use Homebrew to install [Node.js](https://nodejs.org/). +#### Node.js -> NodeJS 4.0 or greater is required for React Native. The default Homebrew package for Node is -> currently 6.0, so that is not an issue. +We recommend installing Node.js via [Homebrew](http://brew.sh/), a popular package manager for OS X: ``` brew install node ``` -#### React Native Command Line Tools +#### React Native command line tools -The React Native command line tools allow you to easily create and initialize projects, etc. +Use Node's package manager to install the React Native command line tools. These will allow you to easily create your first React Native project. ``` npm install -g react-native-cli @@ -93,69 +89,6 @@ npm install -g react-native-cli > If you see the error, `EACCES: permission denied`, please run the command: > `sudo npm install -g react-native-cli`. - - -#### Xcode - -[Xcode](https://developer.apple.com/xcode/downloads/) 7.0 or higher is required. You can install Xcode via the App Store or [Apple developer downloads](https://developer.apple.com/xcode/downloads/). This will install the Xcode IDE and Xcode Command Line Tools. - -> While generally installed by default, you can verify that the Xcode Command Line Tools are installed by launching Xcode and selecting `Xcode | Preferences | Locations` and ensuring there is a version of the command line tools shown in the `Command Line Tools` list box. The Command Line Tools give you `git`, etc. - - - -#### Android Studio - -[Android Studio](http://developer.android.com/sdk/index.html) 2.0 or higher. - -> Android Studio requires the Java Development Kit [JDK] 1.8 or higher. You can type -> `javac -version` to see what version you have, if any. If you do not meet the JDK requirement, -> you can -> [download it](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html). - -Android Studio will provide you the Android SDK and emulator required to run and test your React -Native apps. - -> Unless otherwise mentioned, keep all the setup defaults intact. For example, the -> `Android Support Repository` is installed automatically with Android Studio, and we need that -> for React Native. - -You will need to customize your installation: - -- Choose a `Custom` installation - -![custom installation](img/react-native-android-studio-custom-install.png) - -- Choose both `Performance` and `Android Virtual Device` - -![additional installs](img/react-native-android-studio-additional-installs.png) - -- After installation, choose `Configure | SDK Manager` from the Android Studio welcome window. - -![configure sdk](img/react-native-android-studio-configure-sdk.png) - -- In the `SDK Platforms` window, choose `Show Package Details` and under `Android 6.0 (Marshmallow)`, make sure that `Google APIs`, `Intel x86 Atom System Image`, `Intel x86 Atom_64 System Image`, and `Google APIs Intel x86 Atom_64 System Image` are checked. - -![platforms](img/react-native-android-studio-android-sdk-platforms.png) - -- In the `SDK Tools` window, choose `Show Package Details` and under `Android SDK Build Tools`, make sure that `Android SDK Build-Tools 23.0.1` is selected. - -![build tools](img/react-native-android-studio-android-sdk-build-tools.png) - -#### ANDROID_HOME Environment Variable - -Ensure the `ANDROID_HOME` environment variable points to your existing Android SDK. To do that, add -this to your `~/.bashrc`, `~/.bash_profile` (or whatever your shell uses) and re-open your terminal: - -``` -# If you installed the SDK without Android Studio, then it may be something like: -# /usr/local/opt/android-sdk -export ANDROID_HOME=~/Library/Android/sdk -``` - - - -### Highly Recommended Installs - #### Watchman [Watchman](https://facebook.github.io/watchman/docs/install.html) is a tool by Facebook for watching @@ -165,513 +98,103 @@ changes in the filesystem. It is recommended you install it for better performan brew install watchman ``` -#### Flow + -[Flow](http://www.flowtype.org), for static typechecking of your React Native code (when using -Flow as part of your codebase). +#### Xcode - -``` -brew install flow -``` +You can install Xcode via the [Mac App Store](https://itunes.apple.com/us/app/xcode/id497799835?mt=12), or download it directly from the [Apple Developer portal](https://developer.apple.com/xcode/downloads/). -#### Add Android Tools Directory to your `PATH` - -You can add the Android tools directory on your `PATH` in case you need to run any of the Android -tools from the command line such as `android avd`. In your `~/.bash` or `~/.bash_profile`: +#### Android Studio -``` -# Your exact string here may be different. -PATH="~/Library/Android/sdk/tools:~/Library/Android/sdk/platform-tools:${PATH}" -export PATH -``` +Download and install [Android Studio](https://developer.android.com/studio/install.html). #### Gradle Daemon -Enable [Gradle Daemon](https://docs.gradle.org/2.9/userguide/gradle_daemon.html) which greatly improves incremental build times for changes in java code. - -### Other Optional Installs - -#### Git - -Git version control. If you have installed [Xcode](https://developer.apple.com/xcode/), Git is -already installed, otherwise run the following: - -``` -brew install git -``` - - - -#### Nuclide - -[Nuclide](http://nuclide.io) is an IDE from Facebook providing a first-class development environment -for writing, [running](http://nuclide.io/docs/platforms/react-native/#running-applications) and -[debugging](http://nuclide.io/docs/platforms/react-native/#debugging) -[React Native](http://nuclide.io/docs/platforms/react-native/) applications. - -Get started with Nuclide [here](http://nuclide.io/docs/quick-start/getting-started/). - - - -#### Genymotion - -Genymotion is an alternative to the stock Google emulator that comes with Android Studio. -However, it's only free for personal use. If you want to use Genymotion, see below. - -1. Download and install [Genymotion](https://www.genymotion.com/). -2. Open Genymotion. It might ask you to install VirtualBox unless you already have it. -3. Create a new emulator and start it. -4. To bring up the developer menu press ⌘+M - -### Troubleshooting - -#### Virtual Device Not Created When Installing Android Studio - -There is a [known bug](https://code.google.com/p/android/issues/detail?id=207563) on some versions -of Android Studio where a virtual device will not be created, even though you selected it in the -installation sequence. You may see this at the end of the installation: - -``` -Creating Android virtual device -Unable to create a virtual device: Unable to create Android virtual device -``` - -If you see this, run `android avd` and create the virtual device manually. - -![avd](img/react-native-android-studio-avd.png) - -Then select the new device in the AVD Manager window and click `Start...`. - -#### Shell Command Unresponsive Exception - -If you encounter: - -``` -Execution failed for task ':app:installDebug'. - com.android.builder.testing.api.DeviceException: com.android.ddmlib.ShellCommandUnresponsiveException -``` - -try downgrading your Gradle version to 1.2.3 in `/android/build.gradle` (https://github.com/facebook/react-native/issues/2720) +While optional, enabling [Gradle Daemon](https://docs.gradle.org/2.9/userguide/gradle_daemon.html) will greatly improve incremental build times for changes in Java code. -## Installation - -### Required Prerequisites - - - -#### Chocolatey +## Installing React Native -[Chocolatey](https://chocolatey.org) is a package manager for Windows similar to `yum` and -`apt-get`. See the [website](https://chocolatey.org) for updated instructions, but installing from -the Terminal should be something like: - -``` -@powershell -NoProfile -ExecutionPolicy Bypass -Command "iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1'))" && SET PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin -``` - -> Normally when you run Chocolatey to install a package, you should run your Terminal as -> Administrator. - -#### Python 2 - -Fire up the Terminal and use Chocolatey to install Python 2. - -> Python 3 will currently not work when initializing a React Native project. - -``` -choco install python2 -``` +There's a few things you need to install first. You will need Node.js, the React Native command line tools, Watchman, and Android Studio. -#### Node +#### Node.js -Fire up the Terminal and type the following commands to install NodeJS from the NodeSource -repository: - -``` -sudo apt-get install -y build-essential -curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash - -sudo apt-get install -y nodejs -sudo ln -s /usr/bin/nodejs /usr/bin/node -``` +Follow the [installation instructions for your Linux distribution](https://nodejs.org/en/download/package-manager/) to install Node.js 4 or newer. -Fire up the Termimal and use Chocolatey to install NodeJS. +We recommend installing Node.js via [Chocolatey](https://chocolatey.org), a popular package manager for Windows. Open a Command Prompt as Administrator, then run the following command: ``` choco install nodejs.install ``` - - -#### React Native Command Line Tools +##### Python -The React Native command line tools allow you to easily create and initialize projects, etc. +The React Native command line tools require Python2. Install it using Chocolatey: ``` -npm install -g react-native-cli +choco install python2 ``` -> If you see the error, `EACCES: permission denied`, please run the command: -> `sudo npm install -g react-native-cli`. - -#### Android Studio - -[Android Studio](http://developer.android.com/sdk/index.html) 2.0 or higher. - -> Android Studio requires the Java Development Kit [JDK] 1.8 or higher. You can type -> `javac -version` to see what version you have, if any. If you do not meet the JDK requirement, -> you can -> [download it](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html), -> or use a pacakage manager to install it (e.g. `choco install jdk8`, -> `apt-get install default-jdk`). - -Android Studio will provide you the Android SDK and emulator required to run and test your React -Native apps. - -> Unless otherwise mentioned, keep all the setup defaults intact. For example, the -> `Android Support Repository` is installed automatically with Android Studio, and we need that -> for React Native. - - - -You will need to customize your installation: - -- Choose a `Custom` installation - -![custom installation](img/react-native-android-studio-custom-install-linux.png) - -- Choose `Android Virtual Device` - -![additional installs](img/react-native-android-studio-additional-installs-linux.png) - - - -- Make sure all components are checked for the install, particularly the `Android SDK` and `Android Device Emulator`. - -- After the initial install, choose a `Custom` installation. - -![custom installation](img/react-native-android-studio-custom-install-windows.png) - -- Verify installed components, particularly the emulator and the HAXM accelerator. They should be checked. - -![verify installs](img/react-native-android-studio-verify-installs-windows.png) - - - -- After installation, choose `Configure | SDK Manager` from the Android Studio welcome window. - - - -![configure sdk](img/react-native-android-studio-configure-sdk-linux.png) - - - -![configure sdk](img/react-native-android-studio-configure-sdk-windows.png) - - - -- In the `SDK Platforms` window, choose `Show Package Details` and under `Android 6.0 (Marshmallow)`, make sure that `Google APIs`, `Intel x86 Atom System Image`, `Intel x86 Atom_64 System Image`, and `Google APIs Intel x86 Atom_64 System Image` are checked. - - - -![platforms](img/react-native-android-studio-android-sdk-platforms-linux.png) - - - -![platforms](img/react-native-android-studio-android-sdk-platforms-windows.png) - -- In the `SDK Tools` window, choose `Show Package Details` and under `Android SDK Build Tools`, make sure that `Android SDK Build-Tools 23.0.1` is selected. +#### React Native command line tools - - -![build tools](img/react-native-android-studio-android-sdk-build-tools-linux.png) - - - -![build tools](img/react-native-android-studio-android-sdk-build-tools-windows.png) - - - -#### ANDROID_HOME Environment Variable - -Ensure the `ANDROID_HOME` environment variable points to your existing Android SDK. - - - -To do that, add this to your `~/.bashrc`, `~/.bash_profile` (or whatever your shell uses) and -re-open your terminal: +Use Node's package manager to install the React Native command line tools. These will allow you to easily create your first React Native project. ``` -# If you installed the SDK without Android Studio, then it may be something like: -# /usr/local/opt/android-sdk; Generally with Android Studio, the SDK is installed here... -export ANDROID_HOME=~/Android/Sdk +npm install -g react-native-cli ``` -> You need to restart the Terminal to apply the new environment variables (or `source` the relevant -> bash file). - - - -Go to `Control Panel` -> `System and Security` -> `System` -> `Change settings` -> -`Advanced System Settings` -> `Environment variables` -> `New` - -> Your path to the SDK will vary to the one shown below. - -![env variable](img/react-native-android-sdk-environment-variable-windows.png) +> If you see the error, `EACCES: permission denied`, please run the command: +> `sudo npm install -g react-native-cli`. -> You need to restart the Command Prompt (Windows) to apply the new environment variables. + - +#### Android Studio -### Highly Recommended Installs +Download and install [Android Studio](https://developer.android.com/studio/install.html). #### Watchman -Watchman is a tool by Facebook for watching changes in the filesystem. It is recommended you install -it for better performance. - -> This also helps avoid a node file-watching bug. - -Type the following into your terminal to compile watchman from source and install it: - -``` -git clone https://github.com/facebook/watchman.git -cd watchman -git checkout v4.5.0 # the latest stable release -./autogen.sh -./configure -make -sudo make install -``` - -#### Flow - -[Flow](http://www.flowtype.org), for static typechecking of your React Native code (when using -Flow as part of your codebase). - -Type the following in the terminal: - -``` -npm install -g flow-bin -``` +[Watchman](https://facebook.github.io/watchman) is a tool by Facebook for watching changes in the filesystem. It is recommended you install +it for better performance. You can follow the [Watchman installation guide](https://facebook.github.io/watchman/docs/install.html#installing-from-source) to compile and install from source. #### Gradle Daemon -Enable [Gradle Daemon](https://docs.gradle.org/2.9/userguide/gradle_daemon.html) which greatly -improves incremental build times for changes in java code. - - - -``` -touch ~/.gradle/gradle.properties && echo "org.gradle.daemon=true" >> ~/.gradle/gradle.properties -``` - - - -``` -(if not exist "%USERPROFILE%/.gradle" mkdir "%USERPROFILE%/.gradle") && (echo org.gradle.daemon=true >> "%USERPROFILE%/.gradle/gradle.properties") -``` - - - -#### Android Emulator Accelerator - -You may have seen the following screen when installing Android Studio. - -![accelerator](img/react-native-android-studio-kvm-linux.png) - -If your system supports KVM, you should install the -[Intel Android Emulator Accelerator](https://software.intel.com/en-us/android/articles/speeding-up-the-android-emulator-on-intel-architecture#_Toc358213272). - - - -#### Add Android Tools Directory to your `PATH` - -You can add the Android tools directory on your `PATH` in case you need to run any of the Android -tools from the command line such as `android avd`. - - - -In your `~/.bashrc` or `~/.bash_profile`: - -``` -# Your exact string here may be different. -PATH="~/Android/Sdk/tools:~/Android/Sdk/platform-tools:${PATH}" -export PATH -``` - - - -Go to `Control Panel` -> `System and Security` -> `System` -> `Change settings` -> -`Advanced System Settings` -> `Environment variables` -> highlight `PATH` -> `Edit...` - -> The location of your Android tools directories will vary. - -![env variable](img/react-native-android-tools-environment-variable-windows.png) - - - -### Other Optional Installs - -#### Git - - - -Install Git [via your package manager](https://git-scm.com/download/linux) -(e.g., `sudo apt-get install git-all`). - - - -You can use Chocolatey to install `git` via: - -``` -choco install git -``` - -Alternatively, you can download and install [Git for Windows](https://git-for-windows.github.io/). -During the setup process, choose "Run Git from Windows Command Prompt", which will add `git` to your -`PATH` environment variable. - - - -#### Nuclide - -[Nuclide] is an IDE from Facebook providing a first-class development environment for writing, -[running](http://nuclide.io/docs/platforms/react-native/#running-applications) and -[debugging](http://nuclide.io/docs/platforms/react-native/#debugging) -[React Native](http://nuclide.io/docs/platforms/react-native/) applications. - -Get started with Nuclide [here](http://nuclide.io/docs/quick-start/getting-started/). - - - -#### Genymotion - -Genymotion is an alternative to the stock Google emulator that comes with Android Studio. -However, it's only free for personal use. If you want to use the stock Google emulator, see below. - -1. Download and install [Genymotion](https://www.genymotion.com/). -2. Open Genymotion. It might ask you to install VirtualBox unless you already have it. -3. Create a new emulator and start it. -4. To bring up the developer menu press ⌘+M - - - -#### Visual Studio Emulator for Android - -The [Visual Studio Emulator for Android](https://www.visualstudio.com/en-us/features/msft-android-emulator-vs.aspx) -is a free android emulator that is hardware accelerated via Hyper-V. It is an alternative to the -stock Google emulator that comes with Android Studio. It doesn't require you to install Visual -Studio at all. - -To use it with react-native you just have to add a key and value to your registry: - -1. Open the Run Command (Windows+R) -2. Enter `regedit.exe` -3. In the Registry Editor navigate to `HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Android SDK Tools` -4. Right Click on `Android SDK Tools` and choose `New > String Value` -5. Set the name to `Path` -6. Double Click the new `Path` Key and set the value to `C:\Program Files\Android\sdk`. The path value might be different on your machine. - -You will also need to run the command `adb reverse tcp:8081 tcp:8081` with this emulator. - -Then restart the emulator and when it runs you can just do `react-native run-android` as usual. - - - -### Troubleshooting - -#### Unable to run mksdcard SDK Tool - -When installing Android Studio, if you get the error: - -``` -Unable to run mksdcard SDK tool -``` - -then install the standard C++ library: - -``` -sudo apt-get install lib32stdc++6 -``` - -#### Virtual Device Not Created When Installing Android Studio - -There is a [known bug](https://code.google.com/p/android/issues/detail?id=207563) on some versions -of Android Studio where a virtual device will not be created, even though you selected it in the -installation sequence. You may see this at the end of the installation: - - - -``` -Creating Android virtual device -Unable to create a virtual device: Unable to create Android virtual device -``` - - - -![no virtual device](img/react-native-android-studio-no-virtual-device-windows.png) - - - -If you see this, run `android avd` and create the virtual device manually. - - - -![avd](img/react-native-android-studio-avd-linux.png) - - - -![avd](img/react-native-android-studio-avd-windows.png) - - - -Then select the new device in the AVD Manager window and click `Start...`. - - - -#### Shell Command Unresponsive Exception - -In case you encounter - -``` -Execution failed for task ':app:installDebug'. - com.android.builder.testing.api.DeviceException: com.android.ddmlib.ShellCommandUnresponsiveException -``` - -try downgrading your Gradle version to 1.2.3 in `/android/build.gradle` (https://github.com/facebook/react-native/issues/2720) +While optional, enabling [Gradle Daemon](https://docs.gradle.org/2.9/userguide/gradle_daemon.html) will greatly improve incremental build times for changes in Java code. -## Testing Installation +## Testing your React Native Installation +Use the React Native command line tools to generate a new React Native project called "AwesomeProject", then run `react-native run-ios` inside the newly created folder. + ``` react-native init AwesomeProject cd AwesomeProject react-native run-ios ``` +If everything is set up correctly, you should see your new app running in the iOS Simulator shortly. + > You can also > [open the `AwesomeProject`](http://nuclide.io/docs/quick-start/getting-started/#adding-a-project) > folder in [Nuclide](http://nuclide.io) and @@ -680,12 +203,16 @@ react-native run-ios +Use the React Native command line tools to generate a new React Native project called "AwesomeProject", then run `react-native run-android` inside the newly created folder. + ``` react-native init AwesomeProject cd AwesomeProject react-native run-android ``` +If everything is set up correctly, you should see your new app running in your Android emulator shortly. + > You can also > [open the `AwesomeProject`](http://nuclide.io/docs/quick-start/getting-started/#adding-a-project) > folder in [Nuclide](http://nuclide.io) and @@ -693,24 +220,23 @@ react-native run-android -### Modifying Project +### Modifying your app -Now that you successfully started the project, let's modify it: +Now that you have successfully run the app, let's modify it. -- Open `index.ios.js` in your text editor of choice (e.g. [Nuclide](http://nuclide.io/docs/platforms/react-native/)) and edit some lines. -- Hit ⌘-R in your iOS simulator to reload the app and see your change! +- Open `index.ios.js` in your text editor of choice and edit some lines. +- Hit `Command⌘ + R` in your iOS Simulator to reload the app and see your change! -- Open `index.android.js` in your text editor of choice (e.g. [Nuclide](http://nuclide.io/docs/platforms/react-native/)) and edit some lines. -- Press the `R` key twice **OR** open the menu (F2 by default, or ⌘-M in Genymotion) and select Reload JS to see your change! -- Run `adb logcat *:S ReactNative:V ReactNativeJS:V` in a terminal to see your app's logs +- Open `index.android.js` in your text editor of choice and edit some lines. +- Press the `R` key twice or select `Reload` from the Developer Menu to see your change! -### That's It +### That's it! Congratulations! You've successfully run and modified your first React Native app. @@ -718,7 +244,9 @@ Congratulations! You've successfully run and modified your first React Native ap -## Testing Installation +## Testing your React Native Installation + +Use the React Native command line tools to generate a new React Native project called "AwesomeProject", then run `react-native run-android` inside the newly created folder. ``` react-native init AwesomeProject @@ -726,33 +254,25 @@ cd AwesomeProject react-native run-android ``` - +If everything is set up correctly, you should see your new app running in your Android emulator shortly. -### Troubleshooting Run - -A common issue is that the packager is not started automatically when you run -`react-native run-android`. You can start it manually using: - -``` -cd AwesomeProject -react-native start -``` +> A common issue is that the packager is not started automatically when you run +`react-native run-android`. You can start it manually using `react-native start`. -Or if you hit a `ERROR Watcher took too long to load` on Windows, try increasing the timeout in [this file](https://github.com/facebook/react-native/blob/5fa33f3d07f8595a188f6fe04d6168a6ede1e721/packager/react-packager/src/DependencyResolver/FileWatcher/index.js#L16) (under your `node_modules/react-native/`). +> If you hit a `ERROR Watcher took too long to load` on Windows, try increasing the timeout in [this file](https://github.com/facebook/react-native/blob/5fa33f3d07f8595a188f6fe04d6168a6ede1e721/packager/react-packager/src/DependencyResolver/FileWatcher/index.js#L16) (under your `node_modules/react-native/`). -### Modifying Project +### Modifying your app -Now that you successfully started the project, let's modify it: +Now that you have successfully run the app, let's modify it. -- Open `index.android.js` in your text editor of choice (e.g. [Nuclide](http://nuclide.io/docs/platforms/react-native/)) and edit some lines. -- Press the `R` key twice **OR** open the menu (F2 by default, or ctrl-M in the emulator) and select Reload JS to see your change! -- Run `adb logcat *:S ReactNative:V ReactNativeJS:V` in a terminal to see your app's logs +- Open `index.android.js` in your text editor of choice and edit some lines. +- Press the `R` key twice or select `Reload` from the Developer Menu to see your change! -### That's It +### That's it! Congratulations! You've successfully run and modified your first React Native app. @@ -772,8 +292,7 @@ Congratulations! You've successfully run and modified your first React Native ap -- If you run into any issues getting started, see the [Troubleshooting page](docs/troubleshooting.html#content). - +- If you run into any issues getting started, see the [Troubleshooting](docs/troubleshooting.html#content) and [Debugging](docs/debugging.html#content) pages. @@ -781,7 +300,7 @@ Congratulations! You've successfully run and modified your first React Native ap - If you want to run on a physical device, see the [Running on Android Device page](docs/running-on-device-android.html#content). -- If you run into any issues getting started, see the [Troubleshooting page](docs/troubleshooting.html#content). +- If you run into any issues getting started, see the [Troubleshooting](docs/troubleshooting.html#content) and [Debugging](docs/debugging.html#content) pages. diff --git a/website/src/react-native/img/react-native-add-react-native-integration-example-high-scores.png b/website/src/react-native/img/react-native-add-react-native-integration-example-high-scores.png new file mode 100644 index 0000000000000000000000000000000000000000..6d07707ba8686a7b0731b2f18540e9b3ca9074fd GIT binary patch literal 7420 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 literal 0 HcmV?d00001 diff --git a/website/src/react-native/img/react-native-add-react-native-integration-example-home-screen.png b/website/src/react-native/img/react-native-add-react-native-integration-example-home-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..2b1b8b28735a4c01b67716ca9b826e51341c0b9f GIT binary patch literal 6610 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 literal 0 HcmV?d00001 diff --git a/website/src/react-native/img/react-native-add-react-native-integration-link.png b/website/src/react-native/img/react-native-add-react-native-integration-link.png new file mode 100644 index 0000000000000000000000000000000000000000..3d89eaf020ac13fa33e822a96c932dbd11a39c65 GIT binary patch literal 22551 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 R`uyT26-5n&qK8jk{SQqF!|(tA literal 0 HcmV?d00001 diff --git a/website/src/react-native/img/react-native-add-react-native-integration-wire-up.png b/website/src/react-native/img/react-native-add-react-native-integration-wire-up.png new file mode 100644 index 0000000000000000000000000000000000000000..43d2add57ba54053b7cee84fdc85a3ea0c5c2716 GIT binary patch literal 118362 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 YT_B8AlYAux ztQ49XlQpa{KHm1B{;t@xUc)3b`LFGnBo!EO`OrA?`qf1kpZf%PZu6I(=_CX5JD2b( zKFq=h55LNY8QO($oE~0ejB7)My5q;VwAYtTIt9HBIN3dQt*;P_$$}tT^@pcyw93ny zinb}J(wu;7O5}Y(`LGXastdaGVJ_RetotR_L#+sx-EWQ|{)4!?w1ioVg=2c`^y^HI+5?~NCE-!DH`!Kcy1282TpJWifb)weM7gK=--Q603 z-Zf_2MBcdUl+vc|Pw+EWN@(;Ad4@^&5v4+|3G{;gS%3@bPY!H0tGSqf> z*>*;iMEU?zl)(~0e~zRf5;(O}wdPu($kqAf;WVsj%%m!Aj_$$FJMcQIXJjCOj|EZ3 zJT$9ni+K?4$vpPTC>533R!a8dQ6R-y;|l4bun((lHMQQNuOVIDG|e6nm_Plbz3wCp zE(RidIM3Dlc+G|3p?xlVEiQA<5odezg|&bJ&il15SN!jg_9%0pJ(zbs@NkUO5Wb(b zxnJiDHT6z$toyoiuqwIV_YGq01MyftKkI~$)pd%2K0Lo92kl)SKa6?GZ5c+}wx50?k+_$jiO{TZezE+a{ z8S~pC#hw|dbmu3Ax=Q_`FE+E02x+5Spqs+83&*#a&NZ)-y;k;7{GCJ3bzSGF zA$uGn%Nj4Jm6!DnT+jK&xAU_K+wO`V&#|-ncXd~|b}4hem?Mq3>`_wYF^H>^oB5jU zx@{MY+2lW(95Ir|7x{%qx|U|X8fX&MI^(g?=6A%8vCxzdyIhUv45Idypl75_@?w?X(Gx5aNq*Z1*^Vcj7( z8e3`S`?DB8K{Y(~ac`<5L?W7!1aM+Q;k$kl5`H2C5dsi_h#=dWJpJ=Y|%j^MY5e^{O< z>UJKk6QPS$&(xq!ELrJ}41SKal#|ZmL(!`T-uHe5zEOjlMfS?!bOP(c4g$iGV7`MzBs?>e>;6~bJW!O;wg5kJi`dNIcjUpH=VNOxhc8P#B8MR-KBg?pzH21j_wg`Y+AC}YNlXv z{%z}sJnlRy#wPqpipZx&ndPzV>Z%sy#Ig=eTlIZ!t@51(+?F79F%nv`hdYhG%#_9$sHe>>2_jG^9WdH0uG|GEBo zmx}gR<>T*pcbQ?oH5?cbF*2bBcoBZaWi?(IlVs`!?2VY+FZZP;uapyjUK`8J3_wf~ zGsy({1JF4-_OOz){SBToL3gmI!7Z75eX+hR!P~^prI3-8dEU_(c52w1VAp22N|xN_ z;^!v-`_B_dAP~eT3stXqpJ~|t_1b++)Fs^ehua>_))-*iQnln;WW1qXBZp2g;qAQ= za6lwaQ*{tmrc8`nsFP_;PMhWNMP_CCh_8B4IA#|6O!nq9mM*LM)?!4bw{n>3X~>`K zA9p`$YUp5WhF}`S-27!B3q9a8f_Pj${v-{>)&WK&zSP%qJ`eZ;gDwC;VkiDn-Uk7p zXTi!37)3oD4q5WW(jCrC?o852>seG4$vV9$<;CiiP3d+|l3g>klh9P5ghb(>4(QjR zTv?hE`Dk_n3WPX2iOxCsUdzhq(8yU@m=MTaHhit>!Rx%J9VXvC!!47sc(Jj-c}XnU zB}q4usoE)+uQgkfLL;&$b9UI{vi~+myFM%0_UZ{sbOVtH1($c}ioM(5r~sW>ksP;f z(gA^MY`YoxB;V6ao`5qo^aor>mdFpKi6935^_Yn#lOw5>!s64XCjnG>=I&8;tfw$( z{<4jPfeFJ*no=6B!~$_MD1VIvNm|Zmjb94trGZclXaEy4miaZu4BsrALjCQAidh0a zEot9V^48K(OF+O@r8eWo8@`s+kD;;iK5gXDs|Tk0mAu9 z`W=JQPg0tVA22R>%M5hqztxu*j|!8ZtMtL)ew9YPuuU|E6wTReuFrx7ZMhEdGDA|f zLnlCB=t4>PAaEwV1FRs>aB$#o2V%hh75T+l?CX4ZW@ct}EEC|K&W)vfAF- zu~oOwA@PUYSE;r{g3Iw5-*AM8l^8z-HEPdty5?X%s_-MNaVZHk8P}W$3sQOwLJ*n9 zeC}*(BBARgJo0y0v2k|!HE|>55b?ub^&-h7KM0PGKZ@?K(z$o|c-Qn}l5}>)$+i%> ziW2wigdB0a8MdyH`EW?p^Ij}q{MkzI^S;uuLejJX);MAs2xPrcv?_*q+1V@eHf=SC zbd^J&QTPIVd?2oGZUAddI-OjpM16)eYFPSq&fsi}c|FLUpz^^2I-2(TBL(D_rdDrh zluit;B4)4G#;^`vf?6QXI&ueRPCny1MVxjKuD<@MAxI9YRGA5%7Cn8J1aY=YRad^V zLtouiS37muR;S(3_}Th;gJMk?On0f-AxXm~pQVn?#nD931zOQ5G0)UK*i{+HwQlf3 z4p^*k&xXUB=D6cEounYZ2`Ka8t>DXCYV!NBtUB(|RFrzNABNVCQ*q?S-I^$epGq|O z8U{3imwLVZR(9fwJ%s8B`=IsdbMe-f#+pK*(o6(RSFF0})-P;AyZR@2(-o#8$OI|s zIERx9)mYrD{EeR;y+=fv-1pQTs&Gj3M1D{u;q9)m{D2C5|HIW5TQ|A)o4fw=@%+7~ z3Ga-kUe}Sqz`&4ja;kAB)TN*Fy>Q0Bz=(>8SueK|Gxw%%&woQJkqndYo<%)~7123Vd&7G#)&xKVG?YvLMkPLXu|&1#t`l2u)iJ9^PC+@YL4V$9MSGJ6|~#7VW?pp4gNQ1x2niZo{tU7FtM8JJ$1d z4wA*GQS$-oH_R+;-br%_b3Zoco%2f5?KPz>3GI+kLNsCl3h-Lb#@007YU6!UgZHX! zG@L)LVS}~kd7J|n<3vZbcSI2EwzoIvqNgDp>i!1fGtqfFZ{7UubOx)`*YZr`|Hd#U z5_s6)$A3e(T^YV_K8nbFpSjv%`V}A=5qh}Yydt3qCgArX0H#`csnR@FBj>~SnGGO& zh!PPkO(1ijWWzse((PbpQ;xG9ANgqd-Oa0bB{#(c!C||se}K$tgE8xM#yLKpcRcRQ z*3efDk-jhIAZVD}$w9Hv-K$U+$8#ZsWUv;BHp30vwL>C%2vZ#u2i|tMEK1!fVViqW ztVzG)3G?FdR~7c~HiO25!e z+vp$9NqM;D8VA87jCj@ic?H!}RFAOnOitA{ZVWzYiL4aj1s=G=$W~u`_j6|a^*L*@ z{OAVnxFUI(RI(Mk!@*>ng zmV+ia-DM{`e3v-D*TjxpiEpNv9a^~OI2!x|zD;(N9+j>3hOqg;Z}?+7&#OSq-pPT2 zUK=V3d1~fL(zXNS^<*lyB@@&qP5Ip8A4Vdkfg%5oO#pwgdwm?=cAq2V#_RpqJat(7 zq3UO|tNp=GJrooaTS-f{18?AkS{9mf3tjF68~vO)Mr1L$k(RE4Cko_kNloXSP@$5t zW$g8J-G#5#8-EqB-_$L7&|3>|J@!h!v{6+g_~?#=#Q!1{yx892oX`#HYIU9`#o|d4Bub>Y{V7ZUcl&2E(dB~&l9+efr=Mb$t6eAdi8CHu7$f}OEVPU=72>>$ z*$IbGIp`tVvX_N1h@Bm-i||vu>Sa>xRZ2BSdcKv}ygnfK*i;8cWRr^A_GB^->+nHS zxrLn#No-^zQ=S*a;ZEb#+?$#b&7hloE+R`Mt?QJlLJQX~7mX81;&%_&)@6I2h`gJE zyxFT#?9V1gR8Jn6QSNEGzPa0!g$tBGEW_5G6c~*5hItu9{*7g1$KL|^H@P#>UGLu< z0f9k{51g$SlzH0UAD$&2%)qkjKn#sY{=jYy@r?uaw&>{Zq^ynph~^Woi)^{IOu`$T{_{{mY#IIMU; ziSu^e5xQqt(6KJbrEeP9TeJT=hlz#3YSuKkB1ty(mt68E;QF2-`F)3Jp+jy0ZrJVj zH)l16`O-Bu;W#orFbN&Fwg~@0W8hE0V!(HUIjFw4bwNpG1Qt3#HfWiweV4jVRK}=T zs9NzxskYZBd=mo&YeVk)_m1R3lp7^TLg4?c&_Xx2w2T%gQvI7!E^0C)cFe!AQ&f{U9QDK$0x_wV25O5VYQi3KwS1qA`l?}92SDgq*s zF}+YyLFKVF6xg~0@%&eZ>d@k9@?s+FY!kgkjkE~h)7!WD>6LcInzyghlHS&>$d{~Y z3QK#%Vi9LuhFK0Lhxp!-E*mCR>MO`SCHMXVb-|FN&%WzJDcm@e(GsjVxw)i(DFPe{ zk+7Rvqj9v6dYyIHU+Ap1y84Y&D$lIC{~VoFAC#0CLY$B%@M$2qoOddrL$y)=Y@jL( zq^M^i`z0N#9R&KCm+NNq@x3jXwq`x+Q~7(-4BMjOFiQ%{R;9_t zP!|~4n$j;VbSA+?t3S@(xAyxhr%6ttsT%(q0q>X*#Puln_=o^eXcyoKhe}9Tg1`jB zM)oBkKcD)I`2;+G=Y`bN5`;uRz~!5%BXFkXQ;?3)X|4PoN|Y(^&jmL4-Y1Hwd?KwX zqE9k$y)!&TB69Z z+U{L@n}xfJ7N=iI0hOMoEzXAG*VJLv)Kfx|b~%k3j}cau^s6q4!0l<+C9b;-3?2R1 z?UR{8a|!(g8e7@Uvpn)^6s^_rw{K1-`*Q6oq@pLD>}{KvC)N{9v}t6yVHEGZzfEgm)rDj*t$wI94_X$yCVqBKo00A%%!rZ)Jr9PRQoXjJ=-q^G@_bYb$ zt-dl*lhpc-B0TjG()S$go;vsgc0{vd5yN{A2h)U*2%Lad=SLdw#TUZqT1`a=%l2Ok=&*3*I^D5;x| zTiRODKDP>zWaNLYDHW*D^UvBJ`|~pW*G=RH_XeXN>@BVQE(NvZ+zTUI2R``0+Y~`$ z9d&=bMEHI;-}xrPI1<%d<^v2UCEb8V<{?(aC&S6G%JshXoa{wfyQyV_hg6lO%zW^c zpo!2fePyZ5bcM?U|BH`#O*3E5$0>Q(4O>-!SDQuQUfzOLY453SifRoE>6$5eW71oV zEP&F;pQf(kpBUy0_h~*b1>FDVJ~d^DMKC(dN42DGIAsI#9p-hU7kkqqBz0sJfC$(L zrnU+~@e@^mR*?hrM7+9Ps-<~;#NZnwuwW>>2iho^`zlcbt#ENdgWE6e;vjDSM@885 z3}B>~PIro4WQ4m+PkH8DGtICpE)NqzhLue~eEdQJv}TI~)~xC)-Hvvex!^>aqIk7I zD)@~~s*hagRE}LYF*P%ac>uZ}Q!9!mQ^~zgKo9wen^awW5P*PzJ@T$hB}`5To+2?3 z=VUNdIQun(9;CDWND&#cCmn+p^K|ChIFl)<*>V_9yr#9O*$A}jt~L^9yn+s)mX+=q zzQVBbNB`M_)Gn1-KM|KgCtYOh#~JiB*xIRD`Hl63_M?DWgN}shV$yK>!?mY3#^L(` zclgU0H`IeUPxn}HVJR=UD8&e`S{r$p*MEst7QVA0a_&11Q=hx+^b3+So+G@|(usXp?D|zs-@1Mt45BsUT{hhF=s31{2J{p>Q zp#Xz=wK#Wz^X2~ROr!E$k80ti+W&nzN0D%U*W5dmn7ZUvZcz-huBR3G8({;ZhzUol zaK9R^o1wn6dPF~-m3w)(Cw(|nL)Qmz$G=5zIq_qF0YFtpv7UPZhC zQ{|{ZFxg1!mSg^ee(?~8i_bs$6z;-9{mSdc?)$sYd6)M^xx-@C))?onF2xq{O->$r zw7$I~g8^CX#V#~b)LJ0S!r>-rAG2FX)ic)GIA$DZw34`R&|o6h#8q{-C9pX@QsT?cA^3zN+Nh%gehc zCe|66O@@EsE;L&jsO%HM5JA^f@{036g{gfDHo?jXr!SbID-Ks@PkUkcoAdb%4QP2k zy|v$b)}_xKhyjWv?Sh_X!Oa1CHtz1JE#vwt9@-;|{nwsRgXbj8_`!N3si&zz&-#lej@IgR>l1ljC^Y-$)$IB!N z)#mHhr0fWv*Av-rbwbXJvt3?P6Gr)T3w;YwqrB{v?a21C=|XbTkkVj|2yqWF(Zv6N{v01yeoI$HPIkKv12g#>p5rEu?%)4u2kaYgAy)99Pk$n38%U z+dDj;592s-!OIBod+v-}MGXzck+4HgQGsi(r$;XoPgaQwGf~}gA4kP@;uFS0Lhplq zvnhnvO^+2eFvfWH3Dsg3u|mYM0*{zo%;0LfydeRX@|7sIRWm;YL>W9*A10{>a_m0q z$;P*Mu=S0JZ#O@9uVi+6VPZ&;_k}-aL1-T(-YAjSmb|+;zhN}tTvbxbue5)Vs1F_5 zOu60*JPzwnfzPnN*86>*1S-mH?{05N-@e5K>>zu5AYUNTX`AvPfxnOzy1)P#-o?Iy zNLd?dR}wGG_xB~}@Bs-tMtsp7b^lX_o}U2 z#*}4UNRvKx|4+-jCF<7oSSXQIb_-1_g(fDF9p3OL61t;!%FR{cfUx~ z*ES=g#biI!X0pyU77>4-TcbRY3=|yyLQOvX^JhY;&RNj$caN&k$ZoinIaHK$E&IW4=Li?AMv4vCX_xN(wDVa@f{fCy8pLeE0xs+ zd?lX;lp7s(d@ZI+W6j$u31?F+#cEaO0sz>6P9Ja!6do@e4#$iT@|oPBAB|tZBU|1zDj530o$y)xytkvCD;S)TqN!ERq)^gpAA0#Km@&hM_1SR~!EArybsr-kqfiqy#QAG&Gf?$i<|!Z0)hNX_$7 zm>UJnCletp`oyzaibnLXedJ`E7>M=@-&%Dt=K;18cVl!*pr*gxJl^a^ju2P%tiN>5 z5ZCSeCMMSdcdn=??ic4jROt#Aw3-j3@sDp`0K0h$I5c+`mvmsxaqj7G6|nd00Saa z#Eqy4NmhH%<g_$VNEB|_9a_an$k~% zs$GeUr>NRzAHdchaRSKlyc=GUjHBoc#|b-_u2@sIgBE>-(^N0)%?!zwso&5V7-pTa zycwWMQV(A(lr>z7{EHX`I3xzBW|$xtP=HtF2OnE%8SCO;46qx?AL8s_04mh2DD0P5 z7b&|7-PSi$vbKu7a?=%^kQ$hRd^%l399>IQG9A8hblx+IXli~U+_XjzH1N342)+XUn;iW+bAF(gmRe9TCN z!wgBxA$sdX))Kd^rY@<{{C52PrkNu>BJb7HQq!NECnezh$ z-WvMu3#0yFl7G(=9`t&c2qCxAx{!-2OsE0h&u8pN24F{W$gFtgKOqUA@a(ipZR&oe z4?SooEnUPK`2LX_X8s;S57!2hz4kTnM}q;;%r0H}_cS`I-8DT)Iw%-${lN-fQXt1Q z2|DnfLOI*IJ@z7;P)nxjA6Rt}fgc!1#zDY_pzV8+wShG{%h_y&mMd)21*7;HTTE6| zV^Es!3j&z$)94J*O~U4nv;YnRrE3dL5xOWTS-oz2rWG%n3YU#@Y?7ZtqXVN2oC-cW zn6(|282tA3NgA){a~Z^cFw_Y`!gbZAGRIjdtF-zPl4ZL}cSkhIqx~_2-WWijf!#a9M>|%0S z<)Tc`LC+sC!sr6mN!+T1pI4$aZ=(EQS(v6XS*nc9zrPW^m>*uOq1GyYH)dSMOFQ!K zcFVaU9J5U{L-k~&X#*5&a5R3=Do_}A{R0n#G~_2D#7bva-K3L6#yah=gPub6z*h(r zV?G?#>2HV(w2B#vIo73+@jJAAU!g9U@ak#7-{ToZ_jonTnn%`*-h*gUF5v4~+<82&oT^A=EP>8T%_Z#I*d>(jyIKo{wwJlh%h8ai&kl(sMG zXsG4c$|rqLWMs6U?QZTG&iYSBE;ibyu8xD&FnUR5a?b{-uztej)D-5Xrt2Sd0-HouBnBV7k z%gzM$ivxB?tz?@qk>>jGaXP!VJy+H=r3|$^Z`%y+3P(mM&k+_JckBUNj3#DhHuyut z7T$`f&mw1o?BR%)8GG&S!!FWyW6O4GTlYvNUP;C?!}9UpwAXq(~7FQp2*IIDZ0snfgMDb_OSc- z={nt;M1Vk_UNwEl(*k>rBbGGurr~nDcgFoez`^DNF3w| znhm}W<-u*pZns6zpgsy_;eRAsYWMc%F>GaD&~I{MN0um)>PlMH%+_BWI9>|;z(Q1l zH}lHq@vPl=zuf>t&d0P~R{Snakq$?a&M;nrZ6sAIE{mt8xq3WnB(Hi%buZt^lc=Wq z-v|K`9N>i&e0=;d`K>)HFo#iZxADFLRy9wCker-69k4733=8Ytoh%AbUe?yb*4>XP zXm`Tar9ECRw}=h*x&10S9Q~V@bP|E|2W{)9^;=_ZB7W*NOvx}k{@V+nFbo)!U(+-% zRQs;)Zp%(9%dStWT6}#wh4xhV5fO)8m2Zp5z~an!n4t)QY;faD*^(ppr5DKh=?--i zA&rJ6+_2@N+!5w&2Y)%%u8}dRs^yOc4@3UT+#Q0P%=e}f zsY|NFIv9tTNIK@zt-(sRElL886FlZ(H3FgByF1?My;k>W^{_OA9erPA%&Mt_Az6%6Pt#<{P=W6Z_AoVVx$=H_R4tq%eF zq8Bs~w~B*usoFkKMl`ZLo=`g7wKNBYn7vmmpOH0M-x;^S9UJz&yMK7qz_7#7ytF4_ zIZ-=p+Fea-uESnrD|IlEFDzg+TVzr8DpvI@%+hXQo{V^MA!^ObeJ_5l{tg*G(qzE~ z-&AZu+qNgOOz1HxWz0M&CU1^fTh29$=R?Gt;#t4wL1BivOpSf=zw73OLdJXZ4zL-- zW4FvN@e2wMXLR8Hy5NL~g_Tp{@z(i}prfNBlCd^~dl%>-%afQ!hJCHHiuJ+O8lZ)vV$k*XlQ7@p$c^b)02IP|BEmk?u%uLrjm8azb3^8rVTnZ z8obIO6dN`}T1sY~+1r*s^wOUO?@1;+cnrU-9hGOa0U1vTN5j;7N_ey2Nnf!;aGz~P ze~ss8ur;rc>#&k)koW(v_m*K*ZEgQ3E#2KAAl;>uG}4{YT?$BdcZqa&clVO+mJaD| zM4B_%?(Oz@|Ihp3T<6od&haB&u;v|67R1#dt48Vq#+YfB&v`q_^VRONz22 zBP}hWsQ9_7o~Y=d3M^-`@Fl<=(+-GKrNsFHNOy6~FHlp%TcL=DNki9fOKqi6cGO79Q)6uCup4;COOU(5c59wtNM z#wA|>p%C0r96!5%qaDvsIyb;ca7X4%X&Qj{nin?Z&uN5`BeM zqR#`0obc<03edPW7dw{A3Mm$=giNVmAKucYq9y!`UI1)Gp)-^-f?`iftPr08c)q-d zwl^GRl6MTO(BET}NN(t8_V!n8V)z>{27d7)IzSqyLM_BBSAuOwBrmC|Pz!d34*L89 z5Ylq&^Z>OqvE^~d?==W~GRPLVu-Fnx71VV@gip!ai>r7)b1Cxw@8wedw`uD$D4hwQ z3yE)>7^SIVIm4%ch?8F`7SUf!2{GWHfP#Vojg86GRk)oBeah}X{px?svFkP_W0gh}s^+944^JS9 zz^JPL1oD+DRlQ~fYw&a@>FU%K*3UU)UB}}6){K2so*Lp%6cER`zA3Hs)qY)Cu94{v z{tPp~8&egCR-6EQe$S^apl3=xRg`~E(=8$@ngtxW+`l$ULfTGXdqRDHPX?wYbQ-Q> zi6T0wzNo`5khqo(p7~eRwEa*?ixH|T&-Hbi4im@Kxwacv4*exT4E^(B5_?HR>j z=S5KqM44iH7#8h7K9wJ>Ijo^N=BL-z88@^x1+zin8Y0j4j}CX=)ClNJ-9K$&$aHQ9 ze3g%-j=u22{aGVX7tzlwwguz|>Q2w$C@}88Jn}v;sLBxsHDI~lXnpj~i{`NvL|KCR zW2q8~2?DVpAt4j0?S!vCNob7#VHH@GcbCiQh&DDhn4)Q~9coqB$~5bIwc*(8HYI|r z#ev-?wCP*~K!bSgd(u_iA#QcsCk_9`DPDmP!yZt(v{v$Ev}I*w)8U34ocijz=q4!c zM|iK?;m)cb+flXmjorh>ZE~VvbVYi{#DfBxuAOZgt)ausfj)CE7D6(Gs1}o@Nb@uw z2Al%H47=Y(u6FGkaw3<8bS7?PXfW{mD4_`y^Vy_He?28H_0HmtEO#L$8BI+c%h`nGl`3cuM!>TD__$ag<_DrE<2$v)f{=xPN_K-1u+LiYY3bLfr z6%rPHyN=*d(6B=T$D-&TQ(_8GK67*Gr8;d3I&lW`0N;UK9;%QC&^|0EhXIzT%(s-% zf9(i*j|855i79(99=+FeH;Q-`EN%=K|0buv>ak|7Fw!(;-g#^*;wOr5Yl zl@OWtfNCf5xy~nQd;`?^){6?Z9ew>Lf_85vC-bE0v|?_59PC6d#sFw|$Czk{8^GUo z?>3TS^oxUb6IlMypnkf_Qkeav{ zwN%IAM1>PFhx6pX``@{YO_~!7ZlaLb*X!6A*DOfC^X6D%mN8ZLJcHtQy6Ij|i3xByIS<$ISIqDVg z>-BQ?JH;Q@Dr2{0FyfB7Z?pb%eIc(^P`2d&x zW4tr(Bq;TZ?~B&fxFg|!3S6=Yz|!7jaGt(R{=|9(=Z>|QUyDI>?_=zDYmhc>*WYIe zP>Pi7GeG+McKZeP{YjyTq-ofAeT*c2gg=&)J_D)l`A7)&q4U-)UZZYlFHhws(%zSo z?l+{Ydqhy2SUVHJ;x?4_4aaKD!9UQV46}?S^1sib3Eb+JrD{*a#=tqd(L5S^Mg@;{ z^|)}}?8t}X4moyjyqyr>OUedk`cP9yHP)F#HQJe_k;O{L`un;ookS^!Hz9 zbthC>jb(J2bqbxGKcJkQdmJ3%1jDC!x2+?7uK!v=>N;&WzB0guLRNbrAtTR{Fam0> zJPyvn!Zj$F6ql6iT2wqP{Bg>Lv2y)Nwl>YDPN*_wOUu7pPbAI(Yg@`$!_Ad_*+J&%5T?vyk_@Pi-`M4U(q#rZ4AYzY; zO*(c~H4^Iw5kEcNS@g4Jq%Dv@!KCy zH~@gT3;Vm@D+d+9!`aRQ3T9vn(I8M`a5KkujDnx&Ismw7-7UI_gq9hpql9#AW)+ z0Q`&Cltt(eH&fV&ZguaNJhODk%St9tZ6zIg5yWuKkdt}){mmBoCGpSr9t@iAmv7J8 zY0Sv!U0H(NqXY6zPY=&gN?%ZD5y+G8O!0SykK?I&PVY1kO`Ud!zn}TsG(sIJ9F^gn zp#F`O^E=OxBdrWr*9Yf~iAU!}O>}4WbB5uAW#yDE;QDi?a}pp)z+}N7@nnvCX{gj( zSsh>R3x%Bn+@+00Ise^~e2Qg$(+`eEp(Sb+@$h0t3w2fYJF?m>jz~b$bejJ*thjBU zU#ccYWAN=+kAtfHA+|!ToWOHCcgVggo97^{fQ<JTQXQfi_COHjo;gU~QTG8&OlYt|X29%S@Zn`}!1_W`b!cDII^$?P<3L_N5T*`2^^rd&Z zymSBo@Yx#ci~2m~OI1#d$my8JlKV|0xmy;=)o?4<7U{(e17+2P>woKuVoq=X*wrHs zh@IqB?2kDD6a_bZazg@%lRw$G&H_fSu;uY5u^Jf>n6aGg=IY)7hq4;XuR~c4TW@nF z<`imy&d3ce9)rHmhVMyq{#H2l!~3}GRB&OfXe*|Z>`(0n#YH4{5aF0y#02%NVBlh* z#9IElYmq1G{$R1uYThoMBh;|XvuvwPbe9RRBQ-jLnN=EHAhW}iNrjhQUDY~r+?u9A#X&srg^ejm#!49oo$%N`tGSz$$#L` z#kjNFwK-^J80eyorngz}J@v?>LdPJUA8 ziE$_`NWDHj=7d%thwam*dQoY()*Z(+U27(3&>`PA;53kWys~+lTm%PO3?^&}8h6b6MKqYaGl}$cR!Z%# z%i#9v39dVgat2zG0GQr@e9O63FoT`S5P`VWqL-|dC-K_~4@2u9{;rw&Jw)QF2zpt< zm-HVBvK`$z?vcsgEZa0f4^U;#t8xz5>&J(*@6jZZ#8rndPgm?y#AWlo8yYW6O&+uT zJxah%8O??5a5$&%#{XyA&lNhX;C7F@bFx+;tE>~l8=ZL>w2pyOs=m>Seig2YKRwP8H z8v&^DuwO3HCS$LcCJ$`*o;EbwdCy-PY3hm`wPscUgxZ3fc36`fEX%uK!V_sv?26FX zP|7}2oL0Xz6a*h(-r~w^JbHoP1{rVQK#AWuh)OUK4e<-;A?sf6&7vU+)Q`H>u8)ua zt-l?Ceo_i1;6wdD%)x|r^qLI&>G&rY?-xK(;3j|^)qhaSM1=n0L`;8UB5aqa3W^EY z%DTNoayaV|5tjnI*M#lOi3GSq4@TH$7?mbWQ8D8RV5buXsT9F!xibC*9lW9w`fONZ)m}weY^)8(YY7_mD8m6EMAX=a4bZD|@x| zt(ov&P*6KV5V&Smnom(VySW)&?oMz!pX!CcEH^t?(!X!U#KXgLJ|CdA1=8Em>vB}r zg`HLK<6e%^{e#VUPaqKfy%wvx_QdrSMeihTGMwC95*Vlq z2}eIT5fbO5f@6d_1q4sY^MnQJjAw*v`;tBW{8|$DhPRsO`xKVa*+x!<{&3e1Tj`~3 zcIlOEBbP$tk(&3&JX{~IcKjqX?^#!5+oHw6Yot?i=j=ecMw=6hSynn@>OA!0X zzFJ#LWrMEXfHI^^-DO9m{>`<+84r3YaH%?{2O=^{J%)mvFYiZGQ{LwzSo-b%p}$hL zhB$ZYi6#?9B_@t!GaV%bM1phcA(*!4=;-5x3KZSFy#|1V#HBs#%NJ$@W=cx6$REHB zRQ!6WZU{|Y7cfHoy_5MqlsH#t0Z6qS15u`h(T;6GFlyL%i2 zci_scXLAp$dW0`DCdG#BANOtGzwK*hUf{&){5f}r1dl~`#f&U^fGwEIp8;xu&W zBG;`@z?`Z>CB-7amLqCVs%2s-*TsHXjNxqc>7$_4kE)-T1tUU3p?(|pHVIm3<;%09R zaf`#xqxp9kOAC^KxfG(`IFH$&B&cHE*<_O1qR|ahRs*CF?F{BhL&Y~nOw!&(t2-Q4 z7_)RkJJ;<_mHI23J66|dN;}q&Yz@a3acaqD0D56k;$iQ|B0aq1Mp=B} zn>d58V@|aw1NGvocN!Xjw+cfk*8GV#E1Xd5s1fgP(NbpHA}VB!vqX2OSu#^xRHgzv z5XRjmsYZ;8<&Mn7{794|tHTEu>2ex|IK%g)S`;+QcY^-FI?wNf{a}z#ad3hHkq9j4 z^s{qv5SAlfHV}!5ikj2u0)5PafG%4Ue0MBL2c&jP^)Q%LE`EbpR8UHiDl_sTm;ibufDeLqX~r@g9@=*$o9ww z`V}p9R3xN87vSpVycTk1AcZUN2V!674kh6asGB3}(ZwQ3RZ3@MB_${PacN5X#o;*= zo#9|%$B~!fIGfIm0Yfb^mab6?;30vJJJbiy|15*sqLl@Q8v^%x7L&z@%Hl_8)D>Nw zC(k|85?ue%5{5+@O4{(3x>oPhgWGDwi(6sj#U6?;jM$xwhGyHMNQAnR_^cnP;yZ)c zJS|l^0zdq;>dY2#xKnM{{6lA#C>TIZ+Ck#t>S_R}&tZ{~XOOoU`psRw+ZY&-e~yif ziHkFS;MbNh#QzF#F%B2UYN+a?)qy$pEOT}M^{{j}kSrzSvwR(OI@mo0_{sds8 zrnkyyx5c{`s%W@XD8C0t@Ee8TYRqPtL=@i@F2D^20ScQePmhZp3`I;H&wFm5OGp%u z&M(sPLqd@@?4%8F<8+RZ03`Cq$+<0fDbOws-QW8%mY!MlE)!i+{+W`FOCaFok=9M) z(x@gMdH*%tp79Sjaxl>Q@lHX%?aQ~H)PSy>LKpl(!@I~kwv$ms6QBaL|Guj?gDELc z19)5LYqmq2KYpE-)2GuP_jXiQHRn|^7{x>D#ps*5F7G_1^Vdf}i=(!2FZDNoOJ@7_ z*(IaB1fo%U0CvT{wL-_oGmW(ap0DHpsSrUHdw|gp2E1O=Je_tFPw8u`}{&~VI(yQe>YiUV7K?ayVW99IY1=&bXlRu_zp z$6GpN68>-?ngu_Ug&y<@M2KblOtdu-QR%hv31ex!q)1Y={sI_CC@ShV3O1o~2ai?H z_qAjHSX00o`a`pv zH*yIcBl?;4G5_WBUNL^D?Q{{2zc(~XwrWpZ^O9$U4`WKC169NbDEuTq$$-WiAO}_d+kRXWv~R$l>?oblr8-hcG&dZ1+afG#@0JO z|Bi|A3JDxtb(-^+7UX|(nE(`!xHl9!4QRSl0|SFf8f4wDtXhoBsM{V6P(K^Mh-6+p zE7d1k<21YnhWwPIGufXUo2m0wwSLo_m%7v2mMUnJ9KHOEg{j@PMGWt@S;Y%9}X8-i?)A0WK z3>u5UF`ak&``t;$?EV>iqQORut`9WsofUTw9A?7rCjhF`^+`u*7l5$piqZP_Dw-&$ zLi&AgF9g6;pkBNX4rPYsQ*@X7i}pmXuC9is)y(Ea*K-9LXaY}=P>AFf$7!<^3`9N% z4w(RqWghw*Fo@vn<(*K6r~t(=LiL-E`R`6!Dqzh1@yHN}Sy<32Dk^>ix*+5h6j*X< ztB<)A#C!m1eFk6P$B{)#XWLf-WPV|_OBgpaGr!SR*Ovddc< zD@=w)cTp@o_h#9i{eG~e+cOVD1TUn3i0YwWwNvE-&AApks+l}&)rDXsy!rDHUA!M% zBY&cci2v0Q<3RIY95Lv6K=hmsaJb`azN@;B&ESs!nuD1?S3jKmcUbD)RbzqniGsGz zi)Q9;bzLaL@OR2Z=KKg3fzm<$^;wjPw}RJp@9=~En?+{tw?*dW4~vYo^*qA|#&tyS z-b7)}YBo08$3v7k_9uZGk=WB~Ip*n2xXpXg-UoU`YYNtIg_~CCa~zJV9@TOQdjmQh zXK4YifZ6Zpzc+t?6qSeCtj|eI#I@cU$`TG@HlKV6bREl+;GZc{dhxeRf>fYfUeDj#cs3Xt z{gp7{2lIA#xkq%h4DGXo?q{yZzPs2L1r-}&@J{GsQNkI6P)mdmzB?M800|NP_WDN8 zT0ItXew9!mULhPumQbN&jiX6HD`iJ6M0iR{N?njIxt&b}cmJ82I-SV|DEU>m#$(dA zbqsE-vz&#EnE^~hrcQX={f%G77ZDmu`x)4{SQt0^Ns4;;N=%-ZNmKNVpIM< z%rHDO|EC#7(mRIEesCQQ3F%uhu&Mie0`>?}X`A9f3}CfD|2jFEOQCRSFy+09bw`7z zh8_Vn2s#vE5~qbf6z7^q$os4wXimuc^AA7F7Y=4e+QgCh2Jijj9#ds>a51LH+9MJl z+cBh0-MWj#>MBD`U1_FIQgLS=0f`fehk1@2J9dkG=7(JFz3fq4+l9^QKi(?9Si6iL z12?@oNGG7(?EBq%3OAd9BMGSl#&0&F`?D1p!ZyzoAMw%O_U^y*2>=B6%R0^|1)k4N zIxzdW_AS#LkZrw~jpJp}B6$*$$hU91q|^|oo$gbD)84foCw`{9Dac+6&ukz%l$^Kg zvcpjdPY?{1RSVUT96ny(&^07wBMnim(;Rw8_k!6IWONaV_ueG~v?q}|jkcpSV<4AC zjC&RAGG-+^s`6}qiQXd^pp-J}?51R!C{dx9MlE^6#>TXsz!xI%7OWHExEt`C=-sN` zeMYfm#umS?-1QSZEujl^cSX2=F+6emg}IyBsoLtd#?$PX;SuyQHgXZgi-d+lxazQi!TLC5k+XPJDxQJW2tVR z-b1+P>WystVW0BVsSJ}!zo)n1qm}FIays)n)o!g4eA~{kCx9}rL7Z1C6oIPXKv$yT z%|f=l;aqz@2)Z4&0c;cA`s<}hYqvEfjCtg>rv9jU=LuX9!J_xk@$YoGK0GW2+`{7h^3?j_D4^;*lnK0P})x|8PI1FjtuNxmB|Hx)px*%bgDZl~vzEhQ59f&Wi3z49nNxRoM;_C#@)2;ta+i=_od6;1#dh)j1Lmp`pyV={lI-|6f zwX;t#%mM_BsiYe(TF*L}TN%8sKY5@XyhE25e1OAh>PG25W`;ZZe2{ zk`2}dpOh*ELt96<`OUuY{AORAtbR@mmc8W!H%UX~ilIPn^jtHDOS!L%7whL8*t?HvQ&7F7&(ad`ednd#Y~ z;jLLWVC-Ui*W&<^7k$BC8`WSdsJw^z(E(c_`kb0a6_-1SZrn-LTI?+Z5C@Rq;pYq;1cx9LkP*<30tfgO;wikFob1NwI|a4Kn7 zW^pK`3q#$dMn)U`eeQdiMF7?Ho>*H4y78RO0dB&yK66 zM>Ab&z(=j--3WQ~#{+6b$?k@>Ap%fk_hO1v9 zJJ(jb;WMf0ump?24EZ5@aqR0~W(z~sFj1`VzJr|hNAn<==EBNuq6Qbk`tpkDL)!gP zA;W%{KlQ;4)8GsPt>yb=qcj0aWbr>YsWrG$?3X}!eM<(M^I6Lz9D2PrJ3x`ucWm*Q%P;jdzqrSzC!UsG^fg-PG@wWP5&>q|!m)eq6SvE%gVwQglQb~^Q^=xYA;3TM zDm|^39MN($FsOR8!u|}our(&>G6+}OkjM&v!u?UX0$>rJW+E?BdJV0+ngfP;z4ntH z0Xk%Vdu*Tce|)Qcf-s=(Yk$xw=ZEt5Jx6P6J{I4K&7WFYTC_GMsXCMt)5DcCWm)rR zUTUSwyHDhD98Mm82JXj z4MRs!yjqh0bvr5}iEQt8v@o(rAGDpJK)oqn`6ZeU!mQkofum9}qRX1W|BwxKJ8RK> zQ^^_kz7eh<>#uZr-c(#Be809>=R5(7Z|b-y`N`7GmKa3%OvV|X)o4`cWVlM>K{hq zanlT@R4CQQHPC?)#X>LMrBB!>X8$%dR2CJu2*qW5f_nye2wrH^z(7F^#YUX0xZsVo zHTxk3LLh#R!V)TMUbVxxyqW4CEzvPt+Olp5U7!k8PpKBm9T%e0vuanbc?%TB+U1v3 zwy`^t^sg;a1T2KFu&VC~tn;E$J6s-;lL%_qyg2jg`FVQ(#X@{Sr9q2}1VD!1jH*{S zF&{pVk3b*OQl{0=3+R$!Vq$zeA8yU{L+}-8&&XQg!w1og!Skz!G4C|+p7oozf8$je zfQ>vtmABAykozXDA%UIGd4MzATxr$j2LSPB4h5@QdRFvr;jXq&(%(WvU%aVrYa+4k zx?L-m%Aly-fwGOL;zgj;NDZ)4&=!7ZJi@miW4uhq4YQTw)hlO$BDg^4D7$a2)LCGV zknz>UDbXYjJnYyw1$s6ylQ}c-AR-}+vhI=8VG7@$_^BZ|IUDJM(&L_bQI2$a5`zvl z3L8ggL;!eyr)r372WIrq9aS<+w>PljX;^9T=N?xy?>^Z`A7J*X=Uo--rpJp?MY4K< z|Ky^_=J=fj5xo?J5ut0>;oM^PJ}UKRW{(y^8iu`ByMd*0^&Ea&LXDNf>4AYxPvf2y z$?ei*WGF-D{npPghCk~0!v6ne4Ol7z^8Eh*;(?Z2J;fg?0380RUs>a4Ezy_9RbKCzDe48;SRI35rx*j?ZXlJJhO# zW4?_v(gfT9uD2PWGZfUc)1*58l*S120QC_24kxO5x#MuPE6p1Ch-fB@w+{4^i^8f)`!#`0_>W(53DkZ(^exK&Mj&rlNSyT5k>TABo(lU#4u!22IrTM=dWo!d3!16Ry}h+gx!*Q88yE$E==cFK2QYs3x*GXk;p>Bq6@&)sW+5W zKkTso`J^HLD)u+tVU^S=DJrs)1^Q!Yh`}>~gM*)5TucQrskJ{xoMf$sStUZ_B7NAp z{R$zymCSTiZvZ$5(SGT6kF3DOKIbM_o)?tvDhgaJG|YOv{c-6~Sh^zXu`o2&9!FPC zs*+=%X*M0Gs|TCDy99$&jPNzfM8+unmWi{Y3Dyl^-q3J5{?PwE#@5aFa?425PV!M;M)9z3-@hEsGtkQpaqgHa zoAx^EeT(CB>-YUxE?r(S(U^_CD8r}6hq*Sl24R~w)jMea$EpKLXWL(;Gmthnor+@$ zmgHfOP@9aM2*M_j}WPVKSL+N|7xKB8|DTm(*HBYY)Ju&^jT1- z{Qv0V-YbHO=~(y44Zv-N7P~`858T+w{Q0mx=ai%seT-q$?Ci>xjx(hiW-F~tj=PmX zK`)9`O7b60j*o33q*7WNea?;kf35%j$F%-HpKJ*^xml}fX&|{34HXqkKtNz59`IKU z#8PVwafgB2nmf)gKghFMc66YsTNK|ZwPeQ*y`aN&D#$`m?=bn1Cm*6IIpMeyh5$k@Q z2eMnl#E{80%|DZkthEQD^U5&pSd{wFyMyYgbiM8V!fc=>UzMQ@xe&TKr6wUqJwXY} zq$5Aod>>}{sQb_HPmA&25{rCNQnY-!qF2ujri=VsF1E8&N>roi^u;0x_bQJh0P-!k z!@#5(Oo?SDpsg6_!wop9ex(Z@0U&gxI@zn*R+;bpVt|5$sZiU=$r+XO>@yLWPBj9K z{+}2uV>1L5>cLO(?2uk6$OGXP{jB#te88|#lY0SQgU{(8kFWd_vuOXTH}TNUh9ef} ztxyNz4QLrbgd^LG{3w^vGM-{flLCGA9#pS9m1y9HoCr5XUHmV1_W#0VmuwA;T*3qH zjQ)1p$m78x61~P;TU%QgOo)z-HXtdbLphX@kQi6(d{!C&1r9PECva4L-CYs`W)kCv z=x+0iVvu7RQVFYhx^gg35KScDXP)0xeJEh;IP7ts^@e9t{PEIdLq{DcXPuw>X6B z;r0hzeXy`~3EBTViFUni-WB{^n$?5FKI!g5>dpuE%Q|W4wt7GmP}pj}(0*Nu@Ko{x z^>u9{(eW`(73ZaGcW%fxWXt&MowV6XS}Rg?|K!ZLjKA>I4S7G}-Pvu&Beu`>d*bx? zyqF*E+JZ-7d>W^u${PlHw=h<}$0=-7s?E=C0GU7eQM*AMcPkdX4U19nKll7c0am4c3luwS?Qa(>BD+*cmg@oU?xBRf_{4c%|Nf%Q0QK^2` zZu>|`QkLX5%SrqVALU=KK`rz@xk>ll0|ug!p7EMwH(g9nL)3f$*bxU7C0|}f)rdQ; z3#?7d#J%D?wydhKkwbK8j8T1WLwCnQiJis9X>NWSZW#jM8GUL*51KPwrMQ1^&w$(S z#k?jr33nxoIG90oxB0Gk`*eC$4G8NW@z@_XFnGC)0#%}EWi`%Sb2r43t^vdxeB>fMlYNw<;md&wGMN|8MCCJ%~_?Am&4f|5E5@-r_nHK+lQl zVwwzvi($YYDNe_(q^yh)xqTRIFnxP;ygX{GA1xI`hexWZ7 zHhJjQ9U1hh8KlCn63hPHz)#x0aF1)B753%hEtjU++wqFL zF>%T(6emaWk7oND`aV$COEa$l@-h(ck^jMlzL5Qqgrrq(HQ}0laxQul@Bfz*p@Gcf zmlI+7_G%E&0Y^#ynJgCWiTAfj8<1H)hp`~&gRZWDP!X;WED{bYASeVumNW81Ct!CL zJ=)}bnL(!zneTnqB9+;ASPLcsa(PC;1cFm&!%0DvXU{A47^Yf0f$u-6#@4MTtQVta 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 Ijl!q=1=LUw>Hq)$ literal 0 HcmV?d00001 diff --git a/website/src/react-native/img/react-native-existing-app-integration-ios-before.png b/website/src/react-native/img/react-native-existing-app-integration-ios-before.png new file mode 100644 index 0000000000000000000000000000000000000000..445dd790444cc3e836de7f349014a13112dd539a GIT binary patch literal 5409 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 Ae*gdg literal 0 HcmV?d00001 From 1f8cd9ddbc72e69b7ac7329cb72c58440f778650 Mon Sep 17 00:00:00 2001 From: Dave Miller Date: Thu, 16 Jun 2016 09:29:14 -0700 Subject: [PATCH 388/843] Add support placeholder in AdsManager for proper deep linking Reviewed By: foghina Differential Revision: D3444315 fbshipit-source-id: 894b7e7899bd59ac87b175cd2767afe5a741487f --- .../react/modules/core/DeviceEventManagerModule.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java index 2ac4f45024ff8a..d28cf84f17b98f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/DeviceEventManagerModule.java @@ -13,12 +13,14 @@ import android.net.Uri; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.SupportsWebWorkers; import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.WritableMap; /** * Native module that handles device hardware events like hardware back presses. @@ -58,9 +60,11 @@ public void emitHardwareBackPressed() { * Sends an event to the JS instance that a new intent was received. */ public void emitNewIntentReceived(Uri uri) { + WritableMap map = Arguments.createMap(); + map.putString("url", uri.toString()); getReactApplicationContext() .getJSModule(RCTDeviceEventEmitter.class) - .emit("url", uri.toString()); + .emit("url", map); } /** From bc42861cbfa00d6cff487d05a93c0a5cd47b8401 Mon Sep 17 00:00:00 2001 From: Benoit Lemaire Date: Thu, 16 Jun 2016 14:16:22 -0700 Subject: [PATCH 389/843] Cleanup packager dead debug endpoint Summary: When trying to access the debug dependency graph through the `/debug/graph` endpoint of the packager server (documented in the packager README and listed as well when hitting the root `/debug` endpoint), all I am getting back is a nasty HTTP 500 error :'( What triggers this HTTP 500 is a `TypeError` being thrown while trying to access a function that doesn't exists (anymore). Here is the error log of the packager when trying to access this endpoint : ``` TypeError: this._depGraph.getDebugInfo is not a function at Resolver.getDebugInfo (index.js:270:27) at Bundler.getGraphDebugInfo (index.js:575:27) at Server._processDebugRequest (index.js:369:28) at Server.processRequest (index.js:423:12) at next (/Users/blemair/Code/DependencyGraphTest/node_modules/connect/lib/proto.js:174:15) at Object.module.exports [as handle] (cpuProfilerMiddleware.js:17:5) at next (/Users/blemair/Code/DependencyGraphTest/node_modules/connect/lib/proto.js:174:15) at Object.module.exports [as Closes https://github.com/facebook/react-native/pull/8117 Differential Revision: D3445582 fbshipit-source-id: cf5af8bbba293f39773f32814a3b388b7ff67bf7 --- packager/README.md | 4 +--- packager/react-packager/src/Bundler/index.js | 4 ---- packager/react-packager/src/Resolver/index.js | 4 ---- packager/react-packager/src/Server/index.js | 5 ----- 4 files changed, 1 insertion(+), 16 deletions(-) diff --git a/packager/README.md b/packager/README.md index 9a89cd9da3badb..c8e314cc984ad3 100644 --- a/packager/README.md +++ b/packager/README.md @@ -72,12 +72,10 @@ Here are the current options the packager accepts: ### /debug -This is a page used for debugging, it has links to two pages: +This is a page used for debugging, it offers a link to a single page : * Cached Packages: which shows you the packages that's been already generated and cached -* Dependency Graph: is the in-memory graph of all the modules and - their dependencies ## Programmatic API diff --git a/packager/react-packager/src/Bundler/index.js b/packager/react-packager/src/Bundler/index.js index 8b411be9fddf57..d2053fa1bcd9d0 100644 --- a/packager/react-packager/src/Bundler/index.js +++ b/packager/react-packager/src/Bundler/index.js @@ -566,10 +566,6 @@ class Bundler { }); } - getGraphDebugInfo() { - return this._resolver.getDebugInfo(); - } - _generateAssetModule_DEPRECATED(bundle, module, getModuleId) { return Promise.all([ sizeOf(module.path), diff --git a/packager/react-packager/src/Resolver/index.js b/packager/react-packager/src/Resolver/index.js index 865cd683b5dcc8..c8faa60a5b300e 100644 --- a/packager/react-packager/src/Resolver/index.js +++ b/packager/react-packager/src/Resolver/index.js @@ -265,10 +265,6 @@ class Resolver { minifyModule({path, code, map}) { return this._minifyCode(path, code, map); } - - getDebugInfo() { - return this._depGraph.getDebugInfo(); - } } function defineModuleCode(moduleName, code, verboseName = '', dev = true) { diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index f040f7714459be..ef35844fd97aae 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -348,7 +348,6 @@ class Server { const parts = pathname.split('/').filter(Boolean); if (parts.length === 1) { ret += ''; - ret += ''; res.end(ret); } else if (parts[1] === 'bundles') { ret += '

    Cached Bundles

    '; @@ -365,10 +364,6 @@ class Server { console.log(e.stack); // eslint-disable-line no-console-disallow } ); - } else if (parts[1] === 'graph'){ - ret += '

    Dependency Graph

    '; - ret += this._bundler.getGraphDebugInfo(); - res.end(ret); } else { res.writeHead('404'); res.end('Invalid debug request'); From a60b74d974360092008648c58a7fc125fccad3c3 Mon Sep 17 00:00:00 2001 From: Marc Horowitz Date: Thu, 16 Jun 2016 14:28:28 -0700 Subject: [PATCH 390/843] When executing JS throws an exception, capture the js stack in the C++ exception thrown Reviewed By: davidaurelio Differential Revision: D3428921 fbshipit-source-id: 0e8be84a2be92558ea3de0d32d1d4a53308d8270 --- ReactCommon/cxxreact/JSCHelpers.cpp | 13 +++++++++++-- ReactCommon/cxxreact/JSCHelpers.h | 27 +++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/ReactCommon/cxxreact/JSCHelpers.cpp b/ReactCommon/cxxreact/JSCHelpers.cpp index f30d9dbfb4203d..d0aa0a0eda59ef 100644 --- a/ReactCommon/cxxreact/JSCHelpers.cpp +++ b/ReactCommon/cxxreact/JSCHelpers.cpp @@ -52,7 +52,8 @@ JSValueRef evaluateScript(JSContextRef context, JSStringRef script, JSStringRef // file/resource, or was a constructed statement. The location // info will include that source, if any. std::string locationInfo = source != nullptr ? String::ref(source).str() : ""; - auto line = exception.asObject().getProperty("line"); + Object exObject = exception.asObject(); + auto line = exObject.getProperty("line"); if (line != nullptr && line.isNumber()) { if (locationInfo.empty() && line.asInteger() != 1) { // If there is a non-trivial line number, but there was no @@ -71,7 +72,15 @@ JSValueRef evaluateScript(JSContextRef context, JSStringRef script, JSStringRef } LOG(ERROR) << "Got JS Exception: " << exceptionText; - throwJSExecutionException("%s", exceptionText.c_str()); + + Value jsStack = exObject.getProperty("stack"); + if (jsStack.isNull() || !jsStack.isString()) { + throwJSExecutionException("%s", exceptionText.c_str()); + } else { + LOG(ERROR) << "Got JS Stack: " << jsStack.toString().str(); + throwJSExecutionExceptionWithStack( + exceptionText.c_str(), jsStack.toString().str().c_str()); + } } return result; } diff --git a/ReactCommon/cxxreact/JSCHelpers.h b/ReactCommon/cxxreact/JSCHelpers.h index 57b03e31e62fff..eb29058efe02fe 100644 --- a/ReactCommon/cxxreact/JSCHelpers.h +++ b/ReactCommon/cxxreact/JSCHelpers.h @@ -17,12 +17,26 @@ namespace facebook { namespace react { -struct JsException : std::runtime_error { - using std::runtime_error::runtime_error; +class JSException : public std::runtime_error { +public: + explicit JSException(const char* msg) + : std::runtime_error(msg) + , stack_("") {} + + JSException(const char* msg, const char* stack) + : std::runtime_error(msg) + , stack_(stack) {} + + const std::string& getStack() const { + return stack_; + } + +private: + std::string stack_; }; inline void throwJSExecutionException(const char* msg) { - throw JsException(msg); + throw JSException(msg); } template @@ -31,7 +45,12 @@ inline void throwJSExecutionException(const char* fmt, Args... args) { msgSize = std::min(512, msgSize + 1); char *msg = (char*) alloca(msgSize); snprintf(msg, msgSize, fmt, args...); - throw JsException(msg); + throw JSException(msg); +} + +template +inline void throwJSExecutionExceptionWithStack(const char* msg, const char* stack) { + throw JSException(msg, stack); } void installGlobalFunction( From 92d6632d7a4c268145a79e220e41733052c46c3b Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Thu, 16 Jun 2016 14:33:02 -0700 Subject: [PATCH 391/843] Update React to 15.2.0-rc.1 Summary: Notable changes (excluding DOM-only things): - Improved warning messages for propTypes and key warnings - Production error codes - Improved performance in DEV mode - More accurate data in ReactPerf instrumentation - Experimental JSON test renderer - Minor bug fixes Full changelog: https://github.com/facebook/react/compare/fef495942a1cf7d507c816192fde205b6003df05...c66f40f74958d003601f2c0ee22d135da7811e70. Reviewed By: AaaChiuuu Differential Revision: D3442002 fbshipit-source-id: 940fc65ba5d0b742417bbe2fcbd36eb9dc7443e1 --- Libraries/Inspector/Inspector.js | 2 +- package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Libraries/Inspector/Inspector.js b/Libraries/Inspector/Inspector.js index 04acc996c4007e..0967f9dc3246ac 100644 --- a/Libraries/Inspector/Inspector.js +++ b/Libraries/Inspector/Inspector.js @@ -129,7 +129,7 @@ class Inspector extends React.Component { // therefore we use the internal _instance property directly. var publicInstance = instance['_instance'] || {}; var source = instance['_currentElement'] && instance['_currentElement']['_source']; - UIManager.measure(instance.getNativeNode(), (x, y, width, height, left, top) => { + UIManager.measure(instance.getHostNode(), (x, y, width, height, left, top) => { this.setState({ inspected: { frame: {left, top, width, height}, diff --git a/package.json b/package.json index 22359e4c7d4fba..bab8f999003930 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "react-native": "local-cli/wrong-react-native.js" }, "peerDependencies": { - "react": "~15.1.0" + "react": "~15.2.0-rc.1" }, "dependencies": { "absolute-path": "^0.0.0", @@ -201,7 +201,7 @@ "flow-bin": "^0.27.0", "jest": "12.1.1", "portfinder": "0.4.0", - "react": "~15.1.0", + "react": "~15.2.0-rc.1", "shelljs": "0.6.0" } } From b4a4c71a680bb6f42fe81aa5479eb42b78c1cfd3 Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Thu, 16 Jun 2016 15:34:51 -0700 Subject: [PATCH 392/843] update flowification of react-native Reviewed By: frantic, gabelevi Differential Revision: D3436342 fbshipit-source-id: 7630eac4601d0768f57dc9fc945595039067eceb --- .flowconfig | 62 +------------------ .../NavigationCardStack.js | 2 +- Libraries/react-native/react-native.js.flow | 31 +++++++--- 3 files changed, 27 insertions(+), 68 deletions(-) diff --git a/.flowconfig b/.flowconfig index f018dae7d12908..0bbb11213636bd 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,71 +1,13 @@ [ignore] # We fork some components by platform. -.*/*.web.js .*/*.android.js -# Some modules have their own node_modules with overlap -.*/node_modules/node-haste/.* - -# Ugh -.*/node_modules/babel.* -.*/node_modules/babylon.* -.*/node_modules/invariant.* - -# Ignore react and fbjs where there are overlaps, but don't ignore -# anything that react-native relies on -.*/node_modules/fbjs/lib/Map.js -.*/node_modules/fbjs/lib/ErrorUtils.js - -# Flow has a built-in definition for the 'react' module which we prefer to use -# over the currently-untyped source -.*/node_modules/react/react.js -.*/node_modules/react/lib/React.js -.*/node_modules/react/lib/ReactDOM.js - -.*/__mocks__/.* -.*/__tests__/.* - -.*/commoner/test/source/widget/share.js - -# Ignore commoner tests -.*/node_modules/commoner/test/.* - -# See https://github.com/facebook/flow/issues/442 -.*/react-tools/node_modules/commoner/lib/reader.js - -# Ignore jest -.*/node_modules/jest-cli/.* - -# Ignore Website -.*/website/.* - -# Ignore generators +# Ignore templates with `@flow` in header .*/local-cli/generator.* -# Ignore BUCK generated folders -.*\.buckd/ - -# Ignore RNPM -.*/local-cli/rnpm/.* - -.*/node_modules/is-my-json-valid/test/.*\.json -.*/node_modules/iconv-lite/encodings/tables/.*\.json +# Ignore malformed json .*/node_modules/y18n/test/.*\.json -.*/node_modules/spdx-license-ids/spdx-license-ids.json -.*/node_modules/spdx-exceptions/index.json -.*/node_modules/resolve/test/subdirs/node_modules/a/b/c/x.json -.*/node_modules/resolve/lib/core.json -.*/node_modules/jsonparse/samplejson/.*\.json -.*/node_modules/json5/test/.*\.json -.*/node_modules/ua-parser-js/test/.*\.json -.*/node_modules/builtin-modules/builtin-modules.json -.*/node_modules/binary-extensions/binary-extensions.json -.*/node_modules/url-regex/tlds.json -.*/node_modules/joi/.*\.json -.*/node_modules/isemail/.*\.json -.*/node_modules/tr46/.*\.json - [include] diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js index 78cb1001e8d662..a4a50c2985a6f9 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js @@ -59,7 +59,7 @@ import type { type Props = { direction: NavigationGestureDirection, navigationState: NavigationState, - onNavigateBack: ?Function, + onNavigateBack?: Function, renderOverlay: ?NavigationSceneRenderer, renderScene: NavigationSceneRenderer, style: any, diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index b63a57cb5e2878..7d608a64381b1a 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -16,14 +16,31 @@ */ 'use strict'; -// Export ReactNative, plus some native additions. +// Export some ReactNative properties, plus native additions. // -// The use of Object.create/assign is to work around a Flow bug (#6560135). -// Once that is fixed, change this back to -// -// var ReactNative = {...require('React'), /* additions */} + +// Our ReactNative.js has had its types stripped, so here we +// need to enumerate and type the properties we need access to. // -var ReactNative = Object.assign(Object.create(require('ReactNative')), { +var ReactNativeInternal = (require('ReactNative'): { + // render + render: ( + element: ReactElement, + mountInto: number, + callback?: ?(() => void) + ) => ?ReactComponent, + // findNodeHandle + findNodeHandle: (componentOrHandle: any) => ?number, + // NativeMethodsMixin + NativeMethodsMixin: Object +}); + +// export +var ReactNative = { + // from ReactNative internal + findNodeHandle: ReactNativeInternal.findNodeHandle, + render: ReactNativeInternal.render, + NativeMethodsMixin: ReactNativeInternal.NativeMethodsMixin, // Components ActivityIndicator: require('ActivityIndicator'), ActivityIndicatorIOS: require('ActivityIndicatorIOS'), @@ -118,6 +135,6 @@ var ReactNative = Object.assign(Object.create(require('ReactNative')), { ColorPropType: require('ColorPropType'), EdgeInsetsPropType: require('EdgeInsetsPropType'), PointPropType: require('PointPropType'), -}); +}; module.exports = ReactNative; From 1731598428e37b7d82264b7b9d94f82ae10df5eb Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Thu, 16 Jun 2016 16:00:22 -0700 Subject: [PATCH 393/843] Add some (possibly temporary) noexcept/throw specifications Reviewed By: mhorowitz Differential Revision: D3435575 fbshipit-source-id: 93b60a5d6890d21db6b3726784bc6fd868af5ba0 --- ReactCommon/cxxreact/JSCExecutor.cpp | 18 ++++++++++-------- ReactCommon/cxxreact/JSCExecutor.h | 14 +++++++------- ReactCommon/cxxreact/JSCHelpers.h | 20 -------------------- ReactCommon/cxxreact/MethodCall.cpp | 2 +- ReactCommon/cxxreact/MethodCall.h | 2 +- ReactCommon/cxxreact/Value.cpp | 4 ++-- ReactCommon/cxxreact/Value.h | 25 ++++++++++++++++++++++--- 7 files changed, 43 insertions(+), 42 deletions(-) diff --git a/ReactCommon/cxxreact/JSCExecutor.cpp b/ReactCommon/cxxreact/JSCExecutor.cpp index 536313da4b3e80..c244532aca6fa1 100644 --- a/ReactCommon/cxxreact/JSCExecutor.cpp +++ b/ReactCommon/cxxreact/JSCExecutor.cpp @@ -93,7 +93,7 @@ static JSValueRef nativeInjectHMRUpdate( static std::string executeJSCallWithJSC( JSGlobalContextRef ctx, const std::string& methodName, - const std::vector& arguments) { + const std::vector& arguments) throw(JSException) { SystraceSection s("JSCExecutor.executeJSCall", "method", methodName); @@ -115,7 +115,7 @@ std::unique_ptr JSCExecutorFactory::createJSExecutor( JSCExecutor::JSCExecutor(std::shared_ptr delegate, std::shared_ptr messageQueueThread, const std::string& cacheDir, - const folly::dynamic& jscConfig) : + const folly::dynamic& jscConfig) throw(JSException) : m_delegate(delegate), m_deviceCacheDir(cacheDir), m_messageQueueThread(messageQueueThread), @@ -199,7 +199,7 @@ void JSCExecutor::destroy() { }); } -void JSCExecutor::initOnJSVMThread() { +void JSCExecutor::initOnJSVMThread() throw(JSException) { SystraceSection s("JSCExecutor.initOnJSVMThread"); #if defined(WITH_FB_JSC_TUNING) @@ -269,7 +269,7 @@ void JSCExecutor::terminateOnJSVMThread() { m_context = nullptr; } -void JSCExecutor::loadApplicationScript(std::unique_ptr script, std::string sourceURL) { +void JSCExecutor::loadApplicationScript(std::unique_ptr script, std::string sourceURL) throw(JSException) { SystraceSection s("JSCExecutor::loadApplicationScript", "sourceURL", sourceURL); @@ -300,14 +300,15 @@ void JSCExecutor::setJSModulesUnbundle(std::unique_ptr unbund m_unbundle = std::move(unbundle); } -void JSCExecutor::flush() { +void JSCExecutor::flush() throw(JSException) { // TODO: Make this a first class function instead of evaling. #9317773 std::string calls = executeJSCallWithJSC(m_context, "flushedQueue", std::vector()); m_delegate->callNativeModules(*this, std::move(calls), true); } -void JSCExecutor::callFunction(const std::string& moduleId, const std::string& methodId, const folly::dynamic& arguments) { +void JSCExecutor::callFunction(const std::string& moduleId, const std::string& methodId, const folly::dynamic& arguments) throw(JSException) { // TODO: Make this a first class function instead of evaling. #9317773 + // TODO(cjhopman): This copies args. std::vector call{ moduleId, methodId, @@ -317,8 +318,9 @@ void JSCExecutor::callFunction(const std::string& moduleId, const std::string& m m_delegate->callNativeModules(*this, std::move(calls), true); } -void JSCExecutor::invokeCallback(const double callbackId, const folly::dynamic& arguments) { +void JSCExecutor::invokeCallback(const double callbackId, const folly::dynamic& arguments) throw(JSException) { // TODO: Make this a first class function instead of evaling. #9317773 + // TODO(cjhopman): This copies args. std::vector call{ (double) callbackId, std::move(arguments) @@ -327,7 +329,7 @@ void JSCExecutor::invokeCallback(const double callbackId, const folly::dynamic& m_delegate->callNativeModules(*this, std::move(calls), true); } -void JSCExecutor::setGlobalVariable(std::string propName, std::unique_ptr jsonValue) { +void JSCExecutor::setGlobalVariable(std::string propName, std::unique_ptr jsonValue) throw(JSException) { SystraceSection s("JSCExecutor.setGlobalVariable", "propName", propName); diff --git a/ReactCommon/cxxreact/JSCExecutor.h b/ReactCommon/cxxreact/JSCExecutor.h index 735e1c45e627e6..816bec1a54591a 100644 --- a/ReactCommon/cxxreact/JSCExecutor.h +++ b/ReactCommon/cxxreact/JSCExecutor.h @@ -52,24 +52,24 @@ class JSCExecutor : public JSExecutor { explicit JSCExecutor(std::shared_ptr delegate, std::shared_ptr messageQueueThread, const std::string& cacheDir, - const folly::dynamic& jscConfig); + const folly::dynamic& jscConfig) throw(JSException); ~JSCExecutor() override; virtual void loadApplicationScript( std::unique_ptr script, - std::string sourceURL) override; + std::string sourceURL) throw(JSException) override; virtual void setJSModulesUnbundle( std::unique_ptr unbundle) override; virtual void callFunction( const std::string& moduleId, const std::string& methodId, - const folly::dynamic& arguments) override; + const folly::dynamic& arguments) throw(JSException) override; virtual void invokeCallback( const double callbackId, - const folly::dynamic& arguments) override; + const folly::dynamic& arguments) throw(JSException) override; virtual void setGlobalVariable( std::string propName, - std::unique_ptr jsonValue) override; + std::unique_ptr jsonValue) throw(JSException) override; virtual void* getJavaScriptContext() override; virtual bool supportsProfiling() override; virtual void startProfiler(const std::string &titleString) override; @@ -103,9 +103,9 @@ class JSCExecutor : public JSExecutor { std::unordered_map globalObjAsJSON, const folly::dynamic& jscConfig); - void initOnJSVMThread(); + void initOnJSVMThread() throw(JSException); void terminateOnJSVMThread(); - void flush(); + void flush() throw(JSException); void flushQueueImmediate(std::string queueJSON); void loadModule(uint32_t moduleId); diff --git a/ReactCommon/cxxreact/JSCHelpers.h b/ReactCommon/cxxreact/JSCHelpers.h index eb29058efe02fe..0d2609a754bcdc 100644 --- a/ReactCommon/cxxreact/JSCHelpers.h +++ b/ReactCommon/cxxreact/JSCHelpers.h @@ -12,29 +12,9 @@ #include #include -#include "Value.h" - namespace facebook { namespace react { -class JSException : public std::runtime_error { -public: - explicit JSException(const char* msg) - : std::runtime_error(msg) - , stack_("") {} - - JSException(const char* msg, const char* stack) - : std::runtime_error(msg) - , stack_(stack) {} - - const std::string& getStack() const { - return stack_; - } - -private: - std::string stack_; -}; - inline void throwJSExecutionException(const char* msg) { throw JSException(msg); } diff --git a/ReactCommon/cxxreact/MethodCall.cpp b/ReactCommon/cxxreact/MethodCall.cpp index 2782e05b6f9f4e..efb142c6c9c374 100644 --- a/ReactCommon/cxxreact/MethodCall.cpp +++ b/ReactCommon/cxxreact/MethodCall.cpp @@ -13,7 +13,7 @@ namespace react { #define REQUEST_PARAMSS 2 #define REQUEST_CALLID 3 -std::vector parseMethodCalls(const std::string& json) { +std::vector parseMethodCalls(const std::string& json) throw(std::invalid_argument) { folly::dynamic jsonData = folly::parseJson(json); if (jsonData.isNull()) { diff --git a/ReactCommon/cxxreact/MethodCall.h b/ReactCommon/cxxreact/MethodCall.h index 02e86eb98760a1..a28bf40e5749ef 100644 --- a/ReactCommon/cxxreact/MethodCall.h +++ b/ReactCommon/cxxreact/MethodCall.h @@ -24,6 +24,6 @@ struct MethodCall { , callId(cid) {} }; -std::vector parseMethodCalls(const std::string& json); +std::vector parseMethodCalls(const std::string& json) throw(std::invalid_argument); } } diff --git a/ReactCommon/cxxreact/Value.cpp b/ReactCommon/cxxreact/Value.cpp index a8ccf66148275a..9b236b0cee465e 100644 --- a/ReactCommon/cxxreact/Value.cpp +++ b/ReactCommon/cxxreact/Value.cpp @@ -30,7 +30,7 @@ JSContextRef Value::context() const { return m_context; } -std::string Value::toJSONString(unsigned indent) const { +std::string Value::toJSONString(unsigned indent) const throw(JSException) { JSValueRef exn; auto stringToAdopt = JSValueCreateJSONString(m_context, m_value, indent, &exn); if (stringToAdopt == nullptr) { @@ -41,7 +41,7 @@ std::string Value::toJSONString(unsigned indent) const { } /* static */ -Value Value::fromJSON(JSContextRef ctx, const String& json) { +Value Value::fromJSON(JSContextRef ctx, const String& json) throw(JSException) { auto result = JSValueMakeFromJSONString(ctx, json); if (!result) { throwJSExecutionException("Failed to create String from JSON"); diff --git a/ReactCommon/cxxreact/Value.h b/ReactCommon/cxxreact/Value.h index 7813f083968afb..6291aabbedad95 100644 --- a/ReactCommon/cxxreact/Value.h +++ b/ReactCommon/cxxreact/Value.h @@ -24,6 +24,25 @@ namespace react { class Value; class Context; +class JSException : public std::runtime_error { +public: + explicit JSException(const char* msg) + : std::runtime_error(msg) + , stack_("") {} + + JSException(const char* msg, const char* stack) + : std::runtime_error(msg) + , stack_(stack) {} + + const std::string& getStack() const { + return stack_; + } + +private: + std::string stack_; +}; + + class String : public noncopyable { public: explicit String(const char* utf8) : @@ -228,12 +247,12 @@ class Value : public noncopyable { return JSValueIsString(context(), m_value); } - String toString() { + String toString() noexcept { return String::adopt(JSValueToStringCopy(context(), m_value, nullptr)); } - std::string toJSONString(unsigned indent = 0) const; - static Value fromJSON(JSContextRef ctx, const String& json); + std::string toJSONString(unsigned indent = 0) const throw(JSException); + static Value fromJSON(JSContextRef ctx, const String& json) throw(JSException); protected: JSContextRef context() const; JSContextRef m_context; From f60e80d89db1896e8fd6641f3a268db488e709b3 Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Thu, 16 Jun 2016 19:03:11 -0700 Subject: [PATCH 394/843] RN: Lazily Symbolicate Warnings Summary: Currently, every warning triggers a packager request to symbolicate the stack trace. If there happens to be a lot of warnings or if the packager or device are pretty busy (or low powered), this can cause a significant delay in responsiveness (and development velocity). This fixes the issue by only symbolicating warnings when users inspect them. Reviewed By: sahrens Differential Revision: D3448032 fbshipit-source-id: d6154f336ed34c15f99170da013ae3ff1f1ef075 --- Libraries/ReactIOS/YellowBox.js | 69 +++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/Libraries/ReactIOS/YellowBox.js b/Libraries/ReactIOS/YellowBox.js index dbc15dc9e94d34..82507b56f639ae 100644 --- a/Libraries/ReactIOS/YellowBox.js +++ b/Libraries/ReactIOS/YellowBox.js @@ -16,8 +16,9 @@ const EventEmitter = require('EventEmitter'); const Platform = require('Platform'); const React = require('React'); const StyleSheet = require('StyleSheet'); -const symbolicateStackTrace = require('symbolicateStackTrace'); +const infoLog = require('infoLog'); const parseErrorStack = require('parseErrorStack'); +const symbolicateStackTrace = require('symbolicateStackTrace'); import type EmitterSubscription from 'EmitterSubscription'; import type {StackFrame} from 'parseErrorStack'; @@ -25,6 +26,7 @@ import type {StackFrame} from 'parseErrorStack'; type WarningInfo = { count: number; stacktrace: Array; + symbolicated: boolean; }; const _warningEmitter = new EventEmitter(); @@ -77,7 +79,7 @@ if (__DEV__) { * @return {string} the replaced string */ function sprintf(format, ...args) { - var index = 0; + let index = 0; return format.replace(/%s/g, match => args[index++]); } @@ -91,28 +93,46 @@ function updateWarningMap(format, ...args): void { ...args.slice(argCount).map(stringifySafe), ].join(' '); - var warningInfo = _warningMap.get(warning); + const warningInfo = _warningMap.get(warning); if (warningInfo) { warningInfo.count += 1; } else { - warningInfo = {count: 1, stacktrace: []}; + const error: any = new Error(); + error.framesToPop = 2; + + _warningMap.set(warning, { + count: 1, + stacktrace: parseErrorStack(error), + symbolicated: false, + }); } - _warningMap.set(warning, warningInfo); _warningEmitter.emit('warning', _warningMap); +} - var error : any = new Error(); - error.framesToPop = 2; - - symbolicateStackTrace(parseErrorStack(error)).then(stack => { - warningInfo = _warningMap.get(warning); - if (warningInfo) { - warningInfo.stacktrace = stack; - - _warningMap.set(warning, warningInfo); - _warningEmitter.emit('warning', _warningMap); +function ensureSymbolicatedWarning(warning: string): void { + const prevWarningInfo = _warningMap.get(warning); + if (!prevWarningInfo || prevWarningInfo.symbolicated) { + return; + } + prevWarningInfo.symbolicated = true; + + symbolicateStackTrace(prevWarningInfo.stacktrace).then( + stack => { + const nextWarningInfo = _warningMap.get(warning); + if (nextWarningInfo) { + nextWarningInfo.stacktrace = stack; + _warningEmitter.emit('warning', _warningMap); + } + }, + error => { + const nextWarningInfo = _warningMap.get(warning); + if (nextWarningInfo) { + infoLog('Failed to symbolicate warning, "%s":', warning, error); + _warningEmitter.emit('warning', _warningMap); + } } - }, () => { /* Do nothing when can't load source map */ }); + ); } function isWarningIgnored(warning: string): boolean { @@ -182,7 +202,7 @@ const WarningInspector = ({ let stacktraceList; if (stacktraceVisible && stacktrace) { stacktraceList = ( - + {stacktrace.map((frame, ii) => )} ); @@ -203,7 +223,7 @@ const WarningInspector = ({ style={styles.stacktraceButton} underlayColor="transparent"> - {stacktraceVisible ? 'Hide' : 'Show'} stacktrace + {stacktraceVisible ? 'Hide' : 'Show'} Stacktrace @@ -280,6 +300,13 @@ class YellowBox extends React.Component { }); } + componentDidUpdate() { + const {inspecting} = this.state; + if (inspecting != null) { + ensureSymbolicatedWarning(inspecting); + } + } + componentWillUnmount() { if (this._listener) { this._listener.remove(); @@ -355,9 +382,6 @@ var styles = StyleSheet.create({ backgroundColor: backgroundColor(0.95), flex: 1, }, - inspectorContainer: { - flex: 1, - }, inspectorButtons: { flexDirection: 'row', position: 'absolute', @@ -374,6 +398,9 @@ var styles = StyleSheet.create({ flex: 1, padding: 5, }, + stacktraceList: { + paddingBottom: 5, + }, inspectorButtonText: { color: textColor, fontSize: 14, From 3f74568b9f5ab9678c0bf099a260ce7837876a3a Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Thu, 16 Jun 2016 22:17:28 -0700 Subject: [PATCH 395/843] remove NativeMethodsMixin from react-native.js.flow Reviewed By: spicyj Differential Revision: D3448011 fbshipit-source-id: b1ca6e691eadf59a989626040ae8ba728eafd848 --- Libraries/react-native/react-native.js.flow | 3 --- 1 file changed, 3 deletions(-) diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index 7d608a64381b1a..eebab5e27cd7dc 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -31,8 +31,6 @@ var ReactNativeInternal = (require('ReactNative'): { ) => ?ReactComponent, // findNodeHandle findNodeHandle: (componentOrHandle: any) => ?number, - // NativeMethodsMixin - NativeMethodsMixin: Object }); // export @@ -40,7 +38,6 @@ var ReactNative = { // from ReactNative internal findNodeHandle: ReactNativeInternal.findNodeHandle, render: ReactNativeInternal.render, - NativeMethodsMixin: ReactNativeInternal.NativeMethodsMixin, // Components ActivityIndicator: require('ActivityIndicator'), ActivityIndicatorIOS: require('ActivityIndicatorIOS'), From 675c55e8aded6cdda65fc7b363dd2ab4b6232127 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Fri, 17 Jun 2016 02:50:59 -0700 Subject: [PATCH 396/843] Update fresco from 0.10.0 to 0.11.0 Summary: Updating to new release from 16.06.2016. Reviewed By: bestander Differential Revision: D3444598 fbshipit-source-id: d8c14b3d088bab6c08effcdacde9bf2eccb20d68 --- ReactAndroid/build.gradle | 4 ++-- .../libraries/fresco/fresco-react-native/BUCK | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle index efeb36aebe095c..1a685f17535370 100644 --- a/ReactAndroid/build.gradle +++ b/ReactAndroid/build.gradle @@ -264,8 +264,8 @@ dependencies { compile fileTree(dir: 'src/main/third-party/java/infer-annotations/', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.0.1' compile 'com.android.support:recyclerview-v7:23.0.1' - compile 'com.facebook.fresco:fresco:0.10.0' - compile 'com.facebook.fresco:imagepipeline-okhttp3:0.10.0' + compile 'com.facebook.fresco:fresco:0.11.0' + compile 'com.facebook.fresco:imagepipeline-okhttp3:0.11.0' compile 'com.fasterxml.jackson.core:jackson-core:2.2.3' compile 'com.google.code.findbugs:jsr305:3.0.0' compile 'com.squareup.okhttp3:okhttp:3.2.0' diff --git a/ReactAndroid/src/main/libraries/fresco/fresco-react-native/BUCK b/ReactAndroid/src/main/libraries/fresco/fresco-react-native/BUCK index ee04bc4f0f3358..728e05c59572d9 100644 --- a/ReactAndroid/src/main/libraries/fresco/fresco-react-native/BUCK +++ b/ReactAndroid/src/main/libraries/fresco/fresco-react-native/BUCK @@ -6,8 +6,8 @@ android_prebuilt_aar( remote_file( name = 'fresco-binary-aar', - url = 'mvn:com.facebook.fresco:fresco:aar:0.10.0', - sha1 = '8576630feea3d08eb681dec389a472623179f84d', + url = 'mvn:com.facebook.fresco:fresco:aar:0.11.0', + sha1 = '86df1ab4b0074e1aeceb419593d2ea6d97cdc3b4', ) android_prebuilt_aar( @@ -18,8 +18,8 @@ android_prebuilt_aar( remote_file( name = 'drawee-binary-aar', - url = 'mvn:com.facebook.fresco:drawee:aar:0.10.0', - sha1 = 'c594f9c23f844d08ecd4c0e42df40d80767d4b18', + url = 'mvn:com.facebook.fresco:drawee:aar:0.11.0', + sha1 = '7c8a4d211b2334b6d52b9de614b52423b16f7704', ) android_library( @@ -40,8 +40,8 @@ android_prebuilt_aar( remote_file( name = 'imagepipeline-base-aar', - url = 'mvn:com.facebook.fresco:imagepipeline-base:aar:0.10.0', - sha1 = '1390f28d0e4f16b0008fed481eb1b107d93f35b8', + url = 'mvn:com.facebook.fresco:imagepipeline-base:aar:0.11.0', + sha1 = '0f450cd58350ef5d6734f9772bc780b67cc538c5', ) android_prebuilt_aar( @@ -52,8 +52,8 @@ android_prebuilt_aar( remote_file( name = 'imagepipeline-aar', - url = 'mvn:com.facebook.fresco:imagepipeline:aar:0.10.0', - sha1 = 'ecec308b714039dd9e0cf4b7595a427765f43a01', + url = 'mvn:com.facebook.fresco:imagepipeline:aar:0.11.0', + sha1 = 'd57b234db14899af8c36876d7937d63fddca5b12', ) prebuilt_jar( @@ -76,8 +76,8 @@ android_prebuilt_aar( remote_file( name = 'fbcore-aar', - url = 'mvn:com.facebook.fresco:fbcore:aar:0.10.0', - sha1 = '4650dd9a46064c254e0468bba23804e8f32e6144', + url = 'mvn:com.facebook.fresco:fbcore:aar:0.11.0', + sha1 = 'e732e63ea19b19d053eebfe46870157ee79ad034', ) android_prebuilt_aar( @@ -88,6 +88,6 @@ android_prebuilt_aar( remote_file( name = 'imagepipeline-okhttp3-binary-aar', - url = 'mvn:com.facebook.fresco:imagepipeline-okhttp3:aar:0.10.0', - sha1 = 'a3e95483f116b528bdf2d06f7ff2212cb9b372e2', + url = 'mvn:com.facebook.fresco:imagepipeline-okhttp3:aar:0.11.0', + sha1 = '3d7e6d1a2f2e973a596aae7d523667b5a3d6d8a5', ) From 477cc52945d61dfe51f26d77e9eccb191a9c92b9 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Fri, 17 Jun 2016 03:18:43 -0700 Subject: [PATCH 397/843] Use fresco cache to show low res image if available Summary: ImageView will now interrogate fresco cache for images that can be shown before the one with the best fitting size is downloaded. Cache interrogation does not take into account that the images from cache are smaller or bigger than the best fit. Most of the cases, the smaller one will be displayed. It is also possible that a bigger image is available for being displayed, but ideally we'd still want the best fit to be shown, so as to not decode and resize images that are too big. I've added a ImageSource class to simplify things. This makes it easier to lazy-parse the Uri's when necessary, and cache data related to that uri and wether the image is local. This also gets rid of the Map, which makes parsing the source a bit more elegant. Reviewed By: foghina Differential Revision: D3392751 fbshipit-source-id: f6b803fb7ae2aa1c787aa51f66297a14903e4040 --- .../java/com/facebook/react/views/image/BUCK | 1 + .../react/views/image/ReactImageView.java | 163 ++++++++++++------ 2 files changed, 116 insertions(+), 48 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/image/BUCK index 4ab1fc6f5deb59..84e25e32a98571 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/image/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/BUCK @@ -15,6 +15,7 @@ android_library( react_native_dep('libraries/fresco/fresco-react-native:imagepipeline'), react_native_dep('third-party/android/support/v4:lib-support-v4'), react_native_dep('third-party/android/support-annotations:android-support-annotations'), + react_native_dep('third-party/java/infer-annotations:infer-annotations'), react_native_dep('third-party/java/jsr-305:jsr-305'), ], visibility = [ diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java index 7d029f0e4c4a25..616042cee9b8e8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java @@ -12,8 +12,8 @@ import javax.annotation.Nullable; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; +import java.util.LinkedList; +import java.util.List; import android.content.Context; import android.graphics.Bitmap; @@ -44,11 +44,14 @@ import com.facebook.drawee.generic.RoundingParams; import com.facebook.drawee.view.GenericDraweeView; import com.facebook.imagepipeline.common.ResizeOptions; +import com.facebook.imagepipeline.core.ImagePipeline; +import com.facebook.imagepipeline.core.ImagePipelineFactory; import com.facebook.imagepipeline.image.ImageInfo; import com.facebook.imagepipeline.request.BasePostprocessor; import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequestBuilder; import com.facebook.imagepipeline.request.Postprocessor; +import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; @@ -139,10 +142,67 @@ public void process(Bitmap output, Bitmap source) { } } + private class ImageSource { + private @Nullable Uri mUri; + private String mSource; + private double mSize; + private boolean mIsLocalImage; + + public ImageSource(String source, double width, double height) { + mSource = source; + mSize = width * height; + } + + public ImageSource(String source) { + this(source, 0.0d, 0.0d); + } + + public String getSource() { + return mSource; + } + + public Uri getUri() { + if (mUri == null) { + computeUri(); + } + return Assertions.assertNotNull(mUri); + } + + public double getSize() { + return mSize; + } + + public boolean isLocalImage() { + if (mUri == null) { + computeUri(); + } + return mIsLocalImage; + } + + private void computeUri() { + try { + mUri = Uri.parse(mSource); + // Verify scheme is set, so that relative uri (used by static resources) are not handled. + if (mUri.getScheme() == null) { + mUri = null; + } + } catch (Exception e) { + // ignore malformed uri, then attempt to extract resource ID. + } + if (mUri == null) { + mUri = mResourceDrawableIdHelper.getResourceDrawableUri(getContext(), mSource); + mIsLocalImage = true; + } else { + mIsLocalImage = false; + } + } + } + private final ResourceDrawableIdHelper mResourceDrawableIdHelper; - private final Map mSources; + private final List mSources; - private @Nullable Uri mUri; + private @Nullable ImageSource mImageSource; + private @Nullable ImageSource mCachedImageSource; private @Nullable Drawable mLoadingImageDrawable; private int mBorderColor; private int mOverlayColor; @@ -151,7 +211,6 @@ public void process(Bitmap output, Bitmap source) { private @Nullable float[] mBorderCornerRadii; private ScalingUtils.ScaleType mScaleType; private boolean mIsDirty; - private boolean mIsLocalImage; private final AbstractDraweeControllerBuilder mDraweeControllerBuilder; private final RoundedCornerPostprocessor mRoundedCornerPostprocessor; private @Nullable ControllerListener mControllerListener; @@ -178,7 +237,7 @@ public ReactImageView( mRoundedCornerPostprocessor = new RoundedCornerPostprocessor(); mCallerContext = callerContext; mResourceDrawableIdHelper = resourceDrawableIdHelper; - mSources = new HashMap<>(); + mSources = new LinkedList<>(); } public void setShouldNotifyLoadEvents(boolean shouldNotify) { @@ -270,13 +329,14 @@ public void setSource(@Nullable ReadableArray sources) { if (sources != null && sources.size() != 0) { // Optimize for the case where we have just one uri, case in which we don't need the sizes if (sources.size() == 1) { - mSources.put(sources.getMap(0).getString("uri"), 0.0); + mSources.add(new ImageSource(sources.getMap(0).getString("uri"))); } else { for (int idx = 0; idx < sources.size(); idx++) { ReadableMap source = sources.getMap(idx); - mSources.put( + mSources.add(new ImageSource( source.getString("uri"), - source.getDouble("width") * source.getDouble("height")); + source.getDouble("width"), + source.getDouble("height"))); } } } @@ -319,12 +379,12 @@ public void maybeUpdateView() { return; } - computeSourceUri(); - if (mUri == null) { + setSourceImage(); + if (mImageSource == null) { return; } - boolean doResize = shouldResize(mUri); + boolean doResize = shouldResize(mImageSource); if (doResize && (getWidth() <= 0 || getHeight() <= 0)) { // If need a resize and the size is not yet set, wait until the layout pass provides one return; @@ -362,13 +422,13 @@ public void maybeUpdateView() { hierarchy.setFadeDuration( mFadeDurationMs >= 0 ? mFadeDurationMs - : mIsLocalImage ? 0 : REMOTE_IMAGE_FADE_DURATION_MS); + : mImageSource.isLocalImage() ? 0 : REMOTE_IMAGE_FADE_DURATION_MS); Postprocessor postprocessor = usePostprocessorScaling ? mRoundedCornerPostprocessor : null; ResizeOptions resizeOptions = doResize ? new ResizeOptions(getWidth(), getHeight()) : null; - ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(mUri) + ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(mImageSource.getUri()) .setPostprocessor(postprocessor) .setResizeOptions(resizeOptions) .setAutoRotateEnabled(true) @@ -384,6 +444,17 @@ public void maybeUpdateView() { .setOldController(getController()) .setImageRequest(imageRequest); + if (mCachedImageSource != null) { + ImageRequest cachedImageRequest = + ImageRequestBuilder.newBuilderWithSource(mCachedImageSource.getUri()) + .setPostprocessor(postprocessor) + .setResizeOptions(resizeOptions) + .setAutoRotateEnabled(true) + .setProgressiveRenderingEnabled(mProgressiveRenderingEnabled) + .build(); + mDraweeControllerBuilder.setLowResImageRequest(cachedImageRequest); + } + if (mControllerListener != null && mControllerForTesting != null) { ForwardingControllerListener combinedListener = new ForwardingControllerListener(); combinedListener.addListener(mControllerListener); @@ -427,61 +498,57 @@ private boolean hasMultipleSources() { return mSources.size() > 1; } - private void computeSourceUri() { - mUri = null; + private void setSourceImage() { + mImageSource = null; if (mSources.isEmpty()) { return; } if (hasMultipleSources()) { - setUriFromMultipleSources(); + setImageSourceFromMultipleSources(); return; } - final String singleSource = mSources.keySet().iterator().next(); - setUriFromSingleSource(singleSource); - } - - private void setUriFromSingleSource(String source) { - try { - mUri = Uri.parse(source); - // Verify scheme is set, so that relative uri (used by static resources) are not handled. - if (mUri.getScheme() == null) { - mUri = null; - } - } catch (Exception e) { - // ignore malformed uri, then attempt to extract resource ID. - } - if (mUri == null) { - mUri = mResourceDrawableIdHelper.getResourceDrawableUri(getContext(), source); - mIsLocalImage = true; - } else { - mIsLocalImage = false; - } + mImageSource = mSources.get(0); } /** - * Chooses the uri with the size closest to the target image size. Must be called only after the - * layout pass when the sizes of the target image have been computed, and when there are at least - * two sources to choose from. + * Chooses the image source with the size closest to the target image size. Must be called only + * after the layout pass when the sizes of the target image have been computed, and when there + * are at least two sources to choose from. */ - private void setUriFromMultipleSources() { + private void setImageSourceFromMultipleSources() { + ImagePipeline imagePipeline = ImagePipelineFactory.getInstance().getImagePipeline(); final double targetImageSize = getWidth() * getHeight(); double bestPrecision = Double.MAX_VALUE; - String bestUri = null; - for (Map.Entry source : mSources.entrySet()) { - final double precision = Math.abs(1.0 - (source.getValue()) / targetImageSize); + double bestCachePrecision = Double.MAX_VALUE; + for (ImageSource source : mSources) { + final double precision = Math.abs(1.0 - (source.getSize()) / targetImageSize); if (precision < bestPrecision) { bestPrecision = precision; - bestUri = source.getKey(); + mImageSource = source; } + + if (precision < bestCachePrecision && + (imagePipeline.isInBitmapMemoryCache(source.getUri()) || + imagePipeline.isInDiskCacheSync(source.getUri()))) { + bestCachePrecision = precision; + mCachedImageSource = source; + } + } + + // don't use cached image source if it's the same as the image source + if (mCachedImageSource != null && + mImageSource.getSource().equals(mCachedImageSource.getSource())) { + mCachedImageSource = null; } - setUriFromSingleSource(bestUri); } - private static boolean shouldResize(Uri uri) { + private static boolean shouldResize(ImageSource imageSource) { // Resizing is inferior to scaling. See http://frescolib.org/docs/resizing-rotating.html#_ // We resize here only for images likely to be from the device's camera, where the app developer // has no control over the original size - return UriUtil.isLocalContentUri(uri) || UriUtil.isLocalFileUri(uri); + return + UriUtil.isLocalContentUri(imageSource.getUri()) || + UriUtil.isLocalFileUri(imageSource.getUri()); } } From d78602b8cdb54a2097034ad06e81ae88a56033e0 Mon Sep 17 00:00:00 2001 From: Felix Oghina Date: Fri, 17 Jun 2016 03:24:46 -0700 Subject: [PATCH 398/843] show soft errors as redboxes in dev Summary: These are generally things sent to console.error Reviewed By: lexs Differential Revision: D3445393 fbshipit-source-id: e76e82dbaa32dc71100ae6b1d721f80007d8cd3a --- .../react/modules/core/ExceptionsManagerModule.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java index 337e3abfccd52d..8deeee755d6f9f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java @@ -80,7 +80,11 @@ public void reportFatalException(String title, ReadableArray details, int except @ReactMethod public void reportSoftException(String title, ReadableArray details, int exceptionId) { - FLog.e(ReactConstants.TAG, stackTraceToString(title, details)); + if (mDevSupportManager.getDevSupportEnabled()) { + mDevSupportManager.showNewJSError(title, details, exceptionId); + } else { + FLog.e(ReactConstants.TAG, stackTraceToString(title, details)); + } } private void showOrThrowError(String title, ReadableArray details, int exceptionId) { From 49f20f41546e3ba8e7fe43c84c4c701684d0434d Mon Sep 17 00:00:00 2001 From: Felix Oghina Date: Fri, 17 Jun 2016 03:59:17 -0700 Subject: [PATCH 399/843] Add ReactInstanceHolder Reviewed By: astreet Differential Revision: D3328342 fbshipit-source-id: af4e825d0b7c2d3d4490094a939e97cc527dd242 --- .../com/facebook/react/ReactActivity.java | 130 ++++++------------ .../com/facebook/react/ReactApplication.java | 18 +++ .../com/facebook/react/ReactNativeHost.java | 125 +++++++++++++++++ .../templates/package/MainActivity.java | 25 ---- .../templates/package/MainApplication.java | 35 +++++ .../src/app/src/main/AndroidManifest.xml | 1 + 6 files changed, 219 insertions(+), 115 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/ReactApplication.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/ReactNativeHost.java create mode 100644 local-cli/generator-android/templates/package/MainApplication.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java index ff6cf3f4229312..23f4060b4386a1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java @@ -40,38 +40,8 @@ public abstract class ReactActivity extends Activity private @Nullable PermissionListener mPermissionListener; private @Nullable ReactInstanceManager mReactInstanceManager; private @Nullable ReactRootView mReactRootView; - private LifecycleState mLifecycleState = LifecycleState.BEFORE_RESUME; private DoubleTapReloadRecognizer mDoubleTapReloadRecognizer; - - /** - * Returns the name of the bundle in assets. If this is null, and no file path is specified for - * the bundle, the app will only work with {@code getUseDeveloperSupport} enabled and will - * always try to load the JS bundle from the packager server. - * e.g. "index.android.bundle" - */ - protected @Nullable String getBundleAssetName() { - return "index.android.bundle"; - }; - - /** - * Returns a custom path of the bundle file. This is used in cases the bundle should be loaded - * from a custom path. By default it is loaded from Android assets, from a path specified - * by {@link getBundleAssetName}. - * e.g. "file://sdcard/myapp_cache/index.android.bundle" - */ - protected @Nullable String getJSBundleFile() { - return null; - } - - /** - * Returns the name of the main module. Determines the URL used to fetch the JS bundle - * from the packager server. It is only used when dev support is enabled. - * This is the first file to be executed once the {@link ReactInstanceManager} is created. - * e.g. "index.android" - */ - protected String getJSMainModuleName() { - return "index.android"; - } + private boolean mDoRefresh = false; /** * Returns the launchOptions which will be passed to the {@link ReactInstanceManager} @@ -92,48 +62,31 @@ protected String getJSMainModuleName() { protected abstract String getMainComponentName(); /** - * Returns whether dev mode should be enabled. This enables e.g. the dev menu. - */ - protected abstract boolean getUseDeveloperSupport(); - - /** - * Returns a list of {@link ReactPackage} used by the app. - * You'll most likely want to return at least the {@code MainReactPackage}. - * If your app uses additional views or modules besides the default ones, - * you'll want to include more packages here. + * A subclass may override this method if it needs to use a custom {@link ReactRootView}. */ - protected abstract List getPackages(); + protected ReactRootView createRootView() { + return new ReactRootView(this); + } /** - * A subclass may override this method if it needs to use a custom instance. + * Get the {@link ReactNativeHost} used by this app. By default, assumes {@link #getApplication()} + * is an instance of {@link ReactApplication} and calls + * {@link ReactApplication#getReactNativeHost()}. Override this method if your application class + * does not implement {@code ReactApplication} or you simply have a different mechanism for + * storing a {@code ReactNativeHost}, e.g. as a static field somewhere. */ - protected ReactInstanceManager createReactInstanceManager() { - ReactInstanceManager.Builder builder = ReactInstanceManager.builder() - .setApplication(getApplication()) - .setJSMainModuleName(getJSMainModuleName()) - .setUseDeveloperSupport(getUseDeveloperSupport()) - .setInitialLifecycleState(mLifecycleState); - - for (ReactPackage reactPackage : getPackages()) { - builder.addPackage(reactPackage); - } - - String jsBundleFile = getJSBundleFile(); - - if (jsBundleFile != null) { - builder.setJSBundleFile(jsBundleFile); - } else { - builder.setBundleAssetName(getBundleAssetName()); - } - - return builder.build(); + protected ReactNativeHost getReactNativeHost() { + return ((ReactApplication) getApplication()).getReactNativeHost(); } /** - * A subclass may override this method if it needs to use a custom {@link ReactRootView}. + * Get whether developer support should be enabled or not. By default this delegates to + * {@link ReactNativeHost#getUseDeveloperSupport()}. Override this method if your application + * class does not implement {@code ReactApplication} or you simply have a different logic for + * determining this (default just checks {@code BuildConfig}). */ - protected ReactRootView createRootView() { - return new ReactRootView(this); + protected boolean getUseDeveloperSupport() { + return ((ReactApplication) getApplication()).getReactNativeHost().getUseDeveloperSupport(); } @Override @@ -150,9 +103,11 @@ protected void onCreate(Bundle savedInstanceState) { } } - mReactInstanceManager = createReactInstanceManager(); mReactRootView = createRootView(); - mReactRootView.startReactApplication(mReactInstanceManager, getMainComponentName(), getLaunchOptions()); + mReactRootView.startReactApplication( + getReactNativeHost().getReactInstanceManager(), + getMainComponentName(), + getLaunchOptions()); setContentView(mReactRootView); mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer(); } @@ -161,10 +116,8 @@ protected void onCreate(Bundle savedInstanceState) { protected void onPause() { super.onPause(); - mLifecycleState = LifecycleState.BEFORE_RESUME; - - if (mReactInstanceManager != null) { - mReactInstanceManager.onHostPause(); + if (getReactNativeHost().hasInstance()) { + getReactNativeHost().getReactInstanceManager().onHostPause(); } } @@ -172,10 +125,8 @@ protected void onPause() { protected void onResume() { super.onResume(); - mLifecycleState = LifecycleState.RESUMED; - - if (mReactInstanceManager != null) { - mReactInstanceManager.onHostResume(this, this); + if (getReactNativeHost().hasInstance()) { + getReactNativeHost().getReactInstanceManager().onHostResume(this, this); } } @@ -183,31 +134,30 @@ protected void onResume() { protected void onDestroy() { super.onDestroy(); - mReactRootView.unmountReactApplication(); - mReactRootView = null; - - if (mReactInstanceManager != null) { - mReactInstanceManager.destroy(); + if (mReactRootView != null) { + mReactRootView.unmountReactApplication(); + mReactRootView = null; } + getReactNativeHost().clear(); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (mReactInstanceManager != null) { - mReactInstanceManager.onActivityResult(requestCode, resultCode, data); + if (getReactNativeHost().hasInstance()) { + getReactNativeHost().getReactInstanceManager() + .onActivityResult(requestCode, resultCode, data); } } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - if (mReactInstanceManager != null && - mReactInstanceManager.getDevSupportManager().getDevSupportEnabled()) { + if (getReactNativeHost().hasInstance() && getUseDeveloperSupport()) { if (keyCode == KeyEvent.KEYCODE_MENU) { - mReactInstanceManager.showDevOptionsDialog(); + getReactNativeHost().getReactInstanceManager().showDevOptionsDialog(); return true; } if (mDoubleTapReloadRecognizer.didDoubleTapR(keyCode, getCurrentFocus())) { - mReactInstanceManager.getDevSupportManager().handleReloadJS(); + getReactNativeHost().getReactInstanceManager().getDevSupportManager().handleReloadJS(); } } return super.onKeyUp(keyCode, event); @@ -215,8 +165,8 @@ public boolean onKeyUp(int keyCode, KeyEvent event) { @Override public void onBackPressed() { - if (mReactInstanceManager != null) { - mReactInstanceManager.onBackPressed(); + if (getReactNativeHost().hasInstance()) { + getReactNativeHost().getReactInstanceManager().onBackPressed(); } else { super.onBackPressed(); } @@ -229,8 +179,8 @@ public void invokeDefaultOnBackPressed() { @Override public void onNewIntent(Intent intent) { - if (mReactInstanceManager != null) { - mReactInstanceManager.onNewIntent(intent); + if (getReactNativeHost().hasInstance()) { + getReactNativeHost().getReactInstanceManager().onNewIntent(intent); } else { super.onNewIntent(intent); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactApplication.java b/ReactAndroid/src/main/java/com/facebook/react/ReactApplication.java new file mode 100644 index 00000000000000..6b74532dfe546e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactApplication.java @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react; + +public interface ReactApplication { + + /** + * Get the default {@link ReactNativeHost} for this app. + */ + ReactNativeHost getReactNativeHost(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactNativeHost.java b/ReactAndroid/src/main/java/com/facebook/react/ReactNativeHost.java new file mode 100644 index 00000000000000..b302024426b3bb --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactNativeHost.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react; + +import javax.annotation.Nullable; + +import java.util.List; + +import android.app.Application; + +import com.facebook.infer.annotation.Assertions; + +/** + * Simple class that holds an instance of {@link ReactInstanceManager}. This can be used in your + * {@link Application class} (see {@link ReactApplication}), or as a static field. + */ +public abstract class ReactNativeHost { + + private final Application mApplication; + private @Nullable ReactInstanceManager mReactInstanceManager; + + protected ReactNativeHost(Application application) { + mApplication = application; + } + + /** + * Get the current {@link ReactInstanceManager} instance, or create one. + */ + public ReactInstanceManager getReactInstanceManager() { + if (mReactInstanceManager == null) { + mReactInstanceManager = createReactInstanceManager(); + } + return mReactInstanceManager; + } + + /** + * Get whether this holder contains a {@link ReactInstanceManager} instance, or not. I.e. if + * {@link #getReactInstanceManager()} has been called at least once since this object was created + * or {@link #clear()} was called. + */ + public boolean hasInstance() { + return mReactInstanceManager != null; + } + + /** + * Destroy the current instance and release the internal reference to it, allowing it to be GCed. + */ + public void clear() { + if (mReactInstanceManager != null) { + mReactInstanceManager.destroy(); + mReactInstanceManager = null; + } + } + + protected ReactInstanceManager createReactInstanceManager() { + ReactInstanceManager.Builder builder = ReactInstanceManager.builder() + .setApplication(mApplication) + .setJSMainModuleName(getJSMainModuleName()) + .setUseDeveloperSupport(getUseDeveloperSupport()) + .setInitialLifecycleState(LifecycleState.BEFORE_CREATE); + + for (ReactPackage reactPackage : getPackages()) { + builder.addPackage(reactPackage); + } + + String jsBundleFile = getJSBundleFile(); + if (jsBundleFile != null) { + builder.setJSBundleFile(jsBundleFile); + } else { + builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName())); + } + + return builder.build(); + } + + /** + * Returns the name of the main module. Determines the URL used to fetch the JS bundle + * from the packager server. It is only used when dev support is enabled. + * This is the first file to be executed once the {@link ReactInstanceManager} is created. + * e.g. "index.android" + */ + protected String getJSMainModuleName() { + return "index.android"; + } + + /** + * Returns a custom path of the bundle file. This is used in cases the bundle should be loaded + * from a custom path. By default it is loaded from Android assets, from a path specified + * by {@link getBundleAssetName}. + * e.g. "file://sdcard/myapp_cache/index.android.bundle" + */ + protected @Nullable String getJSBundleFile() { + return null; + } + + /** + * Returns the name of the bundle in assets. If this is null, and no file path is specified for + * the bundle, the app will only work with {@code getUseDeveloperSupport} enabled and will + * always try to load the JS bundle from the packager server. + * e.g. "index.android.bundle" + */ + protected @Nullable String getBundleAssetName() { + return "index.android.bundle"; + } + + /** + * Returns whether dev mode should be enabled. This enables e.g. the dev menu. + */ + protected abstract boolean getUseDeveloperSupport(); + + /** + * Returns a list of {@link ReactPackage} used by the app. + * You'll most likely want to return at least the {@code MainReactPackage}. + * If your app uses additional views or modules besides the default ones, + * you'll want to include more packages here. + */ + protected abstract List getPackages(); +} diff --git a/local-cli/generator-android/templates/package/MainActivity.java b/local-cli/generator-android/templates/package/MainActivity.java index 283dffc92e17f8..c027ca136e01bc 100644 --- a/local-cli/generator-android/templates/package/MainActivity.java +++ b/local-cli/generator-android/templates/package/MainActivity.java @@ -1,11 +1,6 @@ package <%= package %>; import com.facebook.react.ReactActivity; -import com.facebook.react.ReactPackage; -import com.facebook.react.shell.MainReactPackage; - -import java.util.Arrays; -import java.util.List; public class MainActivity extends ReactActivity { @@ -17,24 +12,4 @@ public class MainActivity extends ReactActivity { protected String getMainComponentName() { return "<%= name %>"; } - - /** - * Returns whether dev mode should be enabled. - * This enables e.g. the dev menu. - */ - @Override - protected boolean getUseDeveloperSupport() { - return BuildConfig.DEBUG; - } - - /** - * A list of packages used by the app. If the app uses additional views - * or modules besides the default ones, add more packages here. - */ - @Override - protected List getPackages() { - return Arrays.asList( - new MainReactPackage() - ); - } } diff --git a/local-cli/generator-android/templates/package/MainApplication.java b/local-cli/generator-android/templates/package/MainApplication.java new file mode 100644 index 00000000000000..525eb0088f73d4 --- /dev/null +++ b/local-cli/generator-android/templates/package/MainApplication.java @@ -0,0 +1,35 @@ +package <%= package %>; + +import android.app.Application; +import android.util.Log; + +import com.facebook.react.ReactApplication; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.ReactPackage; +import com.facebook.react.shell.MainReactPackage; + +import java.util.Arrays; +import java.util.List; + +public class MainApplication extends Application implements ReactApplication { + + private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { + @Override + protected boolean getUseDeveloperSupport() { + return BuildConfig.DEBUG; + } + + @Override + protected List getPackages() { + return Arrays.asList( + new MainReactPackage() + ); + } + }; + + @Override + public ReactNativeHost getReactNativeHost() { + return mReactNativeHost; + } +} diff --git a/local-cli/generator-android/templates/src/app/src/main/AndroidManifest.xml b/local-cli/generator-android/templates/src/app/src/main/AndroidManifest.xml index 3bcfaf186f4f52..3da058c9f4b5ef 100644 --- a/local-cli/generator-android/templates/src/app/src/main/AndroidManifest.xml +++ b/local-cli/generator-android/templates/src/app/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ android:targetSdkVersion="22" /> Date: Fri, 17 Jun 2016 06:16:41 -0700 Subject: [PATCH 400/843] move bundle signature to own file Reviewed By: tadeuzagallo Differential Revision: D3437491 fbshipit-source-id: c2e00932bacd799d547110c7214374c51ff0b830 --- local-cli/bundle/output/bundle.js | 13 ++++++---- local-cli/bundle/output/{hash.js => meta.js} | 25 ++++++++++++-------- 2 files changed, 24 insertions(+), 14 deletions(-) rename local-cli/bundle/output/{hash.js => meta.js} (56%) diff --git a/local-cli/bundle/output/bundle.js b/local-cli/bundle/output/bundle.js index 2cbb7be08ee34b..54eab0587c12c6 100644 --- a/local-cli/bundle/output/bundle.js +++ b/local-cli/bundle/output/bundle.js @@ -9,7 +9,7 @@ 'use strict'; const Promise = require('promise'); -const hash = require('./hash'); +const meta = require('./meta'); const writeFile = require('./writeFile'); function buildBundle(packagerClient, requestOptions) { @@ -37,15 +37,20 @@ function saveBundleAndMap(bundle, options, log) { log('Writing bundle output to:', bundleOutput); - const code = hash.appendToString(codeWithMap.code, encoding); + const {code} = codeWithMap; const writeBundle = writeFile(bundleOutput, code, encoding); - writeBundle.then(() => log('Done writing bundle output')); + const writeMetadata = writeFile( + bundleOutput + '.meta', + meta(code, encoding), + 'binary'); + Promise.all([writeBundle, writeMetadata]) + .then(() => log('Done writing bundle output')); if (sourcemapOutput) { log('Writing sourcemap output to:', sourcemapOutput); const writeMap = writeFile(sourcemapOutput, codeWithMap.map, null); writeMap.then(() => log('Done writing sourcemap output')); - return Promise.all([writeBundle, writeMap]); + return Promise.all([writeBundle, writeMetadata, writeMap]); } else { return writeBundle; } diff --git a/local-cli/bundle/output/hash.js b/local-cli/bundle/output/meta.js similarity index 56% rename from local-cli/bundle/output/hash.js rename to local-cli/bundle/output/meta.js index c02c9574615d49..d7f837a0ef486a 100644 --- a/local-cli/bundle/output/hash.js +++ b/local-cli/bundle/output/meta.js @@ -10,14 +10,23 @@ const crypto = require('crypto'); -const createHash = () => crypto.createHash('sha1'); const isUTF8 = encoding => /^utf-?8$/i.test(encoding); -exports.appendToString = (string, encoding) => { - const hash = createHash(); - hash.update(string, encoding); - encoding = tryAsciiPromotion(string, encoding); - return string + formatSignature(encoding, hash); +const constantFor = encoding => + /^ascii$/i.test(encoding) ? 1 : + isUTF8(encoding) ? 2 : + /^(?:utf-?16(?:le)?|ucs-?2)$/ ? 3 : 0; + +module.exports = function(code, encoding) { + const hash = crypto.createHash('sha1'); + hash.update(code, encoding); + const digest = hash.digest('binary'); + const signature = Buffer(digest.length + 1); + signature.write(digest, 'binary'); + signature.writeUInt8( + constantFor(tryAsciiPromotion(code, encoding)), + signature.length - 1); + return signature; }; function tryAsciiPromotion(string, encoding) { @@ -27,7 +36,3 @@ function tryAsciiPromotion(string, encoding) { } return 'ascii'; } - -function formatSignature(encoding, hash) { - return `/*${encoding}:${hash.digest('hex')}*/`; -} From bf79352e4f149cbc66729ffe263c6242e675c996 Mon Sep 17 00:00:00 2001 From: Felix Oghina Date: Fri, 17 Jun 2016 07:52:57 -0700 Subject: [PATCH 401/843] fix dialogmodule crashing with no fragment manager in onHostResume Summary: This can happen now, we shouldn't crash. `showAlert` will crash instead if JS tries to call it. Reviewed By: lexs Differential Revision: D3450018 fbshipit-source-id: bbc062b357315f26ee98ed3b3a59db71dc5fc74a --- .../main/java/com/facebook/react/modules/dialog/BUCK | 1 + .../facebook/react/modules/dialog/DialogModule.java | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/BUCK index 45c6aeae97ab5b..a16ce52e36229c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/BUCK @@ -6,6 +6,7 @@ android_library( deps = [ react_native_target('java/com/facebook/react/bridge:bridge'), react_native_target('java/com/facebook/react/common:common'), + react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'), react_native_dep('third-party/android/support/v4:lib-support-v4'), react_native_dep('third-party/java/infer-annotations:infer-annotations'), react_native_dep('third-party/java/jsr-305:jsr-305'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/DialogModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/DialogModule.java index 30bcedb226ac35..4ed361ff66de8b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/DialogModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/DialogModule.java @@ -20,6 +20,7 @@ import android.os.Bundle; import android.support.v4.app.FragmentActivity; +import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.LifecycleEventListener; @@ -198,11 +199,11 @@ public void onHostResume() { mIsInForeground = true; // Check if a dialog has been created while the host was paused, so that we can show it now. FragmentManagerHelper fragmentManagerHelper = getFragmentManagerHelper(); - Assertions.assertNotNull( - fragmentManagerHelper, - "Attached DialogModule to host with pending alert but no FragmentManager " + - "(not attached to an Activity)."); - fragmentManagerHelper.showPendingAlert(); + if (fragmentManagerHelper != null) { + fragmentManagerHelper.showPendingAlert(); + } else { + FLog.w(DialogModule.class, "onHostResume called but no FragmentManager found"); + } } @ReactMethod From 99e2a67b3992c3d76fa519a5880d7abc5c57b7a2 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Fri, 17 Jun 2016 11:52:32 -0700 Subject: [PATCH 402/843] Don't close reference to image in getSize Summary: ImageLoader improperly closes both the image reference (which is shared) and the DataSource (which is shared, and should only be handled by BaseDataSubscriber when it is correct). Reviewed By: plamenko Differential Revision: D3441752 fbshipit-source-id: bfb3d0281cd9ae789cba4079978ef46d295ac8f5 --- .../react/modules/image/ImageLoaderModule.java | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.java index 3596a46cb63df2..34648f1f164f5a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.java @@ -11,9 +11,6 @@ import android.net.Uri; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.WritableMap; import com.facebook.common.executors.CallerThreadExecutor; import com.facebook.common.references.CloseableReference; import com.facebook.datasource.BaseDataSubscriber; @@ -23,10 +20,12 @@ import com.facebook.imagepipeline.image.CloseableImage; import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequestBuilder; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.WritableMap; public class ImageLoaderModule extends ReactContextBaseJavaModule { @@ -83,27 +82,20 @@ protected void onNewResultImpl( sizes.putInt("width", image.getWidth()); sizes.putInt("height", image.getHeight()); - image.close(); promise.resolve(sizes); } catch (Exception e) { promise.reject(ERROR_GET_SIZE_FAILURE, e); } finally { CloseableReference.closeSafely(ref); - dataSource.close(); } } else { - dataSource.close(); promise.reject(ERROR_GET_SIZE_FAILURE); } } @Override protected void onFailureImpl(DataSource> dataSource) { - try { - promise.reject(ERROR_GET_SIZE_FAILURE, dataSource.getFailureCause()); - } finally { - dataSource.close(); - } + promise.reject(ERROR_GET_SIZE_FAILURE, dataSource.getFailureCause()); } }; dataSource.subscribe(dataSubscriber, CallerThreadExecutor.getInstance()); From 62bb09d1f64c6669d08b885f8952e92cc6f07438 Mon Sep 17 00:00:00 2001 From: David Aurelio Date: Fri, 17 Jun 2016 12:55:43 -0700 Subject: [PATCH 403/843] make module IDs deterministic when bundling Summary: This makes sure that `getModuleId` is called on modules in the order returned by `node-haste`, rather than waiting for a couple of promises to resolve before calling the function. Related: #7758 Reviewed By: frantic Differential Revision: D3450853 fbshipit-source-id: 7f26590b39b94ade32c73a8db9fd31d283d57549 --- local-cli/bundle/output/bundle.js | 5 ++++- packager/react-packager/src/Bundler/index.js | 16 +++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/local-cli/bundle/output/bundle.js b/local-cli/bundle/output/bundle.js index 54eab0587c12c6..70e8a1941dc811 100644 --- a/local-cli/bundle/output/bundle.js +++ b/local-cli/bundle/output/bundle.js @@ -13,7 +13,10 @@ const meta = require('./meta'); const writeFile = require('./writeFile'); function buildBundle(packagerClient, requestOptions) { - return packagerClient.buildBundle(requestOptions); + return packagerClient.buildBundle({ + ...requestOptions, + isolateModuleIDs: true, + }); } function createCodeWithMap(bundle, dev) { diff --git a/packager/react-packager/src/Bundler/index.js b/packager/react-packager/src/Bundler/index.js index d2053fa1bcd9d0..8ee861d2a7128b 100644 --- a/packager/react-packager/src/Bundler/index.js +++ b/packager/react-packager/src/Bundler/index.js @@ -530,12 +530,14 @@ class Bundler { _toModuleTransport({module, bundle, entryFilePath, transformOptions, getModuleId}) { let moduleTransport; + const moduleId = getModuleId(module); + if (module.isAsset_DEPRECATED()) { moduleTransport = - this._generateAssetModule_DEPRECATED(bundle, module, getModuleId); + this._generateAssetModule_DEPRECATED(bundle, module, moduleId); } else if (module.isAsset()) { moduleTransport = this._generateAssetModule( - bundle, module, getModuleId, transformOptions.platform); + bundle, module, moduleId, transformOptions.platform); } if (moduleTransport) { @@ -556,7 +558,7 @@ class Bundler { return new ModuleTransport({ name, - id: getModuleId(module), + id: moduleId, code, map, meta: {dependencies, dependencyOffsets, preloaded}, @@ -566,7 +568,7 @@ class Bundler { }); } - _generateAssetModule_DEPRECATED(bundle, module, getModuleId) { + _generateAssetModule_DEPRECATED(bundle, module, moduleId) { return Promise.all([ sizeOf(module.path), module.getName(), @@ -586,7 +588,7 @@ class Bundler { return new ModuleTransport({ name: id, - id: getModuleId(module), + id: moduleId, code: code, sourceCode: code, sourcePath: module.path, @@ -645,7 +647,7 @@ class Bundler { } - _generateAssetModule(bundle, module, getModuleId, platform = null) { + _generateAssetModule(bundle, module, moduleId, platform = null) { return Promise.all([ module.getName(), this._generateAssetObjAndCode(module, platform), @@ -653,7 +655,7 @@ class Bundler { bundle.addAsset(asset); return new ModuleTransport({ name, - id: getModuleId(module), + id: moduleId, code, meta: meta, sourceCode: code, From a9d8d1cab6d0196604e7ffcb4670ee675b093d69 Mon Sep 17 00:00:00 2001 From: Gant Laborde Date: Fri, 17 Jun 2016 16:06:43 -0700 Subject: [PATCH 404/843] Add caveat on PCH errors Summary: Moving files that have generated PCHs causes an error that you cannot clear easily. Here are the instructions on how. I was prompted to place this info here via #6797 Closes https://github.com/facebook/react-native/pull/7185 Differential Revision: D3453289 Pulled By: mkonicek fbshipit-source-id: 8e16ea8f1bc3495209d1510a1caad2c6208c2e1e --- docs/KnownIssues.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/KnownIssues.md b/docs/KnownIssues.md index 9a26760ce88485..1beddd60a39db0 100644 --- a/docs/KnownIssues.md +++ b/docs/KnownIssues.md @@ -67,6 +67,10 @@ React Native Android depends on [Fresco](https://github.com/facebook/fresco) for Try running `react-native init` with `--verbose` and see [#2797](https://github.com/facebook/react-native/issues/2797) for common causes. +### Moving folder after iOS build + +Moving a React Native project after it has been compiled in iOS can sometimes cause errors with pre-compiled headers e.g. `mv MyAwesomeProject/ newDir/MyAwesomeProject`. This results in a message resembling: `error: PCH was compiled with module cache path ...`. If you were compiling using Xcode you can [hold option when you clean the build](http://stackoverflow.com/a/15463219/3110982). If you're using `react-native run-ios` you can manually clean the header cache by deleting the contents of your ModuleCache folder. e.g. `rm -rf ios/build/ModuleCache/*` + ### Text Input Border The text input has by default a border at the bottom of its view. This border has its padding set by the background image provided by the system, and it cannot be changed. Solutions to avoid this is to either not set height explicitly, case in which the system will take care of displaying the border in the correct position, or to not display the border by setting underlineColorAndroid to transparent. From 8ca3a0c91c7ad6e0d0dd87c83c0550fe60e8d337 Mon Sep 17 00:00:00 2001 From: Alex Harrison Date: Fri, 17 Jun 2016 16:09:53 -0700 Subject: [PATCH 405/843] Add ReacTube to Showcase Summary: The blog post for ReacTube was released recently. In addition, ReacTube is currently on the Play Store and will be released on the iOS store soon. Closes https://github.com/facebook/react-native/pull/7896 Differential Revision: D3453146 Pulled By: mkonicek fbshipit-source-id: 887ca6c9d12cd6f50f10d11091f1501476ae2144 --- website/src/react-native/showcase.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/src/react-native/showcase.js b/website/src/react-native/showcase.js index c18638987e1022..0c6208f7f119fd 100644 --- a/website/src/react-native/showcase.js +++ b/website/src/react-native/showcase.js @@ -795,6 +795,12 @@ var apps = [ link: 'https://itunes.apple.com/us/app/reactto36/id989009293?mt=8', author: 'Jonathan Solichin', }, + { + name: 'ReacTube', + icon: 'https://raw.githubusercontent.com/alexkendall/ReacTube/master/images/RTIconAlternative.png?token=AH3CygkR6xhSOsgO7YZ3kDNcMZ-y-3Qgks5XYuPwwA%3D%3D', + link: 'https://play.google.com/store/apps/details?id=com.reactube', + author: 'Icon Interactive', + }, { name: 'Reading', icon: 'http://7xr0xq.com1.z0.glb.clouddn.com/about_logo.png', From a2f78825f2dbdc0d48eb0a8015b1690a924c5566 Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Fri, 17 Jun 2016 16:23:17 -0700 Subject: [PATCH 406/843] RN: Increase Time Drift Threshold Summary: Increases time drift threshold to reduce chance of false positives. Also, tweaked the error message to contain slightly more information. Reviewed By: frantic Differential Revision: D3450852 fbshipit-source-id: 7fbf3eb25543977f9767c7a57277db336006bd12 --- .../facebook/react/modules/core/Timing.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java index 01bf221bf3eb2f..57ec14bdd326c5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java @@ -217,16 +217,27 @@ public void createTimer( final int duration, final double jsSchedulingTime, final boolean repeat) { + long deviceTime = SystemClock.currentTimeMillis(); + long remoteTime = (long) jsSchedulingTime; + // If the times on the server and device have drifted throw an exception to warn the developer // that things might not work or results may not be accurate. This is required only for // developer builds. - if (mDevSupportManager.getDevSupportEnabled() && Math.abs(jsSchedulingTime - System.currentTimeMillis()) > 1000) { - throw new RuntimeException("System and emulator/device times have drifted. Please correct this by running adb shell \"date `date +%m%d%H%M%Y.%S`\" on your dev machine"); + if (mDevSupportManager.getDevSupportEnabled()) { + long driftTime = Math.abs(remoteTime - deviceTime); + if (driftTime > 60000) { + throw new RuntimeException( + "Debugger and device times have drifted by " + driftTime + "ms. " + + "Please correct this by running adb shell " + + "\"date `date +%m%d%H%M%Y.%S`\" on your debugger machine.\n" + + "Debugger Time = " + remoteTime + "\n" + + "Device Time = " + deviceTime + ); + } } + // Adjust for the amount of time it took for native to receive the timer registration call - long adjustedDuration = (long) Math.max( - 0, - jsSchedulingTime - SystemClock.currentTimeMillis() + duration); + long adjustedDuration = Math.max(0, remoteTime - deviceTime + duration); if (duration == 0 && !repeat) { WritableArray timerToCall = Arguments.createArray(); timerToCall.pushInt(callbackID); From 12754839a1ec98207d84f19eb72436758dc37649 Mon Sep 17 00:00:00 2001 From: carlos Date: Fri, 17 Jun 2016 16:30:29 -0700 Subject: [PATCH 407/843] =?UTF-8?q?if=20quick=20call=20jumpTo=20the=20tran?= =?UTF-8?q?sitionQueue=20will=20be=20quit=20before=20handle=20a=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: (You can skip this if you're fixing a typo or adding an app to the Showcase.) Explain the **motivation** for making this change. What existing problem does the pull request solve? Prefer **small pull requests**. These are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise please split it. **Test plan (required)** Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. Make sure tests pass on both Travis and Circle CI. **Code formatting** Look around. Match the style of the rest of the codebase. See also the simple [style guide](https://github.com/facebook/react-native/blob/master/CONTRIBUTING.md#style-guide). For more info, see the ["Pull Requests" section of our "Contributing" guidelines](https://github.com/facebook/react-native/blob/mas Closes https://github.com/facebook/react-native/pull/8071 Differential Revision: D3453372 Pulled By: ericvicenti fbshipit-source-id: 3494faee1a83574c3937a6545b246713ec38e0d0 --- Libraries/CustomComponents/Navigator/Navigator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index e1aa9ed091b766..1a685bb29e0d72 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -373,7 +373,7 @@ var Navigator = React.createClass({ }, _transitionTo: function(destIndex, velocity, jumpSpringTo, cb) { - if (destIndex === this.state.presentedIndex) { + if (destIndex === this.state.presentedIndex && this.state.transitionQueue.length > 0) { return; } if (this.state.transitionFromIndex !== null) { From d929f1ccef98ffb737c22eb640126040f97927c8 Mon Sep 17 00:00:00 2001 From: Chirag Shah Date: Fri, 17 Jun 2016 22:20:18 -0700 Subject: [PATCH 408/843] Added userAgent prop type in docs for webview Summary: userAgent as a prop type is available in WebView for android but is not documented. This PR fixes it. **TestPlan** : Not required as this just adds an entry in the documentation ![webview react native a framework for building native apps using react 2016-06-18 02-14-58](https://cloud.githubusercontent.com/assets/6805530/16164289/e8908526-34fa-11e6-98fe-face38ff9f51.png) Closes https://github.com/facebook/react-native/pull/8200 Differential Revision: D3454625 fbshipit-source-id: 260087044f78a1339cf7ec8760e92cd9fbdb5111 --- Libraries/Components/WebView/WebView.ios.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index c3865963e571c7..9e8fd88ab0ffb0 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -212,6 +212,13 @@ var WebView = React.createClass({ */ injectedJavaScript: PropTypes.string, + /** + * Sets the user-agent for this WebView. The user-agent can also be set in native using + * WebViewConfig. This prop will overwrite that config. + * @platform android + */ + userAgent: PropTypes.string, + /** * Sets whether the webpage scales to fit the view and the user can change the scale. */ From e08197b9984cde795d483522860c7e3ea99ada37 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Sat, 18 Jun 2016 02:30:42 -0700 Subject: [PATCH 409/843] Add note about submitting PRs on master Summary: We got a lot of PRs on stable branches recently, this adds a note about that in CONTRIBUTING that (hopefully) people will read. Closes https://github.com/facebook/react-native/pull/8209 Differential Revision: D3454813 fbshipit-source-id: 4f972d9e5edc1f13b249d9f7a627f84ba9826a0a --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f6c1e24073c2fc..5208e8111d9889 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,8 @@ We will do our best to keep `master` in good shape, with tests passing at all ti The core team will be monitoring for pull requests. When we get one, we'll run some Facebook-specific integration tests on it first. From here, we'll need to get another person to sign off on the changes and then merge the pull request. For API changes we may need to fix internal uses, which could cause some delay. We'll do our best to provide updates and feedback throughout the process. +Please submit your pull request on the *master* branch. If the fix is critical and should be included in a stable branch please mention it and it will be cherry picked into it. + *Before* submitting a pull request, please make sure the following is done… 1. Fork the repo and create your branch from `master`. From c6020a0ef4880bead15a0ed2f4058a70cc9b6ad5 Mon Sep 17 00:00:00 2001 From: Daniel Braun Date: Sat, 18 Jun 2016 08:53:58 -0700 Subject: [PATCH 410/843] Added logging to push registration failure. Summary: It seems it's a common problem people trying to register for push notifications in their simulator, and not understanding why the "register" event never works. I've wasted a few hours myself on this issue. This commit simply logs any failures with push registration, preventing confusion. Closes https://github.com/facebook/react-native/pull/8046 Differential Revision: D3454922 Pulled By: javache fbshipit-source-id: a96896d97d97cfe1bd319e6490750838dfaad3cd --- Libraries/PushNotificationIOS/PushNotificationIOS.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Libraries/PushNotificationIOS/PushNotificationIOS.js b/Libraries/PushNotificationIOS/PushNotificationIOS.js index f6f5ffc90e5469..5089e7ee601b4b 100644 --- a/Libraries/PushNotificationIOS/PushNotificationIOS.js +++ b/Libraries/PushNotificationIOS/PushNotificationIOS.js @@ -70,6 +70,10 @@ const DEVICE_LOCAL_NOTIF_EVENT = 'localNotificationReceived'; * { * [RCTPushNotificationManager didReceiveLocalNotification:notification]; * } + * - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error + * { + * NSLog(@"%@", error); + * } * ``` */ class PushNotificationIOS { From ee0333c65d22edd438b715ccd211c30c635dc931 Mon Sep 17 00:00:00 2001 From: Siqi Liu Date: Sat, 18 Jun 2016 12:46:20 -0700 Subject: [PATCH 411/843] Enable Logging information from redboxes in Android Ads Manager to Scuba Reviewed By: mkonicek Differential Revision: D3433990 fbshipit-source-id: 54fa60fa746c9d0d834f86b7dd2e3ef18a694a32 --- .../devsupport/DevSupportManagerImpl.java | 20 +++++++++++++------ .../react/devsupport/RedBoxHandler.java | 14 ++++++++++++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index d23384bfe48624..0f3995006dc497 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -82,6 +82,10 @@ public class DevSupportManagerImpl implements DevSupportManager { private static final int JAVA_ERROR_COOKIE = -1; private static final String JS_BUNDLE_FILE_NAME = "ReactNativeDevBundle.js"; + private static enum ErrorType { + JS, + NATIVE + } private static final String EXOPACKAGE_LOCATION_FORMAT = "/data/local/tmp/exopackage/%s//secondary-dex"; @@ -184,7 +188,7 @@ public void handleException(Exception e) { @Override public void showNewJavaError(String message, Throwable e) { - showNewError(message, StackTraceHelper.convertJavaStackTrace(e), JAVA_ERROR_COOKIE); + showNewError(message, StackTraceHelper.convertJavaStackTrace(e), JAVA_ERROR_COOKIE, ErrorType.NATIVE); } /** @@ -201,7 +205,7 @@ public void addCustomDevOption( @Override public void showNewJSError(String message, ReadableArray details, int errorCookie) { - showNewError(message, StackTraceHelper.convertJsStackTrace(details), errorCookie); + showNewError(message, StackTraceHelper.convertJsStackTrace(details), errorCookie, ErrorType.JS); } @Override @@ -224,8 +228,9 @@ public void run() { StackFrame[] stack = StackTraceHelper.convertJsStackTrace(details); mRedBoxDialog.setExceptionDetails(message, stack); mRedBoxDialog.setErrorCookie(errorCookie); + // JS errors are reported here after source mapping. if (mRedBoxHandler != null) { - mRedBoxHandler.handleRedbox(message, stack); + mRedBoxHandler.handleRedbox(message, stack, RedBoxHandler.ErrorType.JS); } mRedBoxDialog.show(); } @@ -243,7 +248,8 @@ public void hideRedboxDialog() { private void showNewError( final String message, final StackFrame[] stack, - final int errorCookie) { + final int errorCookie, + final ErrorType errorType) { UiThreadUtil.runOnUiThread( new Runnable() { @Override @@ -259,8 +265,10 @@ public void run() { } mRedBoxDialog.setExceptionDetails(message, stack); mRedBoxDialog.setErrorCookie(errorCookie); - if (mRedBoxHandler != null) { - mRedBoxHandler.handleRedbox(message, stack); + // Only report native errors here. JS errors are reported + // inside {@link #updateJSError} after source mapping. + if (mRedBoxHandler != null && errorType == ErrorType.NATIVE) { + mRedBoxHandler.handleRedbox(message, stack, RedBoxHandler.ErrorType.NATIVE); } mRedBoxDialog.show(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxHandler.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxHandler.java index 61b9bce9006b74..e526257d61bfc9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxHandler.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxHandler.java @@ -17,5 +17,17 @@ * The implementation should be passed by {@link #setRedBoxHandler} in {@link ReactInstanceManager}. */ public interface RedBoxHandler { - void handleRedbox(String title, StackFrame[] stack); + enum ErrorType { + JS("JS"), + NATIVE("Native"); + + private final String name; + ErrorType(String name) { + this.name = name; + } + public String getName() { + return name; + } + } + void handleRedbox(String title, StackFrame[] stack, ErrorType errorType); } From cfa9d5ef34339c31c03e9ce69fd183b3cc9fddcb Mon Sep 17 00:00:00 2001 From: Nathan Azaria Date: Mon, 20 Jun 2016 05:41:30 -0700 Subject: [PATCH 412/843] Added nil check to RCTModuleData Reviewed By: javache Differential Revision: D3456536 fbshipit-source-id: b23e618cf5b4a33822579b456c0f50a41a1c7a31 --- React/Base/RCTModuleData.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/React/Base/RCTModuleData.m b/React/Base/RCTModuleData.m index c1509bbe6eb129..4d6caaf5df01db 100644 --- a/React/Base/RCTModuleData.m +++ b/React/Base/RCTModuleData.m @@ -102,7 +102,7 @@ - (void)setUpInstanceAndBridge } } - if (RCTProfileIsProfiling()) { + if (_instance && RCTProfileIsProfiling()) { RCTProfileHookInstance(_instance); } From dcc2abc1f63f91e53bf01f4bc56c5b7c76300617 Mon Sep 17 00:00:00 2001 From: leeight Date: Mon, 20 Jun 2016 08:42:43 -0700 Subject: [PATCH 413/843] Fix Examples/{UIExplorer,Movies} Summary: Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: (You can skip this if you're fixing a typo or adding an app to the Showcase.) UIExplorerActivity.java and MoviesActivity.java should override `getReactNativeHost` method. And this PR will fix https://github.com/facebook/react-native/issues/8215. **Test plan (required)** Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. Make sure tests pass on both Travis and Circle CI. **Code formatting** Look around. Match the style of the rest of the codebase. See also the simple [style guide](https://github.com/facebook/react-native/blob/master/CONTRIBUTING.md#style-guide). For more info, see the ["Pull Requests" section of our "Contributing" guidelines](https://github.com/facebook/react-native/blob/master/CONTRIBUTING.md#pull-requests). Closes https://github.com/facebook/react-native/pull/8223 Differential Revision: D3456957 fbshipit-source-id: cc0b51e5bfaec71d210bfba81b1f7cd06a723d8c --- .../android/app/src/main/AndroidManifest.xml | 1 + .../facebook/react/movies/MoviesActivity.java | 29 ------- .../react/movies/MoviesApplication.java | 58 ++++++++++++++ .../android/app/src/main/AndroidManifest.xml | 1 + .../app/src/main/java/UIExplorerActivity.java | 78 ------------------- .../react/uiapp/UIExplorerActivity.java | 49 ++++++++++++ .../react/uiapp/UIExplorerApplication.java | 58 ++++++++++++++ 7 files changed, 167 insertions(+), 107 deletions(-) create mode 100644 Examples/Movies/android/app/src/main/java/com/facebook/react/movies/MoviesApplication.java delete mode 100644 Examples/UIExplorer/android/app/src/main/java/UIExplorerActivity.java create mode 100644 Examples/UIExplorer/android/app/src/main/java/com/facebook/react/uiapp/UIExplorerActivity.java create mode 100644 Examples/UIExplorer/android/app/src/main/java/com/facebook/react/uiapp/UIExplorerApplication.java diff --git a/Examples/Movies/android/app/src/main/AndroidManifest.xml b/Examples/Movies/android/app/src/main/AndroidManifest.xml index 9c5b43e36c1691..34f5b0bc47ee60 100644 --- a/Examples/Movies/android/app/src/main/AndroidManifest.xml +++ b/Examples/Movies/android/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:targetSdkVersion="22" /> getPackages() { - return Arrays.asList( - new MainReactPackage() - ); - } } diff --git a/Examples/Movies/android/app/src/main/java/com/facebook/react/movies/MoviesApplication.java b/Examples/Movies/android/app/src/main/java/com/facebook/react/movies/MoviesApplication.java new file mode 100644 index 00000000000000..a620b3eb699416 --- /dev/null +++ b/Examples/Movies/android/app/src/main/java/com/facebook/react/movies/MoviesApplication.java @@ -0,0 +1,58 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.facebook.react.movies; + +import android.app.Application; + +import com.facebook.react.ReactApplication; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.ReactPackage; +import com.facebook.react.shell.MainReactPackage; + +import java.util.Arrays; +import java.util.List; + +import javax.annotation.Nullable; + +public class MoviesApplication extends Application implements ReactApplication { + private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { + @Override + public String getJSMainModuleName() { + return "Examples/Movies/MoviesApp.android"; + } + + @Override + public @Nullable String getBundleAssetName() { + return "MoviesApp.android.bundle"; + } + + @Override + protected boolean getUseDeveloperSupport() { + return true; + } + + @Override + protected List getPackages() { + return Arrays.asList( + new MainReactPackage() + ); + } + }; + + @Override + public ReactNativeHost getReactNativeHost() { + return mReactNativeHost; + } +} diff --git a/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml b/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml index 8efcb2ef4ecef6..ab2757720e6519 100644 --- a/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml +++ b/Examples/UIExplorer/android/app/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ android:targetSdkVersion="23" /> getPackages() { - return Arrays.asList( - new MainReactPackage() - ); - } -} diff --git a/Examples/UIExplorer/android/app/src/main/java/com/facebook/react/uiapp/UIExplorerActivity.java b/Examples/UIExplorer/android/app/src/main/java/com/facebook/react/uiapp/UIExplorerActivity.java new file mode 100644 index 00000000000000..1867e884dc4112 --- /dev/null +++ b/Examples/UIExplorer/android/app/src/main/java/com/facebook/react/uiapp/UIExplorerActivity.java @@ -0,0 +1,49 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.facebook.react.uiapp; + +import android.os.Bundle; + +import com.facebook.react.ReactActivity; + +public class UIExplorerActivity extends ReactActivity { + private final String PARAM_ROUTE = "route"; + private Bundle mInitialProps = null; + + @Override + protected void onCreate(Bundle savedInstanceState) { + // Get remote param before calling super which uses it + Bundle bundle = getIntent().getExtras(); + if (bundle != null && bundle.containsKey(PARAM_ROUTE)) { + String routeUri = new StringBuilder("rnuiexplorer://example/") + .append(bundle.getString(PARAM_ROUTE)) + .append("Example") + .toString(); + mInitialProps = new Bundle(); + mInitialProps.putString("exampleFromAppetizeParams", routeUri); + } + super.onCreate(savedInstanceState); + } + + @Override + protected Bundle getLaunchOptions() { + return mInitialProps; + } + + @Override + protected String getMainComponentName() { + return "UIExplorerApp"; + } +} diff --git a/Examples/UIExplorer/android/app/src/main/java/com/facebook/react/uiapp/UIExplorerApplication.java b/Examples/UIExplorer/android/app/src/main/java/com/facebook/react/uiapp/UIExplorerApplication.java new file mode 100644 index 00000000000000..87dbd3735d4b3d --- /dev/null +++ b/Examples/UIExplorer/android/app/src/main/java/com/facebook/react/uiapp/UIExplorerApplication.java @@ -0,0 +1,58 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.facebook.react.uiapp; + +import android.app.Application; + +import com.facebook.react.ReactApplication; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.ReactPackage; +import com.facebook.react.shell.MainReactPackage; + +import java.util.Arrays; +import java.util.List; + +import javax.annotation.Nullable; + +public class UIExplorerApplication extends Application implements ReactApplication { + private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { + @Override + public String getJSMainModuleName() { + return "Examples/UIExplorer/UIExplorerApp.android"; + } + + @Override + public @Nullable String getBundleAssetName() { + return "UIExplorerApp.android.bundle"; + } + + @Override + public boolean getUseDeveloperSupport() { + return true; + } + + @Override + public List getPackages() { + return Arrays.asList( + new MainReactPackage() + ); + } + }; + + @Override + public ReactNativeHost getReactNativeHost() { + return mReactNativeHost; + } +}; From b39fc05dbb379773bf38fe3442424fe02d3272f1 Mon Sep 17 00:00:00 2001 From: David Aurelio Date: Mon, 20 Jun 2016 10:08:50 -0700 Subject: [PATCH 414/843] Print nicer error message if an asset directory is not found in any of the roots Summary: When an asset is included in a module, and the directory for that asset can't be found in any of the roots, it is hard to debug that, because the error message contains neither the name of the requested file, the sub directory it is located in, nor the roots that have been searched for it. It becomes more difficult, because that exception is created asynchronously. It contains the calling promise code. This diff makes the error message more useful by including the name of the file, the sub directory, and the roots. Reviewed By: bestander Differential Revision: D3456738 fbshipit-source-id: 60b81f04626ad386f7120247c5f5361c81c52968 --- packager/react-packager/src/AssetServer/index.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packager/react-packager/src/AssetServer/index.js b/packager/react-packager/src/AssetServer/index.js index c9b198a500ff7a..65f1edcfc2686b 100644 --- a/packager/react-packager/src/AssetServer/index.js +++ b/packager/react-packager/src/AssetServer/index.js @@ -106,7 +106,8 @@ class AssetServer { return ( this._findRoot( this._roots, - path.dirname(assetPath) + path.dirname(assetPath), + assetPath, ) .then(dir => Promise.all([ dir, @@ -138,7 +139,7 @@ class AssetServer { ); } - _findRoot(roots, dir) { + _findRoot(roots, dir, debugInfoFile) { return Promise.all( roots.map(root => { const absRoot = path.resolve(root); @@ -162,7 +163,9 @@ class AssetServer { return stats[i].path; } } - throw new Error('Could not find any directories'); + + const rootsString = roots.map(s => `'${s}'`).join(', '); + throw new Error(`'${debugInfoFile}' could not be found, because '${dir}' is not a subdirectory of any of the roots (${rootsString})`); }); } @@ -194,7 +197,7 @@ class AssetServer { return map; } - + _getAssetDataFromName(platform, file) { return getAssetDataFromName(file, platform); } From bc8954babbb2384c1e1018ed97e6fa5a2fa0a1c1 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Mon, 20 Jun 2016 10:49:58 -0700 Subject: [PATCH 415/843] Use messageSocket to broadcast reload command on global shortcut Reviewed By: yungsters Differential Revision: D3433464 fbshipit-source-id: 74111f419d224310b546e7c80fa871417436e1ab --- local-cli/server/util/messageSocket.js | 79 +++++++------------------- 1 file changed, 21 insertions(+), 58 deletions(-) diff --git a/local-cli/server/util/messageSocket.js b/local-cli/server/util/messageSocket.js index 3c56aa0544cdec..1a62e30013ea47 100644 --- a/local-cli/server/util/messageSocket.js +++ b/local-cli/server/util/messageSocket.js @@ -15,70 +15,33 @@ function attachToServer(server, path) { server: server, path: path }); - var interfaceSocket, shellSocket; - - function send(dest, message) { - if (!dest) { - return; - } - - try { - dest.send(message); - } catch(e) { - console.warn(e); - // Sometimes this call throws 'not opened' - } + var clients = []; + + function sendFrom(source, data) { + clients.forEach((client) => { + if (client !== source) { + try { + client.send(data); + } catch (e) { + // Sometimes this call throws 'not opened' + } + } + }); } wss.on('connection', function(ws) { - const {url} = ws.upgradeReq; - - if (url.indexOf('role=interface') > -1) { - if (interfaceSocket) { - ws.close(1011, 'Another debugger is already connected'); - return; - } - interfaceSocket = ws; - interfaceSocket.onerror = - interfaceSocket.onclose = () => { - interfaceSocket = null; - // if (shellSocket) { - // shellSocket.close(1011, 'Interface was disconnected'); - // } - }; - - interfaceSocket.onmessage = ({data}) => { - send(shellSocket, data) - }; - } else if (url.indexOf('role=shell') > -1) { - if (shellSocket) { - shellSocket.onerror = shellSocket.onclose = shellSocket.onmessage = null; - shellSocket.close(1011, 'Another client connected'); - } - shellSocket = ws; - shellSocket.onerror = - shellSocket.onclose = () => { - shellSocket = null; - send(interfaceSocket, JSON.stringify({method: '$disconnected'})); - }; - shellSocket.onmessage = ({data}) => send(interfaceSocket, data); - - // console.log('CLIENT ----'); - // if (doIt) { - // console.log('> sending: %s', str); - // send(shellSocket, str); - // console.log('< sending'); - // } - - } else { - ws.close(1011, 'Missing role param'); - } + clients.push(ws); + ws.onclose = + ws.onerror = () => { + ws.onmessage = null; + clients = clients.filter((client) => client !== ws); + }; + ws.onmessage = ({data}) => sendFrom(ws, data); }); return { - server: wss, - isChromeConnected: function() { - return !!interfaceSocket; + broadcast: (message) => { + sendFrom(null, JSON.stringify(message)); } }; } From 662ec705cbc42f1cbba5d20f6d0d4d809cbb8556 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Mon, 20 Jun 2016 10:49:59 -0700 Subject: [PATCH 416/843] Reconnect RCTWebSocketManager when packager restarts Reviewed By: bottledwalter Differential Revision: D3434769 fbshipit-source-id: a0b165129b66d03403defb39a20c86ab982fc8b5 --- Libraries/WebSocket/RCTWebSocketManager.m | 35 ++++++++++++++--------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/Libraries/WebSocket/RCTWebSocketManager.m b/Libraries/WebSocket/RCTWebSocketManager.m index eaf67d78e20ddc..16b31958358309 100644 --- a/Libraries/WebSocket/RCTWebSocketManager.m +++ b/Libraries/WebSocket/RCTWebSocketManager.m @@ -20,7 +20,9 @@ #pragma mark - RCTWebSocketObserver -@interface RCTWebSocketObserver : NSObject +@interface RCTWebSocketObserver : NSObject { + NSURL *_url; +} @property (nonatomic, strong) RCTSRWebSocket *socket; @property (nonatomic, weak) id delegate; @@ -35,9 +37,7 @@ @implementation RCTWebSocketObserver - (instancetype)initWithURL:(NSURL *)url delegate:(id)delegate { if ((self = [self init])) { - _socket = [[RCTSRWebSocket alloc] initWithURL:url]; - _socket.delegate = self; - + _url = url; _delegate = delegate; } return self; @@ -45,9 +45,11 @@ - (instancetype)initWithURL:(NSURL *)url delegate:(id - (void)start { - _socketOpenSemaphore = dispatch_semaphore_create(0); + [self stop]; + _socket = [[RCTSRWebSocket alloc] initWithURL:_url]; + _socket.delegate = self; + [_socket open]; - dispatch_semaphore_wait(_socketOpenSemaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 2)); } - (void)stop @@ -71,18 +73,25 @@ - (void)webSocket:(RCTSRWebSocket *)webSocket didReceiveMessage:(id)message } } -- (void)webSocketDidOpen:(RCTSRWebSocket *)webSocket +- (void)reconnect { - dispatch_semaphore_signal(_socketOpenSemaphore); + __weak RCTSRWebSocket *socket = _socket; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + // Only reconnect if the observer wasn't stoppped while we were waiting + if (socket) { + [self start]; + } + }); } - (void)webSocket:(RCTSRWebSocket *)webSocket didFailWithError:(NSError *)error { - dispatch_semaphore_signal(_socketOpenSemaphore); - dispatch_async(dispatch_get_main_queue(), ^{ - // Give the setUp method an opportunity to report an error first - RCTLogError(@"WebSocket connection failed with error %@", error); - }); + [self reconnect]; +} + +- (void)webSocket:(RCTSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean +{ + [self reconnect]; } @end From d26ce2e2c0a38235d2fc394743b64893037afec0 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Mon, 20 Jun 2016 10:50:01 -0700 Subject: [PATCH 417/843] Attempt packager connection even when loaded from offline bundle Reviewed By: bottledwalter Differential Revision: D3446086 fbshipit-source-id: b6400cf9b1ec0b59bb53f94b306698359072fb7a --- React/Modules/RCTDevMenu.m | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m index d7cbc5dc5724f1..0b2fdaa9e9faa0 100644 --- a/React/Modules/RCTDevMenu.m +++ b/React/Modules/RCTDevMenu.m @@ -239,11 +239,12 @@ - (instancetype)init - (NSURL *)packagerURL { NSString *host = [_bridge.bundleURL host]; + NSString *scheme = [_bridge.bundleURL scheme]; if (!host) { - return nil; + host = @"localhost"; + scheme = @"http"; } - NSString *scheme = [_bridge.bundleURL scheme]; NSNumber *port = [_bridge.bundleURL port]; if (!port) { port = @8081; // Packager default port From cc8cf8f0f286412a76c4a310b6ec3ddb8d32d942 Mon Sep 17 00:00:00 2001 From: Christine Abernathy Date: Mon, 20 Jun 2016 12:23:33 -0700 Subject: [PATCH 418/843] Simplify guide and make it more prescriptive Summary: This is related to #8157. Updated the guide and made it simpler and more prescriptive. **Testing** - cd react-native - npm install - cd website - npm install - npm start - Go to http://localhost:8079/react-native/docs/platform-specific-code.html - Checked content ![platform-specific-update](https://cloud.githubusercontent.com/assets/691109/16205926/f5fd0662-36da-11e6-978c-e364688facf6.png) Closes https://github.com/facebook/react-native/pull/8251 Differential Revision: D3458249 Pulled By: JoelMarcey fbshipit-source-id: 5568337af92da53ee6cd53541591eb01ae323f4f --- docs/PlatformSpecificInformation.md | 75 ++++++++++++++--------------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/docs/PlatformSpecificInformation.md b/docs/PlatformSpecificInformation.md index 260b00b4661972..561acaeecaee4d 100644 --- a/docs/PlatformSpecificInformation.md +++ b/docs/PlatformSpecificInformation.md @@ -7,34 +7,12 @@ permalink: docs/platform-specific-code.html next: native-modules-ios --- -When building a cross-platform app, the need to write different code for different platforms may arise. This can always be achieved by organizing the various components in different folders: +When building a cross-platform app, you'll want to re-use as much code as possible. Scenarios may arise where it makes sense for the code to be different, for example you may want to implement separate visual components for iOS and Android. -```sh -/common/components/ -/android/components/ -/ios/components/ -``` - -Another option may be naming the components differently depending on the platform they are going to be used in: - -```sh -BigButtonIOS.js -BigButtonAndroid.js -``` - -But React Native provides two alternatives to easily organize your code separating it by platform: - -## Platform specific extensions -React Native will detect when a file has a `.ios.` or `.android.` extension and load the right file for each platform when requiring them from other components. - -For example, you can have these files in your project: - -```sh -BigButton.ios.js -BigButton.android.js -``` +React Native provides two ways to easily organize your code and separate it by platform: -With this setup, you can just require the files from a different component without paying attention to the platform in which the app will run. +* Using the `Platform` module. +* Using platform-specific file extensions. ```javascript import BigButton from './components/BigButton'; @@ -43,25 +21,24 @@ import BigButton from './components/BigButton'; React Native will import the correct component for the running platform. ## Platform module -A module is provided by React Native to detect what is the platform in which the app is running. This piece of functionality can be useful when only small parts of a component are platform specific. +React Native provides a module that detects the platform in which the app is running. You can use the detection logic to implement platform-specific code. Use this option when only small parts of a component are platform-specific. ```javascript -var { Platform } = ReactNative; +import { Platform, StyleSheet } from 'react-native'; -var styles = StyleSheet.create({ +const styles = StyleSheet.create({ height: (Platform.OS === 'ios') ? 200 : 100, }); ``` -`Platform.OS` will be `ios` when running in iOS and `android` when running in an Android device or simulator. +`Platform.OS` will be `ios` when running on iOS and `android` when running on Android. -There is also a `Platform.select` method available, that given an object containing Platform.OS as keys, -returns the value for the platform you are currently running on. +There is also a `Platform.select` method available, that given an object containing Platform.OS as keys, returns the value for the platform you are currently running on. ```javascript -var { Platform } = ReactNative; +import { Platform, StyleSheet } from 'react-native'; -var styles = StyleSheet.create({ +const styles = StyleSheet.create({ container: { flex: 1, ...Platform.select({ @@ -76,13 +53,12 @@ var styles = StyleSheet.create({ }); ``` -This will result in a container having `flex: 1` on both platforms and backgroundColor - red on iOS and blue -on Android. +This will result in a container having `flex: 1` on both platforms, a red background color on iOS, and a blue background color on Android. Since it accepts `any` value, you can also use it to return platform specific component, like below: ```javascript -var Component = Platform.select({ +const Component = Platform.select({ ios: () => require('ComponentIOS'), android: () => require('ComponentAndroid'), })(); @@ -90,13 +66,32 @@ var Component = Platform.select({ ; ``` -###Detecting Android version -On Android, the Platform module can be also used to detect which is the version of the Android Platform in which the app is running +### Detecting the Android version + +On Android, the `Platform` module can also be used to detect the version of the Android Platform in which the app is running: ```javascript -var {Platform} = ReactNative; +import { Platform } from 'react-native'; if(Platform.Version === 21){ console.log('Running on Lollipop!'); } ``` + +## Platform-specific extensions +When your platform-specific code is more complex, you should consider splitting the code out into separate files. React Native will detect when a file has a `.ios.` or `.android.` extension and load the relevant platform file when required from other components. + +For example, say you have the following files in your project: + +```sh +BigButton.ios.js +BigButton.android.js +``` + +You can then require the component as follows: + +```javascript +const BigButton = require('./BigButton'); +``` + +React Native will automatically pick up the right file based on the running platform. From 6128b7236fe854c1642360f2884d8bbdc6a13c37 Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Mon, 20 Jun 2016 12:43:41 -0700 Subject: [PATCH 419/843] Synchronously fail when calling JS too early Differential Revision: D3453476 fbshipit-source-id: 3fbfda46b4cb7d31f554df6760c5146c412ff468 --- .../java/com/facebook/react/testing/ReactTestHelper.java | 7 ++++++- .../com/facebook/react/cxxbridge/CatalystInstanceImpl.java | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactTestHelper.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactTestHelper.java index 5d4ebb2d1d0d4d..45f4efeb7a9bcd 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactTestHelper.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactTestHelper.java @@ -137,7 +137,12 @@ public Void call() throws Exception { return null; } }).get(); - + InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { + @Override + public void run() { + instance.initialize(); + } + }); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java index 753ef65a61f1dc..d024e9cda0f172 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java @@ -191,6 +191,9 @@ public void callFunction( FLog.w(ReactConstants.TAG, "Calling JS function after bridge has been destroyed."); return; } + if (!mInitialized) { + throw new RuntimeException("Attempt to call JS function before instance initialization."); + } callJSFunction(executorToken, module, method, arguments, tracingName); } From f2affcf24dd7ff12455c0bc12f693dda5a196244 Mon Sep 17 00:00:00 2001 From: Joel Marcey Date: Mon, 20 Jun 2016 15:05:20 -0700 Subject: [PATCH 420/843] Separate Out Core Components Into Individual Parts Summary: Will create new issue to add more information to the `Components` section of the Tutorial since that was gutted by this change. Fixes #8156 Closes https://github.com/facebook/react-native/pull/8256 Differential Revision: D3459601 Pulled By: JoelMarcey fbshipit-source-id: 4038afc463bffcf8efda36d29bc7c443bbc8f4bd --- docs/Basics-Component-Image.md | 30 ++++ docs/Basics-Component-ListView.md | 44 ++++++ docs/Basics-Component-Text.md | 26 ++++ docs/Basics-Component-TextInput.md | 28 ++++ docs/Basics-Component-View.md | 32 ++++ docs/Basics-Components.md | 20 +++ ... => Basics-IntegrationWithExistingApps.md} | 6 +- docs/QuickStart-GettingStarted.md | 2 +- docs/Tutorial-CoreComponents.md | 142 ------------------ 9 files changed, 184 insertions(+), 146 deletions(-) create mode 100644 docs/Basics-Component-Image.md create mode 100644 docs/Basics-Component-ListView.md create mode 100644 docs/Basics-Component-Text.md create mode 100644 docs/Basics-Component-TextInput.md create mode 100644 docs/Basics-Component-View.md create mode 100644 docs/Basics-Components.md rename docs/{Tutorial-IntegrationWithExistingApps.md => Basics-IntegrationWithExistingApps.md} (99%) delete mode 100644 docs/Tutorial-CoreComponents.md diff --git a/docs/Basics-Component-Image.md b/docs/Basics-Component-Image.md new file mode 100644 index 00000000000000..262736048cdadf --- /dev/null +++ b/docs/Basics-Component-Image.md @@ -0,0 +1,30 @@ +--- +id: basics-component-image +title: Image +layout: docs +category: Basics +permalink: docs/basics-component-image.html +next: basics-component-view +--- + +The other basic React Native component is the [`Image`](/react-native/docs/image.html#content) component. Like `Text`, the `Image` component simply renders an image. + +> An `Image` is analogous to using `img` when building websites. + +The simplest way to render an image is to provide a source file to that image via the `source` attribute. + +This example displays a checkbox `Image` on the device. + +```JavaScript +import React from 'react'; +import { AppRegistry, Image } from 'react-native'; + +const App = () => { + return ( + + ); +} + +// App registration and rendering +AppRegistry.registerComponent('MyApp', () => App); +``` diff --git a/docs/Basics-Component-ListView.md b/docs/Basics-Component-ListView.md new file mode 100644 index 00000000000000..63844ed17d444c --- /dev/null +++ b/docs/Basics-Component-ListView.md @@ -0,0 +1,44 @@ +--- +id: basics-component-listview +title: ListView +layout: docs +category: Basics +permalink: docs/basics-component-listview.html +next: basics-integration-with-existing-apps +--- + +On mobile devices, lists are a core element in many applications. The [`ListView`](/react-native/docs/listview.html#content) component is a special type of [`View`](/react-native/docs/tutorial-component-view.html) that displays a vertically scrolling list of changing data. + +The `ListView` component requires two properties, `dataSource` and `renderRow`. `dataSource` is the actual source of information that will be part of the list. `renderRow` takes the data and returns a renderable component to display. + +This example creates a simple `ListView` of hardcoded data. It first initializes the `datasource` that will be used to populate the `ListView`. Then it renders that `ListView` with that data. + +> A `rowHasChanged` function is required to use `ListView`. Here we just say a row has changed if the row we are on is not the same as the previous row. + +```JavaScript +import React from 'react'; +import { AppRegistry, Text, View, ListView} from 'react-native'; + +var SimpleList = React.createClass({ + // Initialize the hardcoded data + getInitialState: function() { + var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); + return { + dataSource: ds.cloneWithRows(['John', 'Joel', 'James', 'Jimmy', 'Jackson', 'Jillian', 'Julie']) + }; + }, + render: function() { + return ( + + {rowData}} + /> + + ); + } +}); + +// App registration and rendering +AppRegistry.registerComponent('MyApp', () => SimpleList); +``` diff --git a/docs/Basics-Component-Text.md b/docs/Basics-Component-Text.md new file mode 100644 index 00000000000000..3167375f788ec1 --- /dev/null +++ b/docs/Basics-Component-Text.md @@ -0,0 +1,26 @@ +--- +id: basics-component-text +title: Text +layout: docs +category: Basics +permalink: docs/basics-component-text.html +next: basics-component-image +--- + +The most basic component in React Native is the [`Text`](/react-native/docs/text.html#content) component. The `Text` component simply renders text. + +This example displays the `string` `"Hello World!"` on the device. + +```JavaScript +import React from 'react'; +import { AppRegistry, Text } from 'react-native'; + +const App = () => { + return ( + Hello World! + ); +} + +// App registration and rendering +AppRegistry.registerComponent('MyApp', () => App); +``` diff --git a/docs/Basics-Component-TextInput.md b/docs/Basics-Component-TextInput.md new file mode 100644 index 00000000000000..0942e2faec08bb --- /dev/null +++ b/docs/Basics-Component-TextInput.md @@ -0,0 +1,28 @@ +--- +id: basics-component-textinput +title: TextInput +layout: docs +category: Basics +permalink: docs/basics-component-textinput.html +next: basics-component-listview +--- + +Direct text-based user input is a foundation for many apps. Writing a post or comment on a page is a canonical example of this. [`TextInput`](/react-native/docs/textinput.html#content) is a basic component that allows the user to enter text. + +This example creates a simple `TextInput` box with the `string` `Hello` as the placeholder when the `TextInput` is empty. + +```JavaScript +import React from 'react'; +import { AppRegistry, TextInput, View } from 'react-native'; + +const App = () => { + return ( + + + + ); +} + +// App registration and rendering +AppRegistry.registerComponent('MyApp', () => App); +``` diff --git a/docs/Basics-Component-View.md b/docs/Basics-Component-View.md new file mode 100644 index 00000000000000..47e2c0530a61b1 --- /dev/null +++ b/docs/Basics-Component-View.md @@ -0,0 +1,32 @@ +--- +id: basics-component-view +title: View +layout: docs +category: Basics +permalink: docs/basics-component-view.html +next: basics-component-textinput +--- + +A [`View`](/react-native/docs/view.html#content) is the most basic building block for a React Native application. The `View` is an abstraction on top of the target platform's native equivalent, such as iOS's `UIView`. + +> A `View` is analogous to using a `div` for building websites. + +While basic components such as `Text` and `Image`, can be displayed without a `View`, this is not generally recommended since the `View` gives you the control for styling and layout of those components. + +This example creates a `View` that aligns the `string` `Hello` in the top center of the device, something which could not be done with a `Text` component alone (i.e., a `Text` component without a `View` would place the `string` in a fixed location in the upper corner): + +```JavaScript +import React from 'react'; +import { AppRegistry, Text, View } from 'react-native'; + +const App = () => { + return ( + + Hello! + + ); +} + +// App registration and rendering +AppRegistry.registerComponent('MyApp', () => App); +``` diff --git a/docs/Basics-Components.md b/docs/Basics-Components.md new file mode 100644 index 00000000000000..a9bd13ca12dfef --- /dev/null +++ b/docs/Basics-Components.md @@ -0,0 +1,20 @@ +--- +id: basics-components +title: Components +layout: docs +category: Basics +permalink: docs/basics-components.html +next: basics-component-text +--- + +Components are the building blocks for a React Native application. A React Native user interface (UI) is specified by declaring components, possibly nested, and then those components are mapped to the native UI on the targeted platform. + +## Core Components. + +React Native has a number of core components that are commonly used in applications, either on their own or combined to build new components. + +- [Text](/react-native/docs/tutorial-component-text.html) +- [Image](/react-native/docs/tutorial-component-image.html) +- [View](/react-native/docs/tutorial-component-view.html) +- [TextInput](/react-native/docs/tutorial-component-textinput.html) +- [ListView](/react-native/docs/tutorial-component-listview.html) diff --git a/docs/Tutorial-IntegrationWithExistingApps.md b/docs/Basics-IntegrationWithExistingApps.md similarity index 99% rename from docs/Tutorial-IntegrationWithExistingApps.md rename to docs/Basics-IntegrationWithExistingApps.md index 9ed7b870f44416..e356a2ae439a26 100644 --- a/docs/Tutorial-IntegrationWithExistingApps.md +++ b/docs/Basics-IntegrationWithExistingApps.md @@ -1,9 +1,9 @@ --- -id: tutorial-integration-with-existing-apps +id: basics-integration-with-existing-apps title: Integration With Existing Apps layout: docs -category: Tutorials -permalink: docs/tutorial-integration-with-existing-apps.html +category: Basics +permalink: docs/basics-integration-with-existing-apps.html next: sample-application-movies --- diff --git a/docs/QuickStart-GettingStarted.md b/docs/QuickStart-GettingStarted.md index 249296abf0e91f..a35288e5241406 100644 --- a/docs/QuickStart-GettingStarted.md +++ b/docs/QuickStart-GettingStarted.md @@ -4,7 +4,7 @@ title: Getting Started layout: docs category: Quick Start permalink: docs/getting-started.html -next: tutorial-core-components +next: basics-components --- diff --git a/docs/Tutorial-CoreComponents.md b/docs/Tutorial-CoreComponents.md deleted file mode 100644 index a0440dc2cc3eb0..00000000000000 --- a/docs/Tutorial-CoreComponents.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -id: tutorial-core-components -title: Core Components -layout: docs -category: Tutorials -permalink: docs/tutorial-core-components.html -next: tutorial-integration-with-existing-apps ---- - -Components are the building blocks for a React Native application. A React Native user interface (UI) is specified by declaring components, possibly nested, and then those components are mapped to the native UI on the targeted platform. - -React Native has a number of core components that are commonly used in applications, either on their own or combined to build new components. - -## Text - -The most basic component in React Native is the [`Text`](/react-native/docs/text.html#content) component. The `Text` component simply renders text. - -This example displays the `string` `"Hello World!"` on the device. - -```JavaScript -import React from 'react'; -import { AppRegistry, Text } from 'react-native'; - -const App = () => { - return ( - Hello World! - ); -} - -// App registration and rendering -AppRegistry.registerComponent('MyApp', () => App); -``` - -## Image - -The other basic React Native component is the [`Image`](/react-native/docs/image.html#content) component. Like `Text`, the `Image` component simply renders an image. - -> An `Image` is analogous to using `img` when building websites. - -The simplest way to render an image is to provide a source file to that image via the `source` attribute. - -This example displays a checkbox `Image` on the device. - -```JavaScript -import React from 'react'; -import { AppRegistry, Image } from 'react-native'; - -const App = () => { - return ( - - ); -} - -// App registration and rendering -AppRegistry.registerComponent('MyApp', () => App); -``` - -## View - -A [`View`](/react-native/docs/view.html#content) is the most basic building block for a React Native application. The `View` is an abstraction on top of the target platform's native equivalent, such as iOS's `UIView`. - -> A `View` is analogous to using a `div` for building websites. - -While basic components such as `Text` and `Image`, can be displayed without a `View`, this is not generally recommended since the `View` gives you the control for styling and layout of those components. - -This example creates a `View` that aligns the `string` `Hello` in the top center of the device, something which could not be done with a `Text` component alone (i.e., a `Text` component without a `View` would place the `string` in a fixed location in the upper corner): - -```JavaScript -import React from 'react'; -import { AppRegistry, Text, View } from 'react-native'; - -const App = () => { - return ( - - Hello! - - ); -} - -// App registration and rendering -AppRegistry.registerComponent('MyApp', () => App); -``` - -## TextInput - -Direct text-based user input is a foundation for many apps. Writing a post or comment on a page is a canonical example of this. [`TextInput`](/react-native/docs/textinput.html#content) is a basic component that allows the user to enter text. - -This example creates a simple `TextInput` box with the `string` `Hello` as the placeholder when the `TextInput` is empty. - -```JavaScript -import React from 'react'; -import { AppRegistry, TextInput, View } from 'react-native'; - -const App = () => { - return ( - - - - ); -} - -// App registration and rendering -AppRegistry.registerComponent('MyApp', () => App); -``` - -## ListView - -On mobile devices, lists are a core element in many applications. The [`ListView`](/react-native/docs/listview.html#content) component is a special type of [`View`](/react-native/docs/tutorials/core-components.html#view) that displays a vertically scrolling list of changing data. - -The `ListView` component requires two properties, `dataSource` and `renderRow`. `dataSource` is the actual source of information that will be part of the list. `renderRow` takes the data and returns a renderable component to display. - -This example creates a simple `ListView` of hardcoded data. It first initializes the `datasource` that will be used to populate the `ListView`. Then it renders that `ListView` with that data. - -> A `rowHasChanged` function is required to use `ListView`. Here we just say a row has changed if the row we are on is not the same as the previous row. - -```JavaScript -import React from 'react'; -import { AppRegistry, Text, View, ListView} from 'react-native'; - -var SimpleList = React.createClass({ - // Initialize the hardcoded data - getInitialState: function() { - var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); - return { - dataSource: ds.cloneWithRows(['John', 'Joel', 'James', 'Jimmy', 'Jackson', 'Jillian', 'Julie']) - }; - }, - render: function() { - return ( - - {rowData}} - /> - - ); - } -}); - -// App registration and rendering -AppRegistry.registerComponent('MyApp', () => SimpleList); -``` From bdf58ae780c537aaaa3834707002242bbd029d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Mon, 20 Jun 2016 15:20:16 -0700 Subject: [PATCH 421/843] Cleanup troubleshooting and debugging docs. Summary: This is a followup to #8010. Troubleshooting has been updated to list only those issues that may affect a user that is setting up their environment. Any issues related to day to day use have been moved or merged into a more relevant doc. Closes https://github.com/facebook/react-native/pull/8254 Reviewed By: caabernathy Differential Revision: D3459018 Pulled By: JoelMarcey fbshipit-source-id: dd76097af34bd33dda376fab39fb0f71061ef3e4 --- docs/Debugging.md | 48 +++++----- docs/QuickStart-GettingStarted.md | 8 +- docs/RunningOnDeviceIOS.md | 2 +- docs/Troubleshooting.md | 85 ++++++------------ .../src/react-native/img/DeveloperMenu.png | Bin 0 -> 49640 bytes 5 files changed, 61 insertions(+), 82 deletions(-) create mode 100644 website/src/react-native/img/DeveloperMenu.png diff --git a/docs/Debugging.md b/docs/Debugging.md index a3143a77811157..a1163438fb585b 100644 --- a/docs/Debugging.md +++ b/docs/Debugging.md @@ -7,27 +7,11 @@ permalink: docs/debugging.html next: testing --- -## In-app Errors and Warnings - -Errors and warnings are displayed inside your app in development builds. - -### Errors - -In-app errors are displayed in a full screen alert with a red background inside your app. This screen is known as a RedBox. You can use `console.error()` to manually trigger one. - -### Warnings - -Warnings will be displayed on screen with a yellow background. These alerts are known as YellowBoxes. Click on the alerts to show more information or to dismiss them. - -As with a RedBox, you can use `console.warn()` to trigger a YellowBox. - -YellowBoxes can be disabled during development by using `console.disableYellowBox = true;`. Specific warnings can be ignored programmatically by setting an array of prefixes that should be ignored: `console.ignoredYellowBox = ['Warning: ...'];` - -> RedBoxes and YellowBoxes are automatically disabled in release (production) builds. - ## Accessing the In-App Developer Menu -You can access the developer menu by shaking your device. You can also use the `Command⌘ + D` keyboard shortcut when your app is running in the iPhone Simulator, or `Command⌘ + M` when running in an Android emulator. +You can access the developer menu by shaking your device or by selecting "Shake Gesture" inside the Hardware menu in the iOS Simulator. You can also use the `Command⌘ + D` keyboard shortcut when your app is running in the iPhone Simulator, or `Command⌘ + M` when running in an Android emulator. + +![](img/DeveloperMenu.png) > The Developer Menu is disabled in release (production) builds. @@ -35,20 +19,42 @@ You can access the developer menu by shaking your device. You can also use the ` Selecting `Reload` from the Developer Menu will reload the JavaScript that powers your application. You can also press `Command⌘ + R` in the iOS Simulator, or press `R` twice on Android emulators. +> If you are using a Dvorak/Colemak layout, use the `Command⌘ + P` keyboard shortcut to reload the simulator. + You will need to rebuild your app for changes to take effect in certain situations: * You have added new resources to your native app's bundle, such as an image in `Images.xcassets` on iOS or in `res/drawable` folder on Android. * You have modified native code (Objective-C/Swift on iOS or Java/C++ on Android). +> If the `Command⌘ + R` keyboard shortcut does not seem to reload the iOS Simulator, go to the Hardware menu, select Keyboard, and make sure that "Connect Hardware Keyboard" is checked. + ### Automatic reloading You may enable Live Reload to automatically trigger a reload whenever your JavaScript code changes. Live Reload is available on iOS via the Developer Menu. On Android, select "Dev Settings" from the Developer Menu and enable "Auto reload on JS change". +## In-app Errors and Warnings + +Errors and warnings are displayed inside your app in development builds. + +### Errors + +In-app errors are displayed in a full screen alert with a red background inside your app. This screen is known as a RedBox. You can use `console.error()` to manually trigger one. + +### Warnings + +Warnings will be displayed on screen with a yellow background. These alerts are known as YellowBoxes. Click on the alerts to show more information or to dismiss them. + +As with a RedBox, you can use `console.warn()` to trigger a YellowBox. + +YellowBoxes can be disabled during development by using `console.disableYellowBox = true;`. Specific warnings can be ignored programmatically by setting an array of prefixes that should be ignored: `console.ignoredYellowBox = ['Warning: ...'];` + +> RedBoxes and YellowBoxes are automatically disabled in release (production) builds. + ## Accessing logs -To view detailed logs on iOS, open your app in Xcode, then Build and Run your app on a device or the iPhone Simulator. The console should appear automatically after the app launches. +To view detailed logs on iOS, open your app in Xcode, then Build and Run your app on a device or the iPhone Simulator. The console should appear automatically after the app launches. If your app is failing to build, check the Issues Navigator in Xcode. Run `adb logcat *:S ReactNative:V ReactNativeJS:V` in a terminal to display the logs for an Android app running on a device or an emulator. @@ -68,6 +74,8 @@ On Android 5.0+ devices connected via USB, you can use the [`adb` command line t Alternatively, select `Dev Settings` from the Developer Menu, then update the `Debug server host for device` setting to match the IP address of your computer. +> If you run into any issues, it may be possible that one of your Chrome extensions is interacting in unexpected ways with the debugger. Try disabling all of your extensions and re-enabling them one-by-one until you find the problematic extension. + ### Debugging using a custom JavaScript debugger To use a custom JavaScript debugger in place of Chrome Developer Tools, set the `REACT_DEBUGGER` environment variable to a command that will start your custom debugger. You can then select `Debug JS Remotely` from the Developer Menu to start debugging. diff --git a/docs/QuickStart-GettingStarted.md b/docs/QuickStart-GettingStarted.md index a35288e5241406..8633e4fcd4750d 100644 --- a/docs/QuickStart-GettingStarted.md +++ b/docs/QuickStart-GettingStarted.md @@ -282,6 +282,8 @@ Congratulations! You've successfully run and modified your first React Native ap ## Common Followups +- Learn how to access the Developer Menu, reload your JavaScript, access logs, and more in the [Debugging guide](docs/debugging.html#content). + - If you want to run on a physical device, see the [Running on iOS Device page](docs/running-on-device-ios.html#content). @@ -292,15 +294,17 @@ Congratulations! You've successfully run and modified your first React Native ap -- If you run into any issues getting started, see the [Troubleshooting](docs/troubleshooting.html#content) and [Debugging](docs/debugging.html#content) pages. +- If you run into any issues getting started, see the [Troubleshooting](docs/troubleshooting.html#content) page. ## Common Followups +- Learn how to access the Developer Menu, reload your JavaScript, access logs, and more in the [Debugging guide](docs/debugging.html#content). + - If you want to run on a physical device, see the [Running on Android Device page](docs/running-on-device-android.html#content). -- If you run into any issues getting started, see the [Troubleshooting](docs/troubleshooting.html#content) and [Debugging](docs/debugging.html#content) pages. +- If you run into any issues getting started, see the [Troubleshooting](docs/troubleshooting.html#content) page. - -## Declare Styles - -The way to declare styles in React Native is the following: - -```javascript -var styles = StyleSheet.create({ - base: { - width: 38, - height: 38, - }, - background: { - backgroundColor: '#222222', - }, - active: { - borderWidth: 2, - borderColor: '#00ff00', - }, -}); -``` - -`StyleSheet.create` construct is optional but provides some key advantages. It ensures that the values are **immutable** and **opaque** by transforming them into plain numbers that reference an internal table. By putting it at the end of the file, you also ensure that they are only created once for the application and not on every render. - -All the attribute names and values are a subset of what works on the web. For layout, React Native implements [Flexbox](docs/flexbox.html). - -## Using Styles - -All the core components accept a style attribute. - -```javascript - - -``` - -They also accept an array of styles. - -```javascript - -``` - -The behavior is the same as `Object.assign`: in case of conflicting values, the one from the right-most element will have precedence and falsy values like `false`, `undefined` and `null` will be ignored. A common pattern is to conditionally add a style based on some condition. - -```javascript - -``` - -Finally, if you really have to, you can also create style objects in render, but they are highly discouraged. Put them last in the array definition. - -```javascript - -``` - -## Pass Styles Around - -In order to let a call site customize the style of your component children, you can pass styles around. Use `View.propTypes.style` and `Text.propTypes.style` in order to make sure only styles are being passed. - -```javascript -var List = React.createClass({ - propTypes: { - style: View.propTypes.style, - elementStyle: View.propTypes.style, - }, - render: function() { - return ( - - {elements.map((element) => - - )} - - ); - } -}); - -// ... in another file ... - -``` -## Supported Properties - -You can checkout latest support of CSS Properties in following Links. - -- [View Properties](docs/view.html#style) -- [Image Properties](docs/image.html#style) -- [Text Properties](docs/text.html#style) -- [Flex Properties](docs/flexbox.html#content) -- [Transform Properties](docs/transforms.html#content) From df6d18358e49ac6c847988087f19c40443e3a607 Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Thu, 23 Jun 2016 14:47:56 -0700 Subject: [PATCH 506/843] Serialize params when making/queuing native call Reviewed By: mhorowitz Differential Revision: D3460507 fbshipit-source-id: a0600ffe3da89791af3eb64fc2973eb6aafa7d2b --- Libraries/BatchedBridge/BatchedBridge.js | 5 ++++- Libraries/Utilities/MessageQueue.js | 6 ++++-- ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp | 3 +++ ReactCommon/cxxreact/JSCExecutor.cpp | 7 ++++++- ReactCommon/cxxreact/MethodCall.cpp | 10 +++++++--- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Libraries/BatchedBridge/BatchedBridge.js b/Libraries/BatchedBridge/BatchedBridge.js index a75c59303c95af..77dc9e6a5d562d 100644 --- a/Libraries/BatchedBridge/BatchedBridge.js +++ b/Libraries/BatchedBridge/BatchedBridge.js @@ -12,8 +12,11 @@ const MessageQueue = require('MessageQueue'); +const serializeNativeParams = typeof global.__fbBatchedBridgeSerializeNativeParams !== 'undefined'; + const BatchedBridge = new MessageQueue( - () => global.__fbBatchedBridgeConfig + () => global.__fbBatchedBridgeConfig, + serializeNativeParams ); // TODO: Move these around to solve the cycle in a cleaner way. diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index cc24f575201238..7f8c7067f55863 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -49,7 +49,7 @@ type Config = { }; class MessageQueue { - constructor(configProvider: () => Config) { + constructor(configProvider: () => Config, serializeNativeParams: boolean) { this._callableModules = {}; this._queue = [[], [], [], 0]; this._callbacks = []; @@ -57,6 +57,7 @@ class MessageQueue { this._callID = 0; this._lastFlush = 0; this._eventLoopStartTime = new Date().getTime(); + this._serializeNativeParams = serializeNativeParams; if (__DEV__) { this._debugInfo = {}; @@ -150,6 +151,7 @@ class MessageQueue { onSucc && params.push(this._callbackID); this._callbacks[this._callbackID++] = onSucc; } + var preparedParams = this._serializeNativeParams ? JSON.stringify(params) : params; if (__DEV__) { global.nativeTraceBeginAsyncFlow && @@ -159,7 +161,7 @@ class MessageQueue { this._queue[MODULE_IDS].push(module); this._queue[METHOD_IDS].push(method); - this._queue[PARAMS].push(params); + this._queue[PARAMS].push(preparedParams); const now = new Date().getTime(); if (global.nativeFlushQueueImmediate && diff --git a/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp b/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp index 7846187260141d..011a12f563e33b 100644 --- a/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp +++ b/ReactAndroid/src/main/jni/xreact/jni/ProxyExecutor.cpp @@ -58,6 +58,9 @@ ProxyExecutor::ProxyExecutor(jni::global_ref&& executorInstance, setGlobalVariable( "__fbBatchedBridgeConfig", folly::make_unique(detail::toStdString(folly::toJson(config)))); + setGlobalVariable( + "__fbBatchedBridgeSerializeNativeParams", + folly::make_unique("1")); } ProxyExecutor::~ProxyExecutor() { diff --git a/ReactCommon/cxxreact/JSCExecutor.cpp b/ReactCommon/cxxreact/JSCExecutor.cpp index b3a8a2db66e6cd..5bd5ba63ea0ae9 100644 --- a/ReactCommon/cxxreact/JSCExecutor.cpp +++ b/ReactCommon/cxxreact/JSCExecutor.cpp @@ -119,12 +119,17 @@ JSCExecutor::JSCExecutor(std::shared_ptr delegate, } folly::dynamic config = - folly::dynamic::object("remoteModuleConfig", std::move(nativeModuleConfig)); + folly::dynamic::object + ("remoteModuleConfig", std::move(nativeModuleConfig)); + SystraceSection t("setGlobalVariable"); setGlobalVariable( "__fbBatchedBridgeConfig", folly::make_unique(detail::toStdString(folly::toJson(config)))); + setGlobalVariable( + "__fbBatchedBridgeSerializeNativeParams", + folly::make_unique("")); } JSCExecutor::JSCExecutor( diff --git a/ReactCommon/cxxreact/MethodCall.cpp b/ReactCommon/cxxreact/MethodCall.cpp index efb142c6c9c374..d9c5e7543eac23 100644 --- a/ReactCommon/cxxreact/MethodCall.cpp +++ b/ReactCommon/cxxreact/MethodCall.cpp @@ -51,16 +51,20 @@ std::vector parseMethodCalls(const std::string& json) throw(std::inv std::vector methodCalls; for (size_t i = 0; i < moduleIds.size(); i++) { - auto paramsValue = params[i]; + if (!params[i].isString()) { + throw std::invalid_argument( + folly::to("Call argument isn't a string")); + } + auto paramsValue = folly::parseJson(params[i].asString()); if (!paramsValue.isArray()) { throw std::invalid_argument( - folly::to("Call argument isn't an array")); + folly::to("Parsed params isn't an array")); } methodCalls.emplace_back( moduleIds[i].getInt(), methodIds[i].getInt(), - std::move(params[i]), + std::move(paramsValue), callId); // only incremement callid if contains valid callid as callid is optional From eadbb919c3b2517c8ece9b368be6c2adaa82aa4f Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Thu, 23 Jun 2016 15:04:39 -0700 Subject: [PATCH 507/843] RN: Scrub InitializeJavaScriptAppEngine Summary: Cleans up `InitializeJavaScriptAppEngine` in a few ways: - Fix bug where `global.navigation.geolocation` was being assigned to `global`. - Rename `polyfillGlobal` to `defineProperty`. - Rename `polyfillLazyGlobal` to `defineLazyProperty`. - Inline `polyfillIfNeeded` (only used once). - Rename `setUpMapAndSet` to `setUpCollections`. - Add `flow`. I've changed `defineProperty` and `defineLazyProperty` to always accept an `object` property since it is not only used for defining properties on `global`. Reviewed By: davidaurelio Differential Revision: D3472147 fbshipit-source-id: 492da62a303cf040211c386fa6260789e50b43c1 --- .../InitializeJavaScriptAppEngine.js | 139 +++++++++--------- 1 file changed, 67 insertions(+), 72 deletions(-) diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index c1a777406c852a..0883d949896179 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -17,6 +17,7 @@ * 2. Bridged modules. * * @providesModule InitializeJavaScriptAppEngine + * @flow */ /* eslint strict: 0 */ @@ -24,15 +25,15 @@ require('regenerator-runtime/runtime'); -if (typeof GLOBAL === 'undefined') { +if (global.GLOBAL === undefined) { global.GLOBAL = global; } -if (typeof window === 'undefined') { +if (global.window === undefined) { global.window = global; } -function setUpProcess() { +function setUpProcess(): void { global.process = global.process || {}; global.process.env = global.process.env || {}; if (!global.process.env.NODE_ENV) { @@ -40,12 +41,12 @@ function setUpProcess() { } } -function setUpProfile() { +function setUpProfile(): void { const Systrace = require('Systrace'); Systrace.setEnabled(global.__RCTProfileIsProfiling || false); } -function setUpConsole() { +function setUpConsole(): void { // ExceptionsManager transitively requires Promise so we install it after const ExceptionsManager = require('ExceptionsManager'); ExceptionsManager.installConsoleErrorReporter(); @@ -56,35 +57,29 @@ function setUpConsole() { } /** - * Assigns a new global property, replacing the existing one if there is one. + * Sets an object's property. If a property with the same name exists, this will + * replace it but maintain its descriptor configuration. * - * Existing properties are preserved as `originalPropertyName`. Both properties - * will maintain the same enumerability & configurability. + * The original property value will be preserved as `original[PropertyName]` so + * that, if necessary, it can be restored. For example, if you want to route + * network requests through DevTools (to trace them): * - * This allows you to undo the more aggressive polyfills, should you need to. - * For example, if you want to route network requests through DevTools (to trace - * them): + * global.XMLHttpRequest = global.originalXMLHttpRequest; * - * global.XMLHttpRequest = global.originalXMLHttpRequest; - * - * For more info on that particular case, see: - * https://github.com/facebook/react-native/issues/934 + * @see https://github.com/facebook/react-native/issues/934 */ -function polyfillGlobal(name, newValue, scope = global) { - const descriptor = Object.getOwnPropertyDescriptor(scope, name); +function defineProperty(object: Object, name: string, newValue: mixed): void { + const descriptor = Object.getOwnPropertyDescriptor(object, name); if (descriptor) { const backupName = `original${name[0].toUpperCase()}${name.substr(1)}`; - Object.defineProperty(scope, backupName, {...descriptor, value: scope[name]}); + Object.defineProperty(object, backupName, { + ...descriptor, + value: object[name], + }); } const {enumerable, writable} = descriptor || {}; - - // jest for some bad reasons runs the polyfill code multiple times. In jest - // environment, XmlHttpRequest doesn't exist so getOwnPropertyDescriptor - // returns undefined and defineProperty default for writable is false. - // Therefore, the second time it runs, defineProperty will fatal :( - - Object.defineProperty(scope, name, { + Object.defineProperty(object, name, { configurable: true, enumerable: enumerable !== false, writable: writable !== false, @@ -92,22 +87,26 @@ function polyfillGlobal(name, newValue, scope = global) { }); } -function polyfillLazyGlobal(name, valueFn, scope = global) { - const descriptor = getPropertyDescriptor(scope, name); +function defineLazyProperty( + object: Object, + name: string, + getValue: () => mixed +): void { + const descriptor = getPropertyDescriptor(object, name); if (descriptor) { const backupName = `original${name[0].toUpperCase()}${name.substr(1)}`; - Object.defineProperty(scope, backupName, descriptor); + Object.defineProperty(object, backupName, descriptor); } const {enumerable, writable} = descriptor || {}; - Object.defineProperty(scope, name, { + Object.defineProperty(object, name, { configurable: true, enumerable: enumerable !== false, get() { - return (global[name] = valueFn()); + return (object[name] = getValue()); }, set(value) { - Object.defineProperty(global, name, { + Object.defineProperty(object, name, { configurable: true, enumerable: enumerable !== false, writable: writable !== false, @@ -117,16 +116,7 @@ function polyfillLazyGlobal(name, valueFn, scope = global) { }); } -/** - * Polyfill a module if it is not already defined in `scope`. - */ -function polyfillIfNeeded(name, polyfill, scope = global, descriptor = {}) { - if (scope[name] === undefined) { - Object.defineProperty(scope, name, {...descriptor, value: polyfill}); - } -} - -function setUpErrorHandler() { +function setUpErrorHandler(): void { if (global.__fbDisableExceptionsManager) { return; } @@ -135,7 +125,9 @@ function setUpErrorHandler() { try { require('ExceptionsManager').handleException(e, isFatal); } catch (ee) { + /* eslint-disable no-console-disallow */ console.log('Failed to print error: ', ee.message); + /* eslint-enable no-console-disallow */ throw e; } } @@ -151,9 +143,9 @@ function setUpErrorHandler() { * implement our own custom timing bridge that should be immune to * unexplainably dropped timing signals. */ -function setUpTimers() { - const defineLazyTimer = (name) => { - polyfillLazyGlobal(name, () => require('JSTimers')[name]); +function setUpTimers(): void { + const defineLazyTimer = name => { + defineLazyProperty(global, name, () => require('JSTimers')[name]); }; defineLazyTimer('setTimeout'); defineLazyTimer('setInterval'); @@ -165,7 +157,7 @@ function setUpTimers() { defineLazyTimer('cancelAnimationFrame'); } -function setUpAlert() { +function setUpAlert(): void { if (!global.alert) { global.alert = function(text) { // Require Alert on demand. Requiring it too early can lead to issues @@ -175,45 +167,48 @@ function setUpAlert() { } } -function setUpPromise() { +function setUpPromise(): void { // The native Promise implementation throws the following error: // ERROR: Event loop not supported. - polyfillLazyGlobal('Promise', () => require('Promise')); + defineLazyProperty(global, 'Promise', () => require('Promise')); } -function setUpXHR() { +function setUpXHR(): void { // The native XMLHttpRequest in Chrome dev tools is CORS aware and won't // let you fetch anything from the internet - polyfillLazyGlobal('XMLHttpRequest', () => require('XMLHttpRequest')); - polyfillLazyGlobal('FormData', () => require('FormData')); + defineLazyProperty(global, 'XMLHttpRequest', () => require('XMLHttpRequest')); + defineLazyProperty(global, 'FormData', () => require('FormData')); - polyfillLazyGlobal('fetch', () => require('fetch').fetch); - polyfillLazyGlobal('Headers', () => require('fetch').Headers); - polyfillLazyGlobal('Request', () => require('fetch').Request); - polyfillLazyGlobal('Response', () => require('fetch').Response); + defineLazyProperty(global, 'fetch', () => require('fetch').fetch); + defineLazyProperty(global, 'Headers', () => require('fetch').Headers); + defineLazyProperty(global, 'Request', () => require('fetch').Request); + defineLazyProperty(global, 'Response', () => require('fetch').Response); - polyfillLazyGlobal('WebSocket', () => require('WebSocket')); + defineLazyProperty(global, 'WebSocket', () => require('WebSocket')); } -function setUpGeolocation() { - polyfillIfNeeded('navigator', {}, global, { - writable: true, - enumerable: true, - configurable: true, - }); - Object.defineProperty(global.navigator, 'product', {value: 'ReactNative'}); - - polyfillLazyGlobal('geolocation', () => require('Geolocation'), global.navigator); +function setUpGeolocation(): void { + if (global.navigator === undefined) { + Object.defineProperty(global, 'navigator', { + configurable: true, + enumerable: true, + writable: true, + value: {}, + }); + } + const {navigator} = global; + Object.defineProperty(navigator, 'product', {value: 'ReactNative'}); + defineLazyProperty(navigator, 'geolocation', () => require('Geolocation')); } -function setUpMapAndSet() { - // We can't make these lazy as Map checks the global.Map to see if it's - // available but in our case it'll be a lazy getter. - polyfillGlobal('Map', require('Map')); - polyfillGlobal('Set', require('Set')); +function setUpCollections(): void { + // We can't make these lazy because `Map` checks for `global.Map` (which would + // not exist if it were lazily defined). + defineProperty(global, 'Map', require('Map')); + defineProperty(global, 'Set', require('Set')); } -function setUpDevTools() { +function setUpDevTools(): void { if (__DEV__) { // not when debugging in chrome if (!window.document && require('Platform').OS === 'ios') { @@ -226,7 +221,7 @@ function setUpDevTools() { } } -function getPropertyDescriptor(object, name) { +function getPropertyDescriptor(object: Object, name: string): any { while (object) { const descriptor = Object.getOwnPropertyDescriptor(object, name); if (descriptor) { @@ -245,7 +240,7 @@ setUpPromise(); setUpErrorHandler(); setUpXHR(); setUpGeolocation(); -setUpMapAndSet(); +setUpCollections(); setUpDevTools(); // Just to make sure the JS gets packaged up. Wait until the JS environment has From 34adde9e96d8949fe3a20144616aaa5e073d5e94 Mon Sep 17 00:00:00 2001 From: Joel Marcey Date: Thu, 23 Jun 2016 15:51:45 -0700 Subject: [PATCH 508/843] Add `extends Component` to Dimensions and Layout Basics Examples Summary: It works without out the `extends`, but I do not really understand why, unless there is some magic implicit `extends` if you don't put it and you call `registerComponent`. But, I figure we should be explicit unless there is a good reason not to be. Closes https://github.com/facebook/react-native/pull/8377 Differential Revision: D3478950 Pulled By: JoelMarcey fbshipit-source-id: 05ea4367c3c8c34aea6c092639ee51d8761bca3f --- docs/Basics-Dimensions.md | 12 ++++++------ docs/Basics-Layout.md | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/Basics-Dimensions.md b/docs/Basics-Dimensions.md index 3003c02faaf973..3497db13e97698 100644 --- a/docs/Basics-Dimensions.md +++ b/docs/Basics-Dimensions.md @@ -14,10 +14,10 @@ A component's dimensions determine its size on the screen. The simplest way to set the dimensions of a component is by adding a fixed `width` and `height` to style. All dimensions in React Native are unitless, and represent density-independent pixels. ```ReactNativeWebPlayer -import React from 'react'; +import React, { Component } from 'react'; import { AppRegistry, View } from 'react-native'; -class AwesomeProject { +class FixedDimensionsBasics extends Component { render() { return ( @@ -29,7 +29,7 @@ class AwesomeProject { } }; -AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject); +AppRegistry.registerComponent('AwesomeProject', () => FixedDimensionsBasics); ``` Setting dimensions this way is common for components that should always render at exactly the same size, regardless of screen dimensions. @@ -41,10 +41,10 @@ Use `flex` in a component's style to have the component expand and shrink dynami > A component can only expand to fill available space if its parent has dimensions greater than 0. If a parent does not have either a fixed `width` and `height` or `flex`, the parent will have dimensions of 0 and the `flex` children will not be visible. ```ReactNativeWebPlayer -import React from 'react'; +import React, { Component } from 'react'; import { AppRegistry, View } from 'react-native'; -class AwesomeProject { +class FlexDimensionsBasics extends Component { render() { return ( // Try removing the `flex: 1` on the parent View. @@ -59,5 +59,5 @@ class AwesomeProject { } }; -AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject); +AppRegistry.registerComponent('AwesomeProject', () => FlexDimensionsBasics); ``` diff --git a/docs/Basics-Layout.md b/docs/Basics-Layout.md index 275d210d56dd22..8d2972f4d1da86 100644 --- a/docs/Basics-Layout.md +++ b/docs/Basics-Layout.md @@ -18,10 +18,10 @@ You will normally use a combination of `flexDirection`, `alignItems`, and `justi Adding `flexDirection` to a component's `style` determines the **primary axis** of its layout. Should the children be organized horizontally (`row`) or vertically (`column`)? The default is `column`. ```ReactNativeWebPlayer -import React from 'react'; +import React, { Component } from 'react'; import { AppRegistry, View } from 'react-native'; -class AwesomeProject { +class FlexDirectionBasics extends Component { render() { return ( // Try setting `flexDirection` to `column`. @@ -34,7 +34,7 @@ class AwesomeProject { } }; -AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject); +AppRegistry.registerComponent('AwesomeProject', () => FlexDirectionBasics); ``` #### Justify Content @@ -42,10 +42,10 @@ AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject); Adding `justifyContent` to a component's style determines the **distribution** of children along the **primary axis**. Should children be distributed at the start, the center, the end, or spaced evenly? Available options are `flex-start`, `center`, `flex-end`, `space-around`, and `space-between`. ```ReactNativeWebPlayer -import React from 'react'; +import React, { Component } from 'react'; import { AppRegistry, View } from 'react-native'; -class AwesomeProject { +class JustifyContentBasics extends Component { render() { return ( // Try setting `justifyContent` to `center`. @@ -63,7 +63,7 @@ class AwesomeProject { } }; -AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject); +AppRegistry.registerComponent('AwesomeProject', () => JustifyContentBasics); ``` #### Align Items @@ -73,10 +73,10 @@ Adding `alignItems` to a component's style determines the **alignment** of child > For `stretch` to have an effect, children must not have a fixed dimension along the secondary axis. In the following example, setting `alignItems: stretch` does nothing until the `width: 50` is removed from the children. ```ReactNativeWebPlayer -import React from 'react'; +import React, { Component } from 'react'; import { AppRegistry, View } from 'react-native'; -class AwesomeProject { +class AlignItemsBasics { render() { return ( // Try setting `alignItems` to 'flex-start' @@ -96,7 +96,7 @@ class AwesomeProject { } }; -AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject); +AppRegistry.registerComponent('AwesomeProject', () => AlignItemsBasics); ``` #### API Reference From f0c79bac31a09c14894efb49bb942c18e9978f90 Mon Sep 17 00:00:00 2001 From: Christine Abernathy Date: Thu, 23 Jun 2016 16:18:12 -0700 Subject: [PATCH 509/843] Bring out prop descriptions, for Flexbox Summary: For Flexbox API docs would like to tease out the prop descriptions. This PR makes that feasible by exposing the description for style. **Test plan (required)** 1. Temporarily modified the flexbox source doc: Libraries/StyleSheet/LayoutPropTypes.js to add a description. 2. Checked it out on local webpage: http://localhost:8079/react-native/docs/flexbox.html ![style_prop_descriptions](https://cloud.githubusercontent.com/assets/691109/16321579/866b186e-3952-11e6-823a-2d38132bd553.png) Closes https://github.com/facebook/react-native/pull/8382 Differential Revision: D3478796 Pulled By: lacker fbshipit-source-id: 49f3b7876ff1ccec9ee837921a78ee0dfb915453 --- website/server/extractDocs.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index b199ef4cadc7d0..6407a539532568 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -437,7 +437,10 @@ function renderStyle(filepath) { const json = docgen.parse( fs.readFileSync(filepath), docgenHelpers.findExportedObject, - [docgen.handlers.propTypeHandler] + [ + docgen.handlers.propTypeHandler, + docgen.handlers.propDocBlockHandler, + ] ); // Remove deprecated transform props from docs From 1ae9ed358f2f6215590ab55f833d68dd3b4a3fb0 Mon Sep 17 00:00:00 2001 From: Devin Abbott Date: Thu, 23 Jun 2016 17:39:48 -0700 Subject: [PATCH 510/843] Update web player in docs for custom registerComponent names Summary: In the web player in the docs, allows `AppRegistry.registerComponent('name', App)` to use *anything* for `'name'`. It is ignored by the web player - last registration wins. Closes https://github.com/facebook/react-native/pull/8383 Differential Revision: D3478922 Pulled By: JoelMarcey fbshipit-source-id: 3d1d96e0ad41216d29134ba384896e86d0cd2b32 --- website/core/WebPlayer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/core/WebPlayer.js b/website/core/WebPlayer.js index 2e985388b6b8fa..a79f27baceedc9 100644 --- a/website/core/WebPlayer.js +++ b/website/core/WebPlayer.js @@ -44,7 +44,7 @@ var WebPlayer = React.createClass({ }, render: function() { - var hash = `#code=${encodeURIComponent(this.props.children)}&runApp=AwesomeProject`; + var hash = `#code=${encodeURIComponent(this.props.children)}`; if (this.props.params) { hash += `&${this.props.params}`; @@ -57,7 +57,7 @@ var WebPlayer = React.createClass({ style={{marginTop: 4}} width='880' height={this.parseParams(this.props.params).platform === 'android' ? '425' : '420'} - data-src={`//cdn.rawgit.com/dabbott/react-native-web-player/v0.1.2/index.html${hash}`} + data-src={`//cdn.rawgit.com/dabbott/react-native-web-player/v0.1.3/index.html${hash}`} frameBorder='0' /> From 7ed9f0dfb37e2bc3f6822f48e85fb90606adf6e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Thu, 23 Jun 2016 18:13:03 -0700 Subject: [PATCH 511/843] Networking Guide Summary: Simplified Networking Guide, based on the old Network polyfill doc. This guide strongly recommends using fetch, while still informing the user about React Native's support for other libraries. In order to provide an actual working networking example, a `movies.json` file is added at the root of the site, allowing the user to fetch a small blob of JSON: ``` fetch('http://facebook.github.io/react-native/movies.json') ``` ![networking](https://cloud.githubusercontent.com/assets/165856/16321804/d2bd7c6a-3953-11e6-9fc5-30baaa38d7a4.png) Closes https://github.com/facebook/react-native/pull/8381 Differential Revision: D3479018 Pulled By: lacker fbshipit-source-id: 1f2078bf2414a13f7f77d5af55b08948909093a3 --- docs/Basics-Network.md | 140 +++++++++++---------------- website/src/react-native/movies.json | 11 +++ 2 files changed, 67 insertions(+), 84 deletions(-) create mode 100644 website/src/react-native/movies.json diff --git a/docs/Basics-Network.md b/docs/Basics-Network.md index 151cc09fea0910..e5487766bb96ea 100644 --- a/docs/Basics-Network.md +++ b/docs/Basics-Network.md @@ -1,25 +1,27 @@ --- id: basics-network -title: Network +title: Networking layout: docs category: The Basics permalink: docs/network.html next: more-resources --- -One of React Native's goals is to be a playground where we can experiment with different architectures and crazy ideas. Since browsers are not flexible enough, we had no choice but to reimplement the entire stack. In the places that we did not intend to change anything, we tried to be as faithful as possible to the browser APIs. The networking stack is a great example. +Many mobile apps need to load resources from a remote URL. You may want to make a POST request to a REST API, or you may simply need to fetch a chunk of static content from another server. -## Fetch +## Using Fetch -[fetch](https://fetch.spec.whatwg.org/) is a better networking API being worked on by the standards committee and is already available in Chrome. It is available in React Native by default. +React Native provides the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) for your networking needs. Fetch will seem familiar if you have used `XMLHttpRequest` or other networking APIs before. You may refer to MDN's guide on [Using Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) for additional information. -#### Usage +#### Making requests + +In order to fetch content from an arbitrary URL, just pass the URL to fetch: ```js -fetch('https://mywebsite.com/endpoint/') +fetch('https://mywebsite.com/mydata.json') ``` -Include a request object as the optional second argument to customize the HTTP request: +Fetch also takes an optional second argument that allows you to customize the HTTP request. You may want to specify additional headers, or make a POST request: ```js fetch('https://mywebsite.com/endpoint/', { @@ -35,74 +37,46 @@ fetch('https://mywebsite.com/endpoint/', { }) ``` -#### Async +Take a look at the [Fetch Request docs](https://developer.mozilla.org/en-US/docs/Web/API/Request) for a full list of properties. + +#### Handling the response -`fetch` returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that can be processed in two ways: +The above examples show how you can make a request. In many cases, you will want to do something with the response. -1. Using `then` and `catch` in synchronous code: +Networking is an inherently asynchronous operation. Fetch methods will return a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that make it straightforward to write code that works in an asynchronous manner: ```js - fetch('https://mywebsite.com/endpoint.php') - .then((response) => response.text()) - .then((responseText) => { - console.log(responseText); - }) - .catch((error) => { - console.warn(error); - }); + getMoviesFromApiAsync() { + return fetch('http://facebook.github.io/react-native/movies.json') + .then((response) => response.json()) + .then((responseJson) => { + return responseJson.movies; + }) + .catch((error) => { + console.error(error); + }); + } ``` -2. Called within an asynchronous function using ES7 `async`/`await` syntax: + +You can also use ES7's `async`/`await` syntax in React Native app: ```js - class MyComponent extends React.Component { - ... - async getUsersFromApi() { - try { - let response = await fetch('https://mywebsite.com/endpoint/'); - let responseJson = await response.json(); - return responseJson.users; - } catch(error) { - // Handle error - console.error(error); - } + async getMoviesFromApi() { + try { + let response = await fetch('http://facebook.github.io/react-native/movies.json'); + let responseJson = await response.json(); + return responseJson.movies; + } catch(error) { + console.error(error); } - ... } ``` -- Note: Errors thrown by rejected Promises need to be caught, or they will be swallowed silently +Don't forget to catch any errors that may be thrown by `fetch`, otherwise they will be dropped silently. -## WebSocket +### Using Other Networking Libraries -WebSocket is a protocol providing full-duplex communication channels over a single TCP connection. - -```js -var ws = new WebSocket('ws://host.com/path'); - -ws.onopen = () => { - // connection opened - ws.send('something'); -}; - -ws.onmessage = (e) => { - // a message was received - console.log(e.data); -}; - -ws.onerror = (e) => { - // an error occurred - console.log(e.message); -}; - -ws.onclose = (e) => { - // connection closed - console.log(e.code, e.reason); -}; -``` - -## XMLHttpRequest - -XMLHttpRequest API is implemented on-top of [iOS networking apis](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html) and [OkHttp](http://square.github.io/okhttp/). The notable difference from web is the security model: you can read from arbitrary websites on the internet since there is no concept of [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing). +The [XMLHttpRequest API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) is built in to React Native. This means that you can use third party libraries such as [frisbee](https://github.com/niftylettuce/frisbee) or [axios](https://github.com/mzabriskie/axios) that depend on it, or you can use the XMLHttpRequest API directly if you prefer. ```js var request = new XMLHttpRequest(); @@ -118,38 +92,36 @@ request.onreadystatechange = (e) => { } }; -request.open('GET', 'https://mywebsite.com/endpoint.php'); +request.open('GET', 'https://mywebsite.com/endpoint/'); request.send(); ``` -You can also use - +> The security model for XMLHttpRequest is different than on web as there is no concept of [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) in native apps. + +## WebSocket Support + +React Native supports [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket), a protocol which provides full-duplex communication channels over a single TCP connection. ```js -var request = new XMLHttpRequest(); +var ws = new WebSocket('ws://host.com/path'); -function onLoad() { - console.log(request.status); - console.log(request.responseText); +ws.onopen = () => { + // connection opened + ws.send('something'); }; -function onTimeout() { - console.log('Timeout'); - console.log(request.responseText); +ws.onmessage = (e) => { + // a message was received + console.log(e.data); }; -function onError() { - console.log('General network error'); - console.log(request.responseText); +ws.onerror = (e) => { + // an error occurred + console.log(e.message); }; -request.onload = onLoad; -request.ontimeout = onTimeout; -request.onerror = onError; -request.open('GET', 'https://mywebsite.com/endpoint.php'); -request.send(); +ws.onclose = (e) => { + // connection closed + console.log(e.code, e.reason); +}; ``` - - -Please follow the [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) for a complete description of the API. - -As a developer, you're probably not going to use XMLHttpRequest directly as its API is very tedious to work with. But the fact that it is implemented and compatible with the browser API gives you the ability to use third-party libraries such as [frisbee](https://github.com/niftylettuce/frisbee) or [axios](https://github.com/mzabriskie/axios) directly from npm. diff --git a/website/src/react-native/movies.json b/website/src/react-native/movies.json new file mode 100644 index 00000000000000..fd48f44a9f5d8a --- /dev/null +++ b/website/src/react-native/movies.json @@ -0,0 +1,11 @@ +{ + "title": "The Basics - Networking", + "description": "Your app fetched this from a remote endpoint!", + "movies": [ + { "title": "Star Wars", "releaseYear": "1977"}, + { "title": "Back to the Future", "releaseYear": "1985"}, + { "title": "The Matrix", "releaseYear": "1999"}, + { "title": "Inception", "releaseYear": "2010"}, + { "title": "Interstellar", "releaseYear": "2014"} + ] +} From c6b1ed649fdc34533b485cef9a3fc527dc326551 Mon Sep 17 00:00:00 2001 From: Joel Marcey Date: Thu, 23 Jun 2016 18:13:41 -0700 Subject: [PATCH 512/843] Move Component Embedded Simulator next to its example Summary: Right now the embedded simulator is always at the top right corner. This can be confusing as to what code is associated with the simulation. So, move the simulator next to its actual code. This has the added benefit of allowing us to use the React Native Web Player for the simpler examples in the components. Closes https://github.com/facebook/react-native/pull/8384 Differential Revision: D3479056 Pulled By: bestander fbshipit-source-id: f400d8387ec771b94d5e798c1e955b25f9a0f1bf --- website/layout/AutodocsLayout.js | 14 +++---- website/src/react-native/css/react-native.css | 42 ++++++++++++------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/website/layout/AutodocsLayout.js b/website/layout/AutodocsLayout.js index 6353bbfad897cd..1f514a6cfe13f7 100644 --- a/website/layout/AutodocsLayout.js +++ b/website/layout/AutodocsLayout.js @@ -775,7 +775,7 @@ var EmbeddedSimulator = React.createClass({ : Run example in simulator; return ( -
    +

    Run this example

    {imagePreview} @@ -855,9 +855,12 @@ var Autodocs = React.createClass({ path={example.path} metadata={metadata} /> - - {example.content.replace(/^[\s\S]*?\*\//, '').trim()} - +
    + + {example.content.replace(/^[\s\S]*?\*\//, '').trim()} + + +
    ); }, @@ -901,9 +904,6 @@ var Autodocs = React.createClass({ {metadata.next && Next →}
    - - - ); diff --git a/website/src/react-native/css/react-native.css b/website/src/react-native/css/react-native.css index 2436c14019ff73..e812927d97da16 100644 --- a/website/src/react-native/css/react-native.css +++ b/website/src/react-native/css/react-native.css @@ -125,6 +125,33 @@ html * { rendering-intent: auto; } +.example-container { + position: relative; +} + +.embedded-simulator, .embedded-simulator * { + box-sizing: border-box; +} + +.embedded-simulator p { + text-align: center; + color: #999; +} + +.embedded-simulator { + width: 210px; + position: absolute; + right: -200px; + top: 0; +} + +@media screen and (max-width: 680px) { + .embedded-simulator { + position: relative; + right: 0; + } +} + .prism { white-space: pre-wrap; font-family: 'source-code-pro', Menlo, 'Courier New', Consolas, monospace; @@ -1027,21 +1054,6 @@ small code, li code, p code { text-decoration: none !important; } -.column-left, .column-left * { - box-sizing: border-box; -} - -.column-left p { - text-align: center; - color: #999; -} - -.column-left { - float: left; - padding: 20px; - width: 210px; -} - /* Modal */ .modal-backdrop { background: rgba(0,0,0,.4); From 1ffecb4b5f0caf7f5655b8707a1b3396cf7d95a5 Mon Sep 17 00:00:00 2001 From: Kevin Lacker Date: Thu, 23 Jun 2016 18:19:27 -0700 Subject: [PATCH 513/843] fix bugs on landing page code, make the url an easter egg Summary: This is just improving a bit of lameness on the homepage - Devin pointed out the <>'s don't work within a Text tag, so I removed them, and someone else pointed out that nonexistent fake urls are suboptimal, so I improved that too. Closes https://github.com/facebook/react-native/pull/8387 Differential Revision: D3479087 Pulled By: JoelMarcey fbshipit-source-id: 45a2d21a9073b58b869e8b344550c28f849e0185 --- website/src/react-native/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/src/react-native/index.js b/website/src/react-native/index.js index 49a957f0cd9697..aeee07b85612dd 100644 --- a/website/src/react-native/index.js +++ b/website/src/react-native/index.js @@ -50,8 +50,8 @@ class WhyReactNativeIsSoGreat extends Component { If you like React on the web, you'll like React Native.
    - You just use native components like '' and '', - instead of web components like '
    ' and ''. + You just use native components like 'View' and 'Text', + instead of web components like 'div' and 'span'. ); @@ -72,12 +72,12 @@ class AwkwardScrollingImageWithText extends Component { render() { return ( - + - On iOS, a React Native '' uses a native UIScrollView. + On iOS, a React Native ScrollView uses a native UIScrollView. On Android, it uses a native ScrollView. - On iOS, a React Native '' uses a native UIImageView. + On iOS, a React Native Image uses a native UIImageView. On Android, it uses a native ImageView. React Native wraps the fundamental native components, giving you From 167654248bdc19cb49b0588a1d7880f706221aa7 Mon Sep 17 00:00:00 2001 From: Eric Nakagawa Date: Thu, 23 Jun 2016 18:32:21 -0700 Subject: [PATCH 514/843] Api documentation update for modal.js Summary: Related to #8203 to update the Modal API reference doc. **Test plan (required)** Started up the website and checked: http://localhost:8079/react-native/docs/modal.html ![modal update](https://cloud.githubusercontent.com/assets/23874/16316792/ecde19cc-393c-11e6-8136-16243a199d9b.png) **Note, copied from a previous PR** The code is not Flow-ified so depended on jsdoc formatting to get the method parameter types. There's a current issue with handling optional types via react-docgen which parses components. There's an open PR to look into this: https://github.com/reactjs/react-docgen/pull/89. When that's resolved the `replaceAtIndex` method parameter type that's documented for `cb` needs to be updated to make it optional. Closes https://github.com/facebook/react-native/pull/8375 Differential Revision: D3479536 Pulled By: caabernathy fbshipit-source-id: de2db3aa221e4adce0c0c5f3d94a1fad528a60da --- Libraries/Modal/Modal.js | 90 ++++++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 13 deletions(-) diff --git a/Libraries/Modal/Modal.js b/Libraries/Modal/Modal.js index 30b5954d0881f5..208ffdee71ca30 100644 --- a/Libraries/Modal/Modal.js +++ b/Libraries/Modal/Modal.js @@ -23,29 +23,93 @@ const requireNativeComponent = require('requireNativeComponent'); const RCTModalHostView = requireNativeComponent('RCTModalHostView', null); /** - * A Modal component covers the native view (e.g. UIViewController, Activity) - * that contains the React Native root. + * The Modal component is a simple way to present content above an enclosing view. * - * Use Modal in hybrid apps that embed React Native; Modal allows the portion of - * your app written in React Native to present content above the enclosing - * native view hierarchy. + * _Note: If you need more control over how to present modals over the rest of your app, + * then consider using a top-level Navigator. Go [here](/react-native/docs/navigator-comparison.html) to compare navigation options._ * - * In apps written with React Native from the root view down, you should use - * Navigator instead of Modal. With a top-level Navigator, you have more control - * over how to present the modal scene over the rest of your app by using the - * configureScene property. + * ```javascript + * import React, { Component } from 'react'; + * import { Modal, Text, TouchableHighlight, View } from 'react-native'; + * + * class ModalExample extends Component { + * + * constructor(props) { + * super(props); + * this.state = {modalVisible: false}; + * } + * + * setModalVisible(visible) { + * this.setState({modalVisible: visible}); + * } + * + * render() { + * return ( + * + * {alert("Modal has been closed.")}} + * > + * + * + * Hello World! + * + * { + * this.setModalVisible(!this.state.modalVisible) + * }}> + * Hide Modal + * + * + * + * + * + * + * { + * this.setModalVisible(true) + * }}> + * Show Modal + * + * + * + * ); + * } + * } + * ``` */ class Modal extends React.Component { static propTypes = { - animated: deprecatedPropType( - PropTypes.bool, - 'Use the `animationType` prop instead.' - ), + /** + * The `animationType` prop controls how the modal animates. + * + * - `slide` slides in from the bottom + * - `fade` fades into view + * - `none` appears without an animation + */ animationType: PropTypes.oneOf(['none', 'slide', 'fade']), + /** + * The `transparent` prop determines whether your modal will fill the entire view. Setting this to `true` will render the modal over a transparent background. + */ transparent: PropTypes.bool, + /** + * The `visible` prop determines whether your modal is visible. + */ visible: PropTypes.bool, + /** + * The `onRequestClose` prop allows passing a function that will be called once the modal has been dismissed. + * + * _On the Android platform, this is a required function._ + */ onRequestClose: Platform.OS === 'android' ? PropTypes.func.isRequired : PropTypes.func, + /** + * The `onShow` prop allows passing a function that will be called once the modal has been shown. + */ onShow: PropTypes.func, + animated: deprecatedPropType( + PropTypes.bool, + 'Use the `animationType` prop instead.' + ), }; static defaultProps = { From fa6022dc1a9551a8cb1d9715fcaf057cdea80389 Mon Sep 17 00:00:00 2001 From: Mengjue Wang Date: Thu, 23 Jun 2016 18:54:01 -0700 Subject: [PATCH 515/843] Connect the OS language with isRTL Summary: Create isDeviceLanguageRTL function to connect the OS language with isRTL. Now even if a new app could support RTL language, without setting forceRTL at JS side it won't directly change into RTL layout. Reviewed By: fkgozali Differential Revision: D3473109 fbshipit-source-id: ac1410c910e980d44750bb88cad7615febb9e076 --- React/Modules/RCTI18nUtil.m | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/React/Modules/RCTI18nUtil.m b/React/Modules/RCTI18nUtil.m index ef1bbc1dfa4235..c66924243db42f 100644 --- a/React/Modules/RCTI18nUtil.m +++ b/React/Modules/RCTI18nUtil.m @@ -24,7 +24,9 @@ + (id)sharedInstance { - (BOOL)isRTL { - if ([self forceRTL]) return YES; + if ([self forceRTL] && [self isDevicePreferredLanguageRTL]) { + return YES; + } return NO; } @@ -41,4 +43,10 @@ - (void)setForceRTL:(BOOL)rtlStatus [[NSUserDefaults standardUserDefaults] synchronize]; } +- (BOOL)isDevicePreferredLanguageRTL +{ + NSLocaleLanguageDirection direction = [NSLocale characterDirectionForLanguage:[[NSLocale preferredLanguages] objectAtIndex:0]]; + return direction == NSLocaleLanguageDirectionRightToLeft; +} + @end From 9cb28b9a7eb9ae06a595dbe6819c0f768c28e543 Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Thu, 23 Jun 2016 19:09:00 -0700 Subject: [PATCH 516/843] RN: Avoid Infinite Loop w/ Polyfills Reviewed By: voideanvalue Differential Revision: D3472319 fbshipit-source-id: 87c8bd6719eb1771ec16c7e363cec9ee247d87fe --- .../InitializeJavaScriptAppEngine.js | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index 0883d949896179..ba5fa68fc9bdf4 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -77,7 +77,6 @@ function defineProperty(object: Object, name: string, newValue: mixed): void { value: object[name], }); } - const {enumerable, writable} = descriptor || {}; Object.defineProperty(object, name, { configurable: true, @@ -87,32 +86,42 @@ function defineProperty(object: Object, name: string, newValue: mixed): void { }); } -function defineLazyProperty( +function defineLazyProperty( object: Object, name: string, - getValue: () => mixed + getNewValue: () => T ): void { const descriptor = getPropertyDescriptor(object, name); if (descriptor) { const backupName = `original${name[0].toUpperCase()}${name.substr(1)}`; Object.defineProperty(object, backupName, descriptor); } - - const {enumerable, writable} = descriptor || {}; - Object.defineProperty(object, name, { + const config = { configurable: true, - enumerable: enumerable !== false, - get() { - return (object[name] = getValue()); - }, - set(value) { - Object.defineProperty(object, name, { - configurable: true, - enumerable: enumerable !== false, - writable: writable !== false, - value, - }); + enumerable: descriptor ? descriptor.enumerable !== false : true, + writable: descriptor ? descriptor.writable !== false : true, + }; + let value; + let valueSet = false; + function getValue(): T { + // WORKAROUND: A weird infinite loop occurs where calling `getValue` calls + // `setValue` which calls `Object.defineProperty` which somehow triggers + // `getValue` again. Adding `valueSet` breaks this loop. + if (!valueSet) { + setValue(getNewValue()); } + return value; + } + function setValue(newValue: T): void { + value = newValue; + valueSet = true; + Object.defineProperty(object, name, {...config, value: newValue}); + } + Object.defineProperty(object, name, { + configurable: config.configurable, + enumerable: config.enumerable, + get: getValue, + set: setValue, }); } From a87c9d5c2c4700ac71d646c8d8352fb9a31f1208 Mon Sep 17 00:00:00 2001 From: "wenzhao.yin" Date: Fri, 24 Jun 2016 03:18:23 -0700 Subject: [PATCH 517/843] Fix GIF Disappear when return from background Summary: GIF Image will disappear after press the home button and return back. Set `removedOnCompletion` to be`false` will fix all `CAAnimation` disappear like stopping after going into the background. Closes https://github.com/facebook/react-native/pull/7612 Differential Revision: D3481403 fbshipit-source-id: 101bded300f5e34bb53ec6c54a40eb5aece22fba --- Libraries/Image/RCTGIFImageDecoder.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Libraries/Image/RCTGIFImageDecoder.m b/Libraries/Image/RCTGIFImageDecoder.m index e578421a7e2f74..f32f8f876f0fa4 100644 --- a/Libraries/Image/RCTGIFImageDecoder.m +++ b/Libraries/Image/RCTGIFImageDecoder.m @@ -91,6 +91,7 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData animation.keyTimes = keyTimes; animation.values = images; animation.duration = duration; + animation.removedOnCompletion = NO; image.reactKeyframeAnimation = animation; } else { From d29e8ae0cafdcdfbc07fa647ec6a0344a1db6467 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Fri, 24 Jun 2016 06:28:38 -0700 Subject: [PATCH 518/843] Implement onTextInput events for RCTTextView Reviewed By: blairvanderhoof Differential Revision: D3475581 fbshipit-source-id: df2fb8e1e898dfe6af455db0f96ecb23b4aa0721 --- Libraries/Components/TextInput/TextInput.js | 61 +++----- Libraries/Text/RCTTextView.h | 1 + Libraries/Text/RCTTextView.m | 158 +++++++++++++++----- Libraries/Text/RCTTextViewManager.m | 1 + 4 files changed, 144 insertions(+), 77 deletions(-) diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 2c7c80e99375ce..2df8d278104d51 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -11,37 +11,32 @@ */ 'use strict'; -var ColorPropType = require('ColorPropType'); -var DocumentSelectionState = require('DocumentSelectionState'); -var EventEmitter = require('EventEmitter'); -var NativeMethodsMixin = require('NativeMethodsMixin'); -var Platform = require('Platform'); -var PropTypes = require('ReactPropTypes'); -var React = require('React'); -var ReactNative = require('ReactNative'); -var ReactChildren = require('ReactChildren'); -var StyleSheet = require('StyleSheet'); -var Text = require('Text'); -var TextInputState = require('TextInputState'); -var TimerMixin = require('react-timer-mixin'); -var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); -var UIManager = require('UIManager'); -var View = require('View'); - -var createReactNativeComponentClass = require('createReactNativeComponentClass'); -var emptyFunction = require('fbjs/lib/emptyFunction'); -var invariant = require('fbjs/lib/invariant'); -var requireNativeComponent = require('requireNativeComponent'); - -var onlyMultiline = { - onTextInput: true, // not supported in Open Source yet +const ColorPropType = require('ColorPropType'); +const DocumentSelectionState = require('DocumentSelectionState'); +const EventEmitter = require('EventEmitter'); +const NativeMethodsMixin = require('NativeMethodsMixin'); +const Platform = require('Platform'); +const PropTypes = require('ReactPropTypes'); +const React = require('React'); +const ReactNative = require('ReactNative'); +const ReactChildren = require('ReactChildren'); +const StyleSheet = require('StyleSheet'); +const Text = require('Text'); +const TextInputState = require('TextInputState'); +const TimerMixin = require('react-timer-mixin'); +const TouchableWithoutFeedback = require('TouchableWithoutFeedback'); +const UIManager = require('UIManager'); +const View = require('View'); + +const emptyFunction = require('fbjs/lib/emptyFunction'); +const invariant = require('fbjs/lib/invariant'); +const requireNativeComponent = require('requireNativeComponent'); + +const onlyMultiline = { + onTextInput: true, children: true, }; -var notMultiline = { - // nothing yet -}; - if (Platform.OS === 'android') { var AndroidTextInput = requireNativeComponent('AndroidTextInput', null); } else if (Platform.OS === 'ios') { @@ -90,7 +85,7 @@ type Event = Object; * `underlineColorAndroid` to transparent. * */ -var TextInput = React.createClass({ +const TextInput = React.createClass({ statics: { /* TODO(brentvatne) docs are needed for this */ State: TextInputState, @@ -472,14 +467,6 @@ var TextInput = React.createClass({ text={this._getText()} />; } else { - for (var propKey in notMultiline) { - if (props[propKey]) { - throw new Error( - 'TextInput prop `' + propKey + '` cannot be used with multiline.' - ); - } - } - var children = props.children; var childCount = 0; ReactChildren.forEach(children, () => ++childCount); diff --git a/Libraries/Text/RCTTextView.h b/Libraries/Text/RCTTextView.h index 93a93d409972aa..8268e7a53ddea9 100644 --- a/Libraries/Text/RCTTextView.h +++ b/Libraries/Text/RCTTextView.h @@ -30,6 +30,7 @@ @property (nonatomic, copy) RCTDirectEventBlock onChange; @property (nonatomic, copy) RCTDirectEventBlock onSelectionChange; +@property (nonatomic, copy) RCTDirectEventBlock onTextInput; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index b8a396aa8965ff..94e99af4cc6001 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -61,17 +61,22 @@ - (void)didMoveToWindow @implementation RCTTextView { RCTEventDispatcher *_eventDispatcher; + NSString *_placeholder; UITextView *_placeholderView; UITextView *_textView; - NSInteger _nativeEventCount; RCTText *_richTextView; NSAttributedString *_pendingAttributedText; - BOOL _blockTextShouldChange; + UIScrollView *_scrollView; + UITextRange *_previousSelectionRange; NSUInteger _previousTextLength; CGFloat _previousContentHeight; - UIScrollView *_scrollView; + NSString *_predictedText; + + BOOL _blockTextShouldChange; + BOOL _nativeUpdatesInFlight; + NSInteger _nativeEventCount; } - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher @@ -179,7 +184,7 @@ - (void)performTextUpdate - (void)performPendingTextUpdate { - if (!_pendingAttributedText || _mostRecentEventCount < _nativeEventCount) { + if (!_pendingAttributedText || _mostRecentEventCount < _nativeEventCount || _nativeUpdatesInFlight) { return; } @@ -205,6 +210,7 @@ - (void)performPendingTextUpdate NSInteger oldTextLength = _textView.attributedText.length; _textView.attributedText = _pendingAttributedText; + _predictedText = _pendingAttributedText.string; _pendingAttributedText = nil; if (selection.empty) { @@ -218,7 +224,7 @@ - (void)performPendingTextUpdate [_textView layoutIfNeeded]; - [self _setPlaceholderVisibility]; + [self updatePlaceholderVisibility]; _blockTextShouldChange = NO; } @@ -267,7 +273,7 @@ - (void)updatePlaceholder _placeholderView.editable = NO; _placeholderView.userInteractionEnabled = NO; _placeholderView.backgroundColor = [UIColor clearColor]; - _placeholderView.scrollEnabled = false; + _placeholderView.scrollEnabled = NO; _placeholderView.scrollsToTop = NO; _placeholderView.attributedText = [[NSAttributedString alloc] initWithString:_placeholder attributes:@{ @@ -277,7 +283,7 @@ - (void)updatePlaceholder _placeholderView.textAlignment = _textView.textAlignment; [self insertSubview:_placeholderView belowSubview:_textView]; - [self _setPlaceholderVisibility]; + [self updatePlaceholderVisibility]; } } @@ -314,21 +320,11 @@ - (void)setContentInset:(UIEdgeInsets)contentInset [self updateFrames]; } -- (NSString *)text -{ - return _textView.text; -} - - (BOOL)textView:(RCTUITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { - if (_blockTextShouldChange) { - return NO; - } - if (textView.textWasPasted) { textView.textWasPasted = NO; } else { - [_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress reactTag:self.reactTag text:nil @@ -336,7 +332,6 @@ - (BOOL)textView:(RCTUITextView *)textView shouldChangeTextInRange:(NSRange)rang eventCount:_nativeEventCount]; if (_blurOnSubmit && [text isEqualToString:@"\n"]) { - // TODO: the purpose of blurOnSubmit on RCTextField is to decide if the // field should lose focus when return is pressed or not. We're cheating a // bit here by using it on RCTextView to decide if return character should @@ -348,7 +343,6 @@ - (BOOL)textView:(RCTUITextView *)textView shouldChangeTextInRange:(NSRange)rang // where _blurOnSubmit = YES, this is still the correct and expected // behavior though, so we'll leave the don't-blur-or-add-newline problem // to be solved another day. - [_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit reactTag:self.reactTag text:self.text @@ -359,27 +353,60 @@ - (BOOL)textView:(RCTUITextView *)textView shouldChangeTextInRange:(NSRange)rang } } - if (_maxLength == nil) { - return YES; + // So we need to track that there is a native update in flight just in case JS manages to come back around and update + // things /before/ UITextView can update itself asynchronously. If there is a native update in flight, we defer the + // JS update when it comes in and apply the deferred update once textViewDidChange fires with the native update applied. + if (_blockTextShouldChange) { + return NO; } - NSUInteger allowedLength = _maxLength.integerValue - textView.text.length + range.length; - if (text.length > allowedLength) { - if (text.length > 1) { - // Truncate the input string so the result is exactly maxLength - NSString *limitedString = [text substringToIndex:allowedLength]; - NSMutableString *newString = textView.text.mutableCopy; - [newString replaceCharactersInRange:range withString:limitedString]; - textView.text = newString; - // Collapse selection at end of insert to match normal paste behavior - UITextPosition *insertEnd = [textView positionFromPosition:textView.beginningOfDocument - offset:(range.location + allowedLength)]; - textView.selectedTextRange = [textView textRangeFromPosition:insertEnd toPosition:insertEnd]; - [self textViewDidChange:textView]; + + if (_maxLength) { + NSUInteger allowedLength = _maxLength.integerValue - textView.text.length + range.length; + if (text.length > allowedLength) { + if (text.length > 1) { + // Truncate the input string so the result is exactly maxLength + NSString *limitedString = [text substringToIndex:allowedLength]; + NSMutableString *newString = textView.text.mutableCopy; + [newString replaceCharactersInRange:range withString:limitedString]; + textView.text = newString; + // Collapse selection at end of insert to match normal paste behavior + UITextPosition *insertEnd = [textView positionFromPosition:textView.beginningOfDocument + offset:(range.location + allowedLength)]; + textView.selectedTextRange = [textView textRangeFromPosition:insertEnd toPosition:insertEnd]; + [self textViewDidChange:textView]; + } + return NO; } - return NO; + } + + _nativeUpdatesInFlight = YES; + + if (range.location + range.length > _predictedText.length) { + // _predictedText got out of sync in a bad way, so let's just force sync it. Haven't been able to repro this, but + // it's causing a real crash here: #6523822 + _predictedText = textView.text; + } + + NSString *previousText = [_predictedText substringWithRange:range]; + if (_predictedText) { + _predictedText = [_predictedText stringByReplacingCharactersInRange:range withString:text]; } else { - return YES; + _predictedText = text; + } + + if (_onTextInput) { + _onTextInput(@{ + @"text": text, + @"previousText": previousText ?: @"", + @"range": @{ + @"start": @(range.location), + @"end": @(range.location + range.length) + }, + @"eventCount": @(_nativeEventCount), + }); } + + return YES; } - (void)textViewDidChangeSelection:(RCTUITextView *)textView @@ -402,6 +429,11 @@ - (void)textViewDidChangeSelection:(RCTUITextView *)textView } } +- (NSString *)text +{ + return _textView.text; +} + - (void)setText:(NSString *)text { NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; @@ -409,6 +441,7 @@ - (void)setText:(NSString *)text UITextRange *selection = _textView.selectedTextRange; NSInteger oldTextLength = _textView.text.length; + _predictedText = text; _textView.text = text; if (selection.empty) { @@ -420,7 +453,7 @@ - (void)setText:(NSString *)text _textView.selectedTextRange = [_textView textRangeFromPosition:position toPosition:position]; } - [self _setPlaceholderVisibility]; + [self updatePlaceholderVisibility]; [self updateContentSize]; //keep the text wrapping when the length of //the textline has been extended longer than the length of textinputView } else if (eventLag > RCTTextUpdateLagWarningThreshold) { @@ -428,7 +461,7 @@ - (void)setText:(NSString *)text } } -- (void)_setPlaceholderVisibility +- (void)updatePlaceholderVisibility { if (_textView.text.length > 0) { [_placeholderView setHidden:YES]; @@ -461,7 +494,7 @@ - (void)textViewDidBeginEditing:(UITextView *)textView { if (_clearTextOnFocus) { _textView.text = @""; - [self _setPlaceholderVisibility]; + [self updatePlaceholderVisibility]; } [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus @@ -471,10 +504,55 @@ - (void)textViewDidBeginEditing:(UITextView *)textView eventCount:_nativeEventCount]; } +static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, NSRange *secondRange) +{ + NSInteger firstMismatch = -1; + for (NSUInteger ii = 0; ii < MAX(first.length, second.length); ii++) { + if (ii >= first.length || ii >= second.length || [first characterAtIndex:ii] != [second characterAtIndex:ii]) { + firstMismatch = ii; + break; + } + } + + if (firstMismatch == -1) { + return NO; + } + + NSUInteger ii = second.length; + NSUInteger lastMismatch = first.length; + while (ii > firstMismatch && lastMismatch > firstMismatch) { + if ([first characterAtIndex:(lastMismatch - 1)] != [second characterAtIndex:(ii - 1)]) { + break; + } + ii--; + lastMismatch--; + } + + *firstRange = NSMakeRange(firstMismatch, lastMismatch - firstMismatch); + *secondRange = NSMakeRange(firstMismatch, ii - firstMismatch); + return YES; +} + - (void)textViewDidChange:(UITextView *)textView { + [self updatePlaceholderVisibility]; [self updateContentSize]; - [self _setPlaceholderVisibility]; + + // Detect when textView updates happend that didn't invoke `shouldChangeTextInRange` + // (e.g. typing simplified chinese in pinyin will insert and remove spaces without + // calling shouldChangeTextInRange). This will cause JS to get out of sync so we + // update the mismatched range. + NSRange currentRange; + NSRange predictionRange; + if (findMismatch(textView.text, _predictedText, ¤tRange, &predictionRange)) { + NSString *replacement = [textView.text substringWithRange:currentRange]; + [self textView:textView shouldChangeTextInRange:predictionRange replacementText:replacement]; + // JS will assume the selection changed based on the location of our shouldChangeTextInRange, so reset it. + [self textViewDidChangeSelection:textView]; + _predictedText = textView.text; + } + + _nativeUpdatesInFlight = NO; _nativeEventCount++; if (!self.reactTag || !_onChange) { diff --git a/Libraries/Text/RCTTextViewManager.m b/Libraries/Text/RCTTextViewManager.m index c3fe596f41224b..97fbe5e351c5d4 100644 --- a/Libraries/Text/RCTTextViewManager.m +++ b/Libraries/Text/RCTTextViewManager.m @@ -36,6 +36,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber) RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onTextInput, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString) RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor) RCT_REMAP_VIEW_PROPERTY(returnKeyType, textView.returnKeyType, UIReturnKeyType) From 82b99a1ca33cc0f9c5e85c8a1485368e321eab46 Mon Sep 17 00:00:00 2001 From: Christine Abernathy Date: Fri, 24 Jun 2016 06:57:49 -0700 Subject: [PATCH 519/843] Update MapView doc Summary: Reference: #8203 Changes made: - Added a MapView example to the intro section - Added more details to prop explanations - Added more info to an exported type, even if it's not used anywhere I can see - Removed mention of ios platform in props. Left an android one in there as I didn't want to touch code. **Test plan (required)** Ran the website locally and checked: http://localhost:8079/react-native/docs/mapview.html ![component_mapview_2](https://cloud.githubusercontent.com/assets/691109/16329753/43419508-3999-11e6-9310-11c53ca8c04b.png) Closes https://github.com/facebook/react-native/pull/8389 Differential Revision: D3481609 Pulled By: JoelMarcey fbshipit-source-id: 71e35ce49193dc09d40546ff16bc48559135d63f --- Libraries/Components/MapView/MapView.js | 89 ++++++++++++++++--------- 1 file changed, 59 insertions(+), 30 deletions(-) diff --git a/Libraries/Components/MapView/MapView.js b/Libraries/Components/MapView/MapView.js index b554585c6e0dce..d6eb93f7e85932 100644 --- a/Libraries/Components/MapView/MapView.js +++ b/Libraries/Components/MapView/MapView.js @@ -27,21 +27,58 @@ const requireNativeComponent = require('requireNativeComponent'); type Event = Object; +/** + * State an annotation on the map. + */ export type AnnotationDragState = $Enum<{ + /** + * Annotation is not being touched. + */ idle: string; + /** + * Annotation dragging has began. + */ starting: string; + /** + * Annotation is being dragged. + */ dragging: string; + /** + * Annotation dragging is being canceled. + */ canceling: string; + /** + * Annotation dragging has ended. + */ ending: string; }>; /** - * A component for displaying embeddable maps and annotations using the native - * iOS MKMapView class. The Android version is not currently available in the - * open source React Native project, but you can use Leland Richardson's - * cross-platform and more feature-complete + * **This component is only supported on iOS.** + * + * `MapView` is used to display embeddable maps and annotations using + * `MKMapView`. + * + * For a cross-platform solution, check out * [react-native-maps](https://github.com/lelandrichardson/react-native-maps) - * instead. + * by Leland Richardson. + * + * ``` + * import React, { Component } from 'react'; + * import { MapView } from 'react-native'; + * + * class MapMyRide extends Component { + * render() { + * return ( + * + * ); + * } + * } + * ``` + * */ const MapView = React.createClass({ @@ -51,8 +88,7 @@ const MapView = React.createClass({ propTypes: { ...View.propTypes, /** - * Used to style and layout the `MapView`. See `StyleSheet.js` and - * `ViewStylePropTypes.js` for more info. + * Used to style and layout the `MapView`. */ style: View.propTypes.style, @@ -60,7 +96,7 @@ const MapView = React.createClass({ * If `true` the app will ask for the user's location and display it on * the map. Default value is `false`. * - * **NOTE**: on iOS, you need to add the `NSLocationWhenInUseUsageDescription` + * **NOTE**: You'll need to add the `NSLocationWhenInUseUsageDescription` * key in Info.plist to enable geolocation, otherwise it will fail silently. */ showsUserLocation: React.PropTypes.bool, @@ -69,21 +105,18 @@ const MapView = React.createClass({ * If `true` the map will follow the user's location whenever it changes. * Note that this has no effect unless `showsUserLocation` is enabled. * Default value is `true`. - * @platform ios */ followUserLocation: React.PropTypes.bool, /** * If `false` points of interest won't be displayed on the map. * Default value is `true`. - * @platform ios */ showsPointsOfInterest: React.PropTypes.bool, /** - * If `false` compass won't be displayed on the map. + * If `false`, compass won't be displayed on the map. * Default value is `true`. - * @platform ios */ showsCompass: React.PropTypes.bool, @@ -96,7 +129,9 @@ const MapView = React.createClass({ /** * When this property is set to `true` and a valid camera is associated with * the map, the camera’s heading angle is used to rotate the plane of the - * map around its center point. When this property is set to `false`, the + * map around its center point. + * + * When this property is set to `false`, the * camera’s heading angle is ignored and the map is always oriented so * that true north is situated at the top of the map view */ @@ -105,7 +140,9 @@ const MapView = React.createClass({ /** * When this property is set to `true` and a valid camera is associated * with the map, the camera’s pitch angle is used to tilt the plane - * of the map. When this property is set to `false`, the camera’s pitch + * of the map. + * + * When this property is set to `false`, the camera’s pitch * angle is ignored and the map is always displayed as if the user * is looking straight down onto it. */ @@ -120,11 +157,9 @@ const MapView = React.createClass({ /** * The map type to be displayed. * - * - standard: standard road map (default) - * - satellite: satellite view - * - hybrid: satellite view with roads and points of interest overlaid - * - * @platform ios + * - `standard`: Standard road map (default). + * - `satellite`: Satellite view. + * - `hybrid`: Satellite view with roads and points of interest overlaid. */ mapType: React.PropTypes.oneOf([ 'standard', @@ -154,8 +189,7 @@ const MapView = React.createClass({ }), /** - * Map annotations with title/subtitle. - * @platform ios + * Map annotations with title and subtitle. */ annotations: React.PropTypes.arrayOf(React.PropTypes.shape({ /** @@ -192,7 +226,7 @@ const MapView = React.createClass({ onBlur: React.PropTypes.func, /** - * Annotation title/subtile. + * Annotation title and subtile. */ title: React.PropTypes.string, subtitle: React.PropTypes.string, @@ -253,7 +287,6 @@ const MapView = React.createClass({ /** * Map overlays - * @platform ios */ overlays: React.PropTypes.arrayOf(React.PropTypes.shape({ /** @@ -278,21 +311,17 @@ const MapView = React.createClass({ })), /** - * Maximum size of area that can be displayed. - * @platform ios + * Maximum size of the area that can be displayed. */ maxDelta: React.PropTypes.number, /** - * Minimum size of area that can be displayed. - * @platform ios + * Minimum size of the area that can be displayed. */ minDelta: React.PropTypes.number, /** * Insets for the map's legal label, originally at bottom left of the map. - * See `EdgeInsetsPropType.js` for more information. - * @platform ios */ legalLabelInsets: EdgeInsetsPropType, @@ -307,7 +336,7 @@ const MapView = React.createClass({ onRegionChangeComplete: React.PropTypes.func, /** - * Deprecated. Use annotation onFocus and onBlur instead. + * Deprecated. Use annotation `onFocus` and `onBlur` instead. */ onAnnotationPress: React.PropTypes.func, From ab4684551f09e6dbe1509c79a454c6c54c9fe6f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Fri, 24 Jun 2016 07:32:17 -0700 Subject: [PATCH 520/843] Accessing console logs Summary: Instructions for accessing the output of a `console.log`. ![debugging](https://cloud.githubusercontent.com/assets/165856/16318119/7aff884e-3942-11e6-9a78-853aaba68308.png) Closes https://github.com/facebook/react-native/pull/8323 Differential Revision: D3480718 Pulled By: JoelMarcey fbshipit-source-id: 4185d2e730277b8ad986d3c8904420e7ae1ceb21 --- docs/Debugging.md | 49 ++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/docs/Debugging.md b/docs/Debugging.md index b5d77797ba494d..55a940f0dcb015 100644 --- a/docs/Debugging.md +++ b/docs/Debugging.md @@ -9,7 +9,7 @@ next: testing ## Accessing the In-App Developer Menu -You can access the developer menu by shaking your device or by selecting "Shake Gesture" inside the Hardware menu in the iOS Simulator. You can also use the `Command⌘ + D` keyboard shortcut when your app is running in the iPhone Simulator, or `Command⌘ + M` when running in an Android emulator. +You can access the developer menu by shaking your device or by selecting "Shake Gesture" inside the Hardware menu in the iOS Simulator. You can also use the **`Command`**`⌘` + **`D`** keyboard shortcut when your app is running in the iPhone Simulator, or **`Command`**`⌘` + **`M`** when running in an Android emulator. ![](img/DeveloperMenu.png) @@ -17,22 +17,22 @@ You can access the developer menu by shaking your device or by selecting "Shake ## Reloading JavaScript -Selecting `Reload` from the Developer Menu will reload the JavaScript that powers your application. You can also press `Command⌘ + R` in the iOS Simulator, or press `R` twice on Android emulators. +Instead of recompiling your app every time you make a change, you can reload your app's JavaScript code instantly. To do so, select "Reload" from the Developer Menu. You can also press **`Command`**`⌘` + **`R`** in the iOS Simulator, or press **`R`** twice on Android emulators. -> If you are using a Dvorak/Colemak layout, use the `Command⌘ + P` keyboard shortcut to reload the simulator. +> If the **`Command`**`⌘` + **`R`** keyboard shortcut does not seem to reload the iOS Simulator, go to the Hardware menu, select Keyboard, and make sure that "Connect Hardware Keyboard" is checked. -You will need to rebuild your app for changes to take effect in certain situations: +### Automatic reloading -* You have added new resources to your native app's bundle, such as an image in `Images.xcassets` on iOS or in `res/drawable` folder on Android. -* You have modified native code (Objective-C/Swift on iOS or Java/C++ on Android). +You can speed up your development times by having your app reload automatically any time your code changes. Automatic reloading can be enabled by selecting "Enable Live Reload" from the Developer Menu. -> If the `Command⌘ + R` keyboard shortcut does not seem to reload the iOS Simulator, go to the Hardware menu, select Keyboard, and make sure that "Connect Hardware Keyboard" is checked. +You may even go a step further and keep your app running as new versions of your files are injected into the JavaScript bundle automatically by enabling [Hot Reloading](https://facebook.github.io/react-native/blog/2016/03/24/introducing-hot-reloading.html) from the Developer Menu. This will allow you to persist the app's state through reloads. -### Automatic reloading +> There are some instances where hot reloading cannot be implemented perfectly. If you run into any issues, use a full reload to reset your app. -You may enable Live Reload to automatically trigger a reload whenever your JavaScript code changes. +You will need to rebuild your app for changes to take effect in certain situations: -Live Reload is available on iOS via the Developer Menu. On Android, select "Dev Settings" from the Developer Menu and enable "Auto reload on JS change". +* You have added new resources to your native app's bundle, such as an image in `Images.xcassets` on iOS or the `res/drawable` folder on Android. +* You have modified native code (Objective-C/Swift on iOS or Java/C++ on Android). ## In-app Errors and Warnings @@ -52,41 +52,46 @@ YellowBoxes can be disabled during development by using `console.disableYellowBo > RedBoxes and YellowBoxes are automatically disabled in release (production) builds. -## Accessing logs +## Accessing console logs -Run `react-native log-ios` in a terminal to display the logs for an iOS app running on a device or a simulator. +You can display the console logs for an iOS or Android app by using the following commands in a terminal while the app is running: -You can also view the iOS logs in Xcode: open your app in Xcode, then Build and Run your app on a device or the iPhone Simulator. The console should appear automatically after the app launches. If your app is failing to build, check the Issues Navigator in Xcode. +``` +$ react-native log-ios +$ react-native log-android +``` -Run `react-native log-android` in a terminal to display the logs for an Android app running on a device or an emulator. +You may also access these through `Debug → Open System Log...` in the iOS Simulator or by running `adb logcat *:S ReactNative:V ReactNativeJS:V` in a terminal while an Android app is running on a device or emulator. ## Chrome Developer Tools -To debug the JavaScript code in Chrome, select `Debug JS Remotely` from the Developer Menu. This will open a new tab at [http://localhost:8081/debugger-ui](http://localhost:8081/debugger-ui). +To debug the JavaScript code in Chrome, select "Debug JS Remotely" from the Developer Menu. This will open a new tab at [http://localhost:8081/debugger-ui](http://localhost:8081/debugger-ui). -In Chrome, press `Command⌘ + Option⌥ + I` or select `View` → `Developer` → `Developer Tools` to toggle the developer tools console. Enable [Pause On Caught Exceptions](http://stackoverflow.com/questions/2233339/javascript-is-there-a-way-to-get-chrome-to-break-on-all-errors/17324511#17324511) for a better debugging experience. +Select `Tools → Developer Tools` from the Chrome Menu to open the [Developer Tools](https://developer.chrome.com/devtools). You may also access the DevTools using keyboard shortcuts (**`Command`**`⌘` + **`Option`**`⌥` + **`I`** on Mac, **`Ctrl`** + **`Shift`** + **`I`** on Windows). You may also want to enable [Pause On Caught Exceptions](http://stackoverflow.com/questions/2233339/javascript-is-there-a-way-to-get-chrome-to-break-on-all-errors/17324511#17324511) for a better debugging experience. > It is [currently not possible](https://github.com/facebook/react-devtools/issues/229) to use the "React" tab in the Chrome Developer Tools to inspect app widgets. You can use Nuclide's "React Native Inspector" as a workaround. ### Debugging on a device with Chrome Developer Tools -On iOS devices, open the file [`RCTWebSocketExecutor.m`](https://github.com/facebook/react-native/blob/master/Libraries/WebSocket/RCTWebSocketExecutor.m) and change `localhost` to the IP address of your computer, then select `Debug JS Remotely` from the Developer Menu. +On iOS devices, open the file [`RCTWebSocketExecutor.m`](https://github.com/facebook/react-native/blob/master/Libraries/WebSocket/RCTWebSocketExecutor.m) and change "localhost" to the IP address of your computer, then select "Debug JS Remotely" from the Developer Menu. On Android 5.0+ devices connected via USB, you can use the [`adb` command line tool](http://developer.android.com/tools/help/adb.html) to setup port forwarding from the device to your computer: `adb reverse tcp:8081 tcp:8081` -Alternatively, select `Dev Settings` from the Developer Menu, then update the `Debug server host for device` setting to match the IP address of your computer. +Alternatively, select "Dev Settings" from the Developer Menu, then update the "Debug server host for device" setting to match the IP address of your computer. > If you run into any issues, it may be possible that one of your Chrome extensions is interacting in unexpected ways with the debugger. Try disabling all of your extensions and re-enabling them one-by-one until you find the problematic extension. ### Debugging using a custom JavaScript debugger -To use a custom JavaScript debugger in place of Chrome Developer Tools, set the `REACT_DEBUGGER` environment variable to a command that will start your custom debugger. You can then select `Debug JS Remotely` from the Developer Menu to start debugging. +To use a custom JavaScript debugger in place of Chrome Developer Tools, set the `REACT_DEBUGGER` environment variable to a command that will start your custom debugger. You can then select "Debug JS Remotely" from the Developer Menu to start debugging. + +The debugger will receive a list of all project roots, separated by a space. For example, if you set `REACT_DEBUGGER="node /path/to/launchDebugger.js --port 2345 --type ReactNative"`, then the command `node /path/to/launchDebugger.js --port 2345 --type ReactNative /path/to/reactNative/app` will be used to start your debugger. -> The debugger will receive a list of all project roots, separated by a space. For example, if you set `REACT_DEBUGGER="node /path/to/launchDebugger.js --port 2345 --type ReactNative"`, then the command `node /path/to/launchDebugger.js --port 2345 --type ReactNative /path/to/reactNative/app` will be used to start your debugger. Custom debugger commands executed this way should be short-lived processes, and they shouldn't produce more than 200 kilobytes of output. +> Custom debugger commands executed this way should be short-lived processes, and they shouldn't produce more than 200 kilobytes of output. -## FPS (Frames per Second) Monitor +## Performance Monitor -You can enable a FPS graph overlay in the Developer Menu in order to help you debug performance problems. +You can enable a performance overlay to help you debug performance problems by selecting "Perf Monitor" in the Developer Menu. From 35f5ce296b47fa1ad13077f7d7d01dd25c5422fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Fri, 24 Jun 2016 07:36:26 -0700 Subject: [PATCH 521/843] Add Navigation Overview Summary: Initial stab at writing a high level guide on navigation. Its main focus is on Navigator due to it being cross-platform and fairly simple to use. This guide should be expanded to cover tabbed applications in a future pull request. The Navigation (Experimental) section will be similarly expanded upon as the API stabilizes. ![navigation](https://cloud.githubusercontent.com/assets/165856/16324560/52b508dc-396a-11e6-94b7-b2d1175f69e0.png) Closes https://github.com/facebook/react-native/pull/8390 Differential Revision: D3480304 Pulled By: caabernathy fbshipit-source-id: 280da9185fca295bc107a2df20106c783b461be7 --- .../Components/Navigation/NavigatorIOS.ios.js | 3 - docs/JavaScriptEnvironment.md | 2 +- docs/Navigation.md | 165 ++++++++++++++++++ 3 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 docs/Navigation.md diff --git a/Libraries/Components/Navigation/NavigatorIOS.ios.js b/Libraries/Components/Navigation/NavigatorIOS.ios.js index 66ea9ee3e27db3..8e8390d15ceac6 100644 --- a/Libraries/Components/Navigation/NavigatorIOS.ios.js +++ b/Libraries/Components/Navigation/NavigatorIOS.ios.js @@ -98,9 +98,6 @@ type Event = Object; * [`Navigator`](/docs/navigator.html) for a similar solution for your * cross-platform needs. * - * > **NOTE**: This component is not maintained by Facebook and is the - * > community's responsibility. - * * To set up the navigator, provide the `initialRoute` prop with a route * object. A route object is used to describe each scene that your app * navigates to. `initialRoute` represents the first route in your navigator. diff --git a/docs/JavaScriptEnvironment.md b/docs/JavaScriptEnvironment.md index 4e2e3c1f21d579..857a3fd71a7de9 100644 --- a/docs/JavaScriptEnvironment.md +++ b/docs/JavaScriptEnvironment.md @@ -4,7 +4,7 @@ title: JavaScript Environment layout: docs category: Guides permalink: docs/javascript-environment.html -next: navigator-comparison +next: navigation --- ## JavaScript Runtime diff --git a/docs/Navigation.md b/docs/Navigation.md new file mode 100644 index 00000000000000..f4a8fcab9880d1 --- /dev/null +++ b/docs/Navigation.md @@ -0,0 +1,165 @@ +--- +id: navigation +title: Navigation +layout: docs +category: Guides +permalink: docs/navigation.html +next: navigator-comparison +--- + +Mobile apps rarely consist of just one screen or scene. As soon as you add a second scene to your app, you will have to take into consideration how the user will navigate from one scene to the other. + +Navigators in React Native allow you to push and pop scenes in a master/detail stack, or to pop up modal scenes. Navigators handle the transitions between scenes, and also maintain the navigational state of your application. + +If you are just getting started with React Native, you will probably want to start with the `Navigator` component. + +## Navigator + +`Navigator` is a cross-platform implementation of a navigation stack, so it works on both iOS and Android. It is easy to customize and includes simple navigation bars. + +```js + { + // We'll get to this function soon. + }} +/> +``` + +Something you will encounter a lot when dealing with navigation is the concept of routes. A route is an object that contains information about a scene. It is used to provide all the context the `renderScene` function needs to render a scene. + +The `push` and `pop` functions provided by Navigator can be used to push and pop routes into the navigation stack. A more complete example that demonstrates the pushing and popping of routes could therefore look something like this: + +```js +class MyScene extends Component { + static propTypes = { + title: PropTypes.string.isRequired, + onForward: PropTypes.func.isRequired, + onBack: PropTypes.func.isRequired, + } + render() { + return ( + + Current Scene: { this.props.title } + + Tap me to load the next scene + + + Tap me to go back + + + ) + } +} + +class SimpleNavigationApp extends Component { + render() { + return ( + + { + const nextIndex = route.index + 1; + navigator.push({ + title: 'Scene ' + nextIndex, + index: nextIndex, + }); + }} + onBack={() => { + if (route.index > 0) { + navigator.pop(); + } + }} + /> + } + /> + ) + } +} +``` + +In this example, the `MyScene` component is passed the title of the current route via the `title` prop. It displays two tappable components that call the `onForward` and `onBack` functions passed through its props, which in turn will call `navigator.push()` and `navigator.pop()` as needed. + +While this is a very basic example, it can easily be adapted to render an entirely different component based on the route that is passed to the `renderScene` function. Navigator will push new scenes from the right by default, and you can control this behavior by using the `configureScene` function. Check out the [Navigator API reference](docs/navigator.html) to learn more. + +## NavigatorIOS + +If you are targeting iOS only, you may also want to consider using `NavigatorIOS`. It looks and feels just like `UINavigationController`, because it is actually built on top of it. + +```js + +``` + +Just like Navigator, it it uses routes to represent scenes, with some important differences. The actual component that will be rendered can be specified using the `component` key in the route, and any props that should be passed to this component can be specified in `passProps`. A navigator object is automatically passed as a prop to the component, allowing you to call `push` and `pop` as needed. + +Check out the [NavigatorIOS reference docs](docs/navigatorios.html) to learn more about this component. + +```js +class MyScene extends Component { + static propTypes = { + title: PropTypes.string.isRequired, + navigator: PropTypes.object.isRequired, + } + + constructor(props, context) { + super(props, context); + this._onForward = this._onForward.bind(this); + this._onBack = this._onBack.bind(this); + } + + _onForward() { + this.props.navigator.push({ + title: 'Scene ' + nextIndex, + }); + } + + _onBack() { + this.props.navigator.pop(); + } + + render() { + return ( + + Current Scene: { this.props.title } + + Tap me to load the next scene + + + Tap me to go back + + + ) + } +} + +class NavigatorIOSApp extends Component { + render() { + return ( + + + } + /> + ) + } +} +``` + +> You may also want to check out [react-native-navigation](https://github.com/wix/react-native-navigation), a component that aims to provide native navigation on both iOS and Android. + +## Navigation (Experimental) + +If you are looking for a more powerful navigation API, check out [NavigationExperimental](https://github.com/facebook/react-native/tree/master/Examples/UIExplorer/NavigationExperimental). It provides greater customization over your transitions, uses single-directional data flow using reducers to manipulate state at a top-level object, and offloads transition animations to the GPU. From 1c290d69c164f34ee00f57f92a203341aa202e26 Mon Sep 17 00:00:00 2001 From: Franklyn Tackitt Date: Fri, 24 Jun 2016 08:23:42 -0700 Subject: [PATCH 522/843] Remove iOS platform check for running devtools Summary: Currently, DevTools only work under ios (although this is undocumented!), because the JavaScriptEngine initialization process skips setupDevTools() on android. DevTools work fine with Android, as tested on 0.26, 0.27, and 0.28 using Nuclide's inspector. For reference, [the relevant issue on react-devtools](https://github.com/facebook/react-devtools/issues/229). Closes https://github.com/facebook/react-native/pull/8095 Reviewed By: javache Differential Revision: D3443980 Pulled By: andreicoman11 fbshipit-source-id: 3d7b2e83cf4158a1228d2e21510509ab63411a5d --- .../InitializeJavaScriptAppEngine.js | 2 +- .../react/testing/FakeWebSocketModule.java | 60 +++++++++++++++++++ .../testing/ReactInstanceSpecForTest.java | 4 +- ...alystNativeJSToJavaParametersTestCase.java | 6 +- ...talystNativeJavaToJSArgumentsTestCase.java | 2 + .../react/tests/InitialPropsTestCase.java | 2 + .../facebook/react/tests/JSLocaleTest.java | 5 +- .../react/tests/ProgressBarTestCase.java | 2 + .../react/tests/ViewRenderingTestCase.java | 2 + 9 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/testing/FakeWebSocketModule.java diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index ba5fa68fc9bdf4..c83c52047a4a99 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -220,7 +220,7 @@ function setUpCollections(): void { function setUpDevTools(): void { if (__DEV__) { // not when debugging in chrome - if (!window.document && require('Platform').OS === 'ios') { + if (!window.document) { const setupDevtools = require('setupDevtools'); setupDevtools(); } diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/FakeWebSocketModule.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/FakeWebSocketModule.java new file mode 100644 index 00000000000000..76c9edb2345a93 --- /dev/null +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/FakeWebSocketModule.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.testing; + +import javax.annotation.Nullable; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; + +/** + * Dummy implementation of storage module, used for testing + */ +public final class FakeWebSocketModule extends BaseJavaModule { + + private static WritableMap errorMessage; + static { + errorMessage = Arguments.createMap(); + errorMessage.putString("message", "Fake Fake Web Socke tModule"); + } + + @Override + public String getName() { + return "WebSocketModule"; + } + + @Override + public boolean canOverrideExistingModule() { + return true; + } + + @ReactMethod + public void connect( + final String url, + @Nullable final ReadableArray protocols, + @Nullable final ReadableMap headers, + final int id) { + } + + @ReactMethod + public void close(int code, String reason, int id) { + } + + @ReactMethod + public void send(String message, int id) { + } + + @ReactMethod + public void sendBinary(String base64String, int id) { + } +} diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactInstanceSpecForTest.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactInstanceSpecForTest.java index b9de7a1e7726fa..bf179010b18312 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactInstanceSpecForTest.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactInstanceSpecForTest.java @@ -9,6 +9,7 @@ package com.facebook.react.testing; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import android.annotation.SuppressLint; @@ -26,7 +27,8 @@ @SuppressLint("JavatestsIncorrectFolder") public class ReactInstanceSpecForTest { - private final List mNativeModules = new ArrayList<>(); + private final List mNativeModules = + new ArrayList(Arrays.asList(new FakeWebSocketModule())); private final List> mJSModuleSpecs = new ArrayList<>(); private final List mViewManagers = new ArrayList<>(); private ReactPackage mReactPackage = null; diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java index f6130be3aaec7f..06ff91a0cfa626 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java @@ -30,6 +30,7 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.modules.systeminfo.AndroidInfoModule; +import com.facebook.react.testing.FakeWebSocketModule; import com.facebook.react.testing.ReactIntegrationTestCase; import com.facebook.react.testing.ReactTestHelper; import com.facebook.react.uimanager.UIImplementation; @@ -37,8 +38,6 @@ import com.facebook.react.uimanager.ViewManager; import com.facebook.react.views.view.ReactViewManager; -import org.junit.Ignore; - /** * Integration test to verify passing various types of parameters from JS to Java works */ @@ -74,7 +73,7 @@ private interface TestJSToJavaParametersModule extends JavaScriptModule { @Override protected void setUp() throws Exception { super.setUp(); - + List viewManagers = Arrays.asList( new ReactViewManager()); final UIManagerModule mUIManager = new UIManagerModule( @@ -94,6 +93,7 @@ public void run() { mCatalystInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(mRecordingTestModule) .addNativeModule(new AndroidInfoModule()) + .addNativeModule(new FakeWebSocketModule()) .addNativeModule(mUIManager) .addJSModule(TestJSToJavaParametersModule.class) .build(); diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java index fd6b1d779b2d68..587f99121860ea 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java @@ -20,6 +20,7 @@ import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.testing.AssertModule; +import com.facebook.react.testing.FakeWebSocketModule; import com.facebook.react.testing.ReactIntegrationTestCase; import com.facebook.react.testing.ReactTestHelper; import com.facebook.react.uimanager.UIImplementation; @@ -75,6 +76,7 @@ public void run() { mInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(mAssertModule) + .addNativeModule(new FakeWebSocketModule()) .addJSModule(TestJavaToJSArgumentsModule.class) .addNativeModule(mUIManager) .build(); diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/InitialPropsTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/InitialPropsTestCase.java index 6f532e15ce411c..951c593533e453 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/InitialPropsTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/InitialPropsTestCase.java @@ -12,6 +12,7 @@ import android.test.ActivityInstrumentationTestCase2; import com.facebook.react.bridge.BaseJavaModule; +import com.facebook.react.testing.FakeWebSocketModule; import com.facebook.react.testing.ReactInstanceSpecForTest; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; @@ -70,6 +71,7 @@ public void testInitialProps() throws Throwable { @Override public void run() { ReactInstanceSpecForTest catalystInstanceSpec = new ReactInstanceSpecForTest(); + catalystInstanceSpec.addNativeModule(new FakeWebSocketModule()); catalystInstanceSpec.addNativeModule(mRecordingModule); Bundle props = new Bundle(); props.putString("key1", "string"); diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/JSLocaleTest.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/JSLocaleTest.java index 370f353c322cf9..d20b72fb47c613 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/JSLocaleTest.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/JSLocaleTest.java @@ -11,6 +11,7 @@ import java.util.Arrays; import java.util.List; +import com.facebook.react.testing.FakeWebSocketModule; import com.facebook.react.testing.ReactIntegrationTestCase; import com.facebook.react.testing.ReactTestHelper; import com.facebook.react.testing.StringRecordingModule; @@ -59,9 +60,9 @@ public void run() { mInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(mStringRecordingModule) .addNativeModule(mUIManager) + .addNativeModule(new FakeWebSocketModule()) .addJSModule(TestJSLocaleModule.class) .build(); - } public void testToUpper() { @@ -100,6 +101,4 @@ public void testToLower() { assertEquals("γαζίες καὶ μυρτιὲς δὲν θὰ βρῶ πιὰ στὸ χρυσαφὶ ξέφωτο", answers[3]); assertEquals("chinese: 幓 厏吪吙 鈊釿閍 碞碠粻 曮禷", answers[4]); } - - } diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java index b3282629bd11ce..0886f20fa4b0f5 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java @@ -30,6 +30,7 @@ import com.facebook.react.uimanager.ViewManager; import com.facebook.react.views.progressbar.ReactProgressBarViewManager; import com.facebook.react.views.view.ReactViewManager; +import com.facebook.react.testing.FakeWebSocketModule; import com.facebook.react.testing.ReactIntegrationTestCase; import com.facebook.react.testing.ReactTestHelper; @@ -83,6 +84,7 @@ public void run() { mInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(mUIManager) .addNativeModule(new AndroidInfoModule()) + .addNativeModule(new FakeWebSocketModule()) .addJSModule(ProgressBarTestModule.class) .build(); diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java index f4f817c2b30482..38fff46acd7dc7 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java @@ -26,6 +26,7 @@ import com.facebook.react.uimanager.ViewManager; import com.facebook.react.views.view.ReactViewGroup; import com.facebook.react.views.view.ReactViewManager; +import com.facebook.react.testing.FakeWebSocketModule; import com.facebook.react.testing.ReactIntegrationTestCase; import com.facebook.react.testing.ReactTestHelper; @@ -64,6 +65,7 @@ public void run() { mCatalystInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(uiManager) .addNativeModule(new AndroidInfoModule()) + .addNativeModule(new FakeWebSocketModule()) .addJSModule(ViewRenderingTestModule.class) .build(); From b664e6e73465f9eb5c748e4960c95b65f620952e Mon Sep 17 00:00:00 2001 From: Christine Abernathy Date: Fri, 24 Jun 2016 08:42:28 -0700 Subject: [PATCH 523/843] Update AsyncStorage doc Summary: Relates to #8203 for AsyncStorage API update. - Added a small example to the intro section. - Added jsdoc format tags to show up class description, parameter descriptions. - Word-smithed many of the method descriptions. I also made a bug fix to the autogen. It wasn't handling the scenario where a method may have no parameters. **Test plan (required)** Wrote a small sample app to test the snippet added to the intro section. Ran website locally: http://localhost:8079/react-native/docs/asyncstorage.html ![api_asyncstorage](https://cloud.githubusercontent.com/assets/691109/16329457/84f9d69c-3997-11e6-9e68-3a475df90377.png) Ran changed files through the linter. Closes https://github.com/facebook/react-native/pull/8396 Differential Revision: D3481783 Pulled By: JoelMarcey fbshipit-source-id: ebc4b9695482ada8a3455e621534d2a7fb11edf4 --- Libraries/Storage/AsyncStorage.js | 191 +++++++++++++++++++++--------- website/layout/AutodocsLayout.js | 2 +- 2 files changed, 136 insertions(+), 57 deletions(-) diff --git a/Libraries/Storage/AsyncStorage.js b/Libraries/Storage/AsyncStorage.js index 01d76748d86869..471c5304c9f9af 100644 --- a/Libraries/Storage/AsyncStorage.js +++ b/Libraries/Storage/AsyncStorage.js @@ -9,6 +9,7 @@ * @providesModule AsyncStorage * @noflow * @flow-weak + * @jsdoc */ 'use strict'; @@ -21,18 +22,45 @@ var RCTAsyncFileStorage = NativeModules.AsyncLocalStorage; var RCTAsyncStorage = RCTAsyncRocksDBStorage || RCTAsyncSQLiteStorage || RCTAsyncFileStorage; /** - * AsyncStorage is a simple, asynchronous, persistent, key-value storage + * @class + * @description + * `AsyncStorage` is a simple, asynchronous, persistent, key-value storage * system that is global to the app. It should be used instead of LocalStorage. * - * It is recommended that you use an abstraction on top of AsyncStorage instead - * of AsyncStorage directly for anything more than light usage since it - * operates globally. + * It is recommended that you use an abstraction on top of `AsyncStorage` + * instead of `AsyncStorage` directly for anything more than light usage since + * it operates globally. * - * On iOS, AsyncStorage is backed by native code that stores small values in a serialized - * dictionary and larger values in separate files. On Android, AsyncStorage will use either - * RocksDB or SQLite based on what is available. This JS code is a simple facade that - * provides a clear JS API, real Error objects, and simple non-multi functions. Each - * method returns a `Promise` object. + * On iOS, `AsyncStorage` is backed by native code that stores small values in a + * serialized dictionary and larger values in separate files. On Android, + * `AsyncStorage` will use either [RocksDB](http://rocksdb.org/) or SQLite + * based on what is available. + * + * The `AsyncStorage` JavaScript code is a simple facade that provides a clear + * JavaScript API, real `Error` objects, and simple non-multi functions. Each + * method in the API returns a `Promise` object. + * + * Persisting data: + * ``` + * try { + * await AsyncStorage.setItem('@MySuperStore:key', 'I like to save it.'); + * } catch (error) { + * // Error saving data + * } + * ``` + * + * Fetching data: + * ``` + * try { + * const value = await AsyncStorage.getItem('@MySuperStore:key'); + * if (value !== null){ + * // We have data!! + * console.log(value); + * } + * } catch (error) { + * // Error retrieving data + * } + * ``` */ var AsyncStorage = { _getRequests: ([]: Array), @@ -40,8 +68,12 @@ var AsyncStorage = { _immediate: (null: ?number), /** - * Fetches `key` and passes the result to `callback`, along with an `Error` if - * there is any. Returns a `Promise` object. + * Fetches an item for a `key` and invokes a callback upon completion. + * Returns a `Promise` object. + * @param key Key of the item to fetch. + * @param callback Function that will be called with a result if found or + * any error. + * @returns A `Promise` object. */ getItem: function( key: string, @@ -63,8 +95,12 @@ var AsyncStorage = { }, /** - * Sets `value` for `key` and calls `callback` on completion, along with an - * `Error` if there is any. Returns a `Promise` object. + * Sets the value for a `key` and invokes a callback upon completion. + * Returns a `Promise` object. + * @param key Key of the item to set. + * @param value Value to set for the `key`. + * @param callback Function that will be called with any error. + * @returns A `Promise` object. */ setItem: function( key: string, @@ -85,7 +121,11 @@ var AsyncStorage = { }, /** + * Removes an item for a `key` and invokes a callback upon completion. * Returns a `Promise` object. + * @param key Key of the item to remove. + * @param callback Function that will be called with any error. + * @returns A `Promise` object. */ removeItem: function( key: string, @@ -105,32 +145,39 @@ var AsyncStorage = { }, /** - * Merges existing value with input value, assuming they are stringified json. - * Returns a `Promise` object. Not supported by all native implementations. + * Merges an existing `key` value with an input value, assuming both values + * are stringified JSON. Returns a `Promise` object. * - * Example: - * ```javascript + * **NOTE:** This is not supported by all native implementations. + * + * @param key Key of the item to modify. + * @param value New value to merge for the `key`. + * @param callback Function that will be called with any error. + * @returns A `Promise` object. + * + * @example Example * let UID123_object = { * name: 'Chris', * age: 30, * traits: {hair: 'brown', eyes: 'brown'}, * }; - - // need only define what will be added or updated + * // You only need to define what will be added or updated * let UID123_delta = { * age: 31, * traits: {eyes: 'blue', shoe_size: 10} * }; - + * * AsyncStorage.setItem('UID123', JSON.stringify(UID123_object), () => { * AsyncStorage.mergeItem('UID123', JSON.stringify(UID123_delta), () => { * AsyncStorage.getItem('UID123', (err, result) => { * console.log(result); - * // => {'name':'Chris','age':31,'traits':{'shoe_size':10,'hair':'brown','eyes':'blue'}} * }); * }); * }); - * ``` + * + * // Console log result: + * // => {'name':'Chris','age':31,'traits': + * // {'shoe_size':10,'hair':'brown','eyes':'blue'}} */ mergeItem: function( key: string, @@ -151,9 +198,11 @@ var AsyncStorage = { }, /** - * Erases *all* AsyncStorage for all clients, libraries, etc. You probably - * don't want to call this - use removeItem or multiRemove to clear only your - * own keys instead. Returns a `Promise` object. + * Erases *all* `AsyncStorage` for all clients, libraries, etc. You probably + * don't want to call this; use `removeItem` or `multiRemove` to clear only + * your app's keys. Returns a `Promise` object. + * @param callback Function that will be called with any error. + * @returns A `Promise` object. */ clear: function(callback?: ?(error: ?Error) => void): Promise { return new Promise((resolve, reject) => { @@ -169,9 +218,12 @@ var AsyncStorage = { }, /** - * Gets *all* keys known to the app, for all callers, libraries, etc. Returns a `Promise` object. + * Gets *all* keys known to your app; for all callers, libraries, etc. + * Returns a `Promise` object. + * @param callback Function that will be called the keys found and any error. + * @returns A `Promise` object. * - * Example: see multiGet for example + * Example: see the `multiGet` example. */ getAllKeys: function(callback?: ?(error: ?Error, keys: ?Array) => void): Promise { return new Promise((resolve, reject) => { @@ -196,7 +248,7 @@ var AsyncStorage = { * indicate which key caused the error. */ - /** Flushes any pending requests using a single multiget */ + /** Flushes any pending requests using a single batch call to get the data. */ flushGetRequests: function(): void { const getRequests = this._getRequests; const getKeys = this._getKeys; @@ -225,13 +277,23 @@ var AsyncStorage = { }, /** - * multiGet invokes callback with an array of key-value pair arrays that - * matches the input format of multiSet. Returns a `Promise` object. + * This allows you to batch the fetching of items given an array of `key` + * inputs. Your callback will be invoked with an array of corresponding + * key-value pairs found: * - * multiGet(['k1', 'k2'], cb) -> cb([['k1', 'val1'], ['k2', 'val2']]) + * ``` + * multiGet(['k1', 'k2'], cb) -> cb([['k1', 'val1'], ['k2', 'val2']]) + * ``` + * + * The method returns a `Promise` object. + * + * @param keys Array of key for the items to get. + * @param callback Function that will be called with a key-value array of + * the results, plus an array of any key-specific errors found. + * @returns A `Promise` object. + * + * @example Example * - * Example: - * ```javascript * AsyncStorage.getAllKeys((err, keys) => { * AsyncStorage.multiGet(keys, (err, stores) => { * stores.map((result, i, store) => { @@ -241,7 +303,6 @@ var AsyncStorage = { * }); * }); * }); - * ``` */ multiGet: function( keys: Array, @@ -280,12 +341,20 @@ var AsyncStorage = { }, /** - * multiSet and multiMerge take arrays of key-value array pairs that match - * the output of multiGet, e.g. Returns a `Promise` object. + * Use this as a batch operation for storing multiple key-value pairs. When + * the operation completes you'll get a single callback with any errors: * - * multiSet([['k1', 'val1'], ['k2', 'val2']], cb); + * ``` + * multiSet([['k1', 'val1'], ['k2', 'val2']], cb); + * ``` + * + * The method returns a `Promise` object. * - * Example: see multiMerge for an example + * @param keyValuePairs Array of key-value array for the items to set. + * @param callback Function that will be called with an array of any + * key-specific errors found. + * @returns A `Promise` object. + * Example: see the `multiMerge` example. */ multiSet: function( keyValuePairs: Array>, @@ -305,16 +374,20 @@ var AsyncStorage = { }, /** - * Delete all the keys in the `keys` array. Returns a `Promise` object. + * Call this to batch the deletion of all keys in the `keys` array. Returns + * a `Promise` object. * - * Example: - * ```javascript + * @param keys Array of key for the items to delete. + * @param callback Function that will be called an array of any key-specific + * errors found. + * @returns A `Promise` object. + * + * @example Example * let keys = ['k1', 'k2']; * AsyncStorage.multiRemove(keys, (err) => { * // keys k1 & k2 removed, if they existed * // do most stuff after removal (if you want) * }); - * ``` */ multiRemove: function( keys: Array, @@ -334,42 +407,47 @@ var AsyncStorage = { }, /** - * Merges existing values with input values, assuming they are stringified - * json. Returns a `Promise` object. + * Batch operation to merge in existing and new values for a given set of + * keys. This assumes that the values are stringified JSON. Returns a + * `Promise` object. * - * Not supported by all native implementations. + * **NOTE**: This is not supported by all native implementations. * - * Example: - * ```javascript - // first user, initial values + * @param keyValuePairs Array of key-value array for the items to merge. + * @param callback Function that will be called with an array of any + * key-specific errors found. + * @returns A `Promise` object. + * + * @example Example + * // first user, initial values * let UID234_object = { * name: 'Chris', * age: 30, * traits: {hair: 'brown', eyes: 'brown'}, * }; - + * * // first user, delta values * let UID234_delta = { * age: 31, * traits: {eyes: 'blue', shoe_size: 10}, * }; - + * * // second user, initial values * let UID345_object = { * name: 'Marge', * age: 25, * traits: {hair: 'blonde', eyes: 'blue'}, * }; - + * * // second user, delta values * let UID345_delta = { * age: 26, * traits: {eyes: 'green', shoe_size: 6}, * }; - + * * let multi_set_pairs = [['UID234', JSON.stringify(UID234_object)], ['UID345', JSON.stringify(UID345_object)]] * let multi_merge_pairs = [['UID234', JSON.stringify(UID234_delta)], ['UID345', JSON.stringify(UID345_delta)]] - + * * AsyncStorage.multiSet(multi_set_pairs, (err) => { * AsyncStorage.multiMerge(multi_merge_pairs, (err) => { * AsyncStorage.multiGet(['UID234','UID345'], (err, stores) => { @@ -377,13 +455,14 @@ var AsyncStorage = { * let key = store[i][0]; * let val = store[i][1]; * console.log(key, val); - * // => UID234 {"name":"Chris","age":31,"traits":{"shoe_size":10,"hair":"brown","eyes":"blue"}} - * // => UID345 {"name":"Marge","age":26,"traits":{"shoe_size":6,"hair":"blonde","eyes":"green"}} * }); * }); * }); * }); - * ``` + * + * // Console log results: + * // => UID234 {"name":"Chris","age":31,"traits":{"shoe_size":10,"hair":"brown","eyes":"blue"}} + * // => UID345 {"name":"Marge","age":26,"traits":{"shoe_size":6,"hair":"blonde","eyes":"green"}} */ multiMerge: function( keyValuePairs: Array>, diff --git a/website/layout/AutodocsLayout.js b/website/layout/AutodocsLayout.js index 1f514a6cfe13f7..f85016b6e4d190 100644 --- a/website/layout/AutodocsLayout.js +++ b/website/layout/AutodocsLayout.js @@ -650,7 +650,7 @@ var Method = React.createClass({ || ''} {this.props.name} - ({this.props.params + ({this.props.params && this.props.params.length && this.props.params .map((param) => { var res = param.name; res += param.optional ? '?' : ''; From 3ffaedaeaa9f5a23fbf894fb58f632b4f9505228 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Fri, 24 Jun 2016 08:58:20 -0700 Subject: [PATCH 524/843] Fix typo in maxHeight Reviewed By: andreicoman11 Differential Revision: D3481774 fbshipit-source-id: 98e025fe0c5c7f2d8a80edcb1c07fbd30d7425ef --- React/Views/RCTShadowView.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index 9a2890550407f2..a15e0e7ada57d7 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -524,7 +524,7 @@ - (CGFloat)getProp \ RCT_DIMENSION_PROPERTY(MinWidth, minWidth, WIDTH, minDimensions) RCT_DIMENSION_PROPERTY(MaxWidth, maxWidth, WIDTH, maxDimensions) RCT_DIMENSION_PROPERTY(MinHeight, minHeight, HEIGHT, minDimensions) -RCT_DIMENSION_PROPERTY(maxHeight, maxHeight, HEIGHT, maxDimensions) +RCT_DIMENSION_PROPERTY(MaxHeight, maxHeight, HEIGHT, maxDimensions) // Position From f66acad30b00e89a81c19812300ca1c4bae895a1 Mon Sep 17 00:00:00 2001 From: Christine Abernathy Date: Fri, 24 Jun 2016 10:01:09 -0700 Subject: [PATCH 525/843] Fix errors related to typehint when generating docs Summary: After pulling in AsyncStorage doc changes, getting typehint errors when running docs. This fixes that issue. **Test plan (required)** Opened http://localhost:8079/react-native/index.html Clicked around. No errors. Also successfully ran: ``` node server/generate.js ``` Closes https://github.com/facebook/react-native/pull/8412 Differential Revision: D3482007 Pulled By: JoelMarcey fbshipit-source-id: 7b0da2b2b38fd1f1bdec1b7c810ee70c536dd2bb --- website/server/extractDocs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index 6407a539532568..f78aef2eedde57 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -377,7 +377,7 @@ function getTypehint(typehint) { try { var typehint = JSON.parse(typehint); } catch (e) { - return typehint.split('|').map(type => type.trim()); + return typehint.toString().split('|').map(type => type.trim()); } return getTypehintRec(typehint); } From db7b44ec8ebb9a72f13a8397dd35007e24f4e903 Mon Sep 17 00:00:00 2001 From: Joel Marcey Date: Fri, 24 Jun 2016 10:39:06 -0700 Subject: [PATCH 526/843] Update Image API Summary: - Provide runnable examples - Add more details to properties and jsdoc-ify the methods Ref #8203 Closes https://github.com/facebook/react-native/pull/8413 Differential Revision: D3482168 Pulled By: caabernathy fbshipit-source-id: 04fce5133317af282cced5850a53858e3f5b72f2 --- Libraries/Image/Image.ios.js | 139 ++++++++++++++++++++++++----------- 1 file changed, 98 insertions(+), 41 deletions(-) diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index ed78817f89d25f..766d8c21ebf871 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -34,27 +34,72 @@ const ImageViewManager = NativeModules.ImageViewManager; * including network images, static resources, temporary local images, and * images from local disk, such as the camera roll. * - * Example usage: + * This exmaples shows both fetching and displaying an image from local storage as well as on from + * network. * + * ```ReactNativeWebPlayer + * import React, { Component } from 'react'; + * import { AppRegistry, View, Image } from 'react-native'; + * + * class DisplayAnImage extends Component { + * render() { + * return ( + * + * + * + * + * ); + * } + * } + * + * // App registration and rendering + * AppRegistry.registerComponent('DisplayAnImage', () => DisplayAnImage); * ``` - * renderImages: function() { - * return ( - * - * - * - * - * ); - * }, + * + * You can also add `style` to an image: + * + * ```ReactNativeWebPlayer + * import React, { Component } from 'react'; + * import { AppRegistry, View, Image, StyleSheet} from 'react-native'; + * + * const styles = StyleSheet.create({ + * stretch: { + * width: 50, + * height: 200 + * } + * }); + * + *class DisplayAnImageWithStyle extends Component { + * render() { + * return ( + * + * + * + * ); + * } + * } + * + * // App registration and rendering + * AppRegistry.registerComponent( + * 'DisplayAnImageWithStyle', + * () => DisplayAnImageWithStyle + * ); * ``` */ const Image = React.createClass({ propTypes: { + /** + * > `ImageResizeMode` is an `Enum` for different image resizing modes, set via the + * > `resizeMode` style property on `Image` components. The values are `contain`, `cover`, + * > `stretch`, `center`, `repeat`. + */ style: StyleSheetPropType(ImageStylePropTypes), /** * The image source (either a remote URL or a local file resource). @@ -62,29 +107,26 @@ const Image = React.createClass({ source: ImageSourcePropType, /** * A static image to display while loading the image source. + * + * - `uri` - a string representing the resource identifier for the image, which + * should be either a local file path or the name of a static image resource + * (which should be wrapped in the `require('./path/to/image.png')` function). + * - `width`, `height` - can be specified if known at build time, in which case + * these will be used to set the default `` component dimensions. + * - `scale` - used to indicate the scale factor of the image. Defaults to 1.0 if + * unspecified, meaning that one image pixel equates to one display point / DIP. + * - `number` - Opaque type returned by something like `require('./image.jpg')`. + * * @platform ios */ defaultSource: PropTypes.oneOfType([ + // TODO: Tooling to support documenting these directly and having them display in the docs. PropTypes.shape({ - /** - * `uri` is a string representing the resource identifier for the image, which - * should be either a local file path or the name of a static image resource - * (which should be wrapped in the `require('./path/to/image.png')` function). - */ uri: PropTypes.string, - /** - * `width` and `height` can be specified if known at build time, in which case - * these will be used to set the default `` component dimensions. - */ width: PropTypes.number, height: PropTypes.number, - /** - * `scale` is used to indicate the scale factor of the image. Defaults to 1.0 if - * unspecified, meaning that one image pixel equates to one display point / DIP. - */ scale: PropTypes.number, }), - // Opaque type returned by require('./image.jpg') PropTypes.number, ]), /** @@ -105,10 +147,11 @@ const Image = React.createClass({ blurRadius: PropTypes.number, /** * When the image is resized, the corners of the size specified - * by capInsets will stay a fixed size, but the center content and borders + * by `capInsets` will stay a fixed size, but the center content and borders * of the image will be stretched. This is useful for creating resizable - * rounded buttons, shadows, and other resizable assets. More info on - * [Apple documentation](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIImage_Class/index.html#//apple_ref/occ/instm/UIImage/resizableImageWithCapInsets) + * rounded buttons, shadows, and other resizable assets. More info in the + * [official Apple documentation](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIImage_Class/index.html#//apple_ref/occ/instm/UIImage/resizableImageWithCapInsets). + * * @platform ios */ capInsets: EdgeInsetsPropType, @@ -116,18 +159,18 @@ const Image = React.createClass({ * Determines how to resize the image when the frame doesn't match the raw * image dimensions. * - * 'cover': Scale the image uniformly (maintain the image's aspect ratio) + * - `cover`: Scale the image uniformly (maintain the image's aspect ratio) * so that both dimensions (width and height) of the image will be equal * to or larger than the corresponding dimension of the view (minus padding). * - * 'contain': Scale the image uniformly (maintain the image's aspect ratio) + * - `contain`: Scale the image uniformly (maintain the image's aspect ratio) * so that both dimensions (width and height) of the image will be equal to * or less than the corresponding dimension of the view (minus padding). * - * 'stretch': Scale width and height independently, This may change the + * - `stretch`: Scale width and height independently, This may change the * aspect ratio of the src. * - * 'repeat': Repeat the image to cover the frame of the view. The + * - `repeat`: Repeat the image to cover the frame of the view. The * image will keep it's size and aspect ratio. (iOS only) */ resizeMode: PropTypes.oneOf(['cover', 'contain', 'stretch', 'repeat']), @@ -142,25 +185,27 @@ const Image = React.createClass({ */ onLayout: PropTypes.func, /** - * Invoked on load start + * Invoked on load start. + * + * e.g., `onLoadStart={(e) => this.setState({loading: true})}` */ onLoadStart: PropTypes.func, /** - * Invoked on download progress with `{nativeEvent: {loaded, total}}` + * Invoked on download progress with `{nativeEvent: {loaded, total}}`. * @platform ios */ onProgress: PropTypes.func, /** - * Invoked on load error with `{nativeEvent: {error}}` + * Invoked on load error with `{nativeEvent: {error}}`. * @platform ios */ onError: PropTypes.func, /** - * Invoked when load completes successfully + * Invoked when load completes successfully. */ onLoad: PropTypes.func, /** - * Invoked when load either succeeds or fails + * Invoked when load either succeeds or fails. */ onLoadEnd: PropTypes.func, }, @@ -178,6 +223,14 @@ const Image = React.createClass({ * does not fully load/download the image data. A proper, supported way to * preload images will be provided as a separate API. * + * @param uri The location of the image. + * @param success The function that will be called if the image was sucessfully found and width + * and height retrieved. + * @param failure The function that will be called if there was an error, such as failing to + * to retrieve the image. + * + * @returns void + * * @platform ios */ getSize: function( @@ -192,6 +245,10 @@ const Image = React.createClass({ /** * Prefetches a remote image for later use by downloading it to the disk * cache + * + * @param url The remote location of the image. + * + * @return The prefetched image. */ prefetch(url: string) { return ImageViewManager.prefetchImage(url); From 9dd37f6c077cd8fa554fbdeac676e21c008e9d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Fri, 24 Jun 2016 10:47:20 -0700 Subject: [PATCH 527/843] Replace NavigatorComparison with the new Navigation guide. Summary: Several external sites link back to docs/navigator-comparison.html when talking about React Native's navigation. The Navigation guide added in #8390 is meant to replace this content, but it was added at docs/navigation.html. This pull request removes the comparison guide and replaces it with the Navigation guide's content. There is no content update in this PR. For review purposes, note that the next link from the previous document (JS Environment) has been updated to point to navigator-comparison, and the content of the Navigation guide remain unchanged from #8390. Closes https://github.com/facebook/react-native/pull/8417 Differential Revision: D3482273 Pulled By: caabernathy fbshipit-source-id: 9e04e11a5829d48541f8612fb65c01fe319e768b --- docs/JavaScriptEnvironment.md | 2 +- docs/Navigation.md | 165 ------------------------------ docs/NavigatorComparison.md | 184 +++++++++++++++++++++++++++------- 3 files changed, 151 insertions(+), 200 deletions(-) delete mode 100644 docs/Navigation.md diff --git a/docs/JavaScriptEnvironment.md b/docs/JavaScriptEnvironment.md index 857a3fd71a7de9..4e2e3c1f21d579 100644 --- a/docs/JavaScriptEnvironment.md +++ b/docs/JavaScriptEnvironment.md @@ -4,7 +4,7 @@ title: JavaScript Environment layout: docs category: Guides permalink: docs/javascript-environment.html -next: navigation +next: navigator-comparison --- ## JavaScript Runtime diff --git a/docs/Navigation.md b/docs/Navigation.md deleted file mode 100644 index f4a8fcab9880d1..00000000000000 --- a/docs/Navigation.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -id: navigation -title: Navigation -layout: docs -category: Guides -permalink: docs/navigation.html -next: navigator-comparison ---- - -Mobile apps rarely consist of just one screen or scene. As soon as you add a second scene to your app, you will have to take into consideration how the user will navigate from one scene to the other. - -Navigators in React Native allow you to push and pop scenes in a master/detail stack, or to pop up modal scenes. Navigators handle the transitions between scenes, and also maintain the navigational state of your application. - -If you are just getting started with React Native, you will probably want to start with the `Navigator` component. - -## Navigator - -`Navigator` is a cross-platform implementation of a navigation stack, so it works on both iOS and Android. It is easy to customize and includes simple navigation bars. - -```js - { - // We'll get to this function soon. - }} -/> -``` - -Something you will encounter a lot when dealing with navigation is the concept of routes. A route is an object that contains information about a scene. It is used to provide all the context the `renderScene` function needs to render a scene. - -The `push` and `pop` functions provided by Navigator can be used to push and pop routes into the navigation stack. A more complete example that demonstrates the pushing and popping of routes could therefore look something like this: - -```js -class MyScene extends Component { - static propTypes = { - title: PropTypes.string.isRequired, - onForward: PropTypes.func.isRequired, - onBack: PropTypes.func.isRequired, - } - render() { - return ( - - Current Scene: { this.props.title } - - Tap me to load the next scene - - - Tap me to go back - - - ) - } -} - -class SimpleNavigationApp extends Component { - render() { - return ( - - { - const nextIndex = route.index + 1; - navigator.push({ - title: 'Scene ' + nextIndex, - index: nextIndex, - }); - }} - onBack={() => { - if (route.index > 0) { - navigator.pop(); - } - }} - /> - } - /> - ) - } -} -``` - -In this example, the `MyScene` component is passed the title of the current route via the `title` prop. It displays two tappable components that call the `onForward` and `onBack` functions passed through its props, which in turn will call `navigator.push()` and `navigator.pop()` as needed. - -While this is a very basic example, it can easily be adapted to render an entirely different component based on the route that is passed to the `renderScene` function. Navigator will push new scenes from the right by default, and you can control this behavior by using the `configureScene` function. Check out the [Navigator API reference](docs/navigator.html) to learn more. - -## NavigatorIOS - -If you are targeting iOS only, you may also want to consider using `NavigatorIOS`. It looks and feels just like `UINavigationController`, because it is actually built on top of it. - -```js - -``` - -Just like Navigator, it it uses routes to represent scenes, with some important differences. The actual component that will be rendered can be specified using the `component` key in the route, and any props that should be passed to this component can be specified in `passProps`. A navigator object is automatically passed as a prop to the component, allowing you to call `push` and `pop` as needed. - -Check out the [NavigatorIOS reference docs](docs/navigatorios.html) to learn more about this component. - -```js -class MyScene extends Component { - static propTypes = { - title: PropTypes.string.isRequired, - navigator: PropTypes.object.isRequired, - } - - constructor(props, context) { - super(props, context); - this._onForward = this._onForward.bind(this); - this._onBack = this._onBack.bind(this); - } - - _onForward() { - this.props.navigator.push({ - title: 'Scene ' + nextIndex, - }); - } - - _onBack() { - this.props.navigator.pop(); - } - - render() { - return ( - - Current Scene: { this.props.title } - - Tap me to load the next scene - - - Tap me to go back - - - ) - } -} - -class NavigatorIOSApp extends Component { - render() { - return ( - - - } - /> - ) - } -} -``` - -> You may also want to check out [react-native-navigation](https://github.com/wix/react-native-navigation), a component that aims to provide native navigation on both iOS and Android. - -## Navigation (Experimental) - -If you are looking for a more powerful navigation API, check out [NavigationExperimental](https://github.com/facebook/react-native/tree/master/Examples/UIExplorer/NavigationExperimental). It provides greater customization over your transitions, uses single-directional data flow using reducers to manipulate state at a top-level object, and offloads transition animations to the GPU. diff --git a/docs/NavigatorComparison.md b/docs/NavigatorComparison.md index a6ab1c904dd31b..06a93e8be03be8 100644 --- a/docs/NavigatorComparison.md +++ b/docs/NavigatorComparison.md @@ -1,49 +1,165 @@ --- id: navigator-comparison -title: Navigator Comparison +title: Navigation layout: docs category: Guides permalink: docs/navigator-comparison.html next: performance --- -The differences between [Navigator](docs/navigator.html) -and [NavigatorIOS](docs/navigatorios.html) are a common -source of confusion for newcomers. - -Both `Navigator` and `NavigatorIOS` are components that allow you to -manage the navigation in your app between various "scenes" (another word -for screens). They manage a route stack and allow you to pop, push, and -replace states. In this way, [they are similar to the html5 history -API](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history). -The primary distinction between the two is that `NavigatorIOS` leverages -the iOS -[UINavigationController](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UINavigationController_Class/) -class, and `Navigator` re-implements that functionality entirely in -JavaScript as a React component. A corollary of this is that `Navigator` -will be compatible with Android and iOS, whereas `NavigatorIOS` will -only work on the one platform. Below is an itemized list of differences -between the two. +Mobile apps rarely consist of just one screen or scene. As soon as you add a second scene to your app, you will have to take into consideration how the user will navigate from one scene to the other. + +Navigators in React Native allow you to push and pop scenes in a master/detail stack, or to pop up modal scenes. Navigators handle the transitions between scenes, and also maintain the navigational state of your application. + +If you are just getting started with React Native, you will probably want to start with the `Navigator` component. ## Navigator -- Extensive API makes it completely customizable from JavaScript. -- Under active development from the React Native team. -- Written in JavaScript. -- Works on iOS and Android. -- Includes a simple navigation bar component similar to the default `NavigatorIOS` bar: `Navigator.NavigationBar`, and another with breadcrumbs called `Navigator.BreadcrumbNavigationBar`. See the UIExplorer demo to try them out and see how to use them. - - Currently animations are good and improving, but they are still less refined than Apple's, which you get from `NavigatorIOS`. -- You can provide your own navigation bar by passing it through the `navigationBar` prop. +`Navigator` is a cross-platform implementation of a navigation stack, so it works on both iOS and Android. It is easy to customize and includes simple navigation bars. + +```js + { + // We'll get to this function soon. + }} +/> +``` + +Something you will encounter a lot when dealing with navigation is the concept of routes. A route is an object that contains information about a scene. It is used to provide all the context the `renderScene` function needs to render a scene. + +The `push` and `pop` functions provided by Navigator can be used to push and pop routes into the navigation stack. A more complete example that demonstrates the pushing and popping of routes could therefore look something like this: + +```js +class MyScene extends Component { + static propTypes = { + title: PropTypes.string.isRequired, + onForward: PropTypes.func.isRequired, + onBack: PropTypes.func.isRequired, + } + render() { + return ( + + Current Scene: { this.props.title } + + Tap me to load the next scene + + + Tap me to go back + + + ) + } +} + +class SimpleNavigationApp extends Component { + render() { + return ( + + { + const nextIndex = route.index + 1; + navigator.push({ + title: 'Scene ' + nextIndex, + index: nextIndex, + }); + }} + onBack={() => { + if (route.index > 0) { + navigator.pop(); + } + }} + /> + } + /> + ) + } +} +``` + +In this example, the `MyScene` component is passed the title of the current route via the `title` prop. It displays two tappable components that call the `onForward` and `onBack` functions passed through its props, which in turn will call `navigator.push()` and `navigator.pop()` as needed. + +While this is a very basic example, it can easily be adapted to render an entirely different component based on the route that is passed to the `renderScene` function. Navigator will push new scenes from the right by default, and you can control this behavior by using the `configureScene` function. Check out the [Navigator API reference](docs/navigator.html) to learn more. ## NavigatorIOS -- Small, limited API makes it much less customizable than `Navigator` in its current form. -- Development belongs to open-source community - not used by the React Native team on their apps. - - A result of this is that there is currently a backlog of unresolved bugs, nobody who uses this has stepped up to take ownership for it yet. - - You may find an alternative in the community project [react-native-navigation](https://github.com/wix/react-native-navigation) which replaces `NavigatorIOS`. -- Wraps UIKit, so it works exactly the same as it would on another native app. Lives in Objective-C and JavaScript. - - Consequently, you get the animations and behavior that Apple has developed. -- iOS only. -- Includes a navigation bar by default; this navigation bar is not a React Native view component and the style can only be slightly modified. +If you are targeting iOS only, you may also want to consider using `NavigatorIOS`. It looks and feels just like `UINavigationController`, because it is actually built on top of it. + +```js + +``` + +Just like Navigator, it it uses routes to represent scenes, with some important differences. The actual component that will be rendered can be specified using the `component` key in the route, and any props that should be passed to this component can be specified in `passProps`. A navigator object is automatically passed as a prop to the component, allowing you to call `push` and `pop` as needed. + +Check out the [NavigatorIOS reference docs](docs/navigatorios.html) to learn more about this component. + +```js +class MyScene extends Component { + static propTypes = { + title: PropTypes.string.isRequired, + navigator: PropTypes.object.isRequired, + } + + constructor(props, context) { + super(props, context); + this._onForward = this._onForward.bind(this); + this._onBack = this._onBack.bind(this); + } + + _onForward() { + this.props.navigator.push({ + title: 'Scene ' + nextIndex, + }); + } + + _onBack() { + this.props.navigator.pop(); + } + + render() { + return ( + + Current Scene: { this.props.title } + + Tap me to load the next scene + + + Tap me to go back + + + ) + } +} + +class NavigatorIOSApp extends Component { + render() { + return ( + + + } + /> + ) + } +} +``` + +> You may also want to check out [react-native-navigation](https://github.com/wix/react-native-navigation), a component that aims to provide native navigation on both iOS and Android. + +## Navigation (Experimental) -For most non-trivial apps, you will want to use `Navigator` - it won't be long before you run into issues when trying to do anything complex with `NavigatorIOS`. +If you are looking for a more powerful navigation API, check out [NavigationExperimental](https://github.com/facebook/react-native/tree/master/Examples/UIExplorer/NavigationExperimental). It provides greater customization over your transitions, uses single-directional data flow using reducers to manipulate state at a top-level object, and offloads transition animations to the GPU. From 64cdc3547c7253114435870621209ad51f3c8e01 Mon Sep 17 00:00:00 2001 From: Kevin Lacker Date: Fri, 24 Jun 2016 11:47:23 -0700 Subject: [PATCH 528/843] Overhaul the Flexbox documentation Summary: Closes https://github.com/facebook/react-native/pull/8395 Differential Revision: D3482652 Pulled By: lacker fbshipit-source-id: 0bf8955341221b74f69ba24dcf5ab332c910a52c --- Libraries/StyleSheet/LayoutPropTypes.js | 273 +++++++++++++++++++++++- docs/Basics-Layout.md | 10 +- website/server/extractDocs.js | 4 +- 3 files changed, 270 insertions(+), 17 deletions(-) diff --git a/Libraries/StyleSheet/LayoutPropTypes.js b/Libraries/StyleSheet/LayoutPropTypes.js index 3d6b7345fbf08c..384c4581bc34b6 100644 --- a/Libraries/StyleSheet/LayoutPropTypes.js +++ b/Libraries/StyleSheet/LayoutPropTypes.js @@ -20,48 +20,252 @@ var ReactPropTypes = require('ReactPropTypes'); * * The implementation in css-layout is slightly different from what the * Flexbox spec defines - for example, we chose more sensible default - * values. Please refer to the css-layout README for details. + * values. Since our layout docs are generated from the comments in this + * file, please keep a brief comment describing each prop type. * * These properties are a subset of our styles that are consumed by the layout * algorithm and affect the positioning and sizing of views. */ var LayoutPropTypes = { + /** `width` sets the width of this component. + * + * It works similarly to `width` in CSS, but in React Native you + * must use logical pixel units, rather than percents, ems, or any of that. + * See http://www.w3schools.com/cssref/pr_dim_width.asp for more details. + */ width: ReactPropTypes.number, + + /** `height` sets the height of this component. + * + * It works similarly to `height` in CSS, but in React Native you + * must use logical pixel units, rather than percents, ems, or any of that. + * See http://www.w3schools.com/cssref/pr_dim_width.asp for more details. + */ height: ReactPropTypes.number, + + /** `top` is the number of logical pixels to offset the top edge of + * this component. + * + * It works similarly to `top` in CSS, but in React Native you must + * use logical pixel units, rather than percents, ems, or any of that. + * + * See https://developer.mozilla.org/en-US/docs/Web/CSS/top + * for more details of how `top` affects layout. + */ top: ReactPropTypes.number, + + /** `left` is the number of logical pixels to offset the left edge of + * this component. + * + * It works similarly to `left` in CSS, but in React Native you must + * use logical pixel units, rather than percents, ems, or any of that. + * + * See https://developer.mozilla.org/en-US/docs/Web/CSS/left + * for more details of how `left` affects layout. + */ left: ReactPropTypes.number, + + /** `right` is the number of logical pixels to offset the right edge of + * this component. + * + * It works similarly to `right` in CSS, but in React Native you must + * use logical pixel units, rather than percents, ems, or any of that. + * + * See https://developer.mozilla.org/en-US/docs/Web/CSS/right + * for more details of how `right` affects layout. + */ right: ReactPropTypes.number, + + /** `bottom` is the number of logical pixels to offset the bottom edge of + * this component. + * + * It works similarly to `bottom` in CSS, but in React Native you must + * use logical pixel units, rather than percents, ems, or any of that. + * + * See https://developer.mozilla.org/en-US/docs/Web/CSS/bottom + * for more details of how `top` affects layout. + */ bottom: ReactPropTypes.number, + + /** `minWidth` is the minimum width for this component, in logical pixels. + * + * It works similarly to `min-width` in CSS, but in React Native you + * must use logical pixel units, rather than percents, ems, or any of that. + * + * See http://www.w3schools.com/cssref/pr_dim_min-width.asp + * for more details. + */ minWidth: ReactPropTypes.number, + + /** `maxWidth` is the maximum width for this component, in logical pixels. + * + * It works similarly to `max-width` in CSS, but in React Native you + * must use logical pixel units, rather than percents, ems, or any of that. + * + * See http://www.w3schools.com/cssref/pr_dim_max-width.asp + * for more details. + */ maxWidth: ReactPropTypes.number, + + /** `minHeight` is the minimum height for this component, in logical pixels. + * + * It works similarly to `min-height` in CSS, but in React Native you + * must use logical pixel units, rather than percents, ems, or any of that. + * + * See http://www.w3schools.com/cssref/pr_dim_min-height.asp + * for more details. + */ minHeight: ReactPropTypes.number, + + /** `maxHeight` is the maximum height for this component, in logical pixels. + * + * It works similarly to `max-height` in CSS, but in React Native you + * must use logical pixel units, rather than percents, ems, or any of that. + * + * See http://www.w3schools.com/cssref/pr_dim_max-height.asp + * for more details. + */ maxHeight: ReactPropTypes.number, + + /** Setting `margin` has the same effect as setting each of + * `marginTop`, `marginLeft`, `marginBottom`, and `marginRight`. + */ margin: ReactPropTypes.number, + + /** Setting `marginVertical` has the same effect as setting both + * `marginTop` and `marginBottom`. + */ marginVertical: ReactPropTypes.number, + + /** Setting `marginHorizontal` has the same effect as setting + * both `marginLeft` and `marginRight`. + */ marginHorizontal: ReactPropTypes.number, + + /** `marginTop` works like `margin-top` in CSS. + * See http://www.w3schools.com/cssref/pr_margin-top.asp + * for more details. + */ marginTop: ReactPropTypes.number, + + /** `marginBottom` works like `margin-bottom` in CSS. + * See http://www.w3schools.com/cssref/pr_margin-bottom.asp + * for more details. + */ marginBottom: ReactPropTypes.number, + + /** `marginLeft` works like `margin-left` in CSS. + * See http://www.w3schools.com/cssref/pr_margin-left.asp + * for more details. + */ marginLeft: ReactPropTypes.number, + + /** `marginRight` works like `margin-right` in CSS. + * See http://www.w3schools.com/cssref/pr_margin-right.asp + * for more details. + */ marginRight: ReactPropTypes.number, + + /** `padding` works like `padding` in CSS. + * It's like setting each of `paddingTop`, `paddingBottom`, + * `paddingLeft`, and `paddingRight` to the same thing. + * See http://www.w3schools.com/css/css_padding.asp + * for more details. + */ padding: ReactPropTypes.number, + + /** Setting `paddingVertical` is like setting both of + * `paddingTop` and `paddingBottom`. + */ paddingVertical: ReactPropTypes.number, + + /** Setting `paddingHorizontal` is like setting both of + * `paddingLeft` and `paddingRight`. + */ paddingHorizontal: ReactPropTypes.number, + + /** `paddingTop` works like `padding-top` in CSS. + * See http://www.w3schools.com/cssref/pr_padding-top.asp + * for more details. + */ paddingTop: ReactPropTypes.number, + + /** `paddingBottom` works like `padding-bottom` in CSS. + * See http://www.w3schools.com/cssref/pr_padding-bottom.asp + * for more details. + */ paddingBottom: ReactPropTypes.number, + + /** `paddingLeft` works like `padding-left` in CSS. + * See http://www.w3schools.com/cssref/pr_padding-left.asp + * for more details. + */ paddingLeft: ReactPropTypes.number, + + /** `paddingRight` works like `padding-right` in CSS. + * See http://www.w3schools.com/cssref/pr_padding-right.asp + * for more details. + */ paddingRight: ReactPropTypes.number, + + /** `borderWidth` works like `border-width` in CSS. + * See http://www.w3schools.com/cssref/pr_border-width.asp + * for more details. + */ borderWidth: ReactPropTypes.number, + + /** `borderTopWidth` works like `border-top-width` in CSS. + * See http://www.w3schools.com/cssref/pr_border-top_width.asp + * for more details. + */ borderTopWidth: ReactPropTypes.number, + + /** `borderRightWidth` works like `border-right-width` in CSS. + * See http://www.w3schools.com/cssref/pr_border-right_width.asp + * for more details. + */ borderRightWidth: ReactPropTypes.number, + + /** `borderBottomWidth` works like `border-bottom-width` in CSS. + * See http://www.w3schools.com/cssref/pr_border-bottom_width.asp + * for more details. + */ borderBottomWidth: ReactPropTypes.number, + + /** `borderLeftWidth` works like `border-left-width` in CSS. + * See http://www.w3schools.com/cssref/pr_border-bottom_width.asp + * for more details. + */ borderLeftWidth: ReactPropTypes.number, + /** `position` in React Native is similar to regular CSS, but + * everything is set to `relative` by default, so `absolute` + * positioning is always just relative to the parent. + * + * If you want to position a child using specific numbers of logical + * pixels relative to its parent, set the child to have `absolute` + * position. + * + * If you want to position a child relative to something + * that is not its parent, just don't use styles for that. Use the + * component tree. + * + * See https://github.com/facebook/css-layout + * for more details on how `position` differs between React Native + * and CSS. + */ position: ReactPropTypes.oneOf([ 'absolute', 'relative' ]), - // https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction + /** `flexDirection` controls which directions children of a container go. + * `row` goes left to right, `column` goes top to bottom, and you may + * be able to guess what the other two do. It works like `flex-direction` + * in CSS, except the default is `column`. See + * https://css-tricks.com/almanac/properties/f/flex-direction/ + * for more detail. + */ flexDirection: ReactPropTypes.oneOf([ 'row', 'row-reverse', @@ -69,14 +273,24 @@ var LayoutPropTypes = { 'column-reverse' ]), - // https://developer.mozilla.org/en-US/docs/Web/CSS/flex-wrap + /** `flexWrap` controls whether children can wrap around after they + * hit the end of a flex container. + * It works like `flex-wrap` in CSS. See + * https://css-tricks.com/almanac/properties/f/flex-wrap/ + * for more detail. + */ flexWrap: ReactPropTypes.oneOf([ 'wrap', 'nowrap' ]), - // How to align children in the main direction - // https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content + /** `justifyContent` aligns children in the main direction. + * For example, if children are flowing vertically, `justifyContent` + * controls how they align vertically. + * It works like `justify-content` in CSS. See + * https://css-tricks.com/almanac/properties/j/justify-content/ + * for more detail. + */ justifyContent: ReactPropTypes.oneOf([ 'flex-start', 'flex-end', @@ -85,8 +299,14 @@ var LayoutPropTypes = { 'space-around' ]), - // How to align children in the cross direction - // https://developer.mozilla.org/en-US/docs/Web/CSS/align-items + /** `alignItems` aligns children in the cross direction. + * For example, if children are flowing vertically, `alignItems` + * controls how they align horizontally. + * It works like `align-items` in CSS, except the default value + * is `stretch` instead of `flex-start`. See + * https://css-tricks.com/almanac/properties/a/align-items/ + * for more detail. + */ alignItems: ReactPropTypes.oneOf([ 'flex-start', 'flex-end', @@ -94,8 +314,12 @@ var LayoutPropTypes = { 'stretch' ]), - // How to align the element in the cross direction - // https://developer.mozilla.org/en-US/docs/Web/CSS/align-items + /** `alignSelf` controls how a child aligns in the cross direction, + * overriding the `alignItems` of the parent. It works like `align-self` + * in CSS. See + * https://css-tricks.com/almanac/properties/a/align-self/ + * for more detail. + */ alignSelf: ReactPropTypes.oneOf([ 'auto', 'flex-start', @@ -104,10 +328,37 @@ var LayoutPropTypes = { 'stretch' ]), - // https://developer.mozilla.org/en-US/docs/Web/CSS/flex + /** In React Native `flex` does not work the same way that it does in CSS. + * `flex` is a number rather than a string, and it works + * according to the `css-layout` library + * at https://github.com/facebook/css-layout . + * + * When `flex` is a positive number, it makes the component flexible + * and it will be sized proportional to its flex value. So a + * component with `flex` set to 2 will take twice the space as a + * component with `flex` set to 1. + * + * When `flex` is 0, the component is sized according to `width` + * and `height` and it is inflexible. + * + * When `flex` is -1, the component is normally sized according + * `width` and `height`. However, if there's not enough space, + * the component will shrink to its `minWidth` and `minHeight`. + */ flex: ReactPropTypes.number, - // https://developer.mozilla.org/en-US/docs/Web/CSS/z-index + /** `zIndex` controls which components display on top of others. + * Normally, you don't use `zIndex`. Components render according to + * their order in the document tree, so later components draw over + * earlier ones. `zIndex` may be useful if you have animations or custom + * modal interfaces where you don't want this behavior. + * + * It works like the CSS `z-index` property - components with a larger + * `zIndex` will render on top. Think of the z-direction like it's + * pointing from the phone into your eyeball. See + * https://developer.mozilla.org/en-US/docs/Web/CSS/z-index for + * more detail. + */ zIndex: ReactPropTypes.number, }; diff --git a/docs/Basics-Layout.md b/docs/Basics-Layout.md index 8d2972f4d1da86..fbdd262eeff220 100644 --- a/docs/Basics-Layout.md +++ b/docs/Basics-Layout.md @@ -1,9 +1,9 @@ --- id: basics-layout -title: Layout +title: Layout with Flexbox layout: docs category: The Basics -permalink: docs/basics-layout.html +permalink: docs/flexbox.html next: basics-component-textinput --- @@ -11,7 +11,7 @@ A component can specify the layout of its children using the flexbox algorithm. You will normally use a combination of `flexDirection`, `alignItems`, and `justifyContent` to achieve the right layout. -> Flexbox works the same way in React Native as it does in CSS on the web, with a few exceptions. The most notable one: the defaults are different, with `flexDirection` defaulting to `column` instead of `row`, and `alignItems` defaulting to `stretch` instead of `flex-start`. +> Flexbox works the same way in React Native as it does in CSS on the web, with a few exceptions. The defaults are different, with `flexDirection` defaulting to `column` instead of `row`, and `alignItems` defaulting to `stretch` instead of `flex-start`, and the `flex` parameter only supports a single number. #### Flex Direction @@ -99,6 +99,6 @@ class AlignItemsBasics { AppRegistry.registerComponent('AwesomeProject', () => AlignItemsBasics); ``` -#### API Reference +#### Going Deeper -We've covered the basics, but there are many other styles you may need for layouts. The full list is available [here](./docs/flexbox.html). +We've covered the basics, but there are many other styles you may need for layouts. The full list of props that control layout is documented [here](./docs/layout-props.html). diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index f78aef2eedde57..6930bdb182998e 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -39,7 +39,9 @@ function removeExtName(filepath) { function getNameFromPath(filepath) { filepath = removeExtName(filepath); if (filepath === 'LayoutPropTypes') { - return 'Flexbox'; + return 'Layout Props'; + } else if (filepath == 'ShadowPropTypesIOS') { + return 'Shadow Props'; } else if (filepath === 'TransformPropTypes') { return 'Transforms'; } else if (filepath === 'TabBarItemIOS') { From 91134d16c9c1268de49a5b75a439a1543b5ad82c Mon Sep 17 00:00:00 2001 From: Joel Marcey Date: Fri, 24 Jun 2016 11:53:30 -0700 Subject: [PATCH 529/843] Update TextInput API Summary: - Make the examples runnable (both copy/paste and with the web player) - Add a bit more information in props where needed. Closes https://github.com/facebook/react-native/pull/8392 Differential Revision: D3482747 Pulled By: caabernathy fbshipit-source-id: 8f2d812efc1efb3f14db45b5c054ce0d5c14f5f5 --- Libraries/Components/TextInput/TextInput.js | 212 +++++++++++++------- 1 file changed, 145 insertions(+), 67 deletions(-) diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 2df8d278104d51..505fdea088a8a6 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -57,12 +57,29 @@ type Event = Object; * such as `onSubmitEditing` and `onFocus` that can be subscribed to. A simple * example: * - * ``` - * this.setState({text})} - * value={this.state.text} - * /> + * ```ReactNativeWebPlayer + * import React, { Component } from 'react'; + * import { AppRegistry, TextInput } from 'react-native'; + * + * class UselessTextInput extends Component { + * constructor(props) { + * super(props); + * this.state = { text: 'Useless Placeholder' }; + * } + * + * render() { + * return ( + * this.setState({text})} + * value={this.state.text} + * /> + * ); + * } + * } + * + * // App registration and rendering + * AppRegistry.registerComponent('AwesomeProject', () => UselessTextInput); * ``` * * Note that some props are only available with `multiline={true/false}`. @@ -71,10 +88,55 @@ type Event = Object; * `multiline=false`. To achieve the same effect, you can wrap your `TextInput` * in a `View`: * - * ``` - * - * - * + * ```ReactNativeWebPlayer + * import React, { Component } from 'react'; + * import { AppRegistry, View, TextInput } from 'react-native'; + * + * class UselessTextInput extends Component { + * render() { + * return ( + * + * ); + * } + * } + * + * class UselessTextInputMultiline extends Component { + * constructor(props) { + * super(props); + * this.state = { + * text: 'Useless Multiline Placeholder', + * }; + * } + * + * // If you type something in the text box that is a color, the background will change to that + * // color. + * render() { + * return ( + * + * this.setState({text})} + * value={this.state.text} + * /> + * + * ); + * } + * } + * + * // App registration and rendering + * AppRegistry.registerComponent( + * 'AwesomeProject', + * () => UselessTextInputMultiline + * ); * ``` * * `TextInput` has by default a border at the bottom of its view. This border @@ -94,12 +156,12 @@ const TextInput = React.createClass({ propTypes: { ...View.propTypes, /** - * Can tell TextInput to automatically capitalize certain characters. + * Can tell `TextInput` to automatically capitalize certain characters. * - * - characters: all characters, - * - words: first letter of each word - * - sentences: first letter of each sentence (default) - * - none: don't auto capitalize anything + * - `characters`: all characters. + * - `words`: first letter of each word. + * - `sentences`: first letter of each sentence (*default*). + * - `none`: don't auto capitalize anything. */ autoCapitalize: PropTypes.oneOf([ 'none', @@ -108,16 +170,16 @@ const TextInput = React.createClass({ 'characters', ]), /** - * If false, disables auto-correct. The default value is true. + * If `false`, disables auto-correct. The default value is `true`. */ autoCorrect: PropTypes.bool, /** - * If true, focuses the input on componentDidMount. - * The default value is false. + * If `true`, focuses the input on `componentDidMount`. + * The default value is `false`. */ autoFocus: PropTypes.bool, /** - * If false, text is not editable. The default value is true. + * If `false`, text is not editable. The default value is `true`. */ editable: PropTypes.bool, /** @@ -125,9 +187,9 @@ const TextInput = React.createClass({ * * The following values work across platforms: * - * - default - * - numeric - * - email-address + * - `default` + * - `numeric` + * - `email-address` */ keyboardType: PropTypes.oneOf([ // Cross-platform @@ -158,27 +220,33 @@ const TextInput = React.createClass({ * Determines how the return key should look. On Android you can also use * `returnKeyLabel`. * + * *Cross platform* + * * The following values work across platforms: * - * - done - * - go - * - next - * - search - * - send + * - `done` + * - `go` + * - `next` + * - `search` + * - `send` + * + * *Android Only* * * The following values work on Android only: * - * - none - * - previous + * - `none` + * - `previous` + * + * *iOS Only* * * The following values work on iOS only: * - * - default - * - emergency-call - * - google - * - join - * - route - * - yahoo + * - `default` + * - `emergency-call` + * - `google` + * - `join` + * - `route` + * - `yahoo` */ returnKeyType: PropTypes.oneOf([ // Cross-platform @@ -209,28 +277,28 @@ const TextInput = React.createClass({ */ maxLength: PropTypes.number, /** - * Sets the number of lines for a TextInput. Use it with multiline set to - * true to be able to fill the lines. + * Sets the number of lines for a `TextInput`. Use it with multiline set to + * `true` to be able to fill the lines. * @platform android */ numberOfLines: PropTypes.number, /** - * If true, the keyboard disables the return key when there is no text and - * automatically enables it when there is text. The default value is false. + * If `true`, the keyboard disables the return key when there is no text and + * automatically enables it when there is text. The default value is `false`. * @platform ios */ enablesReturnKeyAutomatically: PropTypes.bool, /** - * If true, the text input can be multiple lines. - * The default value is false. + * If `true`, the text input can be multiple lines. + * The default value is `false`. */ multiline: PropTypes.bool, /** - * Callback that is called when the text input is blurred + * Callback that is called when the text input is blurred. */ onBlur: PropTypes.func, /** - * Callback that is called when the text input is focused + * Callback that is called when the text input is focused. */ onFocus: PropTypes.func, /** @@ -247,18 +315,18 @@ const TextInput = React.createClass({ */ onEndEditing: PropTypes.func, /** - * Callback that is called when the text input selection is changed + * Callback that is called when the text input selection is changed. */ onSelectionChange: PropTypes.func, /** * Callback that is called when the text input's submit button is pressed. - * Invalid if multiline={true} is specified. + * Invalid if `multiline={true}` is specified. */ onSubmitEditing: PropTypes.func, /** * Callback that is called when a key is pressed. * Pressed key value is passed as an argument to the callback handler. - * Fires before onChange callbacks. + * Fires before `onChange` callbacks. * @platform ios */ onKeyPress: PropTypes.func, @@ -267,32 +335,42 @@ const TextInput = React.createClass({ */ onLayout: PropTypes.func, /** - * The string that will be rendered before text input has been entered + * The string that will be rendered before text input has been entered. */ placeholder: PropTypes.string, /** - * The text color of the placeholder string + * The text color of the placeholder string. */ placeholderTextColor: ColorPropType, /** - * If true, the text input obscures the text entered so that sensitive text - * like passwords stay secure. The default value is false. + * If `true`, the text input obscures the text entered so that sensitive text + * like passwords stay secure. The default value is `false`. */ secureTextEntry: PropTypes.bool, /** - * The highlight (and cursor on ios) color of the text input + * The highlight (and cursor on iOS) color of the text input. */ selectionColor: ColorPropType, /** - * See DocumentSelectionState.js, some state that is responsible for - * maintaining selection information for a document + * An instance of `DocumentSelectionState`, this is some state that is responsible for + * maintaining selection information for a document. + * + * Some functionality that can be performed with this instance is: + * + * - `blur()` + * - `focus()` + * - `update()` + * + * > You can refernce `DocumentSelectionState` in + * > [`vendor/document/selection/DocumentSelectionState.js`](https://github.com/facebook/react-native/blob/master/Libraries/vendor/document/selection/DocumentSelectionState.js) + * * @platform ios */ selectionState: PropTypes.instanceOf(DocumentSelectionState), /** - * The value to show for the text input. TextInput is a controlled + * The value to show for the text input. `TextInput` is a controlled * component, which means the native value will be forced to match this - * value prop if provided. For most uses this works great, but in some + * value prop if provided. For most uses, this works great, but in some * cases this may cause flickering - one common cause is preventing edits * by keeping value the same. In addition to simply setting the same value, * either set `editable={false}`, or set/update `maxLength` to prevent @@ -301,12 +379,12 @@ const TextInput = React.createClass({ value: PropTypes.string, /** * Provides an initial value that will change when the user starts typing. - * Useful for simple use-cases where you don't want to deal with listening + * Useful for simple use-cases where you do not want to deal with listening * to events and updating the value prop to keep the controlled state in sync. */ defaultValue: PropTypes.string, /** - * When the clear button should appear on the right side of the text view + * When the clear button should appear on the right side of the text view. * @platform ios */ clearButtonMode: PropTypes.oneOf([ @@ -316,28 +394,28 @@ const TextInput = React.createClass({ 'always', ]), /** - * If true, clears the text field automatically when editing begins + * If `true`, clears the text field automatically when editing begins. * @platform ios */ clearTextOnFocus: PropTypes.bool, /** - * If true, all text will automatically be selected on focus + * If `true`, all text will automatically be selected on focus. */ selectTextOnFocus: PropTypes.bool, /** - * If true, the text field will blur when submitted. + * If `true`, the text field will blur when submitted. * The default value is true for single-line fields and false for - * multiline fields. Note that for multiline fields, setting blurOnSubmit - * to true means that pressing return will blur the field and trigger the - * onSubmitEditing event instead of inserting a newline into the field. + * multiline fields. Note that for multiline fields, setting `blurOnSubmit` + * to `true` means that pressing return will blur the field and trigger the + * `onSubmitEditin`g event instead of inserting a newline into the field. */ blurOnSubmit: PropTypes.bool, /** - * Styles + * [Styles](/react-native/docs/style.html) */ style: Text.propTypes.style, /** - * The color of the textInput underline. + * The color of the `TextInput` underline. * @platform android */ underlineColorAndroid: ColorPropType, @@ -357,7 +435,7 @@ const TextInput = React.createClass({ {})) : Object), /** - * Returns if the input is currently focused. + * Returns `true` if the input is currently focused; `false` otherwise. */ isFocused: function(): boolean { return TextInputState.currentlyFocusedField() === @@ -411,7 +489,7 @@ const TextInput = React.createClass({ }, /** - * Removes all text from the input. + * Removes all text from the `TextInput`. */ clear: function() { this.setNativeProps({text: ''}); From f7eca44046e47abb1ad465e1a26d79263a2d462c Mon Sep 17 00:00:00 2001 From: Franklyn Tackitt Date: Fri, 24 Jun 2016 13:25:15 -0700 Subject: [PATCH 530/843] Reverted commit D3443980 Summary: Currently, DevTools only work under ios (although this is undocumented!), because the JavaScriptEngine initialization process skips setupDevTools() on android. DevTools work fine with Android, as tested on 0.26, 0.27, and 0.28 using Nuclide's inspector. For reference, [the relevant issue on react-devtools](https://github.com/facebook/react-devtools/issues/229). Closes https://github.com/facebook/react-native/pull/8095 Reviewed By: javache Differential Revision: D3443980 Pulled By: bestander fbshipit-source-id: ce0f7dd62ae0f7dfe6654380821660f8660318a6 --- .../InitializeJavaScriptAppEngine.js | 2 +- .../react/testing/FakeWebSocketModule.java | 60 ------------------- .../testing/ReactInstanceSpecForTest.java | 4 +- ...alystNativeJSToJavaParametersTestCase.java | 6 +- ...talystNativeJavaToJSArgumentsTestCase.java | 2 - .../react/tests/InitialPropsTestCase.java | 2 - .../facebook/react/tests/JSLocaleTest.java | 5 +- .../react/tests/ProgressBarTestCase.java | 2 - .../react/tests/ViewRenderingTestCase.java | 2 - 9 files changed, 8 insertions(+), 77 deletions(-) delete mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/testing/FakeWebSocketModule.java diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index c83c52047a4a99..ba5fa68fc9bdf4 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -220,7 +220,7 @@ function setUpCollections(): void { function setUpDevTools(): void { if (__DEV__) { // not when debugging in chrome - if (!window.document) { + if (!window.document && require('Platform').OS === 'ios') { const setupDevtools = require('setupDevtools'); setupDevtools(); } diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/FakeWebSocketModule.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/FakeWebSocketModule.java deleted file mode 100644 index 76c9edb2345a93..00000000000000 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/FakeWebSocketModule.java +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2014-present, Facebook, Inc. - * All rights reserved. - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -package com.facebook.react.testing; - -import javax.annotation.Nullable; - -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.BaseJavaModule; -import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.WritableMap; - -/** - * Dummy implementation of storage module, used for testing - */ -public final class FakeWebSocketModule extends BaseJavaModule { - - private static WritableMap errorMessage; - static { - errorMessage = Arguments.createMap(); - errorMessage.putString("message", "Fake Fake Web Socke tModule"); - } - - @Override - public String getName() { - return "WebSocketModule"; - } - - @Override - public boolean canOverrideExistingModule() { - return true; - } - - @ReactMethod - public void connect( - final String url, - @Nullable final ReadableArray protocols, - @Nullable final ReadableMap headers, - final int id) { - } - - @ReactMethod - public void close(int code, String reason, int id) { - } - - @ReactMethod - public void send(String message, int id) { - } - - @ReactMethod - public void sendBinary(String base64String, int id) { - } -} diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactInstanceSpecForTest.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactInstanceSpecForTest.java index bf179010b18312..b9de7a1e7726fa 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactInstanceSpecForTest.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactInstanceSpecForTest.java @@ -9,7 +9,6 @@ package com.facebook.react.testing; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import android.annotation.SuppressLint; @@ -27,8 +26,7 @@ @SuppressLint("JavatestsIncorrectFolder") public class ReactInstanceSpecForTest { - private final List mNativeModules = - new ArrayList(Arrays.asList(new FakeWebSocketModule())); + private final List mNativeModules = new ArrayList<>(); private final List> mJSModuleSpecs = new ArrayList<>(); private final List mViewManagers = new ArrayList<>(); private ReactPackage mReactPackage = null; diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java index 06ff91a0cfa626..f6130be3aaec7f 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJSToJavaParametersTestCase.java @@ -30,7 +30,6 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.modules.systeminfo.AndroidInfoModule; -import com.facebook.react.testing.FakeWebSocketModule; import com.facebook.react.testing.ReactIntegrationTestCase; import com.facebook.react.testing.ReactTestHelper; import com.facebook.react.uimanager.UIImplementation; @@ -38,6 +37,8 @@ import com.facebook.react.uimanager.ViewManager; import com.facebook.react.views.view.ReactViewManager; +import org.junit.Ignore; + /** * Integration test to verify passing various types of parameters from JS to Java works */ @@ -73,7 +74,7 @@ private interface TestJSToJavaParametersModule extends JavaScriptModule { @Override protected void setUp() throws Exception { super.setUp(); - + List viewManagers = Arrays.asList( new ReactViewManager()); final UIManagerModule mUIManager = new UIManagerModule( @@ -93,7 +94,6 @@ public void run() { mCatalystInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(mRecordingTestModule) .addNativeModule(new AndroidInfoModule()) - .addNativeModule(new FakeWebSocketModule()) .addNativeModule(mUIManager) .addJSModule(TestJSToJavaParametersModule.class) .build(); diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java index 587f99121860ea..fd6b1d779b2d68 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystNativeJavaToJSArgumentsTestCase.java @@ -20,7 +20,6 @@ import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.testing.AssertModule; -import com.facebook.react.testing.FakeWebSocketModule; import com.facebook.react.testing.ReactIntegrationTestCase; import com.facebook.react.testing.ReactTestHelper; import com.facebook.react.uimanager.UIImplementation; @@ -76,7 +75,6 @@ public void run() { mInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(mAssertModule) - .addNativeModule(new FakeWebSocketModule()) .addJSModule(TestJavaToJSArgumentsModule.class) .addNativeModule(mUIManager) .build(); diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/InitialPropsTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/InitialPropsTestCase.java index 951c593533e453..6f532e15ce411c 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/InitialPropsTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/InitialPropsTestCase.java @@ -12,7 +12,6 @@ import android.test.ActivityInstrumentationTestCase2; import com.facebook.react.bridge.BaseJavaModule; -import com.facebook.react.testing.FakeWebSocketModule; import com.facebook.react.testing.ReactInstanceSpecForTest; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; @@ -71,7 +70,6 @@ public void testInitialProps() throws Throwable { @Override public void run() { ReactInstanceSpecForTest catalystInstanceSpec = new ReactInstanceSpecForTest(); - catalystInstanceSpec.addNativeModule(new FakeWebSocketModule()); catalystInstanceSpec.addNativeModule(mRecordingModule); Bundle props = new Bundle(); props.putString("key1", "string"); diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/JSLocaleTest.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/JSLocaleTest.java index d20b72fb47c613..370f353c322cf9 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/JSLocaleTest.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/JSLocaleTest.java @@ -11,7 +11,6 @@ import java.util.Arrays; import java.util.List; -import com.facebook.react.testing.FakeWebSocketModule; import com.facebook.react.testing.ReactIntegrationTestCase; import com.facebook.react.testing.ReactTestHelper; import com.facebook.react.testing.StringRecordingModule; @@ -60,9 +59,9 @@ public void run() { mInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(mStringRecordingModule) .addNativeModule(mUIManager) - .addNativeModule(new FakeWebSocketModule()) .addJSModule(TestJSLocaleModule.class) .build(); + } public void testToUpper() { @@ -101,4 +100,6 @@ public void testToLower() { assertEquals("γαζίες καὶ μυρτιὲς δὲν θὰ βρῶ πιὰ στὸ χρυσαφὶ ξέφωτο", answers[3]); assertEquals("chinese: 幓 厏吪吙 鈊釿閍 碞碠粻 曮禷", answers[4]); } + + } diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java index 0886f20fa4b0f5..b3282629bd11ce 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ProgressBarTestCase.java @@ -30,7 +30,6 @@ import com.facebook.react.uimanager.ViewManager; import com.facebook.react.views.progressbar.ReactProgressBarViewManager; import com.facebook.react.views.view.ReactViewManager; -import com.facebook.react.testing.FakeWebSocketModule; import com.facebook.react.testing.ReactIntegrationTestCase; import com.facebook.react.testing.ReactTestHelper; @@ -84,7 +83,6 @@ public void run() { mInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(mUIManager) .addNativeModule(new AndroidInfoModule()) - .addNativeModule(new FakeWebSocketModule()) .addJSModule(ProgressBarTestModule.class) .build(); diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java index 38fff46acd7dc7..f4f817c2b30482 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ViewRenderingTestCase.java @@ -26,7 +26,6 @@ import com.facebook.react.uimanager.ViewManager; import com.facebook.react.views.view.ReactViewGroup; import com.facebook.react.views.view.ReactViewManager; -import com.facebook.react.testing.FakeWebSocketModule; import com.facebook.react.testing.ReactIntegrationTestCase; import com.facebook.react.testing.ReactTestHelper; @@ -65,7 +64,6 @@ public void run() { mCatalystInstance = ReactTestHelper.catalystInstanceBuilder(this) .addNativeModule(uiManager) .addNativeModule(new AndroidInfoModule()) - .addNativeModule(new FakeWebSocketModule()) .addJSModule(ViewRenderingTestModule.class) .build(); From 614f3c68e63c07cb2f9980a785d9f8f302d9251d Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Fri, 24 Jun 2016 15:23:57 -0700 Subject: [PATCH 531/843] Add `NavigationStateUtils.back()` and `NavigationStateUtils.forward()`. Summary: This makes it easy to build Pager. Reviewed By: ericvicenti Differential Revision: D3479979 fbshipit-source-id: 71c0536eb190e8ad64feea99e4bda5e2d28801d2 --- .../NavigationStateUtils.js | 20 +++++++++++++++++++ .../__tests__/NavigationStateUtils-test.js | 14 +++++++++++++ 2 files changed, 34 insertions(+) diff --git a/Libraries/NavigationExperimental/NavigationStateUtils.js b/Libraries/NavigationExperimental/NavigationStateUtils.js index 14d5166a2b2070..9fdb30a7002ef5 100644 --- a/Libraries/NavigationExperimental/NavigationStateUtils.js +++ b/Libraries/NavigationExperimental/NavigationStateUtils.js @@ -111,6 +111,24 @@ function jumpTo(state: NavigationState, key: string): NavigationState { return jumpToIndex(state, index); } +/** + * Sets the focused route to the previous route. + */ +function back(state: NavigationState): NavigationState { + const index = state.index - 1; + const route = state.routes[index]; + return route ? jumpToIndex(state, index) : state; +} + +/** + * Sets the focused route to the next route. + */ +function forward(state: NavigationState): NavigationState { + const index = state.index + 1; + const route = state.routes[index]; + return route ? jumpToIndex(state, index) : state; +} + /** * Replace a route by a key. * Note that this moves the index to the positon to where the new route in the @@ -190,6 +208,8 @@ function reset( } const NavigationStateUtils = { + back, + forward, get: get, has, indexOf, diff --git a/Libraries/NavigationExperimental/__tests__/NavigationStateUtils-test.js b/Libraries/NavigationExperimental/__tests__/NavigationStateUtils-test.js index 7b037fff893a58..d71d88800d42a0 100644 --- a/Libraries/NavigationExperimental/__tests__/NavigationStateUtils-test.js +++ b/Libraries/NavigationExperimental/__tests__/NavigationStateUtils-test.js @@ -83,6 +83,20 @@ describe('NavigationStateUtils', () => { expect(() => NavigationStateUtils.jumpTo(state, 'c')).toThrow(); }); + it('move backwards', () => { + const state = {index: 1, routes: [{key: 'a'}, {key: 'b'}]}; + const newState = {index: 0, routes: [{key: 'a'}, {key: 'b'}]}; + expect(NavigationStateUtils.back(state)).toEqual(newState); + expect(NavigationStateUtils.back(newState)).toBe(newState); + }); + + it('move forwards', () => { + const state = {index: 0, routes: [{key: 'a'}, {key: 'b'}]}; + const newState = {index: 1, routes: [{key: 'a'}, {key: 'b'}]}; + expect(NavigationStateUtils.forward(state)).toEqual(newState); + expect(NavigationStateUtils.forward(newState)).toBe(newState); + }); + // Replace it('Replaces by key', () => { const state = {index: 0, routes: [{key: 'a'}, {key: 'b'}]}; From 37e41d2549b3e58f80a5cd075049ac1ddff06033 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Fri, 24 Jun 2016 15:25:20 -0700 Subject: [PATCH 532/843] Fix transtion props `layout` in NavigationTransitioner. Summary: When layout is measure, transtion props should be updated. Reviewed By: ericvicenti Differential Revision: D3479967 fbshipit-source-id: 14bcd96b9691b7ee68689393b4fef51dbd04b69f --- .../NavigationExperimental/NavigationTransitioner.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Libraries/NavigationExperimental/NavigationTransitioner.js b/Libraries/NavigationExperimental/NavigationTransitioner.js index 1734f9b92d8bda..7282922001e980 100644 --- a/Libraries/NavigationExperimental/NavigationTransitioner.js +++ b/Libraries/NavigationExperimental/NavigationTransitioner.js @@ -195,7 +195,13 @@ class NavigationTransitioner extends React.Component { layout.height.setValue(height); layout.width.setValue(width); - this.setState({ layout }); + const nextState = { + ...this.state, + layout, + }; + + this._transitionProps = buildTransitionProps(this.props, nextState); + this.setState(nextState); } _onTransitionEnd(): void { @@ -208,7 +214,6 @@ class NavigationTransitioner extends React.Component { }; this._transitionProps = buildTransitionProps(this.props, nextState); - this.setState(nextState); this.props.onTransitionEnd && this.props.onTransitionEnd( From c58a7aa546112457d3303f96b6c3747b05bea609 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Fri, 24 Jun 2016 16:54:52 -0700 Subject: [PATCH 533/843] Improved android e2e tests stability Summary: Some tweaks to make CircleCI pass Closes https://github.com/facebook/react-native/pull/8425 Differential Revision: D3485087 Pulled By: bestander fbshipit-source-id: b1941ecedbcaf81f0bcb34a10686f21f2ded3114 --- circle.yml | 2 +- scripts/circle-ci-android-setup.sh | 1 - scripts/run-ci-e2e-tests.js | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/circle.yml b/circle.yml index 696b51aaa3d184..ba313223e108f5 100644 --- a/circle.yml +++ b/circle.yml @@ -77,7 +77,7 @@ test: - ./gradlew :ReactAndroid:assembleDebugAndroidTest -Pjobs=1 -Pcom.android.build.threadPoolSize=1 # Android e2e test - - node ./scripts/run-ci-e2e-tests.js --android --js --retries 3 + - source scripts/circle-ci-android-setup.sh && retry3 node ./scripts/run-ci-e2e-tests.js --android --js --retries 2 # testing docs generation is not broken - cd website && node ./server/generate.js diff --git a/scripts/circle-ci-android-setup.sh b/scripts/circle-ci-android-setup.sh index 3920cc0c0732aa..9f6174a84f7fc1 100644 --- a/scripts/circle-ci-android-setup.sh +++ b/scripts/circle-ci-android-setup.sh @@ -38,4 +38,3 @@ function retry3 { } done } - diff --git a/scripts/run-ci-e2e-tests.js b/scripts/run-ci-e2e-tests.js index cf3534b6408d6e..0fcf2bbd0ac389 100644 --- a/scripts/run-ci-e2e-tests.js +++ b/scripts/run-ci-e2e-tests.js @@ -78,7 +78,7 @@ try { exec('sleep 10s'); return exec(`react-native init EndToEndTest --version ${PACKAGE}`).code; }, - numberOfRetries, + numberOfRetries, () => rm('-rf', 'EndToEndTest'))) { echo('Failed to execute react-native init'); echo('Most common reason is npm registry connectivity, try again'); @@ -131,7 +131,7 @@ try { }); SERVER_PID = packagerProcess.pid; // wait a bit to allow packager to startup - exec('sleep 5s'); + exec('sleep 15s'); echo('Executing android e2e test'); if (tryExecNTimes( () => { @@ -202,7 +202,7 @@ try { } } exitCode = 0; - + } finally { cd(ROOT); rm(MARKER_IOS); From f8c486f03c7b0b13d0fe6d61a9221dc379b116e8 Mon Sep 17 00:00:00 2001 From: Nicolas Charpentier Date: Fri, 24 Jun 2016 17:15:41 -0700 Subject: [PATCH 534/843] Add missing character into BUCK android template Summary: Closes https://github.com/facebook/react-native/pull/8362 Differential Revision: D3483454 fbshipit-source-id: e35bfa4002374993787cf8a8440efe1aa1c61e6f --- local-cli/generator-android/templates/src/app/BUCK | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-cli/generator-android/templates/src/app/BUCK b/local-cli/generator-android/templates/src/app/BUCK index 918be8863ca856..30774f5dac9795 100644 --- a/local-cli/generator-android/templates/src/app/BUCK +++ b/local-cli/generator-android/templates/src/app/BUCK @@ -5,7 +5,7 @@ import re # - install Buck # - `npm start` - to start the packager # - `cd android` -# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US` +# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck # - `buck install -r android/app` - compile, install and run application # From d055ab548e3fff69ad7f1363c66ae6e125804b1f Mon Sep 17 00:00:00 2001 From: Chris Hopman Date: Fri, 24 Jun 2016 19:18:31 -0700 Subject: [PATCH 535/843] Fix check for if we are ready to make js calls Reviewed By: mhorowitz Differential Revision: D3485010 fbshipit-source-id: 5a3ce9be6a88f02478fb711fd09c57e4b2ccfc0d --- .../react/cxxbridge/CatalystInstanceImpl.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java index d024e9cda0f172..b23335e05cefa5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/cxxbridge/CatalystInstanceImpl.java @@ -75,6 +75,7 @@ public class CatalystInstanceImpl implements CatalystInstance { private final NativeModuleRegistry mJavaRegistry; private final NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler; private boolean mInitialized = false; + private volatile boolean mAcceptCalls = false; private boolean mJSBundleHasLoaded; @@ -165,6 +166,10 @@ private native void initializeBridge(ReactCallback callback, @Override public void runJSBundle() { + // This should really be done when we post the task that runs the JS bundle + // (don't even need to wait for it to finish). Since that is currently done + // synchronously, marking it here is fine. + mAcceptCalls = true; Assertions.assertCondition(!mJSBundleHasLoaded, "JS bundle was already loaded!"); mJSBundleHasLoaded = true; // incrementPendingJSCalls(); @@ -191,8 +196,8 @@ public void callFunction( FLog.w(ReactConstants.TAG, "Calling JS function after bridge has been destroyed."); return; } - if (!mInitialized) { - throw new RuntimeException("Attempt to call JS function before instance initialization."); + if (!mAcceptCalls) { + throw new RuntimeException("Attempt to call JS function before JS bundle is loaded."); } callJSFunction(executorToken, module, method, arguments, tracingName); @@ -253,6 +258,12 @@ public void initialize() { Assertions.assertCondition( !mInitialized, "This catalyst instance has already been initialized"); + // We assume that the instance manager blocks on running the JS bundle. If + // that changes, then we need to set mAcceptCalls just after posting the + // task that will run the js bundle. + Assertions.assertCondition( + mAcceptCalls, + "RunJSBundle hasn't completed."); mInitialized = true; mJavaRegistry.notifyCatalystInstanceInitialized(); } From 0e07c36794b516ccce60909be0aa48bb0d527baa Mon Sep 17 00:00:00 2001 From: Mengjue Wang Date: Fri, 24 Jun 2016 19:32:54 -0700 Subject: [PATCH 536/843] Provide function that could read app current using language Summary: Provide function that could read the language currently used in the app, so the isRTL is using the app current using language to set isRTL. Reviewed By: fkgozali Differential Revision: D3480654 fbshipit-source-id: dea3ef4769296f594f7f32da2961b4fec1b99a7a --- React/Modules/RCTI18nUtil.m | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/React/Modules/RCTI18nUtil.m b/React/Modules/RCTI18nUtil.m index c66924243db42f..95c8ed735d2c4c 100644 --- a/React/Modules/RCTI18nUtil.m +++ b/React/Modules/RCTI18nUtil.m @@ -22,9 +22,11 @@ + (id)sharedInstance { return sharedRCTI18nUtilInstance; } +// If currnent using language is RTL language and meanwhile set forceRTL on the JS side, +// the RN app will automatically have a RTL layout. - (BOOL)isRTL { - if ([self forceRTL] && [self isDevicePreferredLanguageRTL]) { + if ([self forceRTL] && [self isApplicationPreferredLanguageRTL]) { return YES; } return NO; @@ -43,10 +45,19 @@ - (void)setForceRTL:(BOOL)rtlStatus [[NSUserDefaults standardUserDefaults] synchronize]; } +// Check if the current device language is RTL - (BOOL)isDevicePreferredLanguageRTL { NSLocaleLanguageDirection direction = [NSLocale characterDirectionForLanguage:[[NSLocale preferredLanguages] objectAtIndex:0]]; return direction == NSLocaleLanguageDirectionRightToLeft; } +// Check if the current application language is RTL +- (BOOL)isApplicationPreferredLanguageRTL +{ + NSString *preferredAppLanguage = [[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0]; + NSLocaleLanguageDirection direction = [NSLocale characterDirectionForLanguage:preferredAppLanguage]; + return direction == NSLocaleLanguageDirectionRightToLeft; +} + @end From bfb4c054f4a8916d3deb7ad33c16888b618aee4e Mon Sep 17 00:00:00 2001 From: Kevin Lacker Date: Sun, 26 Jun 2016 12:30:58 -0700 Subject: [PATCH 537/843] Make "The Basics" flow like a linear tutorial Summary: Closes https://github.com/facebook/react-native/pull/8429 Differential Revision: D3487369 Pulled By: lacker fbshipit-source-id: 59b32f2a2a67370192c91dc43da3d4b76a43b810 --- docs/Basics-Component-Image.md | 32 --------- docs/Basics-Component-ScrollView.md | 71 ------------------ docs/Basics-Component-Text.md | 53 -------------- docs/Basics-Component-TextInput.md | 39 ---------- docs/Basics-Component-View.md | 34 --------- docs/Basics-Components.md | 36 ---------- ...rt-GettingStarted.md => GettingStarted.md} | 35 ++++++--- docs/HandlingTextInput.md | 46 ++++++++++++ ...Basics-Dimensions.md => HeightAndWidth.md} | 12 ++-- ...Apps.md => IntegrationWithExistingApps.md} | 0 ...{Basics-Layout.md => LayoutWithFlexbox.md} | 6 +- docs/{Basics-Network.md => Networking.md} | 4 +- docs/Props.md | 72 +++++++++++++++++++ docs/State.md | 59 +++++++++++++++ docs/{Basics-Style.md => Style.md} | 8 ++- docs/Tutorial.md | 53 ++++++++++++++ ...omponent-ListView.md => UsingAListView.md} | 18 ++--- docs/UsingAScrollView.md | 64 +++++++++++++++++ 18 files changed, 349 insertions(+), 293 deletions(-) delete mode 100644 docs/Basics-Component-Image.md delete mode 100644 docs/Basics-Component-ScrollView.md delete mode 100644 docs/Basics-Component-Text.md delete mode 100644 docs/Basics-Component-TextInput.md delete mode 100644 docs/Basics-Component-View.md delete mode 100644 docs/Basics-Components.md rename docs/{QuickStart-GettingStarted.md => GettingStarted.md} (90%) create mode 100644 docs/HandlingTextInput.md rename docs/{Basics-Dimensions.md => HeightAndWidth.md} (88%) rename docs/{Basics-IntegrationWithExistingApps.md => IntegrationWithExistingApps.md} (100%) rename docs/{Basics-Layout.md => LayoutWithFlexbox.md} (93%) rename docs/{Basics-Network.md => Networking.md} (93%) create mode 100644 docs/Props.md create mode 100644 docs/State.md rename docs/{Basics-Style.md => Style.md} (80%) create mode 100644 docs/Tutorial.md rename docs/{Basics-Component-ListView.md => UsingAListView.md} (56%) create mode 100644 docs/UsingAScrollView.md diff --git a/docs/Basics-Component-Image.md b/docs/Basics-Component-Image.md deleted file mode 100644 index 0682fa62aff56d..00000000000000 --- a/docs/Basics-Component-Image.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -id: basics-component-image -title: Image -layout: docs -category: The Basics -permalink: docs/basics-component-image.html -next: basics-component-view ---- - -Another basic React Native component is the [`Image`](/react-native/docs/image.html#content) component. Like `Text`, the `Image` component simply renders an image. - -> An `Image` is analogous to the `` HTML tag when building websites. - -The simplest way to render an image is to provide a source file to that image via the `source` attribute. - -This example displays a checkbox `Image` on the device. - -```ReactNativeWebPlayer -import React, { Component } from 'react'; -import { AppRegistry, Image } from 'react-native'; - -class ImageBasics extends Component { - render() { - return ( - - ); - } -} - -// App registration and rendering -AppRegistry.registerComponent('AwesomeProject', () => ImageBasics); -``` diff --git a/docs/Basics-Component-ScrollView.md b/docs/Basics-Component-ScrollView.md deleted file mode 100644 index 7acdb6404a8b2b..00000000000000 --- a/docs/Basics-Component-ScrollView.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -id: basics-component-scrollview -title: ScrollView -layout: docs -category: The Basics -permalink: docs/basics-component-scrollview.html -next: basics-component-listview ---- - -Given the screen sizes of mobile devices, the ability to scroll through data is generally paramount for a proper usability experience. - -The [`ScrollView`](/react-native/docs/scrollview.html) is a generic scrolling container that can host multiple components and views. The scrollable items need not be homogenous, and you can scroll both vertically and horizontally (by setting the `horizontal` property). - -`ScrollView` works best to present a list of short, static items of a known quantity. All the elements and views of a `ScrollView` are rendered a priori, even if they are not currently shown on the screen. Contrast this with a `ListView`, which render only those views that are on the screen and remove views that go off-screen. - -> [`TextView`](/react-native/docs/basics-component-textview.html) and [`ListView`](/react-native/docs/basics-component-listview.html) are specialized scrollable containers. - -This contrived example creates a horizontal `ScrollView` with a static amount of heterogenous elements (images and text). - -```ReactNativeWebPlayer -import React, { Component } from 'react'; -import{ AppRegistry, ScrollView, Image, Text, View } from 'react-native' - -class ScrollViewBasics extends Component { - render() { - return( - - - - - - - - - - - - Text1 - - - Text2 - - - Text3 - - - Text4 - - - - - - - - - - - - Text5 - - - Text6 - - - ); - } -} - - -AppRegistry.registerComponent('AwesomeProject', () => ScrollViewBasics); -``` diff --git a/docs/Basics-Component-Text.md b/docs/Basics-Component-Text.md deleted file mode 100644 index b2b888ef704194..00000000000000 --- a/docs/Basics-Component-Text.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -id: basics-component-text -title: Text -layout: docs -category: The Basics -permalink: docs/basics-component-text.html -next: basics-component-image ---- - -The most basic component in React Native is the [`Text`](/react-native/docs/text.html#content) component. The `Text` component simply renders text. - -This example displays the `string` `"Hello World!"` on the device. - -```ReactNativeWebPlayer -import React, { Component } from 'react'; -import { AppRegistry, Text } from 'react-native'; - -class TextBasics extends Component { - render() { - return ( - Hello World! - ); - } -} - -// App registration and rendering -AppRegistry.registerComponent('AwesomeProject', () => TextBasics); -``` - -In this slightly more advanced example we will display the `string` `"Hello World"` retrieved from this.state on the device and stored in the `text` variable. The value of the `text` variable is rendered by using `{text}`. - -```ReactNativeWebPlayer -import React, {Component} from 'react'; -import { AppRegistry, Text } from 'react-native'; - -class TextBasicsWithState extends Component { - constructor(props) { - super(props); - this.state = {text: "Hello World"}; - } - render() { - var text = this.state.text; - return ( - - {text} - - ) - } -} - -// App registration and rendering -AppRegistry.registerComponent('AwesomeProject', () => TextBasicsWithState); -``` diff --git a/docs/Basics-Component-TextInput.md b/docs/Basics-Component-TextInput.md deleted file mode 100644 index bee7a574dcd500..00000000000000 --- a/docs/Basics-Component-TextInput.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -id: basics-component-textinput -title: TextInput -layout: docs -category: The Basics -permalink: docs/basics-component-textinput.html -next: basics-component-scrollview ---- - -Direct text-based user input is a foundation for many apps. Writing a post or comment on a page is a canonical example of this. [`TextInput`](/react-native/docs/textinput.html#content) is a basic component that allows the user to enter text. - -This example creates a simple `TextInput` box with the `string` `Type something here` as the placeholder when the `TextInput` is empty. - -```ReactNativeWebPlayer -import React, { Component } from 'react'; -import { AppRegistry, Text, TextInput, View } from 'react-native'; - -class TextInputBasics extends Component { - render() { - return ( - - - - ); - } -} - -// App registration and rendering -AppRegistry.registerComponent('AwesomeProject', () => TextInputBasics); -``` diff --git a/docs/Basics-Component-View.md b/docs/Basics-Component-View.md deleted file mode 100644 index 52b83bdd89ade6..00000000000000 --- a/docs/Basics-Component-View.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -id: basics-component-view -title: View -layout: docs -category: The Basics -permalink: docs/basics-component-view.html -next: style ---- - -A [`View`](/react-native/docs/view.html#content) is the most basic building block for a React Native application. The `View` is an abstraction on top of the target platform's native equivalent, such as iOS's `UIView`. - -> A `View` is analogous to using a `
    ` HTML tag for building websites. - -It is recommended that you wrap your components in a `View` to style and control layout. - -The example below creates a `View` that aligns the `string` `Hello` in the top center of the device, something which could not be done with a `Text` component alone (i.e., a `Text` component without a `View` would place the `string` in a fixed location in the upper corner): - -```ReactNativeWebPlayer -import React, { Component } from 'react'; -import { AppRegistry, Text, View } from 'react-native'; - -class ViewBasics extends Component { - render() { - return ( - - Hello! - - ); - } -} - -// App registration and rendering -AppRegistry.registerComponent('AwesomeProject', () => ViewBasics); -``` diff --git a/docs/Basics-Components.md b/docs/Basics-Components.md deleted file mode 100644 index beeb418cbc77bd..00000000000000 --- a/docs/Basics-Components.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -id: basics-components -title: Components -layout: docs -category: The Basics -permalink: docs/basics-components.html -next: basics-component-text ---- - -Components are the building blocks for a React Native application. A React Native user interface (UI) is specified by declaring components, often nested. Those components are then mapped to the native UI on the targeted platform. - -####Props#### - -#####`this.props`##### - -Components can be configured by passing properties `props` to the constructor. You can access `props` from your component's methods by accessing `this.props`. You should not alter your props within component methods. - -####State#### - -#####`this.state`##### - -Components maintain their state using the state object. You can access your component state via `this.state`. Each component should manage its own state. Parent components should not manage children state and children components should not manage parent component state. - -#####`this.setState({key: value, ...})`##### - -To update or change the state of your component passing a new object representing your newly desired state to `this.setState(obj)`. The specificed keys will be merged into `this.state`. Any existing keys will be overridden by new values. - -## Core Components. - -React Native has a number of core components that are commonly used in applications, either on their own or combined to build new components. - -- [Text](/react-native/docs/basics-component-text.html) -- [Image](/react-native/docs/basics-component-image.html) -- [View](/react-native/docs/basics-component-view.html) -- [TextInput](/react-native/docs/basics-component-textinput.html) -- [ListView](/react-native/docs/basics-component-listview.html) \ No newline at end of file diff --git a/docs/QuickStart-GettingStarted.md b/docs/GettingStarted.md similarity index 90% rename from docs/QuickStart-GettingStarted.md rename to docs/GettingStarted.md index 03863fcb59c43c..2532eff21a6734 100644 --- a/docs/QuickStart-GettingStarted.md +++ b/docs/GettingStarted.md @@ -4,9 +4,17 @@ title: Getting Started layout: docs category: The Basics permalink: docs/getting-started.html -next: basics-components +next: tutorial --- +Welcome to React Native! This page will help you install React Native on +your system, so that you can build apps with it right away. If you already +have React Native installed, you can skip ahead to the +[Tutorial](/react-native/docs/tutorial.html). + +The instructions are a bit different depending on your development operating system, and whether you want to start developing for iOS or Android. If you +want to develop for both iOS and Android, that's fine - you just have to pick +one to start with, since the setup is a bit different.
    -Target: +Mobile OS: iOS Android Development OS: @@ -58,13 +66,13 @@ block { display: none; } -## Dependencies +## Dependencies for Mac + iOS You will need Xcode, node.js, the React Native command line tools, and Watchman. -## Dependencies +## Dependencies for Mac + Android You will need Android Studio, node.js, the React Native command line tools, and Watchman. @@ -98,9 +106,15 @@ If you plan to make changes in Java code, we recommend [Gradle Daemon](https://d - + -## Dependencies +## Dependencies for Linux + Android + + + +## Dependencies for Windows + Android + + You will need node.js, the React Native command line tools, Watchman, and Android Studio. @@ -228,11 +242,14 @@ Congratulations! You've successfully run and modified a React Native app. -## Special Cases +## Now What? + +- If you want to add this new React Native code to an existing application, check out the [Integration guide](docs/integration-with-existing-apps.html). -- This page explains how to create a new React Native app. If you are adding React Native components to an existing application, check out the [Integration guide](docs/integration-with-existing-apps.html). +- If you can't get this to work, see the [Troubleshooting](docs/troubleshooting.html#content) page. -- If you run into any issues getting started, see the [Troubleshooting](docs/troubleshooting.html#content) page. +- If you're curious to learn more about React Native, continue on +to the [Tutorial](docs/tutorial.html).