RN-321 Update Settings and Notifications UX/UI Screens (#879)

* Fix Android and iOS flow

* Own review

* RN-321 Update Settings and Notifications UX/UI Screens

* Fix style for separator in android modals

* Feedback review
This commit is contained in:
enahum 2017-09-08 15:38:50 -03:00 committed by GitHub
parent 12cac7305f
commit adbaead58e
35 changed files with 3212 additions and 1202 deletions

View file

@ -11,12 +11,12 @@ export function handleUpdateUserNotifyProps(notifyProps) {
const config = state.entities.general.config;
const {currentUserId} = state.entities.users;
const {interval, ...otherProps} = notifyProps;
const {interval, user_id, ...otherProps} = notifyProps;
const email = notifyProps.email;
if (config.EnableEmailBatching === 'true' && email !== 'false') {
const emailInterval = [{
user_id: notifyProps.user_id,
user_id,
category: Preferences.CATEGORY_NOTIFICATIONS,
name: Preferences.EMAIL_INTERVAL,
value: interval

View file

@ -0,0 +1,10 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import RadioButton from './radio_button';
import RadioButtonGroup from './radio_button_group';
export {
RadioButton,
RadioButtonGroup
};

View file

@ -0,0 +1,170 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Animated, Text, View} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import {getTheme} from 'app/selectors/preferences';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
const DISABLED_OPACITY = 0.26;
const DEFAULT_OPACITY = 1;
class RadioButton extends PureComponent {
static propTypes = {
label: PropTypes.string,
theme: PropTypes.object,
value: PropTypes.string,
checked: PropTypes.bool,
disabled: PropTypes.bool,
onCheck: PropTypes.func
};
constructor(props) {
super(props);
this.state = {
scaleValue: new Animated.Value(0.001),
opacityValue: new Animated.Value(0.1)
};
this.responder = {
onStartShouldSetResponder: () => true,
onResponderGrant: this.highlight,
onResponderRelease: this.handleResponderEnd,
onResponderTerminate: this.unHighlight
};
}
highlight = () => {
Animated.timing(
this.state.scaleValue,
{
toValue: 1,
duration: 150
}
).start();
Animated.timing(
this.state.opacityValue,
{
toValue: 0.1,
duration: 100
}
).start();
};
unHighlight = () => {
Animated.timing(
this.state.scaleValue,
{
toValue: 0.001,
duration: 1500
}
).start();
Animated.timing(
this.state.opacityValue,
{
toValue: 0
}
).start();
};
handleResponderEnd = () => {
const {checked, disabled, onCheck, value} = this.props;
if (!checked && !disabled && onCheck) {
onCheck(value);
}
this.unHighlight();
};
render() {
const {scaleValue, opacityValue} = this.state;
const {theme, checked, disabled} = this.props;
const styles = getStyleSheet(theme);
const color = checked ? theme.buttonBg : theme.centerChannelColor;
const opacity = disabled ? DISABLED_OPACITY : DEFAULT_OPACITY;
return (
<View
style={styles.container}
{...this.responder}
>
<Animated.View
style={[
styles.ripple,
{
transform: [
{scale: scaleValue}
],
opacity: opacityValue,
backgroundColor: color
}
]}
/>
<Icon
name={checked ? 'radio-button-checked' : 'radio-button-unchecked'}
size={24}
color={color}
style={{
opacity
}}
/>
<View style={styles.labelContainer}>
<Text
style={[
styles.label,
{opacity: disabled ? DISABLED_OPACITY : DEFAULT_OPACITY}
]}
>
{this.props.label}
</Text>
</View>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0)',
marginBottom: 15
},
ripple: {
position: 'absolute',
width: 48,
height: 48,
borderRadius: 56,
top: 6
},
labelContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center'
},
label: {
color: theme.centerChannelColor,
flex: 1,
fontSize: 17,
marginLeft: 15,
textAlignVertical: 'center',
includeFontPadding: false
}
};
});
function mapStateToProps(state, ownProps) {
return {
...ownProps,
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(RadioButton);

View file

@ -0,0 +1,99 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import RadioButton from './radio_button';
export default class RadioButtonGroup extends PureComponent {
static propTypes = {
children: PropTypes.node.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string,
onSelect: PropTypes.func
};
state = {};
constructor(props) {
super(props);
if (props.value) {
this.state = {
selected: props.value
};
} else {
React.Children.map(this.props.children, (option) => {
if (option) {
const {
value,
checked
} = option.props;
if (!this.state.selected && checked) {
this.state = {
selected: value
};
}
}
});
}
}
get value() {
return this.state.selected;
}
set value(value) {
this.onChange(value);
}
onChange = (value) => {
const {onSelect} = this.props;
this.setState({
selected: value
}, () => {
if (onSelect) {
onSelect(value);
}
});
};
render = () => {
const options = React.Children.map(this.props.children, (option) => {
if (option) {
const {
value,
label,
disabled,
...other
} = option.props;
const {name} = this.props;
return (
<RadioButton
{...other}
ref={value}
name={name}
key={`${name}-${value}`}
value={value}
label={label}
disabled={disabled}
onCheck={this.onChange}
checked={this.state.selected && value === this.state.selected}
/>
);
}
return null;
}, this);
return (
<View>
{options}
</View>
);
};
}

View file

@ -288,7 +288,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03)
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06)
},
scrollViewContent: {
paddingBottom: 30

View file

@ -1,601 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
Keyboard,
Platform,
ScrollView,
View
} from 'react-native';
import {Preferences, RequestStatus} from 'mattermost-redux/constants';
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
import Loading from 'app/components/loading';
import StatusBar from 'app/components/status_bar';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import Section from './section';
import SectionItem from './section_item';
class AccountNotifications extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
handleUpdateUserNotifyProps: PropTypes.func.isRequired
}),
config: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
intl: intlShape.isRequired,
myPreferences: PropTypes.object.isRequired,
navigator: PropTypes.object,
theme: PropTypes.object.isRequired,
updateMeRequest: PropTypes.object.isRequired
};
saveButton = {
id: 'save-notifications',
showAsAction: 'always'
};
constructor(props) {
super(props);
const {currentUser} = props;
const notifyProps = currentUser.notify_props || {};
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
props.navigator.setButtons({
rightButtons: [{
...this.saveButton,
title: props.intl.formatMessage({id: 'edit_post.save', defaultMessage: 'Save'})
}]
});
this.setStateFromNotifyProps(notifyProps);
}
componentDidMount() {
if (Platform.OS === 'android') {
Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard);
}
}
componentWillUnmount() {
if (Platform.OS === 'android') {
Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard);
}
}
componentWillReceiveProps(nextProps) {
const {currentUser, updateMeRequest} = nextProps;
if (currentUser !== this.props.currentUser) {
const {notify_props: notifyProps} = currentUser;
this.setStateFromNotifyProps(notifyProps);
}
if (this.props.updateMeRequest !== updateMeRequest) {
switch (updateMeRequest.status) {
case RequestStatus.STARTED:
this.savingNotifyProps(true);
this.setState({error: null, saving: true});
break;
case RequestStatus.SUCCESS:
this.savingNotifyProps(false);
this.setState({error: null, saving: false});
this.close();
break;
case RequestStatus.FAILURE:
this.savingNotifyProps(false);
this.setState({error: updateMeRequest.error, saving: false});
break;
}
}
}
handleAndroidKeyboard = () => {
this.refs.mention_keys.getWrappedInstance().blur();
};
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'save-notifications') {
this.saveUserNotifyProps();
}
}
};
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);
if (emailPreferences.size) {
interval = emailPreferences.get(Preferences.EMAIL_INTERVAL).value;
}
}
const comments = notifyProps.comments || 'never';
const newState = {
...notifyProps,
comments,
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()
});
};
focusMentionKeys = () => {
this.refs.mention_keys.getWrappedInstance().focus();
};
updateMentionKeys = (text) => {
this.setState({
mention_keys: text
});
};
savingNotifyProps = (loading) => {
this.props.navigator.setButtons({
rightButtons: [{...this.saveButton, disabled: loading}]
});
};
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 = () => {
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}
action={this.focusMentionKeys}
>
<TextInputWithLocalizedPlaceholder
ref='mention_keys'
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>
);
};
close = () => {
this.props.navigator.pop({animated: true});
};
render() {
const {theme} = this.props;
const style = getStyleSheet(theme);
if (this.state.saving) {
return (
<View style={style.wrapper}>
<StatusBar/>
<Loading/>
</View>
);
}
return (
<View style={style.wrapper}>
<StatusBar/>
<ScrollView
style={style.scrollView}
contentContainerStyle={style.scrollViewContent}
>
{this.buildMentionSection()}
{this.buildEmailSection()}
{this.buildReplySection()}
{this.buildMobilePushSection()}
{this.buildMobilePushStatusSection()}
</ScrollView>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
wrapper: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
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
}
};
});
export default injectIntl(AccountNotifications);

View file

@ -1,159 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
TouchableOpacity,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import FormattedText from 'app/components/formatted_text';
import StatusBar from 'app/components/status_bar';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
class AccountSettings extends PureComponent {
static propTypes = {
intl: intlShape.isRequired,
navigator: PropTypes.object,
theme: PropTypes.object.isRequired
};
// The enabled arg can be removed once all the scenes have been implemented.
buildItemRow = (icon, id, defaultMessage, action, separator = true, enabled = false) => {
if (!enabled) {
return null;
}
const {theme} = this.props;
const style = getStyleSheet(theme);
return (
<View
key={id}
style={style.itemWrapper}
>
<TouchableOpacity
style={style.item}
onPress={() => preventDoubleTap(action)}
>
<View style={style.itemLeftIconContainer}>
<Icon
name={icon}
size={18}
style={style.itemLeftIcon}
/>
</View>
<FormattedText
id={id}
defaultMessage={defaultMessage}
style={style.itemText}
/>
<Icon
name='angle-right'
size={18}
style={style.itemRightIcon}
/>
</TouchableOpacity>
{separator && <View style={style.separator}/>}
</View>
);
};
goToAccountNotifications = () => {
const {intl, navigator, theme} = this.props;
navigator.push({
backButtonTitle: '',
screen: 'AccountNotifications',
title: intl.formatMessage({id: 'user.settings.modal.notifications', defaultMessage: 'Notifications'}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
}
});
};
renderItems = () => {
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', this.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)
];
};
render() {
const {theme} = this.props;
const style = getStyleSheet(theme);
return (
<View style={style.wrapper}>
<StatusBar/>
<View style={style.container}>
<View style={style.itemsContainer}>
{this.renderItems()}
</View>
</View>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03)
},
item: {
height: 45,
flexDirection: 'row',
alignItems: 'center'
},
itemLeftIcon: {
color: changeOpacity(theme.centerChannelColor, 0.5)
},
itemLeftIconContainer: {
width: 18,
marginRight: 15,
alignItems: 'center',
justifyContent: 'center'
},
itemText: {
fontSize: 16,
color: theme.centerChannelColor,
flex: 1
},
itemRightIcon: {
color: changeOpacity(theme.centerChannelColor, 0.5)
},
itemsContainer: {
marginTop: 30,
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderBottomWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1)
},
itemWrapper: {
marginHorizontal: 15
},
separator: {
height: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1)
},
wrapper: {
flex: 1,
backgroundColor: theme.centerChannelBg
}
};
});
export default injectIntl(AccountSettings);

