diff --git a/app/containers/HeaderButton.js b/app/containers/HeaderButton.js index cd1808d2f..4dc72f8cc 100644 --- a/app/containers/HeaderButton.js +++ b/app/containers/HeaderButton.js @@ -61,6 +61,12 @@ export const SaveButton = React.memo(({ onPress, testID, ...props }) => ( )); +export const PreferencesButton = React.memo(({ onPress, testID, ...props }) => ( + + + +)); + export const LegalButton = React.memo(({ navigation, testID }) => ( navigation.navigate('LegalView')} testID={testID} /> )); @@ -89,6 +95,10 @@ SaveButton.propTypes = { onPress: PropTypes.func.isRequired, testID: PropTypes.string.isRequired }; +PreferencesButton.propTypes = { + onPress: PropTypes.func.isRequired, + testID: PropTypes.string.isRequired +}; LegalButton.propTypes = { navigation: PropTypes.object.isRequired, testID: PropTypes.string.isRequired diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index 72ebbdb4e..061a6228d 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -181,6 +181,8 @@ export default { description: 'description', Description: 'Description', DESKTOP_OPTIONS: 'DESKTOP OPTIONS', + DESKTOP_NOTIFICATIONS: 'DESKTOP NOTIFICATIONS', + Desktop_Alert_info: 'These notifications are delivered in desktop', Directory: 'Directory', Direct_Messages: 'Direct Messages', Disable_notifications: 'Disable notifications', @@ -197,6 +199,8 @@ export default { Edit: 'Edit', Edit_Status: 'Edit Status', Edit_Invite: 'Edit Invite', + Email_Notification_Mode_All: 'Every Mention/DM', + Email_Notification_Mode_Disabled: 'Disabled', Email_or_password_field_is_empty: 'Email or password field is empty', Email: 'Email', EMAIL: 'EMAIL', @@ -567,6 +571,7 @@ export default { You: 'You', Logged_out_by_server: 'You\'ve been logged out by the server. Please log in again.', You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'You need to access at least one Rocket.Chat server to share something.', + You_need_to_verifiy_your_email_address_to_get_notications: 'You need to verify your email address to get notifications', Your_certificate: 'Your Certificate', Your_message: 'Your message', Your_invite_link_will_expire_after__usesLeft__uses: 'Your invite link will expire after {{usesLeft}} uses.', diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index 8ed4e76da..84941b09d 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -175,6 +175,8 @@ export default { deleting_room: 'excluindo sala', Direct_Messages: 'Mensagens Diretas', DESKTOP_OPTIONS: 'OPÇÕES DE ÁREA DE TRABALHO', + DESKTOP_NOTIFICATIONS: 'NOTIFICAÇÕES DE ÁREA DE TRABALHO', + Desktop_Alert_info: 'Essas notificações são entregues a você na área de trabalho', Directory: 'Diretório', description: 'descrição', Description: 'Descrição', @@ -195,6 +197,8 @@ export default { Email: 'Email', email: 'e-mail', Empty_title: 'Título vazio', + Email_Notification_Mode_All: 'Cada Menção / Mensagem Direta', + Email_Notification_Mode_Disabled: 'Desativado', Enable_Auto_Translate: 'Ativar a tradução automática', Enable_notifications: 'Habilitar notificações', Everyone_can_access_this_channel: 'Todos podem acessar este canal', @@ -508,6 +512,7 @@ export default { You_are_offline: 'Você está offline', You_can_search_using_RegExp_eg: 'Você pode usar expressões regulares, por exemplo `/^text$/i`', Your_message: 'Sua mensagem', + You_need_to_verifiy_your_email_address_to_get_notications: 'Você precisa confirmar seu endereço de e-mail para obter notificações', You_colon: 'Você: ', you_were_mentioned: 'você foi mencionado', You_were_removed_from_channel: 'Você foi removido de {{channel}}', diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 0d6af7258..3ac30ebf6 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -726,6 +726,10 @@ const RocketChat = { setUserPresenceOnline() { return this.methodCall('UserPresence:online'); }, + setUserPreferences(userId, data) { + // RC 0.62.0 + return this.sdk.post('users.setPreferences', { userId, data }); + }, setUserStatus(status, message) { // RC 1.2.0 return this.post('users.setStatus', { status, message }); @@ -780,6 +784,10 @@ const RocketChat = { // RC 0.48.0 return this.sdk.get('users.info', { userId }); }, + getUserPreferences(userId) { + // RC 0.62.0 + return this.sdk.get('users.getPreferences', { userId }); + }, getRoomInfo(roomId) { // RC 0.72.0 return this.sdk.get('rooms.info', { roomId }); diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.js index e9a4a7a74..e9f2f1aeb 100644 --- a/app/stacks/InsideStack.js +++ b/app/stacks/InsideStack.js @@ -41,6 +41,8 @@ import LanguageView from '../views/LanguageView'; import ThemeView from '../views/ThemeView'; import DefaultBrowserView from '../views/DefaultBrowserView'; import ScreenLockConfigView from '../views/ScreenLockConfigView'; +import PreferencesView from '../views/PreferencesView'; +import UserNotificationPrefView from '../views/UserNotificationPreferencesView'; // Admin Stack import AdminPanelView from '../views/AdminPanelView'; @@ -220,6 +222,21 @@ const SettingsStackNavigator = () => { component={ScreenLockConfigView} options={ScreenLockConfigView.navigationOptions} /> + + + ); }; diff --git a/app/stacks/MasterDetailStack/index.js b/app/stacks/MasterDetailStack/index.js index 800bad1d7..f3389b67b 100644 --- a/app/stacks/MasterDetailStack/index.js +++ b/app/stacks/MasterDetailStack/index.js @@ -42,6 +42,8 @@ import AdminPanelView from '../../views/AdminPanelView'; import NewMessageView from '../../views/NewMessageView'; import CreateChannelView from '../../views/CreateChannelView'; import QueueListView from '../../views/QueueListView'; +import PreferencesView from '../../views/PreferencesView'; +import UserNotificationPrefView from '../../views/UserNotificationPreferencesView'; // InsideStackNavigator import AttachmentView from '../../views/AttachmentView'; @@ -253,6 +255,16 @@ const ModalStackNavigator = React.memo(({ navigation }) => { name='CreateDiscussionView' component={CreateDiscussionView} /> + + ); diff --git a/app/views/NotificationPreferencesView/Info.js b/app/views/NotificationPreferencesView/Info.js new file mode 100644 index 000000000..0c01e3c16 --- /dev/null +++ b/app/views/NotificationPreferencesView/Info.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { + Text +} from 'react-native'; +import PropTypes from 'prop-types'; + +import styles from './styles'; +import { themes } from '../../constants/colors'; + +const Info = React.memo(({ info, theme }) => ( + + {info} + +)); + +Info.propTypes = { + info: PropTypes.string, + theme: PropTypes.string +}; + +export default Info; diff --git a/app/views/NotificationPreferencesView/SectionSeparator.js b/app/views/NotificationPreferencesView/SectionSeparator.js new file mode 100644 index 000000000..88acd1a0e --- /dev/null +++ b/app/views/NotificationPreferencesView/SectionSeparator.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { + View +} from 'react-native'; +import PropTypes from 'prop-types'; + +import styles from './styles'; +import { themes } from '../../constants/colors'; + +const SectionSeparator = React.memo(({ theme }) => ( + +)); + +SectionSeparator.propTypes = { + theme: PropTypes.string +}; + +export default SectionSeparator; diff --git a/app/views/NotificationPreferencesView/SectionTitle.js b/app/views/NotificationPreferencesView/SectionTitle.js new file mode 100644 index 000000000..e93cb0e01 --- /dev/null +++ b/app/views/NotificationPreferencesView/SectionTitle.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { + Text +} from 'react-native'; +import PropTypes from 'prop-types'; + +import styles from './styles'; +import { themes } from '../../constants/colors'; + +const SectionTitle = React.memo(({ title, theme }) => ( + + {title} + +)); + +SectionTitle.propTypes = { + title: PropTypes.string, + theme: PropTypes.string +}; + +export default SectionTitle; diff --git a/app/views/NotificationPreferencesView/index.js b/app/views/NotificationPreferencesView/index.js index 4fad41839..1cbc23db3 100644 --- a/app/views/NotificationPreferencesView/index.js +++ b/app/views/NotificationPreferencesView/index.js @@ -17,126 +17,10 @@ import { withTheme } from '../../theme'; import protectedFunction from '../../lib/methods/helpers/protectedFunction'; import SafeAreaView from '../../containers/SafeAreaView'; import log, { events, logEvent } from '../../utils/log'; - -const SectionTitle = React.memo(({ title, theme }) => ( - - {title} - -)); - -const SectionSeparator = React.memo(({ theme }) => ( - -)); - -const Info = React.memo(({ info, theme }) => ( - - {info} - -)); - -SectionTitle.propTypes = { - title: PropTypes.string, - theme: PropTypes.string -}; - -SectionSeparator.propTypes = { - theme: PropTypes.string -}; - -Info.propTypes = { - info: PropTypes.string, - theme: PropTypes.string -}; - -const OPTIONS = { - desktopNotifications: [{ - label: 'Default', value: 'default' - }, { - label: 'All_Messages', value: 'all' - }, { - label: 'Mentions', value: 'mentions' - }, { - label: 'Nothing', value: 'nothing' - }], - audioNotifications: [{ - label: 'Default', value: 'default' - }, { - label: 'All_Messages', value: 'all' - }, { - label: 'Mentions', value: 'mentions' - }, { - label: 'Nothing', value: 'nothing' - }], - mobilePushNotifications: [{ - label: 'Default', value: 'default' - }, { - label: 'All_Messages', value: 'all' - }, { - label: 'Mentions', value: 'mentions' - }, { - label: 'Nothing', value: 'nothing' - }], - emailNotifications: [{ - label: 'Default', value: 'default' - }, { - label: 'All_Messages', value: 'all' - }, { - label: 'Mentions', value: 'mentions' - }, { - label: 'Nothing', value: 'nothing' - }], - desktopNotificationDuration: [{ - label: 'Default', value: 0 - }, { - label: 'Seconds', second: 1, value: 1 - }, { - label: 'Seconds', second: 2, value: 2 - }, { - label: 'Seconds', second: 3, value: 3 - }, { - label: 'Seconds', second: 4, value: 4 - }, { - label: 'Seconds', second: 5, value: 5 - }], - audioNotificationValue: [{ - label: 'None', value: 'none None' - }, { - label: 'Default', value: '0 Default' - }, { - label: 'Beep', value: 'beep Beep' - }, { - label: 'Ding', value: 'ding Ding' - }, { - label: 'Chelle', value: 'chelle Chelle' - }, { - label: 'Droplet', value: 'droplet Droplet' - }, { - label: 'Highbell', value: 'highbell Highbell' - }, { - label: 'Seasons', value: 'seasons Seasons' - }] -}; +import SectionTitle from './SectionTitle'; +import SectionSeparator from './SectionSeparator'; +import Info from './Info'; +import { OPTIONS } from './options'; class NotificationPreferencesView extends React.Component { static navigationOptions = () => ({ diff --git a/app/views/NotificationPreferencesView/options.js b/app/views/NotificationPreferencesView/options.js new file mode 100644 index 000000000..7c572718f --- /dev/null +++ b/app/views/NotificationPreferencesView/options.js @@ -0,0 +1,68 @@ +export const OPTIONS = { + desktopNotifications: [{ + label: 'Default', value: 'default' + }, { + label: 'All_Messages', value: 'all' + }, { + label: 'Mentions', value: 'mentions' + }, { + label: 'Nothing', value: 'nothing' + }], + audioNotifications: [{ + label: 'Default', value: 'default' + }, { + label: 'All_Messages', value: 'all' + }, { + label: 'Mentions', value: 'mentions' + }, { + label: 'Nothing', value: 'nothing' + }], + mobilePushNotifications: [{ + label: 'Default', value: 'default' + }, { + label: 'All_Messages', value: 'all' + }, { + label: 'Mentions', value: 'mentions' + }, { + label: 'Nothing', value: 'nothing' + }], + emailNotifications: [{ + label: 'Default', value: 'default' + }, { + label: 'All_Messages', value: 'all' + }, { + label: 'Mentions', value: 'mentions' + }, { + label: 'Nothing', value: 'nothing' + }], + desktopNotificationDuration: [{ + label: 'Default', value: 0 + }, { + label: 'Seconds', second: 1, value: 1 + }, { + label: 'Seconds', second: 2, value: 2 + }, { + label: 'Seconds', second: 3, value: 3 + }, { + label: 'Seconds', second: 4, value: 4 + }, { + label: 'Seconds', second: 5, value: 5 + }], + audioNotificationValue: [{ + label: 'None', value: 'none None' + }, { + label: 'Default', value: '0 Default' + }, { + label: 'Beep', value: 'beep Beep' + }, { + label: 'Ding', value: 'ding Ding' + }, { + label: 'Chelle', value: 'chelle Chelle' + }, { + label: 'Droplet', value: 'droplet Droplet' + }, { + label: 'Highbell', value: 'highbell Highbell' + }, { + label: 'Seasons', value: 'seasons Seasons' + }] +}; diff --git a/app/views/PreferencesView/index.js b/app/views/PreferencesView/index.js new file mode 100644 index 000000000..027cc9824 --- /dev/null +++ b/app/views/PreferencesView/index.js @@ -0,0 +1,65 @@ +import React from 'react'; +import { ScrollView } from 'react-native'; +import PropTypes from 'prop-types'; + +import I18n from '../../i18n'; +import { + logEvent, events +} from '../../utils/log'; +import scrollPersistTaps from '../../utils/scrollPersistTaps'; +import Separator from '../../containers/Separator'; +import SafeAreaView from '../../containers/SafeAreaView'; +import StatusBar from '../../containers/StatusBar'; +import ListItem from '../../containers/ListItem'; +import { DisclosureImage } from '../../containers/DisclosureIndicator'; +import { withTheme } from '../../theme'; + +class PreferencesView extends React.Component { + static navigationOptions = () => ({ + title: I18n.t('Preferences') + }); + + static propTypes = { + navigation: PropTypes.object, + theme: PropTypes.string + } + + renderDisclosure = () => { + const { theme } = this.props; + return ; + } + + navigateToScreen = (screen, params) => { + logEvent(events[`SE_GO_${ screen.replace('View', '').toUpperCase() }`]); + const { navigation } = this.props; + navigation.navigate(screen, params); + } + + render() { + const { theme } = this.props; + + return ( + + + + this.navigateToScreen('UserNotificationPrefView')} + showActionIndicator + testID='preferences-view-notifications' + right={this.renderDisclosure} + theme={theme} + /> + + + + ); + } +} + +export default withTheme(PreferencesView); diff --git a/app/views/ProfileView/index.js b/app/views/ProfileView/index.js index 8d289935b..5fcd421c3 100644 --- a/app/views/ProfileView/index.js +++ b/app/views/ProfileView/index.js @@ -24,7 +24,7 @@ import Button from '../../containers/Button'; import Avatar from '../../containers/Avatar'; import { setUser as setUserAction } from '../../actions/login'; import { CustomIcon } from '../../lib/Icons'; -import { DrawerButton } from '../../containers/HeaderButton'; +import { DrawerButton, PreferencesButton } from '../../containers/HeaderButton'; import StatusBar from '../../containers/StatusBar'; import { themes } from '../../constants/colors'; import { withTheme } from '../../theme'; @@ -39,6 +39,9 @@ class ProfileView extends React.Component { if (!isMasterDetail) { options.headerLeft = () => ; } + options.headerRight = () => ( + navigation.navigate('PreferencesView')} testID='preferences-view-open' /> + ); return options; } diff --git a/app/views/UserNotificationPreferencesView/Info.js b/app/views/UserNotificationPreferencesView/Info.js new file mode 100644 index 000000000..0c01e3c16 --- /dev/null +++ b/app/views/UserNotificationPreferencesView/Info.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { + Text +} from 'react-native'; +import PropTypes from 'prop-types'; + +import styles from './styles'; +import { themes } from '../../constants/colors'; + +const Info = React.memo(({ info, theme }) => ( + + {info} + +)); + +Info.propTypes = { + info: PropTypes.string, + theme: PropTypes.string +}; + +export default Info; diff --git a/app/views/UserNotificationPreferencesView/SectionSeparator.js b/app/views/UserNotificationPreferencesView/SectionSeparator.js new file mode 100644 index 000000000..88acd1a0e --- /dev/null +++ b/app/views/UserNotificationPreferencesView/SectionSeparator.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { + View +} from 'react-native'; +import PropTypes from 'prop-types'; + +import styles from './styles'; +import { themes } from '../../constants/colors'; + +const SectionSeparator = React.memo(({ theme }) => ( + +)); + +SectionSeparator.propTypes = { + theme: PropTypes.string +}; + +export default SectionSeparator; diff --git a/app/views/UserNotificationPreferencesView/SectionTitle.js b/app/views/UserNotificationPreferencesView/SectionTitle.js new file mode 100644 index 000000000..e93cb0e01 --- /dev/null +++ b/app/views/UserNotificationPreferencesView/SectionTitle.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { + Text +} from 'react-native'; +import PropTypes from 'prop-types'; + +import styles from './styles'; +import { themes } from '../../constants/colors'; + +const SectionTitle = React.memo(({ title, theme }) => ( + + {title} + +)); + +SectionTitle.propTypes = { + title: PropTypes.string, + theme: PropTypes.string +}; + +export default SectionTitle; diff --git a/app/views/UserNotificationPreferencesView/index.js b/app/views/UserNotificationPreferencesView/index.js new file mode 100644 index 000000000..e07888594 --- /dev/null +++ b/app/views/UserNotificationPreferencesView/index.js @@ -0,0 +1,168 @@ +import React from 'react'; +import { + View, ScrollView, Text +} from 'react-native'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { themes } from '../../constants/colors'; +import StatusBar from '../../containers/StatusBar'; +import ListItem from '../../containers/ListItem'; +import Separator from '../../containers/Separator'; +import I18n from '../../i18n'; +import scrollPersistTaps from '../../utils/scrollPersistTaps'; +import styles from './styles'; +import RocketChat from '../../lib/rocketchat'; +import { withTheme } from '../../theme'; +import SafeAreaView from '../../containers/SafeAreaView'; +import SectionTitle from './SectionTitle'; +import SectionSeparator from './SectionSeparator'; +import Info from './Info'; +import { OPTIONS } from './options'; +import ActivityIndicator from '../../containers/ActivityIndicator'; +import { DisclosureImage } from '../../containers/DisclosureIndicator'; +import { getUserSelector } from '../../selectors/login'; + +class UserNotificationPreferencesView extends React.Component { + static navigationOptions = () => ({ + title: I18n.t('Notification_Preferences') + }) + + static propTypes = { + navigation: PropTypes.object, + route: PropTypes.object, + theme: PropTypes.string, + user: PropTypes.shape({ + id: PropTypes.string + }) + }; + + constructor(props) { + super(props); + this.state = { + preferences: {}, + loading: false + }; + } + + async componentDidMount() { + const { user } = this.props; + const { id } = user; + const result = await RocketChat.getUserPreferences(id); + const { preferences } = result; + this.setState({ preferences, loading: true }); + } + + findOption = (key) => { + const { preferences } = this.state; + const option = preferences[key] ? OPTIONS[key].find(item => item.value === preferences[key]) : OPTIONS[key][0]; + return option; + } + + renderPickerOption = (key) => { + const { theme } = this.props; + const text = this.findOption(key); + return {I18n.t(text?.label, { defaultValue: text?.label, second: text?.second })}; + } + + pickerSelection = (title, key) => { + const { preferences } = this.state; + const { navigation } = this.props; + let values = OPTIONS[key]; + if (OPTIONS[key][0]?.value !== 'default') { + values = [{ label: `${ I18n.t('Default') } (${ I18n.t(this.findOption(key).label) })`, value: preferences[key]?.value }, ...OPTIONS[key]]; + } + navigation.navigate('PickerView', { + title, + data: values, + value: preferences[key], + onChangeValue: value => this.onValueChangePicker(key, value) + }); + } + + onValueChangePicker = (key, value) => this.saveNotificationPreferences({ [key]: value.toString() }); + + saveNotificationPreferences = async(params) => { + const { user } = this.props; + const { id } = user; + const result = await RocketChat.setUserPreferences(id, params); + const { user: { settings } } = result; + this.setState({ preferences: settings.preferences }); + } + + renderDisclosure = () => { + const { theme } = this.props; + return ; + } + + render() { + const { theme } = this.props; + const { loading } = this.state; + return ( + + + + {loading + ? ( + <> + + + + this.pickerSelection(title, 'desktopNotifications')} + right={() => this.renderPickerOption('desktopNotifications')} + theme={theme} + /> + + + + + + + + this.pickerSelection(title, 'mobileNotifications')} + right={() => this.renderPickerOption('mobileNotifications')} + theme={theme} + /> + + + + + + + + this.pickerSelection(title, 'emailNotificationMode')} + right={() => this.renderPickerOption('emailNotificationMode')} + theme={theme} + /> + + + + + ) : + } + + + + ); + } +} + +const mapStateToProps = state => ({ + user: getUserSelector(state) +}); + +export default connect(mapStateToProps)(withTheme(UserNotificationPreferencesView)); diff --git a/app/views/UserNotificationPreferencesView/options.js b/app/views/UserNotificationPreferencesView/options.js new file mode 100644 index 000000000..7ed02e12e --- /dev/null +++ b/app/views/UserNotificationPreferencesView/options.js @@ -0,0 +1,19 @@ +const commonOptions = [{ + label: 'Default', value: 'default' +}, { + label: 'All_Messages', value: 'all' +}, { + label: 'Mentions', value: 'mentions' +}, { + label: 'Nothing', value: 'nothing' +}]; + +export const OPTIONS = { + desktopNotifications: commonOptions, + mobileNotifications: commonOptions, + emailNotificationMode: [{ + label: 'Email_Notification_Mode_All', value: 'mentions' + }, { + label: 'Email_Notification_Mode_Disabled', value: 'nothing' + }] +}; diff --git a/app/views/UserNotificationPreferencesView/styles.js b/app/views/UserNotificationPreferencesView/styles.js new file mode 100644 index 000000000..9b4c9e0e6 --- /dev/null +++ b/app/views/UserNotificationPreferencesView/styles.js @@ -0,0 +1,31 @@ +import { StyleSheet } from 'react-native'; + +import sharedStyles from '../Styles'; + +export default StyleSheet.create({ + sectionSeparatorBorder: { + height: 10 + }, + marginBottom: { + height: 30 + }, + contentContainer: { + marginVertical: 10 + }, + infoText: { + ...sharedStyles.textRegular, + fontSize: 13, + paddingHorizontal: 15, + paddingVertical: 10 + }, + sectionTitle: { + ...sharedStyles.separatorBottom, + paddingHorizontal: 15, + paddingVertical: 10, + fontSize: 14 + }, + pickerText: { + ...sharedStyles.textRegular, + fontSize: 16 + } +});