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.jsx3
-rw-r--r--web/react/components/admin_console/admin_controller.jsx2
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx4
-rw-r--r--web/react/components/admin_console/audits.jsx5
-rw-r--r--web/react/components/admin_console/license_settings.jsx18
-rw-r--r--web/react/components/audit_table.jsx655
-rw-r--r--web/react/components/create_comment.jsx15
-rw-r--r--web/react/components/create_post.jsx55
-rw-r--r--web/react/components/delete_post_modal.jsx2
-rw-r--r--web/react/components/file_upload.jsx2
-rw-r--r--web/react/components/post_body.jsx30
-rw-r--r--web/react/components/post_info.jsx25
-rw-r--r--web/react/components/textbox.jsx12
-rw-r--r--web/react/components/user_settings/manage_command_hooks.jsx29
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx6
15 files changed, 461 insertions, 402 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx
index 98b1d7cc1..af4d3fb0f 100644
--- a/web/react/components/access_history_modal.jsx
+++ b/web/react/components/access_history_modal.jsx
@@ -73,7 +73,8 @@ class AccessHistoryModal extends React.Component {
content = (
<AuditTable
audits={this.state.audits}
- moreInfo={this.state.moreInfo}
+ showIp={true}
+ showSession={true}
/>
);
}
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index 360ae3ef3..695e2083a 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -160,7 +160,7 @@ export default class AdminController extends React.Component {
} else if (this.state.selected === 'ldap_settings') {
tab = <LdapSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'license') {
- tab = <LicenseSettingsTab />;
+ tab = <LicenseSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'team_users') {
if (this.state.teams) {
tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index 642bfe9d7..eadd8d412 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -225,7 +225,7 @@ export default class AdminSidebar extends React.Component {
>
<FormattedMessage
id='admin.sidebar.audits'
- defaultMessage='Audits'
+ defaultMessage='Compliance and Auditing'
/>
</a>
</li>
@@ -454,6 +454,7 @@ export default class AdminSidebar extends React.Component {
</ul>
<ul className='nav nav__sub-menu padded'>
{licenseSettings}
+ {audits}
<li>
<a
href='#'
@@ -466,7 +467,6 @@ export default class AdminSidebar extends React.Component {
/>
</a>
</li>
- {audits}
</ul>
</li>
</ul>
diff --git a/web/react/components/admin_console/audits.jsx b/web/react/components/admin_console/audits.jsx
index 866539b3d..173e63b45 100644
--- a/web/react/components/admin_console/audits.jsx
+++ b/web/react/components/admin_console/audits.jsx
@@ -60,8 +60,9 @@ export default class Audits extends React.Component {
<div style={{margin: '10px'}}>
<AuditTable
audits={this.state.audits}
- oneLine={true}
showUserId={true}
+ showIp={true}
+ showSession={true}
/>
</div>
);
@@ -72,7 +73,7 @@ export default class Audits extends React.Component {
<h3>
<FormattedMessage
id='admin.audits.title'
- defaultMessage='Server Audits'
+ defaultMessage='User Activity'
/>
</h3>
<button
diff --git a/web/react/components/admin_console/license_settings.jsx b/web/react/components/admin_console/license_settings.jsx
index 539acd869..3332f37ef 100644
--- a/web/react/components/admin_console/license_settings.jsx
+++ b/web/react/components/admin_console/license_settings.jsx
@@ -109,7 +109,17 @@ class LicenseSettings extends React.Component {
);
licenseType = (
<FormattedHTMLMessage
- id='admin.license.entrepriseType'
+ id='admin.license.enterpriseType'
+ values={{
+ terms: global.window.mm_config.TermsOfServiceLink,
+ name: global.window.mm_license.Name,
+ company: global.window.mm_license.Company,
+ users: global.window.mm_license.Users,
+ issued: Utils.displayDate(parseInt(global.window.mm_license.IssuedAt, 10)) + ' ' + Utils.displayTime(parseInt(global.window.mm_license.IssuedAt, 10), true),
+ start: Utils.displayDate(parseInt(global.window.mm_license.StartsAt, 10)),
+ expires: Utils.displayDate(parseInt(global.window.mm_license.ExpiresAt, 10)),
+ ldap: global.window.mm_license.LDAP
+ }}
defaultMessage='<div><p>This compiled release of Mattermost platform is provided under a <a href="http://mattermost.com" target="_blank">commercial license</a>
from Mattermost, Inc. based on your subscription level and is subject to the <a href="{terms}" target="_blank">Terms of Service.</a></p>
<p>Your subscription details are as follows:</p>
@@ -126,6 +136,7 @@ class LicenseSettings extends React.Component {
licenseKey = (
<div className='col-sm-8'>
<button
+ disabled={this.props.config.LdapSettings.Enable}
className='btn btn-danger'
onClick={this.handleRemove}
id='remove-button'
@@ -256,7 +267,8 @@ class LicenseSettings extends React.Component {
}
LicenseSettings.propTypes = {
- intl: intlShape.isRequired
+ intl: intlShape.isRequired,
+ config: React.PropTypes.object
};
-export default injectIntl(LicenseSettings); \ No newline at end of file
+export default injectIntl(LicenseSettings);
diff --git a/web/react/components/audit_table.jsx b/web/react/components/audit_table.jsx
index cdca7e8d6..49892ff98 100644
--- a/web/react/components/audit_table.jsx
+++ b/web/react/components/audit_table.jsx
@@ -183,389 +183,388 @@ const holders = defineMessages({
loginFailure: {
id: 'audit_table.loginFailure',
defaultMessage: ' (Login failure)'
- },
- userId: {
- id: 'audit_table.userId',
- defaultMessage: 'User ID'
}
});
class AuditTable extends React.Component {
constructor(props) {
super(props);
-
- this.handleMoreInfo = this.handleMoreInfo.bind(this);
- this.formatAuditInfo = this.formatAuditInfo.bind(this);
- this.handleRevokedSession = this.handleRevokedSession.bind(this);
-
- this.state = {moreInfo: []};
- }
- handleMoreInfo(index) {
- var newMoreInfo = this.state.moreInfo;
- newMoreInfo[index] = true;
- this.setState({moreInfo: newMoreInfo});
}
- handleRevokedSession(sessionId) {
- return this.props.intl.formatMessage(holders.sessionRevoked, {sessionId: sessionId});
- }
- formatAuditInfo(currentAudit) {
- const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, '');
+ render() {
+ var accessList = [];
const {formatMessage} = this.props.intl;
- 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;
- }
- }
+ for (var i = 0; i < this.props.audits.length; i++) {
+ const audit = this.props.audits[i];
+ const auditInfo = formatAuditInfo(audit, formatMessage);
- switch (currentActionURL) {
- case '/channels/create':
- currentAuditDesc = formatMessage(holders.channelCreated, {channelName: channelName});
- break;
- case '/channels/create_direct':
- currentAuditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username});
- break;
- case '/channels/update':
- currentAuditDesc = formatMessage(holders.nameUpdated, {channelName: channelName});
- break;
- case '/channels/update_desc': // support the old path
- case '/channels/update_header':
- currentAuditDesc = formatMessage(holders.headerUpdated, {channelName: channelName});
- 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;
- }
- }
+ let uContent;
+ if (this.props.showUserId) {
+ uContent = <td>{auditInfo.userId}</td>;
+ }
- if (/\/channels\/[A-Za-z0-9]+\/delete/.test(currentActionURL)) {
- currentAuditDesc = formatMessage(holders.channelDeleted, {url: channelURL});
- } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(currentActionURL)) {
- currentAuditDesc = formatMessage(holders.userAdded, {username: username, channelName: channelName});
- } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(currentActionURL)) {
- currentAuditDesc = formatMessage(holders.userRemoved, {username: username, channelName: channelName});
- }
+ let iContent;
+ if (this.props.showIp) {
+ iContent = <td>{auditInfo.ip}</td>;
+ }
- break;
+ let sContent;
+ if (this.props.showSession) {
+ sContent = <td>{auditInfo.sessionId}</td>;
}
+
+ let descStyle = {};
+ if (auditInfo.desc.toLowerCase().indexOf('fail') !== -1) {
+ descStyle.color = 'red';
}
- } else if (currentActionURL.indexOf('/oauth') === 0) {
- const oauthInfo = currentAudit.extra_info.split(' ');
- switch (currentActionURL) {
- case '/oauth/register': {
- const clientIdField = oauthInfo[0].split('=');
+ accessList[i] = (
+ <tr key={audit.id}>
+ <td>{auditInfo.timestamp}</td>
+ {uContent}
+ <td style={descStyle}>{auditInfo.desc}</td>
+ {iContent}
+ {sContent}
+ </tr>
+ );
+ }
- if (clientIdField[0] === 'client_id') {
- currentAuditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]});
- }
+ let userIdContent;
+ if (this.props.showUserId) {
+ userIdContent = (
+ <th>
+ <FormattedMessage
+ id='audit_table.userId'
+ defaultMessage='User ID'
+ />
+ </th>
+ );
+ }
- break;
- }
- case '/oauth/allow':
- if (oauthInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedAllowOAuthAccess);
- } else if (oauthInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullOAuthAccess);
- } else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
- currentAuditDesc = formatMessage(holders.failedOAuthAccess);
- }
+ let ipContent;
+ if (this.props.showIp) {
+ ipContent = (
+ <th>
+ <FormattedMessage
+ id='audit_table.ip'
+ defaultMessage='IP Address'
+ />
+ </th>
+ );
+ }
- break;
- case '/oauth/access_token':
- if (oauthInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedOAuthToken);
- } else if (oauthInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullOAuthToken);
- } else {
- const oauthTokenFailure = oauthInfo[0].split('-');
-
- if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
- currentAuditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()});
- }
- }
+ let sessionContent;
+ if (this.props.showSession) {
+ sessionContent = (
+ <th>
+ <FormattedMessage
+ id='audit_table.session'
+ defaultMessage='Session ID'
+ />
+ </th>
+ );
+ }
- 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 = formatMessage(holders.attemptedLogin);
- } else if (userInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullLogin);
- } else if (userInfo[0]) {
- currentAuditDesc = formatMessage(holders.failedLogin);
- }
+ return (
+ <table className='table'>
+ <thead>
+ <tr>
+ <th>
+ <FormattedMessage
+ id='audit_table.timestamp'
+ defaultMessage='Timestamp'
+ />
+ </th>
+ {userIdContent}
+ <th>
+ <FormattedMessage
+ id='audit_table.action'
+ defaultMessage='Action'
+ />
+ </th>
+ {ipContent}
+ {sessionContent}
+ </tr>
+ </thead>
+ <tbody>
+ {accessList}
+ </tbody>
+ </table>
+ );
+ }
+}
- break;
- case '/users/revoke_session':
- currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]);
- break;
- case '/users/newimage':
- currentAuditDesc = formatMessage(holders.updatePicture);
- break;
- case '/users/update':
- currentAuditDesc = formatMessage(holders.updateGeneral);
- break;
- case '/users/newpassword':
- if (userInfo[0] === 'attempted') {
- currentAuditDesc = formatMessage(holders.attemptedPassword);
- } else if (userInfo[0] === 'completed') {
- currentAuditDesc = formatMessage(holders.successfullPassword);
- } else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
- currentAuditDesc = formatMessage(holders.failedPassword);
- }
+AuditTable.propTypes = {
+ intl: intlShape.isRequired,
+ audits: React.PropTypes.array.isRequired,
+ showUserId: React.PropTypes.bool,
+ showIp: React.PropTypes.bool,
+ showSession: React.PropTypes.bool
+};
- break;
- case '/users/update_roles': {
- const userRoles = userInfo[0].split('=')[1];
+export default injectIntl(AuditTable);
- currentAuditDesc = formatMessage(holders.updatedRol);
- if (userRoles.trim()) {
- currentAuditDesc += userRoles;
- } else {
- currentAuditDesc += formatMessage(holders.member);
+export function formatAuditInfo(audit, formatMessage) {
+ const actionURL = audit.action.replace(/\/api\/v[1-9]/, '');
+ let auditDesc = '';
+
+ if (actionURL.indexOf('/channels') === 0) {
+ const channelInfo = audit.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 (actionURL) {
+ case '/channels/create':
+ auditDesc = formatMessage(holders.channelCreated, {channelName: channelName});
+ break;
+ case '/channels/create_direct':
+ auditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username});
+ break;
+ case '/channels/update':
+ auditDesc = formatMessage(holders.nameUpdated, {channelName: channelName});
+ break;
+ case '/channels/update_desc': // support the old path
+ case '/channels/update_header':
+ auditDesc = formatMessage(holders.headerUpdated, {channelName: channelName});
+ 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;
}
+ }
- break;
+ if (/\/channels\/[A-Za-z0-9]+\/delete/.test(actionURL)) {
+ auditDesc = formatMessage(holders.channelDeleted, {url: channelURL});
+ } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(actionURL)) {
+ auditDesc = formatMessage(holders.userAdded, {username: username, channelName: channelName});
+ } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(actionURL)) {
+ auditDesc = formatMessage(holders.userRemoved, {username: username, channelName: channelName});
}
- 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 = formatMessage(holders.accountActive);
- } else if (updateField === 'false') {
- currentAuditDesc = formatMessage(holders.accountInactive);
- }
- 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 += formatMessage(holders.by, {username: actingUser.username});
- } else if (currentUser && actingUser) {
- currentAuditDesc += formatMessage(holders.byAdmin);
- }
- }
- } else if (updateType === 'session_id') {
- currentAuditDesc = this.handleRevokedSession(updateField);
- }
+ break;
+ }
+ }
+ } else if (actionURL.indexOf('/oauth') === 0) {
+ const oauthInfo = audit.extra_info.split(' ');
+
+ switch (actionURL) {
+ case '/oauth/register': {
+ const clientIdField = oauthInfo[0].split('=');
- break;
+ if (clientIdField[0] === 'client_id') {
+ auditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]});
}
- case '/users/send_password_reset':
- currentAuditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]});
- break;
- case '/users/reset_password':
- if (userInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedReset);
- } else if (userInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullReset);
- }
- break;
- case '/users/update_notify':
- currentAuditDesc = formatMessage(holders.updateGlobalNotifications);
- break;
- default:
- break;
+ break;
+ }
+ case '/oauth/allow':
+ if (oauthInfo[0] === 'attempt') {
+ auditDesc = formatMessage(holders.attemptedAllowOAuthAccess);
+ } else if (oauthInfo[0] === 'success') {
+ auditDesc = formatMessage(holders.successfullOAuthAccess);
+ } else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
+ auditDesc = formatMessage(holders.failedOAuthAccess);
}
- } else if (currentActionURL.indexOf('/hooks') === 0) {
- const webhookInfo = currentAudit.extra_info.split(' ');
-
- switch (currentActionURL) {
- case '/hooks/incoming/create':
- if (webhookInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedWebhookCreate);
- } else if (webhookInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.succcessfullWebhookCreate);
- } else if (webhookInfo[0] === 'fail - bad channel permissions') {
- currentAuditDesc = formatMessage(holders.failedWebhookCreate);
- }
- break;
- case '/hooks/incoming/delete':
- if (webhookInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedWebhookDelete);
- } else if (webhookInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullWebhookDelete);
- } else if (webhookInfo[0] === 'fail - inappropriate conditions') {
- currentAuditDesc = formatMessage(holders.failedWebhookDelete);
+ break;
+ case '/oauth/access_token':
+ if (oauthInfo[0] === 'attempt') {
+ auditDesc = formatMessage(holders.attemptedOAuthToken);
+ } else if (oauthInfo[0] === 'success') {
+ auditDesc = formatMessage(holders.successfullOAuthToken);
+ } else {
+ const oauthTokenFailure = oauthInfo[0].split('-');
+
+ if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
+ auditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()});
}
+ }
- break;
- default:
- break;
+ break;
+ default:
+ break;
+ }
+ } else if (actionURL.indexOf('/users') === 0) {
+ const userInfo = audit.extra_info.split(' ');
+
+ switch (actionURL) {
+ case '/users/login':
+ if (userInfo[0] === 'attempt') {
+ auditDesc = formatMessage(holders.attemptedLogin);
+ } else if (userInfo[0] === 'success') {
+ auditDesc = formatMessage(holders.successfullLogin);
+ } else if (userInfo[0]) {
+ auditDesc = formatMessage(holders.failedLogin);
}
- } else {
- switch (currentActionURL) {
- case '/logout':
- currentAuditDesc = formatMessage(holders.logout);
- break;
- case '/verify_email':
- currentAuditDesc = formatMessage(holders.verified);
- break;
- default:
- break;
+
+ break;
+ case '/users/revoke_session':
+ auditDesc = formatMessage(holders.sessionRevoked, {sessionId: userInfo[0].split('=')[1]});
+ break;
+ case '/users/newimage':
+ auditDesc = formatMessage(holders.updatePicture);
+ break;
+ case '/users/update':
+ auditDesc = formatMessage(holders.updateGeneral);
+ break;
+ case '/users/newpassword':
+ if (userInfo[0] === 'attempted') {
+ auditDesc = formatMessage(holders.attemptedPassword);
+ } else if (userInfo[0] === 'completed') {
+ auditDesc = formatMessage(holders.successfullPassword);
+ } else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
+ auditDesc = formatMessage(holders.failedPassword);
}
- }
- /* If all else fails... */
- if (!currentAuditDesc) {
- /* Currently not called anywhere */
- if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) {
- currentAuditDesc = formatMessage(holders.revokedAll);
+ break;
+ case '/users/update_roles': {
+ const userRoles = userInfo[0].split('=')[1];
+
+ auditDesc = formatMessage(holders.updatedRol);
+ if (userRoles.trim()) {
+ auditDesc += userRoles;
} else {
- let currentActionDesc = '';
- if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) {
- currentActionDesc = currentActionURL.substring(currentActionURL.lastIndexOf('/') + 1).replace('_', ' ');
- currentActionDesc = Utils.toTitleCase(currentActionDesc);
- }
+ auditDesc += formatMessage(holders.member);
+ }
- let currentExtraInfoDesc = '';
- if (currentAudit.extra_info) {
- currentExtraInfoDesc = currentAudit.extra_info;
+ 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') {
+ auditDesc = formatMessage(holders.accountActive);
+ } else if (updateField === 'false') {
+ auditDesc = formatMessage(holders.accountInactive);
+ }
- if (currentExtraInfoDesc.indexOf('=') !== -1) {
- currentExtraInfoDesc = currentExtraInfoDesc.substring(currentExtraInfoDesc.indexOf('=') + 1);
+ const actingUserInfo = userInfo[1].split('=');
+ if (actingUserInfo[0] === 'session_user') {
+ const actingUser = UserStore.getProfile(actingUserInfo[1]);
+ const user = UserStore.getCurrentUser();
+ if (user && actingUser && (Utils.isAdmin(user.roles) || Utils.isSystemAdmin(user.roles))) {
+ auditDesc += formatMessage(holders.by, {username: actingUser.username});
+ } else if (user && actingUser) {
+ auditDesc += formatMessage(holders.byAdmin);
}
}
- currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc;
+ } else if (updateType === 'session_id') {
+ auditDesc = formatMessage(holders.sessionRevoked, {sessionId: updateField});
}
- }
- const currentDate = new Date(currentAudit.create_at);
- let currentAuditInfo = currentDate.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' + currentDate.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'});
+ break;
+ }
+ case '/users/send_password_reset':
+ auditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]});
+ break;
+ case '/users/reset_password':
+ if (userInfo[0] === 'attempt') {
+ auditDesc = formatMessage(holders.attemptedReset);
+ } else if (userInfo[0] === 'success') {
+ auditDesc = formatMessage(holders.successfullReset);
+ }
- if (this.props.showUserId) {
- currentAuditInfo += ' | ' + formatMessage(holders.userId) + ': ' + currentAudit.user_id;
+ break;
+ case '/users/update_notify':
+ auditDesc = formatMessage(holders.updateGlobalNotifications);
+ break;
+ default:
+ break;
}
+ } else if (actionURL.indexOf('/hooks') === 0) {
+ const webhookInfo = audit.extra_info.split(' ');
+
+ switch (actionURL) {
+ case '/hooks/incoming/create':
+ if (webhookInfo[0] === 'attempt') {
+ auditDesc = formatMessage(holders.attemptedWebhookCreate);
+ } else if (webhookInfo[0] === 'success') {
+ auditDesc = formatMessage(holders.succcessfullWebhookCreate);
+ } else if (webhookInfo[0] === 'fail - bad channel permissions') {
+ auditDesc = formatMessage(holders.failedWebhookCreate);
+ }
- currentAuditInfo += ' | ' + currentAuditDesc;
+ break;
+ case '/hooks/incoming/delete':
+ if (webhookInfo[0] === 'attempt') {
+ auditDesc = formatMessage(holders.attemptedWebhookDelete);
+ } else if (webhookInfo[0] === 'success') {
+ auditDesc = formatMessage(holders.successfullWebhookDelete);
+ } else if (webhookInfo[0] === 'fail - inappropriate conditions') {
+ auditDesc = formatMessage(holders.failedWebhookDelete);
+ }
- return currentAuditInfo;
+ break;
+ default:
+ break;
+ }
+ } else {
+ switch (actionURL) {
+ case '/logout':
+ auditDesc = formatMessage(holders.logout);
+ break;
+ case '/verify_email':
+ auditDesc = formatMessage(holders.verified);
+ break;
+ default:
+ break;
+ }
}
- render() {
- var accessList = [];
- const {formatMessage} = this.props.intl;
- for (var i = 0; i < this.props.audits.length; i++) {
- const currentAudit = this.props.audits[i];
- const currentAuditInfo = this.formatAuditInfo(currentAudit);
-
- let moreInfo;
- if (!this.props.oneLine) {
- moreInfo = (
- <a
- href='#'
- className='theme'
- onClick={this.handleMoreInfo.bind(this, i)}
- >
- <FormattedMessage
- id='audit_table.moreInfo'
- defaultMessage='More info'
- />
- </a>
- );
+ /* If all else fails... */
+ if (!auditDesc) {
+ /* Currently not called anywhere */
+ if (audit.extra_info.indexOf('revoked_all=') >= 0) {
+ auditDesc = formatMessage(holders.revokedAll);
+ } else {
+ let actionDesc = '';
+ if (actionURL && actionURL.lastIndexOf('/') !== -1) {
+ actionDesc = actionURL.substring(actionURL.lastIndexOf('/') + 1).replace('_', ' ');
+ actionDesc = Utils.toTitleCase(actionDesc);
}
- if (this.state.moreInfo[i]) {
- if (!currentAudit.session_id) {
- currentAudit.session_id = 'N/A';
+ let extraInfoDesc = '';
+ if (audit.extra_info) {
+ extraInfoDesc = audit.extra_info;
- if (currentAudit.action.search('/users/login') >= 0) {
- if (currentAudit.extra_info === 'attempt') {
- currentAudit.session_id += formatMessage(holders.loginAttempt);
- } else {
- currentAudit.session_id += formatMessage(holders.loginFailure);
- }
- }
+ if (extraInfoDesc.indexOf('=') !== -1) {
+ extraInfoDesc = extraInfoDesc.substring(extraInfoDesc.indexOf('=') + 1);
}
-
- moreInfo = (
- <div>
- <div>
- <FormattedMessage
- id='audit_table.ip'
- defaultMessage='IP: {ip}'
- values={{
- ip: currentAudit.ip_address
- }}
- />
- </div>
- <div>
- <FormattedMessage
- id='audit_table.session'
- defaultMessage='Session ID: {id}'
- values={{
- id: currentAudit.session_id
- }}
- />
- </div>
- </div>
- );
}
-
- var divider = null;
- if (i < this.props.audits.length - 1) {
- divider = (<div className='divider-light'></div>);
- }
-
- accessList[i] = (
- <div
- key={'accessHistoryEntryKey' + i}
- className='access-history__table'
- >
- <div className='access__report'>
- <div className='report__time'>{currentAuditInfo}</div>
- <div className='report__info'>
- {moreInfo}
- </div>
- {divider}
- </div>
- </div>
- );
+ auditDesc = actionDesc + ' ' + extraInfoDesc;
}
-
- return <form role='form'>{accessList}</form>;
}
-}
-AuditTable.propTypes = {
- intl: intlShape.isRequired,
- audits: React.PropTypes.array.isRequired,
- oneLine: React.PropTypes.bool,
- showUserId: React.PropTypes.bool
-};
+ const date = new Date(audit.create_at);
+ let auditInfo = {};
+ auditInfo.timestamp = date.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' + date.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'});
+ auditInfo.userId = audit.user_id;
+ auditInfo.desc = auditDesc;
+ auditInfo.ip = audit.ip_address;
+ auditInfo.sessionId = audit.session_id;
-export default injectIntl(AuditTable);
+ return auditInfo;
+}
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 8c49315e7..9e7c67515 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -51,6 +51,7 @@ class CreateComment extends React.Component {
this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.handleUploadClick = this.handleUploadClick.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
@@ -74,6 +75,8 @@ class CreateComment extends React.Component {
componentDidMount() {
PreferenceStore.addChangeListener(this.onPreferenceChange);
window.addEventListener('resize', this.handleResize);
+
+ this.refs.textbox.focus();
}
componentWillUnmount() {
PreferenceStore.removeChangeListener(this.onPreferenceChange);
@@ -94,6 +97,10 @@ class CreateComment extends React.Component {
$('.post-right__scroll').perfectScrollbar('update');
}
}
+
+ if (prevProps.rootId !== this.props.rootId) {
+ this.refs.textbox.focus();
+ }
}
handleSubmit(e) {
e.preventDefault();
@@ -218,6 +225,9 @@ class CreateComment extends React.Component {
});
}
}
+ handleUploadClick() {
+ this.refs.textbox.focus();
+ }
handleUploadStart(clientIds) {
let draft = PostStore.getCommentDraft(this.props.rootId);
@@ -225,6 +235,10 @@ class CreateComment extends React.Component {
PostStore.storeCommentDraft(this.props.rootId, draft);
this.setState({uploadsInProgress: draft.uploadsInProgress});
+
+ // this is a bit redundant with the code that sets focus when the file input is clicked,
+ // but this also resets the focus after a drag and drop
+ this.refs.textbox.focus();
}
handleFileUploadComplete(filenames, clientIds) {
let draft = PostStore.getCommentDraft(this.props.rootId);
@@ -365,6 +379,7 @@ class CreateComment extends React.Component {
<FileUpload
ref='fileUpload'
getFileCount={this.getFileCount}
+ onClick={this.handleUploadClick}
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 20892898e..6ea80cd13 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -40,10 +40,6 @@ const holders = defineMessages({
write: {
id: 'create_post.write',
defaultMessage: 'Write a message...'
- },
- deleteMsg: {
- id: 'create_post.deleteMsg',
- defaultMessage: '(message deleted)'
}
});
@@ -57,7 +53,7 @@ class CreatePost extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.postMsgKeyPress = this.postMsgKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
- this.resizePostHolder = this.resizePostHolder.bind(this);
+ this.handleUploadClick = this.handleUploadClick.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
@@ -66,11 +62,9 @@ class CreatePost extends React.Component {
this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
- this.handleResize = this.handleResize.bind(this);
this.sendMessage = this.sendMessage.bind(this);
PostStore.clearDraftUploads();
- PostStore.deleteMessage(this.props.intl.formatMessage(holders.deleteMsg));
const draft = this.getCurrentDraft();
@@ -81,34 +75,10 @@ class CreatePost extends React.Component {
previews: draft.previews,
submitting: false,
initialText: draft.messageText,
- windowWidth: Utils.windowWidth(),
- windowHeight: Utils.windowHeight(),
ctrlSend: false,
showTutorialTip: false
};
}
- handleResize() {
- this.setState({
- windowWidth: Utils.windowWidth(),
- windowHeight: Utils.windowHeight()
- });
- }
- componentDidUpdate(prevProps, prevState) {
- if (prevState.previews.length !== this.state.previews.length) {
- this.resizePostHolder();
- return;
- }
-
- if (prevState.uploadsInProgress !== this.state.uploadsInProgress) {
- this.resizePostHolder();
- return;
- }
-
- if (prevState.windowWidth !== this.state.windowWidth || prevState.windowHeight !== this.state.windowHeight) {
- this.resizePostHolder();
- return;
- }
- }
getCurrentDraft() {
const draft = PostStore.getCurrentDraft();
const safeDraft = {previews: [], messageText: '', uploadsInProgress: []};
@@ -245,10 +215,8 @@ class CreatePost extends React.Component {
draft.message = messageText;
PostStore.storeCurrentDraft(draft);
}
- resizePostHolder() {
- if (this.state.windowWidth > 960) {
- $('#post_textbox').focus();
- }
+ handleUploadClick() {
+ this.refs.textbox.focus();
}
handleUploadStart(clientIds, channelId) {
const draft = PostStore.getDraft(channelId);
@@ -257,6 +225,10 @@ class CreatePost extends React.Component {
PostStore.storeDraft(channelId, draft);
this.setState({uploadsInProgress: draft.uploadsInProgress});
+
+ // this is a bit redundant with the code that sets focus when the file input is clicked,
+ // but this also resets the focus after a drag and drop
+ this.refs.textbox.focus();
}
handleFileUploadComplete(filenames, clientIds, channelId) {
const draft = PostStore.getDraft(channelId);
@@ -333,13 +305,16 @@ class CreatePost extends React.Component {
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
PreferenceStore.addChangeListener(this.onPreferenceChange);
- this.resizePostHolder();
- window.addEventListener('resize', this.handleResize);
+ this.refs.textbox.focus();
+ }
+ componentDidUpdate(prevProps, prevState) {
+ if (prevState.channelId !== this.state.channelId) {
+ this.refs.textbox.focus();
+ }
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChange);
PreferenceStore.removeChangeListener(this.onPreferenceChange);
- window.removeEventListener('resize', this.handleResize);
}
onChange() {
const channelId = ChannelStore.getCurrentId();
@@ -462,7 +437,6 @@ class CreatePost extends React.Component {
onUserInput={this.handleUserInput}
onKeyPress={this.postMsgKeyPress}
onKeyDown={this.handleKeyDown}
- onHeightChange={this.resizePostHolder}
messageText={this.state.messageText}
createMessage={this.props.intl.formatMessage(holders.write)}
channelId={this.state.channelId}
@@ -472,6 +446,7 @@ class CreatePost extends React.Component {
<FileUpload
ref='fileUpload'
getFileCount={this.getFileCount}
+ onClick={this.handleUploadClick}
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
@@ -506,4 +481,4 @@ CreatePost.propTypes = {
intl: intlShape.isRequired
};
-export default injectIntl(CreatePost); \ No newline at end of file
+export default injectIntl(CreatePost);
diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx
index 34fd724f5..9d7dcb3e5 100644
--- a/web/react/components/delete_post_modal.jsx
+++ b/web/react/components/delete_post_modal.jsx
@@ -88,7 +88,7 @@ export default class DeletePostModal extends React.Component {
}
}
- PostStore.removePost(this.state.post.id, this.state.post.channel_id);
+ PostStore.deletePost(this.state.post);
AsyncClient.getPosts(this.state.post.channel_id);
},
(err) => {
diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx
index 746289653..f5c32c825 100644
--- a/web/react/components/file_upload.jsx
+++ b/web/react/components/file_upload.jsx
@@ -310,6 +310,7 @@ class FileUpload extends React.Component {
ref='fileInput'
type='file'
onChange={this.handleChange}
+ onClick={this.props.onClick}
multiple={multiple}
accept={accept}
/>
@@ -322,6 +323,7 @@ FileUpload.propTypes = {
intl: intlShape.isRequired,
onUploadError: React.PropTypes.func,
getFileCount: React.PropTypes.func,
+ onClick: React.PropTypes.func,
onFileUpload: React.PropTypes.func,
onUploadStart: React.PropTypes.func,
onTextDrop: React.PropTypes.func,
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 16f8528b2..d71ac6ec7 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -44,7 +44,6 @@ class PostBody extends React.Component {
this.state = {
links: linkData.links,
- message: linkData.text,
post: this.props.post,
hasUserProfiles: profiles && Object.keys(profiles).length > 1
};
@@ -106,7 +105,9 @@ class PostBody extends React.Component {
if (this.props.post.filenames.length === 0 && this.state.links && this.state.links.length > 0) {
this.embed = this.createEmbed(linkData.links[0]);
}
- this.setState({links: linkData.links, message: linkData.text});
+ this.setState({
+ links: linkData.links
+ });
}
createEmbed(link) {
@@ -310,6 +311,23 @@ class PostBody extends React.Component {
);
}
+ let message;
+ if (this.props.post.state === Constants.POST_DELETED) {
+ message = (
+ <FormattedMessage
+ id='post_body.deleted'
+ defaultMessage='(message deleted)'
+ />
+ );
+ } else {
+ message = (
+ <span
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message)}}
+ />
+ );
+ }
+
return (
<div>
{comment}
@@ -320,11 +338,7 @@ class PostBody extends React.Component {
className={postClass}
>
{loading}
- <span
- ref='message_span'
- onClick={TextFormatting.handleClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
- />
+ {message}
</div>
<PostBodyAdditionalContent
post={this.state.post}
@@ -346,4 +360,4 @@ PostBody.propTypes = {
handleCommentClick: React.PropTypes.func.isRequired
};
-export default injectIntl(PostBody); \ No newline at end of file
+export default injectIntl(PostBody);
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index ddb393520..b1bc8ca14 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -23,13 +23,14 @@ export default class PostInfo extends React.Component {
};
this.handlePermalinkCopy = this.handlePermalinkCopy.bind(this);
+ this.removePost = this.removePost.bind(this);
}
createDropdown() {
var post = this.props.post;
var isOwner = UserStore.getCurrentId() === post.user_id;
var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles);
- if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || post.state === Constants.POST_DELETED) {
+ if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || Utils.isPostEphemeral(post)) {
return '';
}
@@ -166,6 +167,25 @@ export default class PostInfo extends React.Component {
this.setState({copiedLink: false});
}
}
+ removePost() {
+ EventHelpers.emitRemovePost(this.props.post);
+ }
+ createRemovePostButton(post) {
+ if (!Utils.isPostEphemeral(post)) {
+ return null;
+ }
+
+ return (
+ <a
+ href='#'
+ className='post__remove theme'
+ type='button'
+ onClick={this.removePost}
+ >
+ {'×'}
+ </a>
+ );
+ }
render() {
var post = this.props.post;
var comments = '';
@@ -178,7 +198,7 @@ export default class PostInfo extends React.Component {
commentCountText = '';
}
- if (post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && post.state !== Constants.POST_DELETED) {
+ if (post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && !Utils.isPostEphemeral(post)) {
comments = (
<a
href='#'
@@ -264,6 +284,7 @@ export default class PostInfo extends React.Component {
>
{permalinkOverlay}
</Overlay>
+ {this.createRemovePostButton(post)}
</li>
</ul>
);
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index 00e5ace98..ec299087d 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -20,6 +20,7 @@ export default class Textbox extends React.Component {
constructor(props) {
super(props);
+ this.focus = this.focus.bind(this);
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onRecievedError = this.onRecievedError.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
@@ -81,6 +82,10 @@ export default class Textbox extends React.Component {
}
}
+ focus() {
+ this.refs.message.getTextbox().focus();
+ }
+
resize() {
const textbox = this.refs.message.getTextbox();
const $textbox = $(textbox);
@@ -90,8 +95,6 @@ export default class Textbox extends React.Component {
const borders = parseInt($textbox.css('border-bottom-width'), 10) + parseInt($textbox.css('border-top-width'), 10);
const maxHeight = parseInt($textbox.css('max-height'), 10) - borders;
- const prevHeight = $textbox.height();
-
// set the height to auto and remove the scrollbar so we can get the actual size of the contents
$textbox.css('height', 'auto').css('overflow-y', 'hidden');
@@ -116,10 +119,6 @@ export default class Textbox extends React.Component {
if (this.state.preview) {
$(ReactDOM.findDOMNode(this.refs.preview)).height(height + borders);
}
-
- if (height !== prevHeight && this.props.onHeightChange) {
- this.props.onHeightChange();
- }
}
showPreview(e) {
@@ -211,7 +210,6 @@ Textbox.propTypes = {
messageText: React.PropTypes.string.isRequired,
onUserInput: React.PropTypes.func.isRequired,
onKeyPress: React.PropTypes.func.isRequired,
- onHeightChange: React.PropTypes.func,
createMessage: React.PropTypes.string.isRequired,
onKeyDown: React.PropTypes.func,
supportsCommands: React.PropTypes.bool.isRequired
diff --git a/web/react/components/user_settings/manage_command_hooks.jsx b/web/react/components/user_settings/manage_command_hooks.jsx
index bcf0a6c82..b2fc0a4e1 100644
--- a/web/react/components/user_settings/manage_command_hooks.jsx
+++ b/web/react/components/user_settings/manage_command_hooks.jsx
@@ -39,6 +39,14 @@ const holders = defineMessages({
adUrlPlaceholder: {
id: 'user.settings.cmds.url.placeholder',
defaultMessage: 'Must start with http:// or https://'
+ },
+ autocompleteYes: {
+ id: 'user.settings.cmds.auto_complete.yes',
+ defaultMessage: 'yes'
+ },
+ autocompleteNo: {
+ id: 'user.settings.cmds.auto_complete.no',
+ defaultMessage: 'no'
}
});
@@ -295,7 +303,7 @@ export default class ManageCommandCmds extends React.Component {
id='user.settings.cmds.auto_complete'
defaultMessage='Auto Complete: '
/>
- </strong><span className='word-break--all'>{cmd.auto_complete ? 'yes' : 'no'}</span>
+ </strong><span className='word-break--all'>{cmd.auto_complete ? this.props.intl.formatMessage(holders.autocompleteYes) : this.props.intl.formatMessage(holders.autocompleteNo)}</span>
</div>
<div className='padding-top x2'>
<strong>
@@ -414,7 +422,12 @@ export default class ManageCommandCmds extends React.Component {
id='user.settings.cmds.add_desc'
defaultMessage='Create commands to send message events to an external integration. Please see <a href="http://mattermost.org/commands">http://mattermost.org/commands</a> to learn more.'
/>
- <div><label className='control-label padding-top x2'>{'Add a new command'}</label></div>
+ <div><label className='control-label padding-top x2'>
+ <FormattedMessage
+ id='user.settings.cmds.add_new'
+ defaultMessage='Add a new command'
+ />
+ </label></div>
<div className='padding-top divider-light'></div>
<div className='padding-top'>
<div className='padding-top x2'>
@@ -433,7 +446,12 @@ export default class ManageCommandCmds extends React.Component {
placeholder={this.props.intl.formatMessage(holders.addDisplayNamePlaceholder)}
/>
</div>
- <div className='padding-top'>{'Command display name.'}</div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.cmd_display_name'
+ defaultMessage='Command display name.'
+ />
+ </div>
</div>
<div className='padding-top x2'>
<label className='control-label'>
@@ -638,7 +656,10 @@ export default class ManageCommandCmds extends React.Component {
disabled={disableButton}
onClick={this.addNewCmd}
>
- {'Add'}
+ <FormattedMessage
+ id='user.settings.cmds.add'
+ defaultMessage='Add'
+ />
</a>
</div>
</div>
diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index 91a03eb70..786e53f10 100644
--- a/web/react/components/user_settings/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -294,7 +294,7 @@ class NotificationsTab extends React.Component {
<span>
<FormattedMessage
id='user.settings.notifications.info'
- defaultMessage='Desktop notification sounds are available on Firefox, Safari, Chrome, Internet Explorer, and Edge.'
+ defaultMessage='Desktop notifications are available on Firefox, Safari, Chrome, Internet Explorer, and Edge.'
/>
</span>
);
@@ -395,8 +395,8 @@ class NotificationsTab extends React.Component {
const extraInfo = (
<span>
<FormattedMessage
- id='user.settings.notifications.info'
- defaultMessage='Desktop notification sounds are available on Firefox, Safari, Chrome, Internet Explorer, and Edge.'
+ id='user.settings.notifications.sounds_info'
+ defaultMessage='Desktop notifications sounds are available on Firefox, Safari, Chrome, Internet Explorer, and Edge.'
/>
</span>
);