View file

@ -1,159 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
Alert,
TouchableOpacity,
View
} from 'react-native';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FormattedText from 'app/components/formatted_text';
import StatusBar from 'app/components/status_bar';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
class AdvancedSettings extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
purgeOfflineStore: PropTypes.func.isRequired
}).isRequired,
intl: intlShape.isRequired,
theme: PropTypes.object
};
buildItemRow = (icon, id, defaultMessage, action, separator = true, nextArrow = false) => {
const {theme} = this.props;
const style = getStyleSheet(theme);
return (
<View
key={id}
style={style.itemWrapper}
>
<TouchableOpacity
style={style.item}
onPress={() => this.handlePress(action)}
>
<View style={style.itemLeftIconContainer}>
<MaterialIcon
name={icon}
size={18}
style={style.itemLeftIcon}
/>
</View>
<FormattedText
id={id}
defaultMessage={defaultMessage}
style={style.itemText}
/>
{nextArrow &&
<MaterialIcon
name='angle-right'
size={18}
style={style.itemRightIcon}
/>
}
</TouchableOpacity>
{separator && <View style={style.separator}/>}
</View>
);
};
clearOfflineCache = () => {
const {actions, intl} = this.props;
Alert.alert(
intl.formatMessage({id: 'mobile.advanced_settings.reset_title', defaultMessage: 'Reset Cache'}),
intl.formatMessage({id: 'mobile.advanced_settings.reset_message', defaultMessage: '\nThis will reset all offline data and restart the app. You will be automatically logged back in once the app restarts.\n'}),
[{
text: intl.formatMessage({id: 'mobile.advanced_settings.reset_button', defaultMessage: 'Reset'}),
onPress: () => actions.purgeOfflineStore()
}, {
text: intl.formatMessage({id: 'channel_modal.cancel', defaultMessage: 'Cancel'}),
onPress: () => true
}]
);
};
handlePress = (action) => {
preventDoubleTap(action, this);
};
renderItems = () => {
return [
this.buildItemRow('storage', 'mobile.advanced_settings.reset_title', 'Reset Cache', this.clearOfflineCache, false, false)
];
};
render() {
const {theme} = this.props;
const style = getStyleSheet(theme);
return (
<View style={style.wrapper}>
<StatusBar/>
<View style={style.container}>
<View style={style.itemsContainer}>
{this.renderItems()}
</View>
</View>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03)
},
item: {
height: 45,
flexDirection: 'row',
alignItems: 'center'
},
itemLeftIcon: {
color: changeOpacity(theme.centerChannelColor, 0.5)
},
itemLeftIconContainer: {
width: 18,
marginRight: 15,
alignItems: 'center',
justifyContent: 'center'
},
itemText: {
fontSize: 16,
color: theme.centerChannelColor,
flex: 1
},
itemRightIcon: {
color: changeOpacity(theme.centerChannelColor, 0.5)
},
itemsContainer: {
marginTop: 30,
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderBottomWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1)
},
itemWrapper: {
marginHorizontal: 15
},
separator: {
height: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1)
},
wrapper: {
flex: 1,
backgroundColor: theme.centerChannelBg
}
};
});
export default injectIntl(AdvancedSettings);

View file

@ -5,10 +5,9 @@ import React from 'react';
import {Navigation} from 'react-native-navigation';
import About from 'app/screens/about';
import AccountSettings from 'app/screens/account_settings';
import AccountNotifications from 'app/screens/account_notifications';
import AddReaction from 'app/screens/add_reaction';
import AdvancedSettings from 'app/screens/advanced_settings';
import AdvancedSettings from 'app/screens/settings/advanced_settings';
import Channel from 'app/screens/channel';
import ChannelAddMembers from 'app/screens/channel_add_members';
import ChannelInfo from 'app/screens/channel_info';
@ -24,13 +23,18 @@ import Mfa from 'app/screens/mfa';
import MoreChannels from 'app/screens/more_channels';
import MoreDirectMessages from 'app/screens/more_dms';
import Notification from 'app/screens/notification';
import NotificationSettings from 'app/screens/settings/notification_settings';
import NotificationSettingsEmail from 'app/screens/settings/notification_settings_email';
import NotificationSettingsMentions from 'app/screens/settings/notification_settings_mentions';
import NotificationSettingsMentionsKeywords from 'app/screens/settings/notification_settings_mentions_keywords';
import NotificationSettingsMobile from 'app/screens/settings/notification_settings_mobile';
import OptionsModal from 'app/screens/options_modal';
import Root from 'app/screens/root';
import SSO from 'app/screens/sso';
import Search from 'app/screens/search';
import SelectServer from 'app/screens/select_server';
import SelectTeam from 'app/screens/select_team';
import Settings from 'app/screens/settings';
import Settings from 'app/screens/settings/general';
import Thread from 'app/screens/thread';
import UserProfile from 'app/screens/user_profile';
@ -52,8 +56,6 @@ function wrapWithContextProvider(Comp, excludeEvents = false) {
export function registerScreens(store, Provider) {
Navigation.registerComponent('About', () => wrapWithContextProvider(About), store, Provider);
Navigation.registerComponent('AccountSettings', () => wrapWithContextProvider(AccountSettings), store, Provider);
Navigation.registerComponent('AccountNotifications', () => wrapWithContextProvider(AccountNotifications), store, Provider);
Navigation.registerComponent('AddReaction', () => wrapWithContextProvider(AddReaction), store, Provider);
Navigation.registerComponent('AdvancedSettings', () => wrapWithContextProvider(AdvancedSettings), store, Provider);
Navigation.registerComponent('Channel', () => wrapWithContextProvider(Channel), store, Provider);
@ -72,6 +74,11 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('MoreDirectMessages', () => wrapWithContextProvider(MoreDirectMessages), store, Provider);
Navigation.registerComponent('OptionsModal', () => wrapWithContextProvider(OptionsModal), store, Provider);
Navigation.registerComponent('Notification', () => wrapWithContextProvider(Notification, true), store, Provider);
Navigation.registerComponent('NotificationSettings', () => wrapWithContextProvider(NotificationSettings), store, Provider);
Navigation.registerComponent('NotificationSettingsEmail', () => wrapWithContextProvider(NotificationSettingsEmail), store, Provider);
Navigation.registerComponent('NotificationSettingsMentions', () => wrapWithContextProvider(NotificationSettingsMentions), store, Provider);
Navigation.registerComponent('NotificationSettingsMentionsKeywords', () => wrapWithContextProvider(NotificationSettingsMentionsKeywords), store, Provider);
Navigation.registerComponent('NotificationSettingsMobile', () => wrapWithContextProvider(NotificationSettingsMobile), store, Provider);
Navigation.registerComponent('Root', () => Root, store, Provider);
Navigation.registerComponent('Search', () => wrapWithContextProvider(Search), store, Provider);
Navigation.registerComponent('SelectServer', () => wrapWithContextProvider(SelectServer), store, Provider);

View file

@ -0,0 +1,95 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
Alert,
Platform,
View
} from 'react-native';
import SettingsItem from 'app/screens/settings/settings_item';
import StatusBar from 'app/components/status_bar';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
class AdvancedSettings extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
purgeOfflineStore: PropTypes.func.isRequired
}).isRequired,
intl: intlShape.isRequired,
theme: PropTypes.object
};
clearOfflineCache = () => {
const {actions, intl} = this.props;
Alert.alert(
intl.formatMessage({id: 'mobile.advanced_settings.reset_title', defaultMessage: 'Reset Cache'}),
intl.formatMessage({id: 'mobile.advanced_settings.reset_message', defaultMessage: '\nThis will reset all offline data and restart the app. You will be automatically logged back in once the app restarts.\n'}),
[{
text: intl.formatMessage({id: 'mobile.advanced_settings.reset_button', defaultMessage: 'Reset'}),
onPress: () => actions.purgeOfflineStore()
}, {
text: intl.formatMessage({id: 'channel_modal.cancel', defaultMessage: 'Cancel'}),
onPress: () => true
}]
);
};
handlePress = (action) => {
preventDoubleTap(action, this);
};
render() {
const {theme} = this.props;
const style = getStyleSheet(theme);
return (
<View style={style.container}>
<StatusBar/>
<View style={style.wrapper}>
<View style={style.divider}/>
<SettingsItem
defaultMessage='Reset Cache'
i18nId='mobile.advanced_settings.reset_title'
iconName='ios-refresh'
iconType='ion'
onPress={() => this.handlePress(this.clearOfflineCache)}
separator={false}
showArrow={false}
theme={theme}
/>
<View style={style.divider}/>
</View>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
wrapper: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
flex: 1,
...Platform.select({
ios: {
paddingTop: 35
}
})
},
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 1
}
};
});
export default injectIntl(AdvancedSettings);

View file

