From 88d043a97164544587cdbeba9acfb8e740685ad3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?=
Date: Tue, 19 Sep 2023 15:29:57 +0200
Subject: [PATCH] Make GM behave as DM (#24289)
* Make GM behave as DM
* Fix lint
* Add desktop notification special behavior
* Change notification preferences menu
* Make changes to the GM channel intro
* Fix tests
* Fix i18n and style lint
* Add system notice and update style
* Fix style and fix tests
* Fix tests
* Handle push notifications as desktop notifications
* Fix tests
* Add test and default GMs to none when user level config is none
* Fix test
* Update only for mentions text
* Add tests
* Fix lint
* Fix lint
---
server/channels/api4/preference_test.go | 4 +-
server/channels/app/notification.go | 18 +-
server/channels/app/notification_push.go | 29 +-
server/channels/app/notification_push_test.go | 76 ++-
server/channels/app/notification_test.go | 120 ++--
server/channels/app/plugin_api_test.go | 37 +-
.../app/post_persistent_notification.go | 9 +-
server/channels/app/user.go | 3 +-
server/channels/app/user_test.go | 10 +
server/public/model/preference.go | 2 +
.../src/actions/notification_actions.jsx | 89 ++-
.../src/actions/notification_actions.test.js | 106 +++
.../channel_notifications_modal.test.tsx.snap | 151 +++++
.../channel_notifications_modal.test.tsx | 31 +
.../channel_notifications_modal.tsx | 78 ++-
.../__snapshots__/describe.test.tsx.snap | 6 +-
.../__snapshots__/expand_view.test.tsx.snap | 610 +++++++++++++++++-
.../notification_section.test.jsx.snap | 3 +
.../components/describe.tsx | 6 +-
.../components/expand_view.test.tsx | 79 ++-
.../components/expand_view.tsx | 4 +
.../components/notification_section.jsx | 7 +
.../components/notification_section.test.jsx | 1 +
.../channel_intro_message.test.tsx | 4 +
.../channel_intro_message.tsx | 55 +-
.../__snapshots__/system_notice.test.tsx.snap | 422 ++++++------
.../src/components/system_notice/index.ts | 2 +
.../src/components/system_notice/notices.tsx | 44 +-
.../system_notice/system_notice.test.tsx | 26 +-
.../system_notice/system_notice.tsx | 62 +-
.../src/components/system_notice/types.ts | 7 +-
...esktop_notification_settings.test.tsx.snap | 12 +-
.../desktop_notification_settings.tsx | 2 +-
webapp/channels/src/i18n/en.json | 11 +-
.../src/sass/components/_system-notice.scss | 42 +-
webapp/channels/src/sass/layout/_headers.scss | 1 +
webapp/channels/src/utils/emoticons.tsx | 4 +-
.../utils/markdown/mentionable_renderer.tsx | 79 +--
.../src/utils/markdown/plain_renderer.ts | 81 +++
webapp/channels/src/utils/text_formatting.tsx | 2 +-
40 files changed, 1824 insertions(+), 511 deletions(-)
create mode 100644 webapp/channels/src/utils/markdown/plain_renderer.ts
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
/>
`;
+
+exports[`components/channel_notifications_modal/ChannelNotificationsModal should match snapshot for GMs 1`] = `
+
+
+
+
+
+ channel_display_name
+
+
+
+
+
+
+
+`;
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 b681e4e65a..b2f7d3f62f 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
@@ -54,6 +54,37 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', ()
expect(wrapper).toMatchSnapshot();
});
+ test('should match snapshot for GMs', () => {
+ const wrapper = shallow(
+ ,
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
test('should provide default notify props when missing', () => {
const wrapper = shallow(
export type PushNotificationProps = Pick
-const getDefaultDesktopNotificationLevel = (currentUserNotifyProps: UserNotifyProps): Exclude => {
+const getDefaultDesktopNotificationLevel = (currentUserNotifyProps: UserNotifyProps, isGM: boolean): Exclude => {
if (currentUserNotifyProps?.desktop) {
- if (currentUserNotifyProps.desktop === 'default') {
+ if (currentUserNotifyProps.desktop === NotificationLevels.DEFAULT) {
+ return NotificationLevels.ALL;
+ }
+
+ if (isGM && currentUserNotifyProps.desktop === NotificationLevels.MENTION) {
return NotificationLevels.ALL;
}
return currentUserNotifyProps.desktop;
@@ -86,11 +90,16 @@ const getDefaultDesktopThreadsNotifyLevel = (currentUserNotifyProps: UserNotifyP
return NotificationLevels.ALL;
};
-const getDefaultPushNotifyLevel = (currentUserNotifyProps: UserNotifyProps): Exclude => {
+const getDefaultPushNotifyLevel = (currentUserNotifyProps: UserNotifyProps, isGM: boolean): Exclude => {
if (currentUserNotifyProps?.push) {
- if (currentUserNotifyProps.push === 'default') {
+ if (currentUserNotifyProps.push === NotificationLevels.DEFAULT) {
return NotificationLevels.ALL;
}
+
+ if (isGM && currentUserNotifyProps.desktop === NotificationLevels.MENTION) {
+ return NotificationLevels.ALL;
+ }
+
return currentUserNotifyProps.push;
}
return NotificationLevels.ALL;
@@ -142,7 +151,7 @@ export default class ChannelNotificationsModal extends React.PureComponent{serverError} ;
}
+ const isGM = this.isGM();
+
return (
{!isChannelMuted(channelMember) &&
@@ -457,8 +478,8 @@ export default class ChannelNotificationsModal extends React.PureComponent
{sendPushNotifications &&
@@ -476,7 +498,7 @@ export default class ChannelNotificationsModal extends React.PureComponent
}
}
-
-
+ {!isGM &&
+ <>
+
+
+ >
+ }
diff --git a/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/describe.test.tsx.snap b/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/describe.test.tsx.snap
index d2ca4ebd67..917c769285 100644
--- a/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/describe.test.tsx.snap
+++ b/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/describe.test.tsx.snap
@@ -2,7 +2,7 @@
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, on DESKTOP/PUSH & ALL 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ ]
+ }
+ saving={false}
+ section=""
+ serverError=""
+ submit={[MockFunction]}
+ title={
+
+ }
+ updateSection={[MockFunction]}
+/>
+`;
+
+exports[`components/channel_notifications_modal/ExpandView gms should match snapshot, PUSH on expanded view when mentions is selected 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ ]
+ }
+ saving={false}
+ section=""
+ serverError=""
+ submit={[MockFunction]}
+ title={
+
+ }
+ updateSection={[MockFunction]}
+/>
+`;
+
+exports[`components/channel_notifications_modal/ExpandView normal channels should match snapshot, DESKTOP on expanded view 1`] = `
`;
-exports[`components/channel_notifications_modal/ExpandView should match snapshot, MARK_UNREAD on expanded view 1`] = `
+exports[`components/channel_notifications_modal/ExpandView normal channels should match snapshot, DESKTOP on expanded view when mentions is selected 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ ]
+ }
+ saving={false}
+ section=""
+ serverError=""
+ submit={[MockFunction]}
+ title={
+
+ }
+ updateSection={[MockFunction]}
+/>
+`;
+
+exports[`components/channel_notifications_modal/ExpandView normal channels should match snapshot, MARK_UNREAD on expanded view 1`] = `
`;
-exports[`components/channel_notifications_modal/ExpandView should match snapshot, PUSH on expanded view 1`] = `
+exports[`components/channel_notifications_modal/ExpandView normal channels should match snapshot, PUSH on expanded view 1`] = `
`;
+
+exports[`components/channel_notifications_modal/ExpandView normal channels should match snapshot, PUSH on expanded view when mentions is selected 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ ]
+ }
+ saving={false}
+ section=""
+ serverError=""
+ submit={[MockFunction]}
+ title={
+
+ }
+ updateSection={[MockFunction]}
+/>
+`;
diff --git a/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/notification_section.test.jsx.snap b/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/notification_section.test.jsx.snap
index 888861bc5d..be94eadce7 100644
--- a/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/notification_section.test.jsx.snap
+++ b/webapp/channels/src/components/channel_notifications_modal/components/__snapshots__/notification_section.test.jsx.snap
@@ -21,6 +21,7 @@ exports[`components/channel_notifications_modal/NotificationSection should match
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, DESKTOP on expanded view 1`] = `
>}}
/>
);
@@ -113,7 +113,7 @@ export default function Describe({section, isCollapsed, memberNotifyLevel, globa
return (
>}}
/>
);
@@ -132,7 +132,7 @@ export default function Describe({section, isCollapsed, memberNotifyLevel, globa
return (
>}}
/>
);
diff --git a/webapp/channels/src/components/channel_notifications_modal/components/expand_view.test.tsx b/webapp/channels/src/components/channel_notifications_modal/components/expand_view.test.tsx
index caec34c4bd..205bc3c42a 100644
--- a/webapp/channels/src/components/channel_notifications_modal/components/expand_view.test.tsx
+++ b/webapp/channels/src/components/channel_notifications_modal/components/expand_view.test.tsx
@@ -10,7 +10,7 @@ import {NotificationLevels, NotificationSections} from 'utils/constants';
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
- useSelector: jest.fn(),
+ useSelector: jest.fn(() => true),
}));
describe('components/channel_notifications_modal/ExpandView', () => {
@@ -25,31 +25,72 @@ describe('components/channel_notifications_modal/ExpandView', () => {
onCollapseSection: jest.fn(),
onSubmit: jest.fn(),
onReset: jest.fn(),
+ isGM: false,
};
- test('should match snapshot, DESKTOP on expanded view', () => {
- const wrapper = shallow(
- ,
- );
+ describe('normal channels', () => {
+ test('should match snapshot, DESKTOP on expanded view', () => {
+ const wrapper = shallow(
+ ,
+ );
- expect(wrapper).toMatchSnapshot();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('should match snapshot, PUSH on expanded view', () => {
+ const props = {...baseProps, section: NotificationSections.PUSH};
+ const wrapper = shallow(
+ ,
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('should match snapshot, MARK_UNREAD on expanded view', () => {
+ const props = {...baseProps, section: NotificationSections.MARK_UNREAD};
+ const wrapper = shallow(
+ ,
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('should match snapshot, DESKTOP on expanded view when mentions is selected', () => {
+ const props = {...baseProps, memberNotifyLevel: NotificationLevels.MENTION};
+ const wrapper = shallow(
+ ,
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('should match snapshot, PUSH on expanded view when mentions is selected', () => {
+ const props = {...baseProps, section: NotificationSections.PUSH, memberNotifyLevel: NotificationLevels.MENTION};
+ const wrapper = shallow(
+ ,
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ });
});
- test('should match snapshot, PUSH on expanded view', () => {
- const props = {...baseProps, section: NotificationSections.PUSH};
- const wrapper = shallow(
- ,
- );
+ describe('gms', () => {
+ test('should match snapshot, DESKTOP on expanded view when mentions is selected', () => {
+ const props = {...baseProps, isGM: true, memberNotifyLevel: NotificationLevels.MENTION};
+ const wrapper = shallow(
+ ,
+ );
- expect(wrapper).toMatchSnapshot();
- });
+ expect(wrapper).toMatchSnapshot();
+ });
- test('should match snapshot, MARK_UNREAD on expanded view', () => {
- const props = {...baseProps, section: NotificationSections.MARK_UNREAD};
- const wrapper = shallow(
- ,
- );
+ test('should match snapshot, PUSH on expanded view when mentions is selected', () => {
+ const props = {...baseProps, section: NotificationSections.PUSH, isGM: true, memberNotifyLevel: NotificationLevels.MENTION};
+ const wrapper = shallow(
+ ,
+ );
- expect(wrapper).toMatchSnapshot();
+ expect(wrapper).toMatchSnapshot();
+ });
});
});
diff --git a/webapp/channels/src/components/channel_notifications_modal/components/expand_view.tsx b/webapp/channels/src/components/channel_notifications_modal/components/expand_view.tsx
index aff565d2fc..eac49ac2fb 100644
--- a/webapp/channels/src/components/channel_notifications_modal/components/expand_view.tsx
+++ b/webapp/channels/src/components/channel_notifications_modal/components/expand_view.tsx
@@ -45,6 +45,7 @@ type Props = {
memberDesktopNotificationSound?: string;
section: string;
serverError?: string;
+ isGM: boolean;
}
const sounds = Array.from(notificationSounds.keys());
@@ -74,6 +75,7 @@ export default function ExpandView({
onCollapseSection,
ignoreChannelMentions,
channelAutoFollowThreads,
+ isGM,
}: Props) {
const isCRTEnabled = useSelector(isCollapsedThreadsEnabled);
@@ -275,6 +277,7 @@ export default function ExpandView({
{isCRTEnabled &&
section === NotificationSections.DESKTOP &&
memberNotifyLevel === NotificationLevels.MENTION &&
+ !isGM &&
<>
@@ -378,6 +381,7 @@ export default function ExpandView({
{isCRTEnabled &&
section === NotificationSections.PUSH &&
memberNotifyLevel === NotificationLevels.MENTION &&
+ !isGM &&
<>
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 1883613fe4..3cfebcc179 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
@@ -88,6 +88,11 @@ export default class NotificationSection extends React.PureComponent {
* Error string from the server
*/
serverError: PropTypes.string,
+
+ /**
+ * Whether the preferences are those of a GM
+ */
+ isGM: PropTypes.bool,
};
handleOnChange = (e) => {
@@ -134,6 +139,7 @@ export default class NotificationSection extends React.PureComponent {
onReset,
section,
serverError,
+ isGM,
} = this.props;
if (expand) {
@@ -157,6 +163,7 @@ export default class NotificationSection extends React.PureComponent {
onSubmit={onSubmit}
serverError={serverError}
onCollapseSection={this.handleCollapseSection}
+ isGM={isGM}
/>
);
}
diff --git a/webapp/channels/src/components/channel_notifications_modal/components/notification_section.test.jsx b/webapp/channels/src/components/channel_notifications_modal/components/notification_section.test.jsx
index d6e805cad0..fdc22b0e79 100644
--- a/webapp/channels/src/components/channel_notifications_modal/components/notification_section.test.jsx
+++ b/webapp/channels/src/components/channel_notifications_modal/components/notification_section.test.jsx
@@ -21,6 +21,7 @@ describe('components/channel_notifications_modal/NotificationSection', () => {
onSubmit: () => {}, //eslint-disable-line no-empty-function
onUpdateSection: () => {}, //eslint-disable-line no-empty-function
serverError: '',
+ isGM: false,
};
test('should match snapshot, DESKTOP on collapsed view', () => {
diff --git a/webapp/channels/src/components/post_view/channel_intro_message/channel_intro_message.test.tsx b/webapp/channels/src/components/post_view/channel_intro_message/channel_intro_message.test.tsx
index 0bca987362..fb576194e8 100644
--- a/webapp/channels/src/components/post_view/channel_intro_message/channel_intro_message.test.tsx
+++ b/webapp/channels/src/components/post_view/channel_intro_message/channel_intro_message.test.tsx
@@ -32,6 +32,7 @@ describe('components/post_view/ChannelIntroMessages', () => {
const users = [
{id: 'user1', roles: 'system_user'},
{id: 'guest1', roles: 'system_guest'},
+ {id: 'test-user-id', roles: 'system_user'},
] as UserProfile[];
const baseProps = {
@@ -153,6 +154,9 @@ describe('components/post_view/ChannelIntroMessages', () => {
expect(editIcon).toBeInTheDocument();
expect(editIcon).toHaveClass('icon-pencil-outline');
+
+ const notificationPreferencesButton = screen.getByText('Notification Preferences');
+ expect(notificationPreferencesButton).toBeInTheDocument();
});
});
diff --git a/webapp/channels/src/components/post_view/channel_intro_message/channel_intro_message.tsx b/webapp/channels/src/components/post_view/channel_intro_message/channel_intro_message.tsx
index 27a531f689..1d1f380f1f 100644
--- a/webapp/channels/src/components/post_view/channel_intro_message/channel_intro_message.tsx
+++ b/webapp/channels/src/components/post_view/channel_intro_message/channel_intro_message.tsx
@@ -4,12 +4,14 @@
import React from 'react';
import {FormattedDate, FormattedMessage} from 'react-intl';
+import {BellRingOutlineIcon} from '@mattermost/compass-icons/components';
import type {Channel} from '@mattermost/types/channels';
-import type {UserProfile as UserProfileRedux} from '@mattermost/types/users';
+import type {UserProfile as UserProfileType} from '@mattermost/types/users';
import {Permissions} from 'mattermost-redux/constants';
import AddGroupsToTeamModal from 'components/add_groups_to_team_modal';
+import ChannelNotificationsModal from 'components/channel_notifications_modal';
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import LocalizedIcon from 'components/localized_icon';
@@ -32,12 +34,12 @@ type Props = {
channel: Channel;
fullWidth: boolean;
locale: string;
- channelProfiles: UserProfileRedux[];
+ channelProfiles: UserProfileType[];
enableUserCreation?: boolean;
isReadOnly?: boolean;
teamIsGroupConstrained?: boolean;
creatorName: string;
- teammate?: UserProfileRedux;
+ teammate?: UserProfileType;
teammateName?: string;
stats: any;
usersLimit: number;
@@ -89,10 +91,11 @@ export default class ChannelIntroMessage extends React.PureComponent {
}
}
-function createGMIntroMessage(channel: Channel, centeredIntro: string, profiles: UserProfileRedux[], currentUserId: string) {
+function createGMIntroMessage(channel: Channel, centeredIntro: string, profiles: UserProfileType[], currentUserId: string) {
const channelIntroId = 'channelIntro';
if (profiles.length > 0) {
+ const currentUserProfile = profiles.find((v) => v.id === currentUserId);
const pictures = profiles.
filter((profile) => profile.id !== currentUserId).
map((profile) => (
@@ -114,16 +117,21 @@ function createGMIntroMessage(channel: Channel, centeredIntro: string, profiles:
{pictures}
- for all activity in this group message.'}
values={{
+ b: (chunks) => {chunks} ,
names: channel.display_name,
+ br: ,
}}
/>
-
- {createSetHeaderButton(channel)}
+
+ {createNotificationPreferencesButton(channel, currentUserProfile)}
+
+ {createSetHeaderButton(channel)}
+
);
}
@@ -143,7 +151,7 @@ function createGMIntroMessage(channel: Channel, centeredIntro: string, profiles:
);
}
-function createDMIntroMessage(channel: Channel, centeredIntro: string, teammate?: UserProfileRedux, teammateName?: string) {
+function createDMIntroMessage(channel: Channel, centeredIntro: string, teammate?: UserProfileType, teammateName?: string) {
const channelIntroId = 'channelIntro';
if (teammate) {
const src = teammate ? Utils.imageURLForUser(teammate.id, teammate.last_picture_update) : '';
@@ -185,8 +193,10 @@ function createDMIntroMessage(channel: Channel, centeredIntro: string, teammate?
}}
/>
- {pluggableButton}
- {setHeaderButton}
+
+ {pluggableButton}
+ {setHeaderButton}
+
);
}
@@ -555,3 +565,26 @@ function createSetHeaderButton(channel: Channel) {
);
}
+
+function createNotificationPreferencesButton(channel: Channel, currentUser?: UserProfileType) {
+ const isGM = channel.type === 'G';
+ if (!isGM || !currentUser) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/webapp/channels/src/components/system_notice/__snapshots__/system_notice.test.tsx.snap b/webapp/channels/src/components/system_notice/__snapshots__/system_notice.test.tsx.snap
index b4cdcc5020..eac4c99b37 100644
--- a/webapp/channels/src/components/system_notice/__snapshots__/system_notice.test.tsx.snap
+++ b/webapp/channels/src/components/system_notice/__snapshots__/system_notice.test.tsx.snap
@@ -5,64 +5,60 @@ exports[`components/SystemNotice should match snapshot for admin, admin notice 1
className="system-notice bg--white shadow--2"
>
+
+
+
-
some body
-
-
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
`;
@@ -72,47 +68,43 @@ exports[`components/SystemNotice should match snapshot for admin, regular notice
className="system-notice bg--white shadow--2"
>
+
+
+
-
some body
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
`;
@@ -122,47 +114,43 @@ exports[`components/SystemNotice should match snapshot for regular user, admin a
className="system-notice bg--white shadow--2"
>
+
+
+
-
some body2
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
`;
@@ -180,47 +168,43 @@ exports[`components/SystemNotice should match snapshot for regular user, regular
className="system-notice bg--white shadow--2"
>
+
+
+
-
some body
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
`;
@@ -232,47 +216,43 @@ exports[`components/SystemNotice should match snapshot for show function returni
className="system-notice bg--white shadow--2"
>
+
+
+
-
some body
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
`;
@@ -282,37 +262,81 @@ exports[`components/SystemNotice should match snapshot for with allowForget equa
className="system-notice bg--white shadow--2"
>
+
+
+
-
-
-
some title
+ some body
+
+
+
+
+
+
+
+`;
+
+exports[`components/SystemNotice should match snapshot when a custom icon is passed 1`] = `
+
+
+
+ icon
+
- some body
-
-
-
-
-
+ some title
+
+ some body
+
+
+
+
+
+
+
+
`;
diff --git a/webapp/channels/src/components/system_notice/index.ts b/webapp/channels/src/components/system_notice/index.ts
index 394a0e8a24..bb96b687c9 100644
--- a/webapp/channels/src/components/system_notice/index.ts
+++ b/webapp/channels/src/components/system_notice/index.ts
@@ -11,6 +11,7 @@ import {getStandardAnalytics} from 'mattermost-redux/actions/admin';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {Permissions} from 'mattermost-redux/constants';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
+import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
import {haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
@@ -55,6 +56,7 @@ function makeMapStateToProps() {
license,
serverVersion,
analytics,
+ currentChannel: getCurrentChannel(state),
};
};
}
diff --git a/webapp/channels/src/components/system_notice/notices.tsx b/webapp/channels/src/components/system_notice/notices.tsx
index a2fa200fa6..de8c1e3647 100644
--- a/webapp/channels/src/components/system_notice/notices.tsx
+++ b/webapp/channels/src/components/system_notice/notices.tsx
@@ -7,8 +7,8 @@ import {FormattedMessage} from 'react-intl';
import ExternalLink from 'components/external_link';
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import type {Notice} from 'components/system_notice/types';
+import InfoIcon from 'components/widgets/icons/info_icon';
-import mattermostIcon from 'images/icon50x50.png';
import {DocLinks} from 'utils/constants';
import * as ServerVersion from 'utils/server_version';
import * as UserAgent from 'utils/user_agent';
@@ -29,12 +29,11 @@ const notices: Notice[] = [
name: 'apiv3_deprecation',
adminOnly: true,
title: (
-
),
- icon: mattermostIcon,
body: (
),
- icon: mattermostIcon,
body: (
),
- icon: mattermostIcon,
body: (
),
- icon: mattermostIcon,
allowForget: false,
body: (
+ ),
+ icon: ( ),
+ body: (
+ )}}
+ />
+ ),
+ show: (serverVersion, config, license, analytics, currentChannel) => {
+ return currentChannel?.type === 'G';
+ },
+ },
];
export default notices;
diff --git a/webapp/channels/src/components/system_notice/system_notice.test.tsx b/webapp/channels/src/components/system_notice/system_notice.test.tsx
index 5b5be5694f..84d902025a 100644
--- a/webapp/channels/src/components/system_notice/system_notice.test.tsx
+++ b/webapp/channels/src/components/system_notice/system_notice.test.tsx
@@ -6,15 +6,13 @@ import React from 'react';
import SystemNotice from 'components/system_notice/system_notice';
-import mattermostIcon from 'images/icon50x50.png';
-
describe('components/SystemNotice', () => {
const baseProps = {
currentUserId: 'someid',
preferences: {},
dismissedNotices: {},
isSystemAdmin: false,
- notices: [{name: 'notice1', adminOnly: false, title: 'some title', icon: mattermostIcon, body: 'some body', allowForget: true, show: () => true}],
+ notices: [{name: 'notice1', adminOnly: false, title: 'some title', body: 'some body', allowForget: true, show: () => true}],
serverVersion: '5.1',
license: {IsLicensed: 'true'},
config: {},
@@ -38,13 +36,17 @@ describe('components/SystemNotice', () => {
});
test('should match snapshot for regular user, admin notice', () => {
- const props = {...baseProps, notices: [{name: 'notice1', adminOnly: true, title: 'some title', icon: mattermostIcon, body: 'some body', allowForget: true, show: () => true}]};
+ const props = {...baseProps, notices: [{...baseProps.notices[0], adminOnly: true}]};
const wrapper = shallow( );
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot for regular user, admin and regular notice', () => {
- const props = {...baseProps, notices: [{name: 'notice1', adminOnly: true, title: 'some title', icon: mattermostIcon, body: 'some body', allowForget: true}, {name: 'notice2', adminOnly: false, title: 'some title2', icon: mattermostIcon, body: 'some body2', allowForget: true, show: () => true}]};
+ const props = {...baseProps,
+ notices: [
+ {...baseProps.notices[0], adminOnly: true},
+ {...baseProps.notices[0], name: 'notice2', title: 'some title2', body: 'some body2'},
+ ]};
const wrapper = shallow( );
expect(wrapper).toMatchSnapshot();
});
@@ -56,7 +58,7 @@ describe('components/SystemNotice', () => {
});
test('should match snapshot for admin, admin notice', () => {
- const props = {...baseProps, isSystemAdmin: true, notices: [{name: 'notice1', adminOnly: true, title: 'some title', icon: mattermostIcon, body: 'some body', allowForget: true, show: () => true}]};
+ const props = {...baseProps, isSystemAdmin: true, notices: [{...baseProps.notices[0], adminOnly: true}]};
const wrapper = shallow( );
expect(wrapper).toMatchSnapshot();
});
@@ -74,19 +76,25 @@ describe('components/SystemNotice', () => {
});
test('should match snapshot for show function returning false', () => {
- const props = {...baseProps, notices: [{name: 'notice1', adminOnly: false, title: 'some title', icon: mattermostIcon, body: 'some body', allowForget: true, show: () => false}]};
+ const props = {...baseProps, notices: [{...baseProps.notices[0], show: () => false}]};
const wrapper = shallow( );
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot for show function returning true', () => {
- const props = {...baseProps, notices: [{name: 'notice1', adminOnly: false, title: 'some title', icon: mattermostIcon, body: 'some body', allowForget: true, show: () => true}]};
+ const props = {...baseProps, notices: [{...baseProps.notices[0], show: () => true}]};
const wrapper = shallow( );
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot for with allowForget equal false', () => {
- const props = {...baseProps, notices: [{name: 'notice1', adminOnly: false, title: 'some title', icon: mattermostIcon, body: 'some body', allowForget: false, show: () => true}]};
+ const props = {...baseProps, notices: [{...baseProps.notices[0], allowForget: false}]};
+ const wrapper = shallow( );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('should match snapshot when a custom icon is passed', () => {
+ const props = {...baseProps, notices: [{...baseProps.notices[0], icon: {'icon'} }]};
const wrapper = shallow( );
expect(wrapper).toMatchSnapshot();
});
diff --git a/webapp/channels/src/components/system_notice/system_notice.tsx b/webapp/channels/src/components/system_notice/system_notice.tsx
index 05395b0c93..196160b56d 100644
--- a/webapp/channels/src/components/system_notice/system_notice.tsx
+++ b/webapp/channels/src/components/system_notice/system_notice.tsx
@@ -5,6 +5,7 @@ import React from 'react';
import {FormattedMessage} from 'react-intl';
import type {AnalyticsRow} from '@mattermost/types/admin';
+import type {Channel} from '@mattermost/types/channels';
import type {ClientConfig, ClientLicense} from '@mattermost/types/config';
import type {PreferenceType} from '@mattermost/types/preferences';
@@ -25,6 +26,7 @@ type Props = {
config: Partial;
license: ClientLicense;
analytics?: Record;
+ currentChannel?: Channel;
actions: {
savePreferences(userId: string, preferences: PreferenceType[]): void;
dismissNotice(type: string): void;
@@ -60,7 +62,13 @@ export default class SystemNotice extends React.PureComponent {
continue;
}
- if (!notice.show?.(this.props.serverVersion, this.props.config, this.props.license, this.props.analytics)) {
+ if (!notice.show?.(
+ this.props.serverVersion,
+ this.props.config,
+ this.props.license,
+ this.props.analytics,
+ this.props.currentChannel,
+ )) {
continue;
}
@@ -118,44 +126,44 @@ export default class SystemNotice extends React.PureComponent {
);
}
+ const icon = notice.icon || ;
+
return (
-
-
-
-
+
+ {icon}
+
+
-
{notice.body}
-
- {visibleMessage}
-
-
-
-
- {notice.allowForget &&
+ {visibleMessage}
+
- }
+
+ {notice.allowForget &&
+
+
+ }
+
);
diff --git a/webapp/channels/src/components/system_notice/types.ts b/webapp/channels/src/components/system_notice/types.ts
index 7ff5296a8c..adc6d3d974 100644
--- a/webapp/channels/src/components/system_notice/types.ts
+++ b/webapp/channels/src/components/system_notice/types.ts
@@ -4,17 +4,20 @@
import type React from 'react';
import type {AnalyticsRow} from '@mattermost/types/admin';
+import type {Channel} from '@mattermost/types/channels';
export type Notice = {
name: string;
adminOnly?: boolean;
title: React.ReactNode;
- icon: string;
+ icon?: React.ReactNode;
body: React.ReactNode;
allowForget: boolean;
show?(
serverVersion: string,
config: any,
license: any,
- analytics?: Record
): boolean;
+ analytics?: Record,
+ currentChannel?: Channel,
+ ): boolean;
}
diff --git a/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/__snapshots__/desktop_notification_settings.test.tsx.snap b/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/__snapshots__/desktop_notification_settings.test.tsx.snap
index b3f23c10e2..ff64ca5c19 100644
--- a/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/__snapshots__/desktop_notification_settings.test.tsx.snap
+++ b/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/__snapshots__/desktop_notification_settings.test.tsx.snap
@@ -50,7 +50,7 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
type="radio"
/>
@@ -205,7 +205,7 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
type="radio"
/>
@@ -330,7 +330,7 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
type="radio"
/>
@@ -485,7 +485,7 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
type="radio"
/>
@@ -693,7 +693,7 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
type="radio"
/>
@@ -942,7 +942,7 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
type="radio"
/>
diff --git a/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/desktop_notification_settings.tsx b/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/desktop_notification_settings.tsx
index 34a790be5f..c07970f34f 100644
--- a/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/desktop_notification_settings.tsx
+++ b/webapp/channels/src/components/user_settings/notifications/desktop_notification_setting/desktop_notification_settings.tsx
@@ -397,7 +397,7 @@ export default class DesktopNotificationSettings extends React.PureComponent
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index cb4c09b959..27ecfd289e 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -2994,7 +2994,7 @@
"channel_modal.type.private.title": "Private Channel",
"channel_modal.type.public.description": "Anyone can join",
"channel_modal.type.public.title": "Public Channel",
- "channel_notifications.allActivity": "For all activity",
+ "channel_notifications.allActivity": "For all activity {isDefault}",
"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",
@@ -3760,7 +3760,7 @@
"intro_messages.creatorPrivate": "This is the start of the {name} private channel, created by {creator} on {date}.",
"intro_messages.default": "**Welcome to {display_name}!**\n \nPost messages here that you want everyone to see. Everyone automatically becomes a permanent member of this channel when they join the team.",
"intro_messages.DM": "This is the start of your direct message history with {teammate}.\nDirect messages and files shared here are not shown to people outside this area.",
- "intro_messages.GM": "This is the start of your group message history with {names}.\nMessages and files shared here are not shown to people outside this area.",
+ "intro_messages.GM": "This is the start of your group message history with {names}.{br}You'll be notified for all activity in this group message.",
"intro_messages.group_message": "This is the start of your group message history with these teammates. Messages and files shared here are not shown to people outside this area.",
"intro_messages.inviteGropusToChannel.button": "Add groups to this private channel",
"intro_messages.inviteMembersToChannel.button": "Add members to this channel",
@@ -3770,6 +3770,7 @@
"intro_messages.inviteOthersToWorkspace.title": "Let’s add some people to the workspace!",
"intro_messages.noCreator": "This is the start of the {name} channel, created on {date}.",
"intro_messages.noCreatorPrivate": "This is the start of the {name} private channel, created on {date}.",
+ "intro_messages.notificationPreferences": "Notification Preferences",
"intro_messages.offTopic": "This is the start of {display_name}, a channel for non-work-related conversations.",
"intro_messages.onlyInvited": " Only invited members can see this private channel.",
"intro_messages.purpose": " This channel's purpose is: {purpose}",
@@ -4999,7 +5000,9 @@
"system_notice.body.permissions": "Some policy and permission System Console settings have moved with the release of advanced permissions into Mattermost Free and Professional.",
"system_notice.dont_show": "Don't Show Again",
"system_notice.remind_me": "Remind me Later",
- "system_notice.title": "**Notice**\nfrom Mattermost",
+ "system_notice.title": "Notice from Mattermost",
+ "system_notice.title.gm_as_dm": "Updates to Group Messages",
+ "system_noticy.body.gm_as_dm": "You wil now be notified for all activity in your group messages along with a notification badge for every new message.{br}{br}You can configure this in notification preferences for each group message.",
"system_users_list.count": "{count, number} {count, plural, one {user} other {users}}",
"system_users_list.countPage": "{startCount, number} - {endCount, number} {count, plural, one {user} other {users}} of {total, number} total",
"system_users_list.countSearch": "{count, number} {count, plural, one {user} other {users}} of {total, number} total",
@@ -5448,7 +5451,7 @@
"user.settings.notifications.never": "Never",
"user.settings.notifications.off": "Off",
"user.settings.notifications.on": "On",
- "user.settings.notifications.onlyMentions": "Only for mentions and direct messages",
+ "user.settings.notifications.onlyMentions": "Only for mentions, direct messages, and group messages",
"user.settings.notifications.push": "Mobile Push Notifications",
"user.settings.notifications.push_notification.status": "Trigger push notifications when",
"user.settings.notifications.push_threads": "When enabled, any reply to a thread you're following will send a mobile push notification.",
diff --git a/webapp/channels/src/sass/components/_system-notice.scss b/webapp/channels/src/sass/components/_system-notice.scss
index c5b50017ba..6340bc26c2 100644
--- a/webapp/channels/src/sass/components/_system-notice.scss
+++ b/webapp/channels/src/sass/components/_system-notice.scss
@@ -5,7 +5,8 @@
z-index: 9999;
right: 12px;
bottom: 12px;
- width: 280px;
+ display: flex;
+ width: 386px;
padding: 18px 20px 0;
border: 1px solid alpha-color($black, 0.15);
background-color: var(--center-channel-bg);
@@ -13,33 +14,27 @@
box-shadow: 0 20px 30px alpha-color($black, 0.07), 0 14px 20px alpha-color($black, 0.07);
}
-.system-notice__header {
- display: flex;
- align-items: flex-start;
-}
-
.system-notice__logo {
- height: 36px;
+ height: 20px;
svg {
- width: 36px;
- height: 36px;
- fill: rgb(22, 109, 224);
+ width: 20px;
+ height: 20px;
+ fill: var(--button-bg);
}
}
.system-notice__title {
overflow: hidden;
- flex: 10 1 auto;
- padding: 3px 0 0 8px;
+ padding-bottom: 10px;
+ font-weight: bold;
line-height: 16px;
- opacity: 0.7;
text-overflow: ellipsis;
white-space: nowrap;
}
.system-notice__info {
- margin-bottom: 12px;
+ margin-top: 12px;
font-size: 12px;
opacity: 0.5;
@@ -49,33 +44,22 @@
}
.system-notice__body {
- padding: 18px 0 16px;
+ padding: 0 0 16px 16px;
line-height: 16px;
- opacity: 0.7;
}
.system-notice__footer {
display: flex;
- border-top: 1px solid alpha-color($black, 0.2);
- margin: 0 -20px;
+ margin-top: 16px;
.btn {
overflow: hidden;
flex: 1;
+ font-weight: bold;
text-overflow: ellipsis;
- &:hover {
- background: rgb(22, 109, 224);
- color: $white;
- }
-
- &:first-child {
- border-radius: 0 0 0 4px;
- }
-
&:last-child {
- border-left: 1px solid alpha-color($black, 0.2);
- border-radius: 0 0 4px 0;
+ margin-left: 5px;
}
}
}
diff --git a/webapp/channels/src/sass/layout/_headers.scss b/webapp/channels/src/sass/layout/_headers.scss
index b5f1f79711..cac525ebb5 100644
--- a/webapp/channels/src/sass/layout/_headers.scss
+++ b/webapp/channels/src/sass/layout/_headers.scss
@@ -524,6 +524,7 @@
margin-bottom: 10px;
.fa,
+ svg,
i {
margin-right: 5px;
}
diff --git a/webapp/channels/src/utils/emoticons.tsx b/webapp/channels/src/utils/emoticons.tsx
index 2cbd1ccfb2..b2773624b9 100644
--- a/webapp/channels/src/utils/emoticons.tsx
+++ b/webapp/channels/src/utils/emoticons.tsx
@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {formatWithRenderer} from './markdown';
-import MentionableRenderer from './markdown/mentionable_renderer';
+import PlainRenderer from './markdown/plain_renderer';
export const emoticonPatterns: { [key: string]: RegExp } = {
slightly_smiling_face: /(^|\B)(:-?\))($|\B)/g, // :)
@@ -28,7 +28,7 @@ export const emoticonPatterns: { [key: string]: RegExp } = {
export const EMOJI_PATTERN = /(:([a-zA-Z0-9_+-]+):)/g;
export function matchEmoticons(text: string): RegExpMatchArray | null {
- const markdownCleanedText = formatWithRenderer(text, new MentionableRenderer());
+ const markdownCleanedText = formatWithRenderer(text, new PlainRenderer());
let emojis = markdownCleanedText.match(EMOJI_PATTERN);
for (const name of Object.keys(emoticonPatterns)) {
diff --git a/webapp/channels/src/utils/markdown/mentionable_renderer.tsx b/webapp/channels/src/utils/markdown/mentionable_renderer.tsx
index e3a559c6c4..3866f33e28 100644
--- a/webapp/channels/src/utils/markdown/mentionable_renderer.tsx
+++ b/webapp/channels/src/utils/markdown/mentionable_renderer.tsx
@@ -1,81 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import marked from 'marked';
+import {EMOJI_PATTERN} from 'utils/emoticons';
+
+import PlainRenderer from './plain_renderer';
/** A Markdown renderer that converts a post into plain text that we can search for mentions */
-export default class MentionableRenderer extends marked.Renderer {
- public code() {
- // Code blocks can't contain mentions
- return '\n';
- }
-
- public blockquote(text: string) {
- return text + '\n';
- }
-
- public heading(text: string) {
- return text + '\n';
- }
-
- public hr() {
- return '\n';
- }
-
- public list(body: string) {
- return body + '\n';
- }
-
- public listitem(text: string) {
- return text + '\n';
- }
-
- public paragraph(text: string) {
- return text + '\n';
- }
-
- public table(header: string, body: string) {
- return header + '\n' + body;
- }
-
- public tablerow(content: string) {
- return content;
- }
-
- public tablecell(content: string) {
- return content + '\n';
- }
-
- public strong(text: string) {
- return ' ' + text + ' ';
- }
-
- public em(text: string) {
- return ' ' + text + ' ';
- }
-
- public codespan() {
- // Code spans can't contain mentions
- return ' ';
- }
-
- public br() {
- return '\n';
- }
-
- public del(text: string) {
- return ' ' + text + ' ';
- }
-
- public link(href: string, title: string, text: string) {
- return ' ' + text + ' ';
- }
-
- public image(href: string, title: string, text: string) {
- return ' ' + text + ' ';
- }
-
+export default class MentionableRenderer extends PlainRenderer {
public text(text: string) {
- return text;
+ // Remove all emojis
+ return text.replace(EMOJI_PATTERN, '');
}
}
diff --git a/webapp/channels/src/utils/markdown/plain_renderer.ts b/webapp/channels/src/utils/markdown/plain_renderer.ts
new file mode 100644
index 0000000000..429d981db3
--- /dev/null
+++ b/webapp/channels/src/utils/markdown/plain_renderer.ts
@@ -0,0 +1,81 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import marked from 'marked';
+
+/** A Markdown renderer that converts a post into plain text */
+export default class PlainRenderer extends marked.Renderer {
+ public code() {
+ // Code blocks can't contain mentions
+ return '\n';
+ }
+
+ public blockquote(text: string) {
+ return text + '\n';
+ }
+
+ public heading(text: string) {
+ return text + '\n';
+ }
+
+ public hr() {
+ return '\n';
+ }
+
+ public list(body: string) {
+ return body + '\n';
+ }
+
+ public listitem(text: string) {
+ return text + '\n';
+ }
+
+ public paragraph(text: string) {
+ return text + '\n';
+ }
+
+ public table(header: string, body: string) {
+ return header + '\n' + body;
+ }
+
+ public tablerow(content: string) {
+ return content;
+ }
+
+ public tablecell(content: string) {
+ return content + '\n';
+ }
+
+ public strong(text: string) {
+ return ' ' + text + ' ';
+ }
+
+ public em(text: string) {
+ return ' ' + text + ' ';
+ }
+
+ public codespan() {
+ // Code spans can't contain mentions
+ return ' ';
+ }
+
+ public br() {
+ return '\n';
+ }
+
+ public del(text: string) {
+ return ' ' + text + ' ';
+ }
+
+ public link(href: string, title: string, text: string) {
+ return ' ' + text + ' ';
+ }
+
+ public image(href: string, title: string, text: string) {
+ return ' ' + text + ' ';
+ }
+
+ public text(text: string) {
+ return text;
+ }
+}
diff --git a/webapp/channels/src/utils/text_formatting.tsx b/webapp/channels/src/utils/text_formatting.tsx
index b744204543..b7ab1ac772 100644
--- a/webapp/channels/src/utils/text_formatting.tsx
+++ b/webapp/channels/src/utils/text_formatting.tsx
@@ -227,7 +227,7 @@ const DEFAULT_OPTIONS: TextFormattingOptions = {
* Additional CJK and Hangul compatibility characters: \u2de0-\u2dff
**/
// eslint-disable-next-line no-misleading-character-class
-const cjkrPattern = /[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf\uac00-\ud7a3\u1100-\u11ff\u3130-\u318f\u0400-\u04ff\u0500-\u052f\u2de0-\u2dff]/;
+export const cjkrPattern = /[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf\uac00-\ud7a3\u1100-\u11ff\u3130-\u318f\u0400-\u04ff\u0500-\u052f\u2de0-\u2dff]/;
export function formatText(
text: string,