Add a notification disabled notice to notification settings (#9145)
* Adding the notification disabled notice * Change the color of the icon on the section notice to red. * Fix Linter Issues * Add new line due to CI failure * Adressing pull request comments and change requests. * i18 Strings alphabetical order * result of `npm run i18n-extract` * Add a couple more tests for notifications * Remove two unneeded styles * fix linter issue
This commit is contained in:
parent
3efc301da5
commit
42988fddc3
7 changed files with 263 additions and 3 deletions
|
|
@ -103,7 +103,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
|||
color: theme.dndIndicator,
|
||||
},
|
||||
dangerIcon: {
|
||||
color: theme.sidebarTextActiveBorder,
|
||||
color: theme.errorTextColor,
|
||||
},
|
||||
dangerContainer: {
|
||||
borderColor: changeOpacity(theme.dndIndicator, 0.16),
|
||||
|
|
|
|||
103
app/screens/settings/notifications/notifications.test.tsx
Normal file
103
app/screens/settings/notifications/notifications.test.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type ComponentProps} from 'react';
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import * as DeviceHooks from '@hooks/device';
|
||||
import {renderWithEverything, waitFor} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import Notifications from './notifications';
|
||||
|
||||
import type Database from '@nozbe/watermelondb/Database';
|
||||
|
||||
const MockedNotifications = jest.mocked(require('react-native-notifications').Notifications);
|
||||
|
||||
function getBaseProps(): ComponentProps<typeof Notifications> {
|
||||
return {
|
||||
componentId: 'Settings' as const,
|
||||
currentUser: TestHelper.fakeUserModel({id: 'user1', username: 'username1'}),
|
||||
emailInterval: '0',
|
||||
enableAutoResponder: false,
|
||||
enableEmailBatching: false,
|
||||
isCRTEnabled: false,
|
||||
sendEmailNotifications: false,
|
||||
serverVersion: '10.3.0',
|
||||
};
|
||||
}
|
||||
|
||||
describe('Notifications disabled banner', () => {
|
||||
let database: Database;
|
||||
const testId = 'notifications-disabled-notice';
|
||||
const serverUrl = 'server-1';
|
||||
|
||||
beforeAll(async () => {
|
||||
const server = await TestHelper.setupServerDatabase(serverUrl);
|
||||
database = server.database;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be visible if notifications are disabled', async () => {
|
||||
MockedNotifications.isRegisteredForRemoteNotifications.mockResolvedValue(false);
|
||||
const wrapper = renderWithEverything(<Notifications {...getBaseProps()}/>, {database});
|
||||
await waitFor(() => {
|
||||
expect(wrapper.queryByTestId(testId)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not be visible if notifications are enabled', async () => {
|
||||
MockedNotifications.isRegisteredForRemoteNotifications.mockResolvedValue(true);
|
||||
const wrapper = renderWithEverything(<Notifications {...getBaseProps()}/>, {database});
|
||||
await waitFor(() => {
|
||||
expect(wrapper.queryByTestId(testId)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
jest.spyOn(DeviceHooks, 'useAppState').mockReturnValue('active');
|
||||
|
||||
it('should re-check notification registration when appState changes', async () => {
|
||||
MockedNotifications.isRegisteredForRemoteNotifications.mockResolvedValueOnce(false);
|
||||
const appStateSpy = jest.spyOn(DeviceHooks, 'useAppState');
|
||||
appStateSpy.mockReturnValue('active');
|
||||
const wrapper = renderWithEverything(<Notifications {...getBaseProps()}/>, {database});
|
||||
await waitFor(() => {
|
||||
expect(MockedNotifications.isRegisteredForRemoteNotifications).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Testing that this is not called in the background
|
||||
MockedNotifications.isRegisteredForRemoteNotifications.mockResolvedValueOnce(true);
|
||||
appStateSpy.mockReturnValue('background');
|
||||
wrapper.rerender(<Notifications {...getBaseProps()}/>);
|
||||
await waitFor(() => {
|
||||
expect(MockedNotifications.isRegisteredForRemoteNotifications).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
appStateSpy.mockReturnValue('active');
|
||||
wrapper.rerender(<Notifications {...getBaseProps()}/>);
|
||||
await waitFor(() => {
|
||||
expect(MockedNotifications.isRegisteredForRemoteNotifications).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
});
|
||||
|
||||
it('should prevent state update after unmount (isCurrent race prevention)', async () => {
|
||||
jest.spyOn(DeviceHooks, 'useAppState').mockReturnValue('active');
|
||||
|
||||
let resolvePromise!: (value: boolean) => void;
|
||||
const promise = new Promise<boolean>((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
MockedNotifications.isRegisteredForRemoteNotifications.mockReturnValue(promise);
|
||||
|
||||
const wrapper = renderWithEverything(<Notifications {...getBaseProps()}/>, {database});
|
||||
wrapper.unmount();
|
||||
|
||||
resolvePromise(false);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
});
|
||||
});
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
import {Notifications as RNNotifications} from 'react-native-notifications';
|
||||
|
||||
import {getCallsConfig} from '@calls/state';
|
||||
import SettingContainer from '@components/settings/container';
|
||||
|
|
@ -10,10 +11,13 @@ import SettingItem from '@components/settings/item';
|
|||
import {General, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useAppState} from '@hooks/device';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
import {gotoSettingsScreen} from '@screens/settings/config';
|
||||
import {logError} from '@utils/log';
|
||||
import {getEmailInterval, getEmailIntervalTexts, getNotificationProps} from '@utils/user';
|
||||
|
||||
import NotificationsDisabledNotice from './notifications_disabled_notice';
|
||||
import SendTestNotificationNotice from './send_test_notification_notice';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
|
@ -38,7 +42,7 @@ const mentionTexts = defineMessages({
|
|||
},
|
||||
});
|
||||
|
||||
type NotificationsProps = {
|
||||
export type NotificationsProps = {
|
||||
componentId: AvailableScreens;
|
||||
currentUser?: UserModel;
|
||||
emailInterval: string;
|
||||
|
|
@ -62,6 +66,31 @@ const Notifications = ({
|
|||
const serverUrl = useServerUrl();
|
||||
const notifyProps = useMemo(() => getNotificationProps(currentUser), [currentUser?.notifyProps]);
|
||||
const callsRingingEnabled = useMemo(() => getCallsConfig(serverUrl).EnableRinging, [serverUrl]);
|
||||
const [isRegistered, setIsRegistered] = useState(true);
|
||||
|
||||
const appState = useAppState();
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true;
|
||||
if (appState === 'active') {
|
||||
const checkNotificationStatus = async () => {
|
||||
try {
|
||||
const registered = await RNNotifications.isRegisteredForRemoteNotifications();
|
||||
if (isCurrent) {
|
||||
setIsRegistered(registered);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isCurrent) {
|
||||
logError('Error checking notification registration status:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
checkNotificationStatus();
|
||||
}
|
||||
return () => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [appState]);
|
||||
|
||||
const emailIntervalPref = useMemo(() =>
|
||||
getEmailInterval(
|
||||
|
|
@ -124,6 +153,10 @@ const Notifications = ({
|
|||
|
||||
return (
|
||||
<SettingContainer testID='notification_settings'>
|
||||
{!isRegistered &&
|
||||
<NotificationsDisabledNotice
|
||||
testID='notifications-disabled-notice'
|
||||
/>}
|
||||
<SettingItem
|
||||
onPress={goToNotificationSettingsMentions}
|
||||
optionName='mentions'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fireEvent, waitFor} from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
|
||||
import DatabaseManager from '@database/manager/__mocks__';
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import NotificationsDisabledNotice from './index';
|
||||
|
||||
import type Database from '@nozbe/watermelondb/Database';
|
||||
|
||||
const Permissions = jest.mocked(require('react-native-permissions'));
|
||||
|
||||
describe('Notifications Disabled Notice', () => {
|
||||
let database: Database;
|
||||
const testId = 'notifications-disabled-notice';
|
||||
const serverUrl = 'server-1';
|
||||
|
||||
beforeAll(async () => {
|
||||
const server = await TestHelper.setupServerDatabase(serverUrl);
|
||||
database = server.database;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders the notice with correct title and body', async () => {
|
||||
const {getByText} = renderWithEverything(
|
||||
<NotificationsDisabledNotice testID={testId}/>, {database},
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText('Notifications are disabled')).toBeTruthy();
|
||||
expect(getByText(/You will still see mention badges/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the testID on the wrapper View', async () => {
|
||||
const {getByTestId} = renderWithEverything(
|
||||
<NotificationsDisabledNotice testID={testId}/>, {database},
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByTestId(testId)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls Permissions.openSettings when button is pressed', async () => {
|
||||
const {queryByText} = renderWithEverything(
|
||||
<NotificationsDisabledNotice testID={testId}/>, {database},
|
||||
);
|
||||
const button = queryByText('Enable notifications');
|
||||
expect(button).toBeVisible();
|
||||
fireEvent.press(button);
|
||||
expect(Permissions.openSettings).toHaveBeenCalledWith('notifications');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import SectionNotice from '@components/section_notice';
|
||||
import {Screens} from '@constants';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
marginVertical: 16,
|
||||
},
|
||||
});
|
||||
|
||||
type NotificationsDisabledNoticeProps = {
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const NotificationsDisabledNotice = (props: NotificationsDisabledNoticeProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const onEnableNotificationClick = useCallback(() => {
|
||||
Permissions.openSettings('notifications');
|
||||
}, []);
|
||||
|
||||
const primaryButton = useMemo(() => {
|
||||
const text = intl.formatMessage({
|
||||
id: 'user_settings.notifications.notifications_disabled_notice.button',
|
||||
defaultMessage: 'Enable notifications',
|
||||
});
|
||||
return {
|
||||
onClick: onEnableNotificationClick,
|
||||
text,
|
||||
testID: 'enable-notifications-button',
|
||||
};
|
||||
}, [intl, onEnableNotificationClick]);
|
||||
|
||||
return (
|
||||
<View
|
||||
testID={props.testID}
|
||||
style={styles.wrapper}
|
||||
>
|
||||
<SectionNotice
|
||||
text={intl.formatMessage({
|
||||
id: 'user_settings.notifications.notifications_disabled_notice.body',
|
||||
defaultMessage: 'You will still see mention badges within the app, but you will not receive push notifications on your device.',
|
||||
})}
|
||||
title={intl.formatMessage({id: 'user_settings.notifications.notifications_disabled_notice.title', defaultMessage: 'Notifications are disabled'})}
|
||||
primaryButton={primaryButton}
|
||||
type='danger'
|
||||
location={Screens.SETTINGS_NOTIFICATION_PUSH}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationsDisabledNotice;
|
||||
|
|
@ -1364,6 +1364,9 @@
|
|||
"user_profile.custom_status": "Custom Status",
|
||||
"user_profile.system_admin": "System Admin",
|
||||
"user_profile.team_admin": "Team Admin",
|
||||
"user_settings.notifications.notifications_disabled_notice.body": "You will still see mention badges within the app, but you will not receive push notifications on your device.",
|
||||
"user_settings.notifications.notifications_disabled_notice.button": "Enable notifications",
|
||||
"user_settings.notifications.notifications_disabled_notice.title": "Notifications are disabled",
|
||||
"user_settings.notifications.test_notification.body": "Not receiving notifications? Start by sending a test notification to all your devices to check if they’re working as expected. If issues persist, explore ways to solve them with troubleshooting steps.",
|
||||
"user_settings.notifications.test_notification.go_to_docs": "Troubleshooting docs",
|
||||
"user_settings.notifications.test_notification.send_button.error": "Error sending test notification",
|
||||
|
|
|
|||
|
|
@ -380,6 +380,7 @@ jest.mock('react-native-notifications', () => {
|
|||
Notifications: {
|
||||
registerRemoteNotifications: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
isRegisteredForRemoteNotifications: jest.fn(),
|
||||
setDeliveredNotifications: jest.fn((notifications) => {
|
||||
deliveredNotifications = notifications;
|
||||
}),
|
||||
|
|
|
|||
Loading…
Reference in a new issue