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 <koox00@Kyriakoss-MacBook-Pro.local>
This commit is contained in:
Kyriakos Z 2021-08-20 19:49:00 +03:00 committed by GitHub
parent 8228ed3400
commit 6896129c73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 884 additions and 22 deletions

View file

@ -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};
};
}

View file

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

View file

@ -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;

View file

@ -654,3 +654,20 @@ export const makeIsPostCommentMention = (): ((b: GlobalState, postId: $ID<Post>,
export function getExpandedLink(state: GlobalState, link: string): string {
return state.entities.posts.expandedURLs[link];
}
export function isPostSelected(state: GlobalState, id: $ID<Post>) {
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;
}

View file

@ -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';

View file

@ -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);

View file

@ -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),

View file

@ -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<NodeJS.Timeout | null>(null);
const tapped = useRef<boolean>(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});
}
}
});

View file

@ -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": <NotificationSettings
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 {}}
/>,
"_firstWorkInProgressHook": null,
"_forcedUpdate": false,
"_instance": [Circular],
"_isReRender": false,
"_newState": null,
"_numberOfReRenders": 0,
"_renderPhaseUpdates": null,
"_rendered": <RNCSafeAreaView
edges={
Array [
"left",
"right",
]
}
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
}
}
testID="notification_settings.screen"
>
<Connect(StatusBar) />
<ScrollView
alwaysBounceVertical={false}
contentContainerStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.06)",
"flex": 1,
"paddingTop": 35,
}
}
>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.1)",
"height": 1,
"width": "100%",
}
}
/>
<SettingsItem
defaultMessage="Mentions"
i18nId="mobile.notification_settings.mentions"
iconName="at"
isDestructor={false}
isLink={false}
onPress={[Function]}
separator={true}
showArrow={true}
testID="notification_settings.mentions_replies.action"
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",
}
}
/>
<SettingsItem
defaultMessage="Mobile"
i18nId="mobile.notification_settings.mobile"
iconName="cellphone"
isDestructor={false}
isLink={false}
onPress={[Function]}
separator={true}
showArrow={true}
testID="notification_settings.mobile.action"
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",
}
}
/>
<SettingsItem
defaultMessage="Email"
i18nId="mobile.notification_settings.email"
iconName="email-outline"
isDestructor={false}
isLink={false}
onPress={[Function]}
separator={false}
showArrow={true}
testID="notification_settings.email.action"
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",
}
}
/>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.1)",
"height": 1,
"width": "100%",
}
}
/>
</ScrollView>
</RNCSafeAreaView>,
"_rendering": false,
"_updater": [Circular],
"_workInProgressHook": null,
},
},
}
`;

View file

@ -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),
};
}

View file

@ -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 (
<SafeAreaView
edges={['left', 'right']}
@ -223,8 +229,8 @@ export default class NotificationSettings extends PureComponent {
>
<View style={style.divider}/>
<SettingsItem
defaultMessage='Mentions and Replies'
i18nId={t('mobile.notification_settings.mentions_replies')}
defaultMessage={mentionsI18nDefault}
i18nId={mentionsI18nId}
iconName='at'
onPress={() => this.handlePress(this.goToNotificationSettingsMentions)}
separator={true}

View file

@ -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(
<NotificationSettings
{...baseProps}
isCollapsedThreadsEnabled={true}
/>,
);
expect(wrapper.instance()).toMatchSnapshot();
});
test('should include previous notification props when saving new ones', () => {
const wrapper = shallowWithIntl(
<NotificationSettings {...baseProps}/>,

View file

@ -147,3 +147,55 @@ exports[`NotificationSettingsEmailAndroid should match snapshot 2`] = `
</View>
</Modal>
`;
exports[`NotificationSettingsEmailAndroid should match snapshot, renderEmailThreadsSection 1`] = `
<View>
<sectionItem
action={[Function]}
actionType="toggle"
description={
<InjectIntl(FormattedText)
defaultMessage="Notify me about all replies to threads I'm following"
id="user.settings.notifications.email_threads.description"
/>
}
label={
<InjectIntl(FormattedText)
defaultMessage="Thread reply notifications"
id="user.settings.notifications.email_threads.title_android"
/>
}
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",
}
}
/>
<View />
</View>
`;

View file

@ -135,3 +135,86 @@ exports[`NotificationSettingsEmailIos should match snapshot, renderEmailSection
</View>
</section>
`;
exports[`NotificationSettingsEmailIos should match snapshot, renderEmailThreadsSection 1`] = `
<section
footerDefaultMessage="When enabled, any reply to a thread you're following will send an email notification."
footerId="user.settings.notifications.email_threads.info"
headerDefaultMessage="THREAD REPLY NOTIFICATIONS"
headerId="user.settings.notifications.email_threads.title"
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",
}
}
>
<sectionItem
action={[Function]}
actionType="toggle"
description={<View />}
label={
<InjectIntl(FormattedText)
defaultMessage="Notify me about all replies to threads I'm following"
id="user.settings.notifications.email_threads.description"
/>
}
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",
}
}
/>
<View
style={Object {}}
/>
</section>
`;

View file

@ -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),
};
}

View file

@ -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 (
<View>
<SectionItem
label={(
<FormattedText
id='user.settings.notifications.email_threads.title_android'
defaultMessage='Thread reply notifications'
/>
)}
description={(
<FormattedText
id='user.settings.notifications.email_threads.description'
defaultMessage={'Notify me about all replies to threads I\'m following'}
/>
)}
action={this.handleEmailThreadsChanged}
actionType='toggle'
selected={this.state.emailThreads === 'all'}
theme={theme}
/>
<View style={style.separator}/>
</View>
);
}
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()}
<View style={style.separator}/>
{isCollapsedThreadsEnabled && notifyProps.email === 'true' && (
this.renderEmailThreadsSection(style)
)}
{this.renderEmailNotificationsModal(style)}
</ScrollView>
</View>

View file

@ -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(
<NotificationSettingsEmailAndroid {...props}/>,
);
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(
<NotificationSettingsEmailAndroid {...baseProps}/>,

View file

@ -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 (
<Section
headerId={t('user.settings.notifications.email_threads.title')}
headerDefaultMessage='THREAD REPLY NOTIFICATIONS'
footerId={t('user.settings.notifications.email_threads.info')}
footerDefaultMessage={'When enabled, any reply to a thread you\'re following will send an email notification.'}
theme={theme}
>
<SectionItem
label={(
<FormattedText
id='user.settings.notifications.email_threads.description'
defaultMessage={'Notify me about all replies to threads I\'m following'}
/>
)}
description={<View/>}
action={this.handleEmailThreadsChanged}
actionType='toggle'
selected={this.state.emailThreads === 'all'}
theme={theme}
/>
<View style={style.separator}/>
</Section>
);
}
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)
)}
</ScrollView>
</SafeAreaView>
);

View file

@ -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(
<NotificationSettingsEmailIos {...{...baseProps, isCollapsedThreadsEnabled: true}}/>,
);
expect(wrapper.instance().renderEmailThreadsSection(style)).toMatchSnapshot();
});
test('should save preference on back button only if email interval has changed', () => {
const wrapper = shallow(
<NotificationSettingsEmailIos {...baseProps}/>,

View file

@ -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;

View file

@ -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),
};
}

View file

@ -360,8 +360,12 @@ class NotificationSettingsMentionsAndroid extends NotificationSettingsMentionsBa
theme={theme}
/>
<View style={style.separator}/>
{this.renderReplySection()}
<View style={style.separator}/>
{!this.props.isCollapsedThreadsEnabled && (
<>
{this.renderReplySection()}
<View style={style.separator}/>
</>
)}
</ScrollView>
{this.renderKeywordsModal(style)}
{this.renderReplyModal(style)}

View file

@ -200,7 +200,9 @@ class NotificationSettingsMentionsIos extends NotificationSettingsMentionsBase {
alwaysBounceVertical={false}
>
{this.renderMentionSection(style)}
{this.renderReplySection(style)}
{!this.props.isCollapsedThreadsEnabled && (
this.renderReplySection(style)
)}
</ScrollView>
</SafeAreaView>
);

View file

@ -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),
};
}

View file

@ -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 (
<View>
<SectionItem
label={(
<FormattedText
id='mobile.notification_settings.push_threads.title_android'
defaultMessage='Thread reply notifications'
/>
)}
description={(
<FormattedText
id='mobile.notification_settings.push_threads.description'
defaultMessage={'Notify me about all replies to threads I\'m following'}
/>
)}
action={this.onMobilePushThreadChanged}
actionType='toggle'
selected={this.state.push_threads === 'all'}
theme={theme}
/>
<View style={style.separator}/>
</View>
);
}
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()}
<View style={style.separator}/>
{isCollapsedThreadsEnabled && this.state.push === 'mention' && (
this.renderMobilePushThreadsSection(style)
)}
{this.renderMobilePushStatusSection(style)}
{this.renderNotificationOptions(style)}
{this.renderMobileTestSection(style)}

View file

@ -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 (
<Section
headerId={t('mobile.notification_settings.push_threads.title')}
headerDefaultMessage='THREAD REPLY NOTIFICATIONS'
footerId={t('mobile.notification_settings.push_threads.info')}
footerDefaultMessage={'When enabled, any reply to a thread you\'re following will send a mobile push notification'}
theme={theme}
>
<SectionItem
label={(
<FormattedText
id='mobile.notification_settings.push_threads.description'
defaultMessage={'Notify me about all replies to threads I\'m following'}
/>
)}
description={<View/>}
action={this.onMobilePushThreadChanged}
actionType='toggle'
selected={this.state.push_threads === 'all'}
theme={theme}
/>
<View style={style.separator}/>
</Section>
);
}
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)}
</ScrollView>
</View>

View file

@ -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,
});
};

View file

@ -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) {

View file

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