From 6896129c7363907d65311708c79ea63b20aa4194 Mon Sep 17 00:00:00 2001 From: Kyriakos Z <3829551+koox00@users.noreply.github.com> Date: Fri, 20 Aug 2021 19:49:00 +0300 Subject: [PATCH] MM-36980, MM-37030: Opens the thread on push notification and Notifications settings (#5618) * MM-36980: Opens the thread on push notification This commit enables tapping on a push notification for a reply to open the thread. This is done only if the user has CRT set to 'on'. * Refactor, and handle in app notifications * Minor change * Fixes erroneous check * Fixes on dismiss in app notification * Fixes in app push notification for CRT * Adds CRT notification settings for android Adds support for CRT (*_threads) notification settings (notify_props). Adds android comoponents to toggle those settings for push notifications and email notifications. * Adds CRT notification settings for iOS Adds iOS components to toggle those CRT notifications settings for push notifications and email notifications. * Fixes bad JSON and intl ids * Fixes i18n ids * Fixes tests * Adds email_threads to the default notify_props * Fixes push_threads notify_prop default value * Fixes style, and channel missing on thread open * Fixes test * Fixes click notification to open thread Previous handling of opening the thread on notification clicked fell into an infinite loop when the app was closed. This commit fixes that by adding the post to selectedPostId reducer and then emitting an event to open the thread on channel_base, and only if the app was started by the notification. When the app is in the background emitting the event from handleNotification works just fine. When the app is in the foreground the notification clicked gets handled elsewhere and this commit does not change that. * Removes reply settings when CRT is ON "Mentions and Replies" section becomes just "Mentions" when the user has the Collapsed Reply Threads set to "ON". * Fixes prop types Co-authored-by: Kyriakos Ziakoulis --- app/actions/views/root.js | 6 +- app/init/global_event_handler.js | 7 +- app/init/push_notifications.ts | 11 +- app/mm-redux/selectors/entities/posts.ts | 17 + app/mm-redux/types/users.ts | 3 + app/screens/channel/channel_base.js | 7 + app/screens/channel/index.js | 2 + app/screens/notification/index.tsx | 9 +- .../notification_settings.test.js.snap | 409 ++++++++++++++++++ .../settings/notification_settings/index.js | 3 +- .../notification_settings.js | 22 +- .../notification_settings.test.js | 12 + ...cation_settings_email.android.test.js.snap | 52 +++ ...tification_settings_email.ios.test.js.snap | 83 ++++ .../notification_settings_email/index.js | 2 + .../notification_settings_email.android.js | 42 +- ...otification_settings_email.android.test.js | 29 ++ .../notification_settings_email.ios.js | 43 +- .../notification_settings_email.ios.test.js | 14 + .../notification_settings_email_base.js | 11 + .../notification_settings_mentions/index.js | 3 +- .../notification_settings_mentions.android.js | 8 +- .../notification_settings_mentions.ios.js | 4 +- .../notification_settings_mobile/index.js | 3 +- .../notification_settings_mobile.android.js | 44 +- .../notification_settings_mobile.ios.js | 43 +- .../notification_settings_mobile_base.js | 6 + app/utils/notify_props.js | 2 + assets/base/i18n/en.json | 9 + 29 files changed, 884 insertions(+), 22 deletions(-) diff --git a/app/actions/views/root.js b/app/actions/views/root.js index 65b715c9d..b152a34a4 100644 --- a/app/actions/views/root.js +++ b/app/actions/views/root.js @@ -8,7 +8,7 @@ import {NavigationTypes, ViewTypes} from '@constants'; import {ChannelTypes, GeneralTypes, TeamTypes} from '@mm-redux/action_types'; import {getChannelAndMyMember} from '@mm-redux/actions/channels'; import {getDataRetentionPolicy} from '@mm-redux/actions/general'; -import {receivedNewPost} from '@mm-redux/actions/posts'; +import {receivedNewPost, selectPost} from '@mm-redux/actions/posts'; import {getMyTeams, getMyTeamMembers, getMyTeamUnreads} from '@mm-redux/actions/teams'; import {General} from '@mm-redux/constants'; import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences'; @@ -98,6 +98,10 @@ export function loadFromPushNotification(notification, isInitialNotification) { dispatch(handleSelectTeamAndChannel(teamId, channelId)); + const {root_id: rootId} = notification.payload || {}; + if (isCollapsedThreadsEnabled(state) && rootId) { + dispatch(selectPost(rootId)); + } return {data: true}; }; } diff --git a/app/init/global_event_handler.js b/app/init/global_event_handler.js index 9dff47f5f..8ed7d0fc3 100644 --- a/app/init/global_event_handler.js +++ b/app/init/global_event_handler.js @@ -28,6 +28,8 @@ import {autoUpdateTimezone} from '@mm-redux/actions/timezone'; import {General} from '@mm-redux/constants'; import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels'; import {getConfig} from '@mm-redux/selectors/entities/general'; +import {isPostSelected} from '@mm-redux/selectors/entities/posts'; +import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences'; import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone'; import {getCurrentUser, getUser} from '@mm-redux/selectors/entities/users'; import EventEmitter from '@mm-redux/utils/event_emitter'; @@ -334,7 +336,10 @@ class GlobalEventHandler { const state = getState(); const currentChannelId = getCurrentChannelId(state); - if (payload?.channel_id !== currentChannelId) { + if ( + payload?.channel_id !== currentChannelId || + (payload?.root_id && isCollapsedThreadsEnabled(state) && !isPostSelected(state, payload?.root_id)) + ) { const screen = 'Notification'; const passProps = { notification, diff --git a/app/init/push_notifications.ts b/app/init/push_notifications.ts index 020e3f625..a35064645 100644 --- a/app/init/push_notifications.ts +++ b/app/init/push_notifications.ts @@ -23,6 +23,7 @@ import {NavigationTypes, ViewTypes} from '@constants'; import {getLocalizedMessage} from '@i18n'; import {setDeviceToken} from '@mm-redux/actions/general'; import {General} from '@mm-redux/constants'; +import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences'; import EventEmitter from '@mm-redux/utils/event_emitter'; import {getCurrentLocale} from '@selectors/i18n'; import {getBadgeCount} from '@selectors/views'; @@ -31,7 +32,7 @@ import Store from '@store/store'; import {waitForHydration} from '@store/utils'; import {t} from '@utils/i18n'; -import type {DispatchFunc} from '@mm-redux/types/actions'; +import type {DispatchFunc, GetStateFunc} from '@mm-redux/types/actions'; const CATEGORY = 'CAN_REPLY'; const REPLY_ACTION = 'REPLY_ACTION'; @@ -160,6 +161,7 @@ class PushNotifications { if (Store.redux && payload) { const dispatch = Store.redux.dispatch as DispatchFunc; + const getState = Store.redux.getState as GetStateFunc; waitForHydration(Store.redux, async () => { switch (payload.type) { @@ -182,6 +184,13 @@ class PushNotifications { await dismissAllModals(); await popToRoot(); + + if (!isInitialNotification) { + const {root_id: rootId, channel_id: channelId} = notification.payload || {}; + if (rootId && isCollapsedThreadsEnabled(getState())) { + EventEmitter.emit('goToThread', {id: rootId, channel_id: channelId}); + } + } } } break; diff --git a/app/mm-redux/selectors/entities/posts.ts b/app/mm-redux/selectors/entities/posts.ts index 2781997f2..9d4b8f84c 100644 --- a/app/mm-redux/selectors/entities/posts.ts +++ b/app/mm-redux/selectors/entities/posts.ts @@ -654,3 +654,20 @@ export const makeIsPostCommentMention = (): ((b: GlobalState, postId: $ID, export function getExpandedLink(state: GlobalState, link: string): string { return state.entities.posts.expandedURLs[link]; } + +export function isPostSelected(state: GlobalState, id: $ID) { + if (!id) { + return false; + } + + return state.entities.posts.selectedPostId === id; +} + +export function getSelectedPost(state: GlobalState): Post | null { + const selectedPostId = state.entities.posts.selectedPostId; + if (selectedPostId) { + return getPost(state, selectedPostId); + } + + return null; +} diff --git a/app/mm-redux/types/users.ts b/app/mm-redux/types/users.ts index e828d5b49..a98db89c2 100644 --- a/app/mm-redux/types/users.ts +++ b/app/mm-redux/types/users.ts @@ -8,11 +8,14 @@ export type UserNotifyProps = { auto_responder_active?: 'true' | 'false'; auto_responder_message?: string; desktop: 'default' | 'all' | 'mention' | 'none'; + desktop_threads?: 'all' | 'mention'; desktop_notification_sound?: string; desktop_sound: 'true' | 'false'; email: 'true' | 'false'; + email_threads?: 'all' | 'mention'; mark_unread: 'all' | 'mention'; push: 'default' | 'all' | 'mention' | 'none'; + push_threads?: 'all' | 'mention'; push_status: 'ooo' | 'offline' | 'away' | 'dnd' | 'online'; comments: 'never' | 'root' | 'any'; first_name: 'true' | 'false'; diff --git a/app/screens/channel/channel_base.js b/app/screens/channel/channel_base.js index 7afb5c154..0cc07980e 100644 --- a/app/screens/channel/channel_base.js +++ b/app/screens/channel/channel_base.js @@ -37,6 +37,10 @@ export default class ChannelBase extends PureComponent { showTermsOfService: PropTypes.bool, skipMetrics: PropTypes.bool, viewingGlobalThreads: PropTypes.bool, + selectedPost: PropTypes.shape({ + id: PropTypes.string.isRequired, + channel_id: PropTypes.string.isRequired, + }), }; static contextTypes = { @@ -195,6 +199,9 @@ export default class ChannelBase extends PureComponent { loadChannels = (teamId) => { const {loadChannelsForTeam, selectInitialChannel} = this.props.actions; if (EphemeralStore.getStartFromNotification()) { + if (this.props.selectedPost) { + EventEmitter.emit('goToThread', this.props.selectedPost); + } // eslint-disable-next-line no-console console.log('Switch to channel from a push notification'); EphemeralStore.setStartFromNotification(false); diff --git a/app/screens/channel/index.js b/app/screens/channel/index.js index 6966f9586..d98cf0382 100644 --- a/app/screens/channel/index.js +++ b/app/screens/channel/index.js @@ -11,6 +11,7 @@ import {ViewTypes} from '@constants'; import {getChannelStats} from '@mm-redux/actions/channels'; import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels'; import {getServerVersion} from '@mm-redux/selectors/entities/general'; +import {getSelectedPost} from '@mm-redux/selectors/entities/posts'; import {getTheme, isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences'; import {getCurrentTeam} from '@mm-redux/selectors/entities/teams'; import {getCurrentUserId, getCurrentUserRoles, shouldShowTermsOfService} from '@mm-redux/selectors/entities/users'; @@ -46,6 +47,7 @@ function mapStateToProps(state) { currentUserId, isSupportedServer, isSystemAdmin, + selectedPost: getSelectedPost(state), showTermsOfService: shouldShowTermsOfService(state), teamName: currentTeam?.display_name, theme: getTheme(state), diff --git a/app/screens/notification/index.tsx b/app/screens/notification/index.tsx index 6cd18f879..79edd4637 100644 --- a/app/screens/notification/index.tsx +++ b/app/screens/notification/index.tsx @@ -7,11 +7,12 @@ import * as Animatable from 'react-native-animatable'; import {PanGestureHandler} from 'react-native-gesture-handler'; import {Navigation} from 'react-native-navigation'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; -import {useDispatch} from 'react-redux'; +import {useDispatch, useSelector} from 'react-redux'; import {popToRoot, dismissAllModals, dismissOverlay} from '@actions/navigation'; import {loadFromPushNotification} from '@actions/views/root'; import {NavigationTypes} from '@constants'; +import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences'; import EventEmitter from '@mm-redux/utils/event_emitter'; import {changeOpacity} from '@utils/theme'; @@ -49,6 +50,7 @@ const Notification = ({componentId, notification}: NotificationProps) => { const dispatch = useDispatch(); const dismissTimerRef = useRef(null); const tapped = useRef(false); + const collapsedThreadsEnabled = useSelector(isCollapsedThreadsEnabled); const animateDismissOverlay = () => { cancelDismissTimer(); @@ -100,6 +102,11 @@ const Notification = ({componentId, notification}: NotificationProps) => { if (componentId === screen && tapped.current) { await dismissAllModals(); await popToRoot(); + + const {root_id: rootId, channel_id: channelId} = notification.payload || {}; + if (rootId && collapsedThreadsEnabled) { + EventEmitter.emit('goToThread', {id: rootId, channel_id: channelId}); + } } }); diff --git a/app/screens/settings/notification_settings/__snapshots__/notification_settings.test.js.snap b/app/screens/settings/notification_settings/__snapshots__/notification_settings.test.js.snap index 936af58cb..627a2703e 100644 --- a/app/screens/settings/notification_settings/__snapshots__/notification_settings.test.js.snap +++ b/app/screens/settings/notification_settings/__snapshots__/notification_settings.test.js.snap @@ -69,6 +69,7 @@ NotificationSettings { "textComponent": "span", "timeZone": null, }, + "isCollapsedThreadsEnabled": false, "theme": Object { "awayIndicator": "#ffbc42", "buttonBg": "#166de0", @@ -193,6 +194,7 @@ NotificationSettings { "timeZone": null, } } + isCollapsedThreadsEnabled={false} theme={ Object { "awayIndicator": "#ffbc42", @@ -404,3 +406,410 @@ NotificationSettings { }, } `; + +exports[`NotificationSettings should match snapshot, when CRT is ON 1`] = ` +NotificationSettings { + "context": Object { + "intl": Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object {}, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "onError": [Function], + "textComponent": "span", + "timeZone": null, + }, + }, + "goToNotificationSettingsAutoResponder": [Function], + "goToNotificationSettingsEmail": [Function], + "goToNotificationSettingsMentions": [Function], + "goToNotificationSettingsMobile": [Function], + "handlePress": [Function], + "props": Object { + "actions": Object { + "updateMe": [MockFunction], + }, + "componentId": "component-id", + "currentUser": Object { + "id": "current_user_id", + }, + "currentUserStatus": "status", + "enableAutoResponder": false, + "intl": Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object {}, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "onError": [Function], + "textComponent": "span", + "timeZone": null, + }, + "isCollapsedThreadsEnabled": true, + "theme": Object { + "awayIndicator": "#ffbc42", + "buttonBg": "#166de0", + "buttonColor": "#ffffff", + "centerChannelBg": "#ffffff", + "centerChannelColor": "#3d3c40", + "codeTheme": "github", + "dndIndicator": "#f74343", + "errorTextColor": "#fd5960", + "linkColor": "#2389d7", + "mentionBg": "#ffffff", + "mentionBj": "#ffffff", + "mentionColor": "#145dbf", + "mentionHighlightBg": "#ffe577", + "mentionHighlightLink": "#166de0", + "newMessageSeparator": "#ff8800", + "onlineIndicator": "#06d6a0", + "sidebarBg": "#145dbf", + "sidebarHeaderBg": "#1153ab", + "sidebarHeaderTextColor": "#ffffff", + "sidebarText": "#ffffff", + "sidebarTextActiveBorder": "#579eff", + "sidebarTextActiveColor": "#ffffff", + "sidebarTextHoverBg": "#4578bf", + "sidebarUnreadText": "#ffffff", + "type": "Mattermost", + }, + "updateMeRequest": Object {}, + }, + "refs": Object {}, + "sanitizeNotificationProps": [Function], + "saveAutoResponder": [Function], + "saveNotificationProps": [Function], + "setState": [Function], + "shouldSaveAutoResponder": [Function], + "state": null, + "updater": Updater { + "_callbacks": Array [], + "_renderer": ReactShallowRenderer { + "_context": Object { + "intl": Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object {}, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "onError": [Function], + "textComponent": "span", + "timeZone": null, + }, + }, + "_didScheduleRenderPhaseUpdate": false, + "_dispatcher": Object { + "readContext": [Function], + "useCallback": [Function], + "useContext": [Function], + "useDebugValue": [Function], + "useDeferredValue": [Function], + "useEffect": [Function], + "useImperativeHandle": [Function], + "useLayoutEffect": [Function], + "useMemo": [Function], + "useReducer": [Function], + "useRef": [Function], + "useResponder": [Function], + "useState": [Function], + "useTransition": [Function], + }, + "_element": , + "_firstWorkInProgressHook": null, + "_forcedUpdate": false, + "_instance": [Circular], + "_isReRender": false, + "_newState": null, + "_numberOfReRenders": 0, + "_renderPhaseUpdates": null, + "_rendered": + + + + + + + + + , + "_rendering": false, + "_updater": [Circular], + "_workInProgressHook": null, + }, + }, +} +`; diff --git a/app/screens/settings/notification_settings/index.js b/app/screens/settings/notification_settings/index.js index 7364d34b4..a8f7c3cba 100644 --- a/app/screens/settings/notification_settings/index.js +++ b/app/screens/settings/notification_settings/index.js @@ -6,7 +6,7 @@ import {bindActionCreators} from 'redux'; import {updateMe} from '@mm-redux/actions/users'; import {getConfig} from '@mm-redux/selectors/entities/general'; -import {getMyPreferences, getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getMyPreferences, getTheme, isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences'; import {getCurrentUser, getStatusForUserId} from '@mm-redux/selectors/entities/users'; import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; import {isLandscape} from '@selectors/device'; @@ -29,6 +29,7 @@ function mapStateToProps(state) { theme: getTheme(state), enableAutoResponder, isLandscape: isLandscape(state), + isCollapsedThreadsEnabled: isCollapsedThreadsEnabled(state), }; } diff --git a/app/screens/settings/notification_settings/notification_settings.js b/app/screens/settings/notification_settings/notification_settings.js index 2428860f5..97ce1a234 100644 --- a/app/screens/settings/notification_settings/notification_settings.js +++ b/app/screens/settings/notification_settings/notification_settings.js @@ -34,6 +34,7 @@ export default class NotificationSettings extends PureComponent { updateMeRequest: PropTypes.object.isRequired, currentUserStatus: PropTypes.string.isRequired, enableAutoResponder: PropTypes.bool.isRequired, + isCollapsedThreadsEnabled: PropTypes.bool.isRequired, }; static contextTypes = { @@ -89,13 +90,15 @@ export default class NotificationSettings extends PureComponent { }; goToNotificationSettingsMentions = () => { - const {currentUser} = this.props; + const {currentUser, isCollapsedThreadsEnabled} = this.props; const {intl} = this.context; const screen = 'NotificationSettingsMentions'; - const title = intl.formatMessage({ - id: 'mobile.notification_settings.mentions_replies', - defaultMessage: 'Mentions and Replies', - }); + + const id = isCollapsedThreadsEnabled ? 'mobile.notification_settings.mentions' : 'mobile.notification.notification_settings.mentions_replies'; + const defaultMessage = isCollapsedThreadsEnabled ? 'Mentions' : 'Mentions and Replies'; + + const title = intl.formatMessage({id, defaultMessage}); + const passProps = { currentUser, onBack: this.saveNotificationProps, @@ -190,7 +193,7 @@ export default class NotificationSettings extends PureComponent { }; render() { - const {theme, enableAutoResponder} = this.props; + const {theme, enableAutoResponder, isCollapsedThreadsEnabled} = this.props; const style = getStyleSheet(theme); const showArrow = Platform.OS === 'ios'; @@ -210,6 +213,9 @@ export default class NotificationSettings extends PureComponent { ); } + const mentionsI18nId = isCollapsedThreadsEnabled ? t('mobile.notification_settings.mentions') : t('mobile.notification_settings.mentions_replies'); + const mentionsI18nDefault = isCollapsedThreadsEnabled ? 'Mentions' : 'Mentions and Replies'; + return ( this.handlePress(this.goToNotificationSettingsMentions)} separator={true} diff --git a/app/screens/settings/notification_settings/notification_settings.test.js b/app/screens/settings/notification_settings/notification_settings.test.js index e6debc892..04fdd70f0 100644 --- a/app/screens/settings/notification_settings/notification_settings.test.js +++ b/app/screens/settings/notification_settings/notification_settings.test.js @@ -21,6 +21,7 @@ describe('NotificationSettings', () => { updateMeRequest: {}, currentUserStatus: 'status', enableAutoResponder: false, + isCollapsedThreadsEnabled: false, }; test('should match snapshot', () => { @@ -31,6 +32,17 @@ describe('NotificationSettings', () => { expect(wrapper.instance()).toMatchSnapshot(); }); + test('should match snapshot, when CRT is ON', () => { + const wrapper = shallowWithIntl( + , + ); + + expect(wrapper.instance()).toMatchSnapshot(); + }); + test('should include previous notification props when saving new ones', () => { const wrapper = shallowWithIntl( , diff --git a/app/screens/settings/notification_settings_email/__snapshots__/notification_settings_email.android.test.js.snap b/app/screens/settings/notification_settings_email/__snapshots__/notification_settings_email.android.test.js.snap index bc044d089..c13b16b0e 100644 --- a/app/screens/settings/notification_settings_email/__snapshots__/notification_settings_email.android.test.js.snap +++ b/app/screens/settings/notification_settings_email/__snapshots__/notification_settings_email.android.test.js.snap @@ -147,3 +147,55 @@ exports[`NotificationSettingsEmailAndroid should match snapshot 2`] = ` `; + +exports[`NotificationSettingsEmailAndroid should match snapshot, renderEmailThreadsSection 1`] = ` + + + } + label={ + + } + selected={true} + theme={ + Object { + "awayIndicator": "#ffbc42", + "buttonBg": "#166de0", + "buttonColor": "#ffffff", + "centerChannelBg": "#ffffff", + "centerChannelColor": "#3d3c40", + "codeTheme": "github", + "dndIndicator": "#f74343", + "errorTextColor": "#fd5960", + "linkColor": "#2389d7", + "mentionBg": "#ffffff", + "mentionBj": "#ffffff", + "mentionColor": "#145dbf", + "mentionHighlightBg": "#ffe577", + "mentionHighlightLink": "#166de0", + "newMessageSeparator": "#ff8800", + "onlineIndicator": "#06d6a0", + "sidebarBg": "#145dbf", + "sidebarHeaderBg": "#1153ab", + "sidebarHeaderTextColor": "#ffffff", + "sidebarText": "#ffffff", + "sidebarTextActiveBorder": "#579eff", + "sidebarTextActiveColor": "#ffffff", + "sidebarTextHoverBg": "#4578bf", + "sidebarUnreadText": "#ffffff", + "type": "Mattermost", + } + } + /> + + +`; diff --git a/app/screens/settings/notification_settings_email/__snapshots__/notification_settings_email.ios.test.js.snap b/app/screens/settings/notification_settings_email/__snapshots__/notification_settings_email.ios.test.js.snap index 98fdf2439..1eae0a8ed 100644 --- a/app/screens/settings/notification_settings_email/__snapshots__/notification_settings_email.ios.test.js.snap +++ b/app/screens/settings/notification_settings_email/__snapshots__/notification_settings_email.ios.test.js.snap @@ -135,3 +135,86 @@ exports[`NotificationSettingsEmailIos should match snapshot, renderEmailSection `; + +exports[`NotificationSettingsEmailIos should match snapshot, renderEmailThreadsSection 1`] = ` +
+ } + label={ + + } + selected={true} + theme={ + Object { + "awayIndicator": "#ffbc42", + "buttonBg": "#166de0", + "buttonColor": "#ffffff", + "centerChannelBg": "#ffffff", + "centerChannelColor": "#3d3c40", + "codeTheme": "github", + "dndIndicator": "#f74343", + "errorTextColor": "#fd5960", + "linkColor": "#2389d7", + "mentionBg": "#ffffff", + "mentionBj": "#ffffff", + "mentionColor": "#145dbf", + "mentionHighlightBg": "#ffe577", + "mentionHighlightLink": "#166de0", + "newMessageSeparator": "#ff8800", + "onlineIndicator": "#06d6a0", + "sidebarBg": "#145dbf", + "sidebarHeaderBg": "#1153ab", + "sidebarHeaderTextColor": "#ffffff", + "sidebarText": "#ffffff", + "sidebarTextActiveBorder": "#579eff", + "sidebarTextActiveColor": "#ffffff", + "sidebarTextHoverBg": "#4578bf", + "sidebarUnreadText": "#ffffff", + "type": "Mattermost", + } + } + /> + +
+`; diff --git a/app/screens/settings/notification_settings_email/index.js b/app/screens/settings/notification_settings_email/index.js index eebc57aa7..0b86c639b 100644 --- a/app/screens/settings/notification_settings_email/index.js +++ b/app/screens/settings/notification_settings_email/index.js @@ -11,6 +11,7 @@ import {getConfig} from '@mm-redux/selectors/entities/general'; import { get as getPreference, getTheme, + isCollapsedThreadsEnabled, } from '@mm-redux/selectors/entities/preferences'; import {getCurrentUser} from '@mm-redux/selectors/entities/users'; import {getNotificationProps} from '@utils/notify_props'; @@ -38,6 +39,7 @@ function mapStateToProps(state) { emailInterval, sendEmailNotifications, theme: getTheme(state), + isCollapsedThreadsEnabled: isCollapsedThreadsEnabled(state), }; } diff --git a/app/screens/settings/notification_settings_email/notification_settings_email.android.js b/app/screens/settings/notification_settings_email/notification_settings_email.android.js index 4ab8e2a53..6598bf6b5 100644 --- a/app/screens/settings/notification_settings_email/notification_settings_email.android.js +++ b/app/screens/settings/notification_settings_email/notification_settings_email.android.js @@ -36,6 +36,15 @@ class NotificationSettingsEmailAndroid extends NotificationSettingsEmailBase { this.saveEmailNotifyProps(); }; + handleEmailThreadsChanged = (value) => { + let emailThreads = 'mention'; + if (value) { + emailThreads = 'all'; + } + + this.setEmailThreads(emailThreads, this.saveEmailThreadsNotifyProps); + }; + showEmailModal = () => { this.setState({showEmailNotificationsModal: true}); }; @@ -95,6 +104,34 @@ class NotificationSettingsEmailAndroid extends NotificationSettingsEmailBase { ); } + renderEmailThreadsSection(style) { + const {theme} = this.props; + + return ( + + + )} + description={( + + )} + action={this.handleEmailThreadsChanged} + actionType='toggle' + selected={this.state.emailThreads === 'all'} + theme={theme} + /> + + + ); + } + renderEmailNotificationsModal(style) { const {intl} = this.context; const { @@ -227,7 +264,7 @@ class NotificationSettingsEmailAndroid extends NotificationSettingsEmailBase { } render() { - const {theme} = this.props; + const {theme, isCollapsedThreadsEnabled, notifyProps} = this.props; const style = getStyleSheet(theme); return ( @@ -242,6 +279,9 @@ class NotificationSettingsEmailAndroid extends NotificationSettingsEmailBase { > {this.renderEmailSection()} + {isCollapsedThreadsEnabled && notifyProps.email === 'true' && ( + this.renderEmailThreadsSection(style) + )} {this.renderEmailNotificationsModal(style)} diff --git a/app/screens/settings/notification_settings_email/notification_settings_email.android.test.js b/app/screens/settings/notification_settings_email/notification_settings_email.android.test.js index e82a9911b..3289b502f 100644 --- a/app/screens/settings/notification_settings_email/notification_settings_email.android.test.js +++ b/app/screens/settings/notification_settings_email/notification_settings_email.android.test.js @@ -14,6 +14,7 @@ describe('NotificationSettingsEmailAndroid', () => { currentUser: {id: 'current_user_id'}, notifyProps: { email: 'true', + email_threads: 'all', }, emailInterval: '30', enableEmailBatching: false, @@ -24,6 +25,7 @@ describe('NotificationSettingsEmailAndroid', () => { sendEmailNotifications: true, theme: Preferences.THEMES.default, componentId: 'component-id', + isCollapsedThreadsEnabled: false, }; test('should match snapshot', () => { @@ -49,6 +51,33 @@ describe('NotificationSettingsEmailAndroid', () => { expect(wrapper.instance().renderEmailNotificationsModal(style)).toMatchSnapshot(); }); + test('should match snapshot, renderEmailThreadsSection', () => { + const props = { + ...baseProps, + isCollapsedThreadsEnabled: true, + }; + + const wrapper = shallowWithIntl( + , + ); + + const style = { + divider: {}, + modal: {}, + modalBody: {}, + modalTitleContainer: {}, + modalTitle: {}, + modalOptionDisabled: {}, + modalHelpText: {}, + modalFooter: {}, + modalFooterContainer: {}, + modalFooterOptionContainer: {}, + modalFooterOption: {}, + }; + + expect(wrapper.instance().renderEmailThreadsSection(style)).toMatchSnapshot(); + }); + test('should match state on setEmailInterval', () => { const wrapper = shallowWithIntl( , diff --git a/app/screens/settings/notification_settings_email/notification_settings_email.ios.js b/app/screens/settings/notification_settings_email/notification_settings_email.ios.js index a38ee0289..28cc06473 100644 --- a/app/screens/settings/notification_settings_email/notification_settings_email.ios.js +++ b/app/screens/settings/notification_settings_email/notification_settings_email.ios.js @@ -19,6 +19,15 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import NotificationSettingsEmailBase from './notification_settings_email_base'; class NotificationSettingsEmailIos extends NotificationSettingsEmailBase { + handleEmailThreadsChanged = (value) => { + let emailThreads = 'mention'; + if (value) { + emailThreads = 'all'; + } + + this.setEmailThreads(emailThreads, this.saveEmailThreadsNotifyProps); + }; + renderEmailSection() { const { enableEmailBatching, @@ -113,8 +122,37 @@ class NotificationSettingsEmailIos extends NotificationSettingsEmailBase { ); } - render() { + renderEmailThreadsSection(style) { const {theme} = this.props; + + return ( +
+ + )} + description={} + action={this.handleEmailThreadsChanged} + actionType='toggle' + selected={this.state.emailThreads === 'all'} + theme={theme} + /> + +
+ ); + } + + render() { + const {theme, isCollapsedThreadsEnabled, notifyProps} = this.props; const style = getStyleSheet(theme); return ( @@ -130,6 +168,9 @@ class NotificationSettingsEmailIos extends NotificationSettingsEmailBase { alwaysBounceVertical={false} > {this.renderEmailSection()} + {isCollapsedThreadsEnabled && notifyProps.email === 'true' && ( + this.renderEmailThreadsSection(style) + )}
); diff --git a/app/screens/settings/notification_settings_email/notification_settings_email.ios.test.js b/app/screens/settings/notification_settings_email/notification_settings_email.ios.test.js index e3b0cfcaf..8e97ad3b4 100644 --- a/app/screens/settings/notification_settings_email/notification_settings_email.ios.test.js +++ b/app/screens/settings/notification_settings_email/notification_settings_email.ios.test.js @@ -22,6 +22,7 @@ describe('NotificationSettingsEmailIos', () => { currentUser: {id: 'current_user_id'}, notifyProps: { email: 'true', + email_threads: 'all', }, emailInterval: '30', enableEmailBatching: false, @@ -32,6 +33,7 @@ describe('NotificationSettingsEmailIos', () => { sendEmailNotifications: true, theme: Preferences.THEMES.default, componentId: 'component-id', + isCollapsedThreadsEnabled: false, }; test('should match snapshot, renderEmailSection', () => { @@ -42,6 +44,18 @@ describe('NotificationSettingsEmailIos', () => { expect(wrapper.instance().renderEmailSection()).toMatchSnapshot(); }); + test('should match snapshot, renderEmailThreadsSection', () => { + const style = { + separator: {}, + }; + + const wrapper = shallow( + , + ); + + expect(wrapper.instance().renderEmailThreadsSection(style)).toMatchSnapshot(); + }); + test('should save preference on back button only if email interval has changed', () => { const wrapper = shallow( , diff --git a/app/screens/settings/notification_settings_email/notification_settings_email_base.js b/app/screens/settings/notification_settings_email/notification_settings_email_base.js index 23504b95e..7fdd645d7 100644 --- a/app/screens/settings/notification_settings_email/notification_settings_email_base.js +++ b/app/screens/settings/notification_settings_email/notification_settings_email_base.js @@ -37,6 +37,7 @@ export default class NotificationSettingsEmailBase extends PureComponent { emailInterval, newInterval: this.computeEmailInterval(notifyProps?.email === 'true' && sendEmailNotifications, enableEmailBatching, emailInterval), showEmailNotificationsModal: false, + emailThreads: notifyProps.email_threads, }; } @@ -83,6 +84,16 @@ export default class NotificationSettingsEmailBase extends PureComponent { this.setState({newInterval: value}); }; + setEmailThreads = (value, callback) => { + this.setState({emailThreads: value}, callback); + }; + + saveEmailThreadsNotifyProps = () => { + const {emailThreads} = this.state; + const {actions, notifyProps} = this.props; + actions.updateMe({notify_props: {...notifyProps, email_threads: emailThreads}}); + } + saveEmailNotifyProps = () => { const {emailInterval, newInterval} = this.state; diff --git a/app/screens/settings/notification_settings_mentions/index.js b/app/screens/settings/notification_settings_mentions/index.js index 1d5e46718..349b9e7f0 100644 --- a/app/screens/settings/notification_settings_mentions/index.js +++ b/app/screens/settings/notification_settings_mentions/index.js @@ -3,12 +3,13 @@ import {connect} from 'react-redux'; -import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getTheme, isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences'; import NotificationSettingsMentions from './notification_settings_mentions'; function mapStateToProps(state) { return { + isCollapsedThreadsEnabled: isCollapsedThreadsEnabled(state), theme: getTheme(state), }; } 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 index fccd7cdf5..eaa85ca11 100644 --- a/app/screens/settings/notification_settings_mentions/notification_settings_mentions.android.js +++ b/app/screens/settings/notification_settings_mentions/notification_settings_mentions.android.js @@ -360,8 +360,12 @@ class NotificationSettingsMentionsAndroid extends NotificationSettingsMentionsBa theme={theme} /> - {this.renderReplySection()} - + {!this.props.isCollapsedThreadsEnabled && ( + <> + {this.renderReplySection()} + + + )} {this.renderKeywordsModal(style)} {this.renderReplyModal(style)} 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 index e2d8fef94..2ed408f17 100644 --- a/app/screens/settings/notification_settings_mentions/notification_settings_mentions.ios.js +++ b/app/screens/settings/notification_settings_mentions/notification_settings_mentions.ios.js @@ -200,7 +200,9 @@ class NotificationSettingsMentionsIos extends NotificationSettingsMentionsBase { alwaysBounceVertical={false} > {this.renderMentionSection(style)} - {this.renderReplySection(style)} + {!this.props.isCollapsedThreadsEnabled && ( + this.renderReplySection(style) + )} ); diff --git a/app/screens/settings/notification_settings_mobile/index.js b/app/screens/settings/notification_settings_mobile/index.js index e894a9ed4..8efb81c36 100644 --- a/app/screens/settings/notification_settings_mobile/index.js +++ b/app/screens/settings/notification_settings_mobile/index.js @@ -6,7 +6,7 @@ import {bindActionCreators} from 'redux'; import {updateMe} from '@mm-redux/actions/users'; import {getConfig} from '@mm-redux/selectors/entities/general'; -import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getTeammateNameDisplaySetting, getTheme, isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences'; import {getCurrentUser} from '@mm-redux/selectors/entities/users'; import NotificationSettingsMobile from './notification_settings_mobile'; @@ -23,6 +23,7 @@ function mapStateToProps(state) { theme, updateMeRequest, currentUser, + isCollapsedThreadsEnabled: isCollapsedThreadsEnabled(state), }; } 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 index 0ef82f042..d2a02bb3b 100644 --- a/app/screens/settings/notification_settings_mobile/notification_settings_mobile.android.js +++ b/app/screens/settings/notification_settings_mobile/notification_settings_mobile.android.js @@ -67,6 +67,15 @@ class NotificationSettingsMobileAndroid extends NotificationSettingsMobileBase { this.setState({newPush: value}); }; + onMobilePushThreadChanged = (value) => { + let pushThreads = 'mention'; + if (value) { + pushThreads = 'all'; + } + + this.setMobilePushThreads(pushThreads, this.saveNotificationProps); + }; + onMobilePushStatusChanged = (value) => { this.setState({newPushStatus: value}); }; @@ -611,6 +620,34 @@ class NotificationSettingsMobileAndroid extends NotificationSettingsMobileBase { ); } + renderMobilePushThreadsSection(style) { + const {theme} = this.props; + + return ( + + + )} + description={( + + )} + action={this.onMobilePushThreadChanged} + actionType='toggle' + selected={this.state.push_threads === 'all'} + theme={theme} + /> + + + ); + } + renderNotificationOptions(style) { if (Platform.Version >= 26) { return null; @@ -645,6 +682,7 @@ class NotificationSettingsMobileAndroid extends NotificationSettingsMobileBase { mention_keys: mentionKeys, push, push_status: pushStatus, + push_threads: pushThreads, } = this.state; const {currentUser} = this.props; @@ -658,6 +696,7 @@ class NotificationSettingsMobileAndroid extends NotificationSettingsMobileBase { mention_keys: mentionKeys, push, push_status: pushStatus, + push_threads: pushThreads, user_id: currentUser.id, }; @@ -737,7 +776,7 @@ class NotificationSettingsMobileAndroid extends NotificationSettingsMobileBase { }; render() { - const {theme} = this.props; + const {theme, isCollapsedThreadsEnabled} = this.props; const style = getStyleSheet(theme); return ( @@ -753,6 +792,9 @@ class NotificationSettingsMobileAndroid extends NotificationSettingsMobileBase { > {this.renderMobilePushSection()} + {isCollapsedThreadsEnabled && this.state.push === 'mention' && ( + this.renderMobilePushThreadsSection(style) + )} {this.renderMobilePushStatusSection(style)} {this.renderNotificationOptions(style)} {this.renderMobileTestSection(style)} 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 index 50b10f07e..ea6d7e5c8 100644 --- a/app/screens/settings/notification_settings_mobile/notification_settings_mobile.ios.js +++ b/app/screens/settings/notification_settings_mobile/notification_settings_mobile.ios.js @@ -19,6 +19,15 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import NotificationSettingsMobileBase from './notification_settings_mobile_base'; class NotificationSettingsMobileIos extends NotificationSettingsMobileBase { + onMobilePushThreadChanged = (value) => { + let pushThreads = 'mention'; + if (value) { + pushThreads = 'all'; + } + + this.setMobilePushThreads(pushThreads, this.saveNotificationProps); + }; + renderMobilePushSection(style) { const {config, theme} = this.props; @@ -88,6 +97,35 @@ class NotificationSettingsMobileIos extends NotificationSettingsMobileBase { ); } + renderMobilePushThreadsSection(style) { + const {theme} = this.props; + + return ( +
+ + )} + description={} + action={this.onMobilePushThreadChanged} + actionType='toggle' + selected={this.state.push_threads === 'all'} + theme={theme} + /> + +
+ ); + } + renderMobilePushStatusSection(style) { const {config, theme} = this.props; @@ -148,7 +186,7 @@ class NotificationSettingsMobileIos extends NotificationSettingsMobileBase { } render() { - const {theme} = this.props; + const {theme, isCollapsedThreadsEnabled} = this.props; const style = getStyleSheet(theme); return ( @@ -167,6 +205,9 @@ class NotificationSettingsMobileIos extends NotificationSettingsMobileBase { alwaysBounceVertical={false} > {this.renderMobilePushSection(style)} + {isCollapsedThreadsEnabled && this.state.push === 'mention' && ( + this.renderMobilePushThreadsSection(style) + )} {this.renderMobilePushStatusSection(style)}
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 index 377eb4db2..39305ff9d 100644 --- a/app/screens/settings/notification_settings_mobile/notification_settings_mobile_base.js +++ b/app/screens/settings/notification_settings_mobile/notification_settings_mobile_base.js @@ -86,6 +86,10 @@ export default class NotificationSettingsMobileBase extends PureComponent { this.setState({push}, callback); }; + setMobilePushThreads = (value, callback) => { + this.setState({push_threads: value}, callback); + }; + setMobilePushStatus = (value, callback) => { this.setState({push_status: value}, callback); }; @@ -100,6 +104,7 @@ export default class NotificationSettingsMobileBase extends PureComponent { mention_keys: mentionKeys, push, push_status: pushStatus, + push_threads: pushThreads, } = this.state; this.props.onBack({ @@ -111,6 +116,7 @@ export default class NotificationSettingsMobileBase extends PureComponent { mention_keys: mentionKeys, push, push_status: pushStatus, + push_threads: pushThreads, user_id: this.props.currentUser.id, }); }; diff --git a/app/utils/notify_props.js b/app/utils/notify_props.js index 918b2de9d..3103d1ef9 100644 --- a/app/utils/notify_props.js +++ b/app/utils/notify_props.js @@ -12,9 +12,11 @@ export function getNotificationProps(user) { desktop: 'all', desktop_sound: 'true', email: 'true', + email_threads: 'all', mention_keys: user ? `${user.username},@${user.username}` : '', push: 'mention', push_status: 'online', + push_threads: 'all', }; if (!user || !user.first_name) { diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index af07a901f..072e50b92 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -428,6 +428,7 @@ "mobile.notification_settings.email": "Email", "mobile.notification_settings.email_title": "Email Notifications", "mobile.notification_settings.email.send": "SEND EMAIL NOTIFICATIONS", + "mobile.notification_settings.mentions": "Mentions", "mobile.notification_settings.mentions_replies": "Mentions and Replies", "mobile.notification_settings.mentions.channelWide": "Channel-wide mentions", "mobile.notification_settings.mentions.reply_title": "Send Reply notifications for", @@ -438,6 +439,10 @@ "mobile.notification_settings.modal_cancel": "CANCEL", "mobile.notification_settings.modal_save": "SAVE", "mobile.notification_settings.ooo_auto_responder": "Automatic Direct Message Replies", + "mobile.notification_settings.push_threads.description": "Notify me about all replies to threads I'm following", + "mobile.notification_settings.push_threads.info": "When enabled, any reply to a thread you're following will send a mobile push notification.", + "mobile.notification_settings.push_threads.title": "THREAD REPLY NOTIFICATIONS", + "mobile.notification_settings.push_threads.title_android": "Thread reply 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.oauth.failed_to_login": "Your login attempt failed. Please try again.", @@ -739,6 +744,10 @@ "user.settings.modal.notifications": "Notifications", "user.settings.notifications.allActivity": "For all activity", "user.settings.notifications.comments": "Reply notifications", + "user.settings.notifications.email_threads.description": "Notify me about all replies to threads I'm following.", + "user.settings.notifications.email_threads.info": "When enabled, any reply to a thread you're following will send an email notification.", + "user.settings.notifications.email_threads.title": "THREAD REPLY NOTIFICATIONS", + "user.settings.notifications.email_threads.title_android": "Thread reply notifications", "user.settings.notifications.email.disabled": "Email notifications are not enabled", "user.settings.notifications.email.everyHour": "Every hour", "user.settings.notifications.email.immediately": "Immediately",