-
-
Notifications
You must be signed in to change notification settings - Fork 655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
message_actions: Add move message option #5189
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |}>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the intended meaning of I'm not sure what it's supposed to mean here. That in turn is necessary in order to properly review the logic below that consumes it. |
||
|}>; | ||
|
||
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); | ||
Comment on lines
+43
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These and some other spots would be made a bit simpler to read by pulling the route params out as locals:
That also replaces the need for a |
||
const [propagateMode, setPropagateMode] = useState('change_one'); | ||
const [streamId, setStreamId] = useState(currentStreamId); | ||
const [topicFocus, setTopicFocus] = useState(false); | ||
const _ = useContext(TranslationContext); | ||
|
||
Comment on lines
+34
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Organize this by grouping together different kinds of definitions, with blank lines to separate groups. Follow the patterns seen in other components in the codebase: first props, then context, then selectors, then state. |
||
const styles = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: put styles just before the big JSX expression they're styling, so after all the callback definitions |
||
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) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: use |
||
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, { | ||
Comment on lines
+111
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These As discussed here: Without that, the try/catch basically has no effect. You can demonstrate that by editing the implementation of |
||
subject, | ||
stream_id: streamId, | ||
propagate_mode: propagateMode, | ||
}); | ||
} else { | ||
api.updateMessage(auth, messageId, { subject, propagate_mode: propagateMode }); | ||
Comment on lines
+112
to
+119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These would be clearer by unifying as one Instead, use a conditional just to control the one thing that changed. One handy trick for this, if you want an extra property in one case and not the other, is to spread a conditional expression: {
subject,
...(isAdmin ? { stream_id: streamId } : {}),
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) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. handleNarrow Noticed a bug when I made the PR. When move message function was accessed through the topic narrow, the topic auto complete didn't suggest any topic. So I had to create a new stream narrow. To create a stream narrow, I needed the name of the stream. First idea was to use the label I added to the items in the Picker component, but I later realized that I can't access the label after adding it, so I used the below method. Since I had the id of the stream, mapped through the current stream list, and found the name, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool, looking up the stream name given the stream ID is the right direction. Don't do it with a linear search through the whole list of streams, though: #3339. Instead, use |
||
setStreamId(pickedStreamId); | ||
setNarrow(streamNarrow(pickedStreamId)); | ||
}; | ||
|
||
return ( | ||
<ScrollView style={styles.parent}> | ||
<View style={styles.layout}> | ||
<View style={styles.viewTitle}> | ||
<TouchableOpacity onPress={() => props.navigation.goBack()}> | ||
<Icon size={20} color="gray" name={iconName} /> | ||
</TouchableOpacity> | ||
<Text style={styles.title}>Move Message</Text> | ||
<View /> | ||
</View> | ||
Comment on lines
+138
to
+144
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't try to make this sort of custom replacement for common widgets that exist across the app. Instead, use the same standard widgets that the rest of the app uses. You can see the consequences in your screenshots at the top of the thread. The "go back" arrow here doesn't match what appears in the rest of the app: Similarly, having the screen's title in the middle of the app bar is inconsistent with the rest of the app, which has it at the left. To find the right pattern to use, find any other screen in the app that has the piece of UI you're looking for -- so here, an app bar with a go-back button -- and look at the code for that screen. |
||
<Text style={{ fontSize: 14, color: 'gray', marginBottom: 10 }}>Stream:</Text> | ||
{isAdmin ? ( | ||
<View style={styles.picker}> | ||
<Picker | ||
selectedValue={currentStreamName} | ||
onValueChange={(itemValue, itemIndex) => handleNarrow(parseInt(itemValue, 10))} | ||
style={styles.textColor} | ||
> | ||
{allStreams.map(item => ( | ||
<Picker.Item label={item.name} value={item.stream_id.toString()} /> | ||
))} | ||
</Picker> | ||
</View> | ||
) : ( | ||
<Text style={[styles.textColor, { marginBottom: 10 }]}>{currentStreamName}</Text> | ||
)} | ||
<Text style={{ fontSize: 14, color: 'gray', marginBottom: 10 }}>Topic:</Text> | ||
<View style={{ marginBottom: 20 }}> | ||
<Input | ||
underlineColorAndroid="transparent" | ||
placeholder="Topic" | ||
autoFocus={false} | ||
defaultValue={subject} | ||
selectTextOnFocus | ||
onChangeText={value => handleTopicChange(value)} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: simplify to just |
||
onFocus={handleTopicFocus} | ||
blurOnSubmit={false} | ||
returnKeyType="next" | ||
style={styles.topicInput} | ||
/> | ||
<TopicAutocomplete | ||
isFocused={topicFocus} | ||
narrow={narrow} | ||
text={subject} | ||
onAutocomplete={handleTopicAutocomplete} | ||
/> | ||
</View> | ||
<Text style={{ fontSize: 14, color: 'gray', marginBottom: 10 }}>Move options:</Text> | ||
<View style={styles.picker}> | ||
<Picker | ||
selectedValue={propagateMode} | ||
// $FlowFixMe[incompatible-call] : the itemValue will always be one of these values - change_one | change_later | change_all | ||
onValueChange={(itemValue, itemIndex) => setPropagateMode(itemValue)} | ||
Comment on lines
+186
to
+187
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool, this fixme comment is helpful and this code is clearer (cf #5189 (comment) on the previous version.) Let's do two things to help clean this up further:
|
||
style={styles.textColor} | ||
> | ||
<Picker.Item label="Change only this message" value="change_one" /> | ||
<Picker.Item label="Change later messages to this topic" value="change_later" /> | ||
<Picker.Item | ||
label="Change previous and following messages to this topic" | ||
value="change_all" | ||
/> | ||
</Picker> | ||
</View> | ||
<Text style={{ fontSize: 14, marginBottom: 10, color: 'gray' }}>Content:</Text> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm this doesn't get translated, does it. Use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That will also ensure the text gets an appropriate style. In particular, looking back at the screenshots in dark mode I see that the contrast is pretty low -- the text is harder to read than it should be. |
||
<Text style={[styles.textColor, { marginBottom: 20 }]}> | ||
{props.route.params.message.content.replace(/<(?:.|\n)*?>/gm, '')} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's factor this expression out as a local variable in the function. That way it gets a name that helps explain what its intended meaning is. It's kind of opaque as it is. Hmmm, is the idea of this that we're taking the HTML of the message and attempting to turn it into plain text? I don't think this strategy is going to work for making something readable, in general, for complex messages. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of attempting to turn the HTML into readable text, I think a more reliable strategy will be to use the message's Markdown content. We can get that from the server using |
||
</Text> | ||
<ZulipButton | ||
style={{ flex: 1, margin: 8 }} | ||
secondary={false} | ||
text="Submit" | ||
onPress={updateMessage} | ||
/> | ||
</View> | ||
</ScrollView> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,7 @@ import { | |
} from '@react-navigation/native'; | ||
|
||
import * as NavigationService from './NavigationService'; | ||
import type { Message, Narrow, UserId, EmojiType } from '../types'; | ||
import type { Message, Narrow, UserId, EmojiType, Outbox } from '../types'; | ||
import type { PmKeyRecipients } from '../utils/recipient'; | ||
import type { SharedData } from '../sharing/types'; | ||
import type { ApiResponseServerSettings } from '../api/settings/getServerSettings'; | ||
|
@@ -127,6 +127,15 @@ export const navigateToMessageReactionScreen = ( | |
reactionName?: string, | ||
): GenericNavigationAction => StackActions.push('message-reactions', { messageId, reactionName }); | ||
|
||
export const navigateToMoveMessage = ( | ||
message: Message | Outbox, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This type needs to agree with the (This is one of the rare places where the type-checker isn't able to check this sort of thing for you, because of limitations in the types in the react-navigation library: #4757 (comment) .) |
||
messageNarrow: Narrow, | ||
): GenericNavigationAction => | ||
StackActions.push('move-message', { | ||
message, | ||
messageNarrow, | ||
}); | ||
|
||
export const navigateToLegal = (): GenericNavigationAction => StackActions.push('legal'); | ||
|
||
export const navigateToUserStatus = (): GenericNavigationAction => StackActions.push('user-status'); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: blank line between external and internal imports