diff --git a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js
index fe7b0b9143..0257834020 100644
--- a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js
@@ -34,8 +34,8 @@ describe('Verify Accessibility Support in different sections in Settings and Pro
{key: 'desktop', label: 'Desktop Notifications', type: 'radio'},
{key: 'email', label: 'Email Notifications', type: 'radio'},
{key: 'push', label: 'Mobile Push Notifications', type: 'radio'},
- {key: 'keysWithNotification', label: 'Keywords that trigger Notifications', type: 'checkbox'},
- {key: 'comments', label: 'Reply notifications', type: 'radio'},
+ {key: 'keysWithNotification', label: 'Keywords That Trigger Notifications', type: 'checkbox'},
+ {key: 'comments', label: 'Reply Notifications', type: 'radio'},
],
display: [
{key: 'theme', label: 'Theme', type: 'radio'},
diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/notify_admin_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/notify_admin_spec.ts
index 4e7f24bf34..9f4c0d33f9 100644
--- a/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/notify_admin_spec.ts
+++ b/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/notify_admin_spec.ts
@@ -188,6 +188,8 @@ function mapFeatureIdToId(id: string) {
return 'All Professional features';
case 'mattermost.feature.all_enterprise':
return 'All Enterprise features';
+ case 'mattermost.feature.highlight_without_notification':
+ return 'Keywords Highlight Without Notification';
default:
return '';
}
diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/message_auto_response_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/message_auto_response_spec.js
index 42db600700..007b04dc74 100644
--- a/e2e-tests/cypress/tests/integration/channels/messaging/message_auto_response_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/messaging/message_auto_response_spec.js
@@ -48,7 +48,7 @@ describe('Auto Response In DMs', () => {
// # Open 'Settings' modal and view 'Notifications'
cy.uiOpenSettingsModal().within(() => {
// # Click on 'Edit' for 'Automatic Direct Message Replies
- cy.get('#auto-responderEdit').should('be.visible').click();
+ cy.get('#auto-responderEdit').should('exist').scrollIntoView().and('be.visible').click();
// # Click on 'Enabled' checkbox
cy.get('#autoResponderActive').should('be.visible').click();
diff --git a/e2e-tests/cypress/tests/integration/channels/notifications/at_icon_still_shows_mentions_list_with_deactivated_triggers_spec.js b/e2e-tests/cypress/tests/integration/channels/notifications/at_icon_still_shows_mentions_list_with_deactivated_triggers_spec.js
index c50a58c998..521d3f9133 100644
--- a/e2e-tests/cypress/tests/integration/channels/notifications/at_icon_still_shows_mentions_list_with_deactivated_triggers_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/notifications/at_icon_still_shows_mentions_list_with_deactivated_triggers_spec.js
@@ -23,8 +23,8 @@ describe('Notifications', () => {
// # Open 'Settings' modal
cy.uiOpenSettingsModal().within(() => {
- // # Open 'Keywords that trigger Notifications' setting and uncheck all the checkboxes
- cy.findByRole('heading', {name: 'Keywords that trigger Notifications'}).should('be.visible').click();
+ // # Open 'Keywords That Trigger Notifications' setting and uncheck all the checkboxes
+ 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 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');
diff --git a/e2e-tests/cypress/tests/integration/channels/notifications/at_mentions_spec.js b/e2e-tests/cypress/tests/integration/channels/notifications/at_mentions_spec.js
index 716c0319a4..4982846810 100644
--- a/e2e-tests/cypress/tests/integration/channels/notifications/at_mentions_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/notifications/at_mentions_spec.js
@@ -252,7 +252,7 @@ function setNotificationSettings(desiredSettings = {first: true, username: true,
cy.findAllByText('Notifications').should('be.visible');
// Open up 'Words that trigger mentions' sub-section
- cy.findByText('Keywords that trigger Notifications').
+ cy.findByText('Keywords That Trigger Notifications').
scrollIntoView().
click();
diff --git a/e2e-tests/cypress/tests/integration/channels/notifications/deselect_username_mention_trigger_spec.js b/e2e-tests/cypress/tests/integration/channels/notifications/deselect_username_mention_trigger_spec.js
index 93bd1e2355..10bb5465fc 100644
--- a/e2e-tests/cypress/tests/integration/channels/notifications/deselect_username_mention_trigger_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/notifications/deselect_username_mention_trigger_spec.js
@@ -29,8 +29,8 @@ describe('Notifications', () => {
// # Open 'Settings' modal
cy.uiOpenSettingsModal().within(() => {
- // # Open 'Keywords that trigger Notifications' setting
- cy.findByRole('heading', {name: 'Keywords that trigger Notifications'}).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
cy.findByRole('checkbox', {name: `Your non case-sensitive username "${otherUser.username}"`}).should('not.be.checked');
diff --git a/e2e-tests/playwright/support/ui/components/channels/delete_post_modal.ts b/e2e-tests/playwright/support/ui/components/channels/delete_post_modal.ts
index 041f5a5afa..af92689ae1 100644
--- a/e2e-tests/playwright/support/ui/components/channels/delete_post_modal.ts
+++ b/e2e-tests/playwright/support/ui/components/channels/delete_post_modal.ts
@@ -9,7 +9,7 @@ export default class DeletePostModal {
constructor(container: Locator) {
this.container = container;
- this.confirmButton = this.container.locator('#deletePostModalButton');
+ this.confirmButton = container.locator('#deletePostModalButton');
}
async toBeVisible() {
diff --git a/e2e-tests/playwright/support/ui/components/channels/settings/notification_settings.ts b/e2e-tests/playwright/support/ui/components/channels/settings/notification_settings.ts
new file mode 100644
index 0000000000..2482dcaaa9
--- /dev/null
+++ b/e2e-tests/playwright/support/ui/components/channels/settings/notification_settings.ts
@@ -0,0 +1,54 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {expect, Locator} from '@playwright/test';
+
+type NotificationSettingsSection = 'keysWithHighlight' | 'keysWithNotification';
+
+export default class NotificationsSettings {
+ readonly container: Locator;
+
+ constructor(container: Locator) {
+ this.container = container;
+ }
+
+ async toBeVisible() {
+ await expect(this.container).toBeVisible();
+ }
+
+ async expandSection(section: NotificationSettingsSection) {
+ if (section === 'keysWithHighlight') {
+ await this.container.getByText('Keywords That Get Highlighted (without notifications)').click();
+ await this.verifySectionIsExpanded('keysWithHighlight');
+ }
+ }
+
+ async verifySectionIsExpanded(section: NotificationSettingsSection) {
+ await expect(this.container.locator(`#${section}Edit`)).not.toBeVisible();
+
+ if (section === 'keysWithHighlight') {
+ await expect(
+ this.container.getByText(
+ 'Enter non case-sensitive keywords, press Tab or use commas to separate them:',
+ ),
+ ).toBeVisible();
+ await expect(
+ this.container.getByText(
+ 'These keywords will be shown to you with a highlight when anyone sends a message that includes them.',
+ ),
+ ).toBeVisible();
+ }
+ }
+
+ async getKeywordsInput() {
+ await expect(this.container.locator('input')).toBeVisible();
+ return this.container.locator('input');
+ }
+
+ async save() {
+ await expect(this.container.getByText('Save')).toBeVisible();
+ await this.container.getByText('Save').click();
+ }
+}
+
+export {NotificationsSettings};
diff --git a/e2e-tests/playwright/support/ui/components/channels/settings/settings_modal.ts b/e2e-tests/playwright/support/ui/components/channels/settings/settings_modal.ts
new file mode 100644
index 0000000000..2b3e08cc76
--- /dev/null
+++ b/e2e-tests/playwright/support/ui/components/channels/settings/settings_modal.ts
@@ -0,0 +1,39 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {expect, Locator} from '@playwright/test';
+
+import {NotificationsSettings} from './notification_settings';
+
+export default class SettingsModal {
+ readonly container: Locator;
+
+ readonly notificationsSettingsTab;
+ readonly notificationsSettings;
+
+ constructor(container: Locator) {
+ this.container = container;
+
+ this.notificationsSettingsTab = container.locator('#notificationsButton');
+ this.notificationsSettings = new NotificationsSettings(container.locator('#notificationSettings'));
+ }
+
+ async toBeVisible() {
+ await expect(this.container).toBeVisible();
+ }
+
+ async openNotificationsTab() {
+ await expect(this.notificationsSettingsTab).toBeVisible();
+ await this.notificationsSettingsTab.click();
+
+ await this.notificationsSettings.toBeVisible();
+ }
+
+ async closeModal() {
+ await this.container.getByLabel('Close').click();
+
+ await expect(this.container).not.toBeVisible();
+ }
+}
+
+export {SettingsModal};
diff --git a/e2e-tests/playwright/support/ui/components/global_header.ts b/e2e-tests/playwright/support/ui/components/global_header.ts
index 8ab49d6307..a17a62a6fa 100644
--- a/e2e-tests/playwright/support/ui/components/global_header.ts
+++ b/e2e-tests/playwright/support/ui/components/global_header.ts
@@ -7,11 +7,19 @@ export default class GlobalHeader {
readonly container: Locator;
readonly productSwitchMenu;
+ readonly recentMentionsButton;
+ readonly settingsButton;
constructor(container: Locator) {
this.container = container;
this.productSwitchMenu = container.getByRole('button', {name: 'Product switch menu'});
+ this.recentMentionsButton = container.getByRole('button', {name: 'Recent mentions'});
+ this.settingsButton = container.getByRole('button', {name: 'Settings'});
+ }
+
+ async toBeVisible(name: string) {
+ await expect(this.container.getByRole('heading', {name})).toBeVisible();
}
async switchProduct(name: string) {
@@ -19,8 +27,14 @@ export default class GlobalHeader {
await this.container.getByRole('link', {name}).click();
}
- async toBeVisible(name: string) {
- await expect(this.container.getByRole('heading', {name})).toBeVisible();
+ async openSettings() {
+ await expect(this.settingsButton).toBeVisible();
+ await this.settingsButton.click();
+ }
+
+ async openRecentMentions() {
+ await expect(this.recentMentionsButton).toBeVisible();
+ await this.recentMentionsButton.click();
}
}
diff --git a/e2e-tests/playwright/support/ui/components/index.ts b/e2e-tests/playwright/support/ui/components/index.ts
index d05d1705b9..a129f32c1c 100644
--- a/e2e-tests/playwright/support/ui/components/index.ts
+++ b/e2e-tests/playwright/support/ui/components/index.ts
@@ -10,6 +10,7 @@ import {ChannelsSidebarLeft} from './channels/sidebar_left';
import {ChannelsSidebarRight} from './channels/sidebar_right';
import {DeletePostModal} from './channels/delete_post_modal';
import {FindChannelsModal} from './channels/find_channels_modal';
+import {SettingsModal} from './channels/settings/settings_modal';
import {Footer} from './footer';
import {GlobalHeader} from './global_header';
import {MainHeader} from './main_header';
@@ -30,6 +31,7 @@ const components = {
ChannelsPost,
FindChannelsModal,
DeletePostModal,
+ SettingsModal,
PostDotMenu,
PostMenu,
ThreadFooter,
diff --git a/e2e-tests/playwright/support/ui/pages/channels.ts b/e2e-tests/playwright/support/ui/pages/channels.ts
index 1031c51e12..27e231b16e 100644
--- a/e2e-tests/playwright/support/ui/pages/channels.ts
+++ b/e2e-tests/playwright/support/ui/pages/channels.ts
@@ -18,6 +18,7 @@ export default class ChannelsPage {
readonly findChannelsModal;
readonly deletePostModal;
+ readonly settingsModal;
readonly postDotMenu;
readonly postReminderMenu;
@@ -37,6 +38,7 @@ export default class ChannelsPage {
// Modals
this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'}));
this.deletePostModal = new components.DeletePostModal(page.locator('#deletePostModal'));
+ this.settingsModal = new components.SettingsModal(page.getByRole('dialog', {name: 'Settings'}));
// Menus
this.postDotMenu = new components.PostDotMenu(page.getByRole('menu', {name: 'Post extra options'}));
diff --git a/e2e-tests/playwright/tests/functional/channels/settings/notifications/highlight_without_notification.spec.ts b/e2e-tests/playwright/tests/functional/channels/settings/notifications/highlight_without_notification.spec.ts
new file mode 100644
index 0000000000..c59dbb6eac
--- /dev/null
+++ b/e2e-tests/playwright/tests/functional/channels/settings/notifications/highlight_without_notification.spec.ts
@@ -0,0 +1,271 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {expect} from '@playwright/test';
+
+import {test} from '@e2e-support/test_fixture';
+import {getRandomId} from '@e2e-support/util';
+import {createRandomPost} from '@e2e-support/server/post';
+
+const keywords = [`AB${getRandomId()}`, `CD${getRandomId()}`, `EF${getRandomId()}`, `Highlight me ${getRandomId()}`];
+
+const highlightWithoutNotificationClass = 'non-notification-highlight';
+
+test('MM-T5465-1 Should add the keyword when enter, comma or tab is pressed on the textbox', async ({pw, pages}) => {
+ const {user} = await pw.initSetup();
+
+ // # Log in as a user in new browser context
+ const {page} = await pw.testBrowser.login(user);
+
+ // # Visit default channel page
+ const channelPage = new pages.ChannelsPage(page);
+ await channelPage.goto();
+ await channelPage.toBeVisible();
+
+ await channelPage.centerView.postCreate.postMessage('Hello World');
+
+ // # Open settings modal
+ await channelPage.globalHeader.openSettings();
+ await channelPage.settingsModal.toBeVisible();
+
+ // # Open notifications tab
+ await channelPage.settingsModal.openNotificationsTab();
+
+ // # Open keywords that get highlighted section
+ await channelPage.settingsModal.notificationsSettings.expandSection('keysWithHighlight');
+
+ const keywordsInput = await channelPage.settingsModal.notificationsSettings.getKeywordsInput();
+
+ // # Enter keyword 1
+ await keywordsInput.type(keywords[0]);
+
+ // # Press Comma on the textbox
+ await keywordsInput.press(',');
+
+ // # Enter keyword 2
+ await keywordsInput.type(keywords[1]);
+
+ // # Press Tab on the textbox
+ await keywordsInput.press('Tab');
+
+ // # Enter keyword 3
+ await keywordsInput.type(keywords[2]);
+
+ // # Press Enter on the textbox
+ await keywordsInput.press('Enter');
+
+ // * Verify that the keywords have been added to the collapsed description
+ await expect(channelPage.settingsModal.notificationsSettings.container.getByText(keywords[0])).toBeVisible();
+ await expect(channelPage.settingsModal.notificationsSettings.container.getByText(keywords[1])).toBeVisible();
+ await expect(channelPage.settingsModal.notificationsSettings.container.getByText(keywords[2])).toBeVisible();
+});
+
+test('MM-T5465-2 Should highlight the keywords when a message is sent with the keyword in center', async ({
+ pw,
+ pages,
+}) => {
+ const {user} = await pw.initSetup();
+
+ // # Log in as a user in new browser context
+ const {page} = await pw.testBrowser.login(user);
+
+ // # Visit default channel page
+ const channelPage = new pages.ChannelsPage(page);
+ await channelPage.goto();
+ await channelPage.toBeVisible();
+
+ // # Open settings modal
+ await channelPage.globalHeader.openSettings();
+ await channelPage.settingsModal.toBeVisible();
+
+ // # Open notifications tab
+ await channelPage.settingsModal.openNotificationsTab();
+
+ // # Open keywords that get highlighted section
+ await channelPage.settingsModal.notificationsSettings.expandSection('keysWithHighlight');
+
+ // # Enter the keyword
+ const keywordsInput = await channelPage.settingsModal.notificationsSettings.getKeywordsInput();
+ await keywordsInput.type(keywords[3]);
+ await keywordsInput.press('Tab');
+
+ // # Save the keyword
+ await channelPage.settingsModal.notificationsSettings.save();
+
+ // # Close the settings modal
+ await channelPage.settingsModal.closeModal();
+
+ // # Post a message without the keyword
+ const messageWithoutKeyword = 'This message does not contain the keyword';
+ await channelPage.centerView.postCreate.postMessage(messageWithoutKeyword);
+ const lastPostWithoutHighlight = await channelPage.centerView.getLastPost();
+
+ // * Verify that the keywords are not highlighted
+ await expect(lastPostWithoutHighlight.container.getByText(messageWithoutKeyword)).toBeVisible();
+ await expect(lastPostWithoutHighlight.container.getByText(messageWithoutKeyword)).not.toHaveClass(
+ highlightWithoutNotificationClass,
+ );
+
+ // # Post a message with the keyword
+ const messageWithKeyword = `This message contains the keyword ${keywords[3]}`;
+ await channelPage.centerView.postCreate.postMessage(messageWithKeyword);
+ const lastPostWithHighlight = await channelPage.centerView.getLastPost();
+
+ // * Verify that the keywords are highlighted
+ await expect(lastPostWithHighlight.container.getByText(messageWithKeyword)).toBeVisible();
+ await expect(lastPostWithHighlight.container.getByText(keywords[3])).toHaveClass(highlightWithoutNotificationClass);
+});
+
+test('MM-T5465-3 Should highlight the keywords when a message is sent with the keyword in rhs', async ({pw, pages}) => {
+ const {user} = await pw.initSetup();
+
+ // # Log in as a user in new browser context
+ const {page} = await pw.testBrowser.login(user);
+
+ // # Visit default channel page
+ const channelPage = new pages.ChannelsPage(page);
+ await channelPage.goto();
+ await channelPage.toBeVisible();
+
+ // # Open settings modal
+ await channelPage.globalHeader.openSettings();
+ await channelPage.settingsModal.toBeVisible();
+
+ // # Open notifications tab
+ await channelPage.settingsModal.openNotificationsTab();
+
+ // # Open keywords that get highlighted section
+ await channelPage.settingsModal.notificationsSettings.expandSection('keysWithHighlight');
+
+ // # Enter the keyword
+ const keywordsInput = await channelPage.settingsModal.notificationsSettings.getKeywordsInput();
+ await keywordsInput.type(keywords[3]);
+ await keywordsInput.press('Tab');
+
+ // # Save the keyword
+ await channelPage.settingsModal.notificationsSettings.save();
+
+ // # Close the settings modal
+ await channelPage.settingsModal.closeModal();
+
+ // # Post a message without the keyword
+ const messageWithoutKeyword = 'This message does not contain the keyword';
+ await channelPage.centerView.postCreate.postMessage(messageWithoutKeyword);
+ const lastPostWithoutHighlight = await channelPage.centerView.getLastPost();
+
+ // # Open the message in the RHS
+ await lastPostWithoutHighlight.hover();
+ await lastPostWithoutHighlight.postMenu.toBeVisible();
+ await lastPostWithoutHighlight.postMenu.reply();
+ await channelPage.sidebarRight.toBeVisible();
+
+ // # Post a message with the keyword in the RHS
+ const messageWithKeyword = `This message contains the keyword ${keywords[3]}`;
+ await channelPage.sidebarRight.postCreate.postMessage(messageWithKeyword);
+
+ // * Verify that the keywords are highlighted
+ const lastPostWithHighlightInRHS = await channelPage.sidebarRight.getLastPost();
+ await expect(lastPostWithHighlightInRHS.container.getByText(messageWithKeyword)).toBeVisible();
+ await expect(lastPostWithHighlightInRHS.container.getByText(keywords[3])).toHaveClass(
+ highlightWithoutNotificationClass,
+ );
+});
+
+test('MM-T5465-4 Highlighted keywords should not appear in the Recent Mentions', async ({pw, pages}) => {
+ const {user} = await pw.initSetup();
+
+ // # Log in as a user in new browser context
+ const {page} = await pw.testBrowser.login(user);
+
+ // # Visit default channel page
+ const channelPage = new pages.ChannelsPage(page);
+ await channelPage.goto();
+ await channelPage.toBeVisible();
+
+ // # Open settings modal
+ await channelPage.globalHeader.openSettings();
+ await channelPage.settingsModal.toBeVisible();
+
+ // # Open notifications tab
+ await channelPage.settingsModal.openNotificationsTab();
+
+ // # Open keywords that get highlighted section
+ await channelPage.settingsModal.notificationsSettings.expandSection('keysWithHighlight');
+
+ // # Enter the keyword
+ const keywordsInput = await channelPage.settingsModal.notificationsSettings.getKeywordsInput();
+ await keywordsInput.type(keywords[0]);
+ await keywordsInput.press('Tab');
+
+ // # Save the keyword
+ await channelPage.settingsModal.notificationsSettings.save();
+
+ // # Close the settings modal
+ await channelPage.settingsModal.closeModal();
+
+ // # Open the recent mentions
+ await channelPage.globalHeader.openRecentMentions();
+
+ // * Verify recent mentions is empty
+ await channelPage.sidebarRight.toBeVisible();
+ await expect(channelPage.sidebarRight.container.getByText('No mentions yet')).toBeVisible();
+});
+
+test('MM-T5465-5 Should highlight keywords in message sent from another user', async ({pw, pages}) => {
+ const {adminClient, team, adminUser, user} = await pw.initSetup();
+
+ if (!adminUser) {
+ throw new Error('Failed to create admin user');
+ }
+
+ // # Get the default channel of the team for getting the channel id
+ const channel = await adminClient.getChannelByName(team.id, 'town-square');
+
+ const highlightKeyword = keywords[0];
+ const messageWithKeyword = `This recieved message contains the ${highlightKeyword} keyword `;
+
+ // # Create a post containing the keyword in the channel by admin
+ await adminClient.createPost(
+ createRandomPost({
+ message: messageWithKeyword,
+ channel_id: channel.id,
+ user_id: adminUser.id,
+ }),
+ );
+
+ // # Now log in as a user in new browser context
+ const {page} = await pw.testBrowser.login(user);
+
+ // # Visit default channel page
+ const channelPage = new pages.ChannelsPage(page);
+ await channelPage.goto();
+ await channelPage.toBeVisible();
+
+ // # Open settings modal
+ await channelPage.globalHeader.openSettings();
+ await channelPage.settingsModal.toBeVisible();
+
+ // # Open notifications tab
+ await channelPage.settingsModal.openNotificationsTab();
+
+ // # Open keywords that get highlighted section
+ await channelPage.settingsModal.notificationsSettings.expandSection('keysWithHighlight');
+
+ // # Enter the keyword
+ const keywordsInput = await channelPage.settingsModal.notificationsSettings.getKeywordsInput();
+ await keywordsInput.type(keywords[0]);
+ await keywordsInput.press('Tab');
+
+ // # Save the keyword
+ await channelPage.settingsModal.notificationsSettings.save();
+
+ // # Close the settings modal
+ await channelPage.settingsModal.closeModal();
+
+ // * Verify that the keywords are highlighted in the last message recieved
+ const lastPostWithHighlight = await channelPage.centerView.getLastPost();
+ await expect(lastPostWithHighlight.container.getByText(messageWithKeyword)).toBeVisible();
+ await expect(lastPostWithHighlight.container.getByText(highlightKeyword)).toHaveClass(
+ highlightWithoutNotificationClass,
+ );
+});
diff --git a/server/public/model/notify_admin.go b/server/public/model/notify_admin.go
index 48eb238c92..6d496e5fcd 100644
--- a/server/public/model/notify_admin.go
+++ b/server/public/model/notify_admin.go
@@ -13,17 +13,18 @@ import (
type MattermostFeature string
const (
- PaidFeatureGuestAccounts = MattermostFeature("mattermost.feature.guest_accounts")
- PaidFeatureCustomUsergroups = MattermostFeature("mattermost.feature.custom_user_groups")
- PaidFeatureCreateMultipleTeams = MattermostFeature("mattermost.feature.create_multiple_teams")
- PaidFeatureStartcall = MattermostFeature("mattermost.feature.start_call")
- PaidFeaturePlaybooksRetrospective = MattermostFeature("mattermost.feature.playbooks_retro")
- PaidFeatureUnlimitedMessages = MattermostFeature("mattermost.feature.unlimited_messages")
- PaidFeatureUnlimitedFileStorage = MattermostFeature("mattermost.feature.unlimited_file_storage")
- PaidFeatureAllProfessionalfeatures = MattermostFeature("mattermost.feature.all_professional")
- PaidFeatureAllEnterprisefeatures = MattermostFeature("mattermost.feature.all_enterprise")
- UpgradeDowngradedWorkspace = MattermostFeature("mattermost.feature.upgrade_downgraded_workspace")
- PluginFeature = MattermostFeature("mattermost.feature.plugin")
+ PaidFeatureGuestAccounts = MattermostFeature("mattermost.feature.guest_accounts")
+ PaidFeatureCustomUsergroups = MattermostFeature("mattermost.feature.custom_user_groups")
+ PaidFeatureCreateMultipleTeams = MattermostFeature("mattermost.feature.create_multiple_teams")
+ PaidFeatureStartcall = MattermostFeature("mattermost.feature.start_call")
+ PaidFeaturePlaybooksRetrospective = MattermostFeature("mattermost.feature.playbooks_retro")
+ PaidFeatureUnlimitedMessages = MattermostFeature("mattermost.feature.unlimited_messages")
+ PaidFeatureUnlimitedFileStorage = MattermostFeature("mattermost.feature.unlimited_file_storage")
+ PaidFeatureAllProfessionalfeatures = MattermostFeature("mattermost.feature.all_professional")
+ PaidFeatureAllEnterprisefeatures = MattermostFeature("mattermost.feature.all_enterprise")
+ UpgradeDowngradedWorkspace = MattermostFeature("mattermost.feature.upgrade_downgraded_workspace")
+ PluginFeature = MattermostFeature("mattermost.feature.plugin")
+ PaidFeatureHighlightWithoutNotification = MattermostFeature("mattermost.feature.highlight_without_notification")
)
var validSKUs = map[string]struct{}{
@@ -33,16 +34,17 @@ var validSKUs = map[string]struct{}{
// These are the features a non admin would typically ping an admin about
var paidFeatures = map[MattermostFeature]struct{}{
- PaidFeatureGuestAccounts: {},
- PaidFeatureCustomUsergroups: {},
- PaidFeatureCreateMultipleTeams: {},
- PaidFeatureStartcall: {},
- PaidFeaturePlaybooksRetrospective: {},
- PaidFeatureUnlimitedMessages: {},
- PaidFeatureUnlimitedFileStorage: {},
- PaidFeatureAllProfessionalfeatures: {},
- PaidFeatureAllEnterprisefeatures: {},
- UpgradeDowngradedWorkspace: {},
+ PaidFeatureGuestAccounts: {},
+ PaidFeatureCustomUsergroups: {},
+ PaidFeatureCreateMultipleTeams: {},
+ PaidFeatureStartcall: {},
+ PaidFeaturePlaybooksRetrospective: {},
+ PaidFeatureUnlimitedMessages: {},
+ PaidFeatureUnlimitedFileStorage: {},
+ PaidFeatureAllProfessionalfeatures: {},
+ PaidFeatureAllEnterprisefeatures: {},
+ UpgradeDowngradedWorkspace: {},
+ PaidFeatureHighlightWithoutNotification: {},
}
type NotifyAdminToUpgradeRequest struct {
diff --git a/server/public/model/user.go b/server/public/model/user.go
index fa1169c70d..ef42d5998c 100644
--- a/server/public/model/user.go
+++ b/server/public/model/user.go
@@ -36,6 +36,7 @@ const (
ChannelMentionsNotifyProp = "channel"
CommentsNotifyProp = "comments"
MentionKeysNotifyProp = "mention_keys"
+ HighlightsNotifyProp = "highlight_keys"
CommentsNotifyNever = "never"
CommentsNotifyRoot = "root"
CommentsNotifyAny = "any"
diff --git a/webapp/channels/src/components/__snapshots__/setting_item_min.test.tsx.snap b/webapp/channels/src/components/__snapshots__/setting_item_min.test.tsx.snap
new file mode 100644
index 0000000000..7c79e274d1
--- /dev/null
+++ b/webapp/channels/src/components/__snapshots__/setting_item_min.test.tsx.snap
@@ -0,0 +1,75 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`components/SettingItemMin should match snapshot 1`] = `
+
+
+
+ title
+
+
+
+
+ describe
+
+
+`;
+
+exports[`components/SettingItemMin should match snapshot, on disableOpen to true 1`] = `
+
+
+
+ title
+
+
+
+
+ describe
+
+
+`;
diff --git a/webapp/channels/src/components/__snapshots__/textbox.test.tsx.snap b/webapp/channels/src/components/__snapshots__/textbox.test.tsx.snap
index 31aa7f6904..da8232ae7e 100644
--- a/webapp/channels/src/components/__snapshots__/textbox.test.tsx.snap
+++ b/webapp/channels/src/components/__snapshots__/textbox.test.tsx.snap
@@ -18,7 +18,6 @@ exports[`components/TextBox should match snapshot with additional, optional prop
"hideUtilities": true,
}
}
- mentionKeys={Array []}
message="some test text"
/>
@@ -147,7 +146,6 @@ exports[`components/TextBox should match snapshot with required props 1`] = `
"hideUtilities": true,
}
}
- mentionKeys={Array []}
message="some test text"
/>
@@ -271,7 +269,6 @@ exports[`components/TextBox should throw error when new property is too long 1`]
"hideUtilities": true,
}
}
- mentionKeys={Array []}
message="some test text that exceeds char limit"
/>
@@ -395,7 +392,6 @@ exports[`components/TextBox should throw error when value is too long 1`] = `
"hideUtilities": true,
}
}
- mentionKeys={Array []}
message="some test text that exceeds char limit"
/>
diff --git a/webapp/channels/src/components/toggle_modal_button/__snapshots__/toggle_modal_button.test.tsx.snap b/webapp/channels/src/components/__snapshots__/toggle_modal_button.test.tsx.snap
similarity index 90%
rename from webapp/channels/src/components/toggle_modal_button/__snapshots__/toggle_modal_button.test.tsx.snap
rename to webapp/channels/src/components/__snapshots__/toggle_modal_button.test.tsx.snap
index fbd01404d0..58c7b7f2be 100644
--- a/webapp/channels/src/components/toggle_modal_button/__snapshots__/toggle_modal_button.test.tsx.snap
+++ b/webapp/channels/src/components/__snapshots__/toggle_modal_button.test.tsx.snap
@@ -2,11 +2,6 @@
exports[`components/ToggleModalButton component should match snapshot 1`] = `
-
+
}
className=""
id="SystemRoleUsers"
@@ -191,6 +193,7 @@ exports[`admin_console/system_role_users should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -236,6 +239,7 @@ exports[`admin_console/system_role_users should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -283,6 +287,7 @@ exports[`admin_console/system_role_users should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -328,6 +333,7 @@ exports[`admin_console/system_role_users should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -359,7 +365,7 @@ exports[`admin_console/system_role_users should match snapshot 1`] = `
exports[`admin_console/system_role_users should match snapshot with readOnly true 1`] = `
-
+
}
className=""
id="SystemRoleUsers"
@@ -547,6 +555,7 @@ exports[`admin_console/system_role_users should match snapshot with readOnly tru
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -592,6 +601,7 @@ exports[`admin_console/system_role_users should match snapshot with readOnly tru
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -639,6 +649,7 @@ exports[`admin_console/system_role_users should match snapshot with readOnly tru
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -684,6 +695,7 @@ exports[`admin_console/system_role_users should match snapshot with readOnly tru
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
diff --git a/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/__snapshots__/channel_groups.test.tsx.snap b/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/__snapshots__/channel_groups.test.tsx.snap
index ce09ad883c..afafa63097 100644
--- a/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/__snapshots__/channel_groups.test.tsx.snap
+++ b/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/__snapshots__/channel_groups.test.tsx.snap
@@ -3,7 +3,7 @@
exports[`admin_console/team_channel_settings/channel/ChannelGroups should match snapshot 1`] = `
-
+
}
className=""
id="channel_groups"
diff --git a/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/channel_members/__snapshots__/channel_members.test.tsx.snap b/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/channel_members/__snapshots__/channel_members.test.tsx.snap
index 5691dedfa9..d68b076ad6 100644
--- a/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/channel_members/__snapshots__/channel_members.test.tsx.snap
+++ b/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/channel_members/__snapshots__/channel_members.test.tsx.snap
@@ -3,7 +3,7 @@
exports[`admin_console/team_channel_settings/channel/ChannelMembers should match snapshot 1`] = `
-
+
}
className=""
id="channelMembers"
@@ -215,6 +215,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -253,6 +254,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -291,6 +293,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -314,7 +317,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match
exports[`admin_console/team_channel_settings/channel/ChannelMembers should match snapshot loading no users 1`] = `
-
+
}
className=""
id="channelMembers"
diff --git a/webapp/channels/src/components/admin_console/team_channel_settings/group/__snapshots__/group_row.test.tsx.snap b/webapp/channels/src/components/admin_console/team_channel_settings/group/__snapshots__/group_row.test.tsx.snap
index fbba0f889c..66aabf7a88 100644
--- a/webapp/channels/src/components/admin_console/team_channel_settings/group/__snapshots__/group_row.test.tsx.snap
+++ b/webapp/channels/src/components/admin_console/team_channel_settings/group/__snapshots__/group_row.test.tsx.snap
@@ -17,7 +17,7 @@ exports[`admin_console/team_channel_settings/group/GroupRow should match snapsho
-
-
+
-
+
}
className=""
id="team_groups"
diff --git a/webapp/channels/src/components/admin_console/team_channel_settings/team/details/team_members/__snapshots__/team_members.test.tsx.snap b/webapp/channels/src/components/admin_console/team_channel_settings/team/details/team_members/__snapshots__/team_members.test.tsx.snap
index a2bee43b1e..e69d8ee603 100644
--- a/webapp/channels/src/components/admin_console/team_channel_settings/team/details/team_members/__snapshots__/team_members.test.tsx.snap
+++ b/webapp/channels/src/components/admin_console/team_channel_settings/team/details/team_members/__snapshots__/team_members.test.tsx.snap
@@ -3,7 +3,7 @@
exports[`admin_console/team_channel_settings/team/TeamMembers should match snapshot 1`] = `
-
+
}
className=""
id="teamMembers"
@@ -187,6 +187,7 @@ exports[`admin_console/team_channel_settings/team/TeamMembers should match snaps
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -225,6 +226,7 @@ exports[`admin_console/team_channel_settings/team/TeamMembers should match snaps
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -263,6 +265,7 @@ exports[`admin_console/team_channel_settings/team/TeamMembers should match snaps
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -286,7 +289,7 @@ exports[`admin_console/team_channel_settings/team/TeamMembers should match snaps
exports[`admin_console/team_channel_settings/team/TeamMembers should match snapshot loading no users 1`] = `
-
+
}
className=""
id="teamMembers"
diff --git a/webapp/channels/src/components/admin_console/user_grid/__snapshots__/user_grid.test.tsx.snap b/webapp/channels/src/components/admin_console/user_grid/__snapshots__/user_grid.test.tsx.snap
index 7a0a2071a9..d1c11918ff 100644
--- a/webapp/channels/src/components/admin_console/user_grid/__snapshots__/user_grid.test.tsx.snap
+++ b/webapp/channels/src/components/admin_console/user_grid/__snapshots__/user_grid.test.tsx.snap
@@ -84,6 +84,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -128,6 +129,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -187,6 +189,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -234,6 +237,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -278,6 +282,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -337,6 +342,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -453,6 +459,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -505,6 +512,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -556,6 +564,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -603,6 +612,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -647,6 +657,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -706,6 +717,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -753,6 +765,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -797,6 +810,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -856,6 +870,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -972,6 +987,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -1016,6 +1032,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -1075,6 +1092,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
diff --git a/webapp/channels/src/components/channel_header_dropdown/__snapshots__/channel_header_dropdown.test.tsx.snap b/webapp/channels/src/components/channel_header_dropdown/__snapshots__/channel_header_dropdown.test.tsx.snap
index 7017d5f504..b1c1727a7b 100644
--- a/webapp/channels/src/components/channel_header_dropdown/__snapshots__/channel_header_dropdown.test.tsx.snap
+++ b/webapp/channels/src/components/channel_header_dropdown/__snapshots__/channel_header_dropdown.test.tsx.snap
@@ -137,6 +137,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with no plugin i
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -213,6 +214,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with no plugin i
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -757,6 +759,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with no plugin i
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -966,6 +969,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with plugins 1`]
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -1042,6 +1046,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with plugins 1`]
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -1586,6 +1591,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with plugins 1`]
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
diff --git a/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/collapse_view.test.tsx.snap b/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/collapse_view.test.tsx.snap
index e9522877c4..9e2879a20f 100644
--- a/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/collapse_view.test.tsx.snap
+++ b/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/collapse_view.test.tsx.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/channel_notifications_modal/CollapseView should match snapshot, DESKTOP on collapsed view 1`] = `
- void};
diff --git a/webapp/channels/src/components/integrations/installed_outgoing_webhooks/__snapshots__/installed_outgoing_webhooks.test.tsx.snap b/webapp/channels/src/components/integrations/installed_outgoing_webhooks/__snapshots__/installed_outgoing_webhooks.test.tsx.snap
index 6d606b18a4..fa6299d7d0 100644
--- a/webapp/channels/src/components/integrations/installed_outgoing_webhooks/__snapshots__/installed_outgoing_webhooks.test.tsx.snap
+++ b/webapp/channels/src/components/integrations/installed_outgoing_webhooks/__snapshots__/installed_outgoing_webhooks.test.tsx.snap
@@ -48,6 +48,7 @@ exports[`components/integrations/InstalledOutgoingWebhooks should match snapshot
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -155,6 +156,7 @@ exports[`components/integrations/InstalledOutgoingWebhooks should match snapshot
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
diff --git a/webapp/channels/src/components/markdown/index.ts b/webapp/channels/src/components/markdown/index.ts
index ba562269f5..6fbfe1854d 100644
--- a/webapp/channels/src/components/markdown/index.ts
+++ b/webapp/channels/src/components/markdown/index.ts
@@ -24,7 +24,7 @@ function makeGetChannelNamesMap() {
return createSelector(
'makeGetChannelNamesMap',
getChannelNameToDisplayNameMap,
- (state: GlobalState, props: OwnProps) => props && props.channelNamesMap,
+ (_: GlobalState, props: OwnProps) => props && props.channelNamesMap,
(channelNamesMap, channelMentions) => {
if (channelMentions) {
return Object.assign({}, channelMentions, channelNamesMap);
@@ -63,6 +63,7 @@ function makeMapStateToProps() {
}
const connector = connect(makeMapStateToProps);
+
export type PropsFromRedux = ConnectedProps;
export default connector(Markdown);
diff --git a/webapp/channels/src/components/markdown/markdown.tsx b/webapp/channels/src/components/markdown/markdown.tsx
index 3ff9c62376..12c774f234 100644
--- a/webapp/channels/src/components/markdown/markdown.tsx
+++ b/webapp/channels/src/components/markdown/markdown.tsx
@@ -5,6 +5,8 @@ import React from 'react';
import type {PostImage, PostType} from '@mattermost/types/posts';
+import type {HighlightWithoutNotificationKey} from 'mattermost-redux/selectors/entities/users';
+
import PostEditedIndicator from 'components/post_view/post_edited_indicator';
import type EmojiMap from 'utils/emoji_map';
@@ -53,6 +55,7 @@ export type OwnProps = {
* An array of words that can be used to mention a user
*/
mentionKeys?: MentionKey[];
+ highlightKeys?: HighlightWithoutNotificationKey[];
/**
* Any extra props that should be passed into the image component
@@ -92,6 +95,7 @@ function Markdown({
message = '',
channelNamesMap,
mentionKeys,
+ highlightKeys,
imageProps,
channelId,
hasPluginTooltips,
@@ -123,6 +127,7 @@ function Markdown({
autolinkedUrlSchemes,
siteURL,
mentionKeys,
+ highlightKeys,
atMentions: true,
channelNamesMap,
proxyImages: hasImageProxy && proxyImages,
diff --git a/webapp/channels/src/components/more_direct_channels/__snapshots__/more_direct_channels.test.tsx.snap b/webapp/channels/src/components/more_direct_channels/__snapshots__/more_direct_channels.test.tsx.snap
index 1818a0f446..6073e4686f 100644
--- a/webapp/channels/src/components/more_direct_channels/__snapshots__/more_direct_channels.test.tsx.snap
+++ b/webapp/channels/src/components/more_direct_channels/__snapshots__/more_direct_channels.test.tsx.snap
@@ -95,6 +95,7 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -133,6 +134,7 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -171,6 +173,7 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -209,6 +212,7 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -247,6 +251,7 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -380,6 +385,7 @@ exports[`components/MoreDirectChannels should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -418,6 +424,7 @@ exports[`components/MoreDirectChannels should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -456,6 +463,7 @@ exports[`components/MoreDirectChannels should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -499,6 +507,7 @@ exports[`components/MoreDirectChannels should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -539,6 +548,7 @@ exports[`components/MoreDirectChannels should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
diff --git a/webapp/channels/src/components/post_markdown/index.ts b/webapp/channels/src/components/post_markdown/index.ts
index f8f4894e8f..32345947c0 100644
--- a/webapp/channels/src/components/post_markdown/index.ts
+++ b/webapp/channels/src/components/post_markdown/index.ts
@@ -1,14 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {connect} from 'react-redux';
+import {type ConnectedProps, connect} from 'react-redux';
import type {Channel} from '@mattermost/types/channels';
import type {Post} from '@mattermost/types/posts';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
-import {getConfig} from 'mattermost-redux/selectors/entities/general';
+import {getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
+import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {
getMyGroupMentionKeysForChannel,
getMyGroupMentionKeys,
@@ -16,15 +17,16 @@ import {
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
-import {getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users';
+import {getCurrentUserMentionKeys, getHighlightWithoutNotificationKeys} from 'mattermost-redux/selectors/entities/users';
import {canManageMembers} from 'utils/channel_utils';
import {Preferences} from 'utils/constants';
+import {isEnterpriseOrCloudOrSKUStarterFree} from 'utils/license_utils';
import type {MentionKey} from 'utils/text_formatting';
import type {GlobalState} from 'types/store';
-import PostMarkdown from './post_markdown';
+import PostMarkdown, {type OwnProps} from './post_markdown';
export function makeGetMentionKeysForPost(): (
state: GlobalState,
@@ -54,18 +56,19 @@ export function makeGetMentionKeysForPost(): (
);
}
-type OwnProps = {
- channelId: string;
- mentionKeys: MentionKey[];
- post?: Post;
-};
-
function makeMapStateToProps() {
const getMentionKeysForPost = makeGetMentionKeysForPost();
return (state: GlobalState, ownProps: OwnProps) => {
const channel = getChannel(state, ownProps.channelId);
const currentTeam = getCurrentTeam(state) || {};
+
+ const license = getLicense(state);
+ const subscriptionProduct = getSubscriptionProduct(state);
+
+ const config = getConfig(state);
+ const isEnterpriseReady = config.BuildEnterpriseReady === 'true';
+
return {
channel,
currentTeam,
@@ -73,11 +76,18 @@ function makeMapStateToProps() {
hasPluginTooltips: Boolean(state.plugins.components.LinkTooltip),
isUserCanManageMembers: channel && canManageMembers(state, channel),
mentionKeys: getMentionKeysForPost(state, ownProps.post, channel),
+ highlightKeys: getHighlightWithoutNotificationKeys(state),
isMilitaryTime: getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
timezone: getCurrentTimezone(state),
hideGuestTags: getConfig(state).HideGuestTags === 'true',
+ isEnterpriseOrCloudOrSKUStarterFree: isEnterpriseOrCloudOrSKUStarterFree(license, subscriptionProduct, isEnterpriseReady),
+ isEnterpriseReady,
};
};
}
-export default connect(makeMapStateToProps)(PostMarkdown);
+const connector = connect(makeMapStateToProps);
+
+export type PropsFromRedux = ConnectedProps;
+
+export default connector(PostMarkdown);
diff --git a/webapp/channels/src/components/post_markdown/post_markdown.test.tsx b/webapp/channels/src/components/post_markdown/post_markdown.test.tsx
index 33bf5d361b..ef617eb53c 100644
--- a/webapp/channels/src/components/post_markdown/post_markdown.test.tsx
+++ b/webapp/channels/src/components/post_markdown/post_markdown.test.tsx
@@ -10,11 +10,14 @@ import {Posts} from 'mattermost-redux/constants';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
+import type {PluginComponent} from 'types/store/plugins';
+
import PostMarkdown from './post_markdown';
describe('components/PostMarkdown', () => {
const baseProps = {
- imageProps: {},
+ imageProps: {} as Record,
+ pluginHooks: [],
message: 'message',
post: TestHelper.getPostMock(),
mentionKeys: [{key: 'a'}, {key: 'b'}, {key: 'c'}],
@@ -22,6 +25,14 @@ describe('components/PostMarkdown', () => {
channel: TestHelper.getChannelMock(),
currentTeam: TestHelper.getTeamMock(),
hideGuestTags: false,
+ isMilitaryTime: false,
+ timezone: '',
+ highlightKeys: [],
+ hasPluginTooltips: false,
+ isUserCanManageMembers: false,
+ isEnterpriseOrCloudOrSKUStarterFree: true,
+ isEnterpriseReady: false,
+ dispatch: jest.fn(),
};
const state = {entities: {
@@ -224,7 +235,7 @@ describe('components/PostMarkdown', () => {
return updatedMessage + '!';
},
},
- ],
+ ] as PluginComponent[],
};
renderWithContext(, state);
expect(screen.queryByText('world', {exact: true})).not.toBeInTheDocument();
@@ -258,7 +269,7 @@ describe('components/PostMarkdown', () => {
return post.message + '!';
},
},
- ],
+ ] as PluginComponent[],
};
renderWithContext(, state);
expect(screen.queryByText('world', {exact: true})).not.toBeInTheDocument();
diff --git a/webapp/channels/src/components/post_markdown/post_markdown.tsx b/webapp/channels/src/components/post_markdown/post_markdown.tsx
index 2f9c5b97a8..ad77ddb43f 100644
--- a/webapp/channels/src/components/post_markdown/post_markdown.tsx
+++ b/webapp/channels/src/components/post_markdown/post_markdown.tsx
@@ -4,65 +4,45 @@
import memoize from 'memoize-one';
import React from 'react';
-import type {Channel} from '@mattermost/types/channels';
import type {Post} from '@mattermost/types/posts';
-import type {Team} from '@mattermost/types/teams';
import {Posts} from 'mattermost-redux/constants';
import Markdown from 'components/markdown';
-import type {MentionKey, TextFormattingOptions} from 'utils/text_formatting';
+import type {TextFormattingOptions} from 'utils/text_formatting';
import {renderReminderSystemBotMessage, renderSystemMessage} from './system_message_helpers';
-type Props = {
+import {type PropsFromRedux} from './index';
- /*
+export type OwnProps = {
+
+ /**
* Any extra props that should be passed into the image component
*/
- imageProps?: Record;
+ imageProps?: Record;
- /*
+ /**
* The post text to be rendered
*/
message: string;
- /*
+ /**
* The optional post for which this message is being rendered
*/
post?: Post;
-
- /*
- * The id of the channel that this post is being rendered in
- */
- channelId?: string;
- channel: Channel;
- currentTeam: Team;
- options?: TextFormattingOptions;
- pluginHooks?: Array>;
-
- /**
- * Whether or not to place the LinkTooltip component inside links
- */
- hasPluginTooltips?: boolean;
- isUserCanManageMembers?: boolean;
- mentionKeys: MentionKey[];
+ channelId: string;
/**
* Whether or not to render the post edited indicator
* @default true
*/
showPostEditedIndicator?: boolean;
+ options?: TextFormattingOptions;
+};
- /**
- * Whether the user prefers Military time
- */
- isMilitaryTime?: boolean;
- timezone?: string;
-
- hideGuestTags: boolean;
-}
+type Props = PropsFromRedux & OwnProps;
export default class PostMarkdown extends React.PureComponent {
static defaultProps = {
@@ -82,11 +62,10 @@ export default class PostMarkdown extends React.PureComponent {
});
render() {
- let {message} = this.props;
- const {post, mentionKeys} = this.props;
+ let message = this.props.message;
- if (post) {
- const renderedSystemMessage = renderSystemMessage(post,
+ if (this.props.post) {
+ const renderedSystemMessage = renderSystemMessage(this.props.post,
this.props.currentTeam,
this.props.channel,
this.props.hideGuestTags,
@@ -98,39 +77,45 @@ export default class PostMarkdown extends React.PureComponent {
}
}
- if (post && post.type === Posts.POST_TYPES.REMINDER) {
- const renderedSystemBotMessage = renderReminderSystemBotMessage(post, this.props.currentTeam);
+ if (this.props.post && this.props.post.type === Posts.POST_TYPES.REMINDER) {
+ const renderedSystemBotMessage = renderReminderSystemBotMessage(this.props.post, this.props.currentTeam);
return {renderedSystemBotMessage}
;
}
- // Proxy images if we have an image proxy and the server hasn't already rewritten the post's image URLs.
- const proxyImages = !post || !post.message_source || post.message === post.message_source;
- const channelNamesMap = post && post.props && post.props.channel_mentions;
+ // Proxy images if we have an image proxy and the server hasn't already rewritten the this.props.post's image URLs.
+ const proxyImages = !this.props.post || !this.props.post.message_source || this.props.post.message === this.props.post.message_source;
+ const channelNamesMap = this.props.post && this.props.post.props && this.props.post.props.channel_mentions;
this.props.pluginHooks?.forEach((o) => {
- if (o && o.hook && post) {
- message = o.hook(post, message);
+ if (o && o.hook && this.props.post) {
+ message = o.hook(this.props.post, message);
}
});
let mentionHighlight = this.props.options?.mentionHighlight;
- if (post && post.props) {
- mentionHighlight = !post.props.mentionHighlightDisabled;
+ if (this.props.post && this.props.post.props) {
+ mentionHighlight = !this.props.post.props.mentionHighlightDisabled;
}
const options = this.getOptions(
this.props.options,
- post?.props?.disable_group_highlight === true,
+ this.props.post?.props?.disable_group_highlight === true,
mentionHighlight,
- post?.edit_at,
+ this.props.post?.edit_at,
);
+ let highlightKeys;
+ if (!this.props.isEnterpriseOrCloudOrSKUStarterFree && this.props.isEnterpriseReady) {
+ highlightKeys = this.props.highlightKeys;
+ }
+
return (
{
options={options}
post={post}
channelId={post.channel_id}
- mentionKeys={[]}
showPostEditedIndicator={this.props.showPostEditedIndicator}
/>
diff --git a/webapp/channels/src/components/profile_popover/__snapshots__/profile_popover.test.tsx.snap b/webapp/channels/src/components/profile_popover/__snapshots__/profile_popover.test.tsx.snap
index b7d3187bc8..38cbda81e8 100644
--- a/webapp/channels/src/components/profile_popover/__snapshots__/profile_popover.test.tsx.snap
+++ b/webapp/channels/src/components/profile_popover/__snapshots__/profile_popover.test.tsx.snap
@@ -139,6 +139,7 @@ exports[`components/ProfilePopover should disable start call button when user is
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -197,7 +198,7 @@ exports[`components/ProfilePopover should disable start call button when user is
}
>
-
-
+
-
-
+
-
-
+
-
-
+
@@ -1632,6 +1645,7 @@ exports[`components/ProfilePopover should match snapshot when calls are disabled
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -1802,6 +1816,7 @@ exports[`components/ProfilePopover should match snapshot with custom status 1`]
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -1898,7 +1913,7 @@ exports[`components/ProfilePopover should match snapshot with custom status 1`]
}
>
-
-
+
-
-
+
-
-
+
-
-
+
-
-
-
-
-
+ />
+
+
+
+
@@ -4430,6 +4399,7 @@ exports[`components/ProfilePopover should show the start call button when isCall
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@@ -4511,6 +4481,7 @@ exports[`components/ProfilePopover should show the start call button when isCall
"desktop_sound": "false",
"email": "false",
"first_name": "false",
+ "highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
diff --git a/webapp/channels/src/components/setting_item.tsx b/webapp/channels/src/components/setting_item.tsx
index 245702eae7..11bf69c480 100644
--- a/webapp/channels/src/components/setting_item.tsx
+++ b/webapp/channels/src/components/setting_item.tsx
@@ -5,7 +5,7 @@ import React from 'react';
import type {ReactNode, RefObject} from 'react';
import SettingItemMin from 'components/setting_item_min';
-import type SettingItemMinComponent from 'components/setting_item_min/setting_item_min';
+import type SettingItemMinComponent from 'components/setting_item_min';
type Props = {
@@ -27,36 +27,33 @@ type Props = {
/**
* The setting UI when it is maximized (open)
*/
- max: ReactNode | null;
+ max: ReactNode;
// Props to pass through for SettingItemMin
updateSection: (section: string) => void;
title?: ReactNode;
- disableOpen?: boolean;
+ isDisabled?: boolean;
describe?: ReactNode;
+
+ /**
+ * Replacement in place of edit button when the setting (in collapsed mode) is disabled
+ */
+ collapsedEditButtonWhenDisabled?: ReactNode;
}
+
export default class SettingItem extends React.PureComponent {
minRef: RefObject;
- static defaultProps = {
- infoPosition: 'bottom',
- saving: false,
- section: '',
- containerStyle: '',
- };
-
constructor(props: Props) {
super(props);
+
this.minRef = React.createRef();
}
- focusEditButton(): void {
- this.minRef.current?.focus();
- }
-
componentDidUpdate(prevProps: Props) {
- if (prevProps.active && !this.props.active && this.props.areAllSectionsInactive) {
- this.focusEditButton();
+ // We want to bring back focus to the edit button when the section is opened and then closed along with all sections are closed
+ if (!this.props.active && prevProps.active && this.props.areAllSectionsInactive) {
+ this.minRef.current?.focus();
}
}
@@ -67,12 +64,13 @@ export default class SettingItem extends React.PureComponent {
return (
);
}
diff --git a/webapp/channels/src/components/setting_item_min/setting_item_min.test.tsx b/webapp/channels/src/components/setting_item_min.test.tsx
similarity index 82%
rename from webapp/channels/src/components/setting_item_min/setting_item_min.test.tsx
rename to webapp/channels/src/components/setting_item_min.test.tsx
index de8a4a178a..29a627dc46 100644
--- a/webapp/channels/src/components/setting_item_min/setting_item_min.test.tsx
+++ b/webapp/channels/src/components/setting_item_min.test.tsx
@@ -36,26 +36,26 @@ describe('components/SettingItemMin', () => {
expect(wrapper).toMatchSnapshot();
});
- test('should have called updateSection on handleUpdateSection with section', () => {
+ test('should have called updateSection on handleClick with section', () => {
const updateSection = jest.fn();
const props = {...baseProps, updateSection};
const wrapper = shallow(
,
);
- wrapper.instance().handleUpdateSection({preventDefault: jest.fn()} as any);
+ wrapper.instance().handleClick({preventDefault: jest.fn()} as any);
expect(updateSection).toHaveBeenCalled();
expect(updateSection).toHaveBeenCalledWith('section');
});
- test('should have called updateSection on handleUpdateSection with empty string', () => {
+ test('should have called updateSection on handleClick with empty string', () => {
const updateSection = jest.fn();
const props = {...baseProps, updateSection, section: ''};
const wrapper = shallow(
,
);
- wrapper.instance().handleUpdateSection({preventDefault: jest.fn()} as any);
+ wrapper.instance().handleClick({preventDefault: jest.fn()} as any);
expect(updateSection).toHaveBeenCalled();
expect(updateSection).toHaveBeenCalledWith('');
});
diff --git a/webapp/channels/src/components/setting_item_min.tsx b/webapp/channels/src/components/setting_item_min.tsx
new file mode 100644
index 0000000000..ba3af422b5
--- /dev/null
+++ b/webapp/channels/src/components/setting_item_min.tsx
@@ -0,0 +1,118 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import classNames from 'classnames';
+import React, {type ReactNode, type MouseEvent} from 'react';
+import {FormattedMessage} from 'react-intl';
+
+import EditIcon from 'components/widgets/icons/fa_edit_icon';
+
+import {a11yFocus} from 'utils/utils';
+
+interface Props {
+
+ /**
+ * Settings title
+ */
+ title: ReactNode;
+
+ /**
+ * Option to disable opening the setting
+ */
+ isDisabled?: boolean;
+
+ /**
+ * Settings or tab section
+ */
+ section: string;
+
+ /**
+ * Function to update section
+ */
+ updateSection: (section: string) => void;
+
+ /**
+ * Settings description
+ */
+ describe?: ReactNode;
+
+ /**
+ * Replacement in place of edit button when the setting (in collapsed mode) is disabled
+ */
+ collapsedEditButtonWhenDisabled?: ReactNode;
+}
+
+export default class SettingItemMin extends React.PureComponent {
+ private edit: HTMLButtonElement | null = null;
+
+ focus() {
+ a11yFocus(this.edit);
+ }
+
+ private getEdit = (node: HTMLButtonElement) => {
+ this.edit = node;
+ };
+
+ handleClick = (e: MouseEvent) => {
+ if (this.props.isDisabled) {
+ return;
+ }
+
+ e.preventDefault();
+ this.props.updateSection(this.props.section);
+ };
+
+ render() {
+ let editButtonComponent: ReactNode;
+
+ if (this.props.isDisabled) {
+ if (this.props.collapsedEditButtonWhenDisabled) {
+ editButtonComponent = this.props.collapsedEditButtonWhenDisabled;
+ } else {
+ editButtonComponent = null;
+ }
+ } else {
+ editButtonComponent = (
+
+ );
+ }
+
+ return (
+
+
+
+ {this.props.title}
+
+ {editButtonComponent}
+
+
+ {this.props.describe}
+
+
+ );
+ }
+}
diff --git a/webapp/channels/src/components/setting_item_min/__snapshots__/setting_item_min.test.tsx.snap b/webapp/channels/src/components/setting_item_min/__snapshots__/setting_item_min.test.tsx.snap
deleted file mode 100644
index 6dcdb6d06b..0000000000
--- a/webapp/channels/src/components/setting_item_min/__snapshots__/setting_item_min.test.tsx.snap
+++ /dev/null
@@ -1,60 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`components/SettingItemMin should match snapshot 1`] = `
-
-
-
- title
-
-
-
-
-
-
- describe
-
-
-`;
-
-exports[`components/SettingItemMin should match snapshot, on disableOpen to true 1`] = `
-
-`;
diff --git a/webapp/channels/src/components/setting_item_min/index.ts b/webapp/channels/src/components/setting_item_min/index.ts
deleted file mode 100644
index a0a9b0a247..0000000000
--- a/webapp/channels/src/components/setting_item_min/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-import {connect} from 'react-redux';
-
-import {getIsMobileView} from 'selectors/views/browser';
-
-import type {GlobalState} from 'types/store';
-
-import SettingItemMin from './setting_item_min';
-
-function mapStateToProps(state: GlobalState) {
- return {
- isMobileView: getIsMobileView(state),
- };
-}
-
-export default connect(mapStateToProps, null, null, {forwardRef: true})(SettingItemMin);
diff --git a/webapp/channels/src/components/setting_item_min/setting_item_min.tsx b/webapp/channels/src/components/setting_item_min/setting_item_min.tsx
deleted file mode 100644
index 201b264754..0000000000
--- a/webapp/channels/src/components/setting_item_min/setting_item_min.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-import React from 'react';
-import type {ReactNode} from 'react';
-import {FormattedMessage} from 'react-intl';
-
-import EditIcon from 'components/widgets/icons/fa_edit_icon';
-
-import {a11yFocus} from 'utils/utils';
-
-interface Props {
-
- /**
- * Settings title
- */
- title: ReactNode | string;
-
- /**
- * Option to disable opening the setting
- */
- disableOpen?: boolean;
-
- /**
- * Settings or tab section
- */
- section: string;
-
- /**
- * Function to update section
- */
- updateSection: (section: string) => void;
-
- /**
- * Settings description
- */
- describe?: ReactNode;
-
- isMobileView: boolean;
-}
-
-export default class SettingItemMin extends React.PureComponent {
- private edit: HTMLButtonElement | null = null;
-
- focus(): void {
- a11yFocus(this.edit);
- }
-
- private getEdit = (node: HTMLButtonElement) => {
- this.edit = node;
- };
-
- handleUpdateSection = (e: React.MouseEvent) => {
- e.preventDefault();
- this.props.updateSection(this.props.section);
- };
-
- render(): JSX.Element {
- let editButton = null;
- let describeSection = null;
-
- if (!this.props.disableOpen && this.props.isMobileView) {
- editButton = (
-
-
-
- );
- } else if (!this.props.disableOpen) {
- editButton = (
-
-
-
- );
-
- describeSection = (
-
- {this.props.describe}
-
- );
- }
-
- return (
-
-
-
- {this.props.title}
-
- {editButton}
-
- {describeSection}
-
- );
- }
-}
diff --git a/webapp/channels/src/components/sidebar/__snapshots__/invite_members_button.test.tsx.snap b/webapp/channels/src/components/sidebar/__snapshots__/invite_members_button.test.tsx.snap
index f62174b987..acdc673be5 100644
--- a/webapp/channels/src/components/sidebar/__snapshots__/invite_members_button.test.tsx.snap
+++ b/webapp/channels/src/components/sidebar/__snapshots__/invite_members_button.test.tsx.snap
@@ -37,7 +37,7 @@ exports[`components/sidebar/invite_members_button should match snapshot 1`] = `
}
teamId="team_id2sss"
>
-
-
-
-
-
+
+ Invite Members
+
+
+
+
+
diff --git a/webapp/channels/src/components/team_general_tab/__snapshots__/open_invite.test.tsx.snap b/webapp/channels/src/components/team_general_tab/__snapshots__/open_invite.test.tsx.snap
index e2c2093bf7..de9e758967 100644
--- a/webapp/channels/src/components/team_general_tab/__snapshots__/open_invite.test.tsx.snap
+++ b/webapp/channels/src/components/team_general_tab/__snapshots__/open_invite.test.tsx.snap
@@ -102,7 +102,7 @@ exports[`components/TeamSettings/OpenInvite should match snapshot on active with
`;
exports[`components/TeamSettings/OpenInvite should match snapshot on non active allowing open invite 1`] = `
-
-
-
-
-
-
-
-
-
- {
>
diff --git a/webapp/channels/src/components/toggle_modal_button/toggle_modal_button.test.tsx b/webapp/channels/src/components/toggle_modal_button.test.tsx
similarity index 90%
rename from webapp/channels/src/components/toggle_modal_button/toggle_modal_button.test.tsx
rename to webapp/channels/src/components/toggle_modal_button.test.tsx
index 2946f82307..cf487f6367 100644
--- a/webapp/channels/src/components/toggle_modal_button/toggle_modal_button.test.tsx
+++ b/webapp/channels/src/components/toggle_modal_button.test.tsx
@@ -10,6 +10,11 @@ import {ModalIdentifiers} from 'utils/constants';
import ToggleModalButton from './toggle_modal_button';
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux') as typeof import('react-redux'),
+ useDispatch: () => jest.fn(),
+}));
+
class TestModal extends React.PureComponent {
render() {
return (
@@ -33,7 +38,6 @@ describe('components/ToggleModalButton', () => {
role='menuitem'
modalId={ModalIdentifiers.DELETE_CHANNEL}
dialogType={TestModal}
- actions={{openModal: () => true}}
>
+
+
+
+
+
+
+
+
+
+ For all activity, without sound
+
+
+
+
+
+
+ Email notifications are not enabled
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ For all activity, without sound
+
+
+
+
+
+
+ Email notifications are not enabled
+
+
+
+
+
+
+
Desktop Notifications
-
-
-
+
+ Edit
+
Email Notifications
-
-
-
+
+ Edit
+
Mobile Push Notifications
-
-
-
+
+ Edit
+
- Keywords that trigger Notifications
+ Keywords That Trigger Notifications
-
-
-
+
+ Edit
+
+ }
updateSection={[Function]}
/>
`;
@@ -247,13 +252,18 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
section=""
serverError=""
submit={[MockFunction]}
- title="Desktop Notifications"
+ title={
+
+ }
updateSection={[Function]}
/>
`;
exports[`components/user_settings/notifications/DesktopNotificationSettings should match snapshot, on buildMinimizedSetting 1`] = `
-
}
section="desktop"
- title="Desktop Notifications"
+ title={
+
+ }
updateSection={[Function]}
/>
`;
exports[`components/user_settings/notifications/DesktopNotificationSettings should match snapshot, on buildMinimizedSetting 2`] = `
-
}
section="desktop"
- title="Desktop Notifications"
+ title={
+
+ }
updateSection={[Function]}
/>
`;
@@ -430,7 +450,12 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
section=""
serverError=""
submit={[MockFunction]}
- title="Desktop Notifications"
+ title={
+
+ }
updateSection={[Function]}
/>
`;
@@ -638,7 +663,12 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
section=""
serverError=""
submit={[MockFunction]}
- title="Desktop Notifications"
+ title={
+
+ }
updateSection={[Function]}
/>
`;
@@ -892,7 +922,12 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
section=""
serverError=""
submit={[MockFunction]}
- title="Desktop Notifications"
+ title={
+
+ }
updateSection={[Function]}
/>
`;
@@ -1101,13 +1136,18 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
section=""
serverError=""
submit={[MockFunction]}
- title="Desktop Notifications"
+ title={
+
+ }
updateSection={[Function]}
/>
`;
exports[`components/user_settings/notifications/DesktopNotificationSettings should match snapshot, on min setting 1`] = `
-
}
section="desktop"
- title="Desktop Notifications"
+ title={
+
+ }
updateSection={[Function]}
/>
`;
diff --git a/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/desktop_notification_settings.test.tsx b/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/desktop_notification_settings.test.tsx
index a9af7fc62c..ed6fae5791 100644
--- a/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/desktop_notification_settings.test.tsx
+++ b/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/desktop_notification_settings.test.tsx
@@ -19,21 +19,21 @@ jest.mock('utils/notification_sounds', () => {
describe('components/user_settings/notifications/DesktopNotificationSettings', () => {
const baseProps: ComponentProps
= {
- activity: NotificationLevels.MENTION,
- sound: 'false',
- updateSection: jest.fn(),
- setParentState: jest.fn(),
- submit: jest.fn(),
- cancel: jest.fn(),
- error: '',
active: true,
- areAllSectionsInactive: false,
+ updateSection: jest.fn(),
+ onSubmit: jest.fn(),
+ onCancel: jest.fn(),
saving: false,
- selectedSound: 'Bing',
+ error: '',
+ setParentState: jest.fn(),
+ areAllSectionsInactive: false,
isCollapsedThreadsEnabled: false,
+ activity: NotificationLevels.MENTION,
threads: NotificationLevels.ALL,
- callsSelectedSound: 'Dynamic',
+ sound: 'false',
callsSound: 'false',
+ selectedSound: 'Bing',
+ callsSelectedSound: 'Dynamic',
isCallsRingingEnabled: false,
};
@@ -81,8 +81,8 @@ describe('components/user_settings/notifications/DesktopNotificationSettings', (
expect(wrapper).toMatchSnapshot();
});
- test('should call props.updateSection and props.cancel on handleMinUpdateSection', () => {
- const props = {...baseProps, updateSection: jest.fn(), cancel: jest.fn()};
+ test('should call props.updateSection and props.onCancel on handleMinUpdateSection', () => {
+ const props = {...baseProps, updateSection: jest.fn(), onCancel: jest.fn()};
const wrapper = shallow(
,
);
@@ -90,14 +90,14 @@ describe('components/user_settings/notifications/DesktopNotificationSettings', (
wrapper.instance().handleMinUpdateSection('');
expect(props.updateSection).toHaveBeenCalledTimes(1);
expect(props.updateSection).toHaveBeenCalledWith('');
- expect(props.cancel).toHaveBeenCalledTimes(1);
- expect(props.cancel).toHaveBeenCalledWith();
+ expect(props.onCancel).toHaveBeenCalledTimes(1);
+ expect(props.onCancel).toHaveBeenCalledWith();
wrapper.instance().handleMinUpdateSection('desktop');
expect(props.updateSection).toHaveBeenCalledTimes(2);
expect(props.updateSection).toHaveBeenCalledWith('desktop');
- expect(props.cancel).toHaveBeenCalledTimes(2);
- expect(props.cancel).toHaveBeenCalledWith();
+ expect(props.onCancel).toHaveBeenCalledTimes(2);
+ expect(props.onCancel).toHaveBeenCalledWith();
});
test('should call props.updateSection on handleMaxUpdateSection', () => {
diff --git a/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/desktop_notification_settings.tsx b/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/desktop_notification_settings.tsx
index 280318ccd4..423e42f7ae 100644
--- a/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/desktop_notification_settings.tsx
+++ b/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/desktop_notification_settings.tsx
@@ -1,20 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React from 'react';
-import type {ChangeEvent, RefObject} from 'react';
+import React, {type ChangeEvent, type RefObject, type ReactNode} from 'react';
import {FormattedMessage} from 'react-intl';
-import ReactSelect from 'react-select';
-import type {ValueType} from 'react-select';
+import ReactSelect, {type ValueType} from 'react-select';
import SettingItemMax from 'components/setting_item_max';
import SettingItemMin from 'components/setting_item_min';
-import type SettingItemMinComponent from 'components/setting_item_min/setting_item_min';
+import type SettingItemMinComponent from 'components/setting_item_min';
import {NotificationLevels} from 'utils/constants';
-import {t} from 'utils/i18n';
import * as NotificationSounds from 'utils/notification_sounds';
-import * as Utils from 'utils/utils';
+import {a11yFocus} from 'utils/utils';
type SelectedOption = {
label: string;
@@ -22,21 +19,21 @@ type SelectedOption = {
};
type Props = {
+ active: boolean;
+ updateSection: (section: string) => void;
+ onSubmit: () => void;
+ onCancel: () => void;
+ saving: boolean;
+ error: string;
+ setParentState: (key: string, value: string | boolean) => void;
+ areAllSectionsInactive: boolean;
+ isCollapsedThreadsEnabled: boolean;
activity: string;
threads?: string;
sound: string;
callsSound: string;
- updateSection: (section: string) => void;
- setParentState: (key: string, value: string | boolean) => void;
- submit: () => void;
- cancel: () => void;
- error: string;
- active: boolean;
- areAllSectionsInactive: boolean;
- saving: boolean;
selectedSound: string;
callsSelectedSound: string;
- isCollapsedThreadsEnabled: boolean;
isCallsRingingEnabled: boolean;
};
@@ -49,27 +46,29 @@ type State = {
export default class DesktopNotificationSettings extends React.PureComponent {
dropdownSoundRef: RefObject;
callsDropdownRef: RefObject;
- minRef: RefObject;
+ editButtonRef: RefObject;
constructor(props: Props) {
super(props);
+
this.state = {
selectedOption: {value: props.selectedSound, label: props.selectedSound},
callsSelectedOption: {value: props.callsSelectedSound, label: props.callsSelectedSound},
blurDropdown: false,
};
+
this.dropdownSoundRef = React.createRef();
this.callsDropdownRef = React.createRef();
- this.minRef = React.createRef();
+ this.editButtonRef = React.createRef();
}
focusEditButton(): void {
- this.minRef.current?.focus();
+ this.editButtonRef.current?.focus();
}
handleMinUpdateSection = (section: string): void => {
this.props.updateSection(section);
- this.props.cancel();
+ this.props.onCancel();
};
handleMaxUpdateSection = (section: string): void => this.props.updateSection(section);
@@ -79,7 +78,7 @@ export default class DesktopNotificationSettings extends React.PureComponent
+ }
inputs={inputs}
- submit={this.props.submit}
+ submit={this.props.onSubmit}
saving={this.props.saving}
serverError={this.props.error}
updateSection={this.handleMaxUpdateSection}
@@ -446,56 +450,76 @@ export default class DesktopNotificationSettings extends React.PureComponent {
- let formattedMessageProps;
const hasSoundOption = NotificationSounds.hasSoundOptions();
+ let collapsedDescription: ReactNode = null;
+
if (this.props.activity === NotificationLevels.MENTION) {
if (hasSoundOption && this.props.sound !== 'false') {
- formattedMessageProps = {
- id: t('user.settings.notifications.desktop.mentionsSound'),
- defaultMessage: 'For mentions and direct messages, with sound',
- };
+ collapsedDescription = (
+
+ );
} else if (hasSoundOption && this.props.sound === 'false') {
- formattedMessageProps = {
- id: t('user.settings.notifications.desktop.mentionsNoSound'),
- defaultMessage: 'For mentions and direct messages, without sound',
- };
+ collapsedDescription = (
+
+ );
} else {
- formattedMessageProps = {
- id: t('user.settings.notifications.desktop.mentionsSoundHidden'),
- defaultMessage: 'For mentions and direct messages',
- };
+ collapsedDescription = (
+
+ );
}
} else if (this.props.activity === NotificationLevels.NONE) {
- formattedMessageProps = {
- id: t('user.settings.notifications.off'),
- defaultMessage: 'Off',
- };
+ collapsedDescription = (
+
+ );
} else {
if (hasSoundOption && this.props.sound !== 'false') { //eslint-disable-line no-lonely-if
- formattedMessageProps = {
- id: t('user.settings.notifications.desktop.allSound'),
- defaultMessage: 'For all activity, with sound',
- };
+ collapsedDescription = (
+
+ );
} else if (hasSoundOption && this.props.sound === 'false') {
- formattedMessageProps = {
- id: t('user.settings.notifications.desktop.allNoSound'),
- defaultMessage: 'For all activity, without sound',
- };
+ collapsedDescription = (
+
+ );
} else {
- formattedMessageProps = {
- id: t('user.settings.notifications.desktop.allSoundHidden'),
- defaultMessage: 'For all activity',
- };
+ collapsedDescription = (
+
+ );
}
}
return (
}
+ ref={this.editButtonRef}
+ title={
+
+ }
+ describe={collapsedDescription}
section={'desktop'}
updateSection={this.handleMinUpdateSection}
- ref={this.minRef}
/>
);
};
diff --git a/webapp/channels/src/components/user_settings/notifications/email_notification_setting/__snapshots__/email_notification_setting.test.tsx.snap b/webapp/channels/src/components/user_settings/notifications/email_notification_setting/__snapshots__/email_notification_setting.test.tsx.snap
index bac7f86fb9..938f39813f 100644
--- a/webapp/channels/src/components/user_settings/notifications/email_notification_setting/__snapshots__/email_notification_setting.test.tsx.snap
+++ b/webapp/channels/src/components/user_settings/notifications/email_notification_setting/__snapshots__/email_notification_setting.test.tsx.snap
@@ -7,18 +7,19 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
"savePreferences": [MockFunction],
}
}
- activeSection="email"
+ active={true}
+ areAllSectionsInactive={false}
currentUserId="current_user_id"
emailInterval={0}
enableEmail={false}
enableEmailBatching={false}
+ error=""
isCollapsedThreadsEnabled={false}
onCancel={[MockFunction]}
onChange={[MockFunction]}
onSubmit={[MockFunction]}
saving={false}
sendEmailNotifications={true}
- serverError=""
setParentState={[MockFunction]}
threads="all"
updateSection={[MockFunction]}
@@ -92,7 +93,12 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
section=""
serverError=""
submit={[Function]}
- title="Email Notifications"
+ title={
+
+ }
updateSection={[Function]}
>
- Email Notifications
+
+
+ Email Notifications
+
+
}
section="email"
- title="Email Notifications"
+ title={
+
+ }
updateSection={[Function]}
/>
`;
exports[`components/user_settings/notifications/EmailNotificationSetting should match snapshot, active section != email and SendEmailNotifications = true 1`] = `
-
}
section="email"
- title="Email Notifications"
+ title={
+
+ }
updateSection={[Function]}
/>
`;
exports[`components/user_settings/notifications/EmailNotificationSetting should match snapshot, active section != email, SendEmailNotifications = true and enableEmail = true 1`] = `
-
}
section="email"
- title="Email Notifications"
+ title={
+
+ }
updateSection={[Function]}
/>
`;
@@ -314,18 +342,19 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
"savePreferences": [MockFunction],
}
}
- activeSection="email"
+ active={true}
+ areAllSectionsInactive={false}
currentUserId="current_user_id"
emailInterval={0}
enableEmail={false}
enableEmailBatching={true}
+ error=""
isCollapsedThreadsEnabled={false}
onCancel={[MockFunction]}
onChange={[MockFunction]}
onSubmit={[MockFunction]}
saving={false}
sendEmailNotifications={true}
- serverError=""
setParentState={[MockFunction]}
threads="all"
updateSection={[MockFunction]}
@@ -448,7 +477,12 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
section=""
serverError=""
submit={[Function]}
- title="Email Notifications"
+ title={
+
+ }
updateSection={[Function]}
>
- Email Notifications
+
+
+ Email Notifications
+
+
+ }
updateSection={[Function]}
/>
`;
@@ -776,7 +822,12 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
section=""
serverError="serverError"
submit={[Function]}
- title="Email Notifications"
+ title={
+
+ }
updateSection={[Function]}
/>
`;
@@ -889,7 +940,12 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
section=""
serverError=""
submit={[Function]}
- title="Email Notifications"
+ title={
+
+ }
updateSection={[Function]}
/>
`;
@@ -964,7 +1020,12 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
section=""
serverError=""
submit={[Function]}
- title="Email Notifications"
+ title={
+
+ }
updateSection={[Function]}
/>
`;
diff --git a/webapp/channels/src/components/user_settings/notifications/email_notification_setting/email_notification_setting.test.tsx b/webapp/channels/src/components/user_settings/notifications/email_notification_setting/email_notification_setting.test.tsx
index c6cc15533e..1c4f55a40c 100644
--- a/webapp/channels/src/components/user_settings/notifications/email_notification_setting/email_notification_setting.test.tsx
+++ b/webapp/channels/src/components/user_settings/notifications/email_notification_setting/email_notification_setting.test.tsx
@@ -12,24 +12,25 @@ import {Preferences, NotificationLevels} from 'utils/constants';
describe('components/user_settings/notifications/EmailNotificationSetting', () => {
const requiredProps: ComponentProps = {
- currentUserId: 'current_user_id',
- activeSection: 'email',
+ active: true,
updateSection: jest.fn(),
- enableEmail: false,
- emailInterval: Preferences.INTERVAL_NEVER,
onSubmit: jest.fn(),
onCancel: jest.fn(),
- onChange: jest.fn(),
- serverError: '',
saving: false,
+ error: '',
+ setParentState: jest.fn(),
+ areAllSectionsInactive: false,
+ isCollapsedThreadsEnabled: false,
+ enableEmail: false,
+ onChange: jest.fn(),
+ threads: NotificationLevels.ALL,
+ currentUserId: 'current_user_id',
+ emailInterval: Preferences.INTERVAL_NEVER,
sendEmailNotifications: true,
enableEmailBatching: false,
actions: {
savePreferences: jest.fn(),
},
- isCollapsedThreadsEnabled: false,
- threads: NotificationLevels.ALL,
- setParentState: jest.fn(),
};
test('should match snapshot', () => {
@@ -68,7 +69,7 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
const props = {
...requiredProps,
sendEmailNotifications: false,
- activeSection: '',
+ active: false,
};
const wrapper = shallow();
@@ -79,7 +80,7 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
const props = {
...requiredProps,
sendEmailNotifications: true,
- activeSection: '',
+ active: false,
};
const wrapper = shallow();
@@ -90,7 +91,7 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
const props = {
...requiredProps,
sendEmailNotifications: true,
- activeSection: '',
+ active: false,
enableEmail: true,
};
const wrapper = shallow();
@@ -100,7 +101,7 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
test('should match snapshot, on serverError', () => {
const newServerError = 'serverError';
- const props = {...requiredProps, serverError: newServerError};
+ const props = {...requiredProps, error: newServerError};
const wrapper = shallow();
expect(wrapper).toMatchSnapshot();
});
diff --git a/webapp/channels/src/components/user_settings/notifications/email_notification_setting/email_notification_setting.tsx b/webapp/channels/src/components/user_settings/notifications/email_notification_setting/email_notification_setting.tsx
index 083f075c1b..977acd5601 100644
--- a/webapp/channels/src/components/user_settings/notifications/email_notification_setting/email_notification_setting.tsx
+++ b/webapp/channels/src/components/user_settings/notifications/email_notification_setting/email_notification_setting.tsx
@@ -1,8 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React from 'react';
-import type {RefObject} from 'react';
+import React, {type RefObject} from 'react';
import {FormattedMessage} from 'react-intl';
import type {PreferenceType} from '@mattermost/types/preferences';
@@ -12,37 +11,38 @@ import {getEmailInterval} from 'mattermost-redux/utils/notify_props';
import SettingItemMax from 'components/setting_item_max';
import SettingItemMin from 'components/setting_item_min';
-import type SettingItemMinComponent from 'components/setting_item_min/setting_item_min';
+import type SettingItemMinComponent from 'components/setting_item_min';
import {Preferences, NotificationLevels} from 'utils/constants';
-import {a11yFocus, localizeMessage} from 'utils/utils';
+import {a11yFocus} from 'utils/utils';
const SECONDS_PER_MINUTE = 60;
type Props = {
- currentUserId: string;
- activeSection: string;
+ active: boolean;
updateSection: (section: string) => void;
- enableEmail: boolean;
- emailInterval: number;
onSubmit: () => void;
onCancel: () => void;
- onChange: (enableEmail: UserNotifyProps['email']) => void;
- serverError?: string;
saving?: boolean;
+ error?: string;
+ setParentState: (key: string, value: any) => void;
+ areAllSectionsInactive: boolean;
+ isCollapsedThreadsEnabled: boolean;
+ enableEmail: boolean;
+ onChange: (enableEmail: UserNotifyProps['email']) => void;
+ threads: string;
+ currentUserId: string;
+ emailInterval: number;
sendEmailNotifications: boolean;
enableEmailBatching: boolean;
actions: {
savePreferences: (currentUserId: string, emailIntervalPreference: PreferenceType[]) =>
Promise<{data: boolean}>;
};
- isCollapsedThreadsEnabled: boolean;
- threads: string;
- setParentState: (key: string, value: any) => void;
};
type State = {
- activeSection: string;
+ active: boolean;
emailInterval: number;
enableEmail: boolean;
enableEmailBatching: boolean;
@@ -51,7 +51,7 @@ type State = {
};
export default class EmailNotificationSetting extends React.PureComponent {
- minRef: RefObject;
+ editButtonRef: RefObject;
constructor(props: Props) {
super(props);
@@ -61,11 +61,11 @@ export default class EmailNotificationSetting extends React.PureComponent) => {
@@ -234,11 +234,16 @@ export default class EmailNotificationSetting extends React.PureComponent
+ }
describe={description}
section={'email'}
updateSection={this.handleUpdateSection}
- ref={this.minRef}
/>
);
};
@@ -247,7 +252,12 @@ export default class EmailNotificationSetting extends React.PureComponent
+ }
inputs={[
,
]}
- serverError={this.props.serverError}
+ serverError={this.props.error}
section={'email'}
updateSection={this.handleUpdateSection}
/>
@@ -359,7 +369,12 @@ export default class EmailNotificationSetting extends React.PureComponent
+ }
inputs={[
diff --git a/webapp/channels/src/components/user_settings/security/__snapshots__/user_settings_security.test.tsx.snap b/webapp/channels/src/components/user_settings/security/__snapshots__/user_settings_security.test.tsx.snap
index fb9724c662..af594d5a23 100644
--- a/webapp/channels/src/components/user_settings/security/__snapshots__/user_settings_security.test.tsx.snap
+++ b/webapp/channels/src/components/user_settings/security/__snapshots__/user_settings_security.test.tsx.snap
@@ -53,10 +53,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
}
- infoPosition="bottom"
max={null}
- saving={false}
section="signin"
title="Sign-in Method"
updateSection={[Function]}
@@ -115,7 +109,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
-
-
-
+
-
+
`;
@@ -220,10 +214,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
}
- infoPosition="bottom"
max={null}
- saving={false}
section="signin"
title="Sign-in Method"
updateSection={[Function]}
@@ -282,7 +270,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
-
-
-
+
-
+
`;
@@ -387,10 +375,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
}
- infoPosition="bottom"
max={null}
- saving={false}
section="signin"
title="Sign-in Method"
updateSection={[Function]}
@@ -449,7 +431,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
-
-
-
+
-
+
`;
@@ -554,10 +536,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
}
- infoPosition="bottom"
max={null}
- saving={false}
section="signin"
title="Sign-in Method"
updateSection={[Function]}
@@ -616,7 +592,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
-
-
-
+
-
+
`;
diff --git a/webapp/channels/src/components/user_settings/security/mfa_section/__snapshots__/mfa_section.test.tsx.snap b/webapp/channels/src/components/user_settings/security/mfa_section/__snapshots__/mfa_section.test.tsx.snap
index ba4de48c61..59aae139f5 100644
--- a/webapp/channels/src/components/user_settings/security/mfa_section/__snapshots__/mfa_section.test.tsx.snap
+++ b/webapp/channels/src/components/user_settings/security/mfa_section/__snapshots__/mfa_section.test.tsx.snap
@@ -3,7 +3,7 @@
exports[`MfaSection rendering should render nothing when MFA is not available 1`] = `""`;
exports[`MfaSection rendering when section is collapsed and MFA is active 1`] = `
-
-
Extra text
-
+
`;
diff --git a/webapp/channels/src/components/widgets/menu/menu_items/menu_item_toggle_modal_redux.test.tsx b/webapp/channels/src/components/widgets/menu/menu_items/menu_item_toggle_modal_redux.test.tsx
index 5eee282cfa..3803314d46 100644
--- a/webapp/channels/src/components/widgets/menu/menu_items/menu_item_toggle_modal_redux.test.tsx
+++ b/webapp/channels/src/components/widgets/menu/menu_items/menu_item_toggle_modal_redux.test.tsx
@@ -19,7 +19,7 @@ describe('components/MenuItemToggleModalRedux', () => {
expect(wrapper).toMatchInlineSnapshot(`
- {
>
Whatever
-
+
`);
});
diff --git a/webapp/channels/src/components/widgets/menu/menu_items/restricted_indicator.scss b/webapp/channels/src/components/widgets/menu/menu_items/restricted_indicator.scss
index e6de459866..c4d81ba33d 100644
--- a/webapp/channels/src/components/widgets/menu/menu_items/restricted_indicator.scss
+++ b/webapp/channels/src/components/widgets/menu/menu_items/restricted_indicator.scss
@@ -9,7 +9,7 @@
padding: 0 10px;
.RestrictedIndicator__button {
- padding: 0 !important;
+ padding: 0;
}
.RestrictedIndicator__icon-tooltip {
diff --git a/webapp/channels/src/components/widgets/menu/menu_items/restricted_indicator.tsx b/webapp/channels/src/components/widgets/menu/menu_items/restricted_indicator.tsx
index 44b432e08f..79fc7a3f98 100644
--- a/webapp/channels/src/components/widgets/menu/menu_items/restricted_indicator.tsx
+++ b/webapp/channels/src/components/widgets/menu/menu_items/restricted_indicator.tsx
@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import classNames from 'classnames';
-import React, {useCallback} from 'react';
+import React, {useCallback, type ReactNode} from 'react';
import {useIntl} from 'react-intl';
import type {MessageDescriptor} from 'react-intl';
@@ -16,27 +16,27 @@ import {Constants, LicenseSkus, ModalIdentifiers} from 'utils/constants';
import './restricted_indicator.scss';
-type RestrictedIndicatorProps = {
+type Props = {
useModal?: boolean;
- titleAdminPreTrial?: string;
- messageAdminPreTrial?: string | React.ReactNode;
- titleAdminPostTrial?: string;
- messageAdminPostTrial?: string | React.ReactNode;
- titleEndUser?: string;
- messageEndUser?: string | React.ReactNode;
blocked?: boolean;
- tooltipTitle?: string;
- tooltipMessage?: string;
- tooltipMessageBlocked?: string | MessageDescriptor;
- ctaExtraContent?: React.ReactNode;
- clickCallback?: () => void;
- customSecondaryButtonInModal?: {msg: string; action: () => void};
feature?: string;
minimumPlanRequiredForFeature?: string;
+ tooltipTitle?: ReactNode;
+ tooltipMessage?: ReactNode;
+ tooltipMessageBlocked?: string | MessageDescriptor;
+ titleAdminPreTrial?: ReactNode;
+ messageAdminPreTrial?: ReactNode;
+ titleAdminPostTrial?: ReactNode;
+ messageAdminPostTrial?: ReactNode;
+ titleEndUser?: ReactNode;
+ messageEndUser?: ReactNode;
+ ctaExtraContent?: ReactNode;
+ clickCallback?: () => void;
+ customSecondaryButtonInModal?: {msg: string; action: () => void};
}
function capitalizeFirstLetter(s: string) {
- return s.charAt(0).toUpperCase() + s.slice(1);
+ return s?.charAt(0)?.toUpperCase() + s?.slice(1);
}
const RestrictedIndicator = ({
@@ -56,7 +56,7 @@ const RestrictedIndicator = ({
customSecondaryButtonInModal,
feature,
minimumPlanRequiredForFeature,
-}: RestrictedIndicatorProps) => {
+}: Props) => {
const {formatMessage} = useIntl();
const getTooltipMessageBlocked = useCallback(() => {
@@ -92,7 +92,10 @@ const RestrictedIndicator = ({
delayShow={Constants.OVERLAY_TIME_DELAY}
placement='right'
overlay={(
-
+
{tooltipTitle || formatMessage({id: 'restricted_indicator.tooltip.title', defaultMessage: '{minimumPlanRequiredForFeature} feature'}, {minimumPlanRequiredForFeature: capitalizeFirstLetter(minimumPlanRequiredForFeature!)})}
@@ -107,27 +110,29 @@ const RestrictedIndicator = ({
)}
>
{useModal && blocked ? (
-
- {icon}
- {ctaExtraContent}
-
+
+
+ {icon}
+ {ctaExtraContent}
+
+
) : (
{icon}
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index cc9920a467..d352dff961 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -5428,7 +5428,7 @@
"user.settings.notifications.autoResponderHint": "Set a custom message that will be automatically sent in response to Direct Messages. Mentions in Public and Private Channels will not trigger the automated reply. Enabling Automatic Replies sets your status to Out of Office and disables email and push notifications.",
"user.settings.notifications.autoResponderPlaceholder": "Message",
"user.settings.notifications.channelWide": "Channel-wide mentions \"@channel\", \"@all\", \"@here\"",
- "user.settings.notifications.comments": "Reply notifications",
+ "user.settings.notifications.comments": "Reply Notifications",
"user.settings.notifications.commentsAny": "Trigger notifications on messages in reply threads that I start or participate in",
"user.settings.notifications.commentsInfo": "In addition to notifications for when you're mentioned, select if you would like to receive notifications on reply threads.",
"user.settings.notifications.commentsNever": "Do not trigger notifications on messages in reply threads unless I'm mentioned",
@@ -5457,8 +5457,21 @@
"user.settings.notifications.header": "Notifications",
"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.keywordsWithHighlight.disabledTooltipMessage": "This feature is available on the Professional plan",
+ "user.settings.notifications.keywordsWithHighlight.disabledTooltipTitle": "Professional feature",
+ "user.settings.notifications.keywordsWithHighlight.extraInfo": "These keywords will be shown to you with a highlight when anyone sends a message that includes them.",
+ "user.settings.notifications.keywordsWithHighlight.inputTitle": "Enter non case-sensitive keywords, press Tab or use commas to separate them:",
+ "user.settings.notifications.keywordsWithHighlight.none": "None",
+ "user.settings.notifications.keywordsWithHighlight.professional": "Professional",
+ "user.settings.notifications.keywordsWithHighlight.title": "Keywords That Get Highlighted (Without Notifications)",
+ "user.settings.notifications.keywordsWithHighlight.userModal.messageAdminPostTrial": "Get the ability to passively highlight keywords that you care about. Upgrade to Professional plan to unlock this feature.",
+ "user.settings.notifications.keywordsWithHighlight.userModal.messageAdminPreTrial": "Get the ability to passively highlight keywords that you care about. Upgrade to Professional plan to unlock this feature.",
+ "user.settings.notifications.keywordsWithHighlight.userModal.messageEndUser": "Get the ability to passively highlight keywords that you care about.{br}{br}Request your admin to upgrade to Mattermost Professional to access this feature.",
+ "user.settings.notifications.keywordsWithHighlight.userModal.titleAdminPostTrial": "Highlight keywords without notifications with Mattermost Professional",
+ "user.settings.notifications.keywordsWithHighlight.userModal.titleAdminPreTrial": "Highlight keywords without notifications with Mattermost Professional",
+ "user.settings.notifications.keywordsWithHighlight.userModal.titleEndUser": "Highlight keywords without notifications with Mattermost Professional",
"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.keywordsWithNotification.title": "Keywords That Trigger Notifications",
"user.settings.notifications.learnMore": "
Learn more about notifications",
"user.settings.notifications.never": "Never",
"user.settings.notifications.off": "Off",
@@ -5637,6 +5650,7 @@
"webapp.mattermost.feature.create_multiple_teams": "Create Multiple Teams",
"webapp.mattermost.feature.custom_user_groups": "Custom User groups",
"webapp.mattermost.feature.guest_accounts": "Guest Accounts",
+ "webapp.mattermost.feature.highlight_without_notification": "Keywords Highlight Without Notification",
"webapp.mattermost.feature.playbooks_retro": "Playbooks Retrospective",
"webapp.mattermost.feature.start_call": "Start call",
"webapp.mattermost.feature.unlimited_file_storage": "Unlimited File Storage",
diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/users.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/users.ts
index d94d592c74..ea360c73e2 100644
--- a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/users.ts
+++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/users.ts
@@ -224,6 +224,26 @@ export const getCurrentUserMentionKeys: (state: GlobalState) => UserMentionKey[]
},
);
+export type HighlightWithoutNotificationKey = {
+ key: string;
+}
+
+export const getHighlightWithoutNotificationKeys: (state: GlobalState) => HighlightWithoutNotificationKey[] = createSelector(
+ 'getHighlightWithoutNotificationKeys',
+ getCurrentUser,
+ (user: UserProfile) => {
+ const highlightKeys: HighlightWithoutNotificationKey[] = [];
+
+ if (user?.notify_props?.highlight_keys?.length > 0) {
+ user.notify_props.highlight_keys.split(',').forEach((key) => {
+ highlightKeys.push({key});
+ });
+ }
+
+ return highlightKeys;
+ },
+);
+
export const getProfileSetInCurrentChannel: (state: GlobalState) => Set
= createSelector(
'getProfileSetInCurrentChannel',
getCurrentChannelId,
diff --git a/webapp/channels/src/packages/mattermost-redux/test/test_helper.ts b/webapp/channels/src/packages/mattermost-redux/test/test_helper.ts
index 349a867845..52da8a7050 100644
--- a/webapp/channels/src/packages/mattermost-redux/test/test_helper.ts
+++ b/webapp/channels/src/packages/mattermost-redux/test/test_helper.ts
@@ -102,6 +102,7 @@ class TestHelper {
email: 'false',
first_name: 'false',
mark_unread: 'mention',
+ highlight_keys: '',
mention_keys: '',
push: 'none',
push_status: 'offline',
@@ -144,6 +145,7 @@ class TestHelper {
first_name: 'false',
mark_unread: 'mention',
mention_keys: '',
+ highlight_keys: '',
push: 'none',
push_status: 'offline',
},
@@ -472,6 +474,7 @@ class TestHelper {
first_name: 'true',
channel: 'true',
mention_keys: '',
+ highlight_keys: '',
...override,
};
};
diff --git a/webapp/channels/src/plugins/test/__snapshots__/post_type.test.tsx.snap b/webapp/channels/src/plugins/test/__snapshots__/post_type.test.tsx.snap
index 350caf7da0..28d80a9e52 100644
--- a/webapp/channels/src/plugins/test/__snapshots__/post_type.test.tsx.snap
+++ b/webapp/channels/src/plugins/test/__snapshots__/post_type.test.tsx.snap
@@ -134,7 +134,6 @@ exports[`plugins/PostMessageView should match snapshot with no extended post typ
"onImageLoaded": [Function],
}
}
- mentionKeys={Array []}
message="this is some text"
options={Object {}}
post={
diff --git a/webapp/channels/src/sass/components/_mentions.scss b/webapp/channels/src/sass/components/_mentions.scss
index 38ef16ff12..0961efeeb2 100644
--- a/webapp/channels/src/sass/components/_mentions.scss
+++ b/webapp/channels/src/sass/components/_mentions.scss
@@ -54,6 +54,13 @@ del .group-mention-link:focus {
}
}
+.non-notification-highlight {
+ padding: 0 1px;
+ background-color: var(--mention-highlight-bg);
+ border-radius: 4px;
+ color: var(--mention-highlight-link);
+}
+
.group-mention-link {
color: var(--link-color);
}
diff --git a/webapp/channels/src/sass/responsive/_mobile.scss b/webapp/channels/src/sass/responsive/_mobile.scss
index 67a03837c9..6d8852e504 100644
--- a/webapp/channels/src/sass/responsive/_mobile.scss
+++ b/webapp/channels/src/sass/responsive/_mobile.scss
@@ -194,13 +194,8 @@
}
.section-min__edit {
- position: relative;
- top: 0;
- right: 0;
- text-align: left;
-
- button {
- opacity: 1;
+ span {
+ display: none;
}
}
@@ -299,23 +294,6 @@
}
.settings-content {
- .section-min__edit {
- text-align: left;
- text-decoration: none;
-
- button {
- display: flex;
- width: 100%;
- align-items: center;
- color: rgba(var(--center-channel-color-rgb), 0.75);
- text-align: left;
- }
-
- .fa {
- display: inline-block;
- }
- }
-
&.minimize-settings {
display: none;
padding: 0;
diff --git a/webapp/channels/src/sass/routes/_settings.scss b/webapp/channels/src/sass/routes/_settings.scss
index 3a5248181b..08dab46778 100644
--- a/webapp/channels/src/sass/routes/_settings.scss
+++ b/webapp/channels/src/sass/routes/_settings.scss
@@ -658,9 +658,42 @@
text-decoration: underline;
}
- .d-flex {
+ > .secion-min__header {
+ display: flex;
+ flex-direction: row;
justify-content: space-between;
}
+
+ &.isDisabled {
+ cursor: default;
+
+ &:hover {
+ background: inherit;
+ }
+
+ .RestrictedIndicator__icon-tooltip-container {
+ flex: unset;
+ align-self: flex-start;
+ padding: 0;
+
+ button {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ padding: 0 6px;
+ background-color: rgba(var(--button-bg-rgb), 0.08);
+ border-radius: 12px;
+ color: var(--button-bg);
+ font-size: 12px;
+ font-weight: 600;
+
+ i {
+ font-size: 12px;
+ }
+ }
+ }
+ }
}
.section-min__title {
@@ -669,18 +702,15 @@
font-size: 14px;
font-weight: 600;
line-height: 20px;
+
+ &.isDisabled {
+ color: rgba(var(--center-channel-color-rgb), 0.64);
+ }
}
.section-min__edit {
margin-bottom: 5px;
text-align: right;
-
- .fa {
- display: none;
- margin-right: 5px;
- font-size: 12px;
- opacity: 0.5;
- }
}
.section-min__describe {
@@ -689,4 +719,8 @@
opacity: 0.75;
text-overflow: ellipsis;
white-space: pre;
+
+ &.isDisabled {
+ color: rgba(var(--center-channel-color-rgb), 0.4);
+ }
}
diff --git a/webapp/channels/src/types/store/plugins.ts b/webapp/channels/src/types/store/plugins.ts
index c9d61611e2..a84d5b49ca 100644
--- a/webapp/channels/src/types/store/plugins.ts
+++ b/webapp/channels/src/types/store/plugins.ts
@@ -98,6 +98,7 @@ export type PluginComponent = {
filter?: (id: string) => boolean;
action?: (...args: any) => void; // TODO Add more concrete types?
shouldRender?: (state: GlobalState) => boolean;
+ hook?: (post: Post, message?: string) => string;
};
export type AppBarComponent = PluginComponent & {
diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx
index 3c2c4f8557..3586fabbcc 100644
--- a/webapp/channels/src/utils/constants.tsx
+++ b/webapp/channels/src/utils/constants.tsx
@@ -507,6 +507,7 @@ export const MattermostFeatures = {
ALL_ENTERPRISE_FEATURES: 'mattermost.feature.all_enterprise',
UPGRADE_DOWNGRADED_WORKSPACE: 'mattermost.feature.upgrade_downgraded_workspace',
PLUGIN_FEATURE: 'mattermost.feature.plugin',
+ HIGHLIGHT_WITHOUT_NOTIFICATION: 'mattermost.feature.highlight_without_notification',
};
export enum LicenseSkus {
diff --git a/webapp/channels/src/utils/license_utils.ts b/webapp/channels/src/utils/license_utils.ts
index 191133153d..7d89e29bcc 100644
--- a/webapp/channels/src/utils/license_utils.ts
+++ b/webapp/channels/src/utils/license_utils.ts
@@ -3,9 +3,10 @@
import moment from 'moment';
+import type {Product} from '@mattermost/types/cloud';
import type {ClientLicense} from '@mattermost/types/config';
-import {LicenseSkus} from 'utils/constants';
+import {CloudProducts, LicenseSkus, SelfHostedProducts} from 'utils/constants';
const LICENSE_EXPIRY_NOTIFICATION = 1000 * 60 * 60 * 24 * 60; // 60 days
const LICENSE_GRACE_PERIOD = 1000 * 60 * 60 * 24 * 10; // 10 days
@@ -103,3 +104,14 @@ export const licenseSKUWithFirstLetterCapitalized = (license: ClientLicense) =>
const sku = license.SkuShortName;
return sku.charAt(0).toUpperCase() + sku.slice(1);
};
+
+export function isEnterpriseOrCloudOrSKUStarterFree(license: ClientLicense, subscriptionProduct: Product | undefined, isEnterpriseReady: boolean) {
+ const isCloud = license?.Cloud === 'true';
+ const isCloudStarterFree = isCloud && subscriptionProduct?.sku === CloudProducts.STARTER;
+
+ const isSelfHostedStarter = isEnterpriseReady && (license.IsLicensed === 'false');
+
+ const isStarterSKULicense = license.IsLicensed === 'true' && license.SelfHostedProducts === SelfHostedProducts.STARTER;
+
+ return isCloudStarterFree || isSelfHostedStarter || isStarterSKULicense;
+}
diff --git a/webapp/channels/src/utils/notify_admin_utils.ts b/webapp/channels/src/utils/notify_admin_utils.ts
index 06f79f34be..c17fd4533e 100644
--- a/webapp/channels/src/utils/notify_admin_utils.ts
+++ b/webapp/channels/src/utils/notify_admin_utils.ts
@@ -26,6 +26,8 @@ export function mapFeatureIdToTranslation(id: string, formatMessage: Function):
return formatMessage({id: 'webapp.mattermost.feature.all_enterprise', defaultMessage: 'All Enterprise features'});
case MattermostFeatures.UPGRADE_DOWNGRADED_WORKSPACE:
return formatMessage({id: 'webapp.mattermost.feature.upgrade_downgraded_workspace', defaultMessage: 'Revert the workspace to a paid plan'});
+ case MattermostFeatures.HIGHLIGHT_WITHOUT_NOTIFICATION:
+ return formatMessage({id: 'webapp.mattermost.feature.highlight_without_notification', defaultMessage: 'Keywords Highlight Without Notification'});
default:
return '';
}
diff --git a/webapp/channels/src/utils/route.test.ts b/webapp/channels/src/utils/route.test.ts
index fc2371bfa7..0761e9e213 100644
--- a/webapp/channels/src/utils/route.test.ts
+++ b/webapp/channels/src/utils/route.test.ts
@@ -35,7 +35,9 @@ describe('Utils.Route', () => {
comments: 'never',
first_name: 'true',
channel: 'true',
- mention_keys: ''},
+ mention_keys: '',
+ highlight_keys: '',
+ },
last_password_update: 0,
last_picture_update: 0,
locale: '',
@@ -83,7 +85,8 @@ describe('Utils.Route', () => {
position: '',
roles: '',
props: {userid: '121'},
- notify_props: {desktop: 'default',
+ notify_props: {
+ desktop: 'default',
desktop_sound: 'false',
calls_desktop_sound: 'true',
email: 'true',
@@ -93,7 +96,9 @@ describe('Utils.Route', () => {
comments: 'never',
first_name: 'true',
channel: 'true',
- mention_keys: ''},
+ mention_keys: '',
+ highlight_keys: '',
+ },
last_password_update: 0,
last_picture_update: 0,
locale: '',
diff --git a/webapp/channels/src/utils/test_helper.ts b/webapp/channels/src/utils/test_helper.ts
index 6c86016c9a..b00820530f 100644
--- a/webapp/channels/src/utils/test_helper.ts
+++ b/webapp/channels/src/utils/test_helper.ts
@@ -68,6 +68,7 @@ export class TestHelper {
first_name: 'false',
mark_unread: 'mention',
mention_keys: '',
+ highlight_keys: '',
push: 'none',
push_status: 'offline',
},
diff --git a/webapp/channels/src/utils/text_formatting.test.ts b/webapp/channels/src/utils/text_formatting.test.ts
index aa57dbef9d..fc7e18a18d 100644
--- a/webapp/channels/src/utils/text_formatting.test.ts
+++ b/webapp/channels/src/utils/text_formatting.test.ts
@@ -14,7 +14,9 @@ import {
highlightSearchTerms,
handleUnicodeEmoji,
highlightCurrentMentions,
- parseSearchTerms, autolinkChannelMentions,
+ highlightWithoutNotificationKeywords,
+ parseSearchTerms,
+ autolinkChannelMentions,
} from 'utils/text_formatting';
import type {ChannelNamesMap} from 'utils/text_formatting';
@@ -304,6 +306,101 @@ describe('highlightCurrentMentions', () => {
});
});
+describe('highlightWithoutNotificationKeywords', () => {
+ test('should replace highlight keywords with tokens', () => {
+ const text = 'This is a test message with some keywords';
+ const tokens = new Map();
+ const highlightKeys = [
+ {key: 'test message'},
+ {key: 'keywords'},
+ ];
+
+ const expectedOutput = 'This is a $MM_HIGHLIGHTKEYWORD0$ with some $MM_HIGHLIGHTKEYWORD1$';
+ const expectedTokens = new Map([
+ ['$MM_HIGHLIGHTKEYWORD0$', {
+ value: 'test message',
+ originalText: 'test message',
+ }],
+ ['$MM_HIGHLIGHTKEYWORD1$', {
+ value: 'keywords',
+ originalText: 'keywords',
+ }],
+ ]);
+
+ const output = highlightWithoutNotificationKeywords(text, tokens, highlightKeys);
+
+ expect(output).toBe(expectedOutput);
+ expect(tokens).toEqual(expectedTokens);
+ });
+
+ test('should handle empty highlightKeys array', () => {
+ const text = 'This is a test message';
+ const tokens = new Map();
+ const highlightKeys = [] as Array<{key: string}>;
+
+ const expectedOutput = 'This is a test message';
+ const expectedTokens = new Map();
+
+ const output = highlightWithoutNotificationKeywords(text, tokens, highlightKeys);
+
+ expect(output).toBe(expectedOutput);
+ expect(tokens).toEqual(expectedTokens);
+ });
+
+ test('should handle empty text', () => {
+ const text = '';
+ const tokens = new Map();
+ const highlightKeys = [
+ {key: 'test'},
+ {key: 'keywords'},
+ ];
+
+ const expectedOutput = '';
+ const expectedTokens = new Map();
+
+ const output = highlightWithoutNotificationKeywords(text, tokens, highlightKeys);
+
+ expect(output).toBe(expectedOutput);
+ expect(tokens).toEqual(expectedTokens);
+ });
+
+ test('should handle Chinese, Korean, Russian, and Japanese words', () => {
+ const text = 'This is a test message with some keywords: привет, こんにちは, 안녕하세요, 你好';
+ const tokens = new Map();
+ const highlightKeys = [
+ {key: 'こんにちは'}, // Japanese hello
+ {key: '안녕하세요'}, // Korean hello
+ {key: 'привет'}, // Russian hello
+ {key: '你好'}, // Chinese hello
+ ];
+
+ const expectedOutput = 'This is a test message with some keywords: $MM_HIGHLIGHTKEYWORD0$, $MM_HIGHLIGHTKEYWORD1$, $MM_HIGHLIGHTKEYWORD2$, $MM_HIGHLIGHTKEYWORD3$';
+ const expectedTokens = new Map([
+ ['$MM_HIGHLIGHTKEYWORD0$', {
+ value: 'привет',
+ originalText: 'привет',
+ }],
+ ['$MM_HIGHLIGHTKEYWORD1$', {
+ value: 'こんにちは',
+ originalText: 'こんにちは',
+ }],
+ ['$MM_HIGHLIGHTKEYWORD2$', {
+ value: '안녕하세요',
+ originalText: '안녕하세요',
+ }],
+ ['$MM_HIGHLIGHTKEYWORD3$', {
+ value: '你好',
+ originalText: '你好',
+ }],
+ ]);
+
+ const output = highlightWithoutNotificationKeywords(text, tokens, highlightKeys);
+
+ expect(output).toBe(expectedOutput);
+ expect(tokens).toEqual(expectedTokens);
+ });
+});
+
describe('parseSearchTerms', () => {
const tests = [
{
diff --git a/webapp/channels/src/utils/text_formatting.tsx b/webapp/channels/src/utils/text_formatting.tsx
index b7ab1ac772..2d0f97dd5a 100644
--- a/webapp/channels/src/utils/text_formatting.tsx
+++ b/webapp/channels/src/utils/text_formatting.tsx
@@ -6,6 +6,8 @@ import type {Renderer} from 'marked';
import type {SystemEmoji} from '@mattermost/types/emojis';
+import type {HighlightWithoutNotificationKey} from 'mattermost-redux/selectors/entities/users';
+
import {formatWithRenderer} from 'utils/markdown';
import Constants from './constants';
@@ -88,6 +90,11 @@ interface TextFormattingOptionsBase {
*/
mentionKeys: MentionKey[];
+ /**
+ * A list of highlight keys for the current user to highlight without notification.
+ */
+ highlightKeys: HighlightWithoutNotificationKey[];
+
/**
* Specifies whether or not to remove newlines.
*
@@ -360,6 +367,10 @@ export function doFormatText(text: string, options: TextFormattingOptions, emoji
output = highlightCurrentMentions(output, tokens, options.mentionKeys);
}
+ if (options.highlightKeys && options.highlightKeys.length > 0) {
+ output = highlightWithoutNotificationKeywords(output, tokens, options.highlightKeys);
+ }
+
if (!('emoticons' in options) || options.emoticons) {
output = handleUnicodeEmoji(output, emojiMap, UNICODE_EMOJI_REGEX);
}
@@ -704,6 +715,79 @@ export function highlightCurrentMentions(
return output;
}
+export function highlightWithoutNotificationKeywords(
+ text: string,
+ tokens: Tokens,
+ highlightKeys: HighlightWithoutNotificationKey[] = [],
+) {
+ let output = text;
+
+ // Store the new tokens in a separate map since we can't add objects to a map during iteration
+ const newTokens = new Map();
+
+ // Look for highlighting keywords in the tokens
+ tokens.forEach((token, alias) => {
+ const tokenOriginalText = token.originalText.toLowerCase();
+
+ if (highlightKeys.findIndex((highlightKey) => highlightKey.key.toLowerCase() === tokenOriginalText) !== -1) {
+ const newIndex = tokens.size + newTokens.size;
+ const newAlias = `$MM_HIGHLIGHTKEYWORD${newIndex}$`;
+
+ newTokens.set(newAlias, {
+ value: `${alias}`,
+ originalText: token.originalText,
+ });
+ output = output.replace(alias, newAlias);
+ }
+ });
+
+ // Copy the new tokens to the tokens map
+ newTokens.forEach((newToken, newAlias) => {
+ tokens.set(newAlias, newToken);
+ });
+
+ // Look for highlighting keywords in the text
+ function replaceHighlightKeywordsWithToken(
+ _: string,
+ prefix: string,
+ highlightKey: string,
+ suffix = '',
+ ) {
+ const index = tokens.size;
+ const alias = `$MM_HIGHLIGHTKEYWORD${index}$`;
+
+ // Set the token map with the replacement value so that it can be replaced back later
+ tokens.set(alias, {
+ value: `${highlightKey}`,
+ originalText: highlightKey,
+ });
+
+ return prefix + alias + suffix;
+ }
+
+ highlightKeys.
+ sort((a, b) => b.key.length - a.key.length).
+ forEach(({key}) => {
+ if (!key) {
+ return;
+ }
+
+ let pattern;
+ if (cjkrPattern.test(key)) {
+ // If the key contains Chinese, Japanese, Korean or Russian characters, don't mark word boundaries
+ pattern = new RegExp(`()(${escapeRegex(key)})()`, 'gi');
+ } else {
+ // If the key contains only English characters, mark word boundaries
+ pattern = new RegExp(`(^|\\W)(${escapeRegex(key)})(\\b|_+\\b)`, 'gi');
+ }
+
+ // Replace the key with the token for each occurrence of the key
+ output = output.replace(pattern, replaceHighlightKeywordsWithToken);
+ });
+
+ return output;
+}
+
const hashtagRegex = /(^|\W)(#\p{L}[\p{L}\d\-_.]*[\p{L}\d])/gu;
function autolinkHashtags(
diff --git a/webapp/platform/types/src/users.ts b/webapp/platform/types/src/users.ts
index af1b5d45ee..9c291130da 100644
--- a/webapp/platform/types/src/users.ts
+++ b/webapp/platform/types/src/users.ts
@@ -20,6 +20,7 @@ export type UserNotifyProps = {
first_name: 'true' | 'false';
channel: 'true' | 'false';
mention_keys: string;
+ highlight_keys: string;
desktop_notification_sound?: 'Bing' | 'Crackle' | 'Down' | 'Hello' | 'Ripple' | 'Upstairs';
calls_notification_sound?: 'Dynamic' | 'Calm' | 'Urgent' | 'Cheerful';
desktop_threads?: 'default' | 'all' | 'mention' | 'none';