diff --git a/src/chat/ChatScreen.js b/src/chat/ChatScreen.js
index 3942caf4756..ac2ef085e67 100644
--- a/src/chat/ChatScreen.js
+++ b/src/chat/ChatScreen.js
@@ -1,5 +1,5 @@
/* @flow strict-local */
-import React from 'react';
+import React, { useCallback, useContext } from 'react';
import type { Node } from 'react';
import { useIsFocused } from '@react-navigation/native';
@@ -17,11 +17,17 @@ import InvalidNarrow from './InvalidNarrow';
import { fetchMessagesInNarrow } from '../message/fetchActions';
import ComposeBox from '../compose/ComposeBox';
import UnreadNotice from './UnreadNotice';
-import { canSendToNarrow } from '../utils/narrow';
+import { canSendToNarrow, caseNarrowDefault, keyFromNarrow } from '../utils/narrow';
import { getLoading, getSession } from '../directSelectors';
import { getFetchingForNarrow } from './fetchingSelectors';
import { getShownMessagesForNarrow, isNarrowValid as getIsNarrowValid } from './narrowsSelectors';
import { getFirstUnreadIdInNarrow } from '../message/messageSelectors';
+import { getDraftForNarrow } from '../drafts/draftsSelectors';
+import { addToOutbox } from '../actions';
+import { getAuth, getCaughtUpForNarrow } from '../selectors';
+import { showErrorAlert } from '../utils/info';
+import { TranslationContext } from '../boot/TranslationProvider';
+import * as api from '../api';
type Props = $ReadOnly<{|
navigation: AppNavigationProp<'chat'>,
@@ -103,10 +109,13 @@ export default function ChatScreen(props: Props): Node {
const { backgroundColor } = React.useContext(ThemeContext);
const { narrow, editMessage } = route.params;
- const setEditMessage = (value: EditMessage | null) =>
- navigation.setParams({ editMessage: value });
+ const setEditMessage = useCallback(
+ (value: EditMessage | null) => navigation.setParams({ editMessage: value }),
+ [navigation],
+ );
const isNarrowValid = useSelector(state => getIsNarrowValid(state, narrow));
+ const draft = useSelector(state => getDraftForNarrow(state, narrow));
const {
fetchError,
@@ -120,6 +129,43 @@ export default function ChatScreen(props: Props): Node {
const sayNoMessages = haveNoMessages && !isFetching;
const showComposeBox = canSendToNarrow(narrow) && !showMessagePlaceholders;
+ const auth = useSelector(getAuth);
+ const dispatch = useDispatch();
+ const caughtUp = useSelector(state => getCaughtUpForNarrow(state, narrow));
+ const _ = useContext(TranslationContext);
+
+ const sendCallback = useCallback(
+ (message: string, destinationNarrow: Narrow) => {
+ if (editMessage) {
+ const content = editMessage.content !== message ? message : undefined;
+ const subject = caseNarrowDefault(
+ destinationNarrow,
+ { topic: (stream, topic) => (topic !== editMessage.topic ? topic : undefined) },
+ () => undefined,
+ );
+
+ if (
+ (content !== undefined && content !== '')
+ || (subject !== undefined && subject !== '')
+ ) {
+ api.updateMessage(auth, { content, subject }, editMessage.id).catch(error => {
+ showErrorAlert(_('Failed to edit message'), error.message);
+ });
+ }
+
+ setEditMessage(null);
+ } else {
+ if (!caughtUp.newer) {
+ showErrorAlert(_('Failed to send message'));
+ return;
+ }
+
+ dispatch(addToOutbox(destinationNarrow, message));
+ }
+ },
+ [_, auth, caughtUp.newer, dispatch, editMessage, setEditMessage],
+ );
+
return (
@@ -147,8 +193,12 @@ export default function ChatScreen(props: Props): Node {
{showComposeBox && (
setEditMessage(null)}
+ isEditing={editMessage !== null}
+ initialTopic={editMessage ? editMessage.topic : undefined}
+ initialMessage={editMessage ? editMessage.content : draft}
+ onSend={sendCallback}
+ autoFocusMessage={editMessage !== null}
+ key={keyFromNarrow(narrow) + (editMessage?.id.toString() ?? 'noedit')}
/>
)}
diff --git a/src/compose/ComposeBox.js b/src/compose/ComposeBox.js
index 7daf3f88f92..765be789784 100644
--- a/src/compose/ComposeBox.js
+++ b/src/compose/ComposeBox.js
@@ -15,11 +15,9 @@ import { ThemeContext } from '../styles';
import type {
Auth,
Narrow,
- EditMessage,
InputSelection,
UserOrBot,
Dispatch,
- CaughtUp,
GetText,
Subscription,
Stream,
@@ -28,16 +26,17 @@ import type {
} from '../types';
import { connect } from '../react-redux';
import { withGetText } from '../boot/TranslationProvider';
-import { addToOutbox, draftUpdate, sendTypingStart, sendTypingStop } from '../actions';
-import * as api from '../api';
+import { draftUpdate, sendTypingStart, sendTypingStop } from '../actions';
import { FloatingActionButton, Input } from '../common';
-import { showErrorAlert, showToast } from '../utils/info';
+import { showToast } from '../utils/info';
import { IconDone, IconSend } from '../common/Icons';
import {
isStreamNarrow,
isStreamOrTopicNarrow,
+ isTopicNarrow,
streamNameOfNarrow,
topicNarrow,
+ topicOfNarrow,
} from '../utils/narrow';
import ComposeMenu from './ComposeMenu';
import getComposeInputPlaceholder from './getComposeInputPlaceholder';
@@ -45,22 +44,15 @@ import NotSubscribed from '../message/NotSubscribed';
import AnnouncementOnly from '../message/AnnouncementOnly';
import MentionWarnings from './MentionWarnings';
-import {
- getAuth,
- getIsAdmin,
- getLastMessageTopic,
- getCaughtUpForNarrow,
- getStreamInNarrow,
- getVideoChatProvider,
-} from '../selectors';
+import { getAuth, getIsAdmin, getStreamInNarrow, getVideoChatProvider } from '../selectors';
import {
getIsActiveStreamSubscribed,
getIsActiveStreamAnnouncementOnly,
} from '../subscriptions/subscriptionSelectors';
-import { getDraftForNarrow } from '../drafts/draftsSelectors';
import TopicAutocomplete from '../autocomplete/TopicAutocomplete';
import AutocompleteView from '../autocomplete/AutocompleteView';
import { getAllUsersById, getOwnUserId } from '../users/userSelectors';
+import * as api from '../api';
type SelectorProps = {|
auth: Auth,
@@ -69,21 +61,35 @@ type SelectorProps = {|
isAdmin: boolean,
isAnnouncementOnly: boolean,
isSubscribed: boolean,
- draft: string,
- lastMessageTopic: string,
- caughtUp: CaughtUp,
videoChatProvider: VideoChatProvider | null,
stream: Subscription | {| ...Stream, in_home_view: boolean |},
|};
type OuterProps = $ReadOnly<{|
narrow: Narrow,
- editMessage: EditMessage | null,
- completeEditMessage: () => void,
+
+ onSend: (string, Narrow) => void,
+
+ isEditing: boolean,
+
+ /** The contents of the message that the ComposeBox should contain when it's first rendered */
+ initialMessage?: string,
+ /** The topic of the message that the ComposeBox should contain when it's first rendered */
+ initialTopic?: string,
+
+ /** Whether the topic input box should auto-foucs when the component renders.
+ *
+ * Passed through to the TextInput's autofocus prop. */
+ autoFocusTopic?: boolean,
+ /** Whether the message input box should auto-foucs when the component renders.
+ *
+ * Passed through to the TextInput's autofocus prop. */
+ autoFocusMessage?: boolean,
|}>;
type Props = $ReadOnly<{|
...OuterProps,
+ ...SelectorProps,
// From 'withGetText'
_: GetText,
@@ -155,8 +161,10 @@ class ComposeBoxInner extends PureComponent {
isFocused: false,
isMenuExpanded: false,
height: 20,
- topic: this.props.lastMessageTopic,
- message: this.props.draft,
+ topic:
+ this.props.initialTopic
+ ?? (isTopicNarrow(this.props.narrow) ? topicOfNarrow(this.props.narrow) : ''),
+ message: this.props.initialMessage ?? '',
selection: { start: 0, end: 0 },
numUploading: 0,
};
@@ -174,8 +182,8 @@ class ComposeBoxInner extends PureComponent {
};
getCanSelectTopic = () => {
- const { editMessage, narrow } = this.props;
- if (editMessage) {
+ const { isEditing, narrow } = this.props;
+ if (isEditing) {
return isStreamOrTopicNarrow(narrow);
}
if (!isStreamNarrow(narrow)) {
@@ -291,9 +299,11 @@ class ComposeBoxInner extends PureComponent {
handleMessageChange = (message: string) => {
this.setState({ message, isMenuExpanded: false });
- const { dispatch, narrow } = this.props;
+ const { dispatch, isEditing, narrow } = this.props;
dispatch(sendTypingStart(narrow));
- dispatch(draftUpdate(narrow, message));
+ if (!isEditing) {
+ dispatch(draftUpdate(narrow, message));
+ }
};
// See JSDoc on 'onAutocomplete' in 'AutocompleteView.js'.
@@ -320,13 +330,11 @@ class ComposeBoxInner extends PureComponent {
};
handleMessageFocus = () => {
- this.setState((state, { lastMessageTopic }) => ({
- ...state,
- topic: state.topic || lastMessageTopic,
+ this.setState({
isMessageFocused: true,
isFocused: true,
isMenuExpanded: false,
- }));
+ });
};
handleMessageBlur = () => {
@@ -362,8 +370,8 @@ class ComposeBoxInner extends PureComponent {
};
getDestinationNarrow = (): Narrow => {
- const { narrow } = this.props;
- if (isStreamNarrow(narrow)) {
+ const { narrow, isEditing } = this.props;
+ if (isStreamNarrow(narrow) || (isTopicNarrow(narrow) && isEditing)) {
const streamName = streamNameOfNarrow(narrow);
const topic = this.state.topic.trim();
return topicNarrow(streamName, topic || '(no topic)');
@@ -372,15 +380,11 @@ class ComposeBoxInner extends PureComponent {
};
handleSend = () => {
- const { dispatch, narrow, caughtUp, _ } = this.props;
+ const { dispatch } = this.props;
const { message } = this.state;
+ const narrow = this.getDestinationNarrow();
- if (!caughtUp.newer) {
- showErrorAlert(_('Failed to send message'));
- return;
- }
-
- dispatch(addToOutbox(this.getDestinationNarrow(), message));
+ this.props.onSend(message, narrow);
this.setMessageInputValue('');
@@ -391,41 +395,6 @@ class ComposeBoxInner extends PureComponent {
dispatch(sendTypingStop(narrow));
};
- handleEdit = () => {
- const { auth, editMessage, completeEditMessage, _ } = this.props;
- if (!editMessage) {
- throw new Error('expected editMessage');
- }
- const { message, topic } = this.state;
- const content = editMessage.content !== message ? message : undefined;
- const subject = topic !== editMessage.topic ? topic : undefined;
- if ((content !== undefined && content !== '') || (subject !== undefined && subject !== '')) {
- api.updateMessage(auth, { content, subject }, editMessage.id).catch(error => {
- showErrorAlert(_('Failed to edit message'), error.message);
- });
- }
- completeEditMessage();
- if (this.messageInputRef.current !== null) {
- // `.current` is not type-checked; see definition.
- this.messageInputRef.current.blur();
- }
- };
-
- UNSAFE_componentWillReceiveProps(nextProps: Props) {
- if (nextProps.editMessage !== this.props.editMessage) {
- const topic = nextProps.editMessage
- ? nextProps.editMessage.topic
- : nextProps.lastMessageTopic;
- const message = nextProps.editMessage ? nextProps.editMessage.content : '';
- this.setMessageInputValue(message);
- this.setTopicInputValue(topic);
- if (this.messageInputRef.current !== null) {
- // `.current` is not type-checked; see definition.
- this.messageInputRef.current.focus();
- }
- }
- }
-
inputMarginPadding = {
paddingHorizontal: 8,
paddingVertical: Platform.select({
@@ -477,7 +446,7 @@ class ComposeBoxInner extends PureComponent {
ownUserId,
narrow,
allUsersById,
- editMessage,
+ isEditing,
insets,
isAdmin,
isAnnouncementOnly,
@@ -533,6 +502,7 @@ class ComposeBoxInner extends PureComponent {
underlineColorAndroid="transparent"
placeholder="Topic"
defaultValue={topic}
+ autoFocus={this.props.autoFocusTopic}
selectTextOnFocus
textInputRef={this.topicInputRef}
onChangeText={this.handleTopicChange}
@@ -550,6 +520,7 @@ class ComposeBoxInner extends PureComponent {
underlineColorAndroid="transparent"
placeholder={placeholder}
defaultValue={message}
+ autoFocus={this.props.autoFocusMessage}
textInputRef={this.messageInputRef}
onBlur={this.handleMessageBlur}
onChangeText={this.handleMessageChange}
@@ -561,10 +532,10 @@ class ComposeBoxInner extends PureComponent {
0}
- onPress={editMessage === null ? this.handleSend : this.handleEdit}
+ onPress={this.handleSend}
/>
@@ -580,9 +551,6 @@ const ComposeBox: ComponentType = compose(
isAdmin: getIsAdmin(state),
isAnnouncementOnly: getIsActiveStreamAnnouncementOnly(state, props.narrow),
isSubscribed: getIsActiveStreamSubscribed(state, props.narrow),
- draft: getDraftForNarrow(state, props.narrow),
- lastMessageTopic: getLastMessageTopic(state, props.narrow),
- caughtUp: getCaughtUpForNarrow(state, props.narrow),
stream: getStreamInNarrow(state, props.narrow),
videoChatProvider: getVideoChatProvider(state),
})),
diff --git a/src/topics/__tests__/topicsSelectors-test.js b/src/topics/__tests__/topicsSelectors-test.js
index 8d0358bd335..8dbd721137f 100644
--- a/src/topics/__tests__/topicsSelectors-test.js
+++ b/src/topics/__tests__/topicsSelectors-test.js
@@ -1,8 +1,6 @@
/* @flow strict-local */
-import Immutable from 'immutable';
-
-import { getTopicsForNarrow, getLastMessageTopic, getTopicsForStream } from '../topicSelectors';
-import { HOME_NARROW, streamNarrow, keyFromNarrow } from '../../utils/narrow';
+import { getTopicsForNarrow, getTopicsForStream } from '../topicSelectors';
+import { HOME_NARROW, streamNarrow } from '../../utils/narrow';
import { reducer as unreadReducer } from '../../unread/unreadModel';
import * as eg from '../../__tests__/lib/exampleData';
@@ -31,38 +29,6 @@ describe('getTopicsForNarrow', () => {
});
});
-describe('getLastMessageTopic', () => {
- test('when no messages in narrow return an empty string', () => {
- const state = eg.reduxState({
- narrows: Immutable.Map({}),
- users: [eg.selfUser],
- realm: eg.realmState({ user_id: eg.selfUser.user_id, email: eg.selfUser.email }),
- });
-
- const topic = getLastMessageTopic(state, HOME_NARROW);
-
- expect(topic).toEqual('');
- });
-
- test('when one or more messages return the topic of the last one', () => {
- const narrow = streamNarrow('hello');
- const message1 = eg.streamMessage({ id: 1 });
- const message2 = eg.streamMessage({ id: 2, subject: 'some topic' });
- const state = eg.reduxState({
- narrows: Immutable.Map({
- [keyFromNarrow(narrow)]: [1, 2],
- }),
- messages: eg.makeMessagesState([message1, message2]),
- users: [eg.selfUser],
- realm: eg.realmState({ user_id: eg.selfUser.user_id, email: eg.selfUser.email }),
- });
-
- const topic = getLastMessageTopic(state, narrow);
-
- expect(topic).toEqual(message2.subject);
- });
-});
-
describe('getTopicsForStream', () => {
test('when no topics loaded for given stream return undefined', () => {
const state = eg.reduxState({
diff --git a/src/topics/topicSelectors.js b/src/topics/topicSelectors.js
index 2360a6abde0..b090ca8a248 100644
--- a/src/topics/topicSelectors.js
+++ b/src/topics/topicSelectors.js
@@ -1,17 +1,9 @@
/* @flow strict-local */
import { createSelector } from 'reselect';
-import type {
- Narrow,
- GlobalState,
- Selector,
- StreamsState,
- TopicExtended,
- TopicsState,
-} from '../types';
+import type { Narrow, Selector, StreamsState, TopicExtended, TopicsState } from '../types';
import { getMute, getStreams, getTopics } from '../directSelectors';
import { getUnread, getUnreadCountForTopic } from '../unread/unreadModel';
-import { getShownMessagesForNarrow } from '../chat/narrowsSelectors';
import { getStreamsById } from '../subscriptions/subscriptionSelectors';
import { NULL_ARRAY } from '../nullObjects';
import { isStreamNarrow, streamNameOfNarrow } from '../utils/narrow';
@@ -57,8 +49,3 @@ export const getTopicsForStream: Selector(TopicExtended[]), number> = createSe
});
},
);
-
-export const getLastMessageTopic = (state: GlobalState, narrow: Narrow): string => {
- const messages = getShownMessagesForNarrow(state, narrow);
- return messages.length === 0 ? '' : messages[messages.length - 1].subject;
-};