PLT-5492 Add Account Settings > Notifications (#323)

* PLT-5492 RN: Add Account Settings > Notifications

* Review feedback
This commit is contained in:
Chris Duarte 2017-03-13 11:37:44 -07:00 committed by enahum
parent 8bc6a0cb14
commit d36dc3412a
19 changed files with 1034 additions and 7 deletions

View file

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

View file

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

View file

@ -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 (
<Svg
width={this.props.width}
height={this.props.height}
viewBox='0 0 12 12'
>
<Path
d='M11.667 1.756l-0.775-0.624c-0.382-0.307-0.604-0.304-0.932 0.101l-5.636 6.955 -2.623-2.179c-0.362-0.304-0.588-0.288-0.886 0.084l-0.598 0.779c-0.304 0.382-0.265 0.599 0.094 0.899l3.738 3.092c0.385 0.323 0.602 0.291 0.899-0.071l6.817-8.104c0.32-0.385 0.3-0.615-0.098-0.932Z'
fill={this.props.color}
/>
</Svg>
);
}
}

View file

@ -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,

View file

@ -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 <SaveNotificationsButton emitter={emitter}/>;
}
}
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 (
<Section
headerId='user.settings.notifications.wordsTrigger'
headerDefaultMessage='Words that trigger mentions'
footerId='mobile.account_notifications.mentions_footer'
footerDefaultMessage='Your username (\"{username}\") will always trigger mentions.'
footerValues={{username: currentUser.username}}
theme={theme}
>
{currentUser.first_name.length > 0 &&
<View>
<SectionItem
labelId='user.settings.notifications.sensitiveName'
labelDefaultMessage='Your case sensitive first name "{first_name}"'
labelValues={{first_name: currentUser.first_name}}
action={this.toggleFirstNameMention}
actionType='toggle'
selected={this.state.first_name === 'true'}
theme={theme}
/>
<View style={style.separator}/>
</View>
}
<SectionItem
labelId='user.settings.notifications.sensitiveUsername'
labelDefaultMessage='Your non-case sensitive username "{username}"'
labelValues={{username: currentUser.username}}
selected={this.state.usernameMention}
action={this.toggleUsernameMention}
actionType='toggle'
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
labelId='user.settings.notifications.channelWide'
labelDefaultMessage='Channel-wide mentions "@channel", "@all"'
action={this.toggleChannelMentions}
actionType='toggle'
selected={this.state.channel === 'true'}
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
labelId='user.settings.notifications.sensitiveWords'
labelDefaultMessage='Other non-case sensitive words, separated by commas'
theme={theme}
>
<TextInputWithLocalizedPlaceholder
value={this.state.mention_keys}
onChangeText={this.updateMentionKeys}
style={style.input}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'mobile.account_notifications.non-case_sensitive_words', defaultMessage: 'Additional words...'}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.4)}
underlineColorAndroid='transparent'
/>
</SectionItem>
</Section>
);
}
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 (
<Section
headerId='user.settings.notifications.email.send'
headerDefaultMessage='Send email notifications'
footerId='user.settings.notifications.emailInfo'
footerDefaultMessage='Email notifications are sent for mentions and direct messages when you are offline or away from {siteName} for more than 5 minutes.'
footerValues={{siteName: config.SiteName}}
disableFooter={!sendEmailNotifications}
theme={theme}
>
{sendEmailNotifications &&
<View>
<SectionItem
labelId='user.settings.notifications.email.immediately'
labelDefaultMessage='Immediately'
action={this.setEmailNotifications}
actionType='select'
actionValue={sendImmediatleyValue}
selected={sendImmediatley}
theme={theme}
/>
<View style={style.separator}/>
{emailBatchingEnabled &&
<View>
<SectionItem
labelId='user.settings.notifications.email.everyXMinutes'
labelDefaultMessage='Every {count, plural, one {minute} other {{count, number} minutes}}'
labelValues={{count: Preferences.INTERVAL_FIFTEEN_MINUTES / 60}}
action={this.setEmailNotifications}
actionType='select'
actionValue={Preferences.INTERVAL_FIFTEEN_MINUTES.toString()}
selected={fifteenMinutes}
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
labelId='user.settings.notifications.email.everyHour'
labelDefaultMessage='Every hour'
action={this.setEmailNotifications}
actionType='select'
actionValue={Preferences.INTERVAL_HOUR.toString()}
selected={hourly}
theme={theme}
/>
<View style={style.separator}/>
</View>
}
<SectionItem
labelId='user.settings.notifications.email.never'
labelDefaultMessage='Never'
action={this.setEmailNotifications}
actionType='select'
actionValue='false'
selected={never}
theme={theme}
/>
</View>
}
{!sendEmailNotifications &&
<SectionItem
labelId='user.settings.general.emailHelp2'
labelDefaultMessage='Email has been disabled by your System Administrator. No notification emails will be sent until it is enabled.'
theme={theme}
/>
}
</Section>
);
}
buildReplySection = () => {
const {theme} = this.props;
const style = getStyleSheet(theme);
return (
<Section
headerId='mobile.account_notifications.reply.header'
headerDefaultMessage='Send reply notifications for'
footerId='user.settings.notifications.commentsInfo'
footerDefaultMessage="In addition to notifications for when you're mentioned, select if you would like to receive notifications on reply threads."
theme={theme}
>
<SectionItem
labelId='mobile.account_notifications.threads_start_participate'
labelDefaultMessage='Threads that I start or participate in'
action={this.setReplyNotifications}
actionType='select'
actionValue='any'
selected={this.state.comments === 'any'}
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
labelId='mobile.account_notifications.threads_start'
labelDefaultMessage='Threads that I start'
action={this.setReplyNotifications}
actionType='select'
actionValue='root'
selected={this.state.comments === 'root'}
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
labelId='mobile.account_notifications.threads_mentions'
labelDefaultMessage='Mentions in threads'
action={this.setReplyNotifications}
actionType='select'
actionValue='never'
selected={this.state.comments === 'never'}
theme={theme}
/>
</Section>
);
}
buildMobilePushSection = () => {
const {config, theme} = this.props;
const style = getStyleSheet(theme);
const pushNotificationsEnabled = config.SendPushNotifications === 'true';
if (!pushNotificationsEnabled) {
return null;
}
return (
<Section
headerId='user.settings.push_notification.send'
headerDefaultMessage='Send mobile push notifications'
footerId='user.settings.push_notification.info'
footerDefaultMessage='Notification alerts are pushed to your mobile device when there is activity in Mattermost.'
theme={theme}
>
<SectionItem
labelId='user.settings.notifications.allActivity'
labelDefaultMessage='For all activity'
action={this.setMobilePush}
actionType='select'
actionValue='all'
selected={this.state.push === 'all'}
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
labelId='user.settings.notifications.onlyMentions'
labelDefaultMessage='Only for mentions and direct messages'
action={this.setMobilePush}
actionType='select'
actionValue='mention'
selected={this.state.push === 'mention'}
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
labelId='user.settings.notifications.never'
labelDefaultMessage='Never'
action={this.setMobilePush}
actionType='select'
actionValue='none'
selected={this.state.push === 'none'}
theme={theme}
/>
</Section>
);
}
buildMobilePushStatusSection = () => {
const {config, theme} = this.props;
const style = getStyleSheet(theme);
const showSection = config.SendPushNotifications === 'true' && this.state.push !== 'none';
if (!showSection) {
return null;
}
return (
<Section
headerId='user.settings.push_notification.status'
headerDefaultMessage='Trigger push notifications when'
footerId='user.settings.push_notification.status_info'
footerDefaultMessage='Notification alerts are only pushed to your mobile device when your online status matches the selection above.'
theme={theme}
>
<SectionItem
labelId='user.settings.push_notification.online'
labelDefaultMessage='Online, away or offline'
action={this.setMobilePushStatus}
actionType='select'
actionValue='online'
selected={this.state.push_status === 'online'}
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
labelId='user.settings.push_notification.away'
labelDefaultMessage='Away or offline'
action={this.setMobilePushStatus}
actionType='select'
actionValue='away'
selected={this.state.push_status === 'away'}
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
labelId='user.settings.push_notification.offline'
labelDefaultMessage='Offline'
action={this.setMobilePushStatus}
actionType='select'
actionValue='offline'
selected={this.state.push_status === 'offline'}
theme={theme}
/>
</Section>
);
}
render() {
const {theme} = this.props;
const style = getStyleSheet(theme);
return (
<View style={style.wrapper}>
<ScrollView
style={style.scrollView}
contentContainerStyle={style.scrollViewContent}
>
{this.buildMentionSection()}
{this.buildEmailSection()}
{this.buildReplySection()}
{this.buildMobilePushSection()}
{this.buildMobilePushStatusSection()}
</ScrollView>
</View>
);
}
}

