[MM-56695] Consolidate desktop and mobile Notifications to one and a new desktop notification sound settings (#26198)

This commit is contained in:
M-ZubairAhmed 2024-04-08 18:59:05 +00:00 committed by GitHub
parent a81ee87832
commit 9698cfcc19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 2395 additions and 2780 deletions

View File

@ -31,12 +31,12 @@ describe('Verify Accessibility Support in different sections in Settings and Pro
const settings = {
notifications: [
{key: 'desktop', label: 'Desktop Notifications', type: 'radio'},
{key: 'email', label: 'Email Notifications', type: 'radio'},
{key: 'push', label: 'Mobile Push Notifications', type: 'radio'},
{key: 'keysWithNotification', label: 'Keywords That Trigger Notifications', type: 'checkbox'},
{key: 'keysWithHighlight', label: 'Keywords That Get Highlighted (Without Notifications)', type: 'checkbox'},
{key: 'comments', label: 'Reply Notifications', type: 'radio'},
{key: 'desktopAndMobile', label: 'Desktop and mobile notifications', type: 'radio'},
{key: 'desktopNotificationSound', label: 'Desktop notification sounds', type: 'radio'},
{key: 'email', label: 'Email notifications', type: 'radio'},
{key: 'keywordsAndMentions', label: 'Keywords that trigger notifications', type: 'checkbox'},
{key: 'keywordsAndHighlight', label: 'Keywords that get highlighted (without notifications)', type: 'checkbox'},
{key: 'replyNotifications', label: 'Reply notifications', type: 'radio'},
],
display: [
{key: 'theme', label: 'Theme', type: 'radio'},
@ -147,18 +147,6 @@ describe('Verify Accessibility Support in different sections in Settings and Pro
verifySettings(settings.advanced);
});
it('MM-T1481 Verify Correct Radio button behavior in Settings and Profile', () => {
cy.visit(url);
cy.postMessage('hello');
cy.uiOpenSettingsModal();
cy.get('#notificationsButton').click();
cy.get('#desktopEdit').click();
cy.get('#desktopNotificationAllActivity').check().should('be.checked').tab().check();
cy.get('#desktopNotificationMentions').should('be.checked').tab().check();
cy.get('#desktopNotificationNever').should('be.checked');
});
it('MM-T1482 Input fields in Settings and Profile should read labels', () => {
cy.visit(url);
cy.postMessage('hello');

View File

@ -48,7 +48,7 @@ describe('Auto Response In DMs', () => {
// # Open 'Settings' modal and view 'Notifications'
cy.uiOpenSettingsModal().within(() => {
// # Click on 'Edit' for 'Automatic Direct Message Replies
cy.get('#auto-responderEdit').should('exist').scrollIntoView().and('be.visible').click();
cy.get('#autoResponderEdit').should('exist').scrollIntoView().and('be.visible').click();
// # Click on 'Enabled' checkbox
cy.get('#autoResponderActive').should('be.visible').click();

View File

@ -23,8 +23,8 @@ describe('Notifications', () => {
// # Open 'Settings' modal
cy.uiOpenSettingsModal().within(() => {
// # Open 'Keywords That Trigger Notifications' setting and uncheck all the checkboxes
cy.findByRole('heading', {name: 'Keywords That Trigger Notifications'}).should('be.visible').click();
// # Open 'Keywords that trigger notifications' setting and uncheck all the checkboxes
cy.findByRole('heading', {name: 'Keywords that trigger notifications'}).should('be.visible').click();
cy.findByRole('checkbox', {name: `Your case-sensitive first name "${otherUser.first_name}"`}).should('not.be.checked');
cy.findByRole('checkbox', {name: `Your non case-sensitive username "${otherUser.username}"`}).should('not.be.checked');
cy.findByRole('checkbox', {name: 'Channel-wide mentions "@channel", "@all", "@here"'}).click().should('not.be.checked');

View File

@ -252,7 +252,7 @@ function setNotificationSettings(desiredSettings = {first: true, username: true,
cy.findAllByText('Notifications').should('be.visible');
// Open up 'Words that trigger mentions' sub-section
cy.findByText('Keywords That Trigger Notifications').
cy.findByText('Keywords that trigger notifications').
scrollIntoView().
click();

View File

@ -50,8 +50,8 @@ describe('Notifications', () => {
// # Open 'Notifications' of 'Settings' modal
cy.uiOpenSettingsModal().within(() => {
// # Open 'Email Notifications' setting and set to 'Immediately'
cy.findByRole('heading', {name: 'Email Notifications'}).should('be.visible').click();
// # Open 'Email notifications' setting and set to 'Immediately'
cy.findByRole('heading', {name: 'Email notifications'}).should('be.visible').click();
cy.findByRole('radio', {name: 'Immediately'}).click().should('be.checked');
// # Save then close the modal

View File

@ -29,8 +29,8 @@ describe('Notifications', () => {
// # Open 'Settings' modal
cy.uiOpenSettingsModal().within(() => {
// # Open 'Keywords That Trigger Notifications' setting
cy.findByRole('heading', {name: 'Keywords That Trigger Notifications'}).should('be.visible').click();
// # Open 'Keywords that trigger notifications' setting
cy.findByRole('heading', {name: 'Keywords that trigger notifications'}).should('be.visible').click();
// * As otherUser, ensure that 'Your non-case sensitive username' is not checked
cy.findByRole('checkbox', {name: `Your non case-sensitive username "${otherUser.username}"`}).should('not.be.checked');

View File

@ -48,7 +48,7 @@ describe('Desktop notifications', () => {
spyNotificationAs('withoutNotification', 'granted');
// # Ensure notifications are set up to fire a desktop notification if are mentioned.
changeDesktopNotificationAs('#desktopNotificationMentions');
changeDesktopNotificationAs('mentions');
cy.apiGetChannelByName(testTeam.name, 'Off-Topic').then(({channel}) => {
// # Logout the user
@ -100,7 +100,7 @@ describe('Desktop notifications', () => {
const expected = '@' + otherUser.username + ': I\'m hungry :taco: Mattermost';
// # Ensure notifications are set up to fire a desktop notification if are mentioned.
changeDesktopNotificationAs('#desktopNotificationAllActivity');
changeDesktopNotificationAs('all');
cy.apiGetChannelByName(testTeam.name, 'Off-Topic').then(({channel}) => {
// # Have another user send a post.
@ -147,7 +147,7 @@ describe('Desktop notifications', () => {
const expected = '@' + otherUser.username + ' did something new';
// # Ensure notifications are set up to fire a desktop notification for all activity.
changeDesktopNotificationAs('#desktopNotificationAllActivity');
changeDesktopNotificationAs('all');
cy.apiGetChannelByName(testTeam.name, 'Off-Topic').then(({channel}) => {
// # Have another user send a post.
@ -169,7 +169,7 @@ describe('Desktop notifications', () => {
spyNotificationAs('withNotification', 'granted');
// # Ensure notifications are set up to fire a desktop notification if are mentioned
changeDesktopNotificationAs('#desktopNotificationMentions');
changeDesktopNotificationAs('mentions');
// # Ensure display settings are set to "Show username"
changeTeammateNameDisplayAs('#name_formatFormatA');
@ -194,7 +194,7 @@ describe('Desktop notifications', () => {
spyNotificationAs('withNotification', 'granted');
// # Ensure notifications are set up to fire a desktop notification if are mentioned
changeDesktopNotificationAs('#desktopNotificationMentions');
changeDesktopNotificationAs('mentions');
// # Ensure display settings are set to "Show first and last name"
changeTeammateNameDisplayAs('#name_formatFormatC');
@ -235,7 +235,7 @@ describe('Desktop notifications', () => {
spyNotificationAs('withNotification', 'granted');
// # Ensure notifications are set up to fire a desktop notification
changeDesktopNotificationAs('#desktopNotificationMentions');
changeDesktopNotificationAs('mentions');
cy.apiGetChannelByName(testTeam.name, 'Off-Topic').then(({channel}) => {
const messageWithoutNotification = 'message without notification';
@ -271,7 +271,7 @@ describe('Desktop notifications', () => {
spyNotificationAs('withNotification', 'granted');
// # Ensure notifications are set up to never fire a desktop notification
changeDesktopNotificationAs('#desktopNotificationNever');
changeDesktopNotificationAs('nothing');
cy.apiGetChannelByName(testTeam.name, 'Off-Topic').then(({channel}) => {
const messageWithNotification = `random message with mention @${testUser.username}`;
@ -297,14 +297,11 @@ describe('Desktop notifications', () => {
// # Open settings modal
cy.uiOpenSettingsModal().within(() => {
// # Click "Desktop"
cy.findByText('Desktop Notifications').should('be.visible').click();
// # Click "Desktop sound notifications"
cy.findByText('Desktop notification sounds').should('be.visible').click();
// # Select sound off.
cy.get('#soundOff').check();
// # Ensure sound dropdown is not visible
cy.get('#displaySoundNotification').should('not.exist');
cy.findByText('Message notification sound').click({force: true});
// # Click "Save" and close the modal
cy.uiSaveAndClose();

View File

@ -43,7 +43,7 @@ describe('Desktop notifications', () => {
it('MM-T885 Channel notifications: Desktop notifications mentions only', () => {
// # Ensure notifications are set up to fire a desktop notification
changeDesktopNotificationAs('#desktopNotificationAllActivity');
changeDesktopNotificationAs('all');
const messageWithNotification = `random message with mention @${testUser.username}`;
const expected = `@${otherUser.username}: ${messageWithNotification}`;

View File

@ -48,7 +48,7 @@ describe('Desktop notifications', () => {
spyNotificationAs('withNotification', 'granted');
// # Ensure notifications are set up to fire a desktop notification if are mentioned
changeDesktopNotificationAs('#desktopNotificationMentions');
changeDesktopNotificationAs('mentions');
// # Ensure display settings are set to "Show nickname if one exists, otherwise show first and last name"
changeTeammateNameDisplayAs('#name_formatFormatB');
@ -76,7 +76,7 @@ describe('Desktop notifications', () => {
spyNotificationAs('withNotification', 'granted');
// # Ensure notifications are set up to fire a desktop notification if are mentioned
changeDesktopNotificationAs('#desktopNotificationMentions');
changeDesktopNotificationAs('mentions');
// # Ensure display settings are set to "Show nickname if one exists, otherwise show first and last name"
changeTeammateNameDisplayAs('#name_formatFormatB');

View File

@ -4,11 +4,21 @@
export function changeDesktopNotificationAs(category) {
// # Open settings modal
cy.uiOpenSettingsModal().within(() => {
// # Click "Desktop Notifications"
cy.findByText('Desktop Notifications').should('be.visible').click();
// # Click "Desktop and mobile notifications"
cy.findByText('Desktop and mobile notifications').should('be.visible').click();
// # Select category.
cy.get(category).check();
cy.get('#sendDesktopNotificationsSection').should('exist').within(() => {
if (category === 'all') {
// # Click "For all All new messages"
cy.findByText('All new messages').should('be.visible').click({force: true});
} else if (category === 'mentions') {
// # Click "For mentions"
cy.findByText('Mentions, direct messages, and group messages').should('be.visible').click({force: true});
} else if (category === 'nothing') {
// # Click "For nothing"
cy.findByText('Nothing').should('be.visible').click({force: true});
}
});
// # Click "Save" and close the modal
cy.uiSaveAndClose();

View File

@ -31,17 +31,17 @@ describe('Notifications', () => {
});
it('MM-T555 Notification Preferences do not save when modal is closed without saving', () => {
// # Call function that clicks on Settings -> Notifications -> Email Notifications -> Send Email Notifications -> Never without saving
// # Call function that clicks on Settings -> Notifications -> Email notifications -> Send Email notifications -> Never without saving
openSettingsAndClickEmailEdit(true);
// # Call function that checks Settings -> Notifications -> Email Notifications -> Send Email Notifications -> Never is not saved
// # Call function that checks Settings -> Notifications -> Email notifications -> Send Email notifications -> Never is not saved
openSettingsAndClickEmailEdit(false);
});
function openSettingsAndClickEmailEdit(shouldBeClicked = false) {
// # Open 'Settings' modal
cy.uiOpenSettingsModal().within(() => {
// # Click on the 'Edit' button next to Email Notifications
// # Click on the 'Edit' button next to Email notifications
cy.get('#emailEdit').click();
if (shouldBeClicked) {

View File

@ -239,9 +239,7 @@ function setReplyNotificationsSetting(idToToggle) {
and('contain', 'Notifications');
// Open up 'Reply Notifications' sub-section
cy.get('#commentsTitle').
scrollIntoView().
click();
cy.findByText('Reply notifications').should('be.visible').scrollIntoView().click();
cy.get(idToToggle).check().should('be.checked');

View File

@ -20,47 +20,27 @@ describe('Notifications', () => {
});
it('MM-T5458 Notification sound modal selection should reset when settings canceled', () => {
// # Call function that clicks on Settings -> Notifications -> Desktop Notifications -> Notification sound -> Change sound -> Cancel -> Desktop Notifications
openSettingsAndChangeNotification();
});
function openSettingsAndChangeNotification() {
// # Open 'Settings' modal
cy.uiOpenSettingsModal().within(() => {
cy.uiOpenSettingsModal();
// # Navigate to Desktop Notification Settings
navigateToDesktopNotificationSettings();
cy.get('#desktopNotificationSoundEdit').should('be.visible').click();
// # Change Notification selection
setNotificationSound();
cy.get('#messageNotificationSoundSelect').click();
// # Click Cancel button
cy.uiCancelButton().click();
// # Navigate to Desktop Notification Settings
navigateToDesktopNotificationSettings();
cy.uiClose();
});
}
function setNotificationSound() {
// # Change Notification sound selection value is set to Down
cy.get('#displaySoundNotification').click();
// # Select 'Bing' sound
cy.findByText('Down').click();
// * Verify Notification display changed to Down
verifyNotificationSelectionValue('Down');
}
// # Click Cancel button to close the settings
cy.uiCancelButton().click();
function navigateToDesktopNotificationSettings() {
// # Click on the 'Edit' button next to Desktop Notifications
cy.get('#desktopEdit').should('be.visible').click();
// # Click on the 'Edit' button next to Desktop sound notification again
cy.get('#desktopNotificationSoundEdit').should('be.visible').click();
// * Verify that the Notification sound is set to Bing
verifyNotificationSelectionValue('Bing');
}
// * Verify that the Notification sound is set to Bing as we canceled the settings
cy.findByText('Bing').should('be.visible');
function verifyNotificationSelectionValue(value) {
// * Verify that the Notification sound is set to certain value
cy.get('#displaySoundNotification').findByTestId('displaySoundNotificationValue').should('contain', value);
}
cy.uiClose();
});
});

View File

@ -34,7 +34,7 @@ describe('Authentication', () => {
// # Open settings modal
cy.uiOpenSettingsModal().within(() => {
// Click "Desktop"
cy.findByText('Desktop Notifications').should('be.visible').click();
cy.findByText('Desktop and mobile notifications').should('be.visible').click();
// # Set your desktop notifications to Never
cy.get('#desktopNotificationNever').check();

View File

@ -102,13 +102,13 @@ export function sendDesktopNotification(post, msgProps) {
notifyLevel = user?.notify_props?.desktop || NotificationLevels.ALL;
}
if (channel.type === 'G' && channelNotifyProp === NotificationLevels.DEFAULT && user?.notify_props?.desktop === NotificationLevels.MENTION) {
if (channel?.type === 'G' && channelNotifyProp === NotificationLevels.DEFAULT && user?.notify_props?.desktop === NotificationLevels.MENTION) {
notifyLevel = NotificationLevels.ALL;
}
if (notifyLevel === NotificationLevels.NONE) {
return;
} else if (channel.type === 'G' && notifyLevel === NotificationLevels.MENTION) {
} else if (channel?.type === 'G' && notifyLevel === NotificationLevels.MENTION) {
// Compose the whole text in the message, including interactive messages.
let text = post.message;

View File

@ -11,7 +11,7 @@ exports[`components/SettingItemMax should match snapshot 1`] = `
title
</h4>
<div
className="col-sm-12"
className="sectionContent col-sm-10 col-sm-offset-2"
>
<div
className="setting-list"
@ -72,7 +72,7 @@ exports[`components/SettingItemMax should match snapshot, on clientError 1`] = `
title
</h4>
<div
className="col-sm-12"
className="sectionContent col-sm-10 col-sm-offset-2"
>
<div
className="setting-list"
@ -143,7 +143,7 @@ exports[`components/SettingItemMax should match snapshot, on serverError 1`] = `
title
</h4>
<div
className="col-sm-12"
className="sectionContent col-sm-10 col-sm-offset-2"
>
<div
className="setting-list"
@ -214,7 +214,7 @@ exports[`components/SettingItemMax should match snapshot, with new saveTextButto
title
</h4>
<div
className="col-sm-12"
className="sectionContent col-sm-10 col-sm-offset-2"
>
<div
className="setting-list"
@ -270,7 +270,7 @@ exports[`components/SettingItemMax should match snapshot, without submit 1`] = `
title
</h4>
<div
className="col-sm-12"
className="sectionContent col-sm-10 col-sm-offset-2"
>
<div
className="setting-list"

View File

@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React from 'react';
import type {ReactNode} from 'react';
import {FormattedMessage} from 'react-intl';
@ -44,7 +45,7 @@ type Props = {
submitExtra?: ReactNode;
saving?: boolean;
title?: ReactNode;
width?: string;
isFullWidth?: boolean;
cancelButtonText?: ReactNode;
shiftEnter?: boolean;
saveButtonText?: string;
@ -177,14 +178,6 @@ export default class SettingItemMax extends React.PureComponent<Props> {
}
const inputs = this.props.inputs;
let widthClass;
if (this.props.width === 'full') {
widthClass = 'col-sm-12';
} else if (this.props.width === 'medium') {
widthClass = 'col-sm-10 col-sm-offset-2';
} else {
widthClass = 'col-sm-9 col-sm-offset-3';
}
let title;
if (this.props.title) {
@ -231,7 +224,12 @@ export default class SettingItemMax extends React.PureComponent<Props> {
className={`section-max form-horizontal ${this.props.containerStyle}`}
>
{title}
<div className={widthClass}>
<div
className={classNames('sectionContent', {
'col-sm-12': this.props.isFullWidth,
'col-sm-10 col-sm-offset-2': !this.props.isFullWidth,
})}
>
<div
tabIndex={-1}
ref={this.settingList}

View File

@ -256,7 +256,6 @@ export class ManageLanguage extends React.PureComponent<Props, State> {
defaultMessage='Language'
/>
}
width='medium'
submit={this.changeLanguage}
saving={this.state.isSaving}
inputs={[input]}

View File

@ -245,7 +245,6 @@ export default class ManageTimezones extends React.PureComponent<Props, State> {
/>
}
containerStyle='timezone-container'
width='medium'
submit={this.changeTimezone}
saving={this.state.isSaving}
inputs={inputs}

View File

@ -309,7 +309,7 @@ export default class ThemeSetting extends React.PureComponent<Props, State> {
disableEnterSubmit={true}
saving={this.state.isSaving}
serverError={serverError}
width='full'
isFullWidth={true}
updateSection={this.handleUpdateSection}
/>
);

View File

@ -34,7 +34,7 @@ Object {
class="fa fa-angle-left"
/>
</div>
Notification Settings
Notification settings
</h4>
</div>
<div
@ -88,15 +88,15 @@ Object {
>
<h4
class="section-min__title"
id="desktopTitle"
id="desktopAndMobileTitle"
>
Desktop Notifications
Desktop and mobile notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="desktopTitle desktopEdit"
aria-labelledby="desktopAndMobileTitle desktopAndMobileEdit"
class="color--link style--none section-min__edit"
id="desktopEdit"
id="desktopAndMobileEdit"
>
<i
class="icon-pencil-outline"
@ -107,9 +107,44 @@ Object {
</div>
<div
class="section-min__describe"
id="desktopDesc"
id="desktopAndMobileDesc"
>
For all activity, without sound
Configure desktop and mobile settings
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="secion-min__header"
>
<h4
class="section-min__title"
id="desktopNotificationSoundTitle"
>
Desktop notification sounds
</h4>
<button
aria-expanded="false"
aria-labelledby="desktopNotificationSoundTitle desktopNotificationSoundEdit"
class="color--link style--none section-min__edit"
id="desktopNotificationSoundEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="desktopNotificationSoundDesc"
>
No sound
</div>
</div>
<div
@ -125,7 +160,7 @@ Object {
class="section-min__title"
id="emailTitle"
>
Email Notifications
Email notifications
</h4>
<button
aria-expanded="false"
@ -158,15 +193,15 @@ Object {
>
<h4
class="section-min__title"
id="pushTitle"
id="keywordsAndMentionsTitle"
>
Mobile Push Notifications
Keywords that trigger notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="pushTitle pushEdit"
aria-labelledby="keywordsAndMentionsTitle keywordsAndMentionsEdit"
class="color--link style--none section-min__edit"
id="pushEdit"
id="keywordsAndMentionsEdit"
>
<i
class="icon-pencil-outline"
@ -177,42 +212,7 @@ Object {
</div>
<div
class="section-min__describe"
id="pushDesc"
>
Never
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="secion-min__header"
>
<h4
class="section-min__title"
id="keysWithNotificationTitle"
>
Keywords That Trigger Notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="keysWithNotificationTitle keysWithNotificationEdit"
class="color--link style--none section-min__edit"
id="keysWithNotificationEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="keysWithNotificationDesc"
id="keywordsAndMentionsDesc"
>
"@some-user"
</div>
@ -228,15 +228,15 @@ Object {
>
<h4
class="section-min__title"
id="keysWithHighlightTitle"
id="keywordsAndHighlightTitle"
>
Keywords That Get Highlighted (Without Notifications)
Keywords that get highlighted (without notifications)
</h4>
<button
aria-expanded="false"
aria-labelledby="keysWithHighlightTitle keysWithHighlightEdit"
aria-labelledby="keywordsAndHighlightTitle keywordsAndHighlightEdit"
class="color--link style--none section-min__edit"
id="keysWithHighlightEdit"
id="keywordsAndHighlightEdit"
>
<i
class="icon-pencil-outline"
@ -247,7 +247,7 @@ Object {
</div>
<div
class="section-min__describe"
id="keysWithHighlightDesc"
id="keywordsAndHighlightDesc"
>
None
</div>
@ -292,7 +292,7 @@ Object {
class="fa fa-angle-left"
/>
</div>
Notification Settings
Notification settings
</h4>
</div>
<div
@ -346,15 +346,15 @@ Object {
>
<h4
class="section-min__title"
id="desktopTitle"
id="desktopAndMobileTitle"
>
Desktop Notifications
Desktop and mobile notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="desktopTitle desktopEdit"
aria-labelledby="desktopAndMobileTitle desktopAndMobileEdit"
class="color--link style--none section-min__edit"
id="desktopEdit"
id="desktopAndMobileEdit"
>
<i
class="icon-pencil-outline"
@ -365,9 +365,44 @@ Object {
</div>
<div
class="section-min__describe"
id="desktopDesc"
id="desktopAndMobileDesc"
>
For all activity, without sound
Configure desktop and mobile settings
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="secion-min__header"
>
<h4
class="section-min__title"
id="desktopNotificationSoundTitle"
>
Desktop notification sounds
</h4>
<button
aria-expanded="false"
aria-labelledby="desktopNotificationSoundTitle desktopNotificationSoundEdit"
class="color--link style--none section-min__edit"
id="desktopNotificationSoundEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="desktopNotificationSoundDesc"
>
No sound
</div>
</div>
<div
@ -383,7 +418,7 @@ Object {
class="section-min__title"
id="emailTitle"
>
Email Notifications
Email notifications
</h4>
<button
aria-expanded="false"
@ -416,15 +451,15 @@ Object {
>
<h4
class="section-min__title"
id="pushTitle"
id="keywordsAndMentionsTitle"
>
Mobile Push Notifications
Keywords that trigger notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="pushTitle pushEdit"
aria-labelledby="keywordsAndMentionsTitle keywordsAndMentionsEdit"
class="color--link style--none section-min__edit"
id="pushEdit"
id="keywordsAndMentionsEdit"
>
<i
class="icon-pencil-outline"
@ -435,42 +470,7 @@ Object {
</div>
<div
class="section-min__describe"
id="pushDesc"
>
Never
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="secion-min__header"
>
<h4
class="section-min__title"
id="keysWithNotificationTitle"
>
Keywords That Trigger Notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="keysWithNotificationTitle keysWithNotificationEdit"
class="color--link style--none section-min__edit"
id="keysWithNotificationEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="keysWithNotificationDesc"
id="keywordsAndMentionsDesc"
>
"@some-user"
</div>
@ -486,15 +486,15 @@ Object {
>
<h4
class="section-min__title"
id="keysWithHighlightTitle"
id="keywordsAndHighlightTitle"
>
Keywords That Get Highlighted (Without Notifications)
Keywords that get highlighted (without notifications)
</h4>
<button
aria-expanded="false"
aria-labelledby="keysWithHighlightTitle keysWithHighlightEdit"
aria-labelledby="keywordsAndHighlightTitle keywordsAndHighlightEdit"
class="color--link style--none section-min__edit"
id="keysWithHighlightEdit"
id="keywordsAndHighlightEdit"
>
<i
class="icon-pencil-outline"
@ -505,7 +505,7 @@ Object {
</div>
<div
class="section-min__describe"
id="keysWithHighlightDesc"
id="keywordsAndHighlightDesc"
>
None
</div>
@ -609,7 +609,7 @@ Object {
class="fa fa-angle-left"
/>
</div>
Notification Settings
Notification settings
</h4>
</div>
<div
@ -663,15 +663,15 @@ Object {
>
<h4
class="section-min__title"
id="desktopTitle"
id="desktopAndMobileTitle"
>
Desktop Notifications
Desktop and mobile notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="desktopTitle desktopEdit"
aria-labelledby="desktopAndMobileTitle desktopAndMobileEdit"
class="color--link style--none section-min__edit"
id="desktopEdit"
id="desktopAndMobileEdit"
>
<i
class="icon-pencil-outline"
@ -682,9 +682,44 @@ Object {
</div>
<div
class="section-min__describe"
id="desktopDesc"
id="desktopAndMobileDesc"
>
For all activity, without sound
Configure desktop and mobile settings
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="secion-min__header"
>
<h4
class="section-min__title"
id="desktopNotificationSoundTitle"
>
Desktop notification sounds
</h4>
<button
aria-expanded="false"
aria-labelledby="desktopNotificationSoundTitle desktopNotificationSoundEdit"
class="color--link style--none section-min__edit"
id="desktopNotificationSoundEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="desktopNotificationSoundDesc"
>
No sound
</div>
</div>
<div
@ -700,7 +735,7 @@ Object {
class="section-min__title"
id="emailTitle"
>
Email Notifications
Email notifications
</h4>
<button
aria-expanded="false"
@ -733,15 +768,15 @@ Object {
>
<h4
class="section-min__title"
id="pushTitle"
id="keywordsAndMentionsTitle"
>
Mobile Push Notifications
Keywords that trigger notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="pushTitle pushEdit"
aria-labelledby="keywordsAndMentionsTitle keywordsAndMentionsEdit"
class="color--link style--none section-min__edit"
id="pushEdit"
id="keywordsAndMentionsEdit"
>
<i
class="icon-pencil-outline"
@ -752,42 +787,7 @@ Object {
</div>
<div
class="section-min__describe"
id="pushDesc"
>
Never
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="secion-min__header"
>
<h4
class="section-min__title"
id="keysWithNotificationTitle"
>
Keywords That Trigger Notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="keysWithNotificationTitle keysWithNotificationEdit"
class="color--link style--none section-min__edit"
id="keysWithNotificationEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="keysWithNotificationDesc"
id="keywordsAndMentionsDesc"
>
"@some-user"
</div>
@ -806,9 +806,9 @@ Object {
>
<h4
class="section-min__title isDisabled"
id="keysWithHighlightTitle"
id="keywordsAndHighlightTitle"
>
Keywords That Get Highlighted (Without Notifications)
Keywords that get highlighted (without notifications)
</h4>
<span
class="RestrictedIndicator__icon-tooltip-container"
@ -828,7 +828,7 @@ Object {
</div>
<div
class="section-min__describe isDisabled"
id="keysWithHighlightDesc"
id="keywordsAndHighlightDesc"
>
None
</div>
@ -870,7 +870,7 @@ Object {
class="fa fa-angle-left"
/>
</div>
Notification Settings
Notification settings
</h4>
</div>
<div
@ -924,15 +924,15 @@ Object {
>
<h4
class="section-min__title"
id="desktopTitle"
id="desktopAndMobileTitle"
>
Desktop Notifications
Desktop and mobile notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="desktopTitle desktopEdit"
aria-labelledby="desktopAndMobileTitle desktopAndMobileEdit"
class="color--link style--none section-min__edit"
id="desktopEdit"
id="desktopAndMobileEdit"
>
<i
class="icon-pencil-outline"
@ -943,9 +943,44 @@ Object {
</div>
<div
class="section-min__describe"
id="desktopDesc"
id="desktopAndMobileDesc"
>
For all activity, without sound
Configure desktop and mobile settings
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="secion-min__header"
>
<h4
class="section-min__title"
id="desktopNotificationSoundTitle"
>
Desktop notification sounds
</h4>
<button
aria-expanded="false"
aria-labelledby="desktopNotificationSoundTitle desktopNotificationSoundEdit"
class="color--link style--none section-min__edit"
id="desktopNotificationSoundEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="desktopNotificationSoundDesc"
>
No sound
</div>
</div>
<div
@ -961,7 +996,7 @@ Object {
class="section-min__title"
id="emailTitle"
>
Email Notifications
Email notifications
</h4>
<button
aria-expanded="false"
@ -994,15 +1029,15 @@ Object {
>
<h4
class="section-min__title"
id="pushTitle"
id="keywordsAndMentionsTitle"
>
Mobile Push Notifications
Keywords that trigger notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="pushTitle pushEdit"
aria-labelledby="keywordsAndMentionsTitle keywordsAndMentionsEdit"
class="color--link style--none section-min__edit"
id="pushEdit"
id="keywordsAndMentionsEdit"
>
<i
class="icon-pencil-outline"
@ -1013,42 +1048,7 @@ Object {
</div>
<div
class="section-min__describe"
id="pushDesc"
>
Never
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="secion-min__header"
>
<h4
class="section-min__title"
id="keysWithNotificationTitle"
>
Keywords That Trigger Notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="keysWithNotificationTitle keysWithNotificationEdit"
class="color--link style--none section-min__edit"
id="keysWithNotificationEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="keysWithNotificationDesc"
id="keywordsAndMentionsDesc"
>
"@some-user"
</div>
@ -1067,9 +1067,9 @@ Object {
>
<h4
class="section-min__title isDisabled"
id="keysWithHighlightTitle"
id="keywordsAndHighlightTitle"
>
Keywords That Get Highlighted (Without Notifications)
Keywords that get highlighted (without notifications)
</h4>
<span
class="RestrictedIndicator__icon-tooltip-container"
@ -1089,7 +1089,7 @@ Object {
</div>
<div
class="section-min__describe isDisabled"
id="keysWithHighlightDesc"
id="keywordsAndHighlightDesc"
>
None
</div>
@ -1190,7 +1190,7 @@ Object {
class="fa fa-angle-left"
/>
</div>
Notification Settings
Notification settings
</h4>
</div>
<div
@ -1244,15 +1244,15 @@ Object {
>
<h4
class="section-min__title"
id="desktopTitle"
id="desktopAndMobileTitle"
>
Desktop Notifications
Desktop and mobile notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="desktopTitle desktopEdit"
aria-labelledby="desktopAndMobileTitle desktopAndMobileEdit"
class="color--link style--none section-min__edit"
id="desktopEdit"
id="desktopAndMobileEdit"
>
<i
class="icon-pencil-outline"
@ -1263,9 +1263,44 @@ Object {
</div>
<div
class="section-min__describe"
id="desktopDesc"
id="desktopAndMobileDesc"
>
For all activity, without sound
Configure desktop and mobile settings
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="secion-min__header"
>
<h4
class="section-min__title"
id="desktopNotificationSoundTitle"
>
Desktop notification sounds
</h4>
<button
aria-expanded="false"
aria-labelledby="desktopNotificationSoundTitle desktopNotificationSoundEdit"
class="color--link style--none section-min__edit"
id="desktopNotificationSoundEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="desktopNotificationSoundDesc"
>
No sound
</div>
</div>
<div
@ -1281,7 +1316,7 @@ Object {
class="section-min__title"
id="emailTitle"
>
Email Notifications
Email notifications
</h4>
<button
aria-expanded="false"
@ -1314,15 +1349,15 @@ Object {
>
<h4
class="section-min__title"
id="pushTitle"
id="keywordsAndMentionsTitle"
>
Mobile Push Notifications
Keywords that trigger notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="pushTitle pushEdit"
aria-labelledby="keywordsAndMentionsTitle keywordsAndMentionsEdit"
class="color--link style--none section-min__edit"
id="pushEdit"
id="keywordsAndMentionsEdit"
>
<i
class="icon-pencil-outline"
@ -1333,42 +1368,7 @@ Object {
</div>
<div
class="section-min__describe"
id="pushDesc"
>
Never
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="secion-min__header"
>
<h4
class="section-min__title"
id="keysWithNotificationTitle"
>
Keywords That Trigger Notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="keysWithNotificationTitle keysWithNotificationEdit"
class="color--link style--none section-min__edit"
id="keysWithNotificationEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="keysWithNotificationDesc"
id="keywordsAndMentionsDesc"
>
"@some-user"
</div>
@ -1413,7 +1413,7 @@ Object {
class="fa fa-angle-left"
/>
</div>
Notification Settings
Notification settings
</h4>
</div>
<div
@ -1467,15 +1467,15 @@ Object {
>
<h4
class="section-min__title"
id="desktopTitle"
id="desktopAndMobileTitle"
>
Desktop Notifications
Desktop and mobile notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="desktopTitle desktopEdit"
aria-labelledby="desktopAndMobileTitle desktopAndMobileEdit"
class="color--link style--none section-min__edit"
id="desktopEdit"
id="desktopAndMobileEdit"
>
<i
class="icon-pencil-outline"
@ -1486,9 +1486,44 @@ Object {
</div>
<div
class="section-min__describe"
id="desktopDesc"
id="desktopAndMobileDesc"
>
For all activity, without sound
Configure desktop and mobile settings
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="secion-min__header"
>
<h4
class="section-min__title"
id="desktopNotificationSoundTitle"
>
Desktop notification sounds
</h4>
<button
aria-expanded="false"
aria-labelledby="desktopNotificationSoundTitle desktopNotificationSoundEdit"
class="color--link style--none section-min__edit"
id="desktopNotificationSoundEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="desktopNotificationSoundDesc"
>
No sound
</div>
</div>
<div
@ -1504,7 +1539,7 @@ Object {
class="section-min__title"
id="emailTitle"
>
Email Notifications
Email notifications
</h4>
<button
aria-expanded="false"
@ -1537,15 +1572,15 @@ Object {
>
<h4
class="section-min__title"
id="pushTitle"
id="keywordsAndMentionsTitle"
>
Mobile Push Notifications
Keywords that trigger notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="pushTitle pushEdit"
aria-labelledby="keywordsAndMentionsTitle keywordsAndMentionsEdit"
class="color--link style--none section-min__edit"
id="pushEdit"
id="keywordsAndMentionsEdit"
>
<i
class="icon-pencil-outline"
@ -1556,42 +1591,7 @@ Object {
</div>
<div
class="section-min__describe"
id="pushDesc"
>
Never
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="secion-min__header"
>
<h4
class="section-min__title"
id="keysWithNotificationTitle"
>
Keywords That Trigger Notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="keysWithNotificationTitle keysWithNotificationEdit"
class="color--link style--none section-min__edit"
id="keysWithNotificationEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="keysWithNotificationDesc"
id="keywordsAndMentionsDesc"
>
"@some-user"
</div>

View File

@ -0,0 +1,359 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DesktopNotificationSettings should match snapshot, on max setting 1`] = `
<div>
<section
class="section-max form-horizontal "
>
<h4
class="col-sm-12 section-title"
id="settingTitle"
>
Desktop and mobile notifications
</h4>
<div
class="sectionContent col-sm-10 col-sm-offset-2"
>
<div
class="setting-list"
tabindex="-1"
>
<div
class="setting-list-item"
>
<fieldset
id="sendDesktopNotificationsSection"
>
<legend
class="form-legend"
>
Send notifications for:
</legend>
<div
class="radio"
>
<label>
<input
type="radio"
value="all"
/>
All new messages
</label>
</div>
<div
class="radio"
>
<label>
<input
type="radio"
value="mention"
/>
Mentions, direct messages, and group messages
</label>
</div>
<div
class="radio"
>
<label>
<input
type="radio"
value="none"
/>
Nothing
</label>
</div>
</fieldset>
<br />
<div
class="checkbox single-checkbox"
>
<label>
<input
type="checkbox"
/>
Notify me about replies to threads I'm following
</label>
</div>
<hr />
<div
class="checkbox single-checkbox"
>
<label>
<input
type="checkbox"
/>
Use different settings for my mobile devices
</label>
</div>
<br />
<label
class="singleSelectLabel"
for="pushMobileNotificationSelectInput"
id="pushMobileNotificationsLabel"
>
Trigger mobile notifications when I am:
</label>
<div
class="react-select singleSelect css-2b097c-container"
>
<div
class="react-select__control css-yk16xz-control"
>
<div
class="react-select__value-container react-select__value-container--has-value css-1hwfws3"
>
<div
class="react-select__single-value css-1uccc91-singleValue"
>
Offline
</div>
<input
class="css-62g3xt-dummyInput"
id="pushMobileNotificationSelectInput"
readonly=""
tabindex="0"
value=""
/>
</div>
<div
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
>
<div
aria-hidden="true"
class="react-select__indicator react-select__dropdown-indicator css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-6q0nyr-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
<div
class="setting-list-item"
>
<hr />
<button
class="btn btn-primary "
data-testid="saveSetting"
id="saveSetting"
type="submit"
>
<span>
Save
</span>
</button>
<button
class="btn btn-tertiary"
id="cancelSetting"
>
Cancel
</button>
</div>
</div>
</div>
</section>
</div>
`;
exports[`DesktopNotificationSettings should match snapshot, on min setting 1`] = `
<div>
<div
class="section-min"
>
<div
class="secion-min__header"
>
<h4
class="section-min__title"
id="desktopAndMobileTitle"
>
Desktop and mobile notifications
</h4>
<button
aria-expanded="false"
aria-labelledby="desktopAndMobileTitle desktopAndMobileEdit"
class="color--link style--none section-min__edit"
id="desktopAndMobileEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
<div
class="section-min__describe"
id="desktopAndMobileDesc"
>
Configure desktop and mobile settings
</div>
</div>
</div>
`;
exports[`DesktopNotificationSettings should not show desktop thread notification checkbox when collapsed threads are not enabled 1`] = `
<div>
<section
class="section-max form-horizontal "
>
<h4
class="col-sm-12 section-title"
id="settingTitle"
>
Desktop and mobile notifications
</h4>
<div
class="sectionContent col-sm-10 col-sm-offset-2"
>
<div
class="setting-list"
tabindex="-1"
>
<div
class="setting-list-item"
>
<fieldset
id="sendDesktopNotificationsSection"
>
<legend
class="form-legend"
>
Send notifications for:
</legend>
<div
class="radio"
>
<label>
<input
type="radio"
value="all"
/>
All new messages
</label>
</div>
<div
class="radio"
>
<label>
<input
type="radio"
value="mention"
/>
Mentions, direct messages, and group messages
</label>
</div>
<div
class="radio"
>
<label>
<input
type="radio"
value="none"
/>
Nothing
</label>
</div>
</fieldset>
<hr />
<div
class="checkbox single-checkbox"
>
<label>
<input
type="checkbox"
/>
Use different settings for my mobile devices
</label>
</div>
<br />
<label
class="singleSelectLabel"
for="pushMobileNotificationSelectInput"
id="pushMobileNotificationsLabel"
>
Trigger mobile notifications when I am:
</label>
<div
class="react-select singleSelect css-2b097c-container"
>
<div
class="react-select__control css-yk16xz-control"
>
<div
class="react-select__value-container react-select__value-container--has-value css-1hwfws3"
>
<div
class="react-select__single-value css-1uccc91-singleValue"
>
Offline
</div>
<input
class="css-62g3xt-dummyInput"
id="pushMobileNotificationSelectInput"
readonly=""
tabindex="0"
value=""
/>
</div>
<div
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
>
<div
aria-hidden="true"
class="react-select__indicator react-select__dropdown-indicator css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-6q0nyr-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
<div
class="setting-list-item"
>
<hr />
<button
class="btn btn-primary "
data-testid="saveSetting"
id="saveSetting"
type="submit"
>
<span>
Save
</span>
</button>
<button
class="btn btn-tertiary"
id="cancelSetting"
>
Cancel
</button>
</div>
</div>
</div>
</section>
</div>
`;

View File

@ -0,0 +1,312 @@
// 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 Constants, {NotificationLevels} from 'utils/constants';
import type {SelectOption, Props} from './index';
import DesktopNotificationSettings, {
shouldShowDesktopThreadsSection,
shouldShowMobileThreadsSection,
getValueOfSendMobileNotificationForSelect,
getValueOfSendMobileNotificationWhenSelect,
shouldShowTriggerMobileNotificationsSection,
} from './index';
const validNotificationLevels = Object.values(NotificationLevels);
describe('DesktopNotificationSettings', () => {
const baseProps: Props = {
active: true,
updateSection: jest.fn(),
onSubmit: jest.fn(),
onCancel: jest.fn(),
saving: false,
error: '',
setParentState: jest.fn(),
areAllSectionsInactive: false,
isCollapsedThreadsEnabled: true,
desktopActivity: NotificationLevels.DEFAULT,
pushActivity: NotificationLevels.DEFAULT,
pushStatus: Constants.UserStatuses.OFFLINE,
desktopThreads: NotificationLevels.DEFAULT,
pushThreads: NotificationLevels.DEFAULT,
sendPushNotifications: true,
desktopAndMobileSettingsDifferent: false,
};
test('should match snapshot, on max setting', () => {
const {container} = renderWithContext(
<DesktopNotificationSettings {...baseProps}/>,
);
expect(container).toMatchSnapshot();
});
test('should match snapshot, on min setting', () => {
const props = {...baseProps, active: false};
const {container} = renderWithContext(
<DesktopNotificationSettings {...props}/>,
);
expect(container).toMatchSnapshot();
});
test('should not show desktop thread notification checkbox when collapsed threads are not enabled', () => {
const props = {...baseProps, isCollapsedThreadsEnabled: false};
const {container} = renderWithContext(
<DesktopNotificationSettings {...props}/>,
);
expect(screen.queryByText('Notify me about replies to threads I\'m following')).toBeNull();
expect(container).toMatchSnapshot();
});
test('should not show desktop thread notification checkbox when desktop is all', () => {
const props = {...baseProps, desktopActivity: NotificationLevels.ALL};
renderWithContext(
<DesktopNotificationSettings {...props}/>,
);
expect(screen.queryByText('Notify me about replies to threads I\'m following')).toBeNull();
});
test('should not show desktop thread notification checkbox when desktop is none', () => {
const props = {...baseProps, desktopActivity: NotificationLevels.NONE};
renderWithContext(
<DesktopNotificationSettings {...props}/>,
);
expect(screen.queryByText('Notify me about replies to threads I\'m following')).toBeNull();
});
test('should show desktop thread notification checkbox when desktop is mention', () => {
const props = {...baseProps, desktopActivity: NotificationLevels.MENTION};
renderWithContext(
<DesktopNotificationSettings {...props}/>,
);
expect(screen.getByText('Notify me about replies to threads I\'m following')).toBeInTheDocument();
});
test('should show mobile notification when checkbox for use different mobile settings is checked', () => {
const props = {...baseProps, desktopAndMobileSettingsDifferent: true};
renderWithContext(
<DesktopNotificationSettings {...props}/>,
);
expect(screen.getByText('Send mobile notifications for:')).toBeInTheDocument();
});
test('should not show mobile notification when checkbox for use different mobile settings is not checked', () => {
const props = {...baseProps, desktopAndMobileSettingsDifferent: false};
renderWithContext(
<DesktopNotificationSettings {...props}/>,
);
expect(screen.queryByText('Send mobile notifications for:')).toBeNull();
});
test('should shown notify me about mobile threads when mobile setting is mention and desktop setting is anything', () => {
const {rerender} = renderWithContext(
<DesktopNotificationSettings
{...baseProps}
desktopAndMobileSettingsDifferent={true}
pushActivity={NotificationLevels.MENTION}
desktopActivity={NotificationLevels.NONE}
/>,
);
expect(screen.getByText('Notify me on mobile about replies to threads I\'m following')).toBeInTheDocument();
rerender(
<DesktopNotificationSettings
{...baseProps}
desktopAndMobileSettingsDifferent={true}
pushActivity={NotificationLevels.MENTION}
desktopActivity={NotificationLevels.ALL}
/>,
);
expect(screen.getByText('Notify me on mobile about replies to threads I\'m following')).toBeInTheDocument();
});
test('should not show notify me about mobile threads when mobile setting is anything other than mention', () => {
const {rerender} = renderWithContext(
<DesktopNotificationSettings
{...baseProps}
desktopAndMobileSettingsDifferent={true}
pushActivity={NotificationLevels.NONE}
desktopActivity={NotificationLevels.NONE}
/>,
);
expect(screen.queryByText('Notify me on mobile about replies to threads I\'m following')).toBeNull();
rerender(
<DesktopNotificationSettings
{...baseProps}
desktopAndMobileSettingsDifferent={true}
pushActivity={NotificationLevels.ALL}
desktopActivity={NotificationLevels.ALL}
/>,
);
expect(screen.queryByText('Notify me on mobile about replies to threads I\'m following')).toBeNull();
});
test('should show trigger mobile notifications section when desktop setting is mention', () => {
const props = {...baseProps, desktopActivity: NotificationLevels.MENTION, desktopAndMobileSettingsDifferent: false};
renderWithContext(
<DesktopNotificationSettings {...props}/>,
);
expect(screen.getByText('Trigger mobile notifications when I am:')).toBeInTheDocument();
});
test('should not show trigger mobile notifications section when desktop setting is none', () => {
const props = {...baseProps, desktopActivity: NotificationLevels.NONE, desktopAndMobileSettingsDifferent: false};
renderWithContext(
<DesktopNotificationSettings {...props}/>,
);
expect(screen.queryByText('Trigger mobile notifications when I am:')).toBeNull();
});
});
describe('shouldShowDesktopThreadsSection', () => {
test('should not show when collapsed threads are not enabled', () => {
expect(shouldShowDesktopThreadsSection(false, 'hello' as any)).toBe(false);
});
test('should not show when desktop setting is either none or all', () => {
expect(shouldShowDesktopThreadsSection(true, NotificationLevels.NONE)).toBe(false);
expect(shouldShowDesktopThreadsSection(true, NotificationLevels.ALL)).toBe(false);
});
test('should show when desktop setting is mention', () => {
expect(shouldShowDesktopThreadsSection(true, NotificationLevels.MENTION)).toBe(true);
expect(shouldShowDesktopThreadsSection(true, NotificationLevels.DEFAULT)).toBe(true);
});
test('should show by default when desktop setting is undefined', () => {
expect(shouldShowDesktopThreadsSection(true, undefined as any)).toBe(true);
});
});
describe('shouldShowMobileThreadsSection', () => {
test('should return false if sendPushNotifications is false', () => {
const result = shouldShowMobileThreadsSection(false, true, true, 'any' as any);
expect(result).toBe(false);
});
test('should return false if isCollapsedThreadsEnabled is false', () => {
const result = shouldShowMobileThreadsSection(true, false, true, 'any' as any);
expect(result).toBe(false);
});
test('should return false if desktop and mobile settings are same', () => {
const result = shouldShowMobileThreadsSection(true, true, false, 'any' as any);
expect(result).toBe(false);
});
test('should return false if pushActivity is all', () => {
const result = shouldShowMobileThreadsSection(true, true, true, NotificationLevels.ALL);
expect(result).toBe(false);
});
test('should return false if pushActivity is none', () => {
const result = shouldShowMobileThreadsSection(true, true, true, NotificationLevels.NONE);
expect(result).toBe(false);
});
});
describe('getValueOfSendMobileNotificationForSelect', () => {
test('should return the middle option when input is undefined', () => {
expect(getValueOfSendMobileNotificationForSelect(undefined as any)).not.toBeUndefined();
const result = getValueOfSendMobileNotificationForSelect(undefined as any) as SelectOption;
expect(result.value).toBe(NotificationLevels.MENTION);
});
test('should return the middle option when input is not a valid option', () => {
expect(getValueOfSendMobileNotificationForSelect('invalid' as any)).not.toBeUndefined();
const result = getValueOfSendMobileNotificationForSelect('invalid' as any) as SelectOption;
expect(result.value).toBe(NotificationLevels.MENTION);
});
test('should return the same option when input is a valid option', () => {
validNotificationLevels.
filter((level) => level !== NotificationLevels.DEFAULT).
forEach((level) => {
expect(getValueOfSendMobileNotificationForSelect(level)).not.toBeUndefined();
const result = getValueOfSendMobileNotificationForSelect(level) as SelectOption;
expect(result.value).toBe(level);
});
});
});
describe('shouldShowTriggerMobileNotificationsSection', () => {
test('should not show when push notifications are off', () => {
expect(shouldShowTriggerMobileNotificationsSection(false, NotificationLevels.ALL, NotificationLevels.ALL, true)).toBe(false);
});
test('should show if either of desktop or mobile settings are not defined', () => {
expect(shouldShowTriggerMobileNotificationsSection(true, undefined as any, NotificationLevels.ALL, true)).toBe(true);
expect(shouldShowTriggerMobileNotificationsSection(true, NotificationLevels.ALL, undefined as any, true)).toBe(true);
});
test('should show if desktop is either mention or all for same mobile setting', () => {
expect(shouldShowTriggerMobileNotificationsSection(true, NotificationLevels.MENTION, NotificationLevels.MENTION, false)).toBe(true);
expect(shouldShowTriggerMobileNotificationsSection(true, NotificationLevels.ALL, NotificationLevels.ALL, false)).toBe(true);
});
test('should not show if desktop is none for same mobile setting', () => {
expect(shouldShowTriggerMobileNotificationsSection(true, NotificationLevels.NONE, NotificationLevels.NONE, false)).toBe(false);
});
test('should show for any desktop setting if mobile setting is mention', () => {
expect(shouldShowTriggerMobileNotificationsSection(true, NotificationLevels.ALL, NotificationLevels.MENTION, true)).toBe(true);
expect(shouldShowTriggerMobileNotificationsSection(true, NotificationLevels.MENTION, NotificationLevels.MENTION, true)).toBe(true);
expect(shouldShowTriggerMobileNotificationsSection(true, NotificationLevels.NONE, NotificationLevels.MENTION, true)).toBe(true);
});
test('should not show for any desktop setting if mobile setting is none', () => {
expect(shouldShowTriggerMobileNotificationsSection(true, NotificationLevels.ALL, NotificationLevels.NONE, true)).toBe(false);
expect(shouldShowTriggerMobileNotificationsSection(true, NotificationLevels.MENTION, NotificationLevels.NONE, true)).toBe(false);
expect(shouldShowTriggerMobileNotificationsSection(true, NotificationLevels.NONE, NotificationLevels.NONE, true)).toBe(false);
});
});
describe('getValueOfSendMobileNotificationWhenSelect', () => {
test('When input is undefined it should return the last option', () => {
expect(getValueOfSendMobileNotificationWhenSelect(undefined)).not.toBeUndefined();
const result = getValueOfSendMobileNotificationWhenSelect(undefined) as SelectOption;
expect(result.value).toBe(Constants.UserStatuses.OFFLINE);
});
test('when input is defined but is not a valid option it should return the last option', () => {
// We are purposely testing with an invalid value hence the 'any'
expect(getValueOfSendMobileNotificationWhenSelect('invalid' as any)).not.toBeUndefined();
const result = getValueOfSendMobileNotificationWhenSelect('invalid' as any) as SelectOption;
expect(result.value).toBe(Constants.UserStatuses.OFFLINE);
});
test('When input is a valid option it should return the same option', () => {
expect(getValueOfSendMobileNotificationWhenSelect(Constants.UserStatuses.ONLINE)).not.toBeUndefined();
const result = getValueOfSendMobileNotificationWhenSelect(Constants.UserStatuses.ONLINE) as SelectOption;
expect(result.value).toBe(Constants.UserStatuses.ONLINE);
expect(getValueOfSendMobileNotificationWhenSelect(Constants.UserStatuses.AWAY)).not.toBeUndefined();
const result2 = getValueOfSendMobileNotificationWhenSelect(Constants.UserStatuses.AWAY) as SelectOption;
expect(result2.value).toBe(Constants.UserStatuses.AWAY);
});
});

View File

@ -0,0 +1,582 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {Fragment, useCallback, useEffect, useMemo, useRef, memo} from 'react';
import type {ChangeEvent, ReactNode} from 'react';
import {FormattedMessage} from 'react-intl';
import ReactSelect from 'react-select';
import type {ValueType, OptionsType} from 'react-select';
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 Constants, {NotificationLevels, UserSettingsNotificationSections} from 'utils/constants';
import type {Props as UserSettingsNotificationsProps} from '../user_settings_notifications';
export type SelectOption = {
label: ReactNode;
value: string;
};
export type Props = {
active: boolean;
updateSection: (section: string) => void;
onSubmit: () => void;
onCancel: () => void;
saving: boolean;
error: string;
setParentState: (key: string, value: string | boolean) => void;
areAllSectionsInactive: boolean;
isCollapsedThreadsEnabled: boolean;
desktopActivity: UserNotifyProps['desktop'];
sendPushNotifications: UserSettingsNotificationsProps['sendPushNotifications'];
pushActivity: UserNotifyProps['push'];
pushStatus: UserNotifyProps['push_status'];
desktopThreads: UserNotifyProps['desktop_threads'];
pushThreads: UserNotifyProps['push_threads'];
desktopAndMobileSettingsDifferent: boolean;
};
function DesktopAndMobileNotificationSettings({
active,
updateSection,
onSubmit,
onCancel,
saving,
error,
setParentState,
areAllSectionsInactive,
isCollapsedThreadsEnabled,
desktopActivity,
sendPushNotifications,
pushActivity,
pushStatus,
desktopThreads,
pushThreads,
desktopAndMobileSettingsDifferent,
}: Props) {
const editButtonRef = useRef<SettingItemMinComponent>(null);
const previousActiveRef = useRef(active);
// Focus back on the edit button, after this section was closed after it was opened
useEffect(() => {
if (previousActiveRef.current && !active && areAllSectionsInactive) {
editButtonRef.current?.focus();
}
previousActiveRef.current = active;
}, [active, areAllSectionsInactive]);
const handleChangeForSendDesktopNotificationsRadio = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setParentState('desktopActivity', value);
}, [setParentState]);
const handleChangeForDesktopThreadsCheckbox = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.checked ? NotificationLevels.ALL : NotificationLevels.MENTION;
setParentState('desktopThreads', value);
}, [setParentState]);
const handleChangeForDifferentMobileNotificationsCheckbox = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.checked;
setParentState('desktopAndMobileSettingsDifferent', value);
}, [setParentState]);
const handleChangeForSendMobileNotificationsSelect = useCallback((selectedOption: ValueType<SelectOption>) => {
if (selectedOption && 'value' in selectedOption) {
setParentState('pushActivity', selectedOption.value);
}
}, [setParentState]);
const handleChangeForMobileThreadsCheckbox = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.checked ? NotificationLevels.ALL : NotificationLevels.MENTION;
setParentState('pushThreads', value);
}, [setParentState]);
const handleChangeForTriggerMobileNotificationsSelect = useCallback((selectedOption: ValueType<SelectOption>) => {
if (selectedOption && 'value' in selectedOption) {
setParentState('pushStatus', selectedOption.value);
}
}, [setParentState]);
const maximizedSettingsInputs = useMemo(() => {
const maximizedSettingInputs = [];
const sendDesktopNotificationsSection = (
<fieldset
id='sendDesktopNotificationsSection'
key='sendDesktopNotificationsSection'
>
<legend className='form-legend'>
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.sendDesktopNotificationFor'
defaultMessage='Send notifications for:'
/>
</legend>
{optionsOfSendNotifications.map((optionOfSendNotifications) => (
<div
key={optionOfSendNotifications.value}
className='radio'
>
<label>
<input
type='radio'
checked={desktopActivity === optionOfSendNotifications.value}
value={optionOfSendNotifications.value}
onChange={handleChangeForSendDesktopNotificationsRadio}
/>
{optionOfSendNotifications.label}
</label>
</div>
))}
</fieldset>
);
maximizedSettingInputs.push(sendDesktopNotificationsSection);
if (shouldShowDesktopThreadsSection(isCollapsedThreadsEnabled, desktopActivity)) {
const desktopThreadNotificationSection = (
<Fragment key='desktopThreadNotificationSection'>
<br/>
<div className='checkbox single-checkbox'>
<label>
<input
type='checkbox'
checked={desktopThreads === NotificationLevels.ALL}
onChange={handleChangeForDesktopThreadsCheckbox}
/>
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.notifyForDesktopthreads'
defaultMessage={'Notify me about replies to threads I\'m following'}
/>
</label>
</div>
</Fragment>
);
maximizedSettingInputs.push(desktopThreadNotificationSection);
}
if (sendPushNotifications) {
const differentMobileNotificationsSection = (
<Fragment key='differentMobileNotificationsSection'>
<hr/>
<div className='checkbox single-checkbox'>
<label>
<input
type='checkbox'
checked={desktopAndMobileSettingsDifferent}
onChange={handleChangeForDifferentMobileNotificationsCheckbox}
/>
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.differentMobileNotificationsTitle'
defaultMessage='Use different settings for my mobile devices'
/>
</label>
</div>
</Fragment>
);
maximizedSettingInputs.push(differentMobileNotificationsSection);
}
if (shouldShowSendMobileNotificationsSection(sendPushNotifications, desktopAndMobileSettingsDifferent)) {
const sendMobileNotificationsSection = (
<React.Fragment key='sendMobileNotificationsSection'>
<br/>
<label
id='sendMobileNotificationsLabel'
htmlFor='sendMobileNotificationsSelectInput'
className='singleSelectLabel'
>
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.sendMobileNotificationsFor'
defaultMessage='Send mobile notifications for:'
/>
</label>
<ReactSelect
inputId='sendMobileNotificationsSelectInput'
aria-labelledby='sendMobileNotificationsLabel'
className='react-select singleSelect'
classNamePrefix='react-select'
options={optionsOfSendNotifications}
clearable={false}
isClearable={false}
isSearchable={false}
components={{IndicatorSeparator: NoIndicatorSeparatorComponent}}
value={getValueOfSendMobileNotificationForSelect(pushActivity)}
onChange={handleChangeForSendMobileNotificationsSelect}
/>
</React.Fragment>
);
maximizedSettingInputs.push(sendMobileNotificationsSection);
}
if (shouldShowMobileThreadsSection(sendPushNotifications, isCollapsedThreadsEnabled, desktopAndMobileSettingsDifferent, pushActivity)) {
const threadNotificationSection = (
<Fragment key='threadNotificationSection'>
<br/>
<div className='checkbox single-checkbox'>
<label>
<input
type='checkbox'
checked={pushThreads === NotificationLevels.ALL}
onChange={handleChangeForMobileThreadsCheckbox}
/>
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.notifyForMobilethreads'
defaultMessage={'Notify me on mobile about replies to threads I\'m following'}
/>
</label>
</div>
</Fragment>
);
maximizedSettingInputs.push(threadNotificationSection);
}
if (shouldShowTriggerMobileNotificationsSection(sendPushNotifications, desktopActivity, pushActivity, desktopAndMobileSettingsDifferent)) {
const triggerMobileNotificationsSection = (
<React.Fragment key='triggerMobileNotificationsSection'>
<br/>
<label
id='pushMobileNotificationsLabel'
htmlFor='pushMobileNotificationSelectInput'
className='singleSelectLabel'
>
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.pushNotification'
defaultMessage='Trigger mobile notifications when I am:'
/>
</label>
<ReactSelect
inputId='pushMobileNotificationSelectInput'
aria-labelledby='pushMobileNotificationsLabel'
className='react-select singleSelect'
classNamePrefix='react-select'
options={optionsOfSendMobileNotificationsWhenSelect}
clearable={false}
isClearable={false}
isSearchable={false}
components={{IndicatorSeparator: NoIndicatorSeparatorComponent}}
value={getValueOfSendMobileNotificationWhenSelect(pushStatus)}
onChange={handleChangeForTriggerMobileNotificationsSelect}
/>
</React.Fragment>
);
maximizedSettingInputs.push(triggerMobileNotificationsSection);
}
if (!sendPushNotifications) {
const disabledPushNotificationsSection = (
<>
<br/>
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.pushNotificationsDisabled'
defaultMessage={'Mobile push notifications haven\'t been enabled by your system administrator.'}
/>
</>
);
maximizedSettingInputs.push(disabledPushNotificationsSection);
}
return maximizedSettingInputs;
},
[
desktopActivity,
handleChangeForSendDesktopNotificationsRadio,
isCollapsedThreadsEnabled,
desktopThreads,
handleChangeForDesktopThreadsCheckbox,
sendPushNotifications,
desktopAndMobileSettingsDifferent,
handleChangeForDifferentMobileNotificationsCheckbox,
pushActivity,
handleChangeForSendMobileNotificationsSelect,
pushThreads,
handleChangeForMobileThreadsCheckbox,
pushStatus,
handleChangeForTriggerMobileNotificationsSelect,
]);
function handleChangeForMaxSection(section: string) {
updateSection(section);
}
function handleChangeForMinSection(section: string) {
updateSection(section);
onCancel();
}
if (active) {
return (
<SettingItemMax
title={
<FormattedMessage
id={'user.settings.notifications.desktopAndMobile.title'}
defaultMessage='Desktop and mobile notifications'
/>
}
inputs={maximizedSettingsInputs}
submit={onSubmit}
saving={saving}
serverError={error}
updateSection={handleChangeForMaxSection}
/>
);
}
return (
<SettingItemMin
ref={editButtonRef}
title={
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.title'
defaultMessage='Desktop and mobile notifications'
/>
}
describe={getCollapsedText(desktopActivity, pushActivity)}
section={UserSettingsNotificationSections.DESKTOP_AND_MOBILE}
updateSection={handleChangeForMinSection}
/>
);
}
function NoIndicatorSeparatorComponent() {
return null;
}
const optionsOfSendNotifications = [
{
label: (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.allNewMessages'
defaultMessage='All new messages'
/>
),
value: NotificationLevels.ALL,
},
{
label: (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.onlyMentions'
defaultMessage='Mentions, direct messages, and group messages'
/>
),
value: NotificationLevels.MENTION,
},
{
label: (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.nothing'
defaultMessage='Nothing'
/>
),
value: NotificationLevels.NONE,
},
];
export function shouldShowDesktopThreadsSection(isCollapsedThreadsEnabled: boolean, desktopActivity: UserNotifyProps['desktop']) {
if (!isCollapsedThreadsEnabled) {
return false;
}
if (desktopActivity === NotificationLevels.ALL || desktopActivity === NotificationLevels.NONE) {
return false;
}
return true;
}
export function shouldShowMobileThreadsSection(sendPushNotifications: UserSettingsNotificationsProps['sendPushNotifications'], isCollapsedThreadsEnabled: boolean, desktopAndMobileSettingsDifferent: boolean, pushActivity: UserNotifyProps['push']) {
if (!sendPushNotifications) {
return false;
}
if (!isCollapsedThreadsEnabled) {
return false;
}
if (!desktopAndMobileSettingsDifferent) {
return false;
}
if (pushActivity === NotificationLevels.ALL || pushActivity === NotificationLevels.NONE) {
return false;
}
return true;
}
function shouldShowSendMobileNotificationsSection(sendPushNotifications: UserSettingsNotificationsProps['sendPushNotifications'], desktopAndMobileSettingsDifferent: boolean) {
if (!sendPushNotifications) {
return false;
}
if (desktopAndMobileSettingsDifferent) {
return true;
}
return false;
}
export function getValueOfSendMobileNotificationForSelect(pushActivity: UserNotifyProps['push']): ValueType<SelectOption> {
if (!pushActivity) {
return optionsOfSendNotifications[1];
}
const option = optionsOfSendNotifications.find((option) => option.value === pushActivity);
if (!option) {
return optionsOfSendNotifications[1];
}
return option;
}
export function shouldShowTriggerMobileNotificationsSection(sendPushNotifications: UserSettingsNotificationsProps['sendPushNotifications'], desktopActivity: UserNotifyProps['desktop'], pushActivity: UserNotifyProps['push'], desktopAndMobileSettingsDifferent: boolean): boolean {
if (!sendPushNotifications) {
return false;
}
if (!desktopActivity || !pushActivity) {
return true;
}
if (!desktopAndMobileSettingsDifferent) {
if (desktopActivity === NotificationLevels.NONE) {
return false;
}
return true;
}
if (pushActivity === NotificationLevels.NONE) {
return false;
}
return true;
}
const optionsOfSendMobileNotificationsWhenSelect: OptionsType<SelectOption> = [
{
label: (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.online'
defaultMessage='Online, away, or offline'
/>
),
value: Constants.UserStatuses.ONLINE,
},
{
label: (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.away'
defaultMessage='Away or offline'
/>
),
value: Constants.UserStatuses.AWAY,
},
{
label: (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.offline'
defaultMessage='Offline'
/>
),
value: Constants.UserStatuses.OFFLINE,
},
];
export function getValueOfSendMobileNotificationWhenSelect(pushStatus?: UserNotifyProps['push_status']): ValueType<SelectOption> {
if (!pushStatus) {
return optionsOfSendMobileNotificationsWhenSelect[2];
}
const option = optionsOfSendMobileNotificationsWhenSelect.find((option) => option.value === pushStatus);
if (!option) {
return optionsOfSendMobileNotificationsWhenSelect[2];
}
return option;
}
function getCollapsedText(desktopActivity: UserNotifyProps['desktop'], pushActivity: UserNotifyProps['push']): ReactNode {
if (desktopActivity === NotificationLevels.ALL) {
if (pushActivity === NotificationLevels.ALL) {
return (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.allForDesktopAndMobile'
defaultMessage='All new messages'
/>
);
} else if (pushActivity === NotificationLevels.MENTION) {
return (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.allDesktopButMobileMentions'
defaultMessage='All new messages on desktop; mentions, direct messages, and group messages on mobile'
/>
);
} else if (pushActivity === NotificationLevels.NONE) {
return (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.allDesktopButMobileNone'
defaultMessage='All new messages on desktop; never on mobile'
/>
);
}
} else if (desktopActivity === NotificationLevels.MENTION) {
if (pushActivity === NotificationLevels.ALL) {
return (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.mentionsDesktopButMobileAll'
defaultMessage='Mentions, direct messages, and group messages on desktop; all new messages on mobile'
/>
);
} else if (pushActivity === NotificationLevels.MENTION) {
return (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.mentionsForDesktopAndMobile'
defaultMessage='Mentions, direct messages, and group messages'
/>
);
} else if (pushActivity === NotificationLevels.NONE) {
return (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.mentionsForDesktopButMobileNone'
defaultMessage='Mentions, direct messages, and group messages on desktop; never on mobile'
/>
);
}
} else if (desktopActivity === NotificationLevels.NONE) {
if (pushActivity === NotificationLevels.ALL) {
return (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.noneDesktopButMobileAll'
defaultMessage='Never on desktop; all new messages on mobile'
/>
);
} else if (pushActivity === NotificationLevels.MENTION) {
return (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.noneDesktopButMobileMentions'
defaultMessage='Never on desktop; mentions, direct messages, and group messages on mobile'
/>
);
} else if (pushActivity === NotificationLevels.NONE) {
return (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.noneForDesktopAndMobile'
defaultMessage='Never'
/>
);
}
}
return (
<FormattedMessage
id='user.settings.notifications.desktopAndMobile.noValidSettings'
defaultMessage='Configure desktop and mobile settings'
/>
);
}
export default memo(DesktopAndMobileNotificationSettings);

View File

@ -1,155 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {ComponentProps} from 'react';
import {NotificationLevels} from 'utils/constants';
import DesktopNotificationSettings from './desktop_notification_settings';
jest.mock('utils/notification_sounds', () => {
const original = jest.requireActual('utils/notification_sounds');
return {
...original,
hasSoundOptions: jest.fn(() => true),
};
});
describe('components/user_settings/notifications/DesktopNotificationSettings', () => {
const baseProps: ComponentProps<typeof DesktopNotificationSettings> = {
active: true,
updateSection: jest.fn(),
onSubmit: jest.fn(),
onCancel: jest.fn(),
saving: false,
error: '',
setParentState: jest.fn(),
areAllSectionsInactive: false,
isCollapsedThreadsEnabled: false,
activity: NotificationLevels.MENTION,
threads: NotificationLevels.ALL,
sound: 'false',
callsSound: 'false',
selectedSound: 'Bing',
callsSelectedSound: 'Dynamic',
isCallsRingingEnabled: false,
};
test('should match snapshot, on max setting', () => {
const wrapper = shallow(
<DesktopNotificationSettings {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on max setting with sound enabled', () => {
const props = {...baseProps, sound: 'true'};
const wrapper = shallow(
<DesktopNotificationSettings {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on max setting with Calls enabled', () => {
const props = {...baseProps, isCallsRingingEnabled: true};
const wrapper = shallow(
<DesktopNotificationSettings {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on max setting with Calls enabled, calls sound true', () => {
const props = {...baseProps, isCallsRingingEnabled: true, callsSound: 'true'};
const wrapper = shallow(
<DesktopNotificationSettings {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on min setting', () => {
const props = {...baseProps, active: false};
const wrapper = shallow(
<DesktopNotificationSettings {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should call props.updateSection and props.onCancel on handleMinUpdateSection', () => {
const props = {...baseProps, updateSection: jest.fn(), onCancel: jest.fn()};
const wrapper = shallow<DesktopNotificationSettings>(
<DesktopNotificationSettings {...props}/>,
);
wrapper.instance().handleMinUpdateSection('');
expect(props.updateSection).toHaveBeenCalledTimes(1);
expect(props.updateSection).toHaveBeenCalledWith('');
expect(props.onCancel).toHaveBeenCalledTimes(1);
expect(props.onCancel).toHaveBeenCalledWith();
wrapper.instance().handleMinUpdateSection('desktop');
expect(props.updateSection).toHaveBeenCalledTimes(2);
expect(props.updateSection).toHaveBeenCalledWith('desktop');
expect(props.onCancel).toHaveBeenCalledTimes(2);
expect(props.onCancel).toHaveBeenCalledWith();
});
test('should call props.updateSection on handleMaxUpdateSection', () => {
const props = {...baseProps, updateSection: jest.fn()};
const wrapper = shallow<DesktopNotificationSettings>(
<DesktopNotificationSettings {...props}/>,
);
wrapper.instance().handleMaxUpdateSection('');
expect(props.updateSection).toHaveBeenCalledTimes(1);
expect(props.updateSection).toHaveBeenCalledWith('');
wrapper.instance().handleMaxUpdateSection('desktop');
expect(props.updateSection).toHaveBeenCalledTimes(2);
expect(props.updateSection).toHaveBeenCalledWith('desktop');
});
test('should call props.setParentState on handleOnChange', () => {
const props = {...baseProps, setParentState: jest.fn()};
const wrapper = shallow<DesktopNotificationSettings>(
<DesktopNotificationSettings {...props}/>,
);
wrapper.instance().handleOnChange({
currentTarget: {getAttribute: (key: string) => {
return {'data-key': 'dataKey', 'data-value': 'dataValue'}[key];
}},
} as unknown as React.ChangeEvent<HTMLInputElement>);
expect(props.setParentState).toHaveBeenCalledTimes(1);
expect(props.setParentState).toHaveBeenCalledWith('dataKey', 'dataValue');
});
test('should match snapshot, on buildMaximizedSetting', () => {
const wrapper = shallow<DesktopNotificationSettings>(
<DesktopNotificationSettings {...baseProps}/>,
);
expect(wrapper.instance().buildMaximizedSetting()).toMatchSnapshot();
wrapper.setProps({activity: NotificationLevels.NONE});
expect(wrapper.instance().buildMaximizedSetting()).toMatchSnapshot();
});
test('should match snapshot, on buildMinimizedSetting', () => {
const wrapper = shallow<DesktopNotificationSettings>(
<DesktopNotificationSettings {...baseProps}/>,
);
expect(wrapper.instance().buildMinimizedSetting()).toMatchSnapshot();
wrapper.setProps({activity: NotificationLevels.NONE});
expect(wrapper.instance().buildMinimizedSetting()).toMatchSnapshot();
});
});

View File

@ -1,547 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {type ChangeEvent, type RefObject, type ReactNode} from 'react';
import {FormattedMessage} from 'react-intl';
import ReactSelect, {type ValueType} from 'react-select';
import SettingItemMax from 'components/setting_item_max';
import SettingItemMin from 'components/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min';
import {NotificationLevels} from 'utils/constants';
import * as NotificationSounds from 'utils/notification_sounds';
import {a11yFocus} from 'utils/utils';
type SelectedOption = {
label: string;
value: string;
};
type Props = {
active: boolean;
updateSection: (section: string) => void;
onSubmit: () => void;
onCancel: () => void;
saving: boolean;
error: string;
setParentState: (key: string, value: string | boolean) => void;
areAllSectionsInactive: boolean;
isCollapsedThreadsEnabled: boolean;
activity: string;
threads?: string;
sound: string;
callsSound: string;
selectedSound: string;
callsSelectedSound: string;
isCallsRingingEnabled: boolean;
};
type State = {
selectedOption: SelectedOption;
callsSelectedOption: SelectedOption;
blurDropdown: boolean;
};
export default class DesktopNotificationSettings extends React.PureComponent<Props, State> {
dropdownSoundRef: RefObject<ReactSelect>;
callsDropdownRef: RefObject<ReactSelect>;
editButtonRef: RefObject<SettingItemMinComponent>;
constructor(props: Props) {
super(props);
this.state = {
selectedOption: {value: props.selectedSound, label: props.selectedSound},
callsSelectedOption: {value: props.callsSelectedSound, label: props.callsSelectedSound},
blurDropdown: false,
};
this.dropdownSoundRef = React.createRef();
this.callsDropdownRef = React.createRef();
this.editButtonRef = React.createRef();
}
focusEditButton(): void {
this.editButtonRef.current?.focus();
}
handleMinUpdateSection = (section: string): void => {
this.props.updateSection(section);
this.props.onCancel();
};
handleMaxUpdateSection = (section: string): void => this.props.updateSection(section);
handleOnChange = (e: ChangeEvent<HTMLInputElement>): void => {
const key = e.currentTarget.getAttribute('data-key');
const value = e.currentTarget.getAttribute('data-value');
if (key && value) {
this.props.setParentState(key, value);
a11yFocus(e.currentTarget);
}
if (key === 'callsDesktopSound' && value === 'false') {
NotificationSounds.stopTryNotificationRing();
}
};
handleThreadsOnChange = (e: ChangeEvent<HTMLInputElement>): void => {
const value = e.target.checked ? NotificationLevels.ALL : NotificationLevels.MENTION;
this.props.setParentState('desktopThreads', value);
};
setDesktopNotificationSound: ReactSelect['onChange'] = (selectedOption: ValueType<SelectedOption>): void => {
if (selectedOption && 'value' in selectedOption) {
this.props.setParentState('desktopNotificationSound', selectedOption.value);
this.setState({selectedOption});
NotificationSounds.tryNotificationSound(selectedOption.value);
}
};
setCallsNotificationRing: ReactSelect['onChange'] = (selectedOption: ValueType<SelectedOption>): void => {
if (selectedOption && 'value' in selectedOption) {
this.props.setParentState('callsNotificationSound', selectedOption.value);
this.setState({callsSelectedOption: selectedOption});
NotificationSounds.tryNotificationRing(selectedOption.value);
}
};
blurDropdown(): void {
if (!this.state.blurDropdown) {
this.setState({blurDropdown: true});
if (this.dropdownSoundRef.current) {
this.dropdownSoundRef.current.blur();
}
if (this.callsDropdownRef.current) {
this.callsDropdownRef.current.blur();
}
}
}
buildMaximizedSetting = (): JSX.Element => {
const inputs = [];
const activityRadio = [false, false, false];
if (this.props.activity === NotificationLevels.MENTION) {
activityRadio[1] = true;
} else if (this.props.activity === NotificationLevels.NONE) {
activityRadio[2] = true;
} else {
activityRadio[0] = true;
}
let soundSection;
let notificationSelection;
let threadsNotificationSelection;
let callsSection;
let callsNotificationSelection;
if (this.props.activity !== NotificationLevels.NONE) {
const soundRadio = [false, false];
if (this.props.sound === 'false') {
soundRadio[1] = true;
} else {
soundRadio[0] = true;
}
if (this.props.sound === 'true') {
const sounds = Array.from(NotificationSounds.notificationSounds.keys());
const options = sounds.map((sound) => {
return {value: sound, label: sound};
});
notificationSelection = (<div className='pt-2'>
<ReactSelect
className='react-select notification-sound-dropdown'
classNamePrefix='react-select'
id='displaySoundNotification'
options={options}
clearable={false}
onChange={this.setDesktopNotificationSound}
value={this.state.selectedOption}
isSearchable={false}
ref={this.dropdownSoundRef}
components={{SingleValue: (props) => <div data-testid='displaySoundNotificationValue'>{props.children}</div>}}
/></div>);
}
if (this.props.isCallsRingingEnabled) {
const callsSoundRadio = [false, false];
if (this.props.callsSound === 'false') {
callsSoundRadio[1] = true;
} else {
callsSoundRadio[0] = true;
}
if (this.props.callsSound === 'true') {
const callsSounds = Array.from(NotificationSounds.callsNotificationSounds.keys());
const callsOptions = callsSounds.map((sound) => {
return {value: sound, label: sound};
});
callsNotificationSelection = (<div className='pt-2'>
<ReactSelect
className='react-select notification-sound-dropdown'
classNamePrefix='react-select'
id='displayCallsSoundNotification'
options={callsOptions}
clearable={false}
onChange={this.setCallsNotificationRing}
value={this.state.callsSelectedOption}
isSearchable={false}
ref={this.callsDropdownRef}
components={{SingleValue: (props) => <div data-testid='displayCallsSoundNotificationValue'>{props.children}</div>}}
/></div>);
}
callsSection = (
<>
<hr/>
<fieldset>
<legend className='form-legend'>
<FormattedMessage
id='user.settings.notifications.desktop.calls_sound'
defaultMessage='Notification sound for incoming calls'
/>
</legend>
<div className='radio'>
<label>
<input
id='callsSoundOn'
type='radio'
name='callsNotificationSounds'
checked={callsSoundRadio[0]}
data-key={'callsDesktopSound'}
data-value={'true'}
onChange={this.handleOnChange}
/>
<FormattedMessage
id='user.settings.notifications.on'
defaultMessage='On'
/>
</label>
<br/>
</div>
<div className='radio'>
<label>
<input
id='soundOff'
type='radio'
name='callsNotificationSounds'
checked={callsSoundRadio[1]}
data-key={'callsDesktopSound'}
data-value={'false'}
onChange={this.handleOnChange}
/>
<FormattedMessage
id='user.settings.notifications.off'
defaultMessage='Off'
/>
</label>
<br/>
</div>
{callsNotificationSelection}
</fieldset>
</>
);
}
if (NotificationSounds.hasSoundOptions()) {
soundSection = (
<fieldset>
<legend className='form-legend'>
<FormattedMessage
id='user.settings.notifications.desktop.sound'
defaultMessage='Notification sound'
/>
</legend>
<div className='radio'>
<label>
<input
id='soundOn'
type='radio'
name='notificationSounds'
checked={soundRadio[0]}
data-key={'desktopSound'}
data-value={'true'}
onChange={this.handleOnChange}
/>
<FormattedMessage
id='user.settings.notifications.on'
defaultMessage='On'
/>
</label>
<br/>
</div>
<div className='radio'>
<label>
<input
id='soundOff'
type='radio'
name='notificationSounds'
checked={soundRadio[1]}
data-key={'desktopSound'}
data-value={'false'}
onChange={this.handleOnChange}
/>
<FormattedMessage
id='user.settings.notifications.off'
defaultMessage='Off'
/>
</label>
<br/>
</div>
{notificationSelection}
<div className='mt-5'>
<FormattedMessage
id='user.settings.notifications.sounds_info'
defaultMessage='Notification sounds are available on Firefox, Edge, Safari, Chrome and Mattermost Desktop Apps.'
/>
</div>
</fieldset>
);
} else {
soundSection = (
<fieldset>
<legend className='form-legend'>
<FormattedMessage
id='user.settings.notifications.desktop.sound'
defaultMessage='Notification sound'
/>
</legend>
<br/>
<FormattedMessage
id='user.settings.notifications.soundConfig'
defaultMessage='Please configure notification sounds in your browser settings'
/>
</fieldset>
);
}
}
if (this.props.isCollapsedThreadsEnabled && NotificationLevels.MENTION === this.props.activity) {
threadsNotificationSelection = (
<>
<fieldset>
<legend className='form-legend'>
<FormattedMessage
id='user.settings.notifications.threads.desktop'
defaultMessage='Thread reply notifications'
/>
</legend>
<div className='checkbox'>
<label>
<input
id='desktopThreadsNotificationAllActivity'
type='checkbox'
name='desktopThreadsNotificationLevel'
checked={this.props.threads === NotificationLevels.ALL}
onChange={this.handleThreadsOnChange}
/>
<FormattedMessage
id='user.settings.notifications.threads.allActivity'
defaultMessage={'Notify me about threads I\'m following'}
/>
</label>
<br/>
</div>
<div className='mt-5'>
<FormattedMessage
id='user.settings.notifications.threads'
defaultMessage={'When enabled, any reply to a thread you\'re following will send a desktop notification.'}
/>
</div>
</fieldset>
<hr/>
</>
);
}
inputs.push(
<div key='userNotificationLevelOption'>
<fieldset>
<legend className='form-legend'>
<FormattedMessage
id='user.settings.notifications.desktop'
defaultMessage='Send desktop notifications'
/>
</legend>
<div className='radio'>
<label>
<input
id='desktopNotificationAllActivity'
type='radio'
name='desktopNotificationLevel'
checked={activityRadio[0]}
data-key={'desktopActivity'}
data-value={NotificationLevels.ALL}
onChange={this.handleOnChange}
/>
<FormattedMessage
id='user.settings.notifications.allActivity'
defaultMessage='For all activity'
/>
</label>
<br/>
</div>
<div className='radio'>
<label>
<input
id='desktopNotificationMentions'
type='radio'
name='desktopNotificationLevel'
checked={activityRadio[1]}
data-key={'desktopActivity'}
data-value={NotificationLevels.MENTION}
onChange={this.handleOnChange}
/>
<FormattedMessage
id='user.settings.notifications.onlyMentions'
defaultMessage='Only for mentions, direct messages, and group messages'
/>
</label>
<br/>
</div>
<div className='radio'>
<label>
<input
id='desktopNotificationNever'
type='radio'
name='desktopNotificationLevel'
checked={activityRadio[2]}
data-key={'desktopActivity'}
data-value={NotificationLevels.NONE}
onChange={this.handleOnChange}
/>
<FormattedMessage
id='user.settings.notifications.never'
defaultMessage='Never'
/>
</label>
</div>
<div className='mt-5'>
<FormattedMessage
id='user.settings.notifications.info'
defaultMessage='Desktop notifications are available on Edge, Firefox, Safari, Chrome and Mattermost Desktop Apps.'
/>
</div>
</fieldset>
<hr/>
{threadsNotificationSelection}
{soundSection}
{callsSection}
</div>,
);
return (
<SettingItemMax
title={
<FormattedMessage
id={'user.settings.notifications.desktop.title'}
defaultMessage={'Desktop Notifications'}
/>
}
inputs={inputs}
submit={this.props.onSubmit}
saving={this.props.saving}
serverError={this.props.error}
updateSection={this.handleMaxUpdateSection}
/>
);
};
buildMinimizedSetting = () => {
const hasSoundOption = NotificationSounds.hasSoundOptions();
let collapsedDescription: ReactNode = null;
if (this.props.activity === NotificationLevels.MENTION) {
if (hasSoundOption && this.props.sound !== 'false') {
collapsedDescription = (
<FormattedMessage
id='user.settings.notifications.desktop.mentionsSound'
defaultMessage='For mentions and direct messages, with sound'
/>
);
} else if (hasSoundOption && this.props.sound === 'false') {
collapsedDescription = (
<FormattedMessage
id='user.settings.notifications.desktop.mentionsNoSound'
defaultMessage='For mentions and direct messages, without sound'
/>
);
} else {
collapsedDescription = (
<FormattedMessage
id='user.settings.notifications.desktop.mentionsSoundHidden'
defaultMessage='For mentions and direct messages'
/>
);
}
} else if (this.props.activity === NotificationLevels.NONE) {
collapsedDescription = (
<FormattedMessage
id='user.settings.notifications.off'
defaultMessage='Off'
/>
);
} else {
if (hasSoundOption && this.props.sound !== 'false') { //eslint-disable-line no-lonely-if
collapsedDescription = (
<FormattedMessage
id='user.settings.notifications.desktop.allSound'
defaultMessage='For all activity, with sound'
/>
);
} else if (hasSoundOption && this.props.sound === 'false') {
collapsedDescription = (
<FormattedMessage
id='user.settings.notifications.desktop.allNoSound'
defaultMessage='For all activity, without sound'
/>
);
} else {
collapsedDescription = (
<FormattedMessage
id='user.settings.notifications.desktop.allSoundHidden'
defaultMessage='For all activity'
/>
);
}
}
return (
<SettingItemMin
ref={this.editButtonRef}
title={
<FormattedMessage
id={'user.settings.notifications.desktop.title'}
defaultMessage={'Desktop Notifications'}
/>
}
describe={collapsedDescription}
section={'desktop'}
updateSection={this.handleMinUpdateSection}
/>
);
};
componentDidUpdate(prevProps: Props) {
this.blurDropdown();
if (prevProps.active && !this.props.active && this.props.areAllSectionsInactive) {
this.focusEditButton();
}
if (this.props.selectedSound !== prevProps.selectedSound) {
this.setState({selectedOption: {value: this.props.selectedSound, label: this.props.selectedSound}});
}
if (this.props.callsSelectedSound !== prevProps.callsSelectedSound) {
this.setState({callsSelectedOption: {value: this.props.callsSelectedSound, label: this.props.callsSelectedSound}});
}
}
render() {
if (this.props.active) {
return this.buildMaximizedSetting();
}
return this.buildMinimizedSetting();
}
}

View File

@ -0,0 +1,493 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ChangeEvent, ReactNode} from 'react';
import React, {memo, useEffect, useRef, Fragment, useMemo, useCallback} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import type {ValueType} from 'react-select';
import ReactSelect from 'react-select';
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 {UserSettingsNotificationSections} from 'utils/constants';
import {
callsNotificationSounds,
notificationSounds,
stopTryNotificationRing,
tryNotificationSound,
tryNotificationRing,
} from 'utils/notification_sounds';
import type {Props as UserSettingsNotificationsProps} from '../user_settings_notifications';
export type SelectOption = {
value: string;
label: ReactNode;
};
export type Props = {
active: boolean;
updateSection: (section: string) => void;
onSubmit: () => void;
onCancel: () => void;
saving: boolean;
error: string;
setParentState: (key: string, value: string | boolean) => void;
areAllSectionsInactive: boolean;
desktopSound: UserNotifyProps['desktop_sound'];
desktopNotificationSound: UserNotifyProps['desktop_notification_sound'];
isCallsRingingEnabled: UserSettingsNotificationsProps['isCallsRingingEnabled'];
callsDesktopSound: UserNotifyProps['calls_desktop_sound'];
callsNotificationSound: UserNotifyProps['calls_notification_sound'];
};
function DesktopNotificationSoundsSettings({
active,
updateSection,
onSubmit,
onCancel,
saving,
error,
setParentState,
areAllSectionsInactive,
desktopSound,
desktopNotificationSound,
isCallsRingingEnabled,
callsDesktopSound,
callsNotificationSound,
}: Props) {
const intl = useIntl();
const editButtonRef = useRef<SettingItemMinComponent>(null);
const previousActiveRef = useRef(active);
// Focus back on the edit button, after this section was closed after it was opened
useEffect(() => {
if (previousActiveRef.current && !active && areAllSectionsInactive) {
editButtonRef.current?.focus();
}
previousActiveRef.current = active;
}, [active, areAllSectionsInactive]);
const handleChangeForMessageNotificationSoundCheckbox = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.checked ? 'true' : 'false';
setParentState('desktopSound', value);
if (value === 'false') {
stopTryNotificationRing();
}
}, [setParentState]);
const handleChangeForIncomginCallSoundCheckbox = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.checked ? 'true' : 'false';
setParentState('callsDesktopSound', value);
if (value === 'false') {
stopTryNotificationRing();
}
}, [setParentState]);
const handleChangeForMessageNotificationSoundSelect = useCallback((selectedOption: ValueType<SelectOption>) => {
stopTryNotificationRing();
if (selectedOption && 'value' in selectedOption) {
setParentState('desktopNotificationSound', selectedOption.value);
tryNotificationSound(selectedOption.value);
}
}, [setParentState]);
const handleChangeForIncomingCallSoundSelect = useCallback((selectedOption: ValueType<SelectOption>) => {
stopTryNotificationRing();
if (selectedOption && 'value' in selectedOption) {
setParentState('callsNotificationSound', selectedOption.value);
tryNotificationRing(selectedOption.value);
}
}, [setParentState]);
const maximizedSettingInputs = useMemo(() => {
const maximizedSettingInputs = [];
const isMessageNotificationSoundChecked = desktopSound === 'true';
const messageSoundSection = (
<Fragment key='messageSoundSection'>
<div className='checkbox inlineCheckboxSelect'>
<label>
<input
type='checkbox'
checked={desktopSound === 'true'}
onChange={handleChangeForMessageNotificationSoundCheckbox}
/>
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.messageNotificationSound'
defaultMessage='Message notification sound'
/>
</label>
<ReactSelect
id='messageNotificationSoundSelect'
inputId='messageNotificationSoundSelectInput'
className='react-select inlineSelect'
classNamePrefix='react-select'
options={optionsOfMessageNotificationSoundsSelect}
clearable={false}
isClearable={false}
isSearchable={false}
isDisabled={!isMessageNotificationSoundChecked}
placeholder={intl.formatMessage({
id: 'user.settings.notifications.desktopNotificationSound.soundSelectPlaceholder',
defaultMessage: 'Select a sound',
})}
components={{IndicatorSeparator: NoIndicatorSeparatorComponent}}
value={getValueOfMessageNotificationSoundsSelect(desktopNotificationSound)}
onChange={handleChangeForMessageNotificationSoundSelect}
/>
</div>
</Fragment>
);
maximizedSettingInputs.push(messageSoundSection);
if (isCallsRingingEnabled) {
const isIncomingCallSoundChecked = callsDesktopSound === 'true';
const callSoundSection = (
<Fragment key='callSoundSection'>
<br/>
<div className='checkbox inlineCheckboxSelect'>
<label>
<input
type='checkbox'
checked={isIncomingCallSoundChecked}
onChange={handleChangeForIncomginCallSoundCheckbox}
/>
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.incomingCallSound'
defaultMessage='Incoming call sound'
/>
</label>
<ReactSelect
id='incomingCallSoundNotificationSelect'
inputId='incomingCallSoundNotificationSelectInput'
className='react-select inlineSelect'
classNamePrefix='react-select'
options={optionsOfIncomingCallSoundsSelect}
clearable={false}
isClearable={false}
isSearchable={false}
isDisabled={!isIncomingCallSoundChecked}
components={{IndicatorSeparator: NoIndicatorSeparatorComponent}}
placeholder={intl.formatMessage({
id: 'user.settings.notifications.desktopNotificationSound.soundSelectPlaceholder',
defaultMessage: 'Select a sound',
})}
value={getValueOfIncomingCallSoundsSelect(callsNotificationSound)}
onChange={handleChangeForIncomingCallSoundSelect}
/>
</div>
</Fragment>
);
maximizedSettingInputs.push(callSoundSection);
}
return maximizedSettingInputs;
},
[
desktopSound,
handleChangeForMessageNotificationSoundCheckbox,
handleChangeForMessageNotificationSoundSelect,
desktopNotificationSound,
isCallsRingingEnabled,
callsDesktopSound,
handleChangeForIncomginCallSoundCheckbox,
callsNotificationSound,
handleChangeForIncomingCallSoundSelect,
]);
function handleChangeForMaxSection(section: string) {
stopTryNotificationRing();
updateSection(section);
}
function handleChangeForMinSection(section: string) {
stopTryNotificationRing();
updateSection(section);
onCancel();
}
function handleSubmit() {
stopTryNotificationRing();
onSubmit();
}
if (active) {
return (
<SettingItemMax
title={
<FormattedMessage
id='user.settings.notifications.desktopNotificationSounds.title'
defaultMessage='Desktop notification sounds'
/>
}
inputs={maximizedSettingInputs}
submit={handleSubmit}
saving={saving}
serverError={error}
updateSection={handleChangeForMaxSection}
/>
);
}
return (
<SettingItemMin
ref={editButtonRef}
title={
<FormattedMessage
id='user.settings.notifications.desktopNotificationSounds.title'
defaultMessage='Desktop notification sounds'
/>
}
describe={getCollapsedText(isCallsRingingEnabled, desktopSound, desktopNotificationSound, callsDesktopSound, callsNotificationSound)}
section={UserSettingsNotificationSections.DESKTOP_NOTIFICATION_SOUND}
updateSection={handleChangeForMinSection}
/>
);
}
function NoIndicatorSeparatorComponent() {
return null;
}
const notificationSoundKeys = Array.from(notificationSounds.keys());
const optionsOfMessageNotificationSoundsSelect: SelectOption[] = notificationSoundKeys.map((soundName) => {
if (soundName === 'Bing') {
return {
value: soundName,
label: (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.soundBing'
defaultMessage='Bing'
/>
),
};
} else if (soundName === 'Crackle') {
return {
value: soundName,
label: (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.soundCrackle'
defaultMessage='Crackle'
/>
),
};
} else if (soundName === 'Down') {
return {
value: soundName,
label: (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.soundDown'
defaultMessage='Down'
/>
),
};
} else if (soundName === 'Hello') {
return {
value: soundName,
label: (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.soundHello'
defaultMessage='Hello'
/>
),
};
} else if (soundName === 'Ripple') {
return {
value: soundName,
label: (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.soundRipple'
defaultMessage='Ripple'
/>
),
};
} else if (soundName === 'Upstairs') {
return {
value: soundName,
label: (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.soundUpstairs'
defaultMessage='Upstairs'
/>
),
};
}
return {
value: '',
label: '',
};
});
function getValueOfMessageNotificationSoundsSelect(soundName?: string) {
const soundOption = optionsOfMessageNotificationSoundsSelect.find((option) => option.value === soundName);
if (!soundOption) {
return undefined;
}
return soundOption;
}
const callNotificationSoundKeys = Array.from(callsNotificationSounds.keys());
const optionsOfIncomingCallSoundsSelect: SelectOption[] = callNotificationSoundKeys.map((soundName) => {
if (soundName === 'Dynamic') {
return {
value: soundName,
label: (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.soundDynamic'
defaultMessage='Dynamic'
/>
),
};
} else if (soundName === 'Calm') {
return {
value: soundName,
label: (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.soundCalm'
defaultMessage='Calm'
/>
),
};
} else if (soundName === 'Urgent') {
return {
value: soundName,
label: (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.soundUrgent'
defaultMessage='Urgent'
/>
),
};
} else if (soundName === 'Cheerful') {
return {
value: soundName,
label: (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.soundCheerful'
defaultMessage='Cheerful'
/>
),
};
}
return {
value: '',
label: '',
};
});
function getValueOfIncomingCallSoundsSelect(soundName?: string) {
const soundOption = optionsOfIncomingCallSoundsSelect.find((option) => option.value === soundName);
if (!soundOption) {
return undefined;
}
return soundOption;
}
function getCollapsedText(
isCallsRingingEnabled: UserSettingsNotificationsProps['isCallsRingingEnabled'],
desktopSound: UserNotifyProps['desktop_sound'],
desktopNotificationSound: UserNotifyProps['desktop_notification_sound'],
callsDesktopSound: UserNotifyProps['calls_desktop_sound'],
callsNotificationSound: UserNotifyProps['calls_notification_sound'],
) {
const desktopNotificationSoundIsSelected = notificationSoundKeys.includes(desktopNotificationSound as string);
const callNotificationSoundIsSelected = callNotificationSoundKeys.includes(callsNotificationSound as string);
let hasCallsSound: boolean | null = null;
if (isCallsRingingEnabled && callNotificationSoundIsSelected) {
if (callsDesktopSound === 'true') {
hasCallsSound = true;
} else {
hasCallsSound = false;
}
}
let hasDesktopSound: boolean | null = null;
if (desktopNotificationSoundIsSelected) {
if (desktopSound === 'true') {
hasDesktopSound = true;
} else {
hasDesktopSound = false;
}
}
if (hasDesktopSound !== null && hasCallsSound !== null) {
if (hasDesktopSound && hasCallsSound) {
return (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.hasDesktopAndCallsSound'
defaultMessage='"{desktopSound}" for messages, "{callsSound}" for calls'
values={{
desktopSound: desktopNotificationSound,
callsSound: callsNotificationSound,
}}
/>
);
} else if (!hasDesktopSound && hasCallsSound) {
return (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.noDesktopAndhasCallsSound'
defaultMessage='No sound for messages, "{callsSound}" for calls'
values={{callsSound: callsNotificationSound}}
/>
);
} else if (hasDesktopSound && !hasCallsSound) {
return (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.hasDesktopAndNoCallsSound'
defaultMessage='"{desktopSound}" for messages, no sound for calls'
values={{desktopSound: desktopNotificationSound}}
/>
);
}
return (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.noDesktopAndNoCallsSound'
defaultMessage='No sound'
/>
);
} else if (hasDesktopSound !== null && hasCallsSound === null) {
if (hasDesktopSound) {
return (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.hasDesktopSound'
defaultMessage='"{desktopSound}" for messages'
values={{desktopSound: desktopNotificationSound}}
/>
);
}
return (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.noDesktopSound'
defaultMessage='No sound'
/>
);
}
return (
<FormattedMessage
id='user.settings.notifications.desktopNotificationSound.noValidSound'
defaultMessage='Configure desktop notification sounds'
/>
);
}
export default memo(DesktopNotificationSoundsSettings);

View File

@ -95,7 +95,7 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
submit={[Function]}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
defaultMessage="Email notifications"
id="user.settings.notifications.emailNotifications"
/>
}
@ -109,16 +109,16 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
id="settingTitle"
>
<FormattedMessage
defaultMessage="Email Notifications"
defaultMessage="Email notifications"
id="user.settings.notifications.emailNotifications"
>
<span>
Email Notifications
Email notifications
</span>
</FormattedMessage>
</h4>
<div
className="col-sm-9 col-sm-offset-3"
className="sectionContent col-sm-10 col-sm-offset-2"
>
<div
className="setting-list"
@ -289,7 +289,7 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
section="email"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
defaultMessage="Email notifications"
id="user.settings.notifications.emailNotifications"
/>
}
@ -308,7 +308,7 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
section="email"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
defaultMessage="Email notifications"
id="user.settings.notifications.emailNotifications"
/>
}
@ -327,7 +327,7 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
section="email"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
defaultMessage="Email notifications"
id="user.settings.notifications.emailNotifications"
/>
}
@ -479,7 +479,7 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
submit={[Function]}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
defaultMessage="Email notifications"
id="user.settings.notifications.emailNotifications"
/>
}
@ -493,16 +493,16 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
id="settingTitle"
>
<FormattedMessage
defaultMessage="Email Notifications"
defaultMessage="Email notifications"
id="user.settings.notifications.emailNotifications"
>
<span>
Email Notifications
Email notifications
</span>
</FormattedMessage>
</h4>
<div
className="col-sm-9 col-sm-offset-3"
className="sectionContent col-sm-10 col-sm-offset-2"
>
<div
className="setting-list"
@ -744,7 +744,7 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
serverError=""
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
defaultMessage="Email notifications"
id="user.settings.notifications.emailNotifications"
/>
}
@ -824,7 +824,7 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
submit={[Function]}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
defaultMessage="Email notifications"
id="user.settings.notifications.emailNotifications"
/>
}
@ -898,16 +898,8 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
<React.Fragment>
<hr />
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Thread reply notifications"
id="user.settings.notifications.threads.desktop"
/>
</legend>
<div
className="checkbox"
className="checkbox single-checkbox"
>
<label>
<input
@ -918,19 +910,10 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
type="checkbox"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Notify me about threads I'm following"
id="user.settings.notifications.threads.allActivity"
defaultMessage="Notify me about replies to threads Im following"
id="user.settings.notifications.email.notifyForthreads"
/>
</label>
<br />
</div>
<div
className="mt-5"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="When enabled, any reply to a thread you're following will send an email notification."
id="user.settings.notifications.email_threads"
/>
</div>
</fieldset>
</React.Fragment>,
@ -942,7 +925,7 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
submit={[Function]}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
defaultMessage="Email notifications"
id="user.settings.notifications.emailNotifications"
/>
}
@ -1022,7 +1005,7 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
submit={[Function]}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
defaultMessage="Email notifications"
id="user.settings.notifications.emailNotifications"
/>
}

View File

@ -237,8 +237,8 @@ export default class EmailNotificationSetting extends React.PureComponent<Props,
ref={this.editButtonRef}
title={
<FormattedMessage
id={'user.settings.notifications.emailNotifications'}
defaultMessage={'Email Notifications'}
id='user.settings.notifications.emailNotifications'
defaultMessage='Email notifications'
/>
}
describe={description}
@ -254,8 +254,8 @@ export default class EmailNotificationSetting extends React.PureComponent<Props,
<SettingItemMax
title={
<FormattedMessage
id={'user.settings.notifications.emailNotifications'}
defaultMessage={'Email Notifications'}
id='user.settings.notifications.emailNotifications'
defaultMessage='Email notifications'
/>
}
inputs={[
@ -334,13 +334,7 @@ export default class EmailNotificationSetting extends React.PureComponent<Props,
<React.Fragment key='userNotificationEmailThreadsOptions'>
<hr/>
<fieldset>
<legend className='form-legend'>
<FormattedMessage
id='user.settings.notifications.threads.desktop'
defaultMessage='Thread reply notifications'
/>
</legend>
<div className='checkbox'>
<div className='checkbox single-checkbox'>
<label>
<input
id='desktopThreadsNotificationAllActivity'
@ -350,17 +344,10 @@ export default class EmailNotificationSetting extends React.PureComponent<Props,
onChange={this.handleThreadsOnChange}
/>
<FormattedMessage
id='user.settings.notifications.threads.allActivity'
defaultMessage={'Notify me about threads I\'m following'}
id='user.settings.notifications.email.notifyForthreads'
defaultMessage={'Notify me about replies to threads Im following'}
/>
</label>
<br/>
</div>
<div className='mt-5'>
<FormattedMessage
id='user.settings.notifications.email_threads'
defaultMessage={'When enabled, any reply to a thread you\'re following will send an email notification.'}
/>
</div>
</fieldset>
</React.Fragment>
@ -371,8 +358,8 @@ export default class EmailNotificationSetting extends React.PureComponent<Props,
<SettingItemMax
title={
<FormattedMessage
id={'user.settings.notifications.emailNotifications'}
defaultMessage={'Email Notifications'}
id='user.settings.notifications.emailNotifications'
defaultMessage='Email notifications'
/>
}
inputs={[

View File

@ -48,12 +48,11 @@ exports[`components/user_settings/notifications/ManageAutoResponder should match
submit={[MockFunction]}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Automatic Direct Message Replies"
defaultMessage="Automatic direct message replies"
id="user.settings.notifications.autoResponder"
/>
}
updateSection={[MockFunction]}
width="medium"
>
<section
className="section-max form-horizontal "
@ -63,16 +62,16 @@ exports[`components/user_settings/notifications/ManageAutoResponder should match
id="settingTitle"
>
<FormattedMessage
defaultMessage="Automatic Direct Message Replies"
defaultMessage="Automatic direct message replies"
id="user.settings.notifications.autoResponder"
>
<span>
Automatic Direct Message Replies
Automatic direct message replies
</span>
</FormattedMessage>
</h4>
<div
className="col-sm-10 col-sm-offset-2"
className="sectionContent col-sm-10 col-sm-offset-2"
>
<div
className="setting-list"
@ -262,12 +261,11 @@ exports[`components/user_settings/notifications/ManageAutoResponder should match
submit={[MockFunction]}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Automatic Direct Message Replies"
defaultMessage="Automatic direct message replies"
id="user.settings.notifications.autoResponder"
/>
}
updateSection={[MockFunction]}
width="medium"
>
<section
className="section-max form-horizontal "
@ -277,16 +275,16 @@ exports[`components/user_settings/notifications/ManageAutoResponder should match
id="settingTitle"
>
<FormattedMessage
defaultMessage="Automatic Direct Message Replies"
defaultMessage="Automatic direct message replies"
id="user.settings.notifications.autoResponder"
>
<span>
Automatic Direct Message Replies
Automatic direct message replies
</span>
</FormattedMessage>
</h4>
<div
className="col-sm-10 col-sm-offset-2"
className="sectionContent col-sm-10 col-sm-offset-2"
>
<div
className="setting-list"

View File

@ -106,10 +106,9 @@ export default class ManageAutoResponder extends React.PureComponent<Props> {
title={
<FormattedMessage
id='user.settings.notifications.autoResponder'
defaultMessage='Automatic Direct Message Replies'
defaultMessage='Automatic direct message replies'
/>
}
width='medium'
shiftEnter={true}
submit={this.props.submit}
saving={this.props.saving}

View File

@ -5,9 +5,10 @@ import React from 'react';
import {type IntlShape} from 'react-intl';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {NotificationLevels} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import UserSettingsNotifications from './user_settings_notifications';
import UserSettingsNotifications, {areDesktopAndMobileSettingsDifferent} from './user_settings_notifications';
describe('components/user_settings/display/UserSettingsDisplay', () => {
const defaultProps = {
@ -20,7 +21,7 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
isCollapsedThreadsEnabled: true,
sendPushNotifications: false,
enableAutoResponder: false,
isCallsRingingEnabled: true,
isCallsRingingEnabled: false,
intl: {} as IntlShape,
isEnterpriseOrCloudOrSKUStarterFree: false,
isEnterpriseReady: true,
@ -62,3 +63,27 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
expect(screen.getByText('Reply notifications')).toBeInTheDocument();
});
});
describe('areDesktopAndMobileSettingsDifferent', () => {
test('should return true when desktop and push notification levels are different', () => {
expect(areDesktopAndMobileSettingsDifferent(NotificationLevels.ALL, NotificationLevels.MENTION, NotificationLevels.ALL, NotificationLevels.NONE, true)).toBe(true);
expect(areDesktopAndMobileSettingsDifferent(NotificationLevels.ALL, NotificationLevels.NONE, NotificationLevels.ALL, NotificationLevels.MENTION, true)).toBe(true);
expect(areDesktopAndMobileSettingsDifferent(NotificationLevels.ALL, NotificationLevels.NONE, NotificationLevels.MENTION, NotificationLevels.NONE, true)).toBe(true);
expect(areDesktopAndMobileSettingsDifferent(NotificationLevels.ALL, NotificationLevels.MENTION, NotificationLevels.NONE, NotificationLevels.NONE, true)).toBe(true);
expect(areDesktopAndMobileSettingsDifferent(NotificationLevels.ALL, NotificationLevels.NONE, NotificationLevels.NONE, NotificationLevels.NONE, true)).toBe(true);
expect(areDesktopAndMobileSettingsDifferent(NotificationLevels.ALL, NotificationLevels.MENTION, NotificationLevels.ALL, NotificationLevels.MENTION, true)).toBe(true);
expect(areDesktopAndMobileSettingsDifferent(NotificationLevels.ALL, NotificationLevels.NONE, NotificationLevels.ALL, NotificationLevels.NONE, true)).toBe(true);
});
test('should return false when desktop and push notification levels are the same', () => {
expect(areDesktopAndMobileSettingsDifferent(NotificationLevels.ALL, NotificationLevels.ALL, NotificationLevels.ALL, NotificationLevels.ALL, true)).toBe(false);
expect(areDesktopAndMobileSettingsDifferent(NotificationLevels.MENTION, NotificationLevels.MENTION, NotificationLevels.MENTION, NotificationLevels.MENTION, false)).toBe(false);
});
test('should return true any of desktop or push settings are undefined', () => {
expect(areDesktopAndMobileSettingsDifferent(undefined as any, NotificationLevels.ALL, NotificationLevels.ALL, NotificationLevels.ALL, true)).toBe(true);
expect(areDesktopAndMobileSettingsDifferent(NotificationLevels.ALL, undefined as any, NotificationLevels.ALL, NotificationLevels.ALL, true)).toBe(true);
expect(areDesktopAndMobileSettingsDifferent(NotificationLevels.ALL, NotificationLevels.ALL, undefined as any, NotificationLevels.ALL, true)).toBe(true);
expect(areDesktopAndMobileSettingsDifferent(NotificationLevels.ALL, NotificationLevels.ALL, NotificationLevels.ALL, undefined as any, true)).toBe(true);
});
});

View File

@ -11,21 +11,19 @@ import type {Styles as ReactSelectStyles, ValueType} from 'react-select';
import CreatableReactSelect from 'react-select/creatable';
import {LightbulbOutlineIcon} from '@mattermost/compass-icons/components';
import type {ServerError} from '@mattermost/types/errors';
import type {UserNotifyProps, UserProfile} from '@mattermost/types/users';
import type {ActionResult} from 'mattermost-redux/types/actions';
import ExternalLink from 'components/external_link';
import SettingItem from 'components/setting_item';
import SettingItemMax from 'components/setting_item_max';
import RestrictedIndicator from 'components/widgets/menu/menu_items/restricted_indicator';
import Constants, {NotificationLevels, MattermostFeatures, LicenseSkus} from 'utils/constants';
import Constants, {NotificationLevels, MattermostFeatures, LicenseSkus, UserSettingsNotificationSections} from 'utils/constants';
import {stopTryNotificationRing} from 'utils/notification_sounds';
import {a11yFocus} from 'utils/utils';
import DesktopNotificationSettings from './desktop_notification_setting/desktop_notification_settings';
import DesktopAndMobileNotificationSettings from './desktop_and_mobile_notification_setting';
import DesktopNotificationSoundsSettings from './desktop_notification_sounds_setting';
import EmailNotificationSetting from './email_notification_setting';
import ManageAutoResponder from './manage_auto_responder/manage_auto_responder';
@ -50,7 +48,7 @@ type OwnProps = {
collapseModal: () => void;
}
type Props = PropsFromRedux & OwnProps & WrappedComponentProps;
export type Props = PropsFromRedux & OwnProps & WrappedComponentProps;
type State = {
enableEmail: UserNotifyProps['email'];
@ -77,6 +75,7 @@ type State = {
notifyCommentsLevel: UserNotifyProps['comments'];
isSaving: boolean;
serverError: string;
desktopAndMobileSettingsDifferent: boolean;
};
function getDefaultStateFromProps(props: Props): State {
@ -97,6 +96,7 @@ function getDefaultStateFromProps(props: Props): State {
id: 'user.settings.notifications.autoResponderDefault',
defaultMessage: 'Hello, I am out of office and unable to respond to messages.',
});
let desktopAndMobileSettingsDifferent = true;
if (props.user.notify_props) {
if (props.user.notify_props.desktop) {
@ -143,6 +143,10 @@ function getDefaultStateFromProps(props: Props): State {
if (props.user.notify_props.auto_responder_message) {
autoResponderMessage = props.user.notify_props.auto_responder_message;
}
if (props.user.notify_props.desktop && props.user.notify_props.push) {
desktopAndMobileSettingsDifferent = areDesktopAndMobileSettingsDifferent(props.user.notify_props.desktop, props.user.notify_props.push, props.user.notify_props?.desktop_threads, props.user.notify_props?.push_threads, props.isCollapsedThreadsEnabled);
}
}
let usernameKey = false;
@ -211,6 +215,7 @@ function getDefaultStateFromProps(props: Props): State {
notifyCommentsLevel: comments,
isSaving: false,
serverError: '',
desktopAndMobileSettingsDifferent,
};
}
@ -235,8 +240,6 @@ class NotificationsTab extends React.PureComponent<Props, State> {
data.desktop = this.state.desktopActivity;
data.desktop_threads = this.state.desktopThreads;
data.email_threads = this.state.emailThreads;
data.push_threads = this.state.pushThreads;
data.push = this.state.pushActivity;
data.push_status = this.state.pushStatus;
data.comments = this.state.notifyCommentsLevel;
data.auto_responder_active = this.state.autoResponderActive ? 'true' : 'false';
@ -244,6 +247,14 @@ class NotificationsTab extends React.PureComponent<Props, State> {
data.first_name = this.state.firstNameKey ? 'true' : 'false';
data.channel = this.state.channelKey ? 'true' : 'false';
if (this.state.desktopAndMobileSettingsDifferent) {
data.push = this.state.pushActivity;
data.push_threads = this.state.pushThreads;
} else {
data.push = this.state.desktopActivity;
data.push_threads = this.state.desktopThreads;
}
if (!data.auto_responder_message || data.auto_responder_message === '') {
data.auto_responder_message = this.props.intl.formatMessage({
id: 'user.settings.notifications.autoResponderDefault',
@ -273,7 +284,7 @@ class NotificationsTab extends React.PureComponent<Props, State> {
this.setState({isSaving: true});
stopTryNotificationRing();
const {data: updatedUser, error} = await this.props.updateMe({notify_props: data}) as ActionResult<Partial<UserProfile>, ServerError>; // Fix in MM-46907
const {data: updatedUser, error} = await this.props.updateMe({notify_props: data});
if (updatedUser) {
this.handleUpdateSection('');
this.setState(getDefaultStateFromProps(this.props));
@ -305,26 +316,11 @@ class NotificationsTab extends React.PureComponent<Props, State> {
this.setState((prevState) => ({...prevState, ...data}));
};
handleNotifyPushThread = (e: ChangeEvent<HTMLInputElement>): void => {
const pushThreads = e.target.checked ? NotificationLevels.ALL : NotificationLevels.MENTION;
this.setState({pushThreads});
};
handleNotifyCommentsRadio = (notifyCommentsLevel: UserNotifyProps['comments'], e?: React.ChangeEvent): void => {
this.setState({notifyCommentsLevel});
a11yFocus(e?.currentTarget as HTMLElement);
};
handlePushRadio = (pushActivity: UserNotifyProps['push'], e?: React.ChangeEvent): void => {
this.setState({pushActivity});
a11yFocus(e?.currentTarget as HTMLElement);
};
handlePushStatusRadio = (pushStatus: UserNotifyProps['push_status'], e?: React.ChangeEvent): void => {
this.setState({pushStatus});
a11yFocus(e?.currentTarget as HTMLElement);
};
handleEmailRadio = (enableEmail: UserNotifyProps['email']): void => {
this.setState({enableEmail});
};
@ -472,311 +468,10 @@ class NotificationsTab extends React.PureComponent<Props, State> {
this.props.closeModal();
};
createPushNotificationSection = () => {
const active = this.props.activeSection === 'push';
const inputs = [];
let submit = null;
let max = null;
if (active) {
if (this.props.sendPushNotifications) {
const pushActivityRadio = [false, false, false];
if (this.state.pushActivity === NotificationLevels.ALL) {
pushActivityRadio[0] = true;
} else if (this.state.pushActivity === NotificationLevels.NONE) {
pushActivityRadio[2] = true;
} else {
pushActivityRadio[1] = true;
}
const pushStatusRadio = [false, false, false];
if (this.state.pushStatus === Constants.UserStatuses.ONLINE) {
pushStatusRadio[0] = true;
} else if (this.state.pushStatus === Constants.UserStatuses.AWAY) {
pushStatusRadio[1] = true;
} else {
pushStatusRadio[2] = true;
}
let pushThreadsNotificationSelection = null;
if (this.props.isCollapsedThreadsEnabled && this.state.pushActivity === NotificationLevels.MENTION) {
pushThreadsNotificationSelection = (
<React.Fragment key='userNotificationPushThreadsOptions'>
<hr/>
<fieldset>
<legend className='form-legend'>
<FormattedMessage
id='user.settings.notifications.threads.push'
defaultMessage='Thread reply notifications'
/>
</legend>
<div className='checkbox'>
<label>
<input
id='pushThreadsNotificationAllActivity'
type='checkbox'
name='pushThreadsNotificationLevel'
checked={this.state.pushThreads === NotificationLevels.ALL}
onChange={this.handleNotifyPushThread}
/>
<FormattedMessage
id='user.settings.notifications.push_threads.allActivity'
defaultMessage={'Notify me about threads I\'m following'}
/>
</label>
<br/>
</div>
<div className='mt-5'>
<FormattedMessage
id='user.settings.notifications.push_threads'
defaultMessage={'When enabled, any reply to a thread you\'re following will send a mobile push notification.'}
/>
</div>
</fieldset>
</React.Fragment>
);
}
let pushStatusSettings;
if (this.state.pushActivity !== NotificationLevels.NONE) {
pushStatusSettings = (
<React.Fragment key='userNotificationPushStatusOptions'>
<hr/>
<fieldset>
<legend className='form-legend'>
<FormattedMessage
id='user.settings.notifications.push_notification.status'
defaultMessage='Trigger push notifications when'
/>
</legend>
<div className='radio'>
<label>
<input
id='pushNotificationOnline'
type='radio'
name='pushNotificationStatus'
checked={pushStatusRadio[0]}
onChange={this.handlePushStatusRadio.bind(this, Constants.UserStatuses.ONLINE)}
/>
<FormattedMessage
id='user.settings.push_notification.online'
defaultMessage='Online, away or offline'
/>
</label>
</div>
<div className='radio'>
<label>
<input
id='pushNotificationAway'
type='radio'
name='pushNotificationStatus'
checked={pushStatusRadio[1]}
onChange={this.handlePushStatusRadio.bind(this, Constants.UserStatuses.AWAY)}
/>
<FormattedMessage
id='user.settings.push_notification.away'
defaultMessage='Away or offline'
/>
</label>
</div>
<div className='radio'>
<label>
<input
id='pushNotificationOffline'
type='radio'
name='pushNotificationStatus'
checked={pushStatusRadio[2]}
onChange={this.handlePushStatusRadio.bind(this, Constants.UserStatuses.OFFLINE)}
/>
<FormattedMessage
id='user.settings.push_notification.offline'
defaultMessage='Offline'
/>
</label>
</div>
<div className='mt-5'>
<span>
<FormattedMessage
id='user.settings.push_notification.status_info'
defaultMessage='Notification alerts are only pushed to your mobile device when your availability matches the selection above.'
/>
</span>
</div>
</fieldset>
</React.Fragment>
);
}
inputs.push(
<div>
<fieldset key='userNotificationLevelOption'>
<legend className='form-legend'>
<FormattedMessage
id='user.settings.push_notification.send'
defaultMessage='Send mobile push notifications'
/>
</legend>
<div className='radio'>
<label>
<input
id='pushNotificationAllActivity'
type='radio'
name='pushNotificationLevel'
checked={pushActivityRadio[0]}
onChange={this.handlePushRadio.bind(this, NotificationLevels.ALL)}
/>
<FormattedMessage
id='user.settings.push_notification.allActivity'
defaultMessage='For all activity'
/>
</label>
</div>
<div className='radio'>
<label>
<input
id='pushNotificationMentions'
type='radio'
name='pushNotificationLevel'
checked={pushActivityRadio[1]}
onChange={this.handlePushRadio.bind(this, NotificationLevels.MENTION)}
/>
<FormattedMessage
id='user.settings.push_notification.onlyMentions'
defaultMessage='For mentions and direct messages'
/>
</label>
</div>
<div className='radio'>
<label>
<input
id='pushNotificationNever'
type='radio'
name='pushNotificationLevel'
checked={pushActivityRadio[2]}
onChange={this.handlePushRadio.bind(this, NotificationLevels.NONE)}
/>
<FormattedMessage
id='user.settings.notifications.never'
defaultMessage='Never'
/>
</label>
</div>
<div className='mt-5'>
<FormattedMessage
id='user.settings.push_notification.info'
defaultMessage='Notification alerts are pushed to your mobile device when there is activity in Mattermost.'
/>
</div>
</fieldset>
</div>,
pushStatusSettings,
pushThreadsNotificationSelection,
);
submit = this.handleSubmit;
} else {
inputs.push(
<div
key='oauthEmailInfo'
className='pt-2'
>
<FormattedMessage
id='user.settings.push_notification.disabled_long'
defaultMessage='Push notifications have not been enabled by your System Administrator.'
/>
</div>,
);
}
max = (
<SettingItemMax
title={this.props.intl.formatMessage({id: 'user.settings.notifications.push', defaultMessage: 'Mobile Push Notifications'})}
inputs={inputs}
submit={submit}
serverError={this.state.serverError}
updateSection={this.handleUpdateSection}
/>
);
}
let describe: JSX.Element;
if (this.state.pushActivity === NotificationLevels.ALL) {
if (this.state.pushStatus === Constants.UserStatuses.AWAY) {
describe = (
<FormattedMessage
id='user.settings.push_notification.allActivityAway'
defaultMessage='For all activity when away or offline'
/>
);
} else if (this.state.pushStatus === Constants.UserStatuses.OFFLINE) {
describe = (
<FormattedMessage
id='user.settings.push_notification.allActivityOffline'
defaultMessage='For all activity when offline'
/>
);
} else {
describe = (
<FormattedMessage
id='user.settings.push_notification.allActivityOnline'
defaultMessage='For all activity when online, away or offline'
/>
);
}
} else if (this.state.pushActivity === NotificationLevels.NONE) {
describe = (
<FormattedMessage
id='user.settings.notifications.never'
defaultMessage='Never'
/>
);
} else if (this.props.sendPushNotifications) {
if (this.state.pushStatus === Constants.UserStatuses.AWAY) { //eslint-disable-line no-lonely-if
describe = (
<FormattedMessage
id='user.settings.push_notification.onlyMentionsAway'
defaultMessage='For mentions and direct messages when away or offline'
/>
);
} else if (this.state.pushStatus === Constants.UserStatuses.OFFLINE) {
describe = (
<FormattedMessage
id='user.settings.push_notification.onlyMentionsOffline'
defaultMessage='For mentions and direct messages when offline'
/>
);
} else {
describe = (
<FormattedMessage
id='user.settings.push_notification.onlyMentionsOnline'
defaultMessage='For mentions and direct messages when online, away or offline'
/>
);
}
} else {
describe = (
<FormattedMessage
id='user.settings.push_notification.disabled'
defaultMessage='Push notifications are not enabled'
/>
);
}
return (
<SettingItem
title={this.props.intl.formatMessage({id: 'user.settings.notifications.push', defaultMessage: 'Mobile Push Notifications'})}
active={active}
areAllSectionsInactive={this.props.activeSection === ''}
describe={describe}
section={'push'}
updateSection={this.handleUpdateSection}
max={max}
/>
);
};
createKeywordsWithNotificationSection = () => {
const serverError = this.state.serverError;
const user = this.props.user;
const isSectionExpanded = this.props.activeSection === 'keysWithNotification';
const isSectionExpanded = this.props.activeSection === UserSettingsNotificationSections.KEYWORDS_MENTIONS;
let expandedSection = null;
if (isSectionExpanded) {
@ -901,7 +596,7 @@ class NotificationsTab extends React.PureComponent<Props, State> {
expandedSection = (
<SettingItemMax
title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithNotification.title', defaultMessage: 'Keywords That Trigger Notifications'})}
title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithNotification.title', defaultMessage: 'Keywords that trigger notifications'})}
inputs={inputs}
submit={this.handleSubmit}
saving={this.state.isSaving}
@ -932,8 +627,8 @@ class NotificationsTab extends React.PureComponent<Props, State> {
return (
<SettingItem
title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithNotification.title', defaultMessage: 'Keywords That Trigger Notifications'})}
section='keysWithNotification'
title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithNotification.title', defaultMessage: 'Keywords that trigger notifications'})}
section={UserSettingsNotificationSections.KEYWORDS_MENTIONS}
active={isSectionExpanded}
areAllSectionsInactive={this.props.activeSection === ''}
describe={collapsedDescription}
@ -943,7 +638,7 @@ class NotificationsTab extends React.PureComponent<Props, State> {
};
createKeywordsWithHighlightSection = () => {
const isSectionExpanded = this.props.activeSection === 'keysWithHighlight';
const isSectionExpanded = this.props.activeSection === UserSettingsNotificationSections.KEYWORDS_HIGHLIGHT;
let expandedSection = null;
if (isSectionExpanded) {
@ -990,7 +685,7 @@ class NotificationsTab extends React.PureComponent<Props, State> {
expandedSection = (
<SettingItemMax
title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithHighlight.title', defaultMessage: 'Keywords That Get Highlighted (Without Notifications)'})}
title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithHighlight.title', defaultMessage: 'Keywords that get highlighted (without notifications)'})}
inputs={inputs}
submit={this.handleSubmit}
saving={this.state.isSaving}
@ -1063,8 +758,8 @@ class NotificationsTab extends React.PureComponent<Props, State> {
return (
<SettingItem
title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithHighlight.title', defaultMessage: 'Keywords That Get Highlighted (Without Notifications)'})}
section='keysWithHighlight'
title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithHighlight.title', defaultMessage: 'Keywords that get highlighted (without notifications)'})}
section={UserSettingsNotificationSections.KEYWORDS_HIGHLIGHT}
active={isSectionExpanded}
areAllSectionsInactive={this.props.activeSection === ''}
describe={collapsedDescription}
@ -1078,9 +773,8 @@ class NotificationsTab extends React.PureComponent<Props, State> {
createCommentsSection = () => {
const serverError = this.state.serverError;
const active = this.props.activeSection === 'comments';
let max = null;
if (active) {
if (this.props.activeSection === UserSettingsNotificationSections.REPLY_NOTIFCATIONS) {
const commentsActive = [false, false, false];
if (this.state.notifyCommentsLevel === 'never') {
commentsActive[2] = true;
@ -1097,7 +791,7 @@ class NotificationsTab extends React.PureComponent<Props, State> {
<legend className='form-legend hidden-label'>
<FormattedMessage
id='user.settings.notifications.comments'
defaultMessage='Reply Notifications'
defaultMessage='Reply notifications'
/>
</legend>
<div className='radio'>
@ -1161,7 +855,7 @@ class NotificationsTab extends React.PureComponent<Props, State> {
max = (
<SettingItemMax
title={this.props.intl.formatMessage({id: 'user.settings.notifications.comments', defaultMessage: 'Reply Notifications'})}
title={this.props.intl.formatMessage({id: 'user.settings.notifications.comments', defaultMessage: 'Reply notifications'})}
extraInfo={extraInfo}
inputs={inputs}
submit={this.handleSubmit}
@ -1199,9 +893,9 @@ class NotificationsTab extends React.PureComponent<Props, State> {
return (
<SettingItem
title={this.props.intl.formatMessage({id: 'user.settings.notifications.comments', defaultMessage: 'Reply notifications'})}
active={active}
active={this.props.activeSection === UserSettingsNotificationSections.REPLY_NOTIFCATIONS}
describe={describe}
section={'comments'}
section={UserSettingsNotificationSections.REPLY_NOTIFCATIONS}
updateSection={this.handleUpdateSection}
max={max}
areAllSectionsInactive={this.props.activeSection === ''}
@ -1224,16 +918,16 @@ class NotificationsTab extends React.PureComponent<Props, State> {
return (
<SettingItem
active={this.props.activeSection === 'auto-responder'}
active={this.props.activeSection === UserSettingsNotificationSections.AUTO_RESPONDER}
areAllSectionsInactive={this.props.activeSection === ''}
title={
<FormattedMessage
id='user.settings.notifications.autoResponder'
defaultMessage='Automatic Direct Message Replies'
defaultMessage='Automatic direct message replies'
/>
}
describe={describe}
section={'auto-responder'}
section={UserSettingsNotificationSections.AUTO_RESPONDER}
updateSection={this.handleUpdateSection}
max={(
<div>
@ -1254,12 +948,13 @@ class NotificationsTab extends React.PureComponent<Props, State> {
};
render() {
const pushNotificationSection = this.createPushNotificationSection();
const keywordsWithNotificationSection = this.createKeywordsWithNotificationSection();
const keywordsWithHighlightSection = this.createKeywordsWithHighlightSection();
const commentsSection = this.createCommentsSection();
const autoResponderSection = this.createAutoResponderSection();
const areAllSectionsInactive = this.props.activeSection === '';
return (
<div id='notificationSettings'>
<SettingMobileHeader
@ -1268,7 +963,7 @@ class NotificationsTab extends React.PureComponent<Props, State> {
text={
<FormattedMessage
id='user.settings.notifications.title'
defaultMessage='Notification Settings'
defaultMessage='Notification settings'
/>
}
/>
@ -1302,42 +997,56 @@ class NotificationsTab extends React.PureComponent<Props, State> {
}
/>
<div className='divider-dark first'/>
<DesktopNotificationSettings
active={this.props.activeSection === 'desktop'}
<DesktopAndMobileNotificationSettings
active={this.props.activeSection === UserSettingsNotificationSections.DESKTOP_AND_MOBILE}
updateSection={this.handleUpdateSection}
onSubmit={this.handleSubmit}
onCancel={this.handleCancel}
saving={this.state.isSaving}
error={this.state.serverError}
setParentState={this.setStateValue}
areAllSectionsInactive={this.props.activeSection === ''}
areAllSectionsInactive={areAllSectionsInactive}
isCollapsedThreadsEnabled={this.props.isCollapsedThreadsEnabled}
activity={this.state.desktopActivity}
threads={this.state.desktopThreads}
sound={this.state.desktopSound}
callsSound={this.state.callsDesktopSound}
selectedSound={this.state.desktopNotificationSound || 'default'}
callsSelectedSound={this.state.callsNotificationSound || 'default'}
desktopActivity={this.state.desktopActivity}
pushActivity={this.state.pushActivity}
sendPushNotifications={this.props.sendPushNotifications}
pushStatus={this.state.pushStatus}
desktopThreads={this.state.desktopThreads}
pushThreads={this.state.pushThreads}
desktopAndMobileSettingsDifferent={this.state.desktopAndMobileSettingsDifferent}
/>
<div className='divider-light'/>
<DesktopNotificationSoundsSettings
active={this.props.activeSection === UserSettingsNotificationSections.DESKTOP_NOTIFICATION_SOUND}
updateSection={this.handleUpdateSection}
onSubmit={this.handleSubmit}
onCancel={this.handleCancel}
saving={this.state.isSaving}
error={this.state.serverError}
setParentState={this.setStateValue}
areAllSectionsInactive={areAllSectionsInactive}
desktopSound={this.state.desktopSound}
desktopNotificationSound={this.state.desktopNotificationSound}
isCallsRingingEnabled={this.props.isCallsRingingEnabled}
callsDesktopSound={this.state.callsDesktopSound}
callsNotificationSound={this.state.callsNotificationSound}
/>
<div className='divider-light'/>
<EmailNotificationSetting
active={this.props.activeSection === 'email'}
active={this.props.activeSection === UserSettingsNotificationSections.EMAIL}
updateSection={this.handleUpdateSection}
onSubmit={this.handleSubmit}
onCancel={this.handleCancel}
saving={this.state.isSaving}
error={this.state.serverError}
setParentState={this.setStateValue}
areAllSectionsInactive={this.props.activeSection === ''}
areAllSectionsInactive={areAllSectionsInactive}
isCollapsedThreadsEnabled={this.props.isCollapsedThreadsEnabled}
enableEmail={this.state.enableEmail === 'true'}
onChange={this.handleEmailRadio}
threads={this.state.emailThreads || ''}
/>
<div className='divider-light'/>
{pushNotificationSection}
<div className='divider-light'/>
{keywordsWithNotificationSection}
{(!this.props.isEnterpriseOrCloudOrSKUStarterFree && this.props.isEnterpriseReady) && (
<>
@ -1414,4 +1123,39 @@ const customKeywordsSelectorStyles: ReactSelectStyles = {
})),
};
const validNotificationLevels = Object.values(NotificationLevels);
export function areDesktopAndMobileSettingsDifferent(
desktopActivity: UserNotifyProps['desktop'],
pushActivity: UserNotifyProps['push'],
desktopThreads?: UserNotifyProps['desktop_threads'],
pushThreads?: UserNotifyProps['push_threads'],
isCollapsedThreadsEnabled?: boolean,
): boolean {
if (!desktopActivity || !pushActivity || !desktopThreads || !pushThreads) {
return true;
}
if (
!validNotificationLevels.includes(desktopActivity) ||
!validNotificationLevels.includes(pushActivity) ||
!validNotificationLevels.includes(desktopThreads) ||
!validNotificationLevels.includes(pushThreads)
) {
return true;
}
if (desktopActivity === pushActivity) {
if (isCollapsedThreadsEnabled) {
if (desktopThreads === pushThreads) {
return false;
}
return true;
}
return false;
}
return true;
}
export default injectIntl(NotificationsTab);

View File

@ -77,7 +77,6 @@ exports[`MfaSection rendering when section is expanded and MFA is active and enf
/>
}
updateSection={[MockFunction]}
width="medium"
/>
`;
@ -118,7 +117,6 @@ exports[`MfaSection rendering when section is expanded and MFA is active but not
/>
}
updateSection={[MockFunction]}
width="medium"
/>
`;
@ -159,7 +157,6 @@ exports[`MfaSection rendering when section is expanded and MFA is not active 1`]
/>
}
updateSection={[MockFunction]}
width="medium"
/>
`;
@ -200,6 +197,5 @@ exports[`MfaSection rendering when section is expanded with a server error 1`] =
/>
}
updateSection={[MockFunction]}
width="medium"
/>
`;

View File

@ -219,7 +219,6 @@ export default class MfaSection extends React.PureComponent<Props, State> {
extraInfo={this.renderHelpText()}
serverError={this.state.serverError}
updateSection={this.props.updateSection}
width='medium'
/>
);
}

View File

@ -627,7 +627,7 @@ export default class UserAccessTokenSection extends React.PureComponent<Props, S
infoPosition='top'
serverError={this.state.serverError}
updateSection={this.props.updateSection}
width='full'
isFullWidth={true}
saving={this.state.saving}
cancelButtonText={
<FormattedMessage

View File

@ -918,7 +918,7 @@ export class SecurityTab extends React.PureComponent<Props, State> {
inputs={inputs}
serverError={this.state.serverError}
updateSection={this.handleUpdateSection}
width='full'
isFullWidth={true}
cancelButtonText={
<FormattedMessage
id='user.settings.security.close'

View File

@ -5643,50 +5643,83 @@
"user.settings.modal.security": "Security",
"user.settings.modal.sidebar": "Sidebar",
"user.settings.modal.title": "Profile",
"user.settings.notifications.allActivity": "For all activity",
"user.settings.notifications.autoResponder": "Automatic Direct Message Replies",
"user.settings.notifications.autoResponder": "Automatic direct message replies",
"user.settings.notifications.autoResponderDefault": "Hello, I am out of office and unable to respond to messages.",
"user.settings.notifications.autoResponderDisabled": "Disabled",
"user.settings.notifications.autoResponderEnabled": "Enabled",
"user.settings.notifications.autoResponderHint": "Set a custom message that will be automatically sent in response to Direct Messages. Mentions in Public and Private Channels will not trigger the automated reply. Enabling Automatic Replies sets your status to Out of Office and disables email and push notifications.",
"user.settings.notifications.autoResponderPlaceholder": "Message",
"user.settings.notifications.channelWide": "Channel-wide mentions \"@channel\", \"@all\", \"@here\"",
"user.settings.notifications.comments": "Reply Notifications",
"user.settings.notifications.comments": "Reply notifications",
"user.settings.notifications.commentsAny": "Trigger notifications on messages in reply threads that I start or participate in",
"user.settings.notifications.commentsInfo": "In addition to notifications for when you're mentioned, select if you would like to receive notifications on reply threads.",
"user.settings.notifications.commentsNever": "Do not trigger notifications on messages in reply threads unless I'm mentioned",
"user.settings.notifications.commentsRoot": "Trigger notifications on messages in threads that I start",
"user.settings.notifications.desktop": "Send desktop notifications",
"user.settings.notifications.desktop.allNoSound": "For all activity, without sound",
"user.settings.notifications.desktop.allSound": "For all activity, with sound",
"user.settings.notifications.desktop.allSoundHidden": "For all activity",
"user.settings.notifications.desktop.calls_sound": "Notification sound for incoming calls",
"user.settings.notifications.desktop.mentionsNoSound": "For mentions and direct messages, without sound",
"user.settings.notifications.desktop.mentionsSound": "For mentions and direct messages, with sound",
"user.settings.notifications.desktop.mentionsSoundHidden": "For mentions and direct messages",
"user.settings.notifications.desktop.sound": "Notification sound",
"user.settings.notifications.desktop.title": "Desktop Notifications",
"user.settings.notifications.email_threads": "When enabled, any reply to a thread you're following will send an email notification.",
"user.settings.notifications.desktopAndMobile.allDesktopButMobileMentions": "All new messages on desktop; mentions, direct messages, and group messages on mobile",
"user.settings.notifications.desktopAndMobile.allDesktopButMobileNone": "All new messages on desktop; never on mobile",
"user.settings.notifications.desktopAndMobile.allForDesktopAndMobile": "All new messages",
"user.settings.notifications.desktopAndMobile.allNewMessages": "All new messages",
"user.settings.notifications.desktopAndMobile.away": "Away or offline",
"user.settings.notifications.desktopAndMobile.differentMobileNotificationsTitle": "Use different settings for my mobile devices",
"user.settings.notifications.desktopAndMobile.mentionsDesktopButMobileAll": "Mentions, direct messages, and group messages on desktop; all new messages on mobile",
"user.settings.notifications.desktopAndMobile.mentionsForDesktopAndMobile": "Mentions, direct messages, and group messages",
"user.settings.notifications.desktopAndMobile.mentionsForDesktopButMobileNone": "Mentions, direct messages, and group messages on desktop; never on mobile",
"user.settings.notifications.desktopAndMobile.noneDesktopButMobileAll": "Never on desktop; all new messages on mobile",
"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.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",
"user.settings.notifications.desktopAndMobile.offline": "Offline",
"user.settings.notifications.desktopAndMobile.online": "Online, away, or offline",
"user.settings.notifications.desktopAndMobile.onlyMentions": "Mentions, direct messages, and group messages",
"user.settings.notifications.desktopAndMobile.pushNotification": "Trigger mobile notifications when I am:",
"user.settings.notifications.desktopAndMobile.pushNotificationsDisabled": "Mobile push notifications haven't been enabled by your system administrator.",
"user.settings.notifications.desktopAndMobile.sendDesktopNotificationFor": "Send notifications for:",
"user.settings.notifications.desktopAndMobile.sendMobileNotificationsFor": "Send mobile notifications for:",
"user.settings.notifications.desktopAndMobile.title": "Desktop and mobile notifications",
"user.settings.notifications.desktopNotificationSound.hasDesktopAndCallsSound": "\"{desktopSound}\" for messages, \"{callsSound}\" for calls",
"user.settings.notifications.desktopNotificationSound.hasDesktopAndNoCallsSound": "\"{desktopSound}\" for messages, no sound for calls",
"user.settings.notifications.desktopNotificationSound.hasDesktopSound": "\"{desktopSound}\" for messages",
"user.settings.notifications.desktopNotificationSound.incomingCallSound": "Incoming call sound",
"user.settings.notifications.desktopNotificationSound.messageNotificationSound": "Message notification sound",
"user.settings.notifications.desktopNotificationSound.noDesktopAndhasCallsSound": "No sound for messages, \"{callsSound}\" for calls",
"user.settings.notifications.desktopNotificationSound.noDesktopAndNoCallsSound": "No sound",
"user.settings.notifications.desktopNotificationSound.noDesktopSound": "No sound",
"user.settings.notifications.desktopNotificationSound.noValidSound": "Configure desktop notification sounds",
"user.settings.notifications.desktopNotificationSound.soundBing": "Bing",
"user.settings.notifications.desktopNotificationSound.soundCalm": "Calm",
"user.settings.notifications.desktopNotificationSound.soundCheerful": "Cheerful",
"user.settings.notifications.desktopNotificationSound.soundCrackle": "Crackle",
"user.settings.notifications.desktopNotificationSound.soundDown": "Down",
"user.settings.notifications.desktopNotificationSound.soundDynamic": "Dynamic",
"user.settings.notifications.desktopNotificationSound.soundHello": "Hello",
"user.settings.notifications.desktopNotificationSound.soundRipple": "Ripple",
"user.settings.notifications.desktopNotificationSound.soundSelectPlaceholder": "Select a sound",
"user.settings.notifications.desktopNotificationSound.soundUpstairs": "Upstairs",
"user.settings.notifications.desktopNotificationSound.soundUrgent": "Urgent",
"user.settings.notifications.desktopNotificationSounds.title": "Desktop notification sounds",
"user.settings.notifications.email.disabled": "Email notifications are not enabled",
"user.settings.notifications.email.disabled_long": "Email notifications have not been enabled by your System Administrator.",
"user.settings.notifications.email.everyHour": "Every hour",
"user.settings.notifications.email.everyXMinutes": "Every {count, plural, one {minute} other {{count, number} minutes}}",
"user.settings.notifications.email.immediately": "Immediately",
"user.settings.notifications.email.never": "Never",
"user.settings.notifications.email.notifyForthreads": "Notify me about replies to threads Im following",
"user.settings.notifications.email.send": "Send email notifications",
"user.settings.notifications.emailBatchingInfo": "Notifications received over the time period selected are combined and sent in a single email.",
"user.settings.notifications.emailInfo": "Email notifications are sent for mentions and direct messages when you are offline or away for more than 5 minutes.",
"user.settings.notifications.emailNotifications": "Email Notifications",
"user.settings.notifications.emailNotifications": "Email notifications",
"user.settings.notifications.header": "Notifications",
"user.settings.notifications.icon": "Notification Settings Icon",
"user.settings.notifications.info": "Desktop notifications are available on Edge, Firefox, Safari, Chrome and Mattermost Desktop Apps.",
"user.settings.notifications.keywordsWithHighlight.disabledTooltipMessage": "This feature is available on the Professional plan",
"user.settings.notifications.keywordsWithHighlight.disabledTooltipTitle": "Professional feature",
"user.settings.notifications.keywordsWithHighlight.extraInfo": "These keywords will be shown to you with a highlight when anyone sends a message that includes them.",
"user.settings.notifications.keywordsWithHighlight.inputTitle": "Enter non case-sensitive keywords, press Tab or use commas to separate them:",
"user.settings.notifications.keywordsWithHighlight.none": "None",
"user.settings.notifications.keywordsWithHighlight.professional": "Professional",
"user.settings.notifications.keywordsWithHighlight.title": "Keywords That Get Highlighted (Without Notifications)",
"user.settings.notifications.keywordsWithHighlight.title": "Keywords that get highlighted (without notifications)",
"user.settings.notifications.keywordsWithHighlight.userModal.messageAdminPostTrial": "Get the ability to passively highlight keywords that you care about. Upgrade to Professional plan to unlock this feature.",
"user.settings.notifications.keywordsWithHighlight.userModal.messageAdminPreTrial": "Get the ability to passively highlight keywords that you care about. Upgrade to Professional plan to unlock this feature.",
"user.settings.notifications.keywordsWithHighlight.userModal.messageEndUser": "Get the ability to passively highlight keywords that you care about.{br}{br}Request your admin to upgrade to Mattermost Professional to access this feature.",
@ -5694,44 +5727,14 @@
"user.settings.notifications.keywordsWithHighlight.userModal.titleAdminPreTrial": "Highlight keywords without notifications with Mattermost Professional",
"user.settings.notifications.keywordsWithHighlight.userModal.titleEndUser": "Highlight keywords without notifications with Mattermost Professional",
"user.settings.notifications.keywordsWithNotification.extraInfo": "Notifications are triggered when someone sends a message that includes your username (\"@{username}\") or any of the options selected above.",
"user.settings.notifications.keywordsWithNotification.title": "Keywords That Trigger Notifications",
"user.settings.notifications.keywordsWithNotification.title": "Keywords that trigger notifications",
"user.settings.notifications.learnMore": "<a>Learn more about notifications</a>",
"user.settings.notifications.never": "Never",
"user.settings.notifications.off": "Off",
"user.settings.notifications.on": "On",
"user.settings.notifications.onlyMentions": "Only for mentions, direct messages, and group messages",
"user.settings.notifications.push": "Mobile Push Notifications",
"user.settings.notifications.push_notification.status": "Trigger push notifications when",
"user.settings.notifications.push_threads": "When enabled, any reply to a thread you're following will send a mobile push notification.",
"user.settings.notifications.push_threads.allActivity": "Notify me about threads I'm following",
"user.settings.notifications.sensitiveCustomWords": "Other non case-sensitive words, press Tab or use commas to separate keywords:",
"user.settings.notifications.sensitiveName": "Your case-sensitive first name \"{first_name}\"",
"user.settings.notifications.sensitiveUsername": "Your non case-sensitive username \"{username}\"",
"user.settings.notifications.soundConfig": "Please configure notification sounds in your browser settings",
"user.settings.notifications.sounds_info": "Notification sounds are available on Firefox, Edge, Safari, Chrome and Mattermost Desktop Apps.",
"user.settings.notifications.threads": "When enabled, any reply to a thread you're following will send a desktop notification.",
"user.settings.notifications.threads.allActivity": "Notify me about threads I'm following",
"user.settings.notifications.threads.desktop": "Thread reply notifications",
"user.settings.notifications.threads.push": "Thread reply notifications",
"user.settings.notifications.title": "Notification Settings",
"user.settings.notifications.title": "Notification settings",
"user.settings.plugins.title": "{pluginName} Settings",
"user.settings.profile.icon": "Profile Settings Icon",
"user.settings.push_notification.allActivity": "For all activity",
"user.settings.push_notification.allActivityAway": "For all activity when away or offline",
"user.settings.push_notification.allActivityOffline": "For all activity when offline",
"user.settings.push_notification.allActivityOnline": "For all activity when online, away or offline",
"user.settings.push_notification.away": "Away or offline",
"user.settings.push_notification.disabled": "Push notifications are not enabled",
"user.settings.push_notification.disabled_long": "Push notifications have not been enabled by your System Administrator.",
"user.settings.push_notification.info": "Notification alerts are pushed to your mobile device when there is activity in Mattermost.",
"user.settings.push_notification.offline": "Offline",
"user.settings.push_notification.online": "Online, away or offline",
"user.settings.push_notification.onlyMentions": "For mentions and direct messages",
"user.settings.push_notification.onlyMentionsAway": "For mentions and direct messages when away or offline",
"user.settings.push_notification.onlyMentionsOffline": "For mentions and direct messages when offline",
"user.settings.push_notification.onlyMentionsOnline": "For mentions and direct messages when online, away or offline",
"user.settings.push_notification.send": "Send mobile push notifications",
"user.settings.push_notification.status_info": "Notification alerts are only pushed to your mobile device when your availability matches the selection above.",
"user.settings.security.active": "Active",
"user.settings.security.close": "Close",
"user.settings.security.currentPassword": "Current Password",

View File

@ -204,7 +204,3 @@ input[type="radio"]:focus,
input[type="checkbox"]:focus {
outline: none;
}
.notification-sound-dropdown {
padding-top: 5px;
}

View File

@ -79,8 +79,6 @@
}
label {
font-weight: 600;
&.text-left {
text-align: left;
}
@ -221,17 +219,56 @@
.section-max {
@include pie-clearfix;
padding: 1em 0 1.3em;
padding: 12px;
margin-bottom: 0;
background: rgba(var(--center-channel-color-rgb), 0.04);
.section-title {
padding-left: 14px;
margin: 0 0 5px;
padding: 0;
margin: 0;
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.sectionContent {
padding: 20px 0 0 0;
.checkbox.single-checkbox {
min-height: unset;
padding: 0;
}
.inlineCheckboxSelect {
display: flex;
min-height: unset;
align-items: center;
justify-content: space-between;
padding: 0;
label {
margin-inline-end: 24px;
white-space: nowrap;
}
.inlineSelect {
width: 40%;
}
}
.singleSelectLabel {
margin-bottom: 12px;
font-size: 14px;
font-weight: 600;
line-height: 20px;
}
hr {
padding: 0;
margin-block-end: 24px;
margin-block-start: 24px;
}
}
}
.timezone-container {

View File

@ -966,13 +966,14 @@ export const ChannelAutoFollowThreads = {
OFF: 'off',
} as const;
export const NotificationSections = {
IGNORE_CHANNEL_MENTIONS: 'ignoreChannelMentions',
CHANNEL_AUTO_FOLLOW_THREADS: 'channelAutoFollowThreads',
MARK_UNREAD: 'markUnread',
DESKTOP: 'desktop',
PUSH: 'push',
NONE: '',
export const UserSettingsNotificationSections = {
DESKTOP_AND_MOBILE: 'desktopAndMobile',
DESKTOP_NOTIFICATION_SOUND: 'desktopNotificationSound',
EMAIL: 'email',
KEYWORDS_MENTIONS: 'keywordsAndMentions',
KEYWORDS_HIGHLIGHT: 'keywordsAndHighlight',
REPLY_NOTIFCATIONS: 'replyNotifications',
AUTO_RESPONDER: 'autoResponder',
};
export const AdvancedSections = {