[MM-57080] Add Channel-specific notification sound options in new Notifications settings modal (#27730)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
e121bebba5
Коммит
b0d58bb65e
@@ -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();
|
||||
|
||||
@@ -220,6 +220,87 @@ Object {
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mm-modal-generic-section-item"
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section-item__title"
|
||||
data-testid="mm-modal-generic-section-item__title"
|
||||
>
|
||||
Sounds
|
||||
</h4>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content inline"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
>
|
||||
<label
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="mm-modal-generic-section-item__input-checkbox"
|
||||
data-testid="desktopNotificationSoundsCheckbox"
|
||||
name="desktopNotificationSoundsCheckbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
Message notification sounds
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-react-select"
|
||||
>
|
||||
<div
|
||||
class="react-select singleSelect react-select-top css-2b097c-container"
|
||||
id="desktopNotificationSoundsSelect"
|
||||
>
|
||||
<div
|
||||
class="react-select__control css-yk16xz-control"
|
||||
>
|
||||
<div
|
||||
class="react-select__value-container react-select__value-container--has-value css-1hwfws3"
|
||||
>
|
||||
<div
|
||||
class="react-select__single-value css-1uccc91-singleValue"
|
||||
>
|
||||
Bing
|
||||
</div>
|
||||
<input
|
||||
class="css-62g3xt-dummyInput"
|
||||
id="desktopNotificationSoundsSelectInputId"
|
||||
readonly=""
|
||||
tabindex="0"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="react-select__indicator react-select__dropdown-indicator css-tlfecz-indicatorContainer"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="css-6q0nyr-Svg"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div
|
||||
@@ -237,6 +318,7 @@ Object {
|
||||
Mobile Notifications
|
||||
<button
|
||||
class="channel-notifications-settings-modal__reset-btn"
|
||||
data-testid="resetToDefaultButton-push"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
@@ -579,6 +661,7 @@ Object {
|
||||
Desktop Notifications
|
||||
<button
|
||||
class="channel-notifications-settings-modal__reset-btn"
|
||||
data-testid="resetToDefaultButton-desktop"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
@@ -678,6 +761,7 @@ Object {
|
||||
Mobile Notifications
|
||||
<button
|
||||
class="channel-notifications-settings-modal__reset-btn"
|
||||
data-testid="resetToDefaultButton-push"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
@@ -1085,6 +1169,87 @@ Object {
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mm-modal-generic-section-item"
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section-item__title"
|
||||
data-testid="mm-modal-generic-section-item__title"
|
||||
>
|
||||
Sounds
|
||||
</h4>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content inline"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
>
|
||||
<label
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="mm-modal-generic-section-item__input-checkbox"
|
||||
data-testid="desktopNotificationSoundsCheckbox"
|
||||
name="desktopNotificationSoundsCheckbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
Message notification sounds
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-react-select"
|
||||
>
|
||||
<div
|
||||
class="react-select singleSelect react-select-top css-2b097c-container"
|
||||
id="desktopNotificationSoundsSelect"
|
||||
>
|
||||
<div
|
||||
class="react-select__control css-yk16xz-control"
|
||||
>
|
||||
<div
|
||||
class="react-select__value-container react-select__value-container--has-value css-1hwfws3"
|
||||
>
|
||||
<div
|
||||
class="react-select__single-value css-1uccc91-singleValue"
|
||||
>
|
||||
Bing
|
||||
</div>
|
||||
<input
|
||||
class="css-62g3xt-dummyInput"
|
||||
id="desktopNotificationSoundsSelectInputId"
|
||||
readonly=""
|
||||
tabindex="0"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="react-select__indicator react-select__dropdown-indicator css-tlfecz-indicatorContainer"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="css-6q0nyr-Svg"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div
|
||||
@@ -1102,6 +1267,7 @@ Object {
|
||||
Mobile Notifications
|
||||
<button
|
||||
class="channel-notifications-settings-modal__reset-btn"
|
||||
data-testid="resetToDefaultButton-push"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
@@ -1764,6 +1930,87 @@ Object {
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mm-modal-generic-section-item"
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section-item__title"
|
||||
data-testid="mm-modal-generic-section-item__title"
|
||||
>
|
||||
Sounds
|
||||
</h4>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content inline"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
>
|
||||
<label
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="mm-modal-generic-section-item__input-checkbox"
|
||||
data-testid="desktopNotificationSoundsCheckbox"
|
||||
name="desktopNotificationSoundsCheckbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
Message notification sounds
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-react-select"
|
||||
>
|
||||
<div
|
||||
class="react-select singleSelect react-select-top css-2b097c-container"
|
||||
id="desktopNotificationSoundsSelect"
|
||||
>
|
||||
<div
|
||||
class="react-select__control css-yk16xz-control"
|
||||
>
|
||||
<div
|
||||
class="react-select__value-container react-select__value-container--has-value css-1hwfws3"
|
||||
>
|
||||
<div
|
||||
class="react-select__single-value css-1uccc91-singleValue"
|
||||
>
|
||||
Bing
|
||||
</div>
|
||||
<input
|
||||
class="css-62g3xt-dummyInput"
|
||||
id="desktopNotificationSoundsSelectInputId"
|
||||
readonly=""
|
||||
tabindex="0"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="react-select__indicator react-select__dropdown-indicator css-tlfecz-indicatorContainer"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="css-6q0nyr-Svg"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div
|
||||
@@ -1781,6 +2028,7 @@ Object {
|
||||
Mobile Notifications
|
||||
<button
|
||||
class="channel-notifications-settings-modal__reset-btn"
|
||||
data-testid="resetToDefaultButton-push"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
@@ -2132,6 +2380,87 @@ Object {
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mm-modal-generic-section-item"
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section-item__title"
|
||||
data-testid="mm-modal-generic-section-item__title"
|
||||
>
|
||||
Sounds
|
||||
</h4>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content inline"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
>
|
||||
<label
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="mm-modal-generic-section-item__input-checkbox"
|
||||
data-testid="desktopNotificationSoundsCheckbox"
|
||||
name="desktopNotificationSoundsCheckbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
Message notification sounds
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-react-select"
|
||||
>
|
||||
<div
|
||||
class="react-select singleSelect react-select-top css-2b097c-container"
|
||||
id="desktopNotificationSoundsSelect"
|
||||
>
|
||||
<div
|
||||
class="react-select__control css-yk16xz-control"
|
||||
>
|
||||
<div
|
||||
class="react-select__value-container react-select__value-container--has-value css-1hwfws3"
|
||||
>
|
||||
<div
|
||||
class="react-select__single-value css-1uccc91-singleValue"
|
||||
>
|
||||
Bing
|
||||
</div>
|
||||
<input
|
||||
class="css-62g3xt-dummyInput"
|
||||
id="desktopNotificationSoundsSelectInputId"
|
||||
readonly=""
|
||||
tabindex="0"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="react-select__indicator react-select__dropdown-indicator css-tlfecz-indicatorContainer"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="css-6q0nyr-Svg"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div
|
||||
@@ -2149,6 +2478,7 @@ Object {
|
||||
Mobile Notifications
|
||||
<button
|
||||
class="channel-notifications-settings-modal__reset-btn"
|
||||
data-testid="resetToDefaultButton-push"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
|
||||
@@ -24,6 +24,8 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', ()
|
||||
channelMember: {
|
||||
notify_props: {
|
||||
desktop: NotificationLevels.ALL,
|
||||
desktop_sound: 'on',
|
||||
desktop_notification_sound: 'Bing',
|
||||
mark_unread: NotificationLevels.ALL,
|
||||
push: NotificationLevels.DEFAULT,
|
||||
ignore_channel_mentions: IgnoreChannelMentions.DEFAULT,
|
||||
@@ -35,6 +37,8 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', ()
|
||||
id: 'current_user_id',
|
||||
notify_props: {
|
||||
desktop: NotificationLevels.ALL,
|
||||
desktop_sound: 'true',
|
||||
desktop_notification_sound: 'Bing',
|
||||
desktop_threads: NotificationLevels.ALL,
|
||||
} as UserNotifyProps,
|
||||
}),
|
||||
@@ -73,6 +77,8 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', ()
|
||||
ignore_channel_mentions: 'off',
|
||||
mark_unread: 'mention',
|
||||
push: 'all',
|
||||
desktop_sound: 'on',
|
||||
desktop_notification_sound: 'Bing',
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -98,6 +104,8 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', ()
|
||||
mark_unread:
|
||||
baseProps.channelMember?.notify_props.mark_unread,
|
||||
push: 'all',
|
||||
desktop_sound: 'on',
|
||||
desktop_notification_sound: 'Bing',
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -139,12 +147,52 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', ()
|
||||
ignore_channel_mentions: 'off',
|
||||
mark_unread: 'all',
|
||||
push: 'all',
|
||||
desktop_sound: 'on',
|
||||
desktop_notification_sound: 'Bing',
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should disable message notification sound if option is unchecked', async () => {
|
||||
renderWithContext(<ChannelNotificationsModal {...baseProps}/>);
|
||||
|
||||
// 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(<ChannelNotificationsModal {...baseProps}/>);
|
||||
|
||||
// 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(
|
||||
<ChannelNotificationsModal {...baseProps}/>,
|
||||
@@ -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',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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 = (
|
||||
<>
|
||||
<CheckboxSettingItem
|
||||
inputFieldTitle={
|
||||
@@ -139,7 +163,16 @@ export default function ChannelNotificationsModal(props: Props) {
|
||||
</>
|
||||
);
|
||||
|
||||
const DesktopNotificationsSectionContent = (
|
||||
const handleChangeForMessageNotificationSoundSelect = (selectedOption: ValueType<Option>) => {
|
||||
stopTryNotificationRing();
|
||||
|
||||
if (selectedOption && 'value' in selectedOption) {
|
||||
handleChange({desktop_notification_sound: ((selectedOption as Option).value)});
|
||||
tryNotificationSound(selectedOption.value);
|
||||
}
|
||||
};
|
||||
|
||||
const desktopNotificationsSectionContent = (
|
||||
<>
|
||||
<RadioSettingItem
|
||||
title={formatMessage({
|
||||
@@ -165,11 +198,37 @@ export default function ChannelNotificationsModal(props: Props) {
|
||||
/>
|
||||
}
|
||||
handleChange={(e) => handleChange({desktop_threads: e ? 'all' : 'mention'})}
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
{settings.desktop !== 'none' && (
|
||||
<CheckboxWithSelectSettingItem
|
||||
title={formatMessage({
|
||||
id: 'channel_notifications.desktopNotifications.title',
|
||||
defaultMessage: 'Sounds',
|
||||
})}
|
||||
checkboxFieldTitle={
|
||||
<FormattedMessage
|
||||
id='channel_notifications.desktopNotifications.soundEnable'
|
||||
defaultMessage='Message notification sounds'
|
||||
/>
|
||||
}
|
||||
checkboxFieldValue={settings.desktop_sound === DesktopSound.ON}
|
||||
checkboxFieldData={utils.desktopNotificationSoundsCheckboxFieldData}
|
||||
handleCheckboxChange={(isChecked) => handleChange({desktop_sound: isChecked ? DesktopSound.ON : DesktopSound.OFF})}
|
||||
selectFieldData={utils.desktopNotificationSoundsSelectFieldData}
|
||||
selectFieldValue={getValueOfNotificationSoundsSelect(settings.desktop_notification_sound)}
|
||||
isSelectDisabled={settings.desktop_sound !== 'on'}
|
||||
selectPlaceholder={formatMessage({
|
||||
id: 'channel_notifications.desktopNotifications.soundSelectPlaceholder',
|
||||
defaultMessage: 'Select a sound',
|
||||
})}
|
||||
handleSelectChange={handleChangeForMessageNotificationSoundSelect}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const MobileNotificationsSectionContent = (
|
||||
const mobileNotificationsSectionContent = (
|
||||
<>
|
||||
<CheckboxSettingItem
|
||||
inputFieldTitle={
|
||||
@@ -214,7 +273,7 @@ export default function ChannelNotificationsModal(props: Props) {
|
||||
</>
|
||||
);
|
||||
|
||||
const AutoFollowThreadsSectionContent = (
|
||||
const autoFollowThreadsSectionContent = (
|
||||
<CheckboxSettingItem
|
||||
inputFieldTitle={
|
||||
<FormattedMessage
|
||||
@@ -245,39 +304,57 @@ export default function ChannelNotificationsModal(props: Props) {
|
||||
});
|
||||
}
|
||||
|
||||
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 resetToDefaultBtn = useCallback((sectionName: string) => {
|
||||
const userNotifyProps = {
|
||||
...props.currentUser.notify_props,
|
||||
desktop_notification_sound: props.currentUser.notify_props?.desktop_notification_sound ?? notificationSoundKeys[0] as ChannelNotifyProps['desktop_notification_sound'],
|
||||
};
|
||||
|
||||
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 <></>;
|
||||
function resetToDefault(sectionName: string) {
|
||||
if (sectionName === 'desktop') {
|
||||
setSettings({
|
||||
...settings,
|
||||
desktop: userNotifyProps.desktop,
|
||||
desktop_threads: userNotifyProps.desktop_threads || settings.desktop_threads,
|
||||
desktop_sound: convertDesktopSoundNotifyPropFromUserToDesktop(userNotifyProps.desktop_sound),
|
||||
desktop_notification_sound: userNotifyProps?.desktop_notification_sound ?? notificationSoundKeys[0] as ChannelNotifyProps['desktop_notification_sound'],
|
||||
});
|
||||
}
|
||||
|
||||
if (sectionName === 'push') {
|
||||
setSettings({...settings, push: userNotifyProps.desktop, push_threads: userNotifyProps.push_threads || settings.push_threads});
|
||||
}
|
||||
}
|
||||
|
||||
const isDesktopSameAsDefault =
|
||||
userNotifyProps.desktop === settings.desktop &&
|
||||
userNotifyProps.desktop_threads === settings.desktop_threads &&
|
||||
userNotifyProps.desktop_notification_sound === settings.desktop_notification_sound &&
|
||||
convertDesktopSoundNotifyPropFromUserToDesktop(userNotifyProps.desktop_sound) === settings.desktop_sound;
|
||||
|
||||
const isPushSameAsDefault = (userNotifyProps.push === settings.push && userNotifyProps.push_threads === settings.push_threads);
|
||||
|
||||
if ((sectionName === 'desktop' && isDesktopSameAsDefault) || (sectionName === 'push' && isPushSameAsDefault)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className='channel-notifications-settings-modal__reset-btn'
|
||||
onClick={() => resetToDefault(settingName)}
|
||||
onClick={() => resetToDefault(sectionName)}
|
||||
data-testid={`resetToDefaultButton-${sectionName}`}
|
||||
>
|
||||
<RefreshIcon
|
||||
size={14}
|
||||
color={'currentColor'}
|
||||
/>
|
||||
{formatMessage({
|
||||
id: 'channel_notifications.resetToDefault',
|
||||
defaultMessage: 'Reset to default',
|
||||
})}
|
||||
<FormattedMessage
|
||||
id='channel_notifications.resetToDefault'
|
||||
defaultMessage='Reset to default'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}, [props.currentUser, settings]);
|
||||
}, [props.currentUser.notify_props, settings]);
|
||||
|
||||
const desktopAndMobileNotificationSectionContent = settings.mark_unread === 'all' ? (
|
||||
<>
|
||||
@@ -292,7 +369,7 @@ export default function ChannelNotificationsModal(props: Props) {
|
||||
id: 'channel_notifications.desktopNotificationsDesc',
|
||||
defaultMessage: 'Available on Chrome, Edge, Firefox, and the Mattermost Desktop App.',
|
||||
})}
|
||||
content={DesktopNotificationsSectionContent}
|
||||
content={desktopNotificationsSectionContent}
|
||||
/>
|
||||
<div className='channel-notifications-settings-modal__divider'/>
|
||||
<ModalSection
|
||||
@@ -305,7 +382,7 @@ export default function ChannelNotificationsModal(props: Props) {
|
||||
id: 'channel_notifications.mobileNotificationsDesc',
|
||||
defaultMessage: 'Notification alerts are pushed to your mobile device when there is activity in Mattermost.',
|
||||
})}
|
||||
content={MobileNotificationsSectionContent}
|
||||
content={mobileNotificationsSectionContent}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
@@ -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) {
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ChannelNotifyProps> & Pick<UserNotifyProps, 'desktop_threads' | 'push_threads'>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
@@ -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: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundBing'
|
||||
defaultMessage='Bing'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Crackle') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundCrackle'
|
||||
defaultMessage='Crackle'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Down') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundDown'
|
||||
defaultMessage='Down'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Hello') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundHello'
|
||||
defaultMessage='Hello'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Ripple') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundRipple'
|
||||
defaultMessage='Ripple'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Upstairs') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundUpstairs'
|
||||
defaultMessage='Upstairs'
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
value: '',
|
||||
label: '',
|
||||
};
|
||||
});
|
||||
|
||||
function getValueOfMessageNotificationSoundsSelect(soundName?: string) {
|
||||
const soundOption = optionsOfMessageNotificationSoundsSelect.find((option) => option.value === soundName);
|
||||
|
||||
if (!soundOption) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return soundOption;
|
||||
}
|
||||
|
||||
const callNotificationSoundKeys = Array.from(callsNotificationSounds.keys());
|
||||
|
||||
const optionsOfIncomingCallSoundsSelect: SelectOption[] = callNotificationSoundKeys.map((soundName) => {
|
||||
if (soundName === 'Dynamic') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundDynamic'
|
||||
defaultMessage='Dynamic'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Calm') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundCalm'
|
||||
defaultMessage='Calm'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Urgent') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundUrgent'
|
||||
defaultMessage='Urgent'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Cheerful') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundCheerful'
|
||||
defaultMessage='Cheerful'
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
value: '',
|
||||
label: '',
|
||||
};
|
||||
});
|
||||
|
||||
function getValueOfIncomingCallSoundsSelect(soundName?: string) {
|
||||
const soundOption = optionsOfIncomingCallSoundsSelect.find((option) => option.value === soundName);
|
||||
|
||||
if (!soundOption) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return soundOption;
|
||||
}
|
||||
|
||||
function getCollapsedText(
|
||||
isCallsRingingEnabled: UserSettingsNotificationsProps['isCallsRingingEnabled'],
|
||||
desktopSound: UserNotifyProps['desktop_sound'],
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
<div
|
||||
data-testid='mm-modal-generic-section-item__content'
|
||||
className='mm-modal-generic-section-item__content'
|
||||
className={classNames('mm-modal-generic-section-item__content', {
|
||||
inline: isContentInline,
|
||||
})}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
|
||||
@@ -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<Option>) => void;
|
||||
isSelectDisabled?: boolean;
|
||||
selectPlaceholder?: string;
|
||||
}
|
||||
|
||||
export default function CheckboxWithSelectSettingItem({
|
||||
title,
|
||||
description,
|
||||
containerClassName,
|
||||
descriptionAboveContent = false,
|
||||
checkboxFieldTitle,
|
||||
checkboxFieldData,
|
||||
checkboxFieldValue,
|
||||
handleCheckboxChange,
|
||||
selectFieldData,
|
||||
selectFieldValue,
|
||||
handleSelectChange,
|
||||
isSelectDisabled,
|
||||
selectPlaceholder,
|
||||
}: Props) {
|
||||
const content = (
|
||||
<>
|
||||
<fieldset
|
||||
key={checkboxFieldData.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={checkboxFieldData.dataTestId}
|
||||
type='checkbox'
|
||||
name={checkboxFieldData.name}
|
||||
checked={checkboxFieldValue}
|
||||
onChange={(e) => handleCheckboxChange(e.target.checked)}
|
||||
/>
|
||||
{checkboxFieldTitle}
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset className='mm-modal-generic-section-item__fieldset-react-select'>
|
||||
<ReactSelect
|
||||
id={selectFieldData.id}
|
||||
inputId={selectFieldData.inputId}
|
||||
aria-labelledby={selectFieldData.ariaLabelledby}
|
||||
className='react-select singleSelect react-select-top'
|
||||
classNamePrefix='react-select'
|
||||
options={selectFieldData.options}
|
||||
clearable={selectFieldData.clearable}
|
||||
isClearable={selectFieldData.clearable}
|
||||
isDisabled={isSelectDisabled}
|
||||
isSearchable={false}
|
||||
placeholder={selectPlaceholder}
|
||||
onChange={(value) => handleSelectChange(value)}
|
||||
value={selectFieldValue}
|
||||
components={{IndicatorSeparator: NoIndicatorSeparatorComponent}}
|
||||
/>
|
||||
</fieldset>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseSettingItem
|
||||
title={title}
|
||||
content={content}
|
||||
isContentInline={true}
|
||||
description={description}
|
||||
className={containerClassName}
|
||||
descriptionAboveContent={descriptionAboveContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NoIndicatorSeparatorComponent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export type SelectOption = {
|
||||
value: string;
|
||||
label: ReactNode;
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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 type {ValueType} from 'react-select';
|
||||
import ReactSelect from 'react-select';
|
||||
@@ -9,12 +10,17 @@ import type {BaseSettingItemProps} from './base_setting_item';
|
||||
import BaseSettingItem from './base_setting_item';
|
||||
|
||||
export type Option = {
|
||||
value: number;
|
||||
label: string;
|
||||
value: string;
|
||||
label: ReactNode;
|
||||
};
|
||||
|
||||
export type FieldsetReactSelect = {
|
||||
id: string;
|
||||
name?: string;
|
||||
inputId?: string;
|
||||
dataTestId?: string;
|
||||
ariaLabelledby?: string;
|
||||
clearable?: boolean;
|
||||
options: Option[];
|
||||
}
|
||||
|
||||
@@ -23,6 +29,7 @@ type Props = BaseSettingItemProps & {
|
||||
inputFieldValue: Option;
|
||||
handleChange: (selected: ValueType<Option>) => void;
|
||||
}
|
||||
|
||||
function ReactSelectItemCreator({
|
||||
title,
|
||||
description,
|
||||
@@ -33,19 +40,23 @@ function ReactSelectItemCreator({
|
||||
const content = (
|
||||
<fieldset className='mm-modal-generic-section-item__fieldset-react-select'>
|
||||
<ReactSelect
|
||||
className='react-select'
|
||||
id={inputFieldData.id}
|
||||
name={inputFieldData.name}
|
||||
inputId={inputFieldData.inputId}
|
||||
aria-labelledby={inputFieldData.ariaLabelledby}
|
||||
className='react-select singleSelect react-select-top'
|
||||
classNamePrefix='react-select'
|
||||
id='limitVisibleGMsDMs'
|
||||
options={inputFieldData.options}
|
||||
clearable={false}
|
||||
clearable={inputFieldData.clearable}
|
||||
isClearable={inputFieldData.clearable}
|
||||
isSearchable={false}
|
||||
onChange={handleChange}
|
||||
value={inputFieldValue}
|
||||
isSearchable={false}
|
||||
menuPortalTarget={document.body}
|
||||
styles={reactStyles}
|
||||
components={{IndicatorSeparator: NoIndicatorSeparatorComponent}}
|
||||
/>
|
||||
</fieldset>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseSettingItem
|
||||
content={content}
|
||||
@@ -57,9 +68,6 @@ function ReactSelectItemCreator({
|
||||
|
||||
export default ReactSelectItemCreator;
|
||||
|
||||
const reactStyles = {
|
||||
menuPortal: (provided: React.CSSProperties) => ({
|
||||
...provided,
|
||||
zIndex: 9999,
|
||||
}),
|
||||
};
|
||||
function NoIndicatorSeparatorComponent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3202,6 +3202,9 @@
|
||||
"channel_notifications.desktopNotificationAllLabel": "All new messages",
|
||||
"channel_notifications.desktopNotificationMentionLabel": "Mentions, direct messages, and keywords only",
|
||||
"channel_notifications.desktopNotificationNothingLabel": "Nothing",
|
||||
"channel_notifications.desktopNotifications.soundEnable": "Message notification sounds",
|
||||
"channel_notifications.desktopNotifications.soundSelectPlaceholder": "Select a sound",
|
||||
"channel_notifications.desktopNotifications.title": "Sounds",
|
||||
"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",
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import bing from 'sounds/bing.mp3';
|
||||
import calls_calm from 'sounds/calls_calm.mp3';
|
||||
import calls_cheerful from 'sounds/calls_cheerful.mp3';
|
||||
import calls_dynamic from 'sounds/calls_dynamic.mp3';
|
||||
import calls_urgent from 'sounds/calls_urgent.mp3';
|
||||
import crackle from 'sounds/crackle.mp3';
|
||||
import down from 'sounds/down.mp3';
|
||||
import hello from 'sounds/hello.mp3';
|
||||
import ripple from 'sounds/ripple.mp3';
|
||||
import upstairs from 'sounds/upstairs.mp3';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
|
||||
export const notificationSounds = new Map([
|
||||
['Bing', bing],
|
||||
['Crackle', crackle],
|
||||
['Down', down],
|
||||
['Hello', hello],
|
||||
['Ripple', ripple],
|
||||
['Upstairs', upstairs],
|
||||
]);
|
||||
|
||||
export const callsNotificationSounds = new Map([
|
||||
['Dynamic', calls_dynamic],
|
||||
['Calm', calls_calm],
|
||||
['Urgent', calls_urgent],
|
||||
['Cheerful', calls_cheerful],
|
||||
]);
|
||||
|
||||
let canDing = true;
|
||||
export function ding(name: string) {
|
||||
if (hasSoundOptions() && canDing) {
|
||||
tryNotificationSound(name);
|
||||
canDing = false;
|
||||
setTimeout(() => {
|
||||
canDing = true;
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
export function tryNotificationSound(name: string) {
|
||||
const audio = new Audio(notificationSounds.get(name) ?? notificationSounds.get('Bing'));
|
||||
audio.play();
|
||||
}
|
||||
|
||||
let currentRing: HTMLAudioElement | null = null;
|
||||
export function ring(name: string) {
|
||||
if (!hasSoundOptions()) {
|
||||
return;
|
||||
}
|
||||
stopRing();
|
||||
|
||||
currentRing = loopNotificationRing(name);
|
||||
currentRing.addEventListener('pause', () => {
|
||||
stopRing();
|
||||
});
|
||||
}
|
||||
|
||||
export function stopRing() {
|
||||
if (currentRing) {
|
||||
currentRing.pause();
|
||||
currentRing.src = '';
|
||||
currentRing.remove();
|
||||
currentRing = null;
|
||||
}
|
||||
}
|
||||
|
||||
let currentTryRing: HTMLAudioElement | null = null;
|
||||
let currentTimer: NodeJS.Timeout;
|
||||
export function tryNotificationRing(name: string) {
|
||||
if (!hasSoundOptions()) {
|
||||
return;
|
||||
}
|
||||
stopTryNotificationRing();
|
||||
clearTimeout(currentTimer);
|
||||
|
||||
currentTryRing = loopNotificationRing(name);
|
||||
currentTryRing.addEventListener('pause', () => {
|
||||
stopTryNotificationRing();
|
||||
});
|
||||
|
||||
currentTimer = setTimeout(() => {
|
||||
stopTryNotificationRing();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
export function stopTryNotificationRing() {
|
||||
if (currentTryRing) {
|
||||
currentTryRing.pause();
|
||||
currentTryRing.src = '';
|
||||
currentTryRing.remove();
|
||||
currentTryRing = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function loopNotificationRing(name: string) {
|
||||
const audio = new Audio(callsNotificationSounds.get(name) ?? callsNotificationSounds.get('Calm'));
|
||||
audio.loop = true;
|
||||
audio.play();
|
||||
return audio;
|
||||
}
|
||||
|
||||
export function hasSoundOptions() {
|
||||
return (!UserAgent.isEdge());
|
||||
}
|
||||
251
webapp/channels/src/utils/notification_sounds.tsx
Обычный файл
251
webapp/channels/src/utils/notification_sounds.tsx
Обычный файл
@@ -0,0 +1,251 @@
|
||||
// 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 {FormattedMessage} from 'react-intl';
|
||||
|
||||
import bing from 'sounds/bing.mp3';
|
||||
import calls_calm from 'sounds/calls_calm.mp3';
|
||||
import calls_cheerful from 'sounds/calls_cheerful.mp3';
|
||||
import calls_dynamic from 'sounds/calls_dynamic.mp3';
|
||||
import calls_urgent from 'sounds/calls_urgent.mp3';
|
||||
import crackle from 'sounds/crackle.mp3';
|
||||
import down from 'sounds/down.mp3';
|
||||
import hello from 'sounds/hello.mp3';
|
||||
import ripple from 'sounds/ripple.mp3';
|
||||
import upstairs from 'sounds/upstairs.mp3';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
|
||||
export const notificationSounds = new Map([
|
||||
['Bing', bing],
|
||||
['Crackle', crackle],
|
||||
['Down', down],
|
||||
['Hello', hello],
|
||||
['Ripple', ripple],
|
||||
['Upstairs', upstairs],
|
||||
]);
|
||||
|
||||
export const notificationSoundKeys = Array.from(notificationSounds.keys());
|
||||
|
||||
export const optionsOfMessageNotificationSoundsSelect: Array<{value: string; label: ReactNode}> = notificationSoundKeys.map((soundName) => {
|
||||
if (soundName === 'Bing') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundBing'
|
||||
defaultMessage='Bing'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Crackle') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundCrackle'
|
||||
defaultMessage='Crackle'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Down') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundDown'
|
||||
defaultMessage='Down'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Hello') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundHello'
|
||||
defaultMessage='Hello'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Ripple') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundRipple'
|
||||
defaultMessage='Ripple'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Upstairs') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundUpstairs'
|
||||
defaultMessage='Upstairs'
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
value: '',
|
||||
label: '',
|
||||
};
|
||||
});
|
||||
|
||||
export function getValueOfNotificationSoundsSelect(soundName?: string) {
|
||||
const soundOption = optionsOfMessageNotificationSoundsSelect.find((option) => option.value === soundName);
|
||||
|
||||
if (!soundOption) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return soundOption;
|
||||
}
|
||||
|
||||
export const callsNotificationSounds = new Map([
|
||||
['Dynamic', calls_dynamic],
|
||||
['Calm', calls_calm],
|
||||
['Urgent', calls_urgent],
|
||||
['Cheerful', calls_cheerful],
|
||||
]);
|
||||
|
||||
export const callNotificationSoundKeys = Array.from(callsNotificationSounds.keys());
|
||||
|
||||
export const optionsOfIncomingCallSoundsSelect: Array<{value: string; label: ReactNode}> = callNotificationSoundKeys.map((soundName) => {
|
||||
if (soundName === 'Dynamic') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundDynamic'
|
||||
defaultMessage='Dynamic'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Calm') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundCalm'
|
||||
defaultMessage='Calm'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Urgent') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundUrgent'
|
||||
defaultMessage='Urgent'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (soundName === 'Cheerful') {
|
||||
return {
|
||||
value: soundName,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='user.settings.notifications.desktopNotificationSound.soundCheerful'
|
||||
defaultMessage='Cheerful'
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
value: '',
|
||||
label: '',
|
||||
};
|
||||
});
|
||||
|
||||
export function getValueOfIncomingCallSoundsSelect(soundName?: string) {
|
||||
const soundOption = optionsOfIncomingCallSoundsSelect.find((option) => option.value === soundName);
|
||||
|
||||
if (!soundOption) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return soundOption;
|
||||
}
|
||||
|
||||
let canDing = true;
|
||||
export function ding(name: string) {
|
||||
if (hasSoundOptions() && canDing) {
|
||||
tryNotificationSound(name);
|
||||
canDing = false;
|
||||
setTimeout(() => {
|
||||
canDing = true;
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
export function tryNotificationSound(name: string) {
|
||||
const audio = new Audio(notificationSounds.get(name) ?? notificationSounds.get('Bing'));
|
||||
audio.play();
|
||||
}
|
||||
|
||||
let currentRing: HTMLAudioElement | null = null;
|
||||
export function ring(name: string) {
|
||||
if (!hasSoundOptions()) {
|
||||
return;
|
||||
}
|
||||
stopRing();
|
||||
|
||||
currentRing = loopNotificationRing(name);
|
||||
currentRing.addEventListener('pause', () => {
|
||||
stopRing();
|
||||
});
|
||||
}
|
||||
|
||||
export function stopRing() {
|
||||
if (currentRing) {
|
||||
currentRing.pause();
|
||||
currentRing.src = '';
|
||||
currentRing.remove();
|
||||
currentRing = null;
|
||||
}
|
||||
}
|
||||
|
||||
let currentTryRing: HTMLAudioElement | null = null;
|
||||
let currentTimer: NodeJS.Timeout;
|
||||
export function tryNotificationRing(name: string) {
|
||||
if (!hasSoundOptions()) {
|
||||
return;
|
||||
}
|
||||
stopTryNotificationRing();
|
||||
clearTimeout(currentTimer);
|
||||
|
||||
currentTryRing = loopNotificationRing(name);
|
||||
currentTryRing.addEventListener('pause', () => {
|
||||
stopTryNotificationRing();
|
||||
});
|
||||
|
||||
currentTimer = setTimeout(() => {
|
||||
stopTryNotificationRing();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
export function stopTryNotificationRing() {
|
||||
if (currentTryRing) {
|
||||
currentTryRing.pause();
|
||||
currentTryRing.src = '';
|
||||
currentTryRing.remove();
|
||||
currentTryRing = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function loopNotificationRing(name: string) {
|
||||
const audio = new Audio(callsNotificationSounds.get(name) ?? callsNotificationSounds.get('Calm'));
|
||||
audio.loop = true;
|
||||
audio.play();
|
||||
return audio;
|
||||
}
|
||||
|
||||
export function hasSoundOptions() {
|
||||
return (!UserAgent.isEdge());
|
||||
}
|
||||
Ссылка в новой задаче
Block a user