summaryrefslogtreecommitdiffstats
path: root/web/react
diff options
context:
space:
mode:
Diffstat (limited to 'web/react')
-rw-r--r--web/react/.eslintrc2
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx25
-rw-r--r--web/react/components/admin_console/select_team_modal.jsx2
-rw-r--r--web/react/components/center_panel.jsx23
-rw-r--r--web/react/components/command_list.jsx99
-rw-r--r--web/react/components/create_comment.jsx2
-rw-r--r--web/react/components/edit_post_modal.jsx1
-rw-r--r--web/react/components/mention.jsx61
-rw-r--r--web/react/components/mention_list.jsx276
-rw-r--r--web/react/components/post.jsx4
-rw-r--r--web/react/components/post_body.jsx7
-rw-r--r--web/react/components/post_focus_view.jsx2
-rw-r--r--web/react/components/rhs_root_post.jsx8
-rw-r--r--web/react/components/rhs_thread.jsx1
-rw-r--r--web/react/components/search_autocomplete.jsx341
-rw-r--r--web/react/components/search_bar.jsx49
-rw-r--r--web/react/components/search_results_item.jsx10
-rw-r--r--web/react/components/suggestion/at_mention_provider.jsx100
-rw-r--r--web/react/components/suggestion/command_provider.jsx47
-rw-r--r--web/react/components/suggestion/emoticon_provider.jsx74
-rw-r--r--web/react/components/suggestion/search_channel_provider.jsx69
-rw-r--r--web/react/components/suggestion/search_suggestion_list.jsx86
-rw-r--r--web/react/components/suggestion/search_user_provider.jsx62
-rw-r--r--web/react/components/suggestion/suggestion_box.jsx189
-rw-r--r--web/react/components/suggestion/suggestion_list.jsx126
-rw-r--r--web/react/components/team_general_tab.jsx1
-rw-r--r--web/react/components/textbox.jsx206
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx49
-rw-r--r--web/react/dispatcher/event_helpers.jsx30
-rw-r--r--web/react/pages/channel.jsx16
-rw-r--r--web/react/stores/search_store.jsx36
-rw-r--r--web/react/stores/suggestion_store.jsx267
-rw-r--r--web/react/utils/async_client.jsx24
-rw-r--r--web/react/utils/constants.jsx9
-rw-r--r--web/react/utils/emoticons.jsx13
-rw-r--r--web/react/utils/utils.jsx28
36 files changed, 1235 insertions, 1110 deletions
diff --git a/web/react/.eslintrc b/web/react/.eslintrc
index 935bb638a..baaf7eaa5 100644
--- a/web/react/.eslintrc
+++ b/web/react/.eslintrc
@@ -47,7 +47,7 @@
"no-irregular-whitespace": 2,
"no-unexpected-multiline": 2,
"no-unreachable": 2,
- "no-magic-numbers": [1, { "enforceConst": true, "detectObjects": true } ],
+ "no-magic-numbers": [1, { "ignore": [-1, 0, 1, 2], "enforceConst": true, "detectObjects": true } ],
"valid-typeof": 2,
"block-scoped-var": 2,
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index 076a07618..cc98c495e 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -5,6 +5,9 @@ import AdminSidebarHeader from './admin_sidebar_header.jsx';
import SelectTeamModal from './select_team_modal.jsx';
import * as Utils from '../../utils/utils.jsx';
+const Tooltip = ReactBootstrap.Tooltip;
+const OverlayTrigger = ReactBootstrap.OverlayTrigger;
+
export default class AdminSidebar extends React.Component {
constructor(props) {
super(props);
@@ -80,6 +83,12 @@ export default class AdminSidebar extends React.Component {
render() {
var count = '*';
var teams = 'Loading';
+ const removeTooltip = (
+ <Tooltip id='remove-team-tooltip'>{'Remove team from sidebar menu'}</Tooltip>
+ );
+ const addTeamTooltip = (
+ <Tooltip id='add-team-tooltip'>{'Add team from sidebar menu'}</Tooltip>
+ );
if (this.props.teams != null) {
count = '' + Object.keys(this.props.teams).length;
@@ -102,14 +111,19 @@ export default class AdminSidebar extends React.Component {
className={'nav__sub-menu-item ' + this.isSelected('team_users', team.id)}
>
{team.name}
+ <OverlayTrigger
+ delayShow={1000}
+ placement='top'
+ overlay={removeTooltip}
+ >
<span
className='menu-icon--right menu__close'
onClick={this.removeTeam.bind(this, team.id)}
style={{cursor: 'pointer'}}
- title='Remove team from sidebar menu'
>
- {'x'}
+ {'×'}
</span>
+ </OverlayTrigger>
</a>
</li>
<li>
@@ -245,15 +259,20 @@ export default class AdminSidebar extends React.Component {
<span className='icon fa fa-gear'></span>
<span>{'TEAMS (' + count + ')'}</span>
<span className='menu-icon--right'>
+ <OverlayTrigger
+ delayShow={1000}
+ placement='top'
+ overlay={addTeamTooltip}
+ >
<a
href='#'
onClick={this.showTeamSelect}
>
<i
className='fa fa-plus'
- title='Add team to sidebar menu'
></i>
</a>
+ </OverlayTrigger>
</span>
</h4>
</li>
diff --git a/web/react/components/admin_console/select_team_modal.jsx b/web/react/components/admin_console/select_team_modal.jsx
index 22189821b..858b6bbfe 100644
--- a/web/react/components/admin_console/select_team_modal.jsx
+++ b/web/react/components/admin_console/select_team_modal.jsx
@@ -57,7 +57,7 @@ export default class SelectTeamModal extends React.Component {
<select
ref='team'
size='10'
- style={{width: '100%'}}
+ className='form-control'
>
{options}
</select>
diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx
index 3c6a36ad4..848e8952e 100644
--- a/web/react/components/center_panel.jsx
+++ b/web/react/components/center_panel.jsx
@@ -13,6 +13,8 @@ import PreferenceStore from '../stores/preference_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import UserStore from '../stores/user_store.jsx';
+import * as Utils from '../utils/utils.jsx';
+
import Constants from '../utils/constants.jsx';
const TutorialSteps = Constants.TutorialSteps;
const Preferences = Constants.Preferences;
@@ -46,6 +48,8 @@ export default class CenterPanel extends React.Component {
this.setState({showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS});
}
render() {
+ const channel = ChannelStore.getCurrent();
+ var handleClick = null;
let postsContainer;
let createPost;
if (this.state.showTutorialScreens) {
@@ -53,7 +57,24 @@ export default class CenterPanel extends React.Component {
createPost = null;
} else if (this.state.showPostFocus) {
postsContainer = <PostFocusView />;
- createPost = null;
+
+ handleClick = function clickHandler(e) {
+ e.preventDefault();
+ Utils.switchChannel(channel);
+ };
+
+ createPost = (
+ <div
+ id='archive-link-home'
+ >
+ <a
+ href=''
+ onClick={handleClick}
+ >
+ {'You are viewing the Archives. Click here to jump to recent messages.'}
+ </a>
+ </div>
+ );
} else {
postsContainer = <PostsViewContainer />;
createPost = (
diff --git a/web/react/components/command_list.jsx b/web/react/components/command_list.jsx
deleted file mode 100644
index 7fc0f79cf..000000000
--- a/web/react/components/command_list.jsx
+++ /dev/null
@@ -1,99 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import * as client from '../utils/client.jsx';
-
-export default class CommandList extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleClick = this.handleClick.bind(this);
- this.addFirstCommand = this.addFirstCommand.bind(this);
- this.isEmpty = this.isEmpty.bind(this);
- this.getSuggestedCommands = this.getSuggestedCommands.bind(this);
-
- this.state = {
- suggestions: [],
- cmd: ''
- };
- }
-
- handleClick(i) {
- this.props.addCommand(this.state.suggestions[i].suggestion);
- this.setState({suggestions: [], cmd: ''});
- }
-
- addFirstCommand() {
- if (this.state.suggestions.length === 0) {
- return;
- }
- this.handleClick(0);
- }
-
- isEmpty() {
- return this.state.suggestions.length === 0;
- }
-
- getSuggestedCommands(cmd) {
- if (!cmd || cmd.charAt(0) !== '/') {
- this.setState({suggestions: [], cmd: ''});
- return;
- }
-
- client.executeCommand(
- this.props.channelId,
- cmd,
- true,
- function success(data) {
- if (data.suggestions.length === 1 && data.suggestions[0].suggestion === cmd) {
- data.suggestions = [];
- }
- this.setState({suggestions: data.suggestions, cmd: cmd});
- }.bind(this),
- function fail() {
- }
- );
- }
-
- render() {
- if (this.state.suggestions.length === 0) {
- return (<div/>);
- }
-
- var suggestions = [];
-
- for (var i = 0; i < this.state.suggestions.length; i++) {
- if (this.state.suggestions[i].suggestion !== this.state.cmd) {
- suggestions.push(
- <div
- key={i}
- className='command-name'
- onClick={this.handleClick.bind(this, i)}
- >
- <div className='command__title'><strong>{this.state.suggestions[i].suggestion}</strong></div>
- <div className='command__desc'>{this.state.suggestions[i].description}</div>
- </div>
- );
- }
- }
-
- return (
- <div
- ref='mentionlist'
- className='command-box'
- style={{height: (suggestions.length * 56) + 2}}
- >
- {suggestions}
- </div>
- );
- }
-}
-
-CommandList.defaultProps = {
- channelId: null
-};
-
-CommandList.propTypes = {
- addCommand: React.PropTypes.func,
- channelId: React.PropTypes.string
-};
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 8ceda1cf7..fac40e895 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -34,7 +34,6 @@ export default class CreateComment extends React.Component {
this.handleUploadError = this.handleUploadError.bind(this);
this.handleTextDrop = this.handleTextDrop.bind(this);
this.removePreview = this.removePreview.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
this.getFileCount = this.getFileCount.bind(this);
this.handleResize = this.handleResize.bind(this);
this.onPreferenceChange = this.onPreferenceChange.bind(this);
@@ -335,6 +334,7 @@ export default class CreateComment extends React.Component {
messageText={this.state.messageText}
createMessage='Add a comment...'
initialText=''
+ supportsCommands={false}
id='reply_textbox'
ref='textbox'
/>
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index eb58fe721..be57fe7c3 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -160,6 +160,7 @@ export default class EditPostModal extends React.Component {
onKeyDown={this.handleKeyDown}
messageText={this.state.editText}
createMessage='Edit the post...'
+ supportsCommands={false}
id='edit_textbox'
ref='editbox'
/>
diff --git a/web/react/components/mention.jsx b/web/react/components/mention.jsx
deleted file mode 100644
index 44f6210e4..000000000
--- a/web/react/components/mention.jsx
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-import UserStore from '../stores/user_store.jsx';
-import * as Utils from '../utils/utils.jsx';
-
-export default class Mention extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleClick = this.handleClick.bind(this);
-
- this.state = null;
- }
- handleClick() {
- this.props.handleClick(this.props.username);
- }
- render() {
- var icon;
- var timestamp = UserStore.getCurrentUser().update_at;
- if (this.props.id === 'allmention' || this.props.id === 'channelmention') {
- icon = <span><i className='mention-img fa fa-users fa-2x'></i></span>;
- } else if (this.props.id == null) {
- icon = <span><i className='mention-img fa fa-users fa-2x'></i></span>;
- } else {
- icon = (
- <span>
- <img
- className='mention-img'
- src={'/api/v1/users/' + this.props.id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()}
- />
- </span>
- );
- }
- return (
- <div
- className={'mentions-name ' + this.props.isFocused}
- id={this.props.id + '_mentions'}
- onClick={this.handleClick}
- onMouseEnter={this.props.handleMouseEnter}
- >
- <div className='pull-left'>{icon}</div>
- <div className='pull-left mention-align'><span>@{this.props.username}</span><span className='mention-fullname'>{this.props.secondary_text}</span></div>
- </div>
- );
- }
-}
-
-Mention.defaultProps = {
- username: '',
- id: '',
- isFocused: '',
- secondary_text: ''
-};
-Mention.propTypes = {
- handleClick: React.PropTypes.func.isRequired,
- handleMouseEnter: React.PropTypes.func.isRequired,
- username: React.PropTypes.string,
- id: React.PropTypes.string,
- isFocused: React.PropTypes.string,
- secondary_text: React.PropTypes.string
-};
diff --git a/web/react/components/mention_list.jsx b/web/react/components/mention_list.jsx
deleted file mode 100644
index 297d5c719..000000000
--- a/web/react/components/mention_list.jsx
+++ /dev/null
@@ -1,276 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import UserStore from '../stores/user_store.jsx';
-import SearchStore from '../stores/search_store.jsx';
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
-import Mention from './mention.jsx';
-
-import Constants from '../utils/constants.jsx';
-import * as Utils from '../utils/utils.jsx';
-var ActionTypes = Constants.ActionTypes;
-
-var MAX_HEIGHT_LIST = 292;
-var MAX_ITEMS_IN_LIST = 25;
-var ITEM_HEIGHT = 36;
-
-export default class MentionList extends React.Component {
- constructor(props) {
- super(props);
-
- this.onListenerChange = this.onListenerChange.bind(this);
- this.handleClick = this.handleClick.bind(this);
- this.handleMouseEnter = this.handleMouseEnter.bind(this);
- this.getSelection = this.getSelection.bind(this);
- this.addCurrentMention = this.addCurrentMention.bind(this);
- this.addFirstMention = this.addFirstMention.bind(this);
- this.isEmpty = this.isEmpty.bind(this);
- this.scrollToMention = this.scrollToMention.bind(this);
- this.onScroll = this.onScroll.bind(this);
- this.onMentionListKey = this.onMentionListKey.bind(this);
- this.onClick = this.onClick.bind(this);
-
- this.state = {excludeUsers: [], mentionText: '-1', selectedMention: 0, selectedUsername: ''};
- }
- onScroll() {
- if ($('.mentions--top').length) {
- $('#reply_mention_tab .mentions--top').css({bottom: $(window).height() - $('.post-right__scroll #reply_textbox').offset().top});
- }
- }
- onMentionListKey(e) {
- if (!this.isEmpty() && this.state.mentionText !== '-1' && (e.which === 13 || e.which === 9)) {
- e.stopPropagation();
- e.preventDefault();
- this.addCurrentMention();
- } else if (!this.isEmpty() && this.state.mentionText !== '-1' && (e.which === 38 || e.which === 40)) {
- e.stopPropagation();
- e.preventDefault();
-
- if (e.which === 38) {
- if (this.getSelection(this.state.selectedMention - 1)) {
- this.setState({selectedMention: this.state.selectedMention - 1, selectedUsername: this.refs['mention' + (this.state.selectedMention - 1)].props.username});
- }
- } else if (e.which === 40) {
- if (this.getSelection(this.state.selectedMention + 1)) {
- this.setState({selectedMention: this.state.selectedMention + 1, selectedUsername: this.refs['mention' + (this.state.selectedMention + 1)].props.username});
- }
- }
-
- this.scrollToMention(e.which);
- }
- }
- onClick(e) {
- if (!($('#' + this.props.id).is(e.target) || $('#' + this.props.id).has(e.target).length ||
- ('mentionlist' in this.refs && $(ReactDOM.findDOMNode(this.refs.mentionlist)).has(e.target).length))) {
- this.setState({mentionText: '-1'});
- }
- }
- componentDidMount() {
- SearchStore.addMentionDataChangeListener(this.onListenerChange);
-
- $('.post-right__scroll').scroll(this.onScroll);
-
- $('body').on('keydown.mentionlist', '#' + this.props.id, this.onMentionListKey);
- $(document).click(this.onClick);
- }
- componentWillUnmount() {
- SearchStore.removeMentionDataChangeListener(this.onListenerChange);
- $('body').off('keydown.mentionlist', '#' + this.props.id);
- }
-
- /*
- * This component is poorly designed, nessesitating some state modification
- * in the componentDidUpdate function. This is generally discouraged as it
- * is a performance issue and breaks with good react design. This component
- * should be redesigned.
- */
- componentDidUpdate() {
- if (this.state.mentionText !== '-1') {
- if (this.state.selectedUsername !== '' && (!this.getSelection(this.state.selectedMention) || this.state.selectedUsername !== this.refs['mention' + this.state.selectedMention].props.username)) {
- var tempSelectedMention = -1;
- var foundMatch = false;
- while (tempSelectedMention < this.state.selectedMention && this.getSelection(++tempSelectedMention)) {
- if (this.state.selectedUsername === this.refs['mention' + tempSelectedMention].props.username) {
- this.setState({selectedMention: tempSelectedMention}); //eslint-disable-line react/no-did-update-set-state
- foundMatch = true;
- break;
- }
- }
- if (this.getSelection(0) && !foundMatch) {
- this.setState({selectedMention: 0, selectedUsername: this.refs.mention0.props.username}); //eslint-disable-line react/no-did-update-set-state
- }
- }
- } else if (this.state.selectedMention !== 0) {
- this.setState({selectedMention: 0, selectedUsername: ''}); //eslint-disable-line react/no-did-update-set-state
- }
- }
- onListenerChange(id, mentionText) {
- if (id !== this.props.id) {
- return;
- }
-
- var newState = this.state;
- if (mentionText != null) {
- newState.mentionText = mentionText;
- }
-
- this.setState(newState);
- }
- handleClick(name) {
- AppDispatcher.handleViewAction({
- type: ActionTypes.RECIEVED_ADD_MENTION,
- id: this.props.id,
- username: name
- });
-
- this.setState({mentionText: '-1'});
- }
- handleMouseEnter(listId) {
- this.setState({selectedMention: listId, selectedUsername: this.refs['mention' + listId].props.username});
- }
- getSelection(listId) {
- if (!this.refs['mention' + listId]) {
- return false;
- }
- return true;
- }
- addCurrentMention() {
- if (this.getSelection(this.state.selectedMention)) {
- this.refs['mention' + this.state.selectedMention].handleClick();
- } else {
- this.addFirstMention();
- }
- }
- addFirstMention() {
- if (!this.refs.mention0) {
- return;
- }
- this.refs.mention0.handleClick();
- }
- isEmpty() {
- return (!this.refs.mention0);
- }
- scrollToMention(keyPressed) {
- var direction;
- if (keyPressed === 38) {
- direction = 'up';
- } else {
- direction = 'down';
- }
- var scrollAmount = 0;
-
- if (direction === 'up') {
- scrollAmount = '-=' + ($('#' + this.refs['mention' + this.state.selectedMention].props.id + '_mentions').innerHeight() - 5);
- } else if (direction === 'down') {
- scrollAmount = '+=' + ($('#' + this.refs['mention' + this.state.selectedMention].props.id + '_mentions').innerHeight() - 5);
- }
-
- $('#mentionsbox').animate({
- scrollTop: scrollAmount
- }, 75);
- }
- render() {
- var mentionText = this.state.mentionText;
- if (mentionText === '-1') {
- return null;
- }
-
- var profiles = UserStore.getActiveOnlyProfiles();
- var users = [];
- for (let id in profiles) {
- if (profiles[id]) {
- users.push(profiles[id]);
- }
- }
-
- // var all = {};
- // all.username = 'all';
- // all.nickname = '';
- // all.secondary_text = 'Notifies everyone in the team';
- // all.id = 'allmention';
- // users.push(all);
-
- var channel = {};
- channel.username = 'channel';
- channel.nickname = '';
- channel.secondary_text = 'Notifies everyone in the channel';
- channel.id = 'channelmention';
- users.push(channel);
-
- users.sort(function sortByUsername(a, b) {
- if (a.username < b.username) {
- return -1;
- }
- if (a.username > b.username) {
- return 1;
- }
- return 0;
- });
- var mentions = [];
- var index = 0;
-
- for (var i = 0; i < users.length && index < MAX_ITEMS_IN_LIST; i++) {
- if ((users[i].first_name && users[i].first_name.lastIndexOf(mentionText, 0) === 0) ||
- (users[i].last_name && users[i].last_name.lastIndexOf(mentionText, 0) === 0) ||
- users[i].username.lastIndexOf(mentionText, 0) === 0) {
- let isFocused = '';
- if (this.state.selectedMention === index) {
- isFocused = 'mentions-focus';
- }
-
- if (!users[i].secondary_text) {
- users[i].secondary_text = Utils.getFullName(users[i]);
- }
-
- mentions[index] = (
- <Mention
- key={'mention_key_' + index}
- ref={'mention' + index}
- username={users[i].username}
- secondary_text={users[i].secondary_text}
- id={users[i].id}
- listId={index}
- isFocused={isFocused}
- handleMouseEnter={this.handleMouseEnter.bind(this, index)}
- handleClick={this.handleClick}
- />
- );
- index++;
- }
- }
-
- var numMentions = mentions.length;
-
- if (numMentions < 1) {
- return null;
- }
-
- var $mentionTab = $('#' + this.props.id);
- var maxHeight = Math.min(MAX_HEIGHT_LIST, $mentionTab.offset().top - 10);
- var style = {
- height: Math.min(maxHeight, (numMentions * ITEM_HEIGHT) + 4),
- width: $mentionTab.parent().width(),
- bottom: $(window).height() - $mentionTab.offset().top,
- left: $mentionTab.offset().left
- };
-
- return (
- <div
- className='mentions--top'
- style={style}
- >
- <div
- ref='mentionlist'
- className='mentions-box'
- id='mentionsbox'
- >
- {mentions}
- </div>
- </div>
- );
- }
-}
-
-MentionList.propTypes = {
- id: React.PropTypes.string
-};
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index 66d8c507a..b32656bfc 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -95,6 +95,10 @@ export default class Post extends React.Component {
return true;
}
+ if (nextProps.shouldHighlight !== this.props.shouldHighlight) {
+ return true;
+ }
+
return false;
}
getCommentCount(props) {
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index de8195f91..27f7ad2de 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -4,6 +4,7 @@
import FileAttachmentList from './file_attachment_list.jsx';
import UserStore from '../stores/user_store.jsx';
import * as Utils from '../utils/utils.jsx';
+import * as Emoji from '../utils/emoticons.jsx';
import Constants from '../utils/constants.jsx';
import * as TextFormatting from '../utils/text_formatting.jsx';
import twemoji from 'twemoji';
@@ -52,7 +53,11 @@ export default class PostBody extends React.Component {
}
parseEmojis() {
- twemoji.parse(ReactDOM.findDOMNode(this), {size: Constants.EMOJI_SIZE});
+ twemoji.parse(ReactDOM.findDOMNode(this), {
+ className: 'emoji twemoji',
+ base: '',
+ folder: Emoji.getImagePathForEmoticon()
+ });
}
componentWillMount() {
diff --git a/web/react/components/post_focus_view.jsx b/web/react/components/post_focus_view.jsx
index 5c6ad6c28..adcd78839 100644
--- a/web/react/components/post_focus_view.jsx
+++ b/web/react/components/post_focus_view.jsx
@@ -73,7 +73,7 @@ export default class PostFocusView extends React.Component {
getIntroMessage() {
return (
<div className='channel-intro'>
- <h4 className='channel-intro__title'>{'Beginning of Channel'}</h4>
+ <h4 className='channel-intro__title'>{'Beginning of Channel Archives'}</h4>
</div>
);
}
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index 3d3d9e13f..0dd969ad0 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -6,9 +6,9 @@ import UserProfile from './user_profile.jsx';
import UserStore from '../stores/user_store.jsx';
import * as TextFormatting from '../utils/text_formatting.jsx';
import * as utils from '../utils/utils.jsx';
+import * as Emoji from '../utils/emoticons.jsx';
import FileAttachmentList from './file_attachment_list.jsx';
import twemoji from 'twemoji';
-import Constants from '../utils/constants.jsx';
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
@@ -21,7 +21,11 @@ export default class RhsRootPost extends React.Component {
this.state = {};
}
parseEmojis() {
- twemoji.parse(ReactDOM.findDOMNode(this), {size: Constants.EMOJI_SIZE});
+ twemoji.parse(ReactDOM.findDOMNode(this), {
+ className: 'emoji twemoji',
+ base: '',
+ folder: Emoji.getImagePathForEmoticon()
+ });
}
componentDidMount() {
this.parseEmojis();
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
index 61f138539..d111094e7 100644
--- a/web/react/components/rhs_thread.jsx
+++ b/web/react/components/rhs_thread.jsx
@@ -101,7 +101,6 @@ export default class RhsThread extends React.Component {
}
if (currentPosts.posts[currentPosts.order[0]].channel_id === currentSelected.posts[currentSelected.order[0]].channel_id) {
- currentSelected.posts = {};
for (var postId in currentPosts.posts) {
if (currentPosts.posts.hasOwnProperty(postId)) {
currentSelected.posts[postId] = currentPosts.posts[postId];
diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx
deleted file mode 100644
index 4c0aa0166..000000000
--- a/web/react/components/search_autocomplete.jsx
+++ /dev/null
@@ -1,341 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import ChannelStore from '../stores/channel_store.jsx';
-import Constants from '../utils/constants.jsx';
-const KeyCodes = Constants.KeyCodes;
-const Popover = ReactBootstrap.Popover;
-import UserStore from '../stores/user_store.jsx';
-import * as Utils from '../utils/utils.jsx';
-
-const patterns = new Map([
- ['channels', /\b(?:in|channel):\s*(\S*)$/i],
- ['users', /\bfrom:\s*(\S*)$/i]
-]);
-
-export default class SearchAutocomplete extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleClick = this.handleClick.bind(this);
- this.handleDocumentClick = this.handleDocumentClick.bind(this);
- this.handleInputChange = this.handleInputChange.bind(this);
- this.handleKeyDown = this.handleKeyDown.bind(this);
-
- this.completeWord = this.completeWord.bind(this);
- this.getSelection = this.getSelection.bind(this);
- this.scrollToItem = this.scrollToItem.bind(this);
- this.updateSuggestions = this.updateSuggestions.bind(this);
-
- this.renderChannelSuggestion = this.renderChannelSuggestion.bind(this);
- this.renderUserSuggestion = this.renderUserSuggestion.bind(this);
-
- this.state = {
- show: false,
- mode: '',
- filter: '',
- selection: 0,
- suggestions: new Map()
- };
- }
-
- componentDidMount() {
- $(document).on('click', this.handleDocumentClick);
- }
-
- componentDidUpdate(prevProps, prevState) {
- const content = $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content');
-
- if (this.state.show && this.state.suggestions.length > 0) {
- if (!prevState.show) {
- content.perfectScrollbar();
- content.css('max-height', $(window).height() - 200);
- }
-
- // keep the keyboard selection visible when scrolling
- this.scrollToItem(this.getSelection());
- }
- }
-
- componentWillUnmount() {
- $(document).off('click', this.handleDocumentClick);
- }
-
- handleClick(value) {
- this.completeWord(value);
- }
-
- handleDocumentClick(e) {
- const container = $(ReactDOM.findDOMNode(this.refs.searchPopover));
-
- if (!(container.is(e.target) || container.has(e.target).length > 0)) {
- this.setState({
- show: false
- });
- }
- }
-
- handleInputChange(textbox, text) {
- const caret = Utils.getCaretPosition(textbox);
- const preText = text.substring(0, caret);
-
- let mode = '';
- let filter = '';
- for (const [modeForPattern, pattern] of patterns) {
- const result = pattern.exec(preText);
-
- if (result) {
- mode = modeForPattern;
- filter = result[1];
- break;
- }
- }
-
- if (mode !== this.state.mode || filter !== this.state.filter) {
- this.updateSuggestions(mode, filter);
- }
-
- this.setState({
- mode,
- filter,
- show: mode || filter
- });
- }
-
- handleKeyDown(e) {
- if (!this.state.show || this.state.suggestions.length === 0) {
- return;
- }
-
- if (e.which === KeyCodes.UP || e.which === KeyCodes.DOWN) {
- e.preventDefault();
-
- let selection = this.state.selection;
-
- if (e.which === KeyCodes.UP) {
- selection -= 1;
- } else {
- selection += 1;
- }
-
- if (selection >= 0 && selection < this.state.suggestions.length) {
- this.setState({
- selection
- });
- }
- } else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.SPACE) {
- e.preventDefault();
-
- this.completeWord(this.getSelection());
- }
- }
-
- completeWord(value) {
- // add a space so that anything else typed doesn't interfere with the search flag
- this.props.completeWord(this.state.filter, value + ' ');
-
- this.setState({
- show: false,
- mode: '',
- filter: '',
- selection: 0
- });
- }
-
- getSelection() {
- if (this.state.suggestions.length > 0) {
- if (this.state.mode === 'channels') {
- return this.state.suggestions[this.state.selection].name;
- } else if (this.state.mode === 'users') {
- return this.state.suggestions[this.state.selection].username;
- }
- }
-
- return '';
- }
-
- scrollToItem(itemName) {
- const content = $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content');
- const visibleContentHeight = content[0].clientHeight;
- const actualContentHeight = content[0].scrollHeight;
-
- if (this.state.suggestions.length > 0 && visibleContentHeight < actualContentHeight) {
- const contentTop = content.scrollTop();
- const contentTopPadding = parseInt(content.css('padding-top'), 10);
- const contentBottomPadding = parseInt(content.css('padding-top'), 10);
-
- const item = $(this.refs[itemName]);
- const itemTop = item[0].offsetTop - parseInt(item.css('margin-top'), 10);
- const itemBottom = item[0].offsetTop + item.height() + parseInt(item.css('margin-bottom'), 10);
-
- if (itemTop - contentTopPadding < contentTop) {
- // the item is off the top of the visible space
- content.scrollTop(itemTop - contentTopPadding);
- } else if (itemBottom + contentTopPadding + contentBottomPadding > contentTop + visibleContentHeight) {
- // the item has gone off the bottom of the visible space
- content.scrollTop(itemBottom - visibleContentHeight + contentTopPadding + contentBottomPadding);
- }
- }
- }
-
- updateSuggestions(mode, filter) {
- let suggestions = [];
-
- if (mode === 'channels') {
- let channels = ChannelStore.getAll();
-
- if (filter) {
- channels = channels.filter((channel) => channel.name.startsWith(filter) && channel.type !== 'D');
- } else {
- // don't show direct channels
- channels = channels.filter((channel) => channel.type !== 'D');
- }
-
- channels.sort((a, b) => {
- // put public channels first and then sort alphabebetically
- if (a.type === b.type) {
- return a.name.localeCompare(b.name);
- } else if (a.type === Constants.OPEN_CHANNEL) {
- return -1;
- }
-
- return 1;
- });
-
- suggestions = channels;
- } else if (mode === 'users') {
- let users = UserStore.getActiveOnlyProfileList();
-
- if (filter) {
- users = users.filter((user) => user.username.startsWith(filter));
- }
-
- users.sort((a, b) => a.username.localeCompare(b.username));
-
- suggestions = users;
- }
-
- let selection = this.state.selection;
-
- // keep the same user/channel selected if it's still visible as a suggestion
- if (selection > 0 && this.state.suggestions.length > 0) {
- // we can't just use indexOf to find if the selection is still in the list since they are different javascript objects
- const currentSelectionId = this.state.suggestions[selection].id;
- let found = false;
-
- for (let i = 0; i < suggestions.length; i++) {
- if (suggestions[i].id === currentSelectionId) {
- selection = i;
- found = true;
-
- break;
- }
- }
-
- if (!found) {
- selection = 0;
- }
- } else {
- selection = 0;
- }
-
- this.setState({
- suggestions,
- selection
- });
- }
-
- renderChannelSuggestion(channel) {
- let className = 'search-autocomplete__item';
- if (channel.name === this.getSelection()) {
- className += ' selected';
- }
-
- return (
- <div
- key={channel.name}
- ref={channel.name}
- onClick={this.handleClick.bind(this, channel.name)}
- className={className}
- >
- {channel.name}
- </div>
- );
- }
-
- renderUserSuggestion(user) {
- let className = 'search-autocomplete__item';
- if (user.username === this.getSelection()) {
- className += ' selected';
- }
-
- return (
- <div
- key={user.username}
- ref={user.username}
- onClick={this.handleClick.bind(this, user.username)}
- className={className}
- >
- <img
- className='profile-img rounded'
- src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
- />
- {user.username}
- </div>
- );
- }
-
- render() {
- if (!this.state.show || this.state.suggestions.length === 0) {
- return null;
- }
-
- let suggestions = [];
-
- if (this.state.mode === 'channels') {
- const publicChannels = this.state.suggestions.filter((channel) => channel.type === Constants.OPEN_CHANNEL);
- if (publicChannels.length > 0) {
- suggestions.push(
- <div
- key='public-channel-divider'
- className='search-autocomplete__divider'
- >
- <span>{'Public ' + Utils.getChannelTerm(Constants.OPEN_CHANNEL) + 's'}</span>
- </div>
- );
- suggestions = suggestions.concat(publicChannels.map(this.renderChannelSuggestion));
- }
-
- const privateChannels = this.state.suggestions.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL);
- if (privateChannels.length > 0) {
- suggestions.push(
- <div
- key='private-channel-divider'
- className='search-autocomplete__divider'
- >
- <span>{'Private ' + Utils.getChannelTerm(Constants.PRIVATE_CHANNEL) + 's'}</span>
- </div>
- );
- suggestions = suggestions.concat(privateChannels.map(this.renderChannelSuggestion));
- }
- } else if (this.state.mode === 'users') {
- suggestions = this.state.suggestions.map(this.renderUserSuggestion);
- }
-
- return (
- <Popover
- ref='searchPopover'
- onShow={this.componentDidMount}
- id='search-autocomplete__popover'
- className='search-help-popover autocomplete visible'
- placement='bottom'
- >
- {suggestions}
- </Popover>
- );
- }
-}
-
-SearchAutocomplete.propTypes = {
- completeWord: React.PropTypes.func.isRequired
-};
diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx
index 32f0f93bf..77c9e39b9 100644
--- a/web/react/components/search_bar.jsx
+++ b/web/react/components/search_bar.jsx
@@ -5,11 +5,14 @@ import * as client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
import SearchStore from '../stores/search_store.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import SuggestionBox from './suggestion/suggestion_box.jsx';
+import SearchChannelProvider from './suggestion/search_channel_provider.jsx';
+import SearchSuggestionList from './suggestion/search_suggestion_list.jsx';
+import SearchUserProvider from './suggestion/search_user_provider.jsx';
import * as utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
var ActionTypes = Constants.ActionTypes;
var Popover = ReactBootstrap.Popover;
-import SearchAutocomplete from './search_autocomplete.jsx';
export default class SearchBar extends React.Component {
constructor() {
@@ -17,17 +20,17 @@ export default class SearchBar extends React.Component {
this.mounted = false;
this.onListenerChange = this.onListenerChange.bind(this);
- this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
this.handleUserFocus = this.handleUserFocus.bind(this);
this.handleUserBlur = this.handleUserBlur.bind(this);
this.performSearch = this.performSearch.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
- this.completeWord = this.completeWord.bind(this);
const state = this.getSearchTermStateFromStores();
state.focused = false;
this.state = state;
+
+ this.suggestionProviders = [new SearchChannelProvider(), new SearchUserProvider()];
}
getSearchTermStateFromStores() {
var term = SearchStore.getSearchTerm() || '';
@@ -77,18 +80,11 @@ export default class SearchBar extends React.Component {
results: null
});
}
- handleKeyDown(e) {
- if (this.refs.autocomplete) {
- this.refs.autocomplete.handleKeyDown(e);
- }
- }
- handleUserInput(e) {
- var term = e.target.value;
+ handleUserInput(text) {
+ var term = text;
SearchStore.storeSearchTerm(term);
SearchStore.emitSearchTermChange(false);
this.setState({searchTerm: term});
-
- this.refs.autocomplete.handleInputChange(e.target, term);
}
handleUserBlur() {
this.setState({focused: false});
@@ -128,23 +124,6 @@ export default class SearchBar extends React.Component {
this.performSearch(this.state.searchTerm.trim());
}
- completeWord(partialWord, word) {
- const textbox = ReactDOM.findDOMNode(this.refs.search);
- let text = textbox.value;
-
- const caret = utils.getCaretPosition(textbox);
- const preText = text.substring(0, caret - partialWord.length);
- const postText = text.substring(caret);
- text = preText + word + postText;
-
- textbox.value = text;
- utils.setCaretPosition(textbox, preText.length + word.length);
-
- SearchStore.storeSearchTerm(text);
- SearchStore.emitSearchTermChange(false);
- this.setState({searchTerm: text});
- }
-
render() {
var isSearching = null;
if (this.state.isSearching) {
@@ -178,22 +157,18 @@ export default class SearchBar extends React.Component {
autoComplete='off'
>
<span className='glyphicon glyphicon-search sidebar__search-icon' />
- <input
- type='text'
+ <SuggestionBox
ref='search'
className='form-control search-bar'
placeholder='Search'
value={this.state.searchTerm}
onFocus={this.handleUserFocus}
onBlur={this.handleUserBlur}
- onChange={this.handleUserInput}
- onKeyDown={this.handleKeyDown}
+ onUserInput={this.handleUserInput}
+ listComponent={SearchSuggestionList}
+ providers={this.suggestionProviders}
/>
{isSearching}
- <SearchAutocomplete
- ref='autocomplete'
- completeWord={this.completeWord}
- />
<Popover
id='searchbar-help-popup'
placement='bottom'
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index da422fe1b..6e17cfe32 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -41,7 +41,6 @@ export default class SearchResultsItem extends React.Component {
return (
<div
className='search-item-container post'
- onClick={this.handleClick}
>
<div className='search-channel__name'>{channelName}</div>
<div className='post__content'>
@@ -60,6 +59,15 @@ export default class SearchResultsItem extends React.Component {
{utils.displayDate(this.props.post.create_at) + ' ' + utils.displayTime(this.props.post.create_at)}
</time>
</li>
+ <li>
+ <a
+ href='#'
+ className='search-item__jump'
+ onClick={this.handleClick}
+ >
+ {'[Jump]'}
+ </a>
+ </li>
</ul>
<div className='search-item-snippet'>
<span
diff --git a/web/react/components/suggestion/at_mention_provider.jsx b/web/react/components/suggestion/at_mention_provider.jsx
new file mode 100644
index 000000000..8c2893448
--- /dev/null
+++ b/web/react/components/suggestion/at_mention_provider.jsx
@@ -0,0 +1,100 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import SuggestionStore from '../../stores/suggestion_store.jsx';
+import UserStore from '../../stores/user_store.jsx';
+import * as Utils from '../../utils/utils.jsx';
+
+class AtMentionSuggestion extends React.Component {
+ render() {
+ const {item, isSelection, onClick} = this.props;
+
+ let username;
+ let description;
+ let icon;
+ if (item.username === 'all') {
+ username = 'all';
+ description = 'Notifies everyone in the team';
+ icon = <i className='mention-img fa fa-users fa-2x' />;
+ } else if (item.username === 'channel') {
+ username = 'channel';
+ description = 'Notifies everyone in the channel';
+ icon = <i className='mention-img fa fa-users fa-2x' />;
+ } else {
+ username = item.username;
+ description = Utils.getFullName(item);
+ icon = (
+ <img
+ className='mention-img'
+ src={'/api/v1/users/' + item.id + '/image?time=' + item.update_at + '&' + Utils.getSessionIndex()}
+ />
+ );
+ }
+
+ let className = 'mentions-name';
+ if (isSelection) {
+ className += ' suggestion--selected';
+ }
+
+ return (
+ <div
+ className={className}
+ onClick={onClick}
+ >
+ <div className='pull-left'>
+ {icon}
+ </div>
+ <div className='pull-left mention-align'>
+ <span>
+ {'@' + username}
+ </span>
+ <span className='mention-fullname'>
+ {description}
+ </span>
+ </div>
+ </div>
+ );
+ }
+}
+
+AtMentionSuggestion.propTypes = {
+ item: React.PropTypes.object.isRequired,
+ isSelection: React.PropTypes.bool,
+ onClick: React.PropTypes.func
+};
+
+export default class AtMentionProvider {
+ handlePretextChanged(suggestionId, pretext) {
+ const captured = (/@([a-z0-9\-\._]*)$/i).exec(pretext);
+ if (captured) {
+ const usernamePrefix = captured[1];
+
+ const users = UserStore.getProfiles();
+ let filtered = [];
+
+ for (const id of Object.keys(users)) {
+ const user = users[id];
+
+ if (user.username.startsWith(usernamePrefix)) {
+ filtered.push(user);
+ }
+ }
+
+ // add dummy users to represent the @all and @channel special mentions
+ if ('all'.startsWith(usernamePrefix)) {
+ filtered.push({username: 'all'});
+ }
+
+ if ('channel'.startsWith(usernamePrefix)) {
+ filtered.push({username: 'channel'});
+ }
+
+ filtered = filtered.sort((a, b) => a.username.localeCompare(b.username));
+
+ const mentions = filtered.map((user) => '@' + user.username);
+
+ SuggestionStore.setMatchedPretext(suggestionId, captured[0]);
+ SuggestionStore.addSuggestions(suggestionId, mentions, filtered, AtMentionSuggestion);
+ }
+ }
+}
diff --git a/web/react/components/suggestion/command_provider.jsx b/web/react/components/suggestion/command_provider.jsx
new file mode 100644
index 000000000..a2a446de2
--- /dev/null
+++ b/web/react/components/suggestion/command_provider.jsx
@@ -0,0 +1,47 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as AsyncClient from '../../utils/async_client.jsx';
+import SuggestionStore from '../../stores/suggestion_store.jsx';
+
+class CommandSuggestion extends React.Component {
+ render() {
+ const {item, isSelection, onClick} = this.props;
+
+ let className = 'command-name';
+ if (isSelection) {
+ className += ' suggestion--selected';
+ }
+
+ return (
+ <div
+ className={className}
+ onClick={onClick}
+ >
+ <div className='command__title'>
+ <string>{item.suggestion}</string>
+ </div>
+ <div className='command__desc'>
+ {item.description}
+ </div>
+ </div>
+ );
+ }
+}
+
+CommandSuggestion.propTypes = {
+ item: React.PropTypes.object.isRequired,
+ isSelection: React.PropTypes.bool,
+ onClick: React.PropTypes.func
+};
+
+export default class CommandProvider {
+ handlePretextChanged(suggestionId, pretext) {
+ if (pretext.startsWith('/')) {
+ SuggestionStore.setMatchedPretext(suggestionId, pretext);
+ SuggestionStore.setCompleteOnSpace(suggestionId, false);
+
+ AsyncClient.getSuggestedCommands(pretext, suggestionId, CommandSuggestion);
+ }
+ }
+}
diff --git a/web/react/components/suggestion/emoticon_provider.jsx b/web/react/components/suggestion/emoticon_provider.jsx
new file mode 100644
index 000000000..7dcb86442
--- /dev/null
+++ b/web/react/components/suggestion/emoticon_provider.jsx
@@ -0,0 +1,74 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import SuggestionStore from '../../stores/suggestion_store.jsx';
+import * as Emoticons from '../../utils/emoticons.jsx';
+
+const MAX_EMOTICON_SUGGESTIONS = 40;
+
+class EmoticonSuggestion extends React.Component {
+ render() {
+ const text = this.props.term;
+ const name = this.props.item;
+
+ let className = 'emoticon-suggestion';
+ if (this.props.isSelection) {
+ className += ' suggestion--selected';
+ }
+
+ return (
+ <div
+ className={className}
+ onClick={this.props.onClick}
+ >
+ <div className='pull-left'>
+ <img
+ alt={text}
+ className='emoticon-suggestion__image'
+ src={Emoticons.getImagePathForEmoticon(name)}
+ title={text}
+ />
+ </div>
+ <div className='pull-left'>
+ {text}
+ </div>
+ </div>
+ );
+ }
+}
+
+EmoticonSuggestion.propTypes = {
+ item: React.PropTypes.string.isRequired,
+ term: React.PropTypes.string.isRequired,
+ isSelection: React.PropTypes.bool,
+ onClick: React.PropTypes.func
+};
+
+export default class EmoticonProvider {
+ handlePretextChanged(suggestionId, pretext) {
+ const captured = (/(?:^|\s)(:([a-zA-Z0-9_+\-]*))$/g).exec(pretext);
+ if (captured) {
+ const text = captured[1];
+ const partialName = captured[2];
+
+ const terms = [];
+ const names = [];
+
+ for (const emoticon of Emoticons.emoticonMap.keys()) {
+ if (emoticon.indexOf(partialName) !== -1) {
+ terms.push(':' + emoticon + ':');
+ names.push(emoticon);
+
+ if (terms.length >= MAX_EMOTICON_SUGGESTIONS) {
+ break;
+ }
+ }
+ }
+
+ if (terms.length > 0) {
+ SuggestionStore.setMatchedPretext(suggestionId, text);
+ SuggestionStore.addSuggestions(suggestionId, terms, names, EmoticonSuggestion);
+ }
+ }
+ }
+}
diff --git a/web/react/components/suggestion/search_channel_provider.jsx b/web/react/components/suggestion/search_channel_provider.jsx
new file mode 100644
index 000000000..7547a9341
--- /dev/null
+++ b/web/react/components/suggestion/search_channel_provider.jsx
@@ -0,0 +1,69 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import ChannelStore from '../../stores/channel_store.jsx';
+import Constants from '../../utils/constants.jsx';
+import SuggestionStore from '../../stores/suggestion_store.jsx';
+
+class SearchChannelSuggestion extends React.Component {
+ render() {
+ const {item, isSelection, onClick} = this.props;
+
+ let className = 'search-autocomplete__item';
+ if (isSelection) {
+ className += ' selected';
+ }
+
+ return (
+ <div
+ onClick={onClick}
+ className={className}
+ >
+ {item.name}
+ </div>
+ );
+ }
+}
+
+SearchChannelSuggestion.propTypes = {
+ item: React.PropTypes.object.isRequired,
+ isSelection: React.PropTypes.bool,
+ onClick: React.PropTypes.func
+};
+
+export default class SearchChannelProvider {
+ handlePretextChanged(suggestionId, pretext) {
+ const captured = (/\b(?:in|channel):\s*(\S*)$/i).exec(pretext);
+ if (captured) {
+ const channelPrefix = captured[1];
+
+ const channels = ChannelStore.getAll();
+ const publicChannels = [];
+ const privateChannels = [];
+
+ for (const id of Object.keys(channels)) {
+ const channel = channels[id];
+
+ // don't show direct channels
+ if (channel.type !== Constants.DM_CHANNEL && channel.name.startsWith(channelPrefix)) {
+ if (channel.type === Constants.OPEN_CHANNEL) {
+ publicChannels.push(channel);
+ } else {
+ privateChannels.push(channel);
+ }
+ }
+ }
+
+ publicChannels.sort((a, b) => a.name.localeCompare(b.name));
+ const publicChannelNames = publicChannels.map((channel) => channel.name);
+
+ privateChannels.sort((a, b) => a.name.localeCompare(b.name));
+ const privateChannelNames = privateChannels.map((channel) => channel.name);
+
+ SuggestionStore.setMatchedPretext(suggestionId, channelPrefix);
+
+ SuggestionStore.addSuggestions(suggestionId, publicChannelNames, publicChannels, SearchChannelSuggestion);
+ SuggestionStore.addSuggestions(suggestionId, privateChannelNames, privateChannels, SearchChannelSuggestion);
+ }
+ }
+}
diff --git a/web/react/components/suggestion/search_suggestion_list.jsx b/web/react/components/suggestion/search_suggestion_list.jsx
new file mode 100644
index 000000000..542d28ddd
--- /dev/null
+++ b/web/react/components/suggestion/search_suggestion_list.jsx
@@ -0,0 +1,86 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Constants from '../../utils/constants.jsx';
+import SuggestionList from './suggestion_list.jsx';
+import * as Utils from '../../utils/utils.jsx';
+
+export default class SearchSuggestionList extends SuggestionList {
+ componentDidUpdate(prevProps, prevState) {
+ if (this.state.items.length > 0 && prevState.items.length === 0) {
+ this.getContent().perfectScrollbar();
+ }
+ }
+
+ getContent() {
+ return $(ReactDOM.findDOMNode(this.refs.popover)).find('.popover-content');
+ }
+
+ renderChannelDivider(type) {
+ let text;
+ if (type === Constants.OPEN_CHANNEL) {
+ text = 'Public ' + Utils.getChannelTerm(type) + 's';
+ } else {
+ text = 'Private ' + Utils.getChannelTerm(type) + 's';
+ }
+
+ return (
+ <div
+ key={type + '-divider'}
+ className='search-autocomplete__divider'
+ >
+ <span>{text}</span>
+ </div>
+ );
+ }
+
+ render() {
+ if (this.state.items.length === 0 || !this.props.show) {
+ return null;
+ }
+
+ const items = [];
+ for (let i = 0; i < this.state.items.length; i++) {
+ const item = this.state.items[i];
+ const term = this.state.terms[i];
+ const isSelection = term === this.state.selection;
+
+ // ReactComponent names need to be upper case when used in JSX
+ const Component = this.state.components[i];
+
+ // temporary hack to add dividers between public and private channels in the search suggestion list
+ if (i === 0 || item.type !== this.state.items[i - 1].type) {
+ if (item.type === Constants.OPEN_CHANNEL) {
+ items.push(this.renderChannelDivider(Constants.OPEN_CHANNEL));
+ } else if (item.type === Constants.PRIVATE_CHANNEL) {
+ items.push(this.renderChannelDivider(Constants.PRIVATE_CHANNEL));
+ }
+ }
+
+ items.push(
+ <Component
+ key={term}
+ ref={term}
+ item={item}
+ isSelection={isSelection}
+ onClick={this.handleItemClick.bind(this, term)}
+ />
+ );
+ }
+
+ return (
+ <ReactBootstrap.Popover
+ ref='popover'
+ id='search-autocomplete__popover'
+ className='search-help-popover autocomplete visible'
+ placement='bottom'
+ >
+ {items}
+ </ReactBootstrap.Popover>
+ );
+ }
+}
+
+SearchSuggestionList.propTypes = {
+ ...SuggestionList.propTypes
+};
diff --git a/web/react/components/suggestion/search_user_provider.jsx b/web/react/components/suggestion/search_user_provider.jsx
new file mode 100644
index 000000000..cf2953937
--- /dev/null
+++ b/web/react/components/suggestion/search_user_provider.jsx
@@ -0,0 +1,62 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import SuggestionStore from '../../stores/suggestion_store.jsx';
+import UserStore from '../../stores/user_store.jsx';
+
+class SearchUserSuggestion extends React.Component {
+ render() {
+ const {item, isSelection, onClick} = this.props;
+
+ let className = 'search-autocomplete__item';
+ if (isSelection) {
+ className += ' selected';
+ }
+
+ return (
+ <div
+ className={className}
+ onClick={onClick}
+ >
+ <img
+ className='profile-img rounded'
+ src={'/api/v1/users/' + item.id + '/image?time=' + item.update_at}
+ />
+ {item.username}
+ </div>
+ );
+ }
+}
+
+SearchUserSuggestion.propTypes = {
+ item: React.PropTypes.object.isRequired,
+ isSelection: React.PropTypes.bool,
+ onClick: React.PropTypes.func
+};
+
+export default class SearchUserProvider {
+ handlePretextChanged(suggestionId, pretext) {
+ const captured = (/\bfrom:\s*(\S*)$/i).exec(pretext);
+ if (captured) {
+ const usernamePrefix = captured[1];
+
+ const users = UserStore.getProfiles();
+ let filtered = [];
+
+ for (const id of Object.keys(users)) {
+ const user = users[id];
+
+ if (user.username.startsWith(usernamePrefix)) {
+ filtered.push(user);
+ }
+ }
+
+ filtered = filtered.sort((a, b) => a.username.localeCompare(b.username));
+
+ const usernames = filtered.map((user) => user.username);
+
+ SuggestionStore.setMatchedPretext(suggestionId, usernamePrefix);
+ SuggestionStore.addSuggestions(suggestionId, usernames, filtered, SearchUserSuggestion);
+ }
+ }
+}
diff --git a/web/react/components/suggestion/suggestion_box.jsx b/web/react/components/suggestion/suggestion_box.jsx
new file mode 100644
index 000000000..4ca461e82
--- /dev/null
+++ b/web/react/components/suggestion/suggestion_box.jsx
@@ -0,0 +1,189 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Constants from '../../utils/constants.jsx';
+import * as EventHelpers from '../../dispatcher/event_helpers.jsx';
+import SuggestionStore from '../../stores/suggestion_store.jsx';
+import * as Utils from '../../utils/utils.jsx';
+
+const KeyCodes = Constants.KeyCodes;
+
+export default class SuggestionBox extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleDocumentClick = this.handleDocumentClick.bind(this);
+ this.handleFocus = this.handleFocus.bind(this);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleCompleteWord = this.handleCompleteWord.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.handlePretextChanged = this.handlePretextChanged.bind(this);
+
+ this.suggestionId = Utils.generateId();
+
+ this.state = {
+ focused: false
+ };
+ }
+
+ componentDidMount() {
+ SuggestionStore.registerSuggestionBox(this.suggestionId);
+ $(document).on('click', this.handleDocumentClick);
+
+ SuggestionStore.addCompleteWordListener(this.suggestionId, this.handleCompleteWord);
+ SuggestionStore.addPretextChangedListener(this.suggestionId, this.handlePretextChanged);
+ }
+
+ componentWillUnmount() {
+ SuggestionStore.removeCompleteWordListener(this.suggestionId, this.handleCompleteWord);
+ SuggestionStore.removePretextChangedListener(this.suggestionId, this.handlePretextChanged);
+
+ SuggestionStore.unregisterSuggestionBox(this.suggestionId);
+ $(document).off('click', this.handleDocumentClick);
+ }
+
+ getTextbox() {
+ // this is to support old code that looks at the input/textarea DOM nodes
+ return ReactDOM.findDOMNode(this.refs.textbox);
+ }
+
+ handleDocumentClick(e) {
+ if (!this.state.focused) {
+ return;
+ }
+
+ const container = $(ReactDOM.findDOMNode(this));
+ if (!(container.is(e.target) || container.has(e.target).length > 0)) {
+ // we can't just use blur for this because it fires and hides the children before
+ // their click handlers can be called
+ this.setState({
+ focused: false
+ });
+ }
+ }
+
+ handleFocus() {
+ this.setState({
+ focused: true
+ });
+
+ if (this.props.onFocus) {
+ this.props.onFocus();
+ }
+ }
+
+ handleChange(e) {
+ const textbox = ReactDOM.findDOMNode(this.refs.textbox);
+ const caret = Utils.getCaretPosition(textbox);
+ const pretext = textbox.value.substring(0, caret);
+
+ EventHelpers.emitSuggestionPretextChanged(this.suggestionId, pretext);
+
+ if (this.props.onUserInput) {
+ this.props.onUserInput(textbox.value);
+ }
+
+ if (this.props.onChange) {
+ this.props.onChange(e);
+ }
+ }
+
+ handleCompleteWord(term) {
+ const textbox = ReactDOM.findDOMNode(this.refs.textbox);
+ const caret = Utils.getCaretPosition(textbox);
+
+ const text = this.props.value;
+ const prefix = text.substring(0, caret - SuggestionStore.getMatchedPretext(this.suggestionId).length);
+ const suffix = text.substring(caret);
+
+ if (this.props.onUserInput) {
+ this.props.onUserInput(prefix + term + ' ' + suffix);
+ }
+
+ // set the caret position after the next rendering
+ window.requestAnimationFrame(() => {
+ Utils.setCaretPosition(textbox, prefix.length + term.length + 1);
+ });
+ }
+
+ handleKeyDown(e) {
+ if (SuggestionStore.hasSuggestions(this.suggestionId)) {
+ if (e.which === KeyCodes.UP) {
+ EventHelpers.emitSelectPreviousSuggestion(this.suggestionId);
+ e.preventDefault();
+ } else if (e.which === KeyCodes.DOWN) {
+ EventHelpers.emitSelectNextSuggestion(this.suggestionId);
+ e.preventDefault();
+ } else if (e.which === KeyCodes.ENTER || (e.which === KeyCodes.SPACE && SuggestionStore.shouldCompleteOnSpace(this.suggestionId))) {
+ EventHelpers.emitCompleteWordSuggestion(this.suggestionId);
+ e.preventDefault();
+ } else if (this.props.onKeyDown) {
+ this.props.onKeyDown(e);
+ }
+ } else if (this.props.onKeyDown) {
+ this.props.onKeyDown(e);
+ }
+ }
+
+ handlePretextChanged(pretext) {
+ for (const provider of this.props.providers) {
+ provider.handlePretextChanged(this.suggestionId, pretext);
+ }
+ }
+
+ render() {
+ const newProps = Object.assign({}, this.props, {
+ onFocus: this.handleFocus,
+ onChange: this.handleChange,
+ onKeyDown: this.handleKeyDown
+ });
+
+ let textbox = null;
+ if (this.props.type === 'input') {
+ textbox = (
+ <input
+ ref='textbox'
+ type='text'
+ {...newProps}
+ />
+ );
+ } else if (this.props.type === 'textarea') {
+ textbox = (
+ <textarea
+ ref='textbox'
+ {...newProps}
+ />
+ );
+ }
+
+ const SuggestionListComponent = this.props.listComponent;
+
+ return (
+ <div>
+ {textbox}
+ <SuggestionListComponent
+ suggestionId={this.suggestionId}
+ show={this.state.focused}
+ />
+ </div>
+ );
+ }
+}
+
+SuggestionBox.defaultProps = {
+ type: 'input'
+};
+
+SuggestionBox.propTypes = {
+ listComponent: React.PropTypes.func.isRequired,
+ type: React.PropTypes.oneOf(['input', 'textarea']).isRequired,
+ value: React.PropTypes.string.isRequired,
+ onUserInput: React.PropTypes.func,
+ providers: React.PropTypes.arrayOf(React.PropTypes.object),
+
+ // explicitly name any input event handlers we override and need to manually call
+ onChange: React.PropTypes.func,
+ onKeyDown: React.PropTypes.func,
+ onFocus: React.PropTypes.func
+};
diff --git a/web/react/components/suggestion/suggestion_list.jsx b/web/react/components/suggestion/suggestion_list.jsx
new file mode 100644
index 000000000..87021fd94
--- /dev/null
+++ b/web/react/components/suggestion/suggestion_list.jsx
@@ -0,0 +1,126 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as EventHelpers from '../../dispatcher/event_helpers.jsx';
+import SuggestionStore from '../../stores/suggestion_store.jsx';
+
+export default class SuggestionList extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getContent = this.getContent.bind(this);
+
+ this.handleItemClick = this.handleItemClick.bind(this);
+ this.handleSuggestionsChanged = this.handleSuggestionsChanged.bind(this);
+
+ this.scrollToItem = this.scrollToItem.bind(this);
+
+ this.state = {
+ items: [],
+ terms: [],
+ components: [],
+ selection: ''
+ };
+ }
+
+ componentDidMount() {
+ SuggestionStore.addSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
+ }
+
+ componentWillUnmount() {
+ SuggestionStore.removeSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
+ }
+
+ getContent() {
+ return $(ReactDOM.findDOMNode(this.refs.content));
+ }
+
+ handleItemClick(term, e) {
+ EventHelpers.emitCompleteWordSuggestion(this.props.suggestionId, term);
+
+ e.preventDefault();
+ }
+
+ handleSuggestionsChanged() {
+ const selection = SuggestionStore.getSelection(this.props.suggestionId);
+
+ this.setState({
+ items: SuggestionStore.getItems(this.props.suggestionId),
+ terms: SuggestionStore.getTerms(this.props.suggestionId),
+ components: SuggestionStore.getComponents(this.props.suggestionId),
+ selection
+ });
+
+ if (selection) {
+ window.requestAnimationFrame(() => this.scrollToItem(this.state.selection));
+ }
+ }
+
+ scrollToItem(term) {
+ const content = this.getContent();
+ const visibleContentHeight = content[0].clientHeight;
+ const actualContentHeight = content[0].scrollHeight;
+
+ if (visibleContentHeight < actualContentHeight) {
+ const contentTop = content.scrollTop();
+ const contentTopPadding = parseInt(content.css('padding-top'), 10);
+ const contentBottomPadding = parseInt(content.css('padding-top'), 10);
+
+ const item = $(ReactDOM.findDOMNode(this.refs[term]));
+ const itemTop = item[0].offsetTop - parseInt(item.css('margin-top'), 10);
+ const itemBottomMargin = parseInt(item.css('margin-bottom'), 10) + parseInt(item.css('padding-bottom'), 10);
+ const itemBottom = item[0].offsetTop + item.height() + itemBottomMargin;
+
+ if (itemTop - contentTopPadding < contentTop) {
+ // the item is off the top of the visible space
+ content.scrollTop(itemTop - contentTopPadding);
+ } else if (itemBottom + contentTopPadding + contentBottomPadding > contentTop + visibleContentHeight) {
+ // the item has gone off the bottom of the visible space
+ content.scrollTop(itemBottom - visibleContentHeight + contentTopPadding + contentBottomPadding);
+ }
+ }
+ }
+
+ render() {
+ if (this.state.items.length === 0 || !this.props.show) {
+ return null;
+ }
+
+ const items = [];
+ for (let i = 0; i < this.state.items.length; i++) {
+ const item = this.state.items[i];
+ const term = this.state.terms[i];
+ const isSelection = term === this.state.selection;
+
+ // ReactComponent names need to be upper case when used in JSX
+ const Component = this.state.components[i];
+
+ items.push(
+ <Component
+ key={term}
+ ref={term}
+ item={item}
+ term={term}
+ isSelection={isSelection}
+ onClick={this.handleItemClick.bind(this, term)}
+ />
+ );
+ }
+
+ return (
+ <div className='suggestion-list suggestion-list--top'>
+ <div
+ ref='content'
+ className='suggestion-content suggestion-content--top'
+ >
+ {items}
+ </div>
+ </div>
+ );
+ }
+}
+
+SuggestionList.propTypes = {
+ suggestionId: React.PropTypes.string.isRequired,
+ show: React.PropTypes.bool.isRequired
+};
diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx
index 795fad671..03715d585 100644
--- a/web/react/components/team_general_tab.jsx
+++ b/web/react/components/team_general_tab.jsx
@@ -437,6 +437,7 @@ export default class GeneralTab extends React.Component {
<input
className='form-control'
type='text'
+ maxLength='22'
onChange={this.updateName}
value={this.state.name}
/>
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index 10b3c0069..b29f304ab 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -1,16 +1,16 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
-import SearchStore from '../stores/search_store.jsx';
-import CommandList from './command_list.jsx';
+import AtMentionProvider from './suggestion/at_mention_provider.jsx';
+import CommandProvider from './suggestion/command_provider.jsx';
+import EmoticonProvider from './suggestion/emoticon_provider.jsx';
+import SuggestionList from './suggestion/suggestion_list.jsx';
+import SuggestionBox from './suggestion/suggestion_box.jsx';
import ErrorStore from '../stores/error_store.jsx';
import * as TextFormatting from '../utils/text_formatting.jsx';
import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
-const ActionTypes = Constants.ActionTypes;
-const KeyCodes = Constants.KeyCodes;
const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
export default class Textbox extends React.Component {
@@ -18,32 +18,22 @@ export default class Textbox extends React.Component {
super(props);
this.getStateFromStores = this.getStateFromStores.bind(this);
- this.onListenerChange = this.onListenerChange.bind(this);
this.onRecievedError = this.onRecievedError.bind(this);
- this.updateMentionTab = this.updateMentionTab.bind(this);
- this.handleChange = this.handleChange.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
- this.handleBackspace = this.handleBackspace.bind(this);
- this.checkForNewMention = this.checkForNewMention.bind(this);
- this.addMention = this.addMention.bind(this);
- this.addCommand = this.addCommand.bind(this);
this.resize = this.resize.bind(this);
this.handleFocus = this.handleFocus.bind(this);
this.handleBlur = this.handleBlur.bind(this);
- this.handlePaste = this.handlePaste.bind(this);
this.showPreview = this.showPreview.bind(this);
this.state = {
- mentionText: '-1',
- mentions: [],
connection: ''
};
- this.caret = -1;
- this.addedMention = false;
- this.doProcessMentions = false;
- this.mentions = [];
+ this.suggestionProviders = [new AtMentionProvider(), new EmoticonProvider()];
+ if (props.supportsCommands) {
+ this.suggestionProviders.push(new CommandProvider());
+ }
}
getStateFromStores() {
@@ -57,24 +47,15 @@ export default class Textbox extends React.Component {
}
componentDidMount() {
- SearchStore.addAddMentionListener(this.onListenerChange);
ErrorStore.addChangeListener(this.onRecievedError);
this.resize();
- this.updateMentionTab(null);
}
componentWillUnmount() {
- SearchStore.removeAddMentionListener(this.onListenerChange);
ErrorStore.removeChangeListener(this.onRecievedError);
}
- onListenerChange(id, username) {
- if (id === this.props.id) {
- this.addMention(username);
- }
- }
-
onRecievedError() {
const errorState = ErrorStore.getLastError();
@@ -86,158 +67,21 @@ export default class Textbox extends React.Component {
}
componentDidUpdate() {
- if (this.caret >= 0) {
- Utils.setCaretPosition(ReactDOM.findDOMNode(this.refs.message), this.caret);
- this.caret = -1;
- }
- if (this.doProcessMentions) {
- this.updateMentionTab(null);
- this.doProcessMentions = false;
- }
this.resize();
}
- componentWillReceiveProps(nextProps) {
- if (!this.addedMention) {
- this.checkForNewMention(nextProps.messageText);
- }
- const text = ReactDOM.findDOMNode(this.refs.message).value;
- if (nextProps.channelId !== this.props.channelId || nextProps.messageText !== text) {
- this.doProcessMentions = true;
- }
- this.addedMention = false;
- this.refs.commands.getSuggestedCommands(nextProps.messageText);
- }
-
- updateMentionTab(mentionText) {
- // using setTimeout so dispatch isn't called during an in progress dispatch
- setTimeout(() => {
- AppDispatcher.handleViewAction({
- type: ActionTypes.RECIEVED_MENTION_DATA,
- id: this.props.id,
- mention_text: mentionText
- });
- }, 1);
- }
-
- handleChange() {
- const text = ReactDOM.findDOMNode(this.refs.message).value;
- this.props.onUserInput(text);
- }
-
handleKeyPress(e) {
- const text = ReactDOM.findDOMNode(this.refs.message).value;
-
- if (!this.refs.commands.isEmpty() && text.indexOf('/') === 0 && e.which === 13) {
- this.refs.commands.addFirstCommand();
- e.preventDefault();
- return;
- }
-
- if (!this.doProcessMentions) {
- const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
- const preText = text.substring(0, caret);
- const lastSpace = preText.lastIndexOf(' ');
- const lastAt = preText.lastIndexOf('@');
-
- if (caret > lastAt && lastSpace < lastAt) {
- this.doProcessMentions = true;
- }
- }
-
this.props.onKeyPress(e);
}
handleKeyDown(e) {
- if (Utils.getSelectedText(ReactDOM.findDOMNode(this.refs.message)) !== '') {
- this.doProcessMentions = true;
- }
-
- if (e.keyCode === KeyCodes.BACKSPACE) {
- this.handleBackspace(e);
- } else if (this.props.onKeyDown) {
+ if (this.props.onKeyDown) {
this.props.onKeyDown(e);
}
}
- handleBackspace() {
- const text = ReactDOM.findDOMNode(this.refs.message).value;
- if (text.indexOf('/') === 0) {
- this.refs.commands.getSuggestedCommands(text.substring(0, text.length - 1));
- }
-
- if (this.doProcessMentions) {
- return;
- }
-
- const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
- const preText = text.substring(0, caret);
- const lastSpace = preText.lastIndexOf(' ');
- const lastAt = preText.lastIndexOf('@');
-
- if (caret > lastAt && (lastSpace > lastAt || lastSpace === -1)) {
- this.doProcessMentions = true;
- }
- }
-
- checkForNewMention(text) {
- const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
-
- const preText = text.substring(0, caret);
-
- const atIndex = preText.lastIndexOf('@');
-
- // The @ character not typed, so nothing to do.
- if (atIndex === -1) {
- this.updateMentionTab('-1');
- return;
- }
-
- const lastCharSpace = preText.lastIndexOf(String.fromCharCode(160));
- const lastSpace = preText.lastIndexOf(' ');
-
- // If there is a space after the last @, nothing to do.
- if (lastSpace > atIndex || lastCharSpace > atIndex) {
- this.updateMentionTab('-1');
- return;
- }
-
- // Get the name typed so far.
- const name = preText.substring(atIndex + 1, preText.length).toLowerCase();
- this.updateMentionTab(name);
- }
-
- addMention(name) {
- const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
-
- const text = this.props.messageText;
-
- const preText = text.substring(0, caret);
-
- const atIndex = preText.lastIndexOf('@');
-
- // The @ character not typed, so nothing to do.
- if (atIndex === -1) {
- return;
- }
-
- const prefix = text.substring(0, atIndex);
- const suffix = text.substring(caret, text.length);
- this.caret = prefix.length + name.length + 2;
- this.addedMention = true;
- this.doProcessMentions = true;
-
- this.props.onUserInput(`${prefix}@${name} ${suffix}`);
- }
-
- addCommand(cmd) {
- const elm = ReactDOM.findDOMNode(this.refs.message);
- elm.value = cmd;
- this.handleChange();
- }
-
resize() {
- const e = ReactDOM.findDOMNode(this.refs.message);
+ const e = this.refs.message.getTextbox();
const w = ReactDOM.findDOMNode(this.refs.wrapper);
const prevHeight = $(e).height();
@@ -272,23 +116,19 @@ export default class Textbox extends React.Component {
}
handleFocus() {
- const elm = ReactDOM.findDOMNode(this.refs.message);
+ const elm = this.refs.message.getTextbox();
if (elm.title === elm.value) {
elm.value = '';
}
}
handleBlur() {
- const elm = ReactDOM.findDOMNode(this.refs.message);
+ const elm = this.refs.message.getTextbox();
if (elm.value === '') {
elm.value = elm.title;
}
}
- handlePaste() {
- this.doProcessMentions = true;
- }
-
showPreview(e) {
e.preventDefault();
e.target.blur();
@@ -323,15 +163,11 @@ export default class Textbox extends React.Component {
ref='wrapper'
className='textarea-wrapper'
>
- <CommandList
- ref='commands'
- addCommand={this.addCommand}
- channelId={this.props.channelId}
- />
- <textarea
+ <SuggestionBox
id={this.props.id}
ref='message'
className={`form-control custom-textarea ${this.state.connection}`}
+ type='textarea'
spellCheck='true'
autoComplete='off'
autoCorrect='off'
@@ -339,14 +175,15 @@ export default class Textbox extends React.Component {
maxLength={Constants.MAX_POST_LEN}
placeholder={this.props.createMessage}
value={this.props.messageText}
- onInput={this.handleChange}
- onChange={this.handleChange}
+ onUserInput={this.props.onUserInput}
onKeyPress={this.handleKeyPress}
onKeyDown={this.handleKeyDown}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onPaste={this.handlePaste}
style={{visibility: this.state.preview ? 'hidden' : 'visible'}}
+ listComponent={SuggestionList}
+ providers={this.suggestionProviders}
/>
<div
ref='preview'
@@ -367,6 +204,10 @@ export default class Textbox extends React.Component {
}
}
+Textbox.defaultProps = {
+ supportsCommands: true
+};
+
Textbox.propTypes = {
id: React.PropTypes.string.isRequired,
channelId: React.PropTypes.string,
@@ -375,5 +216,6 @@ Textbox.propTypes = {
onKeyPress: React.PropTypes.func.isRequired,
onHeightChange: React.PropTypes.func,
createMessage: React.PropTypes.string.isRequired,
- onKeyDown: React.PropTypes.func
+ onKeyDown: React.PropTypes.func,
+ supportsCommands: React.PropTypes.bool.isRequired
};
diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index e025bf670..f762405af 100644
--- a/web/react/components/user_settings/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -78,7 +78,9 @@ export default class NotificationsTab extends React.Component {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleCancel = this.handleCancel.bind(this);
this.updateSection = this.updateSection.bind(this);
+ this.updateState = this.updateState.bind(this);
this.onListenerChange = this.onListenerChange.bind(this);
this.handleNotifyRadio = this.handleNotifyRadio.bind(this);
this.handleEmailRadio = this.handleEmailRadio.bind(this);
@@ -128,10 +130,21 @@ export default class NotificationsTab extends React.Component {
}.bind(this)
);
}
+ handleCancel(e) {
+ this.updateState();
+ this.props.updateSection('');
+ e.preventDefault();
+ }
updateSection(section) {
- this.setState(getNotificationsStateFromStores());
+ this.updateState();
this.props.updateSection(section);
}
+ updateState() {
+ const newState = getNotificationsStateFromStores();
+ if (!Utils.areObjectsEqual(newState, this.state)) {
+ this.setState(newState);
+ }
+ }
componentDidMount() {
UserStore.addChangeListener(this.onListenerChange);
}
@@ -139,10 +152,7 @@ export default class NotificationsTab extends React.Component {
UserStore.removeChangeListener(this.onListenerChange);
}
onListenerChange() {
- var newState = getNotificationsStateFromStores();
- if (!Utils.areObjectsEqual(newState, this.state)) {
- this.setState(newState);
- }
+ this.updateState();
}
handleNotifyRadio(notifyLevel) {
this.setState({notifyLevel: notifyLevel});
@@ -245,11 +255,6 @@ export default class NotificationsTab extends React.Component {
</div>
);
- handleUpdateDesktopSection = function updateDesktopSection(e) {
- this.props.updateSection('');
- e.preventDefault();
- }.bind(this);
-
const extraInfo = <span>{'Desktop notifications are available on Firefox, Safari, and Chrome.'}</span>;
desktopSection = (
@@ -259,7 +264,7 @@ export default class NotificationsTab extends React.Component {
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
- updateSection={handleUpdateDesktopSection}
+ updateSection={this.handleCancel}
/>
);
} else {
@@ -324,11 +329,6 @@ export default class NotificationsTab extends React.Component {
</div>
);
- handleUpdateSoundSection = function updateSoundSection(e) {
- this.props.updateSection('');
- e.preventDefault();
- }.bind(this);
-
const extraInfo = <span>{'Desktop notification sounds are available on Firefox, Safari, Chrome, Internet Explorer, and Edge.'}</span>;
soundSection = (
@@ -338,7 +338,7 @@ export default class NotificationsTab extends React.Component {
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
- updateSection={handleUpdateSoundSection}
+ updateSection={this.handleCancel}
/>
);
} else {
@@ -405,18 +405,13 @@ export default class NotificationsTab extends React.Component {
</div>
);
- handleUpdateEmailSection = function updateEmailSection(e) {
- this.props.updateSection('');
- e.preventDefault();
- }.bind(this);
-
emailSection = (
<SettingItemMax
title='Email notifications'
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
- updateSection={handleUpdateEmailSection}
+ updateSection={this.handleCancel}
/>
);
} else {
@@ -566,17 +561,13 @@ export default class NotificationsTab extends React.Component {
</div>
);
- handleUpdateKeysSection = function updateKeysSection(e) {
- this.props.updateSection('');
- e.preventDefault();
- }.bind(this);
keysSection = (
<SettingItemMax
title='Words that trigger mentions'
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
- updateSection={handleUpdateKeysSection}
+ updateSection={this.handleCancel}
/>
);
} else {
@@ -653,7 +644,7 @@ export default class NotificationsTab extends React.Component {
ref='wrapper'
className='user-settings'
>
- <h3 className='tab-header'>Notifications</h3>
+ <h3 className='tab-header'>{'Notifications'}</h3>
<div className='divider-dark first'/>
{desktopSection}
<div className='divider-light'/>
diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx
index 57b4eaa11..f792c610f 100644
--- a/web/react/dispatcher/event_helpers.jsx
+++ b/web/react/dispatcher/event_helpers.jsx
@@ -111,3 +111,33 @@ export function showRegisterAppModal() {
value: true
});
}
+
+export function emitSuggestionPretextChanged(suggestionId, pretext) {
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.SUGGESTION_PRETEXT_CHANGED,
+ id: suggestionId,
+ pretext
+ });
+}
+
+export function emitSelectNextSuggestion(suggestionId) {
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.SUGGESTION_SELECT_NEXT,
+ id: suggestionId
+ });
+}
+
+export function emitSelectPreviousSuggestion(suggestionId) {
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.SUGGESTION_SELECT_PREVIOUS,
+ id: suggestionId
+ });
+}
+
+export function emitCompleteWordSuggestion(suggestionId, term = '') {
+ AppDispatcher.handleViewAction({
+ type: Constants.ActionTypes.SUGGESTION_COMPLETE_WORD,
+ id: suggestionId,
+ term
+ });
+}
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
index b73dfdafe..49f0935a9 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -6,7 +6,6 @@ import ChannelLoader from '../components/channel_loader.jsx';
import ErrorBar from '../components/error_bar.jsx';
import ErrorStore from '../stores/error_store.jsx';
-import MentionList from '../components/mention_list.jsx';
import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx';
import RenameChannelModal from '../components/rename_channel_modal.jsx';
import EditPostModal from '../components/edit_post_modal.jsx';
@@ -47,21 +46,6 @@ function setupChannelPage(props, team, channel) {
document.getElementById('channel_view')
);
- ReactDOM.render(
- <MentionList id='post_textbox' />,
- document.getElementById('post_mention_tab')
- );
-
- ReactDOM.render(
- <MentionList id='reply_textbox' />,
- document.getElementById('reply_mention_tab')
- );
-
- ReactDOM.render(
- <MentionList id='edit_textbox' />,
- document.getElementById('edit_mention_tab')
- );
-
//
// Modals
//
diff --git a/web/react/stores/search_store.jsx b/web/react/stores/search_store.jsx
index e8ab6a2ae..f932c379a 100644
--- a/web/react/stores/search_store.jsx
+++ b/web/react/stores/search_store.jsx
@@ -12,8 +12,6 @@ var ActionTypes = Constants.ActionTypes;
var CHANGE_EVENT = 'change';
var SEARCH_CHANGE_EVENT = 'search_change';
var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
-var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
-var ADD_MENTION_EVENT = 'add_mention';
var SHOW_SEARCH_EVENT = 'show_search';
class SearchStoreClass extends EventEmitter {
@@ -32,10 +30,6 @@ class SearchStoreClass extends EventEmitter {
this.addSearchTermChangeListener = this.addSearchTermChangeListener.bind(this);
this.removeSearchTermChangeListener = this.removeSearchTermChangeListener.bind(this);
- this.emitMentionDataChange = this.emitMentionDataChange.bind(this);
- this.addMentionDataChangeListener = this.addMentionDataChangeListener.bind(this);
- this.removeMentionDataChangeListener = this.removeMentionDataChangeListener.bind(this);
-
this.emitShowSearch = this.emitShowSearch.bind(this);
this.addShowSearchListener = this.addShowSearchListener.bind(this);
this.removeShowSearchListener = this.removeShowSearchListener.bind(this);
@@ -113,30 +107,6 @@ class SearchStoreClass extends EventEmitter {
return BrowserStore.getItem('search_term');
}
- emitMentionDataChange(id, mentionText) {
- this.emit(MENTION_DATA_CHANGE_EVENT, id, mentionText);
- }
-
- addMentionDataChangeListener(callback) {
- this.on(MENTION_DATA_CHANGE_EVENT, callback);
- }
-
- removeMentionDataChangeListener(callback) {
- this.removeListener(MENTION_DATA_CHANGE_EVENT, callback);
- }
-
- emitAddMention(id, username) {
- this.emit(ADD_MENTION_EVENT, id, username);
- }
-
- addAddMentionListener(callback) {
- this.on(ADD_MENTION_EVENT, callback);
- }
-
- removeAddMentionListener(callback) {
- this.removeListener(ADD_MENTION_EVENT, callback);
- }
-
storeSearchResults(results, isMentionSearch) {
BrowserStore.setItem('search_results', results);
BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch));
@@ -157,12 +127,6 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => {
SearchStore.storeSearchTerm(action.term);
SearchStore.emitSearchTermChange(action.do_search, action.is_mention_search);
break;
- case ActionTypes.RECIEVED_MENTION_DATA:
- SearchStore.emitMentionDataChange(action.id, action.mention_text);
- break;
- case ActionTypes.RECIEVED_ADD_MENTION:
- SearchStore.emitAddMention(action.id, action.username);
- break;
case ActionTypes.SHOW_SEARCH:
SearchStore.emitShowSearch();
break;
diff --git a/web/react/stores/suggestion_store.jsx b/web/react/stores/suggestion_store.jsx
new file mode 100644
index 000000000..182f5810f
--- /dev/null
+++ b/web/react/stores/suggestion_store.jsx
@@ -0,0 +1,267 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import Constants from '../utils/constants.jsx';
+import EventEmitter from 'events';
+
+const ActionTypes = Constants.ActionTypes;
+
+const COMPLETE_WORD_EVENT = 'complete_word';
+const PRETEXT_CHANGED_EVENT = 'pretext_changed';
+const SUGGESTIONS_CHANGED_EVENT = 'suggestions_changed';
+
+class SuggestionStore extends EventEmitter {
+ constructor() {
+ super();
+
+ this.addSuggestionsChangedListener = this.addSuggestionsChangedListener.bind(this);
+ this.removeSuggestionsChangedListener = this.removeSuggestionsChangedListener.bind(this);
+ this.emitSuggestionsChanged = this.emitSuggestionsChanged.bind(this);
+
+ this.addPretextChangedListener = this.addPretextChangedListener.bind(this);
+ this.removePretextChangedListener = this.removePretextChangedListener.bind(this);
+ this.emitPretextChanged = this.emitPretextChanged.bind(this);
+
+ this.addCompleteWordListener = this.addCompleteWordListener.bind(this);
+ this.removeCompleteWordListener = this.removeCompleteWordListener.bind(this);
+ this.emitCompleteWord = this.emitCompleteWord.bind(this);
+
+ this.handleEventPayload = this.handleEventPayload.bind(this);
+ this.dispatchToken = AppDispatcher.register(this.handleEventPayload);
+
+ // this.suggestions stores the state of all SuggestionBoxes by mapping their unique identifier to an
+ // object with the following fields:
+ // pretext: the text before the cursor
+ // matchedPretext: the text before the cursor that will be replaced if an autocomplete term is selected
+ // terms: a list of strings which the previously typed text may be replaced by
+ // items: a list of objects backing the terms which may be used in rendering
+ // components: a list of react components that can be used to render their corresponding item
+ // selection: the term currently selected by the keyboard
+ // completeOnSpace: whether or not space will trigger the term to be autocompleted
+ this.suggestions = new Map();
+ }
+
+ addSuggestionsChangedListener(id, callback) {
+ this.on(SUGGESTIONS_CHANGED_EVENT + id, callback);
+ }
+ removeSuggestionsChangedListener(id, callback) {
+ this.removeListener(SUGGESTIONS_CHANGED_EVENT + id, callback);
+ }
+ emitSuggestionsChanged(id) {
+ this.emit(SUGGESTIONS_CHANGED_EVENT + id);
+ }
+
+ addPretextChangedListener(id, callback) {
+ this.on(PRETEXT_CHANGED_EVENT + id, callback);
+ }
+ removePretextChangedListener(id, callback) {
+ this.removeListener(PRETEXT_CHANGED_EVENT + id, callback);
+ }
+ emitPretextChanged(id, pretext) {
+ this.emit(PRETEXT_CHANGED_EVENT + id, pretext);
+ }
+
+ addCompleteWordListener(id, callback) {
+ this.on(COMPLETE_WORD_EVENT + id, callback);
+ }
+ removeCompleteWordListener(id, callback) {
+ this.removeListener(COMPLETE_WORD_EVENT + id, callback);
+ }
+ emitCompleteWord(id, term) {
+ this.emit(COMPLETE_WORD_EVENT + id, term);
+ }
+
+ registerSuggestionBox(id) {
+ this.suggestions.set(id, {
+ pretext: '',
+ matchedPretext: '',
+ terms: [],
+ items: [],
+ components: [],
+ selection: '',
+ completeOnSpace: true
+ });
+ }
+
+ unregisterSuggestionBox(id) {
+ this.suggestions.delete(id);
+ }
+
+ clearSuggestions(id) {
+ const suggestion = this.suggestions.get(id);
+
+ suggestion.matchedPretext = '';
+ suggestion.terms = [];
+ suggestion.items = [];
+ suggestion.components = [];
+ suggestion.completeOnSpace = true;
+ }
+
+ clearSelection(id) {
+ const suggestion = this.suggestions.get(id);
+
+ suggestion.selection = '';
+ }
+
+ hasSuggestions(id) {
+ return this.suggestions.get(id).terms.length > 0;
+ }
+
+ setPretext(id, pretext) {
+ const suggestion = this.suggestions.get(id);
+
+ suggestion.pretext = pretext;
+ }
+
+ setMatchedPretext(id, matchedPretext) {
+ const suggestion = this.suggestions.get(id);
+
+ suggestion.matchedPretext = matchedPretext;
+ }
+
+ setCompleteOnSpace(id, completeOnSpace) {
+ const suggestion = this.suggestions.get(id);
+
+ suggestion.completeOnSpace = completeOnSpace;
+ }
+
+ addSuggestion(id, term, item, component) {
+ const suggestion = this.suggestions.get(id);
+
+ suggestion.terms.push(term);
+ suggestion.items.push(item);
+ suggestion.components.push(component);
+ }
+
+ addSuggestions(id, terms, items, component) {
+ const suggestion = this.suggestions.get(id);
+
+ suggestion.terms.push(...terms);
+ suggestion.items.push(...items);
+
+ for (let i = 0; i < terms.length; i++) {
+ suggestion.components.push(component);
+ }
+ }
+
+ // make sure that if suggestions exist, then one of them is selected. return true if the selection changes.
+ ensureSelectionExists(id) {
+ const suggestion = this.suggestions.get(id);
+
+ if (suggestion.terms.length > 0) {
+ // if the current selection is no longer in the map, select the first term in the list
+ if (!suggestion.selection || suggestion.terms.indexOf(suggestion.selection) === -1) {
+ suggestion.selection = suggestion.terms[0];
+
+ return true;
+ }
+ } else if (suggestion.selection) {
+ suggestion.selection = '';
+
+ return true;
+ }
+
+ return false;
+ }
+
+ getPretext(id) {
+ return this.suggestions.get(id).pretext;
+ }
+
+ getMatchedPretext(id) {
+ return this.suggestions.get(id).matchedPretext;
+ }
+
+ getItems(id) {
+ return this.suggestions.get(id).items;
+ }
+
+ getTerms(id) {
+ return this.suggestions.get(id).terms;
+ }
+
+ getComponents(id) {
+ return this.suggestions.get(id).components;
+ }
+
+ getSelection(id) {
+ return this.suggestions.get(id).selection;
+ }
+
+ shouldCompleteOnSpace(id) {
+ return this.suggestions.get(id).completeOnSpace;
+ }
+
+ selectNext(id) {
+ this.setSelectionByDelta(id, 1);
+ }
+
+ selectPrevious(id) {
+ this.setSelectionByDelta(id, -1);
+ }
+
+ setSelectionByDelta(id, delta) {
+ const suggestion = this.suggestions.get(id);
+
+ let selectionIndex = suggestion.terms.indexOf(suggestion.selection);
+
+ if (selectionIndex === -1) {
+ // this should never happen since selection should always be in terms
+ throw new Error('selection is not in terms');
+ }
+
+ selectionIndex += delta;
+
+ if (selectionIndex < 0) {
+ selectionIndex = 0;
+ } else if (selectionIndex > suggestion.terms.length - 1) {
+ selectionIndex = suggestion.terms.length - 1;
+ }
+
+ suggestion.selection = suggestion.terms[selectionIndex];
+ }
+
+ handleEventPayload(payload) {
+ const {type, id, ...other} = payload.action; // eslint-disable-line no-redeclare
+
+ switch (type) {
+ case ActionTypes.SUGGESTION_PRETEXT_CHANGED:
+ this.clearSuggestions(id);
+
+ this.setPretext(id, other.pretext);
+ this.emitPretextChanged(id, other.pretext);
+
+ this.ensureSelectionExists(id);
+ this.emitSuggestionsChanged(id);
+ break;
+ case ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS:
+ if (other.matchedPretext === this.getMatchedPretext(id)) {
+ // ensure the matched pretext hasn't changed so that we don't receive suggestions for outdated pretext
+ this.addSuggestions(id, other.terms, other.items, other.component);
+
+ this.ensureSelectionExists(id);
+ this.emitSuggestionsChanged(id);
+ }
+ break;
+ case ActionTypes.SUGGESTION_SELECT_NEXT:
+ this.selectNext(id);
+ this.emitSuggestionsChanged(id);
+ break;
+ case ActionTypes.SUGGESTION_SELECT_PREVIOUS:
+ this.selectPrevious(id);
+ this.emitSuggestionsChanged(id);
+ break;
+ case ActionTypes.SUGGESTION_COMPLETE_WORD:
+ this.emitCompleteWord(id, other.term || this.getSelection(id), this.getMatchedPretext(id));
+
+ this.setPretext(id, '');
+ this.clearSuggestions(id);
+ this.clearSelection(id);
+ this.emitSuggestionsChanged(id);
+ break;
+ }
+ }
+}
+
+export default new SuggestionStore();
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index 5df43b548..d97c7c3cb 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -741,3 +741,27 @@ export function savePreferences(preferences, success, error) {
}
);
}
+
+export function getSuggestedCommands(command, suggestionId, component) {
+ client.executeCommand(
+ '',
+ command,
+ true,
+ (data) => {
+ // pull out the suggested commands from the returned data
+ const terms = data.suggestions.map((suggestion) => suggestion.suggestion);
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
+ id: suggestionId,
+ matchedPretext: command,
+ terms,
+ items: data.suggestions,
+ component
+ });
+ },
+ (err) => {
+ dispatchError(err, 'getCommandSuggestions');
+ }
+ );
+}
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 0180d6dc5..8164095b9 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -24,6 +24,7 @@ export default {
RECIEVED_POST: null,
RECIEVED_EDIT_POST: null,
RECIEVED_SEARCH: null,
+ RECIEVED_SEARCH_TERM: null,
RECIEVED_POST_SELECTED: null,
RECIEVED_MENTION_DATA: null,
RECIEVED_ADD_MENTION: null,
@@ -50,7 +51,13 @@ export default {
TOGGLE_INVITE_MEMBER_MODAL: null,
TOGGLE_DELETE_POST_MODAL: null,
TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null,
- TOGGLE_REGISTER_APP_MODAL: null
+ TOGGLE_REGISTER_APP_MODAL: null,
+
+ SUGGESTION_PRETEXT_CHANGED: null,
+ SUGGESTION_RECEIVED_SUGGESTIONS: null,
+ SUGGESTION_COMPLETE_WORD: null,
+ SUGGESTION_SELECT_NEXT: null,
+ SUGGESTION_SELECT_PREVIOUS: null
}),
PayloadSources: keyMirror({
diff --git a/web/react/utils/emoticons.jsx b/web/react/utils/emoticons.jsx
index bb948b6dc..ab04936c0 100644
--- a/web/react/utils/emoticons.jsx
+++ b/web/react/utils/emoticons.jsx
@@ -116,19 +116,19 @@ function initializeEmoticonMap() {
const out = new Map();
for (let i = 0; i < emoticonNames.length; i++) {
- out[emoticonNames[i]] = true;
+ out.set(emoticonNames[i], true);
}
return out;
}
-const emoticonMap = initializeEmoticonMap();
+export const emoticonMap = initializeEmoticonMap();
export function handleEmoticons(text, tokens) {
let output = text;
function replaceEmoticonWithToken(fullMatch, prefix, matchText, name) {
- if (emoticonMap[name]) {
+ if (emoticonMap.has(name)) {
const index = tokens.size;
const alias = `MM_EMOTICON${index}`;
@@ -154,6 +154,9 @@ export function handleEmoticons(text, tokens) {
return output;
}
-function getImagePathForEmoticon(name) {
- return `/static/images/emoji/${name}.png`;
+export function getImagePathForEmoticon(name) {
+ if (name) {
+ return `/static/images/emoji/${name}.png`;
+ }
+ return `/static/images/emoji`;
}
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index d2dd6cf38..788d8a45c 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -539,11 +539,11 @@ export function applyTheme(theme) {
if (theme.sidebarText) {
changeCss('.sidebar--left .nav-pills__container li>a, .sidebar--right, .settings-modal .nav-pills>li a, .sidebar--menu', 'color:' + changeOpacity(theme.sidebarText, 0.6), 1);
- changeCss('@media(max-width: 960px){.settings-modal .settings-table .nav>li>a', 'color:' + theme.sidebarText, 1);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'color:' + theme.sidebarText, 1);
changeCss('.sidebar--left .nav-pills__container li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.6), 1);
changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1);
changeCss('.sidebar--left .status path', 'fill:' + changeOpacity(theme.sidebarText, 0.5), 1);
- changeCss('@media(max-width: 960px){.settings-modal .settings-table .nav>li>a', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2);
}
if (theme.sidebarUnreadText) {
@@ -552,7 +552,7 @@ export function applyTheme(theme) {
if (theme.sidebarTextHoverBg) {
changeCss('.sidebar--left .nav-pills__container li>a:hover, .sidebar--left .nav-pills__container li>a:focus, .settings-modal .nav-pills>li:hover a, .settings-modal .nav-pills>li:focus a', 'background:' + theme.sidebarTextHoverBg, 1);
- changeCss('@media(max-width: 960px){.settings-modal .settings-table .nav>li:hover a', 'background:' + theme.sidebarTextHoverBg, 1);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'background:' + theme.sidebarTextHoverBg, 1);
}
if (theme.sidebarTextActiveBorder) {
@@ -568,7 +568,7 @@ export function applyTheme(theme) {
changeCss('.sidebar--left .team__header, .sidebar--menu .team__header', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('.modal .modal-header', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('#navbar .navbar-default', 'background:' + theme.sidebarHeaderBg, 1);
- changeCss('@media(max-width: 960px){.search-bar__container', 'background:' + theme.sidebarHeaderBg, 1);
+ changeCss('@media(max-width: 768px){.search-bar__container', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('.attachment .attachment__container', 'border-left-color:' + theme.sidebarHeaderBg, 1);
}
@@ -579,7 +579,7 @@ export function applyTheme(theme) {
changeCss('.modal .modal-header .modal-title, .modal .modal-header .modal-title .name, .modal .modal-header button.close', 'color:' + theme.sidebarHeaderTextColor, 1);
changeCss('#navbar .navbar-default .navbar-brand .heading', 'color:' + theme.sidebarHeaderTextColor, 1);
changeCss('#navbar .navbar-default .navbar-toggle .icon-bar, ', 'background:' + theme.sidebarHeaderTextColor, 1);
- changeCss('@media(max-width: 960px){.search-bar__container', 'color:' + theme.sidebarHeaderTextColor, 2);
+ changeCss('@media(max-width: 768px){.search-bar__container', 'color:' + theme.sidebarHeaderTextColor, 2);
}
if (theme.onlineIndicator) {
@@ -597,7 +597,7 @@ export function applyTheme(theme) {
}
if (theme.centerChannelBg) {
- changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .command-box, .modal .modal-content, .mentions-name, .mentions--top .mentions-box', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .suggestion-content, .modal .modal-content', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-create', 'background:' + theme.centerChannelBg, 1);
changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1);
@@ -607,7 +607,7 @@ export function applyTheme(theme) {
changeCss('.popover.right>.arrow:after, .tip-overlay.tip-overlay--sidebar .arrow, .tip-overlay.tip-overlay--header .arrow', 'border-right-color:' + theme.centerChannelBg, 1);
changeCss('.popover.left>.arrow:after', 'border-left-color:' + theme.centerChannelBg, 1);
changeCss('.popover.top>.arrow:after, .tip-overlay.tip-overlay--chat .arrow', 'border-top-color:' + theme.centerChannelBg, 1);
- changeCss('@media(min-width: 960px){.search-bar__container .search__form .search-bar, .form-control', 'background:' + theme.centerChannelBg, 1);
+ changeCss('@media(min-width: 768px){.search-bar__container .search__form .search-bar, .form-control', 'background:' + theme.centerChannelBg, 1);
changeCss('.attachment__content', 'background:' + theme.centerChannelBg, 1);
}
@@ -615,9 +615,9 @@ export function applyTheme(theme) {
changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name, .tip-overlay', 'color:' + theme.centerChannelColor, 1);
changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
- changeCss('.mentions--top, .command-box', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 3);
- changeCss('.mentions--top, .command-box', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 2);
- changeCss('.mentions--top, .command-box', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 1);
+ changeCss('.mentions--top, .suggestion-list', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 3);
+ changeCss('.mentions--top, .suggestion-list', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 2);
+ changeCss('.mentions--top, .suggestion-list', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 1);
changeCss('.dropdown-menu, .popover ', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 3);
changeCss('.dropdown-menu, .popover ', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 2);
changeCss('.dropdown-menu, .popover ', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 1);
@@ -626,7 +626,7 @@ export function applyTheme(theme) {
changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
- changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .webhooks__container, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .suggestion-content, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .webhooks__container, .dropdown-menu, .modal .modal-header, .popover', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
changeCss('.search-help-popover .search-autocomplete__divider span', 'color:' + changeOpacity(theme.centerChannelColor, 0.7), 1);
changeCss('.popover.right>.arrow', 'border-right-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
@@ -638,7 +638,7 @@ export function applyTheme(theme) {
changeCss('.post-image__column', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 2);
changeCss('.post-image__column .post-image__details', 'color:' + theme.centerChannelColor, 2);
changeCss('.post-image__column a, .post-image__column a:hover, .post-image__column a:focus', 'color:' + theme.centerChannelColor, 1);
- changeCss('@media(min-width: 960px){.search-bar__container .search__form .search-bar, .form-control', 'color:' + theme.centerChannelColor, 2);
+ changeCss('@media(min-width: 768px){.search-bar__container .search__form .search-bar, .form-control', 'color:' + theme.centerChannelColor, 2);
changeCss('.input-group-addon, .search-bar__container .search__form, .form-control', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.form-control:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.attachment .attachment__content', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
@@ -652,7 +652,7 @@ export function applyTheme(theme) {
changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('.post:hover, .modal .more-table tbody>tr:hover td, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
- changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover, .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
+ changeCss('.command-name:hover, .mentions-name:hover, .suggestion--selected, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover, .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1);
changeCss('@media(min-width: 960px){.post.current--user:hover .post__body ', 'background: none;', 1);
changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);
@@ -1238,4 +1238,4 @@ export function getPostTerm(post) {
export function isFeatureEnabled(feature) {
return PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, Constants.FeatureTogglePrefix + feature.label, {value: 'false'}).value === 'true';
-}
+} \ No newline at end of file