diff --git a/src/compose/ComposeBox.android.js b/src/compose/ComposeBox.android.js index c31384a3151..675f1f0f0fd 100644 --- a/src/compose/ComposeBox.android.js +++ b/src/compose/ComposeBox.android.js @@ -1,9 +1,15 @@ /* @flow */ -import React, { PureComponent } from 'react'; -import { View, TextInput, findNodeHandle } from 'react-native'; -import { connect } from 'react-redux'; -import TextInputReset from 'react-native-text-input-reset'; -import isEqual from 'lodash.isequal'; +import React, { PureComponent } from 'react' +import { + View, + TextInput, + findNodeHandle, + Image, + ScrollView +} from 'react-native' +import { connect } from 'react-redux' +import TextInputReset from 'react-native-text-input-reset' +import isEqual from 'lodash.isequal' import type { Auth, @@ -15,25 +21,32 @@ import type { Dispatch, Dimensions, GlobalState, -} from '../types'; + DraftImagesState +} from '../types' import { addToOutbox, cancelEditMessage, draftAdd, draftRemove, + draftImagesAdd, + draftImagesRemove, fetchTopicsForActiveStream, - sendTypingEvent, -} from '../actions'; -import { updateMessage } from '../api'; -import { FloatingActionButton, Input, MultilineInput } from '../common'; -import { showErrorAlert } from '../utils/info'; -import { IconDone, IconSend } from '../common/Icons'; -import { isStreamNarrow, isStreamOrTopicNarrow, topicNarrow } from '../utils/narrow'; -import ComposeMenu from './ComposeMenu'; -import AutocompleteViewWrapper from '../autocomplete/AutocompleteViewWrapper'; -import getComposeInputPlaceholder from './getComposeInputPlaceholder'; -import NotSubscribed from '../message/NotSubscribed'; -import AnnouncementOnly from '../message/AnnouncementOnly'; + sendTypingEvent +} from '../actions' +import { updateMessage, uploadFile } from '../api' +import { FloatingActionButton, Input, MultilineInput } from '../common' +import { showErrorAlert } from '../utils/info' +import { IconDone, IconSend, IconCross } from '../common/Icons' +import { + isStreamNarrow, + isStreamOrTopicNarrow, + topicNarrow +} from '../utils/narrow' +import ComposeMenu from './ComposeMenu' +import AutocompleteViewWrapper from '../autocomplete/AutocompleteViewWrapper' +import getComposeInputPlaceholder from './getComposeInputPlaceholder' +import NotSubscribed from '../message/NotSubscribed' +import AnnouncementOnly from '../message/AnnouncementOnly' import { getAuth, @@ -42,13 +55,14 @@ import { canSendToActiveNarrow, getLastMessageTopic, getActiveUsers, - getShowMessagePlaceholders, -} from '../selectors'; + getShowMessagePlaceholders +} from '../selectors' import { getIsActiveStreamSubscribed, - getIsActiveStreamAnnouncementOnly, -} from '../subscriptions/subscriptionSelectors'; -import { getDraftForActiveNarrow } from '../drafts/draftsSelectors'; + getIsActiveStreamAnnouncementOnly +} from '../subscriptions/subscriptionSelectors' +import { getDraftForActiveNarrow } from '../drafts/draftsSelectors' +import { getDraftImageData } from '../draftImages/draftImagesSelectors' type Props = { auth: Auth, @@ -56,6 +70,7 @@ type Props = { narrow: Narrow, users: User[], draft: string, + draftImages: DraftImagesState, lastMessageTopic: string, isAdmin: boolean, isAnnouncementOnly: boolean, @@ -63,8 +78,8 @@ type Props = { editMessage: EditMessage, safeAreaInsets: Dimensions, dispatch: Dispatch, - messageInputRef: (component: any) => void, -}; + messageInputRef: (component: any) => void +} type State = { isMessageFocused: boolean, @@ -73,36 +88,36 @@ type State = { topic: string, message: string, height: number, - selection: InputSelectionType, -}; + selection: InputSelectionType +} export const updateTextInput = (textInput: TextInput, text: string): void => { if (!textInput) { // Depending on the lifecycle events this function is called from, // this might not be set yet. - return; + return } - textInput.setNativeProps({ text }); + textInput.setNativeProps({ text }) if (text.length === 0 && TextInputReset) { // React Native has problems with some custom keyboards when clearing // the input's contents. Force reset to make sure it works. - TextInputReset.resetKeyboardInput(findNodeHandle(textInput)); + TextInputReset.resetKeyboardInput(findNodeHandle(textInput)) } -}; +} class ComposeBox extends PureComponent { - context: Context; - props: Props; - state: State; + context: Context + props: Props + state: State - messageInput: TextInput = null; - topicInput: TextInput = null; + messageInput: TextInput = null + topicInput: TextInput = null static contextTypes = { - styles: () => null, - }; + styles: () => null + } state = { isMessageFocused: false, @@ -111,160 +126,195 @@ class ComposeBox extends PureComponent { height: 20, topic: '', message: this.props.draft, - selection: { start: 0, end: 0 }, - }; + selection: { start: 0, end: 0 } + } getCanSelectTopic = () => { - const { isMessageFocused, isTopicFocused } = this.state; - const { editMessage, narrow } = this.props; + const { isMessageFocused, isTopicFocused } = this.state + const { editMessage, narrow, draftImages } = this.props if (editMessage) { - return isStreamOrTopicNarrow(narrow); + return isStreamOrTopicNarrow(narrow) } if (!isStreamNarrow(narrow)) { - return false; + return false } - return isMessageFocused || isTopicFocused; - }; + const hasImages = Boolean(Object.keys(draftImages).length) + return isMessageFocused || isTopicFocused || hasImages + } setMessageInputValue = (message: string) => { - updateTextInput(this.messageInput, message); - this.handleMessageChange(message); - }; + updateTextInput(this.messageInput, message) + this.handleMessageChange(message) + } setTopicInputValue = (topic: string) => { - updateTextInput(this.topicInput, topic); - this.handleTopicChange(topic); - }; + updateTextInput(this.topicInput, topic) + this.handleTopicChange(topic) + } handleComposeMenuToggle = () => { this.setState(({ isMenuExpanded }) => ({ - isMenuExpanded: !isMenuExpanded, - })); - }; + isMenuExpanded: !isMenuExpanded + })) + } + handleImageSelect = (imageEventObj: Object) => { + const { dispatch, response } = imageEventObj + if (!response.images || !response.images.length) { + return + } + const { topic } = this.state + const { lastMessageTopic } = this.props + const newTopic = topic || lastMessageTopic + this.setState({ topic: newTopic }) + for (image of response.images) { + dispatch(draftImagesAdd(Date.now().toString(), image.fileName, image.uri)) + } + } + handleRemoveDraftImage = (id: string) => { + const { dispatch } = this.props + dispatch(draftImagesRemove(id)) + } handleLayoutChange = (event: Object) => { this.setState({ - height: event.nativeEvent.layout.height, - }); - }; + height: event.nativeEvent.layout.height + }) + } handleTopicChange = (topic: string) => { - this.setState({ topic, isMenuExpanded: false }); - }; + this.setState({ topic, isMenuExpanded: false }) + } handleTopicAutocomplete = (topic: string) => { - this.setTopicInputValue(topic); - }; + this.setTopicInputValue(topic) + } handleMessageChange = (message: string) => { - this.setState({ message, isMenuExpanded: false }); - const { dispatch, narrow } = this.props; - dispatch(sendTypingEvent(narrow)); - }; + this.setState({ message, isMenuExpanded: false }) + const { dispatch, narrow } = this.props + dispatch(sendTypingEvent(narrow)) + } handleMessageAutocomplete = (message: string) => { - this.setMessageInputValue(message); - }; + this.setMessageInputValue(message) + } handleMessageSelectionChange = (event: Object) => { - const { selection } = event.nativeEvent; - this.setState({ selection }); - }; + const { selection } = event.nativeEvent + this.setState({ selection }) + } handleMessageFocus = () => { - const { topic } = this.state; - const { lastMessageTopic } = this.props; + const { topic } = this.state + const { lastMessageTopic } = this.props this.setState({ isMessageFocused: true, - isMenuExpanded: false, - }); + isMenuExpanded: false + }) setTimeout(() => { - this.setTopicInputValue(topic || lastMessageTopic); - }, 200); // wait, to hope the component is shown - }; + this.setTopicInputValue(topic || lastMessageTopic) + }, 200) // wait, to hope the component is shown + } handleMessageBlur = () => { setTimeout(() => { this.setState({ isMessageFocused: false, - isMenuExpanded: false, - }); - }, 200); // give a chance to the topic input to get the focus - }; + isMenuExpanded: false + }) + }, 200) // give a chance to the topic input to get the focus + } handleTopicFocus = () => { - const { dispatch, narrow } = this.props; + const { dispatch, narrow } = this.props this.setState({ isTopicFocused: true, - isMenuExpanded: false, - }); - dispatch(fetchTopicsForActiveStream(narrow)); - }; + isMenuExpanded: false + }) + dispatch(fetchTopicsForActiveStream(narrow)) + } handleTopicBlur = () => { setTimeout(() => { this.setState({ isTopicFocused: false, - isMenuExpanded: false, - }); - }, 200); // give a chance to the mesage input to get the focus - }; + isMenuExpanded: false + }) + }, 200) // give a chance to the mesage input to get the focus + } handleInputTouchStart = () => { - this.setState({ isMenuExpanded: false }); - }; + this.setState({ isMenuExpanded: false }) + } + + uploadAllDrafts = async () => { + const { dispatch, narrow, draftImages, auth } = this.props + const messageUris = [] + const imageIds = Object.keys(draftImages) + for (const id of imageIds) { + const imageObj = draftImages[id] + let serverImgUri = await uploadFile(auth, imageObj.uri, imageObj.fileName) + dispatch(draftImagesRemove(id)) + messageUris.push(`[${imageObj.fileName}](${serverImgUri})`) + } + return Promise.resolve(messageUris) + } handleSend = () => { - const { dispatch, narrow } = this.props; - const { topic, message } = this.state; + const { dispatch, narrow } = this.props + let { topic, message } = this.state const destinationNarrow = isStreamNarrow(narrow) ? topicNarrow(narrow[0].operand, topic || '(no topic)') - : narrow; - - dispatch(addToOutbox(destinationNarrow, message)); - dispatch(draftRemove(narrow)); + : narrow - this.setMessageInputValue(''); - }; + this.uploadAllDrafts().then(messageUris => { + message += '\n' + messageUris.join('\n') + if (message && message.length) { + dispatch(addToOutbox(destinationNarrow, message)) + dispatch(draftRemove(narrow)) + } + this.setMessageInputValue('') + }) + } handleEdit = () => { - const { auth, editMessage, dispatch } = this.props; - const { message, topic } = this.state; - const content = editMessage.content !== message ? message : undefined; - const subject = topic !== editMessage.topic ? topic : undefined; + const { auth, editMessage, dispatch } = this.props + const { message, topic } = this.state + const content = editMessage.content !== message ? message : undefined + const subject = topic !== editMessage.topic ? topic : undefined if (content || subject) { updateMessage(auth, { content, subject }, editMessage.id).catch(error => { - showErrorAlert(error.message, 'Failed to edit message'); - }); + showErrorAlert(error.message, 'Failed to edit message') + }) } - dispatch(cancelEditMessage()); - }; + dispatch(cancelEditMessage()) + } tryUpdateDraft = () => { - const { dispatch, draft, narrow } = this.props; - const { message } = this.state; + const { dispatch, draft, narrow } = this.props + const { message } = this.state if (draft.trim() === message.trim()) { - return; + return } if (message.trim().length === 0) { - dispatch(draftRemove(narrow)); + dispatch(draftRemove(narrow)) } else { - dispatch(draftAdd(narrow, message)); + dispatch(draftAdd(narrow, message)) } - }; + } componentDidMount() { - const { message, topic } = this.state; + const { message, topic } = this.state - updateTextInput(this.messageInput, message); - updateTextInput(this.topicInput, topic); + updateTextInput(this.messageInput, message) + updateTextInput(this.topicInput, topic) } componentWillUnmount() { - this.tryUpdateDraft(); + this.tryUpdateDraft() } componentWillReceiveProps(nextProps: Props) { @@ -272,23 +322,30 @@ class ComposeBox extends PureComponent { const topic = isStreamNarrow(nextProps.narrow) && nextProps.editMessage ? nextProps.editMessage.topic - : ''; - const message = nextProps.editMessage ? nextProps.editMessage.content : ''; - this.setMessageInputValue(message); - this.setTopicInputValue(topic); + : '' + const message = nextProps.editMessage ? nextProps.editMessage.content : '' + this.setMessageInputValue(message) + this.setTopicInputValue(topic) if (this.messageInput) { - this.messageInput.focus(); + this.messageInput.focus() } } else if (!isEqual(nextProps.narrow, this.props.narrow)) { - this.tryUpdateDraft(); + this.tryUpdateDraft() - this.setMessageInputValue(nextProps.draft); + this.setMessageInputValue(nextProps.draft) } } render() { - const { styles } = this.context; - const { isTopicFocused, isMenuExpanded, height, message, topic, selection } = this.state; + const { styles } = this.context + const { + isTopicFocused, + isMenuExpanded, + height, + message, + topic, + selection + } = this.state const { auth, canSend, @@ -300,20 +357,42 @@ class ComposeBox extends PureComponent { isAdmin, isAnnouncementOnly, isSubscribed, - } = this.props; + draftImages + } = this.props + + const { handleRemoveDraftImage } = this if (!canSend) { - return null; + return null } if (!isSubscribed) { - return ; + return } else if (isAnnouncementOnly && !isAdmin) { - return ; + return } - const placeholder = getComposeInputPlaceholder(narrow, auth.email, users); - + const placeholder = getComposeInputPlaceholder(narrow, auth.email, users) + let sendButtonDisabled = + message.trim().length === 0 && Object.keys(draftImages).length <= 0 + let imageViews = Object.keys(draftImages).map(id => { + return ( + + handleRemoveDraftImage(id)} + /> + + + ) + }) return ( { narrow={narrow} expanded={isMenuExpanded} onExpandContract={this.handleComposeMenuToggle} + onImageSelect={this.handleImageSelect} /> @@ -342,7 +422,7 @@ class ComposeBox extends PureComponent { placeholder="Topic" selectTextOnFocus textInputRef={component => { - this.topicInput = component; + this.topicInput = component }} onChangeText={this.handleTopicChange} onFocus={this.handleTopicFocus} @@ -350,13 +430,17 @@ class ComposeBox extends PureComponent { onTouchStart={this.handleInputTouchStart} /> )} + + {imageViews} + + { if (component) { - this.messageInput = component; - messageInputRef(component); + this.messageInput = component + messageInputRef(component) } }} onBlur={this.handleMessageBlur} @@ -371,13 +455,13 @@ class ComposeBox extends PureComponent { style={styles.composeSendButton} Icon={editMessage === null ? IconSend : IconDone} size={32} - disabled={message.trim().length === 0} + disabled={sendButtonDisabled} onPress={editMessage === null ? this.handleSend : this.handleEdit} /> - ); + ) } } @@ -388,8 +472,11 @@ export default connect((state: GlobalState, props) => ({ isAdmin: getIsAdmin(state), isAnnouncementOnly: getIsActiveStreamAnnouncementOnly(props.narrow)(state), isSubscribed: getIsActiveStreamSubscribed(props.narrow)(state), - canSend: canSendToActiveNarrow(props.narrow) && !getShowMessagePlaceholders(props.narrow)(state), + canSend: + canSendToActiveNarrow(props.narrow) && + !getShowMessagePlaceholders(props.narrow)(state), editMessage: getSession(state).editMessage, draft: getDraftForActiveNarrow(props.narrow)(state), - lastMessageTopic: getLastMessageTopic(props.narrow)(state), -}))(ComposeBox); + draftImages: getDraftImageData(state), + lastMessageTopic: getLastMessageTopic(props.narrow)(state) +}))(ComposeBox)