@ -3,7 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {intlShape, injectIntl} from 'react-intl';
import {
InteractionManager,
Linking,
@ -12,13 +12,12 @@ import {
} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import SettingsItem from 'app/screens/settings/settings_item';
import StatusBar from 'app/components/status_bar';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {isValidUrl} from 'app/utils/url';
import SettingsItem from './settings_item';
class Settings extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
@ -82,13 +81,13 @@ class Settings extends PureComponent {
});
};
goToAccountSettings = () => {
goToNotifications = () => {
const {intl, navigator, theme} = this.props;
navigator.push({
screen: 'AccountSettings',
title: intl.formatMessage({id: 'user.settings.modal.title', defaultMessage: 'Account Settings'}),
animated: true,
screen: 'NotificationSettings',
backButtonTitle: '',
title: intl.formatMessage({id: 'user.settings.modal.notifications', defaultMessage: 'Notifications'}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
@ -174,28 +173,30 @@ class Settings extends PureComponent {
const style = getStyleSheet(theme);
const showTeams = Object.keys(joinableTeams).length > 0;
const showHelp = isValidUrl(config.HelpLink);
const showArrow = Platform.OS === 'ios';
return (
<View style={style.container}>
<StatusBar/>
<View style={style.wrapper}>
<View style={style.divider}/>
<SettingsItem
defaultMessage='Account Settings'
i18nId='sidebar_right_menu.accountSettings'
iconName='cog'
iconType='fontawesome'
onPress={() => this.handlePress(this.goToAccountSettings)}
separator={true}
defaultMessage='Notifications'
i18nId='user.settings.modal.notifications'
iconName='ios-notifications'
iconType='ion'
onPress={() => this.handlePress(this.goToNotifications)}
showArrow={showArrow}
theme={theme}
/>
{showTeams &&
<SettingsItem
defaultMessage='Open teams you can join'
i18nId='mobile.select_team.join_open'
iconName='group'
iconType='material'
iconName='list'
iconType='foundation'
onPress={() => this.handlePress(this.goToSelectTeam)}
separator={true}
showArrow={showArrow}
theme={theme}
/>
}
@ -203,54 +204,56 @@ class Settings extends PureComponent {
<SettingsItem
defaultMessage='Help'
i18nId='mobile.help.title'
iconName='help'
iconType='material'
onPress={() => preventDoubleTap(this.openHelp, this)}
separator={true}
iconName='md-help'
iconType='ion'
onPress={() => this.handlePress(this.openHelp)}
showArrow={showArrow}
theme={theme}
/>
}
<SettingsItem
defaultMessage='Report a Problem'
i18nId='sidebar_right_menu.report'
iconName='warning'
iconType='material'
iconName='exclamation'
iconType='fontawesome'
onPress={() => this.handlePress(this.openErrorEmail)}
separator={true}
showArrow={showArrow}
theme={theme}
/>
<SettingsItem
defaultMessage='Advanced Settings'
i18nId='mobile.advanced_settings.title'
iconName='ios-construct'
iconName='ios-hammer'
iconType='ion'
onPress={() => this.handlePress(this.goToAdvancedSettings)}
separator={true}
showArrow={showArrow}
theme={theme}
/>
<SettingsItem
defaultMessage='About Mattermost'
i18nId='about.title'
iconName='info-outline'
iconType='material'
iconName='ios-information-circle'
iconType='ion'
onPress={() => this.handlePress(this.goToAbout)}
separator={false}
showArrow={showArrow}
theme={theme}
/>
<View style={style.divider}/>
</View>
<View style={style.footer}>
<View style={style.divider}/>
<SettingsItem
defaultMessage='Logout'
i18nId='sidebar_right_menu.logout'
iconName='exit-to-app'
iconType='material'
isDestructor={true}
onPress={() => this.handlePress(this.logout)}
separator={false}
theme={theme}
/>
<View style={style.footer}>
<View style={style.divider}/>
<SettingsItem
centered={true}
defaultMessage='Logout'
i18nId='sidebar_right_menu.logout'
isDestructor={true}
onPress={() => this.handlePress(this.logout)}
separator={false}
showArrow={false}
theme={theme}
/>
<View style={style.divider}/>
</View>
</View>
</View>
);
@ -259,32 +262,25 @@ class Settings extends PureComponent {
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
leftComponent: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
width: 44
},
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
wrapper: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
flex: 1
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
flex: 1,
...Platform.select({
ios: {
paddingTop: 35
}
})
},
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 1
},
footer: {
height: 51,
justifyContent: 'flex-end',
...Platform.select({
android: {
marginBottom: 20
}
})
marginTop: 35
}
};
});

View file

@ -5,18 +5,19 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {handleUpdateUserNotifyProps} from 'app/actions/views/account_notifications';
import {getTheme} from 'app/selectors/preferences';
import AccountNotifications from './account_notifications';
import NotificationSettings from './notification_settings';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
config: state.entities.general.config,
myPreferences: state.entities.preferences.myPreferences,
currentUser: getCurrentUser(state),
myPreferences: getMyPreferences(state),
updateMeRequest: state.requests.users.updateMe,
theme: getTheme(state)
};
@ -30,4 +31,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AccountNotifications);
export default connect(mapStateToProps, mapDispatchToProps)(NotificationSettings);

View file

@ -0,0 +1,449 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
Alert,
Modal,
Platform,
TouchableOpacity,
View
} from 'react-native';
import deepEqual from 'deep-equal';
import {Preferences, RequestStatus} from 'mattermost-redux/constants';
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
import FormattedText from 'app/components/formatted_text';
import {RadioButton, RadioButtonGroup} from 'app/components/radio_button';
import StatusBar from 'app/components/status_bar';
import SettingsItem from 'app/screens/settings/settings_item';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
class NotificationSettings extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
handleUpdateUserNotifyProps: PropTypes.func.isRequired
}),
config: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
intl: intlShape.isRequired,
myPreferences: PropTypes.object.isRequired,
navigator: PropTypes.object,
theme: PropTypes.object.isRequired,
updateMeRequest: PropTypes.object.isRequired
};
state = {
showEmailNotificationsModal: false
};
componentWillReceiveProps(nextProps) {
const {updateMeRequest, intl} = nextProps;
if (this.props.updateMeRequest !== updateMeRequest && updateMeRequest.status === RequestStatus.FAILURE) {
Alert.alert(
intl.formatMessage({
id: 'mobile.notification_settings.save_failed_title',
defaultMessage: 'Connection issue'
}),
intl.formatMessage({
id: 'mobile.notification_settings.save_failed_description',
defaultMessage: 'The notification settings failed to save due to a connection issue, please try again.'
})
);
}
}
handlePress = (action) => {
preventDoubleTap(action, this);
};
goToNotificationSettingsEmail = () => {
if (Platform.OS === 'ios') {
const {currentUser, intl, navigator, theme} = this.props;
navigator.push({
backButtonTitle: '',
screen: 'NotificationSettingsEmail',
title: intl.formatMessage({
id: 'mobile.notification_settings.email_title',
defaultMessage: 'Email Notifications'
}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
},
passProps: {
currentUser,
onBack: this.saveNotificationProps
}
});
} else {
this.setState({showEmailNotificationsModal: true});
}
};
goToNotificationSettingsMentions = () => {
const {currentUser, intl, navigator, theme} = this.props;
navigator.push({
backButtonTitle: '',
screen: 'NotificationSettingsMentions',
title: intl.formatMessage({id: 'mobile.notification_settings.mentions_replies', defaultMessage: 'Mentions and Replies'}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
},
passProps: {
currentUser,
onBack: this.saveNotificationProps
}
});
};
goToNotificationSettingsMobile = () => {
const {currentUser, intl, navigator, theme} = this.props;
navigator.push({
backButtonTitle: '',
screen: 'NotificationSettingsMobile',
title: intl.formatMessage({id: 'mobile.notification_settings.mobile_title', defaultMessage: 'Mobile Notifications'}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
},
passProps: {
currentUser,
onBack: this.saveNotificationProps
}
});
};
setEmailNotifications = (emailSetting) => {
this.setState({emailSetting});
};
saveEmailNotifications = () => {
const {config, currentUser} = this.props;
const {emailSetting} = this.state;
this.setState({showEmailNotificationsModal: false});
if (emailSetting) {
let email = emailSetting;
let interval;
const emailBatchingEnabled = config.EnableEmailBatching === 'true';
if (emailBatchingEnabled && emailSetting !== 'false') {
interval = emailSetting;
email = 'true';
}
this.saveNotificationProps({
...currentUser.notify_props,
email,
interval
});
}
};
saveNotificationProps = (notifyProps) => {
const {currentUser} = this.props;
const {user_id} = notifyProps;
const previousProps = {
...currentUser.notify_props,
user_id
};
if (notifyProps.interval) {
previousProps.interval = notifyProps.interval;
}
if (!deepEqual(previousProps, notifyProps)) {
this.props.actions.handleUpdateUserNotifyProps(notifyProps);
}
};
renderEmailNotificationSettings = (style) => {
if (Platform.OS === 'ios') {
return null;
}
const {config, currentUser, intl, myPreferences} = this.props;
const notifyProps = currentUser.notify_props || {};
const sendEmailNotifications = config.SendEmailNotifications === 'true';
const emailBatchingEnabled = sendEmailNotifications && config.EnableEmailBatching === 'true';
const never = notifyProps.email === 'false';
let sendImmediatley = notifyProps.email === 'true';
let sendImmediatleyValue = 'true';
let fifteenMinutes;
let hourly;
let interval;
if (emailBatchingEnabled) {
const emailPreferences = getPreferencesByCategory(myPreferences, Preferences.CATEGORY_NOTIFICATIONS);
if (emailPreferences.size) {
interval = emailPreferences.get(Preferences.EMAIL_INTERVAL).value;
}
}
if (emailBatchingEnabled && notifyProps.email !== 'false') {
sendImmediatley = interval === Preferences.INTERVAL_IMMEDIATE.toString();
fifteenMinutes = interval === Preferences.INTERVAL_FIFTEEN_MINUTES.toString();
hourly = interval === Preferences.INTERVAL_HOUR.toString();
sendImmediatleyValue = Preferences.INTERVAL_IMMEDIATE.toString();
}
let helpText;
if (sendEmailNotifications) {
helpText = (
<FormattedText
id='user.settings.notifications.emailInfo'
defaultMessage='Email notifications are sent for mentions and direct messages when you are offline or away from {siteName} for more than 5 minutes.'
values={{siteName: config.SiteName}}
style={style.modalHelpText}
/>
);
}
return (
<Modal
animationType='slide'
transparent={true}
visible={this.state.showEmailNotificationsModal}
onRequestClose={() => this.setState({showEmailNotificationsModal: false})}
>
<View style={style.modalOverlay}>
<View style={style.modal}>
<View style={style.modalBody}>
<View style={style.modalTitleContainer}>
<FormattedText
id='user.settings.notifications.email.send'
defaultMessage='Send email notifications'
style={style.modalTitle}
/>
</View>
{!sendEmailNotifications &&
<FormattedText
id='user.settings.general.emailHelp2'
defaultMessage='Email has been disabled by your System Administrator. No notification emails will be sent until it is enabled.'
style={style.modalOptionDisabled}
/>
}
{sendEmailNotifications &&
<RadioButtonGroup
name='emailSettings'
onSelect={this.setEmailNotifications}
>
<RadioButton
label={intl.formatMessage({
id: 'user.settings.notifications.email.immediately',
defaultMessage: 'Immediately'
})}
value={sendImmediatleyValue}
checked={sendImmediatley}
/>
{emailBatchingEnabled &&
<RadioButton
label={intl.formatMessage({
id: 'user.settings.notifications.email.everyXMinutes',
defaultMessage: 'Every {count, plural, one {minute} other {{count, number} minutes}}'
}, {count: Preferences.INTERVAL_FIFTEEN_MINUTES / 60})}
value={Preferences.INTERVAL_FIFTEEN_MINUTES.toString()}
checked={fifteenMinutes}
/>
}
{emailBatchingEnabled &&
<RadioButton
label={intl.formatMessage({
id: 'user.settings.notifications.email.everyHour',
defaultMessage: 'Every hour'
})}
value={Preferences.INTERVAL_HOUR.toString()}
checked={hourly}
/>
}
<RadioButton
label={intl.formatMessage({
id: 'user.settings.notifications.email.never',
defaultMessage: 'Never'
})}
value='false'
checked={never}
/>
</RadioButtonGroup>
}
{helpText}
</View>
<View style={style.modalFooter}>
<View style={style.divider}/>
<View style={style.modalFooterContainer}>
<TouchableOpacity
style={style.modalFooterOptionContainer}
onPress={this.saveEmailNotifications}
>
<FormattedText
id='mobile.notification_settings.modal_cancel'
defaultMessage='CANCEL'
style={style.modalFooterOption}
/>
</TouchableOpacity>
{sendEmailNotifications &&
<View>
<View style={{marginRight: 10}}/>
<TouchableOpacity
style={style.modalFooterOptionContainer}
onPress={this.saveEmailNotifications}
>
<FormattedText
id='mobile.notification_settings.modal_save'
defaultMessage='SAVE'
style={style.modalFooterOption}
/>
</TouchableOpacity>
</View>
}
</View>
</View>
</View>
</View>
</Modal>
);
};
render() {
const {theme} = this.props;
const style = getStyleSheet(theme);
const showArrow = Platform.OS === 'ios';
return (
<View style={style.container}>
<StatusBar/>
<View style={style.wrapper}>
<View style={style.divider}/>
<SettingsItem
defaultMessage='Mentions and Replies'
i18nId='mobile.notification_settings.mentions_replies'
iconName='md-at'
iconType='ion'
onPress={() => this.handlePress(this.goToNotificationSettingsMentions)}
separator={true}
showArrow={showArrow}
theme={theme}
/>
<SettingsItem
defaultMessage='Mobile'
i18nId='mobile.notification_settings.mobile'
iconName='md-phone-portrait'
iconType='ion'
onPress={() => this.handlePress(this.goToNotificationSettingsMobile)}
separator={true}
showArrow={showArrow}
theme={theme}
/>
<SettingsItem
defaultMessage='Email'
i18nId='mobile.notification_settings.email'
iconName='ios-mail'
iconType='ion'
onPress={() => this.handlePress(this.goToNotificationSettingsEmail)}
separator={false}
showArrow={showArrow}
theme={theme}
/>
<View style={style.divider}/>
</View>
{this.renderEmailNotificationSettings(style)}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
wrapper: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
flex: 1,
...Platform.select({
ios: {
paddingTop: 35
}
})
},
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 1,
width: '100%'
},
modalOverlay: {
backgroundColor: changeOpacity('#000000', 0.6),
alignItems: 'center',
flex: 1
},
modal: {
backgroundColor: theme.centerChannelBg,
borderRadius: 4,
marginTop: 20,
width: '95%'
},
modalBody: {
paddingHorizontal: 24
},
modalTitleContainer: {
marginBottom: 30,
marginTop: 20
},
modalTitle: {
color: theme.centerChannelColor,
fontSize: 19
},
modalOptionDisabled: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 17
},
modalHelpText: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 13,
marginTop: 20
},
modalFooter: {
alignItems: 'flex-end',
height: 58,
marginTop: 40,
width: '100%'
},
modalFooterContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
paddingRight: 24
},
modalFooterOptionContainer: {
alignItems: 'center',
height: 40,
justifyContent: 'center',
paddingHorizontal: 10,
paddingVertical: 5
},
modalFooterOption: {
color: theme.linkColor,
fontSize: 14
}
};
});
export default injectIntl(NotificationSettings);

