PLT-5492 Add Account Settings > Notifications (#323)
* PLT-5492 RN: Add Account Settings > Notifications * Review feedback
This commit is contained in:
parent
8bc6a0cb14
commit
d36dc3412a
19 changed files with 1034 additions and 7 deletions
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
29
app/actions/views/account_notifications.js
Normal file
29
app/actions/views/account_notifications.js
Normal 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);
|
||||
};
|
||||
}
|
||||
30
app/components/checkmark.js
Normal file
30
app/components/checkmark.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
533
app/scenes/account_notifications/account_notifications.js
Normal file
533
app/scenes/account_notifications/account_notifications.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
36
app/scenes/account_notifications/index.js
Normal file
36
app/scenes/account_notifications/index.js
Normal 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);
|
||||
|
|
@ -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);
|
||||
90
app/scenes/account_notifications/section.js
Normal file
90
app/scenes/account_notifications/section.js
Normal 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;
|
||||
115
app/scenes/account_notifications/section_item.js
Normal file
115
app/scenes/account_notifications/section_item.js
Normal 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;
|
||||
|
|
@ -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)
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ function channelInfoRow(props) {
|
|||
value={detail}
|
||||
/> :
|
||||
<Icon
|
||||
name='chevron-right'
|
||||
name='angle-right'
|
||||
size={15}
|
||||
style={style.rightIcon}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue