* PLT-3647 Added config settings for email batching

* PLT-3647 Refactored generation of email notification

* PLT-3647 Added serverside code for email batching

* PLT-3647 Updated settings UI to enable email batching

* PLT-3647 Removed debug code

* PLT-3647 Fixed 0-padding of minutes in batched notification

* PLT-3647 Updated clientside UI for when email batching is disabled

* Go fmt

* PLT-3647 Changed email batching to be disabled by default

* Updated batched email message

* Added email batching toggle to system console

* Changed Email Notifications > Immediate setting to a 30 second batch interval

* Go fmt

* Fixed link to Mattermost icon in batched email notification

* Updated users to use 30 second email batching by default

* Fully disabled email batching when clustering is enabled

* Fixed email batching setting in the system console

* Fixed casing of 'Send Email notifications' -> 'Send email notifications'

* Updating UI Improvements for email batching (#3736)

* Updated text for notification settings and SiteURL.

* Prevented enabling email batching when SiteURL isn't set in the system console

* Re-added a couple debug messages

* Added warning text when clustering is enabled
Этот коммит содержится в:
Harrison Healey
2016-08-16 14:41:47 -04:00
коммит произвёл Christopher Speller
родитель dde158c57f
Коммит 8203fd16ce
25 изменённых файлов: 1123 добавлений и 212 удалений

Просмотреть файл

@@ -0,0 +1,211 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {savePreference} from 'utils/async_client.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import {localizeMessage} from 'utils/utils.jsx';
import {FormattedMessage} from 'react-intl';
import SettingItemMin from 'components/setting_item_min.jsx';
import SettingItemMax from 'components/setting_item_max.jsx';
import {Preferences} from 'utils/constants.jsx';
const INTERVAL_IMMEDIATE = 30; // "immediate" is a 30 second interval
const INTERVAL_FIFTEEN_MINUTES = 15 * 60;
const INTERVAL_HOUR = 60 * 60;
export default class EmailNotificationSetting extends React.Component {
static propTypes = {
activeSection: React.PropTypes.string.isRequired,
updateSection: React.PropTypes.func.isRequired,
enableEmail: React.PropTypes.bool.isRequired,
onChange: React.PropTypes.func.isRequired,
onSubmit: React.PropTypes.func.isRequired,
serverError: React.PropTypes.string.isRequired
};
constructor(props) {
super(props);
this.submit = this.submit.bind(this);
this.expand = this.expand.bind(this);
this.collapse = this.collapse.bind(this);
this.state = {
emailInterval: PreferenceStore.getInt(Preferences.CATEGORY_NOTIFICATIONS, Preferences.EMAIL_INTERVAL, INTERVAL_IMMEDIATE)
};
}
handleChange(enableEmail, emailInterval) {
this.props.onChange(enableEmail);
this.setState({emailInterval});
}
submit() {
// until the rest of the notification settings are moved to preferences, we have to do this separately
savePreference(Preferences.CATEGORY_NOTIFICATIONS, Preferences.EMAIL_INTERVAL, this.state.emailInterval.toString());
this.props.onSubmit();
}
expand() {
this.props.updateSection('email');
}
collapse() {
this.props.updateSection('');
}
render() {
if (this.props.activeSection !== 'email') {
let description;
if (this.props.enableEmail === 'true') {
switch (this.state.emailInterval) {
case INTERVAL_IMMEDIATE:
description = (
<FormattedMessage
id='user.settings.notifications.email.immediately'
defaultMessage='Immediately'
/>
);
break;
case INTERVAL_HOUR:
description = (
<FormattedMessage
id='user.settings.notifications.email.everyHour'
defaultMessage='Every hour'
/>
);
break;
default:
description = (
<FormattedMessage
id='user.settings.notifications.email.everyXMinutes'
defaultMessage='Every {count, plural, one {minute} other {{count, number} minutes}}'
values={{count: this.state.emailInterval / 60}}
/>
);
}
} else {
description = (
<FormattedMessage
id='user.settings.notifications.email.never'
defaultMessage='Never'
/>
);
}
return (
<SettingItemMin
title={localizeMessage('user.settings.notifications.emailNotifications', 'Send Email notifications')}
describe={description}
updateSection={this.expand}
/>
);
}
let batchingOptions = null;
let batchingInfo = null;
if (window.mm_config.EnableEmailBatching === 'true') {
batchingOptions = (
<div>
<div className='radio'>
<label>
<input
type='radio'
name='emailNotifications'
checked={this.props.enableEmail === 'true' && this.state.emailInterval === INTERVAL_FIFTEEN_MINUTES}
onChange={this.handleChange.bind(this, 'true', INTERVAL_FIFTEEN_MINUTES)}
/>
<FormattedMessage
id='user.settings.notifications.email.everyXMinutes'
defaultMessage='Every {count} minutes'
values={{count: INTERVAL_FIFTEEN_MINUTES / 60}}
/>
</label>
</div>
<div className='radio'>
<label>
<input
type='radio'
name='emailNotifications'
checked={this.props.enableEmail === 'true' && this.state.emailInterval === INTERVAL_HOUR}
onChange={this.handleChange.bind(this, 'true', INTERVAL_HOUR)}
/>
<FormattedMessage
id='user.settings.notifications.everyHour'
defaultMessage='Every hour'
/>
</label>
</div>
</div>
);
batchingInfo = (
<FormattedMessage
id='user.settings.notifications.emailBatchingInfo'
defaultMessage='Notifications are combined into a single email and sent at the maximum frequency selected here.'
/>
);
}
return (
<SettingItemMax
title={localizeMessage('user.settings.notifications.emailNotifications', 'Send email notifications')}
inputs={[
<div key='userNotificationEmailOptions'>
<div className='radio'>
<label>
<input
type='radio'
name='emailNotifications'
checked={this.props.enableEmail === 'true' && this.state.emailInterval === INTERVAL_IMMEDIATE}
onChange={this.handleChange.bind(this, 'true', INTERVAL_IMMEDIATE)}
/>
<FormattedMessage
id='user.settings.notifications.email.immediately'
defaultMessage='Immediately'
/>
</label>
</div>
{batchingOptions}
<div className='radio'>
<label>
<input
type='radio'
name='emailNotifications'
checked={this.props.enableEmail === 'false'}
onChange={this.handleChange.bind(this, 'false', INTERVAL_IMMEDIATE)}
/>
<FormattedMessage
id='user.settings.notifications.never'
defaultMessage='Never'
/>
</label>
</div>
<br/>
<div>
<FormattedMessage
id='user.settings.notifications.emailInfo'
defaultMessage='Email notifications that are sent for mentions and direct messages when you are offline or away from {siteName} for more than 5 minutes.'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
{' '}
{batchingInfo}
</div>
</div>
]}
submit={this.submit}
server_error={this.props.serverError}
updateSection={this.collapse}
/>
);
}
}

Просмотреть файл

@@ -2,8 +2,8 @@
// See License.txt for license information.
import $ from 'jquery';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import SettingItemMin from 'components/setting_item_min.jsx';
import SettingItemMax from 'components/setting_item_max.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -12,7 +12,8 @@ import * as AsyncClient from 'utils/async_client.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
import * as Utils from 'utils/utils.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
import EmailNotificationSetting from './email_notification_setting.jsx';
import {FormattedMessage} from 'react-intl';
function getNotificationsStateFromStores() {
var user = UserStore.getCurrentUser();
@@ -30,9 +31,9 @@ function getNotificationsStateFromStores() {
if (user.notify_props && user.notify_props.comments) {
comments = user.notify_props.comments;
}
var email = 'true';
var enableEmail = 'true';
if (user.notify_props && user.notify_props.email) {
email = user.notify_props.email;
enableEmail = user.notify_props.email;
}
var push = 'mention';
if (user.notify_props && user.notify_props.push) {
@@ -78,7 +79,7 @@ function getNotificationsStateFromStores() {
return {
notifyLevel: desktop,
notifyPushLevel: push,
enableEmail: email,
enableEmail,
soundNeeded,
enableSound: sound,
usernameKey,
@@ -91,36 +92,9 @@ function getNotificationsStateFromStores() {
};
}
const holders = defineMessages({
desktop: {
id: 'user.settings.notifications.desktop',
defaultMessage: 'Send desktop notifications'
},
desktopSounds: {
id: 'user.settings.notifications.desktopSounds',
defaultMessage: 'Desktop notification sounds'
},
emailNotifications: {
id: 'user.settings.notifications.emailNotifications',
defaultMessage: 'Email notifications'
},
wordsTrigger: {
id: 'user.settings.notifications.wordsTrigger',
defaultMessage: 'Words that trigger mentions'
},
comments: {
id: 'user.settings.notifications.comments',
defaultMessage: 'Comment threads notifications'
},
close: {
id: 'user.settings.notifications.close',
defaultMessage: 'Close'
}
});
import React from 'react';
class NotificationsTab extends React.Component {
export default class NotificationsTab extends React.Component {
constructor(props) {
super(props);
@@ -142,6 +116,7 @@ class NotificationsTab extends React.Component {
this.state = getNotificationsStateFromStores();
}
handleSubmit() {
var data = {};
data.user_id = this.props.user.id;
@@ -178,31 +153,38 @@ class NotificationsTab extends React.Component {
}
);
}
handleCancel(e) {
this.updateState();
this.props.updateSection('');
e.preventDefault();
$('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
}
updateSection(section) {
this.updateState();
this.props.updateSection(section);
}
updateState() {
const newState = getNotificationsStateFromStores();
if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
componentDidMount() {
UserStore.addChangeListener(this.onListenerChange);
}
componentWillUnmount() {
UserStore.removeChangeListener(this.onListenerChange);
}
onListenerChange() {
this.updateState();
}
handleNotifyRadio(notifyLevel) {
this.setState({notifyLevel});
this.refs.wrapper.focus();
@@ -222,22 +204,28 @@ class NotificationsTab extends React.Component {
this.setState({enableEmail});
this.refs.wrapper.focus();
}
handleSoundRadio(enableSound) {
this.setState({enableSound});
this.refs.wrapper.focus();
}
updateUsernameKey(val) {
this.setState({usernameKey: val});
}
updateMentionKey(val) {
this.setState({mentionKey: val});
}
updateFirstNameKey(val) {
this.setState({firstNameKey: val});
}
updateChannelKey(val) {
this.setState({channelKey: val});
}
updateCustomMentionKeys() {
var checked = this.refs.customcheck.checked;
@@ -250,10 +238,12 @@ class NotificationsTab extends React.Component {
this.setState({customKeys: '', customKeysChecked: false});
}
}
onCustomChange() {
this.refs.customcheck.checked = true;
this.updateCustomMentionKeys();
}
createPushNotificationSection() {
var handleUpdateDesktopSection;
if (this.props.activeSection === 'push') {
@@ -312,8 +302,8 @@ class NotificationsTab extends React.Component {
onChange={this.handlePushRadio.bind(this, 'none')}
/>
<FormattedMessage
id='user.settings.push_notification.off'
defaultMessage='Off'
id='user.settings.notifications.never'
defaultMessage='Never'
/>
</label>
</div>
@@ -346,7 +336,7 @@ class NotificationsTab extends React.Component {
return (
<SettingItemMax
title={Utils.localizeMessage('user.settings.notifications.push', 'Mobile push notifications')}
title={Utils.localizeMessage('user.settings.notifications.push', 'Send mobile push notifications')}
extraInfo={extraInfo}
inputs={inputs}
submit={submit}
@@ -367,8 +357,8 @@ class NotificationsTab extends React.Component {
} else if (this.state.notifyPushLevel === 'none') {
describe = (
<FormattedMessage
id='user.settings.push_notification.off'
defaultMessage='Off'
id='user.settings.notifications.never'
defaultMessage='Never'
/>
);
} else if (global.window.mm_config.SendPushNotifications === 'false') {
@@ -393,14 +383,14 @@ class NotificationsTab extends React.Component {
return (
<SettingItemMin
title={Utils.localizeMessage('user.settings.notifications.push', 'Mobile push notifications')}
title={Utils.localizeMessage('user.settings.notifications.push', 'Send mobile push notifications')}
describe={describe}
updateSection={handleUpdateDesktopSection}
/>
);
}
render() {
const {formatMessage} = this.props.intl;
const serverError = this.state.serverError;
var user = this.props.user;
@@ -479,7 +469,7 @@ class NotificationsTab extends React.Component {
desktopSection = (
<SettingItemMax
title={formatMessage(holders.desktop)}
title={Utils.localizeMessage('user.settings.notifications.desktop', 'Send desktop notifications')}
extraInfo={extraInfo}
inputs={inputs}
submit={this.handleSubmit}
@@ -518,7 +508,7 @@ class NotificationsTab extends React.Component {
desktopSection = (
<SettingItemMin
title={formatMessage(holders.desktop)}
title={Utils.localizeMessage('user.settings.notifications.desktop', 'Send desktop notifications')}
describe={describe}
updateSection={handleUpdateDesktopSection}
/>
@@ -583,7 +573,7 @@ class NotificationsTab extends React.Component {
soundSection = (
<SettingItemMax
title={formatMessage(holders.desktopSounds)}
title={Utils.localizeMessage('user.settings.notifications.desktopSounds', 'Desktop notification sounds')}
extraInfo={extraInfo}
inputs={inputs}
submit={this.handleSubmit}
@@ -622,7 +612,7 @@ class NotificationsTab extends React.Component {
soundSection = (
<SettingItemMin
title={formatMessage(holders.desktopSounds)}
title={Utils.localizeMessage('user.settings.notifications.desktopSounds', 'Desktop notification sounds')}
describe={describe}
updateSection={handleUpdateSoundSection}
disableOpen={!this.state.soundNeeded}
@@ -630,102 +620,6 @@ class NotificationsTab extends React.Component {
);
}
var emailSection;
var handleUpdateEmailSection;
if (this.props.activeSection === 'email') {
var emailActive = [false, false];
if (this.state.enableEmail === 'false') {
emailActive[1] = true;
} else {
emailActive[0] = true;
}
let inputs = [];
inputs.push(
<div key='userNotificationEmailOptions'>
<div className='radio'>
<label>
<input
type='radio'
name='emailNotifications'
checked={emailActive[0]}
onChange={this.handleEmailRadio.bind(this, 'true')}
/>
<FormattedMessage
id='user.settings.notifications.on'
defaultMessage='On'
/>
</label>
<br/>
</div>
<div className='radio'>
<label>
<input
type='radio'
name='emailNotifications'
checked={emailActive[1]}
onChange={this.handleEmailRadio.bind(this, 'false')}
/>
<FormattedMessage
id='user.settings.notifications.off'
defaultMessage='Off'
/>
</label>
<br/>
</div>
<div><br/>
<FormattedMessage
id='user.settings.notifications.emailInfo'
defaultMessage='Email notifications are sent for mentions and direct messages after youve been offline for more than 60 seconds or away from {siteName} for more than 5 minutes.'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
</div>
</div>
);
emailSection = (
<SettingItemMax
title={formatMessage(holders.emailNotifications)}
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
updateSection={this.handleCancel}
/>
);
} else {
let describe = '';
if (this.state.enableEmail === 'false') {
describe = (
<FormattedMessage
id='user.settings.notifications.off'
defaultMessage='Off'
/>
);
} else {
describe = (
<FormattedMessage
id='user.settings.notifications.on'
defaultMessage='On'
/>
);
}
handleUpdateEmailSection = function updateEmailSection() {
this.props.updateSection('email');
}.bind(this);
emailSection = (
<SettingItemMin
title={formatMessage(holders.emailNotifications)}
describe={describe}
updateSection={handleUpdateEmailSection}
/>
);
}
var keysSection;
var handleUpdateKeysSection;
if (this.props.activeSection === 'keys') {
@@ -859,7 +753,7 @@ class NotificationsTab extends React.Component {
keysSection = (
<SettingItemMax
title={formatMessage(holders.wordsTrigger)}
title={Utils.localizeMessage('user.settings.notifications.wordsTrigger', 'Words that trigger mentions')}
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
@@ -910,7 +804,7 @@ class NotificationsTab extends React.Component {
keysSection = (
<SettingItemMin
title={formatMessage(holders.wordsTrigger)}
title={Utils.localizeMessage('user.settings.notifications.wordsTrigger', 'Words that trigger mentions')}
describe={describe}
updateSection={handleUpdateKeysSection}
/>
@@ -991,7 +885,7 @@ class NotificationsTab extends React.Component {
commentsSection = (
<SettingItemMax
title={formatMessage(holders.comments)}
title={Utils.localizeMessage('user.settings.notifications.comments', 'Comment threads notifications')}
extraInfo={extraInfo}
inputs={inputs}
submit={this.handleSubmit}
@@ -1030,7 +924,7 @@ class NotificationsTab extends React.Component {
commentsSection = (
<SettingItemMin
title={formatMessage(holders.comments)}
title={Utils.localizeMessage('user.settings.notifications.comments', 'Comment threads notifications')}
describe={describe}
updateSection={handleUpdateCommentsSection}
/>
@@ -1046,7 +940,6 @@ class NotificationsTab extends React.Component {
type='button'
className='close'
data-dismiss='modal'
aria-label={formatMessage(holders.close)}
onClick={this.props.closeModal}
>
<span aria-hidden='true'>{'×'}</span>
@@ -1082,7 +975,14 @@ class NotificationsTab extends React.Component {
<div className='divider-light'/>
{soundSection}
<div className='divider-light'/>
{emailSection}
<EmailNotificationSetting
activeSection={this.props.activeSection}
updateSection={this.props.updateSection}
enableEmail={this.state.enableEmail}
onChange={this.handleEmailRadio}
onSubmit={this.handleSubmit}
serverError={this.state.serverError}
/>
<div className='divider-light'/>
{pushNotificationSection}
<div className='divider-light'/>
@@ -1103,7 +1003,6 @@ NotificationsTab.defaultProps = {
activeTab: ''
};
NotificationsTab.propTypes = {
intl: intlShape.isRequired,
user: React.PropTypes.object,
updateSection: React.PropTypes.func,
updateTab: React.PropTypes.func,
@@ -1112,5 +1011,3 @@ NotificationsTab.propTypes = {
closeModal: React.PropTypes.func.isRequired,
collapseModal: React.PropTypes.func.isRequired
};
export default injectIntl(NotificationsTab);