-
-
Notifications
You must be signed in to change notification settings - Fork 674
[android] Handle receiving shares from other apps #4124
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ import { | |
| NotificationListener, | ||
| notificationOnAppActive, | ||
| } from '../notification'; | ||
| import { ShareReceivedListener, handleInitialShare } from '../sharing'; | ||
| import { appOnline, appOrientation, initSafeAreaInsets } from '../actions'; | ||
| import PresenceHeartbeat from '../presence/PresenceHeartbeat'; | ||
|
|
||
|
|
@@ -113,6 +114,7 @@ class AppEventHandlers extends PureComponent<Props> { | |
| }; | ||
|
|
||
| notificationListener = new NotificationListener(this.props.dispatch); | ||
| shareListener = new ShareReceivedListener(this.props.dispatch); | ||
|
|
||
| handleMemoryWarning = () => { | ||
| // Release memory here | ||
|
|
@@ -121,6 +123,7 @@ class AppEventHandlers extends PureComponent<Props> { | |
| componentDidMount() { | ||
| const { dispatch } = this.props; | ||
| handleInitialNotification(dispatch); | ||
| handleInitialShare(dispatch); | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general, better to leave a blank line than to just fill it in with new code -- they're often there for a reason, as separators. This one separates |
||
| this.netInfoDisconnectCallback = NetInfo.addEventListener(this.handleConnectivityChange); | ||
| AppState.addEventListener('change', this.handleAppStateChange); | ||
|
|
@@ -130,6 +133,7 @@ class AppEventHandlers extends PureComponent<Props> { | |
| ); | ||
| ScreenOrientation.addOrientationChangeListener(this.handleOrientationChange); | ||
| this.notificationListener.start(); | ||
| this.shareListener.start(); | ||
| } | ||
|
|
||
| componentWillUnmount() { | ||
|
|
@@ -141,6 +145,7 @@ class AppEventHandlers extends PureComponent<Props> { | |
| AppState.removeEventListener('memoryWarning', this.handleMemoryWarning); | ||
| ScreenOrientation.removeOrientationChangeListeners(); | ||
| this.notificationListener.stop(); | ||
| this.shareListener.stop(); | ||
| } | ||
|
|
||
| render() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| /* @flow strict-local */ | ||
| import React, { PureComponent } from 'react'; | ||
| import type { User, Dispatch } from '../types'; | ||
| import { connect } from '../react-redux'; | ||
| import { Screen } from '../common'; | ||
| import UserPickerCard from '../user-picker/UserPickerCard'; | ||
|
|
||
| type Props = $ReadOnly<{| | ||
| dispatch: Dispatch, | ||
| onComplete: (User[]) => void, | ||
| |}>; | ||
|
|
||
| type State = {| | ||
| filter: string, | ||
| |}; | ||
|
|
||
| class ChooseRecipientsScreen extends PureComponent<Props, State> { | ||
| state = { | ||
| filter: '', | ||
| }; | ||
|
|
||
| handleFilterChange = (filter: string) => this.setState({ filter }); | ||
|
|
||
| handleComplete = (selected: Array<User>) => { | ||
| const { onComplete } = this.props; | ||
| onComplete(selected); | ||
| }; | ||
|
|
||
| render() { | ||
| const { filter } = this.state; | ||
| return ( | ||
| <Screen | ||
| search | ||
| scrollEnabled={false} | ||
| searchBarOnChange={this.handleFilterChange} | ||
| canGoBack={false} | ||
| > | ||
| <UserPickerCard filter={filter} onComplete={this.handleComplete} /> | ||
| </Screen> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| export default connect<{||}, _, _>()(ChooseRecipientsScreen); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| /* @flow strict-local */ | ||
| import React from 'react'; | ||
| import { View, StyleSheet, Image, ScrollView, Modal, BackHandler } from 'react-native'; | ||
| import { type NavigationNavigatorProps } from 'react-navigation'; | ||
| import type { Dispatch, SharedData, User, Auth, GetText } from '../types'; | ||
| import { TranslationContext } from '../boot/TranslationProvider'; | ||
|
|
||
| import { connect } from '../react-redux'; | ||
| import { ZulipButton, Input, Label } from '../common'; | ||
| import UserItem from '../users/UserItem'; | ||
| import { sendMessage, uploadFile } from '../api'; | ||
| import { getAuth } from '../selectors'; | ||
| import { showToast } from '../utils/info'; | ||
| import { navigateBack } from '../nav/navActions'; | ||
| import ChooseRecipientsScreen from './ChooseRecipientsScreen'; | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| wrapper: { | ||
| flex: 1, | ||
| padding: 10, | ||
| }, | ||
| imagePreview: { | ||
| margin: 10, | ||
| borderRadius: 5, | ||
| width: 200, | ||
| height: 200, | ||
| }, | ||
| container: { | ||
| flex: 1, | ||
| }, | ||
| actions: { | ||
| flexDirection: 'row', | ||
| }, | ||
| button: { | ||
| flex: 1, | ||
| margin: 8, | ||
| }, | ||
| usersPreview: { | ||
| padding: 10, | ||
| }, | ||
| chooseButton: { | ||
| marginTop: 8, | ||
| marginBottom: 8, | ||
| width: '50%', | ||
| alignSelf: 'flex-end', | ||
| }, | ||
| message: { | ||
| height: 70, | ||
| borderRadius: 5, | ||
| borderWidth: 1, | ||
| borderColor: 'whitesmoke', | ||
| padding: 5, | ||
| }, | ||
| }); | ||
|
|
||
| type Props = $ReadOnly<{| | ||
| ...$Exact<NavigationNavigatorProps<{||}, {| params: {| sharedData: SharedData |} |}>>, | ||
| dispatch: Dispatch, | ||
| auth: Auth, | ||
| |}>; | ||
|
|
||
| type State = $ReadOnly<{| | ||
| selectedRecipients: User[], | ||
| message: string, | ||
| choosingRecipients: boolean, | ||
| sending: boolean, | ||
| |}>; | ||
|
|
||
| class ShareToPm extends React.Component<Props, State> { | ||
| static contextType = TranslationContext; | ||
| context: GetText; | ||
|
|
||
| constructor(props) { | ||
| super(props); | ||
| const { sharedData } = this.props.navigation.state.params; | ||
| this.state = { | ||
| selectedRecipients: [], | ||
| message: sharedData.type === 'text' ? sharedData.sharedText : '', | ||
| choosingRecipients: false, | ||
| sending: false, | ||
| }; | ||
| } | ||
|
|
||
| setSending = () => { | ||
| this.setState({ sending: true }); | ||
| }; | ||
|
|
||
| handleChooseRecipients = (selectedRecipients: Array<User>) => { | ||
| this.setState({ selectedRecipients }); | ||
| this.setState({ choosingRecipients: false }); | ||
| }; | ||
|
|
||
| handleSend = async () => { | ||
| this.setSending(); | ||
|
|
||
| const _ = this.context; | ||
| const { selectedRecipients, message } = this.state; | ||
| let messageToSend = message; | ||
| const { auth } = this.props; | ||
| const { sharedData } = this.props.navigation.state.params; | ||
| const to = JSON.stringify(selectedRecipients.map(user => user.user_id)); | ||
|
|
||
| try { | ||
| showToast(_('Sending Message...')); | ||
|
|
||
| if (sharedData.type === 'image' || sharedData.type === 'file') { | ||
| const url = | ||
| sharedData.type === 'image' ? sharedData.sharedImageUrl : sharedData.sharedFileUrl; | ||
| const fileName = url.split('/').pop(); | ||
| const response = await uploadFile(auth, url, fileName); | ||
| messageToSend += `\n[${fileName}](${response.uri})`; | ||
| } | ||
| await sendMessage(auth, { content: messageToSend, type: 'private', to }); | ||
| } catch (err) { | ||
| showToast(_('Failed to send message')); | ||
| this.finishShare(); | ||
| return; | ||
| } | ||
|
|
||
| showToast(_('Message sent')); | ||
| this.finishShare(); | ||
| }; | ||
|
Comment on lines
+93
to
+122
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's quite a bit of nontrivial logic appearing twice, between here and ShareToStream. Rather than duplicate it, let's pull that out into a common function -- perhaps in a file like Then this
|
||
|
|
||
| finishShare = () => { | ||
| const { dispatch } = this.props; | ||
|
|
||
| dispatch(navigateBack()); | ||
| BackHandler.exitApp(); | ||
| }; | ||
|
|
||
| handleMessageChange = message => { | ||
| this.setState({ message }); | ||
| }; | ||
|
|
||
| isSendButtonEnabled = () => { | ||
| const { message, selectedRecipients } = this.state; | ||
| const { sharedData } = this.props.navigation.state.params; | ||
|
|
||
| if (sharedData.type === 'text') { | ||
| return message !== '' && selectedRecipients.length > 0; | ||
| } | ||
|
|
||
| return selectedRecipients.length > 0; | ||
| }; | ||
|
|
||
| renderUsersPreview = () => { | ||
| const { selectedRecipients } = this.state; | ||
|
|
||
| if (selectedRecipients.length === 0) { | ||
| return <Label text="Please choose recipients to share with" />; | ||
| } | ||
| const preview = []; | ||
| selectedRecipients.forEach((user: User) => { | ||
| preview.push( | ||
| <UserItem | ||
| avatarUrl={user.avatar_url} | ||
| email={user.email} | ||
| fullName={user.full_name} | ||
| onPress={() => {}} | ||
| key={user.user_id} | ||
| />, | ||
| ); | ||
| }); | ||
| return preview; | ||
| }; | ||
|
|
||
| render() { | ||
| const { message, choosingRecipients, sending } = this.state; | ||
|
|
||
| if (choosingRecipients) { | ||
| return ( | ||
| <Modal> | ||
| <ChooseRecipientsScreen onComplete={this.handleChooseRecipients} /> | ||
| </Modal> | ||
| ); | ||
| } | ||
|
|
||
| const { sharedData } = this.props.navigation.state.params; | ||
| let sharePreview = null; | ||
| if (sharedData.type === 'image') { | ||
| sharePreview = ( | ||
| <Image | ||
| source={{ uri: sharedData.sharedImageUrl }} | ||
| width={200} | ||
| height={200} | ||
| style={styles.imagePreview} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <> | ||
| <ScrollView style={styles.wrapper} keyboardShouldPersistTaps="always"> | ||
| <View style={styles.container}>{sharePreview}</View> | ||
| <View style={styles.usersPreview}>{this.renderUsersPreview()}</View> | ||
| <ZulipButton | ||
| onPress={() => this.setState({ choosingRecipients: true })} | ||
| style={styles.chooseButton} | ||
| text="Choose recipients" | ||
| /> | ||
| <Input | ||
| value={message} | ||
| placeholder="Message" | ||
| onChangeText={this.handleMessageChange} | ||
| multiline | ||
| /> | ||
| </ScrollView> | ||
| <View style={styles.actions}> | ||
| <ZulipButton onPress={this.finishShare} style={styles.button} secondary text="Cancel" /> | ||
| <ZulipButton | ||
| style={styles.button} | ||
| onPress={this.handleSend} | ||
| text="Send" | ||
| progress={sending} | ||
| disabled={!this.isSendButtonEnabled()} | ||
| /> | ||
| </View> | ||
| </> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| export default connect(state => ({ | ||
| auth: getAuth(state), | ||
| }))(ShareToPm); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need to delete closing
-->too -- as is, the XML doesn't parse, and so e.g. for me Gradle sync doesn't work (though the actual build seems to work fine! or at leasttools/test androiddoes)