View file

@ -0,0 +1,22 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import NotificationSettingsEmail from './notification_settings_email';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
config: getConfig(state),
myPreferences: getMyPreferences(state),
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(NotificationSettingsEmail);

View file

@ -0,0 +1,249 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
ScrollView,
View
} from 'react-native';
import {Preferences} from 'mattermost-redux/constants';
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
import FormattedText from 'app/components/formatted_text';
import StatusBar from 'app/components/status_bar';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import Section from 'app/screens/settings/section';
import SectionItem from 'app/screens/settings/section_item';
export default class NotificationSettingsEmail extends PureComponent {
static propTypes = {
config: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
myPreferences: PropTypes.object.isRequired,
navigator: PropTypes.object,
onBack: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired
};
constructor(props) {
super(props);
const {currentUser} = props;
const notifyProps = currentUser.notify_props || {};
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
this.setStateFromNotifyProps(notifyProps);
}
onNavigatorEvent = (event) => {
if (event.type === 'ScreenChangedEvent') {
switch (event.id) {
case 'willDisappear':
this.saveUserNotifyProps();
break;
}
}
};
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
});
};
setStateFromNotifyProps = (notifyProps) => {
const {config, myPreferences} = this.props;
let interval;
if (config.SendEmailNotifications === 'true' && config.EnableEmailBatching === 'true') {
const emailPreferences = getPreferencesByCategory(myPreferences, Preferences.CATEGORY_NOTIFICATIONS);
if (emailPreferences.size) {
interval = emailPreferences.get(Preferences.EMAIL_INTERVAL).value;
}
}
const newState = {
...notifyProps,
interval
};
this.state = {...newState};
};
saveUserNotifyProps = () => {
this.props.onBack({
...this.state,
user_id: this.props.currentUser.id
});
};
renderEmailSection = () => {
const {config, theme} = this.props;
const style = getStyleSheet(theme);
const sendEmailNotifications = config.SendEmailNotifications === 'true';
const emailBatchingEnabled = sendEmailNotifications && 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='mobile.notification_settings.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
label={(
<FormattedText
id='user.settings.notifications.email.immediately'
defaultMessage='Immediately'
/>
)}
action={this.setEmailNotifications}
actionType='select'
actionValue={sendImmediatleyValue}
selected={sendImmediatley}
theme={theme}
/>
<View style={style.separator}/>
{emailBatchingEnabled &&
<View>
<SectionItem
label={(
<FormattedText
id='user.settings.notifications.email.everyXMinutes'
defaultMessage='Every {count, plural, one {minute} other {{count, number} minutes}}'
values={{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
label={(
<FormattedText
id='user.settings.notifications.email.everyHour'
defaultMessage='Every hour'
/>
)}
action={this.setEmailNotifications}
actionType='select'
actionValue={Preferences.INTERVAL_HOUR.toString()}
selected={hourly}
theme={theme}
/>
<View style={style.separator}/>
</View>
}
<SectionItem
label={(
<FormattedText
id='user.settings.notifications.email.never'
defaultMessage='Never'
/>
)}
action={this.setEmailNotifications}
actionType='select'
actionValue='false'
selected={never}
theme={theme}
/>
</View>
}
{!sendEmailNotifications &&
<FormattedText
id='user.settings.general.emailHelp2'
defaultMessage='Email has been disabled by your System Administrator. No notification emails will be sent until it is enabled.'
style={style.disabled}
/>
}
</Section>
);
};
render() {
const {theme} = this.props;
const style = getStyleSheet(theme);
return (
<View style={style.container}>
<StatusBar/>
<ScrollView
style={style.scrollView}
contentContainerStyle={style.scrollViewContent}
>
{this.renderEmailSection()}
</ScrollView>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
input: {
color: theme.centerChannelColor,
fontSize: 12,
height: 40
},
separator: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
flex: 1,
height: 1,
marginLeft: 15
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06)
},
scrollViewContent: {
paddingVertical: 35
},
disabled: {
color: theme.centerChannelColor,
fontSize: 15,
paddingHorizontal: 15,
paddingVertical: 10
}
};
});

View file

