Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 2 additions & 10 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,14 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="zulip" android:host="login" />
</intent-filter>
</activity>

<!-- Disabled while the feature is experimental. See #117 and #4124.
Copy link
Copy Markdown
Member

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 least tools/test android does)

<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<!-- Disabled while the feature is experimental. See #117 and #4124.
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
-->
</activity>
-->

<!-- When `react-native run-android` learns from the decoy `package`
attribute in our comment above that the application ID is
Expand Down
5 changes: 5 additions & 0 deletions src/boot/AppEventHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
Expand All @@ -121,6 +123,7 @@ class AppEventHandlers extends PureComponent<Props> {
componentDidMount() {
const { dispatch } = this.props;
handleInitialNotification(dispatch);
handleInitialShare(dispatch);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 handleInitialNotification, which looks for something that's already happened and acts on it, from the rest of this method which sets up various handlers to listen for things that happen in the future. So handleInitialShare should go above it, because it also looks for something that's already happened and acts on that.

this.netInfoDisconnectCallback = NetInfo.addEventListener(this.handleConnectivityChange);
AppState.addEventListener('change', this.handleAppStateChange);
Expand All @@ -130,6 +133,7 @@ class AppEventHandlers extends PureComponent<Props> {
);
ScreenOrientation.addOrientationChangeListener(this.handleOrientationChange);
this.notificationListener.start();
this.shareListener.start();
}

componentWillUnmount() {
Expand All @@ -141,6 +145,7 @@ class AppEventHandlers extends PureComponent<Props> {
AppState.removeEventListener('memoryWarning', this.handleMemoryWarning);
ScreenOrientation.removeOrientationChangeListeners();
this.notificationListener.stop();
this.shareListener.stop();
}

render() {
Expand Down
2 changes: 2 additions & 0 deletions src/nav/AppNavigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import TopicListScreen from '../topics/TopicListScreen';
import EmojiPickerScreen from '../emoji/EmojiPickerScreen';
import LegalScreen from '../settings/LegalScreen';
import UserStatusScreen from '../user-status/UserStatusScreen';
import SharingScreen from '../sharing/SharingScreen';

export default createStackNavigator(
// $FlowFixMe react-navigation types :-/ -- see a36814e80
Expand Down Expand Up @@ -65,6 +66,7 @@ export default createStackNavigator(
notifications: { screen: NotificationsScreen },
legal: { screen: LegalScreen },
'user-status': { screen: UserStatusScreen },
sharing: { screen: SharingScreen },
},
{
initialRouteName: 'main',
Expand Down
4 changes: 4 additions & 0 deletions src/nav/navActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
Narrow,
UserOrBot,
ApiResponseServerSettings,
SharedData,
} from '../types';
import { getSameRoutesCount } from '../selectors';

Expand Down Expand Up @@ -103,3 +104,6 @@ export const navigateToLegal = (): NavigationAction => StackActions.push({ route

export const navigateToUserStatus = (): NavigationAction =>
StackActions.push({ routeName: 'user-status' });

export const navigateToSharing = (sharedData: SharedData): NavigationAction =>
StackActions.push({ routeName: 'sharing', params: { sharedData } });
44 changes: 44 additions & 0 deletions src/sharing/ChooseRecipientsScreen.js
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);
225 changes: 225 additions & 0 deletions src/sharing/ShareToPm.js
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 src/sharing/send.js.

Then this handleSend function will just

  • deal with the component's state and props -- like collecting sharedData from the nav prop
  • do the bits of logic that actually do differ between these two versions, like computing the message's type, to, and content


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);
Loading