diff --git a/e2e-tests/cypress/tests/integration/channels/collapsed_reply_threads/channel_notifications_spec.js b/e2e-tests/cypress/tests/integration/channels/collapsed_reply_threads/channel_notifications_spec.js index 70edbe4c0e8..b54a9d81217 100644 --- a/e2e-tests/cypress/tests/integration/channels/collapsed_reply_threads/channel_notifications_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/collapsed_reply_threads/channel_notifications_spec.js @@ -245,7 +245,7 @@ function setCRTDesktopNotification(type) { cy.get('#desktopTitle'). scrollIntoView(). should('be.visible'). - and('contain', 'Send desktop notifications').click(); + and('contain', 'Desktop notifications').click(); // # Select mentions category for messages. cy.get('#channelNotificationMentions').scrollIntoView().check(); diff --git a/e2e-tests/cypress/tests/integration/channels/notifications/desktop_notifications_2_spec.js b/e2e-tests/cypress/tests/integration/channels/notifications/desktop_notifications_2_spec.js index 433e6ff3806..a4986b7ad00 100644 --- a/e2e-tests/cypress/tests/integration/channels/notifications/desktop_notifications_2_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/notifications/desktop_notifications_2_spec.js @@ -53,7 +53,7 @@ describe('Desktop notifications', () => { // # Set channel notifications to show on mention only cy.uiOpenChannelMenu('Notification Preferences'); - cy.findByText('Send desktop notifications').click(); + cy.findByText('Desktop notifications').click(); cy.findByRole('radio', {name: 'Only for mentions'}).click(); cy.uiSaveAndClose(); diff --git a/server/channels/app/channel.go b/server/channels/app/channel.go index b1f6e964615..de5d7a4a6bf 100644 --- a/server/channels/app/channel.go +++ b/server/channels/app/channel.go @@ -1334,6 +1334,14 @@ func (a *App) UpdateChannelMemberNotifyProps(c request.CTX, data map[string]stri filteredProps[model.DesktopNotifyProp] = desktop } + if desktop_sound, exists := data[model.DesktopSoundNotifyProp]; exists { + filteredProps[model.DesktopSoundNotifyProp] = desktop_sound + } + + if desktop_notification_sound, exists := data["desktop_notification_sound"]; exists { + filteredProps["desktop_notification_sound"] = desktop_notification_sound + } + if desktop_threads, exists := data[model.DesktopThreadsNotifyProp]; exists { filteredProps[model.DesktopThreadsNotifyProp] = desktop_threads } diff --git a/webapp/channels/src/actions/notification_actions.jsx b/webapp/channels/src/actions/notification_actions.jsx index 15255e9ac6d..d63960c818e 100644 --- a/webapp/channels/src/actions/notification_actions.jsx +++ b/webapp/channels/src/actions/notification_actions.jsx @@ -28,6 +28,22 @@ const NOTIFY_TEXT_MAX_LENGTH = 50; // windows notification length is based windows chrome which supports 128 characters and is the lowest length of windows browsers const WINDOWS_NOTIFY_TEXT_MAX_LENGTH = 120; +const getSoundFromChannelMemberAndUser = (member, user) => { + if (member?.notify_props?.desktop_sound) { + return member.notify_props.desktop_sound === 'on'; + } + + return !user.notify_props || user.notify_props.desktop_sound === 'true'; +}; + +const getNotificationSoundFromChannelMemberAndUser = (member, user) => { + if (member?.notify_props?.desktop_notification_sound) { + return member.notify_props.desktop_notification_sound; + } + + return user.notify_props?.desktop_notification_sound ? user.notify_props.desktop_notification_sound : 'Bing'; +}; + export function sendDesktopNotification(post, msgProps) { return async (dispatch, getState) => { const state = getState(); @@ -159,7 +175,7 @@ export function sendDesktopNotification(post, msgProps) { } //Play a sound if explicitly set in settings - const sound = !user.notify_props || user.notify_props.desktop_sound === 'true'; + const sound = getSoundFromChannelMemberAndUser(member, user); // Notify if you're not looking in the right channel or when // the window itself is not active @@ -174,7 +190,7 @@ export function sendDesktopNotification(post, msgProps) { } notify = notify || !state.views.browser.focused; - const soundName = user.notify_props !== undefined && user.notify_props.desktop_notification_sound !== undefined ? user.notify_props.desktop_notification_sound : 'Bing'; + const soundName = getNotificationSoundFromChannelMemberAndUser(member, user); if (notify) { const updatedState = getState(); diff --git a/webapp/channels/src/components/channel_notifications_modal/__snapshots__/channel_notifications_modal.test.tsx.snap b/webapp/channels/src/components/channel_notifications_modal/__snapshots__/channel_notifications_modal.test.tsx.snap index 54c6f5fb8e7..60197883faa 100644 --- a/webapp/channels/src/components/channel_notifications_modal/__snapshots__/channel_notifications_modal.test.tsx.snap +++ b/webapp/channels/src/components/channel_notifications_modal/__snapshots__/channel_notifications_modal.test.tsx.snap @@ -97,10 +97,17 @@ exports[`components/channel_notifications_modal/ChannelNotificationsModal should , ); - expect(wrapper.state('desktopNotifyLevel')).toEqual(NotificationLevels.DEFAULT); + 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.DEFAULT); + expect(wrapper.state('pushNotifyLevel')).toEqual(NotificationLevels.ALL); expect(wrapper.state('ignoreChannelMentions')).toEqual(IgnoreChannelMentions.OFF); expect(wrapper.state('channelAutoFollowThreads')).toEqual(ChannelAutoFollowThreads.OFF); }); @@ -186,7 +190,7 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', () wrapper.instance().handleExit(); expect(baseProps.onExited).toHaveBeenCalledTimes(3); expect(wrapper.state('activeSection')).toEqual(NotificationSections.NONE); - expect(wrapper.state('pushNotifyLevel')).toEqual(NotificationLevels.DEFAULT); + expect(wrapper.state('pushNotifyLevel')).toEqual(NotificationLevels.ALL); }); test('should match state on updateSection', () => { @@ -214,7 +218,7 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', () expect(wrapper.state('desktopNotifyLevel')).toEqual(baseProps.channelMember?.notify_props.desktop); }); - test('should match state on handleSubmitDesktopNotifyLevel', () => { + test('should match state on handleSubmitDesktopNotification', () => { const wrapper = shallow( , ); @@ -223,12 +227,12 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', () instance.handleUpdateChannelNotifyProps = jest.fn(); instance.updateSection = jest.fn(); - wrapper.setState({desktopNotifyLevel: NotificationLevels.DEFAULT}); - instance.handleSubmitDesktopNotifyLevel(); + wrapper.setState({desktopNotifyLevel: NotificationLevels.MENTION}); + instance.handleSubmitDesktopNotification(); expect(instance.handleUpdateChannelNotifyProps).toHaveBeenCalledTimes(1); wrapper.setState({desktopNotifyLevel: NotificationLevels.ALL}); - instance.handleSubmitDesktopNotifyLevel(); + instance.handleSubmitDesktopNotification(); expect(instance.updateSection).toHaveBeenCalledTimes(1); expect(instance.updateSection).toBeCalledWith(''); }); diff --git a/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx b/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx index 20756d7619a..ed0b160fe10 100644 --- a/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx +++ b/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx @@ -6,9 +6,11 @@ import React from 'react'; import {Modal} from 'react-bootstrap'; import {FormattedMessage} from 'react-intl'; +import * as NotificationSounds from 'utils/notification_sounds'; + import {isChannelMuted} from 'mattermost-redux/utils/channel_utils'; -import {ChannelAutoFollowThreads, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants'; +import {ChannelAutoFollowThreads, DesktopSound, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants'; import NotificationSection from 'components/channel_notifications_modal/components/notification_section.jsx'; @@ -42,6 +44,8 @@ type State = { 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']; @@ -50,6 +54,59 @@ type State = { channelAutoFollowThreads: ChannelNotifyProps['channel_auto_follow_threads']; }; +export type DesktopNotificationProps = Pick +export type PushNotificationProps = Pick + +const getDefaultDesktopNotificationLevel = (currentUserNotifyProps: UserNotifyProps): Exclude => { + if (currentUserNotifyProps?.desktop) { + if (currentUserNotifyProps.desktop === 'default') { + return NotificationLevels.ALL; + } + return currentUserNotifyProps.desktop; + } + return NotificationLevels.ALL; +}; + +const getDefaultDesktopSound = (currentUserNotifyProps: UserNotifyProps): Exclude => { + if (currentUserNotifyProps?.desktop_sound) { + return currentUserNotifyProps.desktop_sound === 'true' ? DesktopSound.ON : DesktopSound.OFF; + } + return DesktopSound.ON; +}; + +const getDefaultDesktopNotificationSound = (currentUserNotifyProps: UserNotifyProps): Exclude => { + if (currentUserNotifyProps?.desktop_notification_sound) { + return currentUserNotifyProps.desktop_notification_sound; + } + return 'Bing'; +}; +const getDefaultDesktopThreadsNotifyLevel = (currentUserNotifyProps: UserNotifyProps): Exclude => { + if (currentUserNotifyProps?.desktop_threads) { + return currentUserNotifyProps.desktop_threads; + } + return NotificationLevels.ALL; +}; + +const getDefaultPushNotifyLevel = (currentUserNotifyProps: UserNotifyProps): Exclude => { + if (currentUserNotifyProps?.push) { + if (currentUserNotifyProps.push === 'default') { + return NotificationLevels.ALL; + } + return currentUserNotifyProps.push; + } + return NotificationLevels.ALL; +}; + +const getDefaultPushThreadsNotifyLevel = (currentUserNotifyProps: UserNotifyProps): Exclude => { + if (currentUserNotifyProps?.push_threads) { + if (currentUserNotifyProps.push_threads === 'default') { + return NotificationLevels.ALL; + } + return currentUserNotifyProps.push_threads; + } + return NotificationLevels.ALL; +}; + export default class ChannelNotificationsModal extends React.PureComponent { constructor(props: Props) { super(props); @@ -77,9 +134,63 @@ export default class ChannelNotificationsModal extends React.PureComponent { + const currentUserNotifyProps = this.props.currentUser.notify_props; - handleSubmitDesktopNotifyLevel = () => { + const userDesktopNotificationDefaults = { + desktopNotifyLevel: getDefaultDesktopNotificationLevel(currentUserNotifyProps), + 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), + pushThreadsNotifyLevel: getDefaultPushThreadsNotifyLevel(currentUserNotifyProps), + }; + + this.setState(userPushNotificationDefaults); + }; + + handleSubmitDesktopNotification = () => { const channelNotifyProps = this.props.channelMember && this.props.channelMember.notify_props as ChannelMemberNotifyProps; - const {desktopNotifyLevel, desktopThreadsNotifyLevel} = this.state; + const {desktopNotifyLevel, desktopNotifySound, desktopSound, desktopThreadsNotifyLevel} = this.state; if ( channelNotifyProps?.desktop === desktopNotifyLevel && - channelNotifyProps?.desktop_threads === desktopThreadsNotifyLevel + 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}; + const props = {desktop: desktopNotifyLevel, desktop_threads: desktopThreadsNotifyLevel, desktop_sound: desktopSound, desktop_notification_sound: desktopNotifySound}; this.handleUpdateChannelNotifyProps(props); }; @@ -152,6 +290,15 @@ export default class ChannelNotificationsModal extends React.PureComponent 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; @@ -220,6 +367,8 @@ export default class ChannelNotificationsModal extends React.PureComponent; @@ -295,10 +456,17 @@ export default class ChannelNotificationsModal extends React.PureComponent @@ -310,7 +478,9 @@ export default class ChannelNotificationsModal extends React.PureComponent, + } + } /> `; @@ -16,15 +21,25 @@ exports[`components/channel_notifications_modal/NotificationSection should match exports[`components/channel_notifications_modal/NotificationSection should match snapshot, on MENTION 1`] = ` , + } + } /> `; exports[`components/channel_notifications_modal/NotificationSection should match snapshot, on NONE 1`] = ` , + } + } /> `; diff --git a/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/expand_view.test.tsx.snap b/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/expand_view.test.tsx.snap index 3c0c4dfc65f..c8aeb3fe849 100644 --- a/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/expand_view.test.tsx.snap +++ b/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/expand_view.test.tsx.snap @@ -8,27 +8,14 @@ exports[`components/channel_notifications_modal/ExpandView should match snapshot Array [
-
- -
+ +
@@ -38,12 +25,13 @@ exports[`components/channel_notifications_modal/ExpandView should match snapshot @@ -58,12 +46,13 @@ exports[`components/channel_notifications_modal/ExpandView should match snapshot @@ -76,12 +65,13 @@ exports[`components/channel_notifications_modal/ExpandView should match snapshot @@ -95,6 +85,63 @@ exports[`components/channel_notifications_modal/ExpandView should match snapshot section="desktop" />
+ +
+
+ + + +
+ +
+
+ +
+
+ +
+
+
, ] } @@ -104,6 +151,8 @@ exports[`components/channel_notifications_modal/ExpandView should match snapshot submit={[MockFunction]} title={ } @@ -176,6 +225,8 @@ exports[`components/channel_notifications_modal/ExpandView should match snapshot submit={[MockFunction]} title={ } @@ -191,27 +242,14 @@ exports[`components/channel_notifications_modal/ExpandView should match snapshot Array [
-
- -
+ +
@@ -221,12 +259,13 @@ exports[`components/channel_notifications_modal/ExpandView should match snapshot @@ -241,12 +280,13 @@ exports[`components/channel_notifications_modal/ExpandView should match snapshot @@ -259,12 +299,13 @@ exports[`components/channel_notifications_modal/ExpandView should match snapshot @@ -287,6 +328,8 @@ exports[`components/channel_notifications_modal/ExpandView should match snapshot submit={[MockFunction]} title={ } diff --git a/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/extra_info.test.jsx.snap b/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/extra_info.test.jsx.snap index 631baa43ed2..314504f2874 100644 --- a/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/extra_info.test.jsx.snap +++ b/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/extra_info.test.jsx.snap @@ -3,7 +3,7 @@ exports[`components/channel_notifications_modal/ExtraInfo should match snapshot, on DESKTOP 1`] = ` @@ -21,7 +21,7 @@ exports[`components/channel_notifications_modal/ExtraInfo should match snapshot, exports[`components/channel_notifications_modal/ExtraInfo should match snapshot, on PUSH 1`] = ` diff --git a/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/notification_section.test.jsx.snap b/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/notification_section.test.jsx.snap index 35a1eebbcae..888861bc5da 100644 --- a/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/notification_section.test.jsx.snap +++ b/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/notification_section.test.jsx.snap @@ -24,8 +24,11 @@ exports[`components/channel_notifications_modal/NotificationSection should match memberNotifyLevel="all" memberThreadsNotifyLevel="all" onChange={[Function]} + onChangeDesktopSound={[Function]} + onChangeNotificationSound={[Function]} onChangeThreads={[Function]} onCollapseSection={[Function]} + onReset={[Function]} onSubmit={[Function]} section="desktop" serverError="" @@ -47,8 +50,11 @@ exports[`components/channel_notifications_modal/NotificationSection should match memberNotifyLevel="all" memberThreadsNotifyLevel="all" onChange={[Function]} + onChangeDesktopSound={[Function]} + onChangeNotificationSound={[Function]} onChangeThreads={[Function]} onCollapseSection={[Function]} + onReset={[Function]} onSubmit={[Function]} section="markUnread" serverError="" @@ -70,8 +76,11 @@ exports[`components/channel_notifications_modal/NotificationSection should match memberNotifyLevel="all" memberThreadsNotifyLevel="all" onChange={[Function]} + onChangeDesktopSound={[Function]} + onChangeNotificationSound={[Function]} onChangeThreads={[Function]} onCollapseSection={[Function]} + onReset={[Function]} onSubmit={[Function]} section="push" serverError="" diff --git a/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/section_title.test.tsx.snap b/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/section_title.test.tsx.snap index bee72bbe86e..fdd6f2511d7 100644 --- a/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/section_title.test.tsx.snap +++ b/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/section_title.test.tsx.snap @@ -1,10 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`components/channel_notifications_modal/ExtraInfo should match snapshot, on DESKTOP 1`] = ` - +
+ +
`; exports[`components/channel_notifications_modal/ExtraInfo should match snapshot, on MARK_UNREAD 1`] = ` @@ -15,8 +19,12 @@ exports[`components/channel_notifications_modal/ExtraInfo should match snapshot, `; exports[`components/channel_notifications_modal/ExtraInfo should match snapshot, on PUSH 1`] = ` - +
+ +
`; diff --git a/webapp/channels/src/components/channel_notifications_modal/components/describe.tsx b/webapp/channels/src/components/channel_notifications_modal/components/describe.tsx index 52a69121f5f..d99e15c974f 100644 --- a/webapp/channels/src/components/channel_notifications_modal/components/describe.tsx +++ b/webapp/channels/src/components/channel_notifications_modal/components/describe.tsx @@ -16,6 +16,13 @@ type Props = { isCollapsed?: boolean; } +const defaultOption = ( + +); + export default function Describe({section, isCollapsed, memberNotifyLevel, globalNotifyLevel, ignoreChannelMentions, channelAutoFollowThreads}: Props) { if (memberNotifyLevel === NotificationLevels.DEFAULT && globalNotifyLevel) { t('channel_notifications.levels.default'); @@ -95,7 +102,8 @@ export default function Describe({section, isCollapsed, memberNotifyLevel, globa return ( }} /> ); } else if ( @@ -105,7 +113,8 @@ export default function Describe({section, isCollapsed, memberNotifyLevel, globa return ( }} /> ); } else if ( @@ -123,7 +132,8 @@ export default function Describe({section, isCollapsed, memberNotifyLevel, globa return ( }} /> ); } diff --git a/webapp/channels/src/components/channel_notifications_modal/components/expand_view.test.tsx b/webapp/channels/src/components/channel_notifications_modal/components/expand_view.test.tsx index 52237c1694b..39e13f68177 100644 --- a/webapp/channels/src/components/channel_notifications_modal/components/expand_view.test.tsx +++ b/webapp/channels/src/components/channel_notifications_modal/components/expand_view.test.tsx @@ -24,6 +24,7 @@ describe('components/channel_notifications_modal/ExpandView', () => { onChangeThreads: jest.fn(), onCollapseSection: jest.fn(), onSubmit: jest.fn(), + onReset: jest.fn(), }; test('should match snapshot, DESKTOP on expanded view', () => { diff --git a/webapp/channels/src/components/channel_notifications_modal/components/expand_view.tsx b/webapp/channels/src/components/channel_notifications_modal/components/expand_view.tsx index a81858d32b9..1e586b2707a 100644 --- a/webapp/channels/src/components/channel_notifications_modal/components/expand_view.tsx +++ b/webapp/channels/src/components/channel_notifications_modal/components/expand_view.tsx @@ -1,41 +1,74 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {ChangeEvent} from 'react'; +import React, {ChangeEvent, useMemo, useRef} from 'react'; import {FormattedMessage} from 'react-intl'; import {useSelector} from 'react-redux'; +import ReactSelect, {ValueType} from 'react-select'; + import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; -import {ChannelAutoFollowThreads, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants'; +import {ChannelAutoFollowThreads, DesktopSound, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants'; import SettingItemMax from 'components/setting_item_max'; +import {ChannelNotifyProps} from '@mattermost/types/channels'; + +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) => void; onChangeThreads?: (e: ChangeEvent) => void; + onChangeDesktopSound?: (e: ChangeEvent) => void; + onChangeNotificationSound?: (selectedOption: ValueType) => 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; } +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, @@ -44,32 +77,33 @@ export default function ExpandView({ }: Props) { const isCRTEnabled = useSelector(isCollapsedThreadsEnabled); + const soundOptions = useMemo(() => sounds.map((sound) => { + return {value: sound, label: sound === globalNotificationSound ? makeDefaultOptionLabel(sound) : sound}; + }), [globalNotificationSound]); + + const dropdownSoundRef = useRef(null); + const inputs = [(
{(section === NotificationSections.DESKTOP || section === NotificationSections.PUSH) &&
-
- -
+ { section === NotificationSections.DESKTOP && + + } + { section === NotificationSections.PUSH && + + }
@@ -85,7 +120,7 @@ export default function ExpandView({
@@ -101,7 +137,7 @@ export default function ExpandView({
@@ -272,6 +309,72 @@ export default function ExpandView({
} + {(section === NotificationSections.DESKTOP) && memberNotifyLevel !== NotificationLevels.NONE && + + <> +
+
+ + + +
+ +
+
+ +
+ {memberDesktopSound === DesktopSound.ON && +
+ +
} +
+ +
+
+ + } {isCRTEnabled && section === NotificationSections.PUSH && memberNotifyLevel === NotificationLevels.MENTION && @@ -314,7 +417,13 @@ export default function ExpandView({ return ( } + title={ + } inputs={inputs} submit={onSubmit} serverError={serverError} diff --git a/webapp/channels/src/components/channel_notifications_modal/components/extra_info.tsx b/webapp/channels/src/components/channel_notifications_modal/components/extra_info.tsx index 7f489acec95..6858a8d7fc8 100644 --- a/webapp/channels/src/components/channel_notifications_modal/components/extra_info.tsx +++ b/webapp/channels/src/components/channel_notifications_modal/components/extra_info.tsx @@ -17,7 +17,7 @@ export default function ExtraInfo({section}: Props) { ); @@ -26,7 +26,7 @@ export default function ExtraInfo({section}: Props) { ); diff --git a/webapp/channels/src/components/channel_notifications_modal/components/notification_section.jsx b/webapp/channels/src/components/channel_notifications_modal/components/notification_section.jsx index 9ff5fbd3a44..1883613fe49 100644 --- a/webapp/channels/src/components/channel_notifications_modal/components/notification_section.jsx +++ b/webapp/channels/src/components/channel_notifications_modal/components/notification_section.jsx @@ -27,6 +27,10 @@ export default class NotificationSection extends React.PureComponent { */ memberNotificationLevel: PropTypes.string.isRequired, + memberDesktopSound: PropTypes.string, + + memberDesktopNotificationSound: PropTypes.string, + /** * Member's desktop_threads notification level */ @@ -47,6 +51,11 @@ export default class NotificationSection extends React.PureComponent { */ globalNotificationLevel: PropTypes.string, + /** + * User's global notification sound + */ + globalNotificationSound: PropTypes.string, + /** * onChange handles update of desktop notification level */ @@ -57,6 +66,14 @@ export default class NotificationSection extends React.PureComponent { */ onChangeThreads: PropTypes.func, + onChangeDesktopSound: PropTypes.func, + + onChangeNotificationSound: PropTypes.func, + + onReset: PropTypes.func, + + isNotificationsSettingSameAsGlobal: PropTypes.bool, + /** * Submit function to save notification level */ @@ -83,6 +100,16 @@ export default class NotificationSection extends React.PureComponent { 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); }; @@ -95,11 +122,16 @@ export default class NotificationSection extends React.PureComponent { const { expand, globalNotificationLevel, + globalNotificationSound, memberNotificationLevel, memberThreadsNotificationLevel, + memberDesktopSound, + memberDesktopNotificationSound, ignoreChannelMentions, + isNotificationsSettingSameAsGlobal, channelAutoFollowThreads, onSubmit, + onReset, section, serverError, } = this.props; @@ -110,11 +142,18 @@ export default class NotificationSection extends React.PureComponent { 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} diff --git a/webapp/channels/src/components/channel_notifications_modal/components/notification_section.test.jsx b/webapp/channels/src/components/channel_notifications_modal/components/notification_section.test.jsx index 982a5ca4b23..bb359305719 100644 --- a/webapp/channels/src/components/channel_notifications_modal/components/notification_section.test.jsx +++ b/webapp/channels/src/components/channel_notifications_modal/components/notification_section.test.jsx @@ -17,6 +17,7 @@ describe('components/channel_notifications_modal/NotificationSection', () => { 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: '', diff --git a/webapp/channels/src/components/channel_notifications_modal/components/section_title.scss b/webapp/channels/src/components/channel_notifications_modal/components/section_title.scss new file mode 100644 index 00000000000..3eeab82fcce --- /dev/null +++ b/webapp/channels/src/components/channel_notifications_modal/components/section_title.scss @@ -0,0 +1,15 @@ +@import 'sass/utils/mixins'; + +.SectionTitle { + &__wrapper { + display: flex; + justify-content: space-between; + } + + &__resetButton { + @include button-style--none; + + display: flex; + align-items: center; + } +} diff --git a/webapp/channels/src/components/channel_notifications_modal/components/section_title.tsx b/webapp/channels/src/components/channel_notifications_modal/components/section_title.tsx index be8704e554b..ee9d06643e4 100644 --- a/webapp/channels/src/components/channel_notifications_modal/components/section_title.tsx +++ b/webapp/channels/src/components/channel_notifications_modal/components/section_title.tsx @@ -6,24 +6,43 @@ 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}: Props) { - if (section === NotificationSections.DESKTOP) { +export default function SectionTitle({section, isExpanded, isNotificationsSettingSameAsGlobal, onClickResetButton}: Props) { + if (section === NotificationSections.DESKTOP || section === NotificationSections.PUSH) { return ( - - ); - } else if (section === NotificationSections.PUSH) { - return ( - +
+ {section === NotificationSections.DESKTOP && + } + + {section === NotificationSections.PUSH && + } + {isExpanded && !isNotificationsSettingSameAsGlobal && + + } +
); } else if (section === NotificationSections.MARK_UNREAD) { return ( diff --git a/webapp/channels/src/components/root/root.test.tsx b/webapp/channels/src/components/root/root.test.tsx index c146bae462e..d5cb9c65878 100644 --- a/webapp/channels/src/components/root/root.test.tsx +++ b/webapp/channels/src/components/root/root.test.tsx @@ -31,11 +31,17 @@ jest.mock('actions/global_actions', () => ({ redirectUserToDefaultTeam: jest.fn(), })); -jest.mock('utils/utils', () => ({ - localizeMessage: () => {}, - applyTheme: jest.fn(), - makeIsEligibleForClick: jest.fn(), -})); +jest.mock('utils/utils', () => { + const original = jest.requireActual('utils/utils'); + + return { + ...original, + localizeMessage: () => {}, + applyTheme: jest.fn(), + makeIsEligibleForClick: jest.fn(), + + }; +}); jest.mock('mattermost-redux/actions/general', () => ({ setUrl: () => {}, diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 203a07f67fd..9e170aefd0e 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -2995,6 +2995,8 @@ "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.", @@ -3009,13 +3011,19 @@ "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", - "channel_notifications.onlyMentions": "Only for mentions", - "channel_notifications.override": "Selecting an option other than \"Default\" will override the global notification settings. Desktop notifications are available on Firefox, Safari, and Chrome.", - "channel_notifications.overridePush": "Selecting an option other than \"Global default\" will override the global notification settings for mobile push notifications in settings. Push notifications must be enabled by the System Admin.", + "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": "Send mobile push notifications", - "channel_notifications.sendDesktop": "Send desktop notifications", + "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_select.placeholder": "--- Select a channel ---", "channel_switch_modal.deactivated": "Deactivated", "channel_toggle_button.private": "Private", diff --git a/webapp/channels/src/packages/mattermost-redux/test/test_helper.ts b/webapp/channels/src/packages/mattermost-redux/test/test_helper.ts index ba8965e5ca7..568fde66f3b 100644 --- a/webapp/channels/src/packages/mattermost-redux/test/test_helper.ts +++ b/webapp/channels/src/packages/mattermost-redux/test/test_helper.ts @@ -449,6 +449,7 @@ class TestHelper { fakeChannelNotifyProps = (override: Partial): ChannelNotifyProps => { return { desktop: 'default', + desktop_sound: 'off', email: 'default', mark_unread: 'mention', push: 'default', diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index 93a10c261d8..e0c5b2fdadc 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -998,6 +998,11 @@ export const NotificationLevels = { NONE: 'none', } as const; +export const DesktopSound = { + ON: 'on', + OFF: 'off', +} as const; + export const IgnoreChannelMentions = { ON: 'on', OFF: 'off', diff --git a/webapp/platform/types/src/channels.ts b/webapp/platform/types/src/channels.ts index 19b9fa58d9c..bd5d6e48d07 100644 --- a/webapp/platform/types/src/channels.ts +++ b/webapp/platform/types/src/channels.ts @@ -21,6 +21,8 @@ export type ChannelStats = { export type ChannelNotifyProps = { 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';