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 subscribe 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 #M3373.
  • Loading branch information
agrawal-d committed May 12, 2020
1 parent 0e2c187 commit c0164a5
Show file tree
Hide file tree
Showing 2 changed files with 105 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
103 changes: 101 additions & 2 deletions src/compose/ComposeBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
UserOrBot,
Dispatch,
Dimensions,
Subscription,
} from '../types';
import { connect } from '../react-redux';
import {
Expand All @@ -27,18 +28,27 @@ 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,
getSubscriptionForId,
} from '../selectors';
import {
getIsActiveStreamSubscribed,
Expand All @@ -60,6 +70,7 @@ type SelectorProps = {|
editMessage: ?EditMessage,
draft: string,
lastMessageTopic: string,
subscription: Subscription | void,
|};

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

export const updateTextInput = (textInput: ?TextInput, text: string): void => {
Expand Down Expand Up @@ -122,6 +134,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 +197,64 @@ class ComposeBox extends PureComponent<Props, State> {
dispatch(draftUpdate(narrow, message));
};

handleMentionSubscribedCheck = (message: string) => {
const { usersByEmail, subscription, narrow } = 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;
// 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
&& subscription
&& !subscription.subscribers.includes(mentionedUser.user_id)
&& !unsubscribedMentions.includes(mentionedUser)
) {
unsubscribedMentions.push(mentionedUser);
this.setState({ unsubscribedMentions });
}
};

handleMentionWarningDismiss = (user: UserOrBot) => {
this.setState(prevState => ({
unsubscribedMentions: prevState.unsubscribedMentions.filter(
(x: UserOrBot) => x.user_id !== 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 +321,7 @@ class ComposeBox extends PureComponent<Props, State> {
dispatch(addToOutbox(this.getDestinationNarrow(), message));

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

Expand Down Expand Up @@ -332,7 +404,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 @@ -344,6 +424,18 @@ class ComposeBox extends PureComponent<Props, State> {
isSubscribed,
} = this.props;

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

if (!isSubscribed) {
return <NotSubscribed narrow={narrow} />;
} else if (isAnnouncementOnly && !isAdmin) {
Expand All @@ -358,6 +450,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 @@ -370,6 +465,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 @@ -435,4 +531,7 @@ export default connect<SelectorProps, _, _>((state, props) => ({
editMessage: getSession(state).editMessage,
draft: getDraftForNarrow(state, props.narrow),
lastMessageTopic: getLastMessageTopic(state, props.narrow),
subscription: isPrivateNarrow(props.narrow)
? undefined
: getSubscriptionForId(state, getStreamInNarrow(state, props.narrow).stream_id),
}))(ComposeBox);

0 comments on commit c0164a5

Please sign in to comment.