summaryrefslogtreecommitdiffstats
path: root/web/react/components
diff options
context:
space:
mode:
Diffstat (limited to 'web/react/components')
-rw-r--r--web/react/components/access_history_modal.jsx303
-rw-r--r--web/react/components/activity_log_modal.jsx14
-rw-r--r--web/react/components/admin_console/admin_controller.jsx12
-rw-r--r--web/react/components/admin_console/admin_navbar_dropdown.jsx4
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx10
-rw-r--r--web/react/components/admin_console/email_settings.jsx50
-rw-r--r--web/react/components/admin_console/gitlab_settings.jsx32
-rw-r--r--web/react/components/admin_console/image_settings.jsx58
-rw-r--r--web/react/components/admin_console/log_settings.jsx30
-rw-r--r--web/react/components/admin_console/privacy_settings.jsx4
-rw-r--r--web/react/components/admin_console/rate_settings.jsx18
-rw-r--r--web/react/components/admin_console/reset_password_modal.jsx4
-rw-r--r--web/react/components/admin_console/select_team_modal.jsx2
-rw-r--r--web/react/components/admin_console/service_settings.jsx62
-rw-r--r--web/react/components/admin_console/sql_settings.jsx22
-rw-r--r--web/react/components/admin_console/team_settings.jsx14
-rw-r--r--web/react/components/admin_console/user_item.jsx6
-rw-r--r--web/react/components/change_url_modal.jsx4
-rw-r--r--web/react/components/channel_header.jsx154
-rw-r--r--web/react/components/channel_info_modal.jsx2
-rw-r--r--web/react/components/channel_invite_modal.jsx4
-rw-r--r--web/react/components/channel_loader.jsx15
-rw-r--r--web/react/components/channel_members.jsx4
-rw-r--r--web/react/components/channel_notifications.jsx6
-rw-r--r--web/react/components/create_comment.jsx73
-rw-r--r--web/react/components/create_post.jsx54
-rw-r--r--web/react/components/delete_channel_modal.jsx2
-rw-r--r--web/react/components/delete_post_modal.jsx2
-rw-r--r--web/react/components/edit_channel_modal.jsx8
-rw-r--r--web/react/components/edit_post_modal.jsx44
-rw-r--r--web/react/components/email_verify.jsx8
-rw-r--r--web/react/components/error_bar.jsx21
-rw-r--r--web/react/components/file_attachment.jsx4
-rw-r--r--web/react/components/file_upload.jsx4
-rw-r--r--web/react/components/find_team.jsx3
-rw-r--r--web/react/components/get_link_modal.jsx7
-rw-r--r--web/react/components/invite_member_modal.jsx25
-rw-r--r--web/react/components/login.jsx6
-rw-r--r--web/react/components/member_list_team_item.jsx6
-rw-r--r--web/react/components/mention_list.jsx2
-rw-r--r--web/react/components/more_channels.jsx14
-rw-r--r--web/react/components/more_direct_channels.jsx368
-rw-r--r--web/react/components/msg_typing.jsx15
-rw-r--r--web/react/components/navbar.jsx74
-rw-r--r--web/react/components/navbar_dropdown.jsx31
-rw-r--r--web/react/components/new_channel_modal.jsx6
-rw-r--r--web/react/components/password_reset_form.jsx3
-rw-r--r--web/react/components/password_reset_send_link.jsx5
-rw-r--r--web/react/components/popover_list_members.jsx47
-rw-r--r--web/react/components/post_body.jsx10
-rw-r--r--web/react/components/post_deleted_modal.jsx2
-rw-r--r--web/react/components/post_info.jsx2
-rw-r--r--web/react/components/post_list.jsx81
-rw-r--r--web/react/components/register_app_modal.jsx12
-rw-r--r--web/react/components/removed_from_channel_modal.jsx8
-rw-r--r--web/react/components/rename_channel_modal.jsx20
-rw-r--r--web/react/components/rhs_comment.jsx28
-rw-r--r--web/react/components/rhs_root_post.jsx2
-rw-r--r--web/react/components/rhs_thread.jsx47
-rw-r--r--web/react/components/search_bar.jsx2
-rw-r--r--web/react/components/search_results.jsx29
-rw-r--r--web/react/components/setting_item_max.jsx11
-rw-r--r--web/react/components/setting_picture.jsx2
-rw-r--r--web/react/components/setting_upload.jsx2
-rw-r--r--web/react/components/settings_sidebar.jsx2
-rw-r--r--web/react/components/sidebar.jsx460
-rw-r--r--web/react/components/sidebar_right_menu.jsx2
-rw-r--r--web/react/components/signup_user_complete.jsx13
-rw-r--r--web/react/components/team_import_tab.jsx6
-rw-r--r--web/react/components/team_members.jsx4
-rw-r--r--web/react/components/team_signup_display_name_page.jsx3
-rw-r--r--web/react/components/team_signup_email_item.jsx5
-rw-r--r--web/react/components/team_signup_password_page.jsx5
-rw-r--r--web/react/components/team_signup_url_page.jsx3
-rw-r--r--web/react/components/team_signup_username_page.jsx12
-rw-r--r--web/react/components/team_signup_welcome_page.jsx3
-rw-r--r--web/react/components/team_signup_with_email.jsx3
-rw-r--r--web/react/components/team_signup_with_sso.jsx3
-rw-r--r--web/react/components/textbox.jsx40
-rw-r--r--web/react/components/user_profile.jsx65
-rw-r--r--web/react/components/user_settings/import_theme_modal.jsx2
-rw-r--r--web/react/components/user_settings/manage_outgoing_hooks.jsx262
-rw-r--r--web/react/components/user_settings/user_settings.jsx12
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx16
-rw-r--r--web/react/components/user_settings/user_settings_display.jsx165
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx5
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx87
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx3
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx79
-rw-r--r--web/react/components/user_settings/user_settings_security.jsx2
-rw-r--r--web/react/components/view_image.jsx12
91 files changed, 2190 insertions, 1008 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx
index 2ad4d5b00..c8af2553d 100644
--- a/web/react/components/access_history_modal.jsx
+++ b/web/react/components/access_history_modal.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
var UserStore = require('../stores/user_store.jsx');
+var ChannelStore = require('../stores/channel_store.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var LoadingScreen = require('./loading_screen.jsx');
var Utils = require('../utils/utils.jsx');
@@ -14,8 +15,10 @@ export default class AccessHistoryModal extends React.Component {
this.handleMoreInfo = this.handleMoreInfo.bind(this);
this.onHide = this.onHide.bind(this);
this.onShow = this.onShow.bind(this);
+ this.formatAuditInfo = this.formatAuditInfo.bind(this);
+ this.handleRevokedSession = this.handleRevokedSession.bind(this);
- let state = this.getStateFromStoresForAudits();
+ const state = this.getStateFromStoresForAudits();
state.moreInfo = [];
this.state = state;
@@ -34,9 +37,9 @@ export default class AccessHistoryModal extends React.Component {
}
componentDidMount() {
UserStore.addAuditsChangeListener(this.onAuditChange);
- $(React.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.onShow);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.onShow);
- $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.onHide);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.onHide);
}
componentWillUnmount() {
UserStore.removeAuditsChangeListener(this.onAuditChange);
@@ -52,23 +55,269 @@ export default class AccessHistoryModal extends React.Component {
newMoreInfo[index] = true;
this.setState({moreInfo: newMoreInfo});
}
- render() {
- var accessList = [];
- var currentHistoryDate = null;
+ handleRevokedSession(sessionId) {
+ return 'The session with id ' + sessionId + ' was revoked';
+ }
+ formatAuditInfo(currentAudit) {
+ const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, '');
- for (var i = 0; i < this.state.audits.length; i++) {
- var currentAudit = this.state.audits[i];
- var newHistoryDate = new Date(currentAudit.create_at);
- var newDate = null;
+ let currentAuditDesc = '';
+
+ if (currentActionURL.indexOf('/channels') === 0) {
+ const channelInfo = currentAudit.extra_info.split(' ');
+ const channelNameField = channelInfo[0].split('=');
+
+ let channelURL = '';
+ let channelObj;
+ let channelName = '';
+ if (channelNameField.indexOf('name') >= 0) {
+ channelURL = channelNameField[channelNameField.indexOf('name') + 1];
+ channelObj = ChannelStore.getByName(channelURL);
+ if (channelObj) {
+ channelName = channelObj.display_name;
+ } else {
+ channelName = channelURL;
+ }
+ }
+
+ switch (currentActionURL) {
+ case '/channels/create':
+ currentAuditDesc = 'Created the ' + channelName + ' channel/group';
+ break;
+ case '/channels/create_direct':
+ currentAuditDesc = 'Established a direct message channel with ' + Utils.getDirectTeammate(channelObj.id).username;
+ break;
+ case '/channels/update':
+ currentAuditDesc = 'Updated the ' + channelName + ' channel/group name';
+ break;
+ case '/channels/update_desc':
+ currentAuditDesc = 'Updated the ' + channelName + ' channel/group description';
+ break;
+ default:
+ let userIdField = [];
+ let userId = '';
+ let username = '';
+
+ if (channelInfo[1]) {
+ userIdField = channelInfo[1].split('=');
+
+ if (userIdField.indexOf('user_id') >= 0) {
+ userId = userIdField[userIdField.indexOf('user_id') + 1];
+ username = UserStore.getProfile(userId).username;
+ }
+ }
+
+ if (/\/channels\/[A-Za-z0-9]+\/delete/.test(currentActionURL)) {
+ currentAuditDesc = 'Deleted the channel/group with the URL ' + channelURL;
+ } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(currentActionURL)) {
+ currentAuditDesc = 'Added ' + username + ' to the ' + channelName + ' channel/group';
+ } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(currentActionURL)) {
+ currentAuditDesc = 'Removed ' + username + ' from the ' + channelName + ' channel/group';
+ }
+
+ break;
+ }
+ } else if (currentActionURL.indexOf('/oauth') === 0) {
+ const oauthInfo = currentAudit.extra_info.split(' ');
+
+ switch (currentActionURL) {
+ case '/oauth/register':
+ const clientIdField = oauthInfo[0].split('=');
+
+ if (clientIdField[0] === 'client_id') {
+ currentAuditDesc = 'Attempted to register a new OAuth Application with ID ' + clientIdField[1];
+ }
+
+ break;
+ case '/oauth/allow':
+ if (oauthInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to allow a new OAuth service access';
+ } else if (oauthInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully gave a new OAuth service access';
+ } else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
+ currentAuditDesc = 'Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback';
+ }
+
+ break;
+ case '/oauth/access_token':
+ if (oauthInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to get an OAuth access token';
+ } else if (oauthInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully added a new OAuth service';
+ } else {
+ const oauthTokenFailure = oauthInfo[0].split('-');
+
+ if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
+ currentAuditDesc = 'Failed to get an OAuth access token - ' + oauthTokenFailure[1].trim();
+ }
+ }
+
+ break;
+ default:
+ break;
+ }
+ } else if (currentActionURL.indexOf('/users') === 0) {
+ const userInfo = currentAudit.extra_info.split(' ');
+
+ switch (currentActionURL) {
+ case '/users/login':
+ if (userInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to login';
+ } else if (userInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully logged in';
+ } else if (userInfo[0]) {
+ currentAuditDesc = 'FAILED login attempt';
+ }
+
+ break;
+ case '/users/revoke_session':
+ currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]);
+ break;
+ case '/users/newimage':
+ currentAuditDesc = 'Updated your profile picture';
+ break;
+ case '/users/update':
+ currentAuditDesc = 'Updated the general settings of your account';
+ break;
+ case '/users/newpassword':
+ if (userInfo[0] === 'attempted') {
+ currentAuditDesc = 'Attempted to change password';
+ } else if (userInfo[0] === 'completed') {
+ currentAuditDesc = 'Successfully changed password';
+ } else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
+ currentAuditDesc = 'Failed to change password - tried to update user password who was logged in through oauth';
+ }
+
+ break;
+ case '/users/update_roles':
+ const userRoles = userInfo[0].split('=')[1];
- if (!currentHistoryDate || currentHistoryDate.toLocaleDateString() !== newHistoryDate.toLocaleDateString()) {
- currentHistoryDate = newHistoryDate;
- newDate = (<div> {currentHistoryDate.toDateString()} </div>);
+ currentAuditDesc = 'Updated user role(s) to ';
+ if (userRoles.trim()) {
+ currentAuditDesc += userRoles;
+ } else {
+ currentAuditDesc += 'member';
+ }
+
+ break;
+ case '/users/update_active':
+ const updateType = userInfo[0].split('=')[0];
+ const updateField = userInfo[0].split('=')[1];
+
+ /* Either describes account activation/deactivation or a revoked session as part of an account deactivation */
+ if (updateType === 'active') {
+ if (updateField === 'true') {
+ currentAuditDesc = 'Account made active';
+ } else if (updateField === 'false') {
+ currentAuditDesc = 'Account made inactive';
+ }
+
+ const actingUserInfo = userInfo[1].split('=');
+ if (actingUserInfo[0] === 'session_user') {
+ const actingUser = UserStore.getProfile(actingUserInfo[1]);
+ const currentUser = UserStore.getCurrentUser();
+ if (currentUser && actingUser && (Utils.isAdmin(currentUser.roles) || Utils.isSystemAdmin(currentUser.roles))) {
+ currentAuditDesc += ' by ' + actingUser.username;
+ } else if (currentUser && actingUser) {
+ currentAuditDesc += ' by an admin';
+ }
+ }
+ } else if (updateType === 'session_id') {
+ currentAuditDesc = this.handleRevokedSession(updateField);
+ }
+
+ break;
+ case '/users/send_password_reset':
+ currentAuditDesc = 'Sent an email to ' + userInfo[0].split('=')[1] + ' to reset your password';
+ break;
+ case '/users/reset_password':
+ if (userInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to reset password';
+ } else if (userInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully reset password';
+ }
+
+ break;
+ case '/users/update_notify':
+ currentAuditDesc = 'Updated your global notification settings';
+ break;
+ default:
+ break;
}
+ } else if (currentActionURL.indexOf('/hooks') === 0) {
+ const webhookInfo = currentAudit.extra_info.split(' ');
+
+ switch (currentActionURL) {
+ case '/hooks/incoming/create':
+ if (webhookInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to create a webhook';
+ } else if (webhookInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully created a webhook';
+ } else if (webhookInfo[0] === 'fail - bad channel permissions') {
+ currentAuditDesc = 'Failed to create a webhook - bad channel permissions';
+ }
- if (!currentAudit.session_id && currentAudit.action.search('/users/login') !== -1) {
- currentAudit.session_id = 'N/A (Login attempt)';
+ break;
+ case '/hooks/incoming/delete':
+ if (webhookInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to delete a webhook';
+ } else if (webhookInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully deleted a webhook';
+ } else if (webhookInfo[0] === 'fail - inappropriate conditions') {
+ currentAuditDesc = 'Failed to delete a webhook - inappropriate conditions';
+ }
+
+ break;
+ default:
+ break;
+ }
+ } else {
+ switch (currentActionURL) {
+ case '/logout':
+ currentAuditDesc = 'Logged out of your account';
+ break;
+ case '/verify_email':
+ currentAuditDesc = 'Sucessfully verified your email address';
+ break;
+ default:
+ break;
}
+ }
+
+ /* If all else fails... */
+ if (!currentAuditDesc) {
+ /* Currently not called anywhere */
+ if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) {
+ currentAuditDesc = 'Revoked all current sessions for the team';
+ } else {
+ let currentActionDesc = '';
+ if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) {
+ currentActionDesc = currentActionURL.substring(currentActionURL.lastIndexOf('/') + 1).replace('_', ' ');
+ currentActionDesc = Utils.toTitleCase(currentActionDesc);
+ }
+
+ let currentExtraInfoDesc = '';
+ if (currentAudit.extra_info) {
+ currentExtraInfoDesc = currentAudit.extra_info;
+
+ if (currentExtraInfoDesc.indexOf('=') !== -1) {
+ currentExtraInfoDesc = currentExtraInfoDesc.substring(currentExtraInfoDesc.indexOf('=') + 1);
+ }
+ }
+ currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc;
+ }
+ }
+
+ const currentDate = new Date(currentAudit.create_at);
+ const currentAuditInfo = currentDate.toDateString() + ' - ' + currentDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute: '2-digit'}) + ' | ' + currentAuditDesc;
+ return currentAuditInfo;
+ }
+ render() {
+ var accessList = [];
+
+ for (var i = 0; i < this.state.audits.length; i++) {
+ const currentAudit = this.state.audits[i];
+ const currentAuditInfo = this.formatAuditInfo(currentAudit);
var moreInfo = (
<a
@@ -76,15 +325,27 @@ export default class AccessHistoryModal extends React.Component {
className='theme'
onClick={this.handleMoreInfo.bind(this, i)}
>
- More info
+ {'More info'}
</a>
);
if (this.state.moreInfo[i]) {
+ if (!currentAudit.session_id) {
+ currentAudit.session_id = 'N/A';
+
+ if (currentAudit.action.search('/users/login') >= 0) {
+ if (currentAudit.extra_info === 'attempt') {
+ currentAudit.session_id += ' (Login attempt)';
+ } else {
+ currentAudit.session_id += ' (Login failure)';
+ }
+ }
+ }
+
moreInfo = (
<div>
+ <div>{'IP: ' + currentAudit.ip_address}</div>
<div>{'Session ID: ' + currentAudit.session_id}</div>
- <div>{'URL: ' + currentAudit.action.replace(/\/api\/v[1-9]/, '')}</div>
</div>
);
}
@@ -99,11 +360,9 @@ export default class AccessHistoryModal extends React.Component {
key={'accessHistoryEntryKey' + i}
className='access-history__table'
>
- <div className='access__date'>{newDate}</div>
<div className='access__report'>
- <div className='report__time'>{newHistoryDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute: '2-digit'})}</div>
+ <div className='report__time'>{currentAuditInfo}</div>
<div className='report__info'>
- <div>{'IP: ' + currentAudit.ip_address}</div>
{moreInfo}
</div>
{divider}
@@ -138,13 +397,13 @@ export default class AccessHistoryModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>&times;</span>
+ <span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'
id='myModalLabel'
>
- Access History
+ {'Access History'}
</h4>
</div>
<div
diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx
index 1fe2133ec..2c944913f 100644
--- a/web/react/components/activity_log_modal.jsx
+++ b/web/react/components/activity_log_modal.jsx
@@ -56,9 +56,9 @@ export default class ActivityLogModal extends React.Component {
}
componentDidMount() {
UserStore.addSessionsChangeListener(this.onListenerChange);
- $(React.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.onShow);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.onShow);
- $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.onHide);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.onHide);
}
componentWillUnmount() {
UserStore.removeSessionsChangeListener(this.onListenerChange);
@@ -81,6 +81,7 @@ export default class ActivityLogModal extends React.Component {
const currentSession = this.state.sessions[i];
const lastAccessTime = new Date(currentSession.last_activity_at);
const firstAccessTime = new Date(currentSession.create_at);
+ let devicePlatform = currentSession.props.platform;
let devicePicture = '';
if (currentSession.props.platform === 'Windows') {
@@ -88,7 +89,12 @@ export default class ActivityLogModal extends React.Component {
} else if (currentSession.props.platform === 'Macintosh' || currentSession.props.platform === 'iPhone') {
devicePicture = 'fa fa-apple';
} else if (currentSession.props.platform === 'Linux') {
- devicePicture = 'fa fa-linux';
+ if (currentSession.props.os.indexOf('Android') >= 0) {
+ devicePlatform = 'Android';
+ devicePicture = 'fa fa-android';
+ } else {
+ devicePicture = 'fa fa-linux';
+ }
}
let moreInfo;
@@ -119,7 +125,7 @@ export default class ActivityLogModal extends React.Component {
className='activity-log__table'
>
<div className='activity-log__report'>
- <div className='report__platform'><i className={devicePicture} />{currentSession.props.platform}</div>
+ <div className='report__platform'><i className={devicePicture} />{devicePlatform}</div>
<div className='report__info'>
<div>{`Last activity: ${lastAccessTime.toDateString()}, ${lastAccessTime.toLocaleTimeString()}`}</div>
{moreInfo}
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index f2fb8ac78..f770d166c 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -40,9 +40,13 @@ export default class AdminController extends React.Component {
config: AdminStore.getConfig(),
teams: AdminStore.getAllTeams(),
selectedTeams,
- selected: 'service_settings',
- selectedTeam: null
+ selected: props.tab || 'service_settings',
+ selectedTeam: props.teamId || null
};
+
+ if (!props.tab) {
+ history.replaceState(null, null, `/admin_console/${this.state.selected}`);
+ }
}
componentDidMount() {
@@ -142,7 +146,9 @@ export default class AdminController extends React.Component {
} else if (this.state.selected === 'service_settings') {
tab = <ServiceSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'team_users') {
- tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
+ if (this.state.teams) {
+ tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
+ }
}
}
diff --git a/web/react/components/admin_console/admin_navbar_dropdown.jsx b/web/react/components/admin_console/admin_navbar_dropdown.jsx
index 21ec5c3cf..df8da94e1 100644
--- a/web/react/components/admin_console/admin_navbar_dropdown.jsx
+++ b/web/react/components/admin_console/admin_navbar_dropdown.jsx
@@ -27,7 +27,7 @@ export default class AdminNavbarDropdown extends React.Component {
}
componentDidMount() {
- $(React.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
+ $(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
this.blockToggle = true;
setTimeout(() => {
this.blockToggle = false;
@@ -36,7 +36,7 @@ export default class AdminNavbarDropdown extends React.Component {
}
componentWillUnmount() {
- $(React.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
+ $(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
}
render() {
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index bc6ad1931..b0e01ff17 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -24,6 +24,7 @@ export default class AdminSidebar extends React.Component {
handleClick(name, teamId, e) {
e.preventDefault();
this.props.selectTab(name, teamId);
+ history.pushState({name: name, teamId: teamId}, null, `/admin_console/${name}/${teamId || ''}`);
}
isSelected(name, teamId) {
@@ -53,6 +54,9 @@ export default class AdminSidebar extends React.Component {
}
componentDidMount() {
+ if ($(window).width() > 768) {
+ $('.nav-pills__container').perfectScrollbar();
+ }
}
showTeamSelect(e) {
@@ -100,6 +104,7 @@ export default class AdminSidebar extends React.Component {
className='menu-icon--right menu__close'
onClick={this.removeTeam.bind(this, team.id)}
style={{cursor: 'pointer'}}
+ title='Remove team from sidebar menu'
>
{'x'}
</span>
@@ -233,7 +238,10 @@ export default class AdminSidebar extends React.Component {
href='#'
onClick={this.showTeamSelect}
>
- <i className='fa fa-plus'></i>
+ <i
+ className='fa fa-plus'
+ title='Add team to sidebar menu'
+ ></i>
</a>
</span>
</h4>
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index c028d605d..40e00ff04 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -41,28 +41,28 @@ export default class EmailSettings extends React.Component {
buildConfig() {
var config = this.props.config;
- config.EmailSettings.EnableSignUpWithEmail = React.findDOMNode(this.refs.allowSignUpWithEmail).checked;
- config.EmailSettings.SendEmailNotifications = React.findDOMNode(this.refs.sendEmailNotifications).checked;
- config.EmailSettings.RequireEmailVerification = React.findDOMNode(this.refs.requireEmailVerification).checked;
- config.EmailSettings.SendEmailNotifications = React.findDOMNode(this.refs.sendEmailNotifications).checked;
- config.EmailSettings.FeedbackName = React.findDOMNode(this.refs.feedbackName).value.trim();
- config.EmailSettings.FeedbackEmail = React.findDOMNode(this.refs.feedbackEmail).value.trim();
- config.EmailSettings.SMTPServer = React.findDOMNode(this.refs.SMTPServer).value.trim();
- config.EmailSettings.SMTPPort = React.findDOMNode(this.refs.SMTPPort).value.trim();
- config.EmailSettings.SMTPUsername = React.findDOMNode(this.refs.SMTPUsername).value.trim();
- config.EmailSettings.SMTPPassword = React.findDOMNode(this.refs.SMTPPassword).value.trim();
- config.EmailSettings.ConnectionSecurity = React.findDOMNode(this.refs.ConnectionSecurity).value.trim();
-
- config.EmailSettings.InviteSalt = React.findDOMNode(this.refs.InviteSalt).value.trim();
+ config.EmailSettings.EnableSignUpWithEmail = ReactDOM.findDOMNode(this.refs.allowSignUpWithEmail).checked;
+ config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked;
+ config.EmailSettings.RequireEmailVerification = ReactDOM.findDOMNode(this.refs.requireEmailVerification).checked;
+ config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked;
+ config.EmailSettings.FeedbackName = ReactDOM.findDOMNode(this.refs.feedbackName).value.trim();
+ config.EmailSettings.FeedbackEmail = ReactDOM.findDOMNode(this.refs.feedbackEmail).value.trim();
+ config.EmailSettings.SMTPServer = ReactDOM.findDOMNode(this.refs.SMTPServer).value.trim();
+ config.EmailSettings.SMTPPort = ReactDOM.findDOMNode(this.refs.SMTPPort).value.trim();
+ config.EmailSettings.SMTPUsername = ReactDOM.findDOMNode(this.refs.SMTPUsername).value.trim();
+ config.EmailSettings.SMTPPassword = ReactDOM.findDOMNode(this.refs.SMTPPassword).value.trim();
+ config.EmailSettings.ConnectionSecurity = ReactDOM.findDOMNode(this.refs.ConnectionSecurity).value.trim();
+
+ config.EmailSettings.InviteSalt = ReactDOM.findDOMNode(this.refs.InviteSalt).value.trim();
if (config.EmailSettings.InviteSalt === '') {
config.EmailSettings.InviteSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
- React.findDOMNode(this.refs.InviteSalt).value = config.EmailSettings.InviteSalt;
+ ReactDOM.findDOMNode(this.refs.InviteSalt).value = config.EmailSettings.InviteSalt;
}
- config.EmailSettings.PasswordResetSalt = React.findDOMNode(this.refs.PasswordResetSalt).value.trim();
+ config.EmailSettings.PasswordResetSalt = ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value.trim();
if (config.EmailSettings.PasswordResetSalt === '') {
config.EmailSettings.PasswordResetSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
- React.findDOMNode(this.refs.PasswordResetSalt).value = config.EmailSettings.PasswordResetSalt;
+ ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value = config.EmailSettings.PasswordResetSalt;
}
return config;
@@ -70,14 +70,14 @@ export default class EmailSettings extends React.Component {
handleGenerateInvite(e) {
e.preventDefault();
- React.findDOMNode(this.refs.InviteSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ ReactDOM.findDOMNode(this.refs.InviteSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s);
}
handleGenerateReset(e) {
e.preventDefault();
- React.findDOMNode(this.refs.PasswordResetSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s);
}
@@ -440,14 +440,16 @@ export default class EmailSettings extends React.Component {
className='table table-bordered'
cellPadding='5'
>
- <tr><td className='help-text'>{'None'}</td><td className='help-text'>{'Mattermost will send email over an unsecure connection.'}</td></tr>
- <tr><td className='help-text'>{'TLS'}</td><td className='help-text'>{'Encrypts the communication between Mattermost and your email server.'}</td></tr>
- <tr><td className='help-text'>{'STARTTLS'}</td><td className='help-text'>{'Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'}</td></tr>
+ <tbody>
+ <tr><td className='help-text'>{'None'}</td><td className='help-text'>{'Mattermost will send email over an unsecure connection.'}</td></tr>
+ <tr><td className='help-text'>{'TLS'}</td><td className='help-text'>{'Encrypts the communication between Mattermost and your email server.'}</td></tr>
+ <tr><td className='help-text'>{'STARTTLS'}</td><td className='help-text'>{'Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'}</td></tr>
+ </tbody>
</table>
</div>
<div className='help-text'>
<button
- className='btn'
+ className='btn btn-default'
onClick={this.handleTestConnection}
disabled={!this.state.sendEmailNotifications}
id='connection-button'
@@ -482,7 +484,7 @@ export default class EmailSettings extends React.Component {
<p className='help-text'>{'32-character salt added to signing of email invites. Randomly generated on install. Click "Re-Generate" to create new salt.'}</p>
<div className='help-text'>
<button
- className='btn'
+ className='btn btn-default'
onClick={this.handleGenerateInvite}
disabled={!this.state.sendEmailNotifications}
>
@@ -513,7 +515,7 @@ export default class EmailSettings extends React.Component {
<p className='help-text'>{'32-character salt added to signing of password reset emails. Randomly generated on install. Click "Re-Generate" to create new salt.'}</p>
<div className='help-text'>
<button
- className='btn'
+ className='btn btn-default'
onClick={this.handleGenerateReset}
disabled={!this.state.sendEmailNotifications}
>
diff --git a/web/react/components/admin_console/gitlab_settings.jsx b/web/react/components/admin_console/gitlab_settings.jsx
index 6b97287e4..8b0f00083 100644
--- a/web/react/components/admin_console/gitlab_settings.jsx
+++ b/web/react/components/admin_console/gitlab_settings.jsx
@@ -37,12 +37,12 @@ export default class GitLabSettings extends React.Component {
$('#save-button').button('loading');
var config = this.props.config;
- config.GitLabSettings.Enable = React.findDOMNode(this.refs.Enable).checked;
- config.GitLabSettings.Secret = React.findDOMNode(this.refs.Secret).value.trim();
- config.GitLabSettings.Id = React.findDOMNode(this.refs.Id).value.trim();
- config.GitLabSettings.AuthEndpoint = React.findDOMNode(this.refs.AuthEndpoint).value.trim();
- config.GitLabSettings.TokenEndpoint = React.findDOMNode(this.refs.TokenEndpoint).value.trim();
- config.GitLabSettings.UserApiEndpoint = React.findDOMNode(this.refs.UserApiEndpoint).value.trim();
+ config.GitLabSettings.Enable = ReactDOM.findDOMNode(this.refs.Enable).checked;
+ config.GitLabSettings.Secret = ReactDOM.findDOMNode(this.refs.Secret).value.trim();
+ config.GitLabSettings.Id = ReactDOM.findDOMNode(this.refs.Id).value.trim();
+ config.GitLabSettings.AuthEndpoint = ReactDOM.findDOMNode(this.refs.AuthEndpoint).value.trim();
+ config.GitLabSettings.TokenEndpoint = ReactDOM.findDOMNode(this.refs.TokenEndpoint).value.trim();
+ config.GitLabSettings.UserApiEndpoint = ReactDOM.findDOMNode(this.refs.UserApiEndpoint).value.trim();
Client.saveConfig(
config,
@@ -113,7 +113,17 @@ export default class GitLabSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'When true, Mattermost allows team creation and account signup using GitLab OAuth. To configure, log in to your GitLab account and go to Applications -> Profile Settings. Enter Redirect URIs "<your-mattermost-url>/login/gitlab/complete" (example: http://localhost:8065/login/gitlab/complete) and "<your-mattermost-url>/signup/gitlab/complete". Then use "Secret" and "Id" fields to complete the options below.'}</p>
+ <p className='help-text'>
+ {'When true, Mattermost allows team creation and account signup using GitLab OAuth.'} <br/>
+ </p>
+ <div className='help-text'>
+ <ol>
+ <li>{'Log in to your GitLab account and go to Applications -> Profile Settings.'}</li>
+ <li>{'Enter Redirect URIs "<your-mattermost-url>/login/gitlab/complete" (example: http://localhost:8065/login/gitlab/complete) and "<your-mattermost-url>/signup/gitlab/complete". '}</li>
+ <li>{'Then use "Secret" and "Id" fields from GitLab to complete the options below.'}</li>
+ <li>{'Complete the Endpoint URLs below. '}</li>
+ </ol>
+ </div>
</div>
</div>
@@ -179,7 +189,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Enable}
/>
- <p className='help-text'>{'Enter <your-gitlab-url>/oauth/authorize (example http://localhost:3000/oauth/authorize). Make sure you use HTTP or HTTPS in your URLs as appropriate.'}</p>
+ <p className='help-text'>{'Enter https://<your-gitlab-url>/oauth/authorize (example https://example.com:3000/oauth/authorize). Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'}</p>
</div>
</div>
@@ -201,7 +211,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Enable}
/>
- <p className='help-text'>{'Enter <your-gitlab-url>/oauth/token. Make sure you use HTTP or HTTPS in your URLs as appropriate.'}</p>
+ <p className='help-text'>{'Enter https://<your-gitlab-url>/oauth/token. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'}</p>
</div>
</div>
@@ -223,7 +233,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Enable}
/>
- <p className='help-text'>{'Enter <your-gitlab-url>/api/v3/user. Make sure you use HTTP or HTTPS in your URLs as appropriate.'}</p>
+ <p className='help-text'>{'Enter https://<your-gitlab-url>/api/v3/user. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'}</p>
</div>
</div>
@@ -250,7 +260,7 @@ export default class GitLabSettings extends React.Component {
}
-//config.GitLabSettings.Scope = React.findDOMNode(this.refs.Scope).value.trim();
+//config.GitLabSettings.Scope = ReactDOM.findDOMNode(this.refs.Scope).value.trim();
// <div className='form-group'>
// <label
// className='control-label col-sm-4'
diff --git a/web/react/components/admin_console/image_settings.jsx b/web/react/components/admin_console/image_settings.jsx
index 24ed5a0a8..8b577e012 100644
--- a/web/react/components/admin_console/image_settings.jsx
+++ b/web/react/components/admin_console/image_settings.jsx
@@ -24,7 +24,7 @@ export default class FileSettings extends React.Component {
var s = {saveNeeded: true, serverError: this.state.serverError};
if (action === 'DriverName') {
- s.DriverName = React.findDOMNode(this.refs.DriverName).value;
+ s.DriverName = ReactDOM.findDOMNode(this.refs.DriverName).value;
}
this.setState(s);
@@ -32,7 +32,7 @@ export default class FileSettings extends React.Component {
handleGenerate(e) {
e.preventDefault();
- React.findDOMNode(this.refs.PublicLinkSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ ReactDOM.findDOMNode(this.refs.PublicLinkSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s);
}
@@ -42,62 +42,62 @@ export default class FileSettings extends React.Component {
$('#save-button').button('loading');
var config = this.props.config;
- config.FileSettings.DriverName = React.findDOMNode(this.refs.DriverName).value;
- config.FileSettings.Directory = React.findDOMNode(this.refs.Directory).value;
- config.FileSettings.AmazonS3AccessKeyId = React.findDOMNode(this.refs.AmazonS3AccessKeyId).value;
- config.FileSettings.AmazonS3SecretAccessKey = React.findDOMNode(this.refs.AmazonS3SecretAccessKey).value;
- config.FileSettings.AmazonS3Bucket = React.findDOMNode(this.refs.AmazonS3Bucket).value;
- config.FileSettings.AmazonS3Region = React.findDOMNode(this.refs.AmazonS3Region).value;
- config.FileSettings.EnablePublicLink = React.findDOMNode(this.refs.EnablePublicLink).checked;
+ config.FileSettings.DriverName = ReactDOM.findDOMNode(this.refs.DriverName).value;
+ config.FileSettings.Directory = ReactDOM.findDOMNode(this.refs.Directory).value;
+ config.FileSettings.AmazonS3AccessKeyId = ReactDOM.findDOMNode(this.refs.AmazonS3AccessKeyId).value;
+ config.FileSettings.AmazonS3SecretAccessKey = ReactDOM.findDOMNode(this.refs.AmazonS3SecretAccessKey).value;
+ config.FileSettings.AmazonS3Bucket = ReactDOM.findDOMNode(this.refs.AmazonS3Bucket).value;
+ config.FileSettings.AmazonS3Region = ReactDOM.findDOMNode(this.refs.AmazonS3Region).value;
+ config.FileSettings.EnablePublicLink = ReactDOM.findDOMNode(this.refs.EnablePublicLink).checked;
- config.FileSettings.PublicLinkSalt = React.findDOMNode(this.refs.PublicLinkSalt).value.trim();
+ config.FileSettings.PublicLinkSalt = ReactDOM.findDOMNode(this.refs.PublicLinkSalt).value.trim();
if (config.FileSettings.PublicLinkSalt === '') {
config.FileSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
- React.findDOMNode(this.refs.PublicLinkSalt).value = config.FileSettings.PublicLinkSalt;
+ ReactDOM.findDOMNode(this.refs.PublicLinkSalt).value = config.FileSettings.PublicLinkSalt;
}
var thumbnailWidth = 120;
- if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10))) {
- thumbnailWidth = parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10);
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailWidth).value, 10))) {
+ thumbnailWidth = parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailWidth).value, 10);
}
config.FileSettings.ThumbnailWidth = thumbnailWidth;
- React.findDOMNode(this.refs.ThumbnailWidth).value = thumbnailWidth;
+ ReactDOM.findDOMNode(this.refs.ThumbnailWidth).value = thumbnailWidth;
var thumbnailHeight = 100;
- if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailHeight).value, 10))) {
- thumbnailHeight = parseInt(React.findDOMNode(this.refs.ThumbnailHeight).value, 10);
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailHeight).value, 10))) {
+ thumbnailHeight = parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailHeight).value, 10);
}
config.FileSettings.ThumbnailHeight = thumbnailHeight;
- React.findDOMNode(this.refs.ThumbnailHeight).value = thumbnailHeight;
+ ReactDOM.findDOMNode(this.refs.ThumbnailHeight).value = thumbnailHeight;
var previewWidth = 1024;
- if (!isNaN(parseInt(React.findDOMNode(this.refs.PreviewWidth).value, 10))) {
- previewWidth = parseInt(React.findDOMNode(this.refs.PreviewWidth).value, 10);
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.PreviewWidth).value, 10))) {
+ previewWidth = parseInt(ReactDOM.findDOMNode(this.refs.PreviewWidth).value, 10);
}
config.FileSettings.PreviewWidth = previewWidth;
- React.findDOMNode(this.refs.PreviewWidth).value = previewWidth;
+ ReactDOM.findDOMNode(this.refs.PreviewWidth).value = previewWidth;
var previewHeight = 0;
- if (!isNaN(parseInt(React.findDOMNode(this.refs.PreviewHeight).value, 10))) {
- previewHeight = parseInt(React.findDOMNode(this.refs.PreviewHeight).value, 10);
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.PreviewHeight).value, 10))) {
+ previewHeight = parseInt(ReactDOM.findDOMNode(this.refs.PreviewHeight).value, 10);
}
config.FileSettings.PreviewHeight = previewHeight;
- React.findDOMNode(this.refs.PreviewHeight).value = previewHeight;
+ ReactDOM.findDOMNode(this.refs.PreviewHeight).value = previewHeight;
var profileWidth = 128;
- if (!isNaN(parseInt(React.findDOMNode(this.refs.ProfileWidth).value, 10))) {
- profileWidth = parseInt(React.findDOMNode(this.refs.ProfileWidth).value, 10);
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ProfileWidth).value, 10))) {
+ profileWidth = parseInt(ReactDOM.findDOMNode(this.refs.ProfileWidth).value, 10);
}
config.FileSettings.ProfileWidth = profileWidth;
- React.findDOMNode(this.refs.ProfileWidth).value = profileWidth;
+ ReactDOM.findDOMNode(this.refs.ProfileWidth).value = profileWidth;
var profileHeight = 128;
- if (!isNaN(parseInt(React.findDOMNode(this.refs.ProfileHeight).value, 10))) {
- profileHeight = parseInt(React.findDOMNode(this.refs.ProfileHeight).value, 10);
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ProfileHeight).value, 10))) {
+ profileHeight = parseInt(ReactDOM.findDOMNode(this.refs.ProfileHeight).value, 10);
}
config.FileSettings.ProfileHeight = profileHeight;
- React.findDOMNode(this.refs.ProfileHeight).value = profileHeight;
+ ReactDOM.findDOMNode(this.refs.ProfileHeight).value = profileHeight;
Client.saveConfig(
config,
diff --git a/web/react/components/admin_console/log_settings.jsx b/web/react/components/admin_console/log_settings.jsx
index 367605f14..7e9eda89b 100644
--- a/web/react/components/admin_console/log_settings.jsx
+++ b/web/react/components/admin_console/log_settings.jsx
@@ -46,12 +46,12 @@ export default class LogSettings extends React.Component {
$('#save-button').button('loading');
var config = this.props.config;
- config.LogSettings.EnableConsole = React.findDOMNode(this.refs.consoleEnable).checked;
- config.LogSettings.ConsoleLevel = React.findDOMNode(this.refs.consoleLevel).value;
- config.LogSettings.EnableFile = React.findDOMNode(this.refs.fileEnable).checked;
- config.LogSettings.FileLevel = React.findDOMNode(this.refs.fileLevel).value;
- config.LogSettings.FileLocation = React.findDOMNode(this.refs.fileLocation).value.trim();
- config.LogSettings.FileFormat = React.findDOMNode(this.refs.fileFormat).value.trim();
+ config.LogSettings.EnableConsole = ReactDOM.findDOMNode(this.refs.consoleEnable).checked;
+ config.LogSettings.ConsoleLevel = ReactDOM.findDOMNode(this.refs.consoleLevel).value;
+ config.LogSettings.EnableFile = ReactDOM.findDOMNode(this.refs.fileEnable).checked;
+ config.LogSettings.FileLevel = ReactDOM.findDOMNode(this.refs.fileLevel).value;
+ config.LogSettings.FileLocation = ReactDOM.findDOMNode(this.refs.fileLocation).value.trim();
+ config.LogSettings.FileFormat = ReactDOM.findDOMNode(this.refs.fileFormat).value.trim();
Client.saveConfig(
config,
@@ -249,22 +249,24 @@ export default class LogSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.fileEnable}
/>
- <p className='help-text'>
+ <div className='help-text'>
{'Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'}
<div className='help-text'>
<table
className='table table-bordered'
cellPadding='5'
>
- <tr><td className='help-text'>{'%T'}</td><td className='help-text'>{'Time (15:04:05 MST)'}</td></tr>
- <tr><td className='help-text'>{'%D'}</td><td className='help-text'>{'Date (2006/01/02)'}</td></tr>
- <tr><td className='help-text'>{'%d'}</td><td className='help-text'>{'Date (01/02/06)'}</td></tr>
- <tr><td className='help-text'>{'%L'}</td><td className='help-text'>{'Level (DEBG, INFO, EROR)'}</td></tr>
- <tr><td className='help-text'>{'%S'}</td><td className='help-text'>{'Source'}</td></tr>
- <tr><td className='help-text'>{'%M'}</td><td className='help-text'>{'Message'}</td></tr>
+ <tbody>
+ <tr><td className='help-text'>{'%T'}</td><td className='help-text'>{'Time (15:04:05 MST)'}</td></tr>
+ <tr><td className='help-text'>{'%D'}</td><td className='help-text'>{'Date (2006/01/02)'}</td></tr>
+ <tr><td className='help-text'>{'%d'}</td><td className='help-text'>{'Date (01/02/06)'}</td></tr>
+ <tr><td className='help-text'>{'%L'}</td><td className='help-text'>{'Level (DEBG, INFO, EROR)'}</td></tr>
+ <tr><td className='help-text'>{'%S'}</td><td className='help-text'>{'Source'}</td></tr>
+ <tr><td className='help-text'>{'%M'}</td><td className='help-text'>{'Message'}</td></tr>
+ </tbody>
</table>
</div>
- </p>
+ </div>
</div>
</div>
diff --git a/web/react/components/admin_console/privacy_settings.jsx b/web/react/components/admin_console/privacy_settings.jsx
index 70ec04f4a..f2d22f36e 100644
--- a/web/react/components/admin_console/privacy_settings.jsx
+++ b/web/react/components/admin_console/privacy_settings.jsx
@@ -28,8 +28,8 @@ export default class PrivacySettings extends React.Component {
$('#save-button').button('loading');
var config = this.props.config;
- config.PrivacySettings.ShowEmailAddress = React.findDOMNode(this.refs.ShowEmailAddress).checked;
- config.PrivacySettings.ShowFullName = React.findDOMNode(this.refs.ShowFullName).checked;
+ config.PrivacySettings.ShowEmailAddress = ReactDOM.findDOMNode(this.refs.ShowEmailAddress).checked;
+ config.PrivacySettings.ShowFullName = ReactDOM.findDOMNode(this.refs.ShowFullName).checked;
Client.saveConfig(
config,
diff --git a/web/react/components/admin_console/rate_settings.jsx b/web/react/components/admin_console/rate_settings.jsx
index 65c39ac42..4d71777c4 100644
--- a/web/react/components/admin_console/rate_settings.jsx
+++ b/web/react/components/admin_console/rate_settings.jsx
@@ -46,23 +46,23 @@ export default class RateSettings extends React.Component {
$('#save-button').button('loading');
var config = this.props.config;
- config.RateLimitSettings.EnableRateLimiter = React.findDOMNode(this.refs.EnableRateLimiter).checked;
- config.RateLimitSettings.VaryByRemoteAddr = React.findDOMNode(this.refs.VaryByRemoteAddr).checked;
- config.RateLimitSettings.VaryByHeader = React.findDOMNode(this.refs.VaryByHeader).value.trim();
+ config.RateLimitSettings.EnableRateLimiter = ReactDOM.findDOMNode(this.refs.EnableRateLimiter).checked;
+ config.RateLimitSettings.VaryByRemoteAddr = ReactDOM.findDOMNode(this.refs.VaryByRemoteAddr).checked;
+ config.RateLimitSettings.VaryByHeader = ReactDOM.findDOMNode(this.refs.VaryByHeader).value.trim();
var PerSec = 10;
- if (!isNaN(parseInt(React.findDOMNode(this.refs.PerSec).value, 10))) {
- PerSec = parseInt(React.findDOMNode(this.refs.PerSec).value, 10);
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.PerSec).value, 10))) {
+ PerSec = parseInt(ReactDOM.findDOMNode(this.refs.PerSec).value, 10);
}
config.RateLimitSettings.PerSec = PerSec;
- React.findDOMNode(this.refs.PerSec).value = PerSec;
+ ReactDOM.findDOMNode(this.refs.PerSec).value = PerSec;
var MemoryStoreSize = 10000;
- if (!isNaN(parseInt(React.findDOMNode(this.refs.MemoryStoreSize).value, 10))) {
- MemoryStoreSize = parseInt(React.findDOMNode(this.refs.MemoryStoreSize).value, 10);
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MemoryStoreSize).value, 10))) {
+ MemoryStoreSize = parseInt(ReactDOM.findDOMNode(this.refs.MemoryStoreSize).value, 10);
}
config.RateLimitSettings.MemoryStoreSize = MemoryStoreSize;
- React.findDOMNode(this.refs.MemoryStoreSize).value = MemoryStoreSize;
+ ReactDOM.findDOMNode(this.refs.MemoryStoreSize).value = MemoryStoreSize;
Client.saveConfig(
config,
diff --git a/web/react/components/admin_console/reset_password_modal.jsx b/web/react/components/admin_console/reset_password_modal.jsx
index 75264be45..35d3cdd17 100644
--- a/web/react/components/admin_console/reset_password_modal.jsx
+++ b/web/react/components/admin_console/reset_password_modal.jsx
@@ -18,7 +18,7 @@ export default class ResetPasswordModal extends React.Component {
doSubmit(e) {
e.preventDefault();
- var password = React.findDOMNode(this.refs.password).value;
+ var password = ReactDOM.findDOMNode(this.refs.password).value;
if (!password || password.length < 5) {
this.setState({serverError: 'Please enter at least 5 characters.'});
@@ -34,7 +34,7 @@ export default class ResetPasswordModal extends React.Component {
Client.resetPassword(data,
() => {
- this.props.onModalSubmit(React.findDOMNode(this.refs.password).value);
+ this.props.onModalSubmit(ReactDOM.findDOMNode(this.refs.password).value);
},
(err) => {
this.setState({serverError: err.message});
diff --git a/web/react/components/admin_console/select_team_modal.jsx b/web/react/components/admin_console/select_team_modal.jsx
index 21d1c25c3..22189821b 100644
--- a/web/react/components/admin_console/select_team_modal.jsx
+++ b/web/react/components/admin_console/select_team_modal.jsx
@@ -13,7 +13,7 @@ export default class SelectTeamModal extends React.Component {
doSubmit(e) {
e.preventDefault();
- this.props.onModalSubmit(React.findDOMNode(this.refs.team).value);
+ this.props.onModalSubmit(ReactDOM.findDOMNode(this.refs.team).value);
}
doCancel() {
this.props.onModalDismissed();
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx
index f29d62646..53c89a942 100644
--- a/web/react/components/admin_console/service_settings.jsx
+++ b/web/react/components/admin_console/service_settings.jsx
@@ -27,28 +27,29 @@ export default class ServiceSettings extends React.Component {
$('#save-button').button('loading');
var config = this.props.config;
- config.ServiceSettings.ListenAddress = React.findDOMNode(this.refs.ListenAddress).value.trim();
+ config.ServiceSettings.ListenAddress = ReactDOM.findDOMNode(this.refs.ListenAddress).value.trim();
if (config.ServiceSettings.ListenAddress === '') {
config.ServiceSettings.ListenAddress = ':8065';
- React.findDOMNode(this.refs.ListenAddress).value = config.ServiceSettings.ListenAddress;
+ ReactDOM.findDOMNode(this.refs.ListenAddress).value = config.ServiceSettings.ListenAddress;
}
- config.ServiceSettings.SegmentDeveloperKey = React.findDOMNode(this.refs.SegmentDeveloperKey).value.trim();
- config.ServiceSettings.GoogleDeveloperKey = React.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
- config.ServiceSettings.EnableIncomingWebhooks = React.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
- config.ServiceSettings.EnablePostUsernameOverride = React.findDOMNode(this.refs.EnablePostUsernameOverride).checked;
- config.ServiceSettings.EnablePostIconOverride = React.findDOMNode(this.refs.EnablePostIconOverride).checked;
- config.ServiceSettings.EnableTesting = React.findDOMNode(this.refs.EnableTesting).checked;
- config.ServiceSettings.EnableSecurityFixAlert = React.findDOMNode(this.refs.EnableSecurityFixAlert).checked;
+ config.ServiceSettings.SegmentDeveloperKey = ReactDOM.findDOMNode(this.refs.SegmentDeveloperKey).value.trim();
+ config.ServiceSettings.GoogleDeveloperKey = ReactDOM.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
+ config.ServiceSettings.EnableIncomingWebhooks = ReactDOM.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
+ config.ServiceSettings.EnableOutgoingWebhooks = React.findDOMNode(this.refs.EnableOutgoingWebhooks).checked;
+ config.ServiceSettings.EnablePostUsernameOverride = ReactDOM.findDOMNode(this.refs.EnablePostUsernameOverride).checked;
+ config.ServiceSettings.EnablePostIconOverride = ReactDOM.findDOMNode(this.refs.EnablePostIconOverride).checked;
+ config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked;
+ config.ServiceSettings.EnableSecurityFixAlert = ReactDOM.findDOMNode(this.refs.EnableSecurityFixAlert).checked;
- //config.ServiceSettings.EnableOAuthServiceProvider = React.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
+ //config.ServiceSettings.EnableOAuthServiceProvider = ReactDOM.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
var MaximumLoginAttempts = 10;
- if (!isNaN(parseInt(React.findDOMNode(this.refs.MaximumLoginAttempts).value, 10))) {
- MaximumLoginAttempts = parseInt(React.findDOMNode(this.refs.MaximumLoginAttempts).value, 10);
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaximumLoginAttempts).value, 10))) {
+ MaximumLoginAttempts = parseInt(ReactDOM.findDOMNode(this.refs.MaximumLoginAttempts).value, 10);
}
config.ServiceSettings.MaximumLoginAttempts = MaximumLoginAttempts;
- React.findDOMNode(this.refs.MaximumLoginAttempts).value = MaximumLoginAttempts;
+ ReactDOM.findDOMNode(this.refs.MaximumLoginAttempts).value = MaximumLoginAttempts;
Client.saveConfig(
config,
@@ -207,7 +208,40 @@ export default class ServiceSettings extends React.Component {
</div>
</div>
- <div className='form-group'>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableOutgoingWebhooks'
+ >
+ {'Enable Outgoing Webhooks: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableOutgoingWebhooks'
+ value='true'
+ ref='EnableOutgoingWebhooks'
+ defaultChecked={this.props.config.ServiceSettings.EnableOutgoingWebhooks}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableOutgoingWebhooks'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableOutgoingWebhooks}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, outgoing webhooks will be allowed.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='EnablePostUsernameOverride'
diff --git a/web/react/components/admin_console/sql_settings.jsx b/web/react/components/admin_console/sql_settings.jsx
index 16a69e664..b43108bf7 100644
--- a/web/react/components/admin_console/sql_settings.jsx
+++ b/web/react/components/admin_console/sql_settings.jsx
@@ -29,27 +29,27 @@ export default class SqlSettings extends React.Component {
$('#save-button').button('loading');
var config = this.props.config;
- config.SqlSettings.Trace = React.findDOMNode(this.refs.Trace).checked;
- config.SqlSettings.AtRestEncryptKey = React.findDOMNode(this.refs.AtRestEncryptKey).value.trim();
+ config.SqlSettings.Trace = ReactDOM.findDOMNode(this.refs.Trace).checked;
+ config.SqlSettings.AtRestEncryptKey = ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value.trim();
if (config.SqlSettings.AtRestEncryptKey === '') {
config.SqlSettings.AtRestEncryptKey = crypto.randomBytes(256).toString('base64').substring(0, 32);
- React.findDOMNode(this.refs.AtRestEncryptKey).value = config.SqlSettings.AtRestEncryptKey;
+ ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value = config.SqlSettings.AtRestEncryptKey;
}
var MaxOpenConns = 10;
- if (!isNaN(parseInt(React.findDOMNode(this.refs.MaxOpenConns).value, 10))) {
- MaxOpenConns = parseInt(React.findDOMNode(this.refs.MaxOpenConns).value, 10);
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxOpenConns).value, 10))) {
+ MaxOpenConns = parseInt(ReactDOM.findDOMNode(this.refs.MaxOpenConns).value, 10);
}
config.SqlSettings.MaxOpenConns = MaxOpenConns;
- React.findDOMNode(this.refs.MaxOpenConns).value = MaxOpenConns;
+ ReactDOM.findDOMNode(this.refs.MaxOpenConns).value = MaxOpenConns;
var MaxIdleConns = 10;
- if (!isNaN(parseInt(React.findDOMNode(this.refs.MaxIdleConns).value, 10))) {
- MaxIdleConns = parseInt(React.findDOMNode(this.refs.MaxIdleConns).value, 10);
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxIdleConns).value, 10))) {
+ MaxIdleConns = parseInt(ReactDOM.findDOMNode(this.refs.MaxIdleConns).value, 10);
}
config.SqlSettings.MaxIdleConns = MaxIdleConns;
- React.findDOMNode(this.refs.MaxIdleConns).value = MaxIdleConns;
+ ReactDOM.findDOMNode(this.refs.MaxIdleConns).value = MaxIdleConns;
Client.saveConfig(
config,
@@ -79,7 +79,7 @@ export default class SqlSettings extends React.Component {
return;
}
- React.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s);
}
@@ -220,7 +220,7 @@ export default class SqlSettings extends React.Component {
<p className='help-text'>{'32-character salt available to encrypt and decrypt sensitive fields in database.'}</p>
<div className='help-text'>
<button
- className='help-link'
+ className='btn btn-default'
onClick={this.handleGenerate}
>
{'Re-Generate'}
diff --git a/web/react/components/admin_console/team_settings.jsx b/web/react/components/admin_console/team_settings.jsx
index a517c56fe..da4299714 100644
--- a/web/react/components/admin_console/team_settings.jsx
+++ b/web/react/components/admin_console/team_settings.jsx
@@ -27,17 +27,17 @@ export default class TeamSettings extends React.Component {
$('#save-button').button('loading');
var config = this.props.config;
- config.TeamSettings.SiteName = React.findDOMNode(this.refs.SiteName).value.trim();
- config.TeamSettings.RestrictCreationToDomains = React.findDOMNode(this.refs.RestrictCreationToDomains).value.trim();
- config.TeamSettings.EnableTeamCreation = React.findDOMNode(this.refs.EnableTeamCreation).checked;
- config.TeamSettings.EnableUserCreation = React.findDOMNode(this.refs.EnableUserCreation).checked;
+ config.TeamSettings.SiteName = ReactDOM.findDOMNode(this.refs.SiteName).value.trim();
+ config.TeamSettings.RestrictCreationToDomains = ReactDOM.findDOMNode(this.refs.RestrictCreationToDomains).value.trim();
+ config.TeamSettings.EnableTeamCreation = ReactDOM.findDOMNode(this.refs.EnableTeamCreation).checked;
+ config.TeamSettings.EnableUserCreation = ReactDOM.findDOMNode(this.refs.EnableUserCreation).checked;
var MaxUsersPerTeam = 50;
- if (!isNaN(parseInt(React.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) {
- MaxUsersPerTeam = parseInt(React.findDOMNode(this.refs.MaxUsersPerTeam).value, 10);
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) {
+ MaxUsersPerTeam = parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10);
}
config.TeamSettings.MaxUsersPerTeam = MaxUsersPerTeam;
- React.findDOMNode(this.refs.MaxUsersPerTeam).value = MaxUsersPerTeam;
+ ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value = MaxUsersPerTeam;
Client.saveConfig(
config,
diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx
index 665ccd719..395e22e6c 100644
--- a/web/react/components/admin_console/user_item.jsx
+++ b/web/react/components/admin_console/user_item.jsx
@@ -111,8 +111,10 @@ export default class UserItem extends React.Component {
const user = this.props.user;
let currentRoles = 'Member';
if (user.roles.length > 0) {
- if (user.roles.indexOf('system_admin') > -1) {
+ if (Utils.isSystemAdmin(user.roles)) {
currentRoles = 'System Admin';
+ } else if (Utils.isAdmin(user.roles)) {
+ currentRoles = 'Team Admin';
} else {
currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1);
}
@@ -158,7 +160,7 @@ export default class UserItem extends React.Component {
href='#'
onClick={this.handleMakeAdmin}
>
- {'Make Admin'}
+ {'Make Team Admin'}
</a>
</li>
);
diff --git a/web/react/components/change_url_modal.jsx b/web/react/components/change_url_modal.jsx
index f8db13392..714e93ff8 100644
--- a/web/react/components/change_url_modal.jsx
+++ b/web/react/components/change_url_modal.jsx
@@ -29,7 +29,7 @@ export default class ChangeUrlModal extends React.Component {
}
componentDidUpdate(prevProps) {
if (this.props.show === true && prevProps.show === false) {
- React.findDOMNode(this.refs.urlinput).select();
+ ReactDOM.findDOMNode(this.refs.urlinput).select();
}
}
onURLChanged(e) {
@@ -60,7 +60,7 @@ export default class ChangeUrlModal extends React.Component {
doSubmit(e) {
e.preventDefault();
- const url = React.findDOMNode(this.refs.urlinput).value;
+ const url = ReactDOM.findDOMNode(this.refs.urlinput).value;
const cleanedURL = Utils.cleanUpUrlable(url);
if (cleanedURL !== url || url.length < 2 || url.indexOf('__') > -1) {
this.setState({urlError: this.getURLError(url)});
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index e47db073d..1b709336f 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -4,7 +4,6 @@
const ChannelStore = require('../stores/channel_store.jsx');
const UserStore = require('../stores/user_store.jsx');
const PostStore = require('../stores/post_store.jsx');
-const SocketStore = require('../stores/socket_store.jsx');
const NavbarSearchBox = require('./search_bar.jsx');
const AsyncClient = require('../utils/async_client.jsx');
const Client = require('../utils/client.jsx');
@@ -17,12 +16,14 @@ const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
const Constants = require('../utils/constants.jsx');
const ActionTypes = Constants.ActionTypes;
+const Popover = ReactBootstrap.Popover;
+const OverlayTrigger = ReactBootstrap.OverlayTrigger;
+
export default class ChannelHeader extends React.Component {
constructor(props) {
super(props);
this.onListenerChange = this.onListenerChange.bind(this);
- this.onSocketChange = this.onSocketChange.bind(this);
this.handleLeave = this.handleLeave.bind(this);
this.searchMentions = this.searchMentions.bind(this);
@@ -42,7 +43,6 @@ export default class ChannelHeader extends React.Component {
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
PostStore.addSearchChangeListener(this.onListenerChange);
UserStore.addChangeListener(this.onListenerChange);
- SocketStore.addChangeListener(this.onSocketChange);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
@@ -57,16 +57,9 @@ export default class ChannelHeader extends React.Component {
}
$('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}});
}
- onSocketChange(msg) {
- if (msg.action === 'new_user' ||
- msg.action === 'user_added' ||
- (msg.action === 'user_removed' && msg.user_id !== UserStore.getCurrentId())) {
- AsyncClient.getChannelExtraInfo(true);
- }
- }
handleLeave() {
Client.leaveChannel(this.state.channel.id,
- function handleLeaveSuccess() {
+ () => {
AppDispatcher.handleViewAction({
type: ActionTypes.LEAVE_CHANNEL,
id: this.state.channel.id
@@ -74,8 +67,8 @@ export default class ChannelHeader extends React.Component {
const townsquare = ChannelStore.getByName('town-square');
Utils.switchChannel(townsquare);
- }.bind(this),
- function handleLeaveError(err) {
+ },
+ (err) => {
AsyncClient.dispatchError(err, 'handleLeave');
}
);
@@ -110,7 +103,21 @@ export default class ChannelHeader extends React.Component {
}
const channel = this.state.channel;
- const popoverContent = React.renderToString(<MessageWrapper message={channel.description}/>);
+ const popoverContent = (
+ <Popover
+ id='hader-popover'
+ bStyle='info'
+ bSize='large'
+ placement='bottom'
+ className='description'
+ onMouseOver={() => this.refs.descriptionOverlay.show()}
+ onMouseOut={() => this.refs.descriptionOverlay.hide()}
+ >
+ <MessageWrapper
+ message={channel.description}
+ />
+ </Popover>
+ );
let channelTitle = channel.display_name;
const currentId = UserStore.getCurrentId();
const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.state.memberTeam.roles);
@@ -301,75 +308,82 @@ export default class ChannelHeader extends React.Component {
return (
<table className='channel-header alt'>
- <tr>
- <th>
- <div className='channel-header__info'>
- <div className='dropdown'>
+ <tbody>
+ <tr>
+ <th>
+ <div className='channel-header__info'>
+ <div className='dropdown'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ id='channel_header_dropdown'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <strong className='heading'>{channelTitle} </strong>
+ <span className='glyphicon glyphicon-chevron-down header-dropdown__icon' />
+ </a>
+ <ul
+ className='dropdown-menu'
+ role='menu'
+ aria-labelledby='channel_header_dropdown'
+ >
+ {dropdownContents}
+ </ul>
+ </div>
+ <OverlayTrigger
+ trigger={['hover', 'focus']}
+ placement='bottom'
+ overlay={popoverContent}
+ ref='descriptionOverlay'
+ >
+ <div
+ onClick={TextFormatting.handleClick}
+ className='description'
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.description, {singleline: true, mentionHighlight: false})}}
+ />
+ </OverlayTrigger>
+ </div>
+ </th>
+ <th>
+ <PopoverListMembers
+ members={this.state.users}
+ channelId={channel.id}
+ />
+ </th>
+ <th className='search-bar__container'><NavbarSearchBox /></th>
+ <th>
+ <div className='dropdown channel-header__links'>
<a
href='#'
className='dropdown-toggle theme'
type='button'
- id='channel_header_dropdown'
+ id='channel_header_right_dropdown'
data-toggle='dropdown'
aria-expanded='true'
>
- <strong className='heading'>{channelTitle} </strong>
- <span className='glyphicon glyphicon-chevron-down header-dropdown__icon' />
+ <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} />
</a>
<ul
- className='dropdown-menu'
+ className='dropdown-menu dropdown-menu-right'
role='menu'
- aria-labelledby='channel_header_dropdown'
+ aria-labelledby='channel_header_right_dropdown'
>
- {dropdownContents}
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.searchMentions}
+ >
+ Recent Mentions
+ </a>
+ </li>
</ul>
</div>
- <div
- data-toggle='popover'
- data-content={popoverContent}
- className='description'
- onClick={TextFormatting.handleClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.description, {singleline: true, mentionHighlight: false})}}
- />
- </div>
- </th>
- <th>
- <PopoverListMembers
- members={this.state.users}
- channelId={channel.id}
- />
- </th>
- <th className='search-bar__container'><NavbarSearchBox /></th>
- <th>
- <div className='dropdown channel-header__links'>
- <a
- href='#'
- className='dropdown-toggle theme'
- type='button'
- id='channel_header_right_dropdown'
- data-toggle='dropdown'
- aria-expanded='true'
- >
- <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} />
- </a>
- <ul
- className='dropdown-menu dropdown-menu-right'
- role='menu'
- aria-labelledby='channel_header_right_dropdown'
- >
- <li role='presentation'>
- <a
- role='menuitem'
- href='#'
- onClick={this.searchMentions}
- >
- Recent Mentions
- </a>
- </li>
- </ul>
- </div>
- </th>
- </tr>
+ </th>
+ </tr>
+ </tbody>
</table>
);
}
diff --git a/web/react/components/channel_info_modal.jsx b/web/react/components/channel_info_modal.jsx
index d6de958e7..bccd8d0b9 100644
--- a/web/react/components/channel_info_modal.jsx
+++ b/web/react/components/channel_info_modal.jsx
@@ -15,7 +15,7 @@ export default class CommandList extends React.Component {
componentDidMount() {
var self = this;
if (this.refs.modal) {
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function show(e) {
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', function show(e) {
var button = e.relatedTarget;
self.setState({channel_id: $(button).attr('data-channelid')});
});
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index 1e621c683..7c7770095 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -56,8 +56,8 @@ export default class ChannelInviteModal extends React.Component {
};
}
componentDidMount() {
- $(React.findDOMNode(this)).on('hidden.bs.modal', this.onHide);
- $(React.findDOMNode(this)).on('show.bs.modal', this.onShow);
+ $(ReactDOM.findDOMNode(this)).on('hidden.bs.modal', this.onHide);
+ $(ReactDOM.findDOMNode(this)).on('show.bs.modal', this.onShow);
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
ChannelStore.addChangeListener(this.onListenerChange);
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index d16069725..270631db2 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -75,15 +75,6 @@ export default class ChannelLoader extends React.Component {
Utils.applyTheme(Constants.THEMES.default);
}
- /* Setup global mouse events */
- $('body').on('click', function hidePopover(e) {
- $('[data-toggle="popover"]').each(function eachPopover() {
- if (!$(this).is(e.target) && $(this).has(e.target).length === 0 && $('.popover').has(e.target).length === 0) {
- $(this).popover('hide');
- }
- });
- });
-
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
@@ -104,12 +95,6 @@ export default class ChannelLoader extends React.Component {
}
});
- /* Setup modal events */
- $('.modal').on('show.bs.modal', function onShow() {
- $('.modal-body').css('overflow-y', 'auto');
- $('.modal-body').css('max-height', $(window).height() * 0.7);
- });
-
/* Prevent backspace from navigating back a page */
$(window).on('keydown.preventBackspace', (e) => {
if (e.which === 8 && !$(e.target).is('input, textarea')) {
diff --git a/web/react/components/channel_members.jsx b/web/react/components/channel_members.jsx
index 0cd384977..86cc2464d 100644
--- a/web/react/components/channel_members.jsx
+++ b/web/react/components/channel_members.jsx
@@ -74,9 +74,9 @@ export default class ChannelMembers extends React.Component {
componentDidMount() {
ChannelStore.addExtraInfoChangeListener(this.onChange);
ChannelStore.addChangeListener(this.onChange);
- $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.onHide);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.onHide);
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
}
componentWillUnmount() {
ChannelStore.removeExtraInfoChangeListener(this.onChange);
diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx
index 2114be905..6151d4bdd 100644
--- a/web/react/components/channel_notifications.jsx
+++ b/web/react/components/channel_notifications.jsx
@@ -51,7 +51,7 @@ export default class ChannelNotifications extends React.Component {
componentDidMount() {
ChannelStore.addChangeListener(this.onListenerChange);
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
@@ -104,7 +104,7 @@ export default class ChannelNotifications extends React.Component {
}
handleUpdateNotifyLevel(notifyLevel) {
this.setState({notifyLevel});
- React.findDOMNode(this.refs.modal).focus();
+ ReactDOM.findDOMNode(this.refs.modal).focus();
}
createNotifyLevelSection(serverError) {
var handleUpdateSection;
@@ -266,7 +266,7 @@ export default class ChannelNotifications extends React.Component {
handleUpdateMarkUnreadLevel(markUnreadLevel) {
this.setState({markUnreadLevel});
- React.findDOMNode(this.refs.modal).focus();
+ ReactDOM.findDOMNode(this.refs.modal).focus();
}
createMarkUnreadLevelSection(serverError) {
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index add4125d7..12d1af6ff 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -32,6 +32,7 @@ export default class CreateComment extends React.Component {
this.removePreview = this.removePreview.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.getFileCount = this.getFileCount.bind(this);
+ this.handleResize = this.handleResize.bind(this);
PostStore.clearCommentDraftUploads();
@@ -40,13 +41,25 @@ export default class CreateComment extends React.Component {
messageText: draft.message,
uploadsInProgress: draft.uploadsInProgress,
previews: draft.previews,
- submitting: false
+ submitting: false,
+ windowWidth: Utils.windowWidth()
};
}
+ componentDidMount() {
+ window.addEventListener('resize', this.handleResize);
+ }
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.handleResize);
+ }
+ handleResize() {
+ this.setState({windowWidth: Utils.windowWidth()});
+ }
componentDidUpdate(prevProps, prevState) {
if (prevState.uploadsInProgress < this.state.uploadsInProgress) {
$('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight);
- $('.post-right__scroll').perfectScrollbar('update');
+ if (this.state.windowWidth > 768) {
+ $('.post-right__scroll').perfectScrollbar('update');
+ }
}
}
handleSubmit(e) {
@@ -126,7 +139,7 @@ export default class CreateComment extends React.Component {
commentMsgKeyPress(e) {
if (e.which === 13 && !e.shiftKey && !e.altKey) {
e.preventDefault();
- React.findDOMNode(this.refs.textbox).blur();
+ ReactDOM.findDOMNode(this.refs.textbox).blur();
this.handleSubmit(e);
}
@@ -189,7 +202,7 @@ export default class CreateComment extends React.Component {
handleTextDrop(text) {
const newText = this.state.messageText + text;
this.handleUserInput(newText);
- Utils.setCaretPosition(React.findDOMNode(this.refs.textbox.refs.message), newText.length);
+ Utils.setCaretPosition(ReactDOM.findDOMNode(this.refs.textbox.refs.message), newText.length);
}
removePreview(id) {
let previews = this.state.previews;
@@ -255,6 +268,17 @@ export default class CreateComment extends React.Component {
postFooterClassName += ' has-error';
}
+ let uploadsInProgressText = null;
+ if (this.state.uploadsInProgress.length > 0) {
+ uploadsInProgressText = (
+ <span
+ className='pull-right post-right-comments-upload-in-progress'
+ >
+ {this.state.uploadsInProgress.length === 1 ? 'File uploading' : 'Files uploading'}
+ </span>
+ );
+ }
+
return (
<form onSubmit={this.handleSubmit}>
<div className='post-create'>
@@ -262,25 +286,27 @@ export default class CreateComment extends React.Component {
id={this.props.rootId}
className='post-create-body comment-create-body'
>
- <Textbox
- onUserInput={this.handleUserInput}
- onKeyPress={this.commentMsgKeyPress}
- messageText={this.state.messageText}
- createMessage='Add a comment...'
- initialText=''
- id='reply_textbox'
- ref='textbox'
- />
- <FileUpload
- ref='fileUpload'
- getFileCount={this.getFileCount}
- onUploadStart={this.handleUploadStart}
- onFileUpload={this.handleFileUploadComplete}
- onUploadError={this.handleUploadError}
- onTextDrop={this.handleTextDrop}
- postType='comment'
- channelId={this.props.channelId}
- />
+ <div className='post-body__cell'>
+ <Textbox
+ onUserInput={this.handleUserInput}
+ onKeyPress={this.commentMsgKeyPress}
+ messageText={this.state.messageText}
+ createMessage='Add a comment...'
+ initialText=''
+ id='reply_textbox'
+ ref='textbox'
+ />
+ <FileUpload
+ ref='fileUpload'
+ getFileCount={this.getFileCount}
+ onUploadStart={this.handleUploadStart}
+ onFileUpload={this.handleFileUploadComplete}
+ onUploadError={this.handleUploadError}
+ onTextDrop={this.handleTextDrop}
+ postType='comment'
+ channelId={this.props.channelId}
+ />
+ </div>
</div>
<MsgTyping
channelId={this.props.channelId}
@@ -293,6 +319,7 @@ export default class CreateComment extends React.Component {
value='Add Comment'
onClick={this.handleSubmit}
/>
+ {uploadsInProgressText}
{postError}
{serverError}
</div>
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index ed265ab02..035899592 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -16,6 +16,7 @@ const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
const ActionTypes = Constants.ActionTypes;
+const KeyCodes = Constants.KeyCodes;
export default class CreatePost extends React.Component {
constructor(props) {
@@ -35,6 +36,8 @@ export default class CreatePost extends React.Component {
this.removePreview = this.removePreview.bind(this);
this.onChange = this.onChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
+ this.handleArrowUp = this.handleArrowUp.bind(this);
+ this.handleResize = this.handleResize.bind(this);
PostStore.clearDraftUploads();
@@ -46,9 +49,17 @@ export default class CreatePost extends React.Component {
uploadsInProgress: draft.uploadsInProgress,
previews: draft.previews,
submitting: false,
- initialText: draft.messageText
+ initialText: draft.messageText,
+ windowWidth: Utils.windowWidth(),
+ windowHeigth: Utils.windowHeight()
};
}
+ handleResize() {
+ this.setState({
+ windowWidth: Utils.windowWidth(),
+ windowHeight: Utils.windowHeight()
+ });
+ }
componentDidUpdate(prevProps, prevState) {
if (prevState.previews.length !== this.state.previews.length) {
this.resizePostHolder();
@@ -59,6 +70,11 @@ export default class CreatePost extends React.Component {
this.resizePostHolder();
return;
}
+
+ if (prevState.windowWidth !== this.state.windowWidth || prevState.windowHeight !== this.state.windowHeigth) {
+ this.resizePostHolder();
+ return;
+ }
}
getCurrentDraft() {
const draft = PostStore.getCurrentDraft();
@@ -172,9 +188,9 @@ export default class CreatePost extends React.Component {
}
}
postMsgKeyPress(e) {
- if (e.which === 13 && !e.shiftKey && !e.altKey) {
+ if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
e.preventDefault();
- React.findDOMNode(this.refs.textbox).blur();
+ ReactDOM.findDOMNode(this.refs.textbox).blur();
this.handleSubmit(e);
}
@@ -192,9 +208,11 @@ export default class CreatePost extends React.Component {
PostStore.storeCurrentDraft(draft);
}
resizePostHolder() {
- const height = $(window).height() - $(React.findDOMNode(this.refs.topDiv)).height() - $('#error_bar').outerHeight() - 50;
+ const height = this.state.windowHeigth - $(ReactDOM.findDOMNode(this.refs.topDiv)).height() - 50;
$('.post-list-holder-by-time').css('height', `${height}px`);
- $(window).trigger('resize');
+ if (this.state.windowWidth > 960) {
+ $('#post_textbox').focus();
+ }
}
handleUploadStart(clientIds, channelId) {
const draft = PostStore.getDraft(channelId);
@@ -240,7 +258,7 @@ export default class CreatePost extends React.Component {
handleTextDrop(text) {
const newText = this.state.messageText + text;
this.handleUserInput(newText);
- Utils.setCaretPosition(React.findDOMNode(this.refs.textbox.refs.message), newText.length);
+ Utils.setCaretPosition(ReactDOM.findDOMNode(this.refs.textbox.refs.message), newText.length);
}
removePreview(id) {
const previews = Object.assign([], this.state.previews);
@@ -269,9 +287,11 @@ export default class CreatePost extends React.Component {
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
this.resizePostHolder();
+ window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChange);
+ window.removeEventListener('resize', this.handleResize);
}
onChange() {
const channelId = ChannelStore.getCurrentId();
@@ -289,6 +309,27 @@ export default class CreatePost extends React.Component {
const draft = PostStore.getDraft(channelId);
return draft.previews.length + draft.uploadsInProgress.length;
}
+ handleArrowUp(e) {
+ if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
+ e.preventDefault();
+
+ const channelId = ChannelStore.getCurrentId();
+ const lastPost = PostStore.getCurrentUsersLatestPost(channelId);
+ if (!lastPost) {
+ return;
+ }
+ var type = (lastPost.root_id && lastPost.root_id.length > 0) ? 'Comment' : 'Post';
+
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.RECIEVED_EDIT_POST,
+ refocusId: '#post_textbox',
+ title: type,
+ message: lastPost.message,
+ postId: lastPost.id,
+ channelId: lastPost.channel_id
+ });
+ }
+ }
render() {
let serverError = null;
if (this.state.serverError) {
@@ -333,6 +374,7 @@ export default class CreatePost extends React.Component {
<Textbox
onUserInput={this.handleUserInput}
onKeyPress={this.postMsgKeyPress}
+ onKeyDown={this.handleArrowUp}
onHeightChange={this.resizePostHolder}
messageText={this.state.messageText}
createMessage='Write a message...'
diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx
index 9358c98d7..b7d633b38 100644
--- a/web/react/components/delete_channel_modal.jsx
+++ b/web/react/components/delete_channel_modal.jsx
@@ -41,7 +41,7 @@ export default class DeleteChannelModal extends React.Component {
});
}
componentDidMount() {
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
}
render() {
const channel = ChannelStore.getCurrent();
diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx
index ea7d76b1e..3a3dabce5 100644
--- a/web/react/components/delete_post_modal.jsx
+++ b/web/react/components/delete_post_modal.jsx
@@ -73,7 +73,7 @@ export default class DeletePostModal extends React.Component {
this.setState(newState);
}
componentDidMount() {
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
PostStore.addSelectedPostChangeListener(this.onListenerChange);
}
componentWillUnmount() {
diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx
index 6ccf12be6..d63a1db30 100644
--- a/web/react/components/edit_channel_modal.jsx
+++ b/web/react/components/edit_channel_modal.jsx
@@ -34,7 +34,7 @@ export default class EditChannelModal extends React.Component {
function handleUpdateSuccess() {
this.setState({serverError: ''});
AsyncClient.getChannel(this.state.channelId);
- $(React.findDOMNode(this.refs.modal)).modal('hide');
+ $(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
}.bind(this),
function handleUpdateError(err) {
if (err.message === 'Invalid channel_description parameter') {
@@ -56,11 +56,11 @@ export default class EditChannelModal extends React.Component {
this.setState({description: $(button).attr('data-desc'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''});
}
componentDidMount() {
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
- $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose);
}
componentWillUnmount() {
- $(React.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose);
+ $(ReactDOM.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose);
}
render() {
var serverError = null;
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index 165a28a64..b259b3c18 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -5,6 +5,7 @@ var Client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var Textbox = require('./textbox.jsx');
var BrowserStore = require('../stores/browser_store.jsx');
+var PostStore = require('../stores/post_store.jsx');
export default class EditPostModal extends React.Component {
constructor() {
@@ -14,6 +15,7 @@ export default class EditPostModal extends React.Component {
this.handleEditInput = this.handleEditInput.bind(this);
this.handleEditKeyPress = this.handleEditKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
+ this.handleEditPostEvent = this.handleEditPostEvent.bind(this);
this.state = {editText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: ''};
}
@@ -35,16 +37,15 @@ export default class EditPostModal extends React.Component {
Client.updatePost(updatedPost,
function success() {
- AsyncClient.getPosts(this.state.channel_id);
+ AsyncClient.getPosts(updatedPost.channel_id);
window.scrollTo(0, 0);
- }.bind(this),
+ },
function error(err) {
AsyncClient.dispatchError(err, 'updatePost');
}
);
$('#edit_post').modal('hide');
- $(this.state.refocusId).focus();
}
handleEditInput(editMessage) {
this.setState({editText: editMessage});
@@ -52,28 +53,57 @@ export default class EditPostModal extends React.Component {
handleEditKeyPress(e) {
if (e.which === 13 && !e.shiftKey && !e.altKey) {
e.preventDefault();
- React.findDOMNode(this.refs.editbox).blur();
+ ReactDOM.findDOMNode(this.refs.editbox).blur();
this.handleEdit(e);
}
}
handleUserInput(e) {
this.setState({editText: e.target.value});
}
+ handleEditPostEvent(options) {
+ this.setState({
+ editText: options.message || '',
+ title: options.title || '',
+ post_id: options.postId || '',
+ channel_id: options.channelId || '',
+ comments: options.comments || 0,
+ refocusId: options.refocusId || ''
+ });
+
+ $(ReactDOM.findDOMNode(this.refs.modal)).modal('show');
+ }
componentDidMount() {
var self = this;
- $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function onHidden() {
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function onHidden() {
self.setState({editText: '', title: '', channel_id: '', post_id: '', comments: 0, refocusId: '', error: ''});
});
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function onShow(e) {
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', function onShow(e) {
var button = e.relatedTarget;
+ if (!button) {
+ return;
+ }
self.setState({editText: $(button).attr('data-message'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments'), refocusId: $(button).attr('data-refoucsid')});
});
- $(React.findDOMNode(this.refs.modal)).on('shown.bs.modal', function onShown() {
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', function onShown() {
self.refs.editbox.resize();
+ $('#edit_textbox').get(0).focus();
});
+
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('hide.bs.modal', function onShown() {
+ if (self.state.refocusId !== '') {
+ setTimeout(() => {
+ $(self.state.refocusId).get(0).focus();
+ });
+ }
+ });
+
+ PostStore.addEditPostListener(this.handleEditPostEvent);
+ }
+ componentWillUnmount() {
+ PostStore.removeEditPostListener(this.handleEditPostEvent);
}
render() {
var error = (<div className='form-group'><br /></div>);
diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx
index 9be7f97f8..940b01f8d 100644
--- a/web/react/components/email_verify.jsx
+++ b/web/react/components/email_verify.jsx
@@ -39,11 +39,9 @@ export default class EmailVerify extends React.Component {
return (
<div className='col-sm-12'>
- <div className='panel panel-default verify_panel'>
- <div className='panel-heading'>
- <h3 className='panel-title'>{title}</h3>
- </div>
- <div className='panel-body'>
+ <div className='signup-team__container'>
+ <h3>{title}</h3>
+ <div>
{body}
{resend}
{resendConfirm}
diff --git a/web/react/components/error_bar.jsx b/web/react/components/error_bar.jsx
index b100fd337..6311d9460 100644
--- a/web/react/components/error_bar.jsx
+++ b/web/react/components/error_bar.jsx
@@ -9,7 +9,6 @@ export default class ErrorBar extends React.Component {
this.onErrorChange = this.onErrorChange.bind(this);
this.handleClose = this.handleClose.bind(this);
- this.resize = this.resize.bind(this);
this.prevTimer = null;
this.state = ErrorStore.getLastError();
@@ -46,34 +45,14 @@ export default class ErrorBar extends React.Component {
return false;
}
- resize() {
- if (this.isValidError(this.state)) {
- var height = $(React.findDOMNode(this)).outerHeight();
- height = height < 30 ? 30 : height;
- $('body').css('padding-top', height + 'px');
- } else {
- $('body').css('padding-top', '0');
- }
- }
-
componentDidMount() {
ErrorStore.addChangeListener(this.onErrorChange);
-
- $(window).resize(() => {
- this.resize();
- });
-
- this.resize();
}
componentWillUnmount() {
ErrorStore.removeChangeListener(this.onErrorChange);
}
- componentDidUpdate() {
- this.resize();
- }
-
onErrorChange() {
var newState = ErrorStore.getLastError();
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index 659da4f5e..c6dff6550 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -43,7 +43,7 @@ export default class FileAttachment extends React.Component {
return function loader() {
$(this).remove();
if (name in self.refs) {
- var imgDiv = React.findDOMNode(self.refs[name]);
+ var imgDiv = ReactDOM.findDOMNode(self.refs[name]);
$(imgDiv).removeClass('post__load');
$(imgDiv).addClass('post__image');
@@ -82,7 +82,7 @@ export default class FileAttachment extends React.Component {
if (nextState.fileSize !== this.state.fileSize) {
if (this.refs.fileSize) {
// update the UI element to display the file size without re-rendering the whole component
- React.findDOMNode(this.refs.fileSize).innerHTML = utils.fileSizeToString(nextState.fileSize);
+ ReactDOM.findDOMNode(this.refs.fileSize).innerHTML = utils.fileSizeToString(nextState.fileSize);
return false;
}
diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx
index e947fc50c..8854a54df 100644
--- a/web/react/components/file_upload.jsx
+++ b/web/react/components/file_upload.jsx
@@ -86,7 +86,7 @@ export default class FileUpload extends React.Component {
}
handleChange() {
- var element = $(React.findDOMNode(this.refs.fileInput));
+ var element = $(ReactDOM.findDOMNode(this.refs.fileInput));
this.uploadFiles(element.prop('files'));
@@ -115,7 +115,7 @@ export default class FileUpload extends React.Component {
}
componentDidMount() {
- var inputDiv = React.findDOMNode(this.refs.input);
+ var inputDiv = ReactDOM.findDOMNode(this.refs.input);
var self = this;
if (this.props.postType === 'post') {
diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx
index 59c4e08e3..e324f3666 100644
--- a/web/react/components/find_team.jsx
+++ b/web/react/components/find_team.jsx
@@ -17,7 +17,7 @@ export default class FindTeam extends React.Component {
var state = { };
- var email = React.findDOMNode(this.refs.email).value.trim().toLowerCase();
+ var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase();
if (!email || !utils.isEmail(email)) {
state.email_error = 'Please enter a valid email address';
this.setState(state);
@@ -70,6 +70,7 @@ export default class FindTeam extends React.Component {
className='form-control'
placeholder='you@domain.com'
maxLength='128'
+ spellCheck='false'
/>
{emailError}
</div>
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index 234013b93..325e86f3d 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -22,12 +22,12 @@ export default class GetLinkModal extends React.Component {
}
componentDidMount() {
if (this.refs.modal) {
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
- $(React.findDOMNode(this.refs.modal)).on('hide.bs.modal', this.onHide);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('hide.bs.modal', this.onHide);
}
}
handleClick() {
- var copyTextarea = $(React.findDOMNode(this.refs.textarea));
+ var copyTextarea = $(ReactDOM.findDOMNode(this.refs.textarea));
copyTextarea.select();
try {
@@ -96,7 +96,6 @@ export default class GetLinkModal extends React.Component {
<p>
Send teammates the link below for them to sign-up to this team site.
<br /><br />
- Be careful not to share this link publicly, since anyone with the link can join your team.
</p>
<textarea
className='form-control no-resize'
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 2ca39d1b1..90290099d 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -36,7 +36,7 @@ export default class InviteMemberModal extends React.Component {
var notEmpty = false;
for (var i = 0; i < self.state.inviteIds.length; i++) {
var index = self.state.inviteIds[i];
- if (React.findDOMNode(self.refs['email' + index]).value.trim() !== '') {
+ if (ReactDOM.findDOMNode(self.refs['email' + index]).value.trim() !== '') {
notEmpty = true;
break;
}
@@ -69,7 +69,7 @@ export default class InviteMemberModal extends React.Component {
for (var i = 0; i < count; i++) {
var index = inviteIds[i];
var invite = {};
- invite.email = React.findDOMNode(this.refs['email' + index]).value.trim();
+ invite.email = ReactDOM.findDOMNode(this.refs['email' + index]).value.trim();
if (!invite.email || !utils.isEmail(invite.email)) {
emailErrors[index] = 'Please enter a valid email address';
valid = false;
@@ -77,9 +77,9 @@ export default class InviteMemberModal extends React.Component {
emailErrors[index] = '';
}
- invite.firstName = React.findDOMNode(this.refs['first_name' + index]).value.trim();
+ invite.firstName = ReactDOM.findDOMNode(this.refs['first_name' + index]).value.trim();
- invite.lastName = React.findDOMNode(this.refs['last_name' + index]).value.trim();
+ invite.lastName = ReactDOM.findDOMNode(this.refs['last_name' + index]).value.trim();
invites.push(invite);
}
@@ -95,8 +95,8 @@ export default class InviteMemberModal extends React.Component {
Client.inviteMembers(data,
function success() {
- $(React.findDOMNode(this.refs.modal)).attr('data-confirm', 'true');
- $(React.findDOMNode(this.refs.modal)).modal('hide');
+ $(ReactDOM.findDOMNode(this.refs.modal)).attr('data-confirm', 'true');
+ $(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
}.bind(this),
function fail(err) {
if (err.message === 'This person is already on your team') {
@@ -110,8 +110,8 @@ export default class InviteMemberModal extends React.Component {
}
componentDidUpdate() {
- $(React.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
- $(React.findDOMNode(this.refs.modalBody)).css('overflow-y', 'scroll');
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('overflow-y', 'scroll');
}
addInviteFields() {
@@ -126,9 +126,9 @@ export default class InviteMemberModal extends React.Component {
for (var i = 0; i < inviteIds.length; i++) {
var index = inviteIds[i];
- React.findDOMNode(this.refs['email' + index]).value = '';
- React.findDOMNode(this.refs['first_name' + index]).value = '';
- React.findDOMNode(this.refs['last_name' + index]).value = '';
+ ReactDOM.findDOMNode(this.refs['email' + index]).value = '';
+ ReactDOM.findDOMNode(this.refs['first_name' + index]).value = '';
+ ReactDOM.findDOMNode(this.refs['last_name' + index]).value = '';
}
this.setState({
@@ -211,6 +211,7 @@ export default class InviteMemberModal extends React.Component {
placeholder='First name'
maxLength='64'
disabled={!this.state.emailEnabled}
+ spellCheck='false'
/>
{firstNameError}
</div>
@@ -224,6 +225,7 @@ export default class InviteMemberModal extends React.Component {
placeholder='Last name'
maxLength='64'
disabled={!this.state.emailEnabled}
+ spellCheck='false'
/>
{lastNameError}
</div>
@@ -242,6 +244,7 @@ export default class InviteMemberModal extends React.Component {
placeholder='email@domain.com'
maxLength='64'
disabled={!this.state.emailEnabled}
+ spellCheck='false'
/>
{emailError}
</div>
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index 997abce68..c982d57ca 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -25,14 +25,14 @@ export default class Login extends React.Component {
return;
}
- const email = React.findDOMNode(this.refs.email).value.trim();
+ const email = ReactDOM.findDOMNode(this.refs.email).value.trim();
if (!email) {
state.serverError = 'An email is required';
this.setState(state);
return;
}
- const password = React.findDOMNode(this.refs.password).value.trim();
+ const password = ReactDOM.findDOMNode(this.refs.password).value.trim();
if (!password) {
state.serverError = 'A password is required';
this.setState(state);
@@ -136,6 +136,7 @@ export default class Login extends React.Component {
defaultValue={priorEmail}
ref='email'
placeholder='Email'
+ spellCheck='false'
/>
</div>
<div className={'form-group' + errorClass}>
@@ -146,6 +147,7 @@ export default class Login extends React.Component {
name='password'
ref='password'
placeholder='Password'
+ spellCheck='false'
/>
</div>
<div className='form-group'>
diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx
index 9a104aa32..3af1d3800 100644
--- a/web/react/components/member_list_team_item.jsx
+++ b/web/react/components/member_list_team_item.jsx
@@ -82,8 +82,10 @@ export default class MemberListTeamItem extends React.Component {
const timestamp = UserStore.getCurrentUser().update_at;
if (user.roles.length > 0) {
- if (user.roles.indexOf('system_admin') > -1) {
+ if (Utils.isSystemAdmin(user.roles)) {
currentRoles = 'System Admin';
+ } else if (Utils.isAdmin(user.roles)) {
+ currentRoles = 'Team Admin';
} else {
currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1);
}
@@ -112,7 +114,7 @@ export default class MemberListTeamItem extends React.Component {
href='#'
onClick={this.handleMakeAdmin}
>
- {'Make Admin'}
+ {'Make Team Admin'}
</a>
</li>
);
diff --git a/web/react/components/mention_list.jsx b/web/react/components/mention_list.jsx
index b9ba5f58c..8c1da942d 100644
--- a/web/react/components/mention_list.jsx
+++ b/web/react/components/mention_list.jsx
@@ -61,7 +61,7 @@ export default class MentionList extends React.Component {
}
onClick(e) {
if (!($('#' + this.props.id).is(e.target) || $('#' + this.props.id).has(e.target).length ||
- ('mentionlist' in this.refs && $(React.findDOMNode(this.refs.mentionlist)).has(e.target).length))) {
+ ('mentionlist' in this.refs && $(ReactDOM.findDOMNode(this.refs.mentionlist)).has(e.target).length))) {
this.setState({mentionText: '-1'});
}
}
diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx
index 884b0d6ca..a0084ad30 100644
--- a/web/react/components/more_channels.jsx
+++ b/web/react/components/more_channels.jsx
@@ -31,12 +31,12 @@ export default class MoreChannels extends React.Component {
}
componentDidMount() {
ChannelStore.addMoreChangeListener(this.onListenerChange);
- $(React.findDOMNode(this.refs.modal)).on('shown.bs.modal', function shown() {
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', function shown() {
asyncClient.getMoreChannels(true);
});
var self = this;
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function show(e) {
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', function show(e) {
var button = e.relatedTarget;
self.setState({channelType: $(button).attr('data-channeltype')});
});
@@ -54,7 +54,7 @@ export default class MoreChannels extends React.Component {
this.setState({joiningChannel: channelIndex});
client.joinChannel(channel.id,
function joinSuccess() {
- $(React.findDOMNode(this.refs.modal)).modal('hide');
+ $(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
asyncClient.getChannel(channel.id);
utils.switchChannel(channel);
this.setState({joiningChannel: -1});
@@ -65,7 +65,7 @@ export default class MoreChannels extends React.Component {
);
}
handleNewChannel() {
- $(React.findDOMNode(this.refs.modal)).modal('hide');
+ $(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
this.setState({showNewChannelModal: true});
}
render() {
@@ -83,7 +83,7 @@ export default class MoreChannels extends React.Component {
moreChannels = <LoadingScreen />;
} else if (channels.length) {
moreChannels = (
- <table className='more-channel-table table'>
+ <table className='more-table table'>
<tbody>
{channels.map(function cMap(channel, index) {
var joinButton;
@@ -108,8 +108,8 @@ export default class MoreChannels extends React.Component {
return (
<tr key={channel.id}>
<td>
- <p className='more-channel-name'>{channel.display_name}</p>
- <p className='more-channel-description'>{channel.description}</p>
+ <p className='more-name'>{channel.display_name}</p>
+ <p className='more-description'>{channel.description}</p>
</td>
<td className='td--action'>
{joinButton}
diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx
index 31ecb4c5d..d5b44d86b 100644
--- a/web/react/components/more_direct_channels.jsx
+++ b/web/react/components/more_direct_channels.jsx
@@ -1,136 +1,290 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var ChannelStore = require('../stores/channel_store.jsx');
-var TeamStore = require('../stores/team_store.jsx');
-var Client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var utils = require('../utils/utils.jsx');
+const AsyncClient = require('../utils/async_client.jsx');
+const ChannelStore = require('../stores/channel_store.jsx');
+const Constants = require('../utils/constants.jsx');
+const Client = require('../utils/client.jsx');
+const Modal = ReactBootstrap.Modal;
+const PreferenceStore = require('../stores/preference_store.jsx');
+const TeamStore = require('../stores/team_store.jsx');
+const UserStore = require('../stores/user_store.jsx');
+const Utils = require('../utils/utils.jsx');
export default class MoreDirectChannels extends React.Component {
constructor(props) {
super(props);
- this.state = {channels: [], loadingDMChannel: -1};
+ this.handleFilterChange = this.handleFilterChange.bind(this);
+ this.handleHide = this.handleHide.bind(this);
+ this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
+ this.handleUserChange = this.handleUserChange.bind(this);
+
+ this.createRowForUser = this.createRowForUser.bind(this);
+
+ this.state = {
+ users: this.getUsersFromStore(),
+ filter: '',
+ loadingDMChannel: -1
+ };
+ }
+
+ getUsersFromStore() {
+ const currentId = UserStore.getCurrentId();
+ const profiles = UserStore.getProfiles();
+ const users = [];
+
+ for (const id in profiles) {
+ if (id !== currentId) {
+ users.push(profiles[id]);
+ }
+ }
+
+ users.sort((a, b) => a.username.localeCompare(b.username));
+
+ return users;
}
componentDidMount() {
- var self = this;
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function showModal(e) {
- var button = e.relatedTarget;
- self.setState({channels: $(button).data('channels')});
- });
+ UserStore.addChangeListener(this.handleUserChange);
}
- render() {
- var self = this;
-
- var directMessageItems = this.state.channels.map(function mapActivityToChannel(channel, index) {
- var badge = '';
- var titleClass = '';
- var active = '';
- var handleClick = null;
-
- if (channel.fake) {
- // It's a direct message channel that doesn't exist yet so let's create it now
- var otherUserId = utils.getUserIdFromChannelName(channel);
-
- if (self.state.loadingDMChannel === index) {
- badge = (
- <img
- className='channel-loading-gif pull-right'
- src='/static/images/load.gif'
- />
- );
- }
+ componentWillUnmount() {
+ UserStore.addChangeListener(this.handleUserChange);
+ }
- if (self.state.loadingDMChannel === -1) {
- handleClick = function clickHandler(e) {
- e.preventDefault();
- self.setState({loadingDMChannel: index});
-
- Client.createDirectChannel(channel, otherUserId,
- function success(data) {
- $(React.findDOMNode(self.refs.modal)).modal('hide');
- self.setState({loadingDMChannel: -1});
- AsyncClient.getChannel(data.id);
- utils.switchChannel(data);
- },
- function error() {
- self.setState({loadingDMChannel: -1});
- window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name;
- }
- );
- };
- }
- } else {
- if (channel.id === ChannelStore.getCurrentId()) {
- active = 'active';
- }
+ handleFilterChange() {
+ const filter = ReactDOM.findDOMNode(this.refs.filter).value;
+
+ if (filter !== this.state.filter) {
+ this.setState({filter});
+ }
+ }
- if (channel.unread) {
- badge = <span className='badge pull-right small'>{channel.unread}</span>;
- titleClass = 'unread-title';
+ handleHide() {
+ if (this.props.onModalDismissed) {
+ this.props.onModalDismissed();
+ }
+
+ this.setState({filter: ''});
+ }
+
+ handleShowDirectChannel(teammate, e) {
+ if (this.state.loadingDMChannel !== -1) {
+ return;
+ }
+
+ e.preventDefault();
+
+ const channelName = Utils.getDirectChannelName(UserStore.getCurrentId(), teammate.id);
+ let channel = ChannelStore.getByName(channelName);
+
+ const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true');
+ AsyncClient.savePreferences([preference]);
+
+ if (channel) {
+ Utils.switchChannel(channel);
+
+ this.handleHide();
+ } else {
+ this.setState({loadingDMChannel: teammate.id});
+
+ channel = {
+ name: channelName,
+ last_post_at: 0,
+ total_msg_count: 0,
+ type: 'D',
+ display_name: teammate.username,
+ teammate_id: teammate.id,
+ status: UserStore.getStatus(teammate.id)
+ };
+
+ Client.createDirectChannel(
+ channel,
+ teammate.id,
+ (data) => {
+ this.setState({loadingDMChannel: -1});
+
+ AsyncClient.getChannel(data.id);
+ Utils.switchChannel(data);
+
+ this.handleHide();
+ },
+ () => {
+ this.setState({loadingDMChannel: -1});
+ window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channelName;
}
+ );
+ }
+ }
- handleClick = function clickHandler(e) {
- e.preventDefault();
- utils.switchChannel(channel);
- $(React.findDOMNode(self.refs.modal)).modal('hide');
- };
- }
+ handleUserChange() {
+ this.setState({users: this.getUsersFromStore()});
+ }
+
+ createRowForUser(user) {
+ const details = [];
+
+ const fullName = Utils.getFullName(user);
+ if (fullName) {
+ details.push(
+ <span
+ key={`${user.id}__full-name`}
+ className='full-name'
+ >
+ {fullName}
+ </span>
+ );
+ }
- return (
- <li
- key={channel.name}
- className={active}
+ if (user.nickname) {
+ const separator = fullName ? ' - ' : '';
+ details.push(
+ <span
+ key={`${user.nickname}__nickname`}
>
- <a
- className={'sidebar-channel ' + titleClass}
- href='#'
- onClick={handleClick}
- >{badge}{channel.display_name}</a>
- </li>
+ {separator + user.nickname}
+ </span>
);
- });
+ }
+
+ let joinButton;
+ if (this.state.loadingDMChannel === user.id) {
+ joinButton = (
+ <img
+ className='channel-loading-gif'
+ src='/static/images/load.gif'
+ />
+ );
+ } else {
+ joinButton = (
+ <button
+ type='button'
+ className='btn btn-primary btn-message'
+ onClick={this.handleShowDirectChannel.bind(this, user)}
+ >
+ {'Message'}
+ </button>
+ );
+ }
return (
- <div
- className='modal fade'
- id='more_direct_channels'
- ref='modal'
- tabIndex='-1'
- role='dialog'
- aria-hidden='true'
+ <tr>
+ <td
+ key={user.id}
+ className='direct-channel'
+ >
+ <img
+ className='profile-img pull-left'
+ width='38'
+ height='38'
+ src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
+ />
+ <div className='more-name'>
+ {user.username}
+ </div>
+ <div className='more-description'>
+ {details}
+ </div>
+ </td>
+ <td className='td--action lg'>
+ {joinButton}
+ </td>
+ </tr>
+ );
+ }
+
+ componentDidUpdate(prevProps) {
+ if (!prevProps.show && this.props.show) {
+ $(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 300);
+ if ($(window).width() > 768) {
+ $(ReactDOM.findDOMNode(this.refs.userList)).perfectScrollbar();
+ }
+ }
+ }
+
+ render() {
+ if (!this.props.show) {
+ return null;
+ }
+
+ let users = this.state.users;
+ if (this.state.filter !== '') {
+ users = users.filter((user) => {
+ return user.username.indexOf(this.state.filter) !== -1 ||
+ user.first_name.indexOf(this.state.filter) !== -1 ||
+ user.last_name.indexOf(this.state.filter) !== -1 ||
+ user.nickname.indexOf(this.state.filter) !== -1;
+ });
+ }
+
+ const userEntries = users.map(this.createRowForUser);
+
+ if (userEntries.length === 0) {
+ userEntries.push(<tr key='no-users-found'><td>{'No users found :('}</td></tr>);
+ }
+
+ let memberString = 'Member';
+ if (users.length !== 1) {
+ memberString += 's';
+ }
+
+ let count;
+ if (users.length === this.state.users.length) {
+ count = `${users.length} ${memberString}`;
+ } else {
+ count = `${users.length} ${memberString} of ${this.state.users.length} Total`;
+ }
+
+ return (
+ <Modal
+ className='modal-direct-channels'
+ show={this.props.show}
+ onHide={this.handleHide}
>
- <div className='modal-dialog'>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- >
- <span aria-hidden='true'>&times;</span>
- <span className='sr-only'>Close</span>
- </button>
- <h4 className='modal-title'>More Direct Messages</h4>
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Direct Messages'}</Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <div className='row filter-row'>
+ <div className='col-sm-6'>
+ <input
+ ref='filter'
+ className='form-control filter-textbox'
+ placeholder='Search members'
+ onInput={this.handleFilterChange}
+ />
</div>
- <div className='modal-body'>
- <ul className='nav nav-pills nav-stacked'>
- {directMessageItems}
- </ul>
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >Close</button>
+ <div className='col-sm-6'>
+ <span className='member-count'>{count}</span>
</div>
</div>
- </div>
- </div>
+ <div
+ ref='userList'
+ className='user-list'
+ >
+ <table className='more-table table'>
+ <tbody>
+ {userEntries}
+ </tbody>
+ </table>
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.handleHide}
+ >
+ {'Close'}
+ </button>
+ </Modal.Footer>
+ </Modal>
);
}
}
+
+MoreDirectChannels.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onModalDismissed: React.PropTypes.func
+};
diff --git a/web/react/components/msg_typing.jsx b/web/react/components/msg_typing.jsx
index 569942390..1bd23c55c 100644
--- a/web/react/components/msg_typing.jsx
+++ b/web/react/components/msg_typing.jsx
@@ -1,8 +1,11 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var SocketStore = require('../stores/socket_store.jsx');
-var UserStore = require('../stores/user_store.jsx');
+const SocketStore = require('../stores/socket_store.jsx');
+const UserStore = require('../stores/user_store.jsx');
+
+const Constants = require('../utils/constants.jsx');
+const SocketEvents = Constants.SocketEvents;
export default class MsgTyping extends React.Component {
constructor(props) {
@@ -33,9 +36,9 @@ export default class MsgTyping extends React.Component {
}
onChange(msg) {
- if (msg.action === 'typing' &&
- this.props.channelId === msg.channel_id &&
- this.props.parentId === msg.props.parent_id) {
+ if (msg.action === SocketEvents.TYPING &&
+ this.props.channelId === msg.channel_id &&
+ this.props.parentId === msg.props.parent_id) {
this.lastTime = new Date().getTime();
var username = 'Someone';
@@ -52,7 +55,7 @@ export default class MsgTyping extends React.Component {
}
}.bind(this), 3000);
}
- } else if (msg.action === 'posted' && msg.channel_id === this.props.channelId) {
+ } else if (msg.action === SocketEvents.POSTED && msg.channel_id === this.props.channelId) {
this.setState({text: ''});
}
}
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index 6503bd801..f9cd525fd 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -14,6 +14,9 @@ var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
+var Popover = ReactBootstrap.Popover;
+var OverlayTrigger = ReactBootstrap.OverlayTrigger;
+
export default class Navbar extends React.Component {
constructor(props) {
super(props);
@@ -36,12 +39,6 @@ export default class Navbar extends React.Component {
ChannelStore.addChangeListener(this.onChange);
ChannelStore.addExtraInfoChangeListener(this.onChange);
$('.inner__wrap').click(this.hideSidebars);
-
- $('body').on('click.infopopover', function handlePopoverClick(e) {
- if ($(e.target).attr('data-toggle') !== 'popover' && $(e.target).parents('.popover.in').length === 0) {
- $('.info-popover').popover('hide');
- }
- });
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChange);
@@ -224,11 +221,15 @@ export default class Navbar extends React.Component {
return (
<div className='navbar-brand'>
<div className='dropdown'>
- <div
- data-toggle='popover'
- data-content={popoverContent}
- className='description info-popover'
- />
+ <OverlayTrigger
+ trigger='click'
+ placement='bottom'
+ overlay={popoverContent}
+ className='description'
+ rootClose={true}
+ >
+ <div className='description info-popover'/>
+ </OverlayTrigger>
<a
href='#'
className='dropdown-toggle theme'
@@ -330,11 +331,17 @@ export default class Navbar extends React.Component {
var isDirect = false;
if (channel) {
- popoverContent = React.renderToString(
- <MessageWrapper
- message={channel.description}
- options={{singleline: true, mentionHighlight: false}}
- />
+ popoverContent = (
+ <Popover
+ bsStyle='info'
+ placement='bottom'
+ id='description-popover'
+ >
+ <MessageWrapper
+ message={channel.description}
+ options={{singleline: true, mentionHighlight: false}}
+ />
+ </Popover>
);
isAdmin = Utils.isAdmin(this.state.member.roles);
@@ -354,19 +361,28 @@ export default class Navbar extends React.Component {
}
if (channel.description.length === 0) {
- popoverContent = React.renderToString(
- <div>
- No channel description yet. <br/>
- <a
- href='#'
- data-toggle='modal'
- data-desc={channel.description}
- data-title={channel.display_name}
- data-channelid={channel.id}
- data-target='#edit_channel'
- >
- Click here
- </a> to add one.</div>
+ popoverContent = (
+ <Popover
+ bsStyle='info'
+ placement='bottom'
+ id='description-popover'
+ >
+ <div>
+ {'No channel description yet.'}
+ <br/>
+ <a
+ href='#'
+ data-toggle='modal'
+ data-desc={channel.description}
+ data-title={channel.display_name}
+ data-channelid={channel.id}
+ data-target='#edit_channel'
+ >
+ {'Click here'}
+ </a>
+ {' to add one.'}
+ </div>
+ </Popover>
);
}
}
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index b6defc393..1cb13bbe5 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -11,7 +11,24 @@ var AboutBuildModal = require('./about_build_modal.jsx');
var Constants = require('../utils/constants.jsx');
function getStateFromStores() {
- return {teams: UserStore.getTeams()};
+ let teams = [];
+ let teamsObject = UserStore.getTeams();
+ for (let teamId in teamsObject) {
+ if (teamsObject.hasOwnProperty(teamId)) {
+ teams.push(teamsObject[teamId]);
+ }
+ }
+ teams.sort(function sortByDisplayName(teamA, teamB) {
+ let teamADisplayName = teamA.display_name.toLowerCase();
+ let teamBDisplayName = teamB.display_name.toLowerCase();
+ if (teamADisplayName < teamBDisplayName) {
+ return -1;
+ } else if (teamADisplayName > teamBDisplayName) {
+ return 1;
+ }
+ return 0;
+ });
+ return {teams};
}
export default class NavbarDropdown extends React.Component {
@@ -40,7 +57,7 @@ export default class NavbarDropdown extends React.Component {
UserStore.addTeamsChangeListener(this.onListenerChange);
TeamStore.addChangeListener(this.onListenerChange);
- $(React.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
+ $(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
this.blockToggle = true;
setTimeout(() => {
this.blockToggle = false;
@@ -51,7 +68,7 @@ export default class NavbarDropdown extends React.Component {
UserStore.removeTeamsChangeListener(this.onListenerChange);
TeamStore.removeChangeListener(this.onListenerChange);
- $(React.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
+ $(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
}
onListenerChange() {
var newState = getStateFromStores();
@@ -111,7 +128,7 @@ export default class NavbarDropdown extends React.Component {
data-toggle='modal'
data-target='#team_members'
>
- {'Manage Team'}
+ {'Manage Members'}
</a>
</li>
);
@@ -154,9 +171,9 @@ export default class NavbarDropdown extends React.Component {
</li>
);
- this.state.teams.forEach((teamName) => {
- if (teamName !== this.props.teamName) {
- teams.push(<li key={teamName}><a href={Utils.getWindowLocationOrigin() + '/' + teamName}>{'Switch to ' + teamName}</a></li>);
+ this.state.teams.forEach((team) => {
+ if (team.name !== this.props.teamName) {
+ teams.push(<li key={team.name}><a href={Utils.getWindowLocationOrigin() + '/' + team.name}>{'Switch to ' + team.display_name}</a></li>);
}
});
}
diff --git a/web/react/components/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx
index c3d9c046d..4e6280c99 100644
--- a/web/react/components/new_channel_modal.jsx
+++ b/web/react/components/new_channel_modal.jsx
@@ -25,7 +25,7 @@ export default class NewChannelModal extends React.Component {
handleSubmit(e) {
e.preventDefault();
- const displayName = React.findDOMNode(this.refs.display_name).value.trim();
+ const displayName = ReactDOM.findDOMNode(this.refs.display_name).value.trim();
if (displayName.length < 1) {
this.setState({displayNameError: 'This field is required'});
return;
@@ -35,8 +35,8 @@ export default class NewChannelModal extends React.Component {
}
handleChange() {
const newData = {
- displayName: React.findDOMNode(this.refs.display_name).value,
- description: React.findDOMNode(this.refs.channel_desc).value
+ displayName: ReactDOM.findDOMNode(this.refs.display_name).value,
+ description: ReactDOM.findDOMNode(this.refs.channel_desc).value
};
this.props.onDataChanged(newData);
}
diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx
index 0ef187114..217f1b393 100644
--- a/web/react/components/password_reset_form.jsx
+++ b/web/react/components/password_reset_form.jsx
@@ -15,7 +15,7 @@ export default class PasswordResetForm extends React.Component {
e.preventDefault();
var state = {};
- var password = React.findDOMNode(this.refs.password).value.trim();
+ var password = ReactDOM.findDOMNode(this.refs.password).value.trim();
if (!password || password.length < 5) {
state.error = 'Please enter at least 5 characters.';
this.setState(state);
@@ -69,6 +69,7 @@ export default class PasswordResetForm extends React.Component {
name='password'
ref='password'
placeholder='Password'
+ spellCheck='false'
/>
</div>
{error}
diff --git a/web/react/components/password_reset_send_link.jsx b/web/react/components/password_reset_send_link.jsx
index 78fbcaa2f..8f1890705 100644
--- a/web/react/components/password_reset_send_link.jsx
+++ b/web/react/components/password_reset_send_link.jsx
@@ -16,7 +16,7 @@ export default class PasswordResetSendLink extends React.Component {
e.preventDefault();
var state = {};
- var email = React.findDOMNode(this.refs.email).value.trim().toLowerCase();
+ var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase();
if (!email || !Utils.isEmail(email)) {
state.error = 'Please enter a valid email address.';
this.setState(state);
@@ -33,7 +33,7 @@ export default class PasswordResetSendLink extends React.Component {
client.sendPasswordReset(data,
function passwordResetSent() {
this.setState({error: null, updateText: <p>A password reset link has been sent to <b>{email}</b> for your <b>{this.props.teamDisplayName}</b> team on {window.location.hostname}.</p>, moreUpdateText: 'Please check your inbox.'});
- $(React.findDOMNode(this.refs.reset_form)).hide();
+ $(ReactDOM.findDOMNode(this.refs.reset_form)).hide();
}.bind(this),
function passwordResetFailedToSend(err) {
this.setState({error: err.message, update_text: null, moreUpdateText: null});
@@ -73,6 +73,7 @@ export default class PasswordResetSendLink extends React.Component {
name='email'
ref='email'
placeholder='Email'
+ spellCheck='false'
/>
</div>
{error}
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index 5d9052fd7..155e88600 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -2,6 +2,8 @@
// See License.txt for license information.
var UserStore = require('../stores/user_store.jsx');
+var Popover = ReactBootstrap.Popover;
+var OverlayTrigger = ReactBootstrap.OverlayTrigger;
export default class PopoverListMembers extends React.Component {
componentDidMount() {
@@ -24,29 +26,29 @@ export default class PopoverListMembers extends React.Component {
});
}
};
-
- $('#member_popover').popover({placement: 'bottom', trigger: 'click', html: true});
- $('body').on('click', function onClick(e) {
- if (e.target.parentNode && $(e.target.parentNode.parentNode)[0] !== $('#member_popover')[0] && $(e.target).parents('.popover.in').length === 0) {
- $('#member_popover').popover('hide');
- }
- });
}
render() {
- let popoverHtml = '';
+ let popoverHtml = [];
let count = 0;
let countText = '-';
const members = this.props.members;
const teamMembers = UserStore.getProfilesUsernameMap();
if (members && teamMembers) {
- members.sort(function compareByLocal(a, b) {
+ members.sort((a, b) => {
return a.username.localeCompare(b.username);
});
- members.forEach(function addMemberElement(m) {
+ members.forEach((m, i) => {
if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) {
- popoverHtml += `<div class='text--nowrap'>${m.username}</div>`;
+ popoverHtml.push(
+ <div
+ className='text--nowrap'
+ key={'popover-member-' + i}
+ >
+ {m.username}
+ </div>
+ );
count++;
}
});
@@ -59,15 +61,21 @@ export default class PopoverListMembers extends React.Component {
}
return (
- <div
- id='member_popover'
- data-toggle='popover'
- data-content={popoverHtml}
- data-original-title='Members'
+ <OverlayTrigger
+ trigger='click'
+ placement='bottom'
+ rootClose={true}
+ overlay={
+ <Popover
+ title='Members'
+ id='member-list-popover'
+ >
+ {popoverHtml}
+ </Popover>
+ }
>
- <div
- id='member_tooltip'
- >
+ <div id='member_popover'>
+ <div>
{countText}
<span
className='fa fa-user'
@@ -75,6 +83,7 @@ export default class PopoverListMembers extends React.Component {
/>
</div>
</div>
+ </OverlayTrigger>
);
}
}
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index e87ac6743..fb838b736 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -37,7 +37,7 @@ export default class PostBody extends React.Component {
}
parseEmojis() {
- twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE});
+ twemoji.parse(ReactDOM.findDOMNode(this), {size: Constants.EMOJI_SIZE});
}
componentDidMount() {
@@ -116,7 +116,7 @@ export default class PostBody extends React.Component {
}
var metadata = data.items[0].snippet;
this.receivedYoutubeData = true;
- this.setState({youtubeUploader: metadata.channelTitle, youtubeTitle: metadata.title});
+ this.setState({youtubeTitle: metadata.title});
}
if (global.window.config.GoogleDeveloperKey && !this.receivedYoutubeData) {
@@ -134,18 +134,12 @@ export default class PostBody extends React.Component {
header = header + ' - ';
}
- let uploader = this.state.youtubeUploader;
- if (!uploader) {
- uploader = 'unknown';
- }
-
return (
<div className='post-comment'>
<h4>
<span className='video-type'>{header}</span>
<span className='video-title'><a href={link}>{this.state.youtubeTitle}</a></span>
</h4>
- <h4 className='video-uploader'>{uploader}</h4>
<div
className='video-div embed-responsive-item'
id={youtubeId}
diff --git a/web/react/components/post_deleted_modal.jsx b/web/react/components/post_deleted_modal.jsx
index 87afc6a7f..ba07a22f5 100644
--- a/web/react/components/post_deleted_modal.jsx
+++ b/web/react/components/post_deleted_modal.jsx
@@ -15,7 +15,7 @@ export default class PostDeletedModal extends React.Component {
this.state = {};
}
componentDidMount() {
- $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', () => {
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', () => {
this.handleClose();
});
}
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 87962641f..36260d77c 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -150,6 +150,8 @@ export default class PostInfo extends React.Component {
<ul className='post-header post-info'>
<li className='post-header-col'>
<OverlayTrigger
+ delayShow={500}
+ container={this}
placement='top'
overlay={tooltip}
>
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index 643b38af5..4402745e1 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -1,19 +1,24 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var PostStore = require('../stores/post_store.jsx');
-var ChannelStore = require('../stores/channel_store.jsx');
-var UserStore = require('../stores/user_store.jsx');
-var UserProfile = require('./user_profile.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var Post = require('./post.jsx');
-var LoadingScreen = require('./loading_screen.jsx');
-var SocketStore = require('../stores/socket_store.jsx');
-var utils = require('../utils/utils.jsx');
-var Client = require('../utils/client.jsx');
-var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var Constants = require('../utils/constants.jsx');
-var ActionTypes = Constants.ActionTypes;
+const Post = require('./post.jsx');
+const UserProfile = require('./user_profile.jsx');
+const AsyncClient = require('../utils/async_client.jsx');
+const LoadingScreen = require('./loading_screen.jsx');
+
+const PostStore = require('../stores/post_store.jsx');
+const ChannelStore = require('../stores/channel_store.jsx');
+const UserStore = require('../stores/user_store.jsx');
+const SocketStore = require('../stores/socket_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
+
+const utils = require('../utils/utils.jsx');
+const Client = require('../utils/client.jsx');
+const Constants = require('../utils/constants.jsx');
+const ActionTypes = Constants.ActionTypes;
+const SocketEvents = Constants.SocketEvents;
+
+const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
export default class PostList extends React.Component {
constructor(props) {
@@ -57,7 +62,7 @@ export default class PostList extends React.Component {
}
}
- postList.order.sort(function postSort(a, b) {
+ postList.order.sort((a, b) => {
if (postList.posts[a].create_at > postList.posts[b].create_at) {
return -1;
}
@@ -81,7 +86,7 @@ export default class PostList extends React.Component {
}
return {
- postList: postList
+ postList
};
}
componentDidMount() {
@@ -105,9 +110,10 @@ export default class PostList extends React.Component {
PostStore.clearUnseenDeletedPosts(this.props.channelId);
PostStore.addChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onTimeChange);
+ PreferenceStore.addChangeListener(this.onTimeChange);
SocketStore.addChangeListener(this.onSocketChange);
- const postHolder = $(React.findDOMNode(this.refs.postlist));
+ const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
$(window).resize(() => {
this.resize();
@@ -133,7 +139,7 @@ export default class PostList extends React.Component {
$('.top-visible-post').removeClass('top-visible-post');
- $(React.findDOMNode(this.refs.postlistcontent)).children().each(function select() {
+ $(ReactDOM.findDOMNode(this.refs.postlistcontent)).children().each(function select() {
if ($(this).position().top + $(this).height() / 2 > 0) {
$(this).addClass('top-visible-post');
return false;
@@ -156,9 +162,10 @@ export default class PostList extends React.Component {
PostStore.removeChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onTimeChange);
SocketStore.removeChangeListener(this.onSocketChange);
+ PreferenceStore.removeChangeListener(this.onTimeChange);
$('body').off('click.userpopover');
$(window).off('resize');
- var postHolder = $(React.findDOMNode(this.refs.postlist));
+ var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
postHolder.off('scroll');
}
componentDidUpdate(prevProps, prevState) {
@@ -214,7 +221,7 @@ export default class PostList extends React.Component {
}
}
componentWillUpdate() {
- var postHolder = $(React.findDOMNode(this.refs.postlist));
+ var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
this.prevScrollTop = postHolder.scrollTop();
}
componentWillReceiveProps(nextProps) {
@@ -225,7 +232,7 @@ export default class PostList extends React.Component {
}
}
resize() {
- const postHolder = $(React.findDOMNode(this.refs.postlist));
+ const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
if ($('#create_post').length > 0) {
const height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50;
postHolder.css('height', height + 'px');
@@ -233,12 +240,12 @@ export default class PostList extends React.Component {
}
scrollTo(val) {
this.isUserScroll = false;
- var postHolder = $(React.findDOMNode(this.refs.postlist));
+ var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
postHolder[0].scrollTop = val;
}
scrollToBottom(force) {
this.isUserScroll = false;
- var postHolder = $(React.findDOMNode(this.refs.postlist));
+ var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
if ($('#new_message_' + this.props.channelId)[0] && !this.userHasSeenNew && !force) {
$('#new_message_' + this.props.channelId)[0].scrollIntoView();
} else {
@@ -260,14 +267,14 @@ export default class PostList extends React.Component {
Client.getPosts(
id,
PostStore.getLatestUpdate(id),
- function success() {
+ () => {
this.loadInProgress = false;
this.setState({isFirstLoadComplete: true});
- }.bind(this),
- function fail() {
+ },
+ () => {
this.loadInProgress = false;
this.setState({isFirstLoadComplete: true});
- }.bind(this)
+ }
);
}
onChange() {
@@ -278,28 +285,16 @@ export default class PostList extends React.Component {
}
}
onSocketChange(msg) {
- var post;
- if (msg.action === 'posted' || msg.action === 'post_edited') {
- post = JSON.parse(msg.props.post);
- PostStore.storePost(post);
- } else if (msg.action === 'post_deleted') {
+ if (msg.action === SocketEvents.POST_DELETED) {
var activeRoot = $(document.activeElement).closest('.comment-create-body')[0];
var activeRootPostId = '';
if (activeRoot && activeRoot.id.length > 0) {
activeRootPostId = activeRoot.id;
}
- post = JSON.parse(msg.props.post);
-
- PostStore.storeUnseenDeletedPost(post);
- PostStore.removePost(post, true);
- PostStore.emitChange();
-
if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() !== msg.user_id) {
$('#post_deleted').modal('show');
}
- } else if (msg.action === 'new_user') {
- AsyncClient.getProfiles();
}
}
onTimeChange() {
@@ -349,7 +344,7 @@ export default class PostList extends React.Component {
data-title={channel.display_name}
data-channelid={channel.id}
>
- <i className='fa fa-pencil'></i>Set a description
+ <i className='fa fa-pencil'></i>{'Set a description'}
</a>
</div>
);
@@ -596,14 +591,14 @@ export default class PostList extends React.Component {
var order = this.state.postList.order;
var channelId = this.props.channelId;
- $(React.findDOMNode(this.refs.loadmore)).text('Retrieving more messages...');
+ $(ReactDOM.findDOMNode(this.refs.loadmore)).text('Retrieving more messages...');
Client.getPostsPage(
channelId,
order.length,
Constants.POST_CHUNK_SIZE,
function success(data) {
- $(React.findDOMNode(this.refs.loadmore)).text('Load more messages');
+ $(ReactDOM.findDOMNode(this.refs.loadmore)).text('Load more messages');
this.gotMorePosts = true;
this.setState({numToDisplay: this.state.numToDisplay + Constants.POST_CHUNK_SIZE});
@@ -628,7 +623,7 @@ export default class PostList extends React.Component {
Client.getProfiles();
}.bind(this),
function fail(err) {
- $(React.findDOMNode(this.refs.loadmore)).text('Load more messages');
+ $(ReactDOM.findDOMNode(this.refs.loadmore)).text('Load more messages');
AsyncClient.dispatchError(err, 'getPosts');
}.bind(this)
);
diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx
index e93c44126..3d4d9bf45 100644
--- a/web/react/components/register_app_modal.jsx
+++ b/web/react/components/register_app_modal.jsx
@@ -14,7 +14,7 @@ export default class RegisterAppModal extends React.Component {
this.state = {clientId: '', clientSecret: '', saved: false};
}
componentDidMount() {
- $(React.findDOMNode(this)).on('hide.bs.modal', this.onHide);
+ $(ReactDOM.findDOMNode(this)).on('hide.bs.modal', this.onHide);
}
register() {
var state = this.state;
@@ -22,7 +22,7 @@ export default class RegisterAppModal extends React.Component {
var app = {};
- var name = this.refs.name.getDOMNode().value;
+ var name = this.refs.name.value;
if (!name || name.length === 0) {
state.nameError = 'Application name must be filled in.';
this.setState(state);
@@ -31,7 +31,7 @@ export default class RegisterAppModal extends React.Component {
state.nameError = null;
app.name = name;
- var homepage = this.refs.homepage.getDOMNode().value;
+ var homepage = this.refs.homepage.value;
if (!homepage || homepage.length === 0) {
state.homepageError = 'Homepage must be filled in.';
this.setState(state);
@@ -40,10 +40,10 @@ export default class RegisterAppModal extends React.Component {
state.homepageError = null;
app.homepage = homepage;
- var desc = this.refs.desc.getDOMNode().value;
+ var desc = this.refs.desc.value;
app.description = desc;
- var rawCallbacks = this.refs.callback.getDOMNode().value.trim();
+ var rawCallbacks = this.refs.callback.value.trim();
if (!rawCallbacks || rawCallbacks.length === 0) {
state.callbackError = 'At least one callback URL must be filled in.';
this.setState(state);
@@ -73,7 +73,7 @@ export default class RegisterAppModal extends React.Component {
this.setState({clientId: '', clientSecret: '', saved: false});
}
save() {
- this.setState({saved: this.refs.save.getDOMNode().checked});
+ this.setState({saved: this.refs.save.checked});
}
render() {
var nameError;
diff --git a/web/react/components/removed_from_channel_modal.jsx b/web/react/components/removed_from_channel_modal.jsx
index ca35d6fb0..7cf0a2ef1 100644
--- a/web/react/components/removed_from_channel_modal.jsx
+++ b/web/react/components/removed_from_channel_modal.jsx
@@ -37,13 +37,13 @@ export default class RemovedFromChannelModal extends React.Component {
}
componentDidMount() {
- $(React.findDOMNode(this)).on('show.bs.modal', this.handleShow);
- $(React.findDOMNode(this)).on('hidden.bs.modal', this.handleClose);
+ $(ReactDOM.findDOMNode(this)).on('show.bs.modal', this.handleShow);
+ $(ReactDOM.findDOMNode(this)).on('hidden.bs.modal', this.handleClose);
}
componentWillUnmount() {
- $(React.findDOMNode(this)).off('show.bs.modal', this.handleShow);
- $(React.findDOMNode(this)).off('hidden.bs.modal', this.handleClose);
+ $(ReactDOM.findDOMNode(this)).off('show.bs.modal', this.handleShow);
+ $(ReactDOM.findDOMNode(this)).off('hidden.bs.modal', this.handleClose);
}
render() {
diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx
index 7f7d4554c..80f0956f2 100644
--- a/web/react/components/rename_channel_modal.jsx
+++ b/web/react/components/rename_channel_modal.jsx
@@ -76,13 +76,13 @@ export default class RenameChannelModal extends React.Component {
Client.updateChannel(channel,
function handleUpdateSuccess() {
- $(React.findDOMNode(this.refs.modal)).modal('hide');
+ $(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
AsyncClient.getChannel(channel.id);
Utils.updateAddressBar(channel.name);
- React.findDOMNode(this.refs.displayName).value = '';
- React.findDOMNode(this.refs.channelName).value = '';
+ ReactDOM.findDOMNode(this.refs.displayName).value = '';
+ ReactDOM.findDOMNode(this.refs.channelName).value = '';
}.bind(this),
function handleUpdateError(err) {
state.serverError = err.message;
@@ -92,15 +92,15 @@ export default class RenameChannelModal extends React.Component {
);
}
onNameChange() {
- this.setState({channelName: React.findDOMNode(this.refs.channelName).value});
+ this.setState({channelName: ReactDOM.findDOMNode(this.refs.channelName).value});
}
onDisplayNameChange() {
- this.setState({displayName: React.findDOMNode(this.refs.displayName).value});
+ this.setState({displayName: ReactDOM.findDOMNode(this.refs.displayName).value});
}
displayNameKeyUp() {
- const displayName = React.findDOMNode(this.refs.displayName).value.trim();
+ const displayName = ReactDOM.findDOMNode(this.refs.displayName).value.trim();
const channelName = Utils.cleanUpUrlable(displayName);
- React.findDOMNode(this.refs.channelName).value = channelName;
+ ReactDOM.findDOMNode(this.refs.channelName).value = channelName;
this.setState({channelName: channelName});
}
handleClose() {
@@ -119,11 +119,11 @@ export default class RenameChannelModal extends React.Component {
this.setState({displayName: button.attr('data-display'), channelName: button.attr('data-name'), channelId: button.attr('data-channelid')});
}
componentDidMount() {
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', this.handleShow);
- $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.handleShow);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose);
}
componentWillUnmount() {
- $(React.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose);
+ $(ReactDOM.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose);
}
render() {
let displayNameError = null;
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index d1ed9b2c0..d3a4cfaeb 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -29,7 +29,7 @@ export default class RhsComment extends React.Component {
var post = this.props.post;
Client.createPost(post, post.channel_id,
- function success(data) {
+ (data) => {
AsyncClient.getPosts(post.channel_id);
var channel = ChannelStore.get(post.channel_id);
@@ -43,11 +43,11 @@ export default class RhsComment extends React.Component {
post: data
});
},
- function fail() {
+ () => {
post.state = Constants.POST_FAILED;
PostStore.updatePendingPost(post);
this.forceUpdate();
- }.bind(this)
+ }
);
post.state = Constants.POST_LOADING;
@@ -55,7 +55,7 @@ export default class RhsComment extends React.Component {
this.forceUpdate();
}
parseEmojis() {
- twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE});
+ twemoji.parse(ReactDOM.findDOMNode(this), {size: Constants.EMOJI_SIZE});
}
componentDidMount() {
this.parseEmojis();
@@ -84,7 +84,10 @@ export default class RhsComment extends React.Component {
if (isOwner) {
dropdownContents.push(
- <li role='presentation'>
+ <li
+ role='presentation'
+ key='edit-button'
+ >
<a
href='#'
role='menuitem'
@@ -95,7 +98,7 @@ export default class RhsComment extends React.Component {
data-postid={post.id}
data-channelid={post.channel_id}
>
- Edit
+ {'Edit'}
</a>
</li>
);
@@ -103,7 +106,10 @@ export default class RhsComment extends React.Component {
if (isOwner || isAdmin) {
dropdownContents.push(
- <li role='presentation'>
+ <li
+ role='presentation'
+ key='delete-button'
+ >
<a
href='#'
role='menuitem'
@@ -114,7 +120,7 @@ export default class RhsComment extends React.Component {
data-channelid={post.channel_id}
data-comments={0}
>
- Delete
+ {'Delete'}
</a>
</li>
);
@@ -162,7 +168,7 @@ export default class RhsComment extends React.Component {
href='#'
onClick={this.retryComment}
>
- Retry
+ {'Retry'}
</a>
);
} else if (post.state === Constants.POST_LOADING) {
@@ -213,14 +219,14 @@ export default class RhsComment extends React.Component {
</li>
</ul>
<div className='post-body'>
- <p className={postClass}>
+ <div className={postClass}>
{loading}
<div
ref='message_holder'
onClick={TextFormatting.handleClick}
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
/>
- </p>
+ </div>
{fileAttachment}
</div>
</div>
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index 442d3483e..a9f1fcd30 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -19,7 +19,7 @@ export default class RhsRootPost extends React.Component {
this.state = {};
}
parseEmojis() {
- twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE});
+ twemoji.parse(ReactDOM.findDOMNode(this), {size: Constants.EMOJI_SIZE});
}
componentDidMount() {
this.parseEmojis();
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
index 41fd74adb..bcdec2870 100644
--- a/web/react/components/rhs_thread.jsx
+++ b/web/react/components/rhs_thread.jsx
@@ -3,7 +3,8 @@
var PostStore = require('../stores/post_store.jsx');
var UserStore = require('../stores/user_store.jsx');
-var utils = require('../utils/utils.jsx');
+var PreferenceStore = require('../stores/preference_store.jsx');
+var Utils = require('../utils/utils.jsx');
var SearchBox = require('./search_bar.jsx');
var CreateComment = require('./create_comment.jsx');
var RhsHeaderPost = require('./rhs_header_post.jsx');
@@ -18,8 +19,13 @@ export default class RhsThread extends React.Component {
this.onChange = this.onChange.bind(this);
this.onChangeAll = this.onChangeAll.bind(this);
+ this.forceUpdateInfo = this.forceUpdateInfo.bind(this);
+ this.handleResize = this.handleResize.bind(this);
- this.state = this.getStateFromStores();
+ const state = this.getStateFromStores();
+ state.windowWidth = Utils.windowWidth();
+ state.windowHeight = Utils.windowHeight();
+ this.state = state;
}
getStateFromStores() {
var postList = PostStore.getSelectedPost();
@@ -43,26 +49,40 @@ export default class RhsThread extends React.Component {
componentDidMount() {
PostStore.addSelectedPostChangeListener(this.onChange);
PostStore.addChangeListener(this.onChangeAll);
+ PreferenceStore.addChangeListener(this.forceUpdateInfo);
this.resize();
- $(window).resize(function resize() {
- this.resize();
- }.bind(this));
+ window.addEventListener('resize', this.handleResize);
}
componentDidUpdate() {
if ($('.post-right__scroll')[0]) {
$('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight);
}
-
- $('.post-right__scroll').perfectScrollbar('update');
this.resize();
}
componentWillUnmount() {
PostStore.removeSelectedPostChangeListener(this.onChange);
PostStore.removeChangeListener(this.onChangeAll);
+ PreferenceStore.removeChangeListener(this.forceUpdateInfo);
+ window.removeEventListener('resize', this.handleResize);
+ }
+ forceUpdateInfo() {
+ if (this.state.postList) {
+ for (var postId in this.state.postList.posts) {
+ if (this.refs[postId]) {
+ this.refs[postId].forceUpdate();
+ }
+ }
+ }
+ }
+ handleResize() {
+ this.setState({
+ windowWidth: Utils.windowWidth(),
+ windowHeight: Utils.windowHeight()
+ });
}
onChange() {
var newState = this.getStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areStatesEqual(newState, this.state)) {
this.setState(newState);
}
}
@@ -92,16 +112,18 @@ export default class RhsThread extends React.Component {
}
var newState = this.getStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areStatesEqual(newState, this.state)) {
this.setState(newState);
}
}
resize() {
- var height = $(window).height() - $('#error_bar').outerHeight() - 100;
+ var height = this.state.windowHeight - $('#error_bar').outerHeight() - 100;
$('.post-right__scroll').css('height', height + 'px');
$('.post-right__scroll').scrollTop(100000);
- $('.post-right__scroll').perfectScrollbar();
- $('.post-right__scroll').perfectScrollbar('update');
+ if (this.state.windowWidth > 768) {
+ $('.post-right__scroll').perfectScrollbar();
+ $('.post-right__scroll').perfectScrollbar('update');
+ }
}
render() {
var postList = this.state.postList;
@@ -174,6 +196,7 @@ export default class RhsThread extends React.Component {
/>
<div className='post-right__scroll'>
<RootPost
+ ref={rootPost.id}
post={rootPost}
commentCount={postsArray.length}
/>
diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx
index a6f9441ec..2e9764bd9 100644
--- a/web/react/components/search_bar.jsx
+++ b/web/react/components/search_bar.jsx
@@ -90,7 +90,7 @@ export default class SearchBar extends React.Component {
function success(data) {
this.setState({isSearching: false});
if (utils.isMobile()) {
- React.findDOMNode(this.refs.search).value = '';
+ ReactDOM.findDOMNode(this.refs.search).value = '';
}
AppDispatcher.handleServerAction({
diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx
index 5eea3c501..30e15d0ad 100644
--- a/web/react/components/search_results.jsx
+++ b/web/react/components/search_results.jsx
@@ -4,7 +4,7 @@
var PostStore = require('../stores/post_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var SearchBox = require('./search_bar.jsx');
-var utils = require('../utils/utils.jsx');
+var Utils = require('../utils/utils.jsx');
var SearchResultsHeader = require('./search_results_header.jsx');
var SearchResultsItem = require('./search_results_item.jsx');
@@ -20,18 +20,19 @@ export default class SearchResults extends React.Component {
this.onChange = this.onChange.bind(this);
this.resize = this.resize.bind(this);
+ this.handleResize = this.handleResize.bind(this);
- this.state = getStateFromStores();
+ const state = getStateFromStores();
+ state.windowWidth = Utils.windowWidth();
+ state.windowHeight = Utils.windowHeight();
+ this.state = state;
}
componentDidMount() {
this.mounted = true;
PostStore.addSearchChangeListener(this.onChange);
this.resize();
- var self = this;
- $(window).resize(function resize() {
- self.resize();
- });
+ window.addEventListener('resize', this.handleResize);
}
componentDidUpdate() {
@@ -41,22 +42,32 @@ export default class SearchResults extends React.Component {
componentWillUnmount() {
PostStore.removeSearchChangeListener(this.onChange);
this.mounted = false;
+ window.removeEventListener('resize', this.handleResize);
+ }
+
+ handleResize() {
+ this.setState({
+ windowWidth: Utils.windowWidth(),
+ windowHeight: Utils.windowHeight()
+ });
}
onChange() {
if (this.mounted) {
var newState = getStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areStatesEqual(newState, this.state)) {
this.setState(newState);
}
}
}
resize() {
- var height = $(window).height() - $('#error_bar').outerHeight() - 100;
+ var height = this.state.windowHeight - $('#error_bar').outerHeight() - 100;
$('#search-items-container').css('height', height + 'px');
$('#search-items-container').scrollTop(0);
- $('#search-items-container').perfectScrollbar();
+ if (this.state.windowWidth > 768) {
+ $('#search-items-container').perfectScrollbar();
+ }
}
render() {
diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx
index d2cbc798e..4f0fe3ed0 100644
--- a/web/react/components/setting_item_max.jsx
+++ b/web/react/components/setting_item_max.jsx
@@ -32,11 +32,17 @@ export default class SettingItemMax extends React.Component {
}
var inputs = this.props.inputs;
+ var widthClass;
+ if (this.props.width === 'full') {
+ widthClass = 'col-sm-12';
+ } else {
+ widthClass = 'col-sm-9 col-sm-offset-3';
+ }
return (
<ul className='section-max form-horizontal'>
<li className='col-sm-12 section-title'>{this.props.title}</li>
- <li className='col-sm-9 col-sm-offset-3'>
+ <li className={widthClass}>
<ul className='setting-list'>
<li className='setting-list-item'>
{inputs}
@@ -69,5 +75,6 @@ SettingItemMax.propTypes = {
extraInfo: React.PropTypes.element,
updateSection: React.PropTypes.func,
submit: React.PropTypes.func,
- title: React.PropTypes.string
+ title: React.PropTypes.string,
+ width: React.PropTypes.string
};
diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx
index c9031e67c..2f577fe39 100644
--- a/web/react/components/setting_picture.jsx
+++ b/web/react/components/setting_picture.jsx
@@ -12,7 +12,7 @@ export default class SettingPicture extends React.Component {
if (file) {
var reader = new FileReader();
- var img = React.findDOMNode(this.refs.image);
+ var img = ReactDOM.findDOMNode(this.refs.image);
reader.onload = function load(e) {
$(img).attr('src', e.target.result);
};
diff --git a/web/react/components/setting_upload.jsx b/web/react/components/setting_upload.jsx
index c7107b84d..a25789dff 100644
--- a/web/react/components/setting_upload.jsx
+++ b/web/react/components/setting_upload.jsx
@@ -37,7 +37,7 @@ export default class SettingsUpload extends React.Component {
doSubmit(e) {
e.preventDefault();
- var inputnode = React.findDOMNode(this.refs.uploadinput);
+ var inputnode = ReactDOM.findDOMNode(this.refs.uploadinput);
if (inputnode.files && inputnode.files[0]) {
this.props.submit(inputnode.files[0]);
} else {
diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx
index b5d2132d7..66568e1c8 100644
--- a/web/react/components/settings_sidebar.jsx
+++ b/web/react/components/settings_sidebar.jsx
@@ -10,7 +10,7 @@ export default class SettingsSidebar extends React.Component {
handleClick(tab, e) {
e.preventDefault();
this.props.updateTab(tab.name);
- $('.settings-modal').addClass('display--content');
+ $(e.target).closest('.settings-modal').addClass('display--content');
}
render() {
let tabList = this.props.tabs.map(function makeTab(tab) {
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 4ac1fd4a0..d1fe37300 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -1,19 +1,21 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var ChannelStore = require('../stores/channel_store.jsx');
-var Client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var SocketStore = require('../stores/socket_store.jsx');
-var UserStore = require('../stores/user_store.jsx');
-var TeamStore = require('../stores/team_store.jsx');
-var BrowserStore = require('../stores/browser_store.jsx');
-var Utils = require('../utils/utils.jsx');
-var SidebarHeader = require('./sidebar_header.jsx');
-var SearchBox = require('./search_bar.jsx');
-var Constants = require('../utils/constants.jsx');
-var NewChannelFlow = require('./new_channel_flow.jsx');
-var UnreadChannelIndicator = require('./unread_channel_indicator.jsx');
+const AsyncClient = require('../utils/async_client.jsx');
+const ChannelStore = require('../stores/channel_store.jsx');
+const Client = require('../utils/client.jsx');
+const Constants = require('../utils/constants.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
+const NewChannelFlow = require('./new_channel_flow.jsx');
+const MoreDirectChannels = require('./more_direct_channels.jsx');
+const SearchBox = require('./search_bar.jsx');
+const SidebarHeader = require('./sidebar_header.jsx');
+const TeamStore = require('../stores/team_store.jsx');
+const UnreadChannelIndicator = require('./unread_channel_indicator.jsx');
+const UserStore = require('../stores/user_store.jsx');
+const Utils = require('../utils/utils.jsx');
+const Tooltip = ReactBootstrap.Tooltip;
+const OverlayTrigger = ReactBootstrap.OverlayTrigger;
export default class Sidebar extends React.Component {
constructor(props) {
@@ -23,138 +25,117 @@ export default class Sidebar extends React.Component {
this.firstUnreadChannel = null;
this.lastUnreadChannel = null;
+ this.getStateFromStores = this.getStateFromStores.bind(this);
+
this.onChange = this.onChange.bind(this);
this.onScroll = this.onScroll.bind(this);
- this.onResize = this.onResize.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);
+ this.hideNewChannelModal = this.hideNewChannelModal.bind(this);
+ this.showMoreDirectChannelsModal = this.showMoreDirectChannelsModal.bind(this);
+ this.hideMoreDirectChannelsModal = this.hideMoreDirectChannelsModal.bind(this);
+
this.createChannelElement = this.createChannelElement.bind(this);
+ this.isLeaving = new Map();
+
const state = this.getStateFromStores();
- state.modal = '';
+ state.newChannelModalType = '';
+ state.showDirectChannelsModal = false;
state.loadingDMChannel = -1;
+ state.windowWidth = Utils.windowWidth();
this.state = state;
}
getStateFromStores() {
- var members = ChannelStore.getAllMembers();
+ const members = ChannelStore.getAllMembers();
var teamMemberMap = UserStore.getActiveOnlyProfiles();
var currentId = ChannelStore.getCurrentId();
+ const currentUserId = UserStore.getCurrentId();
var teammates = [];
for (var id in teamMemberMap) {
- if (id === UserStore.getCurrentId()) {
+ if (id === currentUserId) {
continue;
}
teammates.push(teamMemberMap[id]);
}
- // Create lists of all read and unread direct channels
- var showDirectChannels = [];
- var readDirectChannels = [];
+ const preferences = PreferenceStore.getPreferences(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
+
+ var visibleDirectChannels = [];
+ var hiddenDirectChannelCount = 0;
for (var i = 0; i < teammates.length; i++) {
- var teammate = teammates[i];
+ const teammate = teammates[i];
- if (teammate.id === UserStore.getCurrentId()) {
+ if (teammate.id === currentUserId) {
continue;
}
- var channelName = '';
- if (teammate.id > UserStore.getCurrentId()) {
- channelName = UserStore.getCurrentId() + '__' + teammate.id;
- } else {
- channelName = teammate.id + '__' + UserStore.getCurrentId();
- }
+ const channelName = Utils.getDirectChannelName(currentUserId, teammate.id);
- var channel = ChannelStore.getByName(channelName);
-
- if (channel == null) {
- var tempChannel = {};
- tempChannel.fake = true;
- tempChannel.name = channelName;
- tempChannel.display_name = teammate.username;
- tempChannel.teammate_username = teammate.username;
- tempChannel.status = UserStore.getStatus(teammate.id);
- tempChannel.last_post_at = 0;
- tempChannel.total_msg_count = 0;
- tempChannel.type = 'D';
- readDirectChannels.push(tempChannel);
- } else {
- channel.display_name = teammate.username;
- channel.teammate_username = teammate.username;
+ let forceShow = false;
+ let channel = ChannelStore.getByName(channelName);
- channel.status = UserStore.getStatus(teammate.id);
+ if (channel) {
+ const member = members[channel.id];
+ const msgCount = channel.total_msg_count - member.msg_count;
- var channelMember = members[channel.id];
- var msgCount = channel.total_msg_count - channelMember.msg_count;
- if (msgCount > 0) {
- showDirectChannels.push(channel);
- } else if (currentId === channel.id) {
- showDirectChannels.push(channel);
- } else {
- readDirectChannels.push(channel);
- }
+ // always show a channel if either it is the current one or if it is unread, but it is not currently being left
+ forceShow = (currentId === channel.id || msgCount > 0) && !this.isLeaving.get(channel.id);
+ } else {
+ channel = {};
+ channel.fake = true;
+ channel.name = channelName;
+ channel.last_post_at = 0;
+ channel.total_msg_count = 0;
+ channel.type = 'D';
}
- }
- // If we don't have MAX_DMS unread channels, sort the read list by last_post_at
- if (showDirectChannels.length < Constants.MAX_DMS) {
- readDirectChannels.sort(function sortByLastPost(a, b) {
- // sort by last_post_at first
- if (a.last_post_at > b.last_post_at) {
- return -1;
- }
- if (a.last_post_at < b.last_post_at) {
- return 1;
- }
+ channel.display_name = teammate.username;
+ channel.teammate_id = teammate.id;
+ channel.status = UserStore.getStatus(teammate.id);
- // if last_post_at is equal, sort by name
- if (a.display_name < b.display_name) {
- return -1;
- }
- if (a.display_name > b.display_name) {
- return 1;
- }
- return 0;
- });
+ if (preferences.some((preference) => (preference.name === teammate.id && preference.value !== 'false'))) {
+ visibleDirectChannels.push(channel);
+ } else if (forceShow) {
+ // make sure that unread direct channels are visible
+ const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true');
+ AsyncClient.savePreferences([preference]);
- var index = 0;
- while (showDirectChannels.length < Constants.MAX_DMS && index < readDirectChannels.length) {
- showDirectChannels.push(readDirectChannels[index]);
- index++;
+ visibleDirectChannels.push(channel);
+ } else {
+ hiddenDirectChannelCount += 1;
}
- readDirectChannels = readDirectChannels.slice(index);
-
- showDirectChannels.sort(function directSort(a, b) {
- if (a.display_name < b.display_name) {
- return -1;
- }
- if (a.display_name > b.display_name) {
- return 1;
- }
- return 0;
- });
}
+ visibleDirectChannels.sort(this.sortChannelsByDisplayName);
+
return {
activeId: currentId,
channels: ChannelStore.getAll(),
- members: members,
- showDirectChannels: showDirectChannels,
- hideDirectChannels: readDirectChannels
+ members,
+ visibleDirectChannels,
+ hiddenDirectChannelCount
};
}
+
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
UserStore.addChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange);
TeamStore.addChangeListener(this.onChange);
- SocketStore.addChangeListener(this.onSocketChange);
- $('.nav-pills__container').perfectScrollbar();
+ PreferenceStore.addChangeListener(this.onChange);
this.updateTitle();
this.updateUnreadIndicators();
+ this.updateScrollbar();
- $(window).on('resize', this.onResize);
+ window.addEventListener('resize', this.handleResize);
}
shouldComponentUpdate(nextProps, nextState) {
if (!Utils.areStatesEqual(nextProps, this.props)) {
@@ -169,15 +150,28 @@ export default class Sidebar extends React.Component {
componentDidUpdate() {
this.updateTitle();
this.updateUnreadIndicators();
+ this.updateScrollbar();
}
componentWillUnmount() {
- $(window).off('resize', this.onResize);
+ window.removeEventListener('resize', this.handleResize);
ChannelStore.removeChangeListener(this.onChange);
UserStore.removeChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onChange);
TeamStore.removeChangeListener(this.onChange);
- SocketStore.removeChangeListener(this.onSocketChange);
+ PreferenceStore.removeChangeListener(this.onChange);
+ }
+ handleResize() {
+ this.setState({
+ windowWidth: Utils.windowWidth(),
+ windowHeight: Utils.windowHeight()
+ });
+ }
+ updateScrollbar() {
+ if (this.state.windowWidth > 768) {
+ $('.nav-pills__container').perfectScrollbar();
+ $('.nav-pills__container').perfectScrollbar('update');
+ }
}
onChange() {
var newState = this.getStateFromStores();
@@ -185,94 +179,6 @@ export default class Sidebar extends React.Component {
this.setState(newState);
}
}
- onSocketChange(msg) {
- if (msg.action === 'posted') {
- if (ChannelStore.getCurrentId() === msg.channel_id) {
- if (window.isActive) {
- AsyncClient.updateLastViewedAt();
- }
- } else {
- AsyncClient.getChannels();
- }
-
- if (UserStore.getCurrentId() !== msg.user_id) {
- var mentions = [];
- if (msg.props.mentions) {
- mentions = JSON.parse(msg.props.mentions);
- }
- var channel = ChannelStore.get(msg.channel_id);
-
- const user = UserStore.getCurrentUser();
- const member = ChannelStore.getMember(msg.channel_id);
-
- var notifyLevel = member && member.notify_props ? member.notify_props.desktop : 'default';
- if (notifyLevel === 'default') {
- notifyLevel = user.notify_props.desktop;
- }
-
- if (notifyLevel === 'none') {
- return;
- } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== 'D') {
- return;
- }
-
- var username = 'Someone';
- if (UserStore.hasProfile(msg.user_id)) {
- username = UserStore.getProfile(msg.user_id).username;
- }
-
- var title = 'Posted';
- if (channel) {
- title = channel.display_name;
- }
-
- var repRegex = new RegExp('<br>', 'g');
- var post = JSON.parse(msg.props.post);
- var msgProps = msg.props;
- var notifyText = post.message.replace(repRegex, '\n').replace(/\n+/g, ' ').replace('<mention>', '').replace('</mention>', '');
-
- if (notifyText.length > 50) {
- notifyText = notifyText.substring(0, 49) + '...';
- }
-
- if (notifyText.length === 0) {
- if (msgProps.image) {
- Utils.notifyMe(title, username + ' uploaded an image', channel);
- } else if (msgProps.otherFile) {
- Utils.notifyMe(title, username + ' uploaded a file', channel);
- } else {
- Utils.notifyMe(title, username + ' did something new', channel);
- }
- } else {
- Utils.notifyMe(title, username + ' wrote: ' + notifyText, channel);
- }
- if (!user.notify_props || user.notify_props.desktop_sound === 'true') {
- Utils.ding();
- }
- }
- } else if (msg.action === 'viewed') {
- if (ChannelStore.getCurrentId() !== msg.channel_id && UserStore.getCurrentId() === msg.user_id) {
- AsyncClient.getChannel(msg.channel_id);
- }
- } else if (msg.action === 'user_added') {
- if (UserStore.getCurrentId() === msg.user_id) {
- AsyncClient.getChannel(msg.channel_id);
- }
- } else if (msg.action === 'user_removed') {
- if (msg.user_id === UserStore.getCurrentId()) {
- AsyncClient.getChannels(true);
-
- if (msg.props.remover !== msg.user_id && msg.props.channel_id === ChannelStore.getCurrentId() && $('#removed_from_channel').length > 0) {
- var sentState = {};
- sentState.channelName = ChannelStore.getCurrent().display_name;
- sentState.remover = UserStore.getProfile(msg.props.remover).username;
-
- BrowserStore.setItem('channel-removed-state', sentState);
- $('#removed_from_channel').modal('show');
- }
- }
- }
- }
updateTitle() {
const channel = ChannelStore.getCurrent();
if (channel) {
@@ -292,17 +198,14 @@ export default class Sidebar extends React.Component {
onScroll() {
this.updateUnreadIndicators();
}
- onResize() {
- this.updateUnreadIndicators();
- }
updateUnreadIndicators() {
- const container = $(React.findDOMNode(this.refs.container));
+ const container = $(ReactDOM.findDOMNode(this.refs.container));
var showTopUnread = false;
var showBottomUnread = false;
if (this.firstUnreadChannel) {
- var firstUnreadElement = $(React.findDOMNode(this.refs[this.firstUnreadChannel]));
+ var firstUnreadElement = $(ReactDOM.findDOMNode(this.refs[this.firstUnreadChannel]));
if (firstUnreadElement.position().top + firstUnreadElement.height() < 0) {
showTopUnread = true;
@@ -310,7 +213,7 @@ export default class Sidebar extends React.Component {
}
if (this.lastUnreadChannel) {
- var lastUnreadElement = $(React.findDOMNode(this.refs[this.lastUnreadChannel]));
+ var lastUnreadElement = $(ReactDOM.findDOMNode(this.refs[this.lastUnreadChannel]));
if (lastUnreadElement.position().top > container.height()) {
showBottomUnread = true;
@@ -322,7 +225,51 @@ export default class Sidebar extends React.Component {
showBottomUnread
});
}
- createChannelElement(channel, index) {
+
+ handleLeaveDirectChannel(channel) {
+ if (!this.isLeaving.get(channel.id)) {
+ this.isLeaving.set(channel.id, true);
+
+ const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, channel.teammate_id, 'false');
+
+ // bypass AsyncClient since we've already saved the updated preferences
+ Client.savePreferences(
+ [preference],
+ () => {
+ this.isLeaving.set(channel.id, false);
+ },
+ () => {
+ this.isLeaving.set(channel.id, false);
+ }
+ );
+
+ this.setState(this.getStateFromStores());
+ }
+
+ if (channel.id === this.state.activeId) {
+ Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL));
+ }
+ }
+
+ sortChannelsByDisplayName(a, b) {
+ return a.display_name.localeCompare(b.display_name);
+ }
+
+ showNewChannelModal(type) {
+ this.setState({newChannelModalType: type});
+ }
+ hideNewChannelModal() {
+ this.setState({newChannelModalType: ''});
+ }
+
+ showMoreDirectChannelsModal() {
+ this.setState({showDirectChannelsModal: true});
+ }
+ hideMoreDirectChannelsModal() {
+ this.setState({showDirectChannelsModal: false});
+ }
+
+ createChannelElement(channel, index, arr, handleClose) {
var members = this.state.members;
var activeId = this.state.activeId;
var channelMember = members[channel.id];
@@ -333,15 +280,16 @@ export default class Sidebar extends React.Component {
linkClass = 'active';
}
+ let rowClass = 'sidebar-channel';
+
var unread = false;
if (channelMember) {
msgCount = channel.total_msg_count - channelMember.msg_count;
unread = (msgCount > 0 && channelMember.notify_props.mark_unread !== 'mention') || channelMember.mention_count > 0;
}
- var titleClass = '';
if (unread) {
- titleClass = 'unread-title';
+ rowClass += ' unread-title';
if (channel.id !== activeId) {
if (!this.firstUnreadChannel) {
@@ -374,9 +322,8 @@ export default class Sidebar extends React.Component {
);
}
- var badgeClass;
if (msgCount > 0) {
- badgeClass = 'has-badge';
+ rowClass += ' has-badge';
}
// set up status icon for direct message channels
@@ -405,8 +352,13 @@ export default class Sidebar extends React.Component {
if (!channel.fake) {
handleClick = function clickHandler(e) {
+ if (e.target.attributes.getNamedItem('data-close')) {
+ handleClose(channel);
+ } else {
+ Utils.switchChannel(channel);
+ }
+
e.preventDefault();
- Utils.switchChannel(channel);
};
} else if (channel.fake && teamURL) {
// It's a direct message channel that doesn't exist yet so let's create it now
@@ -415,23 +367,51 @@ export default class Sidebar extends React.Component {
if (this.state.loadingDMChannel === -1) {
handleClick = function clickHandler(e) {
e.preventDefault();
- this.setState({loadingDMChannel: index});
-
- Client.createDirectChannel(channel, otherUserId,
- function success(data) {
- this.setState({loadingDMChannel: -1});
- AsyncClient.getChannel(data.id);
- Utils.switchChannel(data);
- }.bind(this),
- function error() {
- this.setState({loadingDMChannel: -1});
- window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name;
- }.bind(this)
- );
+
+ if (e.target.attributes.getNamedItem('data-close')) {
+ handleClose(channel);
+ } else {
+ this.setState({loadingDMChannel: index});
+
+ Client.createDirectChannel(channel, otherUserId,
+ (data) => {
+ this.setState({loadingDMChannel: -1});
+ AsyncClient.getChannel(data.id);
+ Utils.switchChannel(data);
+ },
+ () => {
+ this.setState({loadingDMChannel: -1});
+ window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name;
+ }
+ );
+ }
}.bind(this);
}
}
+ let closeButton = null;
+ const removeTooltip = (
+ <Tooltip id='remove-dm-tooltip'>{'Remove from list'}</Tooltip>
+ );
+ if (handleClose && !badge) {
+ closeButton = (
+ <OverlayTrigger
+ delayShow={1000}
+ placement='top'
+ overlay={removeTooltip}
+ >
+ <span
+ className='btn-close'
+ data-close='true'
+ >
+ {'×'}
+ </span>
+ </OverlayTrigger>
+ );
+
+ rowClass += ' has-close';
+ }
+
return (
<li
key={channel.name}
@@ -439,13 +419,14 @@ export default class Sidebar extends React.Component {
className={linkClass}
>
<a
- className={'sidebar-channel ' + titleClass + ' ' + badgeClass}
+ className={rowClass}
href={href}
onClick={handleClick}
>
{status}
{channel.display_name}
{badge}
+ {closeButton}
</a>
</li>
);
@@ -464,7 +445,9 @@ export default class Sidebar extends React.Component {
const privateChannels = this.state.channels.filter((channel) => channel.type === 'P');
const privateChannelItems = privateChannels.map(this.createChannelElement);
- const directMessageItems = this.state.showDirectChannels.map(this.createChannelElement);
+ const directMessageItems = this.state.visibleDirectChannels.map((channel, index, arr) => {
+ return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);
+ });
// update the favicon to show if there are any notifications
var link = document.createElement('link');
@@ -484,34 +467,43 @@ export default class Sidebar extends React.Component {
head.appendChild(link);
var directMessageMore = null;
- if (this.state.hideDirectChannels.length > 0) {
+ if (this.state.hiddenDirectChannelCount > 0) {
directMessageMore = (
- <li>
+ <li key='more'>
<a
href='#'
- data-toggle='modal'
- className='nav-more'
- data-target='#more_direct_channels'
- data-channels={JSON.stringify(this.state.hideDirectChannels)}
+ onClick={this.showMoreDirectChannelsModal}
>
- {'More (' + this.state.hideDirectChannels.length + ')'}
+ {'More (' + this.state.hiddenDirectChannelCount + ')'}
</a>
</li>
);
}
let showChannelModal = false;
- if (this.state.modal !== '') {
+ if (this.state.newChannelModalType !== '') {
showChannelModal = true;
}
+ const createChannelTootlip = (
+ <Tooltip id='new-channel-tooltip' >{'Create new channel'}</Tooltip>
+ );
+ const createGroupTootlip = (
+ <Tooltip id='new-group-tooltip'>{'Create new group'}</Tooltip>
+ );
+
return (
<div>
<NewChannelFlow
show={showChannelModal}
- channelType={this.state.modal}
- onModalDismissed={() => this.setState({modal: ''})}
+ channelType={this.state.newChannelModalType}
+ onModalDismissed={this.hideNewChannelModal}
+ />
+ <MoreDirectChannels
+ show={this.state.showDirectChannelsModal}
+ onModalDismissed={this.hideMoreDirectChannelsModal}
/>
+
<SidebarHeader
teamDisplayName={this.props.teamDisplayName}
teamName={this.props.teamName}
@@ -538,14 +530,20 @@ export default class Sidebar extends React.Component {
<ul className='nav nav-pills nav-stacked'>
<li>
<h4>
- Channels
+ {'Channels'}
+ <OverlayTrigger
+ delayShow={500}
+ placement='top'
+ overlay={createChannelTootlip}
+ >
<a
className='add-channel-btn'
href='#'
- onClick={() => this.setState({modal: 'O'})}
+ onClick={this.showNewChannelModal.bind(this, 'O')}
>
{'+'}
</a>
+ </OverlayTrigger>
</h4>
</li>
{publicChannelItems}
@@ -557,7 +555,7 @@ export default class Sidebar extends React.Component {
data-target='#more_channels'
data-channeltype='O'
>
- More...
+ {'More...'}
</a>
</li>
</ul>
@@ -565,20 +563,26 @@ export default class Sidebar extends React.Component {
<ul className='nav nav-pills nav-stacked'>
<li>
<h4>
- Private Groups
+ {'Private Groups'}
+ <OverlayTrigger
+ delayShow={500}
+ placement='top'
+ overlay={createGroupTootlip}
+ >
<a
className='add-channel-btn'
href='#'
- onClick={() => this.setState({modal: 'P'})}
+ onClick={this.showNewChannelModal.bind(this, 'P')}
>
{'+'}
</a>
+ </OverlayTrigger>
</h4>
</li>
{privateChannelItems}
</ul>
<ul className='nav nav-pills nav-stacked'>
- <li><h4>Direct Messages</h4></li>
+ <li><h4>{'Direct Messages'}</h4></li>
{directMessageItems}
{directMessageMore}
</ul>
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index ea2bcf9a4..ac101d631 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -75,7 +75,7 @@ export default class SidebarRightMenu extends React.Component {
data-toggle='modal'
data-target='#team_members'
>
- <i className='glyphicon glyphicon-wrench'></i>Manage Team</a>
+ <i className='glyphicon glyphicon-wrench'></i>Manage Members</a>
</li>
);
}
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index 75661f812..f74c29d27 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -28,7 +28,7 @@ export default class SignupUserComplete extends React.Component {
handleSubmit(e) {
e.preventDefault();
- const providedEmail = React.findDOMNode(this.refs.email).value.trim();
+ const providedEmail = ReactDOM.findDOMNode(this.refs.email).value.trim();
if (!providedEmail) {
this.setState({nameError: '', emailError: 'This field is required', passwordError: ''});
return;
@@ -39,7 +39,7 @@ export default class SignupUserComplete extends React.Component {
return;
}
- const providedUsername = React.findDOMNode(this.refs.name).value.trim().toLowerCase();
+ const providedUsername = ReactDOM.findDOMNode(this.refs.name).value.trim().toLowerCase();
if (!providedUsername) {
this.setState({nameError: 'This field is required', emailError: '', passwordError: '', serverError: ''});
return;
@@ -59,7 +59,7 @@ export default class SignupUserComplete extends React.Component {
return;
}
- const providedPassword = React.findDOMNode(this.refs.password).value.trim();
+ const providedPassword = ReactDOM.findDOMNode(this.refs.password).value.trim();
if (!providedPassword || providedPassword.length < 5) {
this.setState({nameError: '', emailError: '', passwordError: 'Please enter at least 5 characters', serverError: ''});
return;
@@ -149,7 +149,7 @@ export default class SignupUserComplete extends React.Component {
// set up the email entry and hide it if an email was provided
var yourEmailIs = '';
if (this.state.user.email) {
- yourEmailIs = <span>Your email address is {this.state.user.email}. You'll use this address to sign in to {global.window.config.SiteName}.</span>;
+ yourEmailIs = <span>Your email address is <strong>{this.state.user.email}</strong>. You'll use this address to sign in to {global.window.config.SiteName}.</span>;
}
var emailContainerStyle = 'margin--extra';
@@ -169,6 +169,7 @@ export default class SignupUserComplete extends React.Component {
placeholder=''
maxLength='128'
autoFocus={true}
+ spellCheck='false'
/>
{emailError}
</div>
@@ -204,9 +205,10 @@ export default class SignupUserComplete extends React.Component {
className='form-control'
placeholder=''
maxLength='128'
+ spellCheck='false'
/>
{nameError}
- <p className='form__hint'>Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'</p>
+ <span className='help-block'>Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'</span>
</div>
</div>
<div className='margin--extra'>
@@ -218,6 +220,7 @@ export default class SignupUserComplete extends React.Component {
className='form-control'
placeholder=''
maxLength='128'
+ spellCheck='false'
/>
{passwordError}
</div>
diff --git a/web/react/components/team_import_tab.jsx b/web/react/components/team_import_tab.jsx
index 40f06c382..a80b1a472 100644
--- a/web/react/components/team_import_tab.jsx
+++ b/web/react/components/team_import_tab.jsx
@@ -34,14 +34,14 @@ export default class TeamImportTab extends React.Component {
render() {
var uploadHelpText = (
<div>
- <p>{'Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team\'\s public channels.'}</p>
- <p>{'The Slack import to Mattermost is in "Preview". Slack bot posts do not yet import and Slack @mentions are not currently supported.'}</p>
+ <p>{'To import a team from Slack go to Slack > Team Settings > Import/Export Data > Export > Start Export. Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team\'\s public channels.'}</p>
+ <p>{'The Slack import to Mattermost is in "Beta". Slack bot posts do not yet import and Slack @mentions are not currently supported.'}</p>
</div>
);
var uploadSection = (
<SettingUpload
- title='Import from Slack'
+ title='Import from Slack (Beta)'
submit={this.doImportSlack}
helpText={uploadHelpText}
fileTypesAccepted='.zip'
diff --git a/web/react/components/team_members.jsx b/web/react/components/team_members.jsx
index 0f60d93b2..33590c89a 100644
--- a/web/react/components/team_members.jsx
+++ b/web/react/components/team_members.jsx
@@ -44,11 +44,11 @@ export default class TeamMembers extends React.Component {
UserStore.addChangeListener(this.onChange);
var self = this;
- $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function show() {
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function show() {
self.setState({render_members: false});
});
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function hide() {
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', function hide() {
self.setState({render_members: true});
});
}
diff --git a/web/react/components/team_signup_display_name_page.jsx b/web/react/components/team_signup_display_name_page.jsx
index 65da4bc96..4d08274e4 100644
--- a/web/react/components/team_signup_display_name_page.jsx
+++ b/web/react/components/team_signup_display_name_page.jsx
@@ -21,7 +21,7 @@ export default class TeamSignupDisplayNamePage extends React.Component {
submitNext(e) {
e.preventDefault();
- var displayName = React.findDOMNode(this.refs.name).value.trim();
+ var displayName = ReactDOM.findDOMNode(this.refs.name).value.trim();
if (!displayName) {
this.setState({nameError: 'This field is required'});
return;
@@ -66,6 +66,7 @@ export default class TeamSignupDisplayNamePage extends React.Component {
defaultValue={this.props.state.team.display_name}
autoFocus={true}
onFocus={this.handleFocus}
+ spellCheck='false'
/>
</div>
</div>
diff --git a/web/react/components/team_signup_email_item.jsx b/web/react/components/team_signup_email_item.jsx
index 219f14eef..1d2b24ed7 100644
--- a/web/react/components/team_signup_email_item.jsx
+++ b/web/react/components/team_signup_email_item.jsx
@@ -13,10 +13,10 @@ export default class TeamSignupEmailItem extends React.Component {
this.state = {};
}
getValue() {
- return React.findDOMNode(this.refs.email).value.trim();
+ return ReactDOM.findDOMNode(this.refs.email).value.trim();
}
validate(teamEmail) {
- const email = React.findDOMNode(this.refs.email).value.trim().toLowerCase();
+ const email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase();
if (!email) {
return true;
@@ -51,6 +51,7 @@ export default class TeamSignupEmailItem extends React.Component {
placeholder='Email Address'
defaultValue={this.props.email}
maxLength='128'
+ spellCheck='false'
/>
{emailError}
</div>
diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx
index dce8105ca..daa898b53 100644
--- a/web/react/components/team_signup_password_page.jsx
+++ b/web/react/components/team_signup_password_page.jsx
@@ -22,7 +22,7 @@ export default class TeamSignupPasswordPage extends React.Component {
submitNext(e) {
e.preventDefault();
- var password = React.findDOMNode(this.refs.password).value.trim();
+ var password = ReactDOM.findDOMNode(this.refs.password).value.trim();
if (!password || password.length < 5) {
this.setState({passwordError: 'Please enter at least 5 characters'});
return;
@@ -109,8 +109,9 @@ export default class TeamSignupPasswordPage extends React.Component {
className='form-control'
placeholder=''
maxLength='128'
+ spellCheck='false'
/>
- <div className='color--light form__hint'>Passwords must contain 5 to 50 characters. Your password will be strongest if it contains a mix of symbols, numbers, and upper and lowercase characters.</div>
+ <span className='color--light help-block'>Passwords must contain 5 to 50 characters. Your password will be strongest if it contains a mix of symbols, numbers, and upper and lowercase characters.</span>
</div>
</div>
{passwordError}
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx
index 398a52f7d..67e4c9dd7 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/team_signup_url_page.jsx
@@ -23,7 +23,7 @@ export default class TeamSignupUrlPage extends React.Component {
submitNext(e) {
e.preventDefault();
- const name = React.findDOMNode(this.refs.name).value.trim();
+ const name = ReactDOM.findDOMNode(this.refs.name).value.trim();
if (!name) {
this.setState({nameError: 'This field is required'});
return;
@@ -111,6 +111,7 @@ export default class TeamSignupUrlPage extends React.Component {
defaultValue={this.props.state.team.name}
autoFocus={true}
onFocus={this.handleFocus}
+ spellCheck='false'
/>
</div>
</div>
diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/team_signup_username_page.jsx
index 53a389c4c..21e76e2b8 100644
--- a/web/react/components/team_signup_username_page.jsx
+++ b/web/react/components/team_signup_username_page.jsx
@@ -15,13 +15,18 @@ export default class TeamSignupUsernamePage extends React.Component {
}
submitBack(e) {
e.preventDefault();
- this.props.state.wizard = 'send_invites';
+ if (global.window.config.SendEmailNotifications === 'true') {
+ this.props.state.wizard = 'send_invites';
+ } else {
+ this.props.state.wizard = 'team_url';
+ }
+
this.props.updateParent(this.props.state);
}
submitNext(e) {
e.preventDefault();
- var name = React.findDOMNode(this.refs.name).value.trim().toLowerCase();
+ var name = ReactDOM.findDOMNode(this.refs.name).value.trim().toLowerCase();
var usernameError = Utils.isValidUsername(name);
if (usernameError === 'Cannot use a reserved word as a username.') {
@@ -68,8 +73,9 @@ export default class TeamSignupUsernamePage extends React.Component {
placeholder=''
defaultValue={this.props.state.user.username}
maxLength='128'
+ spellCheck='false'
/>
- <div className='color--light form__hint'>Usernames must begin with a letter and contain 3 to 15 characters made up of lowercase letters, numbers, and the symbols '.', '-' and '_'</div>
+ <span className='color--light help-block'>Usernames must begin with a letter and contain 3 to 15 characters made up of lowercase letters, numbers, and the symbols '.', '-' and '_'</span>
</div>
</div>
{nameError}
diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/team_signup_welcome_page.jsx
index 78a41eed5..1e9d8df0a 100644
--- a/web/react/components/team_signup_welcome_page.jsx
+++ b/web/react/components/team_signup_welcome_page.jsx
@@ -36,7 +36,7 @@ export default class TeamSignupWelcomePage extends React.Component {
var state = {useDiff: true, serverError: ''};
- var email = React.findDOMNode(this.refs.email).value.trim().toLowerCase();
+ var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase();
if (!email || !Utils.isEmail(email)) {
state.emailError = 'Please enter a valid email address';
this.setState(state);
@@ -145,6 +145,7 @@ export default class TeamSignupWelcomePage extends React.Component {
className='form-control'
placeholder='Email Address'
maxLength='128'
+ spellCheck='false'
/>
</div>
</div>
diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx
index 9376a4564..ff4ccd4d8 100644
--- a/web/react/components/team_signup_with_email.jsx
+++ b/web/react/components/team_signup_with_email.jsx
@@ -17,7 +17,7 @@ export default class EmailSignUpPage extends React.Component {
var team = {};
var state = {serverError: ''};
- team.email = React.findDOMNode(this.refs.email).value.trim().toLowerCase();
+ team.email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase();
if (!team.email || !Utils.isEmail(team.email)) {
state.emailError = 'Please enter a valid email address';
state.inValid = true;
@@ -63,6 +63,7 @@ export default class EmailSignUpPage extends React.Component {
className='form-control'
placeholder='Email Address'
maxLength='128'
+ spellCheck='false'
/>
</div>
<div className='form-group'>
diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx
index 011bfebff..a0ccdf2c7 100644
--- a/web/react/components/team_signup_with_sso.jsx
+++ b/web/react/components/team_signup_with_sso.jsx
@@ -54,7 +54,7 @@ export default class SSOSignUpPage extends React.Component {
);
}
nameChange() {
- this.setState({name: React.findDOMNode(this.refs.teamname).value.trim()});
+ this.setState({name: ReactDOM.findDOMNode(this.refs.teamname).value.trim()});
}
render() {
var nameError = null;
@@ -104,6 +104,7 @@ export default class SSOSignUpPage extends React.Component {
placeholder='Enter name of new team'
maxLength='128'
onChange={this.nameChange}
+ spellCheck='false'
/>
{nameError}
</div>
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index 0563c294a..86bb42f62 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -9,6 +9,7 @@ const ErrorStore = require('../stores/error_store.jsx');
const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
const ActionTypes = Constants.ActionTypes;
+const KeyCodes = Constants.KeyCodes;
export default class Textbox extends React.Component {
constructor(props) {
@@ -83,7 +84,7 @@ export default class Textbox extends React.Component {
componentDidUpdate() {
if (this.caret >= 0) {
- Utils.setCaretPosition(React.findDOMNode(this.refs.message), this.caret);
+ Utils.setCaretPosition(ReactDOM.findDOMNode(this.refs.message), this.caret);
this.caret = -1;
}
if (this.doProcessMentions) {
@@ -97,7 +98,7 @@ export default class Textbox extends React.Component {
if (!this.addedMention) {
this.checkForNewMention(nextProps.messageText);
}
- const text = React.findDOMNode(this.refs.message).value;
+ const text = ReactDOM.findDOMNode(this.refs.message).value;
if (nextProps.channelId !== this.props.channelId || nextProps.messageText !== text) {
this.doProcessMentions = true;
}
@@ -117,11 +118,11 @@ export default class Textbox extends React.Component {
}
handleChange() {
- this.props.onUserInput(React.findDOMNode(this.refs.message).value);
+ this.props.onUserInput(ReactDOM.findDOMNode(this.refs.message).value);
}
handleKeyPress(e) {
- const text = React.findDOMNode(this.refs.message).value;
+ const text = ReactDOM.findDOMNode(this.refs.message).value;
if (!this.refs.commands.isEmpty() && text.indexOf('/') === 0 && e.which === 13) {
this.refs.commands.addFirstCommand();
@@ -130,7 +131,7 @@ export default class Textbox extends React.Component {
}
if (!this.doProcessMentions) {
- const caret = Utils.getCaretPosition(React.findDOMNode(this.refs.message));
+ const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
const preText = text.substring(0, caret);
const lastSpace = preText.lastIndexOf(' ');
const lastAt = preText.lastIndexOf('@');
@@ -144,17 +145,19 @@ export default class Textbox extends React.Component {
}
handleKeyDown(e) {
- if (Utils.getSelectedText(React.findDOMNode(this.refs.message)) !== '') {
+ if (Utils.getSelectedText(ReactDOM.findDOMNode(this.refs.message)) !== '') {
this.doProcessMentions = true;
}
- if (e.keyCode === 8) {
+ if (e.keyCode === KeyCodes.BACKSPACE) {
this.handleBackspace(e);
+ } else if (this.props.onKeyDown) {
+ this.props.onKeyDown(e);
}
}
handleBackspace() {
- const text = React.findDOMNode(this.refs.message).value;
+ const text = ReactDOM.findDOMNode(this.refs.message).value;
if (text.indexOf('/') === 0) {
this.refs.commands.getSuggestedCommands(text.substring(0, text.length - 1));
}
@@ -163,7 +166,7 @@ export default class Textbox extends React.Component {
return;
}
- const caret = Utils.getCaretPosition(React.findDOMNode(this.refs.message));
+ const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
const preText = text.substring(0, caret);
const lastSpace = preText.lastIndexOf(' ');
const lastAt = preText.lastIndexOf('@');
@@ -174,7 +177,7 @@ export default class Textbox extends React.Component {
}
checkForNewMention(text) {
- const caret = Utils.getCaretPosition(React.findDOMNode(this.refs.message));
+ const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
const preText = text.substring(0, caret);
@@ -201,7 +204,7 @@ export default class Textbox extends React.Component {
}
addMention(name) {
- const caret = Utils.getCaretPosition(React.findDOMNode(this.refs.message));
+ const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
const text = this.props.messageText;
@@ -224,14 +227,14 @@ export default class Textbox extends React.Component {
}
addCommand(cmd) {
- const elm = React.findDOMNode(this.refs.message);
+ const elm = ReactDOM.findDOMNode(this.refs.message);
elm.value = cmd;
this.handleChange();
}
resize() {
- const e = React.findDOMNode(this.refs.message);
- const w = React.findDOMNode(this.refs.wrapper);
+ const e = ReactDOM.findDOMNode(this.refs.message);
+ const w = ReactDOM.findDOMNode(this.refs.wrapper);
const prevHeight = $(e).height();
@@ -246,9 +249,11 @@ export default class Textbox extends React.Component {
if (e.scrollHeight - mod < 167) {
$(e).css({height: 'auto', 'overflow-y': 'hidden'}).height(e.scrollHeight - mod);
$(w).css({height: 'auto'}).height(e.scrollHeight + 2);
+ $(w).closest('.post-body__cell').removeClass('scroll');
} else {
$(e).css({height: 'auto', 'overflow-y': 'scroll'}).height(167);
$(w).css({height: 'auto'}).height(167);
+ $(w).closest('.post-body__cell').addClass('scroll');
}
if (prevHeight !== $(e).height() && this.props.onHeightChange) {
@@ -257,14 +262,14 @@ export default class Textbox extends React.Component {
}
handleFocus() {
- const elm = React.findDOMNode(this.refs.message);
+ const elm = ReactDOM.findDOMNode(this.refs.message);
if (elm.title === elm.value) {
elm.value = '';
}
}
handleBlur() {
- const elm = React.findDOMNode(this.refs.message);
+ const elm = ReactDOM.findDOMNode(this.refs.message);
if (elm.value === '') {
elm.value = elm.title;
}
@@ -316,5 +321,6 @@ Textbox.propTypes = {
onUserInput: React.PropTypes.func.isRequired,
onKeyPress: React.PropTypes.func.isRequired,
onHeightChange: React.PropTypes.func,
- createMessage: React.PropTypes.string.isRequired
+ createMessage: React.PropTypes.string.isRequired,
+ onKeyDown: React.PropTypes.func
};
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index 948f06444..540331663 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -3,6 +3,8 @@
var Utils = require('../utils/utils.jsx');
var UserStore = require('../stores/user_store.jsx');
+var Popover = ReactBootstrap.Popover;
+var OverlayTrigger = ReactBootstrap.OverlayTrigger;
var id = 0;
@@ -32,7 +34,6 @@ export default class UserProfile extends React.Component {
componentDidMount() {
UserStore.addChangeListener(this.onChange);
if (!this.props.disablePopover) {
- $('#profile_' + this.uniqueId).popover({placement: 'right', container: 'body', trigger: 'hover', html: true, delay: {show: 200, hide: 100}});
$('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'});
}
}
@@ -62,23 +63,63 @@ export default class UserProfile extends React.Component {
return <div>{name}</div>;
}
- var dataContent = '<img class="user-popover__image" src="/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '" height="128" width="128" />';
+ var dataContent = [];
+ dataContent.push(
+ <img
+ className='user-popover__image'
+ src={'/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at}
+ height='128'
+ width='128'
+ key='user-popover-image'
+ />
+ );
if (!global.window.config.ShowEmailAddress === 'true') {
- dataContent += '<div class="text-nowrap">Email not shared</div>';
+ dataContent.push(
+ <div
+ className='text-nowrap'
+ key='user-popover-no-email'
+ >
+ {'Email not shared'}
+ </div>
+ );
} else {
- dataContent += '<div data-toggle="tooltip" title="' + this.state.profile.email + '"><a href="mailto:' + this.state.profile.email + '" class="text-nowrap text-lowercase user-popover__email">' + this.state.profile.email + '</a></div>';
+ dataContent.push(
+ <div
+ data-toggle='tooltip'
+ title={this.state.profile.email}
+ key='user-popover-email'
+ >
+ <a
+ href={'mailto:' + this.state.profile.email}
+ className='text-nowrap text-lowercase user-popover__email'
+ >
+ {this.state.profile.email}
+ </a>
+ </div>
+ );
}
return (
- <div
- className='user-popover'
- id={'profile_' + this.uniqueId}
- data-toggle='popover'
- data-content={dataContent}
- data-original-title={this.state.profile.username}
+ <OverlayTrigger
+ trigger='click'
+ placement='right'
+ rootClose={true}
+ overlay={
+ <Popover
+ title={this.state.profile.username}
+ id='user-profile-popover'
+ >
+ {dataContent}
+ </Popover>
+ }
>
- {name}
- </div>
+ <div
+ className='user-popover'
+ id={'profile_' + this.uniqueId}
+ >
+ {name}
+ </div>
+ </OverlayTrigger>
);
}
}
diff --git a/web/react/components/user_settings/import_theme_modal.jsx b/web/react/components/user_settings/import_theme_modal.jsx
index de0663874..1a9ac0ad3 100644
--- a/web/react/components/user_settings/import_theme_modal.jsx
+++ b/web/react/components/user_settings/import_theme_modal.jsx
@@ -35,7 +35,7 @@ export default class ImportThemeModal extends React.Component {
handleSubmit(e) {
e.preventDefault();
- const text = React.findDOMNode(this.refs.input).value;
+ const text = ReactDOM.findDOMNode(this.refs.input).value;
if (!this.isInputValid(text)) {
this.setState({inputError: 'Invalid format, please try copying and pasting in again.'});
diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx
new file mode 100644
index 000000000..e83ae3bd6
--- /dev/null
+++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx
@@ -0,0 +1,262 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Constants = require('../../utils/constants.jsx');
+var ChannelStore = require('../../stores/channel_store.jsx');
+var LoadingScreen = require('../loading_screen.jsx');
+
+export default class ManageOutgoingHooks extends React.Component {
+ constructor() {
+ super();
+
+ this.getHooks = this.getHooks.bind(this);
+ this.addNewHook = this.addNewHook.bind(this);
+ this.updateChannelId = this.updateChannelId.bind(this);
+ this.updateTriggerWords = this.updateTriggerWords.bind(this);
+ this.updateCallbackURLs = this.updateCallbackURLs.bind(this);
+
+ this.state = {hooks: [], channelId: '', triggerWords: '', callbackURLs: '', getHooksComplete: false};
+ }
+ componentDidMount() {
+ this.getHooks();
+ }
+ addNewHook(e) {
+ e.preventDefault();
+
+ if ((this.state.channelId === '' && this.state.triggerWords === '') ||
+ this.state.callbackURLs === '') {
+ return;
+ }
+
+ const hook = {};
+ hook.channel_id = this.state.channelId;
+ if (this.state.triggerWords.length !== 0) {
+ hook.trigger_words = this.state.triggerWords.trim().split(',');
+ }
+ hook.callback_urls = this.state.callbackURLs.split('\n');
+
+ Client.addOutgoingHook(
+ hook,
+ (data) => {
+ let hooks = Object.assign([], this.state.hooks);
+ if (!hooks) {
+ hooks = [];
+ }
+ hooks.push(data);
+ this.setState({hooks, serverError: null, channelId: '', triggerWords: '', callbackURLs: ''});
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ removeHook(id) {
+ const data = {};
+ data.id = id;
+
+ Client.deleteOutgoingHook(
+ data,
+ () => {
+ const hooks = this.state.hooks;
+ let index = -1;
+ for (let i = 0; i < hooks.length; i++) {
+ if (hooks[i].id === id) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index !== -1) {
+ hooks.splice(index, 1);
+ }
+
+ this.setState({hooks});
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ regenToken(id) {
+ const regenData = {};
+ regenData.id = id;
+
+ Client.regenOutgoingHookToken(
+ regenData,
+ (data) => {
+ const hooks = Object.assign([], this.state.hooks);
+ for (let i = 0; i < hooks.length; i++) {
+ if (hooks[i].id === id) {
+ hooks[i] = data;
+ break;
+ }
+ }
+
+ this.setState({hooks, serverError: null});
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ getHooks() {
+ Client.listOutgoingHooks(
+ (data) => {
+ if (data) {
+ this.setState({hooks: data, getHooksComplete: true, serverError: null});
+ }
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ updateChannelId(e) {
+ this.setState({channelId: e.target.value});
+ }
+ updateTriggerWords(e) {
+ this.setState({triggerWords: e.target.value});
+ }
+ updateCallbackURLs(e) {
+ this.setState({callbackURLs: e.target.value});
+ }
+ render() {
+ let serverError;
+ if (this.state.serverError) {
+ serverError = <label className='has-error'>{this.state.serverError}</label>;
+ }
+
+ const channels = ChannelStore.getAll();
+ const options = [<option value=''>{'--- Select a channel ---'}</option>];
+ channels.forEach((channel) => {
+ if (channel.type === Constants.OPEN_CHANNEL) {
+ options.push(<option value={channel.id}>{channel.name}</option>);
+ }
+ });
+
+ const hooks = [];
+ this.state.hooks.forEach((hook) => {
+ const c = ChannelStore.get(hook.channel_id);
+ let channelDiv;
+ if (c) {
+ channelDiv = (
+ <div className='padding-top'>
+ <strong>{'Channel: '}</strong>{c.name}
+ </div>
+ );
+ }
+
+ let triggerDiv;
+ if (hook.trigger_words && hook.trigger_words.length !== 0) {
+ triggerDiv = (
+ <div className='padding-top'>
+ <strong>{'Trigger Words: '}</strong>{hook.trigger_words.join(', ')}
+ </div>
+ );
+ }
+
+ hooks.push(
+ <div className='font--small'>
+ <div className='padding-top x2 divider-light'></div>
+ <div className='padding-top x2'>
+ <strong>{'URLs: '}</strong><span className='word-break--all'>{hook.callback_urls.join(', ')}</span>
+ </div>
+ {channelDiv}
+ {triggerDiv}
+ <div className='padding-top'>
+ <strong>{'Token: '}</strong>{hook.token}
+ </div>
+ <div className='padding-top'>
+ <a
+ className='text-danger'
+ href='#'
+ onClick={this.regenToken.bind(this, hook.id)}
+ >
+ {'Regen Token'}
+ </a>
+ <span>{' - '}</span>
+ <a
+ className='text-danger'
+ href='#'
+ onClick={this.removeHook.bind(this, hook.id)}
+ >
+ {'Remove'}
+ </a>
+ </div>
+ </div>
+ );
+ });
+
+ let displayHooks;
+ if (!this.state.getHooksComplete) {
+ displayHooks = <LoadingScreen/>;
+ } else if (hooks.length > 0) {
+ displayHooks = hooks;
+ } else {
+ displayHooks = <label>{': None'}</label>;
+ }
+
+ const existingHooks = (
+ <div className='padding-top x2'>
+ <label className='control-label padding-top x2'>{'Existing outgoing webhooks'}</label>
+ {displayHooks}
+ </div>
+ );
+
+ const disableButton = (this.state.channelId === '' && this.state.triggerWords === '') || this.state.callbackURLs === '';
+
+ return (
+ <div key='addOutgoingHook'>
+ <label className='control-label'>{'Add a new outgoing webhook'}</label>
+ <div className='padding-top'>
+ <strong>{'Channel:'}</strong>
+ <select
+ ref='channelName'
+ className='form-control'
+ value={this.state.channelId}
+ onChange={this.updateChannelId}
+ >
+ {options}
+ </select>
+ <span>{'Only public channels can be used'}</span>
+ <br/>
+ <br/>
+ <strong>{'Trigger Words:'}</strong>
+ <input
+ ref='triggerWords'
+ className='form-control'
+ value={this.state.triggerWords}
+ onChange={this.updateTriggerWords}
+ placeholder='Optional if channel selected'
+ />
+ <span>{'Comma separated words to trigger on'}</span>
+ <br/>
+ <br/>
+ <strong>{'Callback URLs:'}</strong>
+ <textarea
+ ref='callbackURLs'
+ className='form-control no-resize'
+ value={this.state.callbackURLs}
+ resize={false}
+ rows={3}
+ onChange={this.updateCallbackURLs}
+ />
+ <span>{'New line separated URLs that will receive the HTTP POST event'}</span>
+ {serverError}
+ <div className='padding-top'>
+ <a
+ className={'btn btn-sm btn-primary'}
+ href='#'
+ disabled={disableButton}
+ onClick={this.addNewHook}
+ >
+ {'Add'}
+ </a>
+ </div>
+ </div>
+ {existingHooks}
+ </div>
+ );
+ }
+}
diff --git a/web/react/components/user_settings/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx
index 5ce9b6330..15bf961d6 100644
--- a/web/react/components/user_settings/user_settings.jsx
+++ b/web/react/components/user_settings/user_settings.jsx
@@ -9,6 +9,7 @@ var GeneralTab = require('./user_settings_general.jsx');
var AppearanceTab = require('./user_settings_appearance.jsx');
var DeveloperTab = require('./user_settings_developer.jsx');
var IntegrationsTab = require('./user_settings_integrations.jsx');
+var DisplayTab = require('./user_settings_display.jsx');
export default class UserSettings extends React.Component {
constructor(props) {
@@ -98,6 +99,17 @@ export default class UserSettings extends React.Component {
/>
</div>
);
+ } else if (this.props.activeTab === 'display') {
+ return (
+ <div>
+ <DisplayTab
+ user={this.state.user}
+ activeSection={this.props.activeSection}
+ updateSection={this.props.updateSection}
+ updateTab={this.props.updateTab}
+ />
+ </div>
+ );
}
return <div/>;
diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
index 6d64e83b6..8c62a189d 100644
--- a/web/react/components/user_settings/user_settings_appearance.jsx
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -29,14 +29,14 @@ export default class UserSettingsAppearance extends React.Component {
UserStore.addChangeListener(this.onChange);
if (this.props.activeSection === 'theme') {
- $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
+ $(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
}
$('#user_settings').on('hidden.bs.modal', this.handleClose);
}
componentDidUpdate() {
if (this.props.activeSection === 'theme') {
$('.color-btn').removeClass('active-border');
- $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
+ $(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
}
}
componentWillUnmount() {
@@ -152,9 +152,8 @@ export default class UserSettingsAppearance extends React.Component {
<input type='radio'
checked={!displayCustom}
onChange={this.updateType.bind(this, 'premade')}
- >
- {'Theme Colors'}
- </input>
+ />
+ {'Theme Colors'}
</label>
<br/>
</div>
@@ -164,9 +163,8 @@ export default class UserSettingsAppearance extends React.Component {
<input type='radio'
checked={displayCustom}
onChange={this.updateType.bind(this, 'custom')}
- >
- {'Custom Theme'}
- </input>
+ />
+ {'Custom Theme'}
</label>
<br/>
</div>
@@ -178,7 +176,7 @@ export default class UserSettingsAppearance extends React.Component {
href='#'
onClick={this.submitTheme}
>
- {'Submit'}
+ {'Save'}
</a>
<a
className='btn btn-sm theme'
diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx
new file mode 100644
index 000000000..22a62273c
--- /dev/null
+++ b/web/react/components/user_settings/user_settings_display.jsx
@@ -0,0 +1,165 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {savePreferences} from '../../utils/client.jsx';
+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';
+
+function getDisplayStateFromStores() {
+ const militaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'});
+
+ return {militaryTime: militaryTime.value};
+}
+
+export default class UserSettingsDisplay extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleClockRadio = this.handleClockRadio.bind(this);
+ this.updateSection = this.updateSection.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+
+ this.state = getDisplayStateFromStores();
+ }
+ handleSubmit() {
+ const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime);
+
+ savePreferences([preference],
+ () => {
+ PreferenceStore.emitChange();
+ this.updateSection('');
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+ handleClockRadio(militaryTime) {
+ this.setState({militaryTime});
+ }
+ updateSection(section) {
+ this.setState(getDisplayStateFromStores());
+ this.props.updateSection(section);
+ }
+ handleClose() {
+ this.updateSection('');
+ }
+ componentDidMount() {
+ $('#user_settings').on('hidden.bs.modal', this.handleClose);
+ }
+ componentWillUnmount() {
+ $('#user_settings').off('hidden.bs.modal', this.handleClose);
+ }
+ render() {
+ const serverError = this.state.serverError || null;
+ let clockSection;
+ if (this.props.activeSection === 'clock') {
+ const clockFormat = [false, false];
+ if (this.state.militaryTime === 'true') {
+ clockFormat[1] = true;
+ } else {
+ clockFormat[0] = true;
+ }
+
+ const handleUpdateClockSection = (e) => {
+ this.updateSection('');
+ e.preventDefault();
+ };
+
+ const inputs = [
+ <div key='userDisplayClockOptions'>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={clockFormat[0]}
+ onChange={this.handleClockRadio.bind(this, 'false')}
+ />
+ {'12-hour clock (example: 4:00 PM)'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={clockFormat[1]}
+ onChange={this.handleClockRadio.bind(this, 'true')}
+ />
+ {'24-hour clock (example: 16:00)'}
+ </label>
+ <br/>
+ </div>
+ <div><br/>{'Select how you prefer time displayed.'}</div>
+ </div>
+ ];
+
+ clockSection = (
+ <SettingItemMax
+ title='Clock Display'
+ inputs={inputs}
+ submit={this.handleSubmit}
+ server_error={serverError}
+ updateSection={handleUpdateClockSection}
+ />
+ );
+ } else {
+ let describe = '';
+ if (this.state.militaryTime === 'true') {
+ describe = '24-hour clock (example: 16:00)';
+ } else {
+ describe = '12-hour clock (example: 4:00 PM)';
+ }
+
+ const handleUpdateClockSection = () => {
+ this.props.updateSection('clock');
+ };
+
+ clockSection = (
+ <SettingItemMin
+ title='Clock Display'
+ describe={describe}
+ updateSection={handleUpdateClockSection}
+ />
+ );
+ }
+
+ return (
+ <div>
+ <div className='modal-header'>
+ <button
+ type='button'
+ className='close'
+ data-dismiss='modal'
+ aria-label='Close'
+ >
+ <span aria-hidden='true'>{'×'}</span>
+ </button>
+ <h4
+ className='modal-title'
+ ref='title'
+ >
+ <i className='modal-back'></i>
+ {'Display Settings'}
+ </h4>
+ </div>
+ <div className='user-settings'>
+ <h3 className='tab-header'>{'Display Settings'}</h3>
+ <div className='divider-dark first'/>
+ {clockSection}
+ <div className='divider-dark'/>
+ </div>
+ </div>
+ );
+ }
+}
+
+UserSettingsDisplay.propTypes = {
+ user: React.PropTypes.object,
+ updateSection: React.PropTypes.func,
+ updateTab: React.PropTypes.func,
+ activeSection: React.PropTypes.string
+};
diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index ac3a2e37b..9c03f77a6 100644
--- a/web/react/components/user_settings/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -211,7 +211,7 @@ export default class UserSettingsGeneralTab extends React.Component {
this.props.updateSection(section);
}
handleClose() {
- $(React.findDOMNode(this)).find('.form-control').each(function clearForms() {
+ $(ReactDOM.findDOMNode(this)).find('.form-control').each(function clearForms() {
this.value = '';
});
@@ -368,8 +368,7 @@ export default class UserSettingsGeneralTab extends React.Component {
const extraInfo = (
<span>
- {'Use Nickname for a name you might be called that is different from your first name and user name.'}
- {'This is most often used when two or more people have similar sounding names and usernames.'}
+ {'Use Nickname for a name you might be called that is different from your first name and username. This is most often used when two or more people have similar sounding names and usernames.'}
</span>
);
diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
index 5e20d41f1..231580cc3 100644
--- a/web/react/components/user_settings/user_settings_integrations.jsx
+++ b/web/react/components/user_settings/user_settings_integrations.jsx
@@ -4,6 +4,7 @@
var SettingItemMin = require('../setting_item_min.jsx');
var SettingItemMax = require('../setting_item_max.jsx');
var ManageIncomingHooks = require('./manage_incoming_hooks.jsx');
+var ManageOutgoingHooks = require('./manage_outgoing_hooks.jsx');
export default class UserSettingsIntegrationsTab extends React.Component {
constructor(props) {
@@ -19,6 +20,8 @@ export default class UserSettingsIntegrationsTab extends React.Component {
}
handleClose() {
this.updateSection('');
+ $('.ps-container.modal-body').scrollTop(0);
+ $('.ps-container.modal-body').perfectScrollbar('update');
}
componentDidMount() {
$('#user_settings').on('hidden.bs.modal', this.handleClose);
@@ -28,33 +31,67 @@ export default class UserSettingsIntegrationsTab extends React.Component {
}
render() {
let incomingHooksSection;
+ let outgoingHooksSection;
var inputs = [];
- if (this.props.activeSection === 'incoming-hooks') {
- inputs.push(
- <ManageIncomingHooks />
- );
+ if (global.window.config.EnableIncomingWebhooks === 'true') {
+ if (this.props.activeSection === 'incoming-hooks') {
+ inputs.push(
+ <ManageIncomingHooks />
+ );
- incomingHooksSection = (
- <SettingItemMax
- title='Incoming Webhooks'
- inputs={inputs}
- updateSection={function clearSection(e) {
- this.updateSection('');
- e.preventDefault();
- }.bind(this)}
- />
- );
- } else {
- incomingHooksSection = (
- <SettingItemMin
- title='Incoming Webhooks'
- describe='Manage your incoming webhooks (Developer feature)'
- updateSection={function updateNameSection() {
- this.updateSection('incoming-hooks');
- }.bind(this)}
- />
- );
+ incomingHooksSection = (
+ <SettingItemMax
+ title='Incoming Webhooks'
+ width = 'full'
+ inputs={inputs}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ incomingHooksSection = (
+ <SettingItemMin
+ title='Incoming Webhooks'
+ width = 'full'
+ describe='Manage your incoming webhooks (Developer feature)'
+ updateSection={() => {
+ this.updateSection('incoming-hooks');
+ }}
+ />
+ );
+ }
+ }
+
+ if (global.window.config.EnableOutgoingWebhooks === 'true') {
+ if (this.props.activeSection === 'outgoing-hooks') {
+ inputs.push(
+ <ManageOutgoingHooks />
+ );
+
+ outgoingHooksSection = (
+ <SettingItemMax
+ title='Outgoing Webhooks'
+ inputs={inputs}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ outgoingHooksSection = (
+ <SettingItemMin
+ title='Outgoing Webhooks'
+ describe='Manage your outgoing webhooks'
+ updateSection={() => {
+ this.updateSection('outgoing-hooks');
+ }}
+ />
+ );
+ }
}
return (
@@ -80,6 +117,8 @@ export default class UserSettingsIntegrationsTab extends React.Component {
<h3 className='tab-header'>{'Integration Settings'}</h3>
<div className='divider-dark first'/>
{incomingHooksSection}
+ <div className='divider-light'/>
+ {outgoingHooksSection}
<div className='divider-dark'/>
</div>
</div>
diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx
index 19b97fc85..44cd423b5 100644
--- a/web/react/components/user_settings/user_settings_modal.jsx
+++ b/web/react/components/user_settings/user_settings_modal.jsx
@@ -38,9 +38,10 @@ export default class UserSettingsModal extends React.Component {
if (global.window.config.EnableOAuthServiceProvider === 'true') {
tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'});
}
- if (global.window.config.EnableIncomingWebhooks === 'true') {
+ if (global.window.config.EnableIncomingWebhooks === 'true' || global.window.config.EnableOutgoingWebhooks === 'true') {
tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'});
}
+ tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'});
return (
<div
diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index 4728a33ee..8693af494 100644
--- a/web/react/components/user_settings/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -129,7 +129,7 @@ export default class NotificationsTab extends React.Component {
);
}
handleClose() {
- $(React.findDOMNode(this)).find('.form-control').each(function clearField() {
+ $(ReactDOM.findDOMNode(this)).find('.form-control').each(function clearField() {
this.value = '';
});
@@ -158,15 +158,15 @@ export default class NotificationsTab extends React.Component {
}
handleNotifyRadio(notifyLevel) {
this.setState({notifyLevel: notifyLevel});
- React.findDOMNode(this.refs.wrapper).focus();
+ ReactDOM.findDOMNode(this.refs.wrapper).focus();
}
handleEmailRadio(enableEmail) {
this.setState({enableEmail: enableEmail});
- React.findDOMNode(this.refs.wrapper).focus();
+ ReactDOM.findDOMNode(this.refs.wrapper).focus();
}
handleSoundRadio(enableSound) {
this.setState({enableSound: enableSound});
- React.findDOMNode(this.refs.wrapper).focus();
+ ReactDOM.findDOMNode(this.refs.wrapper).focus();
}
updateUsernameKey(val) {
this.setState({usernameKey: val});
@@ -184,10 +184,10 @@ export default class NotificationsTab extends React.Component {
this.setState({channelKey: val});
}
updateCustomMentionKeys() {
- var checked = React.findDOMNode(this.refs.customcheck).checked;
+ var checked = ReactDOM.findDOMNode(this.refs.customcheck).checked;
if (checked) {
- var text = React.findDOMNode(this.refs.custommentions).value;
+ var text = ReactDOM.findDOMNode(this.refs.custommentions).value;
// remove all spaces and split string into individual keys
this.setState({customKeys: text.replace(/ /g, ''), customKeysChecked: true});
@@ -196,7 +196,7 @@ export default class NotificationsTab extends React.Component {
}
}
onCustomChange() {
- React.findDOMNode(this.refs.customcheck).checked = true;
+ ReactDOM.findDOMNode(this.refs.customcheck).checked = true;
this.updateCustomMentionKeys();
}
render() {
@@ -228,9 +228,8 @@ export default class NotificationsTab extends React.Component {
<input type='radio'
checked={notifyActive[0]}
onChange={this.handleNotifyRadio.bind(this, 'all')}
- >
- For all activity
- </input>
+ />
+ {'For all activity'}
</label>
<br/>
</div>
@@ -240,9 +239,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={notifyActive[1]}
onChange={this.handleNotifyRadio.bind(this, 'mention')}
- >
- Only for mentions and direct messages
- </input>
+ />
+ {'Only for mentions and direct messages'}
</label>
<br/>
</div>
@@ -252,9 +250,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={notifyActive[2]}
onChange={this.handleNotifyRadio.bind(this, 'none')}
- >
- Never
- </input>
+ />
+ {'Never'}
</label>
</div>
</div>
@@ -320,9 +317,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={soundActive[0]}
onChange={this.handleSoundRadio.bind(this, 'true')}
- >
- On
- </input>
+ />
+ {'On'}
</label>
<br/>
</div>
@@ -332,9 +328,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={soundActive[1]}
onChange={this.handleSoundRadio.bind(this, 'false')}
- >
- Off
- </input>
+ />
+ {'Off'}
</label>
<br/>
</div>
@@ -402,9 +397,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={emailActive[0]}
onChange={this.handleEmailRadio.bind(this, 'true')}
- >
- On
- </input>
+ />
+ {'On'}
</label>
<br/>
</div>
@@ -414,9 +408,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={emailActive[1]}
onChange={this.handleEmailRadio.bind(this, 'false')}
- >
- Off
- </input>
+ />
+ {'Off'}
</label>
<br/>
</div>
@@ -482,9 +475,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.firstNameKey}
onChange={handleUpdateFirstNameKey}
- >
- {'Your case sensitive first name "' + user.first_name + '"'}
- </input>
+ />
+ {'Your case sensitive first name "' + user.first_name + '"'}
</label>
</div>
</div>
@@ -502,9 +494,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.usernameKey}
onChange={handleUpdateUsernameKey}
- >
- {'Your non-case sensitive username "' + user.username + '"'}
- </input>
+ />
+ {'Your non-case sensitive username "' + user.username + '"'}
</label>
</div>
</div>
@@ -521,9 +512,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.mentionKey}
onChange={handleUpdateMentionKey}
- >
- {'Your username mentioned "@' + user.username + '"'}
- </input>
+ />
+ {'Your username mentioned "@' + user.username + '"'}
</label>
</div>
</div>
@@ -540,9 +530,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.allKey}
onChange={handleUpdateAllKey}
- >
- {'Team-wide mentions "@all"'}
- </input>
+ />
+ {'Team-wide mentions "@all"'}
</label>
</div>
</div>
@@ -559,9 +548,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.channelKey}
onChange={handleUpdateChannelKey}
- >
- {'Channel-wide mentions "@channel"'}
- </input>
+ />
+ {'Channel-wide mentions "@channel"'}
</label>
</div>
</div>
@@ -576,9 +564,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.customKeysChecked}
onChange={this.updateCustomMentionKeys}
- >
- {'Other non-case sensitive words, separated by commas:'}
- </input>
+ />
+ {'Other non-case sensitive words, separated by commas:'}
</label>
</div>
<input
diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx
index 74190781c..983a10df0 100644
--- a/web/react/components/user_settings/user_settings_security.jsx
+++ b/web/react/components/user_settings/user_settings_security.jsx
@@ -82,7 +82,7 @@ export default class SecurityTab extends React.Component {
$('#user_settings').modal('hide');
}
handleClose() {
- $(React.findDOMNode(this)).find('.form-control').each(function resetValue() {
+ $(ReactDOM.findDOMNode(this)).find('.form-control').each(function resetValue() {
this.value = '';
});
this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null});
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index 8ef68dd0a..322e68c17 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -6,6 +6,7 @@ const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
const ViewImagePopoverBar = require('./view_image_popover_bar.jsx');
const Modal = ReactBootstrap.Modal;
+const KeyCodes = Constants.KeyCodes;
export default class ViewImageModal extends React.Component {
constructor(props) {
@@ -63,11 +64,11 @@ export default class ViewImageModal extends React.Component {
this.loadImage(id);
}
handleKeyPress(e) {
- if (!e) {
+ if (!e || !this.props.show) {
return;
- } else if (e.keyCode === 39) {
+ } else if (e.keyCode === KeyCodes.RIGHT) {
this.handleNext();
- } else if (e.keyCode === 37) {
+ } else if (e.keyCode === KeyCodes.LEFT) {
this.handlePrev();
}
}
@@ -77,7 +78,7 @@ export default class ViewImageModal extends React.Component {
}
onModalHidden() {
if (this.refs.video) {
- var video = React.findDOMNode(this.refs.video);
+ var video = ReactDOM.findDOMNode(this.refs.video);
video.pause();
video.currentTime = 0;
}
@@ -195,6 +196,7 @@ export default class ViewImageModal extends React.Component {
target='_blank'
>
<img
+ style={{maxHeight: this.state.imgHeight}}
ref='image'
src={this.getPreviewImagePath(filename)}
/>
@@ -210,6 +212,7 @@ export default class ViewImageModal extends React.Component {
content = (
<video
+ style={{maxHeight: this.state.imgHeight}}
ref='video'
data-setup='{}'
controls='controls'
@@ -334,7 +337,6 @@ export default class ViewImageModal extends React.Component {
>
<div
className={'image-wrapper ' + bgClass}
- style={{maxHeight: this.state.imgHeight}}
onMouseEnter={this.onMouseEnterImage}
onMouseLeave={this.onMouseLeaveImage}
onClick={(e) => e.stopPropagation()}