[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 {
|
||||
|
||||
Ссылка в новой задаче
Block a user