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/access_history_modal.jsx2
-rw-r--r--web/react/components/activity_log_modal.jsx2
-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.jsx24
-rw-r--r--web/react/components/channel_header.jsx14
-rw-r--r--web/react/components/channel_invite_modal.jsx38
-rw-r--r--web/react/components/channel_loader.jsx4
-rw-r--r--web/react/components/channel_members_modal.jsx17
-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/invite_member_modal.jsx2
-rw-r--r--web/react/components/member_list_item.jsx4
-rw-r--r--web/react/components/mention.jsx61
-rw-r--r--web/react/components/mention_list.jsx276
-rw-r--r--web/react/components/more_direct_channels.jsx2
-rw-r--r--web/react/components/navbar.jsx14
-rw-r--r--web/react/components/post.jsx13
-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/post_header.jsx12
-rw-r--r--web/react/components/posts_view.jsx22
-rw-r--r--web/react/components/posts_view_container.jsx9
-rw-r--r--web/react/components/rhs_root_post.jsx28
-rw-r--r--web/react/components/rhs_thread.jsx10
-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/sidebar.jsx65
-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.jsx163
-rw-r--r--web/react/components/suggestion/suggestion_list.jsx125
-rw-r--r--web/react/components/team_general_tab.jsx1
-rw-r--r--web/react/components/team_members_modal.jsx2
-rw-r--r--web/react/components/textbox.jsx206
-rw-r--r--web/react/components/user_profile.jsx9
-rw-r--r--web/react/components/user_settings/user_settings_display.jsx80
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx49
-rw-r--r--web/react/dispatcher/event_helpers.jsx43
-rw-r--r--web/react/pages/channel.jsx16
-rw-r--r--web/react/stores/browser_store.jsx21
-rw-r--r--web/react/stores/channel_store.jsx62
-rw-r--r--web/react/stores/search_store.jsx36
-rw-r--r--web/react/stores/socket_store.jsx2
-rw-r--r--web/react/stores/suggestion_store.jsx272
-rw-r--r--web/react/utils/async_client.jsx54
-rw-r--r--web/react/utils/channel_intro_messages.jsx (renamed from web/react/utils/channel_intro_mssages.jsx)39
-rw-r--r--web/react/utils/constants.jsx29
-rw-r--r--web/react/utils/emoticons.jsx13
-rw-r--r--web/react/utils/utils.jsx53
58 files changed, 1578 insertions, 1296 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/access_history_modal.jsx b/web/react/components/access_history_modal.jsx
index 165d32339..deef92a54 100644
--- a/web/react/components/access_history_modal.jsx
+++ b/web/react/components/access_history_modal.jsx
@@ -32,7 +32,7 @@ export default class AccessHistoryModal extends React.Component {
onShow() {
AsyncClient.getAudits();
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300);
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 50);
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
}
diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx
index 869d648d2..200f4d724 100644
--- a/web/react/components/activity_log_modal.jsx
+++ b/web/react/components/activity_log_modal.jsx
@@ -51,7 +51,7 @@ export default class ActivityLogModal extends React.Component {
onShow() {
AsyncClient.getSessions();
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300);
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 50);
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
}
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..a1043431d 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,25 @@ 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. '}
+ {<i className='fa fa-arrow-down'></i>}
+ </a>
+ </div>
+ );
} else {
postsContainer = <PostsViewContainer />;
createPost = (
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 08c4a48ea..d5a46721e 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -40,7 +40,6 @@ export default class ChannelHeader extends React.Component {
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
- state.showInviteModal = false;
state.showMembersModal = false;
this.state = state;
}
@@ -201,13 +200,13 @@ export default class ChannelHeader extends React.Component {
key='add_members'
role='presentation'
>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- onClick={() => this.setState({showInviteModal: true})}
+ dialogType={ChannelInviteModal}
+ dialogProps={{channel}}
>
{'Add Members'}
- </a>
+ </ToggleModalButton>
</li>
);
@@ -402,13 +401,10 @@ export default class ChannelHeader extends React.Component {
onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
channel={channel}
/>
- <ChannelInviteModal
- show={this.state.showInviteModal}
- onModalDismissed={() => this.setState({showInviteModal: false})}
- />
<ChannelMembersModal
show={this.state.showMembersModal}
onModalDismissed={() => this.setState({showMembersModal: false})}
+ channel={channel}
/>
</div>
);
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index 0518ccb86..56e2e53f9 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -53,15 +53,8 @@ export default class ChannelInviteModal extends React.Component {
return a.username.localeCompare(b.username);
});
- var channelName = '';
- if (ChannelStore.getCurrent()) {
- channelName = ChannelStore.getCurrent().display_name;
- }
-
return {
nonmembers,
- memberIds,
- channelName,
loading
};
}
@@ -94,28 +87,14 @@ export default class ChannelInviteModal extends React.Component {
}
}
handleInvite(userId) {
- // Make sure the user isn't already a member of the channel
- if (this.state.memberIds.indexOf(userId) > -1) {
- return;
- }
-
var data = {};
data.user_id = userId;
- Client.addChannelMember(ChannelStore.getCurrentId(), data,
+ Client.addChannelMember(
+ this.props.channel.id,
+ data,
() => {
- var nonmembers = this.state.nonmembers;
- var memberIds = this.state.memberIds;
-
- for (var i = 0; i < nonmembers.length; i++) {
- if (userId === nonmembers[i].id) {
- nonmembers[i].invited = true;
- memberIds.push(userId);
- break;
- }
- }
-
- this.setState({inviteError: null, memberIds, nonmembers});
+ this.setState({inviteError: null});
AsyncClient.getChannelExtraInfo();
},
(err) => {
@@ -157,10 +136,10 @@ export default class ChannelInviteModal extends React.Component {
<Modal
dialogClassName='more-modal'
show={this.props.show}
- onHide={this.props.onModalDismissed}
+ onHide={this.props.onHide}
>
<Modal.Header closeButton={true}>
- <Modal.Title>{'Add New Members to '}<span className='name'>{this.state.channelName}</span></Modal.Title>
+ <Modal.Title>{'Add New Members to '}<span className='name'>{this.props.channel.display_nam}</span></Modal.Title>
</Modal.Header>
<Modal.Body
ref='modalBody'
@@ -173,7 +152,7 @@ export default class ChannelInviteModal extends React.Component {
<button
type='button'
className='btn btn-default'
- onClick={this.props.onModalDismissed}
+ onClick={this.props.onHide}
>
{'Close'}
</button>
@@ -185,5 +164,6 @@ export default class ChannelInviteModal extends React.Component {
ChannelInviteModal.propTypes = {
show: React.PropTypes.bool.isRequired,
- onModalDismissed: React.PropTypes.func.isRequired
+ onHide: React.PropTypes.func.isRequired,
+ channel: React.PropTypes.object.isRequired
};
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index c8f1196a8..13045d732 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -10,6 +10,7 @@ import SocketStore from '../stores/socket_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import PostStore from '../stores/post_store.jsx';
import UserStore from '../stores/user_store.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
@@ -69,6 +70,9 @@ export default class ChannelLoader extends React.Component {
Utils.applyTheme(Constants.THEMES.default);
}
+ const selectedFont = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', {value: Constants.DEFAULT_FONT}).value;
+ Utils.applyFont(selectedFont);
+
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx
index f07fc166a..d1b9df988 100644
--- a/web/react/components/channel_members_modal.jsx
+++ b/web/react/components/channel_members_modal.jsx
@@ -69,16 +69,9 @@ export default class ChannelMembersModal extends React.Component {
memberList.sort(compareByUsername);
nonmemberList.sort(compareByUsername);
- const channel = ChannelStore.getCurrent();
- let channelName = '';
- if (channel) {
- channelName = channel.display_name;
- }
-
return {
nonmemberList,
- memberList,
- channelName
+ memberList
};
}
onShow() {
@@ -169,7 +162,7 @@ export default class ChannelMembersModal extends React.Component {
onHide={this.props.onModalDismissed}
>
<Modal.Header closeButton={true}>
- <Modal.Title><span className='name'>{this.state.channelName}</span>{' Members'}</Modal.Title>
+ <Modal.Title><span className='name'>{this.props.channel.display_name}</span>{' Members'}</Modal.Title>
<a
className='btn btn-md btn-primary'
href='#'
@@ -205,7 +198,8 @@ export default class ChannelMembersModal extends React.Component {
</Modal>
<ChannelInviteModal
show={this.state.showInviteModal}
- onModalDismissed={() => this.setState({showInviteModal: false})}
+ onHide={() => this.setState({showInviteModal: false})}
+ channel={this.props.channel}
/>
</div>
);
@@ -218,5 +212,6 @@ ChannelMembersModal.defaultProps = {
ChannelMembersModal.propTypes = {
show: React.PropTypes.bool.isRequired,
- onModalDismissed: React.PropTypes.func.isRequired
+ onModalDismissed: React.PropTypes.func.isRequired,
+ channel: React.PropTypes.object.isRequired
};
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/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 76f52faa9..25a915e22 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -143,7 +143,7 @@ export default class InviteMemberModal extends React.Component {
componentDidUpdate(prevProps, prevState) {
if (!prevState.show && this.state.show) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300);
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 50);
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
}
diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx
index f5d5ab28b..f7f77f48a 100644
--- a/web/react/components/member_list_item.jsx
+++ b/web/react/components/member_list_item.jsx
@@ -31,9 +31,7 @@ export default class MemberListItem extends React.Component {
var timestamp = UserStore.getCurrentUser().update_at;
var invite;
- if (member.invited && this.props.handleInvite) {
- invite = <span className='member-role'>Added</span>;
- } else if (this.props.handleInvite) {
+ if (this.props.handleInvite) {
invite = (
<a
onClick={this.handleInvite}
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/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx
index 9116dc8f1..cf40af6ae 100644
--- a/web/react/components/more_direct_channels.jsx
+++ b/web/react/components/more_direct_channels.jsx
@@ -166,7 +166,7 @@ export default class MoreDirectChannels extends React.Component {
componentDidUpdate(prevProps) {
if (!prevProps.show && this.props.show) {
- $(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 300);
+ $(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 50);
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.userList)).perfectScrollbar();
}
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index 6c3bfc7db..3bdc9efac 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -44,7 +44,6 @@ export default class Navbar extends React.Component {
state.showEditChannelPurposeModal = false;
state.showEditChannelHeaderModal = false;
state.showMembersModal = false;
- state.showInviteModal = false;
this.state = state;
}
getStateFromStores() {
@@ -171,13 +170,13 @@ export default class Navbar extends React.Component {
if (!isDirect && !ChannelStore.isDefault(channel)) {
addMembersOption = (
<li role='presentation'>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- onClick={() => this.setState({showInviteModal: true})}
+ dialogType={ChannelInviteModal}
+ dialogProps={{channel}}
>
{'Add Members'}
- </a>
+ </ToggleModalButton>
</li>
);
@@ -475,10 +474,7 @@ export default class Navbar extends React.Component {
<ChannelMembersModal
show={this.state.showMembersModal}
onModalDismissed={() => this.setState({showMembersModal: false})}
- />
- <ChannelInviteModal
- show={this.state.showInviteModal}
- onModalDismissed={() => this.setState({showInviteModal: false})}
+ channel={{channel}}
/>
</div>
);
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index 66d8c507a..786a4a325 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) {
@@ -169,6 +173,11 @@ export default class Post extends React.Component {
shouldHighlightClass = 'post--highlight';
}
+ let systemMessageClass = '';
+ if (utils.isSystemMessage(post)) {
+ systemMessageClass = 'post--system';
+ }
+
let profilePic = null;
if (!this.props.hideProfilePic) {
let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex();
@@ -176,6 +185,8 @@ export default class Post extends React.Component {
if (post.props.override_icon_url) {
src = post.props.override_icon_url;
}
+ } else if (utils.isSystemMessage(post)) {
+ src = Constants.SYSTEM_MESSAGE_PROFILE_IMAGE;
}
profilePic = (
@@ -191,7 +202,7 @@ export default class Post extends React.Component {
<div>
<div
id={'post_' + post.id}
- className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass}
+ className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass + ' ' + systemMessageClass}
>
<div className='post__content'>
<div className='post__img'>{profilePic}</div>
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/post_header.jsx b/web/react/components/post_header.jsx
index ffc32f82c..76b3a64be 100644
--- a/web/react/components/post_header.jsx
+++ b/web/react/components/post_header.jsx
@@ -3,6 +3,9 @@
import UserProfile from './user_profile.jsx';
import PostInfo from './post_info.jsx';
+import * as Utils from '../utils/utils.jsx';
+
+import Constants from '../utils/constants.jsx';
export default class PostHeader extends React.Component {
constructor(props) {
@@ -27,6 +30,15 @@ export default class PostHeader extends React.Component {
}
botIndicator = <li className='col col__name bot-indicator'>{'BOT'}</li>;
+ } else if (Utils.isSystemMessage(post)) {
+ userProfile = (
+ <UserProfile
+ userId={''}
+ overwriteName={''}
+ overwriteImage={Constants.SYSTEM_MESSAGE_PROFILE_IMAGE}
+ disablePopover={true}
+ />
+ );
}
return (
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
index 242b26b91..740ce04f6 100644
--- a/web/react/components/posts_view.jsx
+++ b/web/react/components/posts_view.jsx
@@ -87,6 +87,7 @@ export default class PostsView extends React.Component {
const post = posts[order[i]];
const parentPost = posts[post.parent_id];
const prevPost = posts[order[i + 1]];
+ const postUserId = Utils.isSystemMessage(post) ? '' : post.user_id;
// If the post is a comment whose parent has been deleted, don't add it to the list.
if (parentPost && parentPost.state === Constants.POST_DELETED) {
@@ -102,6 +103,7 @@ export default class PostsView extends React.Component {
const prevPostIsComment = Utils.isComment(prevPost);
const postFromWebhook = Boolean(post.props && post.props.from_webhook);
const prevPostFromWebhook = Boolean(prevPost.props && prevPost.props.from_webhook);
+ const prevPostUserId = Utils.isSystemMessage(prevPost) ? '' : prevPostUserId;
let prevWebhookName = '';
if (prevPost.props && prevPost.props.override_username) {
prevWebhookName = prevPost.props.override_username;
@@ -116,7 +118,7 @@ export default class PostsView extends React.Component {
// the previous post was made within 5 minutes of the current post,
// the previous post and current post are both from webhooks or both not,
// the previous post and current post have the same webhook usernames
- if (prevPost.user_id === post.user_id &&
+ if (prevPostUserId === postUserId &&
post.create_at - prevPost.create_at <= 1000 * 60 * 5 &&
postFromWebhook === prevPostFromWebhook &&
prevWebhookName === curWebhookName) {
@@ -144,7 +146,7 @@ export default class PostsView extends React.Component {
// the current post is not a comment,
// the previous post and current post are both from webhooks or both not,
// the previous post and current post have the same webhook usernames
- if (prevPost.user_id === post.user_id &&
+ if (prevPostUserId === postUserId &&
!prevPostIsComment &&
!postIsComment &&
postFromWebhook === prevPostFromWebhook &&
@@ -191,7 +193,7 @@ export default class PostsView extends React.Component {
);
}
- if (post.user_id !== userId &&
+ if (postUserId !== userId &&
this.props.messageSeparatorTime !== 0 &&
post.create_at > this.props.messageSeparatorTime &&
!renderedLastViewed) {
@@ -280,18 +282,22 @@ export default class PostsView extends React.Component {
this.updateScrolling();
}
window.addEventListener('resize', this.handleResize);
- $(this.refs.postlist).perfectScrollbar();
- PreferenceStore.addChangeListener(this.updateState);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
- PreferenceStore.removeChangeListener(this.updateState);
}
componentDidUpdate() {
if (this.props.postList != null) {
this.updateScrolling();
}
- $(this.refs.postlist).perfectScrollbar('update');
+ }
+ componentWillReceiveProps(nextProps) {
+ if (!this.props.isActive && nextProps.isActive) {
+ this.updateState();
+ PreferenceStore.addChangeListener(this.updateState);
+ } else if (this.props.isActive && !nextProps.isActive) {
+ PreferenceStore.removeChangeListener(this.updateState);
+ }
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.isActive !== nextProps.isActive) {
@@ -373,7 +379,7 @@ export default class PostsView extends React.Component {
return (
<div
ref='postlist'
- className={'ps-container post-list-holder-by-time ' + activeClass}
+ className={'post-list-holder-by-time ' + activeClass}
onScroll={this.handleScroll}
>
<div className='post-list__table'>
diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx
index 6d6694fec..631bd1872 100644
--- a/web/react/components/posts_view_container.jsx
+++ b/web/react/components/posts_view_container.jsx
@@ -3,7 +3,6 @@
import PostsView from './posts_view.jsx';
import LoadingScreen from './loading_screen.jsx';
-import ChannelInviteModal from './channel_invite_modal.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import PostStore from '../stores/post_store.jsx';
@@ -13,7 +12,7 @@ import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import Constants from '../utils/constants.jsx';
-import {createChannelIntroMessage} from '../utils/channel_intro_mssages.jsx';
+import {createChannelIntroMessage} from '../utils/channel_intro_messages.jsx';
export default class PostsViewContainer extends React.Component {
constructor() {
@@ -177,7 +176,7 @@ export default class PostsViewContainer extends React.Component {
loadMorePostsBottomClicked={() => {}}
showMoreMessagesTop={!this.state.atTop[this.state.currentChannelIndex]}
showMoreMessagesBottom={false}
- introText={channel ? createChannelIntroMessage(channel, () => this.setState({showInviteModal: true})) : null}
+ introText={channel ? createChannelIntroMessage(channel) : null}
messageSeparatorTime={this.state.currentLastViewed}
/>
);
@@ -194,10 +193,6 @@ export default class PostsViewContainer extends React.Component {
return (
<div id='post-list'>
{postListCtls}
- <ChannelInviteModal
- show={this.state.showInviteModal}
- onModalDismissed={() => this.setState({showInviteModal: false})}
- />
</div>
);
}
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index 3d3d9e13f..0a37a6803 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -6,12 +6,14 @@ 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';
+import Constants from '../utils/constants.jsx';
+
export default class RhsRootPost extends React.Component {
constructor(props) {
super(props);
@@ -21,7 +23,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();
@@ -54,6 +60,11 @@ export default class RhsRootPost extends React.Component {
currentUserCss = 'current--user';
}
+ var systemMessageClass = '';
+ if (utils.isSystemMessage(post)) {
+ systemMessageClass = 'post--system';
+ }
+
var channelName;
if (channel) {
if (channel.type === 'D') {
@@ -152,6 +163,15 @@ export default class RhsRootPost extends React.Component {
}
botIndicator = <li className='col col__name bot-indicator'>{'BOT'}</li>;
+ } else if (utils.isSystemMessage(post)) {
+ userProfile = (
+ <UserProfile
+ userId={''}
+ overwriteName={''}
+ overwriteImage={Constants.SYSTEM_MESSAGE_PROFILE_IMAGE}
+ disablePopover={true}
+ />
+ );
}
let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex();
@@ -159,6 +179,8 @@ export default class RhsRootPost extends React.Component {
if (post.props.override_icon_url) {
src = post.props.override_icon_url;
}
+ } else if (utils.isSystemMessage(post)) {
+ src = Constants.SYSTEM_MESSAGE_PROFILE_IMAGE;
}
const profilePic = (
@@ -171,7 +193,7 @@ export default class RhsRootPost extends React.Component {
);
return (
- <div className={'post post--root ' + currentUserCss}>
+ <div className={'post post--root ' + currentUserCss + ' ' + systemMessageClass}>
<div className='post-right-channel__name'>{channelName}</div>
<div className='post__content'>
<div className='post__img'>
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
index 61f138539..2edcd8b37 100644
--- a/web/react/components/rhs_thread.jsx
+++ b/web/react/components/rhs_thread.jsx
@@ -101,7 +101,15 @@ 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 key in currentSelected.posts) {
+ if (currentSelected.posts.hasOwnProperty(key)) {
+ var post = currentSelected.posts[key];
+ if (post.pending_post_id) {
+ Reflect.deleteProperty(currentSelected.posts, key);
+ }
+ }
+ }
+
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..1d4983026 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}
+ >
+ {<i className='fa fa-mail-reply'></i>}
+ </a>
+ </li>
</ul>
<div className='search-item-snippet'>
<span
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 5c8e73874..3d7f449d1 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -19,7 +19,6 @@ import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
const Preferences = Constants.Preferences;
const TutorialSteps = Constants.TutorialSteps;
-const NotificationPrefs = Constants.NotificationPrefs;
const Tooltip = ReactBootstrap.Tooltip;
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
@@ -38,7 +37,6 @@ export default class Sidebar extends React.Component {
this.onScroll = this.onScroll.bind(this);
this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this);
this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this);
- this.updateScrollbar = this.updateScrollbar.bind(this);
this.handleResize = this.handleResize.bind(this);
this.showNewChannelModal = this.showNewChannelModal.bind(this);
@@ -48,8 +46,6 @@ export default class Sidebar extends React.Component {
this.createChannelElement = this.createChannelElement.bind(this);
this.updateTitle = this.updateTitle.bind(this);
- this.setUnreadCountPerChannel = this.setUnreadCountPerChannel.bind(this);
- this.getUnreadCount = this.getUnreadCount.bind(this);
this.isLeaving = new Map();
@@ -59,43 +55,15 @@ export default class Sidebar extends React.Component {
state.loadingDMChannel = -1;
state.windowWidth = Utils.windowWidth();
this.state = state;
-
- this.unreadCountPerChannel = {};
- this.setUnreadCountPerChannel();
- }
- setUnreadCountPerChannel() {
- const channels = ChannelStore.getAll();
- const members = ChannelStore.getAllMembers();
- const channelUnreadCounts = {};
-
- channels.forEach((ch) => {
- const chMember = members[ch.id];
- let chMentionCount = chMember.mention_count;
- let chUnreadCount = ch.total_msg_count - chMember.msg_count - chMentionCount;
-
- if (ch.type === 'D') {
- chMentionCount = chUnreadCount;
- chUnreadCount = 0;
- } else if (chMember.notify_props && chMember.notify_props.mark_unread === NotificationPrefs.MENTION) {
- chUnreadCount = 0;
- }
-
- channelUnreadCounts[ch.id] = {msgs: chUnreadCount, mentions: chMentionCount};
- });
-
- this.unreadCountPerChannel = channelUnreadCounts;
}
- getUnreadCount(channelId) {
- let mentions = 0;
+ getTotalUnreadCount() {
let msgs = 0;
+ let mentions = 0;
+ const unreadCounts = this.state.unreadCounts;
- if (channelId) {
- return this.unreadCountPerChannel[channelId] ? this.unreadCountPerChannel[channelId] : {msgs, mentions};
- }
-
- Object.keys(this.unreadCountPerChannel).forEach((chId) => {
- msgs += this.unreadCountPerChannel[chId].msgs;
- mentions += this.unreadCountPerChannel[chId].mentions;
+ Object.keys(unreadCounts).forEach((chId) => {
+ msgs += unreadCounts[chId].msgs;
+ mentions += unreadCounts[chId].mentions;
});
return {msgs, mentions};
@@ -156,6 +124,7 @@ export default class Sidebar extends React.Component {
privateChannels,
visibleDirectChannels,
hiddenDirectChannelCount,
+ unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())),
showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.CHANNEL_POPOVER
};
}
@@ -169,7 +138,6 @@ export default class Sidebar extends React.Component {
this.updateTitle();
this.updateUnreadIndicators();
- this.updateScrollbar();
window.addEventListener('resize', this.handleResize);
@@ -186,7 +154,6 @@ export default class Sidebar extends React.Component {
componentDidUpdate() {
this.updateTitle();
this.updateUnreadIndicators();
- this.updateScrollbar();
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
@@ -203,8 +170,6 @@ export default class Sidebar extends React.Component {
windowHeight: Utils.windowHeight()
});
}
- updateScrollbar() {
- }
onChange() {
this.setState(this.getStateFromStores());
}
@@ -221,7 +186,7 @@ export default class Sidebar extends React.Component {
currentChannelName = Utils.getDirectTeammate(channel.id).username;
}
- const unread = this.getUnreadCount();
+ const unread = this.getTotalUnreadCount();
const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : '';
const unreadTitle = unread.msgs > 0 ? '* ' : '';
document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + TeamStore.getCurrent().display_name + ' ' + currentSiteName;
@@ -347,13 +312,13 @@ export default class Sidebar extends React.Component {
}
createChannelElement(channel, index, arr, handleClose) {
- var members = this.state.members;
- var activeId = this.state.activeId;
- var channelMember = members[channel.id];
- var unreadCount = this.getUnreadCount(channel.id);
- var msgCount;
+ const members = this.state.members;
+ const activeId = this.state.activeId;
+ const channelMember = members[channel.id];
+ const unreadCount = this.state.unreadCounts[channel.id] || {msgs: 0, mentions: 0};
+ let msgCount;
- var linkClass = '';
+ let linkClass = '';
if (channel.id === activeId) {
linkClass = 'active';
}
@@ -510,8 +475,6 @@ export default class Sidebar extends React.Component {
render() {
this.badgesActive = false;
- this.setUnreadCountPerChannel();
-
// keep track of the first and last unread channels so we can use them to set the unread indicators
this.firstUnreadChannel = null;
this.lastUnreadChannel = null;
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..3378a33a0
--- /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) {
+ 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..4cfb38f8e
--- /dev/null
+++ b/web/react/components/suggestion/suggestion_box.jsx
@@ -0,0 +1,163 @@
+// 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.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();
+ }
+
+ 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) {
+ 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
+ EventHelpers.emitClearSuggestions(this.suggestionId);
+ }
+ }
+
+ 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, {
+ 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} />
+ </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
+};
diff --git a/web/react/components/suggestion/suggestion_list.jsx b/web/react/components/suggestion/suggestion_list.jsx
new file mode 100644
index 000000000..e3ccd0f08
--- /dev/null
+++ b/web/react/components/suggestion/suggestion_list.jsx
@@ -0,0 +1,125 @@
+// 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) {
+ 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
+};
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/team_members_modal.jsx b/web/react/components/team_members_modal.jsx
index 0a30a2202..eed4a1f19 100644
--- a/web/react/components/team_members_modal.jsx
+++ b/web/react/components/team_members_modal.jsx
@@ -26,7 +26,7 @@ export default class TeamMembersModal extends React.Component {
}
onShow() {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300);
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 50);
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
}
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_profile.jsx b/web/react/components/user_profile.jsx
index ea104fedb..385cd0f52 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -65,11 +65,16 @@ export default class UserProfile extends React.Component {
return <div>{name}</div>;
}
+ var profileImg = '/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '&' + Utils.getSessionIndex();
+ if (this.props.overwriteImage) {
+ profileImg = this.props.overwriteImage;
+ }
+
var dataContent = [];
dataContent.push(
<img
className='user-popover__image'
- src={'/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '&' + Utils.getSessionIndex()}
+ src={profileImg}
height='128'
width='128'
key='user-popover-image'
@@ -130,10 +135,12 @@ export default class UserProfile extends React.Component {
UserProfile.defaultProps = {
userId: '',
overwriteName: '',
+ overwriteImage: '',
disablePopover: false
};
UserProfile.propTypes = {
userId: React.PropTypes.string,
overwriteName: React.PropTypes.string,
+ overwriteImage: React.PropTypes.string,
disablePopover: React.PropTypes.bool
};
diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx
index 43c8d33d1..c464258de 100644
--- a/web/react/components/user_settings/user_settings_display.jsx
+++ b/web/react/components/user_settings/user_settings_display.jsx
@@ -6,14 +6,17 @@ import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import Constants from '../../utils/constants.jsx';
import PreferenceStore from '../../stores/preference_store.jsx';
+import * as Utils from '../../utils/utils.jsx';
function getDisplayStateFromStores() {
const militaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'});
const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'username'});
+ const selectedFont = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', {value: Constants.DEFAULT_FONT});
return {
militaryTime: militaryTime.value,
- nameFormat: nameFormat.value
+ nameFormat: nameFormat.value,
+ selectedFont: selectedFont.value
};
}
@@ -24,15 +27,20 @@ export default class UserSettingsDisplay extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClockRadio = this.handleClockRadio.bind(this);
this.handleNameRadio = this.handleNameRadio.bind(this);
+ this.handleFont = this.handleFont.bind(this);
this.updateSection = this.updateSection.bind(this);
this.state = getDisplayStateFromStores();
+ this.selectedFont = this.state.selectedFont;
}
handleSubmit() {
const timePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime);
const namePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', this.state.nameFormat);
+ const fontPreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', this.state.selectedFont);
- savePreferences([timePreference, namePreference],
+ this.selectedFont = this.state.selectedFont;
+
+ savePreferences([timePreference, namePreference, fontPreference],
() => {
PreferenceStore.emitChange();
this.updateSection('');
@@ -48,6 +56,10 @@ export default class UserSettingsDisplay extends React.Component {
handleNameRadio(nameFormat) {
this.setState({nameFormat});
}
+ handleFont(selectedFont) {
+ Utils.applyFont(selectedFont);
+ this.setState({selectedFont});
+ }
updateSection(section) {
this.setState(getDisplayStateFromStores());
this.props.updateSection(section);
@@ -56,6 +68,8 @@ export default class UserSettingsDisplay extends React.Component {
const serverError = this.state.serverError || null;
let clockSection;
let nameFormatSection;
+ let fontSection;
+
if (this.props.activeSection === 'clock') {
const clockFormat = [false, false];
if (this.state.militaryTime === 'true') {
@@ -209,6 +223,66 @@ export default class UserSettingsDisplay extends React.Component {
);
}
+ if (this.props.activeSection === 'font') {
+ const options = [];
+ Object.keys(Constants.FONTS).forEach((fontName, idx) => {
+ const className = Constants.FONTS[fontName];
+ options.push(
+ <option
+ key={'font_' + idx}
+ value={fontName}
+ className={className}
+ >
+ {fontName}
+ </option>
+ );
+ });
+
+ const inputs = [
+ <div key='userDisplayNameOptions'>
+ <div
+ className='dropdown'
+ >
+ <select
+ className='form-control'
+ type='text'
+ value={this.state.selectedFont}
+ onChange={(e) => this.handleFont(e.target.value)}
+ >
+ {options}
+ </select>
+ </div>
+ <div><br/>{'Select the font displayed in the Mattermost user interface.'}</div>
+ </div>
+ ];
+
+ fontSection = (
+ <SettingItemMax
+ title='Display Font'
+ inputs={inputs}
+ submit={this.handleSubmit}
+ server_error={serverError}
+ updateSection={(e) => {
+ if (this.selectedFont !== this.state.selectedFont) {
+ this.handleFont(this.selectedFont);
+ }
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ fontSection = (
+ <SettingItemMin
+ title='Display Font'
+ describe={this.state.selectedFont}
+ updateSection={() => {
+ this.props.updateSection('font');
+ }}
+ />
+ );
+ }
+
return (
<div>
<div className='modal-header'>
@@ -235,6 +309,8 @@ export default class UserSettingsDisplay extends React.Component {
<div className='user-settings'>
<h3 className='tab-header'>{'Display Settings'}</h3>
<div className='divider-dark first'/>
+ {fontSection}
+ <div className='divider-dark'/>
{clockSection}
<div className='divider-dark'/>
{nameFormatSection}
diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx
index f9d03f56d..97c601b5e 100644
--- a/web/react/components/user_settings/user_settings_modal.jsx
+++ b/web/react/components/user_settings/user_settings_modal.jsx
@@ -47,7 +47,7 @@ export default class UserSettingsModal extends React.Component {
}
handleShow() {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300);
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 50);
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
}
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 856eec2f1..306c59e8b 100644
--- a/web/react/dispatcher/event_helpers.jsx
+++ b/web/react/dispatcher/event_helpers.jsx
@@ -10,9 +10,9 @@ import * as AsyncClient from '../utils/async_client.jsx';
import * as Client from '../utils/client.jsx';
export function emitChannelClickEvent(channel) {
- AsyncClient.getChannels();
- AsyncClient.getChannelExtraInfo();
- AsyncClient.updateLastViewedAt();
+ AsyncClient.getChannels(true);
+ AsyncClient.getChannelExtraInfo(channel.id);
+ AsyncClient.updateLastViewedAt(channel.id);
AsyncClient.getPosts(channel.id);
AppDispatcher.handleViewAction({
@@ -111,3 +111,40 @@ 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
+ });
+}
+
+export function emitClearSuggestions(suggestionId) {
+ AppDispatcher.handleViewAction({
+ type: Constants.ActionTypes.SUGGESTION_CLEAR_SUGGESTIONS,
+ id: suggestionId
+ });
+}
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/browser_store.jsx b/web/react/stores/browser_store.jsx
index 2e3a26cff..ff6ae45ea 100644
--- a/web/react/stores/browser_store.jsx
+++ b/web/react/stores/browser_store.jsx
@@ -1,6 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import {generateId} from '../utils/utils.jsx';
+
function getPrefix() {
if (global.window.mm_user) {
return global.window.mm_user.id + '_';
@@ -26,6 +28,7 @@ class BrowserStoreClass {
this.clearAll = this.clearAll.bind(this);
this.checkedLocalStorageSupported = '';
this.signalLogout = this.signalLogout.bind(this);
+ this.isSignallingLogout = this.isSignallingLogout.bind(this);
var currentVersion = sessionStorage.getItem('storage_version');
if (currentVersion !== global.window.mm_config.Version) {
@@ -113,11 +116,19 @@ class BrowserStoreClass {
signalLogout() {
if (this.isLocalStorageSupported()) {
- localStorage.setItem('__logout__', 'yes');
+ // PLT-1285 store an identifier in session storage so we can catch if the logout came from this tab on IE11
+ const logoutId = generateId();
+
+ sessionStorage.setItem('__logout__', logoutId);
+ localStorage.setItem('__logout__', logoutId);
localStorage.removeItem('__logout__');
}
}
+ isSignallingLogout(logoutId) {
+ return logoutId === sessionStorage.getItem('__logout__');
+ }
+
/**
* Preforms the given action on each item that has the given prefix
* Signature for action is action(key, value)
@@ -151,7 +162,14 @@ class BrowserStoreClass {
}
clear() {
+ // don't clear the logout id so IE11 can tell which tab sent a logout request
+ const logoutId = sessionStorage.getItem('__logout__');
+
sessionStorage.clear();
+
+ if (logoutId) {
+ sessionStorage.setItem('__logout__', logoutId);
+ }
}
clearAll() {
@@ -185,3 +203,4 @@ class BrowserStoreClass {
var BrowserStore = new BrowserStoreClass();
export default BrowserStore;
+window.BrowserStore = BrowserStore;
diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx
index dec4926f5..0bfde77b4 100644
--- a/web/react/stores/channel_store.jsx
+++ b/web/react/stores/channel_store.jsx
@@ -7,6 +7,7 @@ import EventEmitter from 'events';
var Utils;
import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
+const NotificationPrefs = Constants.NotificationPrefs;
const CHANGE_EVENT = 'change';
const LEAVE_EVENT = 'leave';
@@ -37,6 +38,10 @@ class ChannelStoreClass extends EventEmitter {
this.getByName = this.getByName.bind(this);
this.pSetPostMode = this.pSetPostMode.bind(this);
this.getPostMode = this.getPostMode.bind(this);
+ this.setUnreadCount = this.setUnreadCount.bind(this);
+ this.setUnreadCounts = this.setUnreadCounts.bind(this);
+ this.getUnreadCount = this.getUnreadCount.bind(this);
+ this.getUnreadCounts = this.getUnreadCounts.bind(this);
this.currentId = null;
this.postMode = this.POST_MODE_CHANNEL;
@@ -45,6 +50,7 @@ class ChannelStoreClass extends EventEmitter {
this.moreChannels = {};
this.moreChannels.loading = true;
this.extraInfos = {};
+ this.unreadCounts = {};
}
get POST_MODE_CHANNEL() {
return 1;
@@ -120,18 +126,18 @@ class ChannelStoreClass extends EventEmitter {
this.currentId = id;
}
resetCounts(id) {
- var cm = this.pGetChannelMembers();
+ const cm = this.channelMembers;
for (var cmid in cm) {
if (cm[cmid].channel_id === id) {
var c = this.get(id);
if (c) {
cm[cmid].msg_count = this.get(id).total_msg_count;
cm[cmid].mention_count = 0;
+ this.setUnreadCount(id);
}
break;
}
}
- this.pStoreChannelMembers(cm);
}
getCurrentId() {
return this.currentId;
@@ -161,18 +167,7 @@ class ChannelStoreClass extends EventEmitter {
this.emitChange();
}
getCurrentExtraInfo() {
- var currentId = this.getCurrentId();
- var extra = null;
-
- if (currentId) {
- extra = this.pGetExtraInfos()[currentId];
- }
-
- if (extra == null) {
- extra = {members: []};
- }
-
- return extra;
+ return this.getExtraInfo(this.getCurrentId());
}
getExtraInfo(channelId) {
var extra = null;
@@ -181,7 +176,10 @@ class ChannelStoreClass extends EventEmitter {
extra = this.pGetExtraInfos()[channelId];
}
- if (extra == null) {
+ if (extra) {
+ // create a defensive copy
+ extra = JSON.parse(JSON.stringify(extra));
+ } else {
extra = {members: []};
}
@@ -250,6 +248,38 @@ class ChannelStoreClass extends EventEmitter {
getPostMode() {
return this.postMode;
}
+
+ setUnreadCount(id) {
+ const ch = this.get(id);
+ const chMember = this.getMember(id);
+
+ let chMentionCount = chMember.mention_count;
+ let chUnreadCount = ch.total_msg_count - chMember.msg_count - chMentionCount;
+
+ if (ch.type === 'D') {
+ chMentionCount = chUnreadCount;
+ chUnreadCount = 0;
+ } else if (chMember.notify_props && chMember.notify_props.mark_unread === NotificationPrefs.MENTION) {
+ chUnreadCount = 0;
+ }
+
+ this.unreadCounts[id] = {msgs: chUnreadCount, mentions: chMentionCount};
+ }
+
+ setUnreadCounts() {
+ const channels = this.getAll();
+ channels.forEach((ch) => {
+ this.setUnreadCount(ch.id);
+ });
+ }
+
+ getUnreadCount(id) {
+ return this.unreadCounts[id] || {msgs: 0, mentions: 0};
+ }
+
+ getUnreadCounts() {
+ return this.unreadCounts;
+ }
}
var ChannelStore = new ChannelStoreClass();
@@ -281,6 +311,7 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => {
if (currentId) {
ChannelStore.resetCounts(currentId);
}
+ ChannelStore.setUnreadCounts();
ChannelStore.emitChange();
break;
@@ -291,6 +322,7 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => {
if (currentId) {
ChannelStore.resetCounts(currentId);
}
+ ChannelStore.setUnreadCount(action.channel.id);
ChannelStore.emitChange();
break;
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/socket_store.jsx b/web/react/stores/socket_store.jsx
index 2e0769cc4..29aa32a08 100644
--- a/web/react/stores/socket_store.jsx
+++ b/web/react/stores/socket_store.jsx
@@ -163,7 +163,7 @@ function handleNewPostEvent(msg) {
}
// Send desktop notification
- if (UserStore.getCurrentId() !== msg.user_id || post.props.from_webhook === 'true') {
+ if ((UserStore.getCurrentId() !== msg.user_id || post.props.from_webhook === 'true') && !Utils.isSystemMessage(post)) {
const msgProps = msg.props;
let mentions = [];
diff --git a/web/react/stores/suggestion_store.jsx b/web/react/stores/suggestion_store.jsx
new file mode 100644
index 000000000..2250ec234
--- /dev/null
+++ b/web/react/stores/suggestion_store.jsx
@@ -0,0 +1,272 @@
+// 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_CLEAR_SUGGESTIONS:
+ this.clearSuggestions(id);
+ this.clearSelection(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 8cf111d55..88b5aa739 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -55,9 +55,13 @@ export function getChannels(checkVersion) {
var serverVersion = xhr.getResponseHeader('X-Version-ID');
if (serverVersion !== BrowserStore.getLastServerVersion()) {
- BrowserStore.setLastServerVersion(serverVersion);
- window.location.reload(true);
- console.log('Detected version update refreshing the page'); //eslint-disable-line no-console
+ if (!BrowserStore.getLastServerVersion() || BrowserStore.getLastServerVersion() === '') {
+ BrowserStore.setLastServerVersion(serverVersion);
+ } else {
+ BrowserStore.setLastServerVersion(serverVersion);
+ window.location.reload(true);
+ console.log('Detected version update refreshing the page'); //eslint-disable-line no-console
+ }
}
}
@@ -106,10 +110,15 @@ export function getChannel(id) {
);
}
-export function updateLastViewedAt() {
- const channelId = ChannelStore.getCurrentId();
+export function updateLastViewedAt(id) {
+ let channelId;
+ if (id) {
+ channelId = id;
+ } else {
+ channelId = ChannelStore.getCurrentId();
+ }
- if (channelId === null) {
+ if (channelId == null) {
return;
}
@@ -159,8 +168,13 @@ export function getMoreChannels(force) {
}
}
-export function getChannelExtraInfo() {
- const channelId = ChannelStore.getCurrentId();
+export function getChannelExtraInfo(id) {
+ let channelId;
+ if (id) {
+ channelId = id;
+ } else {
+ channelId = ChannelStore.getCurrentId();
+ }
if (channelId != null) {
if (isCallInProgress('getChannelExtraInfo_' + channelId)) {
@@ -731,3 +745,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/channel_intro_mssages.jsx b/web/react/utils/channel_intro_messages.jsx
index 6f83778c9..9685f94b0 100644
--- a/web/react/utils/channel_intro_mssages.jsx
+++ b/web/react/utils/channel_intro_messages.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import * as Utils from './utils.jsx';
+import ChannelInviteModal from '../components/channel_invite_modal.jsx';
import EditChannelHeaderModal from '../components/edit_channel_header_modal.jsx';
import ToggleModalButton from '../components/toggle_modal_button.jsx';
import UserProfile from '../components/user_profile.jsx';
@@ -10,15 +11,15 @@ import Constants from '../utils/constants.jsx';
import TeamStore from '../stores/team_store.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
-export function createChannelIntroMessage(channel, showInviteModal) {
+export function createChannelIntroMessage(channel) {
if (channel.type === 'D') {
return createDMIntroMessage(channel);
} else if (ChannelStore.isDefault(channel)) {
return createDefaultIntroMessage(channel);
} else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
- return createOffTopicIntroMessage(channel, showInviteModal);
+ return createOffTopicIntroMessage(channel);
} else if (channel.type === 'O' || channel.type === 'P') {
- return createStandardIntroMessage(channel, showInviteModal);
+ return createStandardIntroMessage(channel);
}
}
@@ -62,7 +63,7 @@ export function createDMIntroMessage(channel) {
);
}
-export function createOffTopicIntroMessage(channel, showInviteModal) {
+export function createOffTopicIntroMessage(channel) {
return (
<div className='channel-intro'>
<h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
@@ -71,13 +72,7 @@ export function createOffTopicIntroMessage(channel, showInviteModal) {
<br/>
</p>
{createSetHeaderButton(channel)}
- <a
- href='#'
- className='intro-links'
- onClick={showInviteModal}
- >
- <i className='fa fa-user-plus'></i>{'Invite others to this channel'}
- </a>
+ {createInviteChannelMemberButton(channel, 'channel')}
</div>
);
}
@@ -122,7 +117,7 @@ export function createDefaultIntroMessage(channel) {
);
}
-export function createStandardIntroMessage(channel, showInviteModal) {
+export function createStandardIntroMessage(channel) {
var uiName = channel.display_name;
var creatorName = '';
@@ -162,17 +157,23 @@ export function createStandardIntroMessage(channel, showInviteModal) {
<br/>
</p>
{createSetHeaderButton(channel)}
- <a
- className='intro-links'
- href='#'
- onClick={showInviteModal}
- >
- <i className='fa fa-user-plus'></i>{'Invite others to this ' + uiType}
- </a>
+ {createInviteChannelMemberButton(channel, uiType)}
</div>
);
}
+function createInviteChannelMemberButton(channel, uiType) {
+ return (
+ <ToggleModalButton
+ className='intro-links'
+ dialogType={ChannelInviteModal}
+ dialogProps={{channel}}
+ >
+ <i className='fa fa-user-plus'></i>{'Invite others to this ' + uiType}
+ </ToggleModalButton>
+ );
+}
+
function createSetHeaderButton(channel) {
return (
<ToggleModalButton
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 99bd2453c..2d0edd596 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,14 @@ 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_CLEAR_SUGGESTIONS: null,
+ SUGGESTION_COMPLETE_WORD: null,
+ SUGGESTION_SELECT_NEXT: null,
+ SUGGESTION_SELECT_PREVIOUS: null
}),
PayloadSources: keyMirror({
@@ -111,7 +119,9 @@ export default {
POST_LOADING: 'loading',
POST_FAILED: 'failed',
POST_DELETED: 'deleted',
- POST_TYPE_JOIN_LEAVE: 'join_leave',
+ POST_TYPE_JOIN_LEAVE: 'system_join_leave',
+ SYSTEM_MESSAGE_PREFIX: 'system_',
+ SYSTEM_MESSAGE_PROFILE_IMAGE: '/static/images/logo_compact.png',
RESERVED_TEAM_NAMES: [
'www',
'web',
@@ -346,6 +356,21 @@ export default {
}
],
DEFAULT_CODE_THEME: 'github',
+ FONTS: {
+ 'Droid Serif': 'font--droid_serif',
+ 'Roboto Slab': 'font--roboto_slab',
+ Lora: 'font--lora',
+ Slabo: 'font--slabo',
+ Arvo: 'font--arvo',
+ 'Open Sans': 'font--open_sans',
+ Roboto: 'font--roboto',
+ 'PT Sans': 'font--pt_sans',
+ Lato: 'font--lato',
+ 'Source Sans Pro': 'font--source_sans_pro',
+ 'Exo 2': 'font--exo_2',
+ Ubuntu: 'font--ubuntu'
+ },
+ DEFAULT_FONT: 'Open Sans',
Preferences: {
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
CATEGORY_DISPLAY_SETTINGS: 'display_settings',
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 d6ed34e70..afe27ef96 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);
@@ -681,7 +681,11 @@ export function applyTheme(theme) {
}
if (theme.mentionHighlightBg) {
- changeCss('.mention-highlight, .search-highlight', 'background:' + theme.mentionHighlightBg, 1);
+ changeCss('.mention-highlight, .search-highlight, #archive-link-home', 'background:' + theme.mentionHighlightBg, 1);
+ }
+
+ if (theme.mentionHighlightBg) {
+ changeCss('.post.post--highlight, #archive-link-home', 'background:' + changeOpacity(theme.mentionHighlightBg, 0.5), 1);
}
if (theme.mentionHighlightLink) {
@@ -693,6 +697,23 @@ export function applyTheme(theme) {
}
updateCodeTheme(theme.codeTheme);
}
+
+export function applyFont(fontName) {
+ const body = $('body');
+
+ for (const key of Reflect.ownKeys(Constants.FONTS)) {
+ const className = Constants.FONTS[key];
+
+ if (fontName === key) {
+ if (!body.hasClass(className)) {
+ body.addClass(className);
+ }
+ } else {
+ body.removeClass(className);
+ }
+ }
+}
+
export function changeCss(className, classValue, classRepeat) {
// we need invisible container to store additional css definitions
var cssMainContainer = $('#css-modifier-container');
@@ -1226,3 +1247,7 @@ export function getPostTerm(post) {
export function isFeatureEnabled(feature) {
return PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, Constants.FeatureTogglePrefix + feature.label, {value: 'false'}).value === 'true';
}
+
+export function isSystemMessage(post) {
+ return post.type && (post.type.lastIndexOf(Constants.SYSTEM_MESSAGE_PREFIX) === 0);
+} \ No newline at end of file