[MM-40384]: Channel notification settings modal UX revamp (#24984)

* Move notification preferences modal to new UI

* fix lint issue

* fix i18n

* fix unit test

* fix type issue and lint errors

* fix test case

* move common components to widget modals dir

* fix css issue

* feedback changes

* fix lint and i18n issues

* more feedback changes

* fix issue with mobile notification ui

* fix test

* clean up

* remove name

* fix test

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Ashish Dhama 2023-11-14 00:50:35 +05:30 committed by GitHub
parent ec88ab4ee9
commit 448d442a0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 4274 additions and 3759 deletions

View File

@ -211,7 +211,7 @@ describe('Channel Info RHS', () => {
cy.uiGetRHS().findByText('Notification Preferences').should('be.visible').click();
// * Ensures the modal is there
cy.get('.settings-modal').should('be.visible');
cy.get('.channel-notifications-settings-modal').should('be.visible');
});
it('should be able to view files and come back', () => {
// # Go to test channel
@ -373,7 +373,7 @@ describe('Channel Info RHS', () => {
cy.uiGetRHS().findByText('Notification Preferences').should('be.visible').click();
// * Ensures the modal is there
cy.get('.settings-modal').should('be.visible');
cy.get('.channel-notifications-settings-modal').should('be.visible');
});
});
});

View File

