diff --git a/app/actions/views/account_notifications.js b/app/actions/views/account_notifications.js index 6b5fae485..c7fc7af9e 100644 --- a/app/actions/views/account_notifications.js +++ b/app/actions/views/account_notifications.js @@ -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 diff --git a/app/components/radio_button/index.js b/app/components/radio_button/index.js new file mode 100644 index 000000000..e27256510 --- /dev/null +++ b/app/components/radio_button/index.js @@ -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 +}; diff --git a/app/components/radio_button/radio_button.js b/app/components/radio_button/radio_button.js new file mode 100644 index 000000000..74f0ef043 --- /dev/null +++ b/app/components/radio_button/radio_button.js @@ -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 ( + + + + + + {this.props.label} + + + + ); + } +} + +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); diff --git a/app/components/radio_button/radio_button_group.js b/app/components/radio_button/radio_button_group.js new file mode 100644 index 000000000..001972d25 --- /dev/null +++ b/app/components/radio_button/radio_button_group.js @@ -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 ( + + ); + } + + return null; + }, this); + + return ( + + {options} + + ); + }; +} diff --git a/app/screens/about/about.js b/app/screens/about/about.js index 2a32c8f94..87a255806 100644 --- a/app/screens/about/about.js +++ b/app/screens/about/about.js @@ -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 diff --git a/app/screens/account_notifications/account_notifications.js b/app/screens/account_notifications/account_notifications.js deleted file mode 100644 index 8c6dff3b0..000000000 --- a/app/screens/account_notifications/account_notifications.js +++ /dev/null @@ -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 ( -
- {currentUser.first_name.length > 0 && - - - - - } - - - - - - - -
- ); - }; - - buildEmailSection = () => { - const {config, theme} = this.props; - const style = getStyleSheet(theme); - - const sendEmailNotifications = config.SendEmailNotifications === 'true'; - const emailBatchingEnabled = config.EnableEmailBatching === 'true'; - - let sendImmediatley = this.state.email === 'true'; - let sendImmediatleyValue = 'true'; - let fifteenMinutes; - let hourly; - const never = this.state.email === 'false'; - - if (emailBatchingEnabled && this.state.email !== 'false') { - sendImmediatley = this.state.interval === Preferences.INTERVAL_IMMEDIATE.toString(); - fifteenMinutes = this.state.interval === Preferences.INTERVAL_FIFTEEN_MINUTES.toString(); - hourly = this.state.interval === Preferences.INTERVAL_HOUR.toString(); - - sendImmediatleyValue = Preferences.INTERVAL_IMMEDIATE.toString(); - } - - return ( -
- {sendEmailNotifications && - - - - {emailBatchingEnabled && - - - - - - - } - - - } - {!sendEmailNotifications && - - } -
- ); - }; - - buildReplySection = () => { - const {theme} = this.props; - const style = getStyleSheet(theme); - - return ( -
- - - - - -
- ); - }; - - buildMobilePushSection = () => { - const {config, theme} = this.props; - const style = getStyleSheet(theme); - - const pushNotificationsEnabled = config.SendPushNotifications === 'true'; - if (!pushNotificationsEnabled) { - return null; - } - - return ( -
- - - - - -
- ); - }; - - buildMobilePushStatusSection = () => { - const {config, theme} = this.props; - const style = getStyleSheet(theme); - - const showSection = config.SendPushNotifications === 'true' && this.state.push !== 'none'; - if (!showSection) { - return null; - } - - return ( -
- - - - - -
- ); - }; - - close = () => { - this.props.navigator.pop({animated: true}); - }; - - render() { - const {theme} = this.props; - const style = getStyleSheet(theme); - - if (this.state.saving) { - return ( - - - - - ); - } - - return ( - - - - {this.buildMentionSection()} - {this.buildEmailSection()} - {this.buildReplySection()} - {this.buildMobilePushSection()} - {this.buildMobilePushStatusSection()} - - - ); - } -} - -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); diff --git a/app/screens/account_settings/account_settings.js b/app/screens/account_settings/account_settings.js deleted file mode 100644 index fb9767165..000000000 --- a/app/screens/account_settings/account_settings.js +++ /dev/null @@ -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 ( - - preventDoubleTap(action)} - > - - - - - - - {separator && } - - ); - }; - - 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 ( - - - - - {this.renderItems()} - - - - ); - } -} - -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); diff --git a/app/screens/advanced_settings/advanced_settings.js b/app/screens/advanced_settings/advanced_settings.js deleted file mode 100644 index e8d236a68..000000000 --- a/app/screens/advanced_settings/advanced_settings.js +++ /dev/null @@ -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 ( - - this.handlePress(action)} - > - - - - - {nextArrow && - - } - - {separator && } - - ); - }; - - 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 ( - - - - - {this.renderItems()} - - - - ); - } -} - -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); diff --git a/app/screens/index.js b/app/screens/index.js index 574c739a2..325d8a446 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -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); diff --git a/app/screens/settings/advanced_settings/advanced_settings.js b/app/screens/settings/advanced_settings/advanced_settings.js new file mode 100644 index 000000000..5764e12f2 --- /dev/null +++ b/app/screens/settings/advanced_settings/advanced_settings.js @@ -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 ( + + + + + this.handlePress(this.clearOfflineCache)} + separator={false} + showArrow={false} + theme={theme} + /> + + + + ); + } +} + +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); diff --git a/app/screens/advanced_settings/index.js b/app/screens/settings/advanced_settings/index.js similarity index 100% rename from app/screens/advanced_settings/index.js rename to app/screens/settings/advanced_settings/index.js diff --git a/app/screens/settings/index.js b/app/screens/settings/general/index.js similarity index 100% rename from app/screens/settings/index.js rename to app/screens/settings/general/index.js diff --git a/app/screens/settings/settings.js b/app/screens/settings/general/settings.js similarity index 81% rename from app/screens/settings/settings.js rename to app/screens/settings/general/settings.js index 0242339b2..de18ad725 100644 --- a/app/screens/settings/settings.js +++ b/app/screens/settings/general/settings.js @@ -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 ( + 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 && this.handlePress(this.goToSelectTeam)} - separator={true} + showArrow={showArrow} theme={theme} /> } @@ -203,54 +204,56 @@ class Settings extends PureComponent { preventDoubleTap(this.openHelp, this)} - separator={true} + iconName='md-help' + iconType='ion' + onPress={() => this.handlePress(this.openHelp)} + showArrow={showArrow} theme={theme} /> } this.handlePress(this.openErrorEmail)} - separator={true} + showArrow={showArrow} theme={theme} /> this.handlePress(this.goToAdvancedSettings)} - separator={true} + showArrow={showArrow} theme={theme} /> this.handlePress(this.goToAbout)} separator={false} + showArrow={showArrow} theme={theme} /> - - - - this.handlePress(this.logout)} - separator={false} - theme={theme} - /> + + + this.handlePress(this.logout)} + separator={false} + showArrow={false} + theme={theme} + /> + + ); @@ -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 } }; }); diff --git a/app/screens/account_notifications/index.js b/app/screens/settings/notification_settings/index.js similarity index 75% rename from app/screens/account_notifications/index.js rename to app/screens/settings/notification_settings/index.js index 0b6e58fc5..8b7066008 100644 --- a/app/screens/account_notifications/index.js +++ b/app/screens/settings/notification_settings/index.js @@ -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); diff --git a/app/screens/settings/notification_settings/notification_settings.js b/app/screens/settings/notification_settings/notification_settings.js new file mode 100644 index 000000000..d8703d82f --- /dev/null +++ b/app/screens/settings/notification_settings/notification_settings.js @@ -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 = ( + + ); + } + + return ( + this.setState({showEmailNotificationsModal: false})} + > + + + + + + + {!sendEmailNotifications && + + } + {sendEmailNotifications && + + + {emailBatchingEnabled && + + } + {emailBatchingEnabled && + + } + + + } + {helpText} + + + + + + + + {sendEmailNotifications && + + + + + + + } + + + + + + ); + }; + + render() { + const {theme} = this.props; + const style = getStyleSheet(theme); + const showArrow = Platform.OS === 'ios'; + + return ( + + + + + this.handlePress(this.goToNotificationSettingsMentions)} + separator={true} + showArrow={showArrow} + theme={theme} + /> + this.handlePress(this.goToNotificationSettingsMobile)} + separator={true} + showArrow={showArrow} + theme={theme} + /> + this.handlePress(this.goToNotificationSettingsEmail)} + separator={false} + showArrow={showArrow} + theme={theme} + /> + + + {this.renderEmailNotificationSettings(style)} + + ); + } +} + +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); diff --git a/app/screens/settings/notification_settings_email/index.js b/app/screens/settings/notification_settings_email/index.js new file mode 100644 index 000000000..0b490e3eb --- /dev/null +++ b/app/screens/settings/notification_settings_email/index.js @@ -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); diff --git a/app/screens/settings/notification_settings_email/notification_settings_email.js b/app/screens/settings/notification_settings_email/notification_settings_email.js new file mode 100644 index 000000000..512ac27de --- /dev/null +++ b/app/screens/settings/notification_settings_email/notification_settings_email.js @@ -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 ( +
+ {sendEmailNotifications && + + + )} + action={this.setEmailNotifications} + actionType='select' + actionValue={sendImmediatleyValue} + selected={sendImmediatley} + theme={theme} + /> + + {emailBatchingEnabled && + + + )} + action={this.setEmailNotifications} + actionType='select' + actionValue={Preferences.INTERVAL_FIFTEEN_MINUTES.toString()} + selected={fifteenMinutes} + theme={theme} + /> + + + )} + action={this.setEmailNotifications} + actionType='select' + actionValue={Preferences.INTERVAL_HOUR.toString()} + selected={hourly} + theme={theme} + /> + + + } + + )} + action={this.setEmailNotifications} + actionType='select' + actionValue='false' + selected={never} + theme={theme} + /> + + } + {!sendEmailNotifications && + + } +
+ ); + }; + + render() { + const {theme} = this.props; + const style = getStyleSheet(theme); + + return ( + + + + {this.renderEmailSection()} + + + ); + } +} + +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 + } + }; +}); diff --git a/app/screens/account_settings/index.js b/app/screens/settings/notification_settings_mentions/index.js similarity index 54% rename from app/screens/account_settings/index.js rename to app/screens/settings/notification_settings_mentions/index.js index adf8be570..e823f0239 100644 --- a/app/screens/account_settings/index.js +++ b/app/screens/settings/notification_settings_mentions/index.js @@ -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); diff --git a/app/screens/settings/notification_settings_mentions/notification_settings_mention_base.js b/app/screens/settings/notification_settings_mentions/notification_settings_mention_base.js new file mode 100644 index 000000000..5dbd99385 --- /dev/null +++ b/app/screens/settings/notification_settings_mentions/notification_settings_mention_base.js @@ -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 + }); + }; +} diff --git a/app/screens/settings/notification_settings_mentions/notification_settings_mentions.android.js b/app/screens/settings/notification_settings_mentions/notification_settings_mentions.android.js new file mode 100644 index 000000000..6e2a5ec5e --- /dev/null +++ b/app/screens/settings/notification_settings_mentions/notification_settings_mentions.android.js @@ -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 ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + + renderReplyModal(style) { + const {intl} = this.props; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + + 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 ( + + )} + label={( + + )} + 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 = ({this.state.mention_keys}); + } else { + mentionKeysComponent = ( + + ); + } + + return ( + + + + {currentUser.first_name.length > 0 && + + + {currentUser.first_name} + + )} + description={( + + )} + action={this.toggleFirstNameMention} + actionType='toggle' + selected={this.state.first_name === 'true'} + theme={theme} + /> + + + } + + {currentUser.username} + + )} + description={( + + )} + selected={this.state.usernameMention} + action={this.toggleUsernameMention} + actionType='toggle' + theme={theme} + /> + + + {'@channel, @all, @here'} + + )} + description={( + + )} + action={this.toggleChannelMentions} + actionType='toggle' + selected={this.state.channel === 'true'} + theme={theme} + /> + + + )} + description={mentionKeysComponent} + action={this.showKeywordsModal} + actionType='default' + theme={theme} + /> + + {this.renderReplySection()} + + + {this.renderKeywordsModal(style)} + {this.renderReplyModal(style)} + + ); + } +} + +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); diff --git a/app/screens/settings/notification_settings_mentions/notification_settings_mentions.ios.js b/app/screens/settings/notification_settings_mentions/notification_settings_mentions.ios.js new file mode 100644 index 000000000..05f2c2304 --- /dev/null +++ b/app/screens/settings/notification_settings_mentions/notification_settings_mentions.ios.js @@ -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 = ({this.state.mention_keys}); + } else { + mentionKeysComponent = ( + + ); + } + + return ( +
+ {currentUser.first_name.length > 0 && + + + {currentUser.first_name} + + )} + description={( + + )} + action={this.toggleFirstNameMention} + actionType='toggle' + selected={this.state.first_name === 'true'} + theme={theme} + /> + + + } + + {currentUser.username} + + )} + description={( + + )} + selected={this.state.usernameMention} + action={this.toggleUsernameMention} + actionType='toggle' + theme={theme} + /> + + + {'@channel, @all, @here'} + + )} + description={( + + )} + action={this.toggleChannelMentions} + actionType='toggle' + selected={this.state.channel === 'true'} + theme={theme} + /> + + + )} + description={mentionKeysComponent} + action={this.goToNotificationSettingsMentionKeywords} + actionType='arrow' + theme={theme} + /> +
+ ); + } + + renderReplySection(style) { + const {theme} = this.props; + + return ( +
+ + )} + action={this.setReplyNotifications} + actionType='select' + actionValue='any' + selected={this.state.comments === 'any'} + theme={theme} + /> + + + )} + action={this.setReplyNotifications} + actionType='select' + actionValue='root' + selected={this.state.comments === 'root'} + theme={theme} + /> + + + )} + action={this.setReplyNotifications} + actionType='select' + actionValue='never' + selected={this.state.comments === 'never'} + theme={theme} + /> +
+ ); + } + + render() { + const {theme} = this.props; + const style = getStyleSheet(theme); + + return ( + + + + {this.renderMentionSection(style)} + {this.renderReplySection(style)} + + + ); + } +} + +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); diff --git a/app/screens/settings/notification_settings_mentions_keywords/index.js b/app/screens/settings/notification_settings_mentions_keywords/index.js new file mode 100644 index 000000000..7fe120834 --- /dev/null +++ b/app/screens/settings/notification_settings_mentions_keywords/index.js @@ -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); diff --git a/app/screens/settings/notification_settings_mentions_keywords/notification_settings_mentions_keywords.js b/app/screens/settings/notification_settings_mentions_keywords/notification_settings_mentions_keywords.js new file mode 100644 index 000000000..4833ba160 --- /dev/null +++ b/app/screens/settings/notification_settings_mentions_keywords/notification_settings_mentions_keywords.js @@ -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 ( + + + + + + + + + + + + ); + } +} + +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 + } + }; +}); diff --git a/app/screens/settings/notification_settings_mobile/index.js b/app/screens/settings/notification_settings_mobile/index.js new file mode 100644 index 000000000..9b57e5c38 --- /dev/null +++ b/app/screens/settings/notification_settings_mobile/index.js @@ -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); diff --git a/app/screens/settings/notification_settings_mobile/notification_settings_mobile.android.js b/app/screens/settings/notification_settings_mobile/notification_settings_mobile.android.js new file mode 100644 index 000000000..3bcf6e4bb --- /dev/null +++ b/app/screens/settings/notification_settings_mobile/notification_settings_mobile.android.js @@ -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 ( + + + + + + + + {pushNotificationsEnabled && + + + + + + } + {!pushNotificationsEnabled && + + } + + + + + + + + {pushNotificationsEnabled && + + + + + + + } + + + + + + ); + } + + renderMobilePushStatusModal(style) { + const {intl} = this.props; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + + 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 = ( + + ); + } else { + props.description = ( + + ); + } + + return ( + + )} + 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 ( + + + )} + label={( + + )} + action={this.showMobilePushStatusModal} + actionType='default' + theme={theme} + /> + + + ); + } + + 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 ( + + + + {this.renderMobilePushSection()} + + {this.renderMobilePushStatusSection(style)} + + {this.renderMobilePushModal(style)} + {this.renderMobilePushStatusModal(style)} + + ); + } +} + +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); diff --git a/app/screens/settings/notification_settings_mobile/notification_settings_mobile.ios.js b/app/screens/settings/notification_settings_mobile/notification_settings_mobile.ios.js new file mode 100644 index 000000000..b3fa71f28 --- /dev/null +++ b/app/screens/settings/notification_settings_mobile/notification_settings_mobile.ios.js @@ -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 ( +
+ {pushNotificationsEnabled && + + + )} + action={this.setMobilePush} + actionType='select' + actionValue='all' + selected={this.state.push === 'all'} + theme={theme} + /> + + + )} + action={this.setMobilePush} + actionType='select' + actionValue='mention' + selected={this.state.push === 'mention'} + theme={theme} + /> + + + )} + action={this.setMobilePush} + actionType='select' + actionValue='none' + selected={this.state.push === 'none'} + theme={theme} + /> + + } + {!pushNotificationsEnabled && + + } +
+ ); + } + + renderMobilePushStatusSection(style) { + const {config, theme} = this.props; + + const showSection = config.SendPushNotifications === 'true' && this.state.push !== 'none'; + if (!showSection) { + return null; + } + + return ( +
+ + )} + action={this.setMobilePushStatus} + actionType='select' + actionValue='online' + selected={this.state.push_status === 'online'} + theme={theme} + /> + + + )} + action={this.setMobilePushStatus} + actionType='select' + actionValue='away' + selected={this.state.push_status === 'away'} + theme={theme} + /> + + + )} + action={this.setMobilePushStatus} + actionType='select' + actionValue='offline' + selected={this.state.push_status === 'offline'} + theme={theme} + /> +
+ ); + } + + render() { + const {theme} = this.props; + const style = getStyleSheet(theme); + + return ( + + + + {this.renderMobilePushSection(style)} + {this.renderMobilePushStatusSection(style)} + + + ); + } +} + +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); diff --git a/app/screens/settings/notification_settings_mobile/notification_settings_mobile_base.js b/app/screens/settings/notification_settings_mobile/notification_settings_mobile_base.js new file mode 100644 index 000000000..71cdf51b5 --- /dev/null +++ b/app/screens/settings/notification_settings_mobile/notification_settings_mobile_base.js @@ -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 + }); + }; +} diff --git a/app/screens/account_notifications/section.js b/app/screens/settings/section.js similarity index 96% rename from app/screens/account_notifications/section.js rename to app/screens/settings/section.js index 05308b728..dcb5d36f0 100644 --- a/app/screens/account_notifications/section.js +++ b/app/screens/settings/section.js @@ -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; diff --git a/app/screens/account_notifications/section_item.js b/app/screens/settings/section_item.js similarity index 51% rename from app/screens/account_notifications/section_item.js rename to app/screens/settings/section_item.js index 0159f4cf0..960564c13 100644 --- a/app/screens/account_notifications/section_item.js +++ b/app/screens/settings/section_item.js @@ -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 = ( + + ); + } + + const labelComponent = React.cloneElement( + label, + {style: style.label} + ); + + let descriptionComponent; + if (description) { + descriptionComponent = React.cloneElement( + description, + {style: style.description} + ); } const component = ( - - - - {actionComponent} + + + {labelComponent} + {descriptionComponent} - {children} + {actionComponent} ); - if (actionType === ActionTypes.DEFAULT || actionType === ActionTypes.SELECT) { + if (actionType === ActionTypes.DEFAULT || actionType === ActionTypes.SELECT || actionType === ActionTypes.ARROW) { return ( 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; diff --git a/app/screens/settings/settings_item.js b/app/screens/settings/settings_item.js deleted file mode 100644 index 7484688cc..000000000 --- a/app/screens/settings/settings_item.js +++ /dev/null @@ -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 = (); - } - - let icon; - switch (iconType) { - case 'fontawesome': - icon = ( - - ); - break; - case 'ion': - icon = ( - - ); - break; - case 'material': - icon = ( - - ); - break; - } - - return ( - - - - - {icon} - - - - - {divider} - - ); - } -} - -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 - } - }; -}); diff --git a/app/screens/settings/settings_item/index.js b/app/screens/settings/settings_item/index.js new file mode 100644 index 000000000..a58d821b4 --- /dev/null +++ b/app/screens/settings/settings_item/index.js @@ -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 = (); + } + + let icon; + if (iconType && iconName) { + icon = ( + + ); + } + + return ( + + + {icon && + + {icon} + + } + + + + {showArrow && + + + + } + + {divider} + + + + ); + } +} diff --git a/app/screens/settings/settings_item/setting_item_icon.js b/app/screens/settings/settings_item/setting_item_icon.js new file mode 100644 index 000000000..1492d2ea4 --- /dev/null +++ b/app/screens/settings/settings_item/setting_item_icon.js @@ -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 ( + + ); + case 'foundation': + return ( + + ); + case 'ion': + return ( + + ); + case 'material': + return ( + + ); + } + + return null; +} + +settingsItemIcon.propTypes = { + name: PropTypes.string, + type: PropTypes.string, + style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]) +}; diff --git a/app/screens/settings/settings_item/style.android.js b/app/screens/settings/settings_item/style.android.js new file mode 100644 index 000000000..17a532681 --- /dev/null +++ b/app/screens/settings/settings_item/style.android.js @@ -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 + } + }; +}); diff --git a/app/screens/settings/settings_item/style.ios.js b/app/screens/settings/settings_item/style.ios.js new file mode 100644 index 000000000..6702342a7 --- /dev/null +++ b/app/screens/settings/settings_item/style.ios.js @@ -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 + } + }; +}); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index f6a2fd001..21380fe11 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -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",