From d860548a7651152409055401871f759fe4964898 Mon Sep 17 00:00:00 2001 From: Vishal Date: Fri, 5 May 2023 12:09:00 +0530 Subject: [PATCH] [MM-37933] Channel preference to auto-follow all threads in the channel (#21430) * Add auto-follow feature --------- Co-authored-by: Mattermod Co-authored-by: Mattermost Build --- server/channels/app/channel.go | 4 ++ server/channels/app/notification.go | 22 +++++-- server/channels/app/notification_test.go | 64 +++++++++++++++++++ server/i18n/en.json | 4 ++ server/model/channel_member.go | 14 ++++ server/model/utils.go | 18 ++++++ .../channel_members.test.tsx.snap | 3 + .../users_to_remove_role.test.tsx.snap | 3 + .../channel_header.test.tsx.snap | 16 +++++ .../channel_notifications_modal.test.tsx.snap | 14 ++++ .../channel_notifications_modal.test.tsx | 7 +- .../channel_notifications_modal.tsx | 34 +++++++++- .../components/collapse_view.tsx | 4 +- .../components/describe.tsx | 25 +++++++- .../components/expand_view.tsx | 44 ++++++++++++- .../components/extra_info.tsx | 9 +++ .../components/notification_section.jsx | 8 +++ .../components/section_title.tsx | 7 ++ webapp/channels/src/i18n/en.json | 4 ++ .../mattermost-redux/test/test_helper.ts | 1 + webapp/channels/src/utils/constants.tsx | 6 ++ webapp/channels/src/utils/test_helper.ts | 1 + webapp/platform/types/src/channels.ts | 1 + 23 files changed, 299 insertions(+), 14 deletions(-) diff --git a/server/channels/app/channel.go b/server/channels/app/channel.go index 833d005ef5..3738132cc4 100644 --- a/server/channels/app/channel.go +++ b/server/channels/app/channel.go @@ -1354,6 +1354,10 @@ func (a *App) UpdateChannelMemberNotifyProps(c request.CTX, data map[string]stri filteredProps[model.IgnoreChannelMentionsNotifyProp] = ignoreChannelMentions } + if channelAutoFollowThreads, exists := data[model.ChannelAutoFollowThreads]; exists { + filteredProps[model.ChannelAutoFollowThreads] = channelAutoFollowThreads + } + member, err := a.Srv().Store().Channel().UpdateMemberNotifyProps(channelID, userID, filteredProps) if err != nil { var appErr *model.AppError diff --git a/server/channels/app/notification.go b/server/channels/app/notification.go index d18aeb5b0f..68ae82bff1 100644 --- a/server/channels/app/notification.go +++ b/server/channels/app/notification.go @@ -101,13 +101,15 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea } channelMemberNotifyPropsMap := result.Data.(map[string]model.StringMap) - followers := make(model.StringArray, 0) + followers := make(model.StringSet, 0) if tchan != nil { result = <-tchan if result.NErr != nil { return nil, result.NErr } - followers = result.Data.([]string) + for _, v := range result.Data.([]string) { + followers.Add(v) + } } groups := make(map[string]*model.Group) @@ -235,6 +237,14 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea threadParticipants[id] = true } + if channel.Type != model.ChannelTypeDirect { + for id, propsMap := range channelMemberNotifyPropsMap { + if ok := followers.Has(id); !ok && propsMap[model.ChannelAutoFollowThreads] == model.ChannelAutoFollowThreadsOn { + threadParticipants[id] = true + } + } + } + // sema is a counting semaphore to throttle the number of concurrent DB requests. // A concurrency of 8 should be sufficient. // We don't want to set a higher limit which can bring down the DB. @@ -286,8 +296,8 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea followersMutex.Lock() // add new followers to existing followers - if threadMembership.Following && !followers.Contains(userID) { - followers = append(followers, userID) + if ok := followers.Has(userID); !ok && threadMembership.Following { + followers.Add(userID) newParticipants[userID] = true } followersMutex.Unlock() @@ -330,7 +340,7 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea notificationsForCRT := &CRTNotifiers{} if isCRTAllowed && post.RootId != "" { - for _, uid := range followers { + for uid := range followers { profile := profileMap[uid] if profile == nil || !a.IsCRTEnabledForUser(c, uid) { continue @@ -578,7 +588,7 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea // If this is a reply in a thread, notify participants if isCRTAllowed && post.RootId != "" { - for _, uid := range followers { + for uid := range followers { // A user following a thread but had left the channel won't get a notification // https://mattermost.atlassian.net/browse/MM-36769 if profileMap[uid] == nil { diff --git a/server/channels/app/notification_test.go b/server/channels/app/notification_test.go index 0fa390dc8d..4dfcd3f74d 100644 --- a/server/channels/app/notification_test.go +++ b/server/channels/app/notification_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/mattermost/mattermost-server/server/v8/channels/store" "github.com/mattermost/mattermost-server/server/v8/channels/utils" "github.com/mattermost/mattermost-server/server/v8/model" "github.com/mattermost/mattermost-server/server/v8/platform/shared/i18n" @@ -2819,3 +2820,66 @@ func TestReplyPostNotificationsWithCRT(t *testing.T) { assert.Nil(t, membership) }) } + +func TestChannelAutoFollowThreads(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + u1 := th.BasicUser + u2 := th.BasicUser2 + u3 := th.CreateUser() + th.LinkUserToTeam(u3, th.BasicTeam) + c1 := th.BasicChannel + th.AddUserToChannel(u2, c1) + th.AddUserToChannel(u3, c1) + + // Set auto-follow for user 2 + member, appErr := th.App.UpdateChannelMemberNotifyProps(th.Context, map[string]string{model.ChannelAutoFollowThreads: model.ChannelAutoFollowThreadsOn}, c1.Id, u2.Id) + require.Nil(t, appErr) + require.Equal(t, model.ChannelAutoFollowThreadsOn, member.NotifyProps[model.ChannelAutoFollowThreads]) + + rootPost := &model.Post{ + ChannelId: c1.Id, + Message: "root post by user3", + UserId: u3.Id, + } + rpost, appErr := th.App.CreatePost(th.Context, rootPost, c1, false, true) + require.Nil(t, appErr) + + replyPost1 := &model.Post{ + ChannelId: c1.Id, + Message: "reply post by user1", + UserId: u1.Id, + RootId: rpost.Id, + } + _, appErr = th.App.CreatePost(th.Context, replyPost1, c1, false, true) + require.Nil(t, appErr) + + // user-2 starts auto-following thread + threadMembership, appErr := th.App.GetThreadMembershipForUser(u2.Id, rpost.Id) + require.Nil(t, appErr) + require.NotNil(t, threadMembership) + assert.True(t, threadMembership.Following) + + // Set "following" to false + _, err := th.App.Srv().Store().Thread().MaintainMembership(u2.Id, rpost.Id, store.ThreadMembershipOpts{ + Following: false, + UpdateFollowing: true, + }) + require.NoError(t, err) + + replyPost2 := &model.Post{ + ChannelId: c1.Id, + Message: "reply post 2 by user1", + UserId: u1.Id, + RootId: rpost.Id, + } + _, appErr = th.App.CreatePost(th.Context, replyPost2, c1, false, true) + require.Nil(t, appErr) + + // Do NOT start auto-following thread, once "un-followed" + threadMembership, appErr = th.App.GetThreadMembershipForUser(u2.Id, rpost.Id) + require.Nil(t, appErr) + require.NotNil(t, threadMembership) + assert.False(t, threadMembership.Following) +} diff --git a/server/i18n/en.json b/server/i18n/en.json index 490d218c87..c359acc24f 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -8459,6 +8459,10 @@ "id": "model.channel.is_valid.update_at.app_error", "translation": "Update at must be a valid time." }, + { + "id": "model.channel_member.is_valid.channel_auto_follow_threads_value.app_error", + "translation": "Invalid channel-auto-follow-threads value." + }, { "id": "model.channel_member.is_valid.channel_id.app_error", "translation": "Invalid channel id." diff --git a/server/model/channel_member.go b/server/model/channel_member.go index dbe0014eac..23768bb6c1 100644 --- a/server/model/channel_member.go +++ b/server/model/channel_member.go @@ -19,6 +19,9 @@ const ( IgnoreChannelMentionsOff = "off" IgnoreChannelMentionsOn = "on" IgnoreChannelMentionsNotifyProp = "ignore_channel_mentions" + ChannelAutoFollowThreadsOff = "off" + ChannelAutoFollowThreadsOn = "on" + ChannelAutoFollowThreads = "channel_auto_follow_threads" ) type ChannelUnread struct { @@ -172,6 +175,12 @@ func (o *ChannelMember) IsValid() *AppError { } } + if channelAutoFollowThreads, ok := o.NotifyProps[ChannelAutoFollowThreads]; ok { + if len(channelAutoFollowThreads) > 3 || !IsChannelAutoFollowThreadsValid(channelAutoFollowThreads) { + return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.channel_auto_follow_threads_value.app_error", nil, "channel_auto_follow_threads="+channelAutoFollowThreads, http.StatusBadRequest) + } + } + if len(o.Roles) > UserRolesMaxLength { return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.roles_limit.app_error", map[string]any{"Limit": UserRolesMaxLength}, "", http.StatusBadRequest) @@ -223,6 +232,10 @@ func IsIgnoreChannelMentionsValid(ignoreChannelMentions string) bool { return ignoreChannelMentions == IgnoreChannelMentionsOn || ignoreChannelMentions == IgnoreChannelMentionsOff || ignoreChannelMentions == IgnoreChannelMentionsDefault } +func IsChannelAutoFollowThreadsValid(channelAutoFollowThreads string) bool { + return channelAutoFollowThreads == ChannelAutoFollowThreadsOn || channelAutoFollowThreads == ChannelAutoFollowThreadsOff +} + func GetDefaultChannelNotifyProps() StringMap { return StringMap{ DesktopNotifyProp: ChannelNotifyDefault, @@ -230,5 +243,6 @@ func GetDefaultChannelNotifyProps() StringMap { PushNotifyProp: ChannelNotifyDefault, EmailNotifyProp: ChannelNotifyDefault, IgnoreChannelMentionsNotifyProp: IgnoreChannelMentionsDefault, + ChannelAutoFollowThreads: ChannelAutoFollowThreadsOff, } } diff --git a/server/model/utils.go b/server/model/utils.go index 956aa8caf3..fb1068e053 100644 --- a/server/model/utils.go +++ b/server/model/utils.go @@ -41,8 +41,26 @@ const ( var ErrMaxPropSizeExceeded = fmt.Errorf("max prop size of %d exceeded", maxPropSizeBytes) type StringInterface map[string]any +type StringSet map[string]struct{} type StringArray []string +func (ss StringSet) Has(val string) bool { + _, ok := ss[val] + return ok +} + +func (ss StringSet) Add(val string) { + ss[val] = struct{}{} +} + +func (ss StringSet) Val() []string { + keys := make([]string, 0, len(ss)) + for k := range ss { + keys = append(keys, k) + } + return keys +} + func (sa StringArray) Remove(input string) StringArray { for index := range sa { if sa[index] == input { 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 de12f30686..337301c12b 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 @@ -124,6 +124,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -145,6 +146,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -166,6 +168,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", diff --git a/webapp/channels/src/components/admin_console/team_channel_settings/group/group_users/__snapshots__/users_to_remove_role.test.tsx.snap b/webapp/channels/src/components/admin_console/team_channel_settings/group/group_users/__snapshots__/users_to_remove_role.test.tsx.snap index 68a500efec..18a77631df 100644 --- a/webapp/channels/src/components/admin_console/team_channel_settings/group/group_users/__snapshots__/users_to_remove_role.test.tsx.snap +++ b/webapp/channels/src/components/admin_console/team_channel_settings/group/group_users/__snapshots__/users_to_remove_role.test.tsx.snap @@ -95,6 +95,7 @@ exports[`components/admin_console/team_channel_settings/group/UsersToRemoveRole "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -186,6 +187,7 @@ exports[`components/admin_console/team_channel_settings/group/UsersToRemoveRole "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -277,6 +279,7 @@ exports[`components/admin_console/team_channel_settings/group/UsersToRemoveRole "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", diff --git a/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap b/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap index 6af48c2eb8..876556517d 100644 --- a/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap +++ b/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap @@ -284,6 +284,7 @@ exports[`components/ChannelHeader should match snapshot with last active display "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -589,6 +590,7 @@ exports[`components/ChannelHeader should match snapshot with no last active disp "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -893,6 +895,7 @@ exports[`components/ChannelHeader should render active channel files 1`] = ` "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -1196,6 +1199,7 @@ exports[`components/ChannelHeader should render active flagged posts 1`] = ` "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -1499,6 +1503,7 @@ exports[`components/ChannelHeader should render active mentions posts 1`] = ` "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -1802,6 +1807,7 @@ exports[`components/ChannelHeader should render active pinned posts 1`] = ` "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -2074,6 +2080,7 @@ exports[`components/ChannelHeader should render archived view 1`] = ` "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -2409,6 +2416,7 @@ exports[`components/ChannelHeader should render correct menu when muted 1`] = ` "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -2712,6 +2720,7 @@ exports[`components/ChannelHeader should render not active channel files 1`] = ` "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -3036,6 +3045,7 @@ exports[`components/ChannelHeader should render properly when custom status is e "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -3381,6 +3391,7 @@ exports[`components/ChannelHeader should render properly when custom status is s "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -3685,6 +3696,7 @@ exports[`components/ChannelHeader should render properly when empty 1`] = ` "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -3988,6 +4000,7 @@ exports[`components/ChannelHeader should render properly when populated 1`] = ` "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -4310,6 +4323,7 @@ exports[`components/ChannelHeader should render properly when populated with cha "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -4626,6 +4640,7 @@ exports[`components/ChannelHeader should render shared view 1`] = ` "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", @@ -4938,6 +4953,7 @@ exports[`components/ChannelHeader should render the pinned icon with the pinned "msg_count": 0, "msg_count_root": 0, "notify_props": Object { + "channel_auto_follow_threads": "off", "desktop": "default", "email": "default", "ignore_channel_mentions": "default", diff --git a/webapp/channels/src/components/channel_notifications_modal/__snapshots__/channel_notifications_modal.test.tsx.snap b/webapp/channels/src/components/channel_notifications_modal/__snapshots__/channel_notifications_modal.test.tsx.snap index 875eeb1d1f..54c6f5fb8e 100644 --- a/webapp/channels/src/components/channel_notifications_modal/__snapshots__/channel_notifications_modal.test.tsx.snap +++ b/webapp/channels/src/components/channel_notifications_modal/__snapshots__/channel_notifications_modal.test.tsx.snap @@ -121,6 +121,20 @@ exports[`components/channel_notifications_modal/ChannelNotificationsModal should serverError={null} /> +
+
diff --git a/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.test.tsx b/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.test.tsx index b4da9f73a2..12ac3c3b1a 100644 --- a/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.test.tsx +++ b/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.test.tsx @@ -4,7 +4,7 @@ import React, {ComponentProps} from 'react'; import {shallow} from 'enzyme'; -import {IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants'; +import {ChannelAutoFollowThreads, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants'; import {TestHelper} from 'utils/test_helper'; import ChannelNotificationsModal from 'components/channel_notifications_modal/channel_notifications_modal'; @@ -25,6 +25,7 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', () mark_unread: NotificationLevels.ALL, push: NotificationLevels.DEFAULT, ignore_channel_mentions: IgnoreChannelMentions.DEFAULT, + channel_auto_follow_threads: ChannelAutoFollowThreads.OFF, desktop_threads: NotificationLevels.ALL, push_threads: NotificationLevels.DEFAULT, }, @@ -62,6 +63,7 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', () expect(wrapper.state('markUnreadNotifyLevel')).toEqual(NotificationLevels.ALL); expect(wrapper.state('pushNotifyLevel')).toEqual(NotificationLevels.DEFAULT); expect(wrapper.state('ignoreChannelMentions')).toEqual(IgnoreChannelMentions.OFF); + expect(wrapper.state('channelAutoFollowThreads')).toEqual(ChannelAutoFollowThreads.OFF); }); test('should provide correct default when currentUser channel notify props is true', () => { @@ -207,7 +209,7 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', () expect(wrapper.state('desktopNotifyLevel')).toEqual(NotificationLevels.NONE); - wrapper.instance().updateSection(''); + wrapper.instance().updateSection(NotificationSections.NONE); expect(wrapper.state('desktopNotifyLevel')).toEqual(baseProps.channelMember?.notify_props.desktop); }); @@ -348,6 +350,7 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', () expect(wrapper.state('markUnreadNotifyLevel')).toEqual(NotificationLevels.MENTION); expect(wrapper.state('pushNotifyLevel')).toEqual(NotificationLevels.ALL); expect(wrapper.state('ignoreChannelMentions')).toEqual(IgnoreChannelMentions.ON); + expect(wrapper.state('channelAutoFollowThreads')).toEqual(ChannelAutoFollowThreads.OFF); wrapper.instance().resetStateFromNotifyProps(currentUserNotifyProps, {...channelMemberNotifyProps, desktop: NotificationLevels.ALL}); expect(wrapper.state('desktopNotifyLevel')).toEqual(NotificationLevels.ALL); diff --git a/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx b/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx index 1b5ff1b8f1..20756d7619 100644 --- a/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx +++ b/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx @@ -8,7 +8,7 @@ import {FormattedMessage} from 'react-intl'; import {isChannelMuted} from 'mattermost-redux/utils/channel_utils'; -import {IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants'; +import {ChannelAutoFollowThreads, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants'; import NotificationSection from 'components/channel_notifications_modal/components/notification_section.jsx'; @@ -47,6 +47,7 @@ type State = { pushNotifyLevel: ChannelNotifyProps['push']; pushThreadsNotifyLevel: UserNotifyProps['push_threads']; ignoreChannelMentions: ChannelNotifyProps['ignore_channel_mentions']; + channelAutoFollowThreads: ChannelNotifyProps['channel_auto_follow_threads']; }; export default class ChannelNotificationsModal extends React.PureComponent { @@ -95,6 +96,7 @@ export default class ChannelNotificationsModal extends React.PureComponent this.setState({channelAutoFollowThreads}); + + handleSubmitChannelAutoFollowThreads = () => { + const channelNotifyProps = this.props.channelMember && this.props.channelMember.notify_props; + const {channelAutoFollowThreads} = this.state; + + if (channelNotifyProps?.channel_auto_follow_threads === channelAutoFollowThreads) { + this.updateSection(NotificationSections.NONE); + return; + } + + const props = {channel_auto_follow_threads: channelAutoFollowThreads}; + this.handleUpdateChannelNotifyProps(props); + }; + render() { const { activeSection, @@ -207,6 +224,7 @@ export default class ChannelNotificationsModal extends React.PureComponent } +
+
diff --git a/webapp/channels/src/components/channel_notifications_modal/components/collapse_view.tsx b/webapp/channels/src/components/channel_notifications_modal/components/collapse_view.tsx index f9306c4a90..2728234484 100644 --- a/webapp/channels/src/components/channel_notifications_modal/components/collapse_view.tsx +++ b/webapp/channels/src/components/channel_notifications_modal/components/collapse_view.tsx @@ -10,13 +10,14 @@ import SectionTitle from './section_title'; type Props = { ignoreChannelMentions?: string; + channelAutoFollowThreads?: string; onExpandSection: (section: string) => void; globalNotifyLevel?: string; memberNotifyLevel: string; section: string; } -export default function CollapseView({onExpandSection, globalNotifyLevel, memberNotifyLevel, section, ignoreChannelMentions}: Props) { +export default function CollapseView({onExpandSection, globalNotifyLevel, memberNotifyLevel, section, ignoreChannelMentions, channelAutoFollowThreads}: Props) { return ( } @@ -24,6 +25,7 @@ export default function CollapseView({onExpandSection, globalNotifyLevel, member ); + } else if ( + section === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS && + channelAutoFollowThreads === ChannelAutoFollowThreads.ON + ) { + return ( + + ); + } else if ( + section === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS && + channelAutoFollowThreads === ChannelAutoFollowThreads.OFF + ) { + return ( + + ); } else if (memberNotifyLevel === NotificationLevels.MENTION) { return ( ) => void; onChangeThreads?: (e: ChangeEvent) => void; onCollapseSection: (section: string) => void; @@ -39,6 +40,7 @@ export default function ExpandView({ serverError, onCollapseSection, ignoreChannelMentions, + channelAutoFollowThreads, }: Props) { const isCRTEnabled = useSelector(isCollapsedThreadsEnabled); @@ -153,6 +155,46 @@ export default function ExpandView({
} + {section === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS && +
+
+ +
+
+ +
+
+ } {section === NotificationSections.MARK_UNREAD &&
diff --git a/webapp/channels/src/components/channel_notifications_modal/components/extra_info.tsx b/webapp/channels/src/components/channel_notifications_modal/components/extra_info.tsx index 1bc65f6c66..7f489acec9 100644 --- a/webapp/channels/src/components/channel_notifications_modal/components/extra_info.tsx +++ b/webapp/channels/src/components/channel_notifications_modal/components/extra_info.tsx @@ -48,6 +48,15 @@ export default function ExtraInfo({section}: Props) { /> ); + case NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS: + return ( + + + + ); default: return null; } diff --git a/webapp/channels/src/components/channel_notifications_modal/components/notification_section.jsx b/webapp/channels/src/components/channel_notifications_modal/components/notification_section.jsx index 9e369dc867..9ff5fbd3a4 100644 --- a/webapp/channels/src/components/channel_notifications_modal/components/notification_section.jsx +++ b/webapp/channels/src/components/channel_notifications_modal/components/notification_section.jsx @@ -37,6 +37,11 @@ export default class NotificationSection extends React.PureComponent { */ ignoreChannelMentions: PropTypes.string, + /** + * Auto-follow all new threads in this channel + */ + channelAutoFollowThreads: PropTypes.string, + /** * User's global notification level */ @@ -93,6 +98,7 @@ export default class NotificationSection extends React.PureComponent { memberNotificationLevel, memberThreadsNotificationLevel, ignoreChannelMentions, + channelAutoFollowThreads, onSubmit, section, serverError, @@ -106,6 +112,7 @@ export default class NotificationSection extends React.PureComponent { memberThreadsNotifyLevel={memberThreadsNotificationLevel} globalNotifyLevel={globalNotificationLevel} ignoreChannelMentions={ignoreChannelMentions} + channelAutoFollowThreads={channelAutoFollowThreads} onChange={this.handleOnChange} onChangeThreads={this.handleOnChangeThreads} onSubmit={onSubmit} @@ -122,6 +129,7 @@ export default class NotificationSection extends React.PureComponent { memberNotifyLevel={memberNotificationLevel} globalNotifyLevel={globalNotificationLevel} ignoreChannelMentions={ignoreChannelMentions} + channelAutoFollowThreads={channelAutoFollowThreads} /> ); } diff --git a/webapp/channels/src/components/channel_notifications_modal/components/section_title.tsx b/webapp/channels/src/components/channel_notifications_modal/components/section_title.tsx index be295810e0..be8704e554 100644 --- a/webapp/channels/src/components/channel_notifications_modal/components/section_title.tsx +++ b/webapp/channels/src/components/channel_notifications_modal/components/section_title.tsx @@ -39,6 +39,13 @@ export default function SectionTitle({section}: Props) { defaultMessage='Ignore mentions for @channel, @here and @all' /> ); + } else if (section === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS) { + return ( + + ); } return null; diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 821883a2ce..91fbdb3a7f 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -2978,6 +2978,10 @@ "channel_modal.type.public.description": "Anyone can join", "channel_modal.type.public.title": "Public Channel", "channel_notifications.allActivity": "For all activity", + "channel_notifications.channelAutoFollowThreads": "Auto-follow all new threads in this channel", + "channel_notifications.channelAutoFollowThreads.help": "When enabled, you will auto-follow all new threads created in this channel unless you unfollow a thread explicitly.", + "channel_notifications.channelAutoFollowThreads.off.title": "Off", + "channel_notifications.channelAutoFollowThreads.on.title": "On", "channel_notifications.globalDefault": "Global default ({notifyLevel})", "channel_notifications.ignoreChannelMentions": "Ignore mentions for @channel, @here and @all", "channel_notifications.ignoreChannelMentions.help": "When enabled, @channel, @here and @all will not trigger mentions or mention notifications in this channel.", 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 4ca6c1cfc2..ba8965e5ca 100644 --- a/webapp/channels/src/packages/mattermost-redux/test/test_helper.ts +++ b/webapp/channels/src/packages/mattermost-redux/test/test_helper.ts @@ -453,6 +453,7 @@ class TestHelper { mark_unread: 'mention', push: 'default', ignore_channel_mentions: 'default', + channel_auto_follow_threads: 'off', ...override, }; }; diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index 29f0ef75f5..e42ff6ae51 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -1009,8 +1009,14 @@ export const IgnoreChannelMentions = { DEFAULT: 'default', } as const; +export const ChannelAutoFollowThreads = { + ON: 'on', + OFF: 'off', +} as const; + export const NotificationSections = { IGNORE_CHANNEL_MENTIONS: 'ignoreChannelMentions', + CHANNEL_AUTO_FOLLOW_THREADS: 'channelAutoFollowThreads', MARK_UNREAD: 'markUnread', DESKTOP: 'desktop', PUSH: 'push', diff --git a/webapp/channels/src/utils/test_helper.ts b/webapp/channels/src/utils/test_helper.ts index fec90b0ff0..f421a089c3 100644 --- a/webapp/channels/src/utils/test_helper.ts +++ b/webapp/channels/src/utils/test_helper.ts @@ -155,6 +155,7 @@ export class TestHelper { mark_unread: 'all', push: 'default', ignore_channel_mentions: 'default', + channel_auto_follow_threads: 'off', }; const notifyProps = Object.assign({}, defaultNotifyProps, overrideNotifyProps); diff --git a/webapp/platform/types/src/channels.ts b/webapp/platform/types/src/channels.ts index 93a38d5199..19b9fa58d9 100644 --- a/webapp/platform/types/src/channels.ts +++ b/webapp/platform/types/src/channels.ts @@ -25,6 +25,7 @@ export type ChannelNotifyProps = { mark_unread: 'all' | 'mention'; push: 'default' | 'all' | 'mention' | 'none'; ignore_channel_mentions: 'default' | 'off' | 'on'; + channel_auto_follow_threads: 'off' | 'on'; }; export type Channel = {