Updates from Thu, July 23

This commit is contained in:
Alex Kotliarskyi 2015-07-23 13:09:48 -07:00
commit 03dccfbf71
23 changed files with 827 additions and 358 deletions

View File

@ -136,10 +136,8 @@ class GameEndOverlay extends React.Component {
return (
<View style={styles.overlay}>
<Text style={styles.overlayMessage}>{message}</Text>
<TouchableBounce onPress={this.props.onRestart}>
<View style={styles.tryAgain}>
<Text style={styles.tryAgainText}>Try Again?</Text>
</View>
<TouchableBounce onPress={this.props.onRestart} style={styles.tryAgain}>
<Text style={styles.tryAgainText}>Try Again?</Text>
</TouchableBounce>
</View>
);

View File

@ -17,7 +17,7 @@
'use strict';
var React = require('react-native');
var UIExplorerList = require('./UIExplorerList');
var UIExplorerList = require('./UIExplorerList.ios');
var {
AppRegistry,
NavigatorIOS,

View File

@ -0,0 +1,176 @@
/**
* 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-native');
var {
AppRegistry,
Settings,
StyleSheet,
} = React;
var { TestModule } = React.addons;
import type { NavigationContext } from 'NavigationContext';
var UIExplorerListBase = require('./UIExplorerListBase');
var COMPONENTS = [
require('./ActivityIndicatorIOSExample'),
require('./DatePickerIOSExample'),
require('./ImageExample'),
require('./LayoutEventsExample'),
require('./ListViewExample'),
require('./ListViewGridLayoutExample'),
require('./ListViewPagingExample'),
require('./MapViewExample'),
require('./Navigator/NavigatorExample'),
require('./NavigatorIOSColorsExample'),
require('./NavigatorIOSExample'),
require('./PickerIOSExample'),
require('./ProgressViewIOSExample'),
require('./ScrollViewExample'),
require('./SegmentedControlIOSExample'),
require('./SliderIOSExample'),
require('./SwitchIOSExample'),
require('./TabBarIOSExample'),
require('./TextExample.ios'),
require('./TextInputExample'),
require('./TouchableExample'),
require('./ViewExample'),
require('./WebViewExample'),
];
var APIS = [
require('./AccessibilityIOSExample'),
require('./ActionSheetIOSExample'),
require('./AdSupportIOSExample'),
require('./AlertIOSExample'),
require('./AnimationExample/AnExApp'),
require('./AppStateIOSExample'),
require('./AsyncStorageExample'),
require('./BorderExample'),
require('./CameraRollExample.ios'),
require('./GeolocationExample'),
require('./LayoutExample'),
require('./NetInfoExample'),
require('./PanResponderExample'),
require('./PointerEventsExample'),
require('./PushNotificationIOSExample'),
require('./StatusBarIOSExample'),
require('./TimerExample'),
require('./VibrationIOSExample'),
require('./XHRExample'),
];
// Register suitable examples for snapshot tests
COMPONENTS.concat(APIS).forEach((Example) => {
if (Example.displayName) {
var Snapshotter = React.createClass({
componentDidMount: function() {
// View is still blank after first RAF :\
global.requestAnimationFrame(() =>
global.requestAnimationFrame(() => TestModule.verifySnapshot(
TestModule.markTestPassed
)
));
},
render: function() {
var Renderable = UIExplorerListBase.makeRenderable(Example);
return <Renderable />;
},
});
AppRegistry.registerComponent(Example.displayName, () => Snapshotter);
}
});
type Props = {
navigator: {
navigationContext: NavigationContext,
push: (route: {title: string, component: ReactClass<any,any,any>}) => void,
},
onExternalExampleRequested: Function,
};
class UIExplorerList extends React.Component {
props: Props;
render() {
return (
<UIExplorerListBase
components={COMPONENTS}
apis={APIS}
searchText={Settings.get('searchText')}
renderAdditionalView={this.renderAdditionalView.bind(this)}
search={this.search.bind(this)}
onPressRow={this.onPressRow.bind(this)}
/>
);
}
componentWillMount() {
this.props.navigator.navigationContext.addListener('didfocus', function(event) {
if (event.data.route.title === 'UIExplorer') {
Settings.set({visibleExample: null});
}
});
}
componentDidMount() {
var visibleExampleTitle = Settings.get('visibleExample');
if (visibleExampleTitle) {
var predicate = (example) => example.title === visibleExampleTitle;
var foundExample = APIS.find(predicate) || COMPONENTS.find(predicate);
if (foundExample) {
setTimeout(() => this._openExample(foundExample), 100);
}
}
}
renderAdditionalView(renderRow: Function, renderTextInput: Function): React.Component {
return renderTextInput(styles.searchTextInput);
}
search(text: mixed) {
Settings.set({searchText: text});
}
_openExample(example: any) {
if (example.external) {
this.props.onExternalExampleRequested(example);
return;
}
var Component = UIExplorerListBase.makeRenderable(example);
this.props.navigator.push({
title: Component.title,
component: Component,
});
}
onPressRow(example: any) {
Settings.set({visibleExample: example.title});
this._openExample(example);
}
}
var styles = StyleSheet.create({
searchTextInput: {
height: 30,
},
});
module.exports = UIExplorerList;

View File

@ -0,0 +1,195 @@
/**
* 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-native');
var {
ListView,
PixelRatio,
StyleSheet,
Text,
TextInput,
TouchableHighlight,
View,
} = React;
var createExamplePage = require('./createExamplePage');
var ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
sectionHeaderHasChanged: (h1, h2) => h1 !== h2,
});
class UIExplorerListBase extends React.Component {
constructor(props: any) {
super(props);
this.state = {
dataSource: ds.cloneWithRowsAndSections({
components: [],
apis: [],
}),
searchText: this.props.searchText,
};
}
componentDidMount(): void {
this.search(this.state.searchText);
}
render() {
var topView = this.props.renderAdditionalView &&
this.props.renderAdditionalView(this.renderRow.bind(this), this.renderTextInput.bind(this));
return (
<View style={styles.listContainer}>
{topView}
<ListView
style={styles.list}
dataSource={this.state.dataSource}
renderRow={this.renderRow.bind(this)}
renderSectionHeader={this._renderSectionHeader}
keyboardShouldPersistTaps={true}
automaticallyAdjustContentInsets={false}
keyboardDismissMode="on-drag"
/>
</View>
);
}
renderTextInput(searchTextInputStyle: any) {
return (
<View style={styles.searchRow}>
<TextInput
autoCapitalize="none"
autoCorrect={false}
clearButtonMode="always"
onChangeText={this.search.bind(this)}
placeholder="Search..."
style={[styles.searchTextInput, searchTextInputStyle]}
testID="explorer_search"
value={this.state.searchText}
/>
</View>
);
}
_renderSectionHeader(data: any, section: string) {
return (
<View style={styles.sectionHeader}>
<Text style={styles.sectionHeaderTitle}>
{section.toUpperCase()}
</Text>
</View>
);
}
renderRow(example: any, i: number) {
return (
<View key={i}>
<TouchableHighlight onPress={() => this.onPressRow(example)}>
<View style={styles.row}>
<Text style={styles.rowTitleText}>
{example.title}
</Text>
<Text style={styles.rowDetailText}>
{example.description}
</Text>
</View>
</TouchableHighlight>
<View style={styles.separator} />
</View>
);
}
search(text: mixed): void {
this.props.search && this.props.search(text);
var regex = new RegExp(text, 'i');
var filter = (component) => regex.test(component.title);
this.setState({
dataSource: ds.cloneWithRowsAndSections({
components: this.props.components.filter(filter),
apis: this.props.apis.filter(filter),
}),
searchText: text,
});
}
onPressRow(example: any): void {
this.props.onPressRow && this.props.onPressRow(example);
}
static makeRenderable(example: any): ReactClass<any, any, any> {
return example.examples ?
createExamplePage(null, example) :
example;
}
}
var styles = StyleSheet.create({
listContainer: {
flex: 1,
},
list: {
backgroundColor: '#eeeeee',
},
sectionHeader: {
padding: 5,
},
group: {
backgroundColor: 'white',
},
sectionHeaderTitle: {
fontWeight: '500',
fontSize: 11,
},
row: {
backgroundColor: 'white',
justifyContent: 'center',
paddingHorizontal: 15,
paddingVertical: 8,
},
separator: {
height: 1 / PixelRatio.get(),
backgroundColor: '#bbbbbb',
marginLeft: 15,
},
rowTitleText: {
fontSize: 17,
fontWeight: '500',
},
rowDetailText: {
fontSize: 15,
color: '#888888',
lineHeight: 20,
},
searchRow: {
backgroundColor: '#eeeeee',
paddingTop: 75,
paddingLeft: 10,
paddingRight: 10,
paddingBottom: 10,
},
searchTextInput: {
backgroundColor: 'white',
borderColor: '#cccccc',
borderRadius: 3,
borderWidth: 1,
paddingLeft: 8,
},
});
module.exports = UIExplorerListBase;

View File

@ -479,7 +479,13 @@ class SpringAnimation extends Animation {
if (this._tension !== 0) {
isDisplacement = Math.abs(this._toValue - position) <= this._restDisplacementThreshold;
}
if (isOvershooting || (isVelocity && isDisplacement)) {
if (this._tension !== 0) {
// Ensure that we end up with a round value
this._onUpdate(this._toValue);
}
this.__debouncedOnEnd({finished: true});
return;
}

View File

@ -127,6 +127,19 @@ describe('Animated', () => {
Animated.spring(anim, {toValue: 0, velocity: 0}).start(callback);
expect(callback).toBeCalled();
});
it('send toValue when a spring stops', () => {
var anim = new Animated.Value(0);
var listener = jest.genMockFunction();
anim.addListener(listener);
Animated.spring(anim, {toValue: 15}).start();
jest.runAllTimers();
var lastValue = listener.mock.calls[listener.mock.calls.length - 2][0].value;
expect(lastValue).not.toBe(15);
expect(lastValue).toBeCloseTo(15);
expect(anim.__getValue()).toBe(15);
});
});

View File

@ -470,7 +470,11 @@ var ScrollResponderMixin = {
},
scrollResponderKeyboardDidShow: function(e: Event) {
this.keyboardWillOpenTo = null;
// TODO(7693961): The event for DidShow is not available on iOS yet.
// Use the one from WillShow and do not assign.
if (e) {
this.keyboardWillOpenTo = e;
}
this.props.onKeyboardDidShow && this.props.onKeyboardDidShow(e);
},

View File

@ -53,44 +53,54 @@ var INNERVIEW = 'InnerScrollView';
* Doesn't yet support other contained responders from blocking this scroll
* view from becoming the responder.
*/
var ScrollView = React.createClass({
propTypes: {
automaticallyAdjustContentInsets: PropTypes.bool, // true
contentInset: EdgeInsetsPropType, // zeros
contentOffset: PointPropType, // zeros
onScroll: PropTypes.func,
onScrollAnimationEnd: PropTypes.func,
scrollEnabled: PropTypes.bool, // true
scrollIndicatorInsets: EdgeInsetsPropType, // zeros
showsHorizontalScrollIndicator: PropTypes.bool,
showsVerticalScrollIndicator: PropTypes.bool,
style: StyleSheetPropType(ViewStylePropTypes),
scrollEventThrottle: PropTypes.number, // null
/**
* Controls whether iOS should automatically adjust the content inset
* for scroll views that are placed behind a navigation bar or
* tab bar/ toolbar. The default value is true.
* @platform ios
*/
automaticallyAdjustContentInsets: PropTypes.bool,
/**
* The amount by which the scroll view content is inset from the edges
* of the scroll view. Defaults to `{0, 0, 0, 0}`.
* @platform ios
*/
contentInset: EdgeInsetsPropType,
/**
* Used to manually set the starting scroll offset.
* The default value is `{x: 0, y: 0}`.
* @platform ios
*/
contentOffset: PointPropType,
/**
* When true, the scroll view bounces when it reaches the end of the
* content if the content is larger then the scroll view along the axis of
* the scroll direction. When false, it disables all bouncing even if
* the `alwaysBounce*` props are true. The default value is true.
* @platform ios
*/
bounces: PropTypes.bool,
/**
* When true, gestures can drive zoom past min/max and the zoom will animate
* to the min/max value at gesture end, otherwise the zoom will not exceed
* the limits.
* @platform ios
*/
bouncesZoom: PropTypes.bool,
/**
* When true, the scroll view bounces horizontally when it reaches the end
* even if the content is smaller than the scroll view itself. The default
* value is true when `horizontal={true}` and false otherwise.
* @platform ios
*/
alwaysBounceHorizontal: PropTypes.bool,
/**
* When true, the scroll view bounces vertically when it reaches the end
* even if the content is smaller than the scroll view itself. The default
* value is false when `horizontal={true}` and true otherwise.
* @platform ios
*/
alwaysBounceVertical: PropTypes.bool,
/**
@ -98,6 +108,7 @@ var ScrollView = React.createClass({
* content is smaller than the scroll view bounds; when the content is
* larger than the scroll view, this property has no effect. The default
* value is false.
* @platform ios
*/
centerContent: PropTypes.bool,
/**
@ -121,6 +132,7 @@ var ScrollView = React.createClass({
* decelerates after the user lifts their finger. Reasonable choices include
* - Normal: 0.998 (the default)
* - Fast: 0.9
* @platform ios
*/
decelerationRate: PropTypes.number,
/**
@ -131,17 +143,19 @@ var ScrollView = React.createClass({
/**
* When true, the ScrollView will try to lock to only vertical or horizontal
* scrolling while dragging. The default value is false.
* @platform ios
*/
directionalLockEnabled: PropTypes.bool,
/**
* When false, once tracking starts, won't try to drag if the touch moves.
* The default value is true.
* @platform ios
*/
canCancelContentTouches: PropTypes.bool,
/**
* Determines whether the keyboard gets dismissed in response to a drag.
* - 'none' (the default), drags do not dismiss the keyboard.
* - 'onDrag', the keyboard is dismissed when a drag begins.
* - 'on-drag', the keyboard is dismissed when a drag begins.
* - 'interactive', the keyboard is dismissed interactively with the drag
* and moves in synchrony with the touch; dragging upwards cancels the
* dismissal.
@ -156,35 +170,83 @@ var ScrollView = React.createClass({
* is up dismisses the keyboard. When true, the scroll view will not catch
* taps, and the keyboard will not dismiss automatically. The default value
* is false.
* @platform ios
*/
keyboardShouldPersistTaps: PropTypes.bool,
/**
* The maximum allowed zoom scale. The default value is 1.0.
* @platform ios
*/
maximumZoomScale: PropTypes.number,
/**
* The minimum allowed zoom scale. The default value is 1.0.
* @platform ios
*/
minimumZoomScale: PropTypes.number,
/**
* Fires at most once per frame during scrolling. The frequency of the
* events can be contolled using the `scrollEventThrottle` prop.
*/
onScroll: PropTypes.func,
/**
* Called when a scrolling animation ends.
* @platform ios
*/
onScrollAnimationEnd: PropTypes.func,
/**
* When true, the scroll view stops on multiples of the scroll view's size
* when scrolling. This can be used for horizontal pagination. The default
* value is false.
* @platform ios
*/
pagingEnabled: PropTypes.bool,
/**
* When false, the content does not scroll.
* The default value is true.
* @platform ios
*/
scrollEnabled: PropTypes.bool,
/**
* This controls how often the scroll event will be fired while scrolling
* (in events per seconds). A higher number yields better accuracy for code
* that is tracking the scroll position, but can lead to scroll performance
* problems due to the volume of information being send over the bridge.
* The default value is zero, which means the scroll event will be sent
* only once each time the view is scrolled.
* @platform ios
*/
scrollEventThrottle: PropTypes.number,
/**
* The amount by which the scroll view indicators are inset from the edges
* of the scroll view. This should normally be set to the same value as
* the `contentInset`. Defaults to `{0, 0, 0, 0}`.
* @platform ios
*/
scrollIndicatorInsets: EdgeInsetsPropType,
/**
* When true, the scroll view scrolls to top when the status bar is tapped.
* The default value is true.
* @platform ios
*/
scrollsToTop: PropTypes.bool,
/**
* When true, shows a horizontal scroll indicator.
*/
showsHorizontalScrollIndicator: PropTypes.bool,
/**
* When true, shows a vertical scroll indicator.
*/
showsVerticalScrollIndicator: PropTypes.bool,
/**
* An array of child indices determining which children get docked to the
* top of the screen when scrolling. For example, passing
* `stickyHeaderIndices={[0]}` will cause the first child to be fixed to the
* top of the scroll view. This property is not supported in conjunction
* with `horizontal={true}`.
* @platform ios
*/
stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number),
style: StyleSheetPropType(ViewStylePropTypes),
/**
* Experimental: When true, offscreen child views (whose `overflow` value is
* `hidden`) are removed from their native backing superview when offscreen.
@ -194,6 +256,7 @@ var ScrollView = React.createClass({
removeClippedSubviews: PropTypes.bool,
/**
* The current scale of the scroll view content. The default value is 1.0.
* @platform ios
*/
zoomScale: PropTypes.number,
},

View File

@ -11,20 +11,12 @@
*/
'use strict';
var AnimationExperimental = require('AnimationExperimental');
var Animated = require('Animated');
var NativeMethodsMixin = require('NativeMethodsMixin');
var POPAnimation = require('POPAnimation');
var React = require('React');
var Touchable = require('Touchable');
var merge = require('merge');
var onlyChild = require('onlyChild');
var invariant = require('invariant');
invariant(
AnimationExperimental || POPAnimation,
'Please add the RCTAnimationExperimental framework to your project, or add //Libraries/FBReactKit:RCTPOPAnimation to your BUCK file if running internally within Facebook.'
);
type State = {
animationID: ?number;
@ -58,40 +50,23 @@ var TouchableBounce = React.createClass({
},
getInitialState: function(): State {
return merge(this.touchableGetInitialState(), {animationID: null});
return {
...this.touchableGetInitialState(),
scale: new Animated.Value(1),
};
},
bounceTo: function(
value: number,
velocity: number,
bounciness: number,
fromValue?: ?number,
callback?: ?Function
) {
if (POPAnimation) {
this.state.animationID && this.removeAnimation(this.state.animationID);
var anim = {
property: POPAnimation.Properties.scaleXY,
dynamicsTension: 0,
toValue: [value, value],
velocity: [velocity, velocity],
springBounciness: bounciness,
fromValue: fromValue ? [fromValue, fromValue] : undefined,
};
this.state.animationID = POPAnimation.createSpringAnimation(anim);
this.addAnimation(this.state.animationID, callback);
} else {
AnimationExperimental.startAnimation(
{
node: this,
duration: 300,
easing: 'easeOutBack',
property: 'scaleXY',
toValue: { x: value, y: value},
},
callback
);
}
Animated.spring(this.state.scale, {
toValue: value,
velocity,
bounciness,
}).start(callback);
},
/**
@ -109,13 +84,14 @@ var TouchableBounce = React.createClass({
touchableHandlePress: function() {
var onPressWithCompletion = this.props.onPressWithCompletion;
if (onPressWithCompletion) {
onPressWithCompletion(
this.bounceTo.bind(this, 1, 10, 10, 0.93, this.props.onPressAnimationComplete)
);
onPressWithCompletion(() => {
this.state.scale.setValue(0.93);
this.bounceTo(1, 10, 10, this.props.onPressAnimationComplete);
});
return;
}
this.bounceTo(1, 10, 10, undefined, this.props.onPressAnimationComplete);
this.bounceTo(1, 10, 10, this.props.onPressAnimationComplete);
this.props.onPress && this.props.onPress();
},
@ -127,18 +103,21 @@ var TouchableBounce = React.createClass({
return 0;
},
render: function() {
var child = onlyChild(this.props.children);
return React.cloneElement(child, {
accessible: true,
testID: this.props.testID,
onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder,
onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest,
onResponderGrant: this.touchableHandleResponderGrant,
onResponderMove: this.touchableHandleResponderMove,
onResponderRelease: this.touchableHandleResponderRelease,
onResponderTerminate: this.touchableHandleResponderTerminate
});
render: function(): ReactElement {
return (
<Animated.View
style={[{transform: [{scale: this.state.scale}]}, this.props.style]}
accessible={true}
testID={this.props.testID}
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
onResponderGrant={this.touchableHandleResponderGrant}
onResponderMove={this.touchableHandleResponderMove}
onResponderRelease={this.touchableHandleResponderRelease}
onResponderTerminate={this.touchableHandleResponderTerminate}>
{this.props.children}
</Animated.View>
);
}
});

View File

@ -27,7 +27,11 @@ var mapWithSeparator = require('mapWithSeparator');
var ElementProperties = React.createClass({
propTypes: {
hierarchy: PropTypes.array.isRequired,
style: PropTypes.array.isRequired,
style: PropTypes.oneOfType([
PropTypes.object,
PropTypes.array,
PropTypes.number,
]),
},
render: function() {

View File

@ -20,7 +20,7 @@ var StyleSheet = require('StyleSheet');
var UIManager = require('NativeModules').UIManager;
var View = require('View');
var REACT_DEVTOOLS_HOOK: ?Object = typeof window !== 'undefined' ? window.__REACT_DEVTOOLS_BACKEND__ : null;
var REACT_DEVTOOLS_HOOK: ?Object = typeof window !== 'undefined' ? window.__REACT_DEVTOOLS_GLOBAL_HOOK__ : null;
if (REACT_DEVTOOLS_HOOK) {
// required for devtools to be able to edit react native styles
@ -34,7 +34,7 @@ class Inspector extends React.Component {
super(props);
this.state = {
devtoolsBackend: null,
devtoolsAgent: null,
panelPos: 'bottom',
inspecting: true,
perfing: false,
@ -45,14 +45,10 @@ class Inspector extends React.Component {
componentDidMount() {
if (REACT_DEVTOOLS_HOOK) {
this.attachToDevtools = this.attachToDevtools.bind(this);
REACT_DEVTOOLS_HOOK.addStartupListener(this.attachToDevtools);
REACT_DEVTOOLS_HOOK.on('react-devtools', this.attachToDevtools);
// if devtools is already started
// TODO(jared): should addStartupListener just go ahead and call the
// listener if the devtools is already started? might be unexpected...
// is there some name other than `addStartupListener` that would be
// better?
if (REACT_DEVTOOLS_HOOK.backend) {
this.attachToDevtools(REACT_DEVTOOLS_HOOK.backend);
if (REACT_DEVTOOLS_HOOK.reactDevtoolsAgent) {
this.attachToDevtools(REACT_DEVTOOLS_HOOK.reactDevtoolsAgent);
}
}
}
@ -62,13 +58,13 @@ class Inspector extends React.Component {
this._subs.map(fn => fn());
}
if (REACT_DEVTOOLS_HOOK) {
REACT_DEVTOOLS_HOOK.removeStartupListener(this.attachToDevtools);
REACT_DEVTOOLS_HOOK.off('react-devtools', this.attachToDevtools);
}
}
attachToDevtools(backend: Object) {
attachToDevtools(agent: Object) {
var _hideWait = null;
var hlSub = backend.sub('highlight', ({node, name, props}) => {
var hlSub = agent.sub('highlight', ({node, name, props}) => {
clearTimeout(_hideWait);
UIManager.measure(node, (x, y, width, height, left, top) => {
this.setState({
@ -80,7 +76,10 @@ class Inspector extends React.Component {
});
});
});
var hideSub = backend.sub('hideHighlight', () => {
var hideSub = agent.sub('hideHighlight', () => {
if (this.state.inspected === null) {
return;
}
// we wait to actually hide in order to avoid flicker
_hideWait = setTimeout(() => {
this.setState({
@ -90,12 +89,12 @@ class Inspector extends React.Component {
});
this._subs = [hlSub, hideSub];
backend.on('shutdown', () => {
this.setState({devtoolsBackend: null});
agent.on('shutdown', () => {
this.setState({devtoolsAgent: null});
this._subs = null;
});
this.setState({
devtoolsBackend: backend,
devtoolsAgent: agent,
});
}
@ -114,8 +113,8 @@ class Inspector extends React.Component {
}
onTouchInstance(instance: Object, frame: Object, pointerY: number) {
if (this.state.devtoolsBackend) {
this.state.devtoolsBackend.selectFromReactInstance(instance, true);
if (this.state.devtoolsAgent) {
this.state.devtoolsAgent.selectFromReactInstance(instance, true);
}
var hierarchy = InspectorUtils.getOwnerHierarchy(instance);
var publicInstance = instance.getPublicInstance();
@ -159,7 +158,7 @@ class Inspector extends React.Component {
/>}
<View style={[styles.panelContainer, panelContainerStyle]}>
<InspectorPanel
devtoolsIsOpen={!!this.state.devtoolsBackend}
devtoolsIsOpen={!!this.state.devtoolsAgent}
inspecting={this.state.inspecting}
perfing={this.state.perfing}
setPerfing={this.setPerfing.bind(this)}

View File

@ -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 <Foundation/Foundation.h>
#import "RCTURLRequestDelegate.h"
#import "RCTURLRequestHandler.h"
typedef void (^RCTURLRequestCompletionBlock)(NSURLResponse *response, NSData *data, NSError *error);
typedef void (^RCTURLRequestCancellationBlock)(void);
typedef void (^RCTURLRequestIncrementalDataBlock)(NSData *data);
typedef void (^RCTURLRequestProgressBlock)(double progress, double total);
typedef void (^RCTURLRequestResponseBlock)(NSURLResponse *response);
@interface RCTDownloadTask : NSObject <RCTURLRequestDelegate>
@property (nonatomic, readonly) NSURLRequest *request;
@property (nonatomic, readonly) NSNumber *requestID;
@property (nonatomic, readonly) id requestToken;
@property (nonatomic, readonly) NSURLResponse *response;
@property (nonatomic, readonly) RCTURLRequestCompletionBlock completionBlock;
@property (nonatomic, copy) RCTURLRequestProgressBlock downloadProgressBlock;
@property (nonatomic, copy) RCTURLRequestIncrementalDataBlock incrementalDataBlock;
@property (nonatomic, copy) RCTURLRequestResponseBlock responseBlock;
@property (nonatomic, copy) RCTURLRequestProgressBlock uploadProgressBlock;
- (instancetype)initWithRequest:(NSURLRequest *)request
handler:(id<RCTURLRequestHandler>)handler
completionBlock:(RCTURLRequestCompletionBlock)completionBlock NS_DESIGNATED_INITIALIZER;
- (void)cancel;
@end

View File

@ -0,0 +1,102 @@
/**
* 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 "RCTDownloadTask.h"
#import "RCTAssert.h"
@implementation RCTDownloadTask
{
NSMutableData *_data;
id<RCTURLRequestHandler> _handler;
RCTDownloadTask *_selfReference;
}
- (instancetype)initWithRequest:(NSURLRequest *)request
handler:(id<RCTURLRequestHandler>)handler
completionBlock:(RCTURLRequestCompletionBlock)completionBlock
{
RCTAssertParam(request);
RCTAssertParam(handler);
RCTAssertParam(completionBlock);
static NSUInteger requestID = 0;
if ((self = [super init])) {
if (!(_requestToken = [handler sendRequest:request withDelegate:self])) {
return nil;
}
_requestID = @(requestID++);
_request = request;
_handler = handler;
_completionBlock = completionBlock;
_selfReference = self;
}
return self;
}
- (void)invalidate
{
_selfReference = nil;
_completionBlock = nil;
_downloadProgressBlock = nil;
_incrementalDataBlock = nil;
_responseBlock = nil;
_uploadProgressBlock = nil;
}
RCT_NOT_IMPLEMENTED(-init)
- (void)cancel
{
if ([_handler respondsToSelector:@selector(cancelRequest:)]) {
[_handler cancelRequest:_requestToken];
}
[self invalidate];
}
- (void)URLRequest:(id)requestToken didSendDataWithProgress:(int64_t)bytesSent
{
RCTAssert([requestToken isEqual:_requestToken], @"Unrecognized request token: %@", requestToken);
if (_uploadProgressBlock) {
_uploadProgressBlock(bytesSent, _request.HTTPBody.length);
}
}
- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response
{
RCTAssert([requestToken isEqual:_requestToken], @"Unrecognized request token: %@", requestToken);
_response = response;
if (_responseBlock) {
_responseBlock(response);
}
}
- (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data
{
RCTAssert([requestToken isEqual:_requestToken], @"Unrecognized request token: %@", requestToken);
if (!_data) {
_data = [[NSMutableData alloc] init];
}
[_data appendData:data];
if (_incrementalDataBlock) {
_incrementalDataBlock(data);
}
if (_downloadProgressBlock && _response.expectedContentLength > 0) {
_downloadProgressBlock(_data.length, _response.expectedContentLength);
}
}
- (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error
{
_completionBlock(_response, _data, error);
[self invalidate];
}
@end

View File

@ -10,6 +10,9 @@
#import "RCTURLRequestHandler.h"
#import "RCTInvalidating.h"
/**
* This is the default RCTURLRequestHandler implementation for HTTP requests.
*/
@interface RCTHTTPRequestHandler : NSObject <RCTURLRequestHandler, RCTInvalidating>
@end

View File

@ -50,8 +50,8 @@ RCT_EXPORT_MODULE()
return [@[@"http", @"https", @"file"] containsObject:[request.URL.scheme lowercaseString]];
}
- (id)sendRequest:(NSURLRequest *)request
withDelegate:(id<RCTURLRequestDelegate>)delegate
- (NSURLSessionDataTask *)sendRequest:(NSURLRequest *)request
withDelegate:(id<RCTURLRequestDelegate>)delegate
{
// Lazy setup
if (!_session && [self isValid]) {
@ -68,9 +68,10 @@ RCT_EXPORT_MODULE()
return task;
}
- (void)cancelRequest:(NSURLSessionDataTask *)requestToken
- (void)cancelRequest:(NSURLSessionDataTask *)task
{
[requestToken cancel];
[task cancel];
[_delegates removeObjectForKey:task];
}
#pragma mark - NSURLSession delegate
@ -81,10 +82,9 @@ RCT_EXPORT_MODULE()
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
[[_delegates objectForKey:task] URLRequest:task didUploadProgress:(double)totalBytesSent total:(double)totalBytesExpectedToSend];
[[_delegates objectForKey:task] URLRequest:task didSendDataWithProgress:totalBytesSent];
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)task
didReceiveResponse:(NSURLResponse *)response

View File

@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 1372B7361AB03E7B00659ED6 /* RCTReachability.m */; };
13D6D66A1B5FCF8200883BE9 /* RCTDownloadTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 13D6D6691B5FCF8200883BE9 /* RCTDownloadTask.m */; };
352DA0BA1B17855800AA15A8 /* RCTHTTPRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 352DA0B81B17855800AA15A8 /* RCTHTTPRequestHandler.m */; };
58B512081A9E6CE300147676 /* RCTNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B512071A9E6CE300147676 /* RCTNetworking.m */; };
/* End PBXBuildFile section */
@ -27,6 +28,8 @@
/* Begin PBXFileReference section */
1372B7351AB03E7B00659ED6 /* RCTReachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTReachability.h; sourceTree = "<group>"; };
1372B7361AB03E7B00659ED6 /* RCTReachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTReachability.m; sourceTree = "<group>"; };
13D6D6681B5FCF8200883BE9 /* RCTDownloadTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDownloadTask.h; sourceTree = "<group>"; };
13D6D6691B5FCF8200883BE9 /* RCTDownloadTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDownloadTask.m; sourceTree = "<group>"; };
352DA0B71B17855800AA15A8 /* RCTHTTPRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTHTTPRequestHandler.h; sourceTree = "<group>"; };
352DA0B81B17855800AA15A8 /* RCTHTTPRequestHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTHTTPRequestHandler.m; sourceTree = "<group>"; };
58B511DB1A9E6C8500147676 /* libRCTNetwork.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTNetwork.a; sourceTree = BUILT_PRODUCTS_DIR; };
@ -48,6 +51,8 @@
58B511D21A9E6C8500147676 = {
isa = PBXGroup;
children = (
13D6D6681B5FCF8200883BE9 /* RCTDownloadTask.h */,
13D6D6691B5FCF8200883BE9 /* RCTDownloadTask.m */,
352DA0B71B17855800AA15A8 /* RCTHTTPRequestHandler.h */,
352DA0B81B17855800AA15A8 /* RCTHTTPRequestHandler.m */,
58B512061A9E6CE300147676 /* RCTNetworking.h */,
@ -124,6 +129,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
13D6D66A1B5FCF8200883BE9 /* RCTDownloadTask.m in Sources */,
1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */,
58B512081A9E6CE300147676 /* RCTNetworking.m in Sources */,
352DA0BA1B17855800AA15A8 /* RCTHTTPRequestHandler.m in Sources */,

View File

@ -14,4 +14,3 @@
@interface RCTNetworking : NSObject <RCTBridgeModule>
@end

View File

@ -11,18 +11,19 @@
#import "RCTAssert.h"
#import "RCTConvert.h"
#import "RCTDownloadTask.h"
#import "RCTURLRequestHandler.h"
#import "RCTEventDispatcher.h"
#import "RCTHTTPRequestHandler.h"
#import "RCTLog.h"
#import "RCTUtils.h"
typedef void (^RCTHTTPQueryResult)(NSError *error, NSDictionary *result);
typedef RCTURLRequestCancellationBlock (^RCTHTTPQueryResult)(NSError *error, NSDictionary *result);
@interface RCTNetworking ()<RCTURLRequestDelegate>
- (void)processDataForHTTPQuery:(NSDictionary *)data callback:(void (^)(NSError *error, NSDictionary *result))callback;
@interface RCTNetworking ()
- (RCTURLRequestCancellationBlock)processDataForHTTPQuery:(NSDictionary *)data
callback:(RCTHTTPQueryResult)callback;
@end
/**
@ -30,7 +31,7 @@ typedef void (^RCTHTTPQueryResult)(NSError *error, NSDictionary *result);
*/
@interface RCTHTTPFormDataHelper : NSObject
@property (nonatomic, weak) RCTNetworking *dataManager;
@property (nonatomic, weak) RCTNetworking *networker;
@end
@ -42,51 +43,49 @@ typedef void (^RCTHTTPQueryResult)(NSError *error, NSDictionary *result);
NSString *boundary;
}
- (void)process:(NSArray *)formData callback:(void (^)(NSError *error, NSDictionary *result))callback
static NSString *RCTGenerateFormBoundary()
{
if (![formData count]) {
callback(nil, nil);
return;
const size_t boundaryLength = 70;
const char *boundaryChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./";
char *bytes = malloc(boundaryLength);
size_t charCount = strlen(boundaryChars);
for (int i = 0; i < boundaryLength; i++) {
bytes[i] = boundaryChars[arc4random_uniform((u_int32_t)charCount)];
}
return [[NSString alloc] initWithBytesNoCopy:bytes length:boundaryLength encoding:NSUTF8StringEncoding freeWhenDone:YES];
}
- (RCTURLRequestCancellationBlock)process:(NSArray *)formData
callback:(RCTHTTPQueryResult)callback
{
if (formData.count == 0) {
return callback(nil, nil);
}
parts = [formData mutableCopy];
_callback = callback;
multipartBody = [[NSMutableData alloc] init];
boundary = [self generateBoundary];
boundary = RCTGenerateFormBoundary();
NSDictionary *currentPart = [parts objectAtIndex: 0];
[_dataManager processDataForHTTPQuery:currentPart callback:^(NSError *e, NSDictionary *r) {
[self handleResult:r error:e];
return [_networker processDataForHTTPQuery:parts[0] callback:^(NSError *error, NSDictionary *result) {
return [self handleResult:result error:error];
}];
}
- (NSString *)generateBoundary
{
NSString *const boundaryChars = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./";
const NSUInteger boundaryLength = 70;
NSMutableString *output = [NSMutableString stringWithCapacity:boundaryLength];
NSUInteger numchars = [boundaryChars length];
for (NSUInteger i = 0; i < boundaryLength; i++) {
[output appendFormat:@"%C", [boundaryChars characterAtIndex:arc4random_uniform((u_int32_t)numchars)]];
}
return output;
}
- (void)handleResult:(NSDictionary *)result error:(NSError *)error
- (RCTURLRequestCancellationBlock)handleResult:(NSDictionary *)result
error:(NSError *)error
{
if (error) {
_callback(error, nil);
return;
return _callback(error, nil);
}
NSDictionary *currentPart = parts[0];
[parts removeObjectAtIndex:0];
// Start with boundary.
[multipartBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary]
dataUsingEncoding:NSUTF8StringEncoding]];
// Print headers.
NSMutableDictionary *headers = [(NSDictionary*)currentPart[@"headers"] mutableCopy];
NSMutableDictionary *headers = [parts[0][@"headers"] mutableCopy];
NSString *partContentType = result[@"contentType"];
if (partContentType != nil) {
[headers setObject:partContentType forKey:@"content-type"];
@ -101,110 +100,18 @@ typedef void (^RCTHTTPQueryResult)(NSError *error, NSDictionary *result);
[multipartBody appendData:result[@"body"]];
[multipartBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
if ([parts count]) {
NSDictionary *nextPart = [parts objectAtIndex: 0];
[_dataManager processDataForHTTPQuery:nextPart callback:^(NSError *e, NSDictionary *r) {
[self handleResult:r error:e];
[parts removeObjectAtIndex:0];
if (parts.count) {
return [_networker processDataForHTTPQuery:parts[0] callback:^(NSError *err, NSDictionary *res) {
return [self handleResult:res error:err];
}];
return;
}
// We've processed the last item. Finish and return.
[multipartBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary]
dataUsingEncoding:NSUTF8StringEncoding]];
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=\"%@\"", boundary];
_callback(nil, @{@"body": multipartBody, @"contentType": contentType});
}
@end
/**
* Helper to package in-flight requests together with their response data.
*/
@interface RCTActiveURLRequest : NSObject
@property (nonatomic, strong) NSNumber *requestID;
@property (nonatomic, strong) NSURLRequest *request;
@property (nonatomic, strong) id<RCTURLRequestHandler> handler;
@property (nonatomic, assign) BOOL incrementalUpdates;
@property (nonatomic, strong) NSURLResponse *response;
@property (nonatomic, strong) NSMutableData *data;
@end
@implementation RCTActiveURLRequest
- (instancetype)init
{
if ((self = [super init])) {
_data = [[NSMutableData alloc] init];
}
return self;
}
@end
/**
* Helper to load request body data using a handler.
*/
@interface RCTDataLoader : NSObject <RCTURLRequestDelegate>
@end
typedef void (^RCTDataLoaderCallback)(NSData *data, NSString *MIMEType, NSError *error);
@implementation RCTDataLoader
{
RCTDataLoaderCallback _callback;
RCTActiveURLRequest *_request;
id _requestToken;
}
- (instancetype)initWithRequest:(NSURLRequest *)request
handler:(id<RCTURLRequestHandler>)handler
callback:(RCTDataLoaderCallback)callback
{
RCTAssertParam(request);
RCTAssertParam(handler);
RCTAssertParam(callback);
if ((self = [super init])) {
_callback = callback;
_request = [[RCTActiveURLRequest alloc] init];
_request.request = request;
_request.handler = handler;
_request.incrementalUpdates = NO;
_requestToken = [handler sendRequest:request withDelegate:self];
}
return self;
}
- (instancetype)init
{
return [self initWithRequest:nil handler:nil callback:nil];
}
- (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total
{
RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen");
}
- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response
{
RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen");
_request.response = response;
}
- (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data
{
RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen");
[_request.data appendData:data];
}
- (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error
{
RCTAssert(_callback != nil, @"The callback property must be set");
_callback(_request.data, _request.response.MIMEType, error);
return _callback(nil, @{@"body": multipartBody, @"contentType": contentType});
}
@end
@ -214,8 +121,7 @@ typedef void (^RCTDataLoaderCallback)(NSData *data, NSString *MIMEType, NSError
*/
@implementation RCTNetworking
{
NSInteger _currentRequestID;
NSMapTable *_activeRequests;
NSMutableDictionary *_tasksByRequestID;
}
@synthesize bridge = _bridge;
@ -226,16 +132,13 @@ RCT_EXPORT_MODULE()
- (instancetype)init
{
if ((self = [super init])) {
_currentRequestID = 0;
_activeRequests = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory
valueOptions:NSPointerFunctionsStrongMemory
capacity:0];
_tasksByRequestID = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)buildRequest:(NSDictionary *)query
completionBlock:(void (^)(NSURLRequest *request))block
- (RCTURLRequestCancellationBlock)buildRequest:(NSDictionary *)query
completionBlock:(void (^)(NSURLRequest *request))block
{
NSURL *URL = [RCTConvert NSURL:query[@"url"]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
@ -243,11 +146,11 @@ RCT_EXPORT_MODULE()
request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]];
NSDictionary *data = [RCTConvert NSDictionary:query[@"data"]];
[self processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary *result) {
return [self processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary *result) {
if (error) {
RCTLogError(@"Error processing request body: %@", error);
// Ideally we'd circle back to JS here and notify an error/abort on the request.
return;
return (RCTURLRequestCancellationBlock)nil;
}
request.HTTPBody = result[@"body"];
NSString *contentType = result[@"contentType"];
@ -262,6 +165,7 @@ RCT_EXPORT_MODULE()
}
block(request);
return (RCTURLRequestCancellationBlock)nil;
}];
}
@ -316,71 +220,56 @@ RCT_EXPORT_MODULE()
* - @"contentType" (NSString): the content type header of the request
*
*/
- (void)processDataForHTTPQuery:(NSDictionary *)query callback:(void (^)(NSError *error, NSDictionary *result))callback
- (RCTURLRequestCancellationBlock)processDataForHTTPQuery:(NSDictionary *)query callback:
(RCTURLRequestCancellationBlock (^)(NSError *error, NSDictionary *result))callback
{
if (!query) {
callback(nil, nil);
return;
return callback(nil, nil);
}
NSData *body = [RCTConvert NSData:query[@"string"]];
if (body) {
callback(nil, @{@"body": body});
return;
return callback(nil, @{@"body": body});
}
NSURLRequest *request = [RCTConvert NSURLRequest:query[@"uri"]];
if (request) {
id<RCTURLRequestHandler> handler = [self handlerForRequest:request];
if (!handler) {
return;
return callback(nil, nil);
}
(void)[[RCTDataLoader alloc] initWithRequest:request handler:handler callback:^(NSData *data, NSString *MIMEType, NSError *error) {
if (data) {
callback(nil, @{@"body": data, @"contentType": MIMEType});
} else {
callback(error, nil);
}
__block RCTURLRequestCancellationBlock cancellationBlock = nil;
RCTDownloadTask *task = [[RCTDownloadTask alloc] initWithRequest:request handler:handler completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) {
cancellationBlock = callback(error, data ? @{@"body": data, @"contentType": RCTNullIfNil(response.MIMEType)} : nil);
}];
return;
__weak RCTDownloadTask *weakTask = task;
return ^{
[weakTask cancel];
if (cancellationBlock) {
cancellationBlock();
}
};
}
NSDictionaryArray *formData = [RCTConvert NSDictionaryArray:query[@"formData"]];
if (formData != nil) {
if (formData) {
RCTHTTPFormDataHelper *formDataHelper = [[RCTHTTPFormDataHelper alloc] init];
formDataHelper.dataManager = self;
[formDataHelper process:formData callback:callback];
return;
formDataHelper.networker = self;
return [formDataHelper process:formData callback:callback];
}
// Nothing in the data payload, at least nothing we could understand anyway.
// Ignore and treat it as if it were null.
callback(nil, nil);
return callback(nil, nil);
}
- (void)sendRequest:(NSURLRequest *)request
incrementalUpdates:(BOOL)incrementalUpdates
responseSender:(RCTResponseSenderBlock)responseSender
{
id<RCTURLRequestHandler> handler = [self handlerForRequest:request];
id token = [handler sendRequest:request withDelegate:self];
if (token) {
RCTActiveURLRequest *activeRequest = [[RCTActiveURLRequest alloc] init];
activeRequest.requestID = @(++_currentRequestID);
activeRequest.request = request;
activeRequest.handler = handler;
activeRequest.incrementalUpdates = incrementalUpdates;
[_activeRequests setObject:activeRequest forKey:token];
responseSender(@[activeRequest.requestID]);
}
}
- (void)sendData:(NSData *)data forRequestToken:(id)requestToken
- (void)sendData:(NSData *)data forTask:(RCTDownloadTask *)task
{
if (data.length == 0) {
return;
}
RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken];
// Get text encoding
NSURLResponse *response = request.response;
NSURLResponse *response = task.response;
NSStringEncoding encoding = NSUTF8StringEncoding;
if (response.textEncodingName) {
CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
@ -393,82 +282,81 @@ RCT_EXPORT_MODULE()
return;
}
NSArray *responseJSON = @[request.requestID, responseText ?: @""];
NSArray *responseJSON = @[task.requestID, responseText ?: @""];
[_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkData"
body:responseJSON];
}
#pragma mark - RCTURLRequestDelegate
- (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total
- (void)sendRequest:(NSURLRequest *)request
incrementalUpdates:(BOOL)incrementalUpdates
responseSender:(RCTResponseSenderBlock)responseSender
{
dispatch_async(_methodQueue, ^{
RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken];
RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken);
id<RCTURLRequestHandler> handler = [self handlerForRequest:request];
if (!handler) {
return;
}
NSArray *responseJSON = @[request.requestID, @(progress), @(total)];
[_bridge.eventDispatcher sendDeviceEventWithName:@"didUploadProgress" body:responseJSON];
});
}
__block RCTDownloadTask *task;
- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response
{
dispatch_async(_methodQueue, ^{
RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken];
RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken);
RCTURLRequestProgressBlock uploadProgressBlock = ^(double progress, double total) {
dispatch_async(_methodQueue, ^{
NSArray *responseJSON = @[task.requestID, @(progress), @(total)];
[_bridge.eventDispatcher sendDeviceEventWithName:@"didSendNetworkData" body:responseJSON];
});
};
request.response = response;
void (^responseBlock)(NSURLResponse *) = ^(NSURLResponse *response) {
dispatch_async(_methodQueue, ^{
NSHTTPURLResponse *httpResponse = nil;
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
// Might be a local file request
httpResponse = (NSHTTPURLResponse *)response;
}
NSArray *responseJSON = @[task.requestID,
@(httpResponse.statusCode ?: 200),
httpResponse.allHeaderFields ?: @{},
];
NSHTTPURLResponse *httpResponse = nil;
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
// Might be a local file request
httpResponse = (NSHTTPURLResponse *)response;
}
[_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse"
body:responseJSON];
});
};
NSArray *responseJSON = @[request.requestID,
@(httpResponse.statusCode ?: 200),
httpResponse.allHeaderFields ?: @{},
];
void (^incrementalDataBlock)(NSData *) = incrementalUpdates ? ^(NSData *data) {
dispatch_async(_methodQueue, ^{
[self sendData:data forTask:task];
});
} : nil;
[_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse"
body:responseJSON];
});
}
RCTURLRequestCompletionBlock completionBlock =
^(NSURLResponse *response, NSData *data, NSError *error) {
dispatch_async(_methodQueue, ^{
if (!incrementalUpdates) {
[self sendData:data forTask:task];
}
NSArray *responseJSON = @[task.requestID,
RCTNullIfNil(error.localizedDescription),
];
- (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data
{
dispatch_async(_methodQueue, ^{
RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken];
RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken);
[_bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse"
body:responseJSON];
if (request.incrementalUpdates) {
[self sendData:data forRequestToken:requestToken];
} else {
[request.data appendData:data];
}
});
}
[_tasksByRequestID removeObjectForKey:task.requestID];
});
};
- (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error
{
dispatch_async(_methodQueue, ^{
RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken];
RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken);
task = [[RCTDownloadTask alloc] initWithRequest:request
handler:handler
completionBlock:completionBlock];
if (!request.incrementalUpdates) {
[self sendData:request.data forRequestToken:requestToken];
}
task.incrementalDataBlock = incrementalDataBlock;
task.responseBlock = responseBlock;
task.uploadProgressBlock = uploadProgressBlock;
NSArray *responseJSON = @[
request.requestID,
RCTNullIfNil(error.localizedDescription),
];
[_bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse"
body:responseJSON];
[_activeRequests removeObjectForKey:requestToken];
});
if (task.requestID) {
_tasksByRequestID[task.requestID] = task;
responseSender(@[task.requestID]);
}
}
#pragma mark - JS API
@ -476,31 +364,22 @@ RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(sendRequest:(NSDictionary *)query
responseSender:(RCTResponseSenderBlock)responseSender)
{
// TODO: buildRequest returns a cancellation block, but there's currently
// no way to invoke it, if, for example the request is cancelled while
// loading a large file to build the request body
[self buildRequest:query completionBlock:^(NSURLRequest *request) {
BOOL incrementalUpdates = [RCTConvert BOOL:query[@"incrementalUpdates"]];
[self sendRequest:request incrementalUpdates:incrementalUpdates
[self sendRequest:request
incrementalUpdates:incrementalUpdates
responseSender:responseSender];
}];
}
RCT_EXPORT_METHOD(cancelRequest:(NSNumber *)requestID)
{
id requestToken = nil;
RCTActiveURLRequest *activeRequest = nil;
for (id token in _activeRequests) {
RCTActiveURLRequest *request = [_activeRequests objectForKey:token];
if ([request.requestID isEqualToNumber:requestID]) {
activeRequest = request;
requestToken = token;
break;
}
}
id<RCTURLRequestHandler> handler = activeRequest.handler;
if ([handler respondsToSelector:@selector(cancelRequest:)]) {
[activeRequest.handler cancelRequest:requestToken];
}
[_tasksByRequestID[requestID] cancel];
[_tasksByRequestID removeObjectForKey:requestID];
}
@end

View File

@ -35,7 +35,7 @@ class XMLHttpRequest extends XMLHttpRequestBase {
_didCreateRequest(requestId: number): void {
this._requestId = requestId;
this._subscriptions.push(RCTDeviceEventEmitter.addListener(
'didUploadProgress',
'didSendNetworkData',
(args) => this._didUploadProgress.call(this, args[0], args[1], args[2])
));
this._subscriptions.push(RCTDeviceEventEmitter.addListener(

View File

@ -21,3 +21,6 @@ declare var fetch: any;
declare var Headers: any;
declare var Request: any;
declare var Response: any;
declare module requestAnimationFrame {
declare var exports: (callback: any) => any;
}

View File

@ -16,10 +16,10 @@
@protocol RCTURLRequestDelegate <NSObject>
/**
* Call this when you first receives a response from the server. This should
* include response headers, etc.
* Call this when you send request data to the server. This is used to track
* upload progress, so should be called multiple times for large request bodies.
*/
- (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total;
- (void)URLRequest:(id)requestToken didSendDataWithProgress:(int64_t)bytesSent;
/**
* Call this when you first receives a response from the server. This should

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Copyright (c) 2015-present, Facebook, Inc.
# All rights reserved.

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Copyright (c) 2015-present, Facebook, Inc.
# All rights reserved.