diff --git a/src/compose/ComposeBox.js b/src/compose/ComposeBox.js index 155f9a03768..8843b84d814 100644 --- a/src/compose/ComposeBox.js +++ b/src/compose/ComposeBox.js @@ -138,14 +138,7 @@ class ComposeBox extends PureComponent { 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>() - // - // but we need our `react-redux` wrapper to be aware of - // `{ forwardRef: true }`, since we use that. - mentionWarnings = React.createRef(); + mentionWarnings = React.createRef>(); inputBlurTimeoutId: ?TimeoutID = null; @@ -503,10 +496,6 @@ class ComposeBox extends PureComponent { return ( - {/* - $FlowFixMe[incompatible-use]: - `MentionWarnings` should use a type-checked `connect` - */} , -|}; - -type SelectorProps = {| - auth: Auth, - allUsersById: Map, -|}; - type Props = $ReadOnly<{| narrow: Narrow, stream: Subscription | {| ...Stream, in_home_view: boolean |}, - - dispatch: Dispatch, - ...SelectorProps, |}>; -class MentionWarnings extends PureComponent { - 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 - // `**`, and therefore, message.split('**')[1] - // is undefined. - if (unformattedMessage === undefined) { - return undefined; - } - - const [userFullName, userIdRaw] = unformattedMessage.split('|'); + handleMentionSubscribedCheck(completion: string): Promise, - 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([]); - 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 + // `**`, 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( - , - ); + if (user === undefined) { + continue; } - return mentionWarnings; + mentionWarnings.push( + , + ); } + + 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 = forwardRef( + MentionWarningsInner, +); + +export default MentionWarnings;