Skip to content

Commit b1454cb

Browse files
committed
autocomplete: Warn on @-mention when user is not subscribed.
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. Fixes: #3373.
1 parent a30cd74 commit b1454cb

File tree

2 files changed

+101
-3
lines changed

2 files changed

+101
-3
lines changed

src/autocomplete/AutocompleteView.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ type Props = $ReadOnly<{|
2020
text: string,
2121
selection: InputSelection,
2222
onAutocomplete: (input: string) => void,
23+
processAutoComplete: (completion: string, completionType: string) => void,
2324
|}>;
2425

2526
export default class AutocompleteView extends PureComponent<Props> {
2627
handleAutocomplete = (autocomplete: string) => {
27-
const { text, onAutocomplete, selection } = this.props;
28+
const { text, onAutocomplete, selection, processAutoComplete } = this.props;
29+
const { lastWordPrefix } = getAutocompleteFilter(text, selection);
2830
const newText = getAutocompletedText(text, autocomplete, selection);
31+
processAutoComplete(autocomplete, lastWordPrefix);
2932
onAutocomplete(newText);
3033
};
3134

src/compose/ComposeBox.js

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,26 @@ import * as api from '../api';
2626
import { FloatingActionButton, Input } from '../common';
2727
import { showErrorAlert } from '../utils/info';
2828
import { IconDone, IconSend } from '../common/Icons';
29-
import { isStreamNarrow, isStreamOrTopicNarrow, topicNarrow } from '../utils/narrow';
29+
import {
30+
isStreamNarrow,
31+
isStreamOrTopicNarrow,
32+
topicNarrow,
33+
isPrivateNarrow,
34+
} from '../utils/narrow';
3035
import ComposeMenu from './ComposeMenu';
3136
import getComposeInputPlaceholder from './getComposeInputPlaceholder';
3237
import NotSubscribed from '../message/NotSubscribed';
3338
import AnnouncementOnly from '../message/AnnouncementOnly';
39+
import MentionedUserNotSubscribed from '../message/MentionedUserNotSubscribed';
40+
import AnimatedScaleComponent from '../animation/AnimatedScaleComponent';
3441

3542
import {
3643
getAuth,
3744
getIsAdmin,
3845
getSession,
3946
getLastMessageTopic,
4047
getActiveUsersByEmail,
48+
getStreamInNarrow,
4149
} from '../selectors';
4250
import {
4351
getIsActiveStreamSubscribed,
@@ -58,6 +66,7 @@ type SelectorProps = {|
5866
isSubscribed: boolean,
5967
draft: string,
6068
lastMessageTopic: string,
69+
streamId: number,
6170
|};
6271

6372
type Props = $ReadOnly<{|
@@ -83,6 +92,7 @@ type State = {|
8392
message: string,
8493
height: number,
8594
selection: InputSelection,
95+
unsubscribedMentions: number[],
8696
|};
8797

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

127138
componentWillUnmount() {
@@ -184,6 +195,64 @@ class ComposeBox extends PureComponent<Props, State> {
184195
dispatch(draftUpdate(narrow, message));
185196
};
186197

198+
handleMentionSubscribedCheck = async (message: string) => {
199+
const { usersByEmail, narrow, auth, streamId } = this.props;
200+
201+
if (isPrivateNarrow(narrow)) {
202+
return;
203+
}
204+
const unformattedMessage = message.split('**')[1];
205+
206+
// We skip user groups, for which autocompletes are of the form
207+
// `*<user_group_name>*`, and therefore, message.split('**')[1]
208+
// is undefined.
209+
if (unformattedMessage === undefined) {
210+
return;
211+
}
212+
const [userFullName, userId] = unformattedMessage.split('|');
213+
const unsubscribedMentions = this.state.unsubscribedMentions.slice();
214+
let mentionedUser: UserOrBot;
215+
216+
// eslint-disable-next-line no-unused-vars
217+
for (const [email, user] of usersByEmail) {
218+
if (userId !== undefined) {
219+
if (user.user_id === userId) {
220+
mentionedUser = user;
221+
break;
222+
}
223+
} else if (user.full_name === userFullName) {
224+
mentionedUser = user;
225+
break;
226+
}
227+
}
228+
if (!mentionedUser || unsubscribedMentions.includes(mentionedUser)) {
229+
return;
230+
}
231+
232+
if (!(await api.getSubscriptionToStream(auth, mentionedUser.user_id, streamId)).is_subscribed) {
233+
unsubscribedMentions.push(mentionedUser.user_id);
234+
this.setState({ unsubscribedMentions });
235+
}
236+
};
237+
238+
handleMentionWarningDismiss = (user: UserOrBot) => {
239+
this.setState(prevState => ({
240+
unsubscribedMentions: prevState.unsubscribedMentions.filter(
241+
(x: number) => x !== user.user_id,
242+
),
243+
}));
244+
};
245+
246+
clearMentionWarnings = () => {
247+
this.setState({ unsubscribedMentions: [] });
248+
};
249+
250+
processAutocomplete = (completion: string, completionType: string) => {
251+
if (completionType === '@') {
252+
this.handleMentionSubscribedCheck(completion);
253+
}
254+
};
255+
187256
handleMessageAutocomplete = (message: string) => {
188257
this.setMessageInputValue(message);
189258
};
@@ -250,6 +319,7 @@ class ComposeBox extends PureComponent<Props, State> {
250319
dispatch(addToOutbox(this.getDestinationNarrow(), message));
251320

252321
this.setMessageInputValue('');
322+
this.clearMentionWarnings();
253323
dispatch(sendTypingStop(narrow));
254324
};
255325

@@ -335,7 +405,15 @@ class ComposeBox extends PureComponent<Props, State> {
335405
};
336406

337407
render() {
338-
const { isTopicFocused, isMenuExpanded, height, message, topic, selection } = this.state;
408+
const {
409+
isTopicFocused,
410+
isMenuExpanded,
411+
height,
412+
message,
413+
topic,
414+
selection,
415+
unsubscribedMentions,
416+
} = this.state;
339417
const {
340418
ownEmail,
341419
narrow,
@@ -347,6 +425,18 @@ class ComposeBox extends PureComponent<Props, State> {
347425
isSubscribed,
348426
} = this.props;
349427

428+
const mentionWarnings = [];
429+
for (const userId of unsubscribedMentions) {
430+
mentionWarnings.push(
431+
<MentionedUserNotSubscribed
432+
narrow={narrow}
433+
userId={userId}
434+
onDismiss={this.handleMentionWarningDismiss}
435+
key={userId}
436+
/>,
437+
);
438+
}
439+
350440
if (!isSubscribed) {
351441
return <NotSubscribed narrow={narrow} />;
352442
} else if (isAnnouncementOnly && !isAdmin) {
@@ -361,6 +451,9 @@ class ComposeBox extends PureComponent<Props, State> {
361451

362452
return (
363453
<View style={this.styles.wrapper}>
454+
<AnimatedScaleComponent visible={mentionWarnings.length !== 0}>
455+
{mentionWarnings}
456+
</AnimatedScaleComponent>
364457
<View style={[this.styles.autocompleteWrapper, { marginBottom: height }]}>
365458
<TopicAutocomplete
366459
isFocused={isTopicFocused}
@@ -373,6 +466,7 @@ class ComposeBox extends PureComponent<Props, State> {
373466
selection={selection}
374467
text={message}
375468
onAutocomplete={this.handleMessageAutocomplete}
469+
processAutoComplete={this.processAutocomplete}
376470
/>
377471
</View>
378472
<View style={[this.styles.composeBox, style]} onLayout={this.handleLayoutChange}>
@@ -437,4 +531,5 @@ export default connect<SelectorProps, _, _>((state, props) => ({
437531
isSubscribed: getIsActiveStreamSubscribed(state, props.narrow),
438532
draft: getDraftForNarrow(state, props.narrow),
439533
lastMessageTopic: getLastMessageTopic(state, props.narrow),
534+
streamId: getStreamInNarrow(state, props.narrow).stream_id,
440535
}))(ComposeBox);

0 commit comments

Comments
 (0)