diff --git a/server/channels/app/notification_push.go b/server/channels/app/notification_push.go index dfb5db200ec..cd45cd22063 100644 --- a/server/channels/app/notification_push.go +++ b/server/channels/app/notification_push.go @@ -813,6 +813,35 @@ func (a *App) buildIdLoadedPushNotificationMessage(rctx request.CTX, channel *mo return msg } +func PriorityNotificationTitle(priority *model.PostPriority, rootID string, channelType model.ChannelType, channelName string, userLocale i18n.TranslateFunc) string { + if rootID != "" || priority == nil || priority.Priority == nil { + return "" + } + + switch *priority.Priority { + case model.PostPriorityImportant: + switch channelType { + case model.ChannelTypeDirect: + return userLocale("api.push_notification.title.important_dm") + case model.ChannelTypeGroup: + return userLocale("api.push_notification.title.important_gm") + default: + return userLocale("api.push_notification.title.important_channel", map[string]any{"channelName": channelName}) + } + case model.PostPriorityUrgent: + switch channelType { + case model.ChannelTypeDirect: + return userLocale("api.push_notification.title.urgent_dm") + case model.ChannelTypeGroup: + return userLocale("api.push_notification.title.urgent_gm") + default: + return userLocale("api.push_notification.title.urgent_channel", map[string]any{"channelName": channelName}) + } + default: + return "" + } +} + func (a *App) buildFullPushNotificationMessage(rctx request.CTX, contentsConfig string, post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string, explicitMention bool, channelWideMention bool, replyToThreadType string, ) *model.PushNotification { @@ -833,6 +862,9 @@ func (a *App) buildFullPushNotificationMessage(rctx request.CTX, contentsConfig cfg := a.Config() if contentsConfig != model.GenericNoChannelNotification || channel.Type == model.ChannelTypeDirect { msg.ChannelName = channelName + if priorityTitle := PriorityNotificationTitle(post.GetPriority(), post.RootId, channel.Type, channelName, userLocale); priorityTitle != "" { + msg.ChannelName = priorityTitle + } } if a.IsCRTEnabledForUser(rctx, user.Id) { diff --git a/server/channels/app/notification_push_test.go b/server/channels/app/notification_push_test.go index 0249fc60cbc..3ebe757930d 100644 --- a/server/channels/app/notification_push_test.go +++ b/server/channels/app/notification_push_test.go @@ -1083,6 +1083,98 @@ func TestBuildPushNotificationMessageMentions(t *testing.T) { } } +func TestBuildFullPushNotificationMessagePriorityTitles(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t) + + for name, tc := range map[string]struct { + priority string + channelType model.ChannelType + channelName string + rootID string + contentsConfig string + expectedTitle string + }{ + "urgent channel root post": { + priority: model.PostPriorityUrgent, + channelType: model.ChannelTypeOpen, + channelName: "Town Square", + expectedTitle: "URGENT message in Town Square", + }, + "urgent direct message root post": { + priority: model.PostPriorityUrgent, + channelType: model.ChannelTypeDirect, + channelName: "@sender", + expectedTitle: "URGENT Direct Message", + }, + "urgent group message root post": { + priority: model.PostPriorityUrgent, + channelType: model.ChannelTypeGroup, + channelName: "sender, receiver", + expectedTitle: "URGENT Group Message", + }, + "important channel root post": { + priority: model.PostPriorityImportant, + channelType: model.ChannelTypeOpen, + channelName: "Town Square", + expectedTitle: "IMPORTANT message in Town Square", + }, + "important direct message root post": { + priority: model.PostPriorityImportant, + channelType: model.ChannelTypeDirect, + channelName: "@sender", + expectedTitle: "IMPORTANT Direct Message", + }, + "important group message root post": { + priority: model.PostPriorityImportant, + channelType: model.ChannelTypeGroup, + channelName: "sender, receiver", + expectedTitle: "IMPORTANT Group Message", + }, + "priority reply keeps channel title": { + priority: model.PostPriorityUrgent, + channelType: model.ChannelTypeOpen, + channelName: "Town Square", + rootID: model.NewId(), + expectedTitle: "Reply in Town Square", + }, + "generic no channel keeps channel title hidden": { + priority: model.PostPriorityUrgent, + channelType: model.ChannelTypeOpen, + channelName: "Town Square", + contentsConfig: model.GenericNoChannelNotification, + expectedTitle: "", + }, + } { + t.Run(name, func(t *testing.T) { + contentsConfig := tc.contentsConfig + if contentsConfig == "" { + contentsConfig = model.FullNotification + } + + post := &model.Post{ + Id: model.NewId(), + UserId: model.NewId(), + RootId: tc.rootID, + Message: "hello", + Metadata: &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewPointer(tc.priority), + }, + }, + } + channel := &model.Channel{ + Id: model.NewId(), + Type: tc.channelType, + } + user := &model.User{Locale: "en"} + + msg := th.App.buildFullPushNotificationMessage(th.Context, contentsConfig, post, user, channel, tc.channelName, "sender", false, false, "") + assert.Equal(t, tc.expectedTitle, msg.ChannelName) + }) + } +} + func TestSendPushNotifications(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) diff --git a/server/channels/app/post_persistent_notification.go b/server/channels/app/post_persistent_notification.go index ddbf59681e3..57de0dff8e1 100644 --- a/server/channels/app/post_persistent_notification.go +++ b/server/channels/app/post_persistent_notification.go @@ -326,6 +326,11 @@ func (a *App) sendPersistentNotifications(post *model.Post, channel *model.Chann Sender: sender, } + if post.GetPriority() == nil { + post = a.PreparePostForClient(request.EmptyContext(a.Log()), post, &model.PreparePostForClientOpts{IncludePriority: true}) + notification.Post = post + } + if int64(len(mentionedUsersList)) > *a.Config().TeamSettings.MaxNotificationsPerChannel { return errors.Errorf("mentioned users: %d are more than allowed users: %d", len(mentionedUsersList), *a.Config().TeamSettings.MaxNotificationsPerChannel) } @@ -372,7 +377,6 @@ func (a *App) sendPersistentNotifications(post *model.Post, channel *model.Chann } if len(desktopUsers) != 0 { - post = a.PreparePostForClient(request.EmptyContext(a.Log()), post, &model.PreparePostForClientOpts{IncludePriority: true}) postJSON, jsonErr := post.ToJSON() if jsonErr != nil { return errors.Wrapf(jsonErr, "failed to encode post to JSON") diff --git a/server/i18n/en.json b/server/i18n/en.json index b826e4c0f33..d5bcede1acc 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -3330,6 +3330,30 @@ "id": "api.push_notification.title.collapsed_threads_dm", "translation": "Reply in Direct Message" }, + { + "id": "api.push_notification.title.important_channel", + "translation": "IMPORTANT message in {{.channelName}}" + }, + { + "id": "api.push_notification.title.important_dm", + "translation": "IMPORTANT Direct Message" + }, + { + "id": "api.push_notification.title.important_gm", + "translation": "IMPORTANT Group Message" + }, + { + "id": "api.push_notification.title.urgent_channel", + "translation": "URGENT message in {{.channelName}}" + }, + { + "id": "api.push_notification.title.urgent_dm", + "translation": "URGENT Direct Message" + }, + { + "id": "api.push_notification.title.urgent_gm", + "translation": "URGENT Group Message" + }, { "id": "api.push_notifications.message.parse.app_error", "translation": "An error occurred building the push notification message." diff --git a/server/public/model/post.go b/server/public/model/post.go index 50f41586673..394d697fe3c 100644 --- a/server/public/model/post.go +++ b/server/public/model/post.go @@ -110,7 +110,8 @@ const ( PostPropsSharedChannelState = "shared_channel_state" PostPropsSharedChannelWorkspaceName = "workspace_name" - PostPriorityUrgent = "urgent" + PostPriorityImportant = "important" + PostPriorityUrgent = "urgent" DefaultExpirySeconds = 60 * 60 * 24 * 7 // 7 days DefaultReadDurationSeconds = 10 * 60 // 10 minutes diff --git a/webapp/channels/src/actions/notification_actions.test.js b/webapp/channels/src/actions/notification_actions.test.js index f4765528d0c..4b028b66781 100644 --- a/webapp/channels/src/actions/notification_actions.test.js +++ b/webapp/channels/src/actions/notification_actions.test.js @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {PostPriority} from '@mattermost/types/posts'; + import {MarkUnread} from 'mattermost-redux/constants/channels'; import testConfigureStore from 'tests/test_store'; @@ -223,6 +225,59 @@ describe('notification_actions', () => { }); }); + test.each([ + ['urgent channel message', 'channel_id', Constants.OPEN_CHANNEL, PostPriority.URGENT, 'URGENT message in Utopia'], + ['urgent direct message', 'channel_id', Constants.DM_CHANNEL, PostPriority.URGENT, 'URGENT Direct Message'], + ['urgent group message', 'gm_channel', Constants.GM_CHANNEL, PostPriority.URGENT, 'URGENT Group Message'], + ['important channel message', 'channel_id', Constants.OPEN_CHANNEL, PostPriority.IMPORTANT, 'IMPORTANT message in Utopia'], + ['important direct message', 'channel_id', Constants.DM_CHANNEL, PostPriority.IMPORTANT, 'IMPORTANT Direct Message'], + ['important group message', 'gm_channel', Constants.GM_CHANNEL, PostPriority.IMPORTANT, 'IMPORTANT Group Message'], + ])('should notify user with priority title for %s', async (_name, channelId, channelType, priority, expectedTitle) => { + post = { + ...post, + root_id: '', + channel_id: channelId, + metadata: { + priority: { + priority, + }, + }, + }; + msgProps = { + ...msgProps, + post: JSON.stringify(post), + channel_type: channelType, + }; + baseState.entities.channels.channels[channelId].type = channelType; + + const store = testConfigureStore(baseState); + + return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => { + expect(spy).toHaveBeenCalledWith(expect.objectContaining({ + title: expectedTitle, + })); + }); + }); + + test('should not use priority title for thread reply notifications', async () => { + post = { + ...post, + metadata: { + priority: { + priority: PostPriority.URGENT, + }, + }, + }; + + const store = testConfigureStore(baseState); + + return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => { + expect(spy).toHaveBeenCalledWith(expect.objectContaining({ + title: 'Utopia', + })); + }); + }); + test('should not notify user when tab and channel are active', async () => { const store = testConfigureStore(baseState); baseState.views.browser.focused = true; diff --git a/webapp/channels/src/actions/notification_actions.tsx b/webapp/channels/src/actions/notification_actions.tsx index f7bbb430596..2128853bec0 100644 --- a/webapp/channels/src/actions/notification_actions.tsx +++ b/webapp/channels/src/actions/notification_actions.tsx @@ -6,6 +6,7 @@ import type {Channel, ChannelMembership} from '@mattermost/types/channels'; import type {ServerError} from '@mattermost/types/errors'; import {isMessageAttachmentArray} from '@mattermost/types/message_attachments'; import type {Post} from '@mattermost/types/posts'; +import {PostPriority} from '@mattermost/types/posts'; import type {UserProfile} from '@mattermost/types/users'; import {logError} from 'mattermost-redux/actions/errors'; @@ -134,7 +135,7 @@ export function sendDesktopNotification(post: Post, msgProps: NewPostMessageProp return {data: skipNotificationReason}; } - const title = getNotificationTitle(channel, msgProps, isCrtReply); + const title = getNotificationTitle(channel, msgProps, isCrtReply, post); const body = getNotificationBody(state, post, msgProps); //Play a sound if explicitly set in settings @@ -174,7 +175,37 @@ export function sendDesktopNotification(post: Post, msgProps: NewPostMessageProp }; } -const getNotificationTitle = (channel: Pick, msgProps: NewPostMessageProps, isCrtReply: boolean) => { +const getPriorityNotificationTitle = (priority: PostPriority | '' | undefined, channelType: Channel['type'] | undefined, channelTitle: string) => { + if (priority === PostPriority.IMPORTANT) { + if (channelType === Constants.DM_CHANNEL) { + return Utils.localizeMessage({id: 'notification.priority.important.dm', defaultMessage: 'IMPORTANT Direct Message'}); + } else if (channelType === Constants.GM_CHANNEL) { + return Utils.localizeMessage({id: 'notification.priority.important.gm', defaultMessage: 'IMPORTANT Group Message'}); + } + + return Utils.localizeMessage( + {id: 'notification.priority.important.channel', defaultMessage: 'IMPORTANT message in {channelName}'}, + {channelName: channelTitle}, + ); + } + + if (priority === PostPriority.URGENT) { + if (channelType === Constants.DM_CHANNEL) { + return Utils.localizeMessage({id: 'notification.priority.urgent.dm', defaultMessage: 'URGENT Direct Message'}); + } else if (channelType === Constants.GM_CHANNEL) { + return Utils.localizeMessage({id: 'notification.priority.urgent.gm', defaultMessage: 'URGENT Group Message'}); + } + + return Utils.localizeMessage( + {id: 'notification.priority.urgent.channel', defaultMessage: 'URGENT message in {channelName}'}, + {channelName: channelTitle}, + ); + } + + return ''; +}; + +const getNotificationTitle = (channel: Pick, msgProps: NewPostMessageProps, isCrtReply: boolean, post: Post) => { let title = Utils.localizeMessage({id: 'channel_loader.title', defaultMessage: 'Posted'}); if (channel.type === Constants.DM_CHANNEL) { title = Utils.localizeMessage({id: 'notification.dm', defaultMessage: 'Direct Message'}); @@ -194,6 +225,10 @@ const getNotificationTitle = (channel: Pick, m title = Utils.localizeMessage({id: 'notification.crt', defaultMessage: 'Reply in {title}'}, {title}); } + if (!isCrtReply && !post.root_id) { + title = getPriorityNotificationTitle(post.metadata?.priority?.priority, channel.type || msgProps.channel_type, title) || title; + } + return title; }; diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index c21786afcec..5853d1d6557 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -5816,6 +5816,12 @@ "no_results.user_groups.title": "No groups yet", "notification.crt": "Reply in {title}", "notification.dm": "Direct Message", + "notification.priority.important.channel": "IMPORTANT message in {channelName}", + "notification.priority.important.dm": "IMPORTANT Direct Message", + "notification.priority.important.gm": "IMPORTANT Group Message", + "notification.priority.urgent.channel": "URGENT message in {channelName}", + "notification.priority.urgent.dm": "URGENT Direct Message", + "notification.priority.urgent.gm": "URGENT Group Message", "notify_admin_to_upgrade_cta.notify-admin.already_notified": "Already notified!", "notify_admin_to_upgrade_cta.notify-admin.failed": "Try again later!", "notify_admin_to_upgrade_cta.notify-admin.notified": "Admin notified!",