Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Share module #5904

Closed
wants to merge 49 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
a76c6c0
Add Share module
deminoth Feb 13, 2016
424a665
change the way handling promise reject
deminoth Feb 13, 2016
5da16c7
buck file added
deminoth Feb 13, 2016
8c3bece
added buck target
deminoth Feb 13, 2016
1f5f0c0
add share module to flow file
deminoth Feb 13, 2016
fb5f6ea
Merge branch 'master' into share-module
deminoth Feb 13, 2016
f21d709
options handling fixed
deminoth Feb 13, 2016
8917788
Keyword "if" must be followed by whitespace.
deminoth Feb 14, 2016
767f914
Merge branch 'master' into share-module
deminoth Feb 15, 2016
de58c94
Merge branch 'master' into share-module
deminoth Apr 7, 2016
68804ca
refactoring
deminoth Apr 7, 2016
fb88c94
fix lint errors
deminoth Apr 7, 2016
9917041
Merge branch 'master' into share-module
deminoth Apr 7, 2016
72c6d50
improves platform check
deminoth Apr 8, 2016
2678aee
show chooser when could not get current activity
deminoth Apr 8, 2016
32847bb
logs when exception
deminoth Apr 8, 2016
0371750
fix CI errors
deminoth Apr 8, 2016
dfe1f63
change invariant module path
deminoth Apr 8, 2016
772523e
Merge branch 'share-module' of https://github.com/dobbit/react-native…
deminoth Apr 8, 2016
43209dc
update example (+3 squashed commits)
deminoth Apr 19, 2016
74e6966
uses legacy http library for robolectric 3.0 (see https://github.com/…
deminoth Apr 20, 2016
dc7fdaa
add a unit test for share module
deminoth Apr 20, 2016
7f39ae3
fix flow errors
deminoth Apr 20, 2016
08c29bd
Merge branch 'master' into share-module
deminoth Apr 20, 2016
375ee94
better invariant
deminoth Apr 26, 2016
851c4ea
Merge branch 'master' into share-module
deminoth May 2, 2016
4d6add8
indent fix
deminoth May 2, 2016
e398fd9
Fix up this pattern var React = require('react-native');
deminoth May 2, 2016
71a67fb
It's enough to use a mock Activity
deminoth May 2, 2016
4351eec
promise check and test case for call with an invalid content
deminoth May 2, 2016
92a793b
import missed
deminoth May 2, 2016
68ca817
replace `shadowOf` with `ShadowExtractor.extract`
deminoth May 2, 2016
d3e13ed
Merge branch 'master' into share-module
deminoth May 6, 2016
ae290f0
add jsr dep for @Nullable annotation
deminoth May 6, 2016
918aaf6
Merge branch 'master' into share-module
deminoth Jul 11, 2016
f4d7d3a
replace `Intent.createChooser` with custom share dialog
deminoth Jul 12, 2016
29b5e3b
android integration tests
deminoth Jul 13, 2016
db99ac4
lint fix
deminoth Jul 13, 2016
d0f6fb8
Block launching activity for next test cases
deminoth Jul 13, 2016
bc14456
back to `Intent.createChooser`
deminoth Jul 14, 2016
a404f49
dep for the `@Nullable` annotation
deminoth Jul 14, 2016
9c896e5
documentation update
deminoth Jul 14, 2016
de1ab8f
Merge branch 'master' into share-module
deminoth Jul 15, 2016
5127f30
move example file (#2f73ca8)
deminoth Jul 15, 2016
0834fc7
Merge branch 'master' into share-module
deminoth Jul 18, 2016
994cda0
Merge branch 'master' into share-module
deminoth Jul 22, 2016
2b52ff5
fix unit tests
deminoth Jul 22, 2016
dc1c57c
do not log the error if we're already rejecting
deminoth Jul 25, 2016
94023f9
`url` in `content` prop is iOS only now
deminoth Jul 25, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions Examples/UIExplorer/ShareExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

property _showResult Property not found in ShareMessageExample

* Facebook reserves all rights not expressly granted.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

property shareText Property not found in ShareMessageExample

*
* 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 {
StyleSheet,
View,
Text,
TextInput,
TouchableHighlight,
Share,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

property Share Property not found in Object.create

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to add it to the react-native.js.flow file.

} = React;

exports.framework = 'React';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

semi: Missing semicolon.

exports.title = 'Share';
exports.description = 'Share data with other Apps.';
exports.examples = [{
title: 'Share Text Content',
render() {
return <ShareMessageExample />;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keyword-spacing: Expected space(s) after "if".

}];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

semi: Missing semicolon.


class ShareMessageExample extends React.Component {

constructor(props) {
super(props);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

semi: Missing semicolon.


this.shareMessage = this.shareMessage.bind(this);
this.shareTextContent = this.shareTextContent.bind(this);

this.state = {
result: ''
};
}

render() {
return (
<View>
<TouchableHighlight style={styles.wrapper}
onPress={this.shareMessage}>
<View style={styles.button}>
<Text>Click to share message</Text>
</View>
</TouchableHighlight>
<TouchableHighlight style={styles.wrapper}
onPress={this.shareTextContent}>
<View style={styles.button}>
<Text>Click to share message, URL and subject</Text>
</View>
</TouchableHighlight>
<Text>{this.state.result}</Text>
</View>
);
}

shareMessage() {
Share.shareTextContent({
message: 'React Native | A framework for building native apps using React'
})
.then((result) => {
if(!result) {
this.setState({result:'Canceled'})
} else {
this.setState({result:result})
}
})
.catch((err) => console.error(err))
}
shareTextContent() {
Share.shareTextContent({
message: 'A framework for building native apps using React',
url: 'http://facebook.github.io/react-native/',
subject: 'React Native'
}, {
dialogTitle: 'Share React Native website',
excludedActivityTypes: [
'com.apple.UIKit.activity.PostToTwitter'
]
})
.then((result) => this.setState({result:result}))
.catch(()=> this.setState({result:'Canceled'}))
}

}


var styles = StyleSheet.create({
wrapper: {
borderRadius: 5,
marginBottom: 5,
},
button: {
backgroundColor: '#eeeeee',
padding: 10,
},
});
1 change: 1 addition & 0 deletions Examples/UIExplorer/UIExplorerList.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ var APIS = [
require('./NetInfoExample'),
require('./PanResponderExample'),
require('./PointerEventsExample'),
require('./ShareExample'),
require('./TimePickerAndroidExample'),
require('./TimerExample'),
require('./ToastAndroidExample.android'),
Expand Down
1 change: 1 addition & 0 deletions Examples/UIExplorer/UIExplorerList.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ var APIS = [
require('./PointerEventsExample'),
require('./PushNotificationIOSExample'),
require('./RCTRootViewIOSExample'),
require('./ShareExample'),
require('./StatusBarIOSExample'),
require('./TimerExample'),
require('./TransformExample'),
Expand Down
81 changes: 81 additions & 0 deletions Libraries/Share/Share.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-trailing-spaces: Trailing spaces not allowed.

* 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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-trailing-spaces: Trailing spaces not allowed.

* of patent rights can be found in the PATENTS file in the same directory.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-trailing-spaces: Trailing spaces not allowed.

*

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-trailing-spaces: Trailing spaces not allowed.

* @providesModule Share

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-trailing-spaces: Trailing spaces not allowed.

* @flow
*/
'use strict';

const Platform = require('Platform');
const {
ActionSheetManager,
ShareModule
} = require('NativeModules');
const invariant = require('invariant');
const processColor = require('processColor');

class Share {

/**
* Open a dialog to share text contents.
*
* ### Contents
*
* - `message` - a message to share
* - `url` - an URL to share. In Android, this will overwrite message
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better just to ignore this prop on Android instead of overwriting message. It becomes more explicit then, and the user can decide what to do. For example, I would usually prefer to combine the message and url into a single text on Android.

* - `subject` - subject of the message
*
* At least one of URL and message is required.
*
* ### Options
*
* #### iOS
*
* - `excludedActivityTypes`
* - `tintColor`
*
* #### Android
*
* - `dialogTitle`
*/
static shareTextContent(contents: Object, options?: Object): Promise<boolean> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: call the argument content, not contents

invariant(
typeof contents === 'object' && contents !== null,
'Contents must a valid object'
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-trailing-spaces: Trailing spaces not allowed.

invariant(
contents.url || contents.message,
'At least one of URL and message is required'
);
for(let content in contents) {
invariant(
typeof content === 'string',
'Invalid Content: should be a string. Was: ' + content
);
}
options = options || {};
return Platform.OS === 'android'
? ShareModule.shareTextContent(contents, options.dialogTitle)
: new Promise((resolve, reject) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add if statements for each Platform type.

ActionSheetManager.showShareActionSheetWithOptions(
{...contents, tintColor: processColor(options.tintColor)},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

property tintColor Property cannot be accessed on possibly undefined value undefined

console.error,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should reject the promise instead of logging.

(success, activityType) => {
if(success) {
resolve(activityType)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the value of activityType? Probably we shouldn't be resolving with a different value based on Platform.

One solution is to resolve with an object with platform-specific properties.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is iOS only and value is ?string (null if information is not available on native side). I've annotated these return types in my PR (linked below), might be helpful as a reference.

} else {
reject()
}
}
);
});
}

}

module.exports = Share;
1 change: 1 addition & 0 deletions Libraries/react-native/react-native.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ var ReactNative = {
get PixelRatio() { return require('PixelRatio'); },
get PushNotificationIOS() { return require('PushNotificationIOS'); },
get Settings() { return require('Settings'); },
get Share() { return require('Share'); },
get StatusBarIOS() { return require('StatusBarIOS'); },
get StyleSheet() { return require('StyleSheet'); },
get TimePickerAndroid() { return require('TimePickerAndroid'); },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

package com.facebook.react.modules.share;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;

import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;

/**
* Intent module. Launch other activities or open URLs.
*/
public class ShareModule extends ReactContextBaseJavaModule {

private Promise mPromise;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this promise a local variable since it doesn't depend on out stuff.


public ShareModule(ReactApplicationContext reactContext) {
super(reactContext);
}

@Override
public String getName() {
return "ShareModule";
}

/**
* Open a chooser dialog to send text contents to other apps.
*
* Refer http://developer.android.com/intl/ko/training/sharing/send.html
*
* @param contents the data to send
* @param title the title of the chooser dialog
*/
@ReactMethod
public void shareTextContent(ReadableMap contents, String title, final Promise promise) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the promise final?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a test for this, checking that: the intent is built correctly, the currentActivity starts the correct intent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: shareTextContent -> shareText.

Activity currentActivity = getCurrentActivity();

if (currentActivity == null) {
promise.reject("Activity doesn't exist");
return;
}

if (contents == null) {
throw new JSApplicationIllegalArgumentException("Invalid contents");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's promise.reject here also instead of a runtime execption?

}

mPromise = promise;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?


try {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setTypeAndNormalize("text/plain");

if(contents.hasKey("subject")) {
intent.putExtra(Intent.EXTRA_SUBJECT, contents.getString("subject"));
}

if(contents.hasKey("message")) {
intent.putExtra(Intent.EXTRA_TEXT, contents.getString("message"));
}

if(contents.hasKey("url")) {
intent.putExtra(Intent.EXTRA_TEXT, contents.getString("url")); // this will overwrite message
}

currentActivity.startActivity(Intent.createChooser(intent, title));
//TODO: use createChooser (Intent target, CharSequence title, IntentSender sender) after API level 22
mPromise.resolve(true);
mPromise = null;
} catch (Exception e) {
mPromise.reject("Failed to open share dialog");
mPromise = null;
}

}


}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.facebook.react.modules.location.LocationModule;
import com.facebook.react.modules.netinfo.NetInfoModule;
import com.facebook.react.modules.network.NetworkingModule;
import com.facebook.react.modules.share.ShareModule;
import com.facebook.react.modules.statusbar.StatusBarModule;
import com.facebook.react.modules.storage.AsyncStorageModule;
import com.facebook.react.modules.timepicker.TimePickerDialogModule;
Expand Down Expand Up @@ -78,6 +79,7 @@ public List<NativeModule> createNativeModules(ReactApplicationContext reactConte
new LocationModule(reactContext),
new NetworkingModule(reactContext),
new NetInfoModule(reactContext),
new ShareModule(reactContext),
new StatusBarModule(reactContext),
new TimePickerDialogModule(reactContext),
new ToastModule(reactContext),
Expand Down