View file

@ -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);

View file

@ -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 (
<Loading
color={color}
size='small'
style={{
alignItems: 'center',
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
paddingHorizontal: 20
}}
/>
);
}
return (
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center', flex: 1}}>
<TouchableOpacity
onPress={this.onPress}
style={{paddingHorizontal: 15}}
>
<FormattedText
id='add_command.save'
defaultMessage='Save'
style={{color}}
/>
</TouchableOpacity>
</View>
);
}
}
function mapStateToProps(state) {
return {
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(AccountNotifcationsButton);

View file

@ -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 (
<View style={style.container}>
<FormattedText
id={headerId}
defaultMessage={headerDefaultMessage}
values={headerValues}
style={style.header}
/>
<View style={style.items}>
{children}
</View>
{(footerId && !disableFooter) &&
<FormattedText
id={footerId}
defaultMessage={footerDefaultMessage}
values={footerValues}
style={style.footer}
/>
}
</View>
);
}
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;

View file

@ -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 = (
<CheckMark
width={12}
height={12}
color={theme.linkColor}
/>
);
} else if (actionType === ActionTypes.TOGGLE) {
actionComponent = (
<Switch
onValueChange={action}
value={selected}
/>
);
}
const component = (
<View style={style.wrapper}>
<View style={style.container}>
<FormattedText
id={labelId}
defaultMessage={labelDefaultMessage}
values={labelValues}
style={[style.label, (children && {paddingBottom: 5})]}
/>
{actionComponent}
</View>
{children}
</View>
);
if (actionType === ActionTypes.DEFAULT || actionType === ActionTypes.SELECT) {
return (
<TouchableWithoutFeedback onPress={() => action(actionValue)}>
{component}
</TouchableWithoutFeedback>
);
}
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;

View file

@ -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)
];

View file

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

View file

@ -79,7 +79,7 @@ function channelInfoRow(props) {
value={detail}
/> :
<Icon
name='chevron-right'
name='angle-right'
size={15}
style={style.rightIcon}
/>

View file

@ -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,

View file

@ -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}",

View file

@ -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
};

View file

@ -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 {

View file

@ -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,

View file

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

View file

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