Skip to content

Commit 139c454

Browse files
committed
message_actions: Add move message option
1 parent 1a281fa commit 139c454

File tree

8 files changed

+256
-2
lines changed

8 files changed

+256
-2
lines changed

src/action-sheets/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { doNarrow, deleteOutboxMessage, navigateToEmojiPicker, navigateToStream
3535
import {
3636
navigateToMessageReactionScreen,
3737
navigateToPmConversationDetails,
38+
navigateToMoveMessage,
3839
} from '../nav/navActions';
3940
import { deleteMessagesForTopic } from '../topics/topicActions';
4041
import * as logging from '../utils/logging';
@@ -137,6 +138,12 @@ const editMessage = async ({ message, dispatch, startEditMessage, auth }) => {
137138
editMessage.title = 'Edit message';
138139
editMessage.errorMessage = 'Failed to edit message';
139140

141+
const moveMessage = async ({ message, dispatch, startEditMessage, auth }) => {
142+
NavigationService.dispatch(navigateToMoveMessage(message, moveMessage.narrow, moveMessage.admin));
143+
};
144+
moveMessage.title = 'Move message';
145+
moveMessage.errorMessage = 'Move message';
146+
140147
const deleteMessage = async ({ auth, message, dispatch }) => {
141148
if (message.isOutbox) {
142149
dispatch(deleteOutboxMessage(message.timestamp));
@@ -449,6 +456,9 @@ export const constructMessageActionButtons = ({
449456
&& (isStreamOrTopicNarrow(narrow) || isPmNarrow(narrow))
450457
) {
451458
buttons.push(editMessage);
459+
moveMessage.narrow = narrow;
460+
moveMessage.admin = ownUser.is_admin;
461+
buttons.push(moveMessage);
452462
}
453463
if (message.sender_id === ownUser.user_id && messageNotDeleted(message)) {
454464
// TODO(#2793): Don't show if message isn't deletable.

src/api/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import getServerSettings from './settings/getServerSettings';
3939
import toggleMobilePushSettings from './settings/toggleMobilePushSettings';
4040
import createStream from './streams/createStream';
4141
import getStreams from './streams/getStreams';
42+
import getStreamId from './streams/getStreamId';
4243
import updateStream from './streams/updateStream';
4344
import sendSubmessage from './submessages/sendSubmessage';
4445
import getSubscriptions from './subscriptions/getSubscriptions';
@@ -86,6 +87,7 @@ export {
8687
toggleMobilePushSettings,
8788
createStream,
8889
getStreams,
90+
getStreamId,
8991
updateStream,
9092
sendSubmessage,
9193
getSubscriptions,

src/api/messages/updateMessage.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ export default async (
77
auth: Auth,
88
params: $ReadOnly<{|
99
subject?: string,
10-
propagate_mode?: boolean,
10+
propagate_mode?: string,
1111
content?: string,
12+
stream_id?: number,
1213
|}>,
1314
id: number,
1415
): Promise<ApiResponse> => apiPatch(auth, `messages/${id}`, params);

src/common/Icons.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,4 @@ export const IconUserMuted: SpecificIconType = makeIcon(FontAwesome, 'user');
8888
export const IconAttach: SpecificIconType = makeIcon(Feather, 'paperclip');
8989
export const IconAttachment: SpecificIconType = makeIcon(IoniconsIcon, 'document-attach-outline');
9090
export const IconGroup: SpecificIconType = makeIcon(FontAwesome, 'group');
91+
export const IconBack: SpecificIconType = makeIcon(FontAwesome, 'arrow-left');

src/message/moveMessage.js

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
/* @flow strict-local */
2+
import React, { useState, useContext } from 'react';
3+
import { Text, View, Platform, Picker, TouchableOpacity, ScrollView } from 'react-native';
4+
import type { Node } from 'react';
5+
import Toast from 'react-native-simple-toast';
6+
import { ThemeContext, BRAND_COLOR } from '../styles';
7+
import type { RouteProp } from '../react-navigation';
8+
import * as api from '../api';
9+
import { Input } from '../common';
10+
import type { AppNavigationProp } from '../nav/AppNavigator';
11+
import { getAuth, getStreams } from '../selectors';
12+
import { useSelector } from '../react-redux';
13+
import { showErrorAlert } from '../utils/info';
14+
import { IconBack } from '../common/Icons';
15+
import type { Narrow, Message } from '../types';
16+
import TopicAutocomplete from '../autocomplete/TopicAutocomplete';
17+
import { streamNameOfNarrow } from '../utils/narrow';
18+
19+
type Props = $ReadOnly<{|
20+
navigation: AppNavigationProp<'move-message'>,
21+
route: RouteProp<'move-message', {| message: Message, messageNarrow: Narrow, isadmin: boolean |}>,
22+
|}>;
23+
24+
const inputMarginPadding = {
25+
paddingHorizontal: 8,
26+
paddingVertical: Platform.select({
27+
ios: 8,
28+
android: 2,
29+
}),
30+
};
31+
32+
export default function MoveMessage(props: Props): Node {
33+
const themeContext = useContext(ThemeContext);
34+
const backgroundColor = themeContext.backgroundColor;
35+
const cardColor = themeContext.cardColor;
36+
const auth = useSelector(getAuth);
37+
const streams = useSelector(getStreams);
38+
const isadmin = props.route.params.isadmin;
39+
const id = props.route.params.message.id;
40+
const names = [];
41+
const [subject, setSubject] = useState(props.route.params.message.subject);
42+
const [propagate_mode, setPropagateMode] = useState('javachange_one');
43+
const [stream, setStream] = useState(streamNameOfNarrow(props.route.params.messageNarrow));
44+
const [topicFocus, setTopicFocus] = useState(false);
45+
const messageInputRef = React.createRef<$FlowFixMe>();
46+
const topicInputRef = React.createRef<$FlowFixMe>();
47+
48+
streams.map(item => names.push({ value: item.name }));
49+
50+
const styles = {
51+
parent: {
52+
backgroundColor: cardColor,
53+
},
54+
layout: {
55+
margin: 10,
56+
},
57+
title: {
58+
fontSize: 18,
59+
color: backgroundColor === 'white' ? 'black' : 'white',
60+
},
61+
autocompleteWrapper: {
62+
position: 'absolute',
63+
top: 0,
64+
width: '100%',
65+
},
66+
composeText: {
67+
flex: 1,
68+
paddingVertical: 8,
69+
},
70+
topicInput: {
71+
height: 50,
72+
backgroundColor,
73+
...inputMarginPadding,
74+
},
75+
viewTitle: {
76+
display: 'flex',
77+
flexDirection: 'row',
78+
justifyContent: 'space-between',
79+
alignItems: 'center',
80+
height: 50,
81+
paddingHorizontal: 10,
82+
marginBottom: 20,
83+
},
84+
textColor: {
85+
color: backgroundColor === 'white' ? 'black' : 'white',
86+
},
87+
picker: { backgroundColor, marginBottom: 20 },
88+
submitButton: {
89+
marginTop: 10,
90+
paddingTop: 15,
91+
paddingBottom: 15,
92+
marginLeft: 30,
93+
marginRight: 30,
94+
backgroundColor: BRAND_COLOR,
95+
borderRadius: 10,
96+
borderWidth: 1,
97+
},
98+
};
99+
100+
const updateTextInput = (textInput, text) => {
101+
if (textInput === null) {
102+
// Depending on the lifecycle events this function is called from,
103+
// this might not be set yet.
104+
return;
105+
}
106+
107+
// `textInput` is untyped; see definition.
108+
textInput.setNativeProps({ text });
109+
};
110+
111+
const handleTopicChange = (Topic: string) => {
112+
setSubject(Topic);
113+
};
114+
115+
const handleTopicFocus = () => {
116+
setTopicFocus(true);
117+
};
118+
119+
const setTopicInputValue = (Topic: string) => {
120+
updateTextInput(topicInputRef.current, subject);
121+
handleTopicChange(Topic);
122+
setTopicFocus(false);
123+
};
124+
125+
const handleTopicAutocomplete = (Topic: string) => {
126+
setTopicInputValue(Topic);
127+
messageInputRef.current?.focus();
128+
};
129+
130+
const updateMessage = async () => {
131+
const stream_info = await api.getStreamId(auth, stream);
132+
const stream_id = stream_info.stream_id;
133+
if (isadmin) {
134+
api.updateMessage(auth, { subject, stream_id, propagate_mode }, id).catch(error => {
135+
showErrorAlert('Failed to move message', error.message);
136+
props.navigation.goBack();
137+
});
138+
} else {
139+
api.updateMessage(auth, { subject, propagate_mode }, id).catch(error => {
140+
showErrorAlert('Failed to move message', error.message);
141+
props.navigation.goBack();
142+
});
143+
}
144+
props.navigation.goBack();
145+
Toast.show('Moved Message');
146+
};
147+
148+
return (
149+
<ScrollView style={styles.parent}>
150+
<View style={styles.layout}>
151+
<View style={styles.viewTitle}>
152+
<IconBack style={{ color: 'gray' }} size={20} onPress={() => props.navigation.goBack()} />
153+
<Text style={styles.title}>Move Message</Text>
154+
<View />
155+
</View>
156+
<Text style={{ fontSize: 14, marginBottom: 10, color: 'gray' }}>Content:</Text>
157+
<Text style={[styles.textColor, { marginBottom: 20 }]}>
158+
{props.route.params.message.content.replace(/<(?:.|\n)*?>/gm, '')}
159+
</Text>
160+
<Text style={{ fontSize: 14, color: 'gray', marginBottom: 10 }}>Stream:</Text>
161+
{isadmin ? (
162+
<View style={styles.picker}>
163+
<Picker
164+
selectedValue={stream}
165+
onValueChange={(itemValue, itemIndex) => setStream(itemValue.toString())}
166+
style={styles.textColor}
167+
>
168+
{streams.map(item => (
169+
<Picker.Item label={item.name} value={item.name} key={item.name} />
170+
))}
171+
</Picker>
172+
</View>
173+
) : (
174+
<Text style={[styles.textColor, { marginBottom: 10 }]}>{stream}</Text>
175+
)}
176+
<Text style={{ fontSize: 14, color: 'gray', marginBottom: 10 }}>Topic:</Text>
177+
<View style={{ marginBottom: 20 }}>
178+
<Input
179+
underlineColorAndroid="transparent"
180+
placeholder="Topic"
181+
autoFocus={false}
182+
defaultValue={subject}
183+
selectTextOnFocus
184+
textInputRef={messageInputRef}
185+
onChangeText={value => handleTopicChange(value)}
186+
onFocus={handleTopicFocus}
187+
onSubmitEditing={() => messageInputRef.current?.focus()}
188+
blurOnSubmit={false}
189+
returnKeyType="next"
190+
style={styles.topicInput}
191+
/>
192+
<TopicAutocomplete
193+
isFocused={topicFocus}
194+
narrow={props.route.params.messageNarrow}
195+
text={subject}
196+
onAutocomplete={handleTopicAutocomplete}
197+
/>
198+
</View>
199+
<Text style={{ fontSize: 14, color: 'gray', marginBottom: 10 }}>Move options:</Text>
200+
<View style={styles.picker}>
201+
<Picker
202+
selectedValue={propagate_mode}
203+
onValueChange={(itemValue, itemIndex) => setPropagateMode(itemValue.toString())}
204+
style={styles.textColor}
205+
>
206+
<Picker.Item label="Change only this message" value="change_one" key="change_one" />
207+
<Picker.Item
208+
label="Change later messages to this topic"
209+
value="change_later"
210+
key="change_later"
211+
/>
212+
<Picker.Item
213+
label="Change previous and following messages to this topic"
214+
value="change_all"
215+
key="change_all"
216+
/>
217+
</Picker>
218+
</View>
219+
<TouchableOpacity onPress={updateMessage} style={styles.submitButton}>
220+
<Text style={{ textAlign: 'center' }}>Submit</Text>
221+
</TouchableOpacity>
222+
</View>
223+
</ScrollView>
224+
);
225+
}

src/nav/AppNavigator.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import SettingsScreen from '../settings/SettingsScreen';
4545
import UserStatusScreen from '../user-status/UserStatusScreen';
4646
import SharingScreen from '../sharing/SharingScreen';
4747
import { useHaveServerDataGate } from '../withHaveServerDataGate';
48+
import moveMessage from '../message/moveMessage';
4849

4950
export type AppNavigatorParamList = {|
5051
'account-pick': RouteParamsOf<typeof AccountPickScreen>,
@@ -56,6 +57,7 @@ export type AppNavigatorParamList = {|
5657
'emoji-picker': RouteParamsOf<typeof EmojiPickerScreen>,
5758
'main-tabs': RouteParamsOf<typeof MainTabsScreen>,
5859
'message-reactions': RouteParamsOf<typeof MessageReactionsScreen>,
60+
'move-message': RouteParamsOf<typeof moveMessage>,
5961
'password-auth': RouteParamsOf<typeof PasswordAuthScreen>,
6062
'realm-input': RouteParamsOf<typeof RealmInputScreen>,
6163
'search-messages': RouteParamsOf<typeof SearchMessagesScreen>,
@@ -124,6 +126,7 @@ export default function AppNavigator(props: Props): Node {
124126
name="message-reactions"
125127
component={useHaveServerDataGate(MessageReactionsScreen)}
126128
/>
129+
<Stack.Screen name="move-message" component={useHaveServerDataGate(moveMessage)} />
127130
<Stack.Screen
128131
name="search-messages"
129132
component={useHaveServerDataGate(SearchMessagesScreen)}

src/nav/navActions.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from '@react-navigation/native';
88

99
import * as NavigationService from './NavigationService';
10-
import type { Message, Narrow, UserId } from '../types';
10+
import type { Message, Narrow, UserId, Outbox } from '../types';
1111
import type { PmKeyRecipients } from '../utils/recipient';
1212
import type { SharedData } from '../sharing/types';
1313
import type { ApiResponseServerSettings } from '../api/settings/getServerSettings';
@@ -125,6 +125,17 @@ export const navigateToMessageReactionScreen = (
125125
reactionName?: string,
126126
): GenericNavigationAction => StackActions.push('message-reactions', { messageId, reactionName });
127127

128+
export const navigateToMoveMessage = (
129+
message: Message | Outbox,
130+
messageNarrow: Narrow,
131+
isadmin: boolean,
132+
): GenericNavigationAction =>
133+
StackActions.push('move-message', {
134+
message,
135+
messageNarrow,
136+
isadmin,
137+
});
138+
128139
export const navigateToLegal = (): GenericNavigationAction => StackActions.push('legal');
129140

130141
export const navigateToUserStatus = (): GenericNavigationAction => StackActions.push('user-status');

static/translations/messages_en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@
191191
"Cancel": "Cancel",
192192
"Message copied": "Message copied",
193193
"Edit message": "Edit message",
194+
"Move message": "Move message",
194195
"Network request failed": "Network request failed",
195196
"Failed to add reaction": "Failed to add reaction",
196197
"Failed to reply": "Failed to reply",

0 commit comments

Comments
 (0)