diff --git a/server/channels/api4/preference_test.go b/server/channels/api4/preference_test.go
index 4c2c15673a..5478da7707 100644
--- a/server/channels/api4/preference_test.go
+++ b/server/channels/api4/preference_test.go
@@ -50,8 +50,8 @@ func TestGetPreferences(t *testing.T) {
prefs, _, err := client.GetPreferences(context.Background(), user1.Id)
require.NoError(t, err)
- // 5 because we have 2 initial preferences tutorial_step and recommended_next_steps added when creating a new user
- require.Equal(t, len(prefs), 5, "received the wrong number of preferences")
+ // 6 because we have 3 initial preferences tutorial_step, recommended_next_steps and system_notification are added when creating a new user
+ require.Equal(t, len(prefs), 6, "received the wrong number of preferences")
for _, preference := range prefs {
require.Equal(t, preference.UserId, th.BasicUser.Id, "user id does not match")
diff --git a/server/channels/app/notification.go b/server/channels/app/notification.go
index 6d1a4a52e2..976644a134 100644
--- a/server/channels/app/notification.go
+++ b/server/channels/app/notification.go
@@ -387,7 +387,9 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
status = &model.Status{UserId: id, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
- if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], true, status, post) {
+ isExplicitlyMentioned := mentions.Mentions[id] > GMMention
+ isGM := channel.Type == model.ChannelTypeGroup
+ if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], isExplicitlyMentioned, status, post, isGM) {
mentionType := mentions.Mentions[id]
replyToThreadType := ""
@@ -428,7 +430,8 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
status = &model.Status{UserId: id, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
- if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], false, status, post) {
+ isGM := channel.Type == model.ChannelTypeGroup
+ if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], false, status, post, isGM) {
a.sendPushNotification(
notification,
profileMap[id],
@@ -770,6 +773,14 @@ func (a *App) getExplicitMentionsAndKeywords(c request.CTX, post *model.Post, ch
keywords = a.getMentionKeywordsInChannel(profileMap, allowChannelMentions, channelMemberNotifyPropsMap)
mentions = getExplicitMentions(post, keywords, groups)
+
+ // Add a GM mention to all members of a GM channel
+ if channel.Type == model.ChannelTypeGroup {
+ for id := range channelMemberNotifyPropsMap {
+ mentions.addMention(id, GMMention)
+ }
+ }
+
// Add an implicit mention when a user is added to a channel
// even if the user has set 'username mentions' to false in account settings.
if post.Type == model.PostTypeAddToChannel {
@@ -1063,6 +1074,9 @@ const (
// A placeholder that should never be used in practice
NoMention MentionType = iota
+ // The post is in a GM
+ GMMention
+
// The post is in a thread that the user has commented on
ThreadMention
diff --git a/server/channels/app/notification_push.go b/server/channels/app/notification_push.go
index d52868ef38..a928f74376 100644
--- a/server/channels/app/notification_push.go
+++ b/server/channels/app/notification_push.go
@@ -512,12 +512,12 @@ func (a *App) getMobileAppSessions(userID string) ([]*model.Session, *model.AppE
return sessions, nil
}
-func ShouldSendPushNotification(user *model.User, channelNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post) bool {
- return DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, wasMentioned) &&
+func ShouldSendPushNotification(user *model.User, channelNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post, isGM bool) bool {
+ return DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, wasMentioned, isGM) &&
DoesStatusAllowPushNotification(user.NotifyProps, status, post.ChannelId)
}
-func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps model.StringMap, post *model.Post, wasMentioned bool) bool {
+func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps model.StringMap, post *model.Post, wasMentioned, isGM bool) bool {
userNotifyProps := user.NotifyProps
userNotify := userNotifyProps[model.PushNotifyProp]
channelNotify, ok := channelNotifyProps[model.PushNotifyProp]
@@ -525,6 +525,14 @@ func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps m
channelNotify = model.ChannelNotifyDefault
}
+ notify := channelNotify
+ if channelNotify == model.ChannelNotifyDefault {
+ notify = userNotify
+ if isGM && userNotify == model.UserNotifyMention {
+ notify = model.ChannelNotifyAll
+ }
+ }
+
// If the channel is muted do not send push notifications
if channelNotifyProps[model.MarkUnreadNotifyProp] == model.ChannelMarkUnreadMention {
return false
@@ -534,28 +542,19 @@ func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps m
return false
}
- if channelNotify == model.UserNotifyNone {
+ if notify == model.ChannelNotifyNone {
return false
}
- if channelNotify == model.ChannelNotifyMention && !wasMentioned {
+ if notify == model.ChannelNotifyMention && !wasMentioned {
return false
}
- if userNotify == model.UserNotifyMention && channelNotify == model.ChannelNotifyDefault && !wasMentioned {
- return false
- }
-
- if (userNotify == model.UserNotifyAll || channelNotify == model.ChannelNotifyAll) &&
+ if (notify == model.ChannelNotifyAll) &&
(post.UserId != user.Id || post.GetProp("from_webhook") == "true") {
return true
}
- if userNotify == model.UserNotifyNone &&
- channelNotify == model.ChannelNotifyDefault {
- return false
- }
-
return true
}
diff --git a/server/channels/app/notification_push_test.go b/server/channels/app/notification_push_test.go
index 0fd3e8848e..dff3655099 100644
--- a/server/channels/app/notification_push_test.go
+++ b/server/channels/app/notification_push_test.go
@@ -36,6 +36,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned bool
isMuted bool
expected bool
+ isGM bool
}{
{
name: "When post is a System Message and has no mentions",
@@ -45,6 +46,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When post is a System Message and has mentions",
@@ -54,6 +56,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is ALL, no channel props is set and has no mentions",
@@ -63,6 +66,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: true,
+ isGM: false,
},
{
name: "When default is ALL, no channel props is set and has mentions",
@@ -72,6 +76,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: true,
+ isGM: false,
},
{
name: "When default is MENTION, no channel props is set and has no mentions",
@@ -81,6 +86,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is MENTION, no channel props is set and has mentions",
@@ -90,6 +96,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: true,
+ isGM: false,
},
{
name: "When default is NONE, no channel props is set and has no mentions",
@@ -99,6 +106,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is NONE, no channel props is set and has mentions",
@@ -108,6 +116,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is ALL, channel is DEFAULT and has no mentions",
@@ -117,6 +126,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: true,
+ isGM: false,
},
{
name: "When default is ALL, channel is DEFAULT and has mentions",
@@ -126,6 +136,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: true,
+ isGM: false,
},
{
name: "When default is MENTION, channel is DEFAULT and has no mentions",
@@ -135,6 +146,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is MENTION, channel is DEFAULT and has mentions",
@@ -144,6 +156,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: true,
+ isGM: false,
},
{
name: "When default is NONE, channel is DEFAULT and has no mentions",
@@ -153,6 +166,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is NONE, channel is DEFAULT and has mentions",
@@ -162,6 +176,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is ALL, channel is ALL and has no mentions",
@@ -171,6 +186,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: true,
+ isGM: false,
},
{
name: "When default is ALL, channel is ALL and has mentions",
@@ -180,6 +196,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: true,
+ isGM: false,
},
{
name: "When default is MENTION, channel is ALL and has no mentions",
@@ -189,6 +206,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: true,
+ isGM: false,
},
{
name: "When default is MENTION, channel is ALL and has mentions",
@@ -198,6 +216,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: true,
+ isGM: false,
},
{
name: "When default is NONE, channel is ALL and has no mentions",
@@ -207,6 +226,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: true,
+ isGM: false,
},
{
name: "When default is NONE, channel is ALL and has mentions",
@@ -216,6 +236,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: true,
+ isGM: false,
},
{
name: "When default is ALL, channel is MENTION and has no mentions",
@@ -225,6 +246,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is ALL, channel is MENTION and has mentions",
@@ -234,6 +256,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: true,
+ isGM: false,
},
{
name: "When default is MENTION, channel is MENTION and has no mentions",
@@ -243,6 +266,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is MENTION, channel is MENTION and has mentions",
@@ -252,6 +276,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: true,
+ isGM: false,
},
{
name: "When default is NONE, channel is MENTION and has no mentions",
@@ -261,6 +286,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is NONE, channel is MENTION and has mentions",
@@ -270,6 +296,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: true,
+ isGM: false,
},
{
name: "When default is ALL, channel is NONE and has no mentions",
@@ -279,6 +306,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is ALL, channel is NONE and has mentions",
@@ -288,6 +316,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is MENTION, channel is NONE and has no mentions",
@@ -297,6 +326,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is MENTION, channel is NONE and has mentions",
@@ -306,6 +336,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is NONE, channel is NONE and has no mentions",
@@ -315,6 +346,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is NONE, channel is NONE and has mentions",
@@ -324,6 +356,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: true,
isMuted: false,
expected: false,
+ isGM: false,
},
{
name: "When default is ALL, and channel is MUTED",
@@ -333,6 +366,47 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
wasMentioned: false,
isMuted: true,
expected: false,
+ isGM: false,
+ },
+ {
+ name: "For GM default for NONE is NONE",
+ userNotifySetting: model.UserNotifyNone,
+ channelNotifySetting: model.ChannelNotifyDefault,
+ withSystemPost: false,
+ wasMentioned: false,
+ isMuted: false,
+ expected: false,
+ isGM: true,
+ },
+ {
+ name: "For GM, mentioned is only called if explicitly mentioned",
+ userNotifySetting: model.UserNotifyNone,
+ channelNotifySetting: model.ChannelNotifyMention,
+ withSystemPost: false,
+ wasMentioned: true,
+ isMuted: false,
+ expected: true,
+ isGM: true,
+ },
+ {
+ name: "For GM default for MENTION is ALL",
+ userNotifySetting: model.UserNotifyMention,
+ channelNotifySetting: model.ChannelNotifyDefault,
+ withSystemPost: false,
+ wasMentioned: false,
+ isMuted: false,
+ expected: true,
+ isGM: true,
+ },
+ {
+ name: "For GM, mentioned is only called if explicitly mentioned",
+ userNotifySetting: model.UserNotifyNone,
+ channelNotifySetting: model.ChannelNotifyMention,
+ withSystemPost: false,
+ wasMentioned: false,
+ isMuted: false,
+ expected: false,
+ isGM: true,
},
}
@@ -352,7 +426,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
if tc.isMuted {
channelNotifyProps[model.MarkUnreadNotifyProp] = model.ChannelMarkUnreadMention
}
- assert.Equal(t, tc.expected, DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, tc.wasMentioned))
+ assert.Equal(t, tc.expected, DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, tc.wasMentioned, tc.isGM))
})
}
}
diff --git a/server/channels/app/notification_test.go b/server/channels/app/notification_test.go
index 6b4f638f6d..983f4a4b00 100644
--- a/server/channels/app/notification_test.go
+++ b/server/channels/app/notification_test.go
@@ -38,19 +38,21 @@ func TestSendNotifications(t *testing.T) {
th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel, false)
- post1, appErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{
+ post1, createPostErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "@" + th.BasicUser2.Username,
Type: model.PostTypeAddToChannel,
Props: map[string]any{model.PostPropsAddedUserId: "junk"},
}, true, true)
- require.Nil(t, appErr)
+ require.Nil(t, createPostErr)
- mentions, err := th.App.SendNotifications(th.Context, post1, th.BasicTeam, th.BasicChannel, th.BasicUser, nil, true)
- require.NoError(t, err)
- require.NotNil(t, mentions)
- require.True(t, pUtils.Contains(mentions, th.BasicUser2.Id), "mentions", mentions)
+ t.Run("Basic channel", func(t *testing.T) {
+ mentions, err := th.App.SendNotifications(th.Context, post1, th.BasicTeam, th.BasicChannel, th.BasicUser, nil, true)
+ require.NoError(t, err)
+ require.NotNil(t, mentions)
+ require.True(t, pUtils.Contains(mentions, th.BasicUser2.Id), "mentions", mentions)
+ })
t.Run("license is required for group mention", func(t *testing.T) {
group := th.CreateGroup()
@@ -70,7 +72,7 @@ func TestSendNotifications(t *testing.T) {
groupMentionPost, createPostErr := th.App.CreatePost(th.Context, groupMentionPost, th.BasicChannel, false, true)
require.Nil(t, createPostErr)
- mentions, err = th.App.SendNotifications(th.Context, groupMentionPost, th.BasicTeam, th.BasicChannel, th.BasicUser, nil, true)
+ mentions, err := th.App.SendNotifications(th.Context, groupMentionPost, th.BasicTeam, th.BasicChannel, th.BasicUser, nil, true)
require.NoError(t, err)
require.NotNil(t, mentions)
require.Len(t, mentions, 0)
@@ -83,40 +85,83 @@ func TestSendNotifications(t *testing.T) {
require.Len(t, mentions, 1)
})
- dm, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id)
- require.Nil(t, appErr)
+ t.Run("message in DM generate mention", func(t *testing.T) {
+ dm, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id)
+ require.Nil(t, appErr)
- post2, appErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{
- UserId: th.BasicUser.Id,
- ChannelId: dm.Id,
- Message: "dm message",
- }, true, true)
- require.Nil(t, appErr)
+ post2, appErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{
+ UserId: th.BasicUser.Id,
+ ChannelId: dm.Id,
+ Message: "dm message",
+ }, true, true)
+ require.Nil(t, appErr)
- mentions, err = th.App.SendNotifications(th.Context, post2, th.BasicTeam, dm, th.BasicUser, nil, true)
- require.NoError(t, err)
- require.NotNil(t, mentions)
+ mentions, err := th.App.SendNotifications(th.Context, post2, th.BasicTeam, dm, th.BasicUser, nil, true)
+ require.NoError(t, err)
+ require.NotNil(t, mentions)
- _, appErr = th.App.UpdateActive(th.Context, th.BasicUser2, false)
- require.Nil(t, appErr)
- appErr = th.App.Srv().InvalidateAllCaches()
- require.Nil(t, appErr)
+ _, appErr = th.App.UpdateActive(th.Context, th.BasicUser2, false)
+ require.Nil(t, appErr)
+ appErr = th.App.Srv().InvalidateAllCaches()
+ require.Nil(t, appErr)
- post3, appErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{
- UserId: th.BasicUser.Id,
- ChannelId: dm.Id,
- Message: "dm message",
- }, true, true)
- require.Nil(t, appErr)
+ post3, appErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{
+ UserId: th.BasicUser.Id,
+ ChannelId: dm.Id,
+ Message: "dm message",
+ }, true, true)
+ require.Nil(t, appErr)
- mentions, err = th.App.SendNotifications(th.Context, post3, th.BasicTeam, dm, th.BasicUser, nil, true)
- require.NoError(t, err)
- require.NotNil(t, mentions)
+ mentions, err = th.App.SendNotifications(th.Context, post3, th.BasicTeam, dm, th.BasicUser, nil, true)
+ require.NoError(t, err)
+ require.NotNil(t, mentions)
- th.BasicChannel.DeleteAt = 1
- mentions, err = th.App.SendNotifications(th.Context, post1, th.BasicTeam, th.BasicChannel, th.BasicUser, nil, true)
- require.NoError(t, err)
- require.Empty(t, mentions)
+ th.BasicChannel.DeleteAt = 1
+ mentions, err = th.App.SendNotifications(th.Context, post1, th.BasicTeam, th.BasicChannel, th.BasicUser, nil, true)
+ require.NoError(t, err)
+ require.Empty(t, mentions)
+ })
+
+ t.Run("message in GM generate mention", func(t *testing.T) {
+ users := []*model.User{}
+ for i := 0; i < 2; i++ {
+ user := th.CreateUser()
+ users = append(users, user)
+ }
+ channel := th.CreateGroupChannel(th.Context, users[0], users[1])
+
+ post2, appErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{
+ UserId: users[0].Id,
+ ChannelId: channel.Id,
+ Message: "gm message",
+ }, true, true)
+ require.Nil(t, appErr)
+
+ mentions, err := th.App.SendNotifications(th.Context, post2, th.BasicTeam, channel, users[0], nil, true)
+ require.NoError(t, err)
+ require.NotNil(t, mentions)
+
+ _, appErr = th.App.UpdateActive(th.Context, users[1], false)
+ require.Nil(t, appErr)
+ appErr = th.App.Srv().InvalidateAllCaches()
+ require.Nil(t, appErr)
+
+ post3, appErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{
+ UserId: users[0].Id,
+ ChannelId: channel.Id,
+ Message: "gm message",
+ }, true, true)
+ require.Nil(t, appErr)
+
+ mentions, err = th.App.SendNotifications(th.Context, post3, th.BasicTeam, channel, users[0], nil, true)
+ require.NoError(t, err)
+ require.NotNil(t, mentions)
+
+ th.BasicChannel.DeleteAt = 1
+ mentions, err = th.App.SendNotifications(th.Context, post1, th.BasicTeam, th.BasicChannel, users[0], nil, true)
+ require.NoError(t, err)
+ require.Empty(t, mentions)
+ })
t.Run("replies to post created by OAuth bot should not notify user", func(t *testing.T) {
th := Setup(t).InitBasic()
@@ -129,7 +174,7 @@ func TestSendNotifications(t *testing.T) {
Props: model.StringInterface{"from_webhook": "true", "override_username": "a bot"},
}
- rootPost, appErr = th.App.CreatePostMissingChannel(th.Context, rootPost, false, true)
+ rootPost, appErr := th.App.CreatePostMissingChannel(th.Context, rootPost, false, true)
require.Nil(t, appErr)
childPost := &model.Post{
@@ -145,11 +190,12 @@ func TestSendNotifications(t *testing.T) {
Order: []string{rootPost.Id, childPost.Id},
Posts: map[string]*model.Post{rootPost.Id: rootPost, childPost.Id: childPost},
}
- mentions, err = th.App.SendNotifications(th.Context, childPost, th.BasicTeam, th.BasicChannel, th.BasicUser2, &postList, true)
+ mentions, err := th.App.SendNotifications(th.Context, childPost, th.BasicTeam, th.BasicChannel, th.BasicUser2, &postList, true)
require.NoError(t, err)
require.False(t, pUtils.Contains(mentions, user.Id))
}
+ var appErr *model.AppError
th.BasicUser.NotifyProps[model.CommentsNotifyProp] = model.CommentsNotifyAny
th.BasicUser, appErr = th.App.UpdateUser(th.Context, th.BasicUser, false)
require.Nil(t, appErr)
diff --git a/server/channels/app/plugin_api_test.go b/server/channels/app/plugin_api_test.go
index 01ef03dec0..c1dc4ae35f 100644
--- a/server/channels/app/plugin_api_test.go
+++ b/server/channels/app/plugin_api_test.go
@@ -179,17 +179,19 @@ func TestPluginAPIGetUserPreferences(t *testing.T) {
preferences, err := api.GetPreferencesForUser(user1.Id)
require.Nil(t, err)
- assert.Equal(t, 2, len(preferences))
+ assert.Equal(t, 3, len(preferences))
assert.Equal(t, user1.Id, preferences[0].UserId)
assert.Equal(t, model.PreferenceRecommendedNextSteps, preferences[0].Category)
assert.Equal(t, "hide", preferences[0].Name)
assert.Equal(t, "false", preferences[0].Value)
- assert.Equal(t, user1.Id, preferences[1].UserId)
- assert.Equal(t, model.PreferenceCategoryTutorialSteps, preferences[1].Category)
- assert.Equal(t, user1.Id, preferences[1].Name)
- assert.Equal(t, "0", preferences[1].Value)
+ assert.Equal(t, model.PreferenceCategorySystemNotice, preferences[1].Category)
+
+ assert.Equal(t, user1.Id, preferences[2].UserId)
+ assert.Equal(t, model.PreferenceCategoryTutorialSteps, preferences[2].Category)
+ assert.Equal(t, user1.Id, preferences[2].Name)
+ assert.Equal(t, "0", preferences[2].Value)
}
func TestPluginAPIDeleteUserPreferences(t *testing.T) {
@@ -207,7 +209,7 @@ func TestPluginAPIDeleteUserPreferences(t *testing.T) {
preferences, err := api.GetPreferencesForUser(user1.Id)
require.Nil(t, err)
- assert.Equal(t, 2, len(preferences))
+ assert.Equal(t, 3, len(preferences))
err = api.DeletePreferencesForUser(user1.Id, preferences)
require.Nil(t, err)
@@ -234,16 +236,16 @@ func TestPluginAPIDeleteUserPreferences(t *testing.T) {
preferences, err = api.GetPreferencesForUser(user2.Id)
require.Nil(t, err)
- assert.Equal(t, 3, len(preferences))
+ assert.Equal(t, 4, len(preferences))
err = api.DeletePreferencesForUser(user2.Id, []model.Preference{preference})
require.Nil(t, err)
preferences, err = api.GetPreferencesForUser(user2.Id)
require.Nil(t, err)
- assert.Equal(t, 2, len(preferences))
+ assert.Equal(t, 3, len(preferences))
assert.ElementsMatch(t,
- []string{model.PreferenceRecommendedNextSteps, model.PreferenceCategoryTutorialSteps},
- []string{preferences[0].Category, preferences[1].Category},
+ []string{model.PreferenceRecommendedNextSteps, model.PreferenceCategoryTutorialSteps, model.PreferenceCategorySystemNotice},
+ []string{preferences[0].Category, preferences[1].Category, preferences[2].Category},
)
}
@@ -262,16 +264,17 @@ func TestPluginAPIUpdateUserPreferences(t *testing.T) {
preferences, err := api.GetPreferencesForUser(user1.Id)
require.Nil(t, err)
- assert.Equal(t, 2, len(preferences))
+ assert.Equal(t, 3, len(preferences))
assert.Equal(t, user1.Id, preferences[0].UserId)
assert.Equal(t, model.PreferenceRecommendedNextSteps, preferences[0].Category)
assert.Equal(t, "hide", preferences[0].Name)
assert.Equal(t, "false", preferences[0].Value)
- assert.Equal(t, user1.Id, preferences[1].UserId)
- assert.Equal(t, model.PreferenceCategoryTutorialSteps, preferences[1].Category)
- assert.Equal(t, user1.Id, preferences[1].Name)
- assert.Equal(t, "0", preferences[1].Value)
+ assert.Equal(t, model.PreferenceCategorySystemNotice, preferences[1].Category)
+ assert.Equal(t, user1.Id, preferences[2].UserId)
+ assert.Equal(t, model.PreferenceCategoryTutorialSteps, preferences[2].Category)
+ assert.Equal(t, user1.Id, preferences[2].Name)
+ assert.Equal(t, "0", preferences[2].Value)
preference := model.Preference{
Name: user1.Id,
@@ -286,8 +289,8 @@ func TestPluginAPIUpdateUserPreferences(t *testing.T) {
preferences, err = api.GetPreferencesForUser(user1.Id)
require.Nil(t, err)
- assert.Equal(t, 3, len(preferences))
- expectedCategories := []string{model.PreferenceCategoryTutorialSteps, model.PreferenceCategoryTheme, model.PreferenceRecommendedNextSteps}
+ assert.Equal(t, 4, len(preferences))
+ expectedCategories := []string{model.PreferenceCategoryTutorialSteps, model.PreferenceCategoryTheme, model.PreferenceRecommendedNextSteps, model.PreferenceCategorySystemNotice}
for _, pref := range preferences {
assert.Contains(t, expectedCategories, pref.Category)
assert.Equal(t, user1.Id, pref.UserId)
diff --git a/server/channels/app/post_persistent_notification.go b/server/channels/app/post_persistent_notification.go
index 773756afb2..a8ff30f1fe 100644
--- a/server/channels/app/post_persistent_notification.go
+++ b/server/channels/app/post_persistent_notification.go
@@ -275,9 +275,9 @@ func (a *App) channelTeamMapsForPosts(posts []*model.Post) (map[string]*model.Ch
func (a *App) sendPersistentNotifications(post *model.Post, channel *model.Channel, team *model.Team, mentions *ExplicitMentions, profileMap model.UserMap, channelNotifyProps map[string]map[string]model.StringMap) error {
mentionedUsersList := make(model.StringArray, 0, len(mentions.Mentions))
- for id := range mentions.Mentions {
- // Don't send notification to post owner
- if id != post.UserId {
+ for id, v := range mentions.Mentions {
+ // Don't send notification to post owner nor GM mentions
+ if id != post.UserId && v > GMMention {
mentionedUsersList = append(mentionedUsersList, id)
}
}
@@ -307,7 +307,8 @@ func (a *App) sendPersistentNotifications(post *model.Post, channel *model.Chann
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
- if ShouldSendPushNotification(profileMap[userID], channelNotifyProps[channel.Id][userID], true, status, post) {
+ isGM := channel.Type == model.ChannelTypeGroup
+ if ShouldSendPushNotification(profileMap[userID], channelNotifyProps[channel.Id][userID], true, status, post, isGM) {
a.sendPushNotification(
notification,
user,
diff --git a/server/channels/app/user.go b/server/channels/app/user.go
index 3864e4949a..335f6eed31 100644
--- a/server/channels/app/user.go
+++ b/server/channels/app/user.go
@@ -282,8 +282,9 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m
recommendedNextStepsPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceRecommendedNextSteps, Name: "hide", Value: "false"}
tutorialStepPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryTutorialSteps, Name: ruser.Id, Value: "0"}
+ gmASdmPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategorySystemNotice, Name: "GMasDM", Value: "true"}
- preferences := model.Preferences{recommendedNextStepsPref, tutorialStepPref}
+ preferences := model.Preferences{recommendedNextStepsPref, tutorialStepPref, gmASdmPref}
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
c.Logger().Warn("Encountered error saving user preferences", mlog.Err(err))
}
diff --git a/server/channels/app/user_test.go b/server/channels/app/user_test.go
index 282586d8ed..5540af2e54 100644
--- a/server/channels/app/user_test.go
+++ b/server/channels/app/user_test.go
@@ -1825,6 +1825,11 @@ func TestCreateUserWithInitialPreferences(t *testing.T) {
assert.Equal(t, model.PreferenceRecommendedNextSteps, recommendedNextStepsPref[0].Category)
assert.Equal(t, "hide", recommendedNextStepsPref[0].Name)
assert.Equal(t, "false", recommendedNextStepsPref[0].Value)
+
+ gmASdmNoticeViewedPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(testUser.Id, model.PreferenceCategorySystemNotice, "GMasDM")
+ require.Nil(t, appErr)
+ assert.Equal(t, "GMasDM", gmASdmNoticeViewedPref.Name)
+ assert.Equal(t, "true", gmASdmNoticeViewedPref.Value)
})
t.Run("successfully create a guest user with initial tutorial and recommended steps preferences", func(t *testing.T) {
@@ -1842,6 +1847,11 @@ func TestCreateUserWithInitialPreferences(t *testing.T) {
assert.Equal(t, model.PreferenceRecommendedNextSteps, recommendedNextStepsPref[0].Category)
assert.Equal(t, "hide", recommendedNextStepsPref[0].Name)
assert.Equal(t, "false", recommendedNextStepsPref[0].Value)
+
+ gmASdmNoticeViewedPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(testUser.Id, model.PreferenceCategorySystemNotice, "GMasDM")
+ require.Nil(t, appErr)
+ assert.Equal(t, "GMasDM", gmASdmNoticeViewedPref.Name)
+ assert.Equal(t, "true", gmASdmNoticeViewedPref.Value)
})
}
diff --git a/server/public/model/preference.go b/server/public/model/preference.go
index a73e19dad6..bd54a88f66 100644
--- a/server/public/model/preference.go
+++ b/server/public/model/preference.go
@@ -31,6 +31,8 @@ const (
PreferenceNameUseMilitaryTime = "use_military_time"
PreferenceRecommendedNextSteps = "recommended_next_steps"
+ PreferenceCategorySystemNotice = "system_notice"
+
PreferenceCategoryTheme = "theme"
// the name for theme props is the team id
diff --git a/webapp/channels/src/actions/notification_actions.jsx b/webapp/channels/src/actions/notification_actions.jsx
index bf0be0dc36..2728d7f9e2 100644
--- a/webapp/channels/src/actions/notification_actions.jsx
+++ b/webapp/channels/src/actions/notification_actions.jsx
@@ -9,6 +9,7 @@ import {
getTeammateNameDisplaySetting,
isCollapsedThreadsEnabled,
} from 'mattermost-redux/selectors/entities/preferences';
+import {getAllUserMentionKeys} from 'mattermost-redux/selectors/entities/search';
import {getCurrentUserId, getCurrentUser, getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users';
import {isChannelMuted} from 'mattermost-redux/utils/channel_utils';
import {isSystemMessage, isUserAddedInChannel} from 'mattermost-redux/utils/post_utils';
@@ -18,11 +19,13 @@ import {getChannelURL, getPermalinkURL} from 'selectors/urls';
import {isThreadOpen} from 'selectors/views/threads';
import {getHistory} from 'utils/browser_history';
-import Constants, {NotificationLevels, UserStatuses} from 'utils/constants';
+import Constants, {NotificationLevels, UserStatuses, IgnoreChannelMentions} from 'utils/constants';
import {t} from 'utils/i18n';
-import {stripMarkdown} from 'utils/markdown';
+import {stripMarkdown, formatWithRenderer} from 'utils/markdown';
+import MentionableRenderer from 'utils/markdown/mentionable_renderer';
import * as NotificationSounds from 'utils/notification_sounds';
import {showNotification} from 'utils/notifications';
+import {cjkrPattern, escapeRegex} from 'utils/text_formatting';
import {isDesktopApp, isMobileApp, isWindowsApp} from 'utils/user_agent';
import * as Utils from 'utils/utils';
@@ -93,14 +96,94 @@ export function sendDesktopNotification(post, msgProps) {
return;
}
- let notifyLevel = member?.notify_props?.desktop || NotificationLevels.DEFAULT;
+ const channelNotifyProp = member?.notify_props?.desktop || NotificationLevels.DEFAULT;
+ let notifyLevel = channelNotifyProp;
if (notifyLevel === NotificationLevels.DEFAULT) {
notifyLevel = user?.notify_props?.desktop || NotificationLevels.ALL;
}
+ if (channel.type === 'G' && channelNotifyProp === NotificationLevels.DEFAULT && user?.notify_props?.desktop === NotificationLevels.MENTION) {
+ notifyLevel = NotificationLevels.ALL;
+ }
+
if (notifyLevel === NotificationLevels.NONE) {
return;
+ } else if (channel.type === 'G' && notifyLevel === NotificationLevels.MENTION) {
+ // Compose the whole text in the message, including interactive messages.
+ let text = post.message;
+
+ // We do this on a try catch block to avoid errors from malformed props
+ try {
+ if (post.props && post.props.attachments) {
+ const attachments = post.props.attachments;
+ function appendText(toAppend) {
+ if (toAppend) {
+ text += `\n${toAppend}`;
+ }
+ }
+ for (const attachment of attachments) {
+ appendText(attachment.pretext);
+ appendText(attachment.title);
+ appendText(attachment.text);
+ appendText(attachment.footer);
+ if (attachment.fields) {
+ for (const field of attachment.fields) {
+ appendText(field.title);
+ appendText(field.value);
+ }
+ }
+ }
+ }
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.log('Could not process the whole attachment for mentions', e);
+ }
+
+ const allMentions = getAllUserMentionKeys(state);
+
+ const ignoreChannelMentionProp = member?.notify_props?.ignore_channel_mentions || IgnoreChannelMentions.DEFAULT;
+ let ignoreChannelMention = ignoreChannelMentionProp === IgnoreChannelMentions.ON;
+ if (ignoreChannelMentionProp === IgnoreChannelMentions.DEFAULT) {
+ ignoreChannelMention = user?.notify_props?.channel === 'false';
+ }
+
+ const mentionableText = formatWithRenderer(text, new MentionableRenderer());
+ let isExplicitlyMentioned = false;
+ for (const mention of allMentions) {
+ if (!mention || !mention.key) {
+ continue;
+ }
+
+ if (ignoreChannelMention && ['@all', '@here', '@channel'].includes(mention.key)) {
+ continue;
+ }
+
+ let flags = 'g';
+ if (!mention.caseSensitive) {
+ flags += 'i';
+ }
+
+ let pattern;
+ if (cjkrPattern.test(mention.key)) {
+ // In the case of CJK mention key, even if there's no delimiters (such as spaces) at both ends of a word, it is recognized as a mention key
+ pattern = new RegExp(`()(${escapeRegex(mention.key)})()`, flags);
+ } else {
+ pattern = new RegExp(
+ `(^|\\W)(${escapeRegex(mention.key)})(\\b|_+\\b)`,
+ flags,
+ );
+ }
+
+ if (pattern.test(mentionableText)) {
+ isExplicitlyMentioned = true;
+ break;
+ }
+ }
+
+ if (!isExplicitlyMentioned) {
+ return;
+ }
} else if (notifyLevel === NotificationLevels.MENTION && mentions.indexOf(user.id) === -1 && msgProps.channel_type !== Constants.DM_CHANNEL) {
return;
} else if (isCrtReply && notifyLevel === NotificationLevels.ALL && followers.indexOf(currentUserId) === -1) {
diff --git a/webapp/channels/src/actions/notification_actions.test.js b/webapp/channels/src/actions/notification_actions.test.js
index 634da76259..0ee8e91984 100644
--- a/webapp/channels/src/actions/notification_actions.test.js
+++ b/webapp/channels/src/actions/notification_actions.test.js
@@ -36,6 +36,9 @@ describe('notification_actions', () => {
desktop: NotificationLevels.ALL,
desktop_sound: false,
desktop_threads: NotificationLevels.ALL,
+ mention_keys: 'mentionkey',
+ first_name: 'true',
+ channel: 'true',
};
post = {
@@ -79,8 +82,13 @@ describe('notification_actions', () => {
current_user_id: {
id: 'current_user_id',
notify_props: userSettings,
+ username: 'currentusername',
+ first_name: 'currentuserfirstname',
},
},
+ profilesInChannel: {
+ gm_channel: new Set(['current_user_id']),
+ },
},
teams: {
currentTeamId: 'team_id',
@@ -109,12 +117,20 @@ describe('notification_actions', () => {
id: 'another_channel_id',
team_id: 'team_id',
},
+ gm_channel: {
+ id: 'gm_channel',
+ type: 'G',
+ },
},
myMembers: {
channel_id: {
id: 'current_user_id',
notify_props: channelSettings,
},
+ gm_channel: {
+ id: 'gm_channel',
+ notify_props: channelSettings,
+ },
},
membersInChannel: {
channel_id: {
@@ -123,6 +139,12 @@ describe('notification_actions', () => {
notify_props: channelSettings,
},
},
+ gm_channel: {
+ current_user_id: {
+ id: 'gm_channel',
+ notify_props: channelSettings,
+ },
+ },
muted_channel_id: {
current_user_id: {
id: 'current_user_id',
@@ -138,6 +160,10 @@ describe('notification_actions', () => {
'display_settings--collapsed_reply_threads': crt,
},
},
+ groups: {
+ groups: {},
+ myGroups: [],
+ },
},
views: {
browser: {
@@ -401,5 +427,85 @@ describe('notification_actions', () => {
});
});
});
+
+ describe('GMs', () => {
+ test('should notify for any message when channel setting is DEFAULT and user setting is MENTION', async () => {
+ const store = testConfigureStore(baseState);
+ userSettings.desktop = NotificationLevels.MENTION;
+ channelSettings.desktop = NotificationLevels.DEFAULT;
+ post.channel_id = 'gm_channel';
+ msgProps.team_id = '';
+
+ return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
+ expect(spy).toHaveBeenCalled();
+ });
+ });
+ test('should not notify for any message when channel setting is DEFAULT and user setting is NONE', async () => {
+ const store = testConfigureStore(baseState);
+ userSettings.desktop = NotificationLevels.NONE;
+ channelSettings.desktop = NotificationLevels.DEFAULT;
+ post.message = '@username';
+ post.channel_id = 'gm_channel';
+ msgProps.team_id = '';
+
+ return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
+ test('should notify when channel setting MENTION and there is a explicit mention', async () => {
+ const store = testConfigureStore(baseState);
+ channelSettings.desktop = NotificationLevels.MENTION;
+ post.message = '@currentusername';
+ post.channel_id = 'gm_channel';
+ msgProps.team_id = '';
+
+ return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
+ expect(spy).toHaveBeenCalled();
+ });
+ });
+ test('should notify when channel setting MENTION and there is a keyword mention', async () => {
+ const store = testConfigureStore(baseState);
+ channelSettings.desktop = NotificationLevels.MENTION;
+ post.message = 'mentionkey';
+ post.channel_id = 'gm_channel';
+ msgProps.team_id = '';
+
+ return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
+ expect(spy).toHaveBeenCalled();
+ });
+ });
+ test('should notify when channel setting MENTION and there is the first name', async () => {
+ const store = testConfigureStore(baseState);
+ channelSettings.desktop = NotificationLevels.MENTION;
+ post.message = 'currentuserfirstname';
+ post.channel_id = 'gm_channel';
+ msgProps.team_id = '';
+
+ return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
+ expect(spy).toHaveBeenCalled();
+ });
+ });
+ test('should notify when channel setting MENTION and there is a channel mention', async () => {
+ const store = testConfigureStore(baseState);
+ channelSettings.desktop = NotificationLevels.MENTION;
+ post.message = '@all';
+ post.channel_id = 'gm_channel';
+ msgProps.team_id = '';
+
+ return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
+ expect(spy).toHaveBeenCalled();
+ });
+ });
+ test('should not notify when channel setting MENTION and there is no explicit mention', async () => {
+ const store = testConfigureStore(baseState);
+ channelSettings.desktop = NotificationLevels.MENTION;
+ post.channel_id = 'gm_channel';
+ msgProps.team_id = '';
+
+ return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
+ });
});
});
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 60197883fa..77bd53db54 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
@@ -70,6 +70,7 @@ exports[`components/channel_notifications_modal/ChannelNotificationsModal should
/>
+
+
+
+
+
+
+
+
+
+