[MM-37933] Channel preference to auto-follow all threads in the channel (#21430)
* Add auto-follow feature --------- Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -121,6 +121,20 @@ exports[`components/channel_notifications_modal/ChannelNotificationsModal should
|
||||
serverError={null}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="divider-light"
|
||||
/>
|
||||
<NotificationSection
|
||||
channelAutoFollowThreads="off"
|
||||
expand={false}
|
||||
ignoreChannelMentions="off"
|
||||
memberNotificationLevel="all"
|
||||
onChange={[Function]}
|
||||
onSubmit={[Function]}
|
||||
onUpdateSection={[Function]}
|
||||
section="channelAutoFollowThreads"
|
||||
serverError={null}
|
||||
/>
|
||||
<div
|
||||
className="divider-dark"
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Props, State> {
|
||||
@@ -95,6 +96,7 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
|
||||
pushNotifyLevel: channelMemberNotifyProps?.push || NotificationLevels.DEFAULT,
|
||||
pushThreadsNotifyLevel: channelMemberNotifyProps?.push_threads || NotificationLevels.ALL,
|
||||
ignoreChannelMentions,
|
||||
channelAutoFollowThreads: channelMemberNotifyProps?.channel_auto_follow_threads || ChannelAutoFollowThreads.OFF,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -190,7 +192,7 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
|
||||
const {ignoreChannelMentions} = this.state;
|
||||
|
||||
if (channelNotifyProps?.ignore_channel_mentions === ignoreChannelMentions) {
|
||||
this.updateSection('');
|
||||
this.updateSection(NotificationSections.NONE);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -198,6 +200,21 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
|
||||
this.handleUpdateChannelNotifyProps(props);
|
||||
};
|
||||
|
||||
handleUpdateChannelAutoFollowThreads = (channelAutoFollowThreads: ChannelNotifyProps['channel_auto_follow_threads']) => 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<Props
|
||||
pushNotifyLevel,
|
||||
pushThreadsNotifyLevel,
|
||||
ignoreChannelMentions,
|
||||
channelAutoFollowThreads,
|
||||
serverError,
|
||||
} = this.state;
|
||||
|
||||
@@ -301,6 +319,18 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div className='divider-light'/>
|
||||
<NotificationSection
|
||||
section={NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS}
|
||||
expand={activeSection === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS}
|
||||
memberNotificationLevel={markUnreadNotifyLevel}
|
||||
ignoreChannelMentions={ignoreChannelMentions}
|
||||
channelAutoFollowThreads={channelAutoFollowThreads}
|
||||
onChange={this.handleUpdateChannelAutoFollowThreads}
|
||||
onSubmit={this.handleSubmitChannelAutoFollowThreads}
|
||||
onUpdateSection={this.updateSection}
|
||||
serverError={serverError}
|
||||
/>
|
||||
<div className='divider-dark'/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<SettingItemMin
|
||||
title={<SectionTitle section={section}/>}
|
||||
@@ -24,6 +25,7 @@ export default function CollapseView({onExpandSection, globalNotifyLevel, member
|
||||
<Describe
|
||||
section={section}
|
||||
ignoreChannelMentions={ignoreChannelMentions}
|
||||
channelAutoFollowThreads={channelAutoFollowThreads}
|
||||
memberNotifyLevel={memberNotifyLevel}
|
||||
globalNotifyLevel={globalNotifyLevel}
|
||||
isCollapsed={true}
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
|
||||
import {ChannelAutoFollowThreads, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
|
||||
import {t} from 'utils/i18n';
|
||||
|
||||
type Props = {
|
||||
globalNotifyLevel?: string;
|
||||
ignoreChannelMentions?: string;
|
||||
channelAutoFollowThreads?: string;
|
||||
memberNotifyLevel: string;
|
||||
section: string;
|
||||
isCollapsed?: boolean;
|
||||
}
|
||||
|
||||
export default function Describe({section, isCollapsed, memberNotifyLevel, globalNotifyLevel, ignoreChannelMentions}: Props) {
|
||||
export default function Describe({section, isCollapsed, memberNotifyLevel, globalNotifyLevel, ignoreChannelMentions, channelAutoFollowThreads}: Props) {
|
||||
if (memberNotifyLevel === NotificationLevels.DEFAULT && globalNotifyLevel) {
|
||||
t('channel_notifications.levels.default');
|
||||
t('channel_notifications.levels.all');
|
||||
@@ -70,6 +71,26 @@ export default function Describe({section, isCollapsed, memberNotifyLevel, globa
|
||||
defaultMessage='Off'
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
section === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS &&
|
||||
channelAutoFollowThreads === ChannelAutoFollowThreads.ON
|
||||
) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='channel_notifications.channelAutoFollowThreads.on.title'
|
||||
defaultMessage='On'
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
section === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS &&
|
||||
channelAutoFollowThreads === ChannelAutoFollowThreads.OFF
|
||||
) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='channel_notifications.channelAutoFollowThreads.off.title'
|
||||
defaultMessage='Off'
|
||||
/>
|
||||
);
|
||||
} else if (memberNotifyLevel === NotificationLevels.MENTION) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
|
||||
@@ -7,7 +7,7 @@ import {useSelector} from 'react-redux';
|
||||
|
||||
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
|
||||
import {ChannelAutoFollowThreads, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
|
||||
|
||||
import SettingItemMax from 'components/setting_item_max';
|
||||
|
||||
@@ -17,6 +17,7 @@ import SectionTitle from './section_title';
|
||||
|
||||
type Props = {
|
||||
ignoreChannelMentions?: string;
|
||||
channelAutoFollowThreads?: string;
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
onChangeThreads?: (e: ChangeEvent<HTMLInputElement>) => 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({
|
||||
</div>
|
||||
</fieldset>
|
||||
}
|
||||
{section === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS &&
|
||||
<fieldset>
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input
|
||||
id='channelAutoFollowThreadsOn'
|
||||
name='channelAutoFollowThreads'
|
||||
type='radio'
|
||||
value={ChannelAutoFollowThreads.ON}
|
||||
checked={channelAutoFollowThreads === ChannelAutoFollowThreads.ON}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Describe
|
||||
section={section}
|
||||
channelAutoFollowThreads={ChannelAutoFollowThreads.ON}
|
||||
memberNotifyLevel={memberNotifyLevel}
|
||||
globalNotifyLevel={globalNotifyLevel}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input
|
||||
id='channelAutoFollowThreadsOff'
|
||||
name='channelAutoFollowThreads'
|
||||
type='radio'
|
||||
value={ChannelAutoFollowThreads.OFF}
|
||||
checked={channelAutoFollowThreads === ChannelAutoFollowThreads.OFF}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Describe
|
||||
section={section}
|
||||
channelAutoFollowThreads={ChannelAutoFollowThreads.OFF}
|
||||
memberNotifyLevel={memberNotifyLevel}
|
||||
globalNotifyLevel={globalNotifyLevel}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
}
|
||||
{section === NotificationSections.MARK_UNREAD &&
|
||||
<fieldset>
|
||||
<div className='radio'>
|
||||
|
||||
@@ -48,6 +48,15 @@ export default function ExtraInfo({section}: Props) {
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
case NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS:
|
||||
return (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='channel_notifications.channelAutoFollowThreads.help'
|
||||
defaultMessage='When enabled, you will auto-follow all new threads created in this channel unless you unfollow a thread explicitly.'
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<FormattedMessage
|
||||
id='channel_notifications.channelAutoFollowThreads'
|
||||
defaultMessage='Auto-follow all new threads in this channel'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -453,6 +453,7 @@ class TestHelper {
|
||||
mark_unread: 'mention',
|
||||
push: 'default',
|
||||
ignore_channel_mentions: 'default',
|
||||
channel_auto_follow_threads: 'off',
|
||||
...override,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Ссылка в новой задаче
Block a user