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",