MM-53478 : Improve design of keyword that trigger notification (#23934)

This commit is contained in:
M-ZubairAhmed 2023-09-09 14:45:50 +05:30 committed by GitHub
parent 5e7d1c6f9e
commit 060a63a3ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 844 additions and 341 deletions

View File

@ -34,7 +34,7 @@ describe('Verify Accessibility Support in different sections in Settings and Pro
{key: 'desktop', label: 'Desktop Notifications', type: 'radio'}, {key: 'desktop', label: 'Desktop Notifications', type: 'radio'},
{key: 'email', label: 'Email Notifications', type: 'radio'}, {key: 'email', label: 'Email Notifications', type: 'radio'},
{key: 'push', label: 'Mobile Push Notifications', type: 'radio'}, {key: 'push', label: 'Mobile Push Notifications', type: 'radio'},
{key: 'keys', label: 'Words That Trigger Mentions', type: 'checkbox'}, {key: 'keysWithNotification', label: 'Keywords that trigger Notifications', type: 'checkbox'},
{key: 'comments', label: 'Reply notifications', type: 'radio'}, {key: 'comments', label: 'Reply notifications', type: 'radio'},
], ],
display: [ display: [

View File

@ -23,12 +23,12 @@ describe('Notifications', () => {
// # Open 'Settings' modal // # Open 'Settings' modal
cy.uiOpenSettingsModal().within(() => { cy.uiOpenSettingsModal().within(() => {
// # Open 'Words That Trigger Mentions' setting and uncheck all the checkboxes // # Open 'Keywords that trigger Notifications' setting and uncheck all the checkboxes
cy.findByRole('heading', {name: 'Words That Trigger Mentions'}).should('be.visible').click(); cy.findByRole('heading', {name: 'Keywords that trigger Notifications'}).should('be.visible').click();
cy.findByRole('checkbox', {name: `Your case-sensitive first name "${otherUser.first_name}"`}).should('not.be.checked'); cy.findByRole('checkbox', {name: `Your case-sensitive first name "${otherUser.first_name}"`}).should('not.be.checked');
cy.findByRole('checkbox', {name: `Your non case-sensitive username "${otherUser.username}"`}).should('not.be.checked'); cy.findByRole('checkbox', {name: `Your non case-sensitive username "${otherUser.username}"`}).should('not.be.checked');
cy.findByRole('checkbox', {name: 'Channel-wide mentions "@channel", "@all", "@here"'}).click().should('not.be.checked'); cy.findByRole('checkbox', {name: 'Channel-wide mentions "@channel", "@all", "@here"'}).click().should('not.be.checked');
cy.findByRole('checkbox', {name: 'Other non case-sensitive words, separated by commas:'}).should('not.be.checked'); cy.findByRole('checkbox', {name: 'Other non case-sensitive words, press Tab or use commas to separate keywords:'}).should('not.be.checked');
// # Save then close the modal // # Save then close the modal
cy.uiSaveAndClose(); cy.uiSaveAndClose();

View File

@ -243,14 +243,16 @@ function setNotificationSettings(desiredSettings = {first: true, username: true,
// Navigate to settings modal // Navigate to settings modal
cy.uiOpenSettingsModal(); cy.uiOpenSettingsModal();
// Notifications header should be visible // # Click on notifications tab
cy.get('#notificationSettingsTitle'). cy.findByRoleExtended('tab', {name: 'Notifications'}).
scrollIntoView().
should('be.visible'). should('be.visible').
and('contain', 'Notifications'); click();
// Notifications header should be visible
cy.findAllByText('Notifications').should('be.visible');
// Open up 'Words that trigger mentions' sub-section // Open up 'Words that trigger mentions' sub-section
cy.get('#keysTitle'). cy.findByText('Keywords that trigger Notifications').
scrollIntoView(). scrollIntoView().
click(); click();
@ -270,8 +272,8 @@ function setNotificationSettings(desiredSettings = {first: true, username: true,
// Set Custom field // Set Custom field
if (desiredSettings.custom && desiredSettings.customText) { if (desiredSettings.custom && desiredSettings.customText) {
cy.get('#notificationTriggerCustomText'). cy.get('#notificationTriggerCustomText').
clear(). type(desiredSettings.customText, {force: true}).
type(desiredSettings.customText); tab();
} }
// Click “Save” and close modal // Click “Save” and close modal

View File

@ -29,14 +29,16 @@ describe('Notifications', () => {
// # Open 'Settings' modal // # Open 'Settings' modal
cy.uiOpenSettingsModal().within(() => { cy.uiOpenSettingsModal().within(() => {
cy.get('#keysEdit').should('be.visible').click(); // # Open 'Keywords that trigger Notifications' setting
cy.findByRole('heading', {name: 'Keywords that trigger Notifications'}).should('be.visible').click();
// * As otherUser, ensure that 'Your non-case sensitive username' is not checked // * As otherUser, ensure that 'Your non-case sensitive username' is not checked
cy.get('#notificationTriggerUsername').should('not.be.checked'); cy.findByRole('checkbox', {name: `Your non case-sensitive username "${otherUser.username}"`}).should('not.be.checked');
// # Close the modal // # Save then close the modal
cy.get('#accountSettingsHeader').find('button').should('be.visible').click(); cy.uiSaveAndClose();
}); });
cy.apiLogout(); cy.apiLogout();
// # Login as sysadmin // # Login as sysadmin

View File

@ -0,0 +1,476 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/user_settings/display/UserSettingsDisplay should match snapshot 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<div
id="notificationSettings"
>
<div
class="modal-header"
>
<button
class="close"
data-dismiss="modal"
id="closeButton"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
</button>
<h4
class="modal-title"
>
<div
class="modal-back"
>
<i
aria-label="Collapse Icon"
class="fa fa-angle-left"
/>
</div>
Notification Settings
</h4>
</div>
<div
class="user-settings"
>
<h3
class="tab-header"
id="notificationSettingsTitle"
>
Notifications
</h3>
<div
class="divider-dark first"
/>
<div
class="section-min"
>
<div
class="d-flex"
>
<h4
class="section-min__title"
id="desktopTitle"
>
Desktop Notifications
</h4>
<div
class="section-min__edit"
>
<button
aria-expanded="false"
aria-labelledby="desktopTitle desktopEdit"
class="color--link cursor--pointer style--none text-left"
id="desktopEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
</div>
<div
class="section-min__describe"
id="desktopDesc"
>
For all activity, without sound
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="d-flex"
>
<h4
class="section-min__title"
id="emailTitle"
>
Email Notifications
</h4>
<div
class="section-min__edit"
>
<button
aria-expanded="false"
aria-labelledby="emailTitle emailEdit"
class="color--link cursor--pointer style--none text-left"
id="emailEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
</div>
<div
class="section-min__describe"
id="emailDesc"
>
Email notifications are not enabled
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="d-flex"
>
<h4
class="section-min__title"
id="pushTitle"
>
Mobile Push Notifications
</h4>
<div
class="section-min__edit"
>
<button
aria-expanded="false"
aria-labelledby="pushTitle pushEdit"
class="color--link cursor--pointer style--none text-left"
id="pushEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
</div>
<div
class="section-min__describe"
id="pushDesc"
>
Never
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="d-flex"
>
<h4
class="section-min__title"
id="keysWithNotificationTitle"
>
Keywords that trigger Notifications
</h4>
<div
class="section-min__edit"
>
<button
aria-expanded="false"
aria-labelledby="keysWithNotificationTitle keysWithNotificationEdit"
class="color--link cursor--pointer style--none text-left"
id="keysWithNotificationEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
</div>
<div
class="section-min__describe"
id="keysWithNotificationDesc"
>
"@some-user"
</div>
</div>
<div
class="divider-light"
/>
<div
class="divider-dark"
/>
</div>
</div>
</div>
</body>,
"container": <div>
<div
id="notificationSettings"
>
<div
class="modal-header"
>
<button
class="close"
data-dismiss="modal"
id="closeButton"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
</button>
<h4
class="modal-title"
>
<div
class="modal-back"
>
<i
aria-label="Collapse Icon"
class="fa fa-angle-left"
/>
</div>
Notification Settings
</h4>
</div>
<div
class="user-settings"
>
<h3
class="tab-header"
id="notificationSettingsTitle"
>
Notifications
</h3>
<div
class="divider-dark first"
/>
<div
class="section-min"
>
<div
class="d-flex"
>
<h4
class="section-min__title"
id="desktopTitle"
>
Desktop Notifications
</h4>
<div
class="section-min__edit"
>
<button
aria-expanded="false"
aria-labelledby="desktopTitle desktopEdit"
class="color--link cursor--pointer style--none text-left"
id="desktopEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
</div>
<div
class="section-min__describe"
id="desktopDesc"
>
For all activity, without sound
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="d-flex"
>
<h4
class="section-min__title"
id="emailTitle"
>
Email Notifications
</h4>
<div
class="section-min__edit"
>
<button
aria-expanded="false"
aria-labelledby="emailTitle emailEdit"
class="color--link cursor--pointer style--none text-left"
id="emailEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
</div>
<div
class="section-min__describe"
id="emailDesc"
>
Email notifications are not enabled
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="d-flex"
>
<h4
class="section-min__title"
id="pushTitle"
>
Mobile Push Notifications
</h4>
<div
class="section-min__edit"
>
<button
aria-expanded="false"
aria-labelledby="pushTitle pushEdit"
class="color--link cursor--pointer style--none text-left"
id="pushEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
</div>
<div
class="section-min__describe"
id="pushDesc"
>
Never
</div>
</div>
<div
class="divider-light"
/>
<div
class="section-min"
>
<div
class="d-flex"
>
<h4
class="section-min__title"
id="keysWithNotificationTitle"
>
Keywords that trigger Notifications
</h4>
<div
class="section-min__edit"
>
<button
aria-expanded="false"
aria-labelledby="keysWithNotificationTitle keysWithNotificationEdit"
class="color--link cursor--pointer style--none text-left"
id="keysWithNotificationEdit"
>
<i
class="icon-pencil-outline"
title="Edit Icon"
/>
Edit
</button>
</div>
</div>
<div
class="section-min__describe"
id="keysWithNotificationDesc"
>
"@some-user"
</div>
</div>
<div
class="divider-light"
/>
<div
class="divider-dark"
/>
</div>
</div>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"replaceStoreState": [Function],
"rerender": [Function],
"unmount": [Function],
"updateStoreState": [Function],
}
`;

View File

@ -1,42 +1,37 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {connect} from 'react-redux'; import {connect, type ConnectedProps} from 'react-redux';
import {bindActionCreators} from 'redux';
import type {ActionCreatorsMapObject, Dispatch} from 'redux';
import {updateMe} from 'mattermost-redux/actions/users'; import {updateMe} from 'mattermost-redux/actions/users';
import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import type {ActionFunc} from 'mattermost-redux/types/actions';
import {isCallsEnabled, isCallsRingingEnabledOnServer} from 'selectors/calls'; import {isCallsEnabled, isCallsRingingEnabledOnServer} from 'selectors/calls';
import type {GlobalState} from 'types/store'; import type {GlobalState} from 'types/store';
import UserSettingsNotifications from './user_settings_notifications'; import UserSettingsNotifications from './user_settings_notifications';
import type {Props} from './user_settings_notifications';
function mapStateToProps(state: GlobalState) { const mapStateToProps = (state: GlobalState) => {
const config = getConfig(state); const config = getConfig(state);
const sendPushNotifications = config.SendPushNotifications === 'true'; const sendPushNotifications = config.SendPushNotifications === 'true';
const enableAutoResponder = config.ExperimentalEnableAutomaticReplies === 'true'; const enableAutoResponder = config.ExperimentalEnableAutomaticReplies === 'true';
return { return {
sendPushNotifications, sendPushNotifications,
enableAutoResponder, enableAutoResponder,
isCollapsedThreadsEnabled: isCollapsedThreadsEnabled(state), isCollapsedThreadsEnabled: isCollapsedThreadsEnabled(state),
isCallsRingingEnabled: isCallsEnabled(state, '0.17.0') && isCallsRingingEnabledOnServer(state), isCallsRingingEnabled: isCallsEnabled(state, '0.17.0') && isCallsRingingEnabledOnServer(state),
}; };
} };
function mapDispatchToProps(dispatch: Dispatch) { const mapDispatchToProps = {
return { updateMe,
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Props['actions']>({ };
updateMe,
}, dispatch), const connector = connect(mapStateToProps, mapDispatchToProps);
};
} export type PropsFromRedux = ConnectedProps<typeof connector>;
export default connect(mapStateToProps, mapDispatchToProps)(UserSettingsNotifications); export default connect(mapStateToProps, mapDispatchToProps)(UserSettingsNotifications);

View File

@ -0,0 +1,5 @@
.customKeywordsWithNotificationSubsection {
& .multiInput {
margin-block-start: 10px;
}
}

View File

@ -1,89 +1,42 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react'; import React from 'react';
import type {ComponentProps} from 'react'; import {type IntlShape} from 'react-intl';
import type {UserNotifyProps} from '@mattermost/types/users';
import {renderWithFullContext, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper'; import {TestHelper} from 'utils/test_helper';
import UserSettingsNotifications from './user_settings_notifications'; import UserSettingsNotifications from './user_settings_notifications';
describe('components/user_settings/display/UserSettingsDisplay', () => { describe('components/user_settings/display/UserSettingsDisplay', () => {
const user = TestHelper.getUserMock({ const defaultProps = {
id: 'user_id', user: TestHelper.getUserMock({id: 'user_id'}),
});
const requiredProps: ComponentProps<typeof UserSettingsNotifications> = {
user,
updateSection: jest.fn(), updateSection: jest.fn(),
activeSection: '', activeSection: '',
closeModal: jest.fn(), closeModal: jest.fn(),
collapseModal: jest.fn(), collapseModal: jest.fn(),
actions: { updateMe: jest.fn(() => Promise.resolve({})),
updateMe: jest.fn(() => Promise.resolve({})), isCollapsedThreadsEnabled: true,
},
isCollapsedThreadsEnabled: false,
sendPushNotifications: false, sendPushNotifications: false,
enableAutoResponder: false, enableAutoResponder: false,
isCallsRingingEnabled: true, isCallsRingingEnabled: true,
intl: {} as IntlShape,
}; };
test('should have called handleSubmit', async () => { test('should match snapshot', () => {
const props = {...requiredProps, actions: {...requiredProps.actions}}; const wrapper = renderWithFullContext(
const wrapper = shallow<UserSettingsNotifications>( <UserSettingsNotifications {...defaultProps}/>,
<UserSettingsNotifications {...props}/>,
); );
await wrapper.instance().handleSubmit(); expect(wrapper).toMatchSnapshot();
expect(requiredProps.actions.updateMe).toHaveBeenCalled();
});
test('should have called handleSubmit', async () => {
const updateMe = jest.fn(() => Promise.resolve({data: true}));
const props = {...requiredProps, actions: {...requiredProps.actions, updateMe}};
const wrapper = shallow<UserSettingsNotifications>(
<UserSettingsNotifications {...props}/>,
);
await wrapper.instance().handleSubmit();
expect(requiredProps.updateSection).toHaveBeenCalled();
expect(requiredProps.updateSection).toHaveBeenCalledWith('');
});
test('should reset state when handleUpdateSection is called', () => {
const newUpdateSection = jest.fn();
const updateArg = 'unreadChannels';
const props = {...requiredProps, updateSection: newUpdateSection, user: {...user, notify_props: {desktop: 'on'} as unknown as UserNotifyProps}};
const wrapper = shallow<UserSettingsNotifications>(
<UserSettingsNotifications {...props}/>,
);
wrapper.setState({isSaving: true, desktopActivity: 'off' as unknown as UserNotifyProps['desktop']});
wrapper.instance().handleUpdateSection(updateArg);
expect(wrapper.state('isSaving')).toEqual(false);
expect(wrapper.state('desktopActivity')).toEqual('on');
expect(newUpdateSection).toHaveBeenCalledTimes(1);
}); });
test('should show reply notifications section when CRT off', () => { test('should show reply notifications section when CRT off', () => {
const wrapper = shallow<UserSettingsNotifications>( const props = {...defaultProps, isCollapsedThreadsEnabled: false};
<UserSettingsNotifications {...requiredProps}/>,
);
expect(wrapper.exists('SettingItem[section="comments"]')).toBe(true);
});
test('should not show reply notifications section when CRT on', () => { renderWithFullContext(<UserSettingsNotifications {...props}/>);
const wrapper = shallow<UserSettingsNotifications>(
<UserSettingsNotifications expect(screen.getByText('Reply notifications')).toBeInTheDocument();
{...requiredProps}
isCollapsedThreadsEnabled={true}
/>,
);
expect(wrapper.exists('SettingItem[section="comments"]')).toBe(false);
}); });
}); });

View File

@ -5,8 +5,12 @@
import React from 'react'; import React from 'react';
import type {ChangeEvent, RefObject} from 'react'; import type {ChangeEvent, RefObject} from 'react';
import {FormattedMessage} from 'react-intl'; import type {WrappedComponentProps} from 'react-intl';
import {FormattedMessage, injectIntl} from 'react-intl';
import type {Styles as ReactSelectStyles, ValueType} from 'react-select';
import CreatableReactSelect from 'react-select/creatable';
import type {ServerError} from '@mattermost/types/errors';
import type {UserNotifyProps, UserProfile} from '@mattermost/types/users'; import type {UserNotifyProps, UserProfile} from '@mattermost/types/users';
import type {ActionResult} from 'mattermost-redux/types/actions'; import type {ActionResult} from 'mattermost-redux/types/actions';
@ -17,28 +21,34 @@ import SettingItemMax from 'components/setting_item_max';
import Constants, {NotificationLevels} from 'utils/constants'; import Constants, {NotificationLevels} from 'utils/constants';
import {t} from 'utils/i18n'; import {t} from 'utils/i18n';
import * as NotificationSounds from 'utils/notification_sounds'; import {stopTryNotificationRing} from 'utils/notification_sounds';
import {a11yFocus, localizeMessage, moveCursorToEnd} from 'utils/utils'; import {a11yFocus} from 'utils/utils';
import DesktopNotificationSettings from './desktop_notification_setting/desktop_notification_settings'; import DesktopNotificationSettings from './desktop_notification_setting/desktop_notification_settings';
import EmailNotificationSetting from './email_notification_setting'; import EmailNotificationSetting from './email_notification_setting';
import ManageAutoResponder from './manage_auto_responder/manage_auto_responder'; import ManageAutoResponder from './manage_auto_responder/manage_auto_responder';
export type Props = { import type {PropsFromRedux} from './index';
import './user_settings_notifications.scss';
const WHITE_SPACE_REGEX = /\s+/g;
const COMMA_REGEX = /,/g;
type MultiInputValue = {
label: string;
value: string;
}
type OwnProps = {
user: UserProfile; user: UserProfile;
updateSection: (section: string) => void; updateSection: (section: string) => void;
activeSection: string; activeSection: string;
closeModal: () => void; closeModal: () => void;
collapseModal: () => void; collapseModal: () => void;
sendPushNotifications: boolean;
enableAutoResponder: boolean;
actions: {
updateMe: (user: UserProfile) => Promise<ActionResult>;
};
isCollapsedThreadsEnabled: boolean;
isCallsRingingEnabled: boolean;
} }
type Props = PropsFromRedux & OwnProps & WrappedComponentProps;
type State = { type State = {
enableEmail: UserNotifyProps['email']; enableEmail: UserNotifyProps['email'];
desktopActivity: UserNotifyProps['desktop']; desktopActivity: UserNotifyProps['desktop'];
@ -52,8 +62,9 @@ type State = {
desktopNotificationSound: UserNotifyProps['desktop_notification_sound']; desktopNotificationSound: UserNotifyProps['desktop_notification_sound'];
callsNotificationSound: UserNotifyProps['calls_notification_sound']; callsNotificationSound: UserNotifyProps['calls_notification_sound'];
usernameKey: boolean; usernameKey: boolean;
customKeys: string; isCustomKeysWithNotificationInputChecked: boolean;
customKeysChecked: boolean; customKeysWithNotification: MultiInputValue[];
customKeysWithNotificationInputValue: string;
firstNameKey: boolean; firstNameKey: boolean;
channelKey: boolean; channelKey: boolean;
autoResponderActive: boolean; autoResponderActive: boolean;
@ -63,9 +74,7 @@ type State = {
serverError: string; serverError: string;
}; };
function getNotificationsStateFromProps(props: Props): State { function getDefaultStateFromProps(props: Props): State {
const user = props.user;
let desktop: UserNotifyProps['desktop'] = NotificationLevels.MENTION; let desktop: UserNotifyProps['desktop'] = NotificationLevels.MENTION;
let desktopThreads: UserNotifyProps['desktop_threads'] = NotificationLevels.ALL; let desktopThreads: UserNotifyProps['desktop_threads'] = NotificationLevels.ALL;
let pushThreads: UserNotifyProps['push_threads'] = NotificationLevels.ALL; let pushThreads: UserNotifyProps['push_threads'] = NotificationLevels.ALL;
@ -79,87 +88,86 @@ function getNotificationsStateFromProps(props: Props): State {
let pushActivity: UserNotifyProps['push'] = NotificationLevels.MENTION; let pushActivity: UserNotifyProps['push'] = NotificationLevels.MENTION;
let pushStatus: UserNotifyProps['push_status'] = Constants.UserStatuses.AWAY; let pushStatus: UserNotifyProps['push_status'] = Constants.UserStatuses.AWAY;
let autoResponderActive = false; let autoResponderActive = false;
let autoResponderMessage: UserNotifyProps['auto_responder_message'] = localizeMessage( let autoResponderMessage: UserNotifyProps['auto_responder_message'] = props.intl.formatMessage({
'user.settings.notifications.autoResponderDefault', id: 'user.settings.notifications.autoResponderDefault',
'Hello, I am out of office and unable to respond to messages.', defaultMessage: 'Hello, I am out of office and unable to respond to messages.',
); });
if (user.notify_props) { if (props.user.notify_props) {
if (user.notify_props.desktop) { if (props.user.notify_props.desktop) {
desktop = user.notify_props.desktop; desktop = props.user.notify_props.desktop;
} }
if (user.notify_props.desktop_threads) { if (props.user.notify_props.desktop_threads) {
desktopThreads = user.notify_props.desktop_threads; desktopThreads = props.user.notify_props.desktop_threads;
} }
if (user.notify_props.push_threads) { if (props.user.notify_props.push_threads) {
pushThreads = user.notify_props.push_threads; pushThreads = props.user.notify_props.push_threads;
} }
if (user.notify_props.email_threads) { if (props.user.notify_props.email_threads) {
emailThreads = user.notify_props.email_threads; emailThreads = props.user.notify_props.email_threads;
} }
if (user.notify_props.desktop_sound) { if (props.user.notify_props.desktop_sound) {
sound = user.notify_props.desktop_sound; sound = props.user.notify_props.desktop_sound;
} }
if (user.notify_props.calls_desktop_sound) { if (props.user.notify_props.calls_desktop_sound) {
callsSound = user.notify_props.calls_desktop_sound; callsSound = props.user.notify_props.calls_desktop_sound;
} }
if (user.notify_props.desktop_notification_sound) { if (props.user.notify_props.desktop_notification_sound) {
desktopNotificationSound = user.notify_props.desktop_notification_sound; desktopNotificationSound = props.user.notify_props.desktop_notification_sound;
} }
if (user.notify_props.calls_notification_sound) { if (props.user.notify_props.calls_notification_sound) {
callsNotificationSound = user.notify_props.calls_notification_sound; callsNotificationSound = props.user.notify_props.calls_notification_sound;
} }
if (user.notify_props.comments) { if (props.user.notify_props.comments) {
comments = user.notify_props.comments; comments = props.user.notify_props.comments;
} }
if (user.notify_props.email) { if (props.user.notify_props.email) {
enableEmail = user.notify_props.email; enableEmail = props.user.notify_props.email;
} }
if (user.notify_props.push) { if (props.user.notify_props.push) {
pushActivity = user.notify_props.push; pushActivity = props.user.notify_props.push;
} }
if (user.notify_props.push_status) { if (props.user.notify_props.push_status) {
pushStatus = user.notify_props.push_status; pushStatus = props.user.notify_props.push_status;
} }
if (user.notify_props.auto_responder_active) { if (props.user.notify_props.auto_responder_active) {
autoResponderActive = user.notify_props.auto_responder_active === 'true'; autoResponderActive = props.user.notify_props.auto_responder_active === 'true';
} }
if (user.notify_props.auto_responder_message) { if (props.user.notify_props.auto_responder_message) {
autoResponderMessage = user.notify_props.auto_responder_message; autoResponderMessage = props.user.notify_props.auto_responder_message;
} }
} }
let usernameKey = false; let usernameKey = false;
let customKeys = '';
let firstNameKey = false; let firstNameKey = false;
let channelKey = false; let channelKey = false;
let isCustomKeysWithNotificationInputChecked = false;
const customKeysWithNotification: MultiInputValue[] = [];
if (user.notify_props) { if (props.user.notify_props) {
if (user.notify_props.mention_keys) { if (props.user.notify_props.mention_keys) {
const keys = user.notify_props.mention_keys.split(','); const mentionKeys = props.user.notify_props.mention_keys.split(',').filter((key) => key.length > 0);
mentionKeys.forEach((mentionKey) => {
if (keys.indexOf(user.username) === -1) { // Remove username(s) from list of keys
usernameKey = false; if (mentionKey !== props.user.username && mentionKey !== `@${props.user.username}`) {
} else { customKeysWithNotification.push({
usernameKey = true; label: mentionKey,
keys.splice(keys.indexOf(user.username), 1); value: mentionKey,
if (keys.indexOf(`@${user.username}`) !== -1) { });
keys.splice(keys.indexOf(`@${user.username}`), 1);
} }
} });
customKeys = keys.join(','); // Check if username is in list of keys, if so, set the checkbox to true
usernameKey = mentionKeys.includes(props.user.username);
// Check if there are any keys in the list, if so, set the checkbox of custom keys to true
isCustomKeysWithNotificationInputChecked = customKeysWithNotification.length > 0;
} }
if (user.notify_props.first_name) { firstNameKey = props.user.notify_props?.first_name === 'true';
firstNameKey = user.notify_props.first_name === 'true'; channelKey = props.user.notify_props?.channel === 'true';
}
if (user.notify_props.channel) {
channelKey = user.notify_props.channel === 'true';
}
} }
return { return {
@ -175,8 +183,9 @@ function getNotificationsStateFromProps(props: Props): State {
desktopNotificationSound, desktopNotificationSound,
callsNotificationSound, callsNotificationSound,
usernameKey, usernameKey,
customKeys, customKeysWithNotification,
customKeysChecked: customKeys.length > 0, isCustomKeysWithNotificationInputChecked,
customKeysWithNotificationInputValue: '',
firstNameKey, firstNameKey,
channelKey, channelKey,
autoResponderActive, autoResponderActive,
@ -187,9 +196,7 @@ function getNotificationsStateFromProps(props: Props): State {
}; };
} }
export default class NotificationsTab extends React.PureComponent<Props, State> { class NotificationsTab extends React.PureComponent<Props, State> {
customCheckRef: RefObject<HTMLInputElement>;
customMentionsRef: RefObject<HTMLInputElement>;
drawerRef: RefObject<HTMLHeadingElement>; drawerRef: RefObject<HTMLHeadingElement>;
wrapperRef: RefObject<HTMLDivElement>; wrapperRef: RefObject<HTMLDivElement>;
@ -200,14 +207,12 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = getNotificationsStateFromProps(props); this.state = getDefaultStateFromProps(props);
this.customCheckRef = React.createRef();
this.customMentionsRef = React.createRef();
this.drawerRef = React.createRef(); this.drawerRef = React.createRef();
this.wrapperRef = React.createRef(); this.wrapperRef = React.createRef();
} }
handleSubmit = (): void => { handleSubmit = async () => {
const data: UserNotifyProps = {} as UserNotifyProps; const data: UserNotifyProps = {} as UserNotifyProps;
data.email = this.state.enableEmail; data.email = this.state.enableEmail;
data.desktop_sound = this.state.desktopSound; data.desktop_sound = this.state.desktopSound;
@ -221,47 +226,46 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
data.push = this.state.pushActivity; data.push = this.state.pushActivity;
data.push_status = this.state.pushStatus; data.push_status = this.state.pushStatus;
data.comments = this.state.notifyCommentsLevel; data.comments = this.state.notifyCommentsLevel;
data.auto_responder_active = this.state.autoResponderActive.toString() as UserNotifyProps['auto_responder_active']; data.auto_responder_active = this.state.autoResponderActive ? 'true' : 'false';
data.auto_responder_message = this.state.autoResponderMessage; data.auto_responder_message = this.state.autoResponderMessage;
data.first_name = this.state.firstNameKey ? 'true' : 'false';
data.channel = this.state.channelKey ? 'true' : 'false';
if (!data.auto_responder_message || data.auto_responder_message === '') { if (!data.auto_responder_message || data.auto_responder_message === '') {
data.auto_responder_message = localizeMessage( data.auto_responder_message = this.props.intl.formatMessage({
'user.settings.notifications.autoResponderDefault', id: 'user.settings.notifications.autoResponderDefault',
'Hello, I am out of office and unable to respond to messages.', defaultMessage: 'Hello, I am out of office and unable to respond to messages.',
); });
} }
const mentionKeys = []; const mentionKeys: string[] = [];
if (this.state.usernameKey) { if (this.state.usernameKey) {
mentionKeys.push(this.props.user.username); mentionKeys.push(this.props.user.username);
} }
if (this.state.isCustomKeysWithNotificationInputChecked && this.state.customKeysWithNotification.length > 0) {
let stringKeys = mentionKeys.join(','); this.state.customKeysWithNotification.forEach((key) => {
if (this.state.customKeys.length > 0 && this.state.customKeysChecked) { mentionKeys.push(key.value);
stringKeys += ',' + this.state.customKeys; });
} }
data.mention_keys = mentionKeys.join(',');
data.mention_keys = stringKeys;
data.first_name = this.state.firstNameKey.toString() as UserNotifyProps['first_name'];
data.channel = this.state.channelKey.toString() as UserNotifyProps['channel'];
this.setState({isSaving: true}); this.setState({isSaving: true});
NotificationSounds.stopTryNotificationRing(); stopTryNotificationRing();
this.props.actions.updateMe({notify_props: data} as UserProfile). const {data: updatedUser, error} = await this.props.updateMe({notify_props: data}) as ActionResult<Partial<UserProfile>, ServerError>; // Fix in MM-46907
then(({data: result, error: err}) => { if (updatedUser) {
if (result) { this.handleUpdateSection('');
this.handleUpdateSection(''); this.setState(getDefaultStateFromProps(this.props));
this.setState(getNotificationsStateFromProps(this.props)); } else if (error) {
} else if (err) { this.setState({serverError: error.message, isSaving: false});
this.setState({serverError: err.message, isSaving: false}); } else {
} this.setState({serverError: '', isSaving: false});
}); }
}; };
handleCancel = (): void => { handleCancel = (): void => {
this.setState(getNotificationsStateFromProps(this.props)); this.setState(getDefaultStateFromProps(this.props));
NotificationSounds.stopTryNotificationRing(); stopTryNotificationRing();
}; };
handleUpdateSection = (section: string): void => { handleUpdateSection = (section: string): void => {
@ -302,30 +306,92 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
handleEmailRadio = (enableEmail: UserNotifyProps['email']): void => this.setState({enableEmail}); handleEmailRadio = (enableEmail: UserNotifyProps['email']): void => this.setState({enableEmail});
updateUsernameKey = (val: boolean): void => this.setState({usernameKey: val}); handleChangeForUsernameKeyCheckbox = (event: ChangeEvent<HTMLInputElement>) => {
const {target: {checked}} = event;
this.setState({usernameKey: checked});
};
updateFirstNameKey = (val: boolean): void => this.setState({firstNameKey: val}); handleChangeForFirstNameKeyCheckbox = (event: ChangeEvent<HTMLInputElement>) => {
const {target: {checked}} = event;
this.setState({firstNameKey: checked});
};
updateChannelKey = (val: boolean): void => this.setState({channelKey: val}); handleChangeForChannelKeyCheckbox = (event: ChangeEvent<HTMLInputElement>) => {
const {target: {checked}} = event;
this.setState({channelKey: checked});
};
updateCustomMentionKeys = (): void => { handleChangeForCustomKeysWithNotificationCheckbox = (event: ChangeEvent<HTMLInputElement>) => {
const checked = this.customCheckRef.current?.checked; const {target: {checked}} = event;
this.setState({isCustomKeysWithNotificationInputChecked: checked});
};
if (checked) { handleChangeForCustomKeysWithNotificationInput = (values: ValueType<{ value: string }>) => {
const text = this.customMentionsRef.current?.value || ''; if (values && Array.isArray(values) && values.length > 0) {
// Check the custom keys input checkbox when atleast a single key is entered
if (this.state.isCustomKeysWithNotificationInputChecked === false) {
this.setState({
isCustomKeysWithNotificationInputChecked: true,
});
}
// remove all spaces and split string into individual keys const customKeysWithNotification = values.
this.setState({customKeys: text.replace(/ /g, ''), customKeysChecked: true}); map((value: MultiInputValue) => {
// Remove all spaces from the value
const formattedValue = value.value.trim().replace(WHITE_SPACE_REGEX, '');
return {value: formattedValue, label: formattedValue};
}).
filter((value) => value.value.length > 0);
this.setState({customKeysWithNotification});
} else { } else {
this.setState({customKeys: '', customKeysChecked: false}); this.setState({
isCustomKeysWithNotificationInputChecked: false,
customKeysWithNotification: [],
});
} }
}; };
onCustomChange = (): void => { updateCustomKeysWithNotificationWithInputValue = (newValue: string) => {
if (this.customCheckRef.current) { const customKeysWithNotification = [
this.customCheckRef.current.checked = true; ...this.state.customKeysWithNotification,
{
value: newValue,
label: newValue,
},
];
this.setState({
customKeysWithNotification,
customKeysWithNotificationInputValue: '', // Clear the input field
});
if (!this.state.isCustomKeysWithNotificationInputChecked) {
this.setState({isCustomKeysWithNotificationInputChecked: true});
}
};
handleOnKeydownForCustomKeysWithNotificationInput = (event: React.KeyboardEvent) => {
if (event.key === Constants.KeyCodes.COMMA[0] || event.key === Constants.KeyCodes.TAB[0]) {
const unsavedCustomKeyWithNotification = this.state.customKeysWithNotificationInputValue?.trim()?.replace(WHITE_SPACE_REGEX, '')?.replace(COMMA_REGEX, '') ?? '';
if (unsavedCustomKeyWithNotification.length > 0) {
this.updateCustomKeysWithNotificationWithInputValue(unsavedCustomKeyWithNotification);
}
}
};
handleChangeForCustomKeysWithNotificationInputValue = (value: string) => {
// Check if input contains comma, if so, add the value to the list of custom keys
if (!value.includes(Constants.KeyCodes.COMMA[0])) {
const formattedValue = value.trim().replace(WHITE_SPACE_REGEX, '');
this.setState({customKeysWithNotificationInputValue: formattedValue});
}
};
handleBlurForCustomKeysWithNotificationInput = () => {
const unsavedCustomKeyWithNotification = this.state.customKeysWithNotificationInputValue?.trim()?.replace(WHITE_SPACE_REGEX, '')?.replace(COMMA_REGEX, '') ?? '';
if (unsavedCustomKeyWithNotification.length > 0) {
this.updateCustomKeysWithNotificationWithInputValue(unsavedCustomKeyWithNotification);
} }
this.updateCustomMentionKeys();
}; };
createPushNotificationSection = () => { createPushNotificationSection = () => {
@ -544,7 +610,7 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
} }
max = ( max = (
<SettingItemMax <SettingItemMax
title={localizeMessage('user.settings.notifications.push', 'Mobile Push Notifications')} title={this.props.intl.formatMessage({id: 'user.settings.notifications.push', defaultMessage: 'Mobile Push Notifications'})}
inputs={inputs} inputs={inputs}
submit={submit} submit={submit}
serverError={this.state.serverError} serverError={this.state.serverError}
@ -618,9 +684,9 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
return ( return (
<SettingItem <SettingItem
title={this.props.intl.formatMessage({id: 'user.settings.notifications.push', defaultMessage: 'Mobile Push Notifications'})}
active={active} active={active}
areAllSectionsInactive={this.props.activeSection === ''} areAllSectionsInactive={this.props.activeSection === ''}
title={localizeMessage('user.settings.notifications.push', 'Mobile Push Notifications')}
describe={describe} describe={describe}
section={'push'} section={'push'}
updateSection={this.handleUpdateSection} updateSection={this.handleUpdateSection}
@ -629,17 +695,16 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
); );
}; };
createKeysSection = () => { createKeywordsWithNotificationSection = () => {
const serverError = this.state.serverError; const serverError = this.state.serverError;
const user = this.props.user; const user = this.props.user;
const active = this.props.activeSection === 'keys'; const isSectionExpanded = this.props.activeSection === 'keysWithNotification';
let max = null; let expandedSection = null;
if (active) { if (isSectionExpanded) {
const inputs = []; const inputs = [];
if (user.first_name) { if (user.first_name) {
const handleUpdateFirstNameKey = (e: ChangeEvent<HTMLInputElement>): void => this.updateFirstNameKey(e.target.checked);
inputs.push( inputs.push(
<div key='userNotificationFirstNameOption'> <div key='userNotificationFirstNameOption'>
<div className='checkbox'> <div className='checkbox'>
@ -648,11 +713,11 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
id='notificationTriggerFirst' id='notificationTriggerFirst'
type='checkbox' type='checkbox'
checked={this.state.firstNameKey} checked={this.state.firstNameKey}
onChange={handleUpdateFirstNameKey} onChange={this.handleChangeForFirstNameKeyCheckbox}
/> />
<FormattedMessage <FormattedMessage
id='user.settings.notifications.sensitiveName' id='user.settings.notifications.sensitiveName'
defaultMessage='Your case sensitive first name "{first_name}"' defaultMessage='Your case-sensitive first name "{first_name}"'
values={{ values={{
first_name: user.first_name, first_name: user.first_name,
}} }}
@ -663,7 +728,6 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
); );
} }
const handleUpdateUsernameKey = (e: ChangeEvent<HTMLInputElement>): void => this.updateUsernameKey(e.target.checked);
inputs.push( inputs.push(
<div key='userNotificationUsernameOption'> <div key='userNotificationUsernameOption'>
<div className='checkbox'> <div className='checkbox'>
@ -672,7 +736,7 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
id='notificationTriggerUsername' id='notificationTriggerUsername'
type='checkbox' type='checkbox'
checked={this.state.usernameKey} checked={this.state.usernameKey}
onChange={handleUpdateUsernameKey} onChange={this.handleChangeForUsernameKeyCheckbox}
/> />
<FormattedMessage <FormattedMessage
id='user.settings.notifications.sensitiveUsername' id='user.settings.notifications.sensitiveUsername'
@ -686,7 +750,6 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
</div>, </div>,
); );
const handleUpdateChannelKey = (e: ChangeEvent<HTMLInputElement>): void => this.updateChannelKey(e.target.checked);
inputs.push( inputs.push(
<div key='userNotificationChannelOption'> <div key='userNotificationChannelOption'>
<div className='checkbox'> <div className='checkbox'>
@ -695,7 +758,7 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
id='notificationTriggerShouts' id='notificationTriggerShouts'
type='checkbox' type='checkbox'
checked={this.state.channelKey} checked={this.state.channelKey}
onChange={handleUpdateChannelKey} onChange={this.handleChangeForChannelKeyCheckbox}
/> />
<FormattedMessage <FormattedMessage
id='user.settings.notifications.channelWide' id='user.settings.notifications.channelWide'
@ -707,51 +770,61 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
); );
inputs.push( inputs.push(
<div key='userNotificationCustomOption'> <div
key='userNotificationCustomOption'
className='customKeywordsWithNotificationSubsection'
>
<div className='checkbox'> <div className='checkbox'>
<label> <label>
<input <input
id='notificationTriggerCustom' id='notificationTriggerCustom'
ref={this.customCheckRef}
type='checkbox' type='checkbox'
checked={this.state.customKeysChecked} checked={this.state.isCustomKeysWithNotificationInputChecked}
onChange={this.updateCustomMentionKeys} onChange={this.handleChangeForCustomKeysWithNotificationCheckbox}
/> />
<FormattedMessage <FormattedMessage
id='user.settings.notifications.sensitiveWords' id='user.settings.notifications.sensitiveCustomWords'
defaultMessage='Other non-case sensitive words, separated by commas:' defaultMessage='Other non case-sensitive words, press Tab or use commas to separate keywords:'
/> />
</label> </label>
</div> </div>
<input <CreatableReactSelect
id='notificationTriggerCustomText' inputId='notificationTriggerCustomText'
autoFocus={this.state.customKeysChecked} autoFocus={true}
ref={this.customMentionsRef} isClearable={false}
className='form-control mentions-input' isMulti={true}
type='text' styles={customKeywordsWithNotificationStyles}
defaultValue={this.state.customKeys} className='multiInput'
onChange={this.onCustomChange} placeholder=''
onFocus={moveCursorToEnd} components={{
DropdownIndicator: () => null,
Menu: () => null,
MenuList: () => null,
}}
aria-labelledby='notificationTriggerCustom' aria-labelledby='notificationTriggerCustom'
onChange={this.handleChangeForCustomKeysWithNotificationInput}
value={this.state.customKeysWithNotification}
inputValue={this.state.customKeysWithNotificationInputValue}
onInputChange={this.handleChangeForCustomKeysWithNotificationInputValue}
onBlur={this.handleBlurForCustomKeysWithNotificationInput}
onKeyDown={this.handleOnKeydownForCustomKeysWithNotificationInput}
/> />
</div>, </div>,
); );
const extraInfo = ( const extraInfo = (
<span> <FormattedMessage
<FormattedMessage id='user.settings.notifications.keywordsWithNotification.extraInfo'
id='user.settings.notifications.mentionsInfo' defaultMessage='Notifications are triggered when someone sends a message that includes your username ("@{username}") or any of the options selected above.'
defaultMessage='Mentions trigger when someone sends a message that includes your username (@{username}) or any of the options selected above.' values={{
values={{ username: user.username,
username: user.username, }}
}} />
/>
</span>
); );
max = ( expandedSection = (
<SettingItemMax <SettingItemMax
title={localizeMessage('user.settings.notifications.wordsTrigger', 'Words That Trigger Mentions')} title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithNotification.title', defaultMessage: 'Keywords that trigger Notifications'})}
inputs={inputs} inputs={inputs}
submit={this.handleSubmit} submit={this.handleSubmit}
saving={this.state.isSaving} saving={this.state.isSaving}
@ -762,51 +835,33 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
); );
} }
let keys = ['@' + user.username]; const selectedMentionKeys = ['@' + user.username];
if (this.state.firstNameKey) { if (this.state.firstNameKey) {
keys.push(user.first_name); selectedMentionKeys.push(user.first_name);
} }
if (this.state.usernameKey) { if (this.state.usernameKey) {
keys.push(user.username); selectedMentionKeys.push(user.username);
} }
if (this.state.channelKey) { if (this.state.channelKey) {
keys.push('@channel'); selectedMentionKeys.push('@channel');
keys.push('@all'); selectedMentionKeys.push('@all');
keys.push('@here'); selectedMentionKeys.push('@here');
} }
if (this.state.customKeys.length > 0) { if (this.state.customKeysWithNotification.length > 0) {
keys = keys.concat(this.state.customKeys.split(',')); const customKeysWithNotificationStringArray = this.state.customKeysWithNotification.map((key) => key.value);
} selectedMentionKeys.push(...customKeysWithNotificationStringArray);
let describe: JSX.Element | string = '';
for (let i = 0; i < keys.length; i++) {
if (keys[i] !== '') {
describe += '"' + keys[i] + '", ';
}
}
if (describe.length > 0) {
describe = describe.substring(0, describe.length - 2);
} else {
describe = (
<FormattedMessage
id='user.settings.notifications.noWords'
defaultMessage='No words configured'
/>
);
} }
const collapsedDescription = selectedMentionKeys.filter((key) => key.trim().length !== 0).map((key) => `"${key}"`).join(', ');
return ( return (
<SettingItem <SettingItem
active={active} title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithNotification.title', defaultMessage: 'Keywords that trigger Notifications'})}
section='keysWithNotification'
active={isSectionExpanded}
areAllSectionsInactive={this.props.activeSection === ''} areAllSectionsInactive={this.props.activeSection === ''}
title={localizeMessage('user.settings.notifications.wordsTrigger', 'Words That Trigger Mentions')} describe={collapsedDescription}
describe={describe}
section={'keys'}
updateSection={this.handleUpdateSection} updateSection={this.handleUpdateSection}
max={max} max={expandedSection}
/>); />);
}; };
@ -830,7 +885,10 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
inputs.push( inputs.push(
<fieldset key='userNotificationLevelOption'> <fieldset key='userNotificationLevelOption'>
<legend className='form-legend hidden-label'> <legend className='form-legend hidden-label'>
{localizeMessage('user.settings.notifications.comments', 'Reply notifications')} <FormattedMessage
id='user.settings.notifications.comments'
defaultMessage='Reply notifications'
/>
</legend> </legend>
<div className='radio'> <div className='radio'>
<label> <label>
@ -893,7 +951,7 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
max = ( max = (
<SettingItemMax <SettingItemMax
title={localizeMessage('user.settings.notifications.comments', 'Reply notifications')} title={this.props.intl.formatMessage({id: 'user.settings.notifications.comments', defaultMessage: 'Reply notifications'})}
extraInfo={extraInfo} extraInfo={extraInfo}
inputs={inputs} inputs={inputs}
submit={this.handleSubmit} submit={this.handleSubmit}
@ -930,8 +988,8 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
return ( return (
<SettingItem <SettingItem
title={this.props.intl.formatMessage({id: 'user.settings.notifications.comments', defaultMessage: 'Reply notifications'})}
active={active} active={active}
title={localizeMessage('user.settings.notifications.comments', 'Reply notifications')}
describe={describe} describe={describe}
section={'comments'} section={'comments'}
updateSection={this.handleUpdateSection} updateSection={this.handleUpdateSection}
@ -942,59 +1000,54 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
}; };
createAutoResponderSection = () => { createAutoResponderSection = () => {
if (this.props.enableAutoResponder) { const describe = this.state.autoResponderActive ? (
const describe = this.state.autoResponderActive ? ( <FormattedMessage
<FormattedMessage id='user.settings.notifications.autoResponderEnabled'
id='user.settings.notifications.autoResponderEnabled' defaultMessage='Enabled'
defaultMessage='Enabled' />
/> ) : (
) : ( <FormattedMessage
<FormattedMessage id='user.settings.notifications.autoResponderDisabled'
id='user.settings.notifications.autoResponderDisabled' defaultMessage='Disabled'
defaultMessage='Disabled' />
/> );
);
return ( return (
<SettingItem <SettingItem
active={this.props.activeSection === 'auto-responder'} active={this.props.activeSection === 'auto-responder'}
areAllSectionsInactive={this.props.activeSection === ''} areAllSectionsInactive={this.props.activeSection === ''}
title={ title={
<FormattedMessage <FormattedMessage
id='user.settings.notifications.autoResponder' id='user.settings.notifications.autoResponder'
defaultMessage='Automatic Direct Message Replies' defaultMessage='Automatic Direct Message Replies'
/>
}
describe={describe}
section={'auto-responder'}
updateSection={this.handleUpdateSection}
max={(
<div>
<ManageAutoResponder
autoResponderActive={this.state.autoResponderActive}
autoResponderMessage={this.state.autoResponderMessage || ''}
updateSection={this.handleUpdateSection}
setParentState={this.setStateValue}
submit={this.handleSubmit}
error={this.state.serverError}
saving={this.state.isSaving}
/> />
} <div className='divider-dark'/>
describe={describe} </div>
section={'auto-responder'} )}
updateSection={this.handleUpdateSection} />
max={( );
<div>
<ManageAutoResponder
autoResponderActive={this.state.autoResponderActive}
autoResponderMessage={this.state.autoResponderMessage || ''}
updateSection={this.handleUpdateSection}
setParentState={this.setStateValue}
submit={this.handleSubmit}
error={this.state.serverError}
saving={this.state.isSaving}
/>
<div className='divider-dark'/>
</div>
)}
/>
);
}
return null;
}; };
render() { render() {
const autoResponderSection = this.createAutoResponderSection();
const commentsSection = this.createCommentsSection();
const keysSection = this.createKeysSection();
const pushNotificationSection = this.createPushNotificationSection(); const pushNotificationSection = this.createPushNotificationSection();
const enableEmailProp = this.state.enableEmail === 'true'; const keywordsWithNotificationSection = this.createKeywordsWithNotificationSection();
const commentsSection = this.createCommentsSection();
const autoResponderSection = this.createAutoResponderSection();
return ( return (
<div id='notificationSettings'> <div id='notificationSettings'>
@ -1064,7 +1117,7 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
<EmailNotificationSetting <EmailNotificationSetting
activeSection={this.props.activeSection} activeSection={this.props.activeSection}
updateSection={this.handleUpdateSection} updateSection={this.handleUpdateSection}
enableEmail={enableEmailProp} enableEmail={this.state.enableEmail === 'true'}
onSubmit={this.handleSubmit} onSubmit={this.handleSubmit}
onCancel={this.handleCancel} onCancel={this.handleCancel}
onChange={this.handleEmailRadio} onChange={this.handleEmailRadio}
@ -1077,7 +1130,7 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
<div className='divider-light'/> <div className='divider-light'/>
{pushNotificationSection} {pushNotificationSection}
<div className='divider-light'/> <div className='divider-light'/>
{keysSection} {keywordsWithNotificationSection}
<div className='divider-light'/> <div className='divider-light'/>
{!this.props.isCollapsedThreadsEnabled && ( {!this.props.isCollapsedThreadsEnabled && (
<> <>
@ -1085,7 +1138,9 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
<div className='divider-light'/> <div className='divider-light'/>
</> </>
)} )}
{autoResponderSection} {this.props.enableAutoResponder && (
autoResponderSection
)}
<div className='divider-dark'/> <div className='divider-dark'/>
</div> </div>
</div> </div>
@ -1093,3 +1148,19 @@ export default class NotificationsTab extends React.PureComponent<Props, State>
); );
} }
} }
const customKeywordsWithNotificationStyles: ReactSelectStyles = {
indicatorSeparator: ((indicatorSeperatorStyles) => ({
...indicatorSeperatorStyles,
display: 'none',
})),
multiValueRemove: ((multiValueRemoveStyles) => ({
...multiValueRemoveStyles,
cursor: 'pointer',
':hover': {
backgroundColor: 'rgba(var(--center-channel-color-rgb), 0.16)',
},
})),
};
export default injectIntl(NotificationsTab);

View File

@ -5422,9 +5422,9 @@
"user.settings.notifications.header": "Notifications", "user.settings.notifications.header": "Notifications",
"user.settings.notifications.icon": "Notification Settings Icon", "user.settings.notifications.icon": "Notification Settings Icon",
"user.settings.notifications.info": "Desktop notifications are available on Edge, Firefox, Safari, Chrome and Mattermost Desktop Apps.", "user.settings.notifications.info": "Desktop notifications are available on Edge, Firefox, Safari, Chrome and Mattermost Desktop Apps.",
"user.settings.notifications.mentionsInfo": "Mentions trigger when someone sends a message that includes your username (\"@{username}\") or any of the options selected above.", "user.settings.notifications.keywordsWithNotification.extraInfo": "Notifications are triggered when someone sends a message that includes your username (\"@{username}\") or any of the options selected above.",
"user.settings.notifications.keywordsWithNotification.title": "Keywords that trigger Notifications",
"user.settings.notifications.never": "Never", "user.settings.notifications.never": "Never",
"user.settings.notifications.noWords": "No words configured",
"user.settings.notifications.off": "Off", "user.settings.notifications.off": "Off",
"user.settings.notifications.on": "On", "user.settings.notifications.on": "On",
"user.settings.notifications.onlyMentions": "Only for mentions and direct messages", "user.settings.notifications.onlyMentions": "Only for mentions and direct messages",
@ -5432,9 +5432,9 @@
"user.settings.notifications.push_notification.status": "Trigger push notifications when", "user.settings.notifications.push_notification.status": "Trigger push notifications when",
"user.settings.notifications.push_threads": "When enabled, any reply to a thread you're following will send a mobile push notification.", "user.settings.notifications.push_threads": "When enabled, any reply to a thread you're following will send a mobile push notification.",
"user.settings.notifications.push_threads.allActivity": "Notify me about threads I'm following", "user.settings.notifications.push_threads.allActivity": "Notify me about threads I'm following",
"user.settings.notifications.sensitiveCustomWords": "Other non case-sensitive words, press Tab or use commas to separate keywords:",
"user.settings.notifications.sensitiveName": "Your case-sensitive first name \"{first_name}\"", "user.settings.notifications.sensitiveName": "Your case-sensitive first name \"{first_name}\"",
"user.settings.notifications.sensitiveUsername": "Your non case-sensitive username \"{username}\"", "user.settings.notifications.sensitiveUsername": "Your non case-sensitive username \"{username}\"",
"user.settings.notifications.sensitiveWords": "Other non case-sensitive words, separated by commas:",
"user.settings.notifications.soundConfig": "Please configure notification sounds in your browser settings", "user.settings.notifications.soundConfig": "Please configure notification sounds in your browser settings",
"user.settings.notifications.sounds_info": "Notification sounds are available on Firefox, Edge, Safari, Chrome and Mattermost Desktop Apps.", "user.settings.notifications.sounds_info": "Notification sounds are available on Firefox, Edge, Safari, Chrome and Mattermost Desktop Apps.",
"user.settings.notifications.threads": "When enabled, any reply to a thread you're following will send a desktop notification.", "user.settings.notifications.threads": "When enabled, any reply to a thread you're following will send a desktop notification.",
@ -5442,7 +5442,6 @@
"user.settings.notifications.threads.desktop": "Thread reply notifications", "user.settings.notifications.threads.desktop": "Thread reply notifications",
"user.settings.notifications.threads.push": "Thread reply notifications", "user.settings.notifications.threads.push": "Thread reply notifications",
"user.settings.notifications.title": "Notification Settings", "user.settings.notifications.title": "Notification Settings",
"user.settings.notifications.wordsTrigger": "Words That Trigger Mentions",
"user.settings.profile.icon": "Profile Settings Icon", "user.settings.profile.icon": "Profile Settings Icon",
"user.settings.push_notification.allActivity": "For all activity", "user.settings.push_notification.allActivity": "For all activity",
"user.settings.push_notification.allActivityAway": "For all activity when away or offline", "user.settings.push_notification.allActivityAway": "For all activity when away or offline",

View File

@ -988,7 +988,7 @@ export function stopPeriodicStatusUpdates(): ActionFunc {
}; };
} }
export function updateMe(user: UserProfile): ActionFunc { export function updateMe(user: Partial<UserProfile>): ActionFunc<Partial<UserProfile>, ServerError> {
return async (dispatch: DispatchFunc) => { return async (dispatch: DispatchFunc) => {
dispatch({type: UserTypes.UPDATE_ME_REQUEST, data: null}); dispatch({type: UserTypes.UPDATE_ME_REQUEST, data: null});