Skip to content

Commit

Permalink
autocomplete: Warn on @-mention when user is not subscribed.
Browse files Browse the repository at this point in the history
Show a warning when @-mentioning a user who is not subscribed to
the stream the user was mentioned in, with an option to subscribe
them to the stream. Set up pipeline to enable the same.

Closes zulip#3373.
  • Loading branch information
agrawal-d committed Jun 12, 2020
1 parent a30cd74 commit fd66ed6
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 3 deletions.
5 changes: 4 additions & 1 deletion src/autocomplete/AutocompleteView.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ type Props = $ReadOnly<{|
text: string,
selection: InputSelection,
onAutocomplete: (input: string) => void,
processAutoComplete: (completion: string, completionType: string) => void,
|}>;

export default class AutocompleteView extends PureComponent<Props> {
handleAutocomplete = (autocomplete: string) => {
const { text, onAutocomplete, selection } = this.props;
const { text, onAutocomplete, selection, processAutoComplete } = this.props;
const { lastWordPrefix } = getAutocompleteFilter(text, selection);
const newText = getAutocompletedText(text, autocomplete, selection);
processAutoComplete(autocomplete, lastWordPrefix);
onAutocomplete(newText);
};

Expand Down
99 changes: 97 additions & 2 deletions src/compose/ComposeBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,26 @@ import * as api from '../api';
import { FloatingActionButton, Input } from '../common';
import { showErrorAlert } from '../utils/info';
import { IconDone, IconSend } from '../common/Icons';
import { isStreamNarrow, isStreamOrTopicNarrow, topicNarrow } from '../utils/narrow';
import {
isStreamNarrow,
isStreamOrTopicNarrow,
topicNarrow,
isPrivateNarrow,
} from '../utils/narrow';
import ComposeMenu from './ComposeMenu';
import getComposeInputPlaceholder from './getComposeInputPlaceholder';
import NotSubscribed from '../message/NotSubscribed';
import AnnouncementOnly from '../message/AnnouncementOnly';
import MentionedUserNotSubscribed from '../message/MentionedUserNotSubscribed';
import AnimatedScaleComponent from '../animation/AnimatedScaleComponent';

import {
getAuth,
getIsAdmin,
getSession,
getLastMessageTopic,
getActiveUsersByEmail,
getStreamInNarrow,
} from '../selectors';
import {
getIsActiveStreamSubscribed,
Expand All @@ -58,6 +66,7 @@ type SelectorProps = {|
isSubscribed: boolean,
draft: string,
lastMessageTopic: string,
streamId: number,
|};

type Props = $ReadOnly<{|
Expand All @@ -83,6 +92,7 @@ type State = {|
message: string,
height: number,
selection: InputSelection,
unsubscribedMentions: number[],
|};

export const updateTextInput = (textInput: ?TextInput, text: string): void => {
Expand Down Expand Up @@ -122,6 +132,7 @@ class ComposeBox extends PureComponent<Props, State> {
topic: this.props.lastMessageTopic,
message: this.props.draft,
selection: { start: 0, end: 0 },
unsubscribedMentions: [],
};

componentWillUnmount() {
Expand Down Expand Up @@ -184,6 +195,64 @@ class ComposeBox extends PureComponent<Props, State> {
dispatch(draftUpdate(narrow, message));
};

handleMentionSubscribedCheck = async (message: string) => {
const { usersByEmail, narrow, auth, streamId } = this.props;

if (isPrivateNarrow(narrow)) {
return;
}
const unformattedMessage = message.split('**')[1];

// We skip user groups, for which autocompletes are of the form
// `*<user_group_name>*`, and therefore, message.split('**')[1]
// is undefined.
if (unformattedMessage === undefined) {
return;
}
const [userFullName, userId] = unformattedMessage.split('|');
const unsubscribedMentions = this.state.unsubscribedMentions.slice();
let mentionedUser: UserOrBot;

// eslint-disable-next-line no-unused-vars
for (const [email, user] of usersByEmail) {
if (userId !== undefined) {
if (user.user_id === userId) {
mentionedUser = user;
break;
}
} else if (user.full_name === userFullName) {
mentionedUser = user;
break;
}
}
if (!mentionedUser || unsubscribedMentions.includes(mentionedUser)) {
return;
}

if (!(await api.getSubscriptionToStream(auth, mentionedUser.user_id, streamId)).is_subscribed) {
unsubscribedMentions.push(mentionedUser.user_id);
this.setState({ unsubscribedMentions });
}
};

handleMentionWarningDismiss = (user: UserOrBot) => {
this.setState(prevState => ({
unsubscribedMentions: prevState.unsubscribedMentions.filter(
(x: number) => x !== user.user_id,
),
}));
};

clearMentionWarnings = () => {
this.setState({ unsubscribedMentions: [] });
};

processAutocomplete = (completion: string, completionType: string) => {
if (completionType === '@') {
this.handleMentionSubscribedCheck(completion);
}
};

handleMessageAutocomplete = (message: string) => {
this.setMessageInputValue(message);
};
Expand Down Expand Up @@ -250,6 +319,7 @@ class ComposeBox extends PureComponent<Props, State> {
dispatch(addToOutbox(this.getDestinationNarrow(), message));

this.setMessageInputValue('');
this.clearMentionWarnings();
dispatch(sendTypingStop(narrow));
};

Expand Down Expand Up @@ -335,7 +405,15 @@ class ComposeBox extends PureComponent<Props, State> {
};

render() {
const { isTopicFocused, isMenuExpanded, height, message, topic, selection } = this.state;
const {
isTopicFocused,
isMenuExpanded,
height,
message,
topic,
selection,
unsubscribedMentions,
} = this.state;
const {
ownEmail,
narrow,
Expand All @@ -347,6 +425,18 @@ class ComposeBox extends PureComponent<Props, State> {
isSubscribed,
} = this.props;

const mentionWarnings = [];
for (const userId of unsubscribedMentions) {
mentionWarnings.push(
<MentionedUserNotSubscribed
narrow={narrow}
userId={userId}
onDismiss={this.handleMentionWarningDismiss}
key={userId}
/>,
);
}

if (!isSubscribed) {
return <NotSubscribed narrow={narrow} />;
} else if (isAnnouncementOnly && !isAdmin) {
Expand All @@ -361,6 +451,9 @@ class ComposeBox extends PureComponent<Props, State> {

return (
<View style={this.styles.wrapper}>
<AnimatedScaleComponent visible={mentionWarnings.length !== 0}>
{mentionWarnings}
</AnimatedScaleComponent>
<View style={[this.styles.autocompleteWrapper, { marginBottom: height }]}>
<TopicAutocomplete
isFocused={isTopicFocused}
Expand All @@ -373,6 +466,7 @@ class ComposeBox extends PureComponent<Props, State> {
selection={selection}
text={message}
onAutocomplete={this.handleMessageAutocomplete}
processAutoComplete={this.processAutocomplete}
/>
</View>
<View style={[this.styles.composeBox, style]} onLayout={this.handleLayoutChange}>
Expand Down Expand Up @@ -437,4 +531,5 @@ export default connect<SelectorProps, _, _>((state, props) => ({
isSubscribed: getIsActiveStreamSubscribed(state, props.narrow),
draft: getDraftForNarrow(state, props.narrow),
lastMessageTopic: getLastMessageTopic(state, props.narrow),
streamId: getStreamInNarrow(state, props.narrow).stream_id,
}))(ComposeBox);

0 comments on commit fd66ed6

Please sign in to comment.