diff --git a/app/actions/navigation/index.js b/app/actions/navigation/index.js
index 1d7986a1b..7646a59b8 100644
--- a/app/actions/navigation/index.js
+++ b/app/actions/navigation/index.js
@@ -232,3 +232,12 @@ export function goToCreateChannel(channelType) {
}, getState);
};
}
+
+export function goToAccountNotifications() {
+ return async (dispatch, getState) => {
+ dispatch({
+ type: NavigationTypes.NAVIGATION_PUSH,
+ route: Routes.AccountNotifications
+ }, getState);
+ };
+}
diff --git a/app/actions/views/account_notifications.js b/app/actions/views/account_notifications.js
new file mode 100644
index 000000000..24f4e6ca0
--- /dev/null
+++ b/app/actions/views/account_notifications.js
@@ -0,0 +1,29 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {updateUserNotifyProps} from 'service/actions/users';
+import {Preferences} from 'service/constants';
+import {savePreferences} from 'service/actions/preferences';
+
+export function handleUpdateUserNotifyProps(notifyProps) {
+ return async (dispatch, getState) => {
+ const state = getState();
+ const config = state.entities.general.config;
+
+ const {interval, ...otherProps} = notifyProps;
+
+ const email = notifyProps.email;
+ if (config.EnableEmailBatching === 'true' && email !== 'false') {
+ const emailInterval = [{
+ user_id: notifyProps.user_id,
+ category: Preferences.CATEGORY_NOTIFICATIONS,
+ name: Preferences.EMAIL_INTERVAL,
+ value: interval
+ }];
+
+ await savePreferences(emailInterval)(dispatch, getState);
+ }
+
+ await updateUserNotifyProps({...otherProps, email})(dispatch, getState);
+ };
+}
diff --git a/app/components/checkmark.js b/app/components/checkmark.js
new file mode 100644
index 000000000..006c0239c
--- /dev/null
+++ b/app/components/checkmark.js
@@ -0,0 +1,30 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+import Svg, {
+ Path
+} from 'react-native-svg';
+
+export default class CheckMark extends React.Component {
+ static propTypes = {
+ width: React.PropTypes.number.isRequired,
+ height: React.PropTypes.number.isRequired,
+ color: React.PropTypes.string.isRequired
+ };
+
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/app/navigation/routes.js b/app/navigation/routes.js
index 4c43f4c40..f96fda3d7 100644
--- a/app/navigation/routes.js
+++ b/app/navigation/routes.js
@@ -2,6 +2,7 @@
// See License.txt for license information.
import {
+ AccountNotifications,
AccountSettings,
ChannelView,
ChannelDrawer,
@@ -33,6 +34,14 @@ export const RouteTransitions = keyMirror({
});
export const Routes = {
+ AccountNotifications: {
+ key: 'AccountNotifications',
+ transition: RouteTransitions.Horizontal,
+ component: AccountNotifications,
+ navigationProps: {
+ title: {id: 'user.settings.modal.notifications', defaultMessage: 'Notifications'}
+ }
+ },
AccountSettings: {
key: 'AccountSettings',
transition: RouteTransitions.Horizontal,
diff --git a/app/scenes/account_notifications/account_notifications.js b/app/scenes/account_notifications/account_notifications.js
new file mode 100644
index 000000000..f03286a4a
--- /dev/null
+++ b/app/scenes/account_notifications/account_notifications.js
@@ -0,0 +1,533 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PropTypes, PureComponent} from 'react';
+import {
+ ScrollView,
+ StyleSheet,
+ View
+} from 'react-native';
+
+import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+import EventEmitter from 'service/utils/event_emitter';
+import {Preferences, RequestStatus} from 'service/constants';
+import {getPreferencesByCategory} from 'service/utils/preference_utils';
+
+import Section from './section';
+import SectionItem from './section_item';
+import SaveNotificationsButton from './save_notifications_button';
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return StyleSheet.create({
+ input: {
+ color: theme.centerChannelColor,
+ fontSize: 12,
+ height: 40
+ },
+ separator: {
+ height: 1,
+ flex: 1,
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
+ marginHorizontal: 15
+ },
+ scrollView: {
+ flex: 1,
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.03)
+ },
+ scrollViewContent: {
+ paddingBottom: 30
+ },
+ wrapper: {
+ flex: 1,
+ backgroundColor: theme.centerChannelBg
+ }
+ });
+});
+
+const SAVE_NOTIFY_PROPS = 'save_notify_props';
+const SAVING_NOTIFY_PROPS = 'saving_notify_props';
+
+export default class AccountNofications extends PureComponent {
+ static propTypes = {
+ actions: PropTypes.shape({
+ goBack: PropTypes.func.isRequired,
+ handleUpdateUserNotifyProps: PropTypes.func.isRequired
+ }),
+ config: PropTypes.object.isRequired,
+ currentUser: PropTypes.object.isRequired,
+ myPreferences: PropTypes.object.isRequired,
+ saveRequestStatus: PropTypes.string.isRequired,
+ subscribeToHeaderEvent: PropTypes.func.isRequired,
+ theme: PropTypes.object.isRequired,
+ unsubscribeFromHeaderEvent: PropTypes.func.isRequired
+ };
+
+ static navigationProps = {
+ renderRightComponent: (props, emitter) => {
+ return ;
+ }
+ }
+
+ constructor(props) {
+ super(props);
+
+ const {currentUser} = props;
+ const notifyProps = currentUser.notify_props || {};
+ this.setStateFromNotifyProps(notifyProps);
+ }
+
+ componentWillMount() {
+ this.props.subscribeToHeaderEvent(SAVE_NOTIFY_PROPS, this.saveUserNotifyProps);
+ }
+
+ componentWillUnmount() {
+ this.props.unsubscribeFromHeaderEvent(SAVE_NOTIFY_PROPS);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.currentUser !== this.props.currentUser) {
+ const {notify_props: notifyProps} = nextProps.currentUser;
+ this.setStateFromNotifyProps(notifyProps);
+ }
+
+ if (nextProps.saveRequestStatus === RequestStatus.SUCCESS && this.props.saveRequestStatus === RequestStatus.STARTED) {
+ this.props.actions.goBack();
+ } else if (nextProps.saveRequestStatus === RequestStatus.FAILURE && this.props.saveRequestStatus === RequestStatus.STARTED) {
+ EventEmitter.emit(SAVING_NOTIFY_PROPS, false);
+ this.setStateFromNotifyProps(this.currentUser.notify_props);
+ }
+ }
+
+ setStateFromNotifyProps = (notifyProps) => {
+ const mentionKeys = (notifyProps.mention_keys || '').split(',');
+ const usernameMentionIndex = mentionKeys.indexOf(this.props.currentUser.username);
+ if (usernameMentionIndex > -1) {
+ mentionKeys.splice(usernameMentionIndex, 1);
+ }
+
+ const email = notifyProps.email;
+ let interval;
+ if (this.props.config.EnableEmailBatching === 'true') {
+ const emailPreferences = getPreferencesByCategory(this.props.myPreferences, Preferences.CATEGORY_NOTIFICATIONS);
+ interval = emailPreferences.get(Preferences.EMAIL_INTERVAL).value;
+ }
+
+ const newState = {
+ ...notifyProps,
+ email,
+ interval,
+ usernameMention: usernameMentionIndex > -1,
+ mention_keys: mentionKeys.join(',')
+ };
+
+ if (this.state) {
+ this.setState(newState);
+ } else {
+ this.state = {...newState};
+ }
+ }
+
+ toggleFirstNameMention = () => {
+ this.setState({
+ first_name: (!(this.state.first_name === 'true')).toString()
+ });
+ }
+
+ toggleUsernameMention = () => {
+ this.setState({
+ usernameMention: !this.state.usernameMention
+ });
+ }
+
+ toggleChannelMentions = () => {
+ this.setState({
+ channel: (!(this.state.channel === 'true')).toString()
+ });
+ }
+
+ updateMentionKeys = (text) => {
+ this.setState({
+ mention_keys: text
+ });
+ }
+
+ setEmailNotifications = (value) => {
+ const {config} = this.props;
+ let email = value;
+ let interval;
+
+ const emailBatchingEnabled = config.EnableEmailBatching === 'true';
+ if (emailBatchingEnabled && value !== 'false') {
+ interval = value;
+ email = 'true';
+ }
+
+ this.setState({
+ email,
+ interval
+ });
+ }
+
+ setReplyNotifications = (value) => {
+ this.setState({
+ comments: value
+ });
+ }
+
+ setMobilePush = (value) => {
+ this.setState({
+ push: value
+ });
+ }
+
+ setMobilePushStatus = (value) => {
+ this.setState({
+ push_status: value
+ });
+ }
+
+ saveUserNotifyProps = () => {
+ EventEmitter.emit(SAVING_NOTIFY_PROPS, true);
+ let {mention_keys: mentionKeys, usernameMention, ...notifyProps} = this.state; //eslint-disable-line prefer-const
+
+ if (mentionKeys.length > 0) {
+ mentionKeys = mentionKeys.split(',').map((m) => m.replace(/\s/g, ''));
+ } else {
+ mentionKeys = [];
+ }
+
+ if (usernameMention) {
+ mentionKeys.push(`${this.props.currentUser.username}`);
+ }
+
+ mentionKeys = mentionKeys.join(',');
+
+ this.props.actions.handleUpdateUserNotifyProps({
+ ...notifyProps,
+ mention_keys: mentionKeys,
+ user_id: this.props.currentUser.id
+ });
+ }
+
+ buildMentionSection = () => {
+ const {currentUser, theme} = this.props;
+ const style = getStyleSheet(theme);
+
+ return (
+
+ {currentUser.first_name.length > 0 &&
+
+
+
+
+ }
+
+
+
+
+
+
+
+
+ );
+ }
+
+ buildEmailSection = () => {
+ const {config, theme} = this.props;
+ const style = getStyleSheet(theme);
+
+ const sendEmailNotifications = config.SendEmailNotifications === 'true';
+ const emailBatchingEnabled = config.EnableEmailBatching === 'true';
+
+ let sendImmediatley = this.state.email === 'true';
+ let sendImmediatleyValue = 'true';
+ let fifteenMinutes;
+ let hourly;
+ const never = this.state.email === 'false';
+
+ if (emailBatchingEnabled && this.state.email !== 'false') {
+ sendImmediatley = this.state.interval === Preferences.INTERVAL_IMMEDIATE.toString();
+ fifteenMinutes = this.state.interval === Preferences.INTERVAL_FIFTEEN_MINUTES.toString();
+ hourly = this.state.interval === Preferences.INTERVAL_HOUR.toString();
+
+ sendImmediatleyValue = Preferences.INTERVAL_IMMEDIATE.toString();
+ }
+
+ return (
+
+ {sendEmailNotifications &&
+
+
+
+ {emailBatchingEnabled &&
+
+
+
+
+
+
+ }
+
+
+ }
+ {!sendEmailNotifications &&
+
+ }
+
+ );
+ }
+
+ buildReplySection = () => {
+ const {theme} = this.props;
+ const style = getStyleSheet(theme);
+
+ return (
+
+ );
+ }
+
+ buildMobilePushSection = () => {
+ const {config, theme} = this.props;
+ const style = getStyleSheet(theme);
+
+ const pushNotificationsEnabled = config.SendPushNotifications === 'true';
+ if (!pushNotificationsEnabled) {
+ return null;
+ }
+
+ return (
+
+ );
+ }
+
+ buildMobilePushStatusSection = () => {
+ const {config, theme} = this.props;
+ const style = getStyleSheet(theme);
+
+ const showSection = config.SendPushNotifications === 'true' && this.state.push !== 'none';
+ if (!showSection) {
+ return null;
+ }
+
+ return (
+
+ );
+ }
+
+ render() {
+ const {theme} = this.props;
+ const style = getStyleSheet(theme);
+
+ return (
+
+
+ {this.buildMentionSection()}
+ {this.buildEmailSection()}
+ {this.buildReplySection()}
+ {this.buildMobilePushSection()}
+ {this.buildMobilePushStatusSection()}
+
+
+ );
+ }
+}
diff --git a/app/scenes/account_notifications/index.js b/app/scenes/account_notifications/index.js
new file mode 100644
index 000000000..5b7bd360d
--- /dev/null
+++ b/app/scenes/account_notifications/index.js
@@ -0,0 +1,36 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {bindActionCreators} from 'redux';
+
+import {goBack} from 'app/actions/navigation';
+import {handleUpdateUserNotifyProps} from 'app/actions/views/account_notifications';
+import {getTheme} from 'service/selectors/entities/preferences';
+import {getCurrentUser} from 'service/selectors/entities/users';
+
+import navigationSceneConnect from '../navigationSceneConnect';
+
+import AccountNotifications from './account_notifications';
+
+function mapStateToProps(state) {
+ const {updateUserNotifyProps: updateRequest} = state.requests.users;
+
+ return {
+ config: state.entities.general.config,
+ myPreferences: state.entities.preferences.myPreferences,
+ currentUser: getCurrentUser(state),
+ saveRequestStatus: updateRequest.status,
+ theme: getTheme(state)
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ goBack,
+ handleUpdateUserNotifyProps
+ }, dispatch)
+ };
+}
+
+export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(AccountNotifications);
diff --git a/app/scenes/account_notifications/save_notifications_button.js b/app/scenes/account_notifications/save_notifications_button.js
new file mode 100644
index 000000000..c9cf77e1c
--- /dev/null
+++ b/app/scenes/account_notifications/save_notifications_button.js
@@ -0,0 +1,95 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PropTypes, PureComponent} from 'react';
+import {connect} from 'react-redux';
+import {
+ TouchableOpacity,
+ View
+} from 'react-native';
+
+import FormattedText from 'app/components/formatted_text';
+import Loading from 'app/components/loading';
+
+import {getTheme} from 'service/selectors/entities/preferences';
+import EventEmitter from 'service/utils/event_emitter';
+
+class AccountNotifcationsButton extends PureComponent {
+ static propTypes = {
+ emitter: PropTypes.func.isRequired,
+ theme: PropTypes.object
+ };
+
+ static defaultProps = {
+ theme: {}
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ loading: false
+ };
+ }
+
+ componentWillMount() {
+ EventEmitter.on('saving_notify_props', this.onLoading);
+ }
+
+ componentWillUnmount() {
+ EventEmitter.off('saving_notify_props', this.onLoading);
+ }
+
+ onLoading = (loading) => {
+ this.setState({loading});
+ };
+
+ onPress = () => {
+ this.props.emitter('save_notify_props');
+ };
+
+ render() {
+ const {theme} = this.props;
+ const {loading} = this.state;
+ const color = theme.sidebarHeaderTextColor;
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ theme: getTheme(state)
+ };
+}
+
+export default connect(mapStateToProps)(AccountNotifcationsButton);
diff --git a/app/scenes/account_notifications/section.js b/app/scenes/account_notifications/section.js
new file mode 100644
index 000000000..051fc31b2
--- /dev/null
+++ b/app/scenes/account_notifications/section.js
@@ -0,0 +1,90 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PropTypes} from 'react';
+import {
+ StyleSheet,
+ View
+} from 'react-native';
+
+import FormattedText from 'app/components/formatted_text';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return StyleSheet.create({
+ container: {
+ marginTop: 30
+ },
+ footer: {
+ marginHorizontal: 15,
+ marginTop: 10,
+ fontSize: 12,
+ color: changeOpacity(theme.centerChannelColor, 0.5)
+ },
+ header: {
+ marginHorizontal: 15,
+ marginBottom: 10,
+ fontSize: 13,
+ color: theme.centerChannelColor
+ },
+ items: {
+ backgroundColor: theme.centerChannelBg,
+ borderTopWidth: 1,
+ borderBottomWidth: 1,
+ borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
+ borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1)
+ }
+ });
+});
+
+function section(props) {
+ const {
+ children,
+ disableFooter,
+ footerDefaultMessage,
+ footerId,
+ footerValues,
+ headerDefaultMessage,
+ headerId,
+ headerValues,
+ theme
+ } = props;
+
+ const style = getStyleSheet(theme);
+
+ return (
+
+
+
+ {children}
+
+ {(footerId && !disableFooter) &&
+
+ }
+
+ );
+}
+
+section.propTypes = {
+ children: PropTypes.node.isRequired,
+ disableFooter: PropTypes.bool,
+ footerDefaultMessage: PropTypes.string,
+ footerId: PropTypes.string,
+ footerValues: PropTypes.object,
+ headerDefaultMessage: PropTypes.string.isRequired,
+ headerId: PropTypes.string.isRequired,
+ headerValues: PropTypes.object,
+ theme: PropTypes.object.isRequired
+};
+
+export default section;
diff --git a/app/scenes/account_notifications/section_item.js b/app/scenes/account_notifications/section_item.js
new file mode 100644
index 000000000..132cb62f1
--- /dev/null
+++ b/app/scenes/account_notifications/section_item.js
@@ -0,0 +1,115 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PropTypes} from 'react';
+import {
+ StyleSheet,
+ Switch,
+ TouchableWithoutFeedback,
+ View
+} from 'react-native';
+
+import FormattedText from 'app/components/formatted_text';
+import {makeStyleSheetFromTheme} from 'app/utils/theme';
+import CheckMark from 'app/components/checkmark';
+
+const ActionTypes = {
+ DEFAULT: 'default',
+ TOGGLE: 'toggle',
+ SELECT: 'select'
+};
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center'
+ },
+ label: {
+ flex: 1,
+ fontSize: 12,
+ color: theme.centerChannelColor,
+ paddingVertical: 15
+ },
+ wrapper: {
+ paddingHorizontal: 15
+ }
+ });
+});
+
+function sectionItem(props) {
+ const {
+ action,
+ actionType,
+ actionValue,
+ children,
+ labelDefaultMessage,
+ labelId,
+ labelValues,
+ theme,
+ selected
+ } = props;
+
+ const style = getStyleSheet(theme);
+
+ let actionComponent;
+ if (actionType === ActionTypes.SELECT && selected) {
+ actionComponent = (
+
+ );
+ } else if (actionType === ActionTypes.TOGGLE) {
+ actionComponent = (
+
+ );
+ }
+
+ const component = (
+
+
+
+ {actionComponent}
+
+ {children}
+
+ );
+
+ if (actionType === ActionTypes.DEFAULT || actionType === ActionTypes.SELECT) {
+ return (
+ action(actionValue)}>
+ {component}
+
+ );
+ }
+
+ return component;
+}
+
+sectionItem.propTypes = {
+ action: PropTypes.func,
+ actionType: PropTypes.oneOf([ActionTypes.DEFAULT, ActionTypes.TOGGLE, ActionTypes.SELECT]),
+ actionValue: PropTypes.string,
+ children: PropTypes.node,
+ labelDefaultMessage: PropTypes.string.isRequired,
+ labelId: PropTypes.string.isRequired,
+ labelValues: PropTypes.object,
+ selected: PropTypes.bool,
+ theme: PropTypes.object.isRequired
+};
+
+sectionItem.defaultProps = {
+ actionType: ActionTypes.DEFAULT
+};
+
+export default sectionItem;
diff --git a/app/scenes/account_settings/account_settings.js b/app/scenes/account_settings/account_settings.js
index c61510b42..2832d2e00 100644
--- a/app/scenes/account_settings/account_settings.js
+++ b/app/scenes/account_settings/account_settings.js
@@ -64,7 +64,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
export default class AccountSettings extends PureComponent {
static propTypes = {
- theme: PropTypes.object.isRequired
+ theme: PropTypes.object.isRequired,
+ actions: PropTypes.shape({
+ goToAccountNotifications: PropTypes.func.isRequired
+ })
}
static navigationProps = {
@@ -129,7 +132,7 @@ export default class AccountSettings extends PureComponent {
return [
this.buildItemRow('gear', 'user.settings.modal.general', 'General', () => true, true, false),
this.buildItemRow('lock', 'user.settings.modal.security', 'Security', () => true, true, false),
- this.buildItemRow('bell', 'user.settings.modal.notifications', 'Notifications', () => true, true, false),
+ this.buildItemRow('bell', 'user.settings.modal.notifications', 'Notifications', this.props.actions.goToAccountNotifications, false, true),
this.buildItemRow('mobile', 'user.settings.modal.display', 'Display', () => true, true, false),
this.buildItemRow('wrench', 'user.settings.modal.advanced', 'Advanced', () => true, false, false)
];
diff --git a/app/scenes/account_settings/account_settings_container.js b/app/scenes/account_settings/account_settings_container.js
index 46f9433da..ef322cc59 100644
--- a/app/scenes/account_settings/account_settings_container.js
+++ b/app/scenes/account_settings/account_settings_container.js
@@ -3,6 +3,7 @@
import {bindActionCreators} from 'redux';
+import {goToAccountNotifications} from 'app/actions/navigation';
import {getTheme} from 'service/selectors/entities/preferences';
import navigationSceneConnect from '../navigationSceneConnect';
@@ -17,7 +18,7 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
-
+ goToAccountNotifications
}, dispatch)
};
}
diff --git a/app/scenes/channel_info/channel_info_row.js b/app/scenes/channel_info/channel_info_row.js
index 8fa6229b0..dc983e145 100644
--- a/app/scenes/channel_info/channel_info_row.js
+++ b/app/scenes/channel_info/channel_info_row.js
@@ -79,7 +79,7 @@ function channelInfoRow(props) {
value={detail}
/> :
diff --git a/app/scenes/index.js b/app/scenes/index.js
index 6e95cc6ff..74c8cca95 100644
--- a/app/scenes/index.js
+++ b/app/scenes/index.js
@@ -1,6 +1,7 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import AccountNotifications from './account_notifications';
import AccountSettings from './account_settings';
import Channel from './channel';
import ChannelDrawer from './channel_drawer';
@@ -25,6 +26,7 @@ import UserProfile from './user_profile';
import Saml from './saml';
module.exports = {
+ AccountNotifications,
AccountSettings,
ChannelView: Channel, // Special case the name for this one to avoid ambiguity
ChannelDrawer,
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 907d87400..12ac6a3e1 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -1484,6 +1484,13 @@
"member_list.noUsersAdd": "No users to add.",
"members_popover.msg": "Message",
"members_popover.title": "Members",
+ "mobile.account.notifications.email.footer": "When offline or away for more than five minutes",
+ "mobile.account_notifications.mentions_footer": "Your username (\"@{username}\") will always trigger mentions.",
+ "mobile.account_notifications.non-case_sensitive_words": "Other non-case sensitive words...",
+ "mobile.account_notifications.reply.header": "Send reply notifications for",
+ "mobile.account_notifications.threads_start_participate": "Threads that I start or participate in",
+ "mobile.account_notifications.threads_start": "Threads that I start",
+ "mobile.account_notifications.threads_mentions": "Mentions in threads",
"mobile.channel_info.publicChannel": "Public Channel",
"mobile.channel_info.privateChannel": "Private Channel",
"mobile.channel_info.alertTitleLeaveChannel": "Leave {term}",
diff --git a/service/actions/users.js b/service/actions/users.js
index dea45322b..544dbf3a7 100644
--- a/service/actions/users.js
+++ b/service/actions/users.js
@@ -449,6 +449,25 @@ export function stopPeriodicStatusUpdates() {
};
}
+export function updateUserNotifyProps(notifyProps) {
+ return async (dispatch, getState) => {
+ dispatch({type: UsersTypes.UPDATE_NOTIFY_PROPS_REQUEST}, getState);
+
+ let data;
+ try {
+ data = await Client.updateUserNotifyProps(notifyProps);
+ } catch (error) {
+ dispatch({type: UsersTypes.UPDATE_NOTIFY_PROPS_FAILURE, error}, getState);
+ return;
+ }
+
+ dispatch(batchActions([
+ {type: UsersTypes.RECEIVED_ME, data},
+ {type: UsersTypes.UPDATE_NOTIFY_PROPS_SUCCESS}
+ ]), getState);
+ };
+}
+
export default {
checkMfa,
login,
@@ -464,5 +483,6 @@ export default {
getAudits,
searchProfiles,
startPeriodicStatusUpdates,
- stopPeriodicStatusUpdates
+ stopPeriodicStatusUpdates,
+ updateUserNotifyProps
};
diff --git a/service/constants/index.js b/service/constants/index.js
index 4d45ea958..fe55967ce 100644
--- a/service/constants/index.js
+++ b/service/constants/index.js
@@ -15,7 +15,12 @@ import WebsocketEvents from './websocket';
const Preferences = {
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
- CATEGORY_THEME: 'theme'
+ CATEGORY_NOTIFICATIONS: 'notifications',
+ CATEGORY_THEME: 'theme',
+ EMAIL_INTERVAL: 'email_interval',
+ INTERVAL_FIFTEEN_MINUTES: 15 * 60,
+ INTERVAL_HOUR: 60 * 60,
+ INTERVAL_IMMEDIATE: 30 // "immediate" is a 30 second interval
};
export {
diff --git a/service/constants/users.js b/service/constants/users.js
index 37dfe153f..18c4035a1 100644
--- a/service/constants/users.js
+++ b/service/constants/users.js
@@ -56,6 +56,10 @@ const UserTypes = keyMirror({
SEARCH_PROFILES_SUCCESS: null,
SEARCH_PROFILES_FAILURE: null,
+ UPDATE_NOTIFY_PROPS_REQUEST: null,
+ UPDATE_NOTIFY_PROPS_SUCCESS: null,
+ UPDATE_NOTIFY_PROPS_FAILURE: null,
+
RECEIVED_ME: null,
RECEIVED_PROFILES: null,
RECEIVED_SEARCH_PROFILES: null,
diff --git a/service/reducers/requests/users.js b/service/reducers/requests/users.js
index 492752c58..a95dbf865 100644
--- a/service/reducers/requests/users.js
+++ b/service/reducers/requests/users.js
@@ -163,6 +163,16 @@ function searchProfiles(state = initialRequestState(), action) {
);
}
+function updateUserNotifyProps(state = initialRequestState(), action) {
+ return handleRequest(
+ UsersTypes.UPDATE_NOTIFY_PROPS_REQUEST,
+ UsersTypes.UPDATE_NOTIFY_PROPS_SUCCESS,
+ UsersTypes.UPDATE_NOTIFY_PROPS_FAILURE,
+ state,
+ action
+ );
+}
+
export default combineReducers({
checkMfa,
login,
@@ -176,5 +186,6 @@ export default combineReducers({
revokeSession,
getAudits,
autocompleteUsersInChannel,
- searchProfiles
+ searchProfiles,
+ updateUserNotifyProps
});
diff --git a/test/service/actions/users.test.js b/test/service/actions/users.test.js
index f3a195710..a88032800 100644
--- a/test/service/actions/users.test.js
+++ b/test/service/actions/users.test.js
@@ -287,4 +287,32 @@ describe('Actions.Users', () => {
assert.ok(data[TestHelper.basicChannel.id].in_channel);
assert.ok(data[TestHelper.basicChannel.id].out_of_channel);
});
+
+ it('updateUserNotifyProps', async () => {
+ await Actions.login(TestHelper.basicUser.email, 'password1')(store.dispatch, store.getState);
+
+ const state = store.getState();
+ const currentUser = state.entities.users.profiles[state.entities.users.currentId];
+ const notifyProps = currentUser.notify_props;
+
+ await Actions.updateUserNotifyProps({
+ ...notifyProps,
+ comments: 'any',
+ email: 'false',
+ first_name: 'false',
+ mention_keys: '',
+ user_id: currentUser.id
+ })(store.dispatch, store.getState);
+
+ setTimeout(() => {
+ const updatedState = store.getState();
+ const updatedCurrentUser = updatedState.entities.users.profiles[state.entities.users.currentId];
+ const updateNotifyProps = updatedCurrentUser.notify_props;
+
+ assert.equal(updateNotifyProps.comments, 'any');
+ assert.equal(updateNotifyProps.email, 'false');
+ assert.equal(updateNotifyProps.first_name, 'false');
+ assert.equal(updateNotifyProps.mention_keys, '');
+ }, 1000);
+ });
});