@ -52,11 +52,46 @@ describe('CRT Desktop notifications', () => {
// # Visit channel
cy.visit(testChannelUrl);
cy.uiOpenChannelMenu('Notification Preferences');
cy.get('[data-testid="muteChannel"]').click().then(() => {
cy.get('.AlertBanner--app').should('be.visible');
});
cy.get('.channel-notifications-settings-modal__save-btn').should('be.visible').click();
// Setup notification spy
spyNotificationAs('notifySpy', 'granted');
// # Set users notification settings
setCRTDesktopNotification('ALL');
cy.uiOpenChannelMenu('Notification Preferences');
// # click on Mute Channel to Unmute Channel
cy.get('[data-testid="muteChannel"]').click();
// # Click "Desktop Notifications"
cy.findByText('Desktop Notifications').should('be.visible');
cy.get('.channel-notifications-settings-modal__body').scrollTo('center').get('#desktopNotification-all').should('be.visible').click();
cy.get('.channel-notifications-settings-modal__body').get('#desktopNotification-all').should('be.checked');
cy.get('#desktopNotification-mention').should('be.visible').click().then(() => {
cy.get('[data-testid="desktopReplyThreads"]').should('be.checked');
cy.get('[data-testid="desktopReplyThreads"]').should('be.visible').click();
cy.get('[data-testid="desktopReplyThreads"]').should('not.be.checked');
});
cy.get('.channel-notifications-settings-modal__body').scrollTo('center').get('#desktopNotification-mention').should('be.checked');
cy.get('.channel-notifications-settings-modal__body').scrollTo('center').get('#desktopNotification-none').should('be.visible').click();
cy.get('.channel-notifications-settings-modal__body').get('#desktopNotification-none').should('be.checked');
// # click on Save button
cy.get('.channel-notifications-settings-modal__save-btn').should('be.visible').click();
// # Set users notification settings
cy.uiOpenChannelMenu('Notification Preferences');
cy.get('.channel-notifications-settings-modal__body').scrollTo('center').get('#desktopNotification-none').should('be.checked');
cy.get('.channel-notifications-settings-modal__body').get('#desktopNotification-all').scrollIntoView().should('be.visible').click();
cy.get('.channel-notifications-settings-modal__save-btn').should('be.visible').click();
// # Post a root message as other user
cy.postMessageAs({sender, message: 'This is a not followed root message', channelId: testChannelId, rootId: ''}).then(({id: postId}) => {
@ -99,14 +134,41 @@ describe('CRT Desktop notifications', () => {
});
});
it('MM-T4417_2 Trigger notifications only on mention replies when channel setting is unchecked', () => {
it('MM-T4417_2 Click on sameMobileSettingsDesktop and check if additional settings still appears', () => {
cy.visit(testChannelUrl);
cy.uiOpenChannelMenu('Notification Preferences');
cy.get('.channel-notifications-settings-modal__body').scrollTo('center').get('#desktopNotification-mention').should('be.visible').click().then(() => {
cy.get('[data-testid="desktopReplyThreads"]').should('be.visible').click();
});
cy.get('.channel-notifications-settings-modal__body').scrollTo('center').get('[data-testid="desktopReplyThreads"]').should('be.visible').click();
cy.get('.channel-notifications-settings-modal__body').get('[data-testid="sameMobileSettingsDesktop"]').scrollIntoView().click().should('be.checked').then(() => {
cy.findByText('Notify me about…').should('not.be.visible');
});
// check the box to see if the additional settings appears
cy.get('.channel-notifications-settings-modal__body').get('[data-testid="sameMobileSettingsDesktop"]').scrollIntoView().click();
cy.get('.mm-modal-generic-section-item__title').should('be.visible').and('contain', 'Notify me about');
cy.get('#MobileNotification-all').should('be.visible').click();
cy.get('#MobileNotification-mention').should('be.visible').click().then(() => {
cy.get('[data-testid="mobileReplyThreads"]').should('be.visible').click();
});
cy.get('#MobileNotification-none').should('be.visible').click();
cy.get('[data-testid="autoFollowThreads"]').should('be.visible').click();
// # click on Save button
cy.get('.channel-notifications-settings-modal__save-btn').should('be.visible').click();
});
it('MM-T4417_3 Trigger notifications only on mention replies when channel setting is unchecked', () => {
cy.visit(testChannelUrl);
// Setup notification spy
spyNotificationAs('notifySpy', 'granted');
// # Set users notification settings
setCRTDesktopNotification('MENTION');
cy.uiOpenChannelMenu('Notification Preferences');
cy.get('.channel-notifications-settings-modal__body').scrollTo('center').get('#desktopNotification-mention').should('be.visible').click();
cy.get('.channel-notifications-settings-modal__save-btn').should('be.visible').click();
// # Post a root message as other user
cy.postMessageAs({sender, message: 'This is a not followed root message', channelId: testChannelId, rootId: ''}).then(({id: postId}) => {
@ -232,32 +294,3 @@ describe('CRT Desktop notifications', () => {
});
});
});
function setCRTDesktopNotification(type) {
if (['ALL', 'MENTION'].indexOf(type) === -1) {
throw new Error(`${type} is invalid`);
}
// # Open settings modal
cy.uiOpenChannelMenu('Notification Preferences');
// # Click "Desktop Notifications"
cy.get('#desktopTitle').
scrollIntoView().
should('be.visible').
and('contain', 'Desktop notifications').click();
// # Select mentions category for messages.
cy.get('#channelNotificationMentions').scrollIntoView().check();
if (type === 'ALL') {
// # Check notify for all replies.
cy.get('#desktopThreadsNotificationAllActivity').scrollIntoView().check().should('be.checked');
} else if (type === 'MENTION') {
// # Check notify only for mentions.
cy.get('#desktopThreadsNotificationAllActivity').scrollIntoView().uncheck().should('not.be.checked');
}
// # Click "Save" and close the modal
cy.uiSaveAndClose();
}

View File

@ -53,9 +53,9 @@ describe('Desktop notifications', () => {
// # Set channel notifications to show on mention only
cy.uiOpenChannelMenu('Notification Preferences');
cy.findByText('Desktop notifications').click();
cy.findByRole('radio', {name: 'Only for mentions'}).click();
cy.uiSaveAndClose();
cy.findByText('Desktop Notifications').should('be.visible');
cy.findByRole('radio', {name: 'Mentions, direct messages, and keywords only'}).click().should('be.checked');
cy.uiSave();
// # Visit off-topic
cy.uiClickSidebarItem('off-topic');

View File

@ -109,26 +109,15 @@ function addNumberOfUsersToChannel(num = 1) {
}
function setIgnoreMentions(toSet) {
let stringToSet = 'Off';
if (toSet) {
stringToSet = 'On';
}
// # Open channel menu and click Notification Preferences
cy.uiOpenChannelMenu('Notification Preferences');
// # Click on the edit button for ignore channel mentions
cy.get('#ignoreChannelMentionsEdit').should('exist').click();
// # find mute or ignore section
cy.findByText('Mute or ignore').should('be.visible');
// # Click on selected option
cy.get(`#ignoreChannelMentions${stringToSet}`).should('exist').click();
// # find Ignore mentions checkbox, set value accordingly
cy.findByRole('checkbox', {name: 'Ignore mentions for @channel, @here and @all'}).click().should(toSet ? 'be.checked' : 'not.be.checked');
// # Click on save to save the configuration
cy.get('#saveSetting').should('exist').click();
// * Assert that the option selected is reflected
cy.get('#ignoreChannelMentionsDesc').should('contain', stringToSet);
// # Click on the X button to close the modal
cy.get('#channelNotificationModalLabel').siblings('.close').click();
cy.uiSave();
}

View File

@ -25,6 +25,7 @@ export type AlertBannerProps = {
id?: string;
mode: ModeType;
title?: React.ReactNode;
customIcon?: React.ReactNode;
message?: React.ReactNode;
children?: React.ReactNode;
className?: string;
@ -41,6 +42,7 @@ const AlertBanner = ({
id,
mode,
title,
customIcon,
message,
className,
variant = 'sys',
@ -57,6 +59,9 @@ const AlertBanner = ({
const [tooltipId] = useState(`alert_banner_close_btn_tooltip_${Math.random()}`);
const bannerIcon = useCallback(() => {
if (customIcon) {
return customIcon;
}
if (mode === 'danger' || mode === 'warning') {
return (
<AlertOutlineIcon
@ -72,7 +77,7 @@ const AlertBanner = ({
<InformationOutlineIcon
size={24}
/>);
}, [mode]);
}, [mode, customIcon]);
return (
<div

View File

@ -0,0 +1,139 @@
.modal .channel-notifications-settings-modal {
overflow: hidden;
width: 100%;
max-width: 600px;
margin-top: -24px;
.modal-content {
display: flex;
overflow: hidden;
max-height: calc(100vh - 240px);
flex-direction: column;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
border-radius: 12px;
box-shadow: var(--elevation-6);
}
&__ctr {
display: flex;
place-items: center;
}
.modal-header {
border: none;
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
background: none;
}
&__body {
display: flex;
width: 100%;
max-width: 1024px;
min-height: 150px;
flex-direction: column;
padding: 32px;
gap: 24px;
overflow-x: hidden;
overflow-y: auto;
}
&__footer {
display: flex;
width: 100%;
max-width: 1024px;
align-items: center;
justify-content: flex-end;
padding: 24px 32px;
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
gap: 8px;
}
&__reset-btn {
display: flex;
padding: 5px 8px;
border: none;
background: transparent;
border-radius: 4px;
color: var(--button-bg);
font-size: 12px;
font-weight: 600;
gap: 4px;
line-height: 9.5px;
place-items: center;
&:hover,
&:active {
background: rgba(var(--button-bg-rgb), 0.08);
}
}
&__server-error {
flex-grow: 1;
color: var(--error-text);
}
&__save-btn {
padding: 12px 20px 12px 20px;
border: none;
background: var(--button-bg);
border-radius: 4px;
color: var(--button-color);
font-size: 14px;
font-weight: 600;
line-height: 14px;
text-transform: capitalize;
&:hover,
&:active {
background: rgba(var(--button-bg-rgb), 0.12);
background: linear-gradient(0deg, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.08)), var(--button-bg);
}
&:focus {
box-sizing: border-box;
padding: 10px 18px;
border: 2px solid var(--sidebar-text-active-border);
box-shadow: none;
}
}
&__cancel-btn {
padding: 12px 20px 12px 20px;
border: none;
background: rgba(var(--button-bg-rgb), 0.08);
border-radius: 4px;
color: var(--button-bg);
font-size: 14px;
font-weight: 600;
line-height: 14px;
text-transform: capitalize;
&:hover,
&:active {
background: rgba(var(--button-bg-rgb), 0.12);
}
}
&__divider {
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
}
@media screen and (max-width: 768px) {
max-width: 100%;
margin: 0;
.modal-content {
display: flex;
height: 100vh;
max-height: unset;
flex-direction: column;
border-radius: unset;
}
}
@media screen and (max-height: 900px) and (min-width: 768px) {
.modal-content {
max-height: calc(100vh - 160px);
}
}
}

View File

@ -1,16 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {screen, fireEvent, waitFor} from '@testing-library/react';
import type {ComponentProps} from 'react';
import React from 'react';
import type {ChannelMembership, ChannelNotifyProps} from '@mattermost/types/channels';
import type {ChannelMembership} from '@mattermost/types/channels';
import type {UserNotifyProps} from '@mattermost/types/users';
import ChannelNotificationsModal from 'components/channel_notifications_modal/channel_notifications_modal';
import {ChannelAutoFollowThreads, DesktopSound, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
import {renderWithContext} from 'tests/react_testing_utils';
import {IgnoreChannelMentions, NotificationLevels} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
describe('components/channel_notifications_modal/ChannelNotificationsModal', () => {
@ -23,12 +24,9 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', ()
channelMember: {
notify_props: {
desktop: NotificationLevels.ALL,
desktop_sound: DesktopSound.ON,
desktop_notification_sound: 'Bing',
mark_unread: NotificationLevels.ALL,
push: NotificationLevels.DEFAULT,
ignore_channel_mentions: IgnoreChannelMentions.DEFAULT,
channel_auto_follow_threads: ChannelAutoFollowThreads.OFF,
desktop_threads: NotificationLevels.ALL,
push_threads: NotificationLevels.DEFAULT,
},
@ -42,359 +40,209 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', ()
}),
sendPushNotifications: true,
actions: {
updateChannelNotifyProps: jest.fn(),
updateChannelNotifyProps: jest.fn().mockImplementation(() => Promise.resolve({data: true})),
},
collapsedReplyThreads: false,
};
test('should match snapshot', () => {
const wrapper = shallow(
it('should not show other settings if channel is mute', async () => {
const wrapper = renderWithContext(
<ChannelNotificationsModal {...baseProps}/>,
);
const muteChannel = screen.getByTestId('muteChannel');
fireEvent.click(muteChannel);
expect(muteChannel).toBeChecked();
const AlertBanner = screen.queryByText('This channel is muted');
expect(AlertBanner).toBeVisible();
expect(screen.queryByText('Desktop Notifications')).toBeNull();
expect(screen.queryByText('Mobile Notifications')).toBeNull();
expect(screen.queryByText('Follow all threads in this channel')).toBeNull();
fireEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
'channel_id',
{
desktop: baseProps.channelMember?.notify_props.desktop,
ignore_channel_mentions: 'off',
mark_unread: 'mention',
push: 'all',
},
),
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot for GMs', () => {
const wrapper = shallow(
<ChannelNotificationsModal
{...{
...baseProps,
channel: TestHelper.getChannelMock({
id: 'channel_id',
display_name: 'channel_display_name',
type: 'G',
}),
channelMember: {
notify_props: {
...baseProps.channelMember!.notify_props,
desktop: NotificationLevels.MENTION,
push: NotificationLevels.MENTION,
},
} as unknown as ChannelMembership,
currentUser: TestHelper.getUserMock({
id: 'current_user_id',
notify_props: {
desktop: NotificationLevels.MENTION,
desktop_threads: NotificationLevels.ALL,
} as UserNotifyProps,
}),
}}
/>,
test('should Ignore mentions for @channel, @here and @all', async () => {
const wrapper = renderWithContext(
<ChannelNotificationsModal {...baseProps}/>,
);
const ignoreChannel = screen.getByTestId('ignoreMentions');
fireEvent.click(ignoreChannel);
expect(ignoreChannel).toBeChecked();
fireEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
'channel_id',
{
desktop: 'all',
ignore_channel_mentions: 'on',
mark_unread:
baseProps.channelMember?.notify_props.mark_unread,
push: 'all',
},
),
);
expect(wrapper).toMatchSnapshot();
});
test('should provide default notify props when missing', () => {
const wrapper = shallow(
<ChannelNotificationsModal
{...baseProps}
channelMember={{notify_props: {}} as ChannelMembership}
/>,
);
expect(wrapper.state('desktopNotifyLevel')).toEqual(NotificationLevels.ALL);
expect(wrapper.state('desktopSound')).toEqual(DesktopSound.ON);
expect(wrapper.state('desktopNotifySound')).toEqual('Bing');
expect(wrapper.state('markUnreadNotifyLevel')).toEqual(NotificationLevels.ALL);
expect(wrapper.state('pushNotifyLevel')).toEqual(NotificationLevels.ALL);
expect(wrapper.state('ignoreChannelMentions')).toEqual(IgnoreChannelMentions.OFF);
expect(wrapper.state('channelAutoFollowThreads')).toEqual(ChannelAutoFollowThreads.OFF);
});
test('should provide correct default when currentUser channel notify props is true', () => {
const currentUser = TestHelper.getUserMock({
id: 'current_user_id',
notify_props: {
desktop: NotificationLevels.ALL,
desktop_threads: NotificationLevels.ALL,
channel: 'true',
} as UserNotifyProps,
});
const props = {...baseProps, currentUser};
const wrapper = shallow(
<ChannelNotificationsModal {...props}/>,
);
expect(wrapper.state('ignoreChannelMentions')).toEqual(IgnoreChannelMentions.OFF);
});
test('should provide correct default when currentUser channel notify props is false', () => {
const currentUser = TestHelper.getUserMock({
id: 'current_user_id',
notify_props: {
desktop: NotificationLevels.ALL,
desktop_threads: NotificationLevels.ALL,
channel: 'false',
} as UserNotifyProps,
});
const props = {...baseProps, currentUser};
const wrapper = shallow(
<ChannelNotificationsModal {...props}/>,
);
expect(wrapper.state('ignoreChannelMentions')).toEqual(IgnoreChannelMentions.ON);
});
test('should provide correct value for ignoreChannelMentions when channelMember channel-wide mentions are off and false on the currentUser', () => {
const currentUser = TestHelper.getUserMock({
id: 'current_user_id',
notify_props: {
desktop: NotificationLevels.ALL,
desktop_threads: NotificationLevels.ALL,
channel: 'false',
} as UserNotifyProps,
});
const channelMember = TestHelper.getChannelMembershipMock({
notify_props: {
ignore_channel_mentions: IgnoreChannelMentions.OFF,
},
});
const props = {...baseProps, channelMember, currentUser};
const wrapper = shallow(
<ChannelNotificationsModal {...props}/>,
);
expect(wrapper.state('ignoreChannelMentions')).toEqual(IgnoreChannelMentions.OFF);
});
test('should provide correct value for ignoreChannelMentions when channelMember channel-wide mentions are on but false on currentUser', () => {
const currentUser = TestHelper.getUserMock({
id: 'current_user_id',
notify_props: {
desktop: NotificationLevels.ALL,
channel: 'true',
} as UserNotifyProps,
});
const channelMember = TestHelper.getChannelMembershipMock({
notify_props: {
ignore_channel_mentions: IgnoreChannelMentions.ON,
},
});
const props = {...baseProps, channelMember, currentUser};
const wrapper = shallow(
<ChannelNotificationsModal {...props}/>,
);
expect(wrapper.state('ignoreChannelMentions')).toEqual(IgnoreChannelMentions.ON);
});
test('should provide correct value for ignoreChannelMentions when channel is muted', () => {
const currentUser = TestHelper.getUserMock({
id: 'current_user_id',
notify_props: {
desktop: NotificationLevels.ALL,
channel: 'true',
} as UserNotifyProps,
});
const channelMember = TestHelper.getChannelMembershipMock({
notify_props: {
mark_unread: NotificationLevels.MENTION,
ignore_channel_mentions: IgnoreChannelMentions.DEFAULT,
},
});
const props = {...baseProps, channelMember, currentUser};
const wrapper = shallow(
<ChannelNotificationsModal {...props}/>,
);
expect(wrapper.state('ignoreChannelMentions')).toEqual(IgnoreChannelMentions.ON);
});
test('should call onExited and match state on handleOnHide', () => {
const wrapper = shallow<ChannelNotificationsModal>(
test('should check the options in the desktop notifications', async () => {
const wrapper = renderWithContext(
<ChannelNotificationsModal {...baseProps}/>,
);
wrapper.setState({activeSection: NotificationSections.DESKTOP, desktopNotifyLevel: NotificationLevels.NONE});
wrapper.instance().handleExit();
expect(baseProps.onExited).toHaveBeenCalledTimes(1);
expect(wrapper.state('activeSection')).toEqual(NotificationSections.NONE);
expect(wrapper.state('desktopNotifyLevel')).toEqual(NotificationLevels.ALL);
expect(screen.queryByText('Desktop Notifications')).toBeVisible();
wrapper.setState({activeSection: NotificationSections.MARK_UNREAD, markUnreadNotifyLevel: NotificationLevels.MENTION});
wrapper.instance().handleExit();
expect(baseProps.onExited).toHaveBeenCalledTimes(2);
expect(wrapper.state('activeSection')).toEqual(NotificationSections.NONE);
expect(wrapper.state('markUnreadNotifyLevel')).toEqual(NotificationLevels.ALL);
const AlllabelRadio: HTMLInputElement = screen.getByTestId(
'desktopNotification-all',
);
fireEvent.click(AlllabelRadio);
expect(AlllabelRadio.checked).toEqual(true);
wrapper.setState({activeSection: NotificationSections.PUSH, pushNotifyLevel: NotificationLevels.NONE});
wrapper.instance().handleExit();
expect(baseProps.onExited).toHaveBeenCalledTimes(3);
expect(wrapper.state('activeSection')).toEqual(NotificationSections.NONE);
expect(wrapper.state('pushNotifyLevel')).toEqual(NotificationLevels.ALL);
const MentionslabelRadio: HTMLInputElement = screen.getByTestId(
'desktopNotification-mention',
);
fireEvent.click(MentionslabelRadio);
expect(MentionslabelRadio.checked).toEqual(true);
const NothinglabelRadio: HTMLInputElement = screen.getByTestId(
'desktopNotification-none',
);
fireEvent.click(NothinglabelRadio);
expect(NothinglabelRadio.checked).toEqual(true);
fireEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
'channel_id',
{
desktop: 'none',
ignore_channel_mentions: 'off',
mark_unread: 'all',
push: 'all',
},
),
);
expect(wrapper).toMatchSnapshot();
});
test('should match state on updateSection', () => {
const wrapper = shallow<ChannelNotificationsModal>(
test('should save the options exactly same as Desktop for mobile if use same as desktop checkbox is checked', async () => {
const wrapper = renderWithContext(
<ChannelNotificationsModal {...baseProps}/>,
);
wrapper.setState({activeSection: NotificationSections.NONE});
wrapper.instance().updateSection(NotificationSections.DESKTOP);
expect(wrapper.state('activeSection')).toEqual(NotificationSections.DESKTOP);
expect(screen.queryByText('Desktop Notifications')).toBeVisible();
const sameAsDesktop: HTMLInputElement = screen.getByTestId(
'sameMobileSettingsDesktop',
);
fireEvent.click(sameAsDesktop);
expect(sameAsDesktop.checked).toEqual(true);
expect(screen.queryByText('All new messages')).toBeNull();
fireEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
'channel_id',
{
desktop: 'all',
ignore_channel_mentions: 'off',
mark_unread: 'all',
push: 'all',
},
),
);
expect(wrapper).toMatchSnapshot();
});
test('should reset state when collapsing a section', () => {
const wrapper = shallow<ChannelNotificationsModal>(
test('should check the options in the mobile notifications', async () => {
const wrapper = renderWithContext(
<ChannelNotificationsModal {...baseProps}/>,
);
wrapper.instance().updateSection(NotificationSections.DESKTOP);
wrapper.instance().handleUpdateDesktopNotifyLevel(NotificationLevels.NONE);
expect(wrapper.state('desktopNotifyLevel')).toEqual(NotificationLevels.NONE);
wrapper.instance().updateSection(NotificationSections.NONE);
expect(wrapper.state('desktopNotifyLevel')).toEqual(baseProps.channelMember?.notify_props.desktop);
});
test('should match state on handleSubmitDesktopNotification', () => {
const wrapper = shallow<ChannelNotificationsModal>(
<ChannelNotificationsModal {...baseProps}/>,
const AlllabelRadio: HTMLInputElement = screen.getByTestId(
'MobileNotification-all',
);
fireEvent.click(AlllabelRadio);
expect(AlllabelRadio.checked).toEqual(true);
const instance = wrapper.instance();
instance.handleUpdateChannelNotifyProps = jest.fn();
instance.updateSection = jest.fn();
wrapper.setState({desktopNotifyLevel: NotificationLevels.MENTION});
instance.handleSubmitDesktopNotification();
expect(instance.handleUpdateChannelNotifyProps).toHaveBeenCalledTimes(1);
wrapper.setState({desktopNotifyLevel: NotificationLevels.ALL});
instance.handleSubmitDesktopNotification();
expect(instance.updateSection).toHaveBeenCalledTimes(1);
expect(instance.updateSection).toBeCalledWith('');
});
test('should match state on handleUpdateDesktopNotifyLevel', () => {
const wrapper = shallow<ChannelNotificationsModal>(
<ChannelNotificationsModal {...baseProps}/>,
const MentionslabelRadio: HTMLInputElement = screen.getByTestId(
'MobileNotification-mention',
);
fireEvent.click(MentionslabelRadio);
expect(MentionslabelRadio.checked).toEqual(true);
wrapper.setState({desktopNotifyLevel: NotificationLevels.ALL});
wrapper.instance().handleUpdateDesktopNotifyLevel(NotificationLevels.MENTION);
expect(wrapper.state('desktopNotifyLevel')).toEqual(NotificationLevels.MENTION);
});
test('should match state on handleSubmitMarkUnreadLevel', () => {
const channelMember = TestHelper.getChannelMembershipMock({
notify_props: {
desktop: NotificationLevels.NONE,
mark_unread: NotificationLevels.ALL,
},
});
const props = {...baseProps, channelMember};
const wrapper = shallow<ChannelNotificationsModal>(
<ChannelNotificationsModal {...props}/>,
const NothinglabelRadio: HTMLInputElement = screen.getByTestId(
'MobileNotification-none',
);
fireEvent.click(NothinglabelRadio);
expect(NothinglabelRadio.checked).toEqual(true);
const instance = wrapper.instance();
instance.handleUpdateChannelNotifyProps = jest.fn();
instance.updateSection = jest.fn();
wrapper.setState({markUnreadNotifyLevel: NotificationLevels.MENTION});
instance.handleSubmitMarkUnreadLevel();
expect(instance.handleUpdateChannelNotifyProps).toHaveBeenCalledTimes(1);
wrapper.setState({markUnreadNotifyLevel: NotificationLevels.ALL});
instance.handleSubmitMarkUnreadLevel();
expect(instance.updateSection).toHaveBeenCalledTimes(1);
expect(instance.updateSection).toBeCalledWith('');
});
test('should match state on handleUpdateMarkUnreadLevel', () => {
const channelMember = TestHelper.getChannelMembershipMock({
notify_props: {
desktop: NotificationLevels.NONE,
mark_unread: NotificationLevels.ALL,
},
});
const props = {...baseProps, channelMember};
const wrapper = shallow<ChannelNotificationsModal>(
<ChannelNotificationsModal {...props}/>,
fireEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
'channel_id',
{
desktop: 'all',
ignore_channel_mentions: 'off',
mark_unread: 'all',
push: 'none',
},
),
);
wrapper.setState({markUnreadNotifyLevel: NotificationLevels.ALL});
wrapper.instance().handleUpdateMarkUnreadLevel(NotificationLevels.MENTION);
expect(wrapper.state('markUnreadNotifyLevel')).toEqual(NotificationLevels.MENTION);
expect(wrapper).toMatchSnapshot();
});
test('should match state on handleSubmitPushNotificationLevel', () => {
const channelMember = {
notify_props: {
desktop: NotificationLevels.NONE,
mark_unread: NotificationLevels.MENTION,
push: NotificationLevels.ALL,
push_threads: NotificationLevels.ALL,
},
} as unknown as ChannelMembership;
const props = {...baseProps, channelMember};
const wrapper = shallow<ChannelNotificationsModal>(
<ChannelNotificationsModal {...props}/>,
);
const instance = wrapper.instance();
instance.handleUpdateChannelNotifyProps = jest.fn();
instance.updateSection = jest.fn();
wrapper.setState({pushNotifyLevel: NotificationLevels.DEFAULT});
instance.handleSubmitPushNotificationLevel();
expect(instance.handleUpdateChannelNotifyProps).toHaveBeenCalledTimes(1);
wrapper.setState({pushNotifyLevel: NotificationLevels.ALL});
instance.handleSubmitPushNotificationLevel();
expect(instance.updateSection).toHaveBeenCalledTimes(1);
expect(instance.updateSection).toBeCalledWith('');
});
test('should match state on handleUpdatePushNotificationLevel', () => {
const channelMember = TestHelper.getChannelMembershipMock({
notify_props: {
desktop: NotificationLevels.NONE,
mark_unread: NotificationLevels.MENTION,
push: NotificationLevels.ALL,
},
});
const props = {...baseProps, channelMember};
const wrapper = shallow<ChannelNotificationsModal>(
<ChannelNotificationsModal {...props}/>,
);
wrapper.setState({pushNotifyLevel: NotificationLevels.ALL});
wrapper.instance().handleUpdatePushNotificationLevel(NotificationLevels.MENTION);
expect(wrapper.state('pushNotifyLevel')).toEqual(NotificationLevels.MENTION);
});
test('should match state on resetStateFromNotifyProps', () => {
const channelMemberNotifyProps: Partial<ChannelNotifyProps> = {
desktop: NotificationLevels.NONE,
mark_unread: NotificationLevels.MENTION,
push: NotificationLevels.ALL,
it('should show auto follow, desktop threads and mobile threads settings if collapsed reply threads is enabled', async () => {
const props = {
...baseProps,
collapsedReplyThreads: true,
};
const currentUserNotifyProps = {
channel: 'false',
} as UserNotifyProps;
const wrapper = shallow<ChannelNotificationsModal>(
<ChannelNotificationsModal {...baseProps}/>,
const wrapper = renderWithContext(
<ChannelNotificationsModal {...props}/>,
);
wrapper.instance().resetStateFromNotifyProps(currentUserNotifyProps, channelMemberNotifyProps);
expect(wrapper.state('desktopNotifyLevel')).toEqual(NotificationLevels.NONE);
expect(wrapper.state('markUnreadNotifyLevel')).toEqual(NotificationLevels.MENTION);
expect(wrapper.state('pushNotifyLevel')).toEqual(NotificationLevels.ALL);
expect(wrapper.state('ignoreChannelMentions')).toEqual(IgnoreChannelMentions.ON);
expect(wrapper.state('channelAutoFollowThreads')).toEqual(ChannelAutoFollowThreads.OFF);
expect(screen.queryByText('Follow all threads in this channel')).toBeVisible();
wrapper.instance().resetStateFromNotifyProps(currentUserNotifyProps, {...channelMemberNotifyProps, desktop: NotificationLevels.ALL});
expect(wrapper.state('desktopNotifyLevel')).toEqual(NotificationLevels.ALL);
fireEvent.click(screen.getByRole('button', {name: /Save/i}));
wrapper.instance().resetStateFromNotifyProps(currentUserNotifyProps, {...channelMemberNotifyProps, mark_unread: NotificationLevels.ALL});
expect(wrapper.state('markUnreadNotifyLevel')).toEqual(NotificationLevels.ALL);
wrapper.instance().resetStateFromNotifyProps(currentUserNotifyProps, {...channelMemberNotifyProps, push: NotificationLevels.NONE});
expect(wrapper.state('pushNotifyLevel')).toEqual(NotificationLevels.NONE);
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
'channel_id',
{
desktop: baseProps.channelMember?.notify_props.desktop,
ignore_channel_mentions: 'off',
mark_unread: 'all',
channel_auto_follow_threads: 'off',
push: 'all',
push_threads: 'default',
desktop_threads: 'all',
},
),
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,23 +1,29 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import deepEqual from 'fast-deep-equal';
import React from 'react';
import React, {useCallback, useState} from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import {FormattedMessage, useIntl} from 'react-intl';
import {BellOffOutlineIcon, RefreshIcon} from '@mattermost/compass-icons/components';
import type {Channel, ChannelNotifyProps} from '@mattermost/types/channels';
import type {UserNotifyProps, UserProfile} from '@mattermost/types/users';
import {isChannelMuted} from 'mattermost-redux/utils/channel_utils';
import AlertBanner from 'components/alert_banner';
import CheckboxSettingItem from 'components/widgets/modals/components/checkbox_setting_item';
import ModalHeader from 'components/widgets/modals/components/modal_header';
import ModalSection from 'components/widgets/modals/components/modal_section';
import RadioSettingItem from 'components/widgets/modals/components/radio_setting_item';
import NotificationSection from 'components/channel_notifications_modal/components/notification_section.jsx';
import {IgnoreChannelMentions, NotificationLevels} from 'utils/constants';
import {ChannelAutoFollowThreads, DesktopSound, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
import * as NotificationSounds from 'utils/notification_sounds';
import type {ChannelMemberNotifyProps} from './utils';
import utils from './utils';
import type {PropsFromRedux} from './index';
import './channel_notifications_modal.scss';
type Props = PropsFromRedux & {
/**
@ -36,508 +42,296 @@ type Props = PropsFromRedux & {
currentUser: UserProfile;
};
export type ChannelMemberNotifyProps = Partial<ChannelNotifyProps> & Pick<UserNotifyProps, 'desktop_threads' | 'push_threads'>
type State = {
show: boolean;
activeSection: string;
serverError: string | null;
desktopNotifyLevel: ChannelNotifyProps['desktop'];
desktopSound: ChannelNotifyProps['desktop_sound'];
desktopNotifySound: ChannelNotifyProps['desktop_notification_sound'];
desktopThreadsNotifyLevel: UserNotifyProps['desktop_threads'];
markUnreadNotifyLevel: ChannelNotifyProps['mark_unread'];
pushNotifyLevel: ChannelNotifyProps['push'];
pushThreadsNotifyLevel: UserNotifyProps['push_threads'];
ignoreChannelMentions: ChannelNotifyProps['ignore_channel_mentions'];
channelAutoFollowThreads: ChannelNotifyProps['channel_auto_follow_threads'];
};
export type DesktopNotificationProps = Pick<State, 'desktopNotifyLevel' | 'desktopNotifySound' | 'desktopSound' | 'desktopThreadsNotifyLevel'>
export type PushNotificationProps = Pick<State, 'pushNotifyLevel' | 'pushThreadsNotifyLevel'>
const getDefaultDesktopNotificationLevel = (currentUserNotifyProps: UserNotifyProps, isGM: boolean): Exclude<ChannelMemberNotifyProps['desktop'], undefined> => {
if (currentUserNotifyProps?.desktop) {
if (currentUserNotifyProps.desktop === NotificationLevels.DEFAULT) {
return NotificationLevels.ALL;
}
if (isGM && currentUserNotifyProps.desktop === NotificationLevels.MENTION) {
return NotificationLevels.ALL;
}
return currentUserNotifyProps.desktop;
}
return NotificationLevels.ALL;
};
const getDefaultDesktopSound = (currentUserNotifyProps: UserNotifyProps): Exclude<ChannelMemberNotifyProps['desktop_sound'], undefined> => {
if (currentUserNotifyProps?.desktop_sound) {
return currentUserNotifyProps.desktop_sound === 'true' ? DesktopSound.ON : DesktopSound.OFF;
}
return DesktopSound.ON;
};
const getDefaultDesktopNotificationSound = (currentUserNotifyProps: UserNotifyProps): Exclude<ChannelMemberNotifyProps['desktop_notification_sound'], undefined> => {
if (currentUserNotifyProps?.desktop_notification_sound) {
return currentUserNotifyProps.desktop_notification_sound;
}
return 'Bing';
};
const getDefaultDesktopThreadsNotifyLevel = (currentUserNotifyProps: UserNotifyProps): Exclude<ChannelMemberNotifyProps['desktop_threads'], undefined> => {
if (currentUserNotifyProps?.desktop_threads) {
return currentUserNotifyProps.desktop_threads;
}
return NotificationLevels.ALL;
};
const getDefaultPushNotifyLevel = (currentUserNotifyProps: UserNotifyProps, isGM: boolean): Exclude<ChannelMemberNotifyProps['push'], undefined> => {
if (currentUserNotifyProps?.push) {
if (currentUserNotifyProps.push === NotificationLevels.DEFAULT) {
return NotificationLevels.ALL;
}
if (isGM && currentUserNotifyProps.desktop === NotificationLevels.MENTION) {
return NotificationLevels.ALL;
}
return currentUserNotifyProps.push;
}
return NotificationLevels.ALL;
};
const getDefaultPushThreadsNotifyLevel = (currentUserNotifyProps: UserNotifyProps, isGM: boolean): Exclude<ChannelMemberNotifyProps['push_threads'], undefined> => {
if (currentUserNotifyProps?.push_threads) {
if (currentUserNotifyProps.push_threads === 'default') {
return NotificationLevels.ALL;
}
if (isGM && currentUserNotifyProps.push_threads === NotificationLevels.MENTION) {
return NotificationLevels.ALL;
}
return currentUserNotifyProps.push_threads;
}
return NotificationLevels.ALL;
};
export default class ChannelNotificationsModal extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
const channelNotifyProps = props.channelMember?.notify_props;
this.state = {
show: true,
activeSection: NotificationSections.NONE,
serverError: null,
...this.getStateFromNotifyProps(props.currentUser.notify_props, channelNotifyProps),
};
}
componentDidUpdate(prevProps: Props) {
const prevChannelNotifyProps = prevProps.channelMember && prevProps.channelMember.notify_props;
const channelNotifyProps = this.props.channelMember && this.props.channelMember.notify_props;
if (!deepEqual(channelNotifyProps, prevChannelNotifyProps)) {
this.resetStateFromNotifyProps(this.props.currentUser.notify_props, channelNotifyProps);
}
}
resetStateFromNotifyProps(currentUserNotifyProps: UserNotifyProps, channelMemberNotifyProps?: Partial<ChannelNotifyProps>) {
this.setState(this.getStateFromNotifyProps(currentUserNotifyProps, channelMemberNotifyProps));
}
verifyNotificationsSettingSameAsGlobal({
desktopNotifyLevel,
desktopNotifySound,
desktopSound,
desktopThreadsNotifyLevel,
}: DesktopNotificationProps) {
const currentUserNotifyProps = this.props.currentUser.notify_props;
if (
desktopNotifyLevel === getDefaultDesktopNotificationLevel(currentUserNotifyProps, this.isGM()) &&
desktopNotifySound === getDefaultDesktopNotificationSound(currentUserNotifyProps) &&
desktopSound === getDefaultDesktopSound(currentUserNotifyProps) &&
desktopThreadsNotifyLevel === getDefaultDesktopThreadsNotifyLevel(currentUserNotifyProps)
) {
return true;
}
return false;
}
isGM() {
return this.props.channel.type === 'G';
}
verifyPushNotificationsSettingSameAsGlobal({
pushNotifyLevel,
pushThreadsNotifyLevel,
}: PushNotificationProps) {
const currentUserNotifyProps = this.props.currentUser.notify_props;
if (
pushNotifyLevel === getDefaultPushNotifyLevel(currentUserNotifyProps, this.isGM()) &&
pushThreadsNotifyLevel === getDefaultPushThreadsNotifyLevel(currentUserNotifyProps, this.isGM())
) {
return true;
}
return false;
}
getStateFromNotifyProps(currentUserNotifyProps: UserNotifyProps, channelMemberNotifyProps?: ChannelMemberNotifyProps) {
let ignoreChannelMentionsDefault: ChannelNotifyProps['ignore_channel_mentions'] = IgnoreChannelMentions.OFF;
const desktopNotifyLevelDefault: ChannelNotifyProps['desktop'] = getDefaultDesktopNotificationLevel(currentUserNotifyProps, this.isGM());
const pushNotifyLevelDefault: ChannelMemberNotifyProps['push'] = getDefaultPushNotifyLevel(currentUserNotifyProps, this.isGM());
const pushThreadsNotifyLevelDefault: ChannelMemberNotifyProps['push_threads'] = getDefaultPushThreadsNotifyLevel(currentUserNotifyProps, this.isGM());
const channelDesktopNotifyProps = channelMemberNotifyProps?.desktop || NotificationLevels.DEFAULT;
let desktopNotifyLevel = desktopNotifyLevelDefault;
if (channelDesktopNotifyProps !== NotificationLevels.DEFAULT) {
desktopNotifyLevel = channelDesktopNotifyProps;
}
const channelPushNotifyProps = channelMemberNotifyProps?.push || NotificationLevels.DEFAULT;
let pushNotifyLevel = pushNotifyLevelDefault;
if (channelPushNotifyProps !== 'default') {
pushNotifyLevel = channelPushNotifyProps;
}
const channelPushThreadsNotifyProps = channelMemberNotifyProps?.push_threads || NotificationLevels.DEFAULT;
let pushThreadsNotifyLevel = pushThreadsNotifyLevelDefault;
if (channelPushThreadsNotifyProps !== 'default') {
pushThreadsNotifyLevel = channelPushThreadsNotifyProps;
}
if (channelMemberNotifyProps?.mark_unread === NotificationLevels.MENTION || (currentUserNotifyProps.channel && currentUserNotifyProps.channel === 'false')) {
ignoreChannelMentionsDefault = IgnoreChannelMentions.ON;
}
let ignoreChannelMentions = channelMemberNotifyProps?.ignore_channel_mentions;
if (!ignoreChannelMentions || ignoreChannelMentions === IgnoreChannelMentions.DEFAULT) {
ignoreChannelMentions = ignoreChannelMentionsDefault;
}
return {
desktopNotifyLevel,
desktopSound: channelMemberNotifyProps?.desktop_sound || getDefaultDesktopSound(currentUserNotifyProps),
desktopNotifySound: channelMemberNotifyProps?.desktop_notification_sound || getDefaultDesktopNotificationSound(currentUserNotifyProps),
desktopThreadsNotifyLevel: channelMemberNotifyProps?.desktop_threads || getDefaultDesktopThreadsNotifyLevel(currentUserNotifyProps),
markUnreadNotifyLevel: channelMemberNotifyProps?.mark_unread || NotificationLevels.ALL,
pushNotifyLevel,
pushThreadsNotifyLevel,
ignoreChannelMentions,
channelAutoFollowThreads: channelMemberNotifyProps?.channel_auto_follow_threads || ChannelAutoFollowThreads.OFF,
};
}
handleHide = () => this.setState({show: false});
handleExit = () => {
this.updateSection(NotificationSections.NONE);
this.props.onExited();
};
updateSection = (section = NotificationSections.NONE) => {
this.setState({activeSection: section});
if (section === NotificationSections.NONE) {
const channelNotifyProps = this.props.channelMember && this.props.channelMember.notify_props;
this.resetStateFromNotifyProps(this.props.currentUser.notify_props, channelNotifyProps);
}
};
handleUpdateChannelNotifyProps = async (props: Partial<ChannelNotifyProps>) => {
const {
actions,
channel,
currentUser,
} = this.props;
const {error} = await actions.updateChannelNotifyProps(currentUser.id, channel.id, props);
if (error) {
this.setState({serverError: error.message});
} else {
this.updateSection(NotificationSections.NONE);
}
};
handleResetDesktopNotification = () => {
const currentUserNotifyProps = this.props.currentUser.notify_props;
const userDesktopNotificationDefaults = {
desktopNotifyLevel: getDefaultDesktopNotificationLevel(currentUserNotifyProps, this.isGM()),
desktopSound: getDefaultDesktopSound(currentUserNotifyProps),
desktopNotifySound: getDefaultDesktopNotificationSound(currentUserNotifyProps),
desktopThreadsNotifyLevel: getDefaultDesktopThreadsNotifyLevel(currentUserNotifyProps),
};
this.setState(userDesktopNotificationDefaults);
};
handleResetPushNotification = () => {
const currentUserNotifyProps = this.props.currentUser.notify_props;
const userPushNotificationDefaults = {
pushNotifyLevel: getDefaultPushNotifyLevel(currentUserNotifyProps, this.isGM()),
pushThreadsNotifyLevel: getDefaultPushThreadsNotifyLevel(currentUserNotifyProps, this.isGM()),
};
this.setState(userPushNotificationDefaults);
};
handleSubmitDesktopNotification = () => {
const channelNotifyProps = this.props.channelMember && this.props.channelMember.notify_props as ChannelMemberNotifyProps;
const {desktopNotifyLevel, desktopNotifySound, desktopSound, desktopThreadsNotifyLevel} = this.state;
if (
channelNotifyProps?.desktop === desktopNotifyLevel &&
channelNotifyProps?.desktop_threads === desktopThreadsNotifyLevel &&
channelNotifyProps?.desktop_sound === desktopSound &&
channelNotifyProps?.desktop_notification_sound === desktopNotifySound
) {
this.updateSection(NotificationSections.NONE);
return;
}
const props = {desktop: desktopNotifyLevel, desktop_threads: desktopThreadsNotifyLevel, desktop_sound: desktopSound, desktop_notification_sound: desktopNotifySound};
this.handleUpdateChannelNotifyProps(props);
};
handleUpdateDesktopNotifyLevel = (desktopNotifyLevel: ChannelNotifyProps['desktop']) => this.setState({desktopNotifyLevel});
handleUpdateDesktopThreadsNotifyLevel = (desktopThreadsNotifyLevel: UserNotifyProps['desktop_threads']) => this.setState({desktopThreadsNotifyLevel});
handleUpdateDesktopSound = (desktopSound: ChannelNotifyProps['desktop_sound']) => this.setState({desktopSound});
handleUpdateDesktopNotifySound = (desktopNotifySound: ChannelNotifyProps['desktop_notification_sound']) => {
if (desktopNotifySound) {
NotificationSounds.tryNotificationSound(desktopNotifySound);
}
this.setState({desktopNotifySound});
};
handleSubmitMarkUnreadLevel = () => {
const channelNotifyProps = this.props.channelMember && this.props.channelMember.notify_props;
const {markUnreadNotifyLevel} = this.state;
if (channelNotifyProps?.mark_unread === markUnreadNotifyLevel) {
this.updateSection(NotificationSections.NONE);
return;
}
const props = {mark_unread: markUnreadNotifyLevel};
this.handleUpdateChannelNotifyProps(props);
};
handleUpdateMarkUnreadLevel = (markUnreadNotifyLevel: ChannelNotifyProps['mark_unread']) => this.setState({markUnreadNotifyLevel});
handleSubmitPushNotificationLevel = () => {
const channelNotifyProps = this.props.channelMember && this.props.channelMember.notify_props as ChannelMemberNotifyProps;
const {pushNotifyLevel, pushThreadsNotifyLevel} = this.state;
if (
channelNotifyProps?.push === pushNotifyLevel &&
channelNotifyProps?.push_threads === pushThreadsNotifyLevel
) {
this.updateSection(NotificationSections.NONE);
return;
}
const props = {push: pushNotifyLevel, push_threads: pushThreadsNotifyLevel};
this.handleUpdateChannelNotifyProps(props);
};
handleUpdatePushNotificationLevel = (pushNotifyLevel: ChannelNotifyProps['push']) => this.setState({pushNotifyLevel});
handleUpdatePushThreadsNotificationLevel = (pushThreadsNotifyLevel: UserNotifyProps['push_threads']) => this.setState({pushThreadsNotifyLevel});
handleUpdateIgnoreChannelMentions = (ignoreChannelMentions: ChannelNotifyProps['ignore_channel_mentions']) => this.setState({ignoreChannelMentions});
handleSubmitIgnoreChannelMentions = () => {
const channelNotifyProps = this.props.channelMember && this.props.channelMember.notify_props;
const {ignoreChannelMentions} = this.state;
if (channelNotifyProps?.ignore_channel_mentions === ignoreChannelMentions) {
this.updateSection(NotificationSections.NONE);
return;
}
const props = {ignore_channel_mentions: ignoreChannelMentions};
this.handleUpdateChannelNotifyProps(props);
};
handleUpdateChannelAutoFollowThreads = (channelAutoFollowThreads: ChannelNotifyProps['channel_auto_follow_threads']) => this.setState({channelAutoFollowThreads});
handleSubmitChannelAutoFollowThreads = () => {
const channelNotifyProps = this.props.channelMember && this.props.channelMember.notify_props;
const {channelAutoFollowThreads} = this.state;
if (channelNotifyProps?.channel_auto_follow_threads === channelAutoFollowThreads) {
this.updateSection(NotificationSections.NONE);
return;
}
const props = {channel_auto_follow_threads: channelAutoFollowThreads};
this.handleUpdateChannelNotifyProps(props);
};
render() {
const {
activeSection,
desktopNotifyLevel,
desktopThreadsNotifyLevel,
desktopSound,
desktopNotifySound,
markUnreadNotifyLevel,
pushNotifyLevel,
pushThreadsNotifyLevel,
ignoreChannelMentions,
channelAutoFollowThreads,
serverError,
} = this.state;
const {
channel,
channelMember,
currentUser,
sendPushNotifications,
} = this.props;
const isNotificationsSettingSameAsGlobal = this.verifyNotificationsSettingSameAsGlobal({
desktopNotifyLevel,
desktopNotifySound,
desktopSound,
desktopThreadsNotifyLevel,
});
const isPushNotificationsSettingSameAsGlobal = this.verifyPushNotificationsSettingSameAsGlobal({
pushNotifyLevel,
pushThreadsNotifyLevel,
});
let serverErrorTag = null;
if (serverError) {
serverErrorTag = <div className='form-group has-error'><label className='control-label'>{serverError}</label></div>;
}
const isGM = this.isGM();
return (
<Modal
dialogClassName='a11y__modal settings-modal settings-modal--tabless'
show={this.state.show}
onHide={this.handleHide}
onExited={this.handleExit}
role='dialog'
aria-labelledby='channelNotificationModalLabel'
>
<Modal.Header closeButton={true}>
<Modal.Title
componentClass='h1'
id='channelNotificationModalLabel'
>
<FormattedMessage
id='channel_notifications.preferences'
defaultMessage='Notification Preferences for '
/>
<span className='name'>{channel.display_name}</span>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className='settings-table'>
<div className='settings-content'>
<div className='user-settings'>
<br/>
<div className='divider-dark first'/>
<NotificationSection
section={NotificationSections.MARK_UNREAD}
expand={activeSection === NotificationSections.MARK_UNREAD}
memberNotificationLevel={markUnreadNotifyLevel}
onChange={this.handleUpdateMarkUnreadLevel}
onSubmit={this.handleSubmitMarkUnreadLevel}
onUpdateSection={this.updateSection}
serverError={serverError}
isGM={isGM}
/>
<div className='divider-light'/>
<NotificationSection
section={NotificationSections.IGNORE_CHANNEL_MENTIONS}
expand={activeSection === NotificationSections.IGNORE_CHANNEL_MENTIONS}
memberNotificationLevel={markUnreadNotifyLevel}
ignoreChannelMentions={ignoreChannelMentions}
onChange={this.handleUpdateIgnoreChannelMentions}
onSubmit={this.handleSubmitIgnoreChannelMentions}
onUpdateSection={this.updateSection}
serverError={serverError}
isGM={isGM}
/>
{!isChannelMuted(channelMember) &&
<div>
<div className='divider-light'/>
<NotificationSection
section={NotificationSections.DESKTOP}
expand={activeSection === NotificationSections.DESKTOP}
memberNotificationLevel={desktopNotifyLevel}
memberThreadsNotificationLevel={desktopThreadsNotifyLevel}
memberDesktopSound={desktopSound}
memberDesktopNotificationSound={desktopNotifySound}
globalNotificationLevel={getDefaultDesktopNotificationLevel(currentUser.notify_props, isGM)}
globalNotificationSound={getDefaultDesktopNotificationSound(currentUser.notify_props)}
isNotificationsSettingSameAsGlobal={isNotificationsSettingSameAsGlobal}
onChange={this.handleUpdateDesktopNotifyLevel}
onChangeThreads={this.handleUpdateDesktopThreadsNotifyLevel}
onChangeDesktopSound={this.handleUpdateDesktopSound}
onChangeNotificationSound={this.handleUpdateDesktopNotifySound}
onReset={this.handleResetDesktopNotification}
onSubmit={this.handleSubmitDesktopNotification}
onUpdateSection={this.updateSection}
serverError={serverError}
isGM={isGM}
/>
<div className='divider-light'/>
{sendPushNotifications &&
<NotificationSection
section={NotificationSections.PUSH}
expand={activeSection === NotificationSections.PUSH}
memberNotificationLevel={pushNotifyLevel}
memberThreadsNotificationLevel={pushThreadsNotifyLevel}
globalNotificationLevel={getDefaultPushNotifyLevel(currentUser.notify_props, isGM)}
isNotificationsSettingSameAsGlobal={isPushNotificationsSettingSameAsGlobal}
onChange={this.handleUpdatePushNotificationLevel}
onReset={this.handleResetPushNotification}
onChangeThreads={this.handleUpdatePushThreadsNotificationLevel}
onSubmit={this.handleSubmitPushNotificationLevel}
onUpdateSection={this.updateSection}
serverError={serverError}
isGM={isGM}
/>
}
</div>
}
{!isGM &&
<>
<div className='divider-light'/>
<NotificationSection
section={NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS}
expand={activeSection === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS}
memberNotificationLevel={markUnreadNotifyLevel}
ignoreChannelMentions={ignoreChannelMentions}
channelAutoFollowThreads={channelAutoFollowThreads}
onChange={this.handleUpdateChannelAutoFollowThreads}
onSubmit={this.handleSubmitChannelAutoFollowThreads}
onUpdateSection={this.updateSection}
serverError={serverError}
isGM={isGM}
/>
</>
}
<div className='divider-dark'/>
</div>
</div>
</div>
{serverErrorTag}
</Modal.Body>
</Modal>
);
}
function getUseSameDesktopSetting(currentUserNotifyProps: UserNotifyProps, channelMemberNotifyProps?: ChannelMemberNotifyProps) {
const isSameAsDesktop = channelMemberNotifyProps ? channelMemberNotifyProps?.desktop === channelMemberNotifyProps?.push :
currentUserNotifyProps.push === currentUserNotifyProps.desktop;
const isSameAsDesktopThreads = channelMemberNotifyProps ? channelMemberNotifyProps?.desktop_threads === channelMemberNotifyProps?.push_threads :
currentUserNotifyProps.push_threads === currentUserNotifyProps.desktop_threads;
return isSameAsDesktop && isSameAsDesktopThreads;
}
function getStateFromNotifyProps(currentUserNotifyProps: UserNotifyProps, channelMemberNotifyProps?: ChannelMemberNotifyProps) {
let ignoreChannelMentionsDefault: ChannelNotifyProps['ignore_channel_mentions'] = IgnoreChannelMentions.OFF;
if (channelMemberNotifyProps?.mark_unread === NotificationLevels.MENTION || (currentUserNotifyProps.channel && currentUserNotifyProps.channel === 'false')) {
ignoreChannelMentionsDefault = IgnoreChannelMentions.ON;
}
let ignoreChannelMentions = channelMemberNotifyProps?.ignore_channel_mentions;
if (!ignoreChannelMentions || ignoreChannelMentions === IgnoreChannelMentions.DEFAULT) {
ignoreChannelMentions = ignoreChannelMentionsDefault;
}
const desktop = channelMemberNotifyProps?.desktop === NotificationLevels.DEFAULT ? currentUserNotifyProps.desktop : (channelMemberNotifyProps?.desktop || currentUserNotifyProps.desktop);
const push = channelMemberNotifyProps?.push === NotificationLevels.DEFAULT ? currentUserNotifyProps.desktop : (channelMemberNotifyProps?.push || currentUserNotifyProps.push);
return {
desktop,
desktop_threads: channelMemberNotifyProps?.desktop_threads || NotificationLevels.ALL,
mark_unread: channelMemberNotifyProps?.mark_unread || NotificationLevels.ALL,
push,
push_threads: channelMemberNotifyProps?.push_threads || NotificationLevels.ALL,
ignore_channel_mentions: ignoreChannelMentions,
channel_auto_follow_threads: channelMemberNotifyProps?.channel_auto_follow_threads || 'off',
};
}
type SettingsType = {
desktop: ChannelNotifyProps['desktop'];
desktop_threads: ChannelNotifyProps['desktop_threads'];
mark_unread: ChannelNotifyProps['mark_unread'];
push: ChannelNotifyProps['push'];
push_threads: ChannelNotifyProps['push_threads'];
ignore_channel_mentions: ChannelNotifyProps['ignore_channel_mentions'];
channel_auto_follow_threads: ChannelNotifyProps['channel_auto_follow_threads'];
};
export default function ChannelNotificationsModal(props: Props) {
const {formatMessage} = useIntl();
const [show, setShow] = useState(true);
const [serverError, setServerError] = useState('');
const [mobileSettingsSameAsDesktop, setMobileSettingsSameAsDesktop] = useState<boolean>(getUseSameDesktopSetting(props.currentUser.notify_props, props.channelMember?.notify_props));
const [settings, setSettings] = useState<SettingsType>(getStateFromNotifyProps(props.currentUser.notify_props, props.channelMember?.notify_props));
function handleHide() {
setShow(false);
}
const handleChange = useCallback((values: Record<string, string>) => {
setSettings((prevSettings) => ({...prevSettings, ...values}));
}, []);
const handleMobileSettingsChange = useCallback(() => {
setMobileSettingsSameAsDesktop((prevSettings) => !prevSettings);
setSettings((prevSettings) => ({...prevSettings, push: prevSettings.desktop, push_threads: prevSettings.desktop_threads}));
}, []);
const MuteIgnoreSectionContent = (
<>
<CheckboxSettingItem
description={utils.MuteChannelDesc}
inputFieldValue={settings.mark_unread === 'mention'}
inputFieldData={utils.MuteChannelInputFieldData}
handleChange={(e) => handleChange({mark_unread: e ? 'mention' : 'all'})}
/>
<CheckboxSettingItem
description={utils.IgnoreMentionsDesc}
inputFieldValue={settings.ignore_channel_mentions === 'on'}
inputFieldData={utils.IgnoreMentionsInputFieldData}
handleChange={(e) => handleChange({ignore_channel_mentions: e ? 'on' : 'off'})}
/>
</>
);
const DesktopNotificationsSectionContent = (
<>
<RadioSettingItem
title={utils.NotifyMeTitle}
inputFieldValue={settings.desktop}
inputFieldData={utils.desktopNotificationInputFieldData(props.currentUser.notify_props.desktop)}
handleChange={(e) => handleChange({desktop: e.target.value})}
/>
{props.collapsedReplyThreads && settings.desktop === 'mention' &&
<CheckboxSettingItem
title={utils.ThreadsReplyTitle}
inputFieldValue={settings.desktop_threads === 'all'}
inputFieldData={utils.DesktopReplyThreadsInputFieldData}
handleChange={(e) => handleChange({desktop_threads: e ? 'all' : 'mention'})}
/>}
</>
);
const MobileNotificationsSectionContent = (
<>
<CheckboxSettingItem
inputFieldValue={mobileSettingsSameAsDesktop}
inputFieldData={utils.sameMobileSettingsDesktopInputFieldData}
handleChange={() => handleMobileSettingsChange()}
/>
{!mobileSettingsSameAsDesktop && (
<>
<RadioSettingItem
title={utils.NotifyMeTitle}
inputFieldValue={settings.push}
inputFieldData={utils.mobileNotificationInputFieldData(props.currentUser.notify_props.push)}
handleChange={(e) => handleChange({push: e.target.value})}
/>
{props.collapsedReplyThreads && settings.push === 'mention' &&
<CheckboxSettingItem
title={utils.ThreadsReplyTitle}
inputFieldValue={settings.push_threads === 'all'}
inputFieldData={utils.MobileReplyThreadsInputFieldData}
handleChange={(e) => handleChange({push_threads: e ? 'all' : 'mention'})}
/>}
</>
)}
</>
);
const AutoFollowThreadsSectionContent = (
<>
<CheckboxSettingItem
inputFieldValue={settings.channel_auto_follow_threads === 'on'}
inputFieldData={utils.AutoFollowThreadsInputFieldData}
handleChange={(e) => handleChange({channel_auto_follow_threads: e ? 'on' : 'off'})}
/>
</>
);
function handleSave() {
const userSettings: Partial<SettingsType> = {...settings};
if (!props.collapsedReplyThreads) {
delete userSettings.push_threads;
delete userSettings.desktop_threads;
delete userSettings.channel_auto_follow_threads;
}
props.actions.updateChannelNotifyProps(props.currentUser.id, props.channel.id, userSettings).then((value) => {
const {error} = value;
if (error) {
setServerError(error.message);
} else {
handleHide();
}
});
}
const resetToDefaultBtn = useCallback((settingName: string) => {
const defaultSettings = props.currentUser.notify_props;
const resetToDefault = (settingName: string) => {
if (settingName === 'desktop') {
setSettings({...settings, desktop: defaultSettings.desktop, desktop_threads: defaultSettings.desktop_threads || settings.desktop_threads});
}
if (settingName === 'push') {
setSettings({...settings, push: defaultSettings.desktop, push_threads: defaultSettings.push_threads || settings.push_threads});
}
};
const isDesktopSameAsDefault = (defaultSettings.desktop === settings.desktop && defaultSettings.desktop_threads === settings.desktop_threads);
const isPushSameAsDefault = (defaultSettings.push === settings.push && defaultSettings.push_threads === settings.push_threads);
if ((settingName === 'desktop' && isDesktopSameAsDefault) || (settingName === 'push' && isPushSameAsDefault)) {
return <></>;
}
return (
<button
className='channel-notifications-settings-modal__reset-btn'
onClick={() => resetToDefault(settingName)}
>
<RefreshIcon
size={14}
color={'currentColor'}
/>
{formatMessage({
id: 'channel_notifications.resetToDefault',
defaultMessage: 'Reset to default',
})}
</button>
);
}, [props.currentUser, settings]);
const settingsAndAlertBanner = settings.mark_unread === 'all' ? (
<>
<div className='channel-notifications-settings-modal__divider'/>
<ModalSection
title={utils.DesktopNotificationsSectionTitle}
description={utils.DesktopNotificationsSectionDesc}
content={DesktopNotificationsSectionContent}
titleSuffix={resetToDefaultBtn('desktop')}
/>
<div className='channel-notifications-settings-modal__divider'/>
<ModalSection
title={utils.MobileNotificationsSectionTitle}
description={utils.MobileNotificationsSectionDesc}
content={MobileNotificationsSectionContent}
titleSuffix={resetToDefaultBtn('push')}
/>
</>
) : (
<AlertBanner
mode='info'
variant='app'
customIcon={
<BellOffOutlineIcon
size={24}
color={'currentColor'}
/>
}
title={
<FormattedMessage
id='channel_notifications.alertBanner.title'
defaultMessage='This channel is muted'
/>
}
message={
<FormattedMessage
id='channel_notifications.alertBanner.description'
defaultMessage='All other notification preferences for this channel are disabled'
/>
}
/>
);
return (
<Modal
dialogClassName='a11y__modal channel-notifications-settings-modal'
show={show}
onHide={handleHide}
onExited={props.onExited}
role='dialog'
aria-labelledby='channelNotificationModalLabel'
style={{display: 'flex', placeItems: 'center'}}
>
<ModalHeader
id={'channelNotificationModalLabel'}
title={formatMessage({
id: 'channel_notifications.preferences',
defaultMessage: 'Notification Preferences',
})}
subtitle={props.channel.display_name}
handleClose={handleHide}
/>
<main className='channel-notifications-settings-modal__body'>
<ModalSection
title={utils.MuteAndIgnoreSectionTitle}
content={MuteIgnoreSectionContent}
/>
{settingsAndAlertBanner}
{props.collapsedReplyThreads &&
<>
<div className='channel-notifications-settings-modal__divider'/>
<ModalSection
title={utils.AutoFollowThreadsTitle}
description={utils.AutoFollowThreadsDesc}
content={AutoFollowThreadsSectionContent}
/>
</>
}
</main>
<footer className='channel-notifications-settings-modal__footer'>
{serverError &&
<span className='channel-notifications-settings-modal__server-error'>
{serverError}
</span>
}
<button
onClick={handleHide}
className='channel-notifications-settings-modal__cancel-btn'
>
<FormattedMessage
id='generic_btn.cancel'
defaultMessage='Cancel'
/>
</button>
<button
className={'channel-notifications-settings-modal__save-btn'}
onClick={handleSave}
>
<FormattedMessage
id='generic_btn.save'
defaultMessage='Save'
/>
</button>
</footer>
</Modal>
);
}

View File

@ -1,61 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/channel_notifications_modal/CollapseView should match snapshot, DESKTOP on collapsed view 1`] = `
<SettingItemMin
describe={
<Describe
globalNotifyLevel="default"
isCollapsed={true}
memberNotifyLevel="all"
section="desktop"
/>
}
section="desktop"
title={
<SectionTitle
section="desktop"
/>
}
updateSection={[MockFunction]}
/>
`;
exports[`components/channel_notifications_modal/CollapseView should match snapshot, MARK_UNREAD on collapsed view 1`] = `
<SettingItemMin
describe={
<Describe
globalNotifyLevel="default"
isCollapsed={true}
memberNotifyLevel="all"
section="markUnread"
/>
}
section="markUnread"
title={
<SectionTitle
section="markUnread"
/>
}
updateSection={[MockFunction]}
/>
`;
exports[`components/channel_notifications_modal/CollapseView should match snapshot, PUSH on collapsed view 1`] = `
<SettingItemMin
describe={
<Describe
globalNotifyLevel="default"
isCollapsed={true}
memberNotifyLevel="all"
section="push"
/>
}
section="push"
title={
<SectionTitle
section="push"
/>
}
updateSection={[MockFunction]}
/>
`;

View File

@ -1,59 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, on DESKTOP/PUSH & ALL 1`] = `
<MemoizedFormattedMessage
defaultMessage="For all activity {isDefault}"
id="channel_notifications.allActivity"
values={
Object {
"isDefault": <React.Fragment />,
}
}
/>
`;
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, on MARK_UNREAD & ALL 1`] = `
<MemoizedFormattedMessage
defaultMessage="Off"
id="channel_notifications.muteChannel.off.title"
/>
`;
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, on MENTION 1`] = `
<MemoizedFormattedMessage
defaultMessage="Only for mentions {isDefault}"
id="channel_notifications.onlyMentions"
values={
Object {
"isDefault": <React.Fragment />,
}
}
/>
`;
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, on NONE 1`] = `
<MemoizedFormattedMessage
defaultMessage="Never {isDefault}"
id="channel_notifications.never"
values={
Object {
"isDefault": <React.Fragment />,
}
}
/>
`;
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, on global DEFAULT 1`] = `
<MemoizedFormattedMessage
defaultMessage="Global default ({notifyLevel})"
id="channel_notifications.globalDefault"
values={
Object {
"notifyLevel": <Memo(MemoizedFormattedMessage)
defaultMessage="default"
id="channel_notifications.levels.default"
/>,
}
}
/>
`;

View File

@ -1,942 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/channel_notifications_modal/ExpandView gms should match snapshot, DESKTOP on expanded view when mentions is selected 1`] = `
<SettingItemMax
containerStyle=""
infoPosition="bottom"
inputs={
Array [
<div>
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Send desktop notifications"
id="channel_notifications.sendDesktop"
/>
</legend>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelNotificationAllActivity"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="all"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="all"
section="desktop"
/>
</label>
</div>
<div
className="radio"
>
<label
className=""
>
<input
checked={true}
id="channelNotificationMentions"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="mention"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="mention"
section="desktop"
/>
</label>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="channelNotificationNever"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="none"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="none"
section="desktop"
/>
</label>
</div>
</fieldset>
<div
className="mt-5"
>
<ExtraInfo
section="desktop"
/>
</div>
<React.Fragment>
<hr />
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Notification sound"
id="channel_notifications.sound"
/>
</legend>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelDesktopSoundOn"
name="channelDesktopSound"
type="radio"
value="on"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="On"
id="channel_notifications.sound.on.title"
/>
</label>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="channelDesktopSoundOff"
name="channelDesktopSound"
type="radio"
value="off"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="channel_notifications.sound.off.title"
/>
</label>
</div>
<div
className="mt-5"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Notification sounds are available on Firefox, Edge, Safari, Chrome and Mattermost Desktop Apps."
id="channel_notifications.sound_info"
/>
</div>
</fieldset>
</React.Fragment>
</div>,
]
}
saving={false}
section=""
serverError=""
submit={[MockFunction]}
title={
<SectionTitle
isExpanded={true}
onClickResetButton={[MockFunction]}
section="desktop"
/>
}
updateSection={[MockFunction]}
/>
`;
exports[`components/channel_notifications_modal/ExpandView gms should match snapshot, PUSH on expanded view when mentions is selected 1`] = `
<SettingItemMax
containerStyle=""
infoPosition="bottom"
inputs={
Array [
<div>
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Send mobile push notifications"
id="channel_notifications.sendMobilePush"
/>
</legend>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelNotificationAllActivity"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="all"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="all"
section="push"
/>
</label>
</div>
<div
className="radio"
>
<label
className=""
>
<input
checked={true}
id="channelNotificationMentions"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="mention"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="mention"
section="push"
/>
</label>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="channelNotificationNever"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="none"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="none"
section="push"
/>
</label>
</div>
</fieldset>
<div
className="mt-5"
>
<ExtraInfo
section="push"
/>
</div>
</div>,
]
}
saving={false}
section=""
serverError=""
submit={[MockFunction]}
title={
<SectionTitle
isExpanded={true}
onClickResetButton={[MockFunction]}
section="push"
/>
}
updateSection={[MockFunction]}
/>
`;
exports[`components/channel_notifications_modal/ExpandView normal channels should match snapshot, DESKTOP on expanded view 1`] = `
<SettingItemMax
containerStyle=""
infoPosition="bottom"
inputs={
Array [
<div>
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Send desktop notifications"
id="channel_notifications.sendDesktop"
/>
</legend>
<div
className="radio"
>
<label
className=""
>
<input
checked={true}
id="channelNotificationAllActivity"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="all"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="all"
section="desktop"
/>
</label>
</div>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelNotificationMentions"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="mention"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="mention"
section="desktop"
/>
</label>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="channelNotificationNever"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="none"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="none"
section="desktop"
/>
</label>
</div>
</fieldset>
<div
className="mt-5"
>
<ExtraInfo
section="desktop"
/>
</div>
<React.Fragment>
<hr />
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Notification sound"
id="channel_notifications.sound"
/>
</legend>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelDesktopSoundOn"
name="channelDesktopSound"
type="radio"
value="on"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="On"
id="channel_notifications.sound.on.title"
/>
</label>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="channelDesktopSoundOff"
name="channelDesktopSound"
type="radio"
value="off"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="channel_notifications.sound.off.title"
/>
</label>
</div>
<div
className="mt-5"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Notification sounds are available on Firefox, Edge, Safari, Chrome and Mattermost Desktop Apps."
id="channel_notifications.sound_info"
/>
</div>
</fieldset>
</React.Fragment>
</div>,
]
}
saving={false}
section=""
serverError=""
submit={[MockFunction]}
title={
<SectionTitle
isExpanded={true}
onClickResetButton={[MockFunction]}
section="desktop"
/>
}
updateSection={[MockFunction]}
/>
`;
exports[`components/channel_notifications_modal/ExpandView normal channels should match snapshot, DESKTOP on expanded view when mentions is selected 1`] = `
<SettingItemMax
containerStyle=""
infoPosition="bottom"
inputs={
Array [
<div>
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Send desktop notifications"
id="channel_notifications.sendDesktop"
/>
</legend>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelNotificationAllActivity"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="all"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="all"
section="desktop"
/>
</label>
</div>
<div
className="radio"
>
<label
className=""
>
<input
checked={true}
id="channelNotificationMentions"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="mention"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="mention"
section="desktop"
/>
</label>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="channelNotificationNever"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="none"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="none"
section="desktop"
/>
</label>
</div>
</fieldset>
<div
className="mt-5"
>
<ExtraInfo
section="desktop"
/>
</div>
<React.Fragment>
<hr />
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Thread reply notifications"
id="user.settings.notifications.threads.desktop"
/>
</legend>
<div
className="checkbox"
>
<label>
<input
checked={true}
id="desktopThreadsNotificationAllActivity"
name="desktopThreadsNotificationLevel"
onChange={[MockFunction]}
type="checkbox"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Notify me about threads I'm following"
id="user.settings.notifications.threads.allActivity"
/>
</label>
<br />
</div>
<div
className="mt-5"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="When enabled, any reply to a thread you're following will send a desktop notification."
id="user.settings.notifications.threads"
/>
</div>
</fieldset>
</React.Fragment>
<React.Fragment>
<hr />
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Notification sound"
id="channel_notifications.sound"
/>
</legend>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelDesktopSoundOn"
name="channelDesktopSound"
type="radio"
value="on"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="On"
id="channel_notifications.sound.on.title"
/>
</label>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="channelDesktopSoundOff"
name="channelDesktopSound"
type="radio"
value="off"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="channel_notifications.sound.off.title"
/>
</label>
</div>
<div
className="mt-5"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Notification sounds are available on Firefox, Edge, Safari, Chrome and Mattermost Desktop Apps."
id="channel_notifications.sound_info"
/>
</div>
</fieldset>
</React.Fragment>
</div>,
]
}
saving={false}
section=""
serverError=""
submit={[MockFunction]}
title={
<SectionTitle
isExpanded={true}
onClickResetButton={[MockFunction]}
section="desktop"
/>
}
updateSection={[MockFunction]}
/>
`;
exports[`components/channel_notifications_modal/ExpandView normal channels should match snapshot, MARK_UNREAD on expanded view 1`] = `
<SettingItemMax
containerStyle=""
infoPosition="bottom"
inputs={
Array [
<div>
<fieldset>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelNotificationUnmute"
name="channelNotificationMute"
onChange={[MockFunction]}
type="radio"
value="mention"
/>
<Describe
memberNotifyLevel="mention"
section="markUnread"
/>
</label>
</div>
<div
className="radio"
>
<label
className=""
>
<input
checked={true}
id="channelNotificationMute"
name="channelNotificationMute"
onChange={[MockFunction]}
type="radio"
value="all"
/>
<Describe
memberNotifyLevel="all"
section="markUnread"
/>
</label>
</div>
</fieldset>
<div
className="mt-5"
>
<ExtraInfo
section="markUnread"
/>
</div>
</div>,
]
}
saving={false}
section=""
serverError=""
submit={[MockFunction]}
title={
<SectionTitle
isExpanded={true}
onClickResetButton={[MockFunction]}
section="markUnread"
/>
}
updateSection={[MockFunction]}
/>
`;
exports[`components/channel_notifications_modal/ExpandView normal channels should match snapshot, PUSH on expanded view 1`] = `
<SettingItemMax
containerStyle=""
infoPosition="bottom"
inputs={
Array [
<div>
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Send mobile push notifications"
id="channel_notifications.sendMobilePush"
/>
</legend>
<div
className="radio"
>
<label
className=""
>
<input
checked={true}
id="channelNotificationAllActivity"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="all"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="all"
section="push"
/>
</label>
</div>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelNotificationMentions"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="mention"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="mention"
section="push"
/>
</label>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="channelNotificationNever"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="none"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="none"
section="push"
/>
</label>
</div>
</fieldset>
<div
className="mt-5"
>
<ExtraInfo
section="push"
/>
</div>
</div>,
]
}
saving={false}
section=""
serverError=""
submit={[MockFunction]}
title={
<SectionTitle
isExpanded={true}
onClickResetButton={[MockFunction]}
section="push"
/>
}
updateSection={[MockFunction]}
/>
`;
exports[`components/channel_notifications_modal/ExpandView normal channels should match snapshot, PUSH on expanded view when mentions is selected 1`] = `
<SettingItemMax
containerStyle=""
infoPosition="bottom"
inputs={
Array [
<div>
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Send mobile push notifications"
id="channel_notifications.sendMobilePush"
/>
</legend>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelNotificationAllActivity"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="all"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="all"
section="push"
/>
</label>
</div>
<div
className="radio"
>
<label
className=""
>
<input
checked={true}
id="channelNotificationMentions"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="mention"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="mention"
section="push"
/>
</label>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="channelNotificationNever"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="none"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="none"
section="push"
/>
</label>
</div>
</fieldset>
<div
className="mt-5"
>
<ExtraInfo
section="push"
/>
</div>
<React.Fragment>
<hr />
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Thread reply notifications"
id="user.settings.notifications.threads.push"
/>
</legend>
<div
className="checkbox"
>
<label>
<input
checked={true}
id="pushThreadsNotificationAllActivity"
name="pushThreadsNotificationLevel"
onChange={[MockFunction]}
type="checkbox"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Notify me about threads I'm following"
id="user.settings.notifications.push_threads.allActivity"
/>
</label>
<br />
</div>
<div
className="mt-5"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="When enabled, any reply to a thread you're following will send a mobile push notification."
id="user.settings.notifications.push_threads"
/>
</div>
</fieldset>
</React.Fragment>
</div>,
]
}
saving={false}
section=""
serverError=""
submit={[MockFunction]}
title={
<SectionTitle
isExpanded={true}
onClickResetButton={[MockFunction]}
section="push"
/>
}
updateSection={[MockFunction]}
/>
`;

View File

@ -1,28 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/channel_notifications_modal/ExtraInfo should match snapshot, on DESKTOP 1`] = `
<span>
<MemoizedFormattedMessage
defaultMessage="Selecting an option other than the \\"default\\" will override the global notification settings. Desktop notifications are available on Firefox, Safari, and Chrome."
id="channel_notifications.override"
/>
</span>
`;
exports[`components/channel_notifications_modal/ExtraInfo should match snapshot, on MARK_UNREAD 1`] = `
<span>
<MemoizedFormattedMessage
defaultMessage="Muting turns off desktop, email and push notifications for this channel. The channel will not be marked as unread unless you are mentioned."
id="channel_notifications.muteChannel.help"
/>
</span>
`;
exports[`components/channel_notifications_modal/ExtraInfo should match snapshot, on PUSH 1`] = `
<span>
<MemoizedFormattedMessage
defaultMessage="Selecting an option other than the \\"default\\" will override the global notification settings for mobile push notifications."
id="channel_notifications.overridePush"
/>
</span>
`;

View File

@ -1,91 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/channel_notifications_modal/NotificationSection should match snapshot on server error 1`] = `
<CollapseView
globalNotifyLevel="default"
memberNotifyLevel="all"
onExpandSection={[Function]}
section="desktop"
/>
`;
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, DESKTOP on collapsed view 1`] = `
<CollapseView
globalNotifyLevel="default"
memberNotifyLevel="all"
onExpandSection={[Function]}
section="desktop"
/>
`;
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, DESKTOP on expanded view 1`] = `
<ExpandView
globalNotifyLevel="default"
isGM={false}
memberNotifyLevel="all"
memberThreadsNotifyLevel="all"
onChange={[Function]}
onChangeDesktopSound={[Function]}
onChangeNotificationSound={[Function]}
onChangeThreads={[Function]}
onCollapseSection={[Function]}
onReset={[Function]}
onSubmit={[Function]}
section="desktop"
serverError=""
/>
`;
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, MARK_UNREAD on collapsed view 1`] = `
<CollapseView
globalNotifyLevel={null}
memberNotifyLevel="all"
onExpandSection={[Function]}
section="markUnread"
/>
`;
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, MARK_UNREAD on expanded view 1`] = `
<ExpandView
globalNotifyLevel={null}
isGM={false}
memberNotifyLevel="all"
memberThreadsNotifyLevel="all"
onChange={[Function]}
onChangeDesktopSound={[Function]}
onChangeNotificationSound={[Function]}
onChangeThreads={[Function]}
onCollapseSection={[Function]}
onReset={[Function]}
onSubmit={[Function]}
section="markUnread"
serverError=""
/>
`;
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, PUSH on collapsed view 1`] = `
<CollapseView
globalNotifyLevel="default"
memberNotifyLevel="all"
onExpandSection={[Function]}
section="push"
/>
`;
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, PUSH on expanded view 1`] = `
<ExpandView
globalNotifyLevel="default"
isGM={false}
memberNotifyLevel="all"
memberThreadsNotifyLevel="all"
onChange={[Function]}
onChangeDesktopSound={[Function]}
onChangeNotificationSound={[Function]}
onChangeThreads={[Function]}
onCollapseSection={[Function]}
onReset={[Function]}
onSubmit={[Function]}
section="push"
serverError=""
/>
`;

View File

@ -1,30 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/channel_notifications_modal/ExtraInfo should match snapshot, on DESKTOP 1`] = `
<div
className="SectionTitle__wrapper"
>
<MemoizedFormattedMessage
defaultMessage="Desktop notifications"
id="channel_notifications.desktopNotifications"
/>
</div>
`;
exports[`components/channel_notifications_modal/ExtraInfo should match snapshot, on MARK_UNREAD 1`] = `
<MemoizedFormattedMessage
defaultMessage="Mute Channel"
id="channel_notifications.muteChannel.settings"
/>
`;
exports[`components/channel_notifications_modal/ExtraInfo should match snapshot, on PUSH 1`] = `
<div
className="SectionTitle__wrapper"
>
<MemoizedFormattedMessage
defaultMessage="Mobile push notifications"
id="channel_notifications.push"
/>
</div>
`;

View File

@ -1,44 +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 CollapseView from 'components/channel_notifications_modal/components/collapse_view';
import {NotificationLevels, NotificationSections} from 'utils/constants';
describe('components/channel_notifications_modal/CollapseView', () => {
const baseProps = {
section: NotificationSections.DESKTOP,
memberNotifyLevel: NotificationLevels.ALL,
globalNotifyLevel: NotificationLevels.DEFAULT,
onExpandSection: jest.fn(),
};
test('should match snapshot, DESKTOP on collapsed view', () => {
const wrapper = shallow(
<CollapseView {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, PUSH on collapsed view', () => {
const props = {...baseProps, section: NotificationSections.PUSH};
const wrapper = shallow(
<CollapseView {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, MARK_UNREAD on collapsed view', () => {
const props = {...baseProps, section: NotificationSections.MARK_UNREAD};
const wrapper = shallow(
<CollapseView {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,38 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import SettingItemMin from 'components/setting_item_min';
import Describe from './describe';
import SectionTitle from './section_title';
type Props = {
ignoreChannelMentions?: string;
channelAutoFollowThreads?: string;
onExpandSection: (section: string) => void;
globalNotifyLevel?: string;
memberNotifyLevel: string;
section: string;
}
export default function CollapseView({onExpandSection, globalNotifyLevel, memberNotifyLevel, section, ignoreChannelMentions, channelAutoFollowThreads}: Props) {
return (
<SettingItemMin
title={<SectionTitle section={section}/>}
describe={
<Describe
section={section}
ignoreChannelMentions={ignoreChannelMentions}
channelAutoFollowThreads={channelAutoFollowThreads}
memberNotifyLevel={memberNotifyLevel}
globalNotifyLevel={globalNotifyLevel}
isCollapsed={true}
/>
}
updateSection={onExpandSection}
section={section}
/>
);
}

View File

@ -1,61 +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 Describe from 'components/channel_notifications_modal/components/describe';
import {NotificationLevels, NotificationSections} from 'utils/constants';
describe('components/channel_notifications_modal/NotificationSection', () => {
const baseProps = {
section: NotificationSections.DESKTOP,
memberNotifyLevel: NotificationLevels.DEFAULT,
globalNotifyLevel: NotificationLevels.DEFAULT,
};
test('should match snapshot, on global DEFAULT', () => {
const wrapper = shallow(
<Describe {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on MENTION', () => {
const props = {...baseProps, memberNotifyLevel: NotificationLevels.MENTION};
const wrapper = shallow(
<Describe {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on DESKTOP/PUSH & ALL', () => {
const props = {...baseProps, memberNotifyLevel: NotificationLevels.ALL};
const wrapper = shallow(
<Describe {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on MARK_UNREAD & ALL', () => {
const props = {...baseProps, section: NotificationSections.MARK_UNREAD, memberNotifyLevel: NotificationLevels.ALL};
const wrapper = shallow(
<Describe {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on NONE', () => {
const props = {...baseProps, memberNotifyLevel: NotificationLevels.NONE};
const wrapper = shallow(
<Describe {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,139 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {ChannelAutoFollowThreads, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
import {t} from 'utils/i18n';
type Props = {
globalNotifyLevel?: string;
ignoreChannelMentions?: string;
channelAutoFollowThreads?: string;
memberNotifyLevel: string;
section: string;
isCollapsed?: boolean;
}
const defaultOption = (
<FormattedMessage
id='channel_notifications.defaultOption'
defaultMessage='(default)'
/>
);
export default function Describe({section, isCollapsed, memberNotifyLevel, globalNotifyLevel, ignoreChannelMentions, channelAutoFollowThreads}: Props) {
if (memberNotifyLevel === NotificationLevels.DEFAULT && globalNotifyLevel) {
t('channel_notifications.levels.default');
t('channel_notifications.levels.all');
t('channel_notifications.levels.mention');
t('channel_notifications.levels.none');
const levelsFormattedMessageId = 'channel_notifications.levels.' + globalNotifyLevel;
const notifyLevel = (
<FormattedMessage
id={levelsFormattedMessageId}
defaultMessage={globalNotifyLevel}
/>
);
return (
<FormattedMessage
id='channel_notifications.globalDefault'
defaultMessage='Global default ({notifyLevel})'
values={{notifyLevel}}
/>
);
} else if (memberNotifyLevel === NotificationLevels.MENTION && section === NotificationSections.MARK_UNREAD) {
if (isCollapsed) {
return (
<FormattedMessage
id='channel_notifications.muteChannel.on.title.collapse'
defaultMessage='Mute is enabled. Desktop, email and push notifications will not be sent for this channel.'
/>
);
}
return (
<FormattedMessage
id='channel_notifications.muteChannel.on.title'
defaultMessage='On'
/>
);
} else if (
section === NotificationSections.IGNORE_CHANNEL_MENTIONS &&
ignoreChannelMentions === IgnoreChannelMentions.ON
) {
return (
<FormattedMessage
id='channel_notifications.ignoreChannelMentions.on.title'
defaultMessage='On'
/>
);
} else if (
section === NotificationSections.IGNORE_CHANNEL_MENTIONS &&
ignoreChannelMentions === IgnoreChannelMentions.OFF
) {
return (
<FormattedMessage
id='channel_notifications.ignoreChannelMentions.off.title'
defaultMessage='Off'
/>
);
} else if (
section === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS &&
channelAutoFollowThreads === ChannelAutoFollowThreads.ON
) {
return (
<FormattedMessage
id='channel_notifications.channelAutoFollowThreads.on.title'
defaultMessage='On'
/>
);
} else if (
section === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS &&
channelAutoFollowThreads === ChannelAutoFollowThreads.OFF
) {
return (
<FormattedMessage
id='channel_notifications.channelAutoFollowThreads.off.title'
defaultMessage='Off'
/>
);
} else if (memberNotifyLevel === NotificationLevels.MENTION) {
return (
<FormattedMessage
id='channel_notifications.onlyMentions'
defaultMessage='Only for mentions {isDefault}'
values={{isDefault: globalNotifyLevel === NotificationLevels.MENTION ? defaultOption : <></>}}
/>
);
} else if (
(section === NotificationSections.DESKTOP || section === NotificationSections.PUSH) &&
memberNotifyLevel === NotificationLevels.ALL
) {
return (
<FormattedMessage
id='channel_notifications.allActivity'
defaultMessage='For all activity {isDefault}'
values={{isDefault: globalNotifyLevel === NotificationLevels.ALL ? defaultOption : <></>}}
/>
);
} else if (
section === NotificationSections.MARK_UNREAD &&
memberNotifyLevel === NotificationLevels.ALL
) {
return (
<FormattedMessage
id='channel_notifications.muteChannel.off.title'
defaultMessage='Off'
/>
);
}
return (
<FormattedMessage
id='channel_notifications.never'
defaultMessage='Never {isDefault}'
values={{isDefault: globalNotifyLevel === NotificationLevels.NONE ? defaultOption : <></>}}
/>
);
}

View File

@ -1,96 +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 ExpandView from 'components/channel_notifications_modal/components/expand_view';
import {NotificationLevels, NotificationSections} from 'utils/constants';
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(() => true),
}));
describe('components/channel_notifications_modal/ExpandView', () => {
const baseProps = {
section: NotificationSections.DESKTOP,
memberNotifyLevel: NotificationLevels.ALL,
memberThreadsNotifyLevel: NotificationLevels.ALL,
globalNotifyLevel: NotificationLevels.DEFAULT,
serverError: '',
onChange: jest.fn(),
onChangeThreads: jest.fn(),
onCollapseSection: jest.fn(),
onSubmit: jest.fn(),
onReset: jest.fn(),
isGM: false,
};
describe('normal channels', () => {
test('should match snapshot, DESKTOP on expanded view', () => {
const wrapper = shallow(
<ExpandView {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, PUSH on expanded view', () => {
const props = {...baseProps, section: NotificationSections.PUSH};
const wrapper = shallow(
<ExpandView {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, MARK_UNREAD on expanded view', () => {
const props = {...baseProps, section: NotificationSections.MARK_UNREAD};
const wrapper = shallow(
<ExpandView {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, DESKTOP on expanded view when mentions is selected', () => {
const props = {...baseProps, memberNotifyLevel: NotificationLevels.MENTION};
const wrapper = shallow(
<ExpandView {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, PUSH on expanded view when mentions is selected', () => {
const props = {...baseProps, section: NotificationSections.PUSH, memberNotifyLevel: NotificationLevels.MENTION};
const wrapper = shallow(
<ExpandView {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
});
describe('gms', () => {
test('should match snapshot, DESKTOP on expanded view when mentions is selected', () => {
const props = {...baseProps, isGM: true, memberNotifyLevel: NotificationLevels.MENTION};
const wrapper = shallow(
<ExpandView {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, PUSH on expanded view when mentions is selected', () => {
const props = {...baseProps, section: NotificationSections.PUSH, isGM: true, memberNotifyLevel: NotificationLevels.MENTION};
const wrapper = shallow(
<ExpandView {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@ -1,437 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo, useRef} from 'react';
import type {ChangeEvent} from 'react';
import {FormattedMessage} from 'react-intl';
import {useSelector} from 'react-redux';
import ReactSelect from 'react-select';
import type {ValueType} from 'react-select';
import type {ChannelNotifyProps} from '@mattermost/types/channels';
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import SettingItemMax from 'components/setting_item_max';
import {ChannelAutoFollowThreads, DesktopSound, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
import {notificationSounds} from 'utils/notification_sounds';
import Describe from './describe';
import ExtraInfo from './extra_info';
import SectionTitle from './section_title';
type SelectedOption = {
label: string;
value: string;
};
type Props = {
ignoreChannelMentions?: string;
channelAutoFollowThreads?: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
onChangeThreads?: (e: ChangeEvent<HTMLInputElement>) => void;
onChangeDesktopSound?: (e: ChangeEvent<HTMLInputElement>) => void;
onChangeNotificationSound?: (selectedOption: ValueType<SelectedOption>) => void;
onCollapseSection: (section: string) => void;
onReset: () => void;
onSubmit: (setting?: string) => void;
isNotificationsSettingSameAsGlobal?: boolean;
globalNotifyLevel?: string;
globalNotificationSound?: ChannelNotifyProps['desktop_notification_sound'];
memberNotifyLevel: string;
memberThreadsNotifyLevel?: string;
memberDesktopSound?: string;
memberDesktopNotificationSound?: string;
section: string;
serverError?: string;
isGM: boolean;
}
const sounds = Array.from(notificationSounds.keys());
const makeDefaultOptionLabel = (option: string) => `${option} (default)`;
const makeReactSelectValue = (option: string, isDefault: boolean) => {
return {value: option, label: isDefault ? makeDefaultOptionLabel(option) : option};
};
export default function ExpandView({
section,
memberNotifyLevel,
memberThreadsNotifyLevel,
memberDesktopSound,
memberDesktopNotificationSound,
globalNotifyLevel,
globalNotificationSound,
isNotificationsSettingSameAsGlobal,
onChange,
onChangeThreads,
onChangeDesktopSound,
onChangeNotificationSound,
onReset,
onSubmit,
serverError,
onCollapseSection,
ignoreChannelMentions,
channelAutoFollowThreads,
isGM,
}: Props) {
const isCRTEnabled = useSelector(isCollapsedThreadsEnabled);
const soundOptions = useMemo(() => sounds.map((sound) => {
return {value: sound, label: sound === globalNotificationSound ? makeDefaultOptionLabel(sound) : sound};
}), [globalNotificationSound]);
const dropdownSoundRef = useRef<ReactSelect>(null);
const inputs = [(
<div key='channel-notification-level-radio'>
{(section === NotificationSections.DESKTOP || section === NotificationSections.PUSH) &&
<fieldset>
{ section === NotificationSections.DESKTOP && <legend className='form-legend'>
<FormattedMessage
id='channel_notifications.sendDesktop'
defaultMessage='Send desktop notifications'
/>
</legend>}
{ section === NotificationSections.PUSH && <legend className='form-legend'>
<FormattedMessage
id='channel_notifications.sendMobilePush'
defaultMessage='Send mobile push notifications'
/>
</legend>}
<div className='radio'>
<label className=''>
<input
id='channelNotificationAllActivity'
name='channelNotifications'
type='radio'
value={NotificationLevels.ALL}
checked={memberNotifyLevel === NotificationLevels.ALL}
onChange={onChange}
/>
<Describe
section={section}
memberNotifyLevel={NotificationLevels.ALL}
globalNotifyLevel={globalNotifyLevel}
/>
</label>
</div>
<div className='radio'>
<label className=''>
<input
id='channelNotificationMentions'
name='channelNotifications'
type='radio'
value={NotificationLevels.MENTION}
checked={memberNotifyLevel === NotificationLevels.MENTION}
onChange={onChange}
/>
<Describe
section={section}
memberNotifyLevel={NotificationLevels.MENTION}
globalNotifyLevel={globalNotifyLevel}
/>
</label>
</div>
<div className='radio'>
<label>
<input
id='channelNotificationNever'
name='channelNotifications'
type='radio'
value={NotificationLevels.NONE}
checked={memberNotifyLevel === NotificationLevels.NONE}
onChange={onChange}
/>
<Describe
section={section}
memberNotifyLevel={NotificationLevels.NONE}
globalNotifyLevel={globalNotifyLevel}
/>
</label>
</div>
</fieldset>
}
{section === NotificationSections.IGNORE_CHANNEL_MENTIONS &&
<fieldset>
<div className='radio'>
<label>
<input
id='ignoreChannelMentionsOn'
name='ignoreChannelMentions'
type='radio'
value={IgnoreChannelMentions.ON}
checked={ignoreChannelMentions === IgnoreChannelMentions.ON}
onChange={onChange}
/>
<Describe
section={section}
ignoreChannelMentions={IgnoreChannelMentions.ON}
memberNotifyLevel={memberNotifyLevel}
globalNotifyLevel={globalNotifyLevel}
/>
</label>
</div>
<div className='radio'>
<label>
<input
id='ignoreChannelMentionsOff'
name='ignoreChannelMentions'
type='radio'
value={IgnoreChannelMentions.OFF}
checked={ignoreChannelMentions === IgnoreChannelMentions.OFF}
onChange={onChange}
/>
<Describe
section={section}
ignoreChannelMentions={IgnoreChannelMentions.OFF}
memberNotifyLevel={memberNotifyLevel}
globalNotifyLevel={globalNotifyLevel}
/>
</label>
</div>
</fieldset>
}
{section === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS &&
<fieldset>
<div className='radio'>
<label>
<input
id='channelAutoFollowThreadsOn'
name='channelAutoFollowThreads'
type='radio'
value={ChannelAutoFollowThreads.ON}
checked={channelAutoFollowThreads === ChannelAutoFollowThreads.ON}
onChange={onChange}
/>
<Describe
section={section}
channelAutoFollowThreads={ChannelAutoFollowThreads.ON}
memberNotifyLevel={memberNotifyLevel}
globalNotifyLevel={globalNotifyLevel}
/>
</label>
</div>
<div className='radio'>
<label>
<input
id='channelAutoFollowThreadsOff'
name='channelAutoFollowThreads'
type='radio'
value={ChannelAutoFollowThreads.OFF}
checked={channelAutoFollowThreads === ChannelAutoFollowThreads.OFF}
onChange={onChange}
/>
<Describe
section={section}
channelAutoFollowThreads={ChannelAutoFollowThreads.OFF}
memberNotifyLevel={memberNotifyLevel}
globalNotifyLevel={globalNotifyLevel}
/>
</label>
</div>
</fieldset>
}
{section === NotificationSections.MARK_UNREAD &&
<fieldset>
<div className='radio'>
<label className=''>
<input
id='channelNotificationUnmute'
name='channelNotificationMute'
type='radio'
value={NotificationLevels.MENTION}
checked={memberNotifyLevel === NotificationLevels.MENTION}
onChange={onChange}
/>
<Describe
section={section}
memberNotifyLevel={NotificationLevels.MENTION}
/>
</label>
</div>
<div className='radio'>
<label className=''>
<input
id='channelNotificationMute'
name='channelNotificationMute'
type='radio'
value={NotificationLevels.ALL}
checked={memberNotifyLevel === NotificationLevels.ALL}
onChange={onChange}
/>
<Describe
section={section}
memberNotifyLevel={NotificationLevels.ALL}
/>
</label>
</div>
</fieldset>
}
<div className='mt-5'>
<ExtraInfo section={section}/>
</div>
{isCRTEnabled &&
section === NotificationSections.DESKTOP &&
memberNotifyLevel === NotificationLevels.MENTION &&
!isGM &&
<>
<hr/>
<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={memberThreadsNotifyLevel === NotificationLevels.ALL}
onChange={onChangeThreads}
/>
<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>
</>
}
{(section === NotificationSections.DESKTOP) && memberNotifyLevel !== NotificationLevels.NONE &&
<>
<hr/>
<fieldset>
<legend className='form-legend'>
<FormattedMessage
id='channel_notifications.sound'
defaultMessage='Notification sound'
/>
</legend>
<div className='radio'>
<label className=''>
<input
id='channelDesktopSoundOn'
name='channelDesktopSound'
type='radio'
value={DesktopSound.ON}
checked={memberDesktopSound === DesktopSound.ON}
onChange={onChangeDesktopSound}
/>
<FormattedMessage
id='channel_notifications.sound.on.title'
defaultMessage='On'
/>
</label>
</div>
<div className='radio'>
<label>
<input
id='channelDesktopSoundOff'
name='channelDesktopSound'
type='radio'
value={DesktopSound.OFF}
checked={memberDesktopSound === DesktopSound.OFF}
onChange={onChangeDesktopSound}
/>
<FormattedMessage
id='channel_notifications.sound.off.title'
defaultMessage='Off'
/>
</label>
</div>
{memberDesktopSound === DesktopSound.ON &&
<div className='pt-2'>
<ReactSelect
className='react-select notification-sound-dropdown'
classNamePrefix='react-select'
id='channelSoundNotification'
options={soundOptions}
clearable={false}
onChange={onChangeNotificationSound}
value={makeReactSelectValue(memberDesktopNotificationSound ?? '', memberDesktopNotificationSound === globalNotificationSound)}
isSearchable={false}
ref={dropdownSoundRef}
/>
</div>}
<div className='mt-5'>
<FormattedMessage
id='channel_notifications.sound_info'
defaultMessage='Notification sounds are available on Firefox, Edge, Safari, Chrome and Mattermost Desktop Apps.'
/>
</div>
</fieldset>
</>
}
{isCRTEnabled &&
section === NotificationSections.PUSH &&
memberNotifyLevel === NotificationLevels.MENTION &&
!isGM &&
<>
<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={memberThreadsNotifyLevel === NotificationLevels.ALL}
onChange={onChangeThreads}
/>
<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>
</>
}
</div>
)];
return (
<SettingItemMax
title={
<SectionTitle
section={section}
isExpanded={true}
isNotificationsSettingSameAsGlobal={isNotificationsSettingSameAsGlobal}
onClickResetButton={onReset}
/>}
inputs={inputs}
submit={onSubmit}
serverError={serverError}
updateSection={onCollapseSection}
/>
);
}

View File

@ -1,41 +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 ExtraInfo from 'components/channel_notifications_modal/components/extra_info';
import {NotificationSections} from 'utils/constants';
describe('components/channel_notifications_modal/ExtraInfo', () => {
const baseProps = {
section: NotificationSections.DESKTOP,
};
test('should match snapshot, on DESKTOP', () => {
const wrapper = shallow(
<ExtraInfo {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on PUSH', () => {
const props = {...baseProps, section: NotificationSections.PUSH};
const wrapper = shallow(
<ExtraInfo {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on MARK_UNREAD', () => {
const props = {...baseProps, section: NotificationSections.MARK_UNREAD};
const wrapper = shallow(
<ExtraInfo {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,64 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {NotificationSections} from 'utils/constants';
type Props = {
section: string;
}
export default function ExtraInfo({section}: Props) {
switch (section) {
case NotificationSections.DESKTOP:
return (
<span>
<FormattedMessage
id='channel_notifications.override'
defaultMessage='Selecting an option other than the "default" will override the global notification settings. Desktop notifications are available on Firefox, Safari, and Chrome.'
/>
</span>
);
case NotificationSections.PUSH:
return (
<span>
<FormattedMessage
id='channel_notifications.overridePush'
defaultMessage='Selecting an option other than the "default" will override the global notification settings for mobile push notifications.'
/>
</span>
);
case NotificationSections.MARK_UNREAD:
return (
<span>
<FormattedMessage
id='channel_notifications.muteChannel.help'
defaultMessage='Muting turns off desktop, email and push notifications for this channel. The channel will not be marked as unread unless you are mentioned.'
/>
</span>
);
case NotificationSections.IGNORE_CHANNEL_MENTIONS:
return (
<span>
<FormattedMessage
id='channel_notifications.ignoreChannelMentions.help'
defaultMessage='When enabled, @channel, @here and @all will not trigger mentions or mention notifications in this channel.'
/>
</span>
);
case NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS:
return (
<span>
<FormattedMessage
id='channel_notifications.channelAutoFollowThreads.help'
defaultMessage='When enabled, you will auto-follow all new threads created in this channel unless you unfollow a thread explicitly.'
/>
</span>
);
default:
return null;
}
}

View File

@ -1,182 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {NotificationSections, NotificationLevels} from 'utils/constants';
import CollapseView from './collapse_view';
import ExpandView from './expand_view';
export default class NotificationSection extends React.PureComponent {
static propTypes = {
/**
* Notification section
*/
section: PropTypes.string.isRequired,
/**
* Expand if true, else collapse the section
*/
expand: PropTypes.bool.isRequired,
/**
* Member's desktop notification level
*/
memberNotificationLevel: PropTypes.string.isRequired,
memberDesktopSound: PropTypes.string,
memberDesktopNotificationSound: PropTypes.string,
/**
* Member's desktop_threads notification level
*/
memberThreadsNotificationLevel: PropTypes.string,
/**
* Ignore channel-wide mentions @channel, @here and @all
*/
ignoreChannelMentions: PropTypes.string,
/**
* Auto-follow all new threads in this channel
*/
channelAutoFollowThreads: PropTypes.string,
/**
* User's global notification level
*/
globalNotificationLevel: PropTypes.string,
/**
* User's global notification sound
*/
globalNotificationSound: PropTypes.string,
/**
* onChange handles update of desktop notification level
*/
onChange: PropTypes.func.isRequired,
/**
* onChangeThreads handles update of desktop_threads notification level
*/
onChangeThreads: PropTypes.func,
onChangeDesktopSound: PropTypes.func,
onChangeNotificationSound: PropTypes.func,
onReset: PropTypes.func,
isNotificationsSettingSameAsGlobal: PropTypes.bool,
/**
* Submit function to save notification level
*/
onSubmit: PropTypes.func.isRequired,
/**
* Update function to to expand or collapse a section
*/
onUpdateSection: PropTypes.func.isRequired,
/**
* Error string from the server
*/
serverError: PropTypes.string,
/**
* Whether the preferences are those of a GM
*/
isGM: PropTypes.bool,
};
handleOnChange = (e) => {
this.props.onChange(e.target.value);
};
handleOnChangeThreads = (e) => {
const value = e.target.checked ? NotificationLevels.ALL : NotificationLevels.MENTION;
this.props.onChangeThreads(value);
};
handleOnChangeDesktopSound = (e) => {
this.props.onChangeDesktopSound(e.target.value);
};
handleOnChangeNotificationSound = (selectedOption) => {
if (selectedOption && 'value' in selectedOption) {
this.props.onChangeNotificationSound(selectedOption.value);
}
};
handleExpandSection = () => {
this.props.onUpdateSection(this.props.section);
};
handleCollapseSection = () => {
this.props.onUpdateSection(NotificationSections.NONE);
};
render() {
const {
expand,
globalNotificationLevel,
globalNotificationSound,
memberNotificationLevel,
memberThreadsNotificationLevel,
memberDesktopSound,
memberDesktopNotificationSound,
ignoreChannelMentions,
isNotificationsSettingSameAsGlobal,
channelAutoFollowThreads,
onSubmit,
onReset,
section,
serverError,
isGM,
} = this.props;
if (expand) {
return (
<ExpandView
section={section}
memberNotifyLevel={memberNotificationLevel}
memberThreadsNotifyLevel={memberThreadsNotificationLevel}
memberDesktopSound={memberDesktopSound}
memberDesktopNotificationSound={memberDesktopNotificationSound}
globalNotifyLevel={globalNotificationLevel}
globalNotificationSound={globalNotificationSound}
ignoreChannelMentions={ignoreChannelMentions}
isNotificationsSettingSameAsGlobal={isNotificationsSettingSameAsGlobal}
channelAutoFollowThreads={channelAutoFollowThreads}
onChange={this.handleOnChange}
onReset={onReset}
onChangeThreads={this.handleOnChangeThreads}
onChangeDesktopSound={this.handleOnChangeDesktopSound}
onChangeNotificationSound={this.handleOnChangeNotificationSound}
onSubmit={onSubmit}
serverError={serverError}
onCollapseSection={this.handleCollapseSection}
isGM={isGM}
/>
);
}
return (
<CollapseView
section={section}
onExpandSection={this.handleExpandSection}
memberNotifyLevel={memberNotificationLevel}
globalNotifyLevel={globalNotificationLevel}
ignoreChannelMentions={ignoreChannelMentions}
channelAutoFollowThreads={channelAutoFollowThreads}
/>
);
}
}

View File

@ -1,121 +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 NotificationSection from 'components/channel_notifications_modal/components/notification_section.jsx';
import {NotificationLevels, NotificationSections} from 'utils/constants';
describe('components/channel_notifications_modal/NotificationSection', () => {
const baseProps = {
section: NotificationSections.DESKTOP,
expand: false,
memberNotificationLevel: NotificationLevels.ALL,
memberThreadsNotificationLevel: NotificationLevels.ALL,
globalNotificationLevel: NotificationLevels.DEFAULT,
onChange: () => {}, //eslint-disable-line no-empty-function
onChangeThreads: () => {}, //eslint-disable-line no-empty-function
onReset: () => {},
onSubmit: () => {}, //eslint-disable-line no-empty-function
onUpdateSection: () => {}, //eslint-disable-line no-empty-function
serverError: '',
isGM: false,
};
test('should match snapshot, DESKTOP on collapsed view', () => {
const wrapper = shallow(
<NotificationSection {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, DESKTOP on expanded view', () => {
const props = {...baseProps, expand: true};
const wrapper = shallow(
<NotificationSection {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, PUSH on collapsed view', () => {
const props = {...baseProps, section: NotificationSections.PUSH};
const wrapper = shallow(
<NotificationSection {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, PUSH on expanded view', () => {
const props = {...baseProps, section: NotificationSections.PUSH, expand: true};
const wrapper = shallow(
<NotificationSection {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, MARK_UNREAD on collapsed view', () => {
const props = {...baseProps, section: NotificationSections.MARK_UNREAD, globalNotificationLevel: null};
const wrapper = shallow(
<NotificationSection {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, MARK_UNREAD on expanded view', () => {
const props = {...baseProps, section: NotificationSections.MARK_UNREAD, expand: true, globalNotificationLevel: null};
const wrapper = shallow(
<NotificationSection {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should have called onChange when handleOnChange is called', () => {
const onChange = jest.fn();
const props = {...baseProps, expand: true, onChange};
const wrapper = shallow(
<NotificationSection {...props}/>,
);
wrapper.instance().handleOnChange({target: {value: NotificationLevels.ALL}});
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(NotificationLevels.ALL);
});
test('should have called onUpdateSection when handleExpandSection is called', () => {
const onUpdateSection = jest.fn();
const props = {...baseProps, expand: true, onUpdateSection};
const wrapper = shallow(
<NotificationSection {...props}/>,
);
wrapper.instance().handleExpandSection({preventDefault: jest.fn()});
expect(onUpdateSection).toHaveBeenCalledTimes(1);
expect(onUpdateSection).toHaveBeenCalledWith(NotificationSections.DESKTOP);
});
test('should have called onUpdateSection when handleCollapseSection is called', () => {
const onUpdateSection = jest.fn();
const props = {...baseProps, expand: true, onUpdateSection};
const wrapper = shallow(
<NotificationSection {...props}/>,
);
wrapper.instance().handleCollapseSection({preventDefault: jest.fn()});
expect(onUpdateSection).toHaveBeenCalledTimes(1);
expect(onUpdateSection).toHaveBeenCalledWith(NotificationSections.NONE);
});
test('should match snapshot on server error', () => {
const props = {...baseProps, serverError: 'server error occurred'};
const wrapper = shallow(
<NotificationSection {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,15 +0,0 @@
@import 'sass/utils/mixins';
.SectionTitle {
&__wrapper {
display: flex;
justify-content: space-between;
}
&__resetButton {
@include button-style--none;
display: flex;
align-items: center;
}
}

View File

@ -1,41 +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 SectionTitle from 'components/channel_notifications_modal/components/section_title';
import {NotificationSections} from 'utils/constants';
describe('components/channel_notifications_modal/ExtraInfo', () => {
const baseProps = {
section: NotificationSections.DESKTOP,
};
test('should match snapshot, on DESKTOP', () => {
const wrapper = shallow(
<SectionTitle {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on PUSH', () => {
const props = {...baseProps, section: NotificationSections.PUSH};
const wrapper = shallow(
<SectionTitle {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on MARK_UNREAD', () => {
const props = {...baseProps, section: NotificationSections.MARK_UNREAD};
const wrapper = shallow(
<SectionTitle {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,71 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {NotificationSections} from 'utils/constants';
import './section_title.scss';
type Props = {
section: string;
isExpanded?: boolean;
isNotificationsSettingSameAsGlobal?: boolean;
onClickResetButton?: () => void;
}
export default function SectionTitle({section, isExpanded, isNotificationsSettingSameAsGlobal, onClickResetButton}: Props) {
if (section === NotificationSections.DESKTOP || section === NotificationSections.PUSH) {
return (
<div className='SectionTitle__wrapper'>
{section === NotificationSections.DESKTOP &&
<FormattedMessage
id='channel_notifications.desktopNotifications'
defaultMessage='Desktop notifications'
/>}
{section === NotificationSections.PUSH &&
<FormattedMessage
id='channel_notifications.push'
defaultMessage='Mobile push notifications'
/>}
{isExpanded && !isNotificationsSettingSameAsGlobal &&
<button
className='SectionTitle__resetButton color--link'
onClick={onClickResetButton}
>
<i className='icon icon-refresh'/>
<FormattedMessage
id='channel_notifications.resetToDefaults'
defaultMessage='Reset to defaults'
/>
</button>
}
</div>
);
} else if (section === NotificationSections.MARK_UNREAD) {
return (
<FormattedMessage
id='channel_notifications.muteChannel.settings'
defaultMessage='Mute Channel'
/>
);
} else if (section === NotificationSections.IGNORE_CHANNEL_MENTIONS) {
return (
<FormattedMessage
id='channel_notifications.ignoreChannelMentions'
defaultMessage='Ignore mentions for @channel, @here and @all'
/>
);
} else if (section === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS) {
return (
<FormattedMessage
id='channel_notifications.channelAutoFollowThreads'
defaultMessage='Auto-follow all new threads in this channel'
/>
);
}
return null;
}

View File

@ -11,6 +11,9 @@ import type {ChannelNotifyProps} from '@mattermost/types/channels';
import {updateChannelNotifyProps} from 'mattermost-redux/actions/channels';
import {getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {
isCollapsedThreadsEnabled,
} from 'mattermost-redux/selectors/entities/preferences';
import type {ActionResult} from 'mattermost-redux/types/actions';
import type {GlobalState} from 'types/store/index';
@ -18,6 +21,7 @@ import type {GlobalState} from 'types/store/index';
import ChannelNotificationsModal from './channel_notifications_modal';
const mapStateToProps = (state: GlobalState) => ({
collapsedReplyThreads: isCollapsedThreadsEnabled(state),
channelMember: getMyCurrentChannelMembership(state),
sendPushNotifications: getConfig(state).SendPushNotifications === 'true',
});

View File

@ -0,0 +1,276 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage, defineMessages} from 'react-intl';
import type {ChannelNotifyProps} from '@mattermost/types/channels';
import type {UserNotifyProps} from '@mattermost/types/users';
import type {FieldsetCheckbox} from 'components/widgets/modals/components/checkbox_setting_item';
import type {FieldsetRadio} from 'components/widgets/modals/components/radio_setting_item';
import {NotificationLevels} from 'utils/constants';
export type ChannelMemberNotifyProps = Partial<ChannelNotifyProps> & Pick<UserNotifyProps, 'desktop_threads' | 'push_threads'>
const translations = defineMessages({
MuteAndIgnoreSectionTitle: {
id: 'channel_notifications.muteAndIgnore',
defaultMessage: 'Mute or ignore',
},
NotifyMeTitle: {
id: 'channel_notifications.NotifyMeTitle',
defaultMessage: 'Notify me about…',
},
ThreadsReplyTitle: {
id: 'channel_notifications.ThreadsReplyTitle',
defaultMessage: 'Thread reply notifications',
},
DesktopNotificationsSectionTitle: {
id: 'channel_notifications.desktopNotificationsTitle',
defaultMessage: 'Desktop Notifications',
},
DesktopNotificationsSectionDesc: {
id: 'channel_notifications.desktopNotificationsDesc',
defaultMessage: 'Available on Chrome, Edge, Firefox, and the Mattermost Desktop App.',
},
MobileNotificationsSectionTitle: {
id: 'channel_notifications.mobileNotificationsTitle',
defaultMessage: 'Mobile Notifications',
},
MobileNotificationsSectionDesc: {
id: 'channel_notifications.mobileNotificationsDesc',
defaultMessage: 'Notification alerts are pushed to your mobile device when there is activity in Mattermost.',
},
MuteChannelDesc: {
id: 'channel_notifications.muteChannelDesc',
defaultMessage: 'Turns off notifications for this channel. Youll still see badges if youre mentioned.',
},
IgnoreMentionsDesc: {
id: 'channel_notifications.ignoreMentionsDesc',
defaultMessage: 'When enabled, @channel, @here and @all will not trigger mentions or mention notifications in this channel',
},
MuteChannelInputFieldTitle: {
id: 'channel_notifications.muteChannelTitle',
defaultMessage: 'Mute channel',
},
DesktopReplyThreadsInputFieldTitle: {
id: 'channel_notifications.checkbox.threadsReplyTitle',
defaultMessage: 'Notify me about replies to threads I\'m following',
},
MobileReplyThreadsInputFieldTitle: {
id: 'channel_notifications.checkbox.threadsReplyTitle',
defaultMessage: 'Notify me about replies to threads I\'m following',
},
sameMobileSettingsDesktopInputFieldTitle: {
id: 'channel_notifications.checkbox.sameMobileSettingsDesktop',
defaultMessage: 'Use the same notification settings as desktop',
},
IgnoreMentionsInputFieldTitle: {
id: 'channel_notifications.ignoreMentionsTitle',
defaultMessage: 'Ignore mentions for @channel, @here and @all',
},
AutoFollowThreadsTitle: {
id: 'channel_notifications.autoFollowThreadsTitle',
defaultMessage: 'Follow all threads in this channel',
},
AutoFollowThreadsDesc: {
id: 'channel_notifications.autoFollowThreadsDesc',
defaultMessage: 'When enabled, all new replies in this channel will be automatically followed and will appear in your Threads view.',
},
AutoFollowThreadsInputFieldTitle: {
id: 'channel_notifications.checkbox.autoFollowThreadsTitle',
defaultMessage: 'Automatically follow threads in this channel',
},
});
const desktopNotificationInputFieldOptions = defineMessages({
allNewMessages: {
id: 'channel_notifications.desktopNotificationAllLabel',
defaultMessage: 'All new messages',
},
mentions: {
id: 'channel_notifications.desktopNotificationMentionLabel',
defaultMessage: 'Mentions, direct messages, and keywords only',
},
nothing: {
id: 'channel_notifications.desktopNotificationNothingLabel',
defaultMessage: 'Nothing',
},
});
const mobileNotificationInputFieldOptions = defineMessages({
allNewMessages: {
id: 'channel_notifications.MobileNotificationAllLabel',
defaultMessage: 'All new messages',
},
mentions: {
id: 'channel_notifications.MobileNotificationMentionLabel',
defaultMessage: 'Mentions, direct messages, and keywords only',
},
nothing: {
id: 'channel_notifications.MobileNotificationNothingLabel',
defaultMessage: 'Nothing',
},
});
const MuteChannelInputFieldData: FieldsetCheckbox = {
title: translations.MuteChannelInputFieldTitle,
name: 'mute channel',
dataTestId: 'muteChannel',
};
const DesktopReplyThreadsInputFieldData: FieldsetCheckbox = {
title: translations.DesktopReplyThreadsInputFieldTitle,
name: 'desktop reply threads',
dataTestId: 'desktopReplyThreads',
};
const MobileReplyThreadsInputFieldData: FieldsetCheckbox = {
title: translations.MobileReplyThreadsInputFieldTitle,
name: 'mobile reply threads',
dataTestId: 'mobileReplyThreads',
};
const sameMobileSettingsDesktopInputFieldData: FieldsetCheckbox = {
title: translations.sameMobileSettingsDesktopInputFieldTitle,
name: 'same mobile settings as Desktop',
dataTestId: 'sameMobileSettingsDesktop',
};
const IgnoreMentionsInputFieldData: FieldsetCheckbox = {
title: translations.IgnoreMentionsInputFieldTitle,
name: 'ignore mentions',
dataTestId: 'ignoreMentions',
};
const AutoFollowThreadsInputFieldData: FieldsetCheckbox = {
title: translations.AutoFollowThreadsInputFieldTitle,
name: 'auto follow threads',
dataTestId: 'autoFollowThreads',
};
const desktopNotificationInputFieldData = (defaultOption: string): FieldsetRadio => {
return {
options: [
{
dataTestId: `desktopNotification-${NotificationLevels.ALL}`,
title: desktopNotificationInputFieldOptions.allNewMessages,
name: `desktopNotification-${NotificationLevels.ALL}`,
key: `desktopNotification-${NotificationLevels.ALL}`,
value: NotificationLevels.ALL,
suffix: defaultOption === NotificationLevels.ALL ? (
<FormattedMessage
id='channel_notifications.default'
defaultMessage='(default)'
/>) : undefined,
},
{
dataTestId: `desktopNotification-${NotificationLevels.MENTION}`,
title: desktopNotificationInputFieldOptions.mentions,
name: `desktopNotification-${NotificationLevels.MENTION}`,
key: `desktopNotification-${NotificationLevels.MENTION}`,
value: NotificationLevels.MENTION,
suffix: defaultOption === NotificationLevels.MENTION ? (
<FormattedMessage
id='channel_notifications.default'
defaultMessage='(default)'
/>) : undefined,
},
{
dataTestId: `desktopNotification-${NotificationLevels.NONE}`,
title: desktopNotificationInputFieldOptions.nothing,
name: `desktopNotification-${NotificationLevels.NONE}`,
key: `desktopNotification-${NotificationLevels.NONE}`,
value: NotificationLevels.NONE,
suffix: defaultOption === NotificationLevels.NONE ? (
<FormattedMessage
id='channel_notifications.default'
defaultMessage='(default)'
/>) : undefined,
},
],
};
};
const mobileNotificationInputFieldData = (defaultOption: string): FieldsetRadio => {
return {
options: [
{
dataTestId: `MobileNotification-${NotificationLevels.ALL}`,
title: mobileNotificationInputFieldOptions.allNewMessages,
name: `MobileNotification-${NotificationLevels.ALL}`,
key: `MobileNotification-${NotificationLevels.ALL}`,
value: NotificationLevels.ALL,
suffix: defaultOption === NotificationLevels.ALL ? (
<FormattedMessage
id='channel_notifications.default'
defaultMessage='(default)'
/>) : undefined,
},
{
dataTestId: `MobileNotification-${NotificationLevels.MENTION}`,
title: mobileNotificationInputFieldOptions.mentions,
name: `MobileNotification-${NotificationLevels.MENTION}`,
key: `MobileNotification-${NotificationLevels.MENTION}`,
value: NotificationLevels.MENTION,
suffix: defaultOption === NotificationLevels.MENTION ? (
<FormattedMessage
id='channel_notifications.default'
defaultMessage='(default)'
/>) : undefined,
},
{
dataTestId: `MobileNotification-${NotificationLevels.NONE}`,
title: mobileNotificationInputFieldOptions.nothing,
name: `MobileNotification-${NotificationLevels.NONE}`,
key: `MobileNotification-${NotificationLevels.NONE}`,
value: NotificationLevels.NONE,
suffix: defaultOption === NotificationLevels.NONE ? (
<FormattedMessage
id='channel_notifications.default'
defaultMessage='(default)'
/>) : undefined,
},
],
};
};
const utils = {
desktopNotificationInputFieldData,
DesktopNotificationsSectionDesc: translations.DesktopNotificationsSectionDesc,
DesktopNotificationsSectionTitle: translations.DesktopNotificationsSectionTitle,
IgnoreMentionsDesc: translations.IgnoreMentionsDesc,
IgnoreMentionsInputFieldData,
mobileNotificationInputFieldData,
MobileNotificationsSectionDesc: translations.MobileNotificationsSectionDesc,
MobileNotificationsSectionTitle: translations.MobileNotificationsSectionTitle,
MuteAndIgnoreSectionTitle: translations.MuteAndIgnoreSectionTitle,
MuteChannelDesc: translations.MuteChannelDesc,
MuteChannelInputFieldData,
NotifyMeTitle: translations.NotifyMeTitle,
DesktopReplyThreadsInputFieldData,
ThreadsReplyTitle: translations.ThreadsReplyTitle,
MobileReplyThreadsInputFieldData,
AutoFollowThreadsTitle: translations.AutoFollowThreadsTitle,
AutoFollowThreadsDesc: translations.AutoFollowThreadsDesc,
AutoFollowThreadsInputFieldData,
sameMobileSettingsDesktopInputFieldData,
};
export default utils;

View File

@ -0,0 +1,152 @@
.mm-modal-generic-section-item {
display: flex;
flex-direction: column;
gap: 8px;
&__title {
display: flex;
align-items: center;
padding: 0;
margin: 0 0 8px;
color: var(--center-channel-color);
font-family: 'Open Sans', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px;
}
&__description {
display: flex;
align-items: center;
padding: 0;
margin: 0;
color: rgba(var(--center-channel-color-rgb), 0.64);
font-family: 'Open Sans', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
&__content {
display: flex;
flex-direction: column;
}
&__fieldset-checkbox-ctr {
display: flex;
flex-direction: row;
}
&__fieldset-checkbox {
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
font-size: 14px;
font-weight: 400;
gap: 8px;
line-height: 20px;
}
&__fieldset-radio {
display: flex;
flex-direction: column;
gap: 12px;
}
&__label-radio {
display: flex;
width: fit-content;
flex-direction: row;
align-items: center;
cursor: pointer;
font-size: 14px;
font-weight: 400;
gap: 8px;
line-height: 20px;
}
// Todo: add styling for react select
//&__fieldset-react-select{
//}
&__input-checkbox {
width: 1.6rem;
height: 1.6rem;
}
input[type=checkbox] {
display: grid;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.24);
margin: 0;
-webkit-appearance: none;
appearance: none;
background-color: transparent;
border-radius: 3px;
color: rgba(var(--center-channel-color-rgb), 0.24);
cursor: pointer;
font: inherit;
place-content: center;
-webkit-transition: background-color 200ms ease-out;
-moz-transition: background-color 200ms ease-out;
-o-transition: background-color 200ms ease-out;
transition: background-color 200ms ease-out;
}
input[type="checkbox"]::before {
width: 10px;
height: 10px;
background: var(--button-color);
clip-path: polygon(0% 57%, 32% 88%, 100% 20%, 88% 8%, 32% 64%, 12% 45%);
content: '';
transform: scale(0);
transform-origin: center center;
transition: 200ms transform ease-in-out;
}
input[type="checkbox"]:checked::before {
transform: scale(1);
}
input[type="checkbox"]:checked {
border-color: var(--denim-button-bg);
background: var(--denim-button-bg);
}
input[type=radio] {
display: grid;
width: 1.6rem;
height: 1.6rem;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.24);
margin: 0;
-webkit-appearance: none;
appearance: none;
background-color: transparent;
border-radius: 50%;
color: rgba(var(--center-channel-color-rgb), 0.24);
cursor: pointer;
font: inherit;
place-content: center;
}
input[type="radio"]::before {
width: 8px;
height: 8px;
background: var(--denim-button-bg);
border-radius: 50%;
content: '';
transform: scale(0);
transform-origin: center center;
transition: 200ms transform ease-in-out;
}
input[type="radio"]:checked::before {
transform: scale(1);
}
input[type="radio"]:checked {
border-color: var(--denim-button-bg);
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {MessageDescriptor} from 'react-intl';
import {useIntl} from 'react-intl';
import './base_setting_item.scss';
export type BaseSettingItemProps = {
title?: MessageDescriptor;
description?: MessageDescriptor;
};
type Props = BaseSettingItemProps & {
content: JSX.Element;
}
function BaseSettingItem({title, description, content}: Props): JSX.Element {
const {formatMessage} = useIntl();
const Title = title && (
<h4 className='mm-modal-generic-section-item__title'>
{formatMessage({id: title.id, defaultMessage: title.defaultMessage})}
</h4>
);
const Description = description && (
<p className='mm-modal-generic-section-item__description'>
{formatMessage({id: description.id, defaultMessage: description.defaultMessage})}
</p>
);
return (
<div className='mm-modal-generic-section-item'>
{Title}
<div className='mm-modal-generic-section-item__content'>
{content}
</div>
{Description}
</div>
);
}
export default BaseSettingItem;

View File

@ -0,0 +1,60 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import type {MessageDescriptor} from 'react-intl';
import type {BaseSettingItemProps} from './base_setting_item';
import BaseSettingItem from './base_setting_item';
export type FieldsetCheckbox = {
dataTestId?: string;
title: MessageDescriptor;
name: string;
}
type Props = BaseSettingItemProps & {
inputFieldData: FieldsetCheckbox;
inputFieldValue: boolean;
handleChange: (e: boolean) => void;
}
function CheckboxSettingItem({
title,
description,
inputFieldData,
inputFieldValue,
handleChange,
}: Props): JSX.Element {
const content = (
<fieldset
key={inputFieldData.name}
className='mm-modal-generic-section-item__fieldset-checkbox-ctr'
>
<label className='mm-modal-generic-section-item__fieldset-checkbox'>
<input
className='mm-modal-generic-section-item__input-checkbox'
data-testid={inputFieldData.dataTestId}
type='checkbox'
name={inputFieldData.name}
checked={inputFieldValue}
onChange={(e) => handleChange(e.target.checked)}
/>
<FormattedMessage
id={inputFieldData.title.id}
defaultMessage={inputFieldData.title.defaultMessage}
/>
</label>
<br/>
</fieldset>
);
return (
<BaseSettingItem
content={content}
title={title}
description={description}
/>
);
}
export default CheckboxSettingItem;

View File

@ -0,0 +1,84 @@
.mm-modal-header {
display: flex;
align-items: center;
padding: 20px 20px 20px 32px;
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
background: none;
gap: 8px;
&__title {
padding: 0;
margin: 0;
color: var(--center-channel-color);
font-family: 'Metropolis', serif;
font-size: 22px;
font-style: normal;
font-weight: 600;
line-height: 28px;
}
&__vertical-divider {
width: 1px;
height: 24px;
background: rgba(var(--center-channel-color-rgb), 0.16);
}
&__subtitle {
overflow: hidden;
padding: 0;
margin: 0;
color: rgba(var(--center-channel-color-rgb), 0.56);
font-family: inherit;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
}
&__ctr {
display: flex;
flex: 1 1 auto;
align-items: center;
justify-content: flex-end;
color: rgba(var(--center-channel-color-rgb), 0.56);
gap: 24px;
}
&__close-btn {
display: flex;
overflow: hidden;
width: 40px;
height: 40px;
align-items: center;
justify-content: center;
border: unset;
margin-left: 8px;
background: transparent;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.56);
&:hover,
&.active {
background: rgba(var(--center-channel-color-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), 0.72);
}
}
@media screen and (max-width: 768px) {
padding: 18px 24px;
&__title {
font-size: 20px;
}
&__subtitle {
display: none;
}
&__vertical-divider {
display: none;
}
}
}

View File

@ -0,0 +1,42 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {CloseIcon} from '@mattermost/compass-icons/components';
import './modal_header.scss';
type Props = {
id: string;
title: string;
subtitle: string;
handleClose?: (e: React.MouseEvent) => void;
}
function ModalHeader({id, title, subtitle, handleClose}: Props) {
return (
<header className='mm-modal-header'>
<h1
id={`mm-modal-header-${id}`}
className='mm-modal-header__title'
tabIndex={0}
>
{title}
</h1>
<div className='mm-modal-header__vertical-divider'/>
<p className='mm-modal-header__subtitle'>{subtitle}</p>
<div
className='mm-modal-header__ctr'
onClick={handleClose}
>
<button className='style--none mm-modal-header__close-btn'>
<CloseIcon
size={24}
color={'currentcolor'}
/>
</button>
</div>
</header>
);
}
export default ModalHeader;

View File

@ -0,0 +1,50 @@
.mm-modal-generic-section {
display: flex;
flex-direction: column;
gap: 16px;
&__info-ctr {
display: flex;
flex-direction: column;
margin-bottom: 8px;
gap: 8px;
}
&__title {
display: flex;
align-items: center;
padding: 0;
margin: 0;
color: var(--center-channel-color);
font-family: 'Metropolis', sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 24px;
}
&__row {
display: flex;
align-items: center;
gap: 8px;
}
&__description {
display: flex;
align-items: center;
padding: 0;
margin: 0;
color: rgba(var(--center-channel-color-rgb), 0.64);
font-family: 'Open Sans', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
&__content {
display: flex;
flex-direction: column;
gap: 24px;
}
}

View File

@ -0,0 +1,59 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {MessageDescriptor} from 'react-intl';
import {useIntl} from 'react-intl';
import './modal_section.scss';
type Props = {
title: MessageDescriptor;
description?: MessageDescriptor;
content: JSX.Element;
titleSuffix?: JSX.Element;
};
function ModalSection({
title,
description,
content,
titleSuffix,
}: Props): JSX.Element {
const {formatMessage} = useIntl();
const titleContent = (
<h4 className='mm-modal-generic-section__title'>
{formatMessage({id: title.id, defaultMessage: title.defaultMessage})}
</h4>
);
const descriptionContent = description && (
<p className='mm-modal-generic-section__description'>
{formatMessage({id: description.id, defaultMessage: description.defaultMessage})}
</p>
);
function titleRow() {
if (titleSuffix) {
return (<div className='mm-modal-generic-section__row'>
{titleContent}
{titleSuffix}
</div>);
}
return titleContent;
}
return (
<section className='mm-modal-generic-section'>
<div className='mm-modal-generic-section__info-ctr'>
{titleRow()}
{descriptionContent}
</div>
<div className='mm-modal-generic-section__content'>
{content}
</div>
</section>
);
}
export default ModalSection;

View File

@ -0,0 +1,35 @@
.mm-modal-sidebar {
display: flex;
flex-direction: column;
align-items: center;
padding: 0;
margin: 0;
gap: 8px;
li {
list-style: none;
}
&__item {
display: flex;
width: 184px;
height: 32px;
flex-direction: row;
align-items: center;
padding: 8px 12px;
border: none;
background: none;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.56);
gap: 8px;
&:hover {
background: rgba(var(--denim-button-bg-rgb), 0.08);
}
}
&__item--active {
background: rgba(var(--denim-button-bg-rgb), 0.08);
color: var(--denim-button-bg);
}
}

View File

@ -0,0 +1,60 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import './modal_sidebar.scss';
export type Tab = {
icon: JSX.Element;
name: string;
uiName: string;
}
export type Props = {
activeTab?: string;
tabs: Tab[];
updateTab: (name: string) => void;
}
function ModalSidebar({tabs, activeTab, updateTab}: Props) {
const handleClick = (tab: Tab, e: React.MouseEvent) => {
e.preventDefault();
updateTab(tab.name);
(e.target as Element).closest('.settings-modal')?.classList.add('display--content');
};
const tabList = tabs.map((tab) => {
const key = `${tab.name}_li`;
let className = 'mm-modal-sidebar__item';
if (activeTab === tab.name) {
className += ' mm-modal-sidebar__item--active';
}
return (
<li
id={`${tab.name}Li`}
key={key}
>
<button
id={`${tab.name}Button`}
className={className}
onClick={handleClick.bind(null, tab)}
aria-label={tab.uiName.toLowerCase()}
>
{tab.icon}
{tab.uiName}
</button>
</li>
);
});
return (
<ul
id='tabList'
className='mm-modal-sidebar'
>
{tabList}
</ul>
);
}
export default ModalSidebar;

View File

@ -0,0 +1,72 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {MessageDescriptor} from 'react-intl';
import {FormattedMessage} from 'react-intl';
import type {BaseSettingItemProps} from './base_setting_item';
import BaseSettingItem from './base_setting_item';
export type FieldsetRadio = {
options: Array<{
dataTestId?: string;
title: MessageDescriptor;
name: string;
key: string;
value: string;
suffix?: JSX.Element;
}>;
}
type Props = BaseSettingItemProps & {
inputFieldData: FieldsetRadio;
inputFieldValue: string;
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
function RadioSettingItem({
title,
description,
inputFieldData,
inputFieldValue,
handleChange,
}: Props): JSX.Element {
const fields = inputFieldData.options.map((option) => {
return (
<label
key={option.key}
className='mm-modal-generic-section-item__label-radio'
>
<input
id={option.key}
data-testid={option.dataTestId}
type='radio'
name={option.name}
checked={option.value === inputFieldValue}
value={option.value}
onChange={handleChange}
/>
<FormattedMessage
id={option.title.id}
defaultMessage={option.title.defaultMessage}
/>
{option.suffix}
</label>
);
});
const content = (
<fieldset className='mm-modal-generic-section-item__fieldset-radio'>
{[...fields]}
</fieldset>
);
return (
<BaseSettingItem
content={content}
title={title}
description={description}
/>
);
}
export default RadioSettingItem;

View File

@ -0,0 +1,65 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {ValueType} from 'react-select';
import ReactSelect from 'react-select';
import type {BaseSettingItemProps} from './base_setting_item';
import BaseSettingItem from './base_setting_item';
export type Option = {
value: number;
label: string;
};
export type FieldsetReactSelect = {
dataTestId?: string;
options: Option[];
}
type Props = BaseSettingItemProps & {
inputFieldData: FieldsetReactSelect;
inputFieldValue: Option;
handleChange: (selected: ValueType<Option>) => void;
}
function ReactSelectItemCreator({
title,
description,
inputFieldData,
inputFieldValue,
handleChange,
}: Props): JSX.Element {
const content = (
<fieldset className='mm-modal-generic-section-item__fieldset-react-select'>
<ReactSelect
className='react-select'
classNamePrefix='react-select'
id='limitVisibleGMsDMs'
options={inputFieldData.options}
clearable={false}
onChange={handleChange}
value={inputFieldValue}
isSearchable={false}
menuPortalTarget={document.body}
styles={reactStyles}
/>
</fieldset>
);
return (
<BaseSettingItem
content={content}
title={title}
description={description}
/>
);
}
export default ReactSelectItemCreator;
const reactStyles = {
menuPortal: (provided: React.CSSProperties) => ({
...provided,
zIndex: 9999,
}),
};

View File

@ -0,0 +1,68 @@
.mm-save-changes-panel {
position: absolute;
z-index: 1000;
right: 20px;
bottom: 20px;
display: flex;
width: calc(100% - 232px - 40px);
align-items: center;
padding: 18px;
background-color: var(--dnd-indicator);
border-radius: 4px;
box-shadow: var(--elevation-3);
font-family: 'Open Sans', sans-serif;
&__message {
display: flex;
align-items: center;
margin: 0;
color: var(--sidebar-text);
font-family: inherit;
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px;
}
&__btn-ctr {
display: flex;
flex: 1 1 auto;
justify-content: flex-end;
gap: 8px;
}
&__cancel-btn {
display: flex;
width: 64px;
height: 32px;
align-items: center;
padding: 10px 16px;
border: none;
background: rgba(255, 255, 255, 0.12);
border-radius: 4px;
color: var(--sidebar-text);
font-family: inherit;
font-size: 12px;
font-weight: 600;
gap: 10px;
line-height: 10px;
}
&__save-btn {
display: flex;
width: 59px;
height: 32px;
flex-direction: column;
align-items: center;
padding: 10px 16px;
border: none;
background: var(--sidebar-text);
border-radius: 4px;
color: var(--denim-button-bg);
font-family: inherit;
font-size: 12px;
font-weight: 600;
gap: 10px;
line-height: 10px;
}
}

View File

@ -0,0 +1,50 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import './save_changes_panel.scss';
import {AlertCircleOutlineIcon} from '@mattermost/compass-icons/components';
type Props = {
handleSubmit: () => void;
handleCancel: () => void;
}
function SaveChangesPanel({handleSubmit, handleCancel}: Props) {
return (
<div className='mm-save-changes-panel'>
<p className='mm-save-changes-panel__message'>
<AlertCircleOutlineIcon
size={18}
color={'currentcolor'}
/>
<FormattedMessage
id='saveChangesPanel.message'
defaultMessage='You have unsaved changes'
/>
</p>
<div className='mm-save-changes-panel__btn-ctr'>
<button
className='mm-save-changes-panel__cancel-btn'
onClick={handleCancel}
>
<FormattedMessage
id='saveChangesPanel.cancel'
defaultMessage='Undo'
/>
</button>
<button
className='mm-save-changes-panel__save-btn'
onClick={handleSubmit}
>
<FormattedMessage
id='saveChangesPanel.save'
defaultMessage='Save'
/>
</button>
</div>
</div>
);
}
export default SaveChangesPanel;

View File

@ -2998,40 +2998,34 @@
"channel_modal.type.private.title": "Private Channel",
"channel_modal.type.public.description": "Anyone can join",
"channel_modal.type.public.title": "Public Channel",
"channel_notifications.allActivity": "For all activity {isDefault}",
"channel_notifications.channelAutoFollowThreads": "Auto-follow all new threads in this channel",
"channel_notifications.channelAutoFollowThreads.help": "When enabled, you will auto-follow all new threads created in this channel unless you unfollow a thread explicitly.",
"channel_notifications.channelAutoFollowThreads.off.title": "Off",
"channel_notifications.channelAutoFollowThreads.on.title": "On",
"channel_notifications.defaultOption": "(default)",
"channel_notifications.desktopNotifications": "Desktop notifications",
"channel_notifications.globalDefault": "Global default ({notifyLevel})",
"channel_notifications.ignoreChannelMentions": "Ignore mentions for @channel, @here and @all",
"channel_notifications.ignoreChannelMentions.help": "When enabled, @channel, @here and @all will not trigger mentions or mention notifications in this channel.",
"channel_notifications.ignoreChannelMentions.off.title": "Off",
"channel_notifications.ignoreChannelMentions.on.title": "On",
"channel_notifications.alertBanner.description": "All other notification preferences for this channel are disabled",
"channel_notifications.alertBanner.title": "This channel is muted",
"channel_notifications.autoFollowThreadsDesc": "When enabled, all new replies in this channel will be automatically followed and will appear in your Threads view.",
"channel_notifications.autoFollowThreadsTitle": "Follow all threads in this channel",
"channel_notifications.checkbox.autoFollowThreadsTitle": "Automatically follow threads in this channel",
"channel_notifications.checkbox.sameMobileSettingsDesktop": "Use the same notification settings as desktop",
"channel_notifications.checkbox.threadsReplyTitle": "Notify me about replies to threads Im following",
"channel_notifications.default": "(default)",
"channel_notifications.desktopNotificationAllLabel": "All new messages",
"channel_notifications.desktopNotificationMentionLabel": "Mentions, direct messages, and keywords only",
"channel_notifications.desktopNotificationNothingLabel": "Nothing",
"channel_notifications.desktopNotificationsDesc": "Available on Chrome, Edge, Firefox, and the Mattermost Desktop App.",
"channel_notifications.desktopNotificationsTitle": "Desktop Notifications",
"channel_notifications.ignoreMentionsDesc": "When enabled, @channel, @here and @all will not trigger mentions or mention notifications in this channel",
"channel_notifications.ignoreMentionsTitle": "Ignore mentions for @channel, @here and @all",
"channel_notifications.levels.all": "All",
"channel_notifications.levels.default": "Default",
"channel_notifications.levels.mention": "Mention",
"channel_notifications.levels.none": "None",
"channel_notifications.muteChannel.help": "Muting turns off desktop, email and push notifications for this channel. The channel will not be marked as unread unless you're mentioned.",
"channel_notifications.muteChannel.off.title": "Off",
"channel_notifications.muteChannel.on.title": "On",
"channel_notifications.muteChannel.on.title.collapse": "Mute is enabled. Desktop, email and push notifications will not be sent for this channel.",
"channel_notifications.muteChannel.settings": "Mute channel",
"channel_notifications.never": "Never {isDefault}",
"channel_notifications.onlyMentions": "Only for mentions {isDefault}",
"channel_notifications.override": "Selecting an option other than the \"default\" will override the global notification settings. Desktop notifications are available on Firefox, Safari, and Chrome.",
"channel_notifications.overridePush": "Selecting an option other than the \"default\" will override the global notification settings for mobile push notifications. ",
"channel_notifications.preferences": "Notification Preferences for ",
"channel_notifications.push": "Mobile push notifications",
"channel_notifications.resetToDefaults": "Reset to defaults",
"channel_notifications.sendDesktop": "Send Desktop notifications",
"channel_notifications.sendMobilePush": "Send mobile push notifications",
"channel_notifications.sound": "Notification sound",
"channel_notifications.sound_info": "Notification sounds are available on Firefox, Edge, Safari, Chrome and Mattermost Desktop Apps.",
"channel_notifications.sound.off.title": "Off",
"channel_notifications.sound.on.title": "On",
"channel_notifications.MobileNotificationAllLabel": "All new messages",
"channel_notifications.MobileNotificationMentionLabel": "Mentions, direct messages, and keywords only",
"channel_notifications.MobileNotificationNothingLabel": "Nothing",
"channel_notifications.mobileNotificationsDesc": "Notification alerts are pushed to your mobile device when there is activity in Mattermost.",
"channel_notifications.mobileNotificationsTitle": "Mobile Notifications",
"channel_notifications.muteAndIgnore": "Mute or ignore",
"channel_notifications.muteChannelDesc": "Turns off notifications for this channel. Youll still see badges if youre mentioned.",
"channel_notifications.muteChannelTitle": "Mute channel",
"channel_notifications.NotifyMeTitle": "Notify me about…",
"channel_notifications.preferences": "Notification Preferences",
"channel_notifications.resetToDefault": "Reset to default",
"channel_notifications.ThreadsReplyTitle": "Thread reply notifications",
"channel_select.placeholder": "--- Select a channel ---",
"channel_switch_modal.deactivated": "Deactivated",
"channel_toggle_button.private": "Private",
@ -3564,6 +3558,8 @@
"general_tab.teamNameRestrictions": "Team Name must be {min} or more characters up to a maximum of {max}. You can add a longer team description.",
"general_tab.title": "General Settings",
"general_tab.yes": "Yes",
"generic_btn.cancel": "Cancel",
"generic_btn.save": "Save",
"generic_icons.add": "Add Icon",
"generic_icons.add-mail": "Add Mail Icon",
"generic_icons.add-reaction": "Add Reaction Icon",
@ -4587,6 +4583,9 @@
"rhs_thread.toast.newReplies": "New Replies",
"save_button.save": "Save",
"save_button.saving": "Saving",
"saveChangesPanel.cancel": "Undo",
"saveChangesPanel.message": "You have unsaved changes",
"saveChangesPanel.save": "Save",
"search_bar.files_tab": "Files",
"search_bar.messages_tab": "Messages",
"search_bar.search": "Search",

View File

@ -17,12 +17,12 @@ import type {Scheme} from '@mattermost/types/schemes';
import type {Team, TeamMembership} from '@mattermost/types/teams';
import type {UserProfile, UserNotifyProps} from '@mattermost/types/users';
import General from 'mattermost-redux/constants/general';
import {generateId} from 'mattermost-redux/utils/helpers';
export const DEFAULT_SERVER = 'http://localhost:8065';
const PASSWORD = 'password1';
import General from 'mattermost-redux/constants/general';
import {generateId} from 'mattermost-redux/utils/helpers';
const {DEFAULT_LOCALE} = General;
class TestHelper {
@ -452,6 +452,8 @@ class TestHelper {
return {
desktop: 'default',
desktop_sound: 'off',
desktop_threads: 'default',
push_threads: 'default',
email: 'default',
mark_unread: 'mention',
push: 'default',

View File

@ -20,12 +20,14 @@ export type ChannelStats = {
};
export type ChannelNotifyProps = {
desktop_threads: 'default' | 'all' | 'mention' | 'none';
desktop: 'default' | 'all' | 'mention' | 'none';
desktop_sound: 'on' | 'off';
desktop_notification_sound?: 'Bing' | 'Crackle' | 'Down' | 'Hello' | 'Ripple' | 'Upstairs';
email: 'default' | 'all' | 'mention' | 'none';
mark_unread: 'all' | 'mention';
push: 'default' | 'all' | 'mention' | 'none';
push_threads: 'default' | 'all' | 'mention' | 'none';
ignore_channel_mentions: 'default' | 'off' | 'on';
channel_auto_follow_threads: 'off' | 'on';
};