forked from zulip/zulip-mobile
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
sharing: Add UI to receive and handle shares from other apps.
This completes most of the implementation of zulip#117. To enable the feature as it currently exists, it's enough to uncomment the commented-out bit of the manifest, so that Zulip appears in the "sharing" UI when attempting to share something from another app. The reason we don't yet enable/advertise the feature is that there are some blocker bugs -- most notably, it crashes when you try to use it. That didn't use to happen, on a branch based on an older version from master, but it does now after rebasing. (The error message suggests it's probably related to our recent upgrades of the RN version we use.) The 'react-navigation' code was suggested by Chris Bobbe. Suggested-in-part-by: Chris Bobbe <[email protected]>
- Loading branch information
Showing
11 changed files
with
685 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}; | ||
|
||
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); |
Oops, something went wrong.