Updates from Thu, July 23
This commit is contained in:
commit
03dccfbf71
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var UIExplorerList = require('./UIExplorerList');
|
||||
var UIExplorerList = require('./UIExplorerList.ios');
|
||||
var {
|
||||
AppRegistry,
|
||||
NavigatorIOS,
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -14,4 +14,3 @@
|
|||
@interface RCTNetworking : NSObject <RCTBridgeModule>
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2015-present, Facebook, Inc.
|
||||
# All rights reserved.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2015-present, Facebook, Inc.
|
||||
# All rights reserved.
|
||||
|
|
Loading…
Reference in New Issue