diff --git a/src/action-sheets/index.js b/src/action-sheets/index.js
index e0199d8d57a..6073c124b1b 100644
--- a/src/action-sheets/index.js
+++ b/src/action-sheets/index.js
@@ -42,6 +42,7 @@ import {
import {
navigateToMessageReactionScreen,
navigateToPmConversationDetails,
+ navigateToMoveMessage,
} from '../nav/navActions';
import { deleteMessagesForTopic } from '../topics/topicActions';
import * as logging from '../utils/logging';
@@ -91,6 +92,7 @@ type MessageArgs = {
dispatch: Dispatch,
_: GetText,
startEditMessage: (editMessage: EditMessage) => void,
+ narrow: Narrow,
...
};
@@ -248,6 +250,14 @@ const resolveTopic = {
action: toggleResolveTopic,
};
+const moveMessage = {
+ title: 'Move message',
+ errorMessage: 'Failed to move message',
+ action: async ({ message, narrow }) => {
+ NavigationService.dispatch(navigateToMoveMessage(message, narrow));
+ },
+};
+
const unresolveTopic = {
title: 'Unresolve topic',
errorMessage: 'Failed to unresolve topic',
@@ -574,6 +584,7 @@ export const constructMessageActionButtons = (args: {|
&& (isStreamOrTopicNarrow(narrow) || isPmNarrow(narrow))
) {
buttons.push(editMessage);
+ buttons.push(moveMessage);
}
if (message.sender_id === ownUser.user_id && messageNotDeleted(message)) {
// TODO(#2793): Don't show if message isn't deletable.
diff --git a/src/api/messages/updateMessage.js b/src/api/messages/updateMessage.js
index af3f92c8c7d..984ac99fb4d 100644
--- a/src/api/messages/updateMessage.js
+++ b/src/api/messages/updateMessage.js
@@ -22,6 +22,7 @@ export default async (
propagate_mode?: PropagateMode,
content?: string,
+ stream_id?: number,
send_notification_to_old_thread?: boolean,
send_notification_to_new_thread?: boolean,
diff --git a/src/message/MoveMessage.js b/src/message/MoveMessage.js
new file mode 100644
index 00000000000..ad3e2998626
--- /dev/null
+++ b/src/message/MoveMessage.js
@@ -0,0 +1,211 @@
+/* @flow strict-local */
+import React, { useState, useContext } from 'react';
+import { Text, View, Platform, Picker, TouchableOpacity, ScrollView } from 'react-native';
+import type { Node } from 'react';
+import { ThemeContext, BRAND_COLOR } from '../styles';
+import type { RouteProp } from '../react-navigation';
+import * as api from '../api';
+import Input from '../common/Input';
+import { streamNarrow, streamIdOfNarrow } from '../utils/narrow';
+import { getStreamForId } from '../subscriptions/subscriptionSelectors';
+import type { AppNavigationProp } from '../nav/AppNavigator';
+import { getAuth, getStreams, getOwnUser } from '../selectors';
+import { useSelector } from '../react-redux';
+import { showErrorAlert, showToast } from '../utils/info';
+import { Icon } from '../common/Icons';
+import type { Narrow, Message, Outbox } from '../types';
+import TopicAutocomplete from '../autocomplete/TopicAutocomplete';
+import { TranslationContext } from '../boot/TranslationProvider';
+import ZulipButton from '../common/ZulipButton';
+
+type Props = $ReadOnly<{|
+ navigation: AppNavigationProp<'move-message'>,
+ route: RouteProp<'move-message', {| message: Message | Outbox, messageNarrow: Narrow |}>,
+|}>;
+
+const inputMarginPadding = {
+ paddingHorizontal: 8,
+ paddingVertical: Platform.select({
+ ios: 8,
+ android: 2,
+ }),
+};
+
+export default function MoveMessage(props: Props): Node {
+ const themeContext = useContext(ThemeContext);
+ const backgroundColor = themeContext.backgroundColor;
+ const cardColor = themeContext.cardColor;
+ const iconName = Platform.OS === 'android' ? 'arrow-left' : 'chevron-left';
+ const auth = useSelector(getAuth);
+ const allStreams = useSelector(getStreams);
+ const isAdmin = useSelector(getOwnUser).is_admin;
+ const messageId = props.route.params.message.id;
+ const currentStreamId = streamIdOfNarrow(props.route.params.messageNarrow);
+ const currentStreamName = useSelector(state => getStreamForId(state, currentStreamId)).name;
+ const [narrow, setNarrow] = useState(streamNarrow(currentStreamId));
+ const [subject, setSubject] = useState(props.route.params.message.subject);
+ const [propagateMode, setPropagateMode] = useState('change_one');
+ const [streamId, setStreamId] = useState(currentStreamId);
+ const [topicFocus, setTopicFocus] = useState(false);
+ const _ = useContext(TranslationContext);
+
+ const styles = {
+ parent: {
+ backgroundColor: cardColor,
+ },
+ layout: {
+ margin: 10,
+ },
+ title: {
+ fontSize: 18,
+ color: backgroundColor === 'white' ? 'black' : 'white',
+ },
+ topicInput: {
+ height: 50,
+ backgroundColor,
+ ...inputMarginPadding,
+ },
+ viewTitle: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ height: 50,
+ paddingHorizontal: 10,
+ marginBottom: 20,
+ },
+ textColor: {
+ color: backgroundColor === 'white' ? 'black' : 'white',
+ },
+ picker: { backgroundColor, marginBottom: 20 },
+ submitButton: {
+ marginTop: 10,
+ paddingTop: 15,
+ paddingBottom: 15,
+ marginLeft: 30,
+ marginRight: 30,
+ backgroundColor: BRAND_COLOR,
+ borderRadius: 10,
+ borderWidth: 1,
+ },
+ };
+
+ const handleTopicChange = (topic: string) => {
+ setSubject(topic);
+ };
+
+ const handleTopicFocus = () => {
+ setTopicFocus(true);
+ };
+
+ const setTopicInputValue = (topic: string) => {
+ handleTopicChange(topic);
+ setTopicFocus(false);
+ };
+
+ const handleTopicAutocomplete = (topic: string) => {
+ setTopicInputValue(topic);
+ };
+
+ const updateMessage = () => {
+ try {
+ if (isAdmin) {
+ api.updateMessage(auth, messageId, {
+ subject,
+ stream_id: streamId,
+ propagate_mode: propagateMode,
+ });
+ } else {
+ api.updateMessage(auth, messageId, { subject, propagate_mode: propagateMode });
+ }
+ } catch (error) {
+ showErrorAlert(_('Failed to move message'), error.message);
+ props.navigation.goBack();
+ return;
+ }
+ props.navigation.goBack();
+ showToast(_('Moved message'));
+ };
+
+ const handleNarrow = (pickedStreamId: number) => {
+ setStreamId(pickedStreamId);
+ setNarrow(streamNarrow(pickedStreamId));
+ };
+
+ return (
+
+
+
+ props.navigation.goBack()}>
+
+
+ Move Message
+
+
+ Stream:
+ {isAdmin ? (
+
+ handleNarrow(parseInt(itemValue, 10))}
+ style={styles.textColor}
+ >
+ {allStreams.map(item => (
+
+ ))}
+
+
+ ) : (
+ {currentStreamName}
+ )}
+ Topic:
+
+ handleTopicChange(value)}
+ onFocus={handleTopicFocus}
+ blurOnSubmit={false}
+ returnKeyType="next"
+ style={styles.topicInput}
+ />
+
+
+ Move options:
+
+ setPropagateMode(itemValue)}
+ style={styles.textColor}
+ >
+
+
+
+
+
+ Content:
+
+ {props.route.params.message.content.replace(/<(?:.|\n)*?>/gm, '')}
+
+
+
+
+ );
+}
diff --git a/src/nav/AppNavigator.js b/src/nav/AppNavigator.js
index 5f2a0279681..1f12f81434f 100644
--- a/src/nav/AppNavigator.js
+++ b/src/nav/AppNavigator.js
@@ -45,6 +45,7 @@ import SettingsScreen from '../settings/SettingsScreen';
import UserStatusScreen from '../user-statuses/UserStatusScreen';
import SharingScreen from '../sharing/SharingScreen';
import { useHaveServerDataGate } from '../withHaveServerDataGate';
+import moveMessage from '../message/MoveMessage';
export type AppNavigatorParamList = {|
'account-pick': RouteParamsOf,
@@ -56,6 +57,7 @@ export type AppNavigatorParamList = {|
'emoji-picker': RouteParamsOf,
'main-tabs': RouteParamsOf,
'message-reactions': RouteParamsOf,
+ 'move-message': RouteParamsOf,
'password-auth': RouteParamsOf,
'realm-input': RouteParamsOf,
'search-messages': RouteParamsOf,
@@ -124,6 +126,7 @@ export default function AppNavigator(props: Props): Node {
name="message-reactions"
component={useHaveServerDataGate(MessageReactionsScreen)}
/>
+
StackActions.push('message-reactions', { messageId, reactionName });
+export const navigateToMoveMessage = (
+ message: Message | Outbox,
+ messageNarrow: Narrow,
+): GenericNavigationAction =>
+ StackActions.push('move-message', {
+ message,
+ messageNarrow,
+ });
+
export const navigateToLegal = (): GenericNavigationAction => StackActions.push('legal');
export const navigateToUserStatus = (): GenericNavigationAction => StackActions.push('user-status');
diff --git a/static/translations/messages_en.json b/static/translations/messages_en.json
index a91f7dc57e4..678d9d581c9 100644
--- a/static/translations/messages_en.json
+++ b/static/translations/messages_en.json
@@ -52,6 +52,7 @@
"Search": "Search",
"Log in": "Log in",
"Enter": "Enter",
+ "Submit": "Submit",
"Switch account": "Switch account",
"Log out": "Log out",
"Log out?": "Log out?",
@@ -200,12 +201,15 @@
"Cancel": "Cancel",
"Message copied": "Message copied",
"Edit message": "Edit message",
+ "Move message": "Move message",
+ "Moved message": "Moved message",
"Network request failed": "Network request failed",
"Failed to add reaction": "Failed to add reaction",
"Failed to reply": "Failed to reply",
"Failed to copy message to clipboard": "Failed to copy message to clipboard",
"Failed to share message": "Failed to share message",
"Failed to edit message": "Failed to edit message",
+ "Failed to move message": "Failed to move message",
"Failed to delete message": "Failed to delete message",
"Failed to star message": "Failed to star message",
"Failed to unstar message": "Failed to unstar message",