[MM-60484 & MM-60485] Add disabled notification banners and section notices for web (#28372)

This commit is contained in:
M-ZubairAhmed
2024-11-01 07:02:58 +05:30
committed by GitHub
parent 2d2c039a27
commit 857fd87a47
29 changed files with 661 additions and 115 deletions

View File

@@ -45,7 +45,6 @@ function mapStateToProps(state: GlobalState) {
};
}
//
function mapDispatchToProps(dispatch: Dispatch) {
const dismissFirstError = dismissError.bind(null, 0);
return {

View File

@@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotificationPermissionBar should render the NotificationPermissionNeverGrantedBar when permission is never granted yet 1`] = `
<div>
<div
class="_StyledDiv-BRlth ksRYxX announcement-bar"
>
<div
class="announcement-bar__text"
>
<i
class="icon icon-alert-circle-outline"
/>
<span>
We need your permission to show notifications in the browser.
</span>
<button>
Enable notifications
</button>
</div>
<a
class="announcement-bar__close"
href="#"
>
×
</a>
</div>
</div>
`;
exports[`NotificationPermissionBar should render the NotificationUnsupportedBar if notifications are not supported 1`] = `
<div>
<div
class="_StyledDiv-BRlth ksRYxX announcement-bar"
>
<div
class="announcement-bar__text"
>
<i
class="icon icon-alert-circle-outline"
/>
<span>
Your browser does not support browser notifications.
</span>
<button>
Update your browser
</button>
</div>
<a
class="announcement-bar__close"
href="#"
>
×
</a>
</div>
</div>
`;

View File

@@ -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(<NotificationPermissionBar/>, initialState);
test('should not render anything if user is not logged in', () => {
const {container} = renderWithContext(<NotificationPermissionBar/>);
expect(screen.getByText('We need your permission to show desktop notifications.')).toBeInTheDocument();
expect(container).toBeEmptyDOMElement();
});
test('should render the NotificationUnsupportedBar if notifications are not supported', () => {
jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(false);
const {container} = renderWithContext(<NotificationPermissionBar/>, initialState);
expect(container).toMatchSnapshot();
expect(screen.queryByText('Your browser does not support browser notifications.')).toBeInTheDocument();
expect(screen.queryByText('Update your browser')).toBeInTheDocument();
});
test('should render the NotificationPermissionNeverGrantedBar when permission is never granted yet', () => {
jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(true);
jest.spyOn(utilsNotifications, 'getNotificationPermission').mockReturnValue(utilsNotifications.NotificationPermissionNeverGranted);
const {container} = renderWithContext(<NotificationPermissionBar/>, initialState);
expect(container).toMatchSnapshot();
expect(screen.getByText('We need your permission to show notifications in the browser.')).toBeInTheDocument();
expect(screen.getByText('Enable notifications')).toBeInTheDocument();
});
test('should not render the notification bar if user is not logged in', () => {
renderWithContext(<NotificationPermissionBar/>);
expect(screen.queryByText('We need your permission to show desktop notifications.')).not.toBeInTheDocument();
expect(screen.queryByText('Enable notifications')).not.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 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(<NotificationPermissionBar/>, initialState);
expect(screen.queryByText('We need your permission to show desktop notifications.')).not.toBeInTheDocument();
expect(screen.queryByText('Enable notifications')).not.toBeInTheDocument();
});
test('should call requestNotificationPermission and hide the bar when the button is clicked', async () => {
(requestNotificationPermission as jest.Mock).mockResolvedValue('granted');
renderWithContext(<NotificationPermissionBar/>, 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(<NotificationPermissionBar/>, 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(<NotificationPermissionBar/>, initialState);
expect(container).toBeEmptyDOMElement();
});
});

View File

@@ -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);
const handleClick = useCallback(async () => {
await requestNotificationPermission();
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 || !isLoggedIn || !isNotificationAPISupported()) {
if (!isLoggedIn) {
return null;
}
return (
<AnnouncementBar
showCloseButton={true}
handleClose={handleClose}
type={AnnouncementBarTypes.ANNOUNCEMENT}
message={
<FormattedMessage
id='announcement_bar.notification.needs_permission'
defaultMessage='We need your permission to show desktop notifications.'
/>
}
ctaText={
<FormattedMessage
id='announcement_bar.notification.enable_notifications'
defaultMessage='Enable notifications'
/>
}
showCTA={true}
showLinkAsButton={true}
onButtonClick={handleClick}
/>
);
// When browser does not support notification API, we show the notification bar to update browser
if (!isNotificationAPISupported()) {
return <NotificationPermissionUnsupportedBar/>;
}
// When user has not granted permission, we show the notification bar to request permission
if (getNotificationPermission() === NotificationPermissionNeverGranted) {
return <NotificationPermissionNeverGrantedBar/>;
}
// When user has denied permission, we don't show since user explicitly denied permission
if (getNotificationPermission() === NotificationPermissionDenied) {
return null;
}
return null;
}

View File

@@ -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 (
<AnnouncementBar
showCloseButton={true}
handleClose={handleClose}
type={AnnouncementBarTypes.ANNOUNCEMENT}
message={
<FormattedMessage
id='announcementBar.notification.permissionNeverGrantedBar.message'
defaultMessage='We need your permission to show notifications in the browser.'
/>
}
ctaText={
<FormattedMessage
id='announcementBar.notification.permissionNeverGrantedBar.cta'
defaultMessage='Enable notifications'
/>
}
showCTA={true}
showLinkAsButton={true}
onButtonClick={handleClick}
/>
);
}

View File

@@ -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 (
<AnnouncementBar
showCloseButton={true}
type={AnnouncementBarTypes.ANNOUNCEMENT}
handleClose={handleClose}
message={
<FormattedMessage
id='announcementBar.notification.unsupportedBar.message'
defaultMessage='Your browser does not support browser notifications.'
/>
}
ctaText={
<FormattedMessage
id='announcementBar.notification.unsupportedBar.cta'
defaultMessage='Update your browser'
/>
}
showCTA={true}
showLinkAsButton={true}
onButtonClick={handleClick}
/>
);
}

View File

@@ -559,7 +559,7 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1
uppercase={true}
>
<div
className="TagWrapper-keYggn hpsCJu Tag Tag--info Tag--xs"
className="TagWrapper-keYggn gnIgwl Tag Tag--info Tag--xs"
>
<AlertCircleOutlineIcon
size={10}

View File

@@ -23,6 +23,10 @@ function getBaseProps(): Props {
onClick: jest.fn(),
text: 'secondary button title',
},
tertiaryButton: {
onClick: jest.fn(),
text: 'tertiary button title',
},
linkButton: {
onClick: jest.fn(),
text: 'link button title',
@@ -39,11 +43,13 @@ describe('PluginAction', () => {
renderWithContext(<SectionNotice {...props}/>);
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(<SectionNotice {...props}/>);

View File

@@ -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 (
<div className={classNames('sectionNoticeContainer', type)}>
<div className={'sectionNoticeContent'}>
@@ -64,9 +66,15 @@ const SectionNotice = ({
{secondaryButton &&
<SectionNoticeButton
button={secondaryButton}
buttonClass='btn-tertiary'
buttonClass='btn-secondary'
/>
}
{tertiaryButton && (
<SectionNoticeButton
button={tertiaryButton}
buttonClass='btn-tertiary'
/>
)}
{linkButton &&
<SectionNoticeButton
button={linkButton}

View File

@@ -74,7 +74,7 @@
}
.sectionNoticeIcon {
font-size: 20px;
font-size: 24px;
&.info, &.hint {
color: var(--sidebar-text-active-border);
@@ -99,7 +99,7 @@
font-family: 'Open Sans';
font-size: 14px;
font-weight: 600;
line-height: 20px;
line-height: 24px;
&.welcome {
font-family: 'Metropolis';

View File

@@ -8,7 +8,7 @@ import type {SectionNoticeButtonProp} from './types';
type Props = {
button: SectionNoticeButtonProp;
buttonClass: 'btn-primary' | 'btn-tertiary' | 'btn-link';
buttonClass: 'btn-primary' | 'btn-secondary' | 'btn-tertiary' | 'btn-link';
}
const SectionNoticeButton = ({

View File

@@ -45,6 +45,7 @@ type Props = {
submitExtra?: ReactNode;
saving?: boolean;
title?: ReactNode;
extraContentBeforeSettingList?: ReactNode;
isFullWidth?: boolean;
cancelButtonText?: ReactNode;
shiftEnter?: boolean;
@@ -224,6 +225,7 @@ export default class SettingItemMax extends React.PureComponent<Props> {
className={`section-max form-horizontal ${this.props.containerStyle}`}
>
{title}
{this.props.extraContentBeforeSettingList}
<div
className={classNames('sectionContent', {
'col-sm-12': this.props.isFullWidth,

View File

@@ -92,6 +92,7 @@ Object {
id="desktopAndMobileTitle"
>
Desktop and mobile notifications
<div />
</h4>
<button
aria-expanded="false"
@@ -351,6 +352,7 @@ Object {
id="desktopAndMobileTitle"
>
Desktop and mobile notifications
<div />
</h4>
<button
aria-expanded="false"
@@ -669,6 +671,7 @@ Object {
id="desktopAndMobileTitle"
>
Desktop and mobile notifications
<div />
</h4>
<button
aria-expanded="false"
@@ -931,6 +934,7 @@ Object {
id="desktopAndMobileTitle"
>
Desktop and mobile notifications
<div />
</h4>
<button
aria-expanded="false"
@@ -1252,6 +1256,7 @@ Object {
id="desktopAndMobileTitle"
>
Desktop and mobile notifications
<div />
</h4>
<button
aria-expanded="false"
@@ -1476,6 +1481,7 @@ Object {
id="desktopAndMobileTitle"
>
Desktop and mobile notifications
<div />
</h4>
<button
aria-expanded="false"

View File

@@ -11,6 +11,7 @@ exports[`DesktopNotificationSettings should match snapshot, on max setting 1`] =
>
Desktop and mobile notifications
</h4>
<div />
<div
class="sectionContent col-sm-10 col-sm-offset-2"
>
@@ -179,6 +180,7 @@ exports[`DesktopNotificationSettings should match snapshot, on min setting 1`] =
id="desktopAndMobileTitle"
>
Desktop and mobile notifications
<div />
</h4>
<button
aria-expanded="false"
@@ -214,6 +216,7 @@ exports[`DesktopNotificationSettings should not show desktop thread notification
>
Desktop and mobile notifications
</h4>
<div />
<div
class="sectionContent col-sm-10 col-sm-offset-2"
>

View File

@@ -17,6 +17,9 @@ import DesktopNotificationSettings, {
const validNotificationLevels = Object.values(NotificationLevels);
jest.mock('components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_section_notice', () => () => <div/>);
jest.mock('components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_title_tag', () => () => <div/>);
describe('DesktopNotificationSettings', () => {
const baseProps: Props = {
active: true,

View File

@@ -12,6 +12,8 @@ import type {UserNotifyProps} from '@mattermost/types/users';
import SettingItemMax from 'components/setting_item_max';
import SettingItemMin from 'components/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min';
import NotificationPermissionSectionNotice from 'components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_section_notice';
import NotificationPermissionTitleTag from 'components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_title_tag';
import Constants, {NotificationLevels, UserSettingsNotificationSections} from 'utils/constants';
@@ -322,6 +324,7 @@ function DesktopAndMobileNotificationSettings({
saving={saving}
serverError={error}
updateSection={handleChangeForMaxSection}
extraContentBeforeSettingList={<NotificationPermissionSectionNotice/>}
/>
);
}
@@ -330,10 +333,13 @@ function DesktopAndMobileNotificationSettings({
<SettingItemMin
ref={editButtonRef}
title={
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.title'
defaultMessage='Desktop and mobile notifications'
/>
<>
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.title'
defaultMessage='Desktop and mobile notifications'
/>
<NotificationPermissionTitleTag/>
</>
}
describe={getCollapsedText(desktopActivity, pushActivity)}
section={UserSettingsNotificationSections.DESKTOP_AND_MOBILE}

View File

@@ -0,0 +1,50 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import * as utilsNotifications from 'utils/notifications';
import NotificationPermissionSectionNotice from './index';
describe('NotificationPermissionSectionNotice', () => {
afterEach(() => {
jest.restoreAllMocks();
});
test('should render "Unsupported" notice when notifications are not supported', () => {
jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(false);
renderWithContext(<NotificationPermissionSectionNotice/>);
expect(screen.getByText('Browser notifications unsupported')).toBeInTheDocument();
});
test('should render "Never granted" notice when notifications are never granted', () => {
jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(true);
jest.spyOn(utilsNotifications, 'getNotificationPermission').mockReturnValue('default');
renderWithContext(<NotificationPermissionSectionNotice/>);
expect(screen.getByText('Browser notifications are disabled')).toBeInTheDocument();
});
test('should render "Denied" notice when notifications are denied', () => {
jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(true);
jest.spyOn(utilsNotifications, 'getNotificationPermission').mockReturnValue('denied');
renderWithContext(<NotificationPermissionSectionNotice/>);
expect(screen.getByText('Browser notification permission was denied')).toBeInTheDocument();
});
test('should render nothing when notifications are granted', () => {
jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(true);
jest.spyOn(utilsNotifications, 'getNotificationPermission').mockReturnValue('granted');
const {container} = renderWithContext(<NotificationPermissionSectionNotice/>);
expect(container).toBeEmptyDOMElement();
});
});

View File

@@ -0,0 +1,35 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react';
import NotificationPermissionDeniedNotice from 'components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_section_notice/notification_permission_denied_section_notice';
import NotificationPermissionNeverGrantedNotice from 'components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_section_notice/notification_permission_never_granted_section_notice';
import NotificationPermissionUnsupportedSectionNotice from 'components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_section_notice/notification_permission_unsupported_section_notice';
import {getNotificationPermission, isNotificationAPISupported, NotificationPermissionDenied, NotificationPermissionNeverGranted} from 'utils/notifications';
export default function NotificationPermissionSectionNotice() {
const isNotificationSupported = isNotificationAPISupported();
const [notificationPermission, setNotificationPermission] = useState(getNotificationPermission());
function handleRequestNotificationClicked(permission: NotificationPermission) {
setNotificationPermission(permission);
}
if (!isNotificationSupported) {
return <NotificationPermissionUnsupportedSectionNotice/>;
}
if (isNotificationSupported && notificationPermission === NotificationPermissionNeverGranted) {
return <NotificationPermissionNeverGrantedNotice onCtaButtonClick={handleRequestNotificationClicked}/>;
}
if (isNotificationSupported && notificationPermission === NotificationPermissionDenied) {
return <NotificationPermissionDeniedNotice/>;
}
return null;
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import SectionNotice from 'components/section_notice';
export default function NotificationPermissionDeniedSectionNotice() {
const intl = useIntl();
const handleClick = useCallback(() => {
window.open('https://mattermost.com/pl/manage-notifications', '_blank', 'noopener,noreferrer');
}, []);
return (
<div className='extraContentBeforeSettingList'>
<SectionNotice
type='danger'
title={intl.formatMessage({
id: 'user.settings.notifications.desktopAndMobile.notificationSection.permissionDenied.title',
defaultMessage: 'Browser notification permission was denied',
})}
text={intl.formatMessage({
id: 'user.settings.notifications.desktopAndMobile.notificationSection.permissionDenied.message',
defaultMessage: 'You\'re missing important message and call notifications from Mattermost. To start receiving notifications, please enable notifications for Mattermost in your browser settings.',
})}
tertiaryButton={{
text: intl.formatMessage({
id: 'user.settings.notifications.desktopAndMobile.notificationSection.permissionDenied.button',
defaultMessage: 'How to enable notifications',
}),
onClick: handleClick,
}}
/>
</div>
);
}

View File

@@ -0,0 +1,48 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import SectionNotice from 'components/section_notice';
import {requestNotificationPermission} from 'utils/notifications';
type Props = {
onCtaButtonClick: (permission: NotificationPermission) => void;
}
export default function NotificationPermissionNeverGrantedSectionNotice(props: Props) {
const intl = useIntl();
const handleClick = useCallback(async () => {
const permission = await requestNotificationPermission();
if (permission) {
props.onCtaButtonClick(permission);
}
}, [props.onCtaButtonClick]);
return (
<div className='extraContentBeforeSettingList'>
<SectionNotice
type='danger'
title={intl.formatMessage({
id: 'user.settings.notifications.desktopAndMobile.notificationSection.permissionNeverGranted.title',
defaultMessage: 'Browser notifications are disabled',
})}
text={intl.formatMessage({
id: 'user.settings.notifications.desktopAndMobile.notificationSection.permissionNeverGranted.message',
defaultMessage: 'You\'re missing important message and call notifications from Mattermost. Mattermost notifications are disabled by this browser.',
})}
primaryButton={{
text: intl.formatMessage({
id: 'user.settings.notifications.desktopAndMobile.notificationSection.permissionNeverGranted.button',
defaultMessage: 'Enable notifications',
}),
onClick: handleClick,
}}
/>
</div>
);
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import SectionNotice from 'components/section_notice';
export default function NotificationPermissionUnsupportedSectionNotice() {
const intl = useIntl();
const handleClick = useCallback(async () => {
window.open('https://mattermost.com/pl/pc-web-requirements', '_blank', 'noopener,noreferrer');
}, []);
return (
<div className='extraContentBeforeSettingList'>
<SectionNotice
type='danger'
title={intl.formatMessage({
id: 'user.settings.notifications.desktopAndMobile.notificationSection.permissionUnsupported.title',
defaultMessage: 'Browser notifications unsupported',
})}
text={intl.formatMessage({
id: 'user.settings.notifications.desktopAndMobile.notificationSection.permissionUnsupported.message',
defaultMessage: 'You\'re missing important message and call notifications from Mattermost. To start receiving notifications, please update to a supported browser.',
})}
tertiaryButton={{
text: intl.formatMessage({
id: 'user.settings.notifications.desktopAndMobile.notificationSection.permissionUnsupported.button',
defaultMessage: 'Update your browser',
}),
onClick: handleClick,
}}
/>
</div>
);
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import * as utilsNotifications from 'utils/notifications';
import NotificationPermissionTitleTag from './index';
describe('NotificationPermissionTitleTag', () => {
afterEach(() => {
jest.restoreAllMocks();
});
test('should render "Not supported" tag when notifications are not supported', () => {
jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(false);
renderWithContext(<NotificationPermissionTitleTag/>);
expect(screen.queryByText('Not supported')).toBeInTheDocument();
});
test('should render "Permission required" tag when permission is never granted yet', () => {
jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(true);
jest.spyOn(utilsNotifications, 'getNotificationPermission').mockReturnValue(utilsNotifications.NotificationPermissionNeverGranted);
renderWithContext(<NotificationPermissionTitleTag/>);
expect(screen.queryByText('Permission required')).toBeInTheDocument();
});
test('should render "Permission required" tag when permission is denied', () => {
jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(true);
jest.spyOn(utilsNotifications, 'getNotificationPermission').mockReturnValue(utilsNotifications.NotificationPermissionDenied);
renderWithContext(<NotificationPermissionTitleTag/>);
expect(screen.queryByText('Permission required')).toBeInTheDocument();
});
test('should render nothing when permission is granted', () => {
jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(true);
jest.spyOn(utilsNotifications, 'getNotificationPermission').mockReturnValue(utilsNotifications.NotificationPermissionGranted);
const {container} = renderWithContext(<NotificationPermissionTitleTag/>);
expect(container).toBeEmptyDOMElement();
});
});

View File

@@ -0,0 +1,51 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import Tag from 'components/widgets/tag/tag';
import {
getNotificationPermission,
isNotificationAPISupported,
NotificationPermissionDenied,
NotificationPermissionNeverGranted,
} from 'utils/notifications';
export default function NotificationPermissionTitleTag() {
const {formatMessage} = useIntl();
if (!isNotificationAPISupported()) {
return (
<Tag
size='sm'
variant='danger'
icon='alert-outline'
text={formatMessage({
id: 'user.settings.notifications.desktopAndMobile.notificationSection.noPermissionIssueTag',
defaultMessage: 'Not supported',
})}
/>
);
}
if (
getNotificationPermission() === NotificationPermissionNeverGranted ||
getNotificationPermission() === NotificationPermissionDenied
) {
return (
<Tag
size='sm'
variant='dangerDim'
icon='alert-outline'
text={formatMessage({
id: 'user.settings.notifications.desktopAndMobile.notificationSection.permissionIssueTag',
defaultMessage: 'Permission required',
})}
/>
);
}
return null;
}

View File

@@ -10,6 +10,9 @@ import {TestHelper} from 'utils/test_helper';
import UserSettingsNotifications, {areDesktopAndMobileSettingsDifferent} from './user_settings_notifications';
jest.mock('components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_section_notice', () => () => <div/>);
jest.mock('components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_title_tag', () => () => <div/>);
describe('components/user_settings/display/UserSettingsDisplay', () => {
const defaultProps = {
user: TestHelper.getUserMock({id: 'user_id'}),

View File

@@ -9,7 +9,7 @@ import styled, {css} from 'styled-components';
import glyphMap from '@mattermost/compass-icons/components';
import type {IconGlyphTypes} from '@mattermost/compass-icons/IconGlyphs';
export type TagVariant = 'info' | 'success' | 'warning' | 'danger';
export type TagVariant = 'info' | 'success' | 'warning' | 'danger' | 'dangerDim';
export type TagSize = 'xs' | 'sm' | 'md' | 'lg'
@@ -26,10 +26,6 @@ type Props = {
type TagWrapperProps = Required<Pick<Props, 'uppercase'>>;
const TagWrapper = styled.div<TagWrapperProps>`
--tag-bg: var(--semantic-color-general);
--tag-bg-opacity: 0.08;
--tag-color: var(--semantic-color-general);
appearance: none;
display: inline-flex;
@@ -84,38 +80,39 @@ const TagWrapper = styled.div<TagWrapperProps>`
padding: 2px 5px;
}
&.Tag--info,
&.Tag--success,
&.Tag--warning,
&.Tag--danger {
--tag-bg-opacity: 1;
--tag-color: 255, 255, 255;
}
background: rgba(var(--semantic-color-general), 0.08);
color: rgb(var(--semantic-color-general));
&.Tag--info {
--tag-bg: var(--semantic-color-info);
background: rgba(var(--semantic-color-info), 1);
color: rgb(255, 255, 255);
}
&.Tag--success {
--tag-bg: var(--semantic-color-success);
background: rgba(var(--semantic-color-success), 1);
color: rgb(255, 255, 255);
}
&.Tag--warning {
--tag-bg: var(--semantic-color-warning);
background: rgba(var(--semantic-color-warning), 1);
color: rgb(255, 255, 255);
}
&.Tag--danger {
--tag-bg: var(--semantic-color-danger);
background: rgba(var(--semantic-color-danger), 1);
color: rgb(255, 255, 255);
}
background: rgba(var(--tag-bg), var(--tag-bg-opacity));
color: rgb(var(--tag-color));
&.Tag--dangerDim {
background: rgba(var(--semantic-color-danger), 0.08);
color: rgb(var(--semantic-color-danger));
}
${({onClick}) => typeof onClick === 'function' && (
css`
&:hover,
&:focus {
background: rgba(var(--tag-bg), 0.08);
background: rgba(var(--semantic-color-general), 0.08);
cursor: pointer;
}
`

View File

@@ -2892,12 +2892,14 @@
"announcement_bar.error.trial_license_expiring_last_day": "This is the last day of your free trial. Purchase a license now to continue using Mattermost Professional and Enterprise features.",
"announcement_bar.error.trial_license_expiring_last_day.short": "This is the last day of your free trial.",
"announcement_bar.notification.email_verified": "Email verified",
"announcement_bar.notification.enable_notifications": "Enable notifications",
"announcement_bar.notification.needs_permission": "We need your permission to show desktop notifications.",
"announcement_bar.warn.contact_support_email": "<a>Contact support</a>.",
"announcement_bar.warn.contact_support_text": "To renew your license, contact support at support@mattermost.com.",
"announcement_bar.warn.no_internet_connection": "Looks like you do not have access to the internet.",
"announcement_bar.warn.renew_license_contact_sales": "Contact sales",
"announcementBar.notification.permissionNeverGrantedBar.cta": "Enable notifications",
"announcementBar.notification.permissionNeverGrantedBar.message": "We need your permission to show notifications in the browser.",
"announcementBar.notification.unsupportedBar.cta": "Update your browser",
"announcementBar.notification.unsupportedBar.message": "Your browser does not support browser notifications.",
"api.channel.add_guest.added": "{addedUsername} added to the channel as a guest by {username}.",
"api.channel.add_member.added": "{addedUsername} added to the channel by {username}.",
"api.channel.delete_channel.archived": "{username} archived the channel.",
@@ -5661,6 +5663,17 @@
"user.settings.notifications.desktopAndMobile.noneDesktopButMobileMentions": "Never on desktop; mentions, direct messages, and group messages on mobile",
"user.settings.notifications.desktopAndMobile.noneForDesktopAndMobile": "Never",
"user.settings.notifications.desktopAndMobile.nothing": "Nothing",
"user.settings.notifications.desktopAndMobile.notificationSection.noPermissionIssueTag": "Not supported",
"user.settings.notifications.desktopAndMobile.notificationSection.permissionDenied.button": "How to enable notifications",
"user.settings.notifications.desktopAndMobile.notificationSection.permissionDenied.message": "You're missing important message and call notifications from Mattermost. To start receiving notifications, please enable notifications for Mattermost in your browser settings.",
"user.settings.notifications.desktopAndMobile.notificationSection.permissionDenied.title": "Browser notification permission was denied",
"user.settings.notifications.desktopAndMobile.notificationSection.permissionIssueTag": "Permission required",
"user.settings.notifications.desktopAndMobile.notificationSection.permissionNeverGranted.button": "Enable notifications",
"user.settings.notifications.desktopAndMobile.notificationSection.permissionNeverGranted.message": "You're missing important message and call notifications from Mattermost. Mattermost notifications are disabled by this browser.",
"user.settings.notifications.desktopAndMobile.notificationSection.permissionNeverGranted.title": "Browser notifications are disabled",
"user.settings.notifications.desktopAndMobile.notificationSection.permissionUnsupported.button": "Update your browser",
"user.settings.notifications.desktopAndMobile.notificationSection.permissionUnsupported.message": "You're missing important message and call notifications from Mattermost. To start receiving notifications, please update to a supported browser.",
"user.settings.notifications.desktopAndMobile.notificationSection.permissionUnsupported.title": "Browser notifications unsupported",
"user.settings.notifications.desktopAndMobile.notifyForDesktopthreads": "Notify me about replies to threads I'm following",
"user.settings.notifications.desktopAndMobile.notifyForMobilethreads": "Notify me on mobile about replies to threads I'm following",
"user.settings.notifications.desktopAndMobile.noValidSettings": "Configure desktop and mobile settings",

View File

@@ -107,7 +107,7 @@
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 16px;
line-height: 14px;
}
}

View File

@@ -255,6 +255,8 @@
.section-max {
@include pie-clearfix;
display: flex;
flex-direction: column;
padding: 12px;
margin-bottom: 0;
background: rgba(var(--center-channel-color-rgb), 0.04);
@@ -267,6 +269,11 @@
line-height: 20px;
}
.extraContentBeforeSettingList {
width: 100%;
padding-block-start: 20px;
}
.sectionContent {
padding: 20px 0 0 0;
@@ -774,10 +781,13 @@
}
.section-min__title {
display: flex;
flex-direction: row;
padding-right: 50px;
margin: 0 0 5px;
font-size: 14px;
font-weight: 600;
gap: 8px;
line-height: 20px;
&.isDisabled {

View File

@@ -105,7 +105,15 @@ export function isNotificationAPISupported() {
return ('Notification' in window) && (typeof Notification.requestPermission === 'function');
}
export async function requestNotificationPermission() {
export function getNotificationPermission(): NotificationPermission | null {
if (!isNotificationAPISupported()) {
return null;
}
return Notification.permission;
}
export async function requestNotificationPermission(): Promise<NotificationPermission | null> {
if (!isNotificationAPISupported()) {
return null;
}
@@ -117,3 +125,7 @@ export async function requestNotificationPermission() {
return null;
}
}
export const NotificationPermissionNeverGranted = 'default';
export const NotificationPermissionGranted = 'granted';
export const NotificationPermissionDenied = 'denied';