From 857fd87a47576fe2c54dd4d54af54321c95574db Mon Sep 17 00:00:00 2001 From: M-ZubairAhmed Date: Fri, 1 Nov 2024 07:02:58 +0530 Subject: [PATCH] [MM-60484 & MM-60485] Add disabled notification banners and section notices for web (#28372) --- .../src/components/announcement_bar/index.ts | 1 - .../__snapshots__/index.test.tsx.snap | 57 +++++++++++++ .../index.test.tsx | 81 +++++++++++-------- .../notification_permission_bar/index.tsx | 61 ++++++-------- ...ification_permission_never_granted_bar.tsx | 59 ++++++++++++++ ...otification_permission_unsupported_bar.tsx | 50 ++++++++++++ .../__snapshots__/panel_body.test.tsx.snap | 2 +- .../components/section_notice/index.test.tsx | 9 +++ .../src/components/section_notice/index.tsx | 12 ++- .../section_notice/section_notice.scss | 4 +- .../section_notice/section_notice_button.tsx | 2 +- .../src/components/setting_item_max.tsx | 2 + .../user_settings_notifications.test.tsx.snap | 6 ++ .../__snapshots__/index.test.tsx.snap | 3 + .../index.test.tsx | 3 + .../index.tsx | 14 +++- .../index.test.tsx | 50 ++++++++++++ .../index.tsx | 35 ++++++++ ...ation_permission_denied_section_notice.tsx | 38 +++++++++ ...ermission_never_granted_section_notice.tsx | 48 +++++++++++ ..._permission_unsupported_section_notice.tsx | 39 +++++++++ .../index.test.tsx | 50 ++++++++++++ .../index.tsx | 51 ++++++++++++ .../user_settings_notifications.test.tsx | 3 + .../src/components/widgets/tag/tag.tsx | 35 ++++---- webapp/channels/src/i18n/en.json | 17 +++- .../sass/components/_announcement-bar.scss | 2 +- .../channels/src/sass/routes/_settings.scss | 10 +++ webapp/channels/src/utils/notifications.ts | 14 +++- 29 files changed, 652 insertions(+), 106 deletions(-) create mode 100644 webapp/channels/src/components/announcement_bar/notification_permission_bar/__snapshots__/index.test.tsx.snap create mode 100644 webapp/channels/src/components/announcement_bar/notification_permission_bar/notification_permission_never_granted_bar.tsx create mode 100644 webapp/channels/src/components/announcement_bar/notification_permission_bar/notification_permission_unsupported_bar.tsx create mode 100644 webapp/channels/src/components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_section_notice/index.test.tsx create mode 100644 webapp/channels/src/components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_section_notice/index.tsx create mode 100644 webapp/channels/src/components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_section_notice/notification_permission_denied_section_notice.tsx create mode 100644 webapp/channels/src/components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_section_notice/notification_permission_never_granted_section_notice.tsx create mode 100644 webapp/channels/src/components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_section_notice/notification_permission_unsupported_section_notice.tsx create mode 100644 webapp/channels/src/components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_title_tag/index.test.tsx create mode 100644 webapp/channels/src/components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_title_tag/index.tsx diff --git a/webapp/channels/src/components/announcement_bar/index.ts b/webapp/channels/src/components/announcement_bar/index.ts index c62d95c57fa..d21ae3dfe98 100644 --- a/webapp/channels/src/components/announcement_bar/index.ts +++ b/webapp/channels/src/components/announcement_bar/index.ts @@ -45,7 +45,6 @@ function mapStateToProps(state: GlobalState) { }; } -// function mapDispatchToProps(dispatch: Dispatch) { const dismissFirstError = dismissError.bind(null, 0); return { diff --git a/webapp/channels/src/components/announcement_bar/notification_permission_bar/__snapshots__/index.test.tsx.snap b/webapp/channels/src/components/announcement_bar/notification_permission_bar/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000..4295d195db4 --- /dev/null +++ b/webapp/channels/src/components/announcement_bar/notification_permission_bar/__snapshots__/index.test.tsx.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NotificationPermissionBar should render the NotificationPermissionNeverGrantedBar when permission is never granted yet 1`] = ` +
+
+
+ + + We need your permission to show notifications in the browser. + + +
+ + × + +
+
+`; + +exports[`NotificationPermissionBar should render the NotificationUnsupportedBar if notifications are not supported 1`] = ` +
+
+
+ + + Your browser does not support browser notifications. + + +
+ + × + +
+
+`; diff --git a/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx index d534ad0194f..2f914ca5da3 100644 --- a/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx +++ b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx @@ -1,19 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {screen, waitFor} from '@testing-library/react'; import React from 'react'; -import {renderWithContext, userEvent} from 'tests/react_testing_utils'; -import {requestNotificationPermission, isNotificationAPISupported} from 'utils/notifications'; +import {renderWithContext, userEvent, screen, waitFor} from 'tests/react_testing_utils'; +import * as utilsNotifications from 'utils/notifications'; import NotificationPermissionBar from './index'; -jest.mock('utils/notifications', () => ({ - requestNotificationPermission: jest.fn(), - isNotificationAPISupported: jest.fn(), -})); - describe('NotificationPermissionBar', () => { const initialState = { entities: { @@ -27,52 +21,71 @@ describe('NotificationPermissionBar', () => { }, }; - beforeEach(() => { - (isNotificationAPISupported as jest.Mock).mockReturnValue(true); - (window as any).Notification = {permission: 'default'}; - }); - afterEach(() => { - jest.clearAllMocks(); - delete (window as any).Notification; + jest.restoreAllMocks(); }); - test('should render the notification bar when conditions are met', () => { - renderWithContext(, initialState); + test('should not render anything if user is not logged in', () => { + const {container} = renderWithContext(); - expect(screen.getByText('We need your permission to show desktop notifications.')).toBeInTheDocument(); - expect(screen.getByText('Enable notifications')).toBeInTheDocument(); + expect(container).toBeEmptyDOMElement(); }); - test('should not render the notification bar if user is not logged in', () => { - renderWithContext(); + test('should render the NotificationUnsupportedBar if notifications are not supported', () => { + jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(false); - expect(screen.queryByText('We need your permission to show desktop notifications.')).not.toBeInTheDocument(); - expect(screen.queryByText('Enable notifications')).not.toBeInTheDocument(); + const {container} = renderWithContext(, initialState); + + expect(container).toMatchSnapshot(); + + expect(screen.queryByText('Your browser does not support browser notifications.')).toBeInTheDocument(); + expect(screen.queryByText('Update your browser')).toBeInTheDocument(); }); - test('should not render the notification bar if Notifications are not supported', () => { - delete (window as any).Notification; - (isNotificationAPISupported as jest.Mock).mockReturnValue(false); + test('should render the NotificationPermissionNeverGrantedBar when permission is never granted yet', () => { + jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(true); + jest.spyOn(utilsNotifications, 'getNotificationPermission').mockReturnValue(utilsNotifications.NotificationPermissionNeverGranted); - renderWithContext(, initialState); + const {container} = renderWithContext(, initialState); + + expect(container).toMatchSnapshot(); - expect(screen.queryByText('We need your permission to show desktop notifications.')).not.toBeInTheDocument(); - expect(screen.queryByText('Enable notifications')).not.toBeInTheDocument(); + expect(screen.getByText('We need your permission to show notifications in the browser.')).toBeInTheDocument(); + expect(screen.getByText('Enable notifications')).toBeInTheDocument(); }); - test('should call requestNotificationPermission and hide the bar when the button is clicked', async () => { - (requestNotificationPermission as jest.Mock).mockResolvedValue('granted'); + test('should call requestNotificationPermission and hide the bar when the button is clicked in NotificationPermissionNeverGrantedBar', async () => { + jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(true); + jest.spyOn(utilsNotifications, 'getNotificationPermission').mockReturnValue(utilsNotifications.NotificationPermissionNeverGranted); + jest.spyOn(utilsNotifications, 'requestNotificationPermission').mockResolvedValue(utilsNotifications.NotificationPermissionGranted); renderWithContext(, initialState); - expect(screen.getByText('We need your permission to show desktop notifications.')).toBeInTheDocument(); + expect(screen.getByText('We need your permission to show notifications in the browser.')).toBeInTheDocument(); await waitFor(async () => { userEvent.click(screen.getByText('Enable notifications')); }); - expect(requestNotificationPermission).toHaveBeenCalled(); - expect(screen.queryByText('We need your permission to show desktop notifications.')).not.toBeInTheDocument(); + expect(utilsNotifications.requestNotificationPermission).toHaveBeenCalled(); + expect(screen.queryByText('We need your permission to show browser notifications.')).not.toBeInTheDocument(); + }); + + test('should not render anything if permission is denied', () => { + jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(true); + jest.spyOn(utilsNotifications, 'getNotificationPermission').mockReturnValue('denied'); + + const {container} = renderWithContext(, initialState); + + expect(container).toBeEmptyDOMElement(); + }); + + test('should not render anything if permission is granted', () => { + jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(true); + jest.spyOn(utilsNotifications, 'getNotificationPermission').mockReturnValue('granted'); + + const {container} = renderWithContext(, initialState); + + expect(container).toBeEmptyDOMElement(); }); }); diff --git a/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx index e4e0c130fd7..a94c4fd4d20 100644 --- a/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx +++ b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx @@ -1,57 +1,42 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback, useState} from 'react'; -import {FormattedMessage} from 'react-intl'; +import React from 'react'; import {useSelector} from 'react-redux'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import AnnouncementBar from 'components/announcement_bar/default_announcement_bar'; +import NotificationPermissionNeverGrantedBar from 'components/announcement_bar/notification_permission_bar/notification_permission_never_granted_bar'; +import NotificationPermissionUnsupportedBar from 'components/announcement_bar/notification_permission_bar/notification_permission_unsupported_bar'; -import {AnnouncementBarTypes} from 'utils/constants'; -import {requestNotificationPermission, isNotificationAPISupported} from 'utils/notifications'; +import { + isNotificationAPISupported, + NotificationPermissionDenied, + NotificationPermissionNeverGranted, + getNotificationPermission, +} from 'utils/notifications'; export default function NotificationPermissionBar() { const isLoggedIn = Boolean(useSelector(getCurrentUserId)); - const [show, setShow] = useState(isNotificationAPISupported() ? Notification.permission === 'default' : false); + if (!isLoggedIn) { + return null; + } - const handleClick = useCallback(async () => { - await requestNotificationPermission(); - setShow(false); - }, []); + // When browser does not support notification API, we show the notification bar to update browser + if (!isNotificationAPISupported()) { + return ; + } - const handleClose = useCallback(() => { - // If the user closes the bar, don't show the notification bar any more for the rest of the session, but - // show it again on app refresh. - setShow(false); - }, []); + // When user has not granted permission, we show the notification bar to request permission + if (getNotificationPermission() === NotificationPermissionNeverGranted) { + return ; + } - if (!show || !isLoggedIn || !isNotificationAPISupported()) { + // When user has denied permission, we don't show since user explicitly denied permission + if (getNotificationPermission() === NotificationPermissionDenied) { return null; } - return ( - - } - ctaText={ - - } - showCTA={true} - showLinkAsButton={true} - onButtonClick={handleClick} - /> - ); + return null; } diff --git a/webapp/channels/src/components/announcement_bar/notification_permission_bar/notification_permission_never_granted_bar.tsx b/webapp/channels/src/components/announcement_bar/notification_permission_bar/notification_permission_never_granted_bar.tsx new file mode 100644 index 00000000000..913882e06bd --- /dev/null +++ b/webapp/channels/src/components/announcement_bar/notification_permission_bar/notification_permission_never_granted_bar.tsx @@ -0,0 +1,59 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useState} from 'react'; +import {FormattedMessage} from 'react-intl'; + +import AnnouncementBar from 'components/announcement_bar/default_announcement_bar'; + +import {AnnouncementBarTypes} from 'utils/constants'; +import {requestNotificationPermission} from 'utils/notifications'; + +export default function NotificationPermissionNeverGrantedBar() { + const [show, setShow] = useState(true); + + const handleClick = useCallback(async () => { + try { + await requestNotificationPermission(); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error requesting notification permission', error); + } finally { + // Dismiss the bar after user makes a choice + setShow(false); + } + }, []); + + const handleClose = useCallback(() => { + // If the user closes the bar, don't show the notification bar any more for the rest of the session, but + // show it again on app refresh. + setShow(false); + }, []); + + if (!show) { + return null; + } + + return ( + + } + ctaText={ + + } + showCTA={true} + showLinkAsButton={true} + onButtonClick={handleClick} + /> + ); +} diff --git a/webapp/channels/src/components/announcement_bar/notification_permission_bar/notification_permission_unsupported_bar.tsx b/webapp/channels/src/components/announcement_bar/notification_permission_bar/notification_permission_unsupported_bar.tsx new file mode 100644 index 00000000000..0caefa83954 --- /dev/null +++ b/webapp/channels/src/components/announcement_bar/notification_permission_bar/notification_permission_unsupported_bar.tsx @@ -0,0 +1,50 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useState} from 'react'; +import {FormattedMessage} from 'react-intl'; + +import AnnouncementBar from 'components/announcement_bar/default_announcement_bar'; + +import {AnnouncementBarTypes} from 'utils/constants'; + +export default function UnsupportedNotificationAnnouncementBar() { + const [show, setShow] = useState(true); + + const handleClick = useCallback(async () => { + window.open('https://mattermost.com/pl/pc-web-requirements', '_blank', 'noopener,noreferrer'); + }, []); + + const handleClose = useCallback(() => { + // If the user closes the bar, don't show the notification bar any more for the rest of the session, but + // show it again on app refresh. + setShow(false); + }, []); + + if (!show) { + return null; + } + + return ( + + } + ctaText={ + + } + showCTA={true} + showLinkAsButton={true} + onButtonClick={handleClick} + /> + ); +} diff --git a/webapp/channels/src/components/drafts/panel/__snapshots__/panel_body.test.tsx.snap b/webapp/channels/src/components/drafts/panel/__snapshots__/panel_body.test.tsx.snap index e726c2436ff..4158c8a1ee6 100644 --- a/webapp/channels/src/components/drafts/panel/__snapshots__/panel_body.test.tsx.snap +++ b/webapp/channels/src/components/drafts/panel/__snapshots__/panel_body.test.tsx.snap @@ -559,7 +559,7 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1 uppercase={true} >
{ renderWithContext(); const primaryButton = screen.getByText(props.primaryButton!.text); const secondaryButton = screen.getByText(props.secondaryButton!.text); + const tertiaryButton = screen.getByText(props.tertiaryButton!.text); const linkButton = screen.getByText(props.linkButton!.text); const closeButton = screen.getByLabelText('Dismiss notice'); expect(primaryButton).toBeInTheDocument(); expect(secondaryButton).toBeInTheDocument(); + expect(tertiaryButton).toBeInTheDocument(); expect(linkButton).toBeInTheDocument(); expect(closeButton).toBeInTheDocument(); expect(screen.queryByText(props.text as string)).toBeInTheDocument(); @@ -52,6 +58,8 @@ describe('PluginAction', () => { expect(props.primaryButton?.onClick).toHaveBeenCalledTimes(1); fireEvent.click(secondaryButton); expect(props.secondaryButton?.onClick).toHaveBeenCalledTimes(1); + fireEvent.click(tertiaryButton); + expect(props.tertiaryButton?.onClick).toHaveBeenCalledTimes(1); fireEvent.click(linkButton); expect(props.linkButton?.onClick).toHaveBeenCalledTimes(1); fireEvent.click(closeButton); @@ -62,6 +70,7 @@ describe('PluginAction', () => { const props = getBaseProps(); props.primaryButton = undefined; props.secondaryButton = undefined; + props.tertiaryButton = undefined; props.linkButton = undefined; props.isDismissable = false; renderWithContext(); diff --git a/webapp/channels/src/components/section_notice/index.tsx b/webapp/channels/src/components/section_notice/index.tsx index 3ca58e905a1..cb04c3085a4 100644 --- a/webapp/channels/src/components/section_notice/index.tsx +++ b/webapp/channels/src/components/section_notice/index.tsx @@ -17,6 +17,7 @@ type Props = { text?: string; primaryButton?: SectionNoticeButtonProp; secondaryButton?: SectionNoticeButtonProp; + tertiaryButton?: SectionNoticeButtonProp; linkButton?: SectionNoticeButtonProp; type?: 'info' | 'success' | 'danger' | 'welcome' | 'warning' | 'hint'; isDismissable?: boolean; @@ -37,6 +38,7 @@ const SectionNotice = ({ text, primaryButton, secondaryButton, + tertiaryButton, linkButton, type = 'info', isDismissable, @@ -45,7 +47,7 @@ const SectionNotice = ({ const intl = useIntl(); const icon = iconByType[type]; const showDismiss = Boolean(isDismissable && onDismissClick); - const hasButtons = Boolean(primaryButton || secondaryButton || linkButton); + const hasButtons = Boolean(primaryButton || secondaryButton || tertiaryButton || linkButton); return (
@@ -64,9 +66,15 @@ const SectionNotice = ({ {secondaryButton && } + {tertiaryButton && ( + + )} {linkButton && { className={`section-max form-horizontal ${this.props.containerStyle}`} > {title} + {this.props.extraContentBeforeSettingList}
Desktop and mobile notifications +