diff --git a/src/compose/ComposeBox.js b/src/compose/ComposeBox.android.js similarity index 100% rename from src/compose/ComposeBox.js rename to src/compose/ComposeBox.android.js diff --git a/src/compose/ComposeBox.ios.js b/src/compose/ComposeBox.ios.js new file mode 100644 index 00000000000..68477ad8324 --- /dev/null +++ b/src/compose/ComposeBox.ios.js @@ -0,0 +1,357 @@ +/* @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 type { + Auth, + Context, + Narrow, + EditMessage, + InputSelectionType, + User, + Dispatch, + Dimensions, + GlobalState, +} from '../types'; +import { + addToOutbox, + cancelEditMessage, + draftAdd, + draftRemove, + 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 { + getAuth, + getSession, + canSendToActiveNarrow, + getLastMessageTopic, + getActiveUsersAndBots, + getShowMessagePlaceholders, +} from '../selectors'; +import { getIsActiveStreamSubscribed } from '../subscriptions/subscriptionSelectors'; +import { getDraftForActiveNarrow } from '../drafts/draftsSelectors'; + +type Props = { + auth: Auth, + canSend: boolean, + narrow: Narrow, + usersAndBots: User[], + draft: string, + lastMessageTopic: string, + isSubscribed: boolean, + editMessage: EditMessage, + safeAreaInsets: Dimensions, + dispatch: Dispatch, + messageInputRef: (component: any) => void, +}; + +type State = { + isMessageFocused: boolean, + isTopicFocused: boolean, + isMenuExpanded: boolean, + topic: string, + message: string, + height: number, + selection: InputSelectionType, +}; + +class ComposeBox extends PureComponent { + context: Context; + props: Props; + state: State; + + messageInput: TextInput = null; + topicInput: TextInput = null; + + static contextTypes = { + styles: () => null, + }; + + state = { + isMessageFocused: false, + isTopicFocused: false, + isMenuExpanded: false, + height: 20, + topic: '', + message: this.props.draft, + selection: { start: 0, end: 0 }, + }; + + getCanSelectTopic = () => { + const { isMessageFocused, isTopicFocused } = this.state; + const { editMessage, narrow } = this.props; + if (editMessage) { + return isStreamOrTopicNarrow(narrow); + } + if (!isStreamNarrow(narrow)) { + return false; + } + return isMessageFocused || isTopicFocused; + }; + + handleComposeMenuToggle = () => { + this.setState(({ isMenuExpanded }) => ({ + isMenuExpanded: !isMenuExpanded, + })); + }; + + handleLayoutChange = (event: Object) => { + this.setState({ + height: event.nativeEvent.layout.height, + }); + }; + + handleTopicChange = (topic: string) => { + this.setState({ topic, isMenuExpanded: false }); + }; + + handleMessageChange = (message: string) => { + this.setState({ message, isMenuExpanded: false }); + const { dispatch, narrow } = this.props; + dispatch(sendTypingEvent(narrow)); + }; + + handleMessageSelectionChange = (event: Object) => { + const { selection } = event.nativeEvent; + this.setState({ selection }); + }; + + handleMessageFocus = () => { + const { topic } = this.state; + const { lastMessageTopic } = this.props; + this.setState({ + isMessageFocused: true, + isMenuExpanded: false, + }); + setTimeout(() => { + this.handleTopicChange(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 + }; + + handleTopicFocus = () => { + const { dispatch, narrow } = this.props; + this.setState({ + isTopicFocused: true, + 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 + }; + + handleInputTouchStart = () => { + this.setState({ isMenuExpanded: false }); + }; + + clearMessageInput = () => { + if (this.messageInput) { + this.messageInput.clear(); + if (TextInputReset) { + TextInputReset.resetKeyboardInput(findNodeHandle(this.messageInput)); + } + } + + this.handleMessageChange(''); + }; + + handleSend = () => { + const { dispatch, narrow } = this.props; + const { topic, message } = this.state; + + const destinationNarrow = isStreamNarrow(narrow) + ? topicNarrow(narrow[0].operand, topic || '(no topic)') + : narrow; + + dispatch(addToOutbox(destinationNarrow, message)); + dispatch(draftRemove(narrow)); + + this.clearMessageInput(); + }; + + 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; + if (content || subject) { + updateMessage(auth, { content, subject }, editMessage.id).catch(error => { + showErrorAlert(error.message, 'Failed to edit message'); + }); + } + dispatch(cancelEditMessage()); + }; + + tryUpdateDraft = () => { + const { dispatch, draft, narrow } = this.props; + const { message } = this.state; + + if (draft.trim() === message.trim()) { + return; + } + + if (message.trim().length === 0) { + dispatch(draftRemove(narrow)); + } else { + dispatch(draftAdd(narrow, message)); + } + }; + + componentWillUnmount() { + this.tryUpdateDraft(); + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.editMessage !== this.props.editMessage) { + const topic = + isStreamNarrow(nextProps.narrow) && nextProps.editMessage + ? nextProps.editMessage.topic + : ''; + this.handleMessageChange(nextProps.editMessage ? nextProps.editMessage.content : ''); + this.handleTopicChange(topic); + if (this.messageInput) { + this.messageInput.focus(); + } + } else if (!isEqual(nextProps.narrow, this.props.narrow)) { + this.tryUpdateDraft(); + + if (!nextProps.draft) { + this.clearMessageInput(); + } + + this.handleMessageChange(nextProps.draft); + } + } + + render() { + const { styles } = this.context; + const { isTopicFocused, isMenuExpanded, height, message, topic, selection } = this.state; + const { + auth, + canSend, + narrow, + usersAndBots, + editMessage, + safeAreaInsets, + messageInputRef, + isSubscribed, + } = this.props; + + if (!canSend) { + return null; + } + + if (!isSubscribed) { + return ; + } + + const placeholder = getComposeInputPlaceholder(narrow, auth.email, usersAndBots); + + return ( + + + + + + + + {this.getCanSelectTopic() && ( + { + this.topicInput = component; + }} + onChangeText={this.handleTopicChange} + onFocus={this.handleTopicFocus} + onBlur={this.handleTopicBlur} + onTouchStart={this.handleInputTouchStart} + value={topic} + /> + )} + { + if (component) { + this.messageInput = component; + messageInputRef(component); + } + }} + value={message} + onBlur={this.handleMessageBlur} + onChange={this.handleMessageChange} + onFocus={this.handleMessageFocus} + onSelectionChange={this.handleMessageSelectionChange} + onTouchStart={this.handleInputTouchStart} + /> + + + + + + + ); + } +} + +export default connect((state: GlobalState, props) => ({ + auth: getAuth(state), + usersAndBots: getActiveUsersAndBots(state), + safeAreaInsets: getSession(state).safeAreaInsets, + isSubscribed: getIsActiveStreamSubscribed(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);