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 6f1aa45dca..4263a5d522 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 @@ -144,8 +144,8 @@ describe('CRT Desktop notifications', () => { 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('#desktopNotification-mention').scrollIntoView().should('be.visible').click().then(() => { + cy.get('[data-testid="desktopReplyThreads"]').scrollIntoView().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(() => { @@ -174,7 +174,7 @@ describe('CRT Desktop notifications', () => { // Setup notification spy spyNotificationAs('notifySpy', 'granted'); cy.uiOpenChannelMenu('Notification Preferences'); - cy.get('.channel-notifications-settings-modal__body').scrollTo('center').get('#desktopNotification-mention').should('be.visible').click(); + cy.get('#desktopNotification-mention').scrollIntoView().should('be.visible').click(); // # Save the changes cy.findByText('Save').should('be.visible').click(); 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 0b27528c39..c4148ed0e4 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 @@ -220,6 +220,87 @@ Object { +
+

+ Sounds +

+
+
+ +
+
+
+
+
+
+ Bing +
+ +
+
+ +
+
+
+
+
+
+
+

+ Sounds +

+
+
+ +
+
+
+
+
+
+ Bing +
+ +
+
+ +
+
+
+
+
+
+
+

+ Sounds +

+
+
+ +
+
+
+
+
+
+ Bing +
+ +
+
+ +
+
+
+
+
+
+
+

+ Sounds +

+
+
+ +
+
+
+
+
+
+ Bing +
+ +
+
+ +
+
+
+
+
+
{ + renderWithContext(); + + // Since the default value is on, we will uncheck the checkbox + fireEvent.click(screen.getByTestId('desktopNotificationSoundsCheckbox')); + expect(screen.getByTestId('desktopNotificationSoundsCheckbox')).not.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: 'off', + mark_unread: 'all', + push: 'all', + desktop_sound: 'off', + desktop_notification_sound: 'Bing', + }, + ); + }); + }); + + test('should default to user desktop notification sound if reset to default is clicked', async () => { + renderWithContext(); + + // Since the default value is on, we will uncheck the checkbox + fireEvent.click(screen.getByTestId('desktopNotificationSoundsCheckbox')); + expect(screen.getByTestId('desktopNotificationSoundsCheckbox')).not.toBeChecked(); + + // Reset to default button is clicked + fireEvent.click(screen.getByTestId('resetToDefaultButton-desktop')); + + // Verify that the checkbox is checked to default to user desktop notification sound + expect(screen.getByTestId('desktopNotificationSoundsCheckbox')).toBeChecked(); + }); + test('should save the options exactly same as Desktop for mobile if use same as desktop checkbox is checked', async () => { const wrapper = renderWithContext( , @@ -170,6 +218,8 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', () ignore_channel_mentions: 'off', mark_unread: 'all', push: 'all', + desktop_sound: 'on', + desktop_notification_sound: 'Bing', }, ), ); @@ -209,6 +259,8 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', () ignore_channel_mentions: 'off', mark_unread: 'all', push: 'none', + desktop_sound: 'on', + desktop_notification_sound: 'Bing', }, ), ); @@ -240,6 +292,8 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', () push: 'all', push_threads: 'default', desktop_threads: 'all', + desktop_sound: 'on', + desktop_notification_sound: 'Bing', }, ), ); 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 1c12e76c5c..cfc2cc93ba 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 @@ -4,6 +4,7 @@ import React, {useCallback, useState} from 'react'; import {Modal} from 'react-bootstrap'; import {FormattedMessage, useIntl} from 'react-intl'; +import type {ValueType} from 'react-select'; import {BellOffOutlineIcon, RefreshIcon} from '@mattermost/compass-icons/components'; import type {Channel, ChannelNotifyProps} from '@mattermost/types/channels'; @@ -11,14 +12,17 @@ import type {UserNotifyProps, UserProfile} from '@mattermost/types/users'; import AlertBanner from 'components/alert_banner'; import CheckboxSettingItem from 'components/widgets/modals/components/checkbox_setting_item'; +import CheckboxWithSelectSettingItem from 'components/widgets/modals/components/checkbox_with_select_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 type {Option} from 'components/widgets/modals/components/react_select_item'; -import {IgnoreChannelMentions, NotificationLevels} from 'utils/constants'; +import {IgnoreChannelMentions, NotificationLevels, DesktopSound} from 'utils/constants'; +import {getValueOfNotificationSoundsSelect, notificationSoundKeys, stopTryNotificationRing, tryNotificationSound} from 'utils/notification_sounds'; import type {ChannelMemberNotifyProps} from './utils'; -import utils from './utils'; +import utils, {convertDesktopSoundNotifyPropFromUserToDesktop} from './utils'; import type {PropsFromRedux} from './index'; @@ -63,9 +67,27 @@ function getStateFromNotifyProps(currentUserNotifyProps: UserNotifyProps, channe const desktop = channelMemberNotifyProps?.desktop === NotificationLevels.DEFAULT ? currentUserNotifyProps.desktop : (channelMemberNotifyProps?.desktop || currentUserNotifyProps.desktop); const push = channelMemberNotifyProps?.push === NotificationLevels.DEFAULT ? currentUserNotifyProps.desktop : (channelMemberNotifyProps?.push || currentUserNotifyProps.push); + let desktopSound; + if (channelMemberNotifyProps && channelMemberNotifyProps.desktop_sound) { + desktopSound = channelMemberNotifyProps.desktop_sound; + } else { + desktopSound = convertDesktopSoundNotifyPropFromUserToDesktop(currentUserNotifyProps.desktop_sound); + } + + let desktopNotificationSound; + if (channelMemberNotifyProps && channelMemberNotifyProps.desktop_notification_sound) { + desktopNotificationSound = channelMemberNotifyProps.desktop_notification_sound; + } else if (currentUserNotifyProps && currentUserNotifyProps.desktop_notification_sound) { + desktopNotificationSound = currentUserNotifyProps.desktop_notification_sound; + } else { + desktopNotificationSound = notificationSoundKeys[0] as ChannelNotifyProps['desktop_notification_sound']; + } + return { desktop, desktop_threads: channelMemberNotifyProps?.desktop_threads || NotificationLevels.ALL, + desktop_sound: desktopSound, + desktop_notification_sound: desktopNotificationSound, mark_unread: channelMemberNotifyProps?.mark_unread || NotificationLevels.ALL, push, push_threads: channelMemberNotifyProps?.push_threads || NotificationLevels.ALL, @@ -77,6 +99,8 @@ function getStateFromNotifyProps(currentUserNotifyProps: UserNotifyProps, channe type SettingsType = { desktop: ChannelNotifyProps['desktop']; desktop_threads: ChannelNotifyProps['desktop_threads']; + desktop_sound: ChannelNotifyProps['desktop_sound']; + desktop_notification_sound: ChannelNotifyProps['desktop_notification_sound']; mark_unread: ChannelNotifyProps['mark_unread']; push: ChannelNotifyProps['push']; push_threads: ChannelNotifyProps['push_threads']; @@ -104,7 +128,7 @@ export default function ChannelNotificationsModal(props: Props) { setSettings((prevSettings) => ({...prevSettings, push: prevSettings.desktop, push_threads: prevSettings.desktop_threads})); }, []); - const MuteOrIgnoreSectionContent = ( + const muteOrIgnoreSectionContent = ( <> ); - const DesktopNotificationsSectionContent = ( + const handleChangeForMessageNotificationSoundSelect = (selectedOption: ValueType
) : ( @@ -359,7 +436,7 @@ export default function ChannelNotificationsModal(props: Props) { id: 'channel_notifications.muteAndIgnore', defaultMessage: 'Mute or ignore', })} - content={MuteOrIgnoreSectionContent} + content={muteOrIgnoreSectionContent} /> {desktopAndMobileNotificationSectionContent} {props.collapsedReplyThreads && @@ -374,7 +451,7 @@ export default function ChannelNotificationsModal(props: Props) { id: 'channel_notifications.autoFollowThreadsDesc', defaultMessage: 'When enabled, all new replies in this channel will be automatically followed and will appear in your Threads view.', })} - content={AutoFollowThreadsSectionContent} + content={autoFollowThreadsSectionContent} /> } @@ -407,3 +484,4 @@ export default function ChannelNotificationsModal(props: Props) { ); } + diff --git a/webapp/channels/src/components/channel_notifications_modal/utils.tsx b/webapp/channels/src/components/channel_notifications_modal/utils.tsx index 96ec724e2f..84ba88b8fb 100644 --- a/webapp/channels/src/components/channel_notifications_modal/utils.tsx +++ b/webapp/channels/src/components/channel_notifications_modal/utils.tsx @@ -9,8 +9,10 @@ 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 type {FieldsetReactSelect} from 'components/widgets/modals/components/react_select_item'; -import {NotificationLevels} from 'utils/constants'; +import {DesktopSound, NotificationLevels} from 'utils/constants'; +import {optionsOfMessageNotificationSoundsSelect} from 'utils/notification_sounds'; export type ChannelMemberNotifyProps = Partial & Pick @@ -102,6 +104,17 @@ export const desktopNotificationInputFieldData = (defaultOption: string): Fields }; }; +export const desktopNotificationSoundsCheckboxFieldData: FieldsetCheckbox = { + name: 'desktopNotificationSoundsCheckbox', + dataTestId: 'desktopNotificationSoundsCheckbox', +}; + +export const desktopNotificationSoundsSelectFieldData: FieldsetReactSelect = { + id: 'desktopNotificationSoundsSelect', + inputId: 'desktopNotificationSoundsSelectInputId', + options: optionsOfMessageNotificationSoundsSelect, +}; + export const mobileNotificationInputFieldData = (defaultOption: string): FieldsetRadio => { return { options: [ @@ -160,8 +173,26 @@ export const mobileNotificationInputFieldData = (defaultOption: string): Fieldse }; }; +/** + * This conversion is needed because User's preference for desktop sound is stored as either true or false. On the other hand, + * Channel's specific desktop sound is stored as either On or Off. + */ +export function convertDesktopSoundNotifyPropFromUserToDesktop(userNotifyDesktopSound?: UserNotifyProps['desktop_sound']) { + if (!userNotifyDesktopSound) { + return DesktopSound.OFF; + } + + if (userNotifyDesktopSound === 'true') { + return DesktopSound.ON; + } + + return DesktopSound.ON; +} + const utils = { desktopNotificationInputFieldData, + desktopNotificationSoundsCheckboxFieldData, + desktopNotificationSoundsSelectFieldData, IgnoreMentionsInputFieldData, mobileNotificationInputFieldData, MuteChannelInputFieldData, diff --git a/webapp/channels/src/components/user_settings/notifications/desktop_notification_sounds_setting/index.tsx b/webapp/channels/src/components/user_settings/notifications/desktop_notification_sounds_setting/index.tsx index b8cfb1c81d..4e38d9642e 100644 --- a/webapp/channels/src/components/user_settings/notifications/desktop_notification_sounds_setting/index.tsx +++ b/webapp/channels/src/components/user_settings/notifications/desktop_notification_sounds_setting/index.tsx @@ -15,11 +15,15 @@ import type SettingItemMinComponent from 'components/setting_item_min'; import {UserSettingsNotificationSections} from 'utils/constants'; import { - callsNotificationSounds, - notificationSounds, + notificationSoundKeys, stopTryNotificationRing, tryNotificationSound, tryNotificationRing, + getValueOfNotificationSoundsSelect, + getValueOfIncomingCallSoundsSelect, + optionsOfMessageNotificationSoundsSelect, + optionsOfIncomingCallSoundsSelect, + callNotificationSoundKeys, } from 'utils/notification_sounds'; import type {Props as UserSettingsNotificationsProps} from '../user_settings_notifications'; @@ -143,7 +147,7 @@ function DesktopNotificationSoundsSettings({ defaultMessage: 'Select a sound', })} components={{IndicatorSeparator: NoIndicatorSeparatorComponent}} - value={getValueOfMessageNotificationSoundsSelect(desktopNotificationSound)} + value={getValueOfNotificationSoundsSelect(desktopNotificationSound)} onChange={handleChangeForMessageNotificationSoundSelect} />
@@ -259,146 +263,6 @@ function NoIndicatorSeparatorComponent() { return null; } -const notificationSoundKeys = Array.from(notificationSounds.keys()); - -const optionsOfMessageNotificationSoundsSelect: SelectOption[] = notificationSoundKeys.map((soundName) => { - if (soundName === 'Bing') { - return { - value: soundName, - label: ( - - ), - }; - } else if (soundName === 'Crackle') { - return { - value: soundName, - label: ( - - ), - }; - } else if (soundName === 'Down') { - return { - value: soundName, - label: ( - - ), - }; - } else if (soundName === 'Hello') { - return { - value: soundName, - label: ( - - ), - }; - } else if (soundName === 'Ripple') { - return { - value: soundName, - label: ( - - ), - }; - } else if (soundName === 'Upstairs') { - return { - value: soundName, - label: ( - - ), - }; - } - return { - value: '', - label: '', - }; -}); - -function getValueOfMessageNotificationSoundsSelect(soundName?: string) { - const soundOption = optionsOfMessageNotificationSoundsSelect.find((option) => option.value === soundName); - - if (!soundOption) { - return undefined; - } - - return soundOption; -} - -const callNotificationSoundKeys = Array.from(callsNotificationSounds.keys()); - -const optionsOfIncomingCallSoundsSelect: SelectOption[] = callNotificationSoundKeys.map((soundName) => { - if (soundName === 'Dynamic') { - return { - value: soundName, - label: ( - - ), - }; - } else if (soundName === 'Calm') { - return { - value: soundName, - label: ( - - ), - }; - } else if (soundName === 'Urgent') { - return { - value: soundName, - label: ( - - ), - }; - } else if (soundName === 'Cheerful') { - return { - value: soundName, - label: ( - - ), - }; - } - return { - value: '', - label: '', - }; -}); - -function getValueOfIncomingCallSoundsSelect(soundName?: string) { - const soundOption = optionsOfIncomingCallSoundsSelect.find((option) => option.value === soundName); - - if (!soundOption) { - return undefined; - } - - return soundOption; -} - function getCollapsedText( isCallsRingingEnabled: UserSettingsNotificationsProps['isCallsRingingEnabled'], desktopSound: UserNotifyProps['desktop_sound'], diff --git a/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.tsx b/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.tsx index 3488639681..fae5fddebe 100644 --- a/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.tsx +++ b/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.tsx @@ -20,7 +20,7 @@ import SettingItemMax from 'components/setting_item_max'; import RestrictedIndicator from 'components/widgets/menu/menu_items/restricted_indicator'; import Constants, {NotificationLevels, MattermostFeatures, LicenseSkus, UserSettingsNotificationSections} from 'utils/constants'; -import {stopTryNotificationRing} from 'utils/notification_sounds'; +import {notificationSoundKeys, stopTryNotificationRing} from 'utils/notification_sounds'; import {a11yFocus} from 'utils/utils'; import DesktopAndMobileNotificationSettings from './desktop_and_mobile_notification_setting'; @@ -88,7 +88,7 @@ function getDefaultStateFromProps(props: Props): State { let emailThreads: UserNotifyProps['email_threads'] = NotificationLevels.ALL; let sound: UserNotifyProps['desktop_sound'] = 'true'; let callsSound: UserNotifyProps['calls_desktop_sound'] = 'true'; - let desktopNotificationSound: UserNotifyProps['desktop_notification_sound'] = 'Bing'; + let desktopNotificationSound: UserNotifyProps['desktop_notification_sound'] = notificationSoundKeys[0] as UserNotifyProps['desktop_notification_sound']; let callsNotificationSound: UserNotifyProps['calls_notification_sound'] = 'Calm'; let comments: UserNotifyProps['comments'] = 'never'; let enableEmail: UserNotifyProps['email'] = 'true'; diff --git a/webapp/channels/src/components/widgets/modals/components/base_setting_item.scss b/webapp/channels/src/components/widgets/modals/components/base_setting_item.scss index b00b72d518..bc9f3111f9 100644 --- a/webapp/channels/src/components/widgets/modals/components/base_setting_item.scss +++ b/webapp/channels/src/components/widgets/modals/components/base_setting_item.scss @@ -50,6 +50,11 @@ &__content { display: flex; flex-direction: column; + + &.inline { + flex-direction: row; + gap: 24px; + } } &__fieldset-checkbox-ctr { @@ -90,9 +95,10 @@ line-height: 20px; } - // Todo: add styling for react select - //&__fieldset-react-select{ - //} + &__fieldset-react-select { + max-width: 240px; + flex-grow: 1; + } &__input-checkbox { width: 1.6rem; diff --git a/webapp/channels/src/components/widgets/modals/components/base_setting_item.tsx b/webapp/channels/src/components/widgets/modals/components/base_setting_item.tsx index 1cb5b5bdc7..60fd4e6734 100644 --- a/webapp/channels/src/components/widgets/modals/components/base_setting_item.tsx +++ b/webapp/channels/src/components/widgets/modals/components/base_setting_item.tsx @@ -23,11 +23,12 @@ export type BaseSettingItemProps = { type Props = BaseSettingItemProps & { content: JSX.Element; + isContentInline?: boolean; className?: string; descriptionAboveContent?: boolean; } -function BaseSettingItem({title, description, content, className, error, descriptionAboveContent = false}: Props): JSX.Element { +function BaseSettingItem({title, description, content, className, error, descriptionAboveContent = false, isContentInline = false}: Props): JSX.Element { const {formatMessage} = useIntl(); const titleComponent = title && ( @@ -64,7 +65,9 @@ function BaseSettingItem({title, description, content, className, error, descrip {descriptionAboveContent ? descriptionComponent : undefined}
{content}
diff --git a/webapp/channels/src/components/widgets/modals/components/checkbox_with_select_item.tsx b/webapp/channels/src/components/widgets/modals/components/checkbox_with_select_item.tsx new file mode 100644 index 0000000000..b43e336f1b --- /dev/null +++ b/webapp/channels/src/components/widgets/modals/components/checkbox_with_select_item.tsx @@ -0,0 +1,101 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {ReactNode} from 'react'; +import React from 'react'; +import ReactSelect from 'react-select'; +import type {ValueType} from 'react-select'; + +import type {BaseSettingItemProps} from './base_setting_item'; +import BaseSettingItem from './base_setting_item'; +import type {FieldsetCheckbox} from './checkbox_setting_item'; +import type {FieldsetReactSelect, Option} from './react_select_item'; + +type Props = BaseSettingItemProps & { + containerClassName?: string; + descriptionAboveContent?: boolean; + checkboxFieldTitle: ReactNode; + checkboxFieldData: FieldsetCheckbox; + checkboxFieldValue: boolean; + handleCheckboxChange: (e: boolean) => void; + selectFieldData: FieldsetReactSelect; + selectFieldValue?: Option; + handleSelectChange: (selected: ValueType