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); + }); });