@ -5,12 +5,13 @@ import {connect} from 'react-redux';
import {getTheme} from 'app/selectors/preferences';
import AccountSettings from './account_settings';
import NotificationSettingsMentions from './notification_settings_mentions';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
return {
...ownProps,
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(AccountSettings);
export default connect(mapStateToProps)(NotificationSettingsMentions);

View file

@ -0,0 +1,118 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
export default class NotificationSettingsMentionsBase extends PureComponent {
static propTypes = {
currentUser: PropTypes.object.isRequired,
intl: intlShape.isRequired,
navigator: PropTypes.object,
onBack: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired
};
constructor(props) {
super(props);
const {currentUser} = props;
const notifyProps = currentUser.notify_props || {};
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
this.goingBack = true; //use to identify if the navigator is popping this screen
this.setStateFromNotifyProps(notifyProps);
}
onNavigatorEvent = (event) => {
if (event.type === 'ScreenChangedEvent' && this.goingBack) {
switch (event.id) {
case 'willDisappear':
this.saveUserNotifyProps();
break;
}
}
};
setStateFromNotifyProps = (notifyProps) => {
const mentionKeys = (notifyProps.mention_keys || '').split(',');
const usernameMentionIndex = mentionKeys.indexOf(this.props.currentUser.username);
if (usernameMentionIndex > -1) {
mentionKeys.splice(usernameMentionIndex, 1);
}
const comments = notifyProps.comments || 'never';
const newState = {
...notifyProps,
comments,
usernameMention: usernameMentionIndex > -1,
mention_keys: mentionKeys.join(','),
showKeywordsModal: false,
showReplyModal: false
};
this.keywords = newState.mention_keys;
this.replyValue = comments;
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.goingBack = true;
this.setState({
mention_keys: text,
showKeywordsModal: false
});
};
setReplyNotifications = (value) => {
this.setState({
comments: value,
showReplyModal: false
});
};
saveUserNotifyProps = () => {
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(',');
Reflect.deleteProperty(notifyProps, 'showKeywordsModal');
Reflect.deleteProperty(notifyProps, 'showReplyModal');
this.props.onBack({
...notifyProps,
mention_keys: mentionKeys,
user_id: this.props.currentUser.id
});
};
}

View file

@ -0,0 +1,445 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {
Modal,
ScrollView,
Text,
TouchableOpacity,
View
} from 'react-native';
import {injectIntl} from 'react-intl';
import FormattedText from 'app/components/formatted_text';
import {RadioButton, RadioButtonGroup} from 'app/components/radio_button';
import StatusBar from 'app/components/status_bar';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import SectionItem from 'app/screens/settings/section_item';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import NotificationSettingsMentionsBase from './notification_settings_mention_base';
class NotificationSettingsMentionsIos extends NotificationSettingsMentionsBase {
cancelMentionKeys = () => {
this.setState({showKeywordsModal: false});
this.keywords = this.state.mention_keys;
};
cancelReplyNotification = () => {
this.setState({showReplyModal: false});
this.replyValue = this.state.comments;
};
onKeywordsChangeText = (value) => {
this.keywords = value;
};
onReplyChanged = (value) => {
this.replyValue = value;
};
renderKeywordsModal(style) {
const {theme} = this.props;
return (
<Modal
animationType='slide'
transparent={true}
visible={this.state.showKeywordsModal}
onRequestClose={this.cancelMentionKeys}
>
<View style={style.modalOverlay}>
<View style={style.modal}>
<View style={style.modalBody}>
<View style={style.modalTitleContainer}>
<FormattedText
id='user.settings.notifications.email.send'
defaultMessage='Send email notifications'
style={style.modalTitle}
/>
</View>
<TextInputWithLocalizedPlaceholder
autoFocus={true}
defaultValue={this.keywords}
blurOnSubmit={false}
onChangeText={this.onKeywordsChangeText}
multiline={false}
style={style.input}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'mobile.notification_settings_mentions.keywordsDescription', defaultMessage: 'Other words that trigger a mention'}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.4)}
returnKeyType='done'
underlineColorAndroid={theme.linkColor}
/>
<FormattedText
id='mobile.notification_settings_mentions.keywordsHelp'
defaultMessage='Keywords are non-case sensitive and should be separated by a comma.'
style={style.modalHelpText}
/>
</View>
<View style={style.modalFooter}>
<View style={style.separator}/>
<View style={style.modalFooterContainer}>
<TouchableOpacity
style={style.modalFooterOptionContainer}
onPress={this.cancelMentionKeys}
>
<FormattedText
id='mobile.notification_settings.modal_cancel'
defaultMessage='CANCEL'
style={style.modalFooterOption}
/>
</TouchableOpacity>
<View style={{marginRight: 10}}/>
<TouchableOpacity
style={style.modalFooterOptionContainer}
onPress={this.saveMentionKeys}
>
<FormattedText
id='mobile.notification_settings.modal_save'
defaultMessage='SAVE'
style={style.modalFooterOption}
/>
</TouchableOpacity>
</View>
</View>
</View>
</View>
</Modal>
);
}
renderReplyModal(style) {
const {intl} = this.props;
return (
<Modal
animationType='slide'
transparent={true}
visible={this.state.showReplyModal}
onRequestClose={this.cancelReplyNotification}
>
<View style={style.modalOverlay}>
<View style={style.modal}>
<View style={style.modalBody}>
<View style={style.modalTitleContainer}>
<FormattedText
id='mobile.notification_settings.mentions.reply_title'
defaultMessage='Send Reply notifications for'
style={style.modalTitle}
/>
</View>
<RadioButtonGroup
name='replySettings'
onSelect={this.onReplyChanged}
>
<RadioButton
label={intl.formatMessage({
id: 'mobile.account_notifications.threads_start_participate',
defaultMessage: 'Threads that I start or participate in'
})}
value='any'
checked={this.state.comments === 'any'}
/>
<RadioButton
label={intl.formatMessage({
id: 'mobile.account_notifications.threads_start',
defaultMessage: 'Threads that I start'
})}
value='root'
checked={this.state.comments === 'root'}
/>
<RadioButton
label={intl.formatMessage({
id: 'mobile.account_notifications.threads_mentions',
defaultMessage: 'Mentions in threads'
})}
value='never'
checked={this.state.comments === 'never'}
/>
</RadioButtonGroup>
</View>
<View style={style.modalFooter}>
<View style={style.separator}/>
<View style={style.modalFooterContainer}>
<TouchableOpacity
style={style.modalFooterOptionContainer}
onPress={this.cancelReplyNotification}
>
<FormattedText
id='mobile.notification_settings.modal_cancel'
defaultMessage='CANCEL'
style={style.modalFooterOption}
/>
</TouchableOpacity>
<View>
<View style={{marginRight: 10}}/>
<TouchableOpacity
style={style.modalFooterOptionContainer}
onPress={this.saveReplyNotification}
>
<FormattedText
id='mobile.notification_settings.modal_save'
defaultMessage='SAVE'
style={style.modalFooterOption}
/>
</TouchableOpacity>
</View>
</View>
</View>
</View>
</View>
</Modal>
);
}
renderReplySection() {
const {theme} = this.props;
let i18nId;
let i18nMessage;
switch (this.state.comments) {
case 'any':
i18nId = 'mobile.account_notifications.threads_start_participate';
i18nMessage = 'Threads that I start or participate in';
break;
case 'root':
i18nId = 'mobile.account_notifications.threads_start';
i18nMessage = 'Threads that I start';
break;
case 'never':
i18nId = 'mobile.account_notifications.threads_mentions';
i18nMessage = 'Mentions in threads';
break;
}
return (
<SectionItem
description={(
<FormattedText
id={i18nId}
defaultMessage={i18nMessage}
/>
)}
label={(
<FormattedText
id='user.settings.notifications.comments'
defaultMessage='Reply notifications'
/>
)}
action={this.showReplyModal}
actionType='default'
theme={theme}
/>
);
}
saveMentionKeys = () => {
this.setState({showKeywordsModal: false});
this.updateMentionKeys(this.keywords);
};
saveReplyNotification = () => {
this.setState({showReplyModal: false});
this.setReplyNotifications(this.replyValue);
};
showKeywordsModal = () => {
this.setState({showKeywordsModal: true});
};
showReplyModal = () => {
this.setState({showReplyModal: true});
};
render() {
const {currentUser, theme} = this.props;
const style = getStyleSheet(theme);
let mentionKeysComponent;
if (this.state.mention_keys) {
mentionKeysComponent = (<Text>{this.state.mention_keys}</Text>);
} else {
mentionKeysComponent = (
<FormattedText
id='mobile.notification_settings_mentions.keywordsDescription'
defaultMessage='Other words that trigger a mention'
/>
);
}
return (
<View style={style.container}>
<StatusBar/>
<ScrollView
style={style.scrollView}
contentContainerStyle={style.scrollViewContent}
>
{currentUser.first_name.length > 0 &&
<View>
<SectionItem
label={(
<Text>
{currentUser.first_name}
</Text>
)}
description={(
<FormattedText
id='mobile.notification_settings.mentions.sensitiveName'
defaultMessage='Your case sensitive first name'
/>
)}
action={this.toggleFirstNameMention}
actionType='toggle'
selected={this.state.first_name === 'true'}
theme={theme}
/>
<View style={style.separator}/>
</View>
}
<SectionItem
label={(
<Text>
{currentUser.username}
</Text>
)}
description={(
<FormattedText
id='mobile.notification_settings.mentions.sensitiveUsername'
defaultMessage='Your non-case sensitive username'
/>
)}
selected={this.state.usernameMention}
action={this.toggleUsernameMention}
actionType='toggle'
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
label={(
<Text>
{'@channel, @all, @here'}
</Text>
)}
description={(
<FormattedText
id='mobile.notification_settings.mentions.channelWide'
defaultMessage='Channel-wide mentions'
/>
)}
action={this.toggleChannelMentions}
actionType='toggle'
selected={this.state.channel === 'true'}
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
label={(
<FormattedText
id='mobile.notification_settings_mentions.keywords'
defaultMessage='Keywords'
/>
)}
description={mentionKeysComponent}
action={this.showKeywordsModal}
actionType='default'
theme={theme}
/>
<View style={style.separator}/>
{this.renderReplySection()}
<View style={style.separator}/>
</ScrollView>
{this.renderKeywordsModal(style)}
{this.renderReplyModal(style)}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
input: {
color: theme.centerChannelColor,
fontSize: 12,
height: 40
},
separator: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
flex: 1,
height: 1
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06)
},
scrollViewContent: {
paddingVertical: 0
},
modalOverlay: {
backgroundColor: changeOpacity('#000000', 0.6),
alignItems: 'center',
flex: 1
},
modal: {
backgroundColor: theme.centerChannelBg,
borderRadius: 4,
marginTop: 20,
width: '95%'
},
modalBody: {
paddingHorizontal: 24
},
modalTitleContainer: {
marginBottom: 30,
marginTop: 20
},
modalTitle: {
color: theme.centerChannelColor,
fontSize: 19
},
modalOptionDisabled: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 17
},
modalInput: {
color: theme.centerChannelColor,
fontSize: 19
},
modalHelpText: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 13,
marginTop: 20
},
modalFooter: {
alignItems: 'flex-end',
height: 58,
marginTop: 40,
width: '100%'
},
modalFooterContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
paddingRight: 24
},
modalFooterOptionContainer: {
alignItems: 'center',
height: 40,
justifyContent: 'center',
paddingHorizontal: 10,
paddingVertical: 5
},
modalFooterOption: {
color: theme.linkColor,
fontSize: 14
}
};
});
export default injectIntl(NotificationSettingsMentionsIos);

View file

@ -0,0 +1,238 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {
ScrollView,
Text,
View
} from 'react-native';
import {injectIntl} from 'react-intl';
import FormattedText from 'app/components/formatted_text';
import StatusBar from 'app/components/status_bar';
import Section from 'app/screens/settings/section';
import SectionItem from 'app/screens/settings/section_item';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import NotificationSettingsMentionsBase from './notification_settings_mention_base';
class NotificationSettingsMentionsIos extends NotificationSettingsMentionsBase {
goToNotificationSettingsMentionKeywords = () => {
const {intl, navigator, theme} = this.props;
this.goingBack = false;
navigator.push({
backButtonTitle: '',
screen: 'NotificationSettingsMentionsKeywords',
title: intl.formatMessage({id: 'mobile.notification_settings_mentions.keywords', defaultMessage: 'Keywords'}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
},
passProps: {
keywords: this.state.mention_keys,
onBack: this.updateMentionKeys
}
});
};
renderMentionSection(style) {
const {currentUser, theme} = this.props;
let mentionKeysComponent;
if (this.state.mention_keys) {
mentionKeysComponent = (<Text>{this.state.mention_keys}</Text>);
} else {
mentionKeysComponent = (
<FormattedText
id='mobile.notification_settings_mentions.keywordsDescription'
defaultMessage='Other words that trigger a mention'
/>
);
}
return (
<Section
headerId='mobile.notification_settings_mentions.wordsTrigger'
headerDefaultMessage='WORDS THAT TRIGGER MENTIONS'
theme={theme}
>
{currentUser.first_name.length > 0 &&
<View>
<SectionItem
label={(
<Text>
{currentUser.first_name}
</Text>
)}
description={(
<FormattedText
id='mobile.notification_settings.mentions.sensitiveName'
defaultMessage='Your case sensitive first name'
/>
)}
action={this.toggleFirstNameMention}
actionType='toggle'
selected={this.state.first_name === 'true'}
theme={theme}
/>
<View style={style.separator}/>
</View>
}
<SectionItem
label={(
<Text>
{currentUser.username}
</Text>
)}
description={(
<FormattedText
id='mobile.notification_settings.mentions.sensitiveUsername'
defaultMessage='Your non-case sensitive username'
/>
)}
selected={this.state.usernameMention}
action={this.toggleUsernameMention}
actionType='toggle'
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
label={(
<Text>
{'@channel, @all, @here'}
</Text>
)}
description={(
<FormattedText
id='mobile.notification_settings.mentions.channelWide'
defaultMessage='Channel-wide mentions'
/>
)}
action={this.toggleChannelMentions}
actionType='toggle'
selected={this.state.channel === 'true'}
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
label={(
<FormattedText
id='mobile.notification_settings_mentions.keywords'
defaultMessage='Keywords'
/>
)}
description={mentionKeysComponent}
action={this.goToNotificationSettingsMentionKeywords}
actionType='arrow'
theme={theme}
/>
</Section>
);
}
renderReplySection(style) {
const {theme} = this.props;
return (
<Section
headerId='mobile.account_notifications.reply.header'
headerDefaultMessage='SEND REPLY NOTIFICATIONS FOR'
theme={theme}
>
<SectionItem
label={(
<FormattedText
id='mobile.account_notifications.threads_start_participate'
defaultMessage='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
label={(
<FormattedText
id='mobile.account_notifications.threads_start'
defaultMessage='Threads that I start'
/>
)}
action={this.setReplyNotifications}
actionType='select'
actionValue='root'
selected={this.state.comments === 'root'}
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
label={(
<FormattedText
id='mobile.account_notifications.threads_mentions'
defaultMessage='Mentions in threads'
/>
)}
action={this.setReplyNotifications}
actionType='select'
actionValue='never'
selected={this.state.comments === 'never'}
theme={theme}
/>
</Section>
);
}
render() {
const {theme} = this.props;
const style = getStyleSheet(theme);
return (
<View style={style.container}>
<StatusBar/>
<ScrollView
style={style.scrollView}
contentContainerStyle={style.scrollViewContent}
>
{this.renderMentionSection(style)}
{this.renderReplySection(style)}
</ScrollView>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
input: {
color: theme.centerChannelColor,
fontSize: 12,
height: 40
},
separator: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
flex: 1,
height: 1,
marginLeft: 15
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06)
},
scrollViewContent: {
paddingVertical: 35
}
};
});
export default injectIntl(NotificationSettingsMentionsIos);

View file

@ -0,0 +1,17 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'app/selectors/preferences';
import NotificationSettingsMentionsKeywords from './notification_settings_mentions_keywords';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(NotificationSettingsMentionsKeywords);

View file

@ -0,0 +1,130 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import StatusBar from 'app/components/status_bar';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class NotificationSettingsMentionsKeywords extends PureComponent {
static propTypes = {
keywords: PropTypes.string,
navigator: PropTypes.object,
onBack: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired
};
constructor(props) {
super(props);
this.state = {
keywords: props.keywords
};
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
}
handleSubmit = () => {
this.props.navigator.pop();
};
keywordsRef = (ref) => {
this.keywordsInput = ref;
};
onKeywordsChangeText = (keywords) => {
if (keywords.endsWith('\n')) {
return this.handleSubmit();
}
return this.setState({keywords});
};
onNavigatorEvent = (event) => {
if (event.type === 'ScreenChangedEvent') {
switch (event.id) {
case 'willDisappear':
this.props.onBack(this.state.keywords);
break;
}
}
};
render() {
const {theme} = this.props;
const {keywords} = this.state;
const style = getStyleSheet(theme);
return (
<View style={style.container}>
<StatusBar/>
<View style={style.wrapper}>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
autoFocus={true}
ref={this.keywordsRef}
value={keywords}
blurOnSubmit={false}
onChangeText={this.onKeywordsChangeText}
multiline={true}
numberOfLines={1}
style={style.input}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'mobile.notification_settings_mentions.keywordsDescription', defaultMessage: 'Other words that trigger a mention'}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.4)}
returnKeyType='done'
/>
</View>
<View style={style.helpContainer}>
<FormattedText
id='mobile.notification_settings_mentions.keywordsHelp'
defaultMessage='Keywords are non-case sensitive and should be separated by a comma.'
style={style.help}
/>
</View>
</View>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
wrapper: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
paddingTop: 35
},
inputContainer: {
borderTopWidth: 1,
borderBottomWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
backgroundColor: theme.centerChannelBg
},
input: {
color: theme.centerChannelColor,
fontSize: 15,
height: 150,
paddingHorizontal: 15,
paddingVertical: 10
},
helpContainer: {
marginTop: 10,
paddingHorizontal: 15
},
help: {
color: changeOpacity(theme.centerChannelColor, 0.4),
fontSize: 13
}
};
});

View file

@ -0,0 +1,20 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'app/selectors/preferences';
import NotificationSettingsMobile from './notification_settings_mobile';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
config: getConfig(state),
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(NotificationSettingsMobile);

View file

@ -0,0 +1,437 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {injectIntl} from 'react-intl';
import {
Modal,
ScrollView,
TouchableOpacity,
View
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import {RadioButton, RadioButtonGroup} from 'app/components/radio_button';
import StatusBar from 'app/components/status_bar';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import SectionItem from 'app/screens/settings/section_item';
import NotificationSettingsMobileBase from './notification_settings_mobile_base';
class NotificationSettingsMobileAndroid extends NotificationSettingsMobileBase {
cancelMobilePushModal = () => {
this.setState({showMobilePushModal: false});
this.push = this.state.push;
};
cancelMobilePushStatusModal = () => {
this.setState({showMobilePushStatusModal: false});
this.pushStatus = this.state.push_status;
};
onMobilePushChanged = (value) => {
this.push = value;
};
onMobilePushStatusChanged = (value) => {
this.pushStatus = value;
};
renderMobilePushModal(style) {
const {config, intl} = this.props;
const pushNotificationsEnabled = config.SendPushNotifications === 'true';
return (
<Modal
animationType='slide'
transparent={true}
visible={this.state.showMobilePushModal}
onRequestClose={this.cancelMobilePushModal}
>
<View style={style.modalOverlay}>
<View style={style.modal}>
<View style={style.modalBody}>
<View style={style.modalTitleContainer}>
<FormattedText
id='mobile.notification_settings_mobile.push_activity_android'
defaultMessage='Send notifications for'
style={style.modalTitle}
/>
</View>
{pushNotificationsEnabled &&
<RadioButtonGroup
name='pushSettings'
onSelect={this.onMobilePushChanged}
>
<RadioButton
label={intl.formatMessage({
id: 'user.settings.notifications.allActivity',
defaultMessage: 'For all activity'
})}
value='all'
checked={this.state.push === 'all'}
/>
<RadioButton
label={intl.formatMessage({
id: 'user.settings.notifications.onlyMentions',
defaultMessage: 'Only for mentions and direct messages'
})}
value='mention'
checked={this.state.push === 'mention'}
/>
<RadioButton
label={intl.formatMessage({
id: 'user.settings.notifications.never',
defaultMessage: 'Never'
})}
value='none'
checked={this.state.push === 'none'}
/>
</RadioButtonGroup>
}
{!pushNotificationsEnabled &&
<FormattedText
id='user.settings.push_notification.disabled_long'
defaultMessage='Push notifications for mobile devices have been disabled by your System Administrator.'
style={style.modalOptionDisabled}
/>
}
</View>
<View style={style.modalFooter}>
<View style={style.separator}/>
<View style={style.modalFooterContainer}>
<TouchableOpacity
style={style.modalFooterOptionContainer}
onPress={this.cancelMobilePushModal}
>
<FormattedText
id='mobile.notification_settings.modal_cancel'
defaultMessage='CANCEL'
style={style.modalFooterOption}
/>
</TouchableOpacity>
{pushNotificationsEnabled &&
<View>
<View style={{marginRight: 10}}/>
<TouchableOpacity
style={style.modalFooterOptionContainer}
onPress={this.saveMobilePushModal}
>
<FormattedText
id='mobile.notification_settings.modal_save'
defaultMessage='SAVE'
style={style.modalFooterOption}
/>
</TouchableOpacity>
</View>
}
</View>
</View>
</View>
</View>
</Modal>
);
}
renderMobilePushStatusModal(style) {
const {intl} = this.props;
return (
<Modal
animationType='slide'
transparent={true}
visible={this.state.showMobilePushStatusModal}
onRequestClose={this.cancelMobilePushStatusModal}
>
<View style={style.modalOverlay}>
<View style={style.modal}>
<View style={style.modalBody}>
<View style={style.modalTitleContainer}>
<FormattedText
id='mobile.notification_settings_model.push_status_android'
defaultMessage='Trigger push notifications when'
style={style.modalTitle}
/>
</View>
<RadioButtonGroup
name='pushStatusSettings'
onSelect={this.onMobilePushStatusChanged}
>
<RadioButton
label={intl.formatMessage({
id: 'user.settings.push_notification.online',
defaultMessage: 'Online, away or offline'
})}
value='online'
checked={this.state.push_status === 'online'}
/>
<RadioButton
label={intl.formatMessage({
id: 'user.settings.push_notification.away',
defaultMessage: 'Away or offline'
})}
value='away'
checked={this.state.push_status === 'away'}
/>
<RadioButton
label={intl.formatMessage({
id: 'user.settings.push_notification.offline',
defaultMessage: 'Offline'
})}
value='offline'
checked={this.state.push_status === 'offline'}
/>
</RadioButtonGroup>
</View>
<View style={style.modalFooter}>
<View style={style.separator}/>
<View style={style.modalFooterContainer}>
<TouchableOpacity
style={style.modalFooterOptionContainer}
onPress={this.cancelMobilePushStatusModal}
>
<FormattedText
id='mobile.notification_settings.modal_cancel'
defaultMessage='CANCEL'
style={style.modalFooterOption}
/>
</TouchableOpacity>
<View style={{marginRight: 10}}/>
<TouchableOpacity
style={style.modalFooterOptionContainer}
onPress={this.saveMobilePushStatusModal}
>
<FormattedText
id='mobile.notification_settings.modal_save'
defaultMessage='SAVE'
style={style.modalFooterOption}
/>
</TouchableOpacity>
</View>
</View>
</View>
</View>
</Modal>
);
}
renderMobilePushSection() {
const {config, theme} = this.props;
const pushNotificationsEnabled = config.SendPushNotifications === 'true';
let i18nId;
let i18nMessage;
const props = {};
if (pushNotificationsEnabled) {
switch (this.state.push) {
case 'all':
i18nId = 'user.settings.notifications.allActivity';
i18nMessage = 'For all activity';
break;
case 'mention':
i18nId = 'user.settings.notifications.onlyMentions';
i18nMessage = 'Only for mentions and direct messages';
break;
case 'none':
i18nId = 'user.settings.notifications.never';
i18nMessage = 'Never';
break;
}
props.description = (
<FormattedText
id={i18nId}
defaultMessage={i18nMessage}
/>
);
} else {
props.description = (
<FormattedText
id='user.settings.push_notification.disabled'
defaultMessage='Disabled by System Administrator'
/>
);
}
return (
<SectionItem
{...props}
label={(
<FormattedText
id='mobile.notification_settings_mobile.push_activity_android'
defaultMessage='Send notifications for'
/>
)}
action={this.showMobilePushModal}
actionType='default'
theme={theme}
/>
);
}
renderMobilePushStatusSection(style) {
const {config, theme} = this.props;
const showSection = config.SendPushNotifications === 'true' && this.state.push !== 'none';
if (!showSection) {
return null;
}
let i18nId;
let i18nMessage;
switch (this.state.push_status) {
case 'online':
i18nId = 'user.settings.push_notification.online';
i18nMessage = 'Online, away or offline';
break;
case 'away':
i18nId = 'user.settings.push_notification.away';
i18nMessage = 'Away or offline';
break;
case 'offline':
i18nId = 'user.settings.push_notification.offline';
i18nMessage = 'Offline';
break;
}
return (
<View>
<SectionItem
description={(
<FormattedText
id={i18nId}
defaultMessage={i18nMessage}
/>
)}
label={(
<FormattedText
id='mobile.notification_settings_model.push_status_android'
defaultMessage='Trigger push notifications when'
/>
)}
action={this.showMobilePushStatusModal}
actionType='default'
theme={theme}
/>
<View style={style.separator}/>
</View>
);
}
saveMobilePushModal = () => {
this.setState({showMobilePushModal: false});
this.setMobilePush(this.push);
};
saveMobilePushStatusModal = () => {
this.setState({showMobilePushStatusModal: false});
this.setMobilePushStatus(this.pushStatus);
};
showMobilePushModal = () => {
this.setState({showMobilePushModal: true});
};
showMobilePushStatusModal = () => {
this.setState({showMobilePushStatusModal: true});
};
render() {
const {theme} = this.props;
const style = getStyleSheet(theme);
return (
<View style={style.container}>
<StatusBar/>
<ScrollView
style={style.scrollView}
contentContainerStyle={style.scrollViewContent}
>
{this.renderMobilePushSection()}
<View style={style.separator}/>
{this.renderMobilePushStatusSection(style)}
</ScrollView>
{this.renderMobilePushModal(style)}
{this.renderMobilePushStatusModal(style)}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
input: {
color: theme.centerChannelColor,
fontSize: 12,
height: 40
},
separator: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
flex: 1,
height: 1
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06)
},
scrollViewContent: {
paddingVertical: 0
},
modalOverlay: {
backgroundColor: changeOpacity('#000000', 0.6),
alignItems: 'center',
flex: 1
},
modal: {
backgroundColor: theme.centerChannelBg,
borderRadius: 4,
marginTop: 20,
width: '95%'
},
modalBody: {
paddingHorizontal: 24
},
modalTitleContainer: {
marginBottom: 30,
marginTop: 20
},
modalTitle: {
color: theme.centerChannelColor,
fontSize: 19
},
modalOptionDisabled: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 17
},
modalFooter: {
alignItems: 'flex-end',
height: 58,
marginTop: 40,
width: '100%'
},
modalFooterContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
paddingRight: 24
},
modalFooterOptionContainer: {
alignItems: 'center',
height: 40,
justifyContent: 'center',
paddingHorizontal: 10,
paddingVertical: 5
},
modalFooterOption: {
color: theme.linkColor,
fontSize: 14
}
};
});
export default injectIntl(NotificationSettingsMobileAndroid);

View file

@ -0,0 +1,198 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {injectIntl} from 'react-intl';
import {
ScrollView,
View
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import StatusBar from 'app/components/status_bar';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import Section from 'app/screens/settings/section';
import SectionItem from 'app/screens/settings/section_item';
import NotificationSettingsMobileBase from './notification_settings_mobile_base';
class NotificationSettingsMobileIos extends NotificationSettingsMobileBase {
renderMobilePushSection(style) {
const {config, theme} = this.props;
const pushNotificationsEnabled = config.SendPushNotifications === 'true';
return (
<Section
headerId='mobile.notification_settings_mobile.push_activity'
headerDefaultMessage='SEND NOTIFICATIONS FOR'
theme={theme}
>
{pushNotificationsEnabled &&
<View>
<SectionItem
label={(
<FormattedText
id='user.settings.notifications.allActivity'
defaultMessage='For all activity'
/>
)}
action={this.setMobilePush}
actionType='select'
actionValue='all'
selected={this.state.push === 'all'}
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
label={(
<FormattedText
id='user.settings.notifications.onlyMentions'
defaultMessage='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
label={(
<FormattedText
id='user.settings.notifications.never'
defaultMessage='Never'
/>
)}
action={this.setMobilePush}
actionType='select'
actionValue='none'
selected={this.state.push === 'none'}
theme={theme}
/>
</View>
}
{!pushNotificationsEnabled &&
<FormattedText
id='user.settings.push_notification.disabled_long'
defaultMessage='Push notifications for mobile devices have been disabled by your System Administrator.'
style={style.disabled}
/>
}
</Section>
);
}
renderMobilePushStatusSection(style) {
const {config, theme} = this.props;
const showSection = config.SendPushNotifications === 'true' && this.state.push !== 'none';
if (!showSection) {
return null;
}
return (
<Section
headerId='mobile.notification_settings_model.push_status'
headerDefaultMessage='TRIGGER PUSH NOTIFICATIONS WHEN'
theme={theme}
>
<SectionItem
label={(
<FormattedText
id='user.settings.push_notification.online'
defaultMessage='Online, away or offline'
/>
)}
action={this.setMobilePushStatus}
actionType='select'
actionValue='online'
selected={this.state.push_status === 'online'}
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
label={(
<FormattedText
id='user.settings.push_notification.away'
defaultMessage='Away or offline'
/>
)}
action={this.setMobilePushStatus}
actionType='select'
actionValue='away'
selected={this.state.push_status === 'away'}
theme={theme}
/>
<View style={style.separator}/>
<SectionItem
label={(
<FormattedText
id='user.settings.push_notification.offline'
defaultMessage='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.container}>
<StatusBar/>
<ScrollView
style={style.scrollView}
contentContainerStyle={style.scrollViewContent}
>
{this.renderMobilePushSection(style)}
{this.renderMobilePushStatusSection(style)}
</ScrollView>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
input: {
color: theme.centerChannelColor,
fontSize: 12,
height: 40
},
separator: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
flex: 1,
height: 1,
marginLeft: 15
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06)
},
scrollViewContent: {
paddingVertical: 35
},
disabled: {
color: theme.centerChannelColor,
fontSize: 15,
paddingHorizontal: 15,
paddingVertical: 10
}
};
});
export default injectIntl(NotificationSettingsMobileIos);

View file

@ -0,0 +1,62 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
export default class NotificationSettingsMobileBase extends PureComponent {
static propTypes = {
config: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
intl: intlShape.isRequired,
navigator: PropTypes.object,
onBack: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired
};
constructor(props) {
super(props);
const {currentUser} = props;
const notifyProps = currentUser.notify_props || {};
this.state = {
...notifyProps,
showMobilePushModal: false,
showMobilePushStatusModal: false
};
this.push = this.state.push;
this.pushStatus = this.state.push_status;
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
}
onNavigatorEvent = (event) => {
if (event.type === 'ScreenChangedEvent') {
switch (event.id) {
case 'willDisappear':
this.saveUserNotifyProps();
break;
}
}
};
setMobilePush = (push) => {
this.setState({push});
};
setMobilePushStatus = (value) => {
this.setState({push_status: value});
};
saveUserNotifyProps = () => {
const props = {...this.state};
Reflect.deleteProperty(props, 'showMobilePushModal');
Reflect.deleteProperty(props, 'showMobilePushStatusModal');
this.props.onBack({
...props,
user_id: this.props.currentUser.id
});
};
}

View file

@ -10,33 +10,6 @@ import {
import FormattedText from 'app/components/formatted_text';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
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,
@ -87,4 +60,31 @@ section.propTypes = {
theme: PropTypes.object.isRequired
};
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
marginBottom: 30
},
header: {
marginHorizontal: 15,
marginBottom: 10,
fontSize: 13,
color: changeOpacity(theme.centerChannelColor, 0.5)
},
items: {
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderBottomWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1)
},
footer: {
marginHorizontal: 15,
marginTop: 10,
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.5)
}
};
});
export default section;

View file

@ -8,46 +8,27 @@ import {
TouchableWithoutFeedback,
View
} from 'react-native';
import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
import FormattedText from 'app/components/formatted_text';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import CheckMark from 'app/components/checkmark';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const ActionTypes = {
ARROW: 'arrow',
DEFAULT: 'default',
TOGGLE: 'toggle',
SELECT: 'select'
};
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
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,
label,
theme,
selected
selected,
description
} = props;
const style = getStyleSheet(theme);
@ -68,24 +49,39 @@ function sectionItem(props) {
value={selected}
/>
);
} else if (actionType === ActionTypes.ARROW) {
actionComponent = (
<FontAwesomeIcon
name='angle-right'
style={style.arrow}
/>
);
}
const labelComponent = React.cloneElement(
label,
{style: style.label}
);
let descriptionComponent;
if (description) {
descriptionComponent = React.cloneElement(
description,
{style: style.description}
);
}
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 style={style.container}>
<View style={description ? style.doubleContainer : style.singleContainer}>
{labelComponent}
{descriptionComponent}
</View>
{children}
{actionComponent}
</View>
);
if (actionType === ActionTypes.DEFAULT || actionType === ActionTypes.SELECT) {
if (actionType === ActionTypes.DEFAULT || actionType === ActionTypes.SELECT || actionType === ActionTypes.ARROW) {
return (
<TouchableWithoutFeedback onPress={() => action(actionValue)}>
{component}
@ -98,14 +94,12 @@ function sectionItem(props) {
sectionItem.propTypes = {
action: PropTypes.func,
actionType: PropTypes.oneOf([ActionTypes.DEFAULT, ActionTypes.TOGGLE, ActionTypes.SELECT]),
actionType: PropTypes.oneOf([ActionTypes.ARROW, ActionTypes.DEFAULT, ActionTypes.TOGGLE, ActionTypes.SELECT]),
actionValue: PropTypes.string,
children: PropTypes.node,
labelDefaultMessage: PropTypes.string.isRequired,
labelId: PropTypes.string.isRequired,
labelValues: PropTypes.object,
label: PropTypes.node.isRequired,
selected: PropTypes.bool,
theme: PropTypes.object.isRequired
theme: PropTypes.object.isRequired,
description: PropTypes.node
};
sectionItem.defaultProps = {
@ -113,4 +107,39 @@ sectionItem.defaultProps = {
actionType: ActionTypes.DEFAULT
};
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 15
},
singleContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
height: 45
},
doubleContainer: {
flex: 1,
flexDirection: 'column',
height: 69,
justifyContent: 'center'
},
label: {
color: theme.centerChannelColor,
fontSize: 15
},
description: {
color: changeOpacity(theme.centerChannelColor, 0.6),
fontSize: 14,
marginTop: 3
},
arrow: {
color: changeOpacity(theme.centerChannelColor, 0.25),
fontSize: 24
}
};
});
export default sectionItem;

View file

@ -1,143 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {TouchableOpacity, View} from 'react-native';
import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
import IonIcon from 'react-native-vector-icons/Ionicons';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FormattedText from 'app/components/formatted_text';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class SettingsItem extends PureComponent {
static propTypes = {
defaultMessage: PropTypes.string.isRequired,
i18nId: PropTypes.string.isRequired,
iconName: PropTypes.string.isRequired,
iconType: PropTypes.oneOf(['fontawesome', 'ion', 'material']).isRequired,
isDestructor: PropTypes.bool,
onPress: PropTypes.func,
separator: PropTypes.bool,
theme: PropTypes.object.isRequired
};
static defaultProps = {
isDestructor: false,
separator: true
};
render() {
const {
defaultMessage,
i18nId,
iconName,
iconType,
isDestructor,
onPress,
separator,
theme
} = this.props;
const style = getStyleSheet(theme);
const destructor = {};
if (isDestructor) {
destructor.color = theme.errorTextColor;
}
let divider;
if (separator) {
divider = (<View style={style.divider}/>);
}
let icon;
switch (iconType) {
case 'fontawesome':
icon = (
<FontAwesomeIcon
name={iconName}
style={[style.icon, destructor]}
/>
);
break;
case 'ion':
icon = (
<IonIcon
name={iconName}
style={[style.icon, destructor]}
/>
);
break;
case 'material':
icon = (
<MaterialIcon
name={iconName}
style={[style.icon, destructor]}
/>
);
break;
}
return (
<View style={style.container}>
<TouchableOpacity
onPress={onPress}
style={{flex: 1}}
>
<View style={style.wrapper}>
<View style={style.iconContainer}>
{icon}
</View>
<FormattedText
id={i18nId}
defaultMessage={defaultMessage}
style={[style.label, destructor]}
/>
</View>
</TouchableOpacity>
{divider}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
backgroundColor: theme.centerChannelBg,
height: 51,
flexDirection: 'column',
justifyContent: 'center'
},
wrapper: {
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
flexDirection: 'row',
height: 50,
paddingLeft: 16
},
iconContainer: {
width: 18,
marginRight: 15,
alignItems: 'center',
justifyContent: 'center'
},
icon: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 20
},
label: {
color: theme.centerChannelColor,
fontSize: 16,
lineHeight: 19,
marginLeft: 8
},
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 1,
marginHorizontal: 16
}
};
});

View file

@ -0,0 +1,101 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {TouchableOpacity, View} from 'react-native';
import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
import FormattedText from 'app/components/formatted_text';
import SettingItemIcon from './setting_item_icon';
import getStyleSheet from './style';
export default class SettingsItem extends PureComponent {
static propTypes = {
defaultMessage: PropTypes.string.isRequired,
i18nId: PropTypes.string.isRequired,
iconName: PropTypes.string,
iconType: PropTypes.oneOf(['fontawesome', 'foundation', 'ion', 'material']),
isDestructor: PropTypes.bool,
centered: PropTypes.bool,
onPress: PropTypes.func,
separator: PropTypes.bool,
showArrow: PropTypes.bool,
theme: PropTypes.object.isRequired
};
static defaultProps = {
isDestructor: false,
separator: true
};
render() {
const {
centered,
defaultMessage,
i18nId,
iconName,
iconType,
isDestructor,
onPress,
separator,
showArrow,
theme
} = this.props;
const style = getStyleSheet(theme);
const destructor = {};
if (isDestructor) {
destructor.color = theme.errorTextColor;
}
let divider;
if (separator) {
divider = (<View style={style.divider}/>);
}
let icon;
if (iconType && iconName) {
icon = (
<SettingItemIcon
name={iconName}
type={iconType}
style={[style.icon, destructor]}
/>
);
}
return (
<TouchableOpacity
onPress={onPress}
>
<View style={style.container}>
{icon &&
<View style={style.iconContainer}>
{icon}
</View>
}
<View style={style.wrapper}>
<View style={style.labelContainer}>
<FormattedText
id={i18nId}
defaultMessage={defaultMessage}
style={[style.label, destructor, centered ? style.centerLabel : {}]}
/>
{showArrow &&
<View style={style.arrowContainer}>
<FontAwesomeIcon
name='angle-right'
style={style.arrow}
/>
</View>
}
</View>
{divider}
</View>
</View>
</TouchableOpacity>
);
}
}

View file

@ -0,0 +1,52 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import IonIcon from 'react-native-vector-icons/Ionicons';
import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FoundationIcon from 'react-native-vector-icons/Foundation';
export default function settingsItemIcon(props) {
const {name, type, style} = props;
switch (type) {
case 'fontawesome':
return (
<FontAwesomeIcon
name={name}
style={style}
/>
);
case 'foundation':
return (
<FoundationIcon
name={name}
style={style}
/>
);
case 'ion':
return (
<IonIcon
name={name}
style={style}
/>
);
case 'material':
return (
<MaterialIcon
name={name}
style={style}
/>
);
}
return null;
}
settingsItemIcon.propTypes = {
name: PropTypes.string,
type: PropTypes.string,
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array])
};

View file

@ -0,0 +1,49 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default makeStyleSheetFromTheme((theme) => {
return {
container: {
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
flexDirection: 'row',
height: 68
},
iconContainer: {
width: 42,
height: 68,
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 15
},
icon: {
color: theme.buttonBg,
fontSize: 25
},
wrapper: {
flex: 1
},
centerLabel: {
textAlign: 'center',
textAlignVertical: 'center'
},
labelContainer: {
flex: 1,
flexDirection: 'row'
},
label: {
color: theme.centerChannelColor,
flex: 1,
fontSize: 17,
textAlignVertical: 'center',
includeFontPadding: false,
paddingRight: 15
},
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 1
}
};
});

View file

@ -0,0 +1,58 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default makeStyleSheetFromTheme((theme) => {
return {
container: {
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
flexDirection: 'row',
height: 45
},
iconContainer: {
width: 29,
height: 29,
backgroundColor: theme.buttonBg,
borderRadius: 7,
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 15
},
icon: {
color: theme.buttonColor,
fontSize: 18,
marginTop: 2
},
wrapper: {
flex: 1
},
centerLabel: {
textAlign: 'center',
textAlignVertical: 'center'
},
labelContainer: {
flex: 1,
flexDirection: 'row'
},
label: {
color: theme.centerChannelColor,
flex: 1,
fontSize: 17,
lineHeight: 43
},
arrowContainer: {
justifyContent: 'center',
paddingRight: 15
},
arrow: {
color: changeOpacity(theme.centerChannelColor, 0.25),
fontSize: 18
},
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 1
}
};
});

View file

@ -1802,7 +1802,7 @@
"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.reply.header": "SEND REPLY NOTIFICATIONS FOR",
"mobile.account_notifications.threads_mentions": "Mentions in threads",
"mobile.account_notifications.threads_start": "Threads that I start",
"mobile.account_notifications.threads_start_participate": "Threads that I start or participate in",
@ -1884,6 +1884,25 @@
"mobile.notice_platform_link": "platform",
"mobile.notice_text": "Mattermost is made possible by the open source software used in our {platform} and {mobile}.",
"mobile.notification.in": " in ",
"mobile.notification_settings.email": "Email",
"mobile.notification_settings.email_title": "Email Notifications",
"mobile.notification_settings.mobile": "Mobile",
"mobile.notification_settings.mobile_title": "Mobile Notifications",
"mobile.notification_settings.save_failed_description": "The notification settings failed to save due to a connection issue, please try again.",
"mobile.notification_settings.save_failed_title": "Connection issue",
"mobile.notification_settings.mentions.reply_title": "Send Reply notifications for",
"mobile.notification_settings.mentions_replies": "Mentions and Replies",
"mobile.notification_settings.mentions.channelWide": "Channel-wide mentions",
"mobile.notification_settings_mentions.keywords": "Keywords",
"mobile.notification_settings_mentions.keywordsDescription": "Other words that trigger a mention",
"mobile.notification_settings_mentions.keywordsHelp": "Keywords are non-case sensitive and should be separated by a comma.",
"mobile.notification_settings.mentions.sensitiveName": "Your case sensitive first name",
"mobile.notification_settings.mentions.sensitiveUsername": "Your non-case sensitive username",
"mobile.notification_settings_mentions.wordsTrigger": "WORDS THAT TRIGGER MENTIONS",
"mobile.notification_settings_mobile.push_activity": "SEND NOTIFICATIONS FOR",
"mobile.notification_settings_mobile.push_activity_android": "Send notifications for",
"mobile.notification_settings_model.push_status": "TRIGGER PUSH NOTIFICATIONS WHEN",
"mobile.notification_settings_model.push_status_android": "Trigger push notifications when",
"mobile.offlineIndicator.connected": "Connected",
"mobile.offlineIndicator.connecting": "Connecting...",
"mobile.offlineIndicator.offline": "No internet connection",