Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 1 addition & 12 deletions src/compose/ComposeBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,7 @@ class ComposeBox extends PureComponent<Props, State> {
messageInputRef = React.createRef<$FlowFixMe>();
topicInputRef = React.createRef<$FlowFixMe>();

// TODO: Type-check this, once we've adjusted our `react-redux`
// wrapper to do the right thing. It should be
//
// mentionWarnings = React.createRef<React$ElementRef<MentionWarnings>>()
//
// but we need our `react-redux` wrapper to be aware of
// `{ forwardRef: true }`, since we use that.
mentionWarnings = React.createRef();
mentionWarnings = React.createRef<React$ElementRef<typeof MentionWarnings>>();

inputBlurTimeoutId: ?TimeoutID = null;

Expand Down Expand Up @@ -503,10 +496,6 @@ class ComposeBox extends PureComponent<Props, State> {

return (
<View style={this.styles.wrapper}>
{/*
$FlowFixMe[incompatible-use]:
`MentionWarnings` should use a type-checked `connect`
*/}
<MentionWarnings narrow={narrow} stream={stream} ref={this.mentionWarnings} />
<View style={[this.styles.autocompleteWrapper, { marginBottom: height }]}>
<TopicAutocomplete
Expand Down
274 changes: 132 additions & 142 deletions src/compose/MentionWarnings.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
/* @flow strict-local */

import React, { PureComponent } from 'react';
import { connect } from 'react-redux';

import type {
Auth,
Stream,
Dispatch,
Narrow,
UserOrBot,
Subscription,
GetText,
UserId,
} from '../types';
import React, { useState, useCallback, useContext, forwardRef, useImperativeHandle } from 'react';
import type { AbstractComponent, Node } from 'react';
import { useSelector } from 'react-redux';

import type { Stream, Narrow, UserOrBot, Subscription, UserId } from '../types';
import { TranslationContext } from '../boot/TranslationProvider';
import { getAllUsersById, getAuth } from '../selectors';
import { is1to1PmNarrow } from '../utils/narrow';
Expand All @@ -22,163 +14,161 @@ import { showToast } from '../utils/info';
import MentionedUserNotSubscribed from '../message/MentionedUserNotSubscribed';
import { makeUserId } from '../api/idTypes';

type State = {|
unsubscribedMentions: Array<UserId>,
|};

type SelectorProps = {|
auth: Auth,
allUsersById: Map<UserId, UserOrBot>,
|};

type Props = $ReadOnly<{|
narrow: Narrow,
stream: Subscription | {| ...Stream, in_home_view: boolean |},

dispatch: Dispatch,
...SelectorProps,
|}>;

class MentionWarnings extends PureComponent<Props, State> {
static contextType = TranslationContext;
context: GetText;

state = {
unsubscribedMentions: [],
};

/**
* Functions expected to be called by ComposeBox using a ref to this
* component.
*/
type ImperativeHandle = {|
/**
* Tries to parse a user object from an @-mention.
* Check whether the message text entered by the user contains
* an @-mention to a user unsubscribed to the current stream, and if
* so, shows a warning.
*
* @param completion The autocomplete option chosend by the user.
See JSDoc for AutoCompleteView for details.
* See JSDoc for AutoCompleteView for details.
*/
getUserFromMention = (completion: string): UserOrBot | void => {
const { allUsersById } = this.props;

const unformattedMessage = completion.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 undefined;
}

const [userFullName, userIdRaw] = unformattedMessage.split('|');
handleMentionSubscribedCheck(completion: string): Promise<void>,

if (userIdRaw !== undefined) {
const userId = makeUserId(Number.parseInt(userIdRaw, 10));
return allUsersById.get(userId);
}
clearMentionWarnings(): void,
|};

for (const user of allUsersById.values()) {
if (user.full_name === userFullName) {
return user;
}
}
function MentionWarningsInner(props: Props, ref): Node {
const { stream, narrow } = props;

return undefined;
};
const auth = useSelector(getAuth);
const allUsersById = useSelector(getAllUsersById);

showSubscriptionStatusLoadError = (mentionedUser: UserOrBot) => {
const _ = this.context;
const [unsubscribedMentions, setUnsubscribedMentions] = useState<UserId[]>([]);

const alertTitle = _('Couldn’t load information about {fullName}', {
fullName: mentionedUser.full_name,
});
showToast(alertTitle);
};
const _ = useContext(TranslationContext);

/**
* Check whether the message text entered by the user contains
* an @-mention to a user unsubscribed to the current stream, and if
* so, shows a warning.
*
* This function is expected to be called by `ComposeBox` using a ref
* to this component.
* Tries to parse a user object from an @-mention.
*
* @param completion The autocomplete option chosend by the user.
See JSDoc for AutoCompleteView for details.
*/
handleMentionSubscribedCheck = async (completion: string) => {
const { narrow, auth, stream } = this.props;
const { unsubscribedMentions } = this.state;
const getUserFromMention = useCallback(
(completion: string): UserOrBot | void => {
const unformattedMessage = completion.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 undefined;
}

if (is1to1PmNarrow(narrow)) {
return;
}
const mentionedUser = this.getUserFromMention(completion);
if (mentionedUser === undefined || unsubscribedMentions.includes(mentionedUser.user_id)) {
return;
}
const [userFullName, userIdRaw] = unformattedMessage.split('|');

let isSubscribed: boolean;
try {
isSubscribed = (
await api.getSubscriptionToStream(auth, mentionedUser.user_id, stream.stream_id)
).is_subscribed;
} catch (err) {
this.showSubscriptionStatusLoadError(mentionedUser);
return;
}
if (userIdRaw !== undefined) {
const userId = makeUserId(Number.parseInt(userIdRaw, 10));
return allUsersById.get(userId);
}

if (!isSubscribed) {
this.setState(prevState => ({
unsubscribedMentions: [...prevState.unsubscribedMentions, mentionedUser.user_id],
}));
}
};

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

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

render() {
const { unsubscribedMentions } = this.state;
const { stream, narrow, allUsersById } = this.props;

if (is1to1PmNarrow(narrow)) {
return null;
}
for (const user of allUsersById.values()) {
if (user.full_name === userFullName) {
return user;
}
}

const mentionWarnings = [];
for (const userId of unsubscribedMentions) {
const user = allUsersById.get(userId);
return undefined;
},
[allUsersById],
);

const showSubscriptionStatusLoadError = useCallback(
(mentionedUser: UserOrBot) => {
const alertTitle = _('Couldn’t load information about {fullName}', {
fullName: mentionedUser.full_name,
});
showToast(alertTitle);
},
[_],
);

useImperativeHandle(
ref,
() => ({
handleMentionSubscribedCheck: async (completion: string) => {
if (is1to1PmNarrow(narrow)) {
return;
}
const mentionedUser = getUserFromMention(completion);
if (mentionedUser === undefined || unsubscribedMentions.includes(mentionedUser.user_id)) {
return;
}

let isSubscribed: boolean;
try {
isSubscribed = (
await api.getSubscriptionToStream(auth, mentionedUser.user_id, stream.stream_id)
).is_subscribed;
} catch (err) {
showSubscriptionStatusLoadError(mentionedUser);
return;
}

if (!isSubscribed) {
setUnsubscribedMentions(prevUnsubscribedMentions => [
...prevUnsubscribedMentions,
mentionedUser.user_id,
]);
}
},

clearMentionWarnings: () => {
setUnsubscribedMentions([]);
},
}),
[
auth,
getUserFromMention,
narrow,
showSubscriptionStatusLoadError,
stream,
unsubscribedMentions,
],
);

const handleMentionWarningDismiss = useCallback((user: UserOrBot) => {
setUnsubscribedMentions(prevUnsubscribedMentions =>
prevUnsubscribedMentions.filter(x => x !== user.user_id),
);
}, []);

if (is1to1PmNarrow(narrow)) {
return null;
}

if (user === undefined) {
continue;
}
const mentionWarnings = [];
for (const userId of unsubscribedMentions) {
const user = allUsersById.get(userId);

mentionWarnings.push(
<MentionedUserNotSubscribed
stream={stream}
user={user}
onDismiss={this.handleMentionWarningDismiss}
key={user.user_id}
/>,
);
if (user === undefined) {
continue;
}

return mentionWarnings;
mentionWarnings.push(
<MentionedUserNotSubscribed
stream={stream}
user={user}
onDismiss={handleMentionWarningDismiss}
key={user.user_id}
/>,
);
}

return mentionWarnings;
}

// $FlowFixMe[missing-annot]. TODO: Use a type checked connect call.
export default connect(
state => ({
auth: getAuth(state),
allUsersById: getAllUsersById(state),
}),
null,
null,
{ forwardRef: true },
)(MentionWarnings);
const MentionWarnings: AbstractComponent<Props, ImperativeHandle> = forwardRef(
MentionWarningsInner,
);

export default MentionWarnings;