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 ( return (
<View style={styles.overlay}> <View style={styles.overlay}>
<Text style={styles.overlayMessage}>{message}</Text> <Text style={styles.overlayMessage}>{message}</Text>
<TouchableBounce onPress={this.props.onRestart}> <TouchableBounce onPress={this.props.onRestart} style={styles.tryAgain}>
<View style={styles.tryAgain}> <Text style={styles.tryAgainText}>Try Again?</Text>
<Text style={styles.tryAgainText}>Try Again?</Text>
</View>
</TouchableBounce> </TouchableBounce>
</View> </View>
); );

View File

@ -17,7 +17,7 @@
'use strict'; 'use strict';
var React = require('react-native'); var React = require('react-native');
var UIExplorerList = require('./UIExplorerList'); var UIExplorerList = require('./UIExplorerList.ios');
var { var {
AppRegistry, AppRegistry,
NavigatorIOS, 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) { if (this._tension !== 0) {
isDisplacement = Math.abs(this._toValue - position) <= this._restDisplacementThreshold; isDisplacement = Math.abs(this._toValue - position) <= this._restDisplacementThreshold;
} }
if (isOvershooting || (isVelocity && isDisplacement)) { 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}); this.__debouncedOnEnd({finished: true});
return; return;
} }

View File

@ -127,6 +127,19 @@ describe('Animated', () => {
Animated.spring(anim, {toValue: 0, velocity: 0}).start(callback); Animated.spring(anim, {toValue: 0, velocity: 0}).start(callback);
expect(callback).toBeCalled(); 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) { 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); 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 * Doesn't yet support other contained responders from blocking this scroll
* view from becoming the responder. * view from becoming the responder.
*/ */
var ScrollView = React.createClass({ var ScrollView = React.createClass({
propTypes: { propTypes: {
automaticallyAdjustContentInsets: PropTypes.bool, // true /**
contentInset: EdgeInsetsPropType, // zeros * Controls whether iOS should automatically adjust the content inset
contentOffset: PointPropType, // zeros * for scroll views that are placed behind a navigation bar or
onScroll: PropTypes.func, * tab bar/ toolbar. The default value is true.
onScrollAnimationEnd: PropTypes.func, * @platform ios
scrollEnabled: PropTypes.bool, // true */
scrollIndicatorInsets: EdgeInsetsPropType, // zeros automaticallyAdjustContentInsets: PropTypes.bool,
showsHorizontalScrollIndicator: PropTypes.bool, /**
showsVerticalScrollIndicator: PropTypes.bool, * The amount by which the scroll view content is inset from the edges
style: StyleSheetPropType(ViewStylePropTypes), * of the scroll view. Defaults to `{0, 0, 0, 0}`.
scrollEventThrottle: PropTypes.number, // null * @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 * 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 * 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 scroll direction. When false, it disables all bouncing even if
* the `alwaysBounce*` props are true. The default value is true. * the `alwaysBounce*` props are true. The default value is true.
* @platform ios
*/ */
bounces: PropTypes.bool, bounces: PropTypes.bool,
/** /**
* When true, gestures can drive zoom past min/max and the zoom will animate * 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 * to the min/max value at gesture end, otherwise the zoom will not exceed
* the limits. * the limits.
* @platform ios
*/ */
bouncesZoom: PropTypes.bool, bouncesZoom: PropTypes.bool,
/** /**
* When true, the scroll view bounces horizontally when it reaches the end * 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 * even if the content is smaller than the scroll view itself. The default
* value is true when `horizontal={true}` and false otherwise. * value is true when `horizontal={true}` and false otherwise.
* @platform ios
*/ */
alwaysBounceHorizontal: PropTypes.bool, alwaysBounceHorizontal: PropTypes.bool,
/** /**
* When true, the scroll view bounces vertically when it reaches the end * 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 * even if the content is smaller than the scroll view itself. The default
* value is false when `horizontal={true}` and true otherwise. * value is false when `horizontal={true}` and true otherwise.
* @platform ios
*/ */
alwaysBounceVertical: PropTypes.bool, alwaysBounceVertical: PropTypes.bool,
/** /**
@ -98,6 +108,7 @@ var ScrollView = React.createClass({
* content is smaller than the scroll view bounds; when the content is * content is smaller than the scroll view bounds; when the content is
* larger than the scroll view, this property has no effect. The default * larger than the scroll view, this property has no effect. The default
* value is false. * value is false.
* @platform ios
*/ */
centerContent: PropTypes.bool, centerContent: PropTypes.bool,
/** /**
@ -121,6 +132,7 @@ var ScrollView = React.createClass({
* decelerates after the user lifts their finger. Reasonable choices include * decelerates after the user lifts their finger. Reasonable choices include
* - Normal: 0.998 (the default) * - Normal: 0.998 (the default)
* - Fast: 0.9 * - Fast: 0.9
* @platform ios
*/ */
decelerationRate: PropTypes.number, decelerationRate: PropTypes.number,
/** /**
@ -131,17 +143,19 @@ var ScrollView = React.createClass({
/** /**
* When true, the ScrollView will try to lock to only vertical or horizontal * When true, the ScrollView will try to lock to only vertical or horizontal
* scrolling while dragging. The default value is false. * scrolling while dragging. The default value is false.
* @platform ios
*/ */
directionalLockEnabled: PropTypes.bool, directionalLockEnabled: PropTypes.bool,
/** /**
* When false, once tracking starts, won't try to drag if the touch moves. * When false, once tracking starts, won't try to drag if the touch moves.
* The default value is true. * The default value is true.
* @platform ios
*/ */
canCancelContentTouches: PropTypes.bool, canCancelContentTouches: PropTypes.bool,
/** /**
* Determines whether the keyboard gets dismissed in response to a drag. * Determines whether the keyboard gets dismissed in response to a drag.
* - 'none' (the default), drags do not dismiss the keyboard. * - '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 * - 'interactive', the keyboard is dismissed interactively with the drag
* and moves in synchrony with the touch; dragging upwards cancels the * and moves in synchrony with the touch; dragging upwards cancels the
* dismissal. * dismissal.
@ -156,35 +170,83 @@ var ScrollView = React.createClass({
* is up dismisses the keyboard. When true, the scroll view will not catch * is up dismisses the keyboard. When true, the scroll view will not catch
* taps, and the keyboard will not dismiss automatically. The default value * taps, and the keyboard will not dismiss automatically. The default value
* is false. * is false.
* @platform ios
*/ */
keyboardShouldPersistTaps: PropTypes.bool, keyboardShouldPersistTaps: PropTypes.bool,
/** /**
* The maximum allowed zoom scale. The default value is 1.0. * The maximum allowed zoom scale. The default value is 1.0.
* @platform ios
*/ */
maximumZoomScale: PropTypes.number, maximumZoomScale: PropTypes.number,
/** /**
* The minimum allowed zoom scale. The default value is 1.0. * The minimum allowed zoom scale. The default value is 1.0.
* @platform ios
*/ */
minimumZoomScale: PropTypes.number, 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 true, the scroll view stops on multiples of the scroll view's size
* when scrolling. This can be used for horizontal pagination. The default * when scrolling. This can be used for horizontal pagination. The default
* value is false. * value is false.
* @platform ios
*/ */
pagingEnabled: PropTypes.bool, 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. * When true, the scroll view scrolls to top when the status bar is tapped.
* The default value is true. * The default value is true.
* @platform ios
*/ */
scrollsToTop: PropTypes.bool, 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 * An array of child indices determining which children get docked to the
* top of the screen when scrolling. For example, passing * top of the screen when scrolling. For example, passing
* `stickyHeaderIndices={[0]}` will cause the first child to be fixed to the * `stickyHeaderIndices={[0]}` will cause the first child to be fixed to the
* top of the scroll view. This property is not supported in conjunction * top of the scroll view. This property is not supported in conjunction
* with `horizontal={true}`. * with `horizontal={true}`.
* @platform ios
*/ */
stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number), stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number),
style: StyleSheetPropType(ViewStylePropTypes),
/** /**
* Experimental: When true, offscreen child views (whose `overflow` value is * Experimental: When true, offscreen child views (whose `overflow` value is
* `hidden`) are removed from their native backing superview when offscreen. * `hidden`) are removed from their native backing superview when offscreen.
@ -194,6 +256,7 @@ var ScrollView = React.createClass({
removeClippedSubviews: PropTypes.bool, removeClippedSubviews: PropTypes.bool,
/** /**
* The current scale of the scroll view content. The default value is 1.0. * The current scale of the scroll view content. The default value is 1.0.
* @platform ios
*/ */
zoomScale: PropTypes.number, zoomScale: PropTypes.number,
}, },

View File

@ -11,20 +11,12 @@
*/ */
'use strict'; 'use strict';
var AnimationExperimental = require('AnimationExperimental'); var Animated = require('Animated');
var NativeMethodsMixin = require('NativeMethodsMixin'); var NativeMethodsMixin = require('NativeMethodsMixin');
var POPAnimation = require('POPAnimation');
var React = require('React'); var React = require('React');
var Touchable = require('Touchable'); var Touchable = require('Touchable');
var merge = require('merge'); 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 = { type State = {
animationID: ?number; animationID: ?number;
@ -58,40 +50,23 @@ var TouchableBounce = React.createClass({
}, },
getInitialState: function(): State { getInitialState: function(): State {
return merge(this.touchableGetInitialState(), {animationID: null}); return {
...this.touchableGetInitialState(),
scale: new Animated.Value(1),
};
}, },
bounceTo: function( bounceTo: function(
value: number, value: number,
velocity: number, velocity: number,
bounciness: number, bounciness: number,
fromValue?: ?number,
callback?: ?Function callback?: ?Function
) { ) {
if (POPAnimation) { Animated.spring(this.state.scale, {
this.state.animationID && this.removeAnimation(this.state.animationID); toValue: value,
var anim = { velocity,
property: POPAnimation.Properties.scaleXY, bounciness,
dynamicsTension: 0, }).start(callback);
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
);
}
}, },
/** /**
@ -109,13 +84,14 @@ var TouchableBounce = React.createClass({
touchableHandlePress: function() { touchableHandlePress: function() {
var onPressWithCompletion = this.props.onPressWithCompletion; var onPressWithCompletion = this.props.onPressWithCompletion;
if (onPressWithCompletion) { if (onPressWithCompletion) {
onPressWithCompletion( onPressWithCompletion(() => {
this.bounceTo.bind(this, 1, 10, 10, 0.93, this.props.onPressAnimationComplete) this.state.scale.setValue(0.93);
); this.bounceTo(1, 10, 10, this.props.onPressAnimationComplete);
});
return; return;
} }
this.bounceTo(1, 10, 10, undefined, this.props.onPressAnimationComplete); this.bounceTo(1, 10, 10, this.props.onPressAnimationComplete);
this.props.onPress && this.props.onPress(); this.props.onPress && this.props.onPress();
}, },
@ -127,18 +103,21 @@ var TouchableBounce = React.createClass({
return 0; return 0;
}, },
render: function() { render: function(): ReactElement {
var child = onlyChild(this.props.children); return (
return React.cloneElement(child, { <Animated.View
accessible: true, style={[{transform: [{scale: this.state.scale}]}, this.props.style]}
testID: this.props.testID, accessible={true}
onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, testID={this.props.testID}
onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
onResponderGrant: this.touchableHandleResponderGrant, onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
onResponderMove: this.touchableHandleResponderMove, onResponderGrant={this.touchableHandleResponderGrant}
onResponderRelease: this.touchableHandleResponderRelease, onResponderMove={this.touchableHandleResponderMove}
onResponderTerminate: this.touchableHandleResponderTerminate 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({ var ElementProperties = React.createClass({
propTypes: { propTypes: {
hierarchy: PropTypes.array.isRequired, hierarchy: PropTypes.array.isRequired,
style: PropTypes.array.isRequired, style: PropTypes.oneOfType([
PropTypes.object,
PropTypes.array,
PropTypes.number,
]),
}, },
render: function() { render: function() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,10 +16,10 @@
@protocol RCTURLRequestDelegate <NSObject> @protocol RCTURLRequestDelegate <NSObject>
/** /**
* Call this when you first receives a response from the server. This should * Call this when you send request data to the server. This is used to track
* include response headers, etc. * 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 * 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. # Copyright (c) 2015-present, Facebook, Inc.
# All rights reserved. # All rights reserved.

View File

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