diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index 4b3d2fabf..0bd3e712f 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -87,6 +87,7 @@ export default { alerts: 'alerts', All_users_in_the_channel_can_write_new_messages: 'All users in the channel can write new messages', All: 'All', + All_Messages: 'All Messages', Allow_Reactions: 'Allow Reactions', Alphabetical: 'Alphabetical', and_more: 'and more', @@ -98,6 +99,7 @@ export default { are_typing: 'are typing', Are_you_sure_question_mark: 'Are you sure?', Are_you_sure_you_want_to_leave_the_room: 'Are you sure you want to leave the room {{room}}?', + Audio: 'Audio', Authenticating: 'Authenticating', Auto_Translate: 'Auto-Translate', Avatar_changed_successfully: 'Avatar changed successfully!', @@ -140,12 +142,14 @@ export default { Created_snippet: 'Created a snippet', Create_a_new_workspace: 'Create a new workspace', Create: 'Create', + Default: 'Default', Delete_Room_Warning: 'Deleting a room will delete all messages posted within the room. This cannot be undone.', delete: 'delete', Delete: 'Delete', DELETE: 'DELETE', description: 'description', Description: 'Description', + DESKTOP_OPTIONS: 'DESKTOP OPTIONS', Directory: 'Directory', Direct_Messages: 'Direct Messages', Disable_notifications: 'Disable notifications', @@ -157,6 +161,7 @@ export default { Edit: 'Edit', Email_or_password_field_is_empty: 'Email or password field is empty', Email: 'Email', + EMAIL: 'EMAIL', email: 'e-mail', Enable_Auto_Translate: 'Enable Auto-Translate', Enable_markdown: 'Enable markdown', @@ -182,6 +187,8 @@ export default { Has_joined_the_channel: 'Has joined the channel', Has_joined_the_conversation: 'Has joined the conversation', Has_left_the_channel: 'Has left the channel', + IN_APP_AND_DESKTOP: 'IN-APP AND DESKTOP', + In_App_and_Desktop_Alert_info: 'Displays a banner at the top of the screen when app is open, and displays a notification on desktop', Invisible: 'Invisible', Invite: 'Invite', is_a_valid_RocketChat_instance: 'is a valid Rocket.Chat instance', @@ -243,9 +250,13 @@ export default { No_Reactions: 'No Reactions', No_Read_Receipts: 'No Read Receipts', Not_logged: 'Not logged', + Nothing: 'Nothing', Nothing_to_save: 'Nothing to save!', Notify_active_in_this_room: 'Notify active users in this room', Notify_all_in_this_room: 'Notify all in this room', + Notifications: 'Notifications', + Notification_Duration: 'Notification Duration', + Notification_Preferences: 'Notification Preferences', Offline: 'Offline', Oops: 'Oops!', Online: 'Online', @@ -269,6 +280,8 @@ export default { Profile: 'Profile', Public_Channel: 'Public Channel', Public: 'Public', + PUSH_NOTIFICATIONS: 'PUSH NOTIFICATIONS', + Push_Notifications_Alert_Info: 'These notifications are delivered to you when the app is not open', Quote: 'Quote', Reactions_are_disabled: 'Reactions are disabled', Reactions_are_enabled: 'Reactions are enabled', @@ -277,6 +290,8 @@ export default { Read_Only_Channel: 'Read Only Channel', Read_Only: 'Read Only', Read_Receipt: 'Read Receipt', + Receive_Group_Mentions: 'Receive Group Mentions', + Receive_Group_Mentions_Info: 'Receive @all and @here mentions', Register: 'Register', Repeat_Password: 'Repeat Password', Replied_on: 'Replied on:', @@ -284,6 +299,8 @@ export default { reply: 'reply', Reply: 'Reply', Report: 'Report', + Receive_Notification: 'Receive Notification', + Receive_notifications_from: 'Receive notifications from {{name}}', Resend: 'Resend', Reset_password: 'Reset password', resetting_password: 'resetting password', @@ -310,6 +327,7 @@ export default { Search_by: 'Search by', Search_global_users: 'Search for global users', Search_global_users_description: 'If you turn-on, you can search for any user from others companies or servers.', + Seconds: '{{second}} seconds', Select_Avatar: 'Select Avatar', Select_Server: 'Select Server', Select_Users: 'Select Users', @@ -327,10 +345,13 @@ export default { Settings_succesfully_changed: 'Settings succesfully changed!', Share: 'Share', Share_this_app: 'Share this app', + Show_Unread_Counter: 'Show Unread Counter', + Show_Unread_Counter_Info: 'Unread counter is displayed as a badge on the right of the channel, in the list', Sign_in_your_server: 'Sign in your server', Sign_Up: 'Sign Up', Some_field_is_invalid_or_empty: 'Some field is invalid or empty', Sorting_by: 'Sorting by {{key}}', + Sound: 'Sound', Star_room: 'Star room', Star: 'Star', Starred_Messages: 'Starred Messages', diff --git a/app/index.js b/app/index.js index dadf08762..0a4f584c9 100644 --- a/app/index.js +++ b/app/index.js @@ -121,6 +121,9 @@ const ChatsStack = createStackNavigator({ }, DirectoryView: { getScreen: () => require('./views/DirectoryView').default + }, + NotificationPrefView: { + getScreen: () => require('./views/NotificationPreferencesView').default } }, { defaultNavigationOptions: defaultHeader diff --git a/app/lib/methods/helpers/mergeSubscriptionsRooms.js b/app/lib/methods/helpers/mergeSubscriptionsRooms.js index f42b5c934..63da2c2c3 100644 --- a/app/lib/methods/helpers/mergeSubscriptionsRooms.js +++ b/app/lib/methods/helpers/mergeSubscriptionsRooms.js @@ -34,12 +34,6 @@ export const merge = (subscription, room) => { } } - if (subscription.mobilePushNotifications === 'nothing') { - subscription.notifications = true; - } else { - subscription.notifications = false; - } - if (!subscription.name) { subscription.name = subscription.fname; } diff --git a/app/lib/realm.js b/app/lib/realm.js index 3a17584cc..4d71b2ebc 100644 --- a/app/lib/realm.js +++ b/app/lib/realm.js @@ -95,14 +95,23 @@ const subscriptionSchema = { reactWhenReadOnly: { type: 'bool', optional: true }, archived: { type: 'bool', optional: true }, joinCodeRequired: { type: 'bool', optional: true }, - notifications: { type: 'bool', optional: true }, muted: 'string[]', broadcast: { type: 'bool', optional: true }, prid: { type: 'string', optional: true }, draftMessage: { type: 'string', optional: true }, lastThreadSync: 'date?', autoTranslate: 'bool?', - autoTranslateLanguage: 'string?' + autoTranslateLanguage: 'string?', + // Notifications + emailNotifications: { type: 'string', default: 'default' }, + disableNotifications: { type: 'bool', default: false }, + muteGroupMentions: { type: 'bool', default: false }, + hideUnreadStatus: { type: 'bool', default: false }, + audioNotifications: { type: 'string', default: 'default' }, + desktopNotifications: { type: 'string', default: 'default' }, + audioNotificationValue: { type: 'string', default: '0 Default' }, + desktopNotificationDuration: { type: 'int', default: 0 }, + mobilePushNotifications: { type: 'string', default: 'default' } } }; @@ -474,7 +483,7 @@ class DB { return this.databases.activeDB = new Realm({ path: `${ RNRealmPath.realmPath }${ path }.realm`, schema, - schemaVersion: 13, + schemaVersion: 14, migration: (oldRealm, newRealm) => { if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 13) { const newSubs = newRealm.objects('subscriptions'); diff --git a/app/views/NotificationPreferencesView/index.js b/app/views/NotificationPreferencesView/index.js new file mode 100644 index 000000000..c3337fa29 --- /dev/null +++ b/app/views/NotificationPreferencesView/index.js @@ -0,0 +1,281 @@ +import React from 'react'; +import { + View, ScrollView, Switch, Text +} from 'react-native'; +import PropTypes from 'prop-types'; +import RNPickerSelect from 'react-native-picker-select'; +import { SafeAreaView } from 'react-navigation'; + +import { SWITCH_TRACK_COLOR } 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 sharedStyles from '../Styles'; +import database from '../../lib/realm'; +import RocketChat from '../../lib/rocketchat'; +import log from '../../utils/log'; + +const SectionTitle = React.memo(({ title }) => {title}); + +const SectionSeparator = React.memo(() => ); + +const Info = React.memo(({ info }) => {info}); + +SectionTitle.propTypes = { + title: PropTypes.string +}; + +Info.propTypes = { + info: PropTypes.string +}; + +const OPTIONS = { + desktopNotifications: [{ + label: I18n.t('Default'), value: 'default' + }, { + label: I18n.t('All_Messages'), value: 'all' + }, { + label: I18n.t('Mentions'), value: 'mentions' + }, { + label: I18n.t('Nothing'), value: 'nothing' + }], + audioNotifications: [{ + label: I18n.t('Default'), value: 'default' + }, { + label: I18n.t('All_Messages'), value: 'all' + }, { + label: I18n.t('Mentions'), value: 'mentions' + }, { + label: I18n.t('Nothing'), value: 'nothing' + }], + mobilePushNotifications: [{ + label: I18n.t('Default'), value: 'default' + }, { + label: I18n.t('All_Messages'), value: 'all' + }, { + label: I18n.t('Mentions'), value: 'mentions' + }, { + label: I18n.t('Nothing'), value: 'nothing' + }], + emailNotifications: [{ + label: I18n.t('Default'), value: 'default' + }, { + label: I18n.t('All_Messages'), value: 'all' + }, { + label: I18n.t('Mentions'), value: 'mentions' + }, { + label: I18n.t('Nothing'), value: 'nothing' + }], + desktopNotificationDuration: [{ + label: I18n.t('Default'), value: 0 + }, { + label: I18n.t('Seconds', { second: 1 }), value: 1 + }, { + label: I18n.t('Seconds', { second: 2 }), value: 2 + }, { + label: I18n.t('Seconds', { second: 3 }), value: 3 + }, { + label: I18n.t('Seconds', { second: 4 }), value: 4 + }, { + label: I18n.t('Seconds', { second: 5 }), value: 5 + }], + audioNotificationValue: [{ + label: 'None', value: 'none None' + }, { + label: I18n.t('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' + }] +}; + +export default class NotificationPreferencesView extends React.Component { + static navigationOptions = () => ({ + title: I18n.t('Notification_Preferences') + }) + + static propTypes = { + navigation: PropTypes.object + } + + constructor(props) { + super(props); + this.rid = props.navigation.getParam('rid'); + this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid); + this.state = { + room: JSON.parse(JSON.stringify(this.rooms[0] || {})) + }; + } + + onValueChangeSwitch = async(key, value) => { + const { room: newRoom } = this.state; + newRoom[key] = value; + this.setState({ room: newRoom }); + const params = { + [key]: value ? '1' : '0' + }; + try { + await RocketChat.saveNotificationSettings(this.rid, params); + } catch (err) { + log('err_save_notification_settings', err); + } + } + + onValueChangePicker = async(key, value) => { + const { room: newRoom } = this.state; + newRoom[key] = value; + this.setState({ room: newRoom }); + const params = { + [key]: value.toString() + }; + try { + await RocketChat.saveNotificationSettings(this.rid, params); + } catch (err) { + log('err_save_notification_settings', err); + } + } + + renderPicker = (key) => { + const { room } = this.state; + return ( + this.onValueChangePicker(key, value)} + items={OPTIONS[key]} + /> + ); + } + + renderSwitch = (key) => { + const { room } = this.state; + return ( + this.onValueChangeSwitch(key, !value)} + /> + ); + } + + render() { + const { room } = this.state; + return ( + + + + + this.renderSwitch('disableNotifications')} + /> + + + + + + this.renderSwitch('muteGroupMentions')} + /> + + + + + + this.renderSwitch('hideUnreadStatus')} + /> + + + + + + + + this.renderPicker('desktopNotifications')} + /> + + + + + + + + this.renderPicker('mobilePushNotifications')} + /> + + + + + + + + this.renderPicker('audioNotifications')} + /> + + this.renderPicker('audioNotificationValue')} + /> + + this.renderPicker('desktopNotificationDuration')} + /> + + + + + + + this.renderPicker('emailNotifications')} + /> + + + + + + ); + } +} diff --git a/app/views/NotificationPreferencesView/styles.js b/app/views/NotificationPreferencesView/styles.js new file mode 100644 index 000000000..d01d019e6 --- /dev/null +++ b/app/views/NotificationPreferencesView/styles.js @@ -0,0 +1,43 @@ +import { StyleSheet } from 'react-native'; + +import { COLOR_BACKGROUND_CONTAINER, COLOR_PRIMARY, COLOR_WHITE } from '../../constants/colors'; +import sharedStyles from '../Styles'; + +export default StyleSheet.create({ + sectionSeparatorBorder: { + backgroundColor: COLOR_BACKGROUND_CONTAINER, + height: 10 + }, + marginBottom: { + height: 30, + backgroundColor: COLOR_BACKGROUND_CONTAINER + }, + contentContainer: { + backgroundColor: COLOR_WHITE, + marginVertical: 10 + }, + infoText: { + ...sharedStyles.textRegular, + ...sharedStyles.textColorNormal, + fontSize: 13, + paddingHorizontal: 15, + paddingVertical: 10, + backgroundColor: COLOR_BACKGROUND_CONTAINER + }, + sectionTitle: { + ...sharedStyles.separatorBottom, + paddingHorizontal: 15, + backgroundColor: COLOR_BACKGROUND_CONTAINER, + paddingVertical: 10, + fontSize: 14, + ...sharedStyles.textColorNormal + }, + viewContainer: { + justifyContent: 'center' + }, + pickerText: { + ...sharedStyles.textRegular, + fontSize: 16, + color: COLOR_PRIMARY + } +}); diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index 5de85245e..fb7c03192 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -168,13 +168,14 @@ class RoomActionsView extends React.Component { room, membersCount, canViewMembers, joined, canAutoTranslate } = this.state; const { - rid, t, blocker, notifications + rid, t, blocker } = room; const notificationsAction = { - icon: notifications ? 'bell' : 'Bell-off', - name: I18n.t(`${ notifications ? 'Enable' : 'Disable' }_notifications`), - event: this.toggleNotifications, + icon: 'bell', + name: I18n.t('Notifications'), + route: 'NotificationPrefView', + params: { rid }, testID: 'room-actions-notifications' }; @@ -386,18 +387,6 @@ class RoomActionsView extends React.Component { ); } - toggleNotifications = () => { - const { room } = this.state; - try { - const notifications = { - mobilePushNotifications: room.notifications ? 'default' : 'nothing' - }; - RocketChat.saveNotificationSettings(room.rid, notifications); - } catch (e) { - log(e); - } - } - renderRoomInfo = ({ item }) => { const { room, member } = this.state; const { name, t, topic } = room; diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js index a91aaa67a..92baa9ff8 100644 --- a/app/views/RoomInfoView/index.js +++ b/app/views/RoomInfoView/index.js @@ -9,7 +9,7 @@ import Status from '../../containers/Status'; import Avatar from '../../containers/Avatar'; import styles from './styles'; import sharedStyles from '../Styles'; -import database from '../../lib/realm'; +import database, { safeAddListener } from '../../lib/realm'; import RocketChat from '../../lib/rocketchat'; import RoomTypeIcon from '../../containers/RoomTypeIcon'; import I18n from '../../i18n'; @@ -84,11 +84,12 @@ class RoomInfoView extends React.Component { } return; } - const rooms = database.objects('subscriptions').filtered('rid = $0', this.rid); + this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid); + safeAddListener(this.rooms, this.updateRoom); let room = {}; - if (rooms.length > 0) { - this.setState({ room: rooms[0] }); - [room] = rooms; + if (this.rooms.length > 0) { + this.setState({ room: this.rooms[0] }); + [room] = this.rooms; } else { try { const result = await RocketChat.getRoomInfo(this.rid); diff --git a/e2e/09-roomactions.spec.js b/e2e/09-roomactions.spec.js index 4f6f7e40e..2dac3e728 100644 --- a/e2e/09-roomactions.spec.js +++ b/e2e/09-roomactions.spec.js @@ -250,21 +250,65 @@ describe('Room actions screen', () => { await backToActions(); }); - it('should enable/disable notifications', async() => { + afterEach(async() => { + takeScreenshot(); + }); + }); + + describe('Notification', async() => { + it('should navigate to notification preference view', async() => { await waitFor(element(by.id('room-actions-notifications'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down'); - await expect(element(by.text('Disable notifications'))).toBeVisible(); + await expect(element(by.id('room-actions-notifications'))).toBeVisible(); await element(by.id('room-actions-notifications')).tap(); - await waitFor(element(by.text('Enable notifications'))).toBeVisible().withTimeout(60000); - await expect(element(by.text('Enable notifications'))).toBeVisible(); - await element(by.id('room-actions-notifications')).tap(); - await waitFor(element(by.text('Disable notifications'))).toBeVisible().withTimeout(60000); - await expect(element(by.text('Disable notifications'))).toBeVisible(); + await waitFor(element(by.text('notification-preference-view'))).toBeVisible().withTimeout(2000); + await expect(element(by.id('notification-preference-view'))).toBeVisible(); + }); + + it('should have receive notification option', async() => { + await expect(element(by.id('notification-preference-view-receive-notification'))).toBeVisible(); + }); + + it('should have show unread count option', async() => { + await expect(element(by.id('notification-preference-view-unread-count'))).toBeVisible(); + }); + + it('should have notification alert option', async() => { + await expect(element(by.id('notification-preference-view-alert'))).toBeVisible(); + }); + + it('should have push notification option', async() => { + await waitFor(element(by.id('notification-preference-view-push-notification'))).toBeVisible().whileElement(by.id('notification-preference-view-list')).scroll(scrollDown, 'down'); + await expect(element(by.id('notification-preference-view-push-notification'))).toBeVisible(); + }); + + it('should have notification audio option', async() => { + await waitFor(element(by.id('notification-preference-view-audio'))).toBeVisible().whileElement(by.id('notification-preference-view-list')).scroll(scrollDown, 'down'); + await expect(element(by.id('notification-preference-view-audio'))).toBeVisible(); + }); + + it('should have notification sound option', async() => { + await waitFor(element(by.id('notification-preference-view-sound'))).toBeVisible().whileElement(by.id('notification-preference-view-list')).scroll(scrollDown, 'down'); + await expect(element(by.id('notification-preference-view-sound'))).toBeVisible(); + }); + + it('should have notification duration option', async() => { + await waitFor(element(by.id('notification-preference-view-notification-duration'))).toBeVisible().whileElement(by.id('notification-preference-view-list')).scroll(scrollDown, 'down'); + await expect(element(by.id('notification-preference-view-notification-duration'))).toBeVisible(); + }); + + it('should have email alert option', async() => { + await waitFor(element(by.id('notification-preference-view-email-alert'))).toBeVisible().whileElement(by.id('notification-preference-view-list')).scroll(scrollDown, 'down'); + await expect(element(by.id('notification-preference-view-email-alert'))).toBeVisible(); }); afterEach(async() => { takeScreenshot(); }); - }); + + after(async() => { + await backToActions(); + }); + }) describe('Channel/Group', async() => { // Currently, there's no way to add more owners to the room