Commit a89ee406 authored by Jesse Hallam's avatar Jesse Hallam Committed by Harrison Healey
Browse files

MM-12233: local user mention results (#1974)

* MM-12233: supply local at mention results immediately

* temporary redux commit

* show special mentions after channel members

* fix edit channel header modal textbox interaction

* updated mattermost-redux commit

* address review feedback
parent 3337e111
......@@ -359,15 +359,6 @@ export async function searchUsers(term, teamId = getCurrentTeamId(getState()), o
}
}
export async function autocompleteUsersInChannel(username, channelId, success) {
const channel = getChannel(getState(), channelId);
const teamId = channel ? channel.team_id : getCurrentTeamId(getState());
const {data} = await UserActions.autocompleteUsers(username, teamId, channelId)(dispatch, getState);
if (success) {
success(data);
}
}
export async function autocompleteUsersInTeam(username, success) {
const {data} = await UserActions.autocompleteUsers(username, getCurrentTeamId(getState()))(dispatch, getState);
if (success) {
......
......@@ -2,11 +2,12 @@
// See LICENSE.txt for license information.
import {leaveChannel as leaveChannelRedux, unfavoriteChannel} from 'mattermost-redux/actions/channels';
import {getChannel, getChannelByName} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
import {getChannel, getChannelByName, getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentRelativeTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getUserByUsername} from 'mattermost-redux/selectors/entities/users';
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {isFavoriteChannel} from 'mattermost-redux/utils/channel_utils';
import {autocompleteUsers} from 'mattermost-redux/actions/users';
import {openDirectChannelToUserId} from 'actions/channel_actions.jsx';
import {getLastViewedChannelName} from 'selectors/local_storage';
......@@ -84,3 +85,14 @@ export function leaveChannel(channelId) {
};
};
}
export function autocompleteUsersInChannel(prefix) {
return async (dispatch, getState) => {
const state = getState();
const currentTeamId = getCurrentTeamId(state);
const currentChannelId = getCurrentChannelId(state);
return dispatch(autocompleteUsers(prefix, currentTeamId, currentChannelId));
};
}
......@@ -24,10 +24,28 @@ exports[`components/TextBox should match snapshot with required props 1`] = `
providers={
Array [
AtMentionProvider {
"channelId": undefined,
"autocompleteUsersInChannel": [MockFunction],
"currentUserId": "currentUserId",
"data": null,
"disableDispatches": false,
"latestComplete": true,
"latestPrefix": "",
"profilesInChannel": Array [
Object {
"id": "id1",
},
Object {
"id": "id2",
},
],
"profilesNotInChannel": Array [
Object {
"id": "id3",
},
Object {
"id": "id4",
},
],
"requestStarted": false,
},
ChannelMentionProvider {
......@@ -156,10 +174,28 @@ exports[`components/TextBox should throw error when new property is too long 1`]
providers={
Array [
AtMentionProvider {
"channelId": undefined,
"autocompleteUsersInChannel": [MockFunction],
"currentUserId": "currentUserId",
"data": null,
"disableDispatches": false,
"latestComplete": true,
"latestPrefix": "",
"profilesInChannel": Array [
Object {
"id": "id1",
},
Object {
"id": "id2",
},
],
"profilesNotInChannel": Array [
Object {
"id": "id3",
},
Object {
"id": "id4",
},
],
"requestStarted": false,
},
ChannelMentionProvider {
......@@ -288,10 +324,28 @@ exports[`components/TextBox should throw error when value is too long 1`] = `
providers={
Array [
AtMentionProvider {
"channelId": undefined,
"autocompleteUsersInChannel": [MockFunction],
"currentUserId": "currentUserId",
"data": null,
"disableDispatches": false,
"latestComplete": true,
"latestPrefix": "",
"profilesInChannel": Array [
Object {
"id": "id1",
},
Object {
"id": "id2",
},
],
"profilesNotInChannel": Array [
Object {
"id": "id3",
},
Object {
"id": "id4",
},
],
"requestStarted": false,
},
ChannelMentionProvider {
......
......@@ -14,7 +14,7 @@ exports[`components/CreateComment should match snapshot read only channel 1`] =
<div
className="post-body__cell"
>
<Textbox
<Connect(Textbox)
badConnection={false}
channelId="g6139tbospd18cmxroesdk3kkc"
characterLimit={4000}
......@@ -30,7 +30,6 @@ exports[`components/CreateComment should match snapshot read only channel 1`] =
onKeyDown={[Function]}
onKeyPress={[Function]}
popoverMentionKeyClick={true}
supportsCommands={true}
value=""
/>
<span
......@@ -106,7 +105,7 @@ exports[`components/CreateComment should match snapshot, comment with message 1`
<div
className="post-body__cell"
>
<Textbox
<Connect(Textbox)
badConnection={false}
channelId="g6139tbospd18cmxroesdk3kkc"
characterLimit={4000}
......@@ -122,7 +121,6 @@ exports[`components/CreateComment should match snapshot, comment with message 1`
onKeyDown={[Function]}
onKeyPress={[Function]}
popoverMentionKeyClick={true}
supportsCommands={true}
value="Test message"
/>
<span
......@@ -234,7 +232,7 @@ exports[`components/CreateComment should match snapshot, emoji picker disabled 1
<div
className="post-body__cell"
>
<Textbox
<Connect(Textbox)
badConnection={false}
channelId="g6139tbospd18cmxroesdk3kkc"
characterLimit={4000}
......@@ -250,7 +248,6 @@ exports[`components/CreateComment should match snapshot, emoji picker disabled 1
onKeyDown={[Function]}
onKeyPress={[Function]}
popoverMentionKeyClick={true}
supportsCommands={true}
value="Test message"
/>
<span
......@@ -350,7 +347,7 @@ exports[`components/CreateComment should match snapshot, empty comment 1`] = `
<div
className="post-body__cell"
>
<Textbox
<Connect(Textbox)
badConnection={false}
channelId="g6139tbospd18cmxroesdk3kkc"
characterLimit={4000}
......@@ -366,7 +363,6 @@ exports[`components/CreateComment should match snapshot, empty comment 1`] = `
onKeyDown={[Function]}
onKeyPress={[Function]}
popoverMentionKeyClick={true}
supportsCommands={true}
value=""
/>
<span
......@@ -478,7 +474,7 @@ exports[`components/CreateComment should match snapshot, non-empty message and u
<div
className="post-body__cell"
>
<Textbox
<Connect(Textbox)
badConnection={false}
channelId="g6139tbospd18cmxroesdk3kkc"
characterLimit={4000}
......@@ -494,7 +490,6 @@ exports[`components/CreateComment should match snapshot, non-empty message and u
onKeyDown={[Function]}
onKeyPress={[Function]}
popoverMentionKeyClick={true}
supportsCommands={true}
value="Test message"
/>
<span
......
......@@ -22,7 +22,7 @@ import FileUpload from 'components/file_upload';
import MsgTyping from 'components/msg_typing';
import PostDeletedModal from 'components/post_deleted_modal.jsx';
import EmojiIcon from 'components/svg/emoji_icon';
import Textbox from 'components/textbox.jsx';
import Textbox from 'components/textbox';
import FormattedMarkdownMessage from 'components/formatted_markdown_message.jsx';
export default class CreateComment extends React.PureComponent {
......@@ -445,7 +445,9 @@ export default class CreateComment extends React.PureComponent {
if (allowSending) {
e.persist();
this.refs.textbox.blur();
if (this.refs.textbox) {
this.refs.textbox.getWrappedInstance().blur();
}
if (withClosedCodeBlock && message) {
const {draft} = this.state;
......@@ -496,7 +498,7 @@ export default class CreateComment extends React.PureComponent {
if (!e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && Utils.isKeyPressed(e, Constants.KeyCodes.UP) && message === '') {
e.preventDefault();
if (this.refs.textbox) {
this.refs.textbox.blur();
this.refs.textbox.getWrappedInstance().blur();
}
const {data: canEditNow} = this.props.onEditLatestPost();
......@@ -646,7 +648,7 @@ export default class CreateComment extends React.PureComponent {
}
getFileUploadTarget = () => {
return this.refs.textbox;
return this.refs.textbox.getWrappedInstance();
}
getCreateCommentControls = () => {
......@@ -655,7 +657,7 @@ export default class CreateComment extends React.PureComponent {
focusTextbox = (keepFocus = false) => {
if (this.refs.textbox && (keepFocus || !UserAgent.isMobile())) {
this.refs.textbox.focus();
this.refs.textbox.getWrappedInstance().focus();
}
}
......
......@@ -700,7 +700,9 @@ describe('components/CreateComment', () => {
const instance = wrapper.instance();
instance.commentMsgKeyPress = jest.fn();
instance.focusTextbox = jest.fn();
instance.refs = {textbox: {blur: jest.fn(), focus: jest.fn()}};
const blur = jest.fn();
const focus = jest.fn();
instance.refs = {textbox: {getWrappedInstance: () => ({blur, focus})}};
const commentMsgKey = {
preventDefault: jest.fn(),
......@@ -741,7 +743,7 @@ describe('components/CreateComment', () => {
instance.handleKeyDown(upKeyForEdit);
expect(upKeyForEdit.preventDefault).toHaveBeenCalledTimes(1);
expect(onEditLatestPost).toHaveBeenCalledTimes(1);
expect(instance.refs.textbox.blur).toHaveBeenCalledTimes(1);
expect(blur).toHaveBeenCalledTimes(1);
instance.handleKeyDown(upKeyForEdit);
expect(upKeyForEdit.preventDefault).toHaveBeenCalledTimes(2);
......
......@@ -16,7 +16,7 @@ exports[`components/create_post Show tutorial 1`] = `
<div
className="post-body__cell"
>
<Textbox
<Connect(Textbox)
badConnection={false}
channelId="owsyt8n43jfxjpzh9np93mx1wa"
characterLimit={4000}
......@@ -25,12 +25,10 @@ exports[`components/create_post Show tutorial 1`] = `
emojiEnabled={true}
handlePostError={[Function]}
id="post_textbox"
isRHS={false}
onBlur={[Function]}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
supportsCommands={true}
value=""
/>
<span
......@@ -181,7 +179,7 @@ exports[`components/create_post should match snapshot for center textbox 1`] = `
<div
className="post-body__cell"
>
<Textbox
<Connect(Textbox)
badConnection={false}
channelId="owsyt8n43jfxjpzh9np93mx1wa"
characterLimit={4000}
......@@ -190,12 +188,10 @@ exports[`components/create_post should match snapshot for center textbox 1`] = `
emojiEnabled={true}
handlePostError={[Function]}
id="post_textbox"
isRHS={false}
onBlur={[Function]}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
supportsCommands={true}
value=""
/>
<span
......@@ -315,7 +311,7 @@ exports[`components/create_post should match snapshot for read only channel 1`]
<div
className="post-body__cell"
>
<Textbox
<Connect(Textbox)
badConnection={false}
channelId="owsyt8n43jfxjpzh9np93mx1wa"
characterLimit={4000}
......@@ -324,12 +320,10 @@ exports[`components/create_post should match snapshot for read only channel 1`]
emojiEnabled={true}
handlePostError={[Function]}
id="post_textbox"
isRHS={false}
onBlur={[Function]}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
supportsCommands={true}
value=""
/>
<span
......@@ -415,7 +409,7 @@ exports[`components/create_post should match snapshot when file upload disabled
<div
className="post-body__cell"
>
<Textbox
<Connect(Textbox)
badConnection={false}
channelId="owsyt8n43jfxjpzh9np93mx1wa"
characterLimit={4000}
......@@ -424,12 +418,10 @@ exports[`components/create_post should match snapshot when file upload disabled
emojiEnabled={true}
handlePostError={[Function]}
id="post_textbox"
isRHS={false}
onBlur={[Function]}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
supportsCommands={true}
value=""
/>
<span
......@@ -549,7 +541,7 @@ exports[`components/create_post should match snapshot, init 1`] = `
<div
className="post-body__cell"
>
<Textbox
<Connect(Textbox)
badConnection={false}
channelId="owsyt8n43jfxjpzh9np93mx1wa"
characterLimit={4000}
......@@ -558,12 +550,10 @@ exports[`components/create_post should match snapshot, init 1`] = `
emojiEnabled={true}
handlePostError={[Function]}
id="post_textbox"
isRHS={false}
onBlur={[Function]}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
supportsCommands={true}
value=""
/>
<span
......
......@@ -23,7 +23,7 @@ import MsgTyping from 'components/msg_typing';
import PostDeletedModal from 'components/post_deleted_modal.jsx';
import ResetStatusModal from 'components/reset_status_modal';
import EmojiIcon from 'components/svg/emoji_icon';
import Textbox from 'components/textbox.jsx';
import Textbox from 'components/textbox';
import TutorialTip from 'components/tutorial/tutorial_tip';
import FormattedMarkdownMessage from 'components/formatted_markdown_message.jsx';
......@@ -527,7 +527,7 @@ export default class CreatePost extends React.Component {
focusTextbox = (keepFocus = false) => {
if (this.refs.textbox && (keepFocus || !UserAgent.isMobile())) {
this.refs.textbox.focus();
this.refs.textbox.getWrappedInstance().focus();
}
}
......@@ -538,7 +538,9 @@ export default class CreatePost extends React.Component {
if (allowSending) {
e.persist();
this.refs.textbox.blur();
if (this.refs.textbox) {
this.refs.textbox.getWrappedInstance().blur();
}
if (withClosedCodeBlock && message) {
this.setState({message}, () => this.handleSubmit(e));
......@@ -728,7 +730,11 @@ export default class CreatePost extends React.Component {
}
getFileUploadTarget = () => {
return this.refs.textbox;
if (this.refs.textbox) {
return this.refs.textbox.getWrappedInstance();
}
return null;
}
getCreatePostControls = () => {
......@@ -781,7 +787,7 @@ export default class CreatePost extends React.Component {
type = Utils.localizeMessage('create_post.post', Posts.MESSAGE_TYPES.POST);
}
if (this.refs.textbox) {
this.refs.textbox.blur();
this.refs.textbox.getWrappedInstance().blur();
}
this.props.actions.setEditingPost(lastPost.id, this.props.commentCountForPost, 'post_textbox', type);
}
......
......@@ -236,7 +236,7 @@ describe('components/create_post', () => {
it('onKeyPress textbox should call emitLocalUserTypingEvent', () => {
const wrapper = shallow(createPost());
wrapper.instance().refs = {textbox: {blur: jest.fn()}};
wrapper.instance().refs = {textbox: {getWrappedInstance: () => ({blur: jest.fn()})}};
const postTextbox = wrapper.find('#post_textbox');
postTextbox.simulate('KeyPress', {key: KeyCodes.ENTER[0], preventDefault: jest.fn(), persist: jest.fn()});
......@@ -619,7 +619,7 @@ describe('components/create_post', () => {
}));
const instance = wrapper.instance();
instance.refs = {textbox: {blur: jest.fn()}};
instance.refs = {textbox: {getWrappedInstance: () => ({blur: jest.fn()})}};
instance.handleKeyDown({ctrlKey: true, key: Constants.KeyCodes.ENTER[0], keyCode: Constants.KeyCodes.ENTER[1], preventDefault: jest.fn(), persist: jest.fn()});
setTimeout(() => {
......
......@@ -56,11 +56,10 @@ exports[`components/EditChannelHeaderModal edit dirrect message channel 1`] = `
values={Object {}}
/>
</p>
<Textbox
<Connect(Textbox)
characterLimit={1024}
createMessage="Edit the Channel Header..."
id="edit_textbox"
isRHS={false}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
......@@ -163,11 +162,10 @@ exports[`components/EditChannelHeaderModal error with intl message 1`] = `
values={Object {}}
/>
</p>
<Textbox
<Connect(Textbox)
characterLimit={1024}
createMessage="Edit the Channel Header..."
id="edit_textbox"
isRHS={false}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
......@@ -280,11 +278,10 @@ exports[`components/EditChannelHeaderModal error without intl message 1`] = `
values={Object {}}
/>
</p>
<Textbox
<Connect(Textbox)
characterLimit={1024}
createMessage="Edit the Channel Header..."
id="edit_textbox"
isRHS={false}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
......@@ -397,11 +394,10 @@ exports[`components/EditChannelHeaderModal hide error message on new request 1`]
values={Object {}}
/>
</p>
<Textbox
<Connect(Textbox)
characterLimit={1024}
createMessage="Edit the Channel Header..."
id="edit_textbox"
isRHS={false}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
......@@ -504,11 +500,10 @@ exports[`components/EditChannelHeaderModal should match snapshot, init 1`] = `
values={Object {}}
/>
</p>
<Textbox
<Connect(Textbox)
characterLimit={1024}
createMessage="Edit the Channel Header..."
id="edit_textbox"
isRHS={false}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
......@@ -611,11 +606,10 @@ exports[`components/EditChannelHeaderModal submitted 1`] = `
values={Object {}}
/>
</p>
<Textbox
<Connect(Textbox)
characterLimit={1024}
createMessage="Edit the Channel Header..."
id="edit_textbox"
isRHS={false}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import {Modal} from 'react-bootstrap';
import {defineMessages, FormattedMessage, injectIntl, intlShape} from 'react-intl';
import {RequestStatus} from 'mattermost-redux/constants';
import Textbox from 'components/textbox.jsx';
import Textbox from 'components/textbox';
import Constants from 'utils/constants.jsx';
import {isMobile} from 'utils/user_agent.jsx';
import {isKeyPressed, localizeMessage} from 'utils/utils.jsx';
......@@ -109,7 +108,13 @@ class EditChannelHeaderModal extends React.PureComponent {
focusTextbox = () => {
if (this.refs.editChannelHeaderTextbox) {
this.refs.editChannelHeaderTextbox.focus();
this.refs.editChannelHeaderTextbox.getWrappedInstance().focus();
}
}
blurTextbox = () => {
if (this.refs.editChannelHeaderTextbox) {
this.refs.editChannelHeaderTextbox.getWrappedInstance().blur();
}
}
......@@ -129,7 +134,7 @@ class EditChannelHeaderModal extends React.PureComponent {
if (!isMobile() && ((ctrlSend && e.ctrlKey) || !ctrlSend)) {
if (isKeyPressed(e, KeyCodes.ENTER) && !e.shiftKey && !e.altKey) {
e.preventDefault();
ReactDOM.findDOMNode(this.refs.editChannelHeaderTextbox).blur();
this.blurTextbox();
this.handleSave(e);
}
}
......
......@@ -55,14 +55,13 @@ exports[`components/EditPostModal should match with default config 1`] = `
bsClass="modal-body edit-modal-body"
componentClass="div"
>
<Textbox
<Connect(Textbox)
channelId="5"
characterLimit={4000}
createMessage="Edit the post..."
emojiEnabled={true}
handlePostError={[Function]}
id="edit_textbox"
isRHS={false}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
......@@ -184,14 +183,13 @@ exports[`components/EditPostModal should match without emoji picker 1`] = `
bsClass="modal-body edit-modal-body"
componentClass="div"
>
<Textbox
<Connect(Textbox)
channelId="5"
characterLimit={4000}
createMessage="Edit the post..."
emojiEnabled={false}
handlePostError={[Function]}
id="edit_textbox"
isRHS={false}
onChange={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
......@@ -291,14 +289,13 @@ exports[`components/EditPostModal should show emojis on emojis click 1`] = `
bsClass="modal-body edit-modal-body"
componentClass="div"
>
<Textbox
<Connect(Textbox)
channelId="5"
characterLimit={4000}
createMessage="Edit the post..."
emojiEnabled={true}
handlePostError={[Function]}
id="edit_textbox"