@@ -26,18 +26,26 @@ import * as api from '../api';
2626import { FloatingActionButton , Input } from '../common' ;
2727import { showErrorAlert } from '../utils/info' ;
2828import { 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' ;
3035import ComposeMenu from './ComposeMenu' ;
3136import getComposeInputPlaceholder from './getComposeInputPlaceholder' ;
3237import NotSubscribed from '../message/NotSubscribed' ;
3338import AnnouncementOnly from '../message/AnnouncementOnly' ;
39+ import MentionedUserNotSubscribed from '../message/MentionedUserNotSubscribed' ;
40+ import AnimatedScaleComponent from '../animation/AnimatedScaleComponent' ;
3441
3542import {
3643 getAuth ,
3744 getIsAdmin ,
3845 getSession ,
3946 getLastMessageTopic ,
4047 getActiveUsersByEmail ,
48+ getStreamInNarrow ,
4149} from '../selectors' ;
4250import {
4351 getIsActiveStreamSubscribed ,
@@ -58,6 +66,7 @@ type SelectorProps = {|
5866 isSubscribed : boolean ,
5967 draft : string ,
6068 lastMessageTopic : string ,
69+ streamId : number ,
6170| } ;
6271
6372type Props = $ReadOnly < { |
@@ -83,6 +92,7 @@ type State = {|
8392 message : string ,
8493 height : number ,
8594 selection : InputSelection ,
95+ unsubscribedMentions : number [ ] ,
8696| } ;
8797
8898export 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