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 3d30c658009..68b81b13df1 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,6 +34,7 @@ describe('Verify Accessibility Support in different sections in Settings and Pro {key: 'desktopAndMobile', label: 'Desktop and mobile notifications', type: 'radio'}, {key: 'desktopNotificationSound', label: 'Desktop notification sounds', type: 'radio'}, {key: 'email', label: 'Email notifications', type: 'radio'}, + {key: 'channelMentionAutoFollow', label: 'Auto-follow threads on channel-wide mentions', type: 'radio'}, {key: 'keywordsAndMentions', label: 'Keywords that trigger notifications', type: 'checkbox'}, {key: 'keywordsAndHighlight', label: 'Keywords that get highlighted (without notifications)', type: 'checkbox'}, {key: 'replyNotifications', label: 'Reply notifications', type: 'radio'}, diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/user_management/user_management_admin_control_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/user_management/user_management_admin_control_spec.js index b0ed8a6d9a8..0bb6590d8c8 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/user_management/user_management_admin_control_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/user_management/user_management_admin_control_spec.js @@ -46,7 +46,7 @@ describe('User Management', () => { verifyManageUserSettingModal(testUser, true); - cy.get('#replyNotificationsTitle').should('be.visible').should('have.text', 'Reply notifications').click(); + cy.findByRole('heading', {name: 'Reply notifications'}).scrollIntoView().click(); cy.get('#notificationCommentsNever').should('be.checked'); cy.get('#notificationCommentsAny').check(); cy.get('button#saveSetting').last().scrollIntoView().click(); @@ -56,7 +56,7 @@ describe('User Management', () => { cy.visit(`/${testTeam.name}/channels/${testChannel.name}`); cy.get('[aria-label="Settings"]').click(); - cy.get('#replyNotificationsTitle').should('be.visible').should('have.text', 'Reply notifications').click(); + cy.findByRole('heading', {name: 'Reply notifications'}).scrollIntoView().click(); cy.get('#notificationCommentsAny').should('be.checked'); cy.apiLogout(); }); diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/settings/notifications_settings.ts b/e2e-tests/playwright/lib/src/ui/components/channels/settings/notifications_settings.ts index a10c45b4429..47bd7eb5798 100644 --- a/e2e-tests/playwright/lib/src/ui/components/channels/settings/notifications_settings.ts +++ b/e2e-tests/playwright/lib/src/ui/components/channels/settings/notifications_settings.ts @@ -17,6 +17,7 @@ export default class NotificationsSettings { readonly desktopAndMobileEditButton; readonly desktopNotificationSoundEditButton; readonly emailEditButton; + readonly channelMentionAutoFollowEditButton; readonly keywordsTriggerNotificationsEditButton; readonly keywordsGetHighlightedEditButton; @@ -35,6 +36,9 @@ export default class NotificationsSettings { this.desktopAndMobileEditButton = container.locator('#desktopAndMobileEdit'); this.desktopNotificationSoundEditButton = container.locator('#desktopNotificationSoundEdit'); this.emailEditButton = container.locator('#emailEdit'); + this.channelMentionAutoFollowEditButton = container.getByRole('button', { + name: 'Auto-follow threads on channel-wide mentions Edit', + }); this.keywordsTriggerNotificationsEditButton = container.locator('#keywordsAndMentionsEdit'); this.keywordsGetHighlightedEditButton = container.locator('#keywordsAndHighlightEdit'); diff --git a/e2e-tests/playwright/specs/accessibility/channels/settings_dialog/notifications.spec.ts b/e2e-tests/playwright/specs/accessibility/channels/settings_dialog/notifications.spec.ts index aa251a3cbd6..955afac8f0e 100644 --- a/e2e-tests/playwright/specs/accessibility/channels/settings_dialog/notifications.spec.ts +++ b/e2e-tests/playwright/specs/accessibility/channels/settings_dialog/notifications.spec.ts @@ -52,6 +52,10 @@ test( await page.keyboard.press('Tab'); await pw.toBeFocusedWithFocusVisible(notificationsSettings.emailEditButton); + // # Press Tab to move focus to Auto-follow threads on channel-wide mentions button + await page.keyboard.press('Tab'); + await pw.toBeFocusedWithFocusVisible(notificationsSettings.channelMentionAutoFollowEditButton); + // # Press Tab to move focus to Keywords that trigger notifications button await page.keyboard.press('Tab'); await pw.toBeFocusedWithFocusVisible(notificationsSettings.keywordsTriggerNotificationsEditButton); @@ -110,6 +114,9 @@ test( - text: "\\"Bing\\" for messages" - heading "Email notifications" [level=4] - button "Email notifications Edit" + - heading "Auto-follow threads on channel-wide mentions" [level=4] + - button "Auto-follow threads on channel-wide mentions Edit" + - text: "On" - heading "Keywords that trigger notifications" [level=4] - button "Keywords that trigger notifications Edit" - text: "\\"@${user.username}\\", \\"@channel\\", \\"@all\\", \\"@here\\"" diff --git a/server/channels/app/notification.go b/server/channels/app/notification.go index f194b20ab6e..c4792a2dabd 100644 --- a/server/channels/app/notification.go +++ b/server/channels/app/notification.go @@ -252,12 +252,22 @@ func (a *App) SendNotifications(rctx request.CTX, post *model.Post, team *model. } if channel.Type != model.ChannelTypeDirect { rootMentions = getExplicitMentions(rootPost, keywords) - for id := range rootMentions.Mentions { + for id, mentionType := range rootMentions.Mentions { + if mentionType == ChannelMention { + if profile, ok := profileMap[id]; ok && profile.NotifyProps[model.ChannelMentionAutoFollowThreadsProp] == "false" { + continue + } + } threadParticipants[id] = true } } } - for id := range mentions.Mentions { + for id, mentionType := range mentions.Mentions { + if mentionType == ChannelMention { + if profile, ok := profileMap[id]; ok && profile.NotifyProps[model.ChannelMentionAutoFollowThreadsProp] == "false" { + continue + } + } threadParticipants[id] = true } diff --git a/server/channels/app/notification_test.go b/server/channels/app/notification_test.go index 4c7d91af8a1..80e84e7a82b 100644 --- a/server/channels/app/notification_test.go +++ b/server/channels/app/notification_test.go @@ -3022,6 +3022,110 @@ func TestChannelAutoFollowThreads(t *testing.T) { assert.False(t, threadMembership.Following) } +func TestChannelMentionAutoFollowThreads(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic(t) + + u1 := th.BasicUser + u2 := th.BasicUser2 + u3 := th.CreateUser(t) + th.LinkUserToTeam(t, u3, th.BasicTeam) + c1 := th.BasicChannel + th.AddUserToChannel(t, u2, c1) + th.AddUserToChannel(t, u3, c1) + + rootPost := &model.Post{ + ChannelId: c1.Id, + Message: "root post by user3", + UserId: u3.Id, + } + rpost, _, appErr := th.App.CreatePost(th.Context, rootPost, c1, model.CreatePostFlags{SetOnline: true}) + require.Nil(t, appErr) + + t.Run("channel mention auto-follow enabled (default)", func(t *testing.T) { + // u2 has default notify props (channel_mention_auto_follow_threads = "true") + require.Equal(t, "true", u2.NotifyProps[model.ChannelMentionAutoFollowThreadsProp]) + + replyPost := &model.Post{ + ChannelId: c1.Id, + Message: "@channel reply by user1", + UserId: u1.Id, + RootId: rpost.Id, + } + _, _, appErr = th.App.CreatePost(th.Context, replyPost, c1, model.CreatePostFlags{SetOnline: true}) + require.Nil(t, appErr) + + // u2 should be auto-following because channel_mention_auto_follow_threads is enabled + threadMembership, getThreadErr := th.App.GetThreadMembershipForUser(u2.Id, rpost.Id) + require.Nil(t, getThreadErr) + require.NotNil(t, threadMembership) + assert.True(t, threadMembership.Following) + }) + + t.Run("channel mention auto-follow disabled for user", func(t *testing.T) { + // Disable auto-follow for u2 + u2.NotifyProps[model.ChannelMentionAutoFollowThreadsProp] = "false" + u2, appErr = th.App.UpdateUser(th.Context, u2, false) + require.Nil(t, appErr) + require.Equal(t, "false", u2.NotifyProps[model.ChannelMentionAutoFollowThreadsProp]) + + // Reset u2 membership so the prior sub-test doesn't interfere + _, err := th.App.Srv().Store().Thread().MaintainMembership(u2.Id, rpost.Id, store.ThreadMembershipOpts{ + Following: false, + UpdateFollowing: true, + }) + require.NoError(t, err) + + replyPost := &model.Post{ + ChannelId: c1.Id, + Message: "@channel reply by user1 again", + UserId: u1.Id, + RootId: rpost.Id, + } + _, _, appErr = th.App.CreatePost(th.Context, replyPost, c1, model.CreatePostFlags{SetOnline: true}) + require.Nil(t, appErr) + + // u2 should NOT be auto-following because they opted out + threadMembership, getThreadErr := th.App.GetThreadMembershipForUser(u2.Id, rpost.Id) + require.Nil(t, getThreadErr) + if threadMembership != nil { + assert.False(t, threadMembership.Following) + } + }) + + t.Run("channel mention auto-follow undefined (old default)", func(t *testing.T) { + // Remove the auto-follow setting for u2 to mimic a user created before this setting was added + delete(u2.NotifyProps, model.ChannelMentionAutoFollowThreadsProp) + u2, appErr = th.App.UpdateUser(th.Context, u2, false) + require.Nil(t, appErr) + + _, ok := u2.NotifyProps[model.ChannelMentionAutoFollowThreadsProp] + require.False(t, ok) + + // Reset u2 membership so the prior sub-test doesn't interfere + _, err := th.App.Srv().Store().Thread().MaintainMembership(u2.Id, rpost.Id, store.ThreadMembershipOpts{ + Following: false, + UpdateFollowing: true, + }) + require.NoError(t, err) + + replyPost := &model.Post{ + ChannelId: c1.Id, + Message: "@channel reply by user1", + UserId: u1.Id, + RootId: rpost.Id, + } + _, _, appErr = th.App.CreatePost(th.Context, replyPost, c1, model.CreatePostFlags{SetOnline: true}) + require.Nil(t, appErr) + + // u2 should be auto-following because channel_mention_auto_follow_threads isn't defined + threadMembership, getThreadErr := th.App.GetThreadMembershipForUser(u2.Id, rpost.Id) + require.Nil(t, getThreadErr) + require.NotNil(t, threadMembership) + assert.True(t, threadMembership.Following) + }) +} + func TestRemoveNotifications(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) diff --git a/server/public/model/user.go b/server/public/model/user.go index d80deea3c9b..828f5597c43 100644 --- a/server/public/model/user.go +++ b/server/public/model/user.go @@ -23,31 +23,32 @@ import ( ) const ( - Me = "me" - UserNotifyAll = "all" - UserNotifyHere = "here" - UserNotifyMention = "mention" - UserNotifyNone = "none" - DesktopNotifyProp = "desktop" - DesktopSoundNotifyProp = "desktop_sound" - MarkUnreadNotifyProp = "mark_unread" - PushNotifyProp = "push" - PushStatusNotifyProp = "push_status" - EmailNotifyProp = "email" - ChannelMentionsNotifyProp = "channel" - CommentsNotifyProp = "comments" - MentionKeysNotifyProp = "mention_keys" - HighlightsNotifyProp = "highlight_keys" - CommentsNotifyNever = "never" - CommentsNotifyRoot = "root" - CommentsNotifyAny = "any" - CommentsNotifyCRT = "crt" - FirstNameNotifyProp = "first_name" - AutoResponderActiveNotifyProp = "auto_responder_active" - AutoResponderMessageNotifyProp = "auto_responder_message" - DesktopThreadsNotifyProp = "desktop_threads" - PushThreadsNotifyProp = "push_threads" - EmailThreadsNotifyProp = "email_threads" + Me = "me" + UserNotifyAll = "all" + UserNotifyHere = "here" + UserNotifyMention = "mention" + UserNotifyNone = "none" + DesktopNotifyProp = "desktop" + DesktopSoundNotifyProp = "desktop_sound" + MarkUnreadNotifyProp = "mark_unread" + PushNotifyProp = "push" + PushStatusNotifyProp = "push_status" + EmailNotifyProp = "email" + ChannelMentionsNotifyProp = "channel" + CommentsNotifyProp = "comments" + MentionKeysNotifyProp = "mention_keys" + HighlightsNotifyProp = "highlight_keys" + CommentsNotifyNever = "never" + CommentsNotifyRoot = "root" + CommentsNotifyAny = "any" + CommentsNotifyCRT = "crt" + FirstNameNotifyProp = "first_name" + AutoResponderActiveNotifyProp = "auto_responder_active" + AutoResponderMessageNotifyProp = "auto_responder_message" + DesktopThreadsNotifyProp = "desktop_threads" + PushThreadsNotifyProp = "push_threads" + EmailThreadsNotifyProp = "email_threads" + ChannelMentionAutoFollowThreadsProp = "channel_mention_auto_follow_threads" DefaultLocale = "en" UserAuthServiceEmail = "email" @@ -607,6 +608,7 @@ func (u *User) SetDefaultNotifications() { u.NotifyProps[DesktopThreadsNotifyProp] = UserNotifyAll u.NotifyProps[EmailThreadsNotifyProp] = UserNotifyAll u.NotifyProps[PushThreadsNotifyProp] = UserNotifyAll + u.NotifyProps[ChannelMentionAutoFollowThreadsProp] = "true" } func (u *User) UpdateMentionKeysFromUsername(oldUsername string) { diff --git a/webapp/channels/src/components/user_settings/notifications/__snapshots__/user_settings_notifications.test.tsx.snap b/webapp/channels/src/components/user_settings/notifications/__snapshots__/user_settings_notifications.test.tsx.snap index 631cf002b25..17c2f4dc73b 100644 --- a/webapp/channels/src/components/user_settings/notifications/__snapshots__/user_settings_notifications.test.tsx.snap +++ b/webapp/channels/src/components/user_settings/notifications/__snapshots__/user_settings_notifications.test.tsx.snap @@ -192,6 +192,42 @@